feat(DeviceDetailOverlay, IECPage, SettingsInput): enhance input components with clear functionality and improved styling
This commit is contained in:
parent
f3b97276c5
commit
6c2267c639
|
|
@ -349,7 +349,9 @@ export default function DeviceDetailOverlay({
|
|||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'name'], v);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
onClear={() => updateDeviceSetting([...getDevicePath(), 'name'], '')}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
containerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -364,7 +366,9 @@ export default function DeviceDetailOverlay({
|
|||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'base_url'], v);
|
||||
}}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
onClear={() => updateDeviceSetting([...getDevicePath(), 'base_url'], '')}
|
||||
containerClassName="flex-1"
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setBrowsingField('base_url')}
|
||||
|
|
@ -373,16 +377,6 @@ export default function DeviceDetailOverlay({
|
|||
>
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'base_url'], '');
|
||||
}}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-red-50 hover:border-red-300 hover:text-red-500"
|
||||
title="Clear Base URL"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -401,17 +395,7 @@ export default function DeviceDetailOverlay({
|
|||
dev.url = newUrl;
|
||||
setConfig(newConfig);
|
||||
}}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setBrowsingField('url')}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-neutral-100"
|
||||
title="Browse"
|
||||
>
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClear={() => {
|
||||
const devicePath = getDevicePath();
|
||||
const newConfig = JSON.parse(JSON.stringify(config));
|
||||
let dev = newConfig;
|
||||
|
|
@ -420,10 +404,15 @@ export default function DeviceDetailOverlay({
|
|||
delete dev.media_set;
|
||||
setConfig(newConfig);
|
||||
}}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-red-50 hover:border-red-300 hover:text-red-500"
|
||||
title="Clear URL and media set"
|
||||
containerClassName="flex-1"
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setBrowsingField('url')}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-neutral-100"
|
||||
title="Browse"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -447,7 +436,9 @@ export default function DeviceDetailOverlay({
|
|||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'cache'], v);
|
||||
}}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
onClear={() => updateDeviceSetting([...getDevicePath(), 'cache'], '')}
|
||||
containerClassName="flex-1"
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setBrowsingField('cache')}
|
||||
|
|
@ -456,16 +447,6 @@ export default function DeviceDetailOverlay({
|
|||
>
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'cache'], '');
|
||||
}}
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-red-50 hover:border-red-300 hover:text-red-500"
|
||||
title="Clear Cache"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,9 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
|
|||
type="text"
|
||||
value={settings.autoboot || ''}
|
||||
onCommit={(v) => updateSetting(['settings', 'autoboot'], v)}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
onClear={() => updateSetting(['settings', 'autoboot'], '')}
|
||||
containerClassName="flex-1"
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowMediaBrowser(true)}
|
||||
|
|
@ -157,7 +159,9 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
|
|||
type="text"
|
||||
value={settings.drive_roms?.[key] || ''}
|
||||
onCommit={(v) => updateSetting(['settings', 'drive_roms', key], v)}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
onClear={() => updateSetting(['settings', 'drive_roms', key], '')}
|
||||
containerClassName="flex-1"
|
||||
className="px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setDriveRomBrowsingKey(key)}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,57 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface SettingsInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onBlur' | 'value'> {
|
||||
value: string | number;
|
||||
onCommit: (value: string) => void;
|
||||
/** When provided, an inline × button appears and calls this on click. */
|
||||
onClear?: () => void;
|
||||
/** className for the outer wrapper div (e.g. "flex-1"). Only used when onClear is set. */
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A controlled input that buffers keystrokes locally and only calls
|
||||
* `onCommit` when the field loses focus, preventing a settings save
|
||||
* on every keystroke.
|
||||
*
|
||||
* When `onClear` is provided the component wraps itself in a relative div
|
||||
* and shows an inline × button. Move any flex-sizing classes (e.g. flex-1)
|
||||
* to `containerClassName` so they apply to the wrapper instead of the input.
|
||||
*/
|
||||
export function SettingsInput({ value, onCommit, ...props }: SettingsInputProps) {
|
||||
export function SettingsInput({ value, onCommit, onClear, containerClassName, className, ...props }: SettingsInputProps) {
|
||||
const [local, setLocal] = useState(String(value ?? ''));
|
||||
|
||||
// Sync if the committed value changes externally (e.g. device switch).
|
||||
useEffect(() => { setLocal(String(value ?? '')); }, [value]);
|
||||
|
||||
return (
|
||||
const input = (
|
||||
<input
|
||||
{...props}
|
||||
className={`${onClear ? 'w-full pr-7' : ''} ${className ?? ''}`}
|
||||
value={local}
|
||||
onChange={e => setLocal(e.target.value)}
|
||||
onBlur={() => onCommit(local)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!onClear) return input;
|
||||
|
||||
return (
|
||||
<div className={`relative ${containerClassName ?? ''}`}>
|
||||
{input}
|
||||
{local !== '' && (
|
||||
<button
|
||||
type="button"
|
||||
// mousedown fires before blur; preventDefault keeps focus so onBlur doesn't commit first
|
||||
onMouseDown={e => { e.preventDefault(); setLocal(''); onClear(); }}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600"
|
||||
tabIndex={-1}
|
||||
aria-label="Clear"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user