diff options
Diffstat (limited to 'src/features/surface-host')
| -rw-r--r-- | src/features/surface-host/logic/plan.test.ts | 41 | ||||
| -rw-r--r-- | src/features/surface-host/logic/plan.ts | 33 | ||||
| -rw-r--r-- | src/features/surface-host/logic/types.ts | 17 | ||||
| -rw-r--r-- | src/features/surface-host/ui/Number.svelte | 43 | ||||
| -rw-r--r-- | src/features/surface-host/ui/SurfaceView.svelte | 3 |
5 files changed, 135 insertions, 2 deletions
diff --git a/src/features/surface-host/logic/plan.test.ts b/src/features/surface-host/logic/plan.test.ts index a5727b4..be296a7 100644 --- a/src/features/surface-host/logic/plan.test.ts +++ b/src/features/surface-host/logic/plan.test.ts @@ -57,6 +57,47 @@ describe("planSurface", () => { expect(plan.fields).toEqual([{ kind: "stat", label: "Tokens", value: "1,234" }]); }); + it("maps a number field to a NumberFieldView, carrying optional hints", () => { + const plan = planSurface( + makeSpec({ + kind: "number", + label: "Interval", + value: 240, + min: 1, + step: 1, + unit: "s", + action: { actionId: "cache-warming/set-interval" }, + }), + ); + expect(plan.fields).toEqual([ + { + kind: "number", + label: "Interval", + value: 240, + min: 1, + step: 1, + unit: "s", + action: { actionId: "cache-warming/set-interval" }, + }, + ]); + }); + + it("omits absent number hints (no max key when undefined)", () => { + const plan = planSurface( + makeSpec({ + kind: "number", + label: "Interval", + value: 240, + min: 1, + action: { actionId: "set" }, + }), + ); + const field = plan.fields[0]; + expect(field).not.toHaveProperty("max"); + expect(field).not.toHaveProperty("step"); + expect(field).not.toHaveProperty("unit"); + }); + it("maps a button field to a ButtonFieldView", () => { const plan = planSurface( makeSpec({ kind: "button", label: "Retry", action: { actionId: "retry" } }), diff --git a/src/features/surface-host/logic/plan.ts b/src/features/surface-host/logic/plan.ts index 769f9f9..89088c3 100644 --- a/src/features/surface-host/logic/plan.ts +++ b/src/features/surface-host/logic/plan.ts @@ -1,7 +1,21 @@ import type { InvokeMessage, SurfaceSpec } from "@dispatch/ui-contract"; -import type { FieldView, RenderGroup, StatFieldView, SurfaceRenderPlan } from "./types"; +import type { + FieldView, + NumberFieldView, + RenderGroup, + StatFieldView, + SurfaceRenderPlan, +} from "./types"; -const KNOWN_KINDS = new Set(["toggle", "progress", "selector", "stat", "button", "custom"]); +const KNOWN_KINDS = new Set([ + "toggle", + "progress", + "selector", + "stat", + "number", + "button", + "custom", +]); /** * Validate and normalise a SurfaceSpec into a renderable plan. @@ -46,6 +60,21 @@ export function planSurface(spec: SurfaceSpec): SurfaceRenderPlan { value: field.value, }); break; + case "number": { + // Carry optional hints only when present (exactOptionalPropertyTypes). + const view: NumberFieldView = { + kind: "number", + label: field.label, + value: field.value, + action: field.action, + ...(field.min !== undefined ? { min: field.min } : {}), + ...(field.max !== undefined ? { max: field.max } : {}), + ...(field.step !== undefined ? { step: field.step } : {}), + ...(field.unit !== undefined ? { unit: field.unit } : {}), + }; + fields.push(view); + break; + } case "button": fields.push({ kind: "button", diff --git a/src/features/surface-host/logic/types.ts b/src/features/surface-host/logic/types.ts index d1888a2..23f8757 100644 --- a/src/features/surface-host/logic/types.ts +++ b/src/features/surface-host/logic/types.ts @@ -31,6 +31,22 @@ export interface StatFieldView { readonly value: string; } +/** + * Normalised view-model for a number field — the free-value counterpart to + * selector. `min`/`max`/`step`/`unit` are optional semantic hints (absent when + * the spec omits them). The renderer posts the new number as the action payload. + */ +export interface NumberFieldView { + readonly kind: "number"; + readonly label: string; + readonly value: number; + readonly min?: number; + readonly max?: number; + readonly step?: number; + readonly unit?: string; + readonly action: ActionRef; +} + /** Normalised view-model for a button field. */ export interface ButtonFieldView { readonly kind: "button"; @@ -55,6 +71,7 @@ export type FieldView = | ProgressFieldView | SelectorFieldView | StatFieldView + | NumberFieldView | ButtonFieldView | CustomFieldView; diff --git a/src/features/surface-host/ui/Number.svelte b/src/features/surface-host/ui/Number.svelte new file mode 100644 index 0000000..0f3323d --- /dev/null +++ b/src/features/surface-host/ui/Number.svelte @@ -0,0 +1,43 @@ +<script lang="ts"> + import type { InvokeMessage } from "@dispatch/ui-contract"; + import type { NumberFieldView } from "../logic/types"; + + let { + field, + surfaceId, + onInvoke, + }: { field: NumberFieldView; surfaceId: string; onInvoke: (msg: InvokeMessage) => void } = + $props(); + + // Commit on change/Enter rather than every keystroke. Ignore empty/non-numeric + // input (the backend also floors/validates); send the new number as payload. + function commit(event: Event) { + const target = event.target as HTMLInputElement; + const next = target.valueAsNumber; + if (Number.isNaN(next)) return; + onInvoke({ + type: "invoke", + surfaceId, + actionId: field.action.actionId, + payload: next, + }); + } +</script> + +<label class="flex items-center justify-between gap-2 text-sm"> + <span>{field.label}</span> + <span class="flex items-center gap-1"> + <input + type="number" + class="input input-bordered input-sm w-24" + value={field.value} + min={field.min} + max={field.max} + step={field.step} + onchange={commit} + /> + {#if field.unit} + <span class="opacity-60">{field.unit}</span> + {/if} + </span> +</label> diff --git a/src/features/surface-host/ui/SurfaceView.svelte b/src/features/surface-host/ui/SurfaceView.svelte index 5210e8c..24be8b8 100644 --- a/src/features/surface-host/ui/SurfaceView.svelte +++ b/src/features/surface-host/ui/SurfaceView.svelte @@ -2,6 +2,7 @@ import type { InvokeMessage, SurfaceSpec } from "@dispatch/ui-contract"; import { groupRenderFields, planSurface } from "../logic/plan"; import Button from "./Button.svelte"; + import Number from "./Number.svelte"; import Progress from "./Progress.svelte"; import Selector from "./Selector.svelte"; import StatTable from "./StatTable.svelte"; @@ -30,6 +31,8 @@ <Progress field={group.field} /> {:else if group.field.kind === "selector"} <Selector field={group.field} surfaceId={spec.id} {onInvoke} /> + {:else if group.field.kind === "number"} + <Number field={group.field} surfaceId={spec.id} {onInvoke} /> {:else if group.field.kind === "button"} <Button field={group.field} surfaceId={spec.id} {onInvoke} /> {:else if group.field.kind === "custom"} |
