@marianmeres/stuic 3.51.2 → 3.52.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.
package/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 46 UI components
26
+ ├── components/ # 50 UI components
27
27
  ├── actions/ # 15 Svelte actions
28
28
  ├── utils/ # 43 utility modules
29
29
  ├── themes/ # Generated theme CSS (css/) — definitions from @marianmeres/design-tokens
package/API.md CHANGED
@@ -130,6 +130,41 @@ Panel-based navigation with slide transitions.
130
130
 
131
131
  Navigation wrapper component.
132
132
 
133
+ #### `Header`
134
+
135
+ Responsive navigation header with logo, nav items, avatar, and automatic hamburger collapse. Renders as `<header>`.
136
+
137
+ | Prop | Type | Default | Description |
138
+ | ------------------- | ----------------- | --------------------- | ---------------------------------- |
139
+ | `logo` | `Snippet` | — | Logo/brand snippet |
140
+ | `projectName` | `string` | — | Simple text logo alternative |
141
+ | `items` | `HeaderNavItem[]` | `[]` | Navigation items |
142
+ | `avatar` | `Snippet` | — | Avatar snippet (far right) |
143
+ | `avatarOnClick` | `() => void` | — | Avatar click handler |
144
+ | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables |
145
+ | `fixed` | `boolean` | `false` | Fixed positioning at top |
146
+ | `isCollapsed` | `boolean` | — | Bindable: collapsed state |
147
+ | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open |
148
+ | `onSelect` | `(item) => void` | — | Item selection callback |
149
+
150
+ ```svelte
151
+ <Header
152
+ projectName="My App"
153
+ items={[
154
+ { id: "home", label: "Home", href: "/", active: true },
155
+ { id: "about", label: "About", href: "/about" },
156
+ ]}
157
+ >
158
+ {#snippet avatar()}
159
+ <Avatar src="/me.jpg" alt="User" />
160
+ {/snippet}
161
+ </Header>
162
+ ```
163
+
164
+ **HeaderNavItem:** `{ id, label, href?, onclick?, icon?, active?, disabled?, class? }`
165
+
166
+ CSS tokens: `--stuic-header-padding-x`, `--stuic-header-padding-y`, `--stuic-header-gap`, `--stuic-header-min-height`, `--stuic-header-nav-gap`, `--stuic-header-bg`, `--stuic-header-text`, `--stuic-header-border-width`, `--stuic-header-border-color`, `--stuic-header-nav-item-bg-active`, `--stuic-header-nav-item-text-active`, `--stuic-header-z-index`.
167
+
133
168
  ---
134
169
 
135
170
  ### Interactive
@@ -316,6 +351,42 @@ Dual-mode JSON object editor with pretty-print and raw edit modes. Validates JSO
316
351
  <FieldObject bind:value={jsonStr} name="metadata" label="Metadata" />
317
352
  ```
318
353
 
354
+ #### `CronInput`
355
+
356
+ Cron expression editor with preset selector, manual 5-field editor, raw expression input, human-readable descriptions, and next-run calculation.
357
+
358
+ | Prop | Type | Default | Description |
359
+ | ----------------- | ----------------------------- | ------------- | ------------------------------- |
360
+ | `value` | `string` | `"* * * * *"` | Bindable cron expression |
361
+ | `mode` | `CronInputMode` | — | Bindable; predefined or manual |
362
+ | `showPresets` | `boolean` | `true` | Show preset selector |
363
+ | `showFields` | `boolean` | `true` | Show 5-column field editor |
364
+ | `showRawInput` | `boolean` | `true` | Show raw expression input |
365
+ | `showDescription` | `boolean` | `true` | Show human-readable description |
366
+ | `showNextRun` | `boolean` | `true` | Show next run time |
367
+ | `presets` | `CronPreset[]` | default set | Custom presets |
368
+ | `onchange` | `(expr: string, valid: boolean) => void` | — | Change callback |
369
+
370
+ Integrates with `InputWrap` — supports `label`, `description`, `renderSize`, `required`, `disabled`, `validate`, `labelLeft`, `below`.
371
+
372
+ ```svelte
373
+ <CronInput bind:value={cronExpr} label="Schedule" showNextRun />
374
+ ```
375
+
376
+ Exports: `CronInput`, `CronInputProps`, `CronPreset`, `CronInputMode`, `CRON_DEFAULT_PRESETS`, `CronNextRun`.
377
+
378
+ **CronNextRun** — reactive helper class for standalone next-run calculations:
379
+
380
+ ```ts
381
+ const next = new CronNextRun("0 9 * * 1-5");
382
+ next.nextRun; // Date | null
383
+ next.nextRunFormatted; // "YYYY-MM-DD HH:MM"
384
+ next.valid; // boolean
385
+ next.destroy(); // cleanup timer
386
+ ```
387
+
388
+ CSS tokens: `--stuic-cron-input-fields-gap`, `--stuic-cron-input-section-gap`, `--stuic-cron-input-field-label-text`, `--stuic-cron-input-summary-text`, `--stuic-cron-input-error-text`, `--stuic-cron-input-field-bg`, `--stuic-cron-input-field-border`, `--stuic-cron-input-field-border-focus`.
389
+
319
390
  #### `Fieldset`
320
391
 
321
392
  Form field grouping with legend.
@@ -1090,6 +1161,86 @@ Always-visible (non-modal) variant of AssetsPreview with the same zoom, pan, swi
1090
1161
  />
1091
1162
  ```
1092
1163
 
1164
+ #### `Card`
1165
+
1166
+ Flexible, responsive card with image, title, description, footer, and interactive states. Renders as `<a>`, `<button>`, or `<div>` depending on props.
1167
+
1168
+ | Prop | Type | Default | Description |
1169
+ | --------------------- | ------------- | ------------ | ------------------------------------------------- |
1170
+ | `image` | `string` | — | Image URL |
1171
+ | `imageAlt` | `string` | `""` | Alt text |
1172
+ | `eyebrow` | `THC` | — | Small label above title |
1173
+ | `title` | `THC` | — | Card title |
1174
+ | `description` | `THC` | — | Short description |
1175
+ | `variant` | `CardVariant` | `"vertical"` | `"vertical"` or `"horizontal"` layout |
1176
+ | `href` | `string` | — | Renders as `<a>` |
1177
+ | `onclick` | `(e) => void` | — | Renders as `<button>` |
1178
+ | `disabled` | `boolean` | `false` | Disable interaction |
1179
+ | `horizontalThreshold` | `number` | `480` | Width (px) to auto-switch to vertical; 0 disables |
1180
+
1181
+ ```svelte
1182
+ <Card
1183
+ image="/photo.jpg"
1184
+ eyebrow="New"
1185
+ title="Product Name"
1186
+ description="A brief description."
1187
+ href="/products/1"
1188
+ >
1189
+ {#snippet renderFooter()}
1190
+ <Button variant="ghost" size="sm">Details</Button>
1191
+ {/snippet}
1192
+ </Card>
1193
+ ```
1194
+
1195
+ Snippets: `children`, `renderImage({ image, imageAlt })`, `renderBadge`, `renderContent({ eyebrow?, title?, description? })`, `renderFooter`.
1196
+
1197
+ Exports: `Card`, `CardProps`, `CardVariant`.
1198
+
1199
+ CSS tokens: `--stuic-card-bg`, `--stuic-card-bg-hover`, `--stuic-card-border`, `--stuic-card-border-hover`, `--stuic-card-padding`, `--stuic-card-content-gap`, `--stuic-card-image-aspect-ratio`, `--stuic-card-image-object-fit`, `--stuic-card-image-width-horizontal`, `--stuic-card-radius`, `--stuic-card-shadow`, `--stuic-card-shadow-hover`, `--stuic-card-ring-width`, `--stuic-card-ring-color`, `--stuic-card-eyebrow-font-size`, `--stuic-card-title-font-size`, `--stuic-card-title-font-weight`, `--stuic-card-description-font-size`, `--stuic-card-opacity-disabled`.
1200
+
1201
+ #### `Tree`
1202
+
1203
+ Accessible, keyboard-navigable hierarchical tree view with optional drag-and-drop reordering, expand/collapse, and localStorage persistence.
1204
+
1205
+ | Prop | Type | Default | Description |
1206
+ | ----------------- | ---------------------------------------- | ------------ | ------------------------------------ |
1207
+ | `items` | `TreeNodeDTO<T>[]` | required | Tree data |
1208
+ | `renderItem` | `Snippet<[item, depth, isExpanded]>` | — | Custom item content |
1209
+ | `renderIcon` | `Snippet<[item, depth, isExpanded]>` | — | Custom item icon |
1210
+ | `activeId` | `string` | — | Selected item ID |
1211
+ | `isActive` | `(item) => boolean` | — | Custom active check |
1212
+ | `onSelect` | `(item) => void` | — | Item selection callback |
1213
+ | `onToggle` | `(item, expanded) => void` | — | Branch toggle callback |
1214
+ | `sort` | `(a, b) => number` | — | Sort comparator (per-level) |
1215
+ | `defaultExpanded` | `boolean` | `false` | Default expand state |
1216
+ | `expandedIds` | `Set<string>` | — | Initially expanded IDs |
1217
+ | `persistState` | `boolean` | `false` | Save expand state to localStorage |
1218
+ | `draggable` | `boolean` | `false` | Enable drag-and-drop |
1219
+ | `isDraggable` | `(item) => boolean` | — | Per-item drag control |
1220
+ | `isDropTarget` | `(item) => boolean` | — | Per-item drop target control |
1221
+ | `onMove` | `(event: TreeMoveEvent) => void \| false`| — | Drop handler; return false to reject |
1222
+ | `dragExpandDelay` | `number` | `800` | Auto-expand delay (ms) on drag hover |
1223
+
1224
+ ```svelte
1225
+ <Tree
1226
+ items={treeData}
1227
+ activeId={selectedId}
1228
+ onSelect={(item) => (selectedId = item.id)}
1229
+ draggable
1230
+ onMove={handleMove}
1231
+ >
1232
+ {#snippet renderItem(item, depth, isExpanded)}
1233
+ <span>{item.value}</span>
1234
+ {/snippet}
1235
+ </Tree>
1236
+ ```
1237
+
1238
+ Keyboard: Arrow keys (up/down/left/right), Enter/Space, Home/End.
1239
+
1240
+ Exports: `Tree`, `TreeProps`, `TreeMoveEvent`, `TreeDropPosition`, `TreeNodeDTO`, `TREE_BASE_CLASSES`, `TREE_ITEM_CLASSES`, `TREE_CHILDREN_CLASSES`.
1241
+
1242
+ CSS tokens: `--stuic-tree-indent`, `--stuic-tree-item-padding-x`, `--stuic-tree-item-padding-y`, `--stuic-tree-item-height`, `--stuic-tree-item-font-size`, `--stuic-tree-item-gap`, `--stuic-tree-chevron-opacity`, `--stuic-tree-icon-opacity`, `--stuic-tree-item-opacity-dragging`, `--stuic-tree-item-bg`, `--stuic-tree-item-text`, `--stuic-tree-item-bg-hover`, `--stuic-tree-item-text-hover`, `--stuic-tree-item-bg-active`, `--stuic-tree-item-text-active`, `--stuic-tree-item-bg-focus`, `--stuic-tree-item-text-focus`, `--stuic-tree-drop-indicator-color`, `--stuic-tree-drop-indicator-height`, `--stuic-tree-item-bg-dragover`.
1243
+
1093
1244
  #### `X`
1094
1245
 
1095
1246
  Styled SVG close/multiply icon with configurable stroke width.
package/README.md CHANGED
@@ -144,11 +144,11 @@ See [API.md](API.md) for the full list of exported theme types (`ThemeSchema`, `
144
144
 
145
145
  ### Layout & Overlays
146
146
 
147
- AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, SlidingPanels, Nav, WithSidePanel
147
+ AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header, SlidingPanels, Nav, WithSidePanel
148
148
 
149
149
  ### Forms & Inputs
150
150
 
151
- FieldInput, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, Fieldset, LoginForm, LoginFormModal
151
+ FieldInput, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, CronInput, Fieldset, LoginForm, LoginFormModal
152
152
 
153
153
  ### Buttons & Controls
154
154
 
@@ -164,7 +164,7 @@ CommandMenu, DropdownMenu, TabbedMenu, TypeaheadInput, KbdShortcut
164
164
 
165
165
  ### Display & Utility
166
166
 
167
- Avatar, Book, BookResponsive, Carousel, Circle, AnimatedElipsis, H, IconSwap, ImageCycler, Separator, ThemePreview, ColorScheme, Thc, HoverExpandableWidth, AssetsPreview, AssetsPreviewInline, DataTable
167
+ Avatar, Book, BookResponsive, Card, Carousel, Circle, AnimatedElipsis, H, IconSwap, ImageCycler, Separator, ThemePreview, Tree, ColorScheme, Thc, HoverExpandableWidth, AssetsPreview, AssetsPreviewInline, DataTable
168
168
 
169
169
  ### E-commerce
170
170
 
@@ -0,0 +1,315 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+ import type {
6
+ DropdownMenuPosition,
7
+ DropdownMenuItem,
8
+ DropdownMenuActionItem,
9
+ } from "../DropdownMenu/DropdownMenu.svelte";
10
+
11
+ export interface HeaderNavItem {
12
+ /** Unique identifier */
13
+ id: string | number;
14
+ /** Display label — supports THC (string, html, component, snippet) */
15
+ label: THC;
16
+ /** Navigation URL (renders as <a> in expanded mode) */
17
+ href?: string;
18
+ /** Click handler (alternative to href) */
19
+ onclick?: () => void;
20
+ /** Icon before the label — supports THC */
21
+ icon?: THC;
22
+ /** Whether this item is the current/active page */
23
+ active?: boolean;
24
+ /** Whether this item is disabled */
25
+ disabled?: boolean;
26
+ /** Additional CSS classes */
27
+ class?: string;
28
+ }
29
+
30
+ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
31
+ /** Logo/brand snippet — full control over the left branding area */
32
+ logo?: Snippet;
33
+ /** Simple text alternative to the logo snippet */
34
+ projectName?: string;
35
+ /** Navigation items — inline when expanded, DropdownMenu when collapsed */
36
+ items?: HeaderNavItem[];
37
+ /** Avatar/user snippet — rendered at the far right */
38
+ avatar?: Snippet;
39
+ /** When provided, makes the avatar interactive. In expanded mode wraps it in a
40
+ * button; in collapsed mode moves it into the dropdown as an action item. */
41
+ avatarOnClick?: () => void;
42
+ /** Label for the avatar dropdown item in collapsed mode (defaults to "Account") */
43
+ avatarLabel?: THC;
44
+ /** Element width (px) below which nav collapses to hamburger. 0 to disable. */
45
+ collapseThreshold?: number;
46
+ /** Fixed positioning (top of viewport) */
47
+ fixed?: boolean;
48
+ /** Bindable: whether the header is currently in collapsed (hamburger) mode */
49
+ isCollapsed?: boolean;
50
+ /** Bindable: whether the hamburger dropdown is currently open */
51
+ isMenuOpen?: boolean;
52
+ /** Position for the collapsed dropdown menu */
53
+ dropdownPosition?: DropdownMenuPosition;
54
+ /** Hamburger/X icon size in px */
55
+ iconSize?: number;
56
+ /** Called when a nav item is selected (both modes) */
57
+ onSelect?: (item: HeaderNavItem) => void;
58
+ /** Skip all default styling */
59
+ unstyled?: boolean;
60
+ /** Additional CSS classes for the root <header> */
61
+ class?: string;
62
+ /** Classes for the logo area */
63
+ classLogo?: string;
64
+ /** Classes for the nav area (expanded mode) */
65
+ classNav?: string;
66
+ /** Classes for individual nav items (expanded mode) */
67
+ classNavItem?: string;
68
+ /** Classes for active nav items */
69
+ classNavItemActive?: string;
70
+ /** Classes for the end area (avatar + hamburger) */
71
+ classEnd?: string;
72
+ /** Classes for the avatar container */
73
+ classAvatar?: string;
74
+ /** Classes for the hamburger button */
75
+ classHamburger?: string;
76
+ /** Classes for the dropdown wrapper (collapsed mode) */
77
+ classDropdown?: string;
78
+ /** Escape hatch: override the entire inner layout */
79
+ children?: Snippet<
80
+ [{ isCollapsed: boolean; items: HeaderNavItem[]; offsetWidth: number }]
81
+ >;
82
+ /** Bindable root element reference */
83
+ el?: HTMLElement;
84
+ }
85
+
86
+ export const HEADER_BASE_CLASSES = "stuic-header";
87
+ export const HEADER_LOGO_CLASSES = "stuic-header-logo";
88
+ export const HEADER_NAV_CLASSES = "stuic-header-nav";
89
+ export const HEADER_NAV_ITEM_CLASSES = "stuic-header-nav-item";
90
+ export const HEADER_END_CLASSES = "stuic-header-end";
91
+ export const HEADER_HAMBURGER_CLASSES = "stuic-header-hamburger";
92
+ </script>
93
+
94
+ <script lang="ts">
95
+ import { twMerge } from "../../utils/tw-merge.js";
96
+ import { iconMenu, iconX } from "../../icons/index.js";
97
+ import Thc from "../Thc/Thc.svelte";
98
+ import Button from "../Button/Button.svelte";
99
+ import DropdownMenu from "../DropdownMenu/DropdownMenu.svelte";
100
+ import IconSwap from "../IconSwap/IconSwap.svelte";
101
+
102
+ let {
103
+ logo,
104
+ projectName,
105
+ items = [],
106
+ avatar,
107
+ avatarOnClick,
108
+ avatarLabel = "Account",
109
+ collapseThreshold = 768,
110
+ fixed = false,
111
+ isCollapsed = $bindable(false),
112
+ isMenuOpen = $bindable(false),
113
+ dropdownPosition = "bottom-span-right",
114
+ iconSize = 24,
115
+ onSelect,
116
+ unstyled = false,
117
+ class: classProp,
118
+ classLogo,
119
+ classNav,
120
+ classNavItem,
121
+ classNavItemActive,
122
+ classEnd,
123
+ classAvatar,
124
+ classHamburger,
125
+ classDropdown,
126
+ children,
127
+ el = $bindable(),
128
+ ...rest
129
+ }: Props = $props();
130
+
131
+ // Width measurement (same pattern as Card)
132
+ let _offsetWidth = $state(0);
133
+
134
+ // Collapsed state based on threshold
135
+ let _isCollapsed = $derived.by(() => {
136
+ if (!collapseThreshold) return false;
137
+ return _offsetWidth > 0 && _offsetWidth < collapseThreshold;
138
+ });
139
+
140
+ // Sync bindable
141
+ $effect(() => {
142
+ isCollapsed = _isCollapsed;
143
+ });
144
+
145
+ // Close menu when switching from collapsed to expanded
146
+ $effect(() => {
147
+ if (!_isCollapsed && isMenuOpen) {
148
+ isMenuOpen = false;
149
+ }
150
+ });
151
+
152
+ // Whether the avatar moves into the dropdown when collapsed
153
+ let _avatarInDropdown = $derived(!!(avatar && avatarOnClick));
154
+
155
+ // Map HeaderNavItem[] to DropdownMenuItem[] for collapsed mode
156
+ let _dropdownItems = $derived.by((): DropdownMenuItem[] => {
157
+ const navItems: DropdownMenuItem[] = items.map(
158
+ (item) =>
159
+ ({
160
+ type: "action" as const,
161
+ id: item.id,
162
+ label: item.label,
163
+ contentBefore: item.icon,
164
+ disabled: item.disabled,
165
+ class: item.class,
166
+ onSelect: () => {
167
+ if (item.href) window.location.href = item.href;
168
+ item.onclick?.();
169
+ onSelect?.(item);
170
+ },
171
+ }) satisfies DropdownMenuActionItem
172
+ );
173
+
174
+ // Append avatar as a dropdown item when avatarOnClick is set
175
+ if (_avatarInDropdown) {
176
+ if (navItems.length > 0) {
177
+ navItems.push({ type: "divider" });
178
+ }
179
+ navItems.push({
180
+ type: "action",
181
+ id: "__avatar__",
182
+ label: avatarLabel!,
183
+ onSelect: () => avatarOnClick!(),
184
+ } satisfies DropdownMenuActionItem);
185
+ }
186
+
187
+ return navItems;
188
+ });
189
+
190
+ // CSS classes
191
+ let _class = $derived(unstyled ? classProp : twMerge(HEADER_BASE_CLASSES, classProp));
192
+ let _classLogo = $derived(
193
+ unstyled ? classLogo : twMerge(HEADER_LOGO_CLASSES, classLogo)
194
+ );
195
+ let _classNav = $derived(unstyled ? classNav : twMerge(HEADER_NAV_CLASSES, classNav));
196
+ let _classEnd = $derived(unstyled ? classEnd : twMerge(HEADER_END_CLASSES, classEnd));
197
+
198
+ function handleItemClick(item: HeaderNavItem) {
199
+ if (item.disabled) return;
200
+ item.onclick?.();
201
+ onSelect?.(item);
202
+ }
203
+ </script>
204
+
205
+ <header
206
+ bind:this={el}
207
+ bind:offsetWidth={_offsetWidth}
208
+ class={_class}
209
+ data-fixed={!unstyled && fixed ? "" : undefined}
210
+ data-collapsed={!unstyled && _isCollapsed ? "" : undefined}
211
+ {...rest}
212
+ >
213
+ {#if children}
214
+ {@render children({
215
+ isCollapsed: _isCollapsed,
216
+ items,
217
+ offsetWidth: _offsetWidth,
218
+ })}
219
+ {:else}
220
+ <!-- Logo / Brand -->
221
+ {#if logo || projectName}
222
+ <div class={_classLogo}>
223
+ {#if logo}
224
+ {@render logo()}
225
+ {:else if projectName}
226
+ <span class={unstyled ? undefined : "stuic-header-project-name"}>
227
+ {projectName}
228
+ </span>
229
+ {/if}
230
+ </div>
231
+ {/if}
232
+
233
+ <!-- Nav items (expanded mode) -->
234
+ {#if !_isCollapsed && items.length > 0}
235
+ <nav class={_classNav}>
236
+ {#each items as item (item.id)}
237
+ <Button
238
+ variant="ghost"
239
+ size="sm"
240
+ href={item.href}
241
+ disabled={item.disabled}
242
+ {unstyled}
243
+ class={twMerge(
244
+ !unstyled && HEADER_NAV_ITEM_CLASSES,
245
+ !unstyled && item.active && classNavItemActive,
246
+ classNavItem,
247
+ item.class
248
+ )}
249
+ data-active={!unstyled && item.active ? "" : undefined}
250
+ aria-current={item.active ? "page" : undefined}
251
+ onclick={() => handleItemClick(item)}
252
+ >
253
+ {#if item.icon}
254
+ <span class={unstyled ? undefined : "stuic-header-nav-icon"}>
255
+ <Thc thc={item.icon} />
256
+ </span>
257
+ {/if}
258
+ <Thc thc={item.label} />
259
+ </Button>
260
+ {/each}
261
+ </nav>
262
+ {/if}
263
+
264
+ <!-- Spacer -->
265
+ <div class={unstyled ? undefined : "stuic-header-spacer"}></div>
266
+
267
+ <!-- End area: avatar + hamburger -->
268
+ <div class={_classEnd}>
269
+ <!-- Avatar: hidden when collapsed + avatarOnClick (moves into dropdown) -->
270
+ {#if avatar && !(_isCollapsed && _avatarInDropdown)}
271
+ {#if avatarOnClick}
272
+ <button
273
+ type="button"
274
+ class={twMerge(!unstyled && "stuic-header-avatar", classAvatar)}
275
+ onclick={avatarOnClick}
276
+ >
277
+ {@render avatar()}
278
+ </button>
279
+ {:else}
280
+ <div class={twMerge(!unstyled && "stuic-header-avatar", classAvatar)}>
281
+ {@render avatar()}
282
+ </div>
283
+ {/if}
284
+ {/if}
285
+
286
+ {#if _isCollapsed && _dropdownItems.length > 0}
287
+ <DropdownMenu
288
+ items={_dropdownItems}
289
+ bind:isOpen={isMenuOpen}
290
+ position={dropdownPosition}
291
+ class={classDropdown}
292
+ >
293
+ {#snippet trigger({ isOpen, toggle, triggerProps })}
294
+ <Button
295
+ variant="ghost"
296
+ roundedFull
297
+ aspect1
298
+ size="sm"
299
+ {unstyled}
300
+ class={twMerge(!unstyled && HEADER_HAMBURGER_CLASSES, classHamburger)}
301
+ onclick={toggle}
302
+ aria-label={isOpen ? "Close menu" : "Open menu"}
303
+ {...triggerProps}
304
+ >
305
+ <IconSwap
306
+ active={isOpen ? 1 : 0}
307
+ states={[iconMenu({ size: iconSize }), iconX({ size: iconSize })]}
308
+ />
309
+ </Button>
310
+ {/snippet}
311
+ </DropdownMenu>
312
+ {/if}
313
+ </div>
314
+ {/if}
315
+ </header>
@@ -0,0 +1,90 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { THC } from "../Thc/Thc.svelte";
4
+ import type { DropdownMenuPosition } from "../DropdownMenu/DropdownMenu.svelte";
5
+ export interface HeaderNavItem {
6
+ /** Unique identifier */
7
+ id: string | number;
8
+ /** Display label — supports THC (string, html, component, snippet) */
9
+ label: THC;
10
+ /** Navigation URL (renders as <a> in expanded mode) */
11
+ href?: string;
12
+ /** Click handler (alternative to href) */
13
+ onclick?: () => void;
14
+ /** Icon before the label — supports THC */
15
+ icon?: THC;
16
+ /** Whether this item is the current/active page */
17
+ active?: boolean;
18
+ /** Whether this item is disabled */
19
+ disabled?: boolean;
20
+ /** Additional CSS classes */
21
+ class?: string;
22
+ }
23
+ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
24
+ /** Logo/brand snippet — full control over the left branding area */
25
+ logo?: Snippet;
26
+ /** Simple text alternative to the logo snippet */
27
+ projectName?: string;
28
+ /** Navigation items — inline when expanded, DropdownMenu when collapsed */
29
+ items?: HeaderNavItem[];
30
+ /** Avatar/user snippet — rendered at the far right */
31
+ avatar?: Snippet;
32
+ /** When provided, makes the avatar interactive. In expanded mode wraps it in a
33
+ * button; in collapsed mode moves it into the dropdown as an action item. */
34
+ avatarOnClick?: () => void;
35
+ /** Label for the avatar dropdown item in collapsed mode (defaults to "Account") */
36
+ avatarLabel?: THC;
37
+ /** Element width (px) below which nav collapses to hamburger. 0 to disable. */
38
+ collapseThreshold?: number;
39
+ /** Fixed positioning (top of viewport) */
40
+ fixed?: boolean;
41
+ /** Bindable: whether the header is currently in collapsed (hamburger) mode */
42
+ isCollapsed?: boolean;
43
+ /** Bindable: whether the hamburger dropdown is currently open */
44
+ isMenuOpen?: boolean;
45
+ /** Position for the collapsed dropdown menu */
46
+ dropdownPosition?: DropdownMenuPosition;
47
+ /** Hamburger/X icon size in px */
48
+ iconSize?: number;
49
+ /** Called when a nav item is selected (both modes) */
50
+ onSelect?: (item: HeaderNavItem) => void;
51
+ /** Skip all default styling */
52
+ unstyled?: boolean;
53
+ /** Additional CSS classes for the root <header> */
54
+ class?: string;
55
+ /** Classes for the logo area */
56
+ classLogo?: string;
57
+ /** Classes for the nav area (expanded mode) */
58
+ classNav?: string;
59
+ /** Classes for individual nav items (expanded mode) */
60
+ classNavItem?: string;
61
+ /** Classes for active nav items */
62
+ classNavItemActive?: string;
63
+ /** Classes for the end area (avatar + hamburger) */
64
+ classEnd?: string;
65
+ /** Classes for the avatar container */
66
+ classAvatar?: string;
67
+ /** Classes for the hamburger button */
68
+ classHamburger?: string;
69
+ /** Classes for the dropdown wrapper (collapsed mode) */
70
+ classDropdown?: string;
71
+ /** Escape hatch: override the entire inner layout */
72
+ children?: Snippet<[
73
+ {
74
+ isCollapsed: boolean;
75
+ items: HeaderNavItem[];
76
+ offsetWidth: number;
77
+ }
78
+ ]>;
79
+ /** Bindable root element reference */
80
+ el?: HTMLElement;
81
+ }
82
+ export declare const HEADER_BASE_CLASSES = "stuic-header";
83
+ export declare const HEADER_LOGO_CLASSES = "stuic-header-logo";
84
+ export declare const HEADER_NAV_CLASSES = "stuic-header-nav";
85
+ export declare const HEADER_NAV_ITEM_CLASSES = "stuic-header-nav-item";
86
+ export declare const HEADER_END_CLASSES = "stuic-header-end";
87
+ export declare const HEADER_HAMBURGER_CLASSES = "stuic-header-hamburger";
88
+ declare const Header: import("svelte").Component<Props, {}, "el" | "isCollapsed" | "isMenuOpen">;
89
+ type Header = ReturnType<typeof Header>;
90
+ export default Header;
@@ -0,0 +1,135 @@
1
+ /* ============================================================================
2
+ HEADER COMPONENT TOKENS
3
+ Override globally: :root { --stuic-header-bg: red; }
4
+ Override locally: <Header style="--stuic-header-bg: red;">
5
+ ============================================================================ */
6
+
7
+ :root {
8
+ /* Structure */
9
+ --stuic-header-padding-x: 1rem;
10
+ --stuic-header-padding-y: 0.75rem;
11
+ --stuic-header-gap: 1rem;
12
+ --stuic-header-min-height: 3.5rem;
13
+
14
+ /* Nav items */
15
+ --stuic-header-nav-gap: 0.25rem;
16
+
17
+ /* Project name */
18
+ --stuic-header-project-name-font-weight: var(--font-weight-semibold, 600);
19
+
20
+ /* Fixed mode */
21
+ --stuic-header-z-index: 40;
22
+ }
23
+
24
+ @layer components {
25
+ /* ============================================================================
26
+ BASE
27
+ ============================================================================ */
28
+
29
+ .stuic-header {
30
+ display: flex;
31
+ align-items: center;
32
+ width: 100%;
33
+ min-height: var(--stuic-header-min-height);
34
+ padding: var(--stuic-header-padding-y) var(--stuic-header-padding-x);
35
+ gap: var(--stuic-header-gap);
36
+ background: var(--stuic-header-bg, var(--color-background, inherit));
37
+ color: var(--stuic-header-text, inherit);
38
+ border-bottom: var(--stuic-header-border-width, var(--stuic-border-width)) solid
39
+ var(--stuic-header-border-color, var(--color-border, transparent));
40
+ }
41
+
42
+ /* ============================================================================
43
+ FIXED
44
+ ============================================================================ */
45
+
46
+ .stuic-header[data-fixed] {
47
+ position: fixed;
48
+ top: 0;
49
+ left: 0;
50
+ right: 0;
51
+ z-index: var(--stuic-header-z-index);
52
+ }
53
+
54
+ /* ============================================================================
55
+ LOGO
56
+ ============================================================================ */
57
+
58
+ .stuic-header-logo {
59
+ display: flex;
60
+ align-items: center;
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ .stuic-header-project-name {
65
+ font-size: var(--stuic-header-project-name-font-size, var(--text-lg, 1.125rem));
66
+ font-weight: var(--stuic-header-project-name-font-weight);
67
+ white-space: nowrap;
68
+ }
69
+
70
+ /* ============================================================================
71
+ NAV (expanded mode) — items are ghost Buttons
72
+ ============================================================================ */
73
+
74
+ .stuic-header-nav {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: var(--stuic-header-nav-gap);
78
+ }
79
+
80
+ /* Active state highlight for nav Buttons */
81
+ .stuic-header-nav-item[data-active] {
82
+ color: var(--stuic-header-nav-item-text-active, var(--color-foreground, inherit));
83
+ background: var(--stuic-header-nav-item-bg-active, var(--color-muted, transparent));
84
+ }
85
+
86
+ /* ============================================================================
87
+ NAV ICON (inline icon before label)
88
+ ============================================================================ */
89
+
90
+ .stuic-header-nav-icon {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ flex-shrink: 0;
94
+ }
95
+
96
+ /* ============================================================================
97
+ SPACER
98
+ ============================================================================ */
99
+
100
+ .stuic-header-spacer {
101
+ flex: 1;
102
+ }
103
+
104
+ /* ============================================================================
105
+ END AREA (avatar + hamburger)
106
+ ============================================================================ */
107
+
108
+ .stuic-header-end {
109
+ display: flex;
110
+ align-items: center;
111
+ gap: var(--stuic-header-gap);
112
+ flex-shrink: 0;
113
+ }
114
+
115
+ .stuic-header-avatar {
116
+ display: flex;
117
+ align-items: center;
118
+ }
119
+
120
+ button.stuic-header-avatar {
121
+ border: none;
122
+ background: none;
123
+ padding: 0;
124
+ cursor: pointer;
125
+ -webkit-tap-highlight-color: transparent;
126
+ }
127
+
128
+ /* ============================================================================
129
+ HAMBURGER
130
+ ============================================================================ */
131
+
132
+ .stuic-header-hamburger {
133
+ /* Ghost Button handles hover/focus — only override sizing */
134
+ }
135
+ }
@@ -0,0 +1 @@
1
+ export { default as Header, type Props as HeaderProps, type HeaderNavItem, HEADER_BASE_CLASSES, HEADER_LOGO_CLASSES, HEADER_NAV_CLASSES, HEADER_NAV_ITEM_CLASSES, HEADER_END_CLASSES, HEADER_HAMBURGER_CLASSES, } from "./Header.svelte";
@@ -0,0 +1 @@
1
+ export { default as Header, HEADER_BASE_CLASSES, HEADER_LOGO_CLASSES, HEADER_NAV_CLASSES, HEADER_NAV_ITEM_CLASSES, HEADER_END_CLASSES, HEADER_HAMBURGER_CLASSES, } from "./Header.svelte";
package/dist/index.css CHANGED
@@ -68,6 +68,7 @@ In practice:
68
68
  @import "./components/DismissibleMessage/index.css";
69
69
  @import "./components/DropdownMenu/index.css";
70
70
  @import "./components/H/index.css";
71
+ @import "./components/Header/index.css";
71
72
  @import "./components/ImageCycler/index.css";
72
73
  @import "./components/IconSwap/index.css";
73
74
  @import "./components/Input/index.css";
package/dist/index.d.ts CHANGED
@@ -44,6 +44,7 @@ export * from "./components/DismissibleMessage/index.js";
44
44
  export * from "./components/Drawer/index.js";
45
45
  export * from "./components/DropdownMenu/index.js";
46
46
  export * from "./components/H/index.js";
47
+ export * from "./components/Header/index.js";
47
48
  export * from "./components/ImageCycler/index.js";
48
49
  export * from "./components/HoverExpandableWidth/index.js";
49
50
  export * from "./components/IconSwap/index.js";
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ export * from "./components/DismissibleMessage/index.js";
45
45
  export * from "./components/Drawer/index.js";
46
46
  export * from "./components/DropdownMenu/index.js";
47
47
  export * from "./components/H/index.js";
48
+ export * from "./components/Header/index.js";
48
49
  export * from "./components/ImageCycler/index.js";
49
50
  export * from "./components/HoverExpandableWidth/index.js";
50
51
  export * from "./components/IconSwap/index.js";
@@ -2,24 +2,25 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- 46 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
5
+ 50 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
6
6
 
7
7
  ## Component Categories
8
8
 
9
9
  ### Layout
10
10
 
11
- | Component | Purpose |
12
- | ------------------------ | -------------------------------------------------- |
13
- | AppShell, AppShellSimple | Page layouts with header/sidebar/content |
14
- | Modal, ModalDialog | Overlay containers |
15
- | Drawer | Side panel overlay |
16
- | Backdrop | Semi-transparent overlay with escape/focus trap |
17
- | Collapsible | Expandable sections |
18
- | Accordion | Exclusive/multi-open expandable sections |
19
- | SlidingPanels | Panel transitions |
20
- | TabbedMenu | Tab navigation |
21
- | Nav | Navigation wrapper |
22
- | WithSidePanel | Two-column layout with collapsible/resizable panel |
11
+ | Component | Purpose |
12
+ | ------------------------ | ---------------------------------------------------- |
13
+ | AppShell, AppShellSimple | Page layouts with header/sidebar/content |
14
+ | Header | Responsive nav header with hamburger collapse |
15
+ | Modal, ModalDialog | Overlay containers |
16
+ | Drawer | Side panel overlay |
17
+ | Backdrop | Semi-transparent overlay with escape/focus trap |
18
+ | Collapsible | Expandable sections |
19
+ | Accordion | Exclusive/multi-open expandable sections |
20
+ | SlidingPanels | Panel transitions |
21
+ | TabbedMenu | Tab navigation |
22
+ | Nav | Navigation wrapper |
23
+ | WithSidePanel | Two-column layout with collapsible/resizable panel |
23
24
 
24
25
  ### Interactive
25
26
 
@@ -56,6 +57,7 @@
56
57
  | Input (FieldInput, FieldSelect, etc.) | Form fields |
57
58
  | FieldPhoneNumber | International phone input with country picker |
58
59
  | FieldObject | Dual-mode JSON object editor (pretty-print/raw) |
60
+ | CronInput | Cron expression editor with presets and validation |
59
61
  | Fieldset | Field grouping with legend |
60
62
  | FieldKeyValues | Key-value pair editor |
61
63
  | FieldAssets | File/asset management |
@@ -81,6 +83,8 @@
81
83
  | H | Semantic heading (h1-h6) with separate visual/semantic levels |
82
84
  | Separator | Horizontal/vertical separator line |
83
85
  | Thc | Flexible renderer for text, HTML, components, or snippets |
86
+ | Card | Flexible card with image, title, footer; vertical/horizontal layout |
87
+ | Tree | Hierarchical tree view with keyboard nav and drag-and-drop |
84
88
  | X | Styled close/multiply SVG icon |
85
89
  | ImageCycler | Auto-cycling image carousel with fade transitions and preloading |
86
90
 
@@ -373,6 +377,199 @@ Components use private CSS vars (`--_*`) set by intent/variant:
373
377
 
374
378
  ---
375
379
 
380
+ ## Card
381
+
382
+ Flexible, responsive card for displaying content in a contained box. Supports images (top or side), titles, descriptions, footers, and interactive states.
383
+
384
+ ### Exports
385
+
386
+ | Export | Kind | Description |
387
+ | ------------- | --------- | ------------------------------------ |
388
+ | `Card` | component | Main card component |
389
+ | `CardProps` | type | Props type |
390
+ | `CardVariant` | type | `"vertical" \| "horizontal"` |
391
+
392
+ ### Key Props
393
+
394
+ | Prop | Type | Default | Description |
395
+ | ---------------------- | ------------------------ | ------------ | -------------------------------------------- |
396
+ | `image` | `string` | — | Image URL |
397
+ | `eyebrow` | `THC` | — | Small label above title |
398
+ | `title` | `THC` | — | Card title |
399
+ | `description` | `THC` | — | Short description |
400
+ | `variant` | `CardVariant` | `"vertical"` | Layout direction |
401
+ | `href` | `string` | — | Renders as `<a>` |
402
+ | `onclick` | `(e) => void` | — | Renders as `<button>` |
403
+ | `disabled` | `boolean` | `false` | Disable interaction |
404
+ | `horizontalThreshold` | `number` | `480` | Width (px) to auto-switch to vertical; 0 disables |
405
+
406
+ Snippets: `children`, `renderImage`, `renderBadge`, `renderContent`, `renderFooter`.
407
+
408
+ ### CSS Tokens
409
+
410
+ Prefix: `--stuic-card-*`
411
+
412
+ `bg`, `bg-hover`, `border`, `border-hover`, `padding`, `content-gap`, `image-aspect-ratio`, `image-object-fit`, `image-width-horizontal`, `radius`, `shadow`, `shadow-hover`, `ring-width`, `ring-color`, `eyebrow-font-size`, `eyebrow-text`, `title-font-size`, `title-font-weight`, `title-text`, `description-font-size`, `description-text`, `opacity-disabled`
413
+
414
+ ---
415
+
416
+ ## CronInput
417
+
418
+ Cron expression editor with preset selector, manual 5-field editor, raw expression input, human-readable descriptions, and next-run calculation.
419
+
420
+ ### Exports
421
+
422
+ | Export | Kind | Description |
423
+ | ---------------------- | --------- | ---------------------------------------- |
424
+ | `CronInput` | component | Main cron editor |
425
+ | `CronInputProps` | type | Props type |
426
+ | `CronPreset` | type | Preset interface `{ label, value }` |
427
+ | `CronInputMode` | type | `"predefined" \| "manual"` |
428
+ | `CRON_DEFAULT_PRESETS` | constant | Default preset array (11 common schedules) |
429
+ | `CronNextRun` | class | Reactive helper for standalone cron calculations |
430
+
431
+ ### Key Props
432
+
433
+ | Prop | Type | Default | Description |
434
+ | ----------------- | ----------------- | ------------- | --------------------------------- |
435
+ | `value` | `string` | `"* * * * *"` | Bindable cron expression |
436
+ | `mode` | `CronInputMode` | — | Bindable; predefined or manual |
437
+ | `showPresets` | `boolean` | `true` | Show preset selector |
438
+ | `showFields` | `boolean` | `true` | Show 5-column field editor |
439
+ | `showRawInput` | `boolean` | `true` | Show raw expression input |
440
+ | `showDescription` | `boolean` | `true` | Show human-readable description |
441
+ | `showNextRun` | `boolean` | `true` | Show next run time |
442
+ | `presets` | `CronPreset[]` | default set | Custom presets |
443
+ | `onchange` | `(expr, valid) => void` | — | Change callback |
444
+
445
+ Integrates with `InputWrap` — supports `label`, `description`, `renderSize`, `required`, `disabled`, `validate`, `labelLeft`, `below`.
446
+
447
+ ### CronNextRun Class
448
+
449
+ Reactive helper for standalone cron calculations:
450
+
451
+ ```ts
452
+ const next = new CronNextRun("0 9 * * 1-5");
453
+ next.nextRun; // Date | null
454
+ next.nextRunFormatted; // "YYYY-MM-DD HH:MM"
455
+ next.valid; // boolean
456
+ next.destroy(); // cleanup timer
457
+ ```
458
+
459
+ ### CSS Tokens
460
+
461
+ Prefix: `--stuic-cron-input-*`
462
+
463
+ `fields-gap`, `section-gap`, `field-label-text`, `summary-text`, `error-text`, `field-bg`, `field-border`, `field-border-focus`
464
+
465
+ ---
466
+
467
+ ## Header
468
+
469
+ Responsive navigation header with logo, nav items, avatar, and automatic hamburger collapse.
470
+
471
+ ### Exports
472
+
473
+ | Export | Kind | Description |
474
+ | -------------------------- | -------- | ------------------------- |
475
+ | `Header` | component | Main header component |
476
+ | `HeaderProps` | type | Props type |
477
+ | `HeaderNavItem` | type | Nav item interface |
478
+ | `HEADER_BASE_CLASSES` | constant | `"stuic-header"` |
479
+ | `HEADER_LOGO_CLASSES` | constant | `"stuic-header-logo"` |
480
+ | `HEADER_NAV_CLASSES` | constant | `"stuic-header-nav"` |
481
+ | `HEADER_NAV_ITEM_CLASSES` | constant | `"stuic-header-nav-item"` |
482
+ | `HEADER_END_CLASSES` | constant | `"stuic-header-end"` |
483
+ | `HEADER_HAMBURGER_CLASSES` | constant | `"stuic-header-hamburger"` |
484
+
485
+ ### HeaderNavItem
486
+
487
+ ```ts
488
+ interface HeaderNavItem {
489
+ id: string | number;
490
+ label: THC;
491
+ href?: string;
492
+ onclick?: () => void;
493
+ icon?: THC;
494
+ active?: boolean;
495
+ disabled?: boolean;
496
+ class?: string;
497
+ }
498
+ ```
499
+
500
+ ### Key Props
501
+
502
+ | Prop | Type | Default | Description |
503
+ | ------------------- | --------------------- | ----------------------- | ------------------------------------ |
504
+ | `logo` | `Snippet` | — | Logo/brand snippet |
505
+ | `projectName` | `string` | — | Simple text logo alternative |
506
+ | `items` | `HeaderNavItem[]` | `[]` | Navigation items |
507
+ | `avatar` | `Snippet` | — | Avatar snippet (far right) |
508
+ | `avatarOnClick` | `() => void` | — | Avatar click handler |
509
+ | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables |
510
+ | `fixed` | `boolean` | `false` | Fixed positioning at top |
511
+ | `isCollapsed` | `boolean` | — | Bindable: collapsed state |
512
+ | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open |
513
+ | `onSelect` | `(item) => void` | — | Item selection callback |
514
+
515
+ Snippets: `logo`, `avatar`, `children({ isCollapsed, items, offsetWidth })`.
516
+
517
+ ### CSS Tokens
518
+
519
+ Prefix: `--stuic-header-*`
520
+
521
+ `padding-x`, `padding-y`, `gap`, `min-height`, `nav-gap`, `project-name-font-weight`, `z-index`, `bg`, `text`, `border-width`, `border-color`, `nav-item-bg-active`, `nav-item-text-active`
522
+
523
+ ---
524
+
525
+ ## Tree
526
+
527
+ Accessible, keyboard-navigable hierarchical tree view with optional drag-and-drop reordering and localStorage persistence.
528
+
529
+ ### Exports
530
+
531
+ | Export | Kind | Description |
532
+ | ------------------- | --------- | ------------------------------------------ |
533
+ | `Tree` | component | Main tree component |
534
+ | `TreeProps` | type | Props type |
535
+ | `TreeMoveEvent` | type | Drag-drop event interface |
536
+ | `TreeDropPosition` | type | `"before" \| "after" \| "inside"` |
537
+ | `TREE_BASE_CLASSES` | constant | `"stuic-tree"` |
538
+ | `TREE_ITEM_CLASSES` | constant | `"stuic-tree-item"` |
539
+ | `TREE_CHILDREN_CLASSES` | constant | `"stuic-tree-children"` |
540
+ | `TreeNodeDTO` | type | Re-exported from `@marianmeres/tree` |
541
+
542
+ ### Key Props
543
+
544
+ | Prop | Type | Default | Description |
545
+ | ----------------- | -------------------------- | -------------- | --------------------------------------- |
546
+ | `items` | `TreeNodeDTO<T>[]` | required | Tree data |
547
+ | `renderItem` | `Snippet<[item, depth, isExpanded]>` | — | Custom item content |
548
+ | `renderIcon` | `Snippet<[item, depth, isExpanded]>` | — | Custom item icon |
549
+ | `activeId` | `string` | — | Selected item ID |
550
+ | `isActive` | `(item) => boolean` | — | Custom active check |
551
+ | `onSelect` | `(item) => void` | — | Item selection callback |
552
+ | `onToggle` | `(item, expanded) => void` | — | Branch toggle callback |
553
+ | `sort` | `(a, b) => number` | — | Sort comparator (per-level) |
554
+ | `defaultExpanded` | `boolean` | `false` | Default expand state |
555
+ | `expandedIds` | `Set<string>` | — | Initially expanded IDs |
556
+ | `persistState` | `boolean` | `false` | Save expand state to localStorage |
557
+ | `draggable` | `boolean` | `false` | Enable drag-and-drop |
558
+ | `isDraggable` | `(item) => boolean` | — | Per-item drag control |
559
+ | `isDropTarget` | `(item) => boolean` | — | Per-item drop target control |
560
+ | `onMove` | `(event) => void \| false` | — | Drop handler; return false to reject |
561
+ | `dragExpandDelay` | `number` | `800` | Auto-expand delay (ms) on drag hover |
562
+
563
+ Features: arrow-key navigation, Home/End, Enter/Space, roving tabindex, slide transitions (respects `prefers-reduced-motion`).
564
+
565
+ ### CSS Tokens
566
+
567
+ Prefix: `--stuic-tree-*`
568
+
569
+ `indent`, `item-padding-x`, `item-padding-y`, `item-height`, `item-font-size`, `item-gap`, `chevron-opacity`, `icon-opacity`, `item-opacity-dragging`, `item-bg`, `item-text`, `item-bg-hover`, `item-text-hover`, `item-bg-active`, `item-text-active`, `item-bg-focus`, `item-text-focus`, `drop-indicator-color`, `drop-indicator-height`, `item-bg-dragover`
570
+
571
+ ---
572
+
376
573
  ## Key Files
377
574
 
378
575
  | File | Purpose |
@@ -382,4 +579,6 @@ Components use private CSS vars (`--_*`) set by intent/variant:
382
579
  | src/lib/components/Input/ | Form field patterns (incl. FieldObject) |
383
580
  | src/lib/components/LoginForm/ | Standalone login form + modal variant |
384
581
  | src/lib/components/Checkout/ | E-commerce checkout flow (14 exported sub-components) |
582
+ | src/lib/components/Card/ | Card with image/title/footer variants |
583
+ | src/lib/components/Tree/ | Hierarchical tree with drag-and-drop |
385
584
  | src/lib/index.ts | All component exports |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.51.2",
3
+ "version": "3.52.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",