@happyvertical/smrt-ui 0.52.0 → 0.53.1

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 (208) hide show
  1. package/AGENTS.md +11 -1
  2. package/README.md +300 -2
  3. package/dist/components/data/DataTable.svelte +60 -15
  4. package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
  5. package/dist/components/data/DataTableResponsive.d.ts +4 -0
  6. package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
  7. package/dist/components/data/DataTableResponsive.js +22 -0
  8. package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
  9. package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
  10. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
  11. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
  12. package/dist/components/data/types.d.ts +8 -0
  13. package/dist/components/data/types.d.ts.map +1 -1
  14. package/dist/components/display/DateDisplay.svelte +76 -14
  15. package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
  16. package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
  17. package/dist/components/display/Icon.svelte +3 -16
  18. package/dist/components/display/Icon.svelte.d.ts +0 -9
  19. package/dist/components/display/Icon.svelte.d.ts.map +1 -1
  20. package/dist/components/display/StatusBadge.svelte +29 -2
  21. package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
  22. package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
  23. package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
  24. package/dist/components/display/__tests__/Icon.test.js +132 -2
  25. package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
  26. package/dist/components/display/icons.svelte.d.ts +11 -0
  27. package/dist/components/display/icons.svelte.d.ts.map +1 -0
  28. package/dist/components/display/icons.svelte.js +60 -0
  29. package/dist/components/display/index.d.ts +3 -1
  30. package/dist/components/display/index.d.ts.map +1 -1
  31. package/dist/components/display/index.js +1 -0
  32. package/dist/components/display/types.d.ts +2 -0
  33. package/dist/components/display/types.d.ts.map +1 -1
  34. package/dist/components/feedback/ConfirmDialog.svelte +59 -48
  35. package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
  36. package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
  37. package/dist/components/feedback/ToastViewport.svelte +156 -8
  38. package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
  39. package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
  40. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
  41. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
  42. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
  43. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
  44. package/dist/components/feedback/__tests__/toast.test.js +142 -1
  45. package/dist/components/feedback/index.d.ts +1 -1
  46. package/dist/components/feedback/index.d.ts.map +1 -1
  47. package/dist/components/feedback/toast.d.ts +13 -1
  48. package/dist/components/feedback/toast.d.ts.map +1 -1
  49. package/dist/components/feedback/toast.js +51 -10
  50. package/dist/components/forms/CameraCapture.svelte +619 -0
  51. package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
  52. package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
  53. package/dist/components/forms/Checkbox.svelte +8 -1
  54. package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
  55. package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
  56. package/dist/components/forms/Combobox.svelte +40 -0
  57. package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
  58. package/dist/components/forms/Form.svelte +5 -0
  59. package/dist/components/forms/Form.svelte.d.ts +1 -0
  60. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  61. package/dist/components/forms/Input.svelte +8 -1
  62. package/dist/components/forms/Input.svelte.d.ts +2 -0
  63. package/dist/components/forms/Input.svelte.d.ts.map +1 -1
  64. package/dist/components/forms/Listbox.svelte +39 -1
  65. package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
  66. package/dist/components/forms/MultiSelect.svelte +41 -1
  67. package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
  68. package/dist/components/forms/Radio.svelte +1 -0
  69. package/dist/components/forms/RadioGroup.svelte +7 -1
  70. package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
  71. package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
  72. package/dist/components/forms/SegmentedControl.svelte +152 -11
  73. package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
  74. package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
  75. package/dist/components/forms/Select.svelte +8 -1
  76. package/dist/components/forms/Select.svelte.d.ts +2 -0
  77. package/dist/components/forms/Select.svelte.d.ts.map +1 -1
  78. package/dist/components/forms/SignaturePad.svelte +436 -0
  79. package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
  80. package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
  81. package/dist/components/forms/Switch.svelte +8 -1
  82. package/dist/components/forms/Switch.svelte.d.ts +2 -0
  83. package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
  84. package/dist/components/forms/Textarea.svelte +8 -1
  85. package/dist/components/forms/Textarea.svelte.d.ts +2 -0
  86. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
  87. package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
  88. package/dist/components/forms/__tests__/Combobox.test.js +279 -12
  89. package/dist/components/forms/__tests__/Form.test.js +46 -0
  90. package/dist/components/forms/__tests__/Listbox.test.js +303 -0
  91. package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
  92. package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
  93. package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
  94. package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
  95. package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
  96. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
  97. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
  98. package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
  99. package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
  100. package/dist/components/forms/__tests__/capture-test-env.js +122 -0
  101. package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
  102. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
  103. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
  104. package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
  105. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
  106. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
  107. package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
  108. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
  109. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
  110. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
  111. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
  112. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
  113. package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
  114. package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
  115. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
  116. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
  117. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
  118. package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
  119. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
  120. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
  121. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
  122. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
  123. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
  124. package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
  125. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
  126. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
  127. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
  128. package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
  129. package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
  130. package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
  131. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
  132. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
  133. package/dist/components/forms/__tests__/touch-density.test.js +66 -0
  134. package/dist/components/forms/camera-capture-session.d.ts +81 -0
  135. package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
  136. package/dist/components/forms/camera-capture-session.js +130 -0
  137. package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
  138. package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
  139. package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
  140. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
  141. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
  142. package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
  143. package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
  144. package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
  145. package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
  146. package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
  147. package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
  148. package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
  149. package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
  150. package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
  151. package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
  152. package/dist/components/forms/form-retry/content-digest.js +169 -0
  153. package/dist/components/forms/form-retry/controller.d.ts +214 -0
  154. package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
  155. package/dist/components/forms/form-retry/controller.js +404 -0
  156. package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
  157. package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
  158. package/dist/components/forms/form-retry/form-draft.js +161 -0
  159. package/dist/components/forms/form-retry/index.d.ts +19 -0
  160. package/dist/components/forms/form-retry/index.d.ts.map +1 -0
  161. package/dist/components/forms/form-retry/index.js +16 -0
  162. package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
  163. package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
  164. package/dist/components/forms/form-retry/submission-key.js +342 -0
  165. package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
  166. package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
  167. package/dist/components/forms/form-retry/submitted-draft.js +270 -0
  168. package/dist/components/forms/form-retry/types.d.ts +84 -0
  169. package/dist/components/forms/form-retry/types.d.ts.map +1 -0
  170. package/dist/components/forms/form-retry/types.js +13 -0
  171. package/dist/components/forms/index.d.ts +7 -1
  172. package/dist/components/forms/index.d.ts.map +1 -1
  173. package/dist/components/forms/index.js +8 -0
  174. package/dist/components/forms/native-file-field.d.ts +94 -0
  175. package/dist/components/forms/native-file-field.d.ts.map +1 -0
  176. package/dist/components/forms/native-file-field.js +147 -0
  177. package/dist/components/forms/signature-pad-logic.d.ts +50 -0
  178. package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
  179. package/dist/components/forms/signature-pad-logic.js +48 -0
  180. package/dist/components/forms/types.d.ts +41 -0
  181. package/dist/components/forms/types.d.ts.map +1 -1
  182. package/dist/components/nav/FilterChips.svelte +8 -1
  183. package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
  184. package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
  185. package/dist/components/ui/Button.svelte +9 -2
  186. package/dist/components/ui/Button.svelte.d.ts +2 -0
  187. package/dist/components/ui/Button.svelte.d.ts.map +1 -1
  188. package/dist/i18n/strings.ui.d.ts +24 -0
  189. package/dist/i18n/strings.ui.d.ts.map +1 -1
  190. package/dist/i18n/strings.ui.js +26 -0
  191. package/dist/styles/tokens.css +2 -0
  192. package/dist/svelte/__tests__/playground.test.js +2 -1
  193. package/dist/svelte/playground/CapturePreview.svelte +56 -0
  194. package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
  195. package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
  196. package/dist/svelte/playground.d.ts.map +1 -1
  197. package/dist/svelte/playground.js +10 -0
  198. package/dist/themes/ThemeProvider.svelte +9 -0
  199. package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
  200. package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
  201. package/dist/themes/css-generator.d.ts.map +1 -1
  202. package/dist/themes/css-generator.js +3 -0
  203. package/dist/themes/styles/glass.css +4 -0
  204. package/dist/themes/styles/happyvertical.css +4 -0
  205. package/dist/themes/styles/material.css +4 -0
  206. package/dist/themes/styles/smrt.css +4 -0
  207. package/dist/themes/styles/studio.css +4 -0
  208. package/package.json +8 -2
package/AGENTS.md CHANGED
@@ -52,7 +52,8 @@ components are exempt — they *are* the primitives.
52
52
  | `./layout` | `Container`, `Grid`, `Header`, `Footer`, `PageHeader`, `EmptyState`, … |
53
53
  | `./calendar` | `CalendarView` (time-zone-aware month grid + phone agenda) and its Svelte-free date model (`toEntries`, `layoutMonth`, `dateKeyInZone`, …); deprecated `Calendar`, `DayView` |
54
54
  | `./chat` | `MessageBubble`, `ReactionPicker`, `TypingIndicator` |
55
- | `./forms` | Provider-free fields, choice controls, sliders/ranges, combobox/listbox/multiselect/tags, date/time/file controls, plus the transport-neutral control interaction registry |
55
+ | `./forms` | Provider-free fields, choice controls, sliders/ranges, combobox/listbox/multiselect/tags, date/time/file controls, `CameraCapture`/`SignaturePad` capture, plus the transport-neutral control interaction registry |
56
+ | `./form-retry` | **Svelte-free** form retry (#3291): submission key, in-flight refusal, conditional reset, opt-in restore-after-reload, browser `runOnce()` digest. Also re-exported from `./forms`. Framework-free TS typed structurally against SvelteKit's `SubmitFunction` — never import `@sveltejs/kit` here. Guide: `docs/content/form-retry.md` |
56
57
  | `./i18n` | i18n **client**: `useI18n`, `<Trans>`, `defineMessages`, `renderTemplate` (no `smrt-languages` import — the server resolver stays in `smrt-svelte/i18n/server`) |
57
58
  | `./registry` | `ModuleUIRegistry` for cross-package component discovery |
58
59
  | `./theme` | deprecated compatibility path forwarding to the canonical theme system |
@@ -153,6 +154,15 @@ other text pairing clears WCAG AA in both schemes.
153
154
  instants into the zone once (`dateKeyInZone`) and do all grid/span math on
154
155
  keys; never `new Date(y, m, d)` / `getDay()` in the browser zone. Months are
155
156
  1-12 in the public API (`CalendarMonth`), unlike `Date#getMonth`.
157
+ - **Capture controls post natively.** `CameraCapture` and `SignaturePad` fill
158
+ a hidden named file input via `DataTransfer`, falling back to a `formdata`
159
+ listener (`native-file-field.ts`). Keep camera lifecycle in the
160
+ framework-free `camera-capture-session.ts` and the pointer gate in
161
+ `signature-pad-logic.ts`; their effects must not read rendered state. The
162
+ `<input capture>` path stays opt-in (`fileInputFallback`), off by default.
163
+ A form `reset` empties a native file input, so both clear their capture on
164
+ their form's reset (`attachFormResetListener`), even while `disabled`;
165
+ otherwise the UI shows a file the next submit no longer posts.
156
166
 
157
167
  - **i18n split**: the client (here) is dependency-free; the Node-only server
158
168
  resolver (`buildI18nSnapshot`, → `@happyvertical/smrt-languages`) stays in
package/README.md CHANGED
@@ -33,6 +33,7 @@ pnpm add @happyvertical/smrt-ui
33
33
  | Text and structured input | `Input`, `Textarea`, `Select`, `Combobox`, `Listbox`, `MultiSelect`, `TagsInput`, `SearchInput` |
34
34
  | Choices | `Checkbox`, `RadioGroup`/`Radio`, `Switch`, `Toggle`, `ToggleButton`, `SegmentedControl` |
35
35
  | Values and files | `Slider`, `RangeSlider`, `DatePicker`, `TimePicker`, `FilePicker` |
36
+ | Capture | `CameraCapture`, `SignaturePad` |
36
37
  | Actions and display | `Button`, `Dropdown`/`Menu`, `Badge`, `Chip`, `Avatar`, `Card`, `Skeleton`, `Tooltip`, `Tree` |
37
38
  | Disclosure and overlays | `Popover`, `Disclosure`, `Accordion`/`AccordionItem`, `Modal`, `Drawer`/`Sheet`, `PhoneSheet`, `ConfirmDialog` |
38
39
  | Feedback | `Alert`, `ToastViewport`, `Progress`, `Meter`, `Spinner`, `LoadingOverlay`, `WorkingStrip` |
@@ -91,12 +92,65 @@ kept in view. It carries `data-shell-tabs`, so AdminShell keeps it sticky
91
92
  under the phone top bar. Pair it with `useLinkSurface` (smrt-svelte) so
92
93
  agents can switch tabs too.
93
94
 
94
- Use the focused subpaths (`/forms`, `/ui`, `/feedback`, `/data`,
95
+ Use the focused subpaths (`/forms`, `/form-retry`, `/ui`, `/feedback`, `/data`,
95
96
  `/data-surface`, `/layout`, `/themes`) to keep imports explicit. The
96
97
  Svelte-free `/data-surface` entry exposes the registry contracts and shared
97
- protocol limits for server adapters. The package root remains a compatibility
98
+ protocol limits for server adapters; the Svelte-free `/form-retry` entry
99
+ exposes the form-retry helper. The package root remains a compatibility
98
100
  barrel.
99
101
 
102
+ ### Calendar dates and shop time
103
+
104
+ `DateDisplay` accepts `timeZone` (an IANA id such as `America/Edmonton`) for
105
+ instant inputs (`Date`, numeric timestamps, and timestamp strings). Omit it to
106
+ keep browser-local absolute formatting and the existing duration-based relative
107
+ format. With an explicit zone, relative today/yesterday/tomorrow boundaries use
108
+ calendar days in that zone, including daylight saving transitions.
109
+
110
+ A bare `YYYY-MM-DD` string always denotes that calendar date, independent of the
111
+ viewer or shop zone. It formats in UTC, retains a date-only `datetime` attribute,
112
+ and ignores `showTime` because it contains no time. Relative calendar-date
113
+ labels compare it with today's date in the supplied zone (or the browser zone).
114
+ Impossible calendar dates render `fallback`. Invalid explicit zones used for
115
+ instant or relative formatting also render `fallback` rather than throwing.
116
+
117
+ ### Combobox form submission
118
+
119
+ A named `Combobox` submits its selected option value through a hidden native
120
+ input. Its visible search text and option label are display-only. With
121
+ `allowCustom`, typed text becomes the submitted value; otherwise searching keeps
122
+ the last committed selection. A named empty selection submits an empty string;
123
+ controls with no name or an empty name are omitted. Disabled controls, including
124
+ those inside disabled fieldsets, are omitted by native `FormData`. Native form
125
+ reset restores the initial selection and its label, unless reset is canceled.
126
+
127
+ ### MultiSelect form submission
128
+
129
+ A named `MultiSelect` submits one hidden native input per selected option value,
130
+ using repeated field names in selection order. Read them with `FormData.getAll`.
131
+ Option labels are display-only, and numeric option values submit as strings.
132
+ Empty selections, missing or empty names, and disabled controls (including
133
+ ancestor fieldsets) contribute no entries. Native reset restores the initial
134
+ selection unless reset is canceled.
135
+
136
+ ### Listbox form submission
137
+
138
+ A named `Listbox` submits one hidden native input containing its selected option
139
+ value, with numeric values encoded as strings. A named listbox without a
140
+ selection submits an empty string. Missing or empty names and disabled controls
141
+ (including ancestor fieldsets) are omitted. Native reset restores the initial
142
+ selection unless reset is canceled.
143
+
144
+ ### Status badge tones
145
+
146
+ `StatusBadge` accepts `tone="success"`, `"warning"`, `"danger"`, `"info"`, or
147
+ `"neutral"` for custom status vocabulary such as `awaiting_cert`. An explicit
148
+ tone overrides the built-in domain scheme while label, size, and outline variant
149
+ continue to work independently. Tones use the active theme's paired container
150
+ and text tokens. Without a tone, known domain statuses retain their existing
151
+ colors and unknown statuses retain the neutral fallback. `StatusTone` is exported
152
+ as a TypeScript type from the package root.
153
+
100
154
  ### Touch targets for Checkbox, Radio and Switch
101
155
 
102
156
  `Checkbox`, `Radio` and `Switch` have an invisible 44x44px hit area around the
@@ -229,6 +283,34 @@ Application-specific editors, maps, charts, media workbenches, and domain
229
283
  records remain composites built from this foundation rather than generic base
230
284
  components.
231
285
 
286
+ ## Native and enhanced forms
287
+
288
+ `Form` forwards native form attributes and Svelte attachments through its rest
289
+ props to the underlying `<form>`. For a SvelteKit action, adapt `enhance` with
290
+ `fromAction` and set `preventDefault={false}` so the enhancement handles submission:
291
+
292
+ ```svelte
293
+ <script lang="ts">
294
+ import { enhance } from '$app/forms';
295
+ import { fromAction } from 'svelte/attachments';
296
+ import { Form, Input } from '@happyvertical/smrt-ui/forms';
297
+ </script>
298
+
299
+ <Form method="POST" action="?/save" preventDefault={false} {@attach fromAction(enhance)}>
300
+ <Input name="displayName" />
301
+ <button type="submit">Save</button>
302
+ </Form>
303
+ ```
304
+
305
+ For a customized enhancement, pass its callback as a getter:
306
+ `{@attach fromAction(enhance, () => submitFunction)}`. The attachment runs on the
307
+ native element and cleans up when it unmounts; the interaction registry and
308
+ staged-review surface remain available. A component reference obtained with
309
+ `bind:this` exposes `getFormElement(): HTMLFormElement | null`, which returns
310
+ `null` before mount and after unmount. For ordinary browser GET/POST forms, use
311
+ `preventDefault={false}` without an attachment. The default remains `true` for
312
+ existing handler-driven forms.
313
+
232
314
  ## Agent-addressable forms
233
315
 
234
316
  `Form` can expose its controls to a chat, voice, tutorial, or test adapter
@@ -362,6 +444,130 @@ disabled, read-only, file, or password controls), and staging only ever
362
444
  creates reviewable proposals. smrt-svelte's `<Form webmcp>` and `FormScope`
363
445
  build their `*_stage_changes` tool from these.
364
446
 
447
+ ## Form retry
448
+
449
+ `createFormRetry()` is the browser half of `runOnce()` (`@happyvertical/smrt-core`)
450
+ for a SvelteKit enhanced form: a per-tab, per-form submission key, a refused
451
+ second submit while one is in flight, typed values kept across a validation or
452
+ transport failure, a reset after success only when the fields are unchanged
453
+ since submit, and opt-in restore after a reload (a file that cannot be restored
454
+ is reported, never silently dropped). It is framework-free and imports neither
455
+ Svelte nor SvelteKit; the submit function is typed against SvelteKit's
456
+ `SubmitFunction` shape structurally.
457
+
458
+ ```svelte
459
+ <script lang="ts">
460
+ import { enhance } from '$app/forms';
461
+ import { createFormRetry } from '@happyvertical/smrt-ui/form-retry';
462
+
463
+ const retry = createFormRetry({ form: 'report' });
464
+ </script>
465
+
466
+ <form method="POST" use:enhance={retry.enhance()} {@attach retry.attach}>
467
+ <input name="title" />
468
+ <button type="submit">Send</button>
469
+ {#if $retry.status === 'transport-error'}<p role="alert">Send it again unchanged.</p>{/if}
470
+ </form>
471
+ ```
472
+
473
+ With smrt-ui's own `Form`, attach the same two pieces to it (see
474
+ [Native and enhanced forms](#native-and-enhanced-forms)):
475
+ `<Form method="POST" preventDefault={false} {@attach fromAction(enhance, () => retry.enhance())} {@attach retry.attach}>`.
476
+
477
+ `/forms` re-exports the same API as the Svelte-free `/form-retry` entry. See the
478
+ [form retry guide](../../docs/content/form-retry.md) for the server half, the
479
+ per-result table, storage and private-window behaviour, and restore.
480
+
481
+ ## Camera and signature capture
482
+
483
+ `CameraCapture` takes a photo from the device camera and `SignaturePad` takes a
484
+ signature, each inside an ordinary form. Both live in `/forms`, need only
485
+ browser APIs, and post their file through a plain multipart form when given a
486
+ `name`; the page wires no hidden input and no submit handler.
487
+
488
+ ```svelte
489
+ <script lang="ts">
490
+ import { CameraCapture, SignaturePad } from '@happyvertical/smrt-ui/forms';
491
+ </script>
492
+
493
+ <form method="POST" enctype="multipart/form-data">
494
+ <CameraCapture name="photo" facingMode="environment" />
495
+ <SignaturePad name="signature" stylusOnly={settings.signatureStylusOnly} />
496
+ <button type="submit">Send</button>
497
+ </form>
498
+ ```
499
+
500
+ **CameraCapture** uses `getUserMedia` with a live preview. The user takes a
501
+ photo, reviews it, and can retake it before "Use photo" commits it. Committing
502
+ calls `onCapture({ blob, dataUrl })` and fills the named field (`photo.jpg` by
503
+ default; set `fileName`, `imageType`, and `quality` to change it). "Retake"
504
+ after a commit empties the field and calls `onClear`. The root's `data-state`
505
+ is one of `starting`, `streaming`, `reviewing`, `committed`, `off` (disabled),
506
+ `permission-denied`, `no-camera`, `unsupported`, `error`, or `fallback`. Each
507
+ problem state has its own copy and, where it can help, a "Try again" action.
508
+ `getUserMedia` exists only in a secure context, so a page served over plain
509
+ HTTP renders `unsupported`.
510
+
511
+ The stream's tracks are stopped on unmount, when `disabled` turns on, when
512
+ `facingMode` changes (the camera is then requested again), and as soon as a
513
+ frame is captured. A permission prompt answered after unmount or disable is
514
+ discarded and its stream released. The lifecycle lives in the framework-free
515
+ `createCameraSession()` (exported with `classifyGetUserMediaError()` and
516
+ `isCameraApiSupported()`), so it can be tested against a fake `MediaDevices`.
517
+
518
+ `fileInputFallback` is opt-in and off by default. With it on, browsers without
519
+ `getUserMedia` render an `<input type="file" accept="image/*" capture>` that
520
+ carries `name` itself (`disabled` blocks it with `aria-disabled` rather than
521
+ the native attribute, so a committed photo keeps posting). It opens the
522
+ operating system's picker, which has no
523
+ live preview and can offer the gallery, so it is not a substitute for the
524
+ camera flow. With it off, those browsers render the `unsupported` state.
525
+
526
+ **SignaturePad** draws on its own canvas. "Use signature" stays disabled until
527
+ an accepted stroke exists, then returns a PNG through `onCapture({ blob,
528
+ dataUrl })` and fills the named field (`signature.png` by default). A committed
529
+ pad is locked until "Clear", which empties the field and calls `onClear`.
530
+ `stylusOnly` is a prop the caller resolves; with it on, only `pointerType ===
531
+ 'pen'` draws. `isAcceptedPointerType()` and `mapPointerToCanvasPoint()` are
532
+ exported as pure functions. The pad is dark ink on white paper in every theme
533
+ and colour scheme, and the exported PNG is opaque white, so it reads the same
534
+ wherever it is shown.
535
+
536
+ **Native form posting.** The named field is a hidden `<input type="file">`
537
+ filled through `DataTransfer` (Chrome 60+, Firefox 62+, Safari 14.1+). Where
538
+ `DataTransfer` cannot be constructed or assigned, the field drops its `name`
539
+ and a capture-phase `formdata` listener appends the file while the browser
540
+ builds the request, which covers native navigation submits as well as
541
+ `new FormData(form)`. Where neither API exists, nothing is posted and
542
+ `onCapture` is the only channel. The root's `data-smrt-file-field` attribute
543
+ reports `data-transfer`, `formdata-event`, `file-input`, or `none`. Before a
544
+ commit the field posts what an empty native file input posts. `disabled`
545
+ freezes the controls but keeps a committed file in the submission; unmount the
546
+ component to drop it, or put it in a disabled `<fieldset>`, which leaves the
547
+ field out of the submission (committed or empty) in every posting strategy.
548
+
549
+ A reset of the owning form (`form.reset()`, a reset button, or SvelteKit
550
+ `enhance`'s `update()` after a success, including `createFormRetry()`'s
551
+ conditional reset) empties the field as it empties a native file input, in
552
+ every posting strategy. A committed photo or signature is discarded as
553
+ "Retake" or "Clear" would and `onClear` is called. A photo under review or
554
+ ink not yet committed is discarded too, without `onClear`, so the next entry
555
+ cannot attach the previous one's capture. `CameraCapture` returns to the live
556
+ camera (`off` while `disabled`, the emptied picker with `fileInputFallback`)
557
+ and supersedes a frame still encoding; with nothing held, a reset changes
558
+ nothing and never re-prompts for the camera. `SignaturePad` returns to blank,
559
+ unlocked paper. A reset clears both even while `disabled`, as it clears a
560
+ disabled native input, and a reset a later listener cancels still leaves the
561
+ component and the posted field both empty.
562
+
563
+ Text comes from the `ui.camera_capture.*` and `ui.signature_pad.*` i18n keys
564
+ and can be overridden per instance with `labels`. Capture is a touch flow, so
565
+ its actions (and the fallback picker) are touch targets at every density: at
566
+ least `--smrt-touch-target-min` (48px by default; see
567
+ [Touch density](#touch-density)), which ThemeProvider `overrides` can raise.
568
+ Both register with an enclosing `Form`'s interaction registry as non-readable,
569
+ non-writable `file` controls.
570
+
365
571
  ## DataTable controller
366
572
 
367
573
  `DataTable` can share one headless `DataTableController` between rendered
@@ -789,3 +995,95 @@ pnpm test
789
995
  pnpm build
790
996
  pnpm verify:pack
791
997
  ```
998
+
999
+ ### Touch density
1000
+
1001
+ Set `<ThemeProvider density="touch">` for floor tablets and phones, or set
1002
+ `density="touch"` on Input, Select, Textarea, Checkbox, Switch, RadioGroup,
1003
+ Button (including links), FilterChips, SegmentedControl, or DataTable. DataTable
1004
+ applies density to its sort buttons. Density is independent of existing `size`
1005
+ props, including native Input/Select sizes. Omitted density inherits; explicitly
1006
+ setting `density="comfortable"` opts that control out and keeps its usual size.
1007
+ RadioGroup expands each option's clickable label, and Checkbox/Switch expand
1008
+ the label hit area while preserving the visual mark.
1009
+
1010
+ `--smrt-touch-target-min` defaults to `48px` across all presets and works without
1011
+ a provider. Customize it globally with ThemeProvider's `overrides`, for example
1012
+ `overrides={{ '--smrt-touch-target-min': '56px' }}`. Touch targets grow with larger
1013
+ content; normal density keeps existing component sizing. `CameraCapture` and
1014
+ `SignaturePad` actions use `--smrt-touch-target-min` at every density, since
1015
+ capture is a touch flow. TenantNav sizing is
1016
+ tracked separately in [#3246](https://github.com/happyvertical/smrt/issues/3246).
1017
+
1018
+ ### Narrow DataTable
1019
+
1020
+ Set `responsiveMode="hide-columns"` to adapt to the table's container width.
1021
+ The default `responsiveBreakpoint={800}` includes both 768px tablets and 390px
1022
+ phones. Narrow mode budgets one column per `responsiveColumnMinWidth={160}`
1023
+ pixels, after reserving space for selection and expansion controls. Higher
1024
+ `column.responsive.priority` values survive first (missing/nonfinite values are
1025
+ zero); ties preserve declared display order. `responsive.keepVisible` columns
1026
+ always survive responsive collapse, while explicitly hidden columns stay hidden.
1027
+
1028
+ Retained cells wrap, and their desktop width and pinning settings resume when
1029
+ the container widens. Responsive presentation never changes controller state,
1030
+ sorting, selection, or persisted column visibility. Header groups and structural
1031
+ row colspans follow the retained columns. If keepVisible columns exceed the
1032
+ budget, all remain visible and share the available width. Custom cell/header
1033
+ snippets should fit their cells. Default `responsiveMode="scroll"` preserves
1034
+ existing horizontal scrolling.
1035
+
1036
+ ConfirmDialog opens a native modal dialog above existing Modal and Drawer surfaces.
1037
+ Its `message` accepts plain text or a Svelte snippet (including lists and emphasis);
1038
+ each instance owns its accessible title and description ids. Escape and backdrop
1039
+ clicks request `oncancel`; the parent controls `open`. The opener regains focus
1040
+ on close. Buttons remain disabled while `loading`, and Escape stays available.
1041
+ The native top layer replaces the previous fixed div; confirmation now stacks
1042
+ above an already open modal and makes its background inert. Escape is scoped to
1043
+ the active dialog rather than handled globally. Existing `open`, `loading` and
1044
+ action callbacks retain their controlled-state contracts.
1045
+
1046
+ Browser feedback contracts run with `pnpm --filter @happyvertical/smrt-ui test:e2e`
1047
+ (after installing Playwright Chromium, or setting
1048
+ `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` to a local Chromium executable).
1049
+
1050
+ ToastViewport follows the topmost native modal dialog and restores its original
1051
+ host when all modals close. Its live region, dismiss buttons and actions stay
1052
+ interactive inside Modal, Drawer and ConfirmDialog. Hover and focus independently
1053
+ pause auto-dismiss, then resume the remaining duration. Configure
1054
+ `createToaster({ successDuration: 0 })` to keep success records until dismissal;
1055
+ per-toast `duration: 0` remains supported. `inset` accepts a CSS length, `anchor`
1056
+ accepts a content-region element, and `top-center`/`bottom-center` positions center
1057
+ the viewport within that region (or the window). Anchor geometry follows resize
1058
+ and scroll. Custom injected Toaster implementations may optionally implement
1059
+ `pause(id, reason)` and `resume(id, reason)` to support interaction pausing.
1060
+
1061
+ ### SegmentedControl forms
1062
+
1063
+ SegmentedControl renders native radio inputs. Set `name` to post the selected
1064
+ option value with ordinary form submission or FormData; numeric values post as
1065
+ strings while bound values retain their declared type. Unselected, unknown,
1066
+ disabled-option, disabled-control and disabled-fieldset values are omitted.
1067
+ `required` uses native form validation and accepts numeric zero. Nameless controls
1068
+ validate without posting a generated field. Arrow keys cycle enabled options;
1069
+ Home/End select the first/last enabled option. Touch density applies to the full
1070
+ clickable segment.
1071
+
1072
+ Native form reset restores the initial bound value if that option is still
1073
+ available and enabled; otherwise it clears the selection. Canceling the reset
1074
+ event preserves the current selection. Reset does not fire `onvaluechange`,
1075
+ matching native controls; bind:value reflects it.
1076
+
1077
+ ### Application icons
1078
+
1079
+ Icon includes the original menu/search/chevron/action glyphs plus `alert`,
1080
+ `warning`, `info`, `home`, `user`, `settings`, `trash`, `edit`, `calendar`,
1081
+ `clock`, `camera`, `upload`, and `download`. Import `registerIcons` from
1082
+ `@happyvertical/smrt-ui` to install an application SVG path map at startup.
1083
+ Registrations update mounted icons; the returned cleanup function removes that
1084
+ registration and restores the previous active set. Last active registration wins,
1085
+ and an explicit Icon `path` prop wins over every named set. Unknown names retain
1086
+ their empty shape. Names and paths must be nonempty strings; invalid mixed sets
1087
+ are rejected atomically. Sets are snapshotted, so later caller mutations do not
1088
+ change glyphs. Register static application assets, never request or identity data,
1089
+ and install the same application set for SSR and client hydration.
@@ -35,6 +35,7 @@ import {
35
35
  type DataTableResolvedColumn,
36
36
  resolveDataTableLayout,
37
37
  } from './DataTableLayout.js';
38
+ import { responsiveColumns } from './DataTableResponsive.js';
38
39
  import {
39
40
  maximumDataTableVirtualScrollTop,
40
41
  resolveDataTableVirtualWindow,
@@ -103,6 +104,10 @@ let {
103
104
  empty,
104
105
  rowClass,
105
106
  size = 'md',
107
+ density,
108
+ responsiveMode = 'scroll',
109
+ responsiveBreakpoint = 800,
110
+ responsiveColumnMinWidth = 160,
106
111
  striped = false,
107
112
  hoverable = true,
108
113
  stickyHeader = false,
@@ -775,6 +780,7 @@ function updateOverflowState() {
775
780
  const container = tableContainer;
776
781
  if (!container) return;
777
782
 
783
+ containerWidth = container.clientWidth;
778
784
  const maxScrollLeft = Math.max(
779
785
  0,
780
786
  container.scrollWidth - container.clientWidth,
@@ -836,9 +842,30 @@ function setIndeterminate(node: HTMLInputElement, value: boolean) {
836
842
  };
837
843
  }
838
844
 
839
- const dataTableLayout = $derived(
845
+ let containerWidth = $state(0);
846
+ const isNarrow = $derived(
847
+ responsiveMode === 'hide-columns' &&
848
+ containerWidth > 0 &&
849
+ containerWidth <=
850
+ (Number.isFinite(responsiveBreakpoint) && responsiveBreakpoint > 0
851
+ ? responsiveBreakpoint
852
+ : 800),
853
+ );
854
+ const declaredLayout = $derived(
840
855
  resolveDataTableLayout(columns, constrainedLayoutState, measuredColumnWidths),
841
856
  );
857
+ const dataTableLayout = $derived(
858
+ isNarrow
859
+ ? resolveDataTableLayout(
860
+ responsiveColumns(
861
+ declaredLayout.columns.map((entry) => entry.column),
862
+ containerWidth - (selectable ? 48 : 0) - (expandedContent ? 48 : 0),
863
+ responsiveColumnMinWidth,
864
+ ),
865
+ { ...constrainedLayoutState, columnWidths: [], columnPinning: [] },
866
+ )
867
+ : declaredLayout,
868
+ );
842
869
  const visibleColumns = $derived(dataTableLayout.columns);
843
870
  const dataBodyStructuralRows = $derived(
844
871
  structuralRows.filter((row) => row.kind !== 'footer'),
@@ -1222,6 +1249,7 @@ function getCellValue(row: T, column: DataTableColumn<T>): unknown {
1222
1249
  function widthForColumn(
1223
1250
  column: DataTableResolvedColumn<T>,
1224
1251
  ): string | undefined {
1252
+ if (isNarrow) return undefined;
1225
1253
  return column.width === undefined ? column.column.width : `${column.width}px`;
1226
1254
  }
1227
1255
 
@@ -1390,10 +1418,18 @@ $effect(() => {
1390
1418
  void displayRows.length;
1391
1419
  if (!container) return;
1392
1420
 
1421
+ let resizeFrame: number | undefined;
1422
+ const scheduleOverflowUpdate = () => {
1423
+ if (resizeFrame !== undefined) return;
1424
+ resizeFrame = requestAnimationFrame(() => {
1425
+ resizeFrame = undefined;
1426
+ updateOverflowState();
1427
+ });
1428
+ };
1393
1429
  const resizeObserver =
1394
1430
  typeof ResizeObserver === 'undefined'
1395
1431
  ? undefined
1396
- : new ResizeObserver(updateOverflowState);
1432
+ : new ResizeObserver(scheduleOverflowUpdate);
1397
1433
  resizeObserver?.observe(container);
1398
1434
  const table = container.querySelector('table');
1399
1435
  if (table) resizeObserver?.observe(table);
@@ -1402,6 +1438,7 @@ $effect(() => {
1402
1438
 
1403
1439
  return () => {
1404
1440
  resizeObserver?.disconnect();
1441
+ if (resizeFrame !== undefined) cancelAnimationFrame(resizeFrame);
1405
1442
  window.removeEventListener('resize', updateOverflowState);
1406
1443
  };
1407
1444
  });
@@ -1411,7 +1448,9 @@ $effect(() => {
1411
1448
  <!-- svelte-ignore a11y_no_noninteractive_tabindex: the region provides horizontal or virtual keyboard scrolling. -->
1412
1449
  <div
1413
1450
  bind:this={tableContainer}
1451
+ data-density={density}
1414
1452
  class="data-table-container"
1453
+ class:data-table-container--narrow={isNarrow}
1415
1454
  class:data-table-container--sticky={stickyHeader || virtualizedBody}
1416
1455
  class:data-table-container--overflowing={hasHorizontalOverflow}
1417
1456
  class:data-table-container--virtualized={virtualizationWindow.enabled}
@@ -1503,8 +1542,8 @@ $effect(() => {
1503
1542
  data-keep-visible={column.responsive?.keepVisible ? 'true' : undefined}
1504
1543
  use:observeColumnWidth={column.id}
1505
1544
  style:width={widthForColumn(resolvedColumn)}
1506
- style:min-width={column.minWidth}
1507
- style:max-width={column.maxWidth}
1545
+ style:min-width={isNarrow ? undefined : column.minWidth}
1546
+ style:max-width={isNarrow ? undefined : column.maxWidth}
1508
1547
  style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
1509
1548
  style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
1510
1549
  style:text-align={column.align}
@@ -1560,7 +1599,7 @@ $effect(() => {
1560
1599
  {:else}
1561
1600
  {column.label}
1562
1601
  {/if}
1563
- {#if column.resizable}
1602
+ {#if column.resizable && !isNarrow}
1564
1603
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions: a focusable ARIA separator is the resize control. -->
1565
1604
  <span
1566
1605
  class="data-table__resize-handle"
@@ -1686,8 +1725,8 @@ $effect(() => {
1686
1725
  data-responsive-priority={column.responsive?.priority}
1687
1726
  data-keep-visible={column.responsive?.keepVisible ? 'true' : undefined}
1688
1727
  style:width={widthForColumn(resolvedColumn)}
1689
- style:min-width={column.minWidth}
1690
- style:max-width={column.maxWidth}
1728
+ style:min-width={isNarrow ? undefined : column.minWidth}
1729
+ style:max-width={isNarrow ? undefined : column.maxWidth}
1691
1730
  style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
1692
1731
  style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
1693
1732
  style:text-align={column.align}
@@ -1743,8 +1782,8 @@ $effect(() => {
1743
1782
  scope="row"
1744
1783
  data-column-id={column.id}
1745
1784
  style:width={widthForColumn(resolvedColumn)}
1746
- style:min-width={column.minWidth}
1747
- style:max-width={column.maxWidth}
1785
+ style:min-width={isNarrow ? undefined : column.minWidth}
1786
+ style:max-width={isNarrow ? undefined : column.maxWidth}
1748
1787
  style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
1749
1788
  style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
1750
1789
  style:text-align={column.align}
@@ -1764,8 +1803,8 @@ $effect(() => {
1764
1803
  class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
1765
1804
  data-column-id={column.id}
1766
1805
  style:width={widthForColumn(resolvedColumn)}
1767
- style:min-width={column.minWidth}
1768
- style:max-width={column.maxWidth}
1806
+ style:min-width={isNarrow ? undefined : column.minWidth}
1807
+ style:max-width={isNarrow ? undefined : column.maxWidth}
1769
1808
  style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
1770
1809
  style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
1771
1810
  style:text-align={column.align}
@@ -1807,8 +1846,8 @@ $effect(() => {
1807
1846
  scope="row"
1808
1847
  data-column-id={column.id}
1809
1848
  style:width={widthForColumn(resolvedColumn)}
1810
- style:min-width={column.minWidth}
1811
- style:max-width={column.maxWidth}
1849
+ style:min-width={isNarrow ? undefined : column.minWidth}
1850
+ style:max-width={isNarrow ? undefined : column.maxWidth}
1812
1851
  style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
1813
1852
  style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
1814
1853
  style:text-align={column.align}
@@ -1828,8 +1867,8 @@ $effect(() => {
1828
1867
  class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
1829
1868
  data-column-id={column.id}
1830
1869
  style:width={widthForColumn(resolvedColumn)}
1831
- style:min-width={column.minWidth}
1832
- style:max-width={column.maxWidth}
1870
+ style:min-width={isNarrow ? undefined : column.minWidth}
1871
+ style:max-width={isNarrow ? undefined : column.maxWidth}
1833
1872
  style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
1834
1873
  style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
1835
1874
  style:text-align={column.align}
@@ -2292,4 +2331,10 @@ $effect(() => {
2292
2331
  animation: none;
2293
2332
  }
2294
2333
  }
2334
+ .data-table-container[data-density='touch'] { --smrt-control-target-min: var(--smrt-touch-target-min, 48px); }
2335
+ .data-table-container[data-density='comfortable'] { --smrt-control-target-min: 0px; }
2336
+ .data-table__sort-button { min-height: var(--smrt-control-target-min, 0px); min-width: var(--smrt-control-target-min, 0px); }
2337
+ .data-table-container--narrow .data-table { table-layout: fixed; }
2338
+ .data-table-container--narrow .data-table__cell { white-space: normal; overflow-wrap: anywhere; }
2339
+ .data-table-container--narrow .data-table__sort-button { max-width: 100%; text-align: left; white-space: normal; overflow-wrap: anywhere; }
2295
2340
  </style>
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/data/DataTable.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,OAAO,EAAa,KAAK,OAAO,EAAiB,MAAM,QAAQ,CAAC;AAmChE,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAGf,MAAM,YAAY,CAAC;AAIpB,UAAU,aAAa,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;IAClD,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CACZ;QAAC;YAAE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;YAAC,GAAG,EAAE,CAAC,CAAC;YAAC,KAAK,EAAE,OAAO,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE;KAAC,CACxE,CAAC;CACH;AAYC,iBAAS,QAAQ,CAAC,CAAC;WAwmDQ,aAAa,CAAC,CAAC,CAAC;;;;;EAAgH;AAC7J,cAAM,iBAAiB,CAAC,CAAC;IACrB,KAAK,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAChD,MAAM,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClD,KAAK,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAChD,QAAQ;IACR,OAAO;CACV;AAED,UAAU,qBAAqB;IAC3B,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;KAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5X,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAC3H,YAAY,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;CACjE;AACD,QAAA,MAAM,SAAS,EAAE,qBAAmC,CAAC;AACnC,KAAK,SAAS,CAAC,CAAC,IAAI,YAAY,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"DataTable.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/data/DataTable.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,OAAO,EAAa,KAAK,OAAO,EAAiB,MAAM,QAAQ,CAAC;AAoChE,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAGf,MAAM,YAAY,CAAC;AAIpB,UAAU,aAAa,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;IAClD,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CACZ;QAAC;YAAE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;YAAC,GAAG,EAAE,CAAC,CAAC;YAAC,KAAK,EAAE,OAAO,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE;KAAC,CACxE,CAAC;CACH;AAYC,iBAAS,QAAQ,CAAC,CAAC;WA6oDQ,aAAa,CAAC,CAAC,CAAC;;;;;EAAgH;AAC7J,cAAM,iBAAiB,CAAC,CAAC;IACrB,KAAK,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAChD,MAAM,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClD,KAAK,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAChD,QAAQ;IACR,OAAO;CACV;AAED,UAAU,qBAAqB;IAC3B,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;KAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5X,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAC3H,YAAY,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;CACjE;AACD,QAAA,MAAM,SAAS,EAAE,qBAAmC,CAAC;AACnC,KAAK,SAAS,CAAC,CAAC,IAAI,YAAY,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,eAAe,SAAS,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { DataTableColumn } from './types.js';
2
+ /** Choose narrow-mode columns without changing their declared display order. */
3
+ export declare function responsiveColumns<T>(columns: readonly DataTableColumn<T>[], width: number, minimumWidth: number): DataTableColumn<T>[];
4
+ //# sourceMappingURL=DataTableResponsive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DataTableResponsive.d.ts","sourceRoot":"","sources":["../../../src/components/data/DataTableResponsive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,SAAS,eAAe,CAAC,CAAC,CAAC,EAAE,EACtC,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GACnB,eAAe,CAAC,CAAC,CAAC,EAAE,CAuBtB"}
@@ -0,0 +1,22 @@
1
+ /** Choose narrow-mode columns without changing their declared display order. */
2
+ export function responsiveColumns(columns, width, minimumWidth) {
3
+ const target = Number.isFinite(minimumWidth) && minimumWidth > 0 ? minimumWidth : 160;
4
+ const budget = Math.max(1, Math.floor(Math.max(0, width) / target));
5
+ const retained = new Set(columns
6
+ .filter((column) => column.responsive?.keepVisible)
7
+ .map((column) => column.id));
8
+ const ranked = columns
9
+ .map((column, index) => ({ column, index }))
10
+ .sort((a, b) => {
11
+ const priority = (column) => Number.isFinite(column.responsive?.priority)
12
+ ? (column.responsive?.priority ?? 0)
13
+ : 0;
14
+ return priority(b.column) - priority(a.column) || a.index - b.index;
15
+ });
16
+ for (const { column } of ranked) {
17
+ if (retained.size >= budget)
18
+ break;
19
+ retained.add(column.id);
20
+ }
21
+ return columns.filter((column) => retained.has(column.id));
22
+ }