@marianmeres/stuic 3.66.1 → 3.68.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 (75) hide show
  1. package/dist/actions/autoscroll.d.ts +7 -0
  2. package/dist/actions/autoscroll.js +7 -0
  3. package/dist/actions/focus-trap.d.ts +7 -0
  4. package/dist/actions/focus-trap.js +8 -3
  5. package/dist/actions/typeahead.svelte.js +40 -4
  6. package/dist/components/Carousel/Carousel.svelte +9 -2
  7. package/dist/components/Carousel/README.md +8 -2
  8. package/dist/components/Cart/Cart.svelte +3 -0
  9. package/dist/components/Cart/README.md +18 -1
  10. package/dist/components/Checkout/CheckoutOrderReview.svelte +4 -14
  11. package/dist/components/Checkout/README.md +184 -0
  12. package/dist/components/Checkout/_internal/checkout-utils.d.ts +6 -0
  13. package/dist/components/Checkout/_internal/checkout-utils.js +24 -0
  14. package/dist/components/Checkout/index.d.ts +1 -1
  15. package/dist/components/Checkout/index.js +1 -1
  16. package/dist/components/CommandMenu/CommandMenu.svelte +23 -7
  17. package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +2 -0
  18. package/dist/components/CronInput/CronInput.svelte +44 -9
  19. package/dist/components/CronInput/CronInput.svelte.d.ts +2 -0
  20. package/dist/components/CronInput/README.md +145 -0
  21. package/dist/components/CronInput/cron-next-run.svelte.d.ts +11 -0
  22. package/dist/components/CronInput/cron-next-run.svelte.js +11 -0
  23. package/dist/components/CronInput/index.css +0 -8
  24. package/dist/components/DataTable/DataTable.svelte +276 -83
  25. package/dist/components/DataTable/DataTable.svelte.d.ts +58 -6
  26. package/dist/components/DataTable/README.md +155 -25
  27. package/dist/components/DataTable/index.css +31 -0
  28. package/dist/components/DropdownMenu/DropdownMenu.svelte +43 -26
  29. package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +5 -1
  30. package/dist/components/DropdownMenu/README.md +37 -9
  31. package/dist/components/Input/FieldAssets.svelte +9 -7
  32. package/dist/components/Input/FieldAssets.svelte.d.ts +3 -7
  33. package/dist/components/Input/FieldFile.svelte +13 -7
  34. package/dist/components/Input/FieldFile.svelte.d.ts +4 -7
  35. package/dist/components/Input/FieldInput.svelte +10 -8
  36. package/dist/components/Input/FieldInput.svelte.d.ts +3 -8
  37. package/dist/components/Input/FieldInputLocalized.svelte +8 -7
  38. package/dist/components/Input/FieldInputLocalized.svelte.d.ts +2 -7
  39. package/dist/components/Input/FieldKeyValues.svelte +8 -7
  40. package/dist/components/Input/FieldKeyValues.svelte.d.ts +2 -7
  41. package/dist/components/Input/FieldLikeButton.svelte +9 -7
  42. package/dist/components/Input/FieldLikeButton.svelte.d.ts +3 -7
  43. package/dist/components/Input/FieldObject.svelte +8 -7
  44. package/dist/components/Input/FieldObject.svelte.d.ts +2 -7
  45. package/dist/components/Input/FieldOptions.svelte +9 -7
  46. package/dist/components/Input/FieldOptions.svelte.d.ts +3 -7
  47. package/dist/components/Input/FieldPhoneNumber.svelte +7 -8
  48. package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +3 -8
  49. package/dist/components/Input/FieldSelect.svelte +9 -8
  50. package/dist/components/Input/FieldSelect.svelte.d.ts +3 -8
  51. package/dist/components/Input/FieldSwitch.svelte +9 -7
  52. package/dist/components/Input/FieldSwitch.svelte.d.ts +3 -7
  53. package/dist/components/Input/FieldTextarea.svelte +7 -8
  54. package/dist/components/Input/FieldTextarea.svelte.d.ts +3 -8
  55. package/dist/components/Input/README.md +20 -0
  56. package/dist/components/Input/_internal/InputWrap.svelte +2 -10
  57. package/dist/components/Input/_internal/InputWrap.svelte.d.ts +2 -10
  58. package/dist/components/Input/types.d.ts +28 -0
  59. package/dist/components/Nav/Nav.svelte +5 -4
  60. package/dist/components/Nav/Nav.svelte.d.ts +2 -2
  61. package/dist/components/Nav/README.md +2 -2
  62. package/dist/components/Nav/index.css +4 -0
  63. package/dist/components/Tree/README.md +189 -0
  64. package/dist/components/Tree/Tree.svelte +46 -2
  65. package/dist/components/Tree/Tree.svelte.d.ts +5 -0
  66. package/dist/utils/input-history.svelte.d.ts +12 -0
  67. package/dist/utils/input-history.svelte.js +12 -0
  68. package/dist/utils/observe-exists.svelte.d.ts +1 -0
  69. package/dist/utils/observe-exists.svelte.js +11 -3
  70. package/dist/utils/switch.svelte.d.ts +12 -0
  71. package/dist/utils/switch.svelte.js +12 -1
  72. package/docs/architecture.md +0 -1
  73. package/docs/testing.md +72 -0
  74. package/docs/upgrading.md +281 -0
  75. package/package.json +12 -13
@@ -2,9 +2,31 @@
2
2
  import type { HTMLAttributes } from "svelte/elements";
3
3
  import type { TreeNodeDTO } from "@marianmeres/tree";
4
4
  import type { Snippet } from "svelte";
5
+ import type { TranslateFn } from "../../types.js";
6
+ import { isPlainObject } from "../../utils/is-plain-object.js";
7
+ import { replaceMap } from "../../utils/replace-map.js";
5
8
 
6
9
  export type TreeDropPosition = "before" | "after" | "inside";
7
10
 
11
+ function t_default(
12
+ k: string,
13
+ values: false | null | undefined | Record<string, string | number> = null,
14
+ fallback: string | boolean = "",
15
+ _i18nSpanWrap: boolean = true
16
+ ) {
17
+ const m: Record<string, string> = {
18
+ move_before: "Moved {source} above {target}",
19
+ move_after: "Moved {source} below {target}",
20
+ move_inside: "Moved {source} into {target}",
21
+ };
22
+ let out = m[k] ?? fallback ?? k;
23
+ return isPlainObject(values)
24
+ ? replaceMap(out, values as any, {
25
+ preSearchKeyTransform: (k) => `{${k}}`,
26
+ })
27
+ : out;
28
+ }
29
+
8
30
  export interface TreeMoveEvent<T = unknown> {
9
31
  /** The node being dragged */
10
32
  source: TreeNodeDTO<T>;
@@ -72,6 +94,12 @@
72
94
  /** Delay in ms before auto-expanding a collapsed branch on drag-over (default: 800) */
73
95
  dragExpandDelay?: number;
74
96
 
97
+ /** Optional translate function (used for drag-drop screen reader announcements) */
98
+ t?: TranslateFn;
99
+
100
+ /** Derive a screen-reader label for a node. Defaults to `String(item.value)`. */
101
+ getNodeLabel?: (item: TreeNodeDTO<T>) => string;
102
+
75
103
  /** Skip all default styling */
76
104
  unstyled?: boolean;
77
105
 
@@ -128,6 +156,8 @@
128
156
  onMove,
129
157
  onError,
130
158
  dragExpandDelay = 800,
159
+ t = t_default,
160
+ getNodeLabel = (item) => String(item.value),
131
161
  unstyled = false,
132
162
  class: classProp,
133
163
  el = $bindable(),
@@ -310,8 +340,10 @@
310
340
 
311
341
  function focusItem(id: string) {
312
342
  focusedId = id;
313
- // Focus the DOM element
314
- const itemEl = el?.querySelector(`[data-tree-id="${id}"]`) as HTMLElement | null;
343
+ // Focus the DOM element. CSS.escape() protects against ids containing quotes/specials.
344
+ const itemEl = el?.querySelector(
345
+ `[data-tree-id="${CSS.escape(id)}"]`
346
+ ) as HTMLElement | null;
315
347
  itemEl?.focus();
316
348
  }
317
349
 
@@ -409,6 +441,7 @@
409
441
  let dropTargetId = $state<string | null>(null);
410
442
  let dropPos = $state<TreeDropPosition | null>(null);
411
443
  let dragExpandTimer: ReturnType<typeof setTimeout> | null = null;
444
+ let liveAnnouncement = $state("");
412
445
 
413
446
  function findNodeById(nodeItems: TreeNode<T>[], id: string): TreeNode<T> | null {
414
447
  for (const item of nodeItems) {
@@ -545,6 +578,14 @@
545
578
  try {
546
579
  const result = await onMove(event);
547
580
  if (result === false) return;
581
+ liveAnnouncement = t(
582
+ `move_${event.position}`,
583
+ {
584
+ source: getNodeLabel(event.source),
585
+ target: getNodeLabel(event.target),
586
+ },
587
+ ""
588
+ ) as string;
548
589
  } catch (err) {
549
590
  onError?.(err);
550
591
  }
@@ -658,4 +699,7 @@
658
699
  {#each sortedItems(items) as item (item.id)}
659
700
  {@render renderNode(item, 0)}
660
701
  {/each}
702
+
703
+ <!-- Screen-reader-only live region for drag-drop move announcements -->
704
+ <div class="sr-only" aria-live="polite" aria-atomic="true">{liveAnnouncement}</div>
661
705
  </div>
@@ -1,6 +1,7 @@
1
1
  import type { HTMLAttributes } from "svelte/elements";
2
2
  import type { TreeNodeDTO } from "@marianmeres/tree";
3
3
  import type { Snippet } from "svelte";
4
+ import type { TranslateFn } from "../../types.js";
4
5
  export type TreeDropPosition = "before" | "after" | "inside";
5
6
  export interface TreeMoveEvent<T = unknown> {
6
7
  /** The node being dragged */
@@ -47,6 +48,10 @@ export interface Props<T = unknown> extends Omit<HTMLAttributes<HTMLDivElement>,
47
48
  onError?: (error: unknown) => void;
48
49
  /** Delay in ms before auto-expanding a collapsed branch on drag-over (default: 800) */
49
50
  dragExpandDelay?: number;
51
+ /** Optional translate function (used for drag-drop screen reader announcements) */
52
+ t?: TranslateFn;
53
+ /** Derive a screen-reader label for a node. Defaults to `String(item.value)`. */
54
+ getNodeLabel?: (item: TreeNodeDTO<T>) => string;
50
55
  /** Skip all default styling */
51
56
  unstyled?: boolean;
52
57
  /** Classes for the wrapper element */
@@ -14,6 +14,15 @@ export interface InputHistoryOptions {
14
14
  /**
15
15
  * A reactive input history manager with localStorage persistence and arrow key navigation.
16
16
  *
17
+ * @remarks
18
+ * Every constructed instance registers its storage key in a **process-wide static Set**
19
+ * (`InputHistory.#registeredKeys`). The Set grows as new instances are created and is
20
+ * only trimmed by `clearAllMatching(prefix)` / `clearAll()` — there is no per-instance
21
+ * deregistration. In practice this is fine: each entry is a short string and the cost
22
+ * is negligible. Typical lifecycle: create instances as users navigate, call
23
+ * `InputHistory.clearAllMatching("<app-prefix>")` on logout to wipe stored keys and
24
+ * clear the registry in one shot.
25
+ *
17
26
  * @example
18
27
  * ```ts
19
28
  * const history = new InputHistory({
@@ -34,6 +43,9 @@ export interface InputHistoryOptions {
34
43
  *
35
44
  * // Reset navigation when user starts typing
36
45
  * history.reset();
46
+ *
47
+ * // On logout (wipes localStorage + internal registry for matching keys):
48
+ * InputHistory.clearAllMatching("joy:");
37
49
  * ```
38
50
  */
39
51
  export declare class InputHistory {
@@ -2,6 +2,15 @@ import { localStorageState } from "./persistent-state.svelte.js";
2
2
  /**
3
3
  * A reactive input history manager with localStorage persistence and arrow key navigation.
4
4
  *
5
+ * @remarks
6
+ * Every constructed instance registers its storage key in a **process-wide static Set**
7
+ * (`InputHistory.#registeredKeys`). The Set grows as new instances are created and is
8
+ * only trimmed by `clearAllMatching(prefix)` / `clearAll()` — there is no per-instance
9
+ * deregistration. In practice this is fine: each entry is a short string and the cost
10
+ * is negligible. Typical lifecycle: create instances as users navigate, call
11
+ * `InputHistory.clearAllMatching("<app-prefix>")` on logout to wipe stored keys and
12
+ * clear the registry in one shot.
13
+ *
5
14
  * @example
6
15
  * ```ts
7
16
  * const history = new InputHistory({
@@ -22,6 +31,9 @@ import { localStorageState } from "./persistent-state.svelte.js";
22
31
  *
23
32
  * // Reset navigation when user starts typing
24
33
  * history.reset();
34
+ *
35
+ * // On logout (wipes localStorage + internal registry for matching keys):
36
+ * InputHistory.clearAllMatching("joy:");
25
37
  * ```
26
38
  */
27
39
  export class InputHistory {
@@ -28,5 +28,6 @@ export declare function observeExists(selector: string, options?: Partial<{
28
28
  }>): {
29
29
  readonly current: boolean;
30
30
  disconnect(): void;
31
+ /** Re-run the selector check immediately and update `current`. */
31
32
  forceCheck(): void;
32
33
  };
@@ -42,13 +42,20 @@ export function observeExists(selector, options = {}) {
42
42
  break;
43
43
  }
44
44
  }
45
+ else if (mutation.type === "attributes") {
46
+ // an attribute changed on an existing element (e.g. class toggle) —
47
+ // may flip whether `selector` matches
48
+ shouldCheck = true;
49
+ break;
50
+ }
45
51
  }
46
52
  if (shouldCheck)
47
53
  current = check();
48
54
  });
49
- // start observing now
55
+ // start observing now — include attributes so selectors like ".active" or
56
+ // "[data-busy]" react when classes/attrs toggle on existing elements
50
57
  clogDebug(`connecting...`);
51
- observer.observe(rootElement, { childList: true, subtree: true });
58
+ observer.observe(rootElement, { childList: true, subtree: true, attributes: true });
52
59
  return {
53
60
  get current() {
54
61
  return current;
@@ -57,8 +64,9 @@ export function observeExists(selector, options = {}) {
57
64
  clogDebug(`disconnecting...`);
58
65
  observer.disconnect();
59
66
  },
67
+ /** Re-run the selector check immediately and update `current`. */
60
68
  forceCheck() {
61
- check();
69
+ current = check();
62
70
  },
63
71
  };
64
72
  }
@@ -33,8 +33,20 @@ export declare class SwitchState<T> {
33
33
  #private;
34
34
  readonly key: string;
35
35
  readonly storageType: "memory" | "local" | "session";
36
+ /**
37
+ * One-shot callback fired the next time the switch transitions off (via `off()`,
38
+ * `toggle()` to off, or `reset()`). **Cleared after firing** — assign again before
39
+ * the next off-transition if you want it to fire repeatedly. Useful for "run X the
40
+ * next time this modal closes" patterns.
41
+ */
36
42
  onOff: ((data: T, self: SwitchState<T>) => void) | undefined | null;
37
43
  constructor(key: string, initial?: boolean | null, storageType?: "memory" | "local" | "session", initialData?: T | null);
44
+ /**
45
+ * @internal
46
+ * Low-level state setter. Prefer `on()`, `off()`, `toggle()`, or `reset()` — they
47
+ * cover every expected case. Kept public only because removing it would be a BC break;
48
+ * future code should treat this as private.
49
+ */
38
50
  __set(value: boolean | null, data?: T | null | undefined): void;
39
51
  on(data?: T | null | undefined): void;
40
52
  off(data?: T | null | undefined): void;
@@ -43,6 +43,12 @@ export class SwitchState {
43
43
  // arbitrary data associated with the switch
44
44
  #data = $state(null);
45
45
  #storage;
46
+ /**
47
+ * One-shot callback fired the next time the switch transitions off (via `off()`,
48
+ * `toggle()` to off, or `reset()`). **Cleared after firing** — assign again before
49
+ * the next off-transition if you want it to fire repeatedly. Useful for "run X the
50
+ * next time this modal closes" patterns.
51
+ */
46
52
  onOff = null;
47
53
  constructor(key, initial = null, storageType = "memory", initialData = null) {
48
54
  this.key = key;
@@ -59,7 +65,12 @@ export class SwitchState {
59
65
  console.warn(`Unknown storageType "${this.storageType}"`);
60
66
  }
61
67
  }
62
- // still public, but should not be used directly unless necessary for some reason
68
+ /**
69
+ * @internal
70
+ * Low-level state setter. Prefer `on()`, `off()`, `toggle()`, or `reset()` — they
71
+ * cover every expected case. Kept public only because removing it would be a BC break;
72
+ * future code should treat this as private.
73
+ */
63
74
  __set(value, data) {
64
75
  if (value !== null && typeof value !== "boolean")
65
76
  value = Boolean(value);
@@ -112,7 +112,6 @@ Props → Component → Data Attributes → CSS Selectors
112
112
  | ------------------------------ | ----------------------------- |
113
113
  | `tailwind-merge` | CSS class conflict resolution |
114
114
  | `runed` | Svelte 5 reactive utilities |
115
- | `esm-env` | Environment detection |
116
115
  | `@marianmeres/icons-fns` | Icon SVG generation |
117
116
  | `@marianmeres/item-collection` | Collection management |
118
117
  | `@marianmeres/ticker` | Animation timing |
@@ -0,0 +1,72 @@
1
+ # Testing
2
+
3
+ STUIC has a **small, focused test suite** that's intentionally narrow. This doc explains what we test, what we don't, and how to add a new test.
4
+
5
+ ## Philosophy
6
+
7
+ This is a component library. Most of its correctness guarantees come from:
8
+
9
+ 1. **TypeScript + `svelte-check`** — API contracts, prop types, snippet shapes.
10
+ 2. **`publint`** — package export hygiene.
11
+ 3. **The build** — every component compiles, every export resolves.
12
+ 4. **Manual/visual review** — styling, animation, keyboard interaction, a11y cues.
13
+
14
+ Unit tests are for what those tools can't see: **pure deterministic logic where a regression silently corrupts data**. We explicitly don't try to test everything.
15
+
16
+ ## What we test
17
+
18
+ ### ✅ High value
19
+
20
+ - **Validation helpers** — `validateEmail`, `validateAddress`, `validateCustomerForm`, `validateLoginForm`, `validatePhoneNumber`, `addressesEqual`.
21
+ - **State-machine classes** — `NotificationsStack`, `AlertConfirmPromptStack`, `SwitchState`, `InputHistory`. Tri-state transitions, dedupe, ordering, cleanup semantics.
22
+ - **Pure utilities** — `replace-map`, `tr`, `storage-abstraction`, and anything else with non-trivial input/output logic.
23
+
24
+ ### ⚠️ Maybe, if motivated by a regression
25
+
26
+ - **Logic extracted from a `.svelte` into a sibling `_internal/*.ts`** — once extracted, same rules as utilities apply. (Examples that would be good candidates if we ever extract them: Tree's `calcDropPosition()` and `isDescendantOf()`, CronInput's `cronToHuman()` and `fieldsToExpression()`.)
27
+
28
+ ### ❌ We don't test
29
+
30
+ - **Full component rendering** via `@testing-library/svelte`. 50+ components × prop combinations = slow suite with tiny yield. Rendering is already gated by `svelte-check` + `publint` + the build.
31
+ - **Visual regression**. That's a separate project (Playwright + screenshot diffing) — not part of `vitest --dir src/`.
32
+ - **Interactive behavior** (keyboard nav, drag-drop, scroll snap) unless the underlying math is extracted to a pure function.
33
+ - **Coverage % targets**. They're the wrong goal for a component library.
34
+
35
+ ## Running tests
36
+
37
+ ```bash
38
+ pnpm run test
39
+ ```
40
+
41
+ Vitest is configured to run everything under `src/`. Tests live next to the code they test: `foo.ts` → `foo.test.ts`.
42
+
43
+ ## Writing a test
44
+
45
+ Match the style of existing tests — plain `vitest` with `assert`, no test framework wrapper ceremony.
46
+
47
+ ```ts
48
+ // src/lib/utils/my-thing.test.ts
49
+ import { assert, test } from "vitest";
50
+ import { myThing } from "./my-thing.js";
51
+
52
+ test("myThing handles the empty case", () => {
53
+ assert.equal(myThing(""), null);
54
+ });
55
+
56
+ test("myThing handles the happy path", () => {
57
+ assert.equal(myThing("foo"), "FOO");
58
+ });
59
+ ```
60
+
61
+ For things that take a `TranslateFn`, inject an identity stub so the output is just the key:
62
+
63
+ ```ts
64
+ import type { TranslateFn } from "$lib/types.js";
65
+ const t: TranslateFn = (k) => k;
66
+ ```
67
+
68
+ ## When in doubt
69
+
70
+ - **Logic in a `.ts` file with clear input/output?** Write a test.
71
+ - **A regression just bit you in production?** Write a test for that specific case before fixing.
72
+ - **Anything else?** Probably don't bother.
@@ -0,0 +1,281 @@
1
+ # Upgrading STUIC
2
+
3
+ Notes for coding agents (and humans) maintaining a project that consumes `@marianmeres/stuic`. This doc describes the deltas introduced on top of **v3.66.1** — grouped by what a consumer actually cares about, not by commit.
4
+
5
+ ## TL;DR
6
+
7
+ **Everything is additive or internal.** No props removed, no snippets renamed, no public-type signatures changed in a breaking way. You can upgrade without changing any consumer code. The reasons to make targeted adjustments afterwards are:
8
+
9
+ 1. Adopt new opt-in features (`unstyled` now on DropdownMenu / CronInput / CommandMenu; new Tree/DataTable snippets; cart/checkout helpers).
10
+ 2. Take advantage of new a11y wiring (the library now emits `aria-current`, `role="combobox"`, `aria-live` announcements, etc. — your CSS may want to react).
11
+ 3. Spot-check the few subtle behavior changes listed under *Subtle behavior deltas* below.
12
+
13
+ **Recommended post-upgrade check:** run the Browser Verification Checklist at the end of this doc.
14
+
15
+ ---
16
+
17
+ ## How to adopt
18
+
19
+ ```bash
20
+ pnpm add @marianmeres/stuic@latest
21
+ ```
22
+
23
+ No codemod needed. The library is backwards-compatible with v3.66.1 call sites.
24
+
25
+ ---
26
+
27
+ ## What's new (opt-in)
28
+
29
+ The following features are new and **only activate when you pass the prop**. Existing call sites continue to render exactly as before.
30
+
31
+ ### Component-level `unstyled` prop coverage
32
+
33
+ `unstyled?: boolean` now exists on every Tier-1 component in the library. When set, the component strips its `stuic-*` base classes and you're expected to supply all styling through the `class*` escape hatches.
34
+
35
+ Newly gained in this release: **DropdownMenu**, **CronInput**, **CommandMenu** (the remaining holdouts — Button, DataTable, Tree, Nav, Cart, Carousel, most Field components already had it).
36
+
37
+ ### DropdownMenu
38
+
39
+ | Prop | Type | Notes |
40
+ | --- | --- | --- |
41
+ | `unstyled` | `boolean` | Strips base classes; keeps functional layout. |
42
+ | `el` | `HTMLDivElement` (bindable) | Wrapper element ref. |
43
+
44
+ ### DataTable
45
+
46
+ | Prop | Type | Notes |
47
+ | --- | --- | --- |
48
+ | `row` | `Snippet<[{ row, columns, rowIndex, isSelected }]>` | Desktop-only — replaces the entire `<tr>`. Parallel to the existing `mobileRow`. |
49
+ | `selectDisabledBy` | `(row, index) => boolean` | Per-row selection disable. Respected by "select all". |
50
+
51
+ The `cell` snippet gained an extra parameter:
52
+
53
+ ```svelte
54
+ <!-- before -->
55
+ {#snippet cell({ column, row, value, rowIndex })} ... {/snippet}
56
+
57
+ <!-- after (both shapes compile; old callers ignore the new param) -->
58
+ {#snippet cell({ column, row, value, rowIndex, variant })} ... {/snippet}
59
+ ```
60
+
61
+ `variant` is `"desktop" | "mobile"` — lets a single snippet adapt to the layout.
62
+
63
+ Also dropped: the **dead `children?: Snippet` prop** that was declared but never rendered. No consumer was using it.
64
+
65
+ ### Tree
66
+
67
+ First README in this release. Also:
68
+
69
+ | Prop | Type | Notes |
70
+ | --- | --- | --- |
71
+ | `t` | `TranslateFn` | Translation function (used only for a11y move-announcements). |
72
+ | `getNodeLabel` | `(item) => string` | How to stringify a node for the a11y announcement. Defaults to `String(item.value)`. |
73
+
74
+ On a successful `onMove`, the component now announces the move via a visually-hidden `aria-live="polite"` region. Translation keys: `move_before`, `move_after`, `move_inside` with `{source}` and `{target}` placeholders.
75
+
76
+ ### Input family (shared wrapper class props)
77
+
78
+ Every `Field*` component that uses `InputWrap` now accepts the full set of **9 wrapper class props**, exposed as a reusable interface:
79
+
80
+ ```ts
81
+ import type { InputWrapClassProps } from "@marianmeres/stuic";
82
+
83
+ interface InputWrapClassProps {
84
+ classLabel?: string;
85
+ classLabelBox?: string;
86
+ classInputBox?: string;
87
+ classInputBoxWrap?: string;
88
+ classInputBoxWrapInvalid?: string;
89
+ classDescBox?: string;
90
+ classDescBoxToggle?: string; // newly forwarded
91
+ classBelowBox?: string;
92
+ classValidationBox?: string; // newly forwarded
93
+ }
94
+ ```
95
+
96
+ Previously most fields only accepted 5–7 of these. You can now pass any of them to **FieldInput, FieldTextarea, FieldSelect, FieldSwitch, FieldFile, FieldLikeButton, FieldObject, FieldOptions, FieldAssets, FieldPhoneNumber, FieldInputLocalized, FieldKeyValues** uniformly. `FieldCheckbox` and `FieldRadios` use bespoke layouts and keep their own narrower class-prop surface.
97
+
98
+ ### Checkout
99
+
100
+ New util:
101
+
102
+ ```ts
103
+ import { addressesEqual } from "@marianmeres/stuic";
104
+
105
+ addressesEqual(a, b); // true if both missing or all fields match (incl. label, is_default)
106
+ ```
107
+
108
+ First README for the Checkout family (explains the kit-of-parts composition model).
109
+
110
+ ### Cart
111
+
112
+ New translation key:
113
+
114
+ ```ts
115
+ remove_item_aria: "Remove {name}" // used as aria-label on the remove button
116
+ ```
117
+
118
+ Override via your `t` prop to localize. README now also explains the `summary` variant and the caller's responsibility to keep `lineTotal` in sync after quantity changes.
119
+
120
+ ### CronInput
121
+
122
+ First README. `unstyled` prop is the user-facing addition. Internals unchanged.
123
+
124
+ `CronNextRun` now has a prominent JSDoc note reminding consumers to call `destroy()` manually — it starts a 60s interval on construction and doesn't self-clean. If you already do `onDestroy(() => nr.destroy())`, no change needed.
125
+
126
+ ### CommandMenu
127
+
128
+ `unstyled` prop added. Everything else about its public API is unchanged.
129
+
130
+ ### Carousel
131
+
132
+ **Zero API changes.** 6 previously-undocumented props are now in the README: `syncActiveOnScroll`, `wheelScroll`, `scrollbar`, `arrows`, `classArrow`, `minItemWidth`. They existed before — just weren't documented.
133
+
134
+ Internally: programmatic scrolling now respects `prefers-reduced-motion: reduce`. When set, arrows, keyboard, and wheel navigation switch to `scrollBehavior: "instant"` automatically regardless of the `scrollBehavior` prop.
135
+
136
+ ### Nav
137
+
138
+ **Doc/code drift fixed.** `NavGroup.defaultCollapsed` in the README was wrong — the real prop name is `defaultExpanded` (and always was). If you were blindly following the README, audit your call sites:
139
+
140
+ ```svelte
141
+ <!-- incorrect (never worked) -->
142
+ <Nav groups={[{ title: "…", items: […], defaultCollapsed: true }]} />
143
+
144
+ <!-- correct -->
145
+ <Nav groups={[{ title: "…", items: […], defaultExpanded: false }]} />
146
+ ```
147
+
148
+ Also added: `aria-current="page"` on active anchor items, `:root.dark` override for the hover background. See *Subtle behavior deltas* below.
149
+
150
+ ---
151
+
152
+ ## Subtle behavior deltas (BC-safe, but noticeable)
153
+
154
+ These don't break existing code, but output or rendering may differ in visible ways.
155
+
156
+ ### Nav active-anchor styling
157
+
158
+ Active anchors now emit `aria-current="page"` **in addition to** the existing `[data-active]` data attribute. If your custom CSS targets only `[data-active]`, nothing changes. If you add new CSS targeting `[aria-current="page"]`, make sure it doesn't conflict with the existing active rule.
159
+
160
+ ### Nav hover in dark mode
161
+
162
+ The hover background was previously `rgb(0 0 0 / 0.1)` with no dark-mode override — effectively invisible on dark surfaces. Now:
163
+
164
+ ```css
165
+ :root.dark {
166
+ --stuic-nav-item-bg-hover: rgb(255 255 255 / 0.08);
167
+ }
168
+ ```
169
+
170
+ Dark-mode hover is now actually visible. If you had been overriding `--stuic-nav-item-bg-hover` globally and relied on it applying in both themes, you may want to add a `:root.dark` override of your own.
171
+
172
+ ### Checkout: address equality now compares `label` + `is_default`
173
+
174
+ `CheckoutOrderReview` internally uses the new `addressesEqual` helper to decide whether to render a separate billing block. The helper now compares **all** `CheckoutAddressData` fields, including `label` and `is_default`, not just the 6 visible fields. If you populate `label` differently on shipping vs. billing (e.g. "home" vs "work") for the same street address, the review will now render them as two distinct blocks where it previously treated them as one. This is the correct behavior for the intent of those fields.
175
+
176
+ ### DataTable: "Select all" aria-label
177
+
178
+ Default text changed from `"Select all rows"` → `"Select all rows on this page"`. Only visible to screen readers. Override via the `t` prop (`select_all_rows` key).
179
+
180
+ ### DataTable: mobile card checkbox class
181
+
182
+ The mobile checkbox wrapper no longer uses inline Tailwind utilities; it uses `stuic-data-table-card-checkbox` (defined in the library's CSS). If you had any custom CSS targeting the old combo `.stuic-checkbox.flex.items-center.gap-2.mb-1` inside a mobile card, re-target to `.stuic-data-table-card-checkbox`.
183
+
184
+ ### DropdownMenu fallback-mode width
185
+
186
+ When CSS anchor-positioning is unavailable (Safari <17.4, Firefox <115, or `forceFallback={true}`), the modal-style fallback is now **384px max-width** (`max-w-sm`) instead of the previously-buggy 128px (`max-w-32`). Longer menu items no longer get clipped into a nearly-unusable column.
187
+
188
+ ### DropdownMenu default `position`
189
+
190
+ The README was wrong — the default was always `"bottom-span-right"`, not `"bottom-span-left"`. Updated. Your call sites are unaffected.
191
+
192
+ ### observeExists
193
+
194
+ `forceCheck()` was previously a no-op due to a missing assignment. It now actually updates `current`. If you had been calling it (the flag was documented but didn't do anything), you'll now see reactive updates where you previously didn't.
195
+
196
+ Also, `observeExists` now observes attribute mutations (not just `childList`). Selectors like `.active` or `[data-busy]` that match based on class/attribute now react when those attributes toggle on existing elements. Previously, only DOM add/remove triggered rechecks.
197
+
198
+ ### typeahead
199
+
200
+ - Stale asynchronous results are now discarded when a newer search has started (previously they could clobber the more recent results).
201
+ - Escape key now dismisses an active suggestion and **stops propagation**. If your typeahead is inside a `Modal`, pressing Escape while a suggestion is visible now clears the suggestion without closing the Modal. Pressing Escape again (with no active suggestion) closes the Modal as before.
202
+ - Parent element's inline `position` is now restored on teardown (it's still mutated to `relative` at setup when needed). If you're disabling/re-enabling typeahead repeatedly on the same container, you'll no longer see the parent stuck in `position: relative` after teardown.
203
+ - Main input now emits `role="combobox"`, `aria-autocomplete="inline"`, and `aria-expanded` reflecting whether a suggestion is active. All removed on teardown.
204
+
205
+ ### CommandMenu `isFetching`
206
+
207
+ Previously flickered back to `false` whenever any in-flight request resolved, including stale ones. Now only the latest request toggles it off. The spinner stays visible as long as *any* fresh request is still in flight.
208
+
209
+ ### Tree `data-tree-id` query
210
+
211
+ Internal: uses `CSS.escape()` when matching `data-tree-id` values to DOM elements. If you're using IDs with special characters (quotes, backslashes, etc.), they now work. Normal IDs are unaffected.
212
+
213
+ ---
214
+
215
+ ## Tree-shaking / dependency changes
216
+
217
+ - **`esm-env` removed** from direct dependencies (it's still resolvable as a transitive dep via `runed` and `svelte`). Nothing in STUIC imports it directly. If your project imports `esm-env` from your own code and relied on `@marianmeres/stuic` pulling it in transitively, add it to your own `package.json`.
218
+ - No other deps changed shape.
219
+
220
+ ---
221
+
222
+ ## Design-tokens update (visual)
223
+
224
+ `@marianmeres/design-tokens` bumped from **1.3.1 → 1.4.0**. This touches all 42 theme CSS files (small tweaks — no structural token renames). If you customize themes at the app level, this is low-risk; if you ship pixel-perfect branding, eyeball the themes you actually use.
225
+
226
+ No token names changed. No new tokens you need to adopt.
227
+
228
+ ---
229
+
230
+ ## Browser verification checklist
231
+
232
+ After upgrading, spot-check the following in a real browser. Tiered by likelihood of surprise.
233
+
234
+ ### Tier 1 — definitely check
235
+
236
+ - [ ] **Themes**: load 2–3 of your in-use themes in light **and** dark. Scan for anything that looks off (color shifts are the most likely artifact of the design-tokens bump).
237
+ - [ ] **DropdownMenu in anchor-less browsers** (Safari <17.4 / Firefox <115) **or** with `forceFallback={true}`: verify the fallback-mode width is comfortable (~384px) and items aren't clipped.
238
+ - [ ] **Nav in dark mode**: hover over an item. The hover state should now be visible. Check that no CSS rule you own fights with the new `:root.dark` override.
239
+ - [ ] **DataTable**:
240
+ - If you use `selectDisabledBy`, verify the disabled rows' checkboxes are disabled and excluded from "select all".
241
+ - If you opted into the new `row` snippet, make sure your custom `<tr>` renders.
242
+ - Mobile view: checkbox wrapper still lays out correctly (new `stuic-data-table-card-checkbox` class replaced the old inline Tailwind).
243
+ - [ ] **Cart summary variant**: if you use `variant="summary"`, check the compact receipt layout.
244
+
245
+ ### Tier 2 — smoke test
246
+
247
+ - [ ] **Input family forms with validation errors**: verify the invalid-state styling looks right — the newly-forwarded `classInputBoxWrapInvalid` / `classValidationBox` won't change anything if you don't pass them, but a few field components are now forwarding more props by default.
248
+ - [ ] **Carousel with OS reduced-motion enabled**: arrow / keyboard / wheel nav should no longer animate.
249
+ - [ ] **typeahead inside a Modal**: Escape with an active suggestion clears it; Escape with no suggestion closes the Modal.
250
+
251
+ ### Tier 3 — network & assistive-tech
252
+
253
+ - [ ] **CommandMenu with throttled network**: rapid typing with "Slow 3G" Chrome throttle — spinner should stay on, no stale results should leak.
254
+ - [ ] **Tree drag-drop with a screen reader**: perform a move; listen for the polite-live announcement.
255
+
256
+ ---
257
+
258
+ ## For coding agents specifically
259
+
260
+ When the user asks you to upgrade this library:
261
+
262
+ 1. **Run `pnpm add @marianmeres/stuic@latest`, then `pnpm run build && pnpm run check && pnpm run test`.** If all three pass, the upgrade is functionally complete. No codemod required.
263
+ 2. **Grep the consuming project for these strings** and confirm they still match your intent:
264
+ - `defaultCollapsed` — if found on a `NavGroup`, this never worked; change to `defaultExpanded` and invert your boolean.
265
+ - `classInputBoxWrap="..."` on a `FieldCheckbox` — `FieldCheckbox` uses a bespoke layout and doesn't forward this prop (it never did). No change needed.
266
+ - `"bottom-span-left"` as a DropdownMenu position default — explicit values still work; it was only the *default* that was documented wrong. No change needed if you were passing it explicitly.
267
+ - `stuic-checkbox.*flex.*items-center.*gap-2.*mb-1` (as a CSS selector combo in the consumer's own CSS) — re-target to `.stuic-data-table-card-checkbox`.
268
+ 3. **If the consuming project has its own dark-mode CSS for Nav**, scan for overrides of `--stuic-nav-item-bg-hover` and confirm your rule still makes sense alongside the library's new `:root.dark` override.
269
+ 4. **If the project exposes an `esm-env` import** in its own source, add `esm-env` to its own `package.json` — it's no longer a direct dep of STUIC.
270
+ 5. **Run the Browser Verification Checklist** above manually, or delegate it to the human if you can't run browsers. Explicitly tell the user: "I cannot verify X visually — please eyeball it."
271
+ 6. **Don't claim success if you skipped #5.** Type-checks + builds passing is necessary, not sufficient, for a library upgrade that touched this much a11y + CSS surface area.
272
+
273
+ ---
274
+
275
+ ## If something does break
276
+
277
+ None of the above should cause failures in `pnpm run build` / `pnpm run check` / existing tests. If it does:
278
+
279
+ - Run `git diff` on the consumer project to see if you accidentally edited a call site while upgrading.
280
+ - Check that the TypeScript version and `svelte-check` versions match what STUIC was built against (see STUIC's own `package.json` `devDependencies`).
281
+ - Open an issue with: the failing file, the full error, and which version you came from.