feat: implemented a basic store and emulatorjs

This commit is contained in:
Simeon Radivoev 2026-03-14 02:15:57 +02:00
parent 2f32cbc730
commit 7286541822
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
121 changed files with 5900 additions and 1092 deletions

View file

@ -7,12 +7,26 @@ import
import classNames from "classnames";
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
export type ButtonStyle = 'base' | 'accent' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
const styles = {
base: 'bg-base-200 text-base-content active:bg-base-300! active:text-base-content! active:ring-offset-base-content',
accent: "bg-accent text-accent-content active:bg-base-content! active:text-base-content active:ring-offset-accent",
primary: "bg-primary text-primary-content active:bg-base-content! active:text-base-content! active:ring-offset-primary",
secondary: "bg-secondary text-secondary-content active:bg-base-content! active:text-base-content! active:ring-offset-secondary",
info: "bg-info text-info-content active:bg-base-content! active:text-base-content! active:ring-offset-info",
success: "bg-success text-success-content active:bg-base-content! active:text-base-content! active:ring-offset-success",
warning: "bg-warning text-warning-content active:bg-base-content! active:text-base-content! active:ring-offset-warning",
error: "bg-error text-error-content active:bg-base-content! active:text-base-content! active:ring-offset-error",
};
export function Button (data: {
id: string,
children?: any,
className?: string,
disabled?: boolean,
type?: "reset" | "button" | "submit";
style?: ButtonStyle,
shortcutLabel?: string;
focusClassName?: string;
} & InteractParams & FocusParams)
@ -20,7 +34,7 @@ export function Button (data: {
const { ref, focused, focusKey } = useFocusable({
focusKey: data.id,
onEnterPress: data.onAction,
onFocus: data.onFocus,
onFocus: (_l, _p, details) => data.onFocus?.(focusKey, ref.current, details),
focusable: !data.disabled
});
@ -31,9 +45,10 @@ export function Button (data: {
return <button
ref={ref}
onClick={data.onAction}
onClick={e => data.onAction?.(e.nativeEvent)}
disabled={data.disabled}
className={twMerge("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg",
className={twMerge("flex items-center justify-center px-4 py-2 disabled:bg-base-200/40 disabled:text-base-content/40 cursor-pointer rounded-3xl md:text-lg not-control-mouse:focused:drop-shadow-lg border border-base-content/5 not-control-mouse:focused:bg-base-content not-control-mouse:focused:text-base-100 control-mouse:hover:bg-base-content control-mouse:hover:text-base-100 active:transition-none active:ring-offset-4",
styles[data.style ?? 'base'],
focused ? data.focusClassName : undefined,
classNames({
"btn-accent": focused,

View file

@ -1,5 +1,5 @@
import { HTMLInputTypeAttribute, JSX } from "react";
import { LocalSettingsSchema, LocalSettingsType } from "../../../shared/constants";
import { LocalSettingsSchema, LocalSettingsType } from "@shared/constants";
import { OptionSpace } from "./OptionSpace";
import { OptionInput } from "./OptionInput";
import { useLocalStorage } from "usehooks-ts";
@ -18,7 +18,7 @@ export function LocalOption (data: {
const [localValue, setLocalValue] = useLocalStorage<any>(data.id, LocalSettingsSchema.shape[data.id].parse(undefined), { deserializer: (v) => LocalSettingsSchema.shape[data.id].parse(JSON.parse(v)) });
return (
<OptionSpace label={data.label}>
<OptionSpace id={`${data.id}-space`} label={data.label}>
{data.type === 'dropdown' && data.values && <OptionDropdown values={data.values} icon={data.icon}
name={data.id ?? ""}
type={data.type}

View file

@ -1,9 +1,7 @@
import classNames from "classnames";
import { ChangeEventHandler, FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef, useState } from "react";
import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef, useState } from "react";
import { twMerge } from "tailwind-merge";
import { useOptionContext } from "./OptionSpace";
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { systemApi } from "../../scripts/clientApi";
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { ContextDialog, ContextList, DialogEntry } from "../ContextDialog";
import { ChevronDown } from "lucide-react";
@ -39,16 +37,13 @@ export function OptionDropdown (data: {
return (
<>
<label ref={ref} className={twMerge("flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent",
classNames({ "[&_button]:not-focus:ring-7 [&_button]:not-focus:ring-accent": focused }))}>
{!!data.icon && <span className={twMerge("text-base-content/80", classNames({
"text-primary-content": option.focused
}))}>{data.icon}</span>}
<label ref={ref} className={twMerge("flex group-focusable items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent")}>
{!!data.icon && <span className={"text-base-content/80 is-focused:text-primary-content"}>{data.icon}</span>}
<button onClick={() =>
{
console.log("Open");
setOpen(true);
}} className={classNames('btn input rounded-full cursor-pointer grow', { "bg-base-200": !focused })}>{data.value}<ChevronDown /></button>
}} className={'flex items-center justify-center border h-10 border-base-content/30 px-4 py-2 rounded-full cursor-pointer grow not-in-focused:bg-base-200 focusable focusable-accent hover:border-base-content hover:bg-base-content hover:text-base-300'}>{data.value}<ChevronDown /></button>
</label>
{open && <ContextDialog id={`${data.name}-context`} open={true} close={handleClose}>
<ContextList options={data.values.map((v, i) => ({

View file

@ -1,10 +1,9 @@
import classNames from "classnames";
import { ChangeEventHandler, FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef } from "react";
import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef } from "react";
import { twMerge } from "tailwind-merge";
import { useOptionContext } from "./OptionSpace";
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { systemApi } from "../../scripts/clientApi";
import { Check, CheckIcon, X } from "lucide-react";
import { CheckIcon, X } from "lucide-react";
export function OptionInput (data: {
name: string;
@ -52,11 +51,8 @@ export function OptionInput (data: {
};
return (
<label ref={ref} className={twMerge("flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent",
classNames({ "[&_.focus-target]:not-focus:ring-7 [&_.focus-target]:not-focus:ring-accent": focused, "pl-1": data.type === 'checkbox' }))}>
{!!data.icon && <span className={twMerge("text-base-content/80", classNames({
"text-primary-content": option.focused
}))}>{data.icon}</span>}
<label ref={ref} className={`flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent group-focusable`}>
{!!data.icon && <span className="text-base-content/80">{data.icon}</span>}
{data.type !== 'checkbox' && <input
ref={inputRef}
id={data.name}
@ -72,17 +68,11 @@ export function OptionInput (data: {
onBlur={data.onBlur}
defaultChecked={typeof data.defaultValue === 'boolean' ? data.defaultValue : undefined}
className={twMerge(
"focus-target text-base-content",
"input grow rounded-full ring-primary-content focus:ring-7", classNames({
"bg-base-200": !focused
}),
"flex text-base-content px-4 py-2 items-center justify-center border border-base-content/20 grow rounded-full focus:ring-base-content in-focused:bg-base-200 focusable focusable-accent focus:not-focused:ring-7 control-mouse:ring-0! hover:border-base-content",
data.className
)}
/>}
{data.type === 'checkbox' && <div className={classNames("toggle focus-target toggle-primary toggle-xl border-base-content/30 rounded-full before:rounded-full text-base-content", {
"bg-base-200": !focused,
"border-0": focused,
})}>
{data.type === 'checkbox' && <div className="toggle toggle-xl before:size-6 h-8 border-base-content/30 rounded-full before:rounded-full text-base-content not-in-focus:bg-base-200 focused-child:border-0 ml-1 ring-7 hover:border-base-content focusable focusable-accent">
<input
ref={inputRef}
id={data.name}

View file

@ -1,16 +1,9 @@
import { FocusContext, FocusDetails, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import { OptionContext } from "@/mainview/scripts/contexts";
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
import classNames from "classnames";
import { createContext, JSX, useContext, useEffect, useMemo } from "react";
import { JSX, useContext, useEffect, useMemo } from "react";
import { twMerge } from "tailwind-merge";
export const OptionContext = createContext(
{} as {
focused: boolean;
focus: (focusDetails?: FocusDetails | undefined) => void;
eventTarget: EventTarget;
},
);
export function useOptionContext (params?: { onOptionEnterPress?: () => void; })
{
const context = useContext(OptionContext);
@ -81,11 +74,7 @@ export function OptionSpace (data: {
<OptionContext value={{ focused, focus: focusSelf, eventTarget }}>
<li
ref={ref}
className={twMerge("flex portrait:flex-col portrait:gap-2 portrait:p-4 md:flex-row sm:p-2 md:p-4 md:pl-8! rounded-3xl border-b border-base-content/5",
classNames(
{
"bg-base-300": focused || hasFocusedChild,
}),
className={twMerge("flex portrait:flex-col portrait:gap-2 portrait:p-4 md:flex-row sm:p-2 md:p-4 md:pl-8! rounded-3xl border-b border-base-content/5 focused:bg-base-300 focused-child:bg-base-300",
data.className,
)}
>

View file

@ -110,7 +110,7 @@ export function PathSettingsOptionBase (data: PathSettingsOptionParams & {
};
return (
<OptionSpace id={data.id} className="gap-2" label={<>{data.label}{changed && <Pen />}</>}>
<OptionSpace id={`${data.id}-space`} className="gap-2" label={<>{data.label}{changed && <Pen />}</>}>
<OptionInput
icon={data.icon}
name={`${data.id}-input`}

View file

@ -18,7 +18,7 @@ export const { useAppForm: useSettingsForm, useTypedAppFormContext: useSettingsF
function FormOption (data: { type: HTMLInputTypeAttribute, icon?: JSX.Element; label?: string | JSX.Element; placeholder?: string; })
{
const field = useFieldContext<string>();
return <OptionSpace label={<div className="flex flex-1 gap-2">
return <OptionSpace id={`${field.name}-space`} label={<div className="flex flex-1 gap-2">
{data.label}
{field.getMeta().errors.length > 0 && <div className="badge badge-error">
{field.state.meta.errors.map(e => e.message).join(',')}

View file

@ -54,7 +54,7 @@ export function SettingsOption (data: {
}, [dirty, setDirty, localValue]);
return (
<OptionSpace label={data.label}>
<OptionSpace id={`${data.id}-space`} label={data.label}>
<OptionInput
icon={data.icon}
name={data.id ?? ""}