@skdx/docs-kit 0.39.0 → 0.40.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 (142) hide show
  1. package/dist/data/angular-inputs.json +1 -1
  2. package/dist/data/api-manifest.json +1 -1
  3. package/dist/shared/contracts/accordion.js +1 -1
  4. package/dist/shared/contracts/alert.js +1 -1
  5. package/dist/shared/contracts/autocomplete.d.ts +1 -1
  6. package/dist/shared/contracts/autocomplete.js +1 -1
  7. package/dist/shared/contracts/avatar.js +1 -1
  8. package/dist/shared/contracts/breadcrumb.js +1 -1
  9. package/dist/shared/contracts/button.js +1 -1
  10. package/dist/shared/contracts/calendar.js +1 -1
  11. package/dist/shared/contracts/card.js +1 -1
  12. package/dist/shared/contracts/carousel.js +1 -1
  13. package/dist/shared/contracts/checkbox-card.js +1 -1
  14. package/dist/shared/contracts/checkbox-group.js +1 -1
  15. package/dist/shared/contracts/collapsible.js +1 -1
  16. package/dist/shared/contracts/combobox.js +1 -1
  17. package/dist/shared/contracts/command.js +1 -1
  18. package/dist/shared/contracts/dashboard-layout.js +1 -1
  19. package/dist/shared/contracts/date-picker.js +1 -1
  20. package/dist/shared/contracts/dialog.d.ts +1 -1
  21. package/dist/shared/contracts/dialog.js +1 -1
  22. package/dist/shared/contracts/drawer.d.ts +1 -1
  23. package/dist/shared/contracts/drawer.js +1 -1
  24. package/dist/shared/contracts/field.js +1 -1
  25. package/dist/shared/contracts/file-upload.js +1 -1
  26. package/dist/shared/contracts/floating-panel.js +1 -1
  27. package/dist/shared/contracts/form.js +1 -1
  28. package/dist/shared/contracts/image.js +1 -1
  29. package/dist/shared/contracts/listbox.js +1 -1
  30. package/dist/shared/contracts/loading-overlay.js +1 -1
  31. package/dist/shared/contracts/mentions.js +1 -1
  32. package/dist/shared/contracts/menu.js +1 -1
  33. package/dist/shared/contracts/menubar.js +1 -1
  34. package/dist/shared/contracts/navigation-menu.js +1 -1
  35. package/dist/shared/contracts/pagination.js +1 -1
  36. package/dist/shared/contracts/password-input.js +1 -1
  37. package/dist/shared/contracts/popover.js +1 -1
  38. package/dist/shared/contracts/progress.js +1 -1
  39. package/dist/shared/contracts/qr-code.js +1 -1
  40. package/dist/shared/contracts/radio-card.js +1 -1
  41. package/dist/shared/contracts/radio-group.js +1 -1
  42. package/dist/shared/contracts/rating.js +1 -1
  43. package/dist/shared/contracts/resizable.js +1 -1
  44. package/dist/shared/contracts/scroll-area.js +1 -1
  45. package/dist/shared/contracts/select.js +1 -1
  46. package/dist/shared/contracts/slider.js +1 -1
  47. package/dist/shared/contracts/speed-dial.js +1 -1
  48. package/dist/shared/contracts/split-button.js +1 -1
  49. package/dist/shared/contracts/stepper.js +1 -1
  50. package/dist/shared/contracts/switch.js +1 -1
  51. package/dist/shared/contracts/table.js +3 -1
  52. package/dist/shared/contracts/tabs.js +1 -1
  53. package/dist/shared/contracts/tag-group.js +1 -1
  54. package/dist/shared/contracts/tags-input.js +1 -1
  55. package/dist/shared/contracts/text-field.js +1 -1
  56. package/dist/shared/contracts/textarea.js +1 -1
  57. package/dist/shared/contracts/timeline.js +1 -1
  58. package/dist/shared/contracts/toast.js +9 -1
  59. package/dist/shared/contracts/toolbar.js +1 -1
  60. package/dist/shared/contracts/tour.js +1 -1
  61. package/dist/shared/contracts/transfer.js +1 -1
  62. package/dist/shared/contracts/tree-select.js +1 -1
  63. package/dist/shared/contracts/tree.js +1 -1
  64. package/dist/shared/contracts/types.d.ts +2 -0
  65. package/dist/shared/contracts/workspace-switcher.js +1 -1
  66. package/dist/shared/core/field-core.d.ts +2 -0
  67. package/dist/shared/core/field-core.js +1 -1
  68. package/dist/skins-src/_foundation-fallback.css +2 -1
  69. package/dist/skins-src/components/_actuator.css +1 -1
  70. package/dist/skins-src/components/_card-control.css +1 -1
  71. package/dist/skins-src/components/_collection.css +2 -2
  72. package/dist/skins-src/components/_overlay.css +1 -1
  73. package/dist/skins-src/components/_trigger.css +1 -1
  74. package/dist/skins-src/components/accordion.css +1 -1
  75. package/dist/skins-src/components/alert.css +2 -2
  76. package/dist/skins-src/components/autocomplete.css +1 -1
  77. package/dist/skins-src/components/avatar.css +1 -1
  78. package/dist/skins-src/components/badge.css +1 -1
  79. package/dist/skins-src/components/button.css +1 -1
  80. package/dist/skins-src/components/calendar.css +8 -6
  81. package/dist/skins-src/components/card.css +1 -1
  82. package/dist/skins-src/components/carousel.css +1 -1
  83. package/dist/skins-src/components/chat-composer.css +1 -1
  84. package/dist/skins-src/components/chat-message.css +1 -1
  85. package/dist/skins-src/components/checkbox-card.css +2 -2
  86. package/dist/skins-src/components/checkbox.css +1 -1
  87. package/dist/skins-src/components/clipboard.css +2 -2
  88. package/dist/skins-src/components/color-picker.css +4 -4
  89. package/dist/skins-src/components/combobox.css +2 -2
  90. package/dist/skins-src/components/command.css +1 -1
  91. package/dist/skins-src/components/dashboard-layout.css +1 -1
  92. package/dist/skins-src/components/date-field.css +2 -2
  93. package/dist/skins-src/components/dialog.css +4 -4
  94. package/dist/skins-src/components/drawer.css +9 -9
  95. package/dist/skins-src/components/editable.css +3 -3
  96. package/dist/skins-src/components/file-upload.css +4 -4
  97. package/dist/skins-src/components/floating-panel.css +1 -1
  98. package/dist/skins-src/components/forgot-password-page.css +1 -1
  99. package/dist/skins-src/components/form-section.css +1 -1
  100. package/dist/skins-src/components/form.css +12 -2
  101. package/dist/skins-src/components/image.css +1 -1
  102. package/dist/skins-src/components/kbd.css +1 -1
  103. package/dist/skins-src/components/link.css +1 -1
  104. package/dist/skins-src/components/listbox.css +1 -1
  105. package/dist/skins-src/components/mentions.css +1 -1
  106. package/dist/skins-src/components/menubar.css +2 -2
  107. package/dist/skins-src/components/navigation-menu.css +1 -1
  108. package/dist/skins-src/components/number-field.css +1 -1
  109. package/dist/skins-src/components/onboarding-checklist.css +1 -1
  110. package/dist/skins-src/components/pagination.css +2 -2
  111. package/dist/skins-src/components/password-input.css +80 -16
  112. package/dist/skins-src/components/pin-input.css +2 -2
  113. package/dist/skins-src/components/popover.css +1 -1
  114. package/dist/skins-src/components/qr-code.css +2 -2
  115. package/dist/skins-src/components/radio-card.css +1 -1
  116. package/dist/skins-src/components/reset-password-page.css +1 -1
  117. package/dist/skins-src/components/select.css +3 -3
  118. package/dist/skins-src/components/sign-in-page.css +1 -1
  119. package/dist/skins-src/components/sign-up-page.css +1 -1
  120. package/dist/skins-src/components/skeleton.css +1 -1
  121. package/dist/skins-src/components/skip-link.css +1 -1
  122. package/dist/skins-src/components/speed-dial.css +3 -3
  123. package/dist/skins-src/components/stat-card.css +6 -1
  124. package/dist/skins-src/components/stepper.css +3 -1
  125. package/dist/skins-src/components/switch.css +21 -1
  126. package/dist/skins-src/components/table.css +4 -4
  127. package/dist/skins-src/components/tags-input.css +2 -2
  128. package/dist/skins-src/components/text-field.css +26 -21
  129. package/dist/skins-src/components/text.css +1 -1
  130. package/dist/skins-src/components/textarea.css +1 -1
  131. package/dist/skins-src/components/time-field.css +2 -2
  132. package/dist/skins-src/components/toast.css +2 -2
  133. package/dist/skins-src/components/toggle-group.css +3 -3
  134. package/dist/skins-src/components/toggle.css +1 -1
  135. package/dist/skins-src/components/toolbar.css +2 -2
  136. package/dist/skins-src/components/tree-select.css +2 -2
  137. package/dist/skins-src/components/tree.css +1 -1
  138. package/dist/skins-src/components/usage-card.css +1 -1
  139. package/dist/skins-src/components/user-menu.css +1 -1
  140. package/dist/skins-src/components/verification-page.css +1 -1
  141. package/dist/skins-src/components/workspace-switcher.css +2 -2
  142. package/package.json +14 -14
@@ -1 +1 @@
1
- import{calendarContract as o}from"./calendar.js";import{changeDetail as d}from"./change-reason.js";import{dateFieldContract as p}from"./date-field.js";import{DATE_CHANGE_VALUE_TYPE as c,DATE_INPUT_RULE as r,DATE_MALFORMED_RULE as u,DATE_OUTPUT_RULE as m,DATE_PARSE_RULE as h}from"./date-value.js";import{constraintValidationMethods as g,setCustomValidityMethod as f}from"./form-validity.js";import{dismissalEvents as y,forceMountProp as v,openChangeCompleteEvent as b,popperContentProps as w,portalPart as i,portalPropsSansForceMount as C}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as k}from"./styling-hooks.js";import{timeFieldContract as E}from"./time-field.js";function n(a,e){return a.filter(t=>!e.includes(t.name)).map(t=>t.name==="readOnly"?{...t,description:`${t.description} OR'd with the picker root's readOnly rather than overriding it, so a read-only picker never leaves this part editable.`}:t)}var s=["value","defaultValue","granularity"],D={frameworks:["react","vue","svelte","angular"],exception:"attribute-passthrough",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the picker part exposes no dir, because the nested <skdx-date-field>/<skdx-time-field> exposes none either \u2014 direction is read off a DOM dir ancestor there."},T={frameworks:["react","vue","svelte","angular"],exception:"attribute-passthrough",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the picker part forwards no form id; the nested field is form-associated natively and the picker root already carries the form the selection submits into."};function l(a){return a.map(e=>e.name==="dir"?{...e,...D}:e.name==="form"?{...e,...T}:e)}var F={name:"DatePicker",purpose:"A button-triggered popup with composed Calendar, positioned via floating-ui. Derives anatomy: labeled trigger, portalled calendar, optional typed date and time fields. Children replace anatomy entirely.",props:[{name:"selectionMode",type:"'single' | 'multiple' | 'range'",defaultValue:"'single'",required:!1,description:"Forwarded to Calendar, shaping value accordingly. Controls popup auto-close: immediately in single, once range is closed in range, never in multiple."},{name:"value",type:"Date | string | Array<Date | string> | { start: Date | string; end: Date | string | null } | null",required:!1,description:`Controlled selection, paired with value-change. Shape follows selectionMode. ${r} Wire string mirrors Calendar format: single "2024-01-05", comma-separated multiple, start/end range. ${h} ${u}`},{name:"defaultValue",type:"Date | string | Array<Date | string> | { start: Date | string; end: Date | string | null } | null",required:!1,description:`Initial selection for uncontrolled usage. Shape follows selectionMode, same Date-or-ISO-string forms as value. ${r}`,frameworks:["react","vue","svelte","angular"],exception:"wc-default",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): property is the single source of truth, like a native input. Form reset restores the value attribute."},{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage.",frameworks:["react","vue","svelte","angular"],exception:"wc-default",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the element has no defaultOpen \u2014 the property is the single source of truth, like a native input, and a form reset restores the selection the value attribute declared."},{name:"granularity",type:"'day' | 'minute' | 'second'",defaultValue:"'day'",required:!1,description:"Selection precision, forwarded to Input and TimeField. Past 'day' = date-time mode: no auto-close on day (incomplete). Hour/minute render together, so no 'hour' option. Only single mode edits time: range and multiple keep the date-only calendar."},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether completed selection dismisses popup. Single: immediately. Range: after end date. Multiple: ignored. Set false to keep open. Ignored past 'day' granularity."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables Trigger and prevents opening the popup."},{name:"readOnly",type:"boolean",defaultValue:"false",required:!1,description:"Non-editable across all parts, forwarded to Calendar and segmented fields. Trigger opens, all parts stay focusable/readable. Declared at root for UI consistency."},{name:"invalid",type:"boolean",defaultValue:"false",required:!1,description:"Marks invalid, reflected as data-invalid (Trigger/Content) and aria-invalid (Trigger). Purely presentational, like segmented fields. Root property for UI painting."},{name:"name",type:"string",required:!1,description:"Form field name. The selected date is submitted under it as YYYY-MM-DD; without it the picker submits nothing."},{name:"form",type:"string | undefined",required:!1,defaultValue:"undefined",description:"Form id for submission, forwarded to all hidden controls. Allows picker outside form. Needs name to associate. In multiple/range, reaches all inputs. Web Components: named formId."},{name:"required",type:"boolean",defaultValue:"false",required:!1,description:"Marks the picker required. valueMissing while no date is selected."},{name:"placeholder",type:"string",defaultValue:"'Select date'",required:!1,description:"Derived trigger text while nothing is selected. Only used in derived anatomy; ignored with custom children."},{name:"locale",type:"string | undefined",defaultValue:"undefined",required:!1,description:"BCP 47 tag for derived trigger formatting via Intl. Separate from Calendar's locale. Ignored with custom children."},{name:"showInput",type:"boolean",defaultValue:"false",required:!1,description:"Whether derived popup includes typed-entry field above calendar for typing dates. Only in single mode. Ignored with custom children."}],events:[{name:"value-change",detail:d(c),description:`Fires with new selection on every change: Date (single), Date[] (multiple), range object (range). ${m}`},{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},b("popup"),...y("popup"),{name:"open-auto-focus",detail:"void",description:"Fired before focus moves into Calendar on open (selected date or today). Prevent default to leave focus as-is."},{name:"close-auto-focus",detail:"void",description:"Fired before focus returns to Trigger on popup close from inside (selection/Close/Escape). Prevent default to suppress return. Never fired for outside dismissals."}],slots:[{name:"trigger",description:"Trigger's content \u2014 button that opens and anchors popup."},{name:"default",description:"Replaces full derived anatomy. Left empty (recommended): picker draws itself. Filled: takes Trigger, Portal, and Content with Calendar and optional Input, TimeField, Close."}],parts:[{name:"Trigger",description:"Opens popup and anchors position. Carries aria-haspopup, aria-expanded, aria-controls, data-state, data-disabled/data-invalid."},{...i,description:`${i.description} React, Vue, Svelte, Web Components ship it. Angular: Content is fixed-positioned, nothing to escape. Web Components: hidden attribute vs unmount.`,frameworks:["react","vue","svelte","webComponents"],exception:"portal",frameworksReason:"Angular uses fixed-positioned Content, nothing to escape. Portal component omitted.",props:[...C,{...v,frameworks:["react","vue","svelte"],exception:"force-mount",frameworksReason:"Web Components portal hides via `hidden`, not unmount, so no force-mount needed."}]},{name:"Content",description:"The dialog popup panel, positioned against Trigger by floating-ui. Named via aria-labelledby pointing at Trigger unless overridden.",props:w({sideOffset:"4"})},{name:"Calendar",description:"Pre-wired Calendar component: value, selectionMode, readOnly forwarded. Accepts all other Calendar props. Hand-composed Calendar works the same.",props:n(o.props,["value","defaultValue","selectionMode"]),events:o.events.filter(a=>a.name!=="value-change")},{name:"Input",lightDom:!0,description:"DateField for typed entry, pre-wired to value, granularity, readOnly. Single mode only (ranges/multi unsupported by segmented fields).",props:l(n(p.props,s))},{name:"TimeField",lightDom:!0,description:"Time-of-day editor for date-time mode. Nothing at 'day' granularity, multiple modes, or before date selected.",props:l(n(E.props,s))},{name:"Close",description:"Dismisses popup without changing selection."}],states:["closed","open","disabled","readonly","invalid"],controlledUncontrolled:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the element has no defaultValue and no defaultOpen, and a form reset there is silent where the other four fire value-change. Recorded here rather than only in prose because a divergence that appears nowhere in the generated API tables reads as a missing prop. value and open each independently support controlled or uncontrolled usage. A prop is controlled only while it is not undefined: value={null} is a controlled empty selection, not a request to fall back to internal state \u2014 otherwise a consumer could never clear a controlled picker. Web Components have no defaultValue/defaultOpen: the property is the single source of truth, like a native input, and a form reset restores the selection the value attribute declared \u2014 falling back to the one the element first rendered with when no attribute was authored.",formBehavior:'Form-associated: with a name, the selection is submitted as local-time YYYY-MM-DD strings, exactly like <input type="date"> \u2014 one entry in single mode, one entry per date in multiple mode, and start then end (two entries under the same name) in range mode. With nothing selected it submits an empty entry and is valueMissing when required. A form reset restores the initial selection \u2014 silently in Web Components, and by firing value-change in React/Vue/Svelte/Angular, where a controlled value belongs to the parent and would otherwise be restored straight back.',validationBehavior:"Range/availability validation is delegated entirely to the composed Calendar (minDate/maxDate/isDateDisabled/isDateUnavailable); required is the only constraint DatePicker itself enforces, reported through checkValidity()/reportValidity(). In range mode an anchored-but-unclosed range (end still null) counts as no selection for required.",accessibility:{role:"dialog on Content, emitted explicitly rather than left implied by Trigger's aria-haspopup \u2014 an implied role is not a role, and the panel announced as a generic container until it was set; the composed Calendar inside carries its own grid semantics",keyboard:[{key:"Enter / Space (on Trigger)",behavior:"Opens or closes the popup. Opening moves focus into the composed Calendar \u2014 onto the selected date, or onto today when nothing is selected."},{key:"Escape",behavior:"Closes the popup and returns focus to Trigger."},{key:"Tab",behavior:`Moves focus out of the popup and dismisses it, rather than cycling inside. This is the coherent pairing with the panel's role="dialog" *without* aria-modal: a non-modal dialog does not trap focus, so Tab reaching the next thing on the page is what a user expects, and leaving the popup open behind them would leave a panel nobody is in. A picker that wanted containment would have to declare aria-modal="true" and trap focus; it deliberately declares neither. No package binds Tab as a key of its own: the dismissal comes from focus landing outside the panel, which is the same route a click-away or a programmatic focus move takes. One consequence follows from that mechanism \u2014 focus arriving on document.body does not count as outside, so tabbing past the last focusable element on the page leaves the popup open.`},{key:"(within the composed Calendar)",behavior:"See the Calendar contract for grid navigation."}],ariaAttributes:['aria-haspopup="dialog", aria-expanded, and aria-controls on Trigger',"Content's id matches Trigger's aria-controls",'role="dialog" on Content, and deliberately no aria-modal: the popup does not trap focus and does not make the rest of the page inert, so claiming modality would be a promise the focus management does not keep','data-invalid on Trigger and Content, and aria-invalid="true" on Trigger, while the picker is invalid',"an accessible name on Content: aria-labelledby pointing at Trigger by default, so the panel announces whatever the button that opened it says, and suppressed the moment the consumer supplies an aria-label or aria-labelledby of their own \u2014 aria-labelledby outranks aria-label, so emitting it unconditionally would shadow the consumer's name"],focusManagement:"Opening moves focus into the composed Calendar (the selected date, else today, else the grid's roving tab stop), announced first as a cancelable open-auto-focus. Every close that started inside the popup \u2014 a completed selection, Close, or Escape \u2014 returns focus to Trigger through one shared close-auto-focus path, so the three cannot drift apart. A dismissal that started outside the popup makes no focus move and fires no close-auto-focus: focus already belongs to whatever the user reached for."},styling:{cssParts:["trigger","content","close","calendar","input","time-field"],dataAttributes:[...k,"data-state","data-side","data-align","data-disabled","data-readonly","data-invalid","data-skdx-calendar-cell","data-skdx-field-control",{name:"data-skdx-date-picker-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[...g,f,{name:"setValue",signature:"(value: CalendarValue | undefined) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new value prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Replaces the selected date and emits value-change, exactly as picking a cell would."},{name:"setOpen",signature:"(open: boolean) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new open prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Opens or closes the picker popup and emits open-change, exactly as the trigger would."}]};export{F as datePickerContract};
1
+ import{calendarContract as o}from"./calendar.js";import{changeDetail as d}from"./change-reason.js";import{dateFieldContract as p}from"./date-field.js";import{DATE_CHANGE_VALUE_TYPE as c,DATE_INPUT_RULE as r,DATE_MALFORMED_RULE as u,DATE_OUTPUT_RULE as m,DATE_PARSE_RULE as h}from"./date-value.js";import{constraintValidationMethods as g,setCustomValidityMethod as f}from"./form-validity.js";import{dismissalEvents as y,forceMountProp as v,openChangeCompleteEvent as b,popperContentProps as w,portalPart as i,portalPropsSansForceMount as C}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as k}from"./styling-hooks.js";import{timeFieldContract as E}from"./time-field.js";function n(a,e){return a.filter(t=>!e.includes(t.name)).map(t=>t.name==="readOnly"?{...t,description:`${t.description} OR'd with the picker root's readOnly rather than overriding it, so a read-only picker never leaves this part editable.`}:t)}var s=["value","defaultValue","granularity"],T={frameworks:["react","vue","svelte","angular"],exception:"attribute-passthrough",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the picker part exposes no dir, because the nested <skdx-date-field>/<skdx-time-field> exposes none either \u2014 direction is read off a DOM dir ancestor there."},D={frameworks:["react","vue","svelte","angular"],exception:"attribute-passthrough",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the picker part forwards no form id; the nested field is form-associated natively and the picker root already carries the form the selection submits into."};function l(a){return a.map(e=>e.name==="dir"?{...e,...T}:e.name==="form"?{...e,...D}:e)}var q={name:"DatePicker",purpose:"A button-triggered popup with composed Calendar, positioned via floating-ui. Derives anatomy: labeled trigger, portalled calendar, optional typed date and time fields. Children replace anatomy entirely.",props:[{name:"selectionMode",type:"'single' | 'multiple' | 'range'",defaultValue:"'single'",required:!1,description:"Forwarded to Calendar, shaping value accordingly. Controls popup auto-close: immediately in single, once range is closed in range, never in multiple."},{name:"value",type:"Date | string | Array<Date | string> | { start: Date | string; end: Date | string | null } | null",required:!1,description:`Controlled selection, paired with value-change. Shape follows selectionMode. ${r} Wire string mirrors Calendar format: single "2024-01-05", comma-separated multiple, start/end range. ${h} ${u}`},{name:"defaultValue",type:"Date | string | Array<Date | string> | { start: Date | string; end: Date | string | null } | null",required:!1,description:`Initial selection for uncontrolled usage. Shape follows selectionMode, same Date-or-ISO-string forms as value. ${r}`,frameworks:["react","vue","svelte","angular"],exception:"wc-default",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): property is the single source of truth, like a native input. Form reset restores the value attribute."},{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage.",frameworks:["react","vue","svelte","angular"],exception:"wc-default",frameworksReason:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the element has no defaultOpen \u2014 the property is the single source of truth, like a native input, and a form reset restores the selection the value attribute declared."},{name:"granularity",type:"'day' | 'minute' | 'second'",defaultValue:"'day'",required:!1,description:"Selection precision, forwarded to Input and TimeField. Past 'day' = date-time mode: no auto-close on day (incomplete). Hour/minute render together, so no 'hour' option. Only single mode edits time: range and multiple keep the date-only calendar."},{name:"closeOnSelect",type:"boolean",defaultValue:"true",required:!1,description:"Whether completed selection dismisses popup. Single: immediately. Range: after end date. Multiple: ignored. Set false to keep open. Ignored past 'day' granularity."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables Trigger and prevents opening the popup."},{name:"readOnly",type:"boolean",defaultValue:"false",required:!1,description:"Non-editable across all parts, forwarded to Calendar and segmented fields. Trigger opens, all parts stay focusable/readable. Declared at root for UI consistency."},{name:"invalid",type:"boolean",defaultValue:"false",required:!1,description:"Marks invalid, reflected as data-invalid (Trigger/Content) and aria-invalid (Trigger). Purely presentational, like segmented fields. Root property for UI painting."},{name:"name",type:"string",required:!1,description:"Form field name. The selected date is submitted under it as YYYY-MM-DD; without it the picker submits nothing."},{name:"form",type:"string | undefined",required:!1,defaultValue:"undefined",description:"Form id for submission, forwarded to all hidden controls. Allows picker outside form. Needs name to associate. In multiple/range, reaches all inputs. Web Components: named formId."},{name:"required",type:"boolean",defaultValue:"false",required:!1,description:"Marks the picker required. valueMissing while no date is selected."},{name:"placeholder",type:"string",defaultValue:"'Select date'",required:!1,description:"Derived trigger text while nothing is selected. Only used in derived anatomy; ignored with custom children."},{name:"locale",type:"string | undefined",defaultValue:"undefined",required:!1,description:"BCP 47 tag for derived trigger formatting via Intl. Separate from Calendar's locale. Ignored with custom children."},{name:"showInput",type:"boolean",defaultValue:"false",required:!1,description:"Whether derived popup includes typed-entry field above calendar for typing dates. Only in single mode. Ignored with custom children."}],events:[{name:"value-change",detail:d(c),description:`Fires with new selection on every change: Date (single), Date[] (multiple), range object (range). ${m}`},{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},b("popup"),...y("popup"),{name:"open-auto-focus",detail:"void",description:"Fired before focus moves into Calendar on open (selected date or today). Prevent default to leave focus as-is."},{name:"close-auto-focus",detail:"void",description:"Fired before focus returns to Trigger on popup close from inside (selection/Close/Escape). Prevent default to suppress return. Never fired for outside dismissals."}],slots:[{name:"default",description:"Replaces full derived anatomy. Left empty (recommended): picker draws itself. Filled: takes Trigger, Portal, and Content with Calendar and optional Input, TimeField, Close."}],parts:[{name:"Trigger",description:"Opens popup and anchors position. Carries aria-haspopup, aria-expanded, aria-controls, data-state, data-disabled/data-invalid.",slots:[{name:"default",description:"Trigger's content \u2014 button that opens and anchors popup."}]},{...i,description:`${i.description} React, Vue, Svelte, Web Components ship it. Angular: Content is fixed-positioned, nothing to escape. Web Components: hidden attribute vs unmount.`,frameworks:["react","vue","svelte","webComponents"],exception:"portal",frameworksReason:"Angular uses fixed-positioned Content, nothing to escape. Portal component omitted.",props:[...C,{...v,frameworks:["react","vue","svelte"],exception:"force-mount",frameworksReason:"Web Components portal hides via `hidden`, not unmount, so no force-mount needed."}]},{name:"Content",description:"The dialog popup panel, positioned against Trigger by floating-ui. Named via aria-labelledby pointing at Trigger unless overridden.",props:w({sideOffset:"4"})},{name:"Calendar",description:"Pre-wired Calendar component: value, selectionMode, readOnly forwarded. Accepts all other Calendar props. Hand-composed Calendar works the same.",props:n(o.props,["value","defaultValue","selectionMode"]),events:o.events.filter(a=>a.name!=="value-change")},{name:"Input",lightDom:!0,description:"DateField for typed entry, pre-wired to value, granularity, readOnly. Single mode only (ranges/multi unsupported by segmented fields).",props:l(n(p.props,s))},{name:"TimeField",lightDom:!0,description:"Time-of-day editor for date-time mode. Nothing at 'day' granularity, multiple modes, or before date selected.",props:l(n(E.props,s))},{name:"Close",description:"Dismisses popup without changing selection."}],states:["closed","open","disabled","readonly","invalid"],controlledUncontrolled:"DELIBERATE FRAMEWORK DIVERGENCE (Web Components): the element has no defaultValue and no defaultOpen, and a form reset there is silent where the other four fire value-change. Recorded here rather than only in prose because a divergence that appears nowhere in the generated API tables reads as a missing prop. value and open each independently support controlled or uncontrolled usage. A prop is controlled only while it is not undefined: value={null} is a controlled empty selection, not a request to fall back to internal state \u2014 otherwise a consumer could never clear a controlled picker. Web Components have no defaultValue/defaultOpen: the property is the single source of truth, like a native input, and a form reset restores the selection the value attribute declared \u2014 falling back to the one the element first rendered with when no attribute was authored.",formBehavior:'Form-associated: with a name, the selection is submitted as local-time YYYY-MM-DD strings, exactly like <input type="date"> \u2014 one entry in single mode, one entry per date in multiple mode, and start then end (two entries under the same name) in range mode. With nothing selected it submits an empty entry and is valueMissing when required. A form reset restores the initial selection \u2014 silently in Web Components, and by firing value-change in React/Vue/Svelte/Angular, where a controlled value belongs to the parent and would otherwise be restored straight back.',validationBehavior:"Range/availability validation is delegated entirely to the composed Calendar (minDate/maxDate/isDateDisabled/isDateUnavailable); required is the only constraint DatePicker itself enforces, reported through checkValidity()/reportValidity(). In range mode an anchored-but-unclosed range (end still null) counts as no selection for required.",accessibility:{role:"dialog on Content, emitted explicitly rather than left implied by Trigger's aria-haspopup \u2014 an implied role is not a role, and the panel announced as a generic container until it was set; the composed Calendar inside carries its own grid semantics",keyboard:[{key:"Enter / Space (on Trigger)",behavior:"Opens or closes the popup. Opening moves focus into the composed Calendar \u2014 onto the selected date, or onto today when nothing is selected."},{key:"Escape",behavior:"Closes the popup and returns focus to Trigger."},{key:"Tab",behavior:`Moves focus out of the popup and dismisses it, rather than cycling inside. This is the coherent pairing with the panel's role="dialog" *without* aria-modal: a non-modal dialog does not trap focus, so Tab reaching the next thing on the page is what a user expects, and leaving the popup open behind them would leave a panel nobody is in. A picker that wanted containment would have to declare aria-modal="true" and trap focus; it deliberately declares neither. No package binds Tab as a key of its own: the dismissal comes from focus landing outside the panel, which is the same route a click-away or a programmatic focus move takes. One consequence follows from that mechanism \u2014 focus arriving on document.body does not count as outside, so tabbing past the last focusable element on the page leaves the popup open.`},{key:"(within the composed Calendar)",behavior:"See the Calendar contract for grid navigation."}],ariaAttributes:['aria-haspopup="dialog", aria-expanded, and aria-controls on Trigger',"Content's id matches Trigger's aria-controls",'role="dialog" on Content, and deliberately no aria-modal: the popup does not trap focus and does not make the rest of the page inert, so claiming modality would be a promise the focus management does not keep','data-invalid on Trigger and Content, and aria-invalid="true" on Trigger, while the picker is invalid',"an accessible name on Content: aria-labelledby pointing at Trigger by default, so the panel announces whatever the button that opened it says, and suppressed the moment the consumer supplies an aria-label or aria-labelledby of their own \u2014 aria-labelledby outranks aria-label, so emitting it unconditionally would shadow the consumer's name"],focusManagement:"Opening moves focus into the composed Calendar (the selected date, else today, else the grid's roving tab stop), announced first as a cancelable open-auto-focus. Every close that started inside the popup \u2014 a completed selection, Close, or Escape \u2014 returns focus to Trigger through one shared close-auto-focus path, so the three cannot drift apart. A dismissal that started outside the popup makes no focus move and fires no close-auto-focus: focus already belongs to whatever the user reached for."},styling:{cssParts:["trigger","content","close","calendar","input","time-field"],dataAttributes:[...k,"data-state","data-side","data-align","data-disabled","data-readonly","data-invalid","data-skdx-calendar-cell","data-skdx-field-control",{name:"data-skdx-date-picker-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[...g,f,{name:"setValue",signature:"(value: CalendarValue | undefined) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new value prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Replaces the selected date and emits value-change, exactly as picking a cell would."},{name:"setOpen",signature:"(open: boolean) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new open prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Opens or closes the picker popup and emits open-change, exactly as the trigger would."}],frameworkExtensions:[{framework:"webComponents",name:"valueAsString",type:"string | null",description:"The current selection as the same wire string the `value` attribute and form submission use, or `null`. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings.",kind:"prop"},{framework:"webComponents",name:"valueAsString",type:"string | null",description:"The current selection as the same wire string the `value` attribute and form submission use, or `null`. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings."}]};export{q as datePickerContract};
@@ -1,3 +1,3 @@
1
- import type { ComponentContract } from './types.js';
1
+ import { type ComponentContract } from './types.js';
2
2
  /** The Dialog contract: a modal-by-default panel with focus trap, scroll lock, and top-layer rendering. */
3
3
  export declare const dialogContract: ComponentContract;
@@ -1 +1 @@
1
- import{autoFocusEvents as t,dismissalEvents as o,forceMountProp as a,openChangeCompleteEvent as n,portalPart as e,portalPropsSansForceMount as s}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as i}from"./styling-hooks.js";var d={name:"Dialog",purpose:`An accessible modal (or non-modal) dialog with focus trapping, scroll locking, and top-layer rendering. Given plain body content as children, the root derives its whole anatomy \u2014 Portal, Overlay, a panel with a derived Title/Description, the body, an optional footer, and a close button. With role="alertdialog" it is the interruptive confirmation: always modal, never closed by an outside interaction, focused on Cancel first, with derived Action and Cancel buttons from confirmLabel/cancelLabel. Manual composition of Trigger/Portal/Overlay/Content/Title/Description/Close/Action/Cancel is reached only through each framework's primitives/dialog subpath, never through this recipe's children.`,props:[{name:"title",type:"string",required:!1,description:"The panel's accessible name, rendered as the Title part. In Web Components, `title` shadows `HTMLElement.title` (harmless since the host is `display: contents`); an empty string counts as unset."},{name:"description",type:"string",required:!1,description:"The derived panel's accessible description, rendered as the Description part."},{name:"dismissible",type:"boolean",defaultValue:"true",required:!1,description:"Whether the panel can be dismissed. `false` forces dismiss to `'none'` (overriding any other value) and withholds the close button. The close button gates on this prop alone, not on `dismiss`, because `dismiss` governs implicit channels (Escape, outside); once `dismiss='none'` the close button is the only exit, so withholding it would strand the panel."},{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage."},{name:"modal",type:"boolean",defaultValue:"true",required:!1,description:"Whether the page is inert and locked from scrolling. true puts up a barrier (native showModal()); false leaves the page interactive (native show()). Owns three things: focus trap, inertness of everything outside, and body-scroll lock. The scroll lock mutates document.body.style.overflow and paddingRight while modal overlays are open, reference-counted across stacked Dialog/Drawer/LoadingOverlay, and restored to their exact prior strings on close. Modal does NOT own whether a dismissal channel closes the dialog \u2014 that is the dismiss prop. Outside dismissal cannot happen while the page is live; a non-modal dialog only reports it."},{name:"dismiss",type:"'any' | 'escape' | 'outside' | 'none'",defaultValue:"'any'",required:!1,description:'Which channels can close the dialog: "any" both Escape and outside, "escape" Escape only, "outside" outside interaction only, "none" neither (closes only via open/defaultOpen, Close part, or Trigger toggle). A policy, not a mute: the three outside events (pointer-down-outside, focus-outside, interact-outside) fire in every mode, and escape-key-down fires wherever the platform delivers Escape to the dialog \u2014 through the cancel event a native <dialog> raises for itself, which it raises only while modal. This prop decides whether an un-prevented one closes the dialog. Dismissal events still fire when prevented, so a consumer can log or veto; dismiss="none" is declarative preventDefault(), not a way to stop hearing about them. Outside dismissal requires modal, so dismiss="outside" on a non-modal dialog still closes on nothing. Resolved value is data-dismiss on Content, not the root.'},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Size rung forwarded to the Content panel's data-size; the attribute may be written on Content directly instead."},{name:"role",type:"'dialog' | 'alertdialog'",defaultValue:"'dialog'",required:!1,description:`The panel's role. "alertdialog" is the interruptive confirmation: Content renders role="alertdialog", modal is forced on, the outside channels of dismiss never close it (any resolves to escape, outside to none, reflected in data-dismiss), initial focus lands on Cancel before the first tabbable, the derived close button is withheld because Cancel is the explicit exit, and confirmLabel/cancelLabel default to "Confirm"/"Cancel" so the recipe always derives both buttons.`},{name:"status",type:"'info' | 'success' | 'warning' | 'danger' | (string & {})",required:!1,description:`Severity rung, written to data-status on the Content panel and forwarded to the derived Action button's own status; the data-status attribute may be set directly instead. "danger" is the destructive confirmation.`},{name:"confirmLabel",type:"string",required:!1,description:'Label of the derived Action button, which renders only when this is given \u2014 or always under role="alertdialog", where it defaults to "Confirm". Ignored when children replace the anatomy under manual composition.'},{name:"cancelLabel",type:"string",required:!1,description:'Label of the derived Cancel button, which renders only when this is given \u2014 or always under role="alertdialog", where it defaults to "Cancel". Ignored when children replace the anatomy under manual composition.'}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},n("dialog"),...o("dialog",{outsideNote:'Only a modal dialog closes on an outside interaction. With modal={false} all three are notification-only \u2014 the payload is still a cancelable event, so the shared payload assertion holds, but there is no dismissal behind it and preventing default changes nothing. They keep firing because they are the only way a consumer learns a non-modal dialog was clicked past. The dismiss policy is the second gate on the same path and behaves the same way: with dismiss="escape" or dismiss="none" these three still fire on a modal dialog and still close nothing, and with dismiss="outside" escape-key-down still fires and still closes nothing.'}),...t("dialog"),{name:"confirm",detail:"Event",description:"Fires when the derived Action button is clicked, before the close. Cancelable: preventDefault() suppresses the close, the supported way to hold a confirmation open while its request is in flight."},{name:"cancel",detail:"Event",description:"Fires when the derived Cancel button is clicked, before the close. Cancelable: preventDefault() suppresses the close."}],parts:[{name:"Trigger",description:'The button that opens the dialog (aria-haspopup="dialog", aria-expanded, aria-controls, data-state).'},{...e,frameworks:["react","webComponents"],frameworksReason:"Vue, Svelte and Angular render no Portal: Content is written where it stands, and showModal()/show() already promote it to the top layer. React and Web Components keep the part so the Overlay can escape a clipping ancestor and land in a chosen container.",description:`${e.description} Shipped by React and Web Components only, and always mounted: Content is a native <dialog> that has to outlive its own close(). Vue, Svelte and Angular render no Portal at all.`,props:[...s]},{name:"Overlay",description:"The scrim behind Content. A real element in all five frameworks \u2014 the four native-<dialog> ports paint its ::backdrop transparent so this part is the single scrim. It carries data-state and animates independently.",props:[{...a,frameworks:["react","vue","svelte"],frameworksReason:"Web Components only hides closed overlays (no unmount for forceMount to prevent). Angular toggles with [hidden] instead of unmounting, so it ships no forceMount."}]},{name:"Content",description:'The panel with role="dialog", labelled/described by Title/Description, aria-modal, data-state and data-dismiss. Handles dismissal (Escape, outside), scroll lock, and focus return; the focus trap, top layer and page inertness come from the platform. A native <dialog> driven by showModal()/show()/close() in all five frameworks, always mounted so close() has an element to fire on \u2014 which is why no framework ships forceMount on this part. Fires all dismissal and auto-focus events (declared on root to avoid duplication across dismissalEvents in overlay-parts).',lightDom:!0,props:[{name:"initialFocus",type:"string",required:!1,description:"CSS selector for the element to focus on open, resolved with querySelector inside Content. A selector rather than a ref because Web Components attributes cannot hold element handles, and five ports take the same value. Applies only when open-auto-focus was not vetoed. Falls back to the first tabbable or Content itself when the selector matches nothing, to prevent focus stranding on <body> inside a modal dialog."},{name:"finalFocus",type:"string",required:!1,description:"CSS selector for the element to focus on close, resolved at document level because Content is gone by then. The element that opened the dialog may no longer exist (a deleted row, a re-rendered list). Applies only when close-auto-focus was not vetoed. Falls back to the element that had focus before opening."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the content panel; the data-size attribute may be set directly instead."}]},{name:"Title",description:"The dialog's accessible name, referenced by Content's aria-labelledby."},{name:"Description",description:"The dialog's accessible description, referenced by Content's aria-describedby."},{name:"Close",description:"A button that closes the dialog. Rendered anywhere inside Content."},{name:"Action",description:"The button that carries out a confirmed action. Closes after the handler runs, unless preventDefault() is called on the confirm event \u2014 the supported way to hold a confirmation open while the request is in flight. Carries data-status when the root has one.",props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Sets the native disabled attribute so the button neither confirms nor closes the dialog when clicked."}]},{name:"Cancel",description:"The button that abandons the action. Marked with data-skdx-dialog-cancel, so an alertdialog focuses it first (the least destructive option). Its close is vetoable through the cancel event.",props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Sets the native disabled attribute so the button neither cancels nor closes the dialog when clicked."}]},{name:"Dialogs",description:"The host that renders the imperative queue: whatever createDialogs is showing \u2014 an alert, a confirm or a prompt \u2014 as one Dialog at a time, keyed on the entry. React/Vue/Svelte/Web Components take the store as a `dialogs` prop; Angular injects SkdxDialogService instead. Takes no other props: title, description, labels and status arrive on each call. Emits data-kind on the panel.",props:[{name:"dialogs",type:"DialogsStore",required:!0,frameworks:["react","vue","svelte","webComponents"],exception:"imperative",frameworksReason:"Angular resolves the queue by injecting SkdxDialogService, so the host takes no store input.",description:"The queue to render, from createDialogs; Web Components default to a store of their own, read back off the element."}]}],contextApi:[{name:"createDialogs",signature:"() => DialogsStore",description:"Creates the alert / confirm / prompt queue once per app; alert resolves on dismissal, confirm with a boolean, prompt with the text or null. Web Components create one per <skdx-dialogs> by default.",accessors:{react:"createDialogs() from '@skdx/react'",vue:"createDialogs() from '@skdx/vue'",svelte:"createDialogs() from '@skdx/svelte'",angular:"inject(SkdxDialogService) \u2014 alert / confirm / prompt on the service",webComponents:"createDialogs() from '@skdx/web-components', or <skdx-dialogs>.dialogs"}}],slots:[{name:"trigger",description:"Trigger's content \u2014 the button that opens the dialog."},{name:"default",description:"Body content of the derived panel, rendered inside Content after Title/Description and before footer/Close. On the recipe root this is always body content; under full manual composition via primitives, it is Content's hand-composed body."},{name:"title",description:"Title content \u2014 the dialog's accessible name."},{name:"description",description:"Description content \u2014 the dialog's accessible description."},{name:"footer",description:"Content after the body, before the close button (typically action buttons)."}],states:["closed","open"],controlledUncontrolled:"Stateless by default (uncontrolled): starts at defaultOpen and manages its own state internally. Passing open switches to controlled \u2014 the consumer owns state and must update it from open-change, or the dialog will not open/close on its own.",formBehavior:'Not applicable \u2014 Dialog participates in no form; Close and Trigger are type="button".',validationBehavior:"Not applicable.",accessibility:{role:'dialog, or alertdialog under role="alertdialog"',keyboard:[{key:"Escape",behavior:'Requests close (cancelable via escape-key-down). The event fires whatever the dismiss policy is; the close only follows when dismiss allows the escape channel ("any" or "escape").'},{key:"Tab / Shift+Tab",behavior:"Cycles focus within Content only, while modal (native focus trap)."}],ariaAttributes:["aria-modal reflects the modal prop while open","aria-labelledby points at Title's generated id, only once a Title renders \u2014 an unconditional dangling IDREF is no name at all","aria-describedby points at Description's generated id, same condition",'aria-haspopup="dialog", aria-expanded, aria-controls on Trigger',"showModal() makes everything outside an open modal dialog inert; aria-modal documents the barrier."],focusManagement:'Moves focus into Content on open \u2014 to a Cancel part first under role="alertdialog" \u2014 and traps it there while open; returns focus to the prior element on close (typically Trigger). Both fire as cancelable open-auto-focus/close-auto-focus events. A veto is honoured either by suppressing the move before it happens or by undoing it after the platform has made it; the end state matches, but in the compensated case focusin/focusout listeners see an extra round trip. For "focus this field, return there": Content accepts initialFocus and finalFocus selectors, applied only when the event was not vetoed.',namingRequirement:'Always render Title for the accessible name via aria-labelledby. If no visible title, add an explicit aria-label on Content \u2014 a dialog with no name announces as bare "dialog". Under role="alertdialog" render Description too: the alertdialog pattern makes the description part of the announcement, and nothing stands in for it.'},appearance:[{attribute:"data-size",values:["sm","md","lg"],stylesheet:"dialog.css"},{attribute:"data-status",values:["info","success","warning","danger"],stylesheet:"dialog.css"}],styling:{cssParts:["trigger","overlay","content","title","description","close","action","cancel","footer","body","close-icon"],dataAttributes:[...i,"data-state","data-kind","data-dismiss","data-skdx-dialog-cancel","data-status","data-disabled",{name:"data-size",frameworks:["webComponents"],exception:"attribute-passthrough",frameworksReason:"The custom element mirrors a consumer-authored data-size from its host onto the native dialog it renders, because the skin sizes the content off that node. The other frameworks spread the attribute straight onto the content element."},{name:"data-skdx-dialog-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[{name:"setOpen",signature:"(open: boolean) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new open prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Opens or closes the dialog and emits open-change, exactly as the trigger would."}]};export{d as dialogContract};
1
+ import{autoFocusEvents as t,dismissalEvents as o,forceMountProp as a,openChangeCompleteEvent as n,portalPart as e,portalPropsSansForceMount as s}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as i}from"./styling-hooks.js";import{withoutScope as r}from"./types.js";var p={name:"Dialog",purpose:`An accessible modal (or non-modal) dialog with focus trapping, scroll locking, and top-layer rendering. Given plain body content as children, the root derives its whole anatomy \u2014 Portal, Overlay, a panel with a derived Title/Description, the body, an optional footer, and a close button. With role="alertdialog" it is the interruptive confirmation: always modal, never closed by an outside interaction, focused on Cancel first, with derived Action and Cancel buttons from confirmLabel/cancelLabel. Manual composition of Trigger/Portal/Overlay/Content/Title/Description/Close/Action/Cancel is reached only through each framework's primitives/dialog subpath, never through this recipe's children.`,props:[{name:"title",type:"string",required:!1,description:"The panel's accessible name, rendered as the Title part. In Web Components, `title` shadows `HTMLElement.title` (harmless since the host is `display: contents`); an empty string counts as unset."},{name:"description",type:"string",required:!1,description:"The derived panel's accessible description, rendered as the Description part."},{name:"dismissible",type:"boolean",defaultValue:"true",required:!1,description:"Whether the panel can be dismissed. `false` forces dismiss to `'none'` (overriding any other value) and withholds the close button. The close button gates on this prop alone, not on `dismiss`, because `dismiss` governs implicit channels (Escape, outside); once `dismiss='none'` the close button is the only exit, so withholding it would strand the panel."},{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage."},{name:"modal",type:"boolean",defaultValue:"true",required:!1,description:"Whether the page is inert and locked from scrolling. true puts up a barrier (native showModal()); false leaves the page interactive (native show()). Owns three things: focus trap, inertness of everything outside, and body-scroll lock. The scroll lock mutates document.body.style.overflow and paddingRight while modal overlays are open, reference-counted across stacked Dialog/Drawer/LoadingOverlay, and restored to their exact prior strings on close. Modal does NOT own whether a dismissal channel closes the dialog \u2014 that is the dismiss prop. Outside dismissal cannot happen while the page is live; a non-modal dialog only reports it."},{name:"dismiss",type:"'any' | 'escape' | 'outside' | 'none'",defaultValue:"'any'",required:!1,description:'Which channels can close the dialog: "any" both Escape and outside, "escape" Escape only, "outside" outside interaction only, "none" neither (closes only via open/defaultOpen, Close part, or Trigger toggle). A policy, not a mute: the three outside events (pointer-down-outside, focus-outside, interact-outside) fire in every mode, and escape-key-down fires wherever the platform delivers Escape to the dialog \u2014 through the cancel event a native <dialog> raises for itself, which it raises only while modal. This prop decides whether an un-prevented one closes the dialog. Dismissal events still fire when prevented, so a consumer can log or veto; dismiss="none" is declarative preventDefault(), not a way to stop hearing about them. Outside dismissal requires modal, so dismiss="outside" on a non-modal dialog still closes on nothing. Resolved value is data-dismiss on Content, not the root.'},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Size rung forwarded to the Content panel's data-size; the attribute may be written on Content directly instead."},{name:"role",type:"'dialog' | 'alertdialog'",defaultValue:"'dialog'",required:!1,description:`The panel's role. "alertdialog" is the interruptive confirmation: Content renders role="alertdialog", modal is forced on, the outside channels of dismiss never close it (any resolves to escape, outside to none, reflected in data-dismiss), initial focus lands on Cancel before the first tabbable, the derived close button is withheld because Cancel is the explicit exit, and confirmLabel/cancelLabel default to "Confirm"/"Cancel" so the recipe always derives both buttons.`},{name:"status",type:"'info' | 'success' | 'warning' | 'danger' | (string & {})",required:!1,description:`Severity rung, written to data-status on the Content panel and forwarded to the derived Action button's own status; the data-status attribute may be set directly instead. "danger" is the destructive confirmation.`},{name:"confirmLabel",type:"string",required:!1,description:'Label of the derived Action button, which renders only when this is given \u2014 or always under role="alertdialog", where it defaults to "Confirm". Ignored when children replace the anatomy under manual composition.'},{name:"cancelLabel",type:"string",required:!1,description:'Label of the derived Cancel button, which renders only when this is given \u2014 or always under role="alertdialog", where it defaults to "Cancel". Ignored when children replace the anatomy under manual composition.'}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},n("dialog"),...o("dialog",{outsideNote:'Only a modal dialog closes on an outside interaction. With modal={false} all three are notification-only \u2014 the payload is still a cancelable event, so the shared payload assertion holds, but there is no dismissal behind it and preventing default changes nothing. They keep firing because they are the only way a consumer learns a non-modal dialog was clicked past. The dismiss policy is the second gate on the same path and behaves the same way: with dismiss="escape" or dismiss="none" these three still fire on a modal dialog and still close nothing, and with dismiss="outside" escape-key-down still fires and still closes nothing.'}),...t("dialog"),{name:"confirm",detail:"Event",description:"Fires when the derived Action button is clicked, before the close. Cancelable: preventDefault() suppresses the close, the supported way to hold a confirmation open while its request is in flight."},{name:"cancel",detail:"Event",description:"Fires when the derived Cancel button is clicked, before the close. Cancelable: preventDefault() suppresses the close."}],parts:[{name:"Trigger",description:'The button that opens the dialog (aria-haspopup="dialog", aria-expanded, aria-controls, data-state).',slots:[{name:"default",description:"Trigger's content \u2014 the button that opens the dialog."}]},{...e,description:`${e.description} Always mounted, unlike the other families': Content is a native <dialog> that has to outlive its own close(). showModal() already promotes Content to the top layer, so the part earns its keep by aiming Overlay and Content at a chosen container.`,props:[...s]},{name:"Overlay",description:"The scrim behind Content. A real element in all five frameworks \u2014 the four native-<dialog> ports paint its ::backdrop transparent so this part is the single scrim. It carries data-state and animates independently.",props:[r(a)]},{name:"Content",description:'The panel with role="dialog", labelled/described by Title/Description, aria-modal, data-state and data-dismiss. Handles dismissal (Escape, outside), scroll lock, and focus return; the focus trap, top layer and page inertness come from the platform. A native <dialog> driven by showModal()/show()/close() in all five frameworks, always mounted so close() has an element to fire on \u2014 which is why no framework ships forceMount on this part. Fires all dismissal and auto-focus events (declared on root to avoid duplication across dismissalEvents in overlay-parts).',lightDom:!0,props:[{name:"initialFocus",type:"string",required:!1,description:"CSS selector for the element to focus on open, resolved with querySelector inside Content. A selector rather than a ref because Web Components attributes cannot hold element handles, and five ports take the same value. Applies only when open-auto-focus was not vetoed. Falls back to the first tabbable or Content itself when the selector matches nothing, to prevent focus stranding on <body> inside a modal dialog."},{name:"finalFocus",type:"string",required:!1,description:"CSS selector for the element to focus on close, resolved at document level because Content is gone by then. The element that opened the dialog may no longer exist (a deleted row, a re-rendered list). Applies only when close-auto-focus was not vetoed. Falls back to the element that had focus before opening."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the content panel; the data-size attribute may be set directly instead."}]},{name:"Title",description:"The dialog's accessible name, referenced by Content's aria-labelledby.",slots:[{name:"default",description:"Title content \u2014 the dialog's accessible name."}]},{name:"Description",description:"The dialog's accessible description, referenced by Content's aria-describedby.",slots:[{name:"default",description:"Description content \u2014 the dialog's accessible description."}]},{name:"Close",description:"A button that closes the dialog. Rendered anywhere inside Content."},{name:"Action",description:"The button that carries out a confirmed action. Closes after the handler runs, unless preventDefault() is called on the confirm event \u2014 the supported way to hold a confirmation open while the request is in flight. Carries data-status when the root has one.",props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Sets the native disabled attribute so the button neither confirms nor closes the dialog when clicked."}]},{name:"Cancel",description:"The button that abandons the action. Marked with data-skdx-dialog-cancel, so an alertdialog focuses it first (the least destructive option). Its close is vetoable through the cancel event.",props:[{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Sets the native disabled attribute so the button neither cancels nor closes the dialog when clicked."}]},{name:"Dialogs",description:"The host that renders the imperative queue: whatever createDialogs is showing \u2014 an alert, a confirm or a prompt \u2014 as one Dialog at a time, keyed on the entry. React/Vue/Svelte/Web Components take the store as a `dialogs` prop; Angular injects SkdxDialogService instead. Takes no other props: title, description, labels and status arrive on each call. Emits data-kind on the panel.",props:[{name:"dialogs",type:"DialogsStore",required:!0,frameworks:["react","vue","svelte","webComponents"],exception:"imperative",frameworksReason:"Angular resolves the queue by injecting SkdxDialogService, so the host takes no store input.",description:"The queue to render, from createDialogs; Web Components default to a store of their own, read back off the element."}]}],contextApi:[{name:"createDialogs",signature:"() => DialogsStore",description:"Creates the alert / confirm / prompt queue once per app; alert resolves on dismissal, confirm with a boolean, prompt with the text or null. Web Components create one per <skdx-dialogs> by default.",accessors:{react:"createDialogs() from '@skdx/react'",vue:"createDialogs() from '@skdx/vue'",svelte:"createDialogs() from '@skdx/svelte'",angular:"inject(SkdxDialogService) \u2014 alert / confirm / prompt on the service",webComponents:"createDialogs() from '@skdx/web-components', or <skdx-dialogs>.dialogs"}}],slots:[{name:"default",description:"Body content of the derived panel, rendered inside Content after Title/Description and before footer/Close. On the recipe root this is always body content; under full manual composition via primitives, it is Content's hand-composed body."},{name:"footer",description:"Content after the body, before the close button (typically action buttons)."}],states:["closed","open"],controlledUncontrolled:"Stateless by default (uncontrolled): starts at defaultOpen and manages its own state internally. Passing open switches to controlled \u2014 the consumer owns state and must update it from open-change, or the dialog will not open/close on its own.",formBehavior:'Not applicable \u2014 Dialog participates in no form; Close and Trigger are type="button".',validationBehavior:"Not applicable.",accessibility:{role:'dialog, or alertdialog under role="alertdialog"',keyboard:[{key:"Escape",behavior:'Requests close (cancelable via escape-key-down). The event fires whatever the dismiss policy is; the close only follows when dismiss allows the escape channel ("any" or "escape").'},{key:"Tab / Shift+Tab",behavior:"Cycles focus within Content only, while modal (native focus trap)."}],ariaAttributes:["aria-modal reflects the modal prop while open","aria-labelledby points at Title's generated id, only once a Title renders \u2014 an unconditional dangling IDREF is no name at all","aria-describedby points at Description's generated id, same condition",'aria-haspopup="dialog", aria-expanded, aria-controls on Trigger',"showModal() makes everything outside an open modal dialog inert; aria-modal documents the barrier."],focusManagement:'Moves focus into Content on open \u2014 to a Cancel part first under role="alertdialog" \u2014 and traps it there while open; returns focus to the prior element on close (typically Trigger). Both fire as cancelable open-auto-focus/close-auto-focus events. A veto is honoured either by suppressing the move before it happens or by undoing it after the platform has made it; the end state matches, but in the compensated case focusin/focusout listeners see an extra round trip. For "focus this field, return there": Content accepts initialFocus and finalFocus selectors, applied only when the event was not vetoed.',namingRequirement:'Always render Title for the accessible name via aria-labelledby. If no visible title, add an explicit aria-label on Content \u2014 a dialog with no name announces as bare "dialog". Under role="alertdialog" render Description too: the alertdialog pattern makes the description part of the announcement, and nothing stands in for it.'},appearance:[{attribute:"data-size",values:["sm","md","lg"],stylesheet:"dialog.css"},{attribute:"data-status",values:["info","success","warning","danger"],stylesheet:"dialog.css"}],styling:{cssParts:["trigger","overlay","content","title","description","close","action","cancel","footer","body","close-icon"],dataAttributes:[...i,"data-state","data-kind","data-dismiss","data-skdx-dialog-cancel","data-status","data-disabled",{name:"data-size",frameworks:["webComponents"],exception:"attribute-passthrough",frameworksReason:"The custom element mirrors a consumer-authored data-size from its host onto the native dialog it renders, because the skin sizes the content off that node. The other frameworks spread the attribute straight onto the content element."},{name:"data-skdx-dialog-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[{name:"setOpen",signature:"(open: boolean) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new open prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Opens or closes the dialog and emits open-change, exactly as the trigger would."}],frameworkExtensions:[{framework:"react",name:"footer",type:"React.ReactNode",description:"Content rendered after the body in the derived panel \u2014 typically the action buttons; only read when `children` is plain body content. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax.",kind:"prop"},{framework:"svelte",name:"footer",type:"Snippet | undefined",description:"The derived panel's action row, rendered after the body inside\n`.skdx-dialog__footer`. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax.",kind:"prop"},{framework:"angular",name:"footer",type:"TemplateRef<unknown>",description:"Content rendered after the body in the derived panel \u2014 typically the action buttons. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax.",kind:"prop"},{framework:"react",name:"footer",type:"React.ReactNode",description:"Content rendered after the body in the derived panel \u2014 typically the action buttons; only read when `children` is plain body content. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax."},{framework:"svelte",name:"footer",type:"Snippet | undefined",description:"The derived panel's action row, rendered after the body inside\n`.skdx-dialog__footer`. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax."},{framework:"angular",name:"footer",type:"TemplateRef<unknown>",description:"Content rendered after the body in the derived panel \u2014 typically the action buttons. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax."}]};export{p as dialogContract};
@@ -1,3 +1,3 @@
1
- import type { ComponentContract } from './types.js';
1
+ import { type ComponentContract } from './types.js';
2
2
  /** The Drawer contract: a panel that slides in from a viewport edge, modal by default. */
3
3
  export declare const drawerContract: ComponentContract;
@@ -1 +1 @@
1
- import{autoFocusEvents as a,dismissalEvents as o,forceMountProp as e,openChangeCompleteEvent as n,portalPart as t}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as s}from"./styling-hooks.js";var l={name:"Drawer",purpose:`An accessible panel that slides in from a viewport edge \u2014 a side sheet, navigation drawer, or (placement="bottom") a bottom sheet. Modal by default with focus trapping, scroll locking and stacking; non-modal keeps dismissal and stacking but leaves the page interactive. The recipe derives its anatomy \u2014 Portal, Overlay, panel with Title/Description, body, optional footer, and close button. Manual composition is reached only through each framework's primitives/drawer subpath.`,props:[{name:"title",type:"string",required:!1,description:"The derived panel's accessible name, rendered as the Title part."},{name:"description",type:"string",required:!1,description:"The derived panel's accessible description, rendered as the Description part."},{name:"dismissible",type:"boolean",defaultValue:"true",required:!1,description:"Whether the panel can be dismissed. `false` forces the dismiss policy to `'none'` and withholds the close button; `dismiss` still applies while `true`. The close button is gated on this prop alone, never on `dismiss` \u2014 if dismiss is `'none'`, the explicit close button is the only way out."},{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage."},{name:"placement",type:"'start' | 'end' | 'top' | 'bottom'",defaultValue:"'end'",required:!1,description:'Which viewport edge the drawer is anchored to, expressed logically: start/end follow the writing direction (start is left under dir="ltr" and right under dir="rtl"), top/bottom are physical. placement="bottom" is the bottom-sheet case. Content publishes it twice, not interchangeably: data-placement is this value verbatim, unresolved, for logical CSS (inset-inline-start, translate with logical sign); data-side is the *physical* edge the same value resolves to under the ambient writing direction \u2014 start becomes left under dir="ltr" and right under dir="rtl", while top and bottom pass through unchanged. All five frameworks resolve it, and @skdx/skins positions from data-side, because a slide-in needs a physical translate and CSS cannot resolve a logical edge to one.'},{name:"modal",type:"boolean",defaultValue:"true",required:!1,description:'Whether the drawer traps focus, locks body scroll, and blocks page interaction. A non-modal drawer skips the focus trap and scroll lock but keeps Escape, outside-interaction dismissal (see dismiss), and overlay stacking. The scroll lock mutates document.body.style: overflow becomes "hidden" and paddingRight becomes the scrollbar width, so the page does not shift. The lock is reference-counted across stacked overlays (Dialog, Drawer, LoadingOverlay share one counter) and both properties restore to their original strings. On by default: a drawer scrolling the page while the user drags inside is the failure mode; defaulting safe costs one prop to opt out.'},{name:"dismiss",type:"'any' | 'escape' | 'outside' | 'none'",defaultValue:"'any'",required:!1,description:"Which dismissal channels close the drawer: any (both); escape (Escape key only); outside (pointer-down or focus-move outside only); none (neither, leaving Close/Trigger/open prop as the only ways out). A policy, not a mute: all four dismissal events (escape-key-down, pointer-down-outside, focus-outside, interact-outside) keep firing and remain cancelable, so a consumer can observe and respond to policy-refused attempts. One prop covering both channels, so the policy reads as a single value across Drawer and Dialog. Unlike Dialog, a drawer dismisses on outside interaction whether or not modal \u2014 that is the point of a side sheet you can shrug off \u2014 so modal={false} does not turn outside dismissal off; dismiss is the only declarative way to disable it. The resolved value reflects as data-dismiss on Content, not the root, because the root renders no element in four of five frameworks."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Size rung forwarded to the Content panel's data-size; the attribute may be written on Content directly instead."}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},n("drawer"),...o("drawer"),...a("drawer")],parts:[{name:"Trigger",description:'The button that opens the drawer (aria-haspopup="dialog", aria-expanded, aria-controls, data-state).'},{...t,description:`${t.description} Shipped by React, Vue, Svelte and Web Components. Angular portals via the [skdxPortal] attribute directive (skdxPortal input carries the same container capability). In Web Components, force-mount does not apply (parts hide with hidden attribute, never unmount) and container is a property, not attribute, because a DOM node cannot cross one.`},{name:"Overlay",description:"The scrim rendered behind Content, typically dimming the page. A real element (not ::backdrop pseudo-element) animated independently of the panel with its own data-state. Rendered whether modal or not; non-modal usually styles it as pointer-events: none or omits it entirely.",props:[e]},{name:"Content",description:'The sliding panel: role="dialog", labelled/described by Title/Description, with aria-modal and data-state reflecting state, plus data-placement (logical prop verbatim) and data-side the physical edge it resolves to under ambient writing direction. Also carries data-dismiss (the resolved dismiss policy value) because the root renders no element in four of five frameworks. Owns dismissal (Escape, outside interaction) and \u2014 while modal \u2014 the focus trap, scroll lock, and focus return on close. Fires all six dismissal/auto-focus events declared on the root (see dismissalEvents in ./overlay-parts).',props:[e,{name:"initialFocus",type:"string",required:!1,description:'CSS selector for the element to receive focus when the drawer opens, resolved with querySelector inside Content. A selector string (not a handle) because an element cannot cross HTML attributes; the Web Components port needs initial-focus="\u2026". Applies only when open-auto-focus was not vetoed (a veto already means "I am placing focus myself"). When the selector matches nothing, focus falls back to the first tabbable in Content with a warning; the fallback prevents strand-on-body mistakes. Inert on non-modal \u2014 a non-modal drawer does not move focus in, so there is no initial move to redirect.'},{name:"finalFocus",type:"string",required:!1,description:"CSS selector for the element focus should return to when the drawer closes, resolved with querySelector at document level. Unlike initialFocus (target inside Content), this target is outside by definition (the list item the drawer was editing, a toast, a field). Applies only when close-auto-focus was not vetoed; falls back to the element that had focus before opening (typically Trigger) with a warning, preventing strand-on-body mistakes. Live on non-modal too: a non-modal drawer never moves focus in but does return focus it would otherwise strand, so when Escape closes a non-modal drawer the user tabbed into, focus goes here instead of Trigger."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the content panel; the data-size attribute may be set directly instead."}]},{name:"Title",description:"The drawer's accessible name, referenced by Content's aria-labelledby."},{name:"Description",description:"The drawer's accessible description, referenced by Content's aria-describedby."},{name:"Close",description:"A button that closes the drawer. Rendered anywhere inside Content."}],slots:[{name:"trigger",description:"Trigger's content \u2014 the button that opens the drawer."},{name:"default",description:"The panel's body content, rendered inside Content after any Title/Description and before footer/Close. On the recipe root always; on manual composition through primitives, this is Content's body."},{name:"title",description:"Title's content \u2014 the drawer's accessible name."},{name:"description",description:"Description's content \u2014 the drawer's accessible description."},{name:"footer",description:"Content placed after the body inside the panel, typically action buttons."}],states:["closed","open"],controlledUncontrolled:"Uncontrolled by default: starts at defaultOpen and manages its own state. Passing open switches to controlled \u2014 the consumer owns state and must update it from open-change.",formBehavior:'Not applicable \u2014 Drawer participates in no form; Close and Trigger are type="button".',validationBehavior:"Not applicable.",accessibility:{role:"dialog",keyboard:[{key:"Escape",behavior:'Requests close (cancelable via escape-key-down), modal or not. Fires under every dismiss policy; actually closes only when dismiss is "any" or "escape".'},{key:"Tab / Shift+Tab",behavior:"Cycles focus within Content only, while modal. A non-modal drawer does not trap Tab \u2014 focus leaves it, reported as focus-outside."}],ariaAttributes:["aria-modal reflects the modal prop while open","aria-labelledby points at Title's generated id, emitted only once a Title has rendered \u2014 an unconditional attribute at an absent id leaves the panel with no accessible name, since the platform does not fall back past a dangling IDREF","aria-describedby points at Description's generated id, on the same condition",'aria-haspopup="dialog", aria-expanded, and aria-controls on Trigger',`While a modal drawer is open, everything outside it is inert. No framework renders a native <dialog> here, so unlike Dialog none of the five gets this from the platform \u2014 each writes inert on the portal container's siblings itself, making aria-modal="true" a true statement`],focusManagement:"While modal, moves focus into Content when opened \u2014 to initialFocus selector when given and matches, else to the first tabbable \u2014 and returns focus on close to finalFocus selector when given and matches, else to the element that had focus before opening (typically Trigger). Both selectors are advisory: no match falls back to the default rather than strand-on-body. Both moves are announced as cancelable open-auto-focus / close-auto-focus events, and preventing default genuinely suppresses the move in all five frameworks (no uncancelable platform move involved). A non-modal drawer does not move focus in \u2014 nothing pulls the user out of the page \u2014 but returns focus it would otherwise strand: if the active element is inside Content when a non-modal drawer closes, close-auto-focus fires and, unless vetoed, focus goes back to Trigger or to finalFocus (live in that mode even though initialFocus is not). Focus never moved in needs no return, so no event fires in that case.",namingRequirement:'Always render Title \u2014 it supplies the accessible name referenced by aria-labelledby. A drawer with no visible title must instead carry an explicit aria-label on Content: role="dialog" with no name announces as a bare "dialog", and omitting Title leaves aria-labelledby pointing at nothing.'},appearance:[{attribute:"data-size",values:["sm","md","lg"],stylesheet:"drawer.css"}],styling:{cssParts:["trigger","overlay","content","title","description","close","footer","body","close-icon"],dataAttributes:[...s,"data-state","data-placement","data-side","data-dismiss","data-size",{name:"data-skdx-drawer-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[{name:"setOpen",signature:"(open: boolean) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new open prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Opens or closes the drawer and emits open-change, exactly as the trigger would."}]};export{l as drawerContract};
1
+ import{autoFocusEvents as o,dismissalEvents as n,forceMountProp as e,openChangeCompleteEvent as a,portalPart as t}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as r}from"./styling-hooks.js";import{withoutScope as s}from"./types.js";var p={name:"Drawer",purpose:`An accessible panel that slides in from a viewport edge \u2014 a side sheet, navigation drawer, or (placement="bottom") a bottom sheet. Modal by default with focus trapping, scroll locking and stacking; non-modal keeps dismissal and stacking but leaves the page interactive. The recipe derives its anatomy \u2014 Portal, Overlay, panel with Title/Description, body, optional footer, and close button. Manual composition is reached only through each framework's primitives/drawer subpath.`,props:[{name:"title",type:"string",required:!1,description:"The derived panel's accessible name, rendered as the Title part."},{name:"description",type:"string",required:!1,description:"The derived panel's accessible description, rendered as the Description part."},{name:"dismissible",type:"boolean",defaultValue:"true",required:!1,description:"Whether the panel can be dismissed. `false` forces the dismiss policy to `'none'` and withholds the close button; `dismiss` still applies while `true`. The close button is gated on this prop alone, never on `dismiss` \u2014 if dismiss is `'none'`, the explicit close button is the only way out."},{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage."},{name:"placement",type:"'start' | 'end' | 'top' | 'bottom'",defaultValue:"'end'",required:!1,description:'Which viewport edge the drawer is anchored to, expressed logically: start/end follow the writing direction (start is left under dir="ltr" and right under dir="rtl"), top/bottom are physical. placement="bottom" is the bottom-sheet case. Content publishes it twice, not interchangeably: data-placement is this value verbatim, unresolved, for logical CSS (inset-inline-start, translate with logical sign); data-side is the *physical* edge the same value resolves to under the ambient writing direction \u2014 start becomes left under dir="ltr" and right under dir="rtl", while top and bottom pass through unchanged. All five frameworks resolve it, and @skdx/skins positions from data-side, because a slide-in needs a physical translate and CSS cannot resolve a logical edge to one.'},{name:"modal",type:"boolean",defaultValue:"true",required:!1,description:'Whether the drawer traps focus, locks body scroll, and blocks page interaction. A non-modal drawer skips the focus trap and scroll lock but keeps Escape, outside-interaction dismissal (see dismiss), and overlay stacking. The scroll lock mutates document.body.style: overflow becomes "hidden" and paddingRight becomes the scrollbar width, so the page does not shift. The lock is reference-counted across stacked overlays (Dialog, Drawer, LoadingOverlay share one counter) and both properties restore to their original strings. On by default: a drawer scrolling the page while the user drags inside is the failure mode; defaulting safe costs one prop to opt out.'},{name:"dismiss",type:"'any' | 'escape' | 'outside' | 'none'",defaultValue:"'any'",required:!1,description:"Which dismissal channels close the drawer: any (both); escape (Escape key only); outside (pointer-down or focus-move outside only); none (neither, leaving Close/Trigger/open prop as the only ways out). A policy, not a mute: all four dismissal events (escape-key-down, pointer-down-outside, focus-outside, interact-outside) keep firing and remain cancelable, so a consumer can observe and respond to policy-refused attempts. One prop covering both channels, so the policy reads as a single value across Drawer and Dialog. Unlike Dialog, a drawer dismisses on outside interaction whether or not modal \u2014 that is the point of a side sheet you can shrug off \u2014 so modal={false} does not turn outside dismissal off; dismiss is the only declarative way to disable it. The resolved value reflects as data-dismiss on Content, not the root, because the root renders no element in four of five frameworks."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Size rung forwarded to the Content panel's data-size; the attribute may be written on Content directly instead."}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever it changes, controlled or not."},a("drawer"),...n("drawer"),...o("drawer")],parts:[{name:"Trigger",description:'The button that opens the drawer (aria-haspopup="dialog", aria-expanded, aria-controls, data-state).',slots:[{name:"default",description:"Trigger's content \u2014 the button that opens the drawer."}]},{...t,description:`${t.description} Shipped by React, Vue, Svelte and Web Components. Angular portals via the [skdxPortal] attribute directive (skdxPortal input carries the same container capability). In Web Components, force-mount does not apply (parts hide with hidden attribute, never unmount) and container is a property, not attribute, because a DOM node cannot cross one.`},{name:"Overlay",description:"The scrim rendered behind Content, typically dimming the page. A real element (not ::backdrop pseudo-element) animated independently of the panel with its own data-state. Rendered whether modal or not; non-modal usually styles it as pointer-events: none or omits it entirely.",props:[s(e)]},{name:"Content",description:'The sliding panel: role="dialog", labelled/described by Title/Description, with aria-modal and data-state reflecting state, plus data-placement (logical prop verbatim) and data-side the physical edge it resolves to under ambient writing direction. Also carries data-dismiss (the resolved dismiss policy value) because the root renders no element in four of five frameworks. Owns dismissal (Escape, outside interaction) and \u2014 while modal \u2014 the focus trap, scroll lock, and focus return on close. Fires all six dismissal/auto-focus events declared on the root (see dismissalEvents in ./overlay-parts).',props:[e,{name:"initialFocus",type:"string",required:!1,description:'CSS selector for the element to receive focus when the drawer opens, resolved with querySelector inside Content. A selector string (not a handle) because an element cannot cross HTML attributes; the Web Components port needs initial-focus="\u2026". Applies only when open-auto-focus was not vetoed (a veto already means "I am placing focus myself"). When the selector matches nothing, focus falls back to the first tabbable in Content with a warning; the fallback prevents strand-on-body mistakes. Inert on non-modal \u2014 a non-modal drawer does not move focus in, so there is no initial move to redirect.'},{name:"finalFocus",type:"string",required:!1,description:"CSS selector for the element focus should return to when the drawer closes, resolved with querySelector at document level. Unlike initialFocus (target inside Content), this target is outside by definition (the list item the drawer was editing, a toast, a field). Applies only when close-auto-focus was not vetoed; falls back to the element that had focus before opening (typically Trigger) with a warning, preventing strand-on-body mistakes. Live on non-modal too: a non-modal drawer never moves focus in but does return focus it would otherwise strand, so when Escape closes a non-modal drawer the user tabbed into, focus goes here instead of Trigger."},{name:"size",type:"'sm' | 'md' | 'lg' | (string & {})",required:!1,description:"Visual size rung, written to data-size on the content panel; the data-size attribute may be set directly instead."}]},{name:"Title",description:"The drawer's accessible name, referenced by Content's aria-labelledby.",slots:[{name:"default",description:"Title's content \u2014 the drawer's accessible name."}]},{name:"Description",description:"The drawer's accessible description, referenced by Content's aria-describedby.",slots:[{name:"default",description:"Description's content \u2014 the drawer's accessible description."}]},{name:"Close",description:"A button that closes the drawer. Rendered anywhere inside Content."}],slots:[{name:"default",description:"The panel's body content, rendered inside Content after any Title/Description and before footer/Close. On the recipe root always; on manual composition through primitives, this is Content's body."},{name:"footer",description:"Content placed after the body inside the panel, typically action buttons."}],states:["closed","open"],controlledUncontrolled:"Uncontrolled by default: starts at defaultOpen and manages its own state. Passing open switches to controlled \u2014 the consumer owns state and must update it from open-change.",formBehavior:'Not applicable \u2014 Drawer participates in no form; Close and Trigger are type="button".',validationBehavior:"Not applicable.",accessibility:{role:"dialog",keyboard:[{key:"Escape",behavior:'Requests close (cancelable via escape-key-down), modal or not. Fires under every dismiss policy; actually closes only when dismiss is "any" or "escape".'},{key:"Tab / Shift+Tab",behavior:"Cycles focus within Content only, while modal. A non-modal drawer does not trap Tab \u2014 focus leaves it, reported as focus-outside."}],ariaAttributes:["aria-modal reflects the modal prop while open","aria-labelledby points at Title's generated id, emitted only once a Title has rendered \u2014 an unconditional attribute at an absent id leaves the panel with no accessible name, since the platform does not fall back past a dangling IDREF","aria-describedby points at Description's generated id, on the same condition",'aria-haspopup="dialog", aria-expanded, and aria-controls on Trigger',`While a modal drawer is open, everything outside it is inert. No framework renders a native <dialog> here, so unlike Dialog none of the five gets this from the platform \u2014 each writes inert on the portal container's siblings itself, making aria-modal="true" a true statement`],focusManagement:"While modal, moves focus into Content when opened \u2014 to initialFocus selector when given and matches, else to the first tabbable \u2014 and returns focus on close to finalFocus selector when given and matches, else to the element that had focus before opening (typically Trigger). Both selectors are advisory: no match falls back to the default rather than strand-on-body. Both moves are announced as cancelable open-auto-focus / close-auto-focus events, and preventing default genuinely suppresses the move in all five frameworks (no uncancelable platform move involved). A non-modal drawer does not move focus in \u2014 nothing pulls the user out of the page \u2014 but returns focus it would otherwise strand: if the active element is inside Content when a non-modal drawer closes, close-auto-focus fires and, unless vetoed, focus goes back to Trigger or to finalFocus (live in that mode even though initialFocus is not). Focus never moved in needs no return, so no event fires in that case.",namingRequirement:'Always render Title \u2014 it supplies the accessible name referenced by aria-labelledby. A drawer with no visible title must instead carry an explicit aria-label on Content: role="dialog" with no name announces as a bare "dialog", and omitting Title leaves aria-labelledby pointing at nothing.'},appearance:[{attribute:"data-size",values:["sm","md","lg"],stylesheet:"drawer.css"}],styling:{cssParts:["trigger","overlay","content","title","description","close","footer","body","close-icon"],dataAttributes:[...r,"data-state","data-placement","data-side","data-dismiss","data-size",{name:"data-skdx-drawer-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[{name:"setOpen",signature:"(open: boolean) => void",frameworks:["webComponents"],exception:"imperative",frameworksReason:"Only the custom element publishes it. The other four re-render with a new open prop and never expose a setter; a Web Components consumer holds the element itself, so the same change is made by calling this method on it.",description:"Opens or closes the drawer and emits open-change, exactly as the trigger would."}],frameworkExtensions:[{framework:"react",name:"footer",type:"React.ReactNode",description:"Content rendered after the body in the derived panel \u2014 typically the action buttons; only read when `children` is plain body content. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax.",kind:"prop"},{framework:"svelte",name:"footer",type:"Snippet | undefined",description:"The derived panel's action row, rendered after the body inside\n`.skdx-drawer__footer`. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax.",kind:"prop"},{framework:"angular",name:"footer",type:"TemplateRef<unknown>",description:'Content rendered in a footer region after the body, typically action\nbuttons \u2014 a `TemplateRef`, so `<ng-template #f>\u2026</ng-template>` bound as\n`[footer]="f"`. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax.',kind:"prop"},{framework:"webComponents",name:"triggerEl",type:"HTMLElement | null",description:"The Trigger's inner button, so dismissal treats the trigger as inside the layer. This is a reference to the underlying native element for integration with platform APIs; the other wrappers hand off elements through their framework ref/context mechanisms instead of this property spelling.",kind:"prop"},{framework:"react",name:"footer",type:"React.ReactNode",description:"Content rendered after the body in the derived panel \u2014 typically the action buttons; only read when `children` is plain body content. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax."},{framework:"svelte",name:"footer",type:"Snippet | undefined",description:"The derived panel's action row, rendered after the body inside\n`.skdx-drawer__footer`. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax."},{framework:"angular",name:"footer",type:"TemplateRef<unknown>",description:'Content rendered in a footer region after the body, typically action\nbuttons \u2014 a `TemplateRef`, so `<ng-template #f>\u2026</ng-template>` bound as\n`[footer]="f"`. This wrapper receives projected content as a render prop, Snippet or TemplateRef. Other wrappers supply the matching named slot or default content of the composed part; the property is framework rendering syntax.'},{framework:"webComponents",name:"triggerEl",type:"HTMLElement | null",description:"The Trigger's inner button, so dismissal treats the trigger as inside the layer. This is a reference to the underlying native element for integration with platform APIs; the other wrappers hand off elements through their framework ref/context mechanisms instead of this property spelling."}]};export{p as drawerContract};
@@ -1 +1 @@
1
- import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var a={name:"Field",purpose:"Wraps one form control and wires its label, description, and error text to it \u2014 generating ids and setting aria-describedby, aria-invalid, and aria-labelledby on the control \u2014 so any control inside a Field satisfies WCAG 3.3.1 and 3.3.3 without consumer-written ARIA. Owns field-level validation: validate callback, change/blur/submit modes, server-error injection, and native constraint-validation fallback.",props:[{name:"validate",type:"(value: unknown) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>",required:!1,description:"Custom validator returning one, several, or no error messages. May be async; superseded runs are discarded. Spelled `validate` in all five frameworks; the method is `checkValidity()` to avoid collision. **Property-only on `<skdx-field>`**: functions have no attribute form."},{name:"mode",type:"'change' | 'blur' | 'submit'",defaultValue:"'blur'",required:!1,description:"When validation first runs. Regardless of mode, a field that has already failed re-validates on every input so the error clears as soon as it is fixed."},{name:"errors",type:"string | string[]",required:!1,description:"Server-injected errors, always shown and listed before `validate` messages. Clearing the prop clears them. Unlike React Aria, keystroke alone does not clear them \u2014 a server error states a fact the client cannot recheck, so dropping it on input would silently hide an unsolved problem. A `reset()` clears them until the next render, since the prop is declarative."},{name:"value",type:"unknown",required:!1,description:"Value handed to `validate`. Omit to read the discovered control's `checked` (checkbox/radio) or `value` property. **Property-only on `<skdx-field>`**: values are rarely strings and don't serialize across the attribute boundary."},{name:"name",type:"string",required:!1,description:"Field name, used as the fallback link text in a Form's ErrorSummary."},{name:"required",type:"boolean",required:!1,description:'Whether an answer is mandatory. Set as the native `required` attribute on labelable controls, `aria-required="true"` on groups and widget-role elements. Render a RequiredIndicator inside the Label; its text joins the accessible name so the requirement is announced. No `data-required` hook \u2014 `[required]` and `[aria-required="true"]` are already selectors.'},{name:"disabled",type:"boolean",required:!1,description:'Whether the field is unavailable. Set as the native `disabled` attribute where available, `aria-disabled="true"` otherwise (widgets stay focusable). Mirrored as `data-disabled` on the Field and control. Both withdrawn only when the Field wrote them, so a consumer-set `disabled` survives untouched.'},{name:"getCharacterCountMessage",type:"(remaining: number) => string",required:!1,description:'Words all character-count copy: rendered count, announced pause message, and over-limit error. Negative remaining means over the limit. One prop for all three to keep announced and rendered counts in sync. Defaults to GOV.UK wording, e.g. "You have 12 characters remaining". **Property-only on `<skdx-field>`**: functions have no attribute form.'},{name:"invalidMessage",type:"string",required:!1,description:'Fallback message when the control fails native constraint validation but reports no message (jsdom, some browsers leave validationMessage empty). Browser messages are already localized; this one needs translating. Defaults to "This field is invalid.".'},{name:"validationDebounceTime",type:"number",defaultValue:"0",required:!1,description:"Milliseconds `mode: 'change'` waits after the last keystroke before running `validate`. Only change mode debounces; blur/submit re-validate already-failed fields undebounced. A focusout cancels pending runs and validates immediately, so blur still answers at once."},{name:"label",type:"string",required:!1,description:"Label text, which derives a Label part with RequiredIndicator inside it before the control. No prop, no element \u2014 labels are opt-in consumer text. A consumer-authored Label takes precedence, so both recipe and hand-composed anatomy work in the same Field."},{name:"description",type:"string",required:!1,description:"Help text, which derives a Description part after the control and adds its id to `aria-describedby`. No prop, no element \u2014 consumer-authored Description takes precedence."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."}],events:[{name:"validity-change",description:"Fires whenever the field's validity or interaction state changes. Bubbles, so Form's ErrorSummary can re-scan. `validity` is the control's live `ValidityState` (or null if none), letting consumers word error messages per constraint or style on which constraint failed.",detail:"{ valid: boolean; errors: string[]; touched: boolean; dirty: boolean; filled: boolean; count: number; validity: ValidityState | null }"}],parts:[{name:"Label",description:"The field's label. Renders a `<label>` with `for` pointing to the control, or `<legend>` in Fieldset mode (groups get `aria-labelledby` instead). Double-click focuses the control instead of selecting text."},{name:"Description",description:"Supplementary help text. Its id joins the control's `aria-describedby` so it is announced with the control.",props:[{name:"as",type:"PolymorphicTag",defaultValue:"'span'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular uses an attribute directive on the consumer's element \u2014 `<section skdx-ng-card>` \u2014 its idiomatic syntax. Eleven such directives predate this pattern.",description:"Element name to render instead of the default <span>, from the closed allowlist in POLYMORPHIC_TAGS. Tag name only, never a component \u2014 the list is closed because Lit needs a static-html literal per tag. React additionally reaches this through asChild, which wins when both are set."}]},{name:"Error",description:"The field's error text. Renders nothing while the field is valid; when it renders, its generated id joins the control's `aria-describedby` and the control gains `aria-invalid=\"true\"`. Renders current messages when given no children of its own. Children are content, not a render function; reach error details through the `validity-change` event's `ValidityState` instead.",props:[{name:"as",type:"PolymorphicTag",defaultValue:"'span'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular uses an attribute directive on the consumer's element \u2014 `<section skdx-ng-card>` \u2014 its idiomatic syntax. Eleven such directives predate this pattern.",description:"Element name to render instead of the default <span>, from the closed allowlist in POLYMORPHIC_TAGS. Tag name only, never a component \u2014 the list is closed because Lit needs a static-html literal per tag. React additionally reaches this through asChild, which wins when both are set."}]},{name:"CharacterCount",description:"A running character count \u2014 GOV.UK's pattern, works for Textarea and TextField alike. maxLength is a soft limit and is deliberately not the control's native maxlength, which truncates silently; exceeding it makes the Field invalid (data-invalid, aria-invalid on the control, a message in Error, blocked submit) and adds data-over-limit here and on the Field. Count is announced one typing pause (1000ms) after the last keystroke, not continuously. Pass your own copy to override generated text.\n\n**Primitives-only in all five packages; the Field root's recipe never derives one.** maxLength is consumer-provided, so `maxLength` and `threshold` are declared on this part and never on the Field root.",props:[{name:"maxLength",type:"number",required:!0,description:"The soft limit the count counts down to."},{name:"threshold",type:"number",defaultValue:"0",required:!1,description:"Percentage of maxLength before the count renders or announces."}]},{name:"RequiredIndicator",description:"The \"mandatory\" marker, rendered *inside* the Label while Field is `required`. Its placement matters: inside the label means its text joins the control's accessible name so the requirement is announced. Deliberately not `aria-hidden` \u2014 hiding it leaves it visible to users who don't need it announced and silent for those who do. Defaults to `*`; pass words for more explicit marking."},{name:"Fieldset",description:'A Field rendering `<fieldset>` with Label in `<legend>` \u2014 the grouping radio/checkbox groups need, since `<label for>` cannot reference `[role="radiogroup"]`; the group gets `aria-labelledby` instead. Same props, same validation model and states as Field root.\n\n**Public and headless in all five packages, deliberately not a recipe.** Field root takes `label`/`description` and derives the parts; Fieldset derives nothing. It is the advanced path \u2014 the consumer composes Label, controls and Error inside it. A recipe Field can sit inside a hand-composed Fieldset.',props:[{name:"validate",type:"(value: unknown) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>",required:!1,description:'The group\'s validator, same shape and async handling as the root\'s. Spelled on Fieldset rather than inherited because group rules are those no single control can state \u2014 "pick at least one", "end date must not precede start". Named `validate` so the method is `checkValidity()`, avoiding collision. **Property-only on `<skdx-fieldset>`** \u2014 functions have no attribute form.'},{name:"mode",type:"'change' | 'blur' | 'submit'",defaultValue:"'blur'",required:!1,description:"When validation first runs for the group. Groups that have failed re-validate on every input regardless of mode."},{name:"errors",type:"string | string[]",required:!1,description:"Server-injected errors for the group, always shown and listed before `validate` messages. Clearing the prop clears them."},{name:"name",type:"string",required:!1,description:"Group name, fallback link text in Form's ErrorSummary when the group renders no error."},{name:"invalidMessage",type:"string",required:!1,description:'Fallback message when the group control fails native constraint validation but reports no message. Browser messages always win \u2014 they are already localized. Defaults to "This field is invalid.".'},{name:"value",type:"unknown",required:!1,description:"Value handed to the group's `validate`. Omit to read the group control's value \u2014 for a radiogroup or checkbox group, whatever its form-participation input would submit."},{name:"required",type:"boolean",required:!1,description:'Whether an answer is mandatory for the group. Groups are always `aria-required="true"` since `[role="radiogroup"]` has no native `required`.'},{name:"disabled",type:"boolean",required:!1,description:"Whether the group is unavailable. Set as the native `disabled` attribute on the rendered `<fieldset>`, which cascades to every native control it contains. Mirrored as `data-disabled` on the Fieldset root."},{name:"getCharacterCountMessage",type:"(remaining: number) => string",required:!1,description:"Words the character-count copy for the group, exactly as on the Field root. Declared on the Fieldset too because dropping it would put announced and rendered counts in different languages."},{name:"validationDebounceTime",type:"number",defaultValue:"0",required:!1,description:"Milliseconds `mode: 'change'` waits after the last input in the group before running `validate`. Group validators receive the whole answer set, so in change mode they do real work \u2014 cross-field comparisons, lookups \u2014 on every keystroke. A focusout cancels pending runs and validates immediately; already-failed groups re-validate undebounced."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the Fieldset; the data-density attribute may be set directly instead."}],methods:[{name:"checkValidity",signature:"(): Promise<boolean>",description:"Runs the group's validation now and resolves to whether it is valid. Form's `trySubmit()` calls this on every Fieldset and Field."},{name:"reset",signature:"(): void",description:"Clears the group's messages and its `touched`/`dirty` state."}]}],slots:[{name:"default",description:"A Label, the control, an optional Description, and an Error."}],appearance:[{attribute:"data-density",values:["compact"],stylesheet:"field.css"}],states:["valid","invalid","dirty","touched","filled","over-limit","required","disabled"],controlledUncontrolled:"The Field never owns the control's value; only validation state. Validation is uncontrolled; `errors` injects state from outside without claiming the rest.",formBehavior:"Not a form control itself \u2014 adds no value to submission and renders no input. Listens (capture-phase) for the host form's `submit` and prevents it while invalid or validation is pending. Async validators block the initial submit; the last pending Field to settle re-dispatches through `requestSubmit()` with `data-skdx-field-resubmit` on the form, and Fields answer from computed state instead of re-running.",validationBehavior:'Four merged sources in order: `errors` (server), the `validate` callback, the native `validationMessage` of the discovered control when `ValidityState` is invalid (so `required`/`type="email"`/`pattern` work without config), and CharacterCount over `maxLength`. Native message is *appended* to the first two, de-duplicated on exact string match, never substituted \u2014 a control that is both `required` and failing a custom rule must report both. `invalidMessage` is the fallback where the engine reports empty `validationMessage`. `mode` decides when the first run happens; `validationDebounceTime` controls `change` mode\'s wait after the last keystroke. After failure, every input re-validates undebounced. Newly-raised errors are announced assertively through the live announcer \u2014 silent on a failed submit only when an ErrorSummary renders.',accessibility:{role:"generic (a `<div>`, or a `<fieldset>` in group mode)",keyboard:[],ariaAttributes:["aria-describedby on the control \u2014 Description id and Error id while invalid",'aria-invalid="true" on the control while invalid',"aria-labelledby on the control in group mode, merging the legend id into whatever exists \u2014 the Field adds and withdraws its own id, never replacing it","required/aria-required and disabled/aria-disabled on the control \u2014 the native attribute where the control has one, the ARIA state where it does not","Every id the Field writes points at a rendered part. Label `for` and control `id` appear as a pair or not at all \u2014 so server renders with no control emit neither rather than a broken link.",'aria-errormessage is deliberately **not** used; Error is routed through `aria-describedby` instead. An `aria-errormessage` target is only exposed while the element carries `aria-invalid="true"` (a rendering-order race condition). Support is inconsistent; the Error is already in `aria-describedby`, where every screen reader reads it. Consumers who want it can set it on their own control \u2014 the Field merges into `aria-describedby` and never touches `aria-errormessage`.'],focusManagement:"Field moves no focus. Form ErrorSummary focuses failing fields, and for groups, the first control inside (GOV.UK rule).",namingRequirement:"Label supplies the accessible name via `for`/`id` (single control) or `aria-labelledby` (group). RequiredIndicator inside the Label is part of that name, making the requirement audible."},styling:{cssParts:["field","label","legend","required-indicator","description","error","character-count"],dataAttributes:[...e,"data-skdx-field","data-skdx-field-name","data-skdx-field-message","data-valid","data-invalid","data-touched","data-dirty","data-filled","data-over-limit","data-disabled","data-skdx-field-required-indicator","data-skdx-field-character-count","data-skdx-field-control","data-skdx-field-description","data-skdx-field-error","data-skdx-field-pending","data-skdx-field-resubmit",{name:"data-skdx-field-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'},"data-orientation","data-density"],cssCustomProperties:[]},frameworkExtensions:[{framework:"angular",name:"control",type:"AbstractControl",description:'An Angular Reactive Forms control to mirror. Its `errors` are mapped through `errorMessages` and merged ahead of `validate` messages; `touched`/`dirty` are replayed as `input`/`focusout` events the engine listens for. Angular-only because Reactive Forms exists nowhere else \u2014 other frameworks reach this place via `errors` and `value` props. Mirroring is deliberately one-way and one-shot per transition \u2014 it can set `touched` and `dirty`, never clear them, because it works by replaying DOM events and there is no event that means "untouched". Clearing those flags is `reset()`\'s job: it withdraws `data-touched` and `data-dirty` and returns the field to mount state, so `markAsUntouched()` on the control pairs with a `reset()` call rather than travelling through the mirror.'},{framework:"angular",name:"errorMessages",type:"Record<string, string>",description:"Maps a Reactive Forms error key (`required`, `minlength`, \u2026) to the message shown for it; unmapped keys are shown verbatim. Meaningless without `control`, and Angular-only because the key space is Angular's validators', not this library's."}],publicMethods:[{name:"checkValidity",signature:"(): Promise<boolean>",description:"Runs validation now and resolves to whether the field is valid, reached through the component instance or element. Named for the platform's own method to avoid collision with the `validate` prop. Returns a promise since validators may be async; the `validity-change` event signals changes."},{name:"reset",signature:"(): void",description:"Resets the field to mount state: clears messages, withdraws `data-touched`/`data-dirty`, cancels pending runs, then re-derives `filled`, `count`, and over-limit error from the control. Async runs in flight are invalidated, not awaited. Clearing `errors` prop as part of reset is deliberate \u2014 it will re-apply on the next render since it is declarative."}]};export{a as fieldContract};
1
+ import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var r={name:"Field",purpose:"Wraps one form control and wires its label, description, and error text to it \u2014 generating ids and setting aria-describedby, aria-invalid, and aria-labelledby on the control \u2014 so any control inside a Field satisfies WCAG 3.3.1 and 3.3.3 without consumer-written ARIA. Owns field-level validation: validate callback, change/blur/submit modes, server-error injection, and native constraint-validation fallback.",props:[{name:"validate",type:"(value: unknown) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>",required:!1,description:"Custom validator returning one, several, or no error messages. May be async; superseded runs are discarded. Spelled `validate` in all five frameworks; the method is `checkValidity()` to avoid collision. **Property-only on `<skdx-field>`**: functions have no attribute form."},{name:"mode",type:"'change' | 'blur' | 'submit'",defaultValue:"'blur'",required:!1,description:"When validation first runs. Regardless of mode, a field that has already failed re-validates on every input so the error clears as soon as it is fixed."},{name:"errors",type:"string | string[]",required:!1,description:"Server-injected errors, always shown and listed before `validate` messages. Clearing the prop clears them. Unlike React Aria, keystroke alone does not clear them \u2014 a server error states a fact the client cannot recheck, so dropping it on input would silently hide an unsolved problem. A `reset()` clears them until the next render, since the prop is declarative."},{name:"value",type:"unknown",required:!1,description:"Value handed to `validate`. Omit to read the discovered control's `checked` (checkbox/radio) or `value` property. **Property-only on `<skdx-field>`**: values are rarely strings and don't serialize across the attribute boundary."},{name:"name",type:"string",required:!1,description:"Field name, used as the fallback link text in a Form's ErrorSummary."},{name:"required",type:"boolean",required:!1,description:'Whether an answer is mandatory. Set as the native `required` attribute on labelable controls, `aria-required="true"` on groups and widget-role elements. Render a RequiredIndicator inside the Label; its text joins the accessible name so the requirement is announced. No `data-required` hook \u2014 `[required]` and `[aria-required="true"]` are already selectors.'},{name:"disabled",type:"boolean",required:!1,description:'Whether the field is unavailable. Set as the native `disabled` attribute where available, `aria-disabled="true"` otherwise (widgets stay focusable). Mirrored as `data-disabled` on the Field and control. Both withdrawn only when the Field wrote them, so a consumer-set `disabled` survives untouched.'},{name:"getCharacterCountMessage",type:"(remaining: number) => string",required:!1,description:'Words all character-count copy: rendered count, announced pause message, and over-limit error. Negative remaining means over the limit. One prop for all three to keep announced and rendered counts in sync. Defaults to GOV.UK wording, e.g. "You have 12 characters remaining". **Property-only on `<skdx-field>`**: functions have no attribute form.'},{name:"invalidMessage",type:"string",required:!1,description:'Fallback message when the control fails native constraint validation but reports no message (jsdom, some browsers leave validationMessage empty). Browser messages are already localized; this one needs translating. Defaults to "This field is invalid.".'},{name:"validationDebounceTime",type:"number",defaultValue:"0",required:!1,description:"Milliseconds `mode: 'change'` waits after the last keystroke before running `validate`. Only change mode debounces; blur/submit re-validate already-failed fields undebounced. A focusout cancels pending runs and validates immediately, so blur still answers at once."},{name:"label",type:"string",required:!1,description:"Label text, which derives a Label part with RequiredIndicator inside it before the control. No prop, no element \u2014 labels are opt-in consumer text. A consumer-authored Label takes precedence, so both recipe and hand-composed anatomy work in the same Field."},{name:"description",type:"string",required:!1,description:"Help text, which derives a Description part after the control and adds its id to `aria-describedby`. No prop, no element \u2014 consumer-authored Description takes precedence."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."}],events:[{name:"validity-change",description:"Fires whenever the field's validity or interaction state changes. Bubbles, so Form's ErrorSummary can re-scan. `validity` is the control's live `ValidityState` (or null if none), letting consumers word error messages per constraint or style on which constraint failed.",detail:"{ valid: boolean; errors: string[]; touched: boolean; dirty: boolean; filled: boolean; count: number; validity: ValidityState | null }"}],parts:[{name:"Label",description:"The field's label. Renders a `<label>` with `for` pointing to the control, or `<legend>` in Fieldset mode (groups get `aria-labelledby` instead). Double-click focuses the control instead of selecting text."},{name:"Description",description:"Supplementary help text. Its id joins the control's `aria-describedby` so it is announced with the control.",props:[{name:"as",type:"PolymorphicTag",defaultValue:"'span'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular uses an attribute directive on the consumer's element \u2014 `<section skdx-ng-card>` \u2014 its idiomatic syntax. Eleven such directives predate this pattern.",description:"Element name to render instead of the default <span>, from the closed allowlist in POLYMORPHIC_TAGS. Tag name only, never a component \u2014 the list is closed because Lit needs a static-html literal per tag. React additionally reaches this through asChild, which wins when both are set."}]},{name:"Error",description:"The field's error text. Renders nothing while the field is valid; when it renders, its generated id joins the control's `aria-describedby` and the control gains `aria-invalid=\"true\"`. Renders current messages when given no children of its own. Children are content, not a render function; reach error details through the `validity-change` event's `ValidityState` instead.",props:[{name:"as",type:"PolymorphicTag",defaultValue:"'span'",required:!1,frameworks:["react","vue","svelte","webComponents"],exception:"as-directive",frameworksReason:"Angular uses an attribute directive on the consumer's element \u2014 `<section skdx-ng-card>` \u2014 its idiomatic syntax. Eleven such directives predate this pattern.",description:"Element name to render instead of the default <span>, from the closed allowlist in POLYMORPHIC_TAGS. Tag name only, never a component \u2014 the list is closed because Lit needs a static-html literal per tag. React additionally reaches this through asChild, which wins when both are set."}]},{name:"CharacterCount",description:"A running character count \u2014 GOV.UK's pattern, works for Textarea and TextField alike. maxLength is a soft limit and is deliberately not the control's native maxlength, which truncates silently; exceeding it makes the Field invalid (data-invalid, aria-invalid on the control, a message in Error, blocked submit) and adds data-over-limit here and on the Field. Count is announced one typing pause (1000ms) after the last keystroke, not continuously. Pass your own copy to override generated text.\n\n**Primitives-only in all five packages; the Field root's recipe never derives one.** maxLength is consumer-provided, so `maxLength` and `threshold` are declared on this part and never on the Field root.",props:[{name:"maxLength",type:"number",required:!0,description:"The soft limit the count counts down to."},{name:"threshold",type:"number",defaultValue:"0",required:!1,description:"Percentage of maxLength before the count renders or announces."}]},{name:"RequiredIndicator",description:"The \"mandatory\" marker, rendered *inside* the Label while Field is `required`. Its placement matters: inside the label means its text joins the control's accessible name so the requirement is announced. Deliberately not `aria-hidden` \u2014 hiding it leaves it visible to users who don't need it announced and silent for those who do. Defaults to `*`; pass words for more explicit marking."},{name:"Fieldset",description:'A Field rendering `<fieldset>` with Label in `<legend>` \u2014 the grouping radio/checkbox groups need, since `<label for>` cannot reference `[role="radiogroup"]`; the group gets `aria-labelledby` instead. Same props, same validation model and states as Field root.\n\n**Public and headless in all five packages, deliberately not a recipe.** Field root takes `label`/`description` and derives the parts; Fieldset derives nothing. It is the advanced path \u2014 the consumer composes Label, controls and Error inside it. A recipe Field can sit inside a hand-composed Fieldset.',props:[{name:"validate",type:"(value: unknown) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>",required:!1,description:'The group\'s validator, same shape and async handling as the root\'s. Spelled on Fieldset rather than inherited because group rules are those no single control can state \u2014 "pick at least one", "end date must not precede start". Named `validate` so the method is `checkValidity()`, avoiding collision. **Property-only on `<skdx-fieldset>`** \u2014 functions have no attribute form.'},{name:"mode",type:"'change' | 'blur' | 'submit'",defaultValue:"'blur'",required:!1,description:"When validation first runs for the group. Groups that have failed re-validate on every input regardless of mode."},{name:"errors",type:"string | string[]",required:!1,description:"Server-injected errors for the group, always shown and listed before `validate` messages. Clearing the prop clears them."},{name:"name",type:"string",required:!1,description:"Group name, fallback link text in Form's ErrorSummary when the group renders no error."},{name:"invalidMessage",type:"string",required:!1,description:'Fallback message when the group control fails native constraint validation but reports no message. Browser messages always win \u2014 they are already localized. Defaults to "This field is invalid.".'},{name:"value",type:"unknown",required:!1,description:"Value handed to the group's `validate`. Omit to read the group control's value \u2014 for a radiogroup or checkbox group, whatever its form-participation input would submit."},{name:"required",type:"boolean",required:!1,description:'Whether an answer is mandatory for the group. Groups are always `aria-required="true"` since `[role="radiogroup"]` has no native `required`.'},{name:"disabled",type:"boolean",required:!1,description:"Whether the group is unavailable. Set as the native `disabled` attribute on the rendered `<fieldset>`, which cascades to every native control it contains. Mirrored as `data-disabled` on the Fieldset root."},{name:"getCharacterCountMessage",type:"(remaining: number) => string",required:!1,description:"Words the character-count copy for the group, exactly as on the Field root. Declared on the Fieldset too because dropping it would put announced and rendered counts in different languages."},{name:"validationDebounceTime",type:"number",defaultValue:"0",required:!1,description:"Milliseconds `mode: 'change'` waits after the last input in the group before running `validate`. Group validators receive the whole answer set, so in change mode they do real work \u2014 cross-field comparisons, lookups \u2014 on every keystroke. A focusout cancels pending runs and validates immediately; already-failed groups re-validate undebounced."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the Fieldset; the data-density attribute may be set directly instead."}],methods:[{name:"checkValidity",signature:"(): Promise<boolean>",description:"Runs the group's validation now and resolves to whether it is valid. Form's `trySubmit()` calls this on every Fieldset and Field."},{name:"reset",signature:"(): void",description:"Clears the group's messages and its `touched`/`dirty` state."}]}],slots:[{name:"default",description:"A Label, the control, an optional Description, and an Error."}],appearance:[{attribute:"data-density",values:["compact"],stylesheet:"field.css"}],states:["valid","invalid","dirty","touched","filled","over-limit","required","disabled"],controlledUncontrolled:"The Field never owns the control's value; only validation state. Validation is uncontrolled; `errors` injects state from outside without claiming the rest.",formBehavior:"Not a form control itself \u2014 adds no value to submission and renders no input. Listens (capture-phase) for the host form's `submit` and prevents it while invalid or validation is pending. Async validators block the initial submit; the last pending Field to settle re-dispatches through `requestSubmit()` with `data-skdx-field-resubmit` on the form, and Fields answer from computed state instead of re-running.",validationBehavior:'Four merged sources in order: `errors` (server), the `validate` callback, the native `validationMessage` of the discovered control when `ValidityState` is invalid (so `required`/`type="email"`/`pattern` work without config), and CharacterCount over `maxLength`. Native message is *appended* to the first two, de-duplicated on exact string match, never substituted \u2014 a control that is both `required` and failing a custom rule must report both. `invalidMessage` is the fallback where the engine reports empty `validationMessage`. `mode` decides when the first run happens; `validationDebounceTime` controls `change` mode\'s wait after the last keystroke. After failure, every input re-validates undebounced. Newly-raised errors are announced assertively through the live announcer \u2014 silent on a failed submit only when an ErrorSummary renders.',accessibility:{role:"generic (a `<div>`, or a `<fieldset>` in group mode)",keyboard:[],ariaAttributes:["aria-describedby on the control \u2014 Description id and Error id while invalid",'aria-invalid="true" on the control while invalid',"aria-labelledby on the control in group mode, merging the legend id into whatever exists \u2014 the Field adds and withdraws its own id, never replacing it","required/aria-required and disabled/aria-disabled on the control \u2014 the native attribute where the control has one, the ARIA state where it does not","Every id the Field writes points at a rendered part. Label `for` and control `id` appear as a pair or not at all \u2014 so server renders with no control emit neither rather than a broken link.",'aria-errormessage is deliberately **not** used; Error is routed through `aria-describedby` instead. An `aria-errormessage` target is only exposed while the element carries `aria-invalid="true"` (a rendering-order race condition). Support is inconsistent; the Error is already in `aria-describedby`, where every screen reader reads it. Consumers who want it can set it on their own control \u2014 the Field merges into `aria-describedby` and never touches `aria-errormessage`.'],focusManagement:"Field moves no focus. Form ErrorSummary focuses failing fields, and for groups, the first control inside (GOV.UK rule).",namingRequirement:"Label supplies the accessible name via `for`/`id` (single control) or `aria-labelledby` (group). RequiredIndicator inside the Label is part of that name, making the requirement audible."},styling:{cssParts:["field","label","legend","required-indicator","description","error","character-count"],dataAttributes:[...e,"data-skdx-field","data-skdx-field-name","data-skdx-field-message","data-valid","data-invalid","data-touched","data-dirty","data-filled","data-over-limit","data-disabled","data-skdx-field-required-indicator","data-skdx-field-character-count","data-skdx-field-control","data-skdx-field-description","data-skdx-field-error","data-skdx-field-pending","data-skdx-field-resubmit",{name:"data-skdx-field-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'},"data-orientation","data-density"],cssCustomProperties:[]},frameworkExtensions:[{framework:"angular",name:"control",type:"AbstractControl",description:'An Angular Reactive Forms control to mirror. Its `errors` are mapped through `errorMessages` and merged ahead of `validate` messages; `touched`/`dirty` are replayed as `input`/`focusout` events the engine listens for. Angular-only because Reactive Forms exists nowhere else \u2014 other frameworks reach this place via `errors` and `value` props. Mirroring is deliberately one-way and one-shot per transition \u2014 it can set `touched` and `dirty`, never clear them, because it works by replaying DOM events and there is no event that means "untouched". Clearing those flags is `reset()`\'s job: it withdraws `data-touched` and `data-dirty` and returns the field to mount state, so `markAsUntouched()` on the control pairs with a `reset()` call rather than travelling through the mirror.'},{framework:"angular",name:"errorMessages",type:"Record<string, string>",description:"Maps a Reactive Forms error key (`required`, `minlength`, \u2026) to the message shown for it; unmapped keys are shown verbatim. Meaningless without `control`, and Angular-only because the key space is Angular's validators', not this library's."},{framework:"angular",part:"Fieldset",name:"control",type:"AbstractControl",description:"An Angular Reactive Forms control to mirror: its `errors` are mapped\nthrough errorMessages and merged into this field's messages, and\nits `touched`/`dirty` are replayed onto the Field. Bind it and the Field\nagrees with Angular's own state instead of keeping a second copy. This part mirrors Angular Reactive Forms control state and validator messages. Other frameworks have no Angular AbstractControl; they use the shared errors/validation and value APIs.",kind:"prop"},{framework:"angular",part:"Fieldset",name:"errorMessages",type:"Record<string, string>",description:"Maps a Reactive Forms error key (`required`, `minlength`, \u2026) to the\nmessage to show for it. Unmapped keys are shown verbatim. This part mirrors Angular Reactive Forms control state and validator messages. Other frameworks have no Angular AbstractControl; they use the shared errors/validation and value APIs.",kind:"prop"},{framework:"webComponents",part:"Fieldset",name:"state",type:"FieldSnapshot",description:"The full current state: `{ controlId, errors, valid, touched, dirty, filled }`. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings.",kind:"prop"},{framework:"webComponents",name:"state",type:"FieldSnapshot",description:"The full current state: `{ controlId, errors, valid, touched, dirty, filled }`. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings.",kind:"prop"},{framework:"angular",part:"Fieldset",name:"control",type:"AbstractControl",description:"An Angular Reactive Forms control to mirror: its `errors` are mapped\nthrough errorMessages and merged into this field's messages, and\nits `touched`/`dirty` are replayed onto the Field. Bind it and the Field\nagrees with Angular's own state instead of keeping a second copy. This part mirrors Angular Reactive Forms control state and validator messages. Other frameworks have no Angular AbstractControl; they use the shared errors/validation and value APIs."},{framework:"angular",part:"Fieldset",name:"errorMessages",type:"Record<string, string>",description:"Maps a Reactive Forms error key (`required`, `minlength`, \u2026) to the\nmessage to show for it. Unmapped keys are shown verbatim. This part mirrors Angular Reactive Forms control state and validator messages. Other frameworks have no Angular AbstractControl; they use the shared errors/validation and value APIs."},{framework:"webComponents",part:"Fieldset",name:"state",type:"FieldSnapshot",description:"The full current state: `{ controlId, errors, valid, touched, dirty, filled }`. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings."},{framework:"webComponents",name:"state",type:"FieldSnapshot",description:"The full current state: `{ controlId, errors, valid, touched, dirty, filled }`. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings."}],publicMethods:[{name:"checkValidity",signature:"(): Promise<boolean>",description:"Runs validation now and resolves to whether the field is valid, reached through the component instance or element. Named for the platform's own method to avoid collision with the `validate` prop. Returns a promise since validators may be async; the `validity-change` event signals changes."},{name:"reset",signature:"(): void",description:"Resets the field to mount state: clears messages, withdraws `data-touched`/`data-dirty`, cancels pending runs, then re-derives `filled`, `count`, and over-limit error from the control. Async runs in flight are invalidated, not awaited. Clearing `errors` prop as part of reset is deliberate \u2014 it will re-apply on the next render since it is declarative."}]};export{r as fieldContract};
@@ -1 +1 @@
1
- import{changeDetail as e}from"./change-reason.js";import{constraintValidationMethods as t,setCustomValidityMethod as i}from"./form-validity.js";import{ROOT_DATA_ATTRIBUTES as a}from"./styling-hooks.js";var s={name:"FileUpload",purpose:"A controllable File[] list. Dropzone and trigger open a visually-hidden native input; drag-and-drop applies the same accept pattern as the picker.",props:[{name:"value",type:"File[]",required:!1,description:"Selected files for controlled usage. PROPERTY-ONLY on <skdx-file-upload>: File[] cannot serialize through HTML attributes."},{name:"defaultValue",type:"File[]",defaultValue:"[]",required:!1,description:"Initial files for uncontrolled usage; form reset restores this list. PROPERTY-ONLY on <skdx-file-upload>: needed so uncontrolled mode has a seed and resets restore the initial state, not always empty."},{name:"accept",type:"string",required:!1,description:"Comma-separated file-type filter (extensions or MIME patterns), applied to picker and drag-and-drop alike."},{name:"multiple",type:"boolean",defaultValue:"false",required:!1,description:"Allow selecting/dropping more than one file; false replaces the existing file on new selection."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables the dropzone, trigger, and file input."},{name:"name",type:"string",required:!1,description:"Form field name. Every selected file is submitted as its own entry under it; without it nothing is submitted."},{name:"form",type:"string | undefined",required:!1,defaultValue:"undefined",description:"Form id when FileUpload lives outside its form. **Web Components:** property is `formId` (not `form`, which is read-only per spec)."},{name:"required",type:"boolean",defaultValue:"false",required:!1,description:"Marks the field required. valueMissing while no file is selected."},{name:"readOnly",type:"boolean",defaultValue:"false",required:!1,description:"Shows selection but blocks changes: dropzone refuses interaction, trigger is inert, ItemDelete disabled. Unlike disabled, the list stays focusable and readable \u2014 dropzone keeps tab stop and announces aria-disabled (not aria-readonly, unsupported on button role)."},{name:"invalid",type:"boolean",defaultValue:"false",required:!1,description:'External invalidity flag (server rejection, cross-field rules). Reflected as data-invalid on dropzone, ORed with derived invalidity (required with no files). Not aria-invalid\u2014role="button" inherits and deprecates it; see ariaAttributes for accessible signalling.'},{name:"maxFiles",type:"number",required:!1,description:"Most files the list may hold. Files past the cap are rejected with `too-many-files` instead of being added. Ignored while `multiple` is false, which caps the list at 1 anyway."},{name:"maxFileSize",type:"number",required:!1,description:"Largest accepted file size in bytes; a bigger file is rejected with `file-too-large`."},{name:"minFileSize",type:"number",required:!1,description:"Smallest accepted file size in bytes; a smaller file is rejected with `file-too-small`."},{name:"removeLabel",type:"(fileName: string) => string",defaultValue:"(fileName) => `Remove ${fileName}`",required:!1,description:"Words every ItemDelete's accessible name from the name of the file it removes. Declared once on the root rather than per row so the wording \u2014 and its translation \u2014 lives in one place; a consumer's own aria-label on a delete button still wins over it."},{name:"validator",type:"(file: File) => string[] | null | undefined",required:!1,description:"Per-file check run after the built-in ones. Return error codes to reject the file, or null/undefined/[] to accept it. Codes are opaque strings, merged into the same `file-reject` errors array as the built-in codes. PROPERTY-ONLY on <skdx-file-upload>: a function cannot round-trip through an HTML attribute."},{name:"directory",type:"boolean",defaultValue:"false",required:!1,description:"Lets the picker select a whole directory, forwarded to the hidden input as `webkitdirectory`. Named for the capability rather than for the attribute, because the attribute is prefixed and specified nowhere: it is supported in Chromium, Firefox and Safari, and a browser without it simply opens an ordinary file picker, so this is browser-dependent by construction. Every file in the chosen directory arrives through the normal validation pipeline, so accept and the size bounds still apply."},{name:"capture",type:"'user' | 'environment'",required:!1,description:"'user' (front camera) or 'environment' (rear). Mobile only; desktop falls back to file picker."},{name:"preventDocumentDrop",type:"boolean",defaultValue:"true",required:!1,description:'Suppress browser open-on-drop outside mounted dropzones. Enabled by default (misses cause destructive navigation). Uses document dragover+drop listeners, exempting `[data-part="dropzone"]`.'},{name:"showList",type:"boolean",defaultValue:"true",required:!1,description:"Whether the derived anatomy includes an ItemGroup listing the selected files. Ignored under hand composition."},{name:"showPreview",type:"boolean",defaultValue:"false",required:!1,description:"Whether a derived Item's default row includes an ItemPreview. Ignored under hand composition, and by renderFile when supplied."},{name:"fileStatus",type:"(file: File) => { status?: FileUploadItemStatus; progress?: number } | undefined",required:!1,description:"Per-file upload state: keyed by File (not index). Surfaces as data-state/data-progress. Consumer owns transport and this callback. Ignored with hand composition."},{name:"renderFile",type:"(file: File) => React.ReactNode",required:!1,description:`Render row content. Route for accessible progress readout: compose role="progressbar" or aria-describedby text when status='uploading'.`},{name:"requiredMessage",type:"string",required:!1,description:"Override valueMissing message so it can be translated. Message is unreachable otherwise: Web Components hides it in a shadow root; others use visually-hidden inputs. Yields to consumer's setCustomValidity."}],events:[{name:"value-change",detail:e("File[]"),description:"Fires with the new file list whenever files are added or removed."},{name:"file-reject",description:"Fires once per rejected file before committed batch. ORDERING IS PART OF THE CONTRACT: every file-reject fires BEFORE the single value-change. Consumer retains rejections; no component state. Batch with all rejects fires no value-change.",detail:"{ file: File; errors: string[] }"}],parts:[{name:"Dropzone",description:'Drag-and-drop / click-to-browse target with role="button", data-dragging, data-disabled. Trigger is its *sibling*, not child.',props:[{name:"disableClick",type:"boolean",defaultValue:"false",required:!1,description:"Suppresses the dropzone's own click-to-open behaviour, leaving drag-and-drop and Enter/Space activation untouched. For layouts where a sibling Trigger is the only thing that should open the picker, so a stray click on the surrounding drop surface does nothing."}]},{name:"Label",description:'Names the Dropzone, which points aria-labelledby at it while it is mounted. The Dropzone is a role="button" and so takes its name from its own content too \u2014 this part is for the layouts where the visible label sits outside the drop surface. Rendered as a span rather than a <label>, which would associate itself with the hidden file input instead of the element carrying the button role.'},{name:"ItemGroup",description:"The list wrapper the Item rows sit in. Pure structure \u2014 it renders no content of its own and reads no state \u2014 so a file list can be laid out as a list rather than as a run of siblings, and so the skin has one element to hang the column gap on. Emits the `item-group` part."},{name:"Item",description:"A single file's row, addressed by the `File` it shows. Consumer owns per-file status (data-state/data-progress). Publishes file to nested parts.",props:[{name:"file",type:"File",required:!0,description:"File this row shows; stable identity for nested parts. Compared by identity, not name. PROPERTY-ONLY on <skdx-file-upload-item>."},{name:"status",type:"'idle' | 'uploading' | 'complete' | 'error'",defaultValue:"'idle'",required:!1,description:"This file's upload status, set by the consumer that owns the transport. Reflected as `data-state` \u2014 the attribute a component emits its own lifecycle on; `data-status` is the separate severity scale other families expose as a prop."},{name:"progress",type:"number",required:!1,description:'Upload progress 0-100, consumer-set. Reflected as `data-progress`. STYLING HOOK AND NOTHING ELSE: when status="uploading", also provide role="progressbar" or aria-describedby for a11y.'}]},{name:"ItemName",description:"The file's name as readable text, taken from the enclosing Item's file. Content rather than decoration: the name is the only thing that distinguishes one row from the next, so it has to be text in the accessibility tree and not a background image or a `::before`. Children override it, for the layouts that truncate or highlight the name themselves; with no children it renders `file.name`."},{name:"ItemSizeText",description:"The file's size as readable text \u2014 `999B`, `1.5kB`, `1.2MB` \u2014 taken from the enclosing Item's file. A raw `File.size` is a byte count, which tells a reader nothing and is read out digit by digit by a screen reader, so this part formats it through Intl.NumberFormat's unit style: the decimal separator, the grouping and the unit abbreviation all come from the viewer's locale. `formatFileSize` in @skdx/shared is the written-down version the five copies are held to. Children override it."},{name:"ItemPreview",description:"The slot for a thumbnail or a file-type icon, in the enclosing Item. A container and nothing else: it renders no image itself, because the only way to show one is an object URL, and an object URL that a component creates is one it also has to revoke \u2014 a lifetime this component cannot know. Compose an <img> whose src the consumer owns, and give it the alt text (usually empty, since ItemName already carries the name)."},{name:"ItemDelete",description:"Delete button, named by file and removeLabel. Moves focus to next/previous delete button before removing its row, or to Dropzone when empty.",props:[{name:"file",type:"File",required:!1,defaultValue:"the enclosing Item's file",description:"File to remove, compared by identity. Defaults to enclosing Item's file. Optional; PROPERTY-ONLY on <skdx-file-upload-item-delete>."}]},{name:"Clear",description:"The button that empties the whole list in one action, firing a single value-change with an empty list and announcing how many files went. Disabled while the list is already empty, and while the root is disabled or readOnly \u2014 the same three conditions ItemDelete answers to. A common need for any multi-file upload; it is a part rather than a root prop because where the control sits, and whether it exists at all, is a layout decision."}],slots:[{name:"default",description:"FileUploadLabel/Dropzone/Trigger/Input plus FileUploadItem rows."},{name:"item",description:"A single selected file's row."}],states:["default","dragging","disabled","readonly","invalid","idle","uploading","complete","error"],controlledUncontrolled:"Supports both: value + onValueChange (controlled) or defaultValue (uncontrolled).",formBehavior:'Form-associated: each file is a separate form entry, so formData.getAll(name) returns the File objects. Empty is valueMissing when required, surfaced on Dropzone data-invalid and aria-describedby description. Form reset restores the files the component was created with (defaultValue) and fires value-change; does not simply clear the list. React/Vue/Svelte/Angular use two visually-hidden controls: one carries value via DataTransfer, another carries required. Split because input.files only binds imperatively. Neither may be type="hidden" or readonly (bars constraint validation). Web Components uses shadow root input and ElementInternals.',validationBehavior:"required is native constraint validation (checkValidity/reportValidity). Selection-time validation is separate, never blocks: files checked against accept/maxFileSize/minFileSize/maxFiles/validator callback; failures drop with file-reject carrying codes (file-invalid-type | file-too-large | file-too-small | too-many-files). Consumer renders rejections; Field owns persistent message.",accessibility:{role:"button (FileUploadDropzone)",keyboard:[{key:"Enter/Space",behavior:"Opens the native file picker when the dropzone is focused."}],ariaAttributes:['aria-disabled on dropzone when disabled or readOnly (role="button" unsupports aria-readonly).',"aria-labelledby on dropzone, pointing at Label part when mounted.","aria-label on each ItemDelete, from removeLabel and file name.",'aria-describedby on the dropzone while required, pointing at visually-hidden description: "Required" when satisfied, "Required. No file selected" when not. This is the sole accessible signal for both required-ness and invalidity. aria-required is global in no version of ARIA; aria-invalid is global but deprecated on this role in ARIA 1.2.',"Live announcements: batch additions/rejections, and each removal."],namingRequirement:'Dropzone MUST have an accessible name (only visible part to a11y). Role="button" takes name from content or Label part. Each ItemDelete named from its file. Tests enforce both.',focusManagement:"Dropzone is keyboard-activatable button; hidden input never focused. Delete moves focus to next/prev ItemDelete, then Dropzone, before row unmounts."},styling:{cssParts:["file-upload","dropzone","label","trigger","item","delete","input","item-delete","item-group","item-name","item-size","item-preview","clear"],dataAttributes:[...a,"data-disabled","data-readonly","data-dragging","data-invalid","data-state","data-progress",{name:"data-skdx-file-upload-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[...t,i,{name:"addFiles",signature:"(files: FileList | File[]): void",description:"Route external sources (paste, drag library, camera, test) through validation and announcement pipeline. NOT a paste listener: component paste handler would compete with all others on the page."}]};export{s as fileUploadContract};
1
+ import{changeDetail as e}from"./change-reason.js";import{constraintValidationMethods as t,setCustomValidityMethod as i}from"./form-validity.js";import{ROOT_DATA_ATTRIBUTES as a}from"./styling-hooks.js";var s={name:"FileUpload",purpose:"A controllable File[] list. Dropzone and trigger open a visually-hidden native input; drag-and-drop applies the same accept pattern as the picker.",props:[{name:"value",type:"File[]",required:!1,description:"Selected files for controlled usage. PROPERTY-ONLY on <skdx-file-upload>: File[] cannot serialize through HTML attributes."},{name:"defaultValue",type:"File[]",defaultValue:"[]",required:!1,description:"Initial files for uncontrolled usage; form reset restores this list. PROPERTY-ONLY on <skdx-file-upload>: needed so uncontrolled mode has a seed and resets restore the initial state, not always empty."},{name:"accept",type:"string",required:!1,description:"Comma-separated file-type filter (extensions or MIME patterns), applied to picker and drag-and-drop alike."},{name:"multiple",type:"boolean",defaultValue:"false",required:!1,description:"Allow selecting/dropping more than one file; false replaces the existing file on new selection."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Disables the dropzone, trigger, and file input."},{name:"name",type:"string",required:!1,description:"Form field name. Every selected file is submitted as its own entry under it; without it nothing is submitted."},{name:"form",type:"string | undefined",required:!1,defaultValue:"undefined",description:"Form id when FileUpload lives outside its form. **Web Components:** property is `formId` (not `form`, which is read-only per spec)."},{name:"required",type:"boolean",defaultValue:"false",required:!1,description:"Marks the field required. valueMissing while no file is selected."},{name:"readOnly",type:"boolean",defaultValue:"false",required:!1,description:"Shows selection but blocks changes: dropzone refuses interaction, trigger is inert, ItemDelete disabled. Unlike disabled, the list stays focusable and readable \u2014 dropzone keeps tab stop and announces aria-disabled (not aria-readonly, unsupported on button role)."},{name:"invalid",type:"boolean",defaultValue:"false",required:!1,description:'External invalidity flag (server rejection, cross-field rules). Reflected as data-invalid on dropzone, ORed with derived invalidity (required with no files). Not aria-invalid\u2014role="button" inherits and deprecates it; see ariaAttributes for accessible signalling.'},{name:"maxFiles",type:"number",required:!1,description:"Most files the list may hold. Files past the cap are rejected with `too-many-files` instead of being added. Ignored while `multiple` is false, which caps the list at 1 anyway."},{name:"maxFileSize",type:"number",required:!1,description:"Largest accepted file size in bytes; a bigger file is rejected with `file-too-large`."},{name:"minFileSize",type:"number",required:!1,description:"Smallest accepted file size in bytes; a smaller file is rejected with `file-too-small`."},{name:"removeLabel",type:"(fileName: string) => string",defaultValue:"(fileName) => `Remove ${fileName}`",required:!1,description:"Words every ItemDelete's accessible name from the name of the file it removes. Declared once on the root rather than per row so the wording \u2014 and its translation \u2014 lives in one place; a consumer's own aria-label on a delete button still wins over it."},{name:"validator",type:"(file: File) => string[] | null | undefined",required:!1,description:"Per-file check run after the built-in ones. Return error codes to reject the file, or null/undefined/[] to accept it. Codes are opaque strings, merged into the same `file-reject` errors array as the built-in codes. PROPERTY-ONLY on <skdx-file-upload>: a function cannot round-trip through an HTML attribute."},{name:"directory",type:"boolean",defaultValue:"false",required:!1,description:"Lets the picker select a whole directory, forwarded to the hidden input as `webkitdirectory`. Named for the capability rather than for the attribute, because the attribute is prefixed and specified nowhere: it is supported in Chromium, Firefox and Safari, and a browser without it simply opens an ordinary file picker, so this is browser-dependent by construction. Every file in the chosen directory arrives through the normal validation pipeline, so accept and the size bounds still apply."},{name:"capture",type:"'user' | 'environment'",required:!1,description:"'user' (front camera) or 'environment' (rear). Mobile only; desktop falls back to file picker."},{name:"preventDocumentDrop",type:"boolean",defaultValue:"true",required:!1,description:'Suppress browser open-on-drop outside mounted dropzones. Enabled by default (misses cause destructive navigation). Uses document dragover+drop listeners, exempting `[data-part="dropzone"]`.'},{name:"showList",type:"boolean",defaultValue:"true",required:!1,description:"Whether the derived anatomy includes an ItemGroup listing the selected files. Ignored under hand composition."},{name:"showPreview",type:"boolean",defaultValue:"false",required:!1,description:"Whether a derived Item's default row includes an ItemPreview. Ignored under hand composition, and by renderFile when supplied."},{name:"fileStatus",type:"(file: File) => { status?: FileUploadItemStatus; progress?: number } | undefined",required:!1,description:"Per-file upload state: keyed by File (not index). Surfaces as data-state/data-progress. Consumer owns transport and this callback. Ignored with hand composition."},{name:"renderFile",type:"(file: File) => React.ReactNode",required:!1,description:`Render row content. Route for accessible progress readout: compose role="progressbar" or aria-describedby text when status='uploading'.`},{name:"requiredMessage",type:"string",required:!1,description:"Override valueMissing message so it can be translated. Message is unreachable otherwise: Web Components hides it in a shadow root; others use visually-hidden inputs. Yields to consumer's setCustomValidity."}],events:[{name:"value-change",detail:e("File[]"),description:"Fires with the new file list whenever files are added or removed."},{name:"file-reject",description:"Fires once per rejected file before committed batch. ORDERING IS PART OF THE CONTRACT: every file-reject fires BEFORE the single value-change. Consumer retains rejections; no component state. Batch with all rejects fires no value-change.",detail:"{ file: File; errors: string[] }"}],parts:[{name:"Dropzone",description:'Drag-and-drop / click-to-browse target with role="button", data-dragging, data-disabled. Trigger is its *sibling*, not child.',props:[{name:"disableClick",type:"boolean",defaultValue:"false",required:!1,description:"Suppresses the dropzone's own click-to-open behaviour, leaving drag-and-drop and Enter/Space activation untouched. For layouts where a sibling Trigger is the only thing that should open the picker, so a stray click on the surrounding drop surface does nothing."}]},{name:"Label",description:'Names the Dropzone, which points aria-labelledby at it while it is mounted. The Dropzone is a role="button" and so takes its name from its own content too \u2014 this part is for the layouts where the visible label sits outside the drop surface. Rendered as a span rather than a <label>, which would associate itself with the hidden file input instead of the element carrying the button role.'},{name:"ItemGroup",description:"The list wrapper the Item rows sit in. Pure structure \u2014 it renders no content of its own and reads no state \u2014 so a file list can be laid out as a list rather than as a run of siblings, and so the skin has one element to hang the column gap on. Emits the `item-group` part."},{name:"Item",description:"A single file's row, addressed by the `File` it shows. Consumer owns per-file status (data-state/data-progress). Publishes file to nested parts.",props:[{name:"file",type:"File",required:!0,description:"File this row shows; stable identity for nested parts. Compared by identity, not name. PROPERTY-ONLY on <skdx-file-upload-item>."},{name:"status",type:"'idle' | 'uploading' | 'complete' | 'error'",defaultValue:"'idle'",required:!1,description:"This file's upload status, set by the consumer that owns the transport. Reflected as `data-state` \u2014 the attribute a component emits its own lifecycle on; `data-status` is the separate severity scale other families expose as a prop."},{name:"progress",type:"number",required:!1,description:'Upload progress 0-100, consumer-set. Reflected as `data-progress`. STYLING HOOK AND NOTHING ELSE: when status="uploading", also provide role="progressbar" or aria-describedby for a11y.'}],slots:[{name:"default",description:"A single selected file's row."}]},{name:"ItemName",description:"The file's name as readable text, taken from the enclosing Item's file. Content rather than decoration: the name is the only thing that distinguishes one row from the next, so it has to be text in the accessibility tree and not a background image or a `::before`. Children override it, for the layouts that truncate or highlight the name themselves; with no children it renders `file.name`."},{name:"ItemSizeText",description:"The file's size as readable text \u2014 `999B`, `1.5kB`, `1.2MB` \u2014 taken from the enclosing Item's file. A raw `File.size` is a byte count, which tells a reader nothing and is read out digit by digit by a screen reader, so this part formats it through Intl.NumberFormat's unit style: the decimal separator, the grouping and the unit abbreviation all come from the viewer's locale. `formatFileSize` in @skdx/shared is the written-down version the five copies are held to. Children override it."},{name:"ItemPreview",description:"The slot for a thumbnail or a file-type icon, in the enclosing Item. A container and nothing else: it renders no image itself, because the only way to show one is an object URL, and an object URL that a component creates is one it also has to revoke \u2014 a lifetime this component cannot know. Compose an <img> whose src the consumer owns, and give it the alt text (usually empty, since ItemName already carries the name)."},{name:"ItemDelete",description:"Delete button, named by file and removeLabel. Moves focus to next/previous delete button before removing its row, or to Dropzone when empty.",props:[{name:"file",type:"File",required:!1,defaultValue:"the enclosing Item's file",description:"File to remove, compared by identity. Defaults to enclosing Item's file. Optional; PROPERTY-ONLY on <skdx-file-upload-item-delete>."}]},{name:"Clear",description:"The button that empties the whole list in one action, firing a single value-change with an empty list and announcing how many files went. Disabled while the list is already empty, and while the root is disabled or readOnly \u2014 the same three conditions ItemDelete answers to. A common need for any multi-file upload; it is a part rather than a root prop because where the control sits, and whether it exists at all, is a layout decision."}],slots:[{name:"default",description:"FileUploadLabel/Dropzone/Trigger/Input plus FileUploadItem rows."},{name:"render-file",frameworks:["svelte"],exception:"render-prop",frameworksReason:"svelte expose render-file as native content projection for the renderFile rendering capability. Other frameworks pass the documented renderFile render prop or template instead of a named slot with this spelling.",description:`Render row content. Route for accessible progress readout: compose role="progressbar" or aria-describedby text when status='uploading'.`}],states:["default","dragging","disabled","readonly","invalid","idle","uploading","complete","error"],controlledUncontrolled:"Supports both: value + onValueChange (controlled) or defaultValue (uncontrolled).",formBehavior:'Form-associated: each file is a separate form entry, so formData.getAll(name) returns the File objects. Empty is valueMissing when required, surfaced on Dropzone data-invalid and aria-describedby description. Form reset restores the files the component was created with (defaultValue) and fires value-change; does not simply clear the list. React/Vue/Svelte/Angular use two visually-hidden controls: one carries value via DataTransfer, another carries required. Split because input.files only binds imperatively. Neither may be type="hidden" or readonly (bars constraint validation). Web Components uses shadow root input and ElementInternals.',validationBehavior:"required is native constraint validation (checkValidity/reportValidity). Selection-time validation is separate, never blocks: files checked against accept/maxFileSize/minFileSize/maxFiles/validator callback; failures drop with file-reject carrying codes (file-invalid-type | file-too-large | file-too-small | too-many-files). Consumer renders rejections; Field owns persistent message.",accessibility:{role:"button (FileUploadDropzone)",keyboard:[{key:"Enter/Space",behavior:"Opens the native file picker when the dropzone is focused."}],ariaAttributes:['aria-disabled on dropzone when disabled or readOnly (role="button" unsupports aria-readonly).',"aria-labelledby on dropzone, pointing at Label part when mounted.","aria-label on each ItemDelete, from removeLabel and file name.",'aria-describedby on the dropzone while required, pointing at visually-hidden description: "Required" when satisfied, "Required. No file selected" when not. This is the sole accessible signal for both required-ness and invalidity. aria-required is global in no version of ARIA; aria-invalid is global but deprecated on this role in ARIA 1.2.',"Live announcements: batch additions/rejections, and each removal."],namingRequirement:'Dropzone MUST have an accessible name (only visible part to a11y). Role="button" takes name from content or Label part. Each ItemDelete named from its file. Tests enforce both.',focusManagement:"Dropzone is keyboard-activatable button; hidden input never focused. Delete moves focus to next/prev ItemDelete, then Dropzone, before row unmounts."},styling:{cssParts:["file-upload","dropzone","label","trigger","item","delete","input","item-delete","item-group","item-name","item-size","item-preview","clear"],dataAttributes:[...a,"data-disabled","data-readonly","data-dragging","data-invalid","data-state","data-progress",{name:"data-skdx-file-upload-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[...t,i,{name:"addFiles",signature:"(files: FileList | File[]): void",description:"Route external sources (paste, drag library, camera, test) through validation and announcement pipeline. NOT a paste listener: component paste handler would compete with all others on the page."}]};export{s as fileUploadContract};
@@ -1 +1 @@
1
- import{dismissalEvents as t,forceMountProp as a,openChangeCompleteEvent as i,portalPart as e}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as r}from"./styling-hooks.js";var s={name:"FloatingPanel",purpose:"A non-modal window: draggable by its header, resizable from eight grips, switchable between default/minimized/maximized stages, and clamped to a boundary or viewport. Position and size are published as CSS custom properties, so the consumer owns the painting.",props:[{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage."},{name:"position",type:"{ x: number; y: number }",required:!1,description:"Controlled top-left corner in CSS pixels, measured from the boundary's top-left. Pair with position-change or the panel will refuse to move."},{name:"defaultPosition",type:"{ x: number; y: number }",defaultValue:"{ x: 0, y: 0 }",required:!1,description:"Starting top-left corner for uncontrolled usage, measured from the boundary."},{name:"size",type:"{ width: number; height: number }",required:!1,description:"Controlled box in CSS pixels. Pair with size-change or the panel will refuse to resize."},{name:"defaultSize",type:"{ width: number; height: number }",defaultValue:"{ width: 320, height: 240 }",required:!1,description:"Starting box for uncontrolled usage."},{name:"minSize",type:"{ width: number; height: number }",defaultValue:"{ width: 160, height: 120 }",required:!1,description:"Smallest box the panel can be resized to. Every resize is clamped to it."},{name:"maxSize",type:"{ width: number; height: number }",defaultValue:"{ width: Infinity, height: Infinity }",required:!1,description:"Largest box the panel can be resized to."},{name:"boundary",type:"Element | null",defaultValue:"null",required:!1,description:"Element the panel may not be dragged or resized out of. Position is measured from this element's top-left corner, and the panel follows that corner as the page scrolls. The viewport constrains the panel when omitted."},{name:"stage",type:"'default' | 'minimized' | 'maximized'",required:!1,description:"Controlled window state. Pair with stage-change. 'default' uses the stored position and size, 'maximized' fills the boundary, 'minimized' collapses to header height."},{name:"defaultStage",type:"'default' | 'minimized' | 'maximized'",defaultValue:"'default'",required:!1,description:"Starting window state, for uncontrolled usage."},{name:"draggable",type:"boolean",defaultValue:"true",required:!1,description:"Whether the header can move the panel. Setting false drops the move cursor."},{name:"resizable",type:"boolean",defaultValue:"true",required:!1,description:"Whether resize grips do anything. Setting false makes every ResizeTrigger inert and non-focusable."},{name:"step",type:"number",defaultValue:"10",required:!1,description:"How far one arrow key moves or resizes the panel, in pixels. Shift+arrow uses ten times this value."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Freezes the geometry: drag, resize, and stage changes are refused. Opening and closing still work."},{name:"getGestureAnnouncement",type:"(gesture: 'move' | 'resize', detail: { position: { x: number; y: number }; size: { width: number; height: number } }) => string",required:!1,description:"Words the live-region message announced once per drag or resize. Numbers arrive rounded to whole CSS pixels. Return an empty string to announce nothing."},{name:"title",type:"string",required:!1,description:"The panel's heading, rendered in the derived Header. Omitting it withholds Content's aria-labelledby \u2014 a dangling IDREF is not a weak name but no name. Name a title-less panel with aria-label on the recipe instead."},{name:"stages",type:"Array<{ stage: 'default' | 'minimized' | 'maximized'; label: string }>",defaultValue:"[]",required:!1,description:"The window controls the derived Header renders \u2014 one StageTrigger per entry, each switching the panel to its stage. Empty by default gives no controls. The label is required rather than derived from the stage name."},{name:"dismissible",type:"boolean",defaultValue:"true",required:!1,description:"Whether the derived Header renders a CloseTrigger. False withholds it for a panel whose only exit is the consumer's own control."}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever the panel opens or closes, controlled or not."},i("panel"),{name:"position-change",description:"Fired with the clamped position on every drag frame and every arrow-key nudge. Throttle if the work is expensive.",detail:"{ x: number; y: number }"},{name:"size-change",description:"Fired with the clamped size on every resize frame and every arrow-key nudge.",detail:"{ width: number; height: number }"},{name:"position-commit",description:"Fired once when a gesture settles, carrying the same position position-change last reported. This is the event to persist geometry: position-change fires on every drag frame and every arrow-key nudge. A resize also fires this because a grip pulling the west or north edge relocates the panel's origin.",detail:"{ x: number; y: number }"},{name:"size-commit",description:"Fired once when a resize gesture settles. The settled counterpart to size-change. A move never fires it \u2014 dragging the header changes no dimension.",detail:"{ width: number; height: number }"},{name:"stage-change",description:"Fired with the new stage whenever a StageTrigger is pressed.",detail:"'default' | 'minimized' | 'maximized'"},...t("panel")],parts:[{name:"Trigger",description:'The button that opens the panel. Carries aria-haspopup="dialog", aria-expanded, and aria-controls.'},{...e,description:`${e.description} Shipped by React, Vue, Svelte and Web Components. Angular uses the [skdxPortal] directive instead. In Web Components the part exists but force-mount does not: its parts never unmount, they hide with the hidden attribute.`},{name:"Content",description:'The movable surface: role="dialog" with aria-modal="false", named by Title, publishing current geometry as --skdx-floating-panel-x/y/width/height. Being non-modal, it reports focus leaving without closing.',props:[a]},{name:"Header",description:"The drag handle. Pointer-drags move the panel; arrow keys nudge it by step (ten times with Shift), clamped to the boundary. The final position is announced once the gesture ends."},{name:"Title",description:"The panel's accessible name, referenced by Content's aria-labelledby."},{name:"CloseTrigger",description:"A button that closes the panel, for a close affordance inside the header."},{name:"ResizeTrigger",description:'One edge or corner grip, rendered as role="separator" with aria-valuenow/valuemin/valuemax describing the dimension it pulls \u2014 width for east/west and corners, height for north/south \u2014 plus aria-orientation on the four edges. Arrow keys resize by step, clamped to minSize, maxSize, and the boundary.',props:[{name:"direction",type:"'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'",required:!0,description:"Which edge or corner this grip pulls; corners move two edges at once. There is no default \u2014 a grip that does not say what it resizes cannot resize anything."}]},{name:"StageTrigger",description:"A minimize/maximize/restore button carrying aria-pressed. Pressing it while already in that stage restores 'default', making one button both minimize and restore.",props:[{name:"stage",type:"'default' | 'minimized' | 'maximized'",required:!0,description:"The stage this button switches to."}]}],slots:[{name:"trigger",description:"Trigger's content \u2014 the button that opens the panel."},{name:"default",description:"Content's body \u2014 typically a Header wrapping a Title, CloseTrigger and StageTriggers, the panel's own content, and the ResizeTrigger grips."}],states:["closed","open","dragging","resizing","default","minimized","maximized","disabled","on","off"],controlledUncontrolled:"Four independent controlled/uncontrolled axes \u2014 open, position, size, and stage \u2014 each starting from its default* prop and switching to controlled when the matching prop is passed. Passing a controlled value without its change event freezes that axis rather than fighting the consumer for it.",formBehavior:'Not applicable \u2014 FloatingPanel participates in no form; Trigger is type="button".',validationBehavior:"Not applicable \u2014 FloatingPanel collects no value.",accessibility:{role:'dialog (Content, aria-modal="false"), separator (ResizeTrigger)',keyboard:[{key:"Enter / Space (on Trigger)",behavior:"Toggles the panel: opens it when closed, closes it when open. Trigger is a disclosure button carrying aria-expanded, which APG requires to toggle rather than only open."},{key:"Arrow keys (on Header)",behavior:"Move the panel by step pixels, clamped to the boundary. The landing position is announced politely once movement stops."},{key:"Shift + Arrow keys (on Header)",behavior:"Move the panel by ten times step, for crossing a large boundary without holding a key down."},{key:"Arrow keys (on a ResizeTrigger)",behavior:"Resize the panel from that edge or corner by step pixels, clamped to minSize, maxSize, and the boundary. The landing size is announced politely once resizing stops."},{key:"Shift + Arrow keys (on a ResizeTrigger)",behavior:"Resize by ten times step."},{key:"Escape",behavior:"Requests close (cancelable via escape-key-down) and returns focus to Trigger."},{key:"Tab",behavior:"Moves through the panel and then out of it into the rest of the page \u2014 the panel is non-modal and traps nothing."}],ariaAttributes:['aria-haspopup="dialog", aria-expanded, and aria-controls on Trigger',`role="dialog" with aria-modal="false" on Content, whose id matches Trigger's aria-controls`,"aria-labelledby on Content pointing at Title",'role="separator" with aria-valuenow/aria-valuemin/aria-valuemax on each ResizeTrigger, describing the dimension it controls. aria-valuemax is the effective ceiling \u2014 min(maxSize, room left in boundary).',"aria-orientation on the four edge ResizeTriggers; corners omitted as they move two axes","aria-pressed on each StageTrigger reflecting whether the panel is in that stage","aria-disabled and data-disabled on every geometry trigger while disabled is set, never the native disabled attribute. A frozen trigger stays reachable and announced as operable-but-refusing.",'aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight" on Header while draggable and each ResizeTrigger while resizable.'],focusManagement:"Focus is not trapped \u2014 the panel is non-modal and the page beneath stays reachable by Tab. Escape and CloseTrigger return focus to Trigger. Header is a tab stop while draggable; each ResizeTrigger while resizable, so a fixed-size panel does not spend eight stops on inert grips. disabled does not remove any: it freezes the geometry and is announced with aria-disabled.",namingRequirement:'Content must be named by a Title (or explicit aria-label). Header and every ResizeTrigger need their own aria-label when focusable: an unnamed "separator" tells the user nothing about what the arrow keys will do to it. Both are keyboard-operable controls, not decoration.'},styling:{cssParts:["trigger","content","header","title","close-trigger","resize-trigger","stage-trigger"],dataAttributes:[...r,"data-state","data-stage","data-dragging","data-resizing","data-direction","data-disabled",{name:"data-skdx-floating-panel-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:["--skdx-floating-panel-x","--skdx-floating-panel-y","--skdx-floating-panel-width","--skdx-floating-panel-height"]},publicMethods:[]};export{s as floatingPanelContract};
1
+ import{dismissalEvents as t,forceMountProp as a,openChangeCompleteEvent as i,portalPart as e}from"./overlay-parts.js";import{PART_DATA_ATTRIBUTES as r}from"./styling-hooks.js";var s={name:"FloatingPanel",purpose:"A non-modal window: draggable by its header, resizable from eight grips, switchable between default/minimized/maximized stages, and clamped to a boundary or viewport. Position and size are published as CSS custom properties, so the consumer owns the painting.",props:[{name:"open",type:"boolean",required:!1,description:"Controlled open state. Pair with the open-change event."},{name:"defaultOpen",type:"boolean",defaultValue:"false",required:!1,description:"Initial open state, for uncontrolled usage."},{name:"position",type:"{ x: number; y: number }",required:!1,description:"Controlled top-left corner in CSS pixels, measured from the boundary's top-left. Pair with position-change or the panel will refuse to move."},{name:"defaultPosition",type:"{ x: number; y: number }",defaultValue:"{ x: 0, y: 0 }",required:!1,description:"Starting top-left corner for uncontrolled usage, measured from the boundary."},{name:"size",type:"{ width: number; height: number }",required:!1,description:"Controlled box in CSS pixels. Pair with size-change or the panel will refuse to resize."},{name:"defaultSize",type:"{ width: number; height: number }",defaultValue:"{ width: 320, height: 240 }",required:!1,description:"Starting box for uncontrolled usage."},{name:"minSize",type:"{ width: number; height: number }",defaultValue:"{ width: 160, height: 120 }",required:!1,description:"Smallest box the panel can be resized to. Every resize is clamped to it."},{name:"maxSize",type:"{ width: number; height: number }",defaultValue:"{ width: Infinity, height: Infinity }",required:!1,description:"Largest box the panel can be resized to."},{name:"boundary",type:"Element | null",defaultValue:"null",required:!1,description:"Element the panel may not be dragged or resized out of. Position is measured from this element's top-left corner, and the panel follows that corner as the page scrolls. The viewport constrains the panel when omitted."},{name:"stage",type:"'default' | 'minimized' | 'maximized'",required:!1,description:"Controlled window state. Pair with stage-change. 'default' uses the stored position and size, 'maximized' fills the boundary, 'minimized' collapses to header height."},{name:"defaultStage",type:"'default' | 'minimized' | 'maximized'",defaultValue:"'default'",required:!1,description:"Starting window state, for uncontrolled usage."},{name:"draggable",type:"boolean",defaultValue:"true",required:!1,description:"Whether the header can move the panel. Setting false drops the move cursor."},{name:"resizable",type:"boolean",defaultValue:"true",required:!1,description:"Whether resize grips do anything. Setting false makes every ResizeTrigger inert and non-focusable."},{name:"step",type:"number",defaultValue:"10",required:!1,description:"How far one arrow key moves or resizes the panel, in pixels. Shift+arrow uses ten times this value."},{name:"disabled",type:"boolean",defaultValue:"false",required:!1,description:"Freezes the geometry: drag, resize, and stage changes are refused. Opening and closing still work."},{name:"getGestureAnnouncement",type:"(gesture: 'move' | 'resize', detail: { position: { x: number; y: number }; size: { width: number; height: number } }) => string",required:!1,description:"Words the live-region message announced once per drag or resize. Numbers arrive rounded to whole CSS pixels. Return an empty string to announce nothing."},{name:"title",type:"string",required:!1,description:"The panel's heading, rendered in the derived Header. Omitting it withholds Content's aria-labelledby \u2014 a dangling IDREF is not a weak name but no name. Name a title-less panel with aria-label on the recipe instead."},{name:"stages",type:"Array<{ stage: 'default' | 'minimized' | 'maximized'; label: string }>",defaultValue:"[]",required:!1,description:"The window controls the derived Header renders \u2014 one StageTrigger per entry, each switching the panel to its stage. Empty by default gives no controls. The label is required rather than derived from the stage name."},{name:"dismissible",type:"boolean",defaultValue:"true",required:!1,description:"Whether the derived Header renders a CloseTrigger. False withholds it for a panel whose only exit is the consumer's own control."}],events:[{name:"open-change",detail:"boolean",description:"Fired with the new open state whenever the panel opens or closes, controlled or not."},i("panel"),{name:"position-change",description:"Fired with the clamped position on every drag frame and every arrow-key nudge. Throttle if the work is expensive.",detail:"{ x: number; y: number }"},{name:"size-change",description:"Fired with the clamped size on every resize frame and every arrow-key nudge.",detail:"{ width: number; height: number }"},{name:"position-commit",description:"Fired once when a gesture settles, carrying the same position position-change last reported. This is the event to persist geometry: position-change fires on every drag frame and every arrow-key nudge. A resize also fires this because a grip pulling the west or north edge relocates the panel's origin.",detail:"{ x: number; y: number }"},{name:"size-commit",description:"Fired once when a resize gesture settles. The settled counterpart to size-change. A move never fires it \u2014 dragging the header changes no dimension.",detail:"{ width: number; height: number }"},{name:"stage-change",description:"Fired with the new stage whenever a StageTrigger is pressed.",detail:"'default' | 'minimized' | 'maximized'"},...t("panel")],parts:[{name:"Trigger",description:'The button that opens the panel. Carries aria-haspopup="dialog", aria-expanded, and aria-controls.',slots:[{name:"default",description:"Trigger's content \u2014 the button that opens the panel."}]},{...e,description:`${e.description} Shipped by React, Vue, Svelte and Web Components. Angular uses the [skdxPortal] directive instead. In Web Components the part exists but force-mount does not: its parts never unmount, they hide with the hidden attribute.`},{name:"Content",description:'The movable surface: role="dialog" with aria-modal="false", named by Title, publishing current geometry as --skdx-floating-panel-x/y/width/height. Being non-modal, it reports focus leaving without closing.',props:[a]},{name:"Header",description:"The drag handle. Pointer-drags move the panel; arrow keys nudge it by step (ten times with Shift), clamped to the boundary. The final position is announced once the gesture ends."},{name:"Title",description:"The panel's accessible name, referenced by Content's aria-labelledby."},{name:"CloseTrigger",description:"A button that closes the panel, for a close affordance inside the header."},{name:"ResizeTrigger",description:'One edge or corner grip, rendered as role="separator" with aria-valuenow/valuemin/valuemax describing the dimension it pulls \u2014 width for east/west and corners, height for north/south \u2014 plus aria-orientation on the four edges. Arrow keys resize by step, clamped to minSize, maxSize, and the boundary.',props:[{name:"direction",type:"'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'",required:!0,description:"Which edge or corner this grip pulls; corners move two edges at once. There is no default \u2014 a grip that does not say what it resizes cannot resize anything."}]},{name:"StageTrigger",description:"A minimize/maximize/restore button carrying aria-pressed. Pressing it while already in that stage restores 'default', making one button both minimize and restore.",props:[{name:"stage",type:"'default' | 'minimized' | 'maximized'",required:!0,description:"The stage this button switches to."}]}],slots:[{name:"default",description:"Content's body \u2014 typically a Header wrapping a Title, CloseTrigger and StageTriggers, the panel's own content, and the ResizeTrigger grips."}],states:["closed","open","dragging","resizing","default","minimized","maximized","disabled","on","off"],controlledUncontrolled:"Four independent controlled/uncontrolled axes \u2014 open, position, size, and stage \u2014 each starting from its default* prop and switching to controlled when the matching prop is passed. Passing a controlled value without its change event freezes that axis rather than fighting the consumer for it.",formBehavior:'Not applicable \u2014 FloatingPanel participates in no form; Trigger is type="button".',validationBehavior:"Not applicable \u2014 FloatingPanel collects no value.",accessibility:{role:'dialog (Content, aria-modal="false"), separator (ResizeTrigger)',keyboard:[{key:"Enter / Space (on Trigger)",behavior:"Toggles the panel: opens it when closed, closes it when open. Trigger is a disclosure button carrying aria-expanded, which APG requires to toggle rather than only open."},{key:"Arrow keys (on Header)",behavior:"Move the panel by step pixels, clamped to the boundary. The landing position is announced politely once movement stops."},{key:"Shift + Arrow keys (on Header)",behavior:"Move the panel by ten times step, for crossing a large boundary without holding a key down."},{key:"Arrow keys (on a ResizeTrigger)",behavior:"Resize the panel from that edge or corner by step pixels, clamped to minSize, maxSize, and the boundary. The landing size is announced politely once resizing stops."},{key:"Shift + Arrow keys (on a ResizeTrigger)",behavior:"Resize by ten times step."},{key:"Escape",behavior:"Requests close (cancelable via escape-key-down) and returns focus to Trigger."},{key:"Tab",behavior:"Moves through the panel and then out of it into the rest of the page \u2014 the panel is non-modal and traps nothing."}],ariaAttributes:['aria-haspopup="dialog", aria-expanded, and aria-controls on Trigger',`role="dialog" with aria-modal="false" on Content, whose id matches Trigger's aria-controls`,"aria-labelledby on Content pointing at Title",'role="separator" with aria-valuenow/aria-valuemin/aria-valuemax on each ResizeTrigger, describing the dimension it controls. aria-valuemax is the effective ceiling \u2014 min(maxSize, room left in boundary).',"aria-orientation on the four edge ResizeTriggers; corners omitted as they move two axes","aria-pressed on each StageTrigger reflecting whether the panel is in that stage","aria-disabled and data-disabled on every geometry trigger while disabled is set, never the native disabled attribute. A frozen trigger stays reachable and announced as operable-but-refusing.",'aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight" on Header while draggable and each ResizeTrigger while resizable.'],focusManagement:"Focus is not trapped \u2014 the panel is non-modal and the page beneath stays reachable by Tab. Escape and CloseTrigger return focus to Trigger. Header is a tab stop while draggable; each ResizeTrigger while resizable, so a fixed-size panel does not spend eight stops on inert grips. disabled does not remove any: it freezes the geometry and is announced with aria-disabled.",namingRequirement:'Content must be named by a Title (or explicit aria-label). Header and every ResizeTrigger need their own aria-label when focusable: an unnamed "separator" tells the user nothing about what the arrow keys will do to it. Both are keyboard-operable controls, not decoration.'},styling:{cssParts:["trigger","content","header","title","close-trigger","resize-trigger","stage-trigger"],dataAttributes:[...r,"data-state","data-stage","data-dragging","data-resizing","data-direction","data-disabled",{name:"data-skdx-floating-panel-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:["--skdx-floating-panel-x","--skdx-floating-panel-y","--skdx-floating-panel-width","--skdx-floating-panel-height"]},publicMethods:[]};export{s as floatingPanelContract};
@@ -1 +1 @@
1
- import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var r={name:"Form",purpose:"Form root wired to the native Constraint Validation API with a styleable error system and per-field snapshots. No custom validators \u2014 the consumer's inputs validate themselves.",props:[{name:"errors",type:"Record<string, string | string[]>",required:!1,description:"Server or cross-field errors, keyed by field `name`. Marks the field `aria-invalid` and clears on next `change`."},{name:"validationMode",type:"'submit' | 'blur' | 'change'",defaultValue:"'change'",required:!1,description:"When ValidityState refreshes and messages show: `change` (default) on change/blur, `blur` on blur only, `submit` on submit only."},{name:"validationBehavior",type:"'native' | 'aria'",defaultValue:"'native'",required:!1,description:"Whether validation blocks submit: `native` blocks it; `aria` lets it through with failing controls marked `aria-invalid`."},{name:"errorSummary",type:"string",required:!1,description:"Error summary heading copy; omit it to skip the summary part."},{name:"submitLabel",type:"string",required:!1,description:"Submit button label; omit it to skip the submit button part."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."}],events:[{name:"submit",detail:"void",description:"Fires only after validation passes, or under `validationBehavior: 'aria'` regardless."},{name:"reset",detail:"void",description:"Fires on native reset, clearing per-field ValidityState snapshots and server errors."},{name:"field-invalid",description:"Fires per failing field on submit, non-cancelable; lets a consumer react to blocked submit.",detail:"{ name: string; validity: ValiditySnapshot }"}],parts:[{name:"Field",description:"Groups Label, Control, and Message for one field; wires their ids together.",props:[{name:"name",type:"string",required:!0,description:"Field name; key for submit, ValidityState storage, and error summary link text."},{name:"label",type:"string",required:!1,description:"Label copy; derives a Label part and points control `aria-labelledby` to it."},{name:"message",type:"string",required:!1,description:"Catch-all message copy, shown when the field is invalid."}]},{name:"Control",description:"Wraps the control and wires `name`/`id` from Field, plus `aria-invalid` and `aria-describedby` on failure.",slots:[{name:"default",description:"One control element \u2014 a native `<input>`/`<select>`/`<textarea>` or a component wrapping one."}]},{name:"Message",description:"Validation message for one Field; shown only while its condition matches.",props:[{name:"match",type:"keyof ValidityState",required:!1,description:"ValidityState flag this message shows for; omit for the catch-all."},{name:"forceMatch",type:"boolean",defaultValue:"false",required:!1,description:"Show message regardless of native validity; for errors the browser cannot derive."}]},{name:"ErrorSummary",lightDom:!0,description:'Error summary with `role="alert"` and `tabindex="-1"`, listing links to failing fields. **It must have an accessible name** \u2014 heading or `aria-label` \u2014 since it takes focus on arrival.',props:[{name:"autoFocus",type:"boolean",defaultValue:"true",required:!1,description:"Focus the summary when it appears; turn off if your routing already moves focus."}]}],slots:[{name:"default",description:"FormField(s) plus a FormSubmit."}],appearance:[{attribute:"data-density",values:["compact"],stylesheet:"form.css"}],states:["default"],controlledUncontrolled:"Not applicable \u2014 Form owns no external controlled state, only internal validity snapshots.",formBehavior:"On submit, checkValidity() runs on every field; failure blocks submit and fires `field-invalid` per field. Fields re-validate on blur/change per `validationMode`. Under `validationBehavior: 'aria'`, submit fires regardless with snapshots taken first.",validationBehavior:"Delegates to native constraint validation on consumer inputs, plus the `errors` map for browser-opaque failures.",accessibility:{role:"generic (native <form>)",keyboard:[],ariaAttributes:["FormControl injects aria-invalid/aria-describedby onto its child once that field fails validation"],focusManagement:"A blocked submit moves focus to the ErrorSummary or the first failing control.",namingRequirement:"The ErrorSummary must have an accessible name \u2014 heading or `aria-label` \u2014 since it takes focus."},styling:{cssParts:["form","field","label","message","submit","control","error-summary","error-summary-title","error-summary-list","error-summary-link"],dataAttributes:[...e,"data-skdx-error-summary","data-skdx-form-field","data-skdx-form-message","data-skdx-field-name","data-density",{name:"data-skdx-form-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'},{name:"data-skdx-form-field-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[{name:"validate",signature:"(): Promise<boolean>",description:"Runs full form validation without short-circuiting; resolves to whether it passed."}]};export{r as formContract};
1
+ import{ROOT_DATA_ATTRIBUTES as e}from"./styling-hooks.js";var r={name:"Form",purpose:"Form root wired to the native Constraint Validation API with a styleable error system and per-field snapshots. No custom validators \u2014 the consumer's inputs validate themselves.",props:[{name:"errors",type:"Record<string, string | string[]>",required:!1,description:"Server or cross-field errors, keyed by field `name`. Marks the field `aria-invalid` and clears on next `change`."},{name:"validationMode",type:"'submit' | 'blur' | 'change'",defaultValue:"'change'",required:!1,description:"When ValidityState refreshes and messages show: `change` (default) on change/blur, `blur` on blur only, `submit` on submit only."},{name:"validationBehavior",type:"'native' | 'aria'",defaultValue:"'native'",required:!1,description:"Whether validation blocks submit: `native` blocks it; `aria` lets it through with failing controls marked `aria-invalid`."},{name:"errorSummary",type:"string",required:!1,description:"Error summary heading copy; omit it to skip the summary part."},{name:"submitLabel",type:"string",required:!1,description:"Submit button label; omit it to skip the submit button part."},{name:"density",type:"'compact' | (string & {})",required:!1,description:"Vertical rhythm rung, written to data-density on the root; the data-density attribute may be set directly instead."}],events:[{name:"submit",detail:"void",description:"Fires only after validation passes, or under `validationBehavior: 'aria'` regardless."},{name:"reset",detail:"void",description:"Fires on native reset, clearing per-field ValidityState snapshots and server errors."},{name:"field-invalid",description:"Fires per failing field on submit, non-cancelable; lets a consumer react to blocked submit.",detail:"{ name: string; validity: ValiditySnapshot }"}],parts:[{name:"Field",description:"Groups Label, Control, and Message for one field; wires their ids together.",props:[{name:"name",type:"string",required:!0,description:"Field name; key for submit, ValidityState storage, and error summary link text."},{name:"label",type:"string",required:!1,description:"Label copy; derives a Label part and points control `aria-labelledby` to it."},{name:"message",type:"string",required:!1,description:"Catch-all message copy, shown when the field is invalid."}]},{name:"Control",description:"Wraps the control and wires `name`/`id` from Field, plus `aria-invalid` and `aria-describedby` on failure.",slots:[{name:"default",description:"One control element \u2014 a native `<input>`/`<select>`/`<textarea>` or a component wrapping one."}]},{name:"Message",description:"Validation message for one Field; shown only while its condition matches.",props:[{name:"match",type:"keyof ValidityState",required:!1,description:"ValidityState flag this message shows for; omit for the catch-all."},{name:"forceMatch",type:"boolean",defaultValue:"false",required:!1,description:"Show message regardless of native validity; for errors the browser cannot derive."}]},{name:"ErrorSummary",lightDom:!0,description:'Error summary with `role="alert"` and `tabindex="-1"`, listing links to failing fields. **It must have an accessible name** \u2014 heading or `aria-label` \u2014 since it takes focus on arrival.',props:[{name:"autoFocus",type:"boolean",defaultValue:"true",required:!1,description:"Focus the summary when it appears; turn off if your routing already moves focus."}]}],slots:[{name:"default",description:"FormField(s) plus a FormSubmit."}],appearance:[{attribute:"data-density",values:["compact"],stylesheet:"form.css"}],states:["default"],controlledUncontrolled:"Not applicable \u2014 Form owns no external controlled state, only internal validity snapshots.",formBehavior:"On submit, checkValidity() runs on every field; failure blocks submit and fires `field-invalid` per field. Fields re-validate on blur/change per `validationMode`. Under `validationBehavior: 'aria'`, submit fires regardless with snapshots taken first.",validationBehavior:"Delegates to native constraint validation on consumer inputs, plus the `errors` map for browser-opaque failures.",accessibility:{role:"generic (native <form>)",keyboard:[],ariaAttributes:["FormControl injects aria-invalid/aria-describedby onto its child once that field fails validation"],focusManagement:"A blocked submit moves focus to the ErrorSummary or the first failing control.",namingRequirement:"The ErrorSummary must have an accessible name \u2014 heading or `aria-label` \u2014 since it takes focus."},styling:{cssParts:["form","field","label","message","submit","control","error-summary","error-summary-title","error-summary-list","error-summary-link"],dataAttributes:[...e,"data-skdx-error-summary","data-skdx-form-field","data-skdx-form-message","data-skdx-field-name","data-density",{name:"data-skdx-form-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'},{name:"data-skdx-form-field-generated",frameworks:["webComponents"],exception:"wc-marker",frameworksReason:'Marks a node the element generated for itself rather than one the consumer authored. React, Vue, Svelte and Angular re-render their derived anatomy declaratively, so "mine or theirs" is answered by the framework and no marker is needed.'}],cssCustomProperties:[]},publicMethods:[{name:"validate",signature:"(): Promise<boolean>",description:"Runs full form validation without short-circuiting; resolves to whether it passed."}],frameworkExtensions:[{framework:"webComponents",part:"ErrorSummary",name:"no-auto-focus",type:"boolean",description:"Set `no-auto-focus` to stop the summary taking focus the moment it starts\nreporting \u2014 for a form that manages focus itself. This is documented configuration/state on the native element or its composed part beyond the common component core. It is explicitly recorded as an element adapter rather than falsely requiring the same property binding on every wrapper.",kind:"prop"},{framework:"webComponents",part:"Field",name:"validity",type:"ValiditySnapshot | undefined",description:"The field's most recent validity snapshot, or `undefined` if not yet validated. This documented getter exposes the native element\u2019s derived live state. Other wrappers expose their state through context or framework refs; the shared component contract does not require identical getter spellings.",kind:"prop"},{framework:"vue",part:"Field",kind:"slot",name:"message",type:"Content projection",description:"message content projected by FormField. This framework spells the content customization as a native named slot/snippet on this part; consumers of other wrappers compose the corresponding part or use their documented rendering property."},{framework:"webComponents",name:"nativeForm",type:"HTMLFormElement | null",description:"The native `<form>` the body lives in \u2014 authored, or generated by the anatomy. This is a reference to the underlying native element for integration with platform APIs; the other wrappers hand off elements through their framework ref/context mechanisms instead of this property spelling.",kind:"prop"},{framework:"webComponents",part:"ErrorSummary",name:"no-auto-focus",type:"boolean",description:"Set `no-auto-focus` to stop the summary taking focus the moment it starts\nreporting \u2014 for a form that manages focus itself. This is documented configuration/state on the native element or its composed part beyond the common component core. It is explicitly recorded as an element adapter rather than falsely requiring the same property binding on every wrapper."},{framework:"webComponents",part:"Field",name:"validity",type:"ValiditySnapshot | undefined",description:"The field's most recent validity snapshot, or `undefined` if not yet validated. This wrapper exposes the underlying native input validity or validationMessage as a live property; Svelte exposes callable instance accessors and other wrappers provide their native input ref/validation methods."},{framework:"vue",part:"Field",kind:"slot",name:"message",type:"Content projection",description:"message content projected by FormField. This framework spells the content customization as a native named slot/snippet on this part; consumers of other wrappers compose the corresponding part or use their documented rendering property."},{framework:"webComponents",name:"nativeForm",type:"HTMLFormElement | null",description:"The native `<form>` the body lives in \u2014 authored, or generated by the anatomy. This is a reference to the underlying native element for integration with platform APIs; the other wrappers hand off elements through their framework ref/context mechanisms instead of this property spelling."}]};export{r as formContract};