@vielzeug/refine 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/checkbox-group.cjs +7 -7
  2. package/dist/checkbox-group.cjs.map +1 -1
  3. package/dist/checkbox-group.js +35 -26
  4. package/dist/checkbox-group.js.map +1 -1
  5. package/dist/checkbox.cjs +3 -3
  6. package/dist/checkbox.cjs.map +1 -1
  7. package/dist/checkbox.js +24 -19
  8. package/dist/checkbox.js.map +1 -1
  9. package/dist/combobox.cjs +24 -24
  10. package/dist/combobox.cjs.map +1 -1
  11. package/dist/combobox.js +133 -129
  12. package/dist/combobox.js.map +1 -1
  13. package/dist/custom-elements.json +4227 -3725
  14. package/dist/headless/auto-resize.cjs +2 -0
  15. package/dist/headless/auto-resize.cjs.map +1 -0
  16. package/dist/headless/auto-resize.d.ts +29 -0
  17. package/dist/headless/auto-resize.d.ts.map +1 -0
  18. package/dist/headless/auto-resize.js +16 -0
  19. package/dist/headless/auto-resize.js.map +1 -0
  20. package/dist/headless/checkable.cjs +1 -1
  21. package/dist/headless/checkable.cjs.map +1 -1
  22. package/dist/headless/checkable.d.ts +25 -7
  23. package/dist/headless/checkable.d.ts.map +1 -1
  24. package/dist/headless/checkable.js +19 -14
  25. package/dist/headless/checkable.js.map +1 -1
  26. package/dist/headless/choice-field.cjs +1 -1
  27. package/dist/headless/choice-field.cjs.map +1 -1
  28. package/dist/headless/choice-field.d.ts +21 -0
  29. package/dist/headless/choice-field.d.ts.map +1 -1
  30. package/dist/headless/choice-field.js +27 -19
  31. package/dist/headless/choice-field.js.map +1 -1
  32. package/dist/headless/composer.cjs +2 -0
  33. package/dist/headless/composer.cjs.map +1 -0
  34. package/dist/headless/composer.d.ts +44 -0
  35. package/dist/headless/composer.d.ts.map +1 -0
  36. package/dist/headless/composer.js +21 -0
  37. package/dist/headless/composer.js.map +1 -0
  38. package/dist/headless/field-base.cjs +1 -1
  39. package/dist/headless/field-base.cjs.map +1 -1
  40. package/dist/headless/field-base.d.ts +97 -84
  41. package/dist/headless/field-base.d.ts.map +1 -1
  42. package/dist/headless/field-base.js +43 -32
  43. package/dist/headless/field-base.js.map +1 -1
  44. package/dist/headless/index.d.ts +3 -1
  45. package/dist/headless/index.d.ts.map +1 -1
  46. package/dist/headless/text-field.cjs +1 -1
  47. package/dist/headless/text-field.cjs.map +1 -1
  48. package/dist/headless/text-field.d.ts +40 -0
  49. package/dist/headless/text-field.d.ts.map +1 -1
  50. package/dist/headless/text-field.js +30 -25
  51. package/dist/headless/text-field.js.map +1 -1
  52. package/dist/headless.cjs +1 -1
  53. package/dist/headless.js +27 -25
  54. package/dist/index.cjs +1 -1
  55. package/dist/index.js +61 -60
  56. package/dist/input.cjs +9 -9
  57. package/dist/input.cjs.map +1 -1
  58. package/dist/input.js +49 -44
  59. package/dist/input.js.map +1 -1
  60. package/dist/inputs/checkbox/checkbox.d.ts +3 -0
  61. package/dist/inputs/checkbox/checkbox.d.ts.map +1 -1
  62. package/dist/inputs/combobox/combobox.d.ts +1 -0
  63. package/dist/inputs/combobox/combobox.d.ts.map +1 -1
  64. package/dist/inputs/combobox/combobox.types.d.ts +2 -0
  65. package/dist/inputs/combobox/combobox.types.d.ts.map +1 -1
  66. package/dist/inputs/index.d.ts +2 -0
  67. package/dist/inputs/index.d.ts.map +1 -1
  68. package/dist/inputs/input/input.d.ts.map +1 -1
  69. package/dist/inputs/message-composer/message-composer.css.cjs +2 -0
  70. package/dist/inputs/message-composer/message-composer.css.cjs.map +1 -0
  71. package/dist/inputs/message-composer/message-composer.css.js +6 -0
  72. package/dist/inputs/message-composer/message-composer.css.js.map +1 -0
  73. package/dist/inputs/message-composer/message-composer.d.ts +139 -0
  74. package/dist/inputs/message-composer/message-composer.d.ts.map +1 -0
  75. package/dist/inputs/shared/field-binding.cjs +1 -1
  76. package/dist/inputs/shared/field-binding.cjs.map +1 -1
  77. package/dist/inputs/shared/field-binding.d.ts +15 -0
  78. package/dist/inputs/shared/field-binding.d.ts.map +1 -1
  79. package/dist/inputs/shared/field-binding.js +4 -3
  80. package/dist/inputs/shared/field-binding.js.map +1 -1
  81. package/dist/inputs/shared/form-context.cjs.map +1 -1
  82. package/dist/inputs/shared/form-context.d.ts +6 -0
  83. package/dist/inputs/shared/form-context.d.ts.map +1 -1
  84. package/dist/inputs/shared/form-context.js.map +1 -1
  85. package/dist/inputs/textarea/textarea.d.ts.map +1 -1
  86. package/dist/message-composer.cjs +52 -0
  87. package/dist/message-composer.cjs.map +1 -0
  88. package/dist/message-composer.js +168 -0
  89. package/dist/message-composer.js.map +1 -0
  90. package/dist/ore/dist/base-element.cjs +1 -1
  91. package/dist/ore/dist/base-element.cjs.map +1 -1
  92. package/dist/ore/dist/base-element.js +13 -3
  93. package/dist/ore/dist/base-element.js.map +1 -1
  94. package/dist/ore/dist/forms/field.cjs +1 -1
  95. package/dist/ore/dist/forms/field.cjs.map +1 -1
  96. package/dist/ore/dist/forms/field.js +29 -22
  97. package/dist/ore/dist/forms/field.js.map +1 -1
  98. package/dist/ore/dist/runtime.cjs +1 -1
  99. package/dist/ore/dist/runtime.cjs.map +1 -1
  100. package/dist/ore/dist/runtime.js +3 -1
  101. package/dist/ore/dist/runtime.js.map +1 -1
  102. package/dist/radio-group.cjs +9 -9
  103. package/dist/radio-group.cjs.map +1 -1
  104. package/dist/radio-group.js +44 -38
  105. package/dist/radio-group.js.map +1 -1
  106. package/dist/radio.cjs +3 -3
  107. package/dist/radio.cjs.map +1 -1
  108. package/dist/radio.js +23 -24
  109. package/dist/radio.js.map +1 -1
  110. package/dist/refine.cjs +228 -178
  111. package/dist/refine.cjs.map +1 -1
  112. package/dist/refine.iife.js +228 -178
  113. package/dist/refine.iife.js.map +1 -1
  114. package/dist/refine.js +251 -201
  115. package/dist/refine.js.map +1 -1
  116. package/dist/select.cjs +31 -31
  117. package/dist/select.cjs.map +1 -1
  118. package/dist/select.js +125 -122
  119. package/dist/select.js.map +1 -1
  120. package/dist/shared/size-presets.cjs +1 -1
  121. package/dist/shared/size-presets.cjs.map +1 -1
  122. package/dist/shared/size-presets.d.ts +23 -0
  123. package/dist/shared/size-presets.d.ts.map +1 -1
  124. package/dist/shared/size-presets.js +17 -1
  125. package/dist/shared/size-presets.js.map +1 -1
  126. package/dist/switch.cjs +3 -3
  127. package/dist/switch.cjs.map +1 -1
  128. package/dist/switch.js +17 -18
  129. package/dist/switch.js.map +1 -1
  130. package/dist/textarea.cjs +15 -15
  131. package/dist/textarea.cjs.map +1 -1
  132. package/dist/textarea.js +64 -60
  133. package/dist/textarea.js.map +1 -1
  134. package/package.json +11 -3
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.cjs","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Field options + handle ───────────────────────────────────────────────────\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n /**\n * Returns the underlying form field object used for validation triggering.\n * Called lazily — safe to return `null` before the first render.\n */\n getFormField?: () => { reportValidity(): void } | null;\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const field = createField({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n helper?: Readable<string | undefined>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not currently consumed by `createField()` itself — visibility is driven\n * solely by `hasLabel`/`label`. Threaded through so component authors can\n * apply placement-specific styling (`label-placement` attribute) without a\n * second prop; if placement-dependent visibility is ever needed, wire it\n * into `labelVisible$` in `createField()` instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n validateOn?: Readable<ControlValidationMode>;\n};\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Field handle ──────────────────────────────────────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`.\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\n/** Flat ARIA attribute bag ready to spread into a template bind call. */\nexport type AriaProps = {\n 'aria-describedby': string | null;\n 'aria-errormessage': string | null;\n 'aria-invalid': 'true' | null;\n 'aria-labelledby': string | null;\n};\n\nexport type FieldHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /**\n * Returns a snapshot of the four ARIA attributes as a plain object.\n * Spread into a template bind call to avoid repeating each signal individually.\n *\n * @example\n * ```ts\n * bind(inputEl, { attr: field.ariaProps() });\n * ```\n */\n ariaProps(): AriaProps;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n fieldId: string;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\n/**\n * Composes the field triad — IDs, assistive state, and label state — into a\n * single `FieldHandle`. Both `createTextField` and `createChoiceField` build\n * on top of this.\n */\nexport const createField = (options: FieldOptions): FieldHandle => {\n // ── Core ──────────────────────────────────────────────────────────────────\n const disabled = computed(() => Boolean(options.disabled?.value));\n const validateOn = options.validateOn;\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const assistiveId = createStableId('helper');\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (validateOn?.value === on) options.getFormField?.()?.reportValidity();\n };\n\n // ── Label + ARIA state ────────────────────────────────────────────────────\n const labelId = createStableId('label');\n const errorId = createStableId('error');\n\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible$ = options.hasLabel ?? computed(() => Boolean(label$.value));\n\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n const ariaLabelledBy = computed(() => (labelVisible$.value ? labelId : null));\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n ariaLabelledBy,\n ariaProps: () => ({\n 'aria-describedby': ariaDescribedBy.value,\n 'aria-errormessage': ariaErrorMessage.value,\n 'aria-invalid': ariaInvalid.value,\n 'aria-labelledby': ariaLabelledBy.value,\n }),\n assistiveId,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n fieldId,\n helperText: computed(() => resolvedAssistive.value.helperText),\n labelId,\n labelVisible: labelVisible$,\n triggerValidation,\n };\n};\n"],"mappings":"6DAoEA,IAAa,EAA0B,IAAA,EAAA,EAAA,SAAA,MACrB,CACd,UAAW,EAAQ,OAAO,OAAS,GACnC,WAAY,EAAQ,QAAQ,OAAS,EACvC,EAAE,EAyBS,EAAsB,IAAA,EAAA,EAAA,SAAA,KACJ,CAC3B,IAAM,EAAQ,EAAQ,OAAO,OAAS,GAChC,EAAY,EAAQ,WAAW,MAC/B,EAAkB,OAAO,CAAS,EAClC,EAAiB,OAAO,SAAS,CAAe,GAAK,EAAkB,EAAI,EAAkB,KAC7F,EAAa,IAAmB,KAChC,EAAc,EAAa,GAAG,EAAM,OAAO,KAAK,IAAmB,GACnE,EAAQ,EAAa,EAAM,OAAS,EAAiB,EAE3D,MAAO,CACL,eAAgB,EAAa,GAAS,EAAI,GAC1C,iBAAkB,EAAa,GAAS,IAAO,EAAQ,EAAI,GAC3D,aACF,CACF,CAAC,EA4EU,EAAe,GAAuC,CAEjE,IAAM,GAAA,EAAA,EAAA,SAAA,KAA0B,EAAQ,EAAQ,UAAU,KAAM,EAC1D,EAAa,EAAQ,WACrB,EAAU,EAAQ,KAAA,EAAA,EAAA,eAAA,CAAqB,EAAQ,QAAU,OAAO,EAChE,GAAA,EAAA,EAAA,eAAA,CAA6B,QAAQ,EAErC,EAAqB,GAA4D,CACjF,GAAY,QAAU,GAAI,EAAQ,eAAe,CAAC,EAAE,eAAe,CACzE,EAGM,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAChC,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAEhC,EAAS,EAAQ,QAAA,EAAA,EAAA,SAAA,KAAwB,IAAA,EAAS,EAMlD,EAAgB,EAAQ,WAAA,EAAA,EAAA,SAAA,KAA2B,EAAQ,EAAO,KAAM,EAExE,EAAoB,EAAuB,CAAE,MAAO,EAAQ,MAAO,OAAQ,EAAQ,MAAO,CAAC,EAE3F,GAAA,EAAA,EAAA,SAAA,KACJ,EAAkB,MAAM,WAAa,EAAkB,MAAM,WAAa,EAAc,IAC1F,EACM,GAAA,EAAA,EAAA,SAAA,KAAmC,EAAkB,MAAM,UAAY,EAAU,IAAK,EACtF,GAAA,EAAA,EAAA,SAAA,KAA6C,EAAkB,MAAM,UAAY,OAAS,IAAK,EAC/F,GAAA,EAAA,EAAA,SAAA,KAAiC,EAAc,MAAQ,EAAU,IAAK,EAE5E,MAAO,CACL,kBACA,mBACA,cACA,iBACA,eAAkB,CAChB,mBAAoB,EAAgB,MACpC,oBAAqB,EAAiB,MACtC,eAAgB,EAAY,MAC5B,kBAAmB,EAAe,KACpC,GACA,cACA,WACA,UACA,WAAA,EAAA,EAAA,SAAA,KAA0B,EAAkB,MAAM,SAAS,EAC3D,UACA,YAAA,EAAA,EAAA,SAAA,KAA2B,EAAkB,MAAM,UAAU,EAC7D,UACA,aAAc,EACd,mBACF,CACF"}
1
+ {"version":3,"file":"field-base.cjs","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":"4FAsBA,IAAa,EAA0B,IAAA,EAAA,EAAA,SAAA,MACrB,CACd,UAAW,EAAQ,OAAO,OAAS,GACnC,WAAY,EAAQ,QAAQ,OAAS,EACvC,EAAE,EAyBS,EAAsB,IAAA,EAAA,EAAA,SAAA,KACJ,CAC3B,IAAM,EAAQ,EAAQ,OAAO,OAAS,GAChC,EAAY,EAAQ,WAAW,MAC/B,EAAkB,OAAO,CAAS,EAClC,EAAiB,OAAO,SAAS,CAAe,GAAK,EAAkB,EAAI,EAAkB,KAC7F,EAAa,IAAmB,KAChC,EAAc,EAAa,GAAG,EAAM,OAAO,KAAK,IAAmB,GACnE,EAAQ,EAAa,EAAM,OAAS,EAAiB,EAE3D,MAAO,CACL,eAAgB,EAAa,GAAS,EAAI,GAC1C,iBAAkB,EAAa,GAAS,IAAO,EAAQ,EAAI,GAC3D,aACF,CACF,CAAC,EAYU,GAAoB,EAAmC,EAAO,YACpE,EAED,EAAQ,eAAuB,GAAG,EAAK,WAEvC,EAAQ,iBAAyB,GAAG,EAAK,aAEtC,EANc,EA4DV,EAAwB,GAAyD,CAC5F,IAAM,GAAA,EAAA,EAAA,SAAA,KAA0B,EAAQ,EAAQ,UAAU,KAAM,EAC1D,GAAA,EAAA,EAAA,eAAA,CAA6B,QAAQ,EACrC,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAChC,EAAoB,EAAuB,CAAE,MAAO,EAAQ,MAAO,OAAQ,EAAQ,MAAO,CAAC,EAE3F,GAAA,EAAA,EAAA,SAAA,KACJ,EAAkB,MAAM,WAAa,EAAkB,MAAM,WAAa,EAAc,IAC1F,EACM,GAAA,EAAA,EAAA,SAAA,KAAmC,EAAkB,MAAM,UAAY,EAAU,IAAK,EACtF,GAAA,EAAA,EAAA,SAAA,KAA6C,EAAkB,MAAM,UAAY,OAAS,IAAK,EAEjG,EAA+C,KAsBnD,MAAO,CACL,kBACA,mBACA,cACA,cACA,gBA1BuB,GAAoD,CAC3E,EAAY,CACd,EAyBE,WACA,UACA,WAAA,EAAA,EAAA,SAAA,KAA0B,EAAkB,MAAM,SAAS,EAC3D,YAAA,EAAA,EAAA,SAAA,KAA2B,EAAkB,MAAM,UAAU,EAC7D,kBA3ByB,GAA4D,CACjF,KAAQ,YAAY,QAAU,EAElC,IAAI,CAAC,EAAW,CAId,EAAA,KACE,2KACF,EAEA,MACF,CAEA,EAAU,eAAe,CAFzB,CAGF,CAaA,CACF,EAmDa,EAAoB,GAAiD,CAChF,IAAM,EAAU,EAAQ,KAAA,EAAA,EAAA,eAAA,CAAqB,EAAQ,QAAU,OAAO,EAChE,GAAA,EAAA,EAAA,eAAA,CAAyB,OAAO,EAChC,EAAS,EAAQ,QAAA,EAAA,EAAA,SAAA,KAAwB,IAAA,EAAS,EAMlD,EAAe,EAAQ,WAAA,EAAA,EAAA,SAAA,KAA2B,EAAQ,EAAO,KAAM,EAG7E,MAAO,CAAE,gBAAA,EAAA,EAAA,SAAA,KAF8B,EAAa,MAAQ,EAAU,IAE7D,EAAgB,UAAS,UAAS,cAAa,CAC1D,EA6Ba,EAAe,IAAwC,CAClE,GAAG,EAAqB,CAAO,EAC/B,GAAG,EAAiB,CAAO,CAC7B"}
@@ -1,53 +1,6 @@
1
1
  import { type Readable } from '@vielzeug/ripple';
2
2
  export type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';
3
3
  export type ControlValidationMode = ValidationTrigger | undefined;
4
- /** Options shared by both `createTextField` and `createChoiceField`. */
5
- export type FieldOptions = {
6
- disabled?: Readable<boolean | undefined>;
7
- error?: Readable<string | undefined>;
8
- /**
9
- * Returns the underlying form field object used for validation triggering.
10
- * Called lazily — safe to return `null` before the first render.
11
- */
12
- getFormField?: () => {
13
- reportValidity(): void;
14
- } | null;
15
- /**
16
- * Override for label visibility that takes precedence over deriving it from
17
- * the `label` text signal. Pass a computed signal that checks both prop and
18
- * slot presence to enable slot-first composition.
19
- *
20
- * @example
21
- * ```ts
22
- * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);
23
- * const field = createField({ ...options, hasLabel });
24
- * ```
25
- */
26
- hasLabel?: Readable<boolean>;
27
- helper?: Readable<string | undefined>;
28
- /**
29
- * When provided, used directly as `fieldId` instead of generating one via
30
- * `createStableId`. Useful in tests for deterministic ID assertions.
31
- */
32
- id?: string;
33
- /**
34
- * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are
35
- * computed reactively from this value (see `hasLabel` for the slot-first override).
36
- */
37
- label?: Readable<string | undefined>;
38
- /**
39
- * Label placement signal. Defaults to `'inset'`.
40
- *
41
- * Not currently consumed by `createField()` itself — visibility is driven
42
- * solely by `hasLabel`/`label`. Threaded through so component authors can
43
- * apply placement-specific styling (`label-placement` attribute) without a
44
- * second prop; if placement-dependent visibility is ever needed, wire it
45
- * into `labelVisible$` in `createField()` instead of adding a new option.
46
- */
47
- labelPlacement?: Readable<LabelPlacement>;
48
- prefix?: string;
49
- validateOn?: Readable<ControlValidationMode>;
50
- };
51
4
  export type ErrorHelperState = {
52
5
  errorText: string;
53
6
  helperText: string;
@@ -77,72 +30,132 @@ export type CounterOptions = {
77
30
  * ```
78
31
  */
79
32
  export declare const createCounterState: (options: CounterOptions) => Readable<CounterState>;
80
- export type LabelPlacement = 'inset' | 'outside' | undefined;
81
33
  /**
82
- * Common handle returned by `createField` — the base for both `TextFieldHandle`
83
- * and `ChoiceFieldHandle`.
84
- *
85
- * ARIA signals and label state are flat on the handle — no nested `aria` or
86
- * `label` sub-objects.
34
+ * The single implementation of "which modifier class does this counter state map to" —
35
+ * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)
36
+ * renders the identical near-limit/at-limit styling, so this is the one place that decides it.
87
37
  *
88
38
  * @example
89
- * ```html
90
- * <label id="${labelId}" ?hidden="${() => !labelVisible.value}">...</label>
91
- * <input
92
- * :aria-labelledby="${ariaLabelledBy}"
93
- * :aria-describedby="${ariaDescribedBy}"
94
- * :aria-invalid="${ariaInvalid}" />
39
+ * ```ts
40
+ * html`<span class="${() => counterClassName(counter?.value)}">...</span>`
95
41
  * ```
96
42
  */
97
- /** Flat ARIA attribute bag ready to spread into a template bind call. */
98
- export type AriaProps = {
99
- 'aria-describedby': string | null;
100
- 'aria-errormessage': string | null;
101
- 'aria-invalid': 'true' | null;
102
- 'aria-labelledby': string | null;
103
- };
104
- export type FieldHandle = {
43
+ export declare const counterClassName: (counter: CounterState | undefined, base?: string) => string;
44
+ export type LabelPlacement = 'inset' | 'outside' | undefined;
45
+ /**
46
+ * A field's error/helper/disabled/validation-trigger state — everything an input
47
+ * needs *except* a visible `<label>`. Split out from label state (below) because
48
+ * not every field has one: `ore-message-composer` names itself via `aria-label`
49
+ * (a chat composer's accessible name conventionally comes from context/placeholder,
50
+ * not a floating `<label>`), so it composes this directly instead of pulling in
51
+ * label plumbing it can't use. `createField()` composes both for fields that do
52
+ * render a `<label>`.
53
+ */
54
+ export type AssistiveStateHandle = {
105
55
  /** `aria-describedby` value. Non-null when helper or error text is present. */
106
56
  ariaDescribedBy: Readable<string | null>;
107
57
  /** `aria-errormessage` value. Non-null when error text is present. */
108
58
  ariaErrorMessage: Readable<string | null>;
109
59
  /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */
110
60
  ariaInvalid: Readable<'true' | null>;
111
- /** `aria-labelledby` value. Non-null when a label is visible. */
112
- ariaLabelledBy: Readable<string | null>;
113
- /**
114
- * Returns a snapshot of the four ARIA attributes as a plain object.
115
- * Spread into a template bind call to avoid repeating each signal individually.
116
- *
117
- * @example
118
- * ```ts
119
- * bind(inputEl, { attr: field.ariaProps() });
120
- * ```
121
- */
122
- ariaProps(): AriaProps;
123
61
  /**
124
62
  * The stable `id` used for `aria-describedby` on the input. Points at the
125
63
  * assistive-text region (covers both helper text and error text per WAI-ARIA).
126
64
  */
127
65
  assistiveId: string;
66
+ /**
67
+ * Registers the real form field handle (the return value of `useField()`) so that
68
+ * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`
69
+ * for why this is a post-hoc call instead of a constructor option — the same forward
70
+ * reference applies here for choice fields.
71
+ */
72
+ attachFormField: (formField: {
73
+ reportValidity(): void;
74
+ }) => void;
128
75
  disabled: Readable<boolean>;
129
76
  /** Stable `id` for the inline error message element (`aria-errormessage`). */
130
77
  errorId: string;
131
78
  /** Reactive error text. Empty string when no error is set. */
132
79
  errorText: Readable<string>;
133
- fieldId: string;
134
80
  /** Reactive helper text. Empty string when no helper is set. */
135
81
  helperText: Readable<string>;
82
+ triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;
83
+ };
84
+ export type AssistiveStateOptions = {
85
+ disabled?: Readable<boolean | undefined>;
86
+ error?: Readable<string | undefined>;
87
+ helper?: Readable<string | undefined>;
88
+ validateOn?: Readable<ControlValidationMode>;
89
+ };
90
+ export declare const createAssistiveState: (options: AssistiveStateOptions) => AssistiveStateHandle;
91
+ export type LabelStateHandle = {
92
+ /** `aria-labelledby` value. Non-null when a label is visible. */
93
+ ariaLabelledBy: Readable<string | null>;
94
+ /** Stable `id` for the underlying form control — the `<label for="...">` target. */
95
+ fieldId: string;
136
96
  /** Stable `id` for the label element. Stamp this on your `<label id="...">`. */
137
97
  labelId: string;
138
98
  /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */
139
99
  labelVisible: Readable<boolean>;
140
- triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;
141
100
  };
101
+ export type LabelStateOptions = {
102
+ /**
103
+ * Override for label visibility that takes precedence over deriving it from
104
+ * the `label` text signal. Pass a computed signal that checks both prop and
105
+ * slot presence to enable slot-first composition.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);
110
+ * const label = createLabelState({ ...options, hasLabel });
111
+ * ```
112
+ */
113
+ hasLabel?: Readable<boolean>;
114
+ /**
115
+ * When provided, used directly as `fieldId` instead of generating one via
116
+ * `createStableId`. Useful in tests for deterministic ID assertions.
117
+ */
118
+ id?: string;
119
+ /**
120
+ * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are
121
+ * computed reactively from this value (see `hasLabel` for the slot-first override).
122
+ */
123
+ label?: Readable<string | undefined>;
124
+ /**
125
+ * Label placement signal. Defaults to `'inset'`.
126
+ *
127
+ * Not consumed by `createLabelState()` itself — visibility is driven solely by
128
+ * `hasLabel`/`label`. Threaded through so component authors can apply
129
+ * placement-specific styling (`label-placement` attribute) without a second
130
+ * option; if placement-dependent visibility is ever needed, wire it into
131
+ * `labelVisible` here instead of adding a new option.
132
+ */
133
+ labelPlacement?: Readable<LabelPlacement>;
134
+ prefix?: string;
135
+ };
136
+ export declare const createLabelState: (options: LabelStateOptions) => LabelStateHandle;
142
137
  /**
143
- * Composes the field triad — IDs, assistive state, and label state — into a
144
- * single `FieldHandle`. Both `createTextField` and `createChoiceField` build
145
- * on top of this.
138
+ * Common handle returned by `createField` — the base for both `TextFieldHandle`
139
+ * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;
140
+ * call those directly instead if a field has no visible `<label>` (see
141
+ * `createAssistiveState`'s doc comment).
142
+ *
143
+ * ARIA signals and label state are flat on the handle — no nested `aria` or
144
+ * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always
145
+ * present but only meaningful when `label`/`hasLabel` was passed — omit both
146
+ * and they resolve to an always-hidden, unreferenced label.
147
+ *
148
+ * @example
149
+ * ```html
150
+ * <label id="${labelId}" ?hidden="${() => !labelVisible.value}">...</label>
151
+ * <input
152
+ * :aria-labelledby="${ariaLabelledBy}"
153
+ * :aria-describedby="${ariaDescribedBy}"
154
+ * :aria-invalid="${ariaInvalid}" />
155
+ * ```
146
156
  */
157
+ export type FieldHandle = AssistiveStateHandle & LabelStateHandle;
158
+ /** Options shared by both `createTextField` and `createChoiceField`. */
159
+ export type FieldOptions = AssistiveStateOptions & LabelStateOptions;
147
160
  export declare const createField: (options: FieldOptions) => FieldHandle;
148
161
  //# sourceMappingURL=field-base.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.d.ts","sourceRoot":"","sources":["../../src/headless/field-base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"field-base.d.ts","sourceRoot":"","sources":["../../src/headless/field-base.ts"],"names":[],"mappings":"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"}
@@ -1,46 +1,57 @@
1
- import { createStableId as e } from "@vielzeug/ore";
2
- import { computed as t } from "@vielzeug/ripple";
1
+ import { warn as e } from "../_dev.js";
2
+ import { createStableId as t } from "@vielzeug/ore";
3
+ import { computed as n } from "@vielzeug/ripple";
3
4
  //#region src/headless/field-base.ts
4
- var n = (e) => t(() => ({
5
+ var r = (e) => n(() => ({
5
6
  errorText: e.error?.value ?? "",
6
7
  helperText: e.helper?.value ?? ""
7
- })), r = (e) => t(() => {
8
+ })), i = (e) => n(() => {
8
9
  let t = e.value?.value ?? "", n = e.maxLength?.value, r = Number(n), i = Number.isFinite(r) && r > 0 ? r : null, a = i !== null, o = a ? `${t.length} / ${i}` : "", s = a ? t.length / i : 0;
9
10
  return {
10
11
  counterAtLimit: a ? s >= 1 : !1,
11
12
  counterNearLimit: a ? s >= .9 && s < 1 : !1,
12
13
  counterText: o
13
14
  };
14
- }), i = (r) => {
15
- let i = t(() => !!r.disabled?.value), a = r.validateOn, o = r.id ?? e(r.prefix ?? "field"), s = e("helper"), c = (e) => {
16
- a?.value === e && r.getFormField?.()?.reportValidity();
17
- }, l = e("label"), u = e("error"), d = r.label ?? t(() => void 0), f = r.hasLabel ?? t(() => !!d.value), p = n({
18
- error: r.error,
19
- helper: r.helper
20
- }), m = t(() => p.value.errorText || p.value.helperText ? s : null), h = t(() => p.value.errorText ? u : null), g = t(() => p.value.errorText ? "true" : null), _ = t(() => f.value ? l : null);
15
+ }), a = (e, t = "counter") => e ? e.counterAtLimit ? `${t} at-limit` : e.counterNearLimit ? `${t} near-limit` : t : t, o = (i) => {
16
+ let a = n(() => !!i.disabled?.value), o = t("helper"), s = t("error"), c = r({
17
+ error: i.error,
18
+ helper: i.helper
19
+ }), l = n(() => c.value.errorText || c.value.helperText ? o : null), u = n(() => c.value.errorText ? s : null), d = n(() => c.value.errorText ? "true" : null), f = null;
21
20
  return {
22
- ariaDescribedBy: m,
23
- ariaErrorMessage: h,
24
- ariaInvalid: g,
25
- ariaLabelledBy: _,
26
- ariaProps: () => ({
27
- "aria-describedby": m.value,
28
- "aria-errormessage": h.value,
29
- "aria-invalid": g.value,
30
- "aria-labelledby": _.value
31
- }),
32
- assistiveId: s,
33
- disabled: i,
34
- errorId: u,
35
- errorText: t(() => p.value.errorText),
36
- fieldId: o,
37
- helperText: t(() => p.value.helperText),
38
- labelId: l,
39
- labelVisible: f,
40
- triggerValidation: c
21
+ ariaDescribedBy: l,
22
+ ariaErrorMessage: u,
23
+ ariaInvalid: d,
24
+ assistiveId: o,
25
+ attachFormField: (e) => {
26
+ f = e;
27
+ },
28
+ disabled: a,
29
+ errorId: s,
30
+ errorText: n(() => c.value.errorText),
31
+ helperText: n(() => c.value.helperText),
32
+ triggerValidation: (t) => {
33
+ if (i.validateOn?.value === t) {
34
+ if (!f) {
35
+ e("triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.");
36
+ return;
37
+ }
38
+ f.reportValidity();
39
+ }
40
+ }
41
41
  };
42
- };
42
+ }, s = (e) => {
43
+ let r = e.id ?? t(e.prefix ?? "field"), i = t("label"), a = e.label ?? n(() => void 0), o = e.hasLabel ?? n(() => !!a.value);
44
+ return {
45
+ ariaLabelledBy: n(() => o.value ? i : null),
46
+ fieldId: r,
47
+ labelId: i,
48
+ labelVisible: o
49
+ };
50
+ }, c = (e) => ({
51
+ ...o(e),
52
+ ...s(e)
53
+ });
43
54
  //#endregion
44
- export { r as createCounterState, n as createErrorHelperState, i as createField };
55
+ export { a as counterClassName, o as createAssistiveState, i as createCounterState, r as createErrorHelperState, c as createField, s as createLabelState };
45
56
 
46
57
  //# sourceMappingURL=field-base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-base.js","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Field options + handle ───────────────────────────────────────────────────\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n /**\n * Returns the underlying form field object used for validation triggering.\n * Called lazily — safe to return `null` before the first render.\n */\n getFormField?: () => { reportValidity(): void } | null;\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const field = createField({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n helper?: Readable<string | undefined>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not currently consumed by `createField()` itself — visibility is driven\n * solely by `hasLabel`/`label`. Threaded through so component authors can\n * apply placement-specific styling (`label-placement` attribute) without a\n * second prop; if placement-dependent visibility is ever needed, wire it\n * into `labelVisible$` in `createField()` instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n validateOn?: Readable<ControlValidationMode>;\n};\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Field handle ──────────────────────────────────────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`.\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\n/** Flat ARIA attribute bag ready to spread into a template bind call. */\nexport type AriaProps = {\n 'aria-describedby': string | null;\n 'aria-errormessage': string | null;\n 'aria-invalid': 'true' | null;\n 'aria-labelledby': string | null;\n};\n\nexport type FieldHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /**\n * Returns a snapshot of the four ARIA attributes as a plain object.\n * Spread into a template bind call to avoid repeating each signal individually.\n *\n * @example\n * ```ts\n * bind(inputEl, { attr: field.ariaProps() });\n * ```\n */\n ariaProps(): AriaProps;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n fieldId: string;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\n/**\n * Composes the field triad — IDs, assistive state, and label state — into a\n * single `FieldHandle`. Both `createTextField` and `createChoiceField` build\n * on top of this.\n */\nexport const createField = (options: FieldOptions): FieldHandle => {\n // ── Core ──────────────────────────────────────────────────────────────────\n const disabled = computed(() => Boolean(options.disabled?.value));\n const validateOn = options.validateOn;\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const assistiveId = createStableId('helper');\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (validateOn?.value === on) options.getFormField?.()?.reportValidity();\n };\n\n // ── Label + ARIA state ────────────────────────────────────────────────────\n const labelId = createStableId('label');\n const errorId = createStableId('error');\n\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible$ = options.hasLabel ?? computed(() => Boolean(label$.value));\n\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n const ariaLabelledBy = computed(() => (labelVisible$.value ? labelId : null));\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n ariaLabelledBy,\n ariaProps: () => ({\n 'aria-describedby': ariaDescribedBy.value,\n 'aria-errormessage': ariaErrorMessage.value,\n 'aria-invalid': ariaInvalid.value,\n 'aria-labelledby': ariaLabelledBy.value,\n }),\n assistiveId,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n fieldId,\n helperText: computed(() => resolvedAssistive.value.helperText),\n labelId,\n labelVisible: labelVisible$,\n triggerValidation,\n };\n};\n"],"mappings":";;;AAoEA,IAAa,KAA0B,MACrC,SAAgB;CACd,WAAW,EAAQ,OAAO,SAAS;CACnC,YAAY,EAAQ,QAAQ,SAAS;AACvC,EAAE,GAyBS,KAAsB,MACjC,QAA6B;CAC3B,IAAM,IAAQ,EAAQ,OAAO,SAAS,IAChC,IAAY,EAAQ,WAAW,OAC/B,IAAkB,OAAO,CAAS,GAClC,IAAiB,OAAO,SAAS,CAAe,KAAK,IAAkB,IAAI,IAAkB,MAC7F,IAAa,MAAmB,MAChC,IAAc,IAAa,GAAG,EAAM,OAAO,KAAK,MAAmB,IACnE,IAAQ,IAAa,EAAM,SAAS,IAAiB;CAE3D,OAAO;EACL,gBAAgB,IAAa,KAAS,IAAI;EAC1C,kBAAkB,IAAa,KAAS,MAAO,IAAQ,IAAI;EAC3D;CACF;AACF,CAAC,GA4EU,KAAe,MAAuC;CAEjE,IAAM,IAAW,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC1D,IAAa,EAAQ,YACrB,IAAU,EAAQ,MAAM,EAAe,EAAQ,UAAU,OAAO,GAChE,IAAc,EAAe,QAAQ,GAErC,KAAqB,MAA4D;EACrF,AAAI,GAAY,UAAU,KAAI,EAAQ,eAAe,CAAC,EAAE,eAAe;CACzE,GAGM,IAAU,EAAe,OAAO,GAChC,IAAU,EAAe,OAAO,GAEhC,IAAS,EAAQ,SAAS,QAAe,KAAA,CAAS,GAMlD,IAAgB,EAAQ,YAAY,QAAe,EAAQ,EAAO,KAAM,GAExE,IAAoB,EAAuB;EAAE,OAAO,EAAQ;EAAO,QAAQ,EAAQ;CAAO,CAAC,GAE3F,IAAkB,QACtB,EAAkB,MAAM,aAAa,EAAkB,MAAM,aAAa,IAAc,IAC1F,GACM,IAAmB,QAAgB,EAAkB,MAAM,YAAY,IAAU,IAAK,GACtF,IAAc,QAA+B,EAAkB,MAAM,YAAY,SAAS,IAAK,GAC/F,IAAiB,QAAgB,EAAc,QAAQ,IAAU,IAAK;CAE5E,OAAO;EACL;EACA;EACA;EACA;EACA,kBAAkB;GAChB,oBAAoB,EAAgB;GACpC,qBAAqB,EAAiB;GACtC,gBAAgB,EAAY;GAC5B,mBAAmB,EAAe;EACpC;EACA;EACA;EACA;EACA,WAAW,QAAe,EAAkB,MAAM,SAAS;EAC3D;EACA,YAAY,QAAe,EAAkB,MAAM,UAAU;EAC7D;EACA,cAAc;EACd;CACF;AACF"}
1
+ {"version":3,"file":"field-base.js","names":[],"sources":["../../src/headless/field-base.ts"],"sourcesContent":["import { createStableId } from '@vielzeug/ore';\nimport { computed, type Readable } from '@vielzeug/ripple';\n\nimport { warn } from '../_dev';\n\n// ── Validation / context types ────────────────────────────────────────────────\n\nexport type ValidationTrigger = 'blur' | 'change' | 'input' | 'submit';\nexport type ControlValidationMode = ValidationTrigger | undefined;\n\n// ── Error/helper assistive state ──────────────────────────────────────────────\n\nexport type ErrorHelperState = {\n errorText: string;\n helperText: string;\n};\n\nexport type ErrorHelperOptions = {\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n};\n\nexport const createErrorHelperState = (options: ErrorHelperOptions): Readable<ErrorHelperState> =>\n computed(() => ({\n errorText: options.error?.value ?? '',\n helperText: options.helper?.value ?? '',\n }));\n\n// ── Counter state (opt-in) ─────────────────────────────────────────────────────\n\n/** Counter state for text fields with `maxLength`. */\nexport type CounterState = {\n counterAtLimit: boolean;\n counterNearLimit: boolean;\n counterText: string;\n};\n\nexport type CounterOptions = {\n maxLength?: Readable<number | undefined>;\n value: Readable<string | undefined>;\n};\n\n/**\n * Creates a reactive counter state signal for text fields with `maxLength`.\n * Only call this when `maxLength` may be set — the state is genuinely opt-in.\n *\n * @example\n * ```ts\n * const counter = createCounterState({ value: tf.value, maxLength: props.maxlength });\n * ```\n */\nexport const createCounterState = (options: CounterOptions): Readable<CounterState> =>\n computed<CounterState>(() => {\n const value = options.value?.value ?? '';\n const maxLength = options.maxLength?.value;\n const parsedMaxLength = Number(maxLength);\n const validMaxLength = Number.isFinite(parsedMaxLength) && parsedMaxLength > 0 ? parsedMaxLength : null;\n const hasCounter = validMaxLength !== null;\n const counterText = hasCounter ? `${value.length} / ${validMaxLength}` : '';\n const ratio = hasCounter ? value.length / validMaxLength : 0;\n\n return {\n counterAtLimit: hasCounter ? ratio >= 1 : false,\n counterNearLimit: hasCounter ? ratio >= 0.9 && ratio < 1 : false,\n counterText,\n };\n });\n\n/**\n * The single implementation of \"which modifier class does this counter state map to\" —\n * every field with a character counter (`ore-input`, `ore-textarea`, `ore-message-composer`)\n * renders the identical near-limit/at-limit styling, so this is the one place that decides it.\n *\n * @example\n * ```ts\n * html`<span class=\"${() => counterClassName(counter?.value)}\">...</span>`\n * ```\n */\nexport const counterClassName = (counter: CounterState | undefined, base = 'counter'): string => {\n if (!counter) return base;\n\n if (counter.counterAtLimit) return `${base} at-limit`;\n\n if (counter.counterNearLimit) return `${base} near-limit`;\n\n return base;\n};\n\n// ── Label placement ───────────────────────────────────────────────────────────\n\nexport type LabelPlacement = 'inset' | 'outside' | undefined;\n\n// ── Assistive state (error/helper text, describedby/errormessage/invalid) ─────\n\n/**\n * A field's error/helper/disabled/validation-trigger state — everything an input\n * needs *except* a visible `<label>`. Split out from label state (below) because\n * not every field has one: `ore-message-composer` names itself via `aria-label`\n * (a chat composer's accessible name conventionally comes from context/placeholder,\n * not a floating `<label>`), so it composes this directly instead of pulling in\n * label plumbing it can't use. `createField()` composes both for fields that do\n * render a `<label>`.\n */\nexport type AssistiveStateHandle = {\n /** `aria-describedby` value. Non-null when helper or error text is present. */\n ariaDescribedBy: Readable<string | null>;\n /** `aria-errormessage` value. Non-null when error text is present. */\n ariaErrorMessage: Readable<string | null>;\n /** `aria-invalid` value. `'true'` when error text is present, otherwise `null`. */\n ariaInvalid: Readable<'true' | null>;\n /**\n * The stable `id` used for `aria-describedby` on the input. Points at the\n * assistive-text region (covers both helper text and error text per WAI-ARIA).\n */\n assistiveId: string;\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` can call its `reportValidity()`. See `TextFieldHandle.attachFormField`\n * for why this is a post-hoc call instead of a constructor option — the same forward\n * reference applies here for choice fields.\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n disabled: Readable<boolean>;\n /** Stable `id` for the inline error message element (`aria-errormessage`). */\n errorId: string;\n /** Reactive error text. Empty string when no error is set. */\n errorText: Readable<string>;\n /** Reactive helper text. Empty string when no helper is set. */\n helperText: Readable<string>;\n triggerValidation: (on: Extract<ValidationTrigger, 'blur' | 'change'>) => void;\n};\n\nexport type AssistiveStateOptions = {\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n helper?: Readable<string | undefined>;\n validateOn?: Readable<ControlValidationMode>;\n};\n\nexport const createAssistiveState = (options: AssistiveStateOptions): AssistiveStateHandle => {\n const disabled = computed(() => Boolean(options.disabled?.value));\n const assistiveId = createStableId('helper');\n const errorId = createStableId('error');\n const resolvedAssistive = createErrorHelperState({ error: options.error, helper: options.helper });\n\n const ariaDescribedBy = computed(() =>\n resolvedAssistive.value.errorText || resolvedAssistive.value.helperText ? assistiveId : null,\n );\n const ariaErrorMessage = computed(() => (resolvedAssistive.value.errorText ? errorId : null));\n const ariaInvalid = computed<'true' | null>(() => (resolvedAssistive.value.errorText ? 'true' : null));\n\n let formField: { reportValidity(): void } | null = null;\n const attachFormField = (nextFormField: { reportValidity(): void }): void => {\n formField = nextFormField;\n };\n\n const triggerValidation = (on: Extract<ValidationTrigger, 'blur' | 'change'>): void => {\n if (options.validateOn?.value !== on) return;\n\n if (!formField) {\n // Not user-facing — this only fires when a component author wired `validateOn` but\n // forgot the matching `attachFormField(useField(...))` call, so validation silently\n // never runs instead of erroring where the mistake actually is.\n warn(\n \"triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.\",\n );\n\n return;\n }\n\n formField.reportValidity();\n };\n\n return {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n assistiveId,\n attachFormField,\n disabled,\n errorId,\n errorText: computed(() => resolvedAssistive.value.errorText),\n helperText: computed(() => resolvedAssistive.value.helperText),\n triggerValidation,\n };\n};\n\n// ── Label state (visible `<label>` + `aria-labelledby`) ────────────────────────\n\nexport type LabelStateHandle = {\n /** `aria-labelledby` value. Non-null when a label is visible. */\n ariaLabelledBy: Readable<string | null>;\n /** Stable `id` for the underlying form control — the `<label for=\"...\">` target. */\n fieldId: string;\n /** Stable `id` for the label element. Stamp this on your `<label id=\"...\">`. */\n labelId: string;\n /** Whether the label should be visible. Use to toggle `hidden` on the `<label>`. */\n labelVisible: Readable<boolean>;\n};\n\nexport type LabelStateOptions = {\n /**\n * Override for label visibility that takes precedence over deriving it from\n * the `label` text signal. Pass a computed signal that checks both prop and\n * slot presence to enable slot-first composition.\n *\n * @example\n * ```ts\n * const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n * const label = createLabelState({ ...options, hasLabel });\n * ```\n */\n hasLabel?: Readable<boolean>;\n /**\n * When provided, used directly as `fieldId` instead of generating one via\n * `createStableId`. Useful in tests for deterministic ID assertions.\n */\n id?: string;\n /**\n * Label text signal. When provided, `labelVisible` and `ariaLabelledBy` are\n * computed reactively from this value (see `hasLabel` for the slot-first override).\n */\n label?: Readable<string | undefined>;\n /**\n * Label placement signal. Defaults to `'inset'`.\n *\n * Not consumed by `createLabelState()` itself — visibility is driven solely by\n * `hasLabel`/`label`. Threaded through so component authors can apply\n * placement-specific styling (`label-placement` attribute) without a second\n * option; if placement-dependent visibility is ever needed, wire it into\n * `labelVisible` here instead of adding a new option.\n */\n labelPlacement?: Readable<LabelPlacement>;\n prefix?: string;\n};\n\nexport const createLabelState = (options: LabelStateOptions): LabelStateHandle => {\n const fieldId = options.id ?? createStableId(options.prefix ?? 'field');\n const labelId = createStableId('label');\n const label$ = options.label ?? computed(() => undefined);\n\n // Slot-first composition: if the caller provides `hasLabel`, use it instead\n // of deriving visibility purely from the label text signal. This allows\n // components with a `<slot name=\"label\">` to stay visible even when the\n // `label` prop is empty but a slotted label element is present.\n const labelVisible = options.hasLabel ?? computed(() => Boolean(label$.value));\n const ariaLabelledBy = computed(() => (labelVisible.value ? labelId : null));\n\n return { ariaLabelledBy, fieldId, labelId, labelVisible };\n};\n\n// ── Field handle (assistive + label, composed) ──────────────────────────────\n\n/**\n * Common handle returned by `createField` — the base for both `TextFieldHandle`\n * and `ChoiceFieldHandle`. Composes `createAssistiveState` + `createLabelState`;\n * call those directly instead if a field has no visible `<label>` (see\n * `createAssistiveState`'s doc comment).\n *\n * ARIA signals and label state are flat on the handle — no nested `aria` or\n * `label` sub-objects. `labelId`/`labelVisible`/`ariaLabelledBy` are always\n * present but only meaningful when `label`/`hasLabel` was passed — omit both\n * and they resolve to an always-hidden, unreferenced label.\n *\n * @example\n * ```html\n * <label id=\"${labelId}\" ?hidden=\"${() => !labelVisible.value}\">...</label>\n * <input\n * :aria-labelledby=\"${ariaLabelledBy}\"\n * :aria-describedby=\"${ariaDescribedBy}\"\n * :aria-invalid=\"${ariaInvalid}\" />\n * ```\n */\nexport type FieldHandle = AssistiveStateHandle & LabelStateHandle;\n\n/** Options shared by both `createTextField` and `createChoiceField`. */\nexport type FieldOptions = AssistiveStateOptions & LabelStateOptions;\n\nexport const createField = (options: FieldOptions): FieldHandle => ({\n ...createAssistiveState(options),\n ...createLabelState(options),\n});\n"],"mappings":";;;;AAsBA,IAAa,KAA0B,MACrC,SAAgB;CACd,WAAW,EAAQ,OAAO,SAAS;CACnC,YAAY,EAAQ,QAAQ,SAAS;AACvC,EAAE,GAyBS,KAAsB,MACjC,QAA6B;CAC3B,IAAM,IAAQ,EAAQ,OAAO,SAAS,IAChC,IAAY,EAAQ,WAAW,OAC/B,IAAkB,OAAO,CAAS,GAClC,IAAiB,OAAO,SAAS,CAAe,KAAK,IAAkB,IAAI,IAAkB,MAC7F,IAAa,MAAmB,MAChC,IAAc,IAAa,GAAG,EAAM,OAAO,KAAK,MAAmB,IACnE,IAAQ,IAAa,EAAM,SAAS,IAAiB;CAE3D,OAAO;EACL,gBAAgB,IAAa,KAAS,IAAI;EAC1C,kBAAkB,IAAa,KAAS,MAAO,IAAQ,IAAI;EAC3D;CACF;AACF,CAAC,GAYU,KAAoB,GAAmC,IAAO,cACpE,IAED,EAAQ,iBAAuB,GAAG,EAAK,aAEvC,EAAQ,mBAAyB,GAAG,EAAK,eAEtC,IANc,GA4DV,KAAwB,MAAyD;CAC5F,IAAM,IAAW,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC1D,IAAc,EAAe,QAAQ,GACrC,IAAU,EAAe,OAAO,GAChC,IAAoB,EAAuB;EAAE,OAAO,EAAQ;EAAO,QAAQ,EAAQ;CAAO,CAAC,GAE3F,IAAkB,QACtB,EAAkB,MAAM,aAAa,EAAkB,MAAM,aAAa,IAAc,IAC1F,GACM,IAAmB,QAAgB,EAAkB,MAAM,YAAY,IAAU,IAAK,GACtF,IAAc,QAA+B,EAAkB,MAAM,YAAY,SAAS,IAAK,GAEjG,IAA+C;CAsBnD,OAAO;EACL;EACA;EACA;EACA;EACA,kBA1BuB,MAAoD;GAC3E,IAAY;EACd;EAyBE;EACA;EACA,WAAW,QAAe,EAAkB,MAAM,SAAS;EAC3D,YAAY,QAAe,EAAkB,MAAM,UAAU;EAC7D,oBA3ByB,MAA4D;GACjF,MAAQ,YAAY,UAAU,GAElC;QAAI,CAAC,GAAW;KAId,EACE,2KACF;KAEA;IACF;IAEA,EAAU,eAAe;GAFzB;EAGF;CAaA;AACF,GAmDa,KAAoB,MAAiD;CAChF,IAAM,IAAU,EAAQ,MAAM,EAAe,EAAQ,UAAU,OAAO,GAChE,IAAU,EAAe,OAAO,GAChC,IAAS,EAAQ,SAAS,QAAe,KAAA,CAAS,GAMlD,IAAe,EAAQ,YAAY,QAAe,EAAQ,EAAO,KAAM;CAG7E,OAAO;EAAE,gBAFc,QAAgB,EAAa,QAAQ,IAAU,IAE7D;EAAgB;EAAS;EAAS;CAAa;AAC1D,GA6Ba,KAAe,OAAwC;CAClE,GAAG,EAAqB,CAAO;CAC/B,GAAG,EAAiB,CAAO;AAC7B"}
@@ -1,8 +1,10 @@
1
1
  export { lifecycleSignal } from './ore';
2
- export { createErrorHelperState, type AriaProps, type ControlValidationMode, type CounterOptions, type CounterState, type ErrorHelperOptions, type ErrorHelperState, type FieldHandle, type FieldOptions, type LabelPlacement, type ValidationTrigger, } from './field-base';
2
+ export { createAutoResize, type AutoResizeControl, type AutoResizeOptions } from './auto-resize';
3
+ export { counterClassName, createAssistiveState, createErrorHelperState, createField, createLabelState, type AssistiveStateHandle, type AssistiveStateOptions, type ControlValidationMode, type CounterOptions, type CounterState, type ErrorHelperOptions, type ErrorHelperState, type FieldHandle, type FieldOptions, type LabelPlacement, type LabelStateHandle, type LabelStateOptions, type ValidationTrigger, } from './field-base';
3
4
  export { createTextField, type TextFieldDetach, type TextFieldHandle, type TextFieldOptions } from './text-field';
4
5
  export { createChoiceField, type ChoiceChangeDetail, type ChoiceFieldHandle, type ChoiceFieldOptions, } from './choice-field';
5
6
  export { createCheckable, type CheckableChangePayload, type CheckableHandle, type CheckableOptions } from './checkable';
7
+ export { createComposerControl, type ComposerControl, type ComposerControlOptions, type SendShortcut, } from './composer';
6
8
  export { type DialogCloseReason, type DropdownCloseReason, type OverlayCloseDetail, type OverlayControl, type OverlayOpenDetail, type OverlayOpenReason, } from './overlay';
7
9
  export { createOptionList, type BaseOptionItem, type OptionListHandle, type OptionListOptions, type PlacementOptions, } from './option-list';
8
10
  export { createPaginatedList, type PaginatedListHandle, type PaginatedListOptions } from './paginated-list';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/headless/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/headless/index.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("./field-base.cjs"),t=require("./signals.cjs");require("@vielzeug/ripple");var n=n=>{let r=t.syncedSignal(n.value,n.signal,e=>String(e??``)),i=e.createField(n),a=n.maxLength?e.createCounterState({maxLength:n.maxLength,value:r}):null,o=e=>{e?.preventDefault?.(),r.value=``,n.onInput?.(e??new InputEvent(`input`,{bubbles:!0}),``),n.onChange?.(e??new Event(`change`,{bubbles:!0}),``),i.triggerValidation(`change`)},s=(e,t)=>{let a=[],o=(t,n)=>{e.addEventListener(t,n),a.push(()=>e.removeEventListener(t,n))};n.onFocus&&o(`focus`,e=>n.onFocus(e)),o(`input`,t=>{t.stopPropagation(),n.onBeforeInput?.(t),r.value=e.value,n.onInput?.(t,e.value)}),o(`change`,t=>{t.stopPropagation(),r.value=e.value,n.onChange?.(t,e.value),i.triggerValidation(`change`)}),o(`blur`,e=>{n.onBlur?.(e),i.triggerValidation(`blur`)});let s=!1,c=()=>{if(!s){s=!0;for(let e of a)e()}};return t?.addEventListener(`abort`,c,{once:!0}),c};return{...i,clear:o,counter:a,value:r,wire:s}};exports.createTextField=n;
1
+ const e=require("./field-base.cjs"),t=require("./signals.cjs");let n=require("@vielzeug/ripple");var r=e=>e.trim()===``,i=e=>String(e??``),a=a=>{let o=t.syncedSignal(a.value,a.signal,i),s=e.createField(a),c=a.maxLength?e.createCounterState({maxLength:a.maxLength,value:o}):null,l=(0,n.computed)(()=>!a.readonly?.value&&a.required?.value&&r(o.value)?{valueMissing:!0}:null),u=(0,n.computed)(()=>l.value?a.requiredMessage?.value||`This field is required.`:``),d=()=>{o.value=i(a.value.value),s.triggerValidation(`change`)},f=e=>{e?.preventDefault?.(),o.value=``,a.onInput?.(e??new InputEvent(`input`,{bubbles:!0}),``),a.onChange?.(e??new Event(`change`,{bubbles:!0}),``),s.triggerValidation(`change`)},p=(e,t)=>{let n=[],r=(t,r)=>{e.addEventListener(t,r),n.push(()=>e.removeEventListener(t,r))};a.onFocus&&r(`focus`,e=>a.onFocus(e)),r(`input`,t=>{t.stopPropagation(),a.onBeforeInput?.(t),o.value=e.value,a.onInput?.(t,e.value)}),r(`change`,t=>{t.stopPropagation(),o.value=e.value,a.onChange?.(t,e.value),s.triggerValidation(`change`)}),r(`blur`,e=>{a.onBlur?.(e),s.triggerValidation(`blur`)});let i=!1,c=()=>{if(!i){i=!0;for(let e of n)e()}};return t?.addEventListener(`abort`,c,{once:!0}),c};return{...s,clear:f,counter:c,reset:d,validationMessage:u,validity:l,value:o,wire:p}};exports.createTextField=a;
2
2
  //# sourceMappingURL=text-field.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"text-field.cjs","names":[],"sources":["../../src/headless/text-field.ts"],"sourcesContent":["import { type Readable, type Signal } from '@vielzeug/ripple';\n\nimport { type CounterState, createCounterState, createField, type FieldHandle, type FieldOptions } from './field-base';\nimport { syncedSignal } from './signals';\n\n// ── Text field ────────────────────────────────────────────────────────────────\n\n/** Detach function returned by `wire()`. Call to remove element listeners. */\nexport type TextFieldDetach = () => void;\n\nexport type TextFieldOptions = FieldOptions & {\n maxLength?: Readable<number | undefined>;\n /**\n * Called synchronously before value extraction on every input event.\n * Use only for DOM side-effects (e.g. auto-resize measurement) that must\n * run before the reactive value is updated.\n */\n onBeforeInput?: (event: Event) => void;\n onBlur?: (event: FocusEvent) => void;\n onChange?: (event: Event, value: string) => void;\n onFocus?: (event: FocusEvent) => void;\n onInput?: (event: Event, value: string) => void;\n /** `AbortSignal` from the component lifecycle. The internal value-sync watcher is disposed on abort. */\n signal: AbortSignal;\n value: Readable<string | undefined>;\n};\n\nexport type TextFieldHandle = FieldHandle & {\n /** Clears the field value and fires synthetic input/change events. */\n clear: (event?: Event) => void;\n /**\n * Reactive counter state. Non-null when `maxLength` was provided; `null` otherwise.\n * Components should only render a counter element when this is non-null.\n */\n counter: Readable<CounterState> | null;\n /** The local mutable field value (two-way bound to the input element via `wire()`). */\n value: Signal<string>;\n /**\n * Attaches event listeners to the underlying input or textarea element.\n *\n * When `signal` is provided, the listeners are detached automatically when\n * the signal's controller calls `abort()`. Without a signal the returned\n * `TextFieldDetach` must be called manually.\n *\n * Safe to call multiple times — each call attaches a fresh set of listeners\n * and returns a distinct detach function. Calling detach multiple times is\n * a no-op (guarded internally).\n */\n wire: (el: HTMLInputElement | HTMLTextAreaElement, signal?: AbortSignal) => TextFieldDetach;\n};\n\nexport const createTextField = (options: TextFieldOptions): TextFieldHandle => {\n const value = syncedSignal(options.value, options.signal, (v) => String(v ?? ''));\n\n const field = createField(options);\n const counter = options.maxLength ? createCounterState({ maxLength: options.maxLength, value }) : null;\n\n const clear = (event?: Event): void => {\n event?.preventDefault?.();\n value.value = '';\n options.onInput?.(event ?? new InputEvent('input', { bubbles: true }), '');\n options.onChange?.(event ?? new Event('change', { bubbles: true }), '');\n field.triggerValidation('change');\n };\n\n const wire = (element: HTMLInputElement | HTMLTextAreaElement, signal?: AbortSignal): TextFieldDetach => {\n const cleanups: Array<() => void> = [];\n const on = (type: string, handler: EventListener): void => {\n element.addEventListener(type, handler);\n cleanups.push(() => element.removeEventListener(type, handler));\n };\n\n if (options.onFocus) {\n on('focus', (e) => options.onFocus!(e as FocusEvent));\n }\n\n on('input', (e) => {\n e.stopPropagation();\n options.onBeforeInput?.(e);\n value.value = element.value;\n options.onInput?.(e, element.value);\n });\n\n on('change', (e) => {\n e.stopPropagation();\n value.value = element.value;\n options.onChange?.(e, element.value);\n field.triggerValidation('change');\n });\n\n on('blur', (e) => {\n options.onBlur?.(e as FocusEvent);\n field.triggerValidation('blur');\n });\n\n let detached = false;\n const detach = (): void => {\n if (detached) return;\n\n detached = true;\n for (const dispose of cleanups) dispose();\n };\n\n signal?.addEventListener('abort', detach, { once: true });\n\n return detach;\n };\n\n return { ...field, clear, counter, value, wire };\n};\n"],"mappings":"2FAmDA,IAAa,EAAmB,GAA+C,CAC7E,IAAM,EAAQ,EAAA,aAAa,EAAQ,MAAO,EAAQ,OAAS,GAAM,OAAO,GAAK,EAAE,CAAC,EAE1E,EAAQ,EAAA,YAAY,CAAO,EAC3B,EAAU,EAAQ,UAAY,EAAA,mBAAmB,CAAE,UAAW,EAAQ,UAAW,OAAM,CAAC,EAAI,KAE5F,EAAS,GAAwB,CACrC,GAAO,iBAAiB,EACxB,EAAM,MAAQ,GACd,EAAQ,UAAU,GAAS,IAAI,WAAW,QAAS,CAAE,QAAS,EAAK,CAAC,EAAG,EAAE,EACzE,EAAQ,WAAW,GAAS,IAAI,MAAM,SAAU,CAAE,QAAS,EAAK,CAAC,EAAG,EAAE,EACtE,EAAM,kBAAkB,QAAQ,CAClC,EAEM,GAAQ,EAAiD,IAA0C,CACvG,IAAM,EAA8B,CAAC,EAC/B,GAAM,EAAc,IAAiC,CACzD,EAAQ,iBAAiB,EAAM,CAAO,EACtC,EAAS,SAAW,EAAQ,oBAAoB,EAAM,CAAO,CAAC,CAChE,EAEI,EAAQ,SACV,EAAG,QAAU,GAAM,EAAQ,QAAS,CAAe,CAAC,EAGtD,EAAG,QAAU,GAAM,CACjB,EAAE,gBAAgB,EAClB,EAAQ,gBAAgB,CAAC,EACzB,EAAM,MAAQ,EAAQ,MACtB,EAAQ,UAAU,EAAG,EAAQ,KAAK,CACpC,CAAC,EAED,EAAG,SAAW,GAAM,CAClB,EAAE,gBAAgB,EAClB,EAAM,MAAQ,EAAQ,MACtB,EAAQ,WAAW,EAAG,EAAQ,KAAK,EACnC,EAAM,kBAAkB,QAAQ,CAClC,CAAC,EAED,EAAG,OAAS,GAAM,CAChB,EAAQ,SAAS,CAAe,EAChC,EAAM,kBAAkB,MAAM,CAChC,CAAC,EAED,IAAI,EAAW,GACT,MAAqB,CACrB,MAEJ,GAAW,GACX,IAAK,IAAM,KAAW,EAAU,EAAQ,CAD7B,CAEb,EAIA,OAFA,GAAQ,iBAAiB,QAAS,EAAQ,CAAE,KAAM,EAAK,CAAC,EAEjD,CACT,EAEA,MAAO,CAAE,GAAG,EAAO,QAAO,UAAS,QAAO,MAAK,CACjD"}
1
+ {"version":3,"file":"text-field.cjs","names":[],"sources":["../../src/headless/text-field.ts"],"sourcesContent":["import { computed, type Readable, type Signal } from '@vielzeug/ripple';\n\nimport { type CounterState, createCounterState, createField, type FieldHandle, type FieldOptions } from './field-base';\nimport { syncedSignal } from './signals';\n\n// ── Text field ────────────────────────────────────────────────────────────────\n\n/** Detach function returned by `wire()`. Call to remove element listeners. */\nexport type TextFieldDetach = () => void;\n\nexport type TextFieldOptions = FieldOptions & {\n maxLength?: Readable<number | undefined>;\n /**\n * Called synchronously before value extraction on every input event.\n * Use only for DOM side-effects (e.g. auto-resize measurement) that must\n * run before the reactive value is updated.\n */\n onBeforeInput?: (event: Event) => void;\n onBlur?: (event: FocusEvent) => void;\n onChange?: (event: Event, value: string) => void;\n onFocus?: (event: FocusEvent) => void;\n onInput?: (event: Event, value: string) => void;\n /**\n * Bars the field from constraint validation while true — matches the native HTML rule that a\n * `readonly` field is never a candidate for constraint validation (a `readonly required`\n * field is always valid, per spec), so `validity`/`validationMessage` don't feed a false\n * `valueMissing` into `useField()` just because a read-only field happens to be blank.\n */\n readonly?: Readable<boolean | undefined>;\n /** Marks a blank value invalid — feeds `validity`/`validationMessage` (see below). */\n required?: Readable<boolean | undefined>;\n /** Message for the blank+required case. Defaults to `'This field is required.'`. */\n requiredMessage?: Readable<string | undefined>;\n /** `AbortSignal` from the component lifecycle. The internal value-sync watcher is disposed on abort. */\n signal: AbortSignal;\n value: Readable<string | undefined>;\n};\n\nexport type TextFieldHandle = FieldHandle & {\n /**\n * Registers the real form field handle (the return value of `useField()`) so that\n * `triggerValidation()` — called internally on blur/change — can call its\n * `reportValidity()`. `useField()` itself needs `value` (below) to exist first, so this\n * is a two-step wiring rather than a constructor option:\n *\n * ```ts\n * const tf = createTextField({ value: props.value, ... });\n * const formField = useField({ value: tf.value, ... });\n * tf.attachFormField(formField);\n * ```\n */\n attachFormField: (formField: { reportValidity(): void }) => void;\n /** Clears the field value and fires synthetic input/change events. */\n clear: (event?: Event) => void;\n /**\n * Reactive counter state. Non-null when `maxLength` was provided; `null` otherwise.\n * Components should only render a counter element when this is non-null.\n */\n counter: Readable<CounterState> | null;\n /**\n * Restores the value to whatever the `value` option currently holds (native form\n * `reset()` semantics: a `<input>` reverts to its *current* `value` content attribute,\n * not a frozen snapshot from element creation — so setting the attribute programmatically\n * after mount changes what a later reset reverts to). Wire into `useField({ onReset: tf.reset })`.\n */\n reset: () => void;\n /** Reactive validation message paired with `validity`. Empty string when valid. */\n validationMessage: Readable<string>;\n /**\n * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and blank,\n * `null` (valid) otherwise. Pass straight to `useField({ validity: tf.validity })`.\n */\n validity: Readable<ValidityStateFlags | null>;\n /** The local mutable field value (two-way bound to the input element via `wire()`). */\n value: Signal<string>;\n /**\n * Attaches event listeners to the underlying input or textarea element.\n *\n * When `signal` is provided, the listeners are detached automatically when\n * the signal's controller calls `abort()`. Without a signal the returned\n * `TextFieldDetach` must be called manually.\n *\n * Safe to call multiple times — each call attaches a fresh set of listeners\n * and returns a distinct detach function. Calling detach multiple times is\n * a no-op (guarded internally).\n */\n wire: (el: HTMLInputElement | HTMLTextAreaElement, signal?: AbortSignal) => TextFieldDetach;\n};\n\nconst isBlank = (value: string): boolean => value.trim() === '';\nconst toFieldValue = (v: string | undefined): string => String(v ?? '');\n\nexport const createTextField = (options: TextFieldOptions): TextFieldHandle => {\n const value = syncedSignal(options.value, options.signal, toFieldValue);\n\n const field = createField(options);\n const counter = options.maxLength ? createCounterState({ maxLength: options.maxLength, value }) : null;\n\n const validity = computed<ValidityStateFlags | null>(() =>\n !options.readonly?.value && options.required?.value && isBlank(value.value) ? { valueMissing: true } : null,\n );\n const validationMessage = computed(() =>\n validity.value ? options.requiredMessage?.value || 'This field is required.' : '',\n );\n\n const reset = (): void => {\n value.value = toFieldValue(options.value.value);\n field.triggerValidation('change');\n };\n\n const clear = (event?: Event): void => {\n event?.preventDefault?.();\n value.value = '';\n options.onInput?.(event ?? new InputEvent('input', { bubbles: true }), '');\n options.onChange?.(event ?? new Event('change', { bubbles: true }), '');\n field.triggerValidation('change');\n };\n\n const wire = (element: HTMLInputElement | HTMLTextAreaElement, signal?: AbortSignal): TextFieldDetach => {\n const cleanups: Array<() => void> = [];\n const on = (type: string, handler: EventListener): void => {\n element.addEventListener(type, handler);\n cleanups.push(() => element.removeEventListener(type, handler));\n };\n\n if (options.onFocus) {\n on('focus', (e) => options.onFocus!(e as FocusEvent));\n }\n\n on('input', (e) => {\n e.stopPropagation();\n options.onBeforeInput?.(e);\n value.value = element.value;\n options.onInput?.(e, element.value);\n });\n\n on('change', (e) => {\n e.stopPropagation();\n value.value = element.value;\n options.onChange?.(e, element.value);\n field.triggerValidation('change');\n });\n\n on('blur', (e) => {\n options.onBlur?.(e as FocusEvent);\n field.triggerValidation('blur');\n });\n\n let detached = false;\n const detach = (): void => {\n if (detached) return;\n\n detached = true;\n for (const dispose of cleanups) dispose();\n };\n\n signal?.addEventListener('abort', detach, { once: true });\n\n return detach;\n };\n\n return { ...field, clear, counter, reset, validationMessage, validity, value, wire };\n};\n"],"mappings":"iGAyFA,IAAM,EAAW,GAA2B,EAAM,KAAK,IAAM,GACvD,EAAgB,GAAkC,OAAO,GAAK,EAAE,EAEzD,EAAmB,GAA+C,CAC7E,IAAM,EAAQ,EAAA,aAAa,EAAQ,MAAO,EAAQ,OAAQ,CAAY,EAEhE,EAAQ,EAAA,YAAY,CAAO,EAC3B,EAAU,EAAQ,UAAY,EAAA,mBAAmB,CAAE,UAAW,EAAQ,UAAW,OAAM,CAAC,EAAI,KAE5F,GAAA,EAAA,EAAA,SAAA,KACJ,CAAC,EAAQ,UAAU,OAAS,EAAQ,UAAU,OAAS,EAAQ,EAAM,KAAK,EAAI,CAAE,aAAc,EAAK,EAAI,IACzG,EACM,GAAA,EAAA,EAAA,SAAA,KACJ,EAAS,MAAQ,EAAQ,iBAAiB,OAAS,0BAA4B,EACjF,EAEM,MAAoB,CACxB,EAAM,MAAQ,EAAa,EAAQ,MAAM,KAAK,EAC9C,EAAM,kBAAkB,QAAQ,CAClC,EAEM,EAAS,GAAwB,CACrC,GAAO,iBAAiB,EACxB,EAAM,MAAQ,GACd,EAAQ,UAAU,GAAS,IAAI,WAAW,QAAS,CAAE,QAAS,EAAK,CAAC,EAAG,EAAE,EACzE,EAAQ,WAAW,GAAS,IAAI,MAAM,SAAU,CAAE,QAAS,EAAK,CAAC,EAAG,EAAE,EACtE,EAAM,kBAAkB,QAAQ,CAClC,EAEM,GAAQ,EAAiD,IAA0C,CACvG,IAAM,EAA8B,CAAC,EAC/B,GAAM,EAAc,IAAiC,CACzD,EAAQ,iBAAiB,EAAM,CAAO,EACtC,EAAS,SAAW,EAAQ,oBAAoB,EAAM,CAAO,CAAC,CAChE,EAEI,EAAQ,SACV,EAAG,QAAU,GAAM,EAAQ,QAAS,CAAe,CAAC,EAGtD,EAAG,QAAU,GAAM,CACjB,EAAE,gBAAgB,EAClB,EAAQ,gBAAgB,CAAC,EACzB,EAAM,MAAQ,EAAQ,MACtB,EAAQ,UAAU,EAAG,EAAQ,KAAK,CACpC,CAAC,EAED,EAAG,SAAW,GAAM,CAClB,EAAE,gBAAgB,EAClB,EAAM,MAAQ,EAAQ,MACtB,EAAQ,WAAW,EAAG,EAAQ,KAAK,EACnC,EAAM,kBAAkB,QAAQ,CAClC,CAAC,EAED,EAAG,OAAS,GAAM,CAChB,EAAQ,SAAS,CAAe,EAChC,EAAM,kBAAkB,MAAM,CAChC,CAAC,EAED,IAAI,EAAW,GACT,MAAqB,CACrB,MAEJ,GAAW,GACX,IAAK,IAAM,KAAW,EAAU,EAAQ,CAD7B,CAEb,EAIA,OAFA,GAAQ,iBAAiB,QAAS,EAAQ,CAAE,KAAM,EAAK,CAAC,EAEjD,CACT,EAEA,MAAO,CAAE,GAAG,EAAO,QAAO,UAAS,QAAO,oBAAmB,WAAU,QAAO,MAAK,CACrF"}
@@ -14,11 +14,37 @@ export type TextFieldOptions = FieldOptions & {
14
14
  onChange?: (event: Event, value: string) => void;
15
15
  onFocus?: (event: FocusEvent) => void;
16
16
  onInput?: (event: Event, value: string) => void;
17
+ /**
18
+ * Bars the field from constraint validation while true — matches the native HTML rule that a
19
+ * `readonly` field is never a candidate for constraint validation (a `readonly required`
20
+ * field is always valid, per spec), so `validity`/`validationMessage` don't feed a false
21
+ * `valueMissing` into `useField()` just because a read-only field happens to be blank.
22
+ */
23
+ readonly?: Readable<boolean | undefined>;
24
+ /** Marks a blank value invalid — feeds `validity`/`validationMessage` (see below). */
25
+ required?: Readable<boolean | undefined>;
26
+ /** Message for the blank+required case. Defaults to `'This field is required.'`. */
27
+ requiredMessage?: Readable<string | undefined>;
17
28
  /** `AbortSignal` from the component lifecycle. The internal value-sync watcher is disposed on abort. */
18
29
  signal: AbortSignal;
19
30
  value: Readable<string | undefined>;
20
31
  };
21
32
  export type TextFieldHandle = FieldHandle & {
33
+ /**
34
+ * Registers the real form field handle (the return value of `useField()`) so that
35
+ * `triggerValidation()` — called internally on blur/change — can call its
36
+ * `reportValidity()`. `useField()` itself needs `value` (below) to exist first, so this
37
+ * is a two-step wiring rather than a constructor option:
38
+ *
39
+ * ```ts
40
+ * const tf = createTextField({ value: props.value, ... });
41
+ * const formField = useField({ value: tf.value, ... });
42
+ * tf.attachFormField(formField);
43
+ * ```
44
+ */
45
+ attachFormField: (formField: {
46
+ reportValidity(): void;
47
+ }) => void;
22
48
  /** Clears the field value and fires synthetic input/change events. */
23
49
  clear: (event?: Event) => void;
24
50
  /**
@@ -26,6 +52,20 @@ export type TextFieldHandle = FieldHandle & {
26
52
  * Components should only render a counter element when this is non-null.
27
53
  */
28
54
  counter: Readable<CounterState> | null;
55
+ /**
56
+ * Restores the value to whatever the `value` option currently holds (native form
57
+ * `reset()` semantics: a `<input>` reverts to its *current* `value` content attribute,
58
+ * not a frozen snapshot from element creation — so setting the attribute programmatically
59
+ * after mount changes what a later reset reverts to). Wire into `useField({ onReset: tf.reset })`.
60
+ */
61
+ reset: () => void;
62
+ /** Reactive validation message paired with `validity`. Empty string when valid. */
63
+ validationMessage: Readable<string>;
64
+ /**
65
+ * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and blank,
66
+ * `null` (valid) otherwise. Pass straight to `useField({ validity: tf.validity })`.
67
+ */
68
+ validity: Readable<ValidityStateFlags | null>;
29
69
  /** The local mutable field value (two-way bound to the input element via `wire()`). */
30
70
  value: Signal<string>;
31
71
  /**