Files
mdg-filius/packages/editor/src/sheet/SheetField.tsx
T
julian.kiedaischandClaude Opus 5.5 eff9433673 feat(editor): F4.5b the device sheet for routers and home routers
Measured first: SheetRun now types into FILIUS' router and home router
panels, records five forwarding tables (holeTabelle) and steps in the
panel's table. Java turns a card address that passes the pattern but not
IP.ipCheck (".0.0.1") into null and the router panel throws; an empty
gateway is marked red yet taken; a refused table cell is stored as null;
a home router's DHCP client leaves its DHCP server running. None of the
nulls is adopted. The core's routeTableRows matches all five tables.

- doc: card addresses (the first card mirrored in the device's settings),
  router gateway, RIP, manual routes (a new row once all four cells pass,
  a cell, removal); the sheet reads cards with what their cable joins and
  the whole table
- editor: Java's tabs (general, each card titled by its address, the
  forwarding table greyed out under RIP; general, LAN, WAN on a home
  router); the open card's cable drawn active in Java's green

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:51:03 +00:00

113 lines
3.6 KiB
TypeScript

// 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';
/** A field still to be filled (a new row's cell): empty and untouched, it is not marked. */
readonly unfilled?: boolean;
}
export function SheetField({ label, value, rule, onCommit, disabled = false, readOnly = false, name, inputMode = 'text', unfilled = false }: 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 && !(unfilled && draft === null && shown === '') && '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>
);
}