@marianmeres/stuic 3.171.0 → 3.173.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 (93) hide show
  1. package/AGENTS.md +10 -8
  2. package/API.md +146 -2
  3. package/README.md +15 -3
  4. package/dist/actions/resizable-width.svelte.d.ts +14 -28
  5. package/dist/actions/resizable-width.svelte.js +16 -171
  6. package/dist/attachments/index.d.ts +1 -0
  7. package/dist/attachments/index.js +1 -0
  8. package/dist/attachments/resizable.d.ts +113 -0
  9. package/dist/attachments/resizable.fixture.svelte +53 -0
  10. package/dist/attachments/resizable.fixture.svelte.d.ts +10 -0
  11. package/dist/attachments/resizable.js +295 -0
  12. package/dist/components/AssetsPreview/AssetsPreview.fixture.svelte +1 -1
  13. package/dist/components/Calendar/Calendar.svelte +799 -0
  14. package/dist/components/Calendar/Calendar.svelte.d.ts +135 -0
  15. package/dist/components/Calendar/README.md +294 -0
  16. package/dist/components/Calendar/calendar-i18n-sk.d.ts +20 -0
  17. package/dist/components/Calendar/calendar-i18n-sk.js +46 -0
  18. package/dist/components/Calendar/calendar-i18n.d.ts +61 -0
  19. package/dist/components/Calendar/calendar-i18n.js +73 -0
  20. package/dist/components/Calendar/index.css +307 -0
  21. package/dist/components/Calendar/index.d.ts +5 -0
  22. package/dist/components/Calendar/index.js +5 -0
  23. package/dist/components/Calendar/iso-date.d.ts +107 -0
  24. package/dist/components/Calendar/iso-date.js +247 -0
  25. package/dist/components/CommandMenu/CommandMenu.fixture.svelte +1 -1
  26. package/dist/components/Input/FieldDate.svelte +349 -0
  27. package/dist/components/Input/FieldDate.svelte.d.ts +79 -0
  28. package/dist/components/Input/FieldDateRange.svelte +373 -0
  29. package/dist/components/Input/FieldDateRange.svelte.d.ts +90 -0
  30. package/dist/components/Input/README.md +149 -16
  31. package/dist/components/Input/_internal/FieldDateShell.svelte +327 -0
  32. package/dist/components/Input/_internal/FieldDateShell.svelte.d.ts +66 -0
  33. package/dist/components/Input/index.css +119 -0
  34. package/dist/components/Input/index.d.ts +2 -0
  35. package/dist/components/Input/index.js +2 -0
  36. package/dist/components/ModalDialog/ModalDialog.fixture.svelte +1 -1
  37. package/dist/components/RangeSlider/README.md +291 -0
  38. package/dist/components/RangeSlider/RangeSlider.svelte +763 -0
  39. package/dist/components/RangeSlider/RangeSlider.svelte.d.ts +130 -0
  40. package/dist/components/RangeSlider/i18n-sk.d.ts +17 -0
  41. package/dist/components/RangeSlider/i18n-sk.js +19 -0
  42. package/dist/components/RangeSlider/i18n.d.ts +33 -0
  43. package/dist/components/RangeSlider/i18n.js +41 -0
  44. package/dist/components/RangeSlider/index.css +430 -0
  45. package/dist/components/RangeSlider/index.d.ts +3 -0
  46. package/dist/components/RangeSlider/index.js +3 -0
  47. package/dist/components/Rating/README.md +206 -0
  48. package/dist/components/Rating/Rating.svelte +355 -0
  49. package/dist/components/Rating/Rating.svelte.d.ts +82 -0
  50. package/dist/components/Rating/i18n-sk.d.ts +17 -0
  51. package/dist/components/Rating/i18n-sk.js +21 -0
  52. package/dist/components/Rating/i18n.d.ts +34 -0
  53. package/dist/components/Rating/i18n.js +42 -0
  54. package/dist/components/Rating/index.css +170 -0
  55. package/dist/components/Rating/index.d.ts +3 -0
  56. package/dist/components/Rating/index.js +3 -0
  57. package/dist/components/SlidingPanels/SlidingPanels.fixture.svelte +1 -1
  58. package/dist/components/SplitPane/README.md +169 -0
  59. package/dist/components/SplitPane/SplitPane.svelte +202 -0
  60. package/dist/components/SplitPane/SplitPane.svelte.d.ts +67 -0
  61. package/dist/components/SplitPane/i18n-sk.d.ts +17 -0
  62. package/dist/components/SplitPane/i18n-sk.js +18 -0
  63. package/dist/components/SplitPane/i18n.d.ts +31 -0
  64. package/dist/components/SplitPane/i18n.js +39 -0
  65. package/dist/components/SplitPane/index.css +153 -0
  66. package/dist/components/SplitPane/index.d.ts +3 -0
  67. package/dist/components/SplitPane/index.js +3 -0
  68. package/dist/components/WithSidePanel/README.md +19 -16
  69. package/dist/icons/index.d.ts +3 -0
  70. package/dist/icons/index.js +4 -0
  71. package/dist/index.css +4 -0
  72. package/dist/index.d.ts +4 -0
  73. package/dist/index.js +4 -0
  74. package/docs/_archive/README.md +10 -0
  75. package/docs/{component-testing → _archive/component-testing}/00-overview-and-roadmap.md +8 -6
  76. package/docs/{component-testing → _archive/component-testing}/01-framework-setup.md +4 -2
  77. package/docs/{component-testing → _archive/component-testing}/03-component-coverage-roadmap.md +3 -1
  78. package/docs/{component-testing → _archive/component-testing}/04-hard-cases-and-e2e.md +5 -3
  79. package/docs/{component-testing → _archive/component-testing}/05-ci.md +2 -0
  80. package/docs/{component-testing → _archive/component-testing}/PROGRESS.md +3 -1
  81. package/docs/_archive/component-testing/README.md +28 -0
  82. package/docs/{maybe-todo.md → _archive/maybe-todo.md} +24 -13
  83. package/docs/{upgrading.md → _archive/upgrading.md} +2 -0
  84. package/docs/architecture.md +19 -11
  85. package/docs/domains/actions.md +21 -20
  86. package/docs/domains/attachments.md +72 -9
  87. package/docs/domains/components.md +195 -49
  88. package/docs/domains/utils.md +2 -1
  89. package/docs/tasks.md +19 -9
  90. package/docs/{component-testing/02-test-conventions.md → testing-components.md} +30 -31
  91. package/docs/testing.md +3 -3
  92. package/package.json +6 -5
  93. package/docs/component-testing/README.md +0 -38
package/AGENTS.md CHANGED
@@ -23,10 +23,10 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 74 component directories
27
- ├── actions/ # 15 Svelte actions (use: directives)
26
+ ├── components/ # 78 component directories
27
+ ├── actions/ # 16 Svelte actions (use: directives)
28
28
  ├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
29
- ├── utils/ # 45 utility modules
29
+ ├── utils/ # 55 utility modules (48 on the barrel)
30
30
  ├── icons/ # Icon re-exports from @marianmeres/icons-fns
31
31
  ├── css/ # CSS-only presets (ratio-locked frame / letterbox)
32
32
  ├── index.css # Centralized CSS imports
@@ -39,7 +39,7 @@ src/lib/
39
39
  > attachments in `src/lib/attachments/` (export from its `index.ts`). The existing `actions/`
40
40
  > are kept as-is for back-compat; no need to migrate them.
41
41
 
42
- Theme CSS files are not bundled in this package — they're provided by `@marianmeres/design-tokens/css/*.css` (42 themes) and imported by `src/lib/index.css`.
42
+ Theme CSS files are not bundled in this package — they're provided by `@marianmeres/design-tokens/css/*.css` (54 themes) and imported by `src/lib/index.css`.
43
43
 
44
44
  ---
45
45
 
@@ -126,15 +126,17 @@ Global tokens that control cross-component visual properties. Defined in `src/li
126
126
  - [Architecture](./docs/architecture.md) — System design, data flow
127
127
  - [Conventions](./docs/conventions.md) — Code standards, patterns
128
128
  - [Tasks](./docs/tasks.md) — Common procedures
129
+ - [Testing](./docs/testing.md) — What we test, what we don't, the two-project split
130
+ - [Component test conventions](./docs/testing-components.md) — How to write a `*.svelte.test.ts`
129
131
 
130
132
  ### Domain Docs
131
133
 
132
- - [Components](./docs/domains/components.md) — 74 component directories, Props pattern, snippets
134
+ - [Components](./docs/domains/components.md) — 78 component directories, Props pattern, snippets
133
135
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
134
136
  - [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
135
- - [Actions](./docs/domains/actions.md) — 15 Svelte directives
137
+ - [Actions](./docs/domains/actions.md) — 16 Svelte directives
136
138
  - [Attachments](./docs/domains/attachments.md) — `{@attach}` DOM helpers (preferred for new ones)
137
- - [Utils](./docs/domains/utils.md) — 45 utility modules
139
+ - [Utils](./docs/domains/utils.md) — 55 utility modules
138
140
 
139
141
  ### Reference
140
142
 
@@ -151,7 +153,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
151
153
  | `src/lib/index.css` | CSS entry point |
152
154
  | `src/lib/index.ts` | JS entry point |
153
155
  | `src/lib/utils/design-tokens.ts` | Re-exports from `@marianmeres/design-tokens` |
154
- | `@marianmeres/design-tokens/css/*.css` | Theme CSS files (42 themes, `--stuic-` prefix) |
156
+ | `@marianmeres/design-tokens/css/*.css` | Theme CSS files (54 themes, `--stuic-` prefix) |
155
157
  | `src/lib/components/Button/` | Reference component |
156
158
 
157
159
  ---
package/API.md CHANGED
@@ -114,6 +114,25 @@ Expandable sections with exclusive or multi-open modes.
114
114
 
115
115
  Responsive two-column layout with collapsible side panel, resizable width, and mobile-aware slide transitions.
116
116
 
117
+ #### `SplitPane`
118
+
119
+ Two panes with a draggable, keyboard-operable separator (ARIA window splitter) — `horizontal` (side by side, a width) or `vertical` (stacked, a height); nestable. Built on the `resizable` attachment.
120
+
121
+ | Prop | Type | Default | Description |
122
+ | --------------- | ---------------------------- | -------------- | ------------------------------------------------------------------------- |
123
+ | `start` / `end` | `Snippet` | — | The two panes' content |
124
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Resize a width (side by side) or a height (stacked) |
125
+ | `primary` | `"start" \| "end"` | `"start"` | Which pane carries the size; the other one fills the rest |
126
+ | `size` | `number` | `50` | Bindable size of the primary pane in `units`; a stored size wins on mount |
127
+ | `units` | `"%" \| "px"` | `"%"` | Of the container, or px |
128
+ | `min` / `max` | `number` | `0` | Bounds in `units` (`0` = none) |
129
+ | `key` | `string \| number` | — | Persist the size (`storage`: `"session"` default, or `"local"`) |
130
+ | `disabled` | `boolean` | `false` | Inert separator, layout kept |
131
+ | `label` / `t` | `string` / `TranslateFn` | `"Resize"` | Accessible name of the separator (Slovak catalog bundled) |
132
+ | `onResize` | `(info) => void` | — | Every applied size |
133
+
134
+ Imperative `reset()` restores the mount size. Class slots `startClass` / `endClass` / `separatorClass`.
135
+
117
136
  #### `Collapsible`
118
137
 
119
138
  Expandable/collapsible content sections.
@@ -329,6 +348,96 @@ Money input whose canonical bindable `value` is an **integer in minor units** (e
329
348
 
330
349
  The `formatMinorUnits`, `parseToMinorUnits`, and `money` helpers are exported for display elsewhere.
331
350
 
351
+ #### `Calendar`
352
+
353
+ Accessible month grid for picking a single date (`value`) or an inclusive range (`start` + `end`) — every value an ISO `YYYY-MM-DD` string. Roving-tabindex keyboard grid (arrows, Home/End, PageUp/PageDown ± Shift, Enter), `min` / `max` / `isDateDisabled`, several `months` side by side, `captionLayout="dropdown"` (native month + year selects), ISO week numbers, fixed 6-week height, a `renderDay` snippet, `locale` for the Intl day/month names and `t` for the UI texts (Slovak bundled). Date math from `@marianmeres/calendar-utils`. Used inside `FieldDate` / `FieldDateRange`; usable inline on its own.
354
+
355
+ | Prop | Type | Default | Description |
356
+ | ------------------------ | ------------------------------------ | ---------- | ---------------------------------------------------------------------- |
357
+ | `mode` | `"single" \| "range"` | `"single"` | Bind `value`, or `start` + `end` |
358
+ | `value` | `string \| null` | `null` | Bindable selected date (single) |
359
+ | `start`, `end` | `string \| null` | `null` | Bindable range ends (range) |
360
+ | `view` | `{ year, month }` | selection | Bindable first displayed month |
361
+ | `months` | `number` | `1` | Consecutive months side by side |
362
+ | `min`, `max` | `string \| null` | — | Selectable window (inclusive); days and navigation beyond are disabled |
363
+ | `isDateDisabled` | `(iso, cell) => boolean` | — | Blackout individual days |
364
+ | `weekStartsOn` | `1..7` | `1` | First day of the week (ISO, 1 = Monday) |
365
+ | `locale`, `zone` | `string` | browser | Intl names / which day is "today" |
366
+ | `captionLayout` | `"label" \| "dropdown"` | `"label"` | Caption text, or month + year selects |
367
+ | `showWeekNumbers` | `boolean` | `false` | ISO week column |
368
+ | `fixedWeeks` | `boolean` | `false` | Always 6 rows |
369
+ | `showToday`, `showClear` | `boolean` | `false` | Footer buttons |
370
+ | `onSelect` | `(value) => void` | — | Single mode pick / clear |
371
+ | `onRangeChange` | `({ start, end, complete }) => void` | — | Range mode pick / clear |
372
+ | `onViewChange` | `(view) => void` | — | Displayed month changed |
373
+
374
+ ```svelte
375
+ <Calendar mode="range" bind:start bind:end months={2} min={todayIso()} />
376
+ ```
377
+
378
+ Exports: `Calendar`, `CalendarProps`, `CalendarMode`, `CalendarCaptionLayout`, `CalendarDayState`, `CalendarRangeChange`, `CalendarDayCell`, `CalendarWeekday`, `createCalendarT`, `CALENDAR_MESSAGES_EN`, `CALENDAR_MESSAGES_SK`, and the ISO date helpers `isIsoDate`, `parseIsoDate`, `normalizeIsoDate`, `toIsoDate`, `todayIso`, `addDaysIso`, `addMonthsIso`, `compareIso`, `daysBetweenIso`, `rangeLengthIso`, `formatIsoDate`, `formatIsoDateRange` (types `IsoDate`, `IsoDateParts`, `YearMonth`).
379
+
380
+ #### `FieldDate`
381
+
382
+ Single-date field around `Calendar`: a trigger button showing the formatted date opens the calendar in a native dialog (default), or the calendar renders inline (`embedded`). The bound `value` is an ISO `YYYY-MM-DD` string; `name` goes on a hidden input carrying it, so the form submits the ISO date regardless of the display format. `required` / valid ISO / `min` / `max` / `isDateDisabled` are enforced by the field's validator (hidden inputs never validate natively).
383
+
384
+ | Prop | Type | Default | Description |
385
+ | --------------- | ---------------------------- | ------------------------- | ----------------------------------------------------------- |
386
+ | `value` | `string \| null` | `null` | Bindable ISO date |
387
+ | `name` | `string` | — | Hidden input name |
388
+ | `embedded` | `boolean` | `false` | Inline calendar instead of trigger + dialog |
389
+ | `closeOnSelect` | `boolean` | `true` | Close the dialog after a pick; `false` adds a "Done" button |
390
+ | `placeholder` | `string` | `t("placeholder_date")` | Trigger text while empty |
391
+ | `clearable` | `boolean` | `true` | Trailing × (dialog) / footer "Clear" (embedded) |
392
+ | `locale` | `string` | browser | Display text + calendar names |
393
+ | `formatOptions` | `Intl.DateTimeFormatOptions` | `{ dateStyle: "medium" }` | Display text format |
394
+ | `format` | `(iso) => string` | — | Custom display text |
395
+ | `months` | `number` | `1` | Months side by side |
396
+ | `calendarProps` | `Partial<CalendarProps>` | — | Anything else for the inner calendar |
397
+ | `validate` | `boolean \| ValidateOptions` | enabled | `false` disables the built-in guard; object merges |
398
+ | `t` | `TranslateFn` | English | i18n (see `createCalendarT`) |
399
+ | `onChange` | `(value) => void` | — | After a pick / clear through the UI |
400
+
401
+ Also forwards `min`, `max`, `isDateDisabled`, `weekStartsOn`, `weekendDays`, `zone`, `captionLayout`, `yearRange`, `showWeekNumbers`, `showOutsideDays`, `fixedWeeks`, `showToday`, `renderDay` to the calendar, and takes the usual field props (`label`, `description`, `renderSize`, `required`, `disabled`, `labelLeft`, class props, …). Methods: the imperative validate API plus `open()` / `close()`.
402
+
403
+ ```svelte
404
+ <FieldDate
405
+ bind:value={delivery}
406
+ label="Delivery date"
407
+ name="delivery"
408
+ min={todayIso()}
409
+ required
410
+ />
411
+ ```
412
+
413
+ Exports: `FieldDate`, `FieldDateProps`, `FieldDateCalendarProps`.
414
+
415
+ #### `FieldDateRange`
416
+
417
+ Date-range field around a range-mode `Calendar`: same trigger + dialog / `embedded` presentations as `FieldDate`, binding `start` + `end` and submitting two hidden inputs (`nameStart`, `nameEnd`). First pick anchors, second completes (either order), a complete range closes the dialog (`closeOnSelect`). A half range fails validation whether or not the field is `required`. The default `months={2}` collapses to one below the `md` breakpoint.
418
+
419
+ | Prop | Type | Default | Description |
420
+ | ---------------------- | -------------------------- | ------- | ---------------------------------------- |
421
+ | `start`, `end` | `string \| null` | `null` | Bindable ISO range ends |
422
+ | `nameStart`, `nameEnd` | `string` | — | Hidden input names |
423
+ | `format` | `(start, end) => string` | — | Custom display text for a complete range |
424
+ | `months` | `number` | `2` | Months side by side (1 below `md`) |
425
+ | `onChange` | `({ start, end }) => void` | — | After a pick / clear through the UI |
426
+
427
+ Everything else as `FieldDate`.
428
+
429
+ ```svelte
430
+ <FieldDateRange
431
+ bind:start={from}
432
+ bind:end={to}
433
+ label="Stay"
434
+ nameStart="from"
435
+ nameEnd="to"
436
+ />
437
+ ```
438
+
439
+ Exports: `FieldDateRange`, `FieldDateRangeProps`, `FieldDateRangeValue`.
440
+
332
441
  #### `FieldTextarea`
333
442
 
334
443
  Multi-line text input.
@@ -1516,10 +1625,12 @@ Visual feedback (class toggle) on drag-over.
1516
1625
 
1517
1626
  ### `resizableWidth`
1518
1627
 
1519
- Make an element's width draggable.
1628
+ Make an element's width draggable. A thin wrapper over the `resizable` attachment (`axis: "x"`, see [Attachments](#attachments)) — the handle is a keyboard-operable `role="separator"`, `min` / `max` clamp, `key` persists.
1520
1629
 
1521
1630
  ```svelte
1522
- <div use:resizableWidth={() => ({ minWidth: 200, maxWidth: 600 })}>Resizable</div>
1631
+ <div use:resizableWidth={() => ({ initial: 300, min: 200, max: 600, key: "sidebar" })}>
1632
+ Resizable
1633
+ </div>
1523
1634
  ```
1524
1635
 
1525
1636
  ### `trim`
@@ -1712,6 +1823,37 @@ When a step has `selector`, the tour uses `document.querySelector(selector)` to
1712
1823
 
1713
1824
  ---
1714
1825
 
1826
+ ## Attachments
1827
+
1828
+ Svelte `{@attach}` helpers — the preferred form for new DOM behavior (reactive, composable, forwardable through components). Imported from the package root like everything else.
1829
+
1830
+ ### `autoHeight`
1831
+
1832
+ Animates the host's height to its single child's natural height (pair it with a CSS `height` transition).
1833
+
1834
+ ```svelte
1835
+ <div class="viewport" {@attach autoHeight}>
1836
+ <div>…variable-height content…</div>
1837
+ </div>
1838
+ ```
1839
+
1840
+ ### `longPress`
1841
+
1842
+ Factory: calls `onLongPress` when a touch / pen pointer stays down for `duration` ms (default 500) without moving beyond `moveTolerance` px.
1843
+
1844
+ ```svelte
1845
+ <div {@attach longPress({ onLongPress: (e) => openAt(e.clientX, e.clientY) })}>…</div>
1846
+ ```
1847
+
1848
+ ### `resizable`
1849
+
1850
+ Factory: drag-resizable width (`axis: "x"`) or height (`axis: "y"`) with an ARIA window-splitter handle (arrows / Home / End / Enter), `min` / `max`, `px` or `%` of the parent, persisted under `key`; `handle` drives an element of your own, `onInit` hands out `set()` / `reset()`.
1851
+
1852
+ ```svelte
1853
+ <aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
1854
+ <div {@attach resizable({ axis: "y", units: "%", initial: 40 })}>…</div>
1855
+ ```
1856
+
1715
1857
  ## Utilities
1716
1858
 
1717
1859
  Import from `@marianmeres/stuic`.
@@ -2237,6 +2379,8 @@ Each component defines customization tokens. Override globally in `:root {}` or
2237
2379
  | Button | `--stuic-button-*` | `bg`, `text`, `border`, `ring-color`, `radius`, `padding-x-{size}` |
2238
2380
  | Switch | `--stuic-switch-*` | `accent` |
2239
2381
  | Slider | `--stuic-slider-*` | `track`, `fill`, `thumb`, `tick`, `tick-on-fill`, `thickness`, `length`, `radius`, `fill-radius` |
2382
+ | RangeSlider | `--stuic-range-slider-*` | `track`, `fill`, `thumb`, `tick`, `tick-on-fill`, `thickness`, `length`, `radius`, `fill-radius`, `ring-width`, `ring-color` |
2383
+ | SplitPane | `--stuic-split-pane-*` | `separator-color`, `separator-color-hover`, `separator-thickness`, `separator-hit-area`, `grip-color`, `grip-color-hover`, `grip-border-color`, `grip-length`, `grip-thickness`, `grip-radius`, `ring-color`, `transition` |
2240
2384
  | Input | `--stuic-input-*` | `accent`, `accent-error` |
2241
2385
  | Progress | `--stuic-progress-*` | `bg`, `accent` |
2242
2386
  | ListItemButton | `--stuic-list-item-button-*` | `bg`, `text`, `border`, `bg-hover`, `text-hover` |
package/README.md CHANGED
@@ -167,15 +167,15 @@ See [API.md](API.md) for the full list of exported theme types (`ThemeSchema`, `
167
167
 
168
168
  ### Layout & Overlays
169
169
 
170
- AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header, SlidingPanels, Nav, WithSidePanel
170
+ AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header, SlidingPanels, Nav, WithSidePanel, SplitPane
171
171
 
172
172
  ### Forms & Inputs
173
173
 
174
- FieldInput, FieldMoney, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, FieldCountry, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, RegisterFormModal, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
174
+ FieldInput, FieldMoney, FieldDate, FieldDateRange, Calendar, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, FieldCountry, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, RegisterFormModal, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
175
175
 
176
176
  ### Buttons & Controls
177
177
 
178
- Button, ButtonGroupRadio, Switch, Slider, TwCheck, ListItemButton, X
178
+ Button, ButtonGroupRadio, Switch, Slider, RangeSlider, TwCheck, ListItemButton, X
179
179
 
180
180
  ### Feedback & Notifications
181
181
 
@@ -206,6 +206,18 @@ Cart, Checkout (CheckoutProgress, CheckoutOrderSummary, CheckoutCartReview, Chec
206
206
 
207
207
  `autogrow` · `validate` · `focusTrap` · `autoscroll` · `dimBehind` · `fileDropzone` · `highlightDragover` · `resizableWidth` · `spotlight` · `trim` · `typeahead` · `onSubmitValidityCheck` · `popover` · `tooltip` · `createTour` / `tourStep` (onboarding)
208
208
 
209
+ ## Attachments
210
+
211
+ Svelte `{@attach}` helpers — preferred over new actions (reactive, composable, forwardable).
212
+
213
+ ```svelte
214
+ <div {@attach autoHeight}>…</div>
215
+ <aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
216
+ <div {@attach longPress({ onLongPress: (e) => openAt(e.clientX, e.clientY) })}>…</div>
217
+ ```
218
+
219
+ `autoHeight` · `longPress` · `resizable`
220
+
209
221
  ## PWA safe-area insets
210
222
 
211
223
  When a stuic app is installed and launched standalone (iOS Home Screen, Android/desktop PWA), the web view fills the entire screen, so edge-anchored chrome can render under the status bar / notch / home indicator. stuic ships an **opt-in** safe-area layer that is **inert in a normal browser tab** (`env()` → `0`) and only engages under `@media (display-mode: standalone), (display-mode: fullscreen)`.
@@ -1,42 +1,28 @@
1
+ import { type ResizableOptions } from "../attachments/resizable.js";
1
2
  /**
2
- * Options for the resizable width action.
3
+ * Options for the {@link resizableWidth} action — the `x`-axis
4
+ * {@link ResizableOptions} of the `resizable` attachment with the legacy
5
+ * `onResize` payload shape (`width` instead of `size`).
3
6
  */
4
- export interface ResizableWidthOptions {
5
- enabled?: boolean;
6
- initial?: number;
7
- min?: number;
8
- max?: number;
9
- units?: "px" | "%";
10
- /** Reverses handle position (left instead of right) and drag direction */
11
- reverse?: boolean;
12
- key?: string | number | null | undefined;
13
- storage?: "local" | "session";
14
- handleClass?: string;
15
- handleDragClass?: string;
7
+ export interface ResizableWidthOptions extends Omit<ResizableOptions, "axis" | "onResize"> {
16
8
  onResize?: (info: {
17
9
  width: number;
18
10
  units: "px" | "%";
19
11
  container: number;
20
12
  }) => void;
21
- debug?: (...args: unknown[]) => void;
22
13
  }
23
14
  /**
24
- * A Svelte action that makes an element's width resizable via drag handle.
15
+ * A Svelte action that makes an element's width resizable via a drag handle on its
16
+ * right (or, with `reverse`, left) edge.
25
17
  *
26
- * Adds a draggable handle to the right (or left with `reverse`) edge of the element.
27
- * Supports mouse and touch input.
28
- * Optionally persists the width to localStorage/sessionStorage.
29
- *
30
- * Features:
31
- * - Drag handle with visual feedback
32
- * - Min/max width constraints
33
- * - Support for px or % units
34
- * - Optional storage persistence with custom key
35
- * - Double-click handle to reset to initial width
36
- * - Touch device support
18
+ * Kept for back-compat: it is a thin wrapper over the `resizable` attachment
19
+ * (`axis: "x"`), so it has everything the attachment has — the handle is a focusable
20
+ * `role="separator"` with arrow / Home / End / Enter keyboard resizing, Pointer Events
21
+ * cover mouse, touch and pen, `min` / `max` clamp, `key` persists, double-click resets.
22
+ * For new code (and for heights) use `{@attach resizable(...)}` directly.
37
23
  *
38
24
  * @param el - The element to make resizable
39
- * @param fn - Function returning configuration options
25
+ * @param fn - Function returning configuration options (re-runs the action on reactive change)
40
26
  *
41
27
  * @remarks
42
28
  * The `units` option should not be changed dynamically after initialization.
@@ -63,4 +49,4 @@ export interface ResizableWidthOptions {
63
49
  * </div>
64
50
  * ```
65
51
  */
66
- export declare function resizableWidth(el: HTMLDivElement, fn?: () => ResizableWidthOptions): void;
52
+ export declare function resizableWidth(el: HTMLElement, fn?: () => ResizableWidthOptions): void;
@@ -1,22 +1,16 @@
1
- import { localStorageState, sessionStorageState, } from "../utils/persistent-state.svelte.js";
2
- import { twMerge } from "../utils/tw-merge.js";
1
+ import { resizable } from "../attachments/resizable.js";
3
2
  /**
4
- * A Svelte action that makes an element's width resizable via drag handle.
3
+ * A Svelte action that makes an element's width resizable via a drag handle on its
4
+ * right (or, with `reverse`, left) edge.
5
5
  *
6
- * Adds a draggable handle to the right (or left with `reverse`) edge of the element.
7
- * Supports mouse and touch input.
8
- * Optionally persists the width to localStorage/sessionStorage.
9
- *
10
- * Features:
11
- * - Drag handle with visual feedback
12
- * - Min/max width constraints
13
- * - Support for px or % units
14
- * - Optional storage persistence with custom key
15
- * - Double-click handle to reset to initial width
16
- * - Touch device support
6
+ * Kept for back-compat: it is a thin wrapper over the `resizable` attachment
7
+ * (`axis: "x"`), so it has everything the attachment has — the handle is a focusable
8
+ * `role="separator"` with arrow / Home / End / Enter keyboard resizing, Pointer Events
9
+ * cover mouse, touch and pen, `min` / `max` clamp, `key` persists, double-click resets.
10
+ * For new code (and for heights) use `{@attach resizable(...)}` directly.
17
11
  *
18
12
  * @param el - The element to make resizable
19
- * @param fn - Function returning configuration options
13
+ * @param fn - Function returning configuration options (re-runs the action on reactive change)
20
14
  *
21
15
  * @remarks
22
16
  * The `units` option should not be changed dynamically after initialization.
@@ -44,162 +38,13 @@ import { twMerge } from "../utils/tw-merge.js";
44
38
  * ```
45
39
  */
46
40
  export function resizableWidth(el, fn) {
47
- const HANDLE_CLS_BASE = [
48
- "group",
49
- "absolute top-0 bottom-0",
50
- "w-[1px]",
51
- "bg-black/20 hover:bg-black/30",
52
- "dark:bg-white/10 dark:hover:bg-white/20",
53
- "transition-colors duration-200",
54
- "touch-none cursor-ew-resize",
55
- ].join(" ");
56
- const DEFAULT_DRAG_HANDLE_CLS = [
57
- "absolute h-[20px] w-[9px]",
58
- "-translate-x-[4px] top-1/2 -translate-y-1/2",
59
- "rounded border border-black/20 dark:border-white/20",
60
- "bg-gray-300 group-hover:bg-gray-400",
61
- "dark:bg-gray-600 dark:group-hover:bg-gray-500",
62
- "transition-colors duration-200",
63
- "touch-none cursor-ew-resize",
64
- ].join(" ");
65
- function create_handle(el, handleClass, handleDragClass, reverse) {
66
- const handle = document.createElement("div");
67
- handle.setAttribute("data-handle", "true");
68
- const dragHandle = document.createElement("div");
69
- dragHandle.classList.add(...twMerge(DEFAULT_DRAG_HANDLE_CLS, handleDragClass).split(" "));
70
- handle.appendChild(dragHandle);
71
- el.appendChild(handle);
72
- //
73
- const positionCls = reverse ? "left-0" : "right-0";
74
- handle.classList.add(...twMerge(HANDLE_CLS_BASE, positionCls, handleClass).split(" "));
75
- return handle;
76
- }
77
41
  $effect(() => {
78
- const { enabled = true, initial: initialValue = 0, min = 0, max = 0, units = "px", reverse = false, key, storage = "session", handleClass = "", handleDragClass = "", onResize, debug, } = fn?.() || {};
79
- let initial = initialValue;
80
- const _debug = (...args) => debug?.("[resizable-width]", ...args);
81
- _debug("$effect");
82
- if (!enabled)
83
- return;
84
- // initialize ////////////////////////////////////////////////////////////////////
85
- //
86
- let isResizing = false;
87
- let startX = 0;
88
- let startWidth = 0;
89
- let containerW = undefined;
90
- //
91
- const handle = create_handle(el, handleClass, handleDragClass, reverse);
92
- const container = el.parentElement;
93
- // do we have a storage? if so, adjust the initial value...
94
- const initialBackup = initial;
95
- const _storage = get_storage(storage, key, initial);
96
- if (_storage)
97
- initial = _storage.current ?? initial;
98
- // handlers/workers/helpers //////////////////////////////////////////////////////
99
- function set_width(pxOrPercent) {
100
- if (pxOrPercent) {
101
- _debug(`set_width(${pxOrPercent})`);
102
- set_width_px(units === "%" ? container.offsetWidth * (pxOrPercent / 100) : pxOrPercent);
103
- }
104
- }
105
- function set_width_px(widthPx) {
106
- _debug(`set_width_px(${widthPx})`);
107
- containerW ??= container.offsetWidth;
108
- const clamp = (value) => {
109
- const _initial = value;
110
- if (min)
111
- value = Math.max(min, value);
112
- if (max)
113
- value = Math.min(max, value);
114
- if (_initial !== value)
115
- _debug("clamped", value, units);
116
- return value;
117
- };
118
- let width;
119
- if (units === "%") {
120
- const widthPercent = Math.min(100, (widthPx / containerW) * 100); // convert to % (with 100 max)
121
- width = clamp(widthPercent);
122
- }
123
- else {
124
- width = clamp(widthPx);
125
- }
126
- el.style.width = `${width}${units}`;
127
- _debug("new width", width, units);
128
- const info = { width, units, container: containerW };
129
- onResize?.(info);
130
- // maybe save to storage
131
- if (_storage)
132
- _storage.current = width;
133
- return info;
134
- }
135
- function resize_start(e) {
136
- e.preventDefault(); // prevent scrolling on touch devices
137
- isResizing = true;
138
- //
139
- const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
140
- startX = clientX;
141
- startWidth = parseInt(getComputedStyle(el).width, 10);
142
- containerW = container.offsetWidth;
143
- //
144
- document.body.style.cursor = "ew-resize";
145
- document.body.style.userSelect = "none";
146
- }
147
- function resize(e) {
148
- if (!isResizing)
149
- return;
150
- e.preventDefault(); // prevent scrolling on touch devices
151
- //
152
- const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
153
- const deltaX = clientX - startX;
154
- const width = reverse ? startWidth - deltaX : startWidth + deltaX;
155
- set_width_px(width);
156
- }
157
- function resize_stop() {
158
- if (isResizing) {
159
- isResizing = false;
160
- containerW = undefined;
161
- //
162
- document.body.style.cursor = "";
163
- document.body.style.userSelect = "";
164
- }
165
- }
166
- function on_dblclick() {
167
- set_width(initialBackup);
168
- }
169
- // initial styles ////////////////////////////////////////////////////////////////
170
- el.style.position = "relative"; // so the handle will work
171
- set_width(initial);
172
- // listeners /////////////////////////////////////////////////////////////////////
173
- // handle
174
- handle.addEventListener("dblclick", on_dblclick);
175
- handle.addEventListener("selectstart", (e) => e.preventDefault()); // prevent text selection during resize
176
- // mouse
177
- handle.addEventListener("mousedown", resize_start);
178
- document.addEventListener("mousemove", resize);
179
- document.addEventListener("mouseup", resize_stop);
180
- // touch
181
- handle.addEventListener("touchstart", resize_start, { passive: false });
182
- document.addEventListener("touchmove", resize, { passive: false });
183
- document.addEventListener("touchend", resize_stop);
184
- document.addEventListener("touchcancel", resize_stop);
185
- // cleanup ///////////////////////////////////////////////////////////////////////
186
- return () => {
187
- // mouse
188
- document.removeEventListener("mousemove", resize);
189
- document.removeEventListener("mouseup", resize_stop);
190
- // touch
191
- document.removeEventListener("touchmove", resize);
192
- document.removeEventListener("touchend", resize_stop);
193
- document.removeEventListener("touchcancel", resize_stop);
194
- // will also remove it's own event listeners
195
- handle.remove();
196
- };
42
+ const { onResize, ...rest } = fn?.() || {};
43
+ return resizable({
44
+ ...rest,
45
+ axis: "x",
46
+ onResize: onResize &&
47
+ (({ size, units, container }) => onResize({ width: size, units, container })),
48
+ })(el);
197
49
  });
198
50
  }
199
- // helpers ///////////////////////////////////////////////////////////////////////////////
200
- function get_storage(type, key, initialValue) {
201
- if (key) {
202
- return (type === "session" ? sessionStorageState : localStorageState)(`resizable-width-${key}`, initialValue);
203
- }
204
- return null;
205
- }
@@ -1,2 +1,3 @@
1
1
  export * from "./auto-height.js";
2
2
  export * from "./long-press.js";
3
+ export * from "./resizable.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./auto-height.js";
2
2
  export * from "./long-press.js";
3
+ export * from "./resizable.js";