@marianmeres/stuic 3.96.0 → 3.97.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,15 +23,16 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 56 UI components
26
+ ├── components/ # 57 component directories
27
27
  ├── actions/ # 15 Svelte actions
28
- ├── utils/ # 43 utility modules
29
- ├── themes/ # Generated theme CSS (css/) — definitions from @marianmeres/design-tokens
30
- ├── icons/ # Icon re-exports
28
+ ├── utils/ # 44 utility modules
29
+ ├── icons/ # Icon re-exports from @marianmeres/icons-fns
31
30
  ├── index.css # Centralized CSS imports
32
31
  └── index.ts # Main exports
33
32
  ```
34
33
 
34
+ Theme CSS files are not bundled in this package — they're provided by `@marianmeres/design-tokens/css/*.css` (42 themes) and imported by `src/lib/index.css`.
35
+
35
36
  ---
36
37
 
37
38
  ## Critical Conventions
@@ -116,10 +117,10 @@ Global tokens that control cross-component visual properties. Defined in `src/li
116
117
 
117
118
  ### Domain Docs
118
119
 
119
- - [Components](./docs/domains/components.md) — 56 component directories, Props pattern, snippets
120
+ - [Components](./docs/domains/components.md) — 57 component directories, Props pattern, snippets
120
121
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
121
122
  - [Actions](./docs/domains/actions.md) — 15 Svelte directives
122
- - [Utils](./docs/domains/utils.md) — 43 utility modules
123
+ - [Utils](./docs/domains/utils.md) — 44 utility modules
123
124
 
124
125
  ### Reference
125
126
 
package/API.md CHANGED
@@ -132,20 +132,41 @@ Navigation wrapper component.
132
132
 
133
133
  #### `Header`
134
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 |
135
+ Responsive navigation header with leading slot, logo, nav items, locale switcher, action icon buttons, avatar, and configurable responsive collapse (`"hamburger"` fold or `"hide"` for app-like shells). Renders as `<header>`.
136
+
137
+ | Prop | Type | Default | Description |
138
+ | ----------------------- | ----------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
139
+ | `leading` | `Snippet<[{ isCollapsed }]>` | — | Leading (left-side) slot. Overrides `leadingHamburger`. |
140
+ | `leadingHamburger` | `boolean \| "collapsed"` | `false` | Built-in hamburger in the leading slot (`"collapsed"` = only below threshold). Ignored when `leading` is provided. |
141
+ | `onLeadingHamburger` | `() => void` | — | Click handler for the built-in leading hamburger (typically opens a drawer). |
142
+ | `leadingHamburgerIcon` | `THC` | menu icon | Icon override for the built-in leading hamburger. |
143
+ | `leadingHamburgerLabel` | `string` | `"Open menu"` | Aria-label for the leading hamburger. |
144
+ | `logo` | `Snippet` | | Logo/brand snippet. |
145
+ | `projectName` | `string` | | Simple text logo alternative. |
146
+ | `navVariant` | `ButtonVariant` | `"ghost"` | Button variant for nav items and the locale switcher trigger. |
147
+ | `items` | `HeaderNavItem[]` | `[]` | Navigation items — inline when expanded, dropdown when collapsed (hamburger mode). |
148
+ | `actions` | `HeaderActionItem[]` | `[]` | Action icon buttons between the locale switcher and the avatar. Always visible never fold into the dropdown. |
149
+ | `onActionSelect` | `(action) => void` | — | Called after the per-item `onclick`. |
150
+ | `avatar` | `Snippet` | — | Avatar snippet (far right). |
151
+ | `avatarOnClick` | `() => void` | — | Makes the avatar interactive. In `"hamburger"` collapse mode it moves into the dropdown. |
152
+ | `avatarLabel` | `THC` | `"Account"` | Label for the avatar entry inside the collapsed dropdown. |
153
+ | `locales` | `HeaderLocaleItem[]` | `[]` | Locale items. Switcher only renders when 2+. |
154
+ | `activeLocale` | `string` | — | Current locale id. |
155
+ | `onLocaleChange` | `(localeId) => void` | — | Locale selection callback. |
156
+ | `localeLabel` | `THC` | `"Language"` | Section header inside the collapsed dropdown. |
157
+ | `contentMaxWidth` | `string \| number` | — | Max-width of the inner content row (outer header stays 100%). Accepts any CSS length. Maps to `--stuic-header-content-max-width`. |
158
+ | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables. |
159
+ | `collapseMode` | `"hamburger" \| "hide"` | `"hamburger"` | Collapse behavior. `"hide"` keeps avatar/actions visible and renders no trailing hamburger (app-shell pattern). |
160
+ | `keepLocaleOnCollapse` | `boolean` | `false` | Keep the locale switcher visible in collapsed mode (only `collapseMode === "hide"`). |
161
+ | `fixed` | `boolean` | `false` | Fixed positioning at the top. |
162
+ | `isCollapsed` | `boolean` | — | Bindable: collapsed state. |
163
+ | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open. |
164
+ | `dropdownPosition` | `DropdownMenuPosition` | `"bottom-span-right"` | Position of the collapsed dropdown. |
165
+ | `iconSize` | `number` | `24` | Hamburger/X icon size in px. |
166
+ | `onSelect` | `(item) => void` | — | Item selection callback (both modes). |
167
+ | `children` | `Snippet<[{ isCollapsed, items, offsetWidth }]>` | — | Escape hatch: override the entire inner layout. |
168
+
169
+ Class slots: `class`, `classContent`, `classLeading`, `classLeadingHamburger`, `classLogo`, `classNav`, `classNavItem`, `classNavItemActive`, `classActions`, `classAction`, `classActionActive`, `classEnd`, `classAvatar`, `classLocale`, `classHamburger`, `classDropdown`.
149
170
 
150
171
  ```svelte
151
172
  <Header
@@ -159,11 +180,32 @@ Responsive navigation header with logo, nav items, avatar, and automatic hamburg
159
180
  <Avatar src="/me.jpg" alt="User" />
160
181
  {/snippet}
161
182
  </Header>
183
+
184
+ <!-- App-shell pattern: avatar + actions stay visible in collapsed mode,
185
+ nav items hide, leading hamburger opens a drawer. -->
186
+ <Header
187
+ projectName="App"
188
+ items={navItems}
189
+ actions={[
190
+ { id: "search", icon: { html: iconSearch() }, label: "Search", onclick: openSearch },
191
+ { id: "cart", icon: { html: iconCart() }, label: "Cart", onclick: openCart },
192
+ ]}
193
+ collapseMode="hide"
194
+ leadingHamburger="collapsed"
195
+ onLeadingHamburger={() => (drawerOpen = true)}
196
+ avatarOnClick={() => goto("/me")}
197
+ >
198
+ {#snippet avatar()}<Avatar initials="MM" autoColor />{/snippet}
199
+ </Header>
162
200
  ```
163
201
 
164
- **HeaderNavItem:** `{ id, label, href?, onclick?, icon?, active?, disabled?, class? }`
202
+ **HeaderNavItem:** `{ id, label, href?, target?, onclick?, icon?, active?, disabled?, class? }`
203
+
204
+ **HeaderActionItem:** `{ id, icon?, label, onclick?, href?, target?, active?, disabled?, class?, render? }` — `render` is an optional `Snippet<[{ action, class, isCollapsed, onclick }]>` that replaces the default `<Button>` for that action (useful for wrapping in a popover/tooltip directive or adding a count badge while keeping default positioning).
165
205
 
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`.
206
+ **HeaderLocaleItem:** `{ id, label }`
207
+
208
+ CSS tokens: `--stuic-header-padding-x`, `--stuic-header-padding-y`, `--stuic-header-gap`, `--stuic-header-min-height`, `--stuic-header-nav-gap`, `--stuic-header-content-max-width`, `--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
209
 
168
210
  ---
169
211
 
@@ -334,6 +376,38 @@ International phone number input with country dial code picker. Parses and compo
334
376
 
335
377
  Exports: `FieldPhoneNumber`, `FieldPhoneNumberProps`, `validatePhoneNumber`, `Country`.
336
378
 
379
+ #### `FieldCountry`
380
+
381
+ Country picker dropdown with searchable, optionally flag-prefixed list. Submits a country ISO alpha-2 code via a hidden input. Pairs naturally with `FieldPhoneNumber` and with checkout/address forms.
382
+
383
+ | Prop | Type | Default | Description |
384
+ | -------------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------- |
385
+ | `value` | `string` | `""` | Bindable ISO alpha-2 code (e.g. `"SK"`). Empty = unselected. |
386
+ | `onChange` | `(iso: string) => void` | — | Called when selection changes. |
387
+ | `countryList` | `Country[] \| string[]` | all | Restrict the list to specific countries (objects or ISO codes). |
388
+ | `preferredCountries` | `string[]` | — | ISO codes pinned at the top of the dropdown. |
389
+ | `countryNames` | `Record<string, string>` | English | Override displayed country names (keyed by ISO code). |
390
+ | `flags` | `boolean` | `true` | Show country flag emoji. |
391
+ | `name` | `string` | — | Hidden input name (enables form submission + native validation). |
392
+ | `placeholder` | `string` | — | Trigger placeholder text when nothing is selected. |
393
+ | `required` | `boolean` | `false` | Required indicator + validation. |
394
+ | `disabled` | `boolean` | `false` | Disable the trigger and dropdown. |
395
+ | `validate` | `boolean \| ValidateOptions` | enabled | Validation behavior (default-on, see Imperative validate API). |
396
+
397
+ Integrates with `InputWrap` — supports `label`, `description`, `renderSize`, `labelLeft`, `labelLeftWidth`, `labelLeftBreakpoint`, `inputBefore`, `inputAfter`, `inputBelow`, `below`, `classInput`, `classDropdown`.
398
+
399
+ ```svelte
400
+ <FieldCountry
401
+ bind:value={address.country}
402
+ name="country"
403
+ label="Country"
404
+ preferredCountries={["SK", "CZ", "AT", "DE"]}
405
+ required
406
+ />
407
+ ```
408
+
409
+ Exports: `FieldCountry`, `FieldCountryProps`, `Country`, `COUNTRIES`, `ISO_MAP`.
410
+
337
411
  #### `FieldObject`
338
412
 
339
413
  Dual-mode JSON object editor with pretty-print and raw edit modes. Validates JSON syntax, supports recursive depth display, auto-grow textarea, and form submission via hidden input.
package/README.md CHANGED
@@ -148,7 +148,7 @@ AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header,
148
148
 
149
149
  ### Forms & Inputs
150
150
 
151
- FieldInput, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
151
+ FieldInput, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, FieldCountry, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, RegisterFormModal, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
152
152
 
153
153
  ### Buttons & Controls
154
154
 
@@ -40,8 +40,10 @@
40
40
  export interface HeaderActionItem {
41
41
  /** Unique identifier */
42
42
  id: string | number;
43
- /** Icon — THC (string/html/component/snippet). The visible content. */
44
- icon: THC;
43
+ /** Icon — THC (string/html/component/snippet). The visible content.
44
+ * Required for the default rendering; ignored when `render` is provided
45
+ * (the snippet owns its own DOM). */
46
+ icon?: THC;
45
47
  /** Accessible label (aria-label). */
46
48
  label: THC;
47
49
  /** Click handler */
@@ -56,6 +58,31 @@
56
58
  disabled?: boolean;
57
59
  /** Additional CSS classes */
58
60
  class?: string;
61
+ /** Optional custom renderer. When provided, replaces the default
62
+ * `<Button>` rendering for this action. The Header still owns
63
+ * positioning (slot in the actions row) and the collapse decision.
64
+ *
65
+ * Use this when an action needs custom DOM around its trigger —
66
+ * e.g. a popover/tooltip directive, or a count/dot badge overlay.
67
+ *
68
+ * Snippet args:
69
+ * - `action` — the item itself (lets a snippet be reused across items)
70
+ * - `class` — the same merged class the default `<Button>` would receive,
71
+ * so consumers can opt into the default look
72
+ * - `isCollapsed`— current collapse state
73
+ * - `onclick` — pre-wired handler that calls `action.onclick` and
74
+ * `onActionSelect`; consumers can wire it to their
75
+ * button or ignore it. */
76
+ render?: Snippet<
77
+ [
78
+ {
79
+ action: HeaderActionItem;
80
+ class: string;
81
+ isCollapsed: boolean;
82
+ onclick: () => void;
83
+ },
84
+ ]
85
+ >;
59
86
  }
60
87
 
61
88
  /** Collapse behavior when the header drops below `collapseThreshold`:
@@ -559,26 +586,38 @@
559
586
  {#if actions.length > 0}
560
587
  <div class={_classActions}>
561
588
  {#each actions as action (action.id)}
562
- <Button
563
- variant="ghost"
564
- iconButton
565
- size="sm"
566
- href={action.href}
567
- target={action.target}
568
- disabled={action.disabled}
569
- {unstyled}
570
- class={twMerge(
571
- !unstyled && HEADER_ACTION_CLASSES,
572
- !unstyled && action.active && classActionActive,
573
- classAction,
574
- action.class
575
- )}
576
- data-active={!unstyled && action.active ? "" : undefined}
577
- aria-label={typeof action.label === "string" ? action.label : undefined}
578
- onclick={() => handleActionClick(action)}
579
- >
580
- <Thc thc={action.icon} />
581
- </Button>
589
+ {@const actionClass = twMerge(
590
+ !unstyled && HEADER_ACTION_CLASSES,
591
+ !unstyled && action.active && classActionActive,
592
+ classAction,
593
+ action.class
594
+ )}
595
+ {#if action.render}
596
+ {@render action.render({
597
+ action,
598
+ class: actionClass,
599
+ isCollapsed: _isCollapsed,
600
+ onclick: () => handleActionClick(action),
601
+ })}
602
+ {:else}
603
+ <Button
604
+ variant="ghost"
605
+ iconButton
606
+ size="sm"
607
+ href={action.href}
608
+ target={action.target}
609
+ disabled={action.disabled}
610
+ {unstyled}
611
+ class={actionClass}
612
+ data-active={!unstyled && action.active ? "" : undefined}
613
+ aria-label={typeof action.label === "string" ? action.label : undefined}
614
+ onclick={() => handleActionClick(action)}
615
+ >
616
+ {#if action.icon !== undefined}
617
+ <Thc thc={action.icon} />
618
+ {/if}
619
+ </Button>
620
+ {/if}
582
621
  {/each}
583
622
  </div>
584
623
  {/if}
@@ -32,8 +32,10 @@ export interface HeaderLocaleItem {
32
32
  export interface HeaderActionItem {
33
33
  /** Unique identifier */
34
34
  id: string | number;
35
- /** Icon — THC (string/html/component/snippet). The visible content. */
36
- icon: THC;
35
+ /** Icon — THC (string/html/component/snippet). The visible content.
36
+ * Required for the default rendering; ignored when `render` is provided
37
+ * (the snippet owns its own DOM). */
38
+ icon?: THC;
37
39
  /** Accessible label (aria-label). */
38
40
  label: THC;
39
41
  /** Click handler */
@@ -48,6 +50,29 @@ export interface HeaderActionItem {
48
50
  disabled?: boolean;
49
51
  /** Additional CSS classes */
50
52
  class?: string;
53
+ /** Optional custom renderer. When provided, replaces the default
54
+ * `<Button>` rendering for this action. The Header still owns
55
+ * positioning (slot in the actions row) and the collapse decision.
56
+ *
57
+ * Use this when an action needs custom DOM around its trigger —
58
+ * e.g. a popover/tooltip directive, or a count/dot badge overlay.
59
+ *
60
+ * Snippet args:
61
+ * - `action` — the item itself (lets a snippet be reused across items)
62
+ * - `class` — the same merged class the default `<Button>` would receive,
63
+ * so consumers can opt into the default look
64
+ * - `isCollapsed`— current collapse state
65
+ * - `onclick` — pre-wired handler that calls `action.onclick` and
66
+ * `onActionSelect`; consumers can wire it to their
67
+ * button or ignore it. */
68
+ render?: Snippet<[
69
+ {
70
+ action: HeaderActionItem;
71
+ class: string;
72
+ isCollapsed: boolean;
73
+ onclick: () => void;
74
+ }
75
+ ]>;
51
76
  }
52
77
  /** Collapse behavior when the header drops below `collapseThreshold`:
53
78
  * - "hamburger": nav items fold into a trailing dropdown along with the
@@ -0,0 +1,35 @@
1
+ # Header
2
+
3
+ Top-bar component with leading slot, project logo, nav items, locale switcher, optional action buttons, avatar, and responsive collapse (either fold into a trailing hamburger dropdown OR hide nav entirely).
4
+
5
+ ## Examples
6
+
7
+ ### App-like collapse: avatar + actions visible, everything else hidden
8
+
9
+ Common "app shell" pattern: when the header collapses below `collapseThreshold`, the avatar and a few key actions (search, notifications, cart…) remain visible, the trailing hamburger is NOT shown, and the nav items + locale switcher are hidden entirely (the nav typically lives in a drawer triggered by the leading hamburger instead).
10
+
11
+ | Requirement | Where it's handled | How |
12
+ | --- | --- | --- |
13
+ | **Avatar stays visible** | [Header.svelte:626](./Header.svelte#L626) — `{#if avatar && !(_isCollapsed && _avatarInDropdown)}` | `_avatarInDropdown` requires `collapseMode === "hamburger"`. In `"hide"` mode it's always `false`, so the avatar always renders. |
14
+ | **Action buttons stay visible** | [Header.svelte:585](./Header.svelte#L585) — `<!-- Actions (icon buttons, always visible) -->` | The actions loop has no collapse gating — items render in both modes. |
15
+ | **No trailing hamburger** | [Header.svelte:642](./Header.svelte#L642) — `{#if _isCollapsed && _dropdownItems.length > 0}` | In `"hide"` mode, `_dropdownItems` short-circuits to `[]`, so the `{#if}` is false → no trailing hamburger. |
16
+ | **Nav items hidden** | [Header.svelte:516](./Header.svelte#L516) — `{#if !_isCollapsed && items.length > 0}` | Inline nav requires `!_isCollapsed`; combined with the empty `_dropdownItems` above, items don't reappear in a dropdown either. |
17
+ | **Locale hidden** | [Header.svelte:335](./Header.svelte#L335) — `!_isCollapsed \|\| (collapseMode === "hide" && keepLocaleOnCollapse)` | Default `keepLocaleOnCollapse={false}` hides the locale switcher in collapsed mode. |
18
+
19
+ Minimal config:
20
+
21
+ ```svelte
22
+ <Header
23
+ projectName="App"
24
+ items={navItems} <!-- shown expanded, hidden collapsed -->
25
+ actions={[...]} <!-- always visible -->
26
+ collapseMode="hide" <!-- no trailing hamburger; avatar stays -->
27
+ leadingHamburger <!-- optional: drives a drawer for the hidden nav -->
28
+ onLeadingHamburger={() => (drawerOpen = true)}
29
+ {locales} {activeLocale} <!-- hidden in collapsed (keepLocaleOnCollapse defaults to false) -->
30
+ onLocaleChange={(id) => (activeLocale = id)}
31
+ avatarOnClick={() => alert("Profile")} <!-- safe in "hide" mode — won't move into dropdown -->
32
+ >
33
+ {#snippet avatar()}<Avatar initials="MM" autoColor />{/snippet}
34
+ </Header>
35
+ ```
@@ -21,6 +21,29 @@
21
21
  /* Actions (icon buttons in the end region) */
22
22
  --stuic-header-actions-gap: 0.25rem;
23
23
 
24
+ /* Hamburger offsets — outdent the built-in hamburger buttons on both
25
+ inline sides so the *icon* (not the invisible iconButton hit area)
26
+ sits flush with the header's content padding on one side and tightens
27
+ against its neighbor (logo / avatar) on the other side. The flex
28
+ `gap` between header regions kicks in *after* the button's invisible
29
+ padding, so without these the perceived gap is `--stuic-header-gap` +
30
+ one button padding.
31
+ Default magnitude is derived from the iconButton's own padding so the
32
+ alignment stays correct if button padding ever changes. Set to 0 to
33
+ disable; override each side independently if needed. */
34
+ --stuic-header-leading-hamburger-offset-inline-start: calc(
35
+ -1 * var(--stuic-button-padding-y-sm)
36
+ );
37
+ --stuic-header-leading-hamburger-offset-inline-end: calc(
38
+ -1 * var(--stuic-button-padding-y-sm)
39
+ );
40
+ --stuic-header-trailing-hamburger-offset-inline-start: calc(
41
+ -1 * var(--stuic-button-padding-y-sm)
42
+ );
43
+ --stuic-header-trailing-hamburger-offset-inline-end: calc(
44
+ -1 * var(--stuic-button-padding-y-sm)
45
+ );
46
+
24
47
  /* Project name */
25
48
  --stuic-header-project-name-font-weight: var(--font-weight-semibold, 600);
26
49
 
@@ -79,7 +102,12 @@
79
102
  }
80
103
 
81
104
  .stuic-header-leading-hamburger {
82
- /* Ghost Button handles hover/focus only override sizing if needed */
105
+ /* Outdent on both inline sides so the icon (not the iconButton hit
106
+ area) aligns with the header's content padding on the start and
107
+ tightens against the logo on the end. Override each side via its
108
+ own token. */
109
+ margin-inline-start: var(--stuic-header-leading-hamburger-offset-inline-start);
110
+ margin-inline-end: var(--stuic-header-leading-hamburger-offset-inline-end);
83
111
  }
84
112
 
85
113
  /* ============================================================================
@@ -214,6 +242,11 @@
214
242
  ============================================================================ */
215
243
 
216
244
  .stuic-header-hamburger {
217
- /* Ghost Button handles hover/focusonly override sizing */
245
+ /* Symmetric counterpart to the leading hamburger outdent on both
246
+ inline sides so the icon tightens against its left neighbor
247
+ (avatar) on the start and aligns with the header's content padding
248
+ on the end. Override each side via its own token. */
249
+ margin-inline-start: var(--stuic-header-trailing-hamburger-offset-inline-start);
250
+ margin-inline-end: var(--stuic-header-trailing-hamburger-offset-inline-end);
218
251
  }
219
252
  }
@@ -0,0 +1,69 @@
1
+ # IconSwap
2
+
3
+ Cross-fades between N visual states (HTML strings or Snippets) at a single position. Commonly used for hamburger ⇄ X toggles, play ⇄ pause, sun ⇄ moon, etc. Respects `prefers-reduced-motion` (animation duration is forced to 0).
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ | ------------ | -------------------------- | -------- | -------------------------------------------------------------------------- |
9
+ | `states` | `Array<string \| Snippet>` | required | The visual states to swap between. Strings are rendered with `{@html}`. |
10
+ | `active` | `number` | `0` | Bindable index of the currently visible state. |
11
+ | `duration` | `number` | `300` | Transition duration in ms. Set `0` to disable. |
12
+ | `easing` | `string` | `"ease"` | CSS `transition-timing-function`. |
13
+ | `unstyled` | `boolean` | `false` | Skip default styling. |
14
+ | `class` | `string` | - | Additional CSS classes for the root `<span>`. |
15
+ | `stateClass` | `string` | - | Additional CSS classes for each state wrapper. |
16
+ | `el` | `HTMLSpanElement` | - | Bindable root element. |
17
+
18
+ ## Usage
19
+
20
+ ### Hamburger ⇄ X
21
+
22
+ ```svelte
23
+ <script lang="ts">
24
+ import { IconSwap, iconMenu, iconX } from "@marianmeres/stuic";
25
+
26
+ let isOpen = $state(false);
27
+ </script>
28
+
29
+ <button onclick={() => (isOpen = !isOpen)}>
30
+ <IconSwap active={isOpen ? 1 : 0} states={[iconMenu(), iconX()]} />
31
+ </button>
32
+ ```
33
+
34
+ ### With snippets
35
+
36
+ ```svelte
37
+ <IconSwap active={tab}>
38
+ {#snippet states_0()}<span>A</span>{/snippet}
39
+ {#snippet states_1()}<strong>B</strong>{/snippet}
40
+ {#snippet states_2()}<em>C</em>{/snippet}
41
+ </IconSwap>
42
+
43
+ <!-- equivalent with an array of snippets -->
44
+ <IconSwap active={tab} states={[stateA, stateB, stateC]} />
45
+ ```
46
+
47
+ ### Custom easing/duration
48
+
49
+ ```svelte
50
+ <IconSwap states={frames} active={i} duration={500} easing="cubic-bezier(0.4, 0, 0.2, 1)" />
51
+ ```
52
+
53
+ ## CSS Variables
54
+
55
+ | Variable | Default | Description |
56
+ | ----------------------------- | ------- | -------------------------- |
57
+ | `--stuic-icon-swap-duration` | (prop) | Transition duration |
58
+ | `--stuic-icon-swap-easing` | (prop) | Transition timing function |
59
+
60
+ ## Data Attributes
61
+
62
+ - `data-active` (root) — current active index
63
+ - `data-visible="true"` (state wrapper) — present on the visible state
64
+
65
+ ## Behavior
66
+
67
+ - All states render concurrently; the inactive ones have `opacity: 0` and `aria-hidden="true"`.
68
+ - The root is positioned so all states stack at the same coordinates.
69
+ - `active` is clamped to `[0, states.length - 1]`.
@@ -0,0 +1,83 @@
1
+ # ImageCycler
2
+
3
+ Auto-cycling background-image carousel with fade transitions. Preloads the next image before swapping, so transitions never reveal a half-loaded asset. Optional `title` and `description` snippets render an absolutely-positioned meta layer on top of the image.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ | -------------------- | ----------------------------------------------------------------- | --------- | ---------------------------------------------------------------------- |
9
+ | `images` | `ImageCyclerImage[]` | required | Images to cycle through. |
10
+ | `fit` | `"cover" \| "contain" \| "fill"` | `"cover"` | Background-size mode. Set via `data-fit` on the image layer. |
11
+ | `minWait` | `number` | `3000` | Minimum wait (ms) on each image before advancing. |
12
+ | `transitionDuration` | `number` | `500` | Fade duration in ms (for both the image and the meta layer). |
13
+ | `onclick` | `(image, index) => void` | - | Click handler. The snippets receive a forwarded version. |
14
+ | `title` | `Snippet<[{ image, index, onclick }]>` | - | Title overlay snippet. |
15
+ | `description` | `Snippet<[{ image, index, onclick }]>` | - | Description overlay snippet. |
16
+ | `unstyled` | `boolean` | `false` | Skip default styling. |
17
+ | `class` | `string` | - | Additional CSS classes. |
18
+ | `el` | `HTMLElement` | - | Bindable root element. |
19
+
20
+ ## `ImageCyclerImage`
21
+
22
+ ```ts
23
+ interface ImageCyclerImage {
24
+ src: string;
25
+ alt?: string;
26
+ title?: string;
27
+ description?: string;
28
+ [key: string]: unknown; // arbitrary extra fields are preserved
29
+ }
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Minimal
35
+
36
+ ```svelte
37
+ <script lang="ts">
38
+ import { ImageCycler } from "@marianmeres/stuic";
39
+
40
+ const slides = [
41
+ { src: "/hero/a.jpg", alt: "A" },
42
+ { src: "/hero/b.jpg", alt: "B" },
43
+ { src: "/hero/c.jpg", alt: "C" },
44
+ ];
45
+ </script>
46
+
47
+ <div style="aspect-ratio: 16/9;">
48
+ <ImageCycler images={slides} />
49
+ </div>
50
+ ```
51
+
52
+ ### With overlay + click
53
+
54
+ ```svelte
55
+ <ImageCycler
56
+ images={slides}
57
+ minWait={5000}
58
+ transitionDuration={800}
59
+ onclick={(img, i) => console.log("clicked", i, img)}
60
+ >
61
+ {#snippet title({ image, onclick })}
62
+ <button type="button" {onclick} class="absolute bottom-12 left-6 text-white">
63
+ {image.title}
64
+ </button>
65
+ {/snippet}
66
+ {#snippet description({ image })}
67
+ <p class="absolute bottom-4 left-6 text-white/80">{image.description}</p>
68
+ {/snippet}
69
+ </ImageCycler>
70
+ ```
71
+
72
+ ## CSS Variables
73
+
74
+ | Variable | Default | Description |
75
+ | ------------------------------------------- | ------- | --------------------- |
76
+ | `--stuic-image-cycler-transition-duration` | `500ms` | (informational; the active transition duration comes from the `transitionDuration` prop) |
77
+
78
+ ## Notes
79
+
80
+ - The root has `position: relative; overflow: hidden`. The parent must define a height (`aspect-ratio`, fixed height, etc.) since the image layer is absolutely positioned.
81
+ - Only the next image is preloaded; earlier images aren't kept warm.
82
+ - A single-image `images` array disables the cycler effect.
83
+ - The `aria-label` for the background layer falls back to `alt → title → ""`.