diff --git a/packages/ui/src/components/form-request.test.ts b/packages/ui/src/components/form-request.test.ts index c42a424a..3bb35fe8 100644 --- a/packages/ui/src/components/form-request.test.ts +++ b/packages/ui/src/components/form-request.test.ts @@ -5,6 +5,7 @@ import { getFormFieldDefaultValue, getFormStringInputType, normalizeFormStringValue, + shouldRenderFormOptionsInline, } from "./form-request.tsx" import { isFormFieldVisible, isHttpFormUrl } from "../lib/form-schema.ts" @@ -41,4 +42,10 @@ describe("form request protocol mapping", () => { assert.equal(isHttpFormUrl("javascript:alert(1)"), false) assert.equal(isHttpFormUrl("file:///tmp/form"), false) }) + + it("uses visible choices for short option lists and menus for long lists", () => { + assert.equal(shouldRenderFormOptionsInline([{ value: "one" }, { value: "two" }]), true) + assert.equal(shouldRenderFormOptionsInline(Array.from({ length: 5 })), false) + assert.equal(shouldRenderFormOptionsInline([]), false) + }) }) diff --git a/packages/ui/src/components/form-request.tsx b/packages/ui/src/components/form-request.tsx index 0fc0535c..3f0178a2 100644 --- a/packages/ui/src/components/form-request.tsx +++ b/packages/ui/src/components/form-request.tsx @@ -37,6 +37,10 @@ export function normalizeFormStringValue( return Number.isNaN(date.getTime()) ? undefined : date.toISOString() } +export function shouldRenderFormOptionsInline(options: readonly unknown[] | undefined): boolean { + return Boolean(options?.length && options.length <= 4) +} + const FormRequest: Component = (props) => { const { t } = useI18n() const [values, setValues] = createSignal>( @@ -57,7 +61,9 @@ const FormRequest: Component = (props) => { if (!form.reportValidity()) return const invalidCollection = visibleFields().find((field) => { const value = values()[field.key] - if (field.type === "external" || field.type === "string" || field.type === "number" || field.type === "integer") return false + if (field.type === "external") return false + if (field.type === "string") return field.required && (typeof value !== "string" || value.length === 0) + if (field.type === "number" || field.type === "integer") return field.required && typeof value !== "number" if (field.type === "boolean") return field.required && value === undefined const count = Array.isArray(value) ? value.length : 0 return (field.required && count === 0) || count < (field.minItems ?? 0) || count > (field.maxItems ?? Infinity) @@ -101,53 +107,102 @@ const FormRequest: Component = (props) => { const label = () => field.title || field.key const stringField = field as Extract const numberField = field as Extract + const stringOptions = () => stringField.options ?? [] + const selectedString = () => String(values()[field.key] ?? "") + const customString = () => stringOptions().some((option) => option.value === selectedString()) ? "" : selectedString() return (
- {label()} }> - {label()} + + {label()} + +

{field.description}

- + update(field.key, normalizeFormStringValue(stringField.format, event.currentTarget.value))} + /> + }> + + + + + {(option) => } + + + + }> +
+ {label()} + {(option) => { + const checked = () => selectedString() === option.value + return ( + + ) + }} +
+ update(field.key, normalizeFormStringValue(stringField.format, event.currentTarget.value))} + onInput={(event) => update(field.key, event.currentTarget.value || undefined)} /> - }> - - - - - {(option) => } - +
= (props) => {
{label()} {(option) => ( -
diff --git a/packages/ui/src/styles/components/form-request-options.css b/packages/ui/src/styles/components/form-request-options.css new file mode 100644 index 00000000..cf96305f --- /dev/null +++ b/packages/ui/src/styles/components/form-request-options.css @@ -0,0 +1,64 @@ +.form-request-options { + display: flex; + flex-direction: column; + gap: var(--space-xs); + margin: 0; + padding: 0; + border: 0; +} + +.form-request-choice { + display: flex; + align-items: flex-start; + gap: var(--space-sm); + min-width: 0; + padding: var(--space-sm); + border: 1px solid var(--border-base); + background: var(--surface-base); + color: var(--text-primary); + cursor: pointer; +} + +.form-request-choice:hover { + border-color: var(--text-muted); +} + +.form-request-choice[data-selected="true"] { + border-color: var(--accent-primary); + background: color-mix(in srgb, var(--accent-primary) 10%, var(--surface-base)); +} + +.form-request-choice:focus-within { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; +} + +.form-request-choice-control { + flex: 0 0 auto; + width: 1rem; + height: 1rem; + margin: 0.125rem 0 0; + accent-color: var(--accent-primary); +} + +.form-request-choice-copy { + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 0.125rem; + min-width: 0; +} + +.form-request-choice-label { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); + overflow-wrap: anywhere; +} + +.form-request-choice-description { + color: var(--text-secondary); + font-size: var(--font-size-xs); + line-height: var(--line-height-normal); + overflow-wrap: anywhere; +} diff --git a/packages/ui/src/styles/components/form-request.css b/packages/ui/src/styles/components/form-request.css index 309e60e9..085bcf3e 100644 --- a/packages/ui/src/styles/components/form-request.css +++ b/packages/ui/src/styles/components/form-request.css @@ -82,25 +82,6 @@ accent-color: var(--accent-primary); } -.form-request-options { - display: flex; - flex-direction: column; - gap: var(--space-xs); - margin: 0; - padding: 0; - border: 0; -} - -.form-request-option { - display: flex; - align-items: center; - gap: var(--space-sm); - color: var(--text-primary); - font-size: var(--font-size-sm); - min-width: 0; - cursor: pointer; -} - .form-request-link { color: var(--accent-primary); font-size: var(--font-size-sm); diff --git a/packages/ui/src/styles/controls.css b/packages/ui/src/styles/controls.css index a9cded89..0c1d6082 100644 --- a/packages/ui/src/styles/controls.css +++ b/packages/ui/src/styles/controls.css @@ -10,6 +10,7 @@ @import "./components/remote-access.css"; @import "./components/permission-notification.css"; @import "./components/form-request.css"; +@import "./components/form-request-options.css"; @import "./components/toast-history.css"; @import "./components/settings-screen.css"; @import "./components/settings-info.css";