@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
@@ -3,11 +3,11 @@
3
3
  import type { HTMLSelectAttributes } from "svelte/elements";
4
4
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
5
5
  import type { THC } from "../Thc/Thc.svelte";
6
- import type { FieldSelectOption } from "./types.js";
6
+ import type { FieldSelectOption, InputWrapClassProps } from "./types.js";
7
7
 
8
8
  type SnippetWithId = Snippet<[{ id: string }]>;
9
9
 
10
- export interface Props extends HTMLSelectAttributes {
10
+ export interface Props extends HTMLSelectAttributes, InputWrapClassProps {
11
11
  input?: HTMLSelectElement;
12
12
  value?: string | number;
13
13
  label?: SnippetWithId | THC;
@@ -28,13 +28,8 @@
28
28
  labelLeft?: boolean;
29
29
  labelLeftWidth?: "normal" | "wide";
30
30
  labelLeftBreakpoint?: number;
31
+ /** Classes for the <select> element */
31
32
  classInput?: string;
32
- classLabel?: string;
33
- classLabelBox?: string;
34
- classInputBox?: string;
35
- classInputBoxWrap?: string;
36
- classDescBox?: string;
37
- classBelowBox?: string;
38
33
  style?: string;
39
34
  }
40
35
  </script>
@@ -82,8 +77,11 @@
82
77
  classLabelBox,
83
78
  classInputBox,
84
79
  classInputBoxWrap,
80
+ classInputBoxWrapInvalid,
85
81
  classDescBox,
82
+ classDescBoxToggle,
86
83
  classBelowBox,
84
+ classValidationBox,
87
85
  style,
88
86
  //
89
87
  ...rest
@@ -128,8 +126,11 @@
128
126
  {classLabelBox}
129
127
  {classInputBox}
130
128
  {classInputBoxWrap}
129
+ {classInputBoxWrapInvalid}
131
130
  {classDescBox}
131
+ {classDescBoxToggle}
132
132
  {classBelowBox}
133
+ {classValidationBox}
133
134
  {validation}
134
135
  {style}
135
136
  >
@@ -2,11 +2,11 @@ import type { Snippet } from "svelte";
2
2
  import type { HTMLSelectAttributes } from "svelte/elements";
3
3
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
4
  import type { THC } from "../Thc/Thc.svelte";
5
- import type { FieldSelectOption } from "./types.js";
5
+ import type { FieldSelectOption, InputWrapClassProps } from "./types.js";
6
6
  type SnippetWithId = Snippet<[{
7
7
  id: string;
8
8
  }]>;
9
- export interface Props extends HTMLSelectAttributes {
9
+ export interface Props extends HTMLSelectAttributes, InputWrapClassProps {
10
10
  input?: HTMLSelectElement;
11
11
  value?: string | number;
12
12
  label?: SnippetWithId | THC;
@@ -27,13 +27,8 @@ export interface Props extends HTMLSelectAttributes {
27
27
  labelLeft?: boolean;
28
28
  labelLeftWidth?: "normal" | "wide";
29
29
  labelLeftBreakpoint?: number;
30
+ /** Classes for the <select> element */
30
31
  classInput?: string;
31
- classLabel?: string;
32
- classLabelBox?: string;
33
- classInputBox?: string;
34
- classInputBoxWrap?: string;
35
- classDescBox?: string;
36
- classBelowBox?: string;
37
32
  style?: string;
38
33
  }
39
34
  declare const FieldSelect: import("svelte").Component<Props, {}, "value" | "input">;
@@ -2,10 +2,11 @@
2
2
  import type { Snippet } from "svelte";
3
3
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
4
  import type { THC } from "../Thc/Thc.svelte";
5
+ import type { InputWrapClassProps } from "./types.js";
5
6
 
6
7
  type SnippetWithId = Snippet<[{ id: string }]>;
7
8
 
8
- export interface Props extends Record<string, any> {
9
+ export interface Props extends InputWrapClassProps, Record<string, any> {
9
10
  input?: HTMLInputElement;
10
11
  checked?: boolean;
11
12
  label?: SnippetWithId | THC;
@@ -27,13 +28,8 @@
27
28
  labelLeft?: boolean;
28
29
  labelLeftWidth?: "normal" | "wide";
29
30
  labelLeftBreakpoint?: number;
31
+ /** Classes for the underlying <Switch> element */
30
32
  classInput?: string;
31
- classLabel?: string;
32
- classLabelBox?: string;
33
- classInputBox?: string;
34
- classInputBoxWrap?: string;
35
- classDescBox?: string;
36
- classBelowBox?: string;
37
33
  style?: string;
38
34
  renderValue?: (rawValue: any) => string;
39
35
  }
@@ -79,8 +75,11 @@
79
75
  classLabelBox,
80
76
  classInputBox,
81
77
  classInputBoxWrap,
78
+ classInputBoxWrapInvalid,
82
79
  classDescBox,
80
+ classDescBoxToggle,
83
81
  classBelowBox,
82
+ classValidationBox,
84
83
  style = "",
85
84
  //
86
85
  renderValue,
@@ -111,8 +110,11 @@
111
110
  {classLabel}
112
111
  {classLabelBox}
113
112
  {classInputBox}
113
+ {classInputBoxWrapInvalid}
114
114
  {classDescBox}
115
+ {classDescBoxToggle}
115
116
  {classBelowBox}
117
+ {classValidationBox}
116
118
  {validation}
117
119
  classInputBoxWrap={twMerge("input-wrap-transparent", classInputBoxWrap)}
118
120
  {style}
@@ -1,10 +1,11 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
3
3
  import type { THC } from "../Thc/Thc.svelte";
4
+ import type { InputWrapClassProps } from "./types.js";
4
5
  type SnippetWithId = Snippet<[{
5
6
  id: string;
6
7
  }]>;
7
- export interface Props extends Record<string, any> {
8
+ export interface Props extends InputWrapClassProps, Record<string, any> {
8
9
  input?: HTMLInputElement;
9
10
  checked?: boolean;
10
11
  label?: SnippetWithId | THC;
@@ -26,13 +27,8 @@ export interface Props extends Record<string, any> {
26
27
  labelLeft?: boolean;
27
28
  labelLeftWidth?: "normal" | "wide";
28
29
  labelLeftBreakpoint?: number;
30
+ /** Classes for the underlying <Switch> element */
29
31
  classInput?: string;
30
- classLabel?: string;
31
- classLabelBox?: string;
32
- classInputBox?: string;
33
- classInputBoxWrap?: string;
34
- classDescBox?: string;
35
- classBelowBox?: string;
36
32
  style?: string;
37
33
  renderValue?: (rawValue: any) => string;
38
34
  }
@@ -3,10 +3,11 @@
3
3
  import type { HTMLTextareaAttributes } from "svelte/elements";
4
4
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
5
5
  import type { THC } from "../Thc/Thc.svelte";
6
+ import type { InputWrapClassProps } from "./types.js";
6
7
 
7
8
  type SnippetWithId = Snippet<[{ id: string }]>;
8
9
 
9
- export interface Props extends HTMLTextareaAttributes {
10
+ export interface Props extends HTMLTextareaAttributes, InputWrapClassProps {
10
11
  input?: HTMLTextAreaElement;
11
12
  value?: string;
12
13
  label?: SnippetWithId | THC;
@@ -28,14 +29,8 @@
28
29
  labelLeft?: boolean;
29
30
  labelLeftWidth?: "normal" | "wide";
30
31
  labelLeftBreakpoint?: number;
32
+ /** Classes for the <textarea> element */
31
33
  classInput?: string;
32
- classLabel?: string;
33
- classLabelBox?: string;
34
- classInputBox?: string;
35
- classInputBoxWrap?: string;
36
- classInputBoxWrapInvalid?: string;
37
- classDescBox?: string;
38
- classBelowBox?: string;
39
34
  style?: string;
40
35
  }
41
36
  </script>
@@ -85,7 +80,9 @@
85
80
  classInputBoxWrap,
86
81
  classInputBoxWrapInvalid,
87
82
  classDescBox,
83
+ classDescBoxToggle,
88
84
  classBelowBox,
85
+ classValidationBox,
89
86
  style,
90
87
  //
91
88
  ...rest
@@ -117,7 +114,9 @@
117
114
  {classInputBoxWrap}
118
115
  {classInputBoxWrapInvalid}
119
116
  {classDescBox}
117
+ {classDescBoxToggle}
120
118
  {classBelowBox}
119
+ {classValidationBox}
121
120
  {validation}
122
121
  {style}
123
122
  >
@@ -2,10 +2,11 @@ import type { Snippet } from "svelte";
2
2
  import type { HTMLTextareaAttributes } from "svelte/elements";
3
3
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
4
  import type { THC } from "../Thc/Thc.svelte";
5
+ import type { InputWrapClassProps } from "./types.js";
5
6
  type SnippetWithId = Snippet<[{
6
7
  id: string;
7
8
  }]>;
8
- export interface Props extends HTMLTextareaAttributes {
9
+ export interface Props extends HTMLTextareaAttributes, InputWrapClassProps {
9
10
  input?: HTMLTextAreaElement;
10
11
  value?: string;
11
12
  label?: SnippetWithId | THC;
@@ -30,14 +31,8 @@ export interface Props extends HTMLTextareaAttributes {
30
31
  labelLeft?: boolean;
31
32
  labelLeftWidth?: "normal" | "wide";
32
33
  labelLeftBreakpoint?: number;
34
+ /** Classes for the <textarea> element */
33
35
  classInput?: string;
34
- classLabel?: string;
35
- classLabelBox?: string;
36
- classInputBox?: string;
37
- classInputBoxWrap?: string;
38
- classInputBoxWrapInvalid?: string;
39
- classDescBox?: string;
40
- classBelowBox?: string;
41
36
  style?: string;
42
37
  }
43
38
  declare const FieldTextarea: import("svelte").Component<Props, {}, "value" | "input">;
@@ -48,6 +48,26 @@ A comprehensive form input system with multiple field components, validation sup
48
48
  | `inputBelow` | `Snippet \| THC` | Content below input |
49
49
  | `below` | `Snippet \| THC` | Content below entire field |
50
50
 
51
+ ## Shared wrapper class props (`InputWrapClassProps`)
52
+
53
+ Every `Field*` component that uses the shared label/input/description scaffolding accepts **the same 9 class props**, exported as the `InputWrapClassProps` interface from `@marianmeres/stuic`. Each Field extends that interface in its own `Props`, so the shape stays in sync and new wrapper targets are added in one place.
54
+
55
+ | Prop | Target |
56
+ | --------------------------- | ------------------------------------------------------------------ |
57
+ | `classLabel` | `<label>` element |
58
+ | `classLabelBox` | Wrapper around the label area |
59
+ | `classInputBox` | Wrapper around the whole input area |
60
+ | `classInputBoxWrap` | Inner input wrap (sibling to description/validation/below) |
61
+ | `classInputBoxWrapInvalid` | Added to `classInputBoxWrap` when validation fails |
62
+ | `classDescBox` | Description/help text box |
63
+ | `classDescBoxToggle` | Collapsible description's toggle button |
64
+ | `classBelowBox` | "Below" slot (rendered under the description) |
65
+ | `classValidationBox` | Validation message box |
66
+
67
+ Component-specific targets (e.g. `classInput` for the inner `<input>`/`<select>`/`<textarea>`, `classFileList` on `FieldFile`, `classOption`/`classOptgroup` on `FieldOptions`, `classPrefixTrigger` on `FieldPhoneNumber`, etc.) live on the component itself alongside these shared props.
68
+
69
+ > `FieldCheckbox` and `FieldRadios` have bespoke inline layouts and don't use the shared wrapper — they declare only the class props relevant to their own layout.
70
+
51
71
  ## Usage
52
72
 
53
73
  ### Basic Text Input
@@ -5,10 +5,11 @@
5
5
  import { twMerge } from "../../../utils/tw-merge.js";
6
6
  import { Collapsible } from "../../Collapsible/index.js";
7
7
  import Thc, { isTHCNotEmpty, type THC } from "../../Thc/Thc.svelte";
8
+ import type { InputWrapClassProps } from "../types.js";
8
9
 
9
10
  type SnippetWithId = Snippet<[{ id: string }]>;
10
11
 
11
- interface Props {
12
+ interface Props extends InputWrapClassProps {
12
13
  id: string;
13
14
  size?: "sm" | "md" | "lg" | string;
14
15
  class?: string;
@@ -28,15 +29,6 @@
28
29
  labelLeftWidth?: "normal" | "wide";
29
30
  labelLeftBreakpoint?: number;
30
31
  //
31
- classLabel?: string;
32
- classLabelBox?: string;
33
- classInputBox?: string;
34
- classInputBoxWrap?: string;
35
- classInputBoxWrapInvalid?: string;
36
- classDescBox?: string;
37
- classDescBoxToggle?: string;
38
- classBelowBox?: string;
39
- classValidationBox?: string;
40
32
  descriptionCollapsible?: boolean;
41
33
  descriptionDefaultExpanded?: boolean;
42
34
  style?: string;
@@ -1,10 +1,11 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { ValidationResult } from "../../../actions/validate.svelte.js";
3
3
  import { type THC } from "../../Thc/Thc.svelte";
4
+ import type { InputWrapClassProps } from "../types.js";
4
5
  type SnippetWithId = Snippet<[{
5
6
  id: string;
6
7
  }]>;
7
- interface Props {
8
+ interface Props extends InputWrapClassProps {
8
9
  id: string;
9
10
  size?: "sm" | "md" | "lg" | string;
10
11
  class?: string;
@@ -22,15 +23,6 @@ interface Props {
22
23
  labelLeft?: boolean;
23
24
  labelLeftWidth?: "normal" | "wide";
24
25
  labelLeftBreakpoint?: number;
25
- classLabel?: string;
26
- classLabelBox?: string;
27
- classInputBox?: string;
28
- classInputBoxWrap?: string;
29
- classInputBoxWrapInvalid?: string;
30
- classDescBox?: string;
31
- classDescBoxToggle?: string;
32
- classBelowBox?: string;
33
- classValidationBox?: string;
34
26
  descriptionCollapsible?: boolean;
35
27
  descriptionDefaultExpanded?: boolean;
36
28
  style?: string;
@@ -9,3 +9,31 @@ export interface FieldRadiosOption {
9
9
  value?: string;
10
10
  description?: THC;
11
11
  }
12
+ /**
13
+ * Class props forwarded to the shared `InputWrap` scaffolding used by every
14
+ * `Field*` component that has a label/input/description layout.
15
+ *
16
+ * Field components extend this interface in their own `Props` so the class-prop
17
+ * shape stays consistent across the library. Component-specific class props
18
+ * (e.g. `classInput`, `classFileList`, `classOption`) live on the component itself.
19
+ */
20
+ export interface InputWrapClassProps {
21
+ /** Classes for the <label> element */
22
+ classLabel?: string;
23
+ /** Classes for the wrapper around the label area */
24
+ classLabelBox?: string;
25
+ /** Classes for the wrapper around the input area (contains input + inputBefore/After/Below) */
26
+ classInputBox?: string;
27
+ /** Classes for the inner input wrap (sibling to description/validation/below) */
28
+ classInputBoxWrap?: string;
29
+ /** Extra classes applied to `classInputBoxWrap` when the field is invalid */
30
+ classInputBoxWrapInvalid?: string;
31
+ /** Classes for the description box (the subtle hint below the input) */
32
+ classDescBox?: string;
33
+ /** Classes for the description's collapsible toggle button */
34
+ classDescBoxToggle?: string;
35
+ /** Classes for the "below" slot — rendered under the description */
36
+ classBelowBox?: string;
37
+ /** Classes for the validation message box */
38
+ classValidationBox?: string;
39
+ }
@@ -11,9 +11,9 @@
11
11
  id: string;
12
12
  /** Display label (supports localization) */
13
13
  label: MaybeLocalized;
14
- /** Navigation URL (use href OR onClick, not both) */
14
+ /** Navigation URL. If both `href` and `onClick` are provided, `onClick` runs first (useful for analytics) and the browser then follows the href. */
15
15
  href?: string;
16
- /** Click handler (alternative to href) */
16
+ /** Click handler. Alternative to `href`, or runs before navigation if both are set. */
17
17
  onClick?: () => void;
18
18
  /** Icon content (THC for flexibility: string, html, component) */
19
19
  icon?: THC;
@@ -603,7 +603,7 @@
603
603
  class={twMerge(!unstyled && NAV_CHILDREN_CLASSES, classChildren)}
604
604
  transition:slide={{ duration: transitionDuration }}
605
605
  >
606
- {#each item.children ?? [] as child}
606
+ {#each item.children ?? [] as child (child.id)}
607
607
  {@render renderItem(child, depth + 1)}
608
608
  {/each}
609
609
  </ul>
@@ -627,6 +627,7 @@
627
627
  data-expanding={!unstyled && isExpanding ? "" : undefined}
628
628
  data-disabled={!unstyled && item.disabled ? "" : undefined}
629
629
  data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
630
+ aria-current={active ? "page" : undefined}
630
631
  aria-disabled={item.disabled}
631
632
  tabindex={item.disabled ? -1 : 0}
632
633
  use:tooltip={() => ({
@@ -691,7 +692,7 @@
691
692
  </li>
692
693
  {/snippet}
693
694
 
694
- {#each group.items ?? [] as item}
695
+ {#each group.items ?? [] as item (item.id)}
695
696
  {@render renderItem(item, 0)}
696
697
  {/each}
697
698
  </ul>
@@ -9,9 +9,9 @@ export interface NavItem {
9
9
  id: string;
10
10
  /** Display label (supports localization) */
11
11
  label: MaybeLocalized;
12
- /** Navigation URL (use href OR onClick, not both) */
12
+ /** Navigation URL. If both `href` and `onClick` are provided, `onClick` runs first (useful for analytics) and the browser then follows the href. */
13
13
  href?: string;
14
- /** Click handler (alternative to href) */
14
+ /** Click handler. Alternative to `href`, or runs before navigation if both are set. */
15
15
  onClick?: () => void;
16
16
  /** Icon content (THC for flexibility: string, html, component) */
17
17
  icon?: THC;
@@ -48,8 +48,8 @@ interface NavGroup {
48
48
  items?: NavItem[];
49
49
  /** Group icon (optional) */
50
50
  icon?: THC;
51
- /** Whether the group starts collapsed */
52
- defaultCollapsed?: boolean;
51
+ /** Whether the group starts expanded (default: false — groups are collapsed by default) */
52
+ defaultExpanded?: boolean;
53
53
  /** Navigation URL for groups without items */
54
54
  href?: string;
55
55
  /** Click handler for groups without items */
@@ -66,6 +66,10 @@
66
66
  --stuic-nav-item-text-focus: var(--stuic-color-primary-foreground);
67
67
  }
68
68
 
69
+ :root.dark {
70
+ --stuic-nav-item-bg-hover: rgb(255 255 255 / 0.08);
71
+ }
72
+
69
73
  @layer components {
70
74
  /* =============================================================================
71
75
  BASE CONTAINER
@@ -0,0 +1,189 @@
1
+ # Tree
2
+
3
+ A generic hierarchical tree view with drag-and-drop reordering, keyboard navigation, expand/collapse state, optional `localStorage` persistence, and full ARIA `treeview` semantics.
4
+
5
+ Backed by [`@marianmeres/tree`](https://www.npmjs.com/package/@marianmeres/tree) for the data model — pass `tree.toJSON().children` or any compatible `TreeNodeDTO[]`.
6
+
7
+ ## Usage
8
+
9
+ ### Basic
10
+
11
+ ```svelte
12
+ <script lang="ts">
13
+ import { Tree } from "@marianmeres/stuic";
14
+
15
+ const items = [
16
+ {
17
+ id: "root",
18
+ value: "Projects",
19
+ children: [
20
+ { id: "a", value: "Alpha", children: [] },
21
+ { id: "b", value: "Beta", children: [] },
22
+ ],
23
+ },
24
+ ];
25
+ </script>
26
+
27
+ <Tree {items} activeId="a" onSelect={(item) => console.log(item.id)} />
28
+ ```
29
+
30
+ ### Drag & Drop
31
+
32
+ ```svelte
33
+ <Tree
34
+ {items}
35
+ draggable
36
+ onMove={({ source, target, position }) => {
37
+ // Mutate your data to move `source` relative to `target`.
38
+ // Return `false` (or throw) to reject the move.
39
+ moveNode(source.id, target.id, position);
40
+ }}
41
+ isDraggable={(item) => !item.data?.locked}
42
+ isDropTarget={(item) => item.data?.acceptsChildren !== false}
43
+ />
44
+ ```
45
+
46
+ `onMove` receives `{ source, target, position: "before" | "after" | "inside" }`. The component does **not** mutate items itself — you own the data. Return `false` or throw to reject.
47
+
48
+ ### Expansion Control
49
+
50
+ Four priority tiers resolve a node's initial expansion state, highest wins:
51
+
52
+ 1. `localStorage` value (when `persistState={true}`)
53
+ 2. `expandedIds` prop (explicit initial set)
54
+ 3. Auto-expanded if any descendant is active
55
+ 4. `defaultExpanded` prop
56
+
57
+ ```svelte
58
+ <Tree
59
+ {items}
60
+ defaultExpanded
61
+ persistState
62
+ storageKeyPrefix="my-app-tree"
63
+ onToggle={(item, expanded) => console.log(item.id, expanded)}
64
+ />
65
+ ```
66
+
67
+ Collapsing a branch also collapses all of its descendants.
68
+
69
+ ### Custom Rendering
70
+
71
+ ```svelte
72
+ <Tree {items}>
73
+ {#snippet renderIcon(item, depth, isExpanded)}
74
+ <MyIcon type={item.data.kind} />
75
+ {/snippet}
76
+ {#snippet renderItem(item, depth, isExpanded)}
77
+ <span class="font-medium">{item.value}</span>
78
+ {#if item.data?.count}
79
+ <span class="ml-auto text-xs opacity-60">{item.data.count}</span>
80
+ {/if}
81
+ {/snippet}
82
+ </Tree>
83
+ ```
84
+
85
+ ## Props
86
+
87
+ | Prop | Type | Default | Description |
88
+ | ------------------- | ---------------------------------------------------- | ----------------- | ------------------------------------------------------------------------ |
89
+ | `items` | `TreeNodeDTO<T>[]` | required | Tree data (e.g. from `tree.toJSON().children`) |
90
+ | `activeId` | `string` | - | ID of the currently active/selected node |
91
+ | `isActive` | `(item) => boolean` | - | Alternative to `activeId` for custom active detection |
92
+ | `onSelect` | `(item) => void` | - | Called when a node is selected (click or Enter/Space) |
93
+ | `onToggle` | `(item, expanded) => void` | - | Called when a branch is expanded/collapsed |
94
+ | `sort` | `(a, b) => number` | - | Per-level sort comparator |
95
+ | `defaultExpanded` | `boolean` | `false` | Default expansion state for branches |
96
+ | `expandedIds` | `Set<string>` | - | Initially-expanded branch IDs |
97
+ | `persistState` | `boolean` | `false` | Persist expand/collapse to `localStorage` |
98
+ | `storageKeyPrefix` | `string` | `"stuic-tree"` | Prefix used for `localStorage` keys |
99
+ | `draggable` | `boolean` | `false` | Enable drag-and-drop reordering |
100
+ | `isDraggable` | `(item) => boolean` | - | Return `false` to block dragging a specific node |
101
+ | `isDropTarget` | `(item) => boolean` | - | Return `false` to block dropping onto a specific node |
102
+ | `onMove` | `(event) => void \| false \| Promise<void \| false>` | - | Called when a valid drop happens; return `false` to reject |
103
+ | `onError` | `(err) => void` | - | Called when `onMove` throws |
104
+ | `dragExpandDelay` | `number` | `800` | ms before auto-expanding a collapsed branch hovered during drag |
105
+ | `t` | `TranslateFn` | built-in | Optional translation function (used for drag-drop a11y announcements) |
106
+ | `getNodeLabel` | `(item) => string` | `String(v.value)` | String used in a11y announcements when a node is moved |
107
+ | `renderItem` | `Snippet<[item, depth, isExpanded]>` | - | Custom node label renderer |
108
+ | `renderIcon` | `Snippet<[item, depth, isExpanded]>` | - | Custom node icon renderer |
109
+ | `unstyled` | `boolean` | `false` | Skip default styling |
110
+ | `class` | `string` | - | Classes for wrapper |
111
+ | `classItem` | `string` | - | Classes for each item button |
112
+ | `classItemActive` | `string` | - | Extra classes when an item is active |
113
+ | `classIcon` | `string` | - | Classes for the icon wrapper |
114
+ | `classLabel` | `string` | - | Classes for the label wrapper |
115
+ | `classChevron` | `string` | - | Classes for the expand/collapse chevron |
116
+ | `classChildren` | `string` | - | Classes for the children container |
117
+ | `el` | `HTMLElement` | - | Bindable wrapper reference |
118
+
119
+ ## Keyboard Navigation
120
+
121
+ | Key | Action |
122
+ | --------------------- | --------------------------------------------------------------------- |
123
+ | `ArrowDown` | Focus next visible node |
124
+ | `ArrowUp` | Focus previous visible node |
125
+ | `ArrowRight` | Expand a collapsed branch, or move to first child of an expanded one |
126
+ | `ArrowLeft` | Collapse an expanded branch, or move to parent |
127
+ | `Home` | Focus first visible node |
128
+ | `End` | Focus last visible node |
129
+ | `Enter` / `Space` | Toggle expansion (if branch) and fire `onSelect` |
130
+
131
+ Focus follows the **roving tabindex** pattern: only the currently-focused node has `tabindex=0`, all others are `-1`.
132
+
133
+ ## Accessibility
134
+
135
+ - Root has `role="tree"`; each node has `role="treeitem"`.
136
+ - Branches have `aria-expanded`; active nodes have `aria-selected`; all nodes have `aria-level`.
137
+ - Children wrappers have `role="group"`.
138
+ - Successful `onMove` calls announce the change via a visually-hidden `aria-live="polite"` region. Translate via the `t` prop — keys: `move_before`, `move_after`, `move_inside` with `{source}` and `{target}` placeholders.
139
+
140
+ ### Drag-drop limitations
141
+
142
+ HTML5 drag-and-drop is **mouse-only** and not keyboard accessible. Touch support is device-dependent and unreliable. If keyboard/touch reordering matters for your app, layer your own UI on top (e.g. a context menu with "Move up / Move down / Move into…" actions that call your move logic directly).
143
+
144
+ ## CSS Variables
145
+
146
+ Override globally in `:root` or locally via `style=""`. Radius / border-width / transition use the standard STUIC fallback pattern and inherit from their shared structural tokens unless overridden.
147
+
148
+ ### Structure
149
+
150
+ | Variable | Default | Description |
151
+ | ---------------------------------- | ------------------------ | --------------------------------- |
152
+ | `--stuic-tree-indent` | `1.25rem` | Indentation per depth level |
153
+ | `--stuic-tree-item-padding-x` | `0.375rem` | Item horizontal padding |
154
+ | `--stuic-tree-item-padding-y` | `0.125rem` | Item vertical padding |
155
+ | `--stuic-tree-item-height` | `1.75rem` | Item row height |
156
+ | `--stuic-tree-item-font-size` | `var(--text-sm)` | Item font size |
157
+ | `--stuic-tree-item-gap` | `0.25rem` | Gap between chevron, icon, label |
158
+ | `--stuic-tree-chevron-size` | `14px` | Chevron icon size |
159
+ | `--stuic-tree-chevron-opacity` | `0.5` | Chevron opacity |
160
+ | `--stuic-tree-icon-opacity` | `0.7` | Icon opacity |
161
+ | `--stuic-tree-item-radius` | `var(--stuic-radius)` | Item border radius |
162
+ | `--stuic-tree-transition` | `var(--stuic-transition)`| Transition duration |
163
+
164
+ ### Colors
165
+
166
+ | Variable | Default | Description |
167
+ | ------------------------------------ | -------------------------------------- | ------------------------- |
168
+ | `--stuic-tree-item-bg` | `transparent` | Item background |
169
+ | `--stuic-tree-item-text` | `inherit` | Item text color |
170
+ | `--stuic-tree-item-bg-hover` | `rgb(0 0 0 / 0.06)` | Hover background |
171
+ | `--stuic-tree-item-bg-focus` | `rgb(0 0 0 / 0.06)` | Keyboard-focus background |
172
+ | `--stuic-tree-item-bg-active` | `var(--stuic-color-primary)` | Active/selected bg |
173
+ | `--stuic-tree-item-text-active` | `var(--stuic-color-primary-foreground)`| Active/selected text |
174
+
175
+ ### Drag & drop
176
+
177
+ | Variable | Default | Description |
178
+ | --------------------------------------- | ---------------------------------- | ------------------------------- |
179
+ | `--stuic-tree-item-opacity-dragging` | `0.4` | Opacity of the dragged item |
180
+ | `--stuic-tree-drop-indicator-color` | `var(--stuic-color-primary)` | Before/after drop line color |
181
+ | `--stuic-tree-drop-indicator-height` | `2px` | Before/after drop line height |
182
+ | `--stuic-tree-item-bg-dragover` | `rgb(0 0 0 / 0.04)` | "Inside"-drop highlight |
183
+
184
+ ## Limitations
185
+
186
+ - **No multi-select / checkbox selection.** Single active node only.
187
+ - **No lazy loading.** All nodes must be present in `items`.
188
+ - **Expansion state is private.** Observe via `onToggle`; set initial via `expandedIds`. There is currently no way to read the full set of expanded IDs from outside.
189
+ - **Drag-drop is mouse-only.** See the a11y section above.