feat: implemented haptics
feat: Implemented a select menu fix: Only used audio clips compile
This commit is contained in:
parent
02a4f2c9a9
commit
54dd9256e3
51 changed files with 580 additions and 466 deletions
|
|
@ -9,13 +9,18 @@ export function LocalOption (data: {
|
|||
label: string;
|
||||
id: keyof LocalSettingsType;
|
||||
type: HTMLInputTypeAttribute | 'dropdown';
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
placeholder?: string;
|
||||
values?: string[];
|
||||
icon?: JSX.Element;
|
||||
children?: any;
|
||||
})
|
||||
{
|
||||
const [localValue, setLocalValue] = useLocalStorage<any>(data.id, LocalSettingsSchema.shape[data.id].parse(undefined), { deserializer: (v) => LocalSettingsSchema.shape[data.id].parse(JSON.parse(v)) });
|
||||
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 id={`${data.id}-space`} label={data.label}>
|
||||
|
|
@ -25,30 +30,21 @@ export function LocalOption (data: {
|
|||
defaultValue={localValue}
|
||||
onChange={(v) =>
|
||||
{
|
||||
if (data.type === 'checkbox')
|
||||
{
|
||||
setLocalValue(v);
|
||||
} else
|
||||
{
|
||||
setLocalValue(v);
|
||||
}
|
||||
setLocalValue(v);
|
||||
}}
|
||||
value={localValue} />}
|
||||
{data.type !== 'dropdown' && <OptionInput
|
||||
icon={data.icon}
|
||||
name={data.id ?? ""}
|
||||
type={data.type}
|
||||
min={data.min}
|
||||
max={data.max}
|
||||
step={data.step}
|
||||
placeholder={data.placeholder}
|
||||
defaultValue={localValue}
|
||||
onChange={(v) =>
|
||||
{
|
||||
if (data.type === 'checkbox')
|
||||
{
|
||||
setLocalValue(v);
|
||||
} else
|
||||
{
|
||||
setLocalValue(v);
|
||||
}
|
||||
setLocalValue(v);
|
||||
}}
|
||||
value={localValue}
|
||||
/>}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useRef } from "react";
|
||||
import { FocusEventHandler, HTMLInputAutoCompleteAttribute, HTMLInputTypeAttribute, JSX, useEffect, useRef, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useOptionContext } from "./OptionSpace";
|
||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { systemApi } from "../../scripts/clientApi";
|
||||
import { CheckIcon, X } from "lucide-react";
|
||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
|
||||
export function OptionInput (data: {
|
||||
name: string;
|
||||
|
|
@ -12,11 +13,14 @@ export function OptionInput (data: {
|
|||
className?: string;
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
value?: string | boolean;
|
||||
defaultValue?: string | boolean;
|
||||
value?: string | boolean | number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
defaultValue?: string | boolean | number;
|
||||
autocomplete?: HTMLInputAutoCompleteAttribute;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
onChange?: (value: any) => void;
|
||||
onChange?: (value: string | number | boolean) => void;
|
||||
})
|
||||
{
|
||||
const handlePress = () =>
|
||||
|
|
@ -30,16 +34,74 @@ export function OptionInput (data: {
|
|||
}
|
||||
oneShot('click');
|
||||
};
|
||||
const { ref } = useFocusable({
|
||||
focusKey: data.name, onEnterPress: handlePress
|
||||
});
|
||||
const [inputFocused, setInputFocused] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { ref, focusKey } = useFocusable({
|
||||
focusKey: data.name,
|
||||
onEnterPress: handlePress,
|
||||
onBlur: () => inputRef.current?.blur()
|
||||
});
|
||||
|
||||
const option = useOptionContext({
|
||||
onOptionEnterPress: handlePress,
|
||||
});
|
||||
const handleFocus = () =>
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (data.type === 'range')
|
||||
{
|
||||
option.setFocusBoundary(inputFocused);
|
||||
option.setFocusBoundaryDirections(['left', 'right']);
|
||||
}
|
||||
}, [inputFocused, option, data.type]);
|
||||
|
||||
useShortcuts(focusKey, () =>
|
||||
{
|
||||
|
||||
const shortcuts: Shortcut[] = [];
|
||||
if (inputFocused && data.type === 'range')
|
||||
{
|
||||
shortcuts.push(
|
||||
{
|
||||
label: "Decrease",
|
||||
button: GamePadButtonCode.Left,
|
||||
action ()
|
||||
{
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current?.stepDown();
|
||||
data.onChange?.(inputRef.current.valueAsNumber);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Increase",
|
||||
button: GamePadButtonCode.Right,
|
||||
action (e)
|
||||
{
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current?.stepUp();
|
||||
data.onChange?.(inputRef.current.valueAsNumber);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (inputFocused)
|
||||
{
|
||||
shortcuts.push({
|
||||
label: "Unfocus",
|
||||
button: GamePadButtonCode.B,
|
||||
action (e)
|
||||
{
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
});
|
||||
}
|
||||
return shortcuts;
|
||||
}, [inputFocused, data.type]);
|
||||
|
||||
const handleInputFocus = () =>
|
||||
{
|
||||
option.focus();
|
||||
setInputFocused(true);
|
||||
if (inputRef.current)
|
||||
{
|
||||
var rect = inputRef.current?.getBoundingClientRect();
|
||||
|
|
@ -52,25 +114,47 @@ export function OptionInput (data: {
|
|||
}
|
||||
};
|
||||
|
||||
const handleInputBlur = (e: any) =>
|
||||
{
|
||||
data.onBlur?.(e);
|
||||
setInputFocused(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<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}
|
||||
min={data.min}
|
||||
max={data.max}
|
||||
step={data.step}
|
||||
data-focus={"input"}
|
||||
name={data.name}
|
||||
value={String(data.value)}
|
||||
defaultValue={typeof data.defaultValue === 'string' ? data.defaultValue : undefined}
|
||||
type={data.type}
|
||||
autoComplete={data.autocomplete}
|
||||
onFocus={handleFocus}
|
||||
onFocus={handleInputFocus}
|
||||
placeholder={data.placeholder}
|
||||
onChange={e => data.onChange?.(typeof data.defaultValue === 'boolean' ? e.target.checked : e.target.value)}
|
||||
onBlur={data.onBlur}
|
||||
onChange={e =>
|
||||
{
|
||||
if (typeof data.defaultValue === 'boolean')
|
||||
{
|
||||
data.onChange?.(e.target.checked);
|
||||
} else if (data.type === 'range')
|
||||
{
|
||||
data.onChange?.(e.target.valueAsNumber);
|
||||
} else
|
||||
{
|
||||
data.onChange?.(e.target.value);
|
||||
}
|
||||
}}
|
||||
onBlur={handleInputBlur}
|
||||
defaultChecked={typeof data.defaultValue === 'boolean' ? data.defaultValue : undefined}
|
||||
className={twMerge(
|
||||
"flex text-base-content px-4 py-2 items-center justify-center border bg-base-200 border-base-content/20 grow rounded-full focus:ring-base-content in-focused:bg-base-100 focusable focusable-accent focus:not-focused:ring-7 control-mouse:ring-0! hover:border-base-content",
|
||||
data.type === 'range' ? "range" : "",
|
||||
data.className
|
||||
)}
|
||||
/>}
|
||||
|
|
@ -83,10 +167,10 @@ export function OptionInput (data: {
|
|||
type={data.type}
|
||||
onClick={() => { oneShot("click"); }}
|
||||
autoComplete={data.autocomplete}
|
||||
onFocus={handleFocus}
|
||||
onFocus={handleInputFocus}
|
||||
placeholder={data.placeholder}
|
||||
onChange={e => data.onChange?.(e.target.checked)}
|
||||
onBlur={data.onBlur}
|
||||
onBlur={handleInputBlur}
|
||||
className={twMerge(
|
||||
"active:bg-base-content rounded-full",
|
||||
data.className
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { OptionContext } from "@/mainview/scripts/contexts";
|
||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import { Direction, FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { JSX, useContext, useEffect, useMemo } from "react";
|
||||
import { JSX, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function useOptionContext (params?: { onOptionEnterPress?: () => void; })
|
||||
|
|
@ -40,12 +40,16 @@ export function OptionSpace (data: {
|
|||
saveLastFocusedChild?: boolean;
|
||||
})
|
||||
{
|
||||
const [focusBoundary, setFocusBoundary] = useState(false);
|
||||
const [focusBoundaryDirections, setFocusBoundaryDirections] = useState<Direction[]>([]);
|
||||
const eventTarget = useMemo(() => new EventTarget(), []);
|
||||
const { ref, focused, focusSelf, focusKey } = useFocusable({
|
||||
focusKey: data.id,
|
||||
focusable: data.focusable !== false,
|
||||
trackChildren: true,
|
||||
saveLastFocusedChild: data.saveLastFocusedChild ?? false,
|
||||
isFocusBoundary: focusBoundary,
|
||||
focusBoundaryDirections,
|
||||
onFocus ()
|
||||
{
|
||||
(ref.current as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
|
|
@ -71,7 +75,7 @@ export function OptionSpace (data: {
|
|||
}
|
||||
|
||||
return (<FocusContext value={focusKey}>
|
||||
<OptionContext value={{ focused, focus: focusSelf, eventTarget }}>
|
||||
<OptionContext value={{ focused, focus: focusSelf, setFocusBoundary, setFocusBoundaryDirections, 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 focused:bg-base-300 focused-child:bg-base-300",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue