feat(editor): F4.5a the device sheet for PCs, notebooks, switches and modems
Measured first: the new oracle SheetRun types 77 texts into FILIUS' own settings panels and records what was marked red, what the device took and what the field showed afterwards. Java's two judgements disagree (red text taken, black text refused, any int as a modem port, a refused text dropped without a word); here one rule decides both (packages/doc fields.ts), a refused text stays marked in the field, and only what Java's model accepts reaches the document. - doc: settings.ts (name, label policy, addresses, IP forwarding, DHCP stopping the own DHCP server, LAN/WLAN with the wireless connection, switch SSID dropping its wireless hosts, retention time, cloud, modem port as its pair), sheet.ts (what the sheet shows) - fls: modemPair/modemPort in one place - editor: DeviceSheet beside the surface or below it on a phone, opened with the selection; every change one undo step - not adopted from Java: the name overwritten by the MAC address, the access point search depending on unrelated devices Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// A text field of the device sheet. Java takes a field's text when it loses
|
||||
// the focus or Enter is pressed, and colours a text its rule refuses red
|
||||
// while it is typed. A refused text stays here, marked, with what the device
|
||||
// keeps meanwhile, instead of being replaced without a word as Java does;
|
||||
// Escape gives it up. A value the device already holds that the rule
|
||||
// refuses (from a file, or written by someone else) is marked, never mended
|
||||
// (plan I6).
|
||||
|
||||
import { useId, useState, type KeyboardEvent } from 'react';
|
||||
import { checkField, type CheckedField, type FieldRule } from '@mdg-filius/doc';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
|
||||
export interface SheetFieldProps {
|
||||
readonly label: string;
|
||||
/** What the device holds. */
|
||||
readonly value: string;
|
||||
readonly rule: FieldRule;
|
||||
/** Gives the text to the device; says whether it was taken. */
|
||||
readonly onCommit: (text: string) => CheckedField;
|
||||
readonly disabled?: boolean;
|
||||
readonly readOnly?: boolean;
|
||||
/** A name for the browser tests and the label's input mode. */
|
||||
readonly name: string;
|
||||
readonly inputMode?: 'text' | 'decimal' | 'numeric';
|
||||
}
|
||||
|
||||
export function SheetField({ label, value, rule, onCommit, disabled = false, readOnly = false, name, inputMode = 'text' }: SheetFieldProps) {
|
||||
const t = useTranslate();
|
||||
const id = useId();
|
||||
// What is typed and not yet taken; null shows the device's value.
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
const [refused, setRefused] = useState(false);
|
||||
const shown = draft ?? value;
|
||||
const marked = !readOnly && 'invalid' in checkField(rule, shown);
|
||||
|
||||
const commit = () => {
|
||||
if (draft === null || readOnly) return;
|
||||
if (draft === value) {
|
||||
setDraft(null);
|
||||
setRefused(false);
|
||||
return;
|
||||
}
|
||||
const result = onCommit(draft);
|
||||
if ('value' in result) {
|
||||
setDraft(null);
|
||||
setRefused(false);
|
||||
} else {
|
||||
setRefused(true);
|
||||
}
|
||||
};
|
||||
|
||||
const key = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') commit();
|
||||
if (event.key === 'Escape') {
|
||||
setDraft(null);
|
||||
setRefused(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hint = refused && draft !== null ? t('sheet.notTaken', { value: value === '' ? t('sheet.empty') : value }) : draft === null && marked ? t('sheet.invalidValue') : null;
|
||||
return (
|
||||
<label className="sheet-field" htmlFor={id}>
|
||||
<span>{label}</span>
|
||||
<input
|
||||
id={id}
|
||||
name={name}
|
||||
value={shown}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
inputMode={inputMode}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
aria-invalid={marked}
|
||||
aria-describedby={hint === null ? undefined : `${id}-hint`}
|
||||
onChange={(event) => {
|
||||
setDraft(event.target.value);
|
||||
setRefused(false);
|
||||
}}
|
||||
onBlur={commit}
|
||||
onKeyDown={key}
|
||||
/>
|
||||
{hint === null ? null : (
|
||||
<span id={`${id}-hint`} className="sheet-hint" role="status">
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function SheetCheck({
|
||||
label,
|
||||
checked,
|
||||
onChange,
|
||||
disabled = false,
|
||||
name,
|
||||
}: {
|
||||
readonly label: string;
|
||||
readonly checked: boolean;
|
||||
readonly onChange: (checked: boolean) => void;
|
||||
readonly disabled?: boolean;
|
||||
readonly name: string;
|
||||
}) {
|
||||
return (
|
||||
<label className={disabled ? 'sheet-check disabled' : 'sheet-check'}>
|
||||
<input type="checkbox" name={name} checked={checked} disabled={disabled} onChange={(event) => onChange(event.target.checked)} />
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user