@marianmeres/stuic 3.95.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 +7 -6
- package/API.md +90 -16
- package/README.md +1 -1
- package/dist/components/Header/Header.svelte +265 -35
- package/dist/components/Header/Header.svelte.d.ts +109 -4
- package/dist/components/Header/README.md +35 -0
- package/dist/components/Header/index.css +95 -7
- package/dist/components/Header/index.d.ts +1 -1
- package/dist/components/Header/index.js +1 -1
- package/dist/components/IconSwap/README.md +69 -0
- package/dist/components/ImageCycler/README.md +83 -0
- package/dist/components/LoginForm/README.md +148 -0
- package/dist/components/LoginOrRegisterForm/README.md +170 -0
- package/dist/components/RegisterForm/README.md +183 -0
- package/dist/components/Separator/README.md +44 -0
- package/docs/architecture.md +14 -10
- package/docs/domains/components.md +112 -28
- package/docs/domains/utils.md +1 -1
- package/package.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -23,15 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── components/ #
|
|
26
|
+
├── components/ # 57 component directories
|
|
27
27
|
├── actions/ # 15 Svelte actions
|
|
28
|
-
├── utils/ #
|
|
29
|
-
├──
|
|
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) —
|
|
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) —
|
|
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
|
|
136
|
-
|
|
137
|
-
| Prop
|
|
138
|
-
|
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
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
|
-
|
|
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
|
|
|
@@ -37,17 +37,95 @@
|
|
|
37
37
|
label: THC;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export interface HeaderActionItem {
|
|
41
|
+
/** Unique identifier */
|
|
42
|
+
id: string | number;
|
|
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;
|
|
47
|
+
/** Accessible label (aria-label). */
|
|
48
|
+
label: THC;
|
|
49
|
+
/** Click handler */
|
|
50
|
+
onclick?: () => void;
|
|
51
|
+
/** Render as a link instead of a button */
|
|
52
|
+
href?: string;
|
|
53
|
+
/** Link target (e.g., "_blank"). Only relevant when href is set. */
|
|
54
|
+
target?: string;
|
|
55
|
+
/** Active state styling (e.g., when a panel triggered by this action is open) */
|
|
56
|
+
active?: boolean;
|
|
57
|
+
/** Whether this action is disabled */
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
/** Additional CSS classes */
|
|
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
|
+
>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Collapse behavior when the header drops below `collapseThreshold`:
|
|
89
|
+
* - "hamburger": nav items fold into a trailing dropdown along with the
|
|
90
|
+
* locale switcher and an interactive avatar.
|
|
91
|
+
* - "hide": nav items are hidden entirely. No trailing hamburger renders.
|
|
92
|
+
* Avatar stays visible. Locale visibility is controlled by
|
|
93
|
+
* `keepLocaleOnCollapse`. */
|
|
94
|
+
export type HeaderCollapseMode = "hamburger" | "hide";
|
|
95
|
+
|
|
96
|
+
/** Visibility for the built-in leading hamburger button:
|
|
97
|
+
* - false/undefined: not rendered
|
|
98
|
+
* - true: always rendered
|
|
99
|
+
* - "collapsed": only rendered when the header is below the collapse threshold */
|
|
100
|
+
export type HeaderLeadingHamburger = boolean | "collapsed";
|
|
101
|
+
|
|
40
102
|
export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
|
|
103
|
+
/** Leading (left-side) slot. Renders before the logo/title.
|
|
104
|
+
* Use for a hamburger button, back arrow, breadcrumbs, etc.
|
|
105
|
+
* When provided, overrides the built-in `leadingHamburger`. */
|
|
106
|
+
leading?: Snippet<[{ isCollapsed: boolean }]>;
|
|
107
|
+
/** Convenience: render a built-in hamburger button in the leading slot.
|
|
108
|
+
* Ignored when the `leading` snippet is provided. */
|
|
109
|
+
leadingHamburger?: HeaderLeadingHamburger;
|
|
110
|
+
/** Click handler for the built-in leading hamburger (typically opens a drawer) */
|
|
111
|
+
onLeadingHamburger?: () => void;
|
|
112
|
+
/** Icon for the built-in leading hamburger (defaults to a menu icon) */
|
|
113
|
+
leadingHamburgerIcon?: THC;
|
|
114
|
+
/** Aria-label for the built-in leading hamburger (defaults to "Open menu") */
|
|
115
|
+
leadingHamburgerLabel?: string;
|
|
41
116
|
/** Logo/brand snippet — full control over the left branding area */
|
|
42
117
|
logo?: Snippet;
|
|
43
|
-
/** Horizontal alignment of the nav items in expanded mode */
|
|
44
|
-
navAlign?: "left" | "right";
|
|
45
118
|
/** Button variant for nav items and locale trigger (defaults to "ghost") */
|
|
46
119
|
navVariant?: ButtonVariant;
|
|
47
120
|
/** Simple text alternative to the logo snippet */
|
|
48
121
|
projectName?: string;
|
|
49
122
|
/** Navigation items — inline when expanded, DropdownMenu when collapsed */
|
|
50
123
|
items?: HeaderNavItem[];
|
|
124
|
+
/** Action icon buttons displayed between the locale switcher and the avatar.
|
|
125
|
+
* Always visible — they do not fold into the trailing dropdown. */
|
|
126
|
+
actions?: HeaderActionItem[];
|
|
127
|
+
/** Called when an action is selected (in addition to the per-item onclick) */
|
|
128
|
+
onActionSelect?: (action: HeaderActionItem) => void;
|
|
51
129
|
/** Avatar/user snippet — rendered at the far right */
|
|
52
130
|
avatar?: Snippet;
|
|
53
131
|
/** When provided, makes the avatar interactive. In expanded mode wraps it in a
|
|
@@ -63,8 +141,21 @@
|
|
|
63
141
|
onLocaleChange?: (localeId: string) => void;
|
|
64
142
|
/** Section header label for locales in collapsed dropdown (defaults to "Language") */
|
|
65
143
|
localeLabel?: THC;
|
|
144
|
+
/** Max-width of the inner content row. The outer `<header>` stays 100%
|
|
145
|
+
* wide (background fills the parent); the inner content is centered and
|
|
146
|
+
* capped at this value. Accepts any CSS length: "1024px", "72rem",
|
|
147
|
+
* "100%", "none". Default: undefined → unbounded.
|
|
148
|
+
* Equivalent global override:
|
|
149
|
+
* `:root { --stuic-header-content-max-width: 72rem; }` */
|
|
150
|
+
contentMaxWidth?: string | number;
|
|
66
151
|
/** Element width (px) below which nav collapses to hamburger. 0 to disable. */
|
|
67
152
|
collapseThreshold?: number;
|
|
153
|
+
/** Collapse behavior when below threshold (defaults to "hamburger") */
|
|
154
|
+
collapseMode?: HeaderCollapseMode;
|
|
155
|
+
/** When `collapseMode === "hide"`, keep the locale switcher visible in
|
|
156
|
+
* collapsed mode. No effect when `collapseMode === "hamburger"`
|
|
157
|
+
* (locale already folds into the trailing dropdown there). */
|
|
158
|
+
keepLocaleOnCollapse?: boolean;
|
|
68
159
|
/** Fixed positioning (top of viewport) */
|
|
69
160
|
fixed?: boolean;
|
|
70
161
|
/** Bindable: whether the header is currently in collapsed (hamburger) mode */
|
|
@@ -81,6 +172,12 @@
|
|
|
81
172
|
unstyled?: boolean;
|
|
82
173
|
/** Additional CSS classes for the root <header> */
|
|
83
174
|
class?: string;
|
|
175
|
+
/** Classes for the inner content wrapper */
|
|
176
|
+
classContent?: string;
|
|
177
|
+
/** Classes for the leading area */
|
|
178
|
+
classLeading?: string;
|
|
179
|
+
/** Classes for the built-in leading hamburger button */
|
|
180
|
+
classLeadingHamburger?: string;
|
|
84
181
|
/** Classes for the logo area */
|
|
85
182
|
classLogo?: string;
|
|
86
183
|
/** Classes for the nav area (expanded mode) */
|
|
@@ -89,13 +186,19 @@
|
|
|
89
186
|
classNavItem?: string;
|
|
90
187
|
/** Classes for active nav items */
|
|
91
188
|
classNavItemActive?: string;
|
|
92
|
-
/** Classes for the
|
|
189
|
+
/** Classes for the actions wrapper */
|
|
190
|
+
classActions?: string;
|
|
191
|
+
/** Classes for individual action buttons */
|
|
192
|
+
classAction?: string;
|
|
193
|
+
/** Classes for active action buttons */
|
|
194
|
+
classActionActive?: string;
|
|
195
|
+
/** Classes for the end area (locale + avatar + trailing hamburger) */
|
|
93
196
|
classEnd?: string;
|
|
94
197
|
/** Classes for the avatar container */
|
|
95
198
|
classAvatar?: string;
|
|
96
199
|
/** Classes for the locale switcher trigger (expanded mode) */
|
|
97
200
|
classLocale?: string;
|
|
98
|
-
/** Classes for the hamburger button */
|
|
201
|
+
/** Classes for the trailing (right-side) hamburger button */
|
|
99
202
|
classHamburger?: string;
|
|
100
203
|
/** Classes for the dropdown wrapper (collapsed mode) */
|
|
101
204
|
classDropdown?: string;
|
|
@@ -108,9 +211,14 @@
|
|
|
108
211
|
}
|
|
109
212
|
|
|
110
213
|
export const HEADER_BASE_CLASSES = "stuic-header";
|
|
214
|
+
export const HEADER_CONTENT_CLASSES = "stuic-header-content";
|
|
215
|
+
export const HEADER_LEADING_CLASSES = "stuic-header-leading";
|
|
216
|
+
export const HEADER_LEADING_HAMBURGER_CLASSES = "stuic-header-leading-hamburger";
|
|
111
217
|
export const HEADER_LOGO_CLASSES = "stuic-header-logo";
|
|
112
218
|
export const HEADER_NAV_CLASSES = "stuic-header-nav";
|
|
113
219
|
export const HEADER_NAV_ITEM_CLASSES = "stuic-header-nav-item";
|
|
220
|
+
export const HEADER_ACTIONS_CLASSES = "stuic-header-actions";
|
|
221
|
+
export const HEADER_ACTION_CLASSES = "stuic-header-action";
|
|
114
222
|
export const HEADER_END_CLASSES = "stuic-header-end";
|
|
115
223
|
export const HEADER_HAMBURGER_CLASSES = "stuic-header-hamburger";
|
|
116
224
|
export const HEADER_LOCALE_CLASSES = "stuic-header-locale";
|
|
@@ -125,11 +233,17 @@
|
|
|
125
233
|
import IconSwap from "../IconSwap/IconSwap.svelte";
|
|
126
234
|
|
|
127
235
|
let {
|
|
236
|
+
leading,
|
|
237
|
+
leadingHamburger = false,
|
|
238
|
+
onLeadingHamburger,
|
|
239
|
+
leadingHamburgerIcon,
|
|
240
|
+
leadingHamburgerLabel = "Open menu",
|
|
128
241
|
logo,
|
|
129
242
|
projectName,
|
|
130
|
-
navAlign = "right",
|
|
131
243
|
navVariant = "ghost",
|
|
132
244
|
items = [],
|
|
245
|
+
actions = [],
|
|
246
|
+
onActionSelect,
|
|
133
247
|
avatar,
|
|
134
248
|
avatarOnClick,
|
|
135
249
|
avatarLabel = "Account",
|
|
@@ -137,7 +251,10 @@
|
|
|
137
251
|
activeLocale,
|
|
138
252
|
onLocaleChange,
|
|
139
253
|
localeLabel = "Language",
|
|
254
|
+
contentMaxWidth,
|
|
140
255
|
collapseThreshold = 768,
|
|
256
|
+
collapseMode = "hamburger",
|
|
257
|
+
keepLocaleOnCollapse = false,
|
|
141
258
|
fixed = false,
|
|
142
259
|
isCollapsed = $bindable(false),
|
|
143
260
|
isMenuOpen = $bindable(false),
|
|
@@ -146,10 +263,16 @@
|
|
|
146
263
|
onSelect,
|
|
147
264
|
unstyled = false,
|
|
148
265
|
class: classProp,
|
|
266
|
+
classContent,
|
|
267
|
+
classLeading,
|
|
268
|
+
classLeadingHamburger,
|
|
149
269
|
classLogo,
|
|
150
270
|
classNav,
|
|
151
271
|
classNavItem,
|
|
152
272
|
classNavItemActive,
|
|
273
|
+
classActions,
|
|
274
|
+
classAction,
|
|
275
|
+
classActionActive,
|
|
153
276
|
classEnd,
|
|
154
277
|
classAvatar,
|
|
155
278
|
classLocale,
|
|
@@ -160,13 +283,19 @@
|
|
|
160
283
|
...rest
|
|
161
284
|
}: Props = $props();
|
|
162
285
|
|
|
163
|
-
// Width measurement
|
|
164
|
-
|
|
286
|
+
// Width measurement. We bind both outer and inner because:
|
|
287
|
+
// - Default layout renders an inner wrapper; the inner row width is what
|
|
288
|
+
// actually determines whether nav items fit, so collapse should key off it.
|
|
289
|
+
// - With the `children` escape hatch there is no inner wrapper, so we
|
|
290
|
+
// fall back to the outer measurement.
|
|
291
|
+
let _outerWidth = $state(0);
|
|
292
|
+
let _innerWidth = $state(0);
|
|
293
|
+
let _measuredWidth = $derived(_innerWidth || _outerWidth);
|
|
165
294
|
|
|
166
295
|
// Collapsed state based on threshold
|
|
167
296
|
let _isCollapsed = $derived.by(() => {
|
|
168
297
|
if (!collapseThreshold) return false;
|
|
169
|
-
return
|
|
298
|
+
return _measuredWidth > 0 && _measuredWidth < collapseThreshold;
|
|
170
299
|
});
|
|
171
300
|
|
|
172
301
|
// Sync bindable
|
|
@@ -181,12 +310,31 @@
|
|
|
181
310
|
}
|
|
182
311
|
});
|
|
183
312
|
|
|
184
|
-
// Whether the avatar moves into the dropdown when collapsed
|
|
185
|
-
|
|
313
|
+
// Whether the avatar moves into the dropdown when collapsed.
|
|
314
|
+
// In "hide" mode the avatar always stays visible (never folds).
|
|
315
|
+
let _avatarInDropdown = $derived(
|
|
316
|
+
collapseMode === "hamburger" && !!(avatar && avatarOnClick)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Whether to render the built-in leading hamburger (ignored when `leading` snippet is set)
|
|
320
|
+
let _showLeadingHamburger = $derived.by(() => {
|
|
321
|
+
if (leading) return false;
|
|
322
|
+
if (leadingHamburger === "collapsed") return _isCollapsed;
|
|
323
|
+
return !!leadingHamburger;
|
|
324
|
+
});
|
|
186
325
|
|
|
187
326
|
// Locale switcher: only render when 2+ locales
|
|
188
327
|
let _hasLocales = $derived(locales.length > 1);
|
|
189
328
|
|
|
329
|
+
// Visibility of the inline (expanded-form) locale switcher.
|
|
330
|
+
// In "hamburger" mode: visible only when not collapsed (it folds into the
|
|
331
|
+
// trailing dropdown when collapsed). In "hide" mode: visible when not
|
|
332
|
+
// collapsed, or when collapsed and `keepLocaleOnCollapse` is set.
|
|
333
|
+
let _showLocaleSwitcher = $derived(
|
|
334
|
+
_hasLocales &&
|
|
335
|
+
(!_isCollapsed || (collapseMode === "hide" && keepLocaleOnCollapse))
|
|
336
|
+
);
|
|
337
|
+
|
|
190
338
|
// Active locale object (for trigger label); fallback to first
|
|
191
339
|
let _activeLocale = $derived(locales.find((l) => l.id === activeLocale) ?? locales[0]);
|
|
192
340
|
|
|
@@ -209,8 +357,11 @@
|
|
|
209
357
|
);
|
|
210
358
|
});
|
|
211
359
|
|
|
212
|
-
// Map HeaderNavItem[] to DropdownMenuItem[] for collapsed mode
|
|
360
|
+
// Map HeaderNavItem[] to DropdownMenuItem[] for collapsed mode.
|
|
361
|
+
// In "hide" mode the trailing hamburger is suppressed entirely — return
|
|
362
|
+
// an empty list so no dropdown trigger renders.
|
|
213
363
|
let _dropdownItems = $derived.by((): DropdownMenuItem[] => {
|
|
364
|
+
if (collapseMode === "hide") return [];
|
|
214
365
|
const navItems: DropdownMenuItem[] = items.map(
|
|
215
366
|
(item) =>
|
|
216
367
|
({
|
|
@@ -269,10 +420,25 @@
|
|
|
269
420
|
|
|
270
421
|
// CSS classes
|
|
271
422
|
let _class = $derived(unstyled ? classProp : twMerge(HEADER_BASE_CLASSES, classProp));
|
|
423
|
+
let _classContent = $derived(
|
|
424
|
+
unstyled ? classContent : twMerge(HEADER_CONTENT_CLASSES, classContent)
|
|
425
|
+
);
|
|
426
|
+
let _styleContent = $derived.by(() => {
|
|
427
|
+
if (contentMaxWidth == null) return undefined;
|
|
428
|
+
const value =
|
|
429
|
+
typeof contentMaxWidth === "number" ? `${contentMaxWidth}px` : contentMaxWidth;
|
|
430
|
+
return `--stuic-header-content-max-width: ${value}`;
|
|
431
|
+
});
|
|
432
|
+
let _classLeading = $derived(
|
|
433
|
+
unstyled ? classLeading : twMerge(HEADER_LEADING_CLASSES, classLeading)
|
|
434
|
+
);
|
|
272
435
|
let _classLogo = $derived(
|
|
273
436
|
unstyled ? classLogo : twMerge(HEADER_LOGO_CLASSES, classLogo)
|
|
274
437
|
);
|
|
275
438
|
let _classNav = $derived(unstyled ? classNav : twMerge(HEADER_NAV_CLASSES, classNav));
|
|
439
|
+
let _classActions = $derived(
|
|
440
|
+
unstyled ? classActions : twMerge(HEADER_ACTIONS_CLASSES, classActions)
|
|
441
|
+
);
|
|
276
442
|
let _classEnd = $derived(unstyled ? classEnd : twMerge(HEADER_END_CLASSES, classEnd));
|
|
277
443
|
let _classLocale = $derived(
|
|
278
444
|
unstyled ? classLocale : twMerge(HEADER_LOCALE_CLASSES, classLocale)
|
|
@@ -283,11 +449,17 @@
|
|
|
283
449
|
item.onclick?.();
|
|
284
450
|
onSelect?.(item);
|
|
285
451
|
}
|
|
452
|
+
|
|
453
|
+
function handleActionClick(action: HeaderActionItem) {
|
|
454
|
+
if (action.disabled) return;
|
|
455
|
+
action.onclick?.();
|
|
456
|
+
onActionSelect?.(action);
|
|
457
|
+
}
|
|
286
458
|
</script>
|
|
287
459
|
|
|
288
460
|
<header
|
|
289
461
|
bind:this={el}
|
|
290
|
-
bind:offsetWidth={
|
|
462
|
+
bind:offsetWidth={_outerWidth}
|
|
291
463
|
class={_class}
|
|
292
464
|
data-fixed={!unstyled && fixed ? "" : undefined}
|
|
293
465
|
data-collapsed={!unstyled && _isCollapsed ? "" : undefined}
|
|
@@ -297,26 +469,48 @@
|
|
|
297
469
|
{@render children({
|
|
298
470
|
isCollapsed: _isCollapsed,
|
|
299
471
|
items,
|
|
300
|
-
offsetWidth:
|
|
472
|
+
offsetWidth: _measuredWidth,
|
|
301
473
|
})}
|
|
302
474
|
{:else}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
475
|
+
<div bind:offsetWidth={_innerWidth} class={_classContent} style={_styleContent}>
|
|
476
|
+
<!-- Leading slot (left-side) -->
|
|
477
|
+
{#if leading}
|
|
478
|
+
<div class={_classLeading}>
|
|
479
|
+
{@render leading({ isCollapsed: _isCollapsed })}
|
|
480
|
+
</div>
|
|
481
|
+
{:else if _showLeadingHamburger}
|
|
482
|
+
<div class={_classLeading}>
|
|
483
|
+
<Button
|
|
484
|
+
variant="ghost"
|
|
485
|
+
iconButton
|
|
486
|
+
size="sm"
|
|
487
|
+
{unstyled}
|
|
488
|
+
class={twMerge(
|
|
489
|
+
!unstyled && HEADER_LEADING_HAMBURGER_CLASSES,
|
|
490
|
+
classLeadingHamburger
|
|
491
|
+
)}
|
|
492
|
+
onclick={onLeadingHamburger}
|
|
493
|
+
aria-label={leadingHamburgerLabel}
|
|
494
|
+
>
|
|
495
|
+
{#if leadingHamburgerIcon}
|
|
496
|
+
<Thc thc={leadingHamburgerIcon} />
|
|
497
|
+
{:else}
|
|
498
|
+
{@html iconMenu({ size: iconSize })}
|
|
499
|
+
{/if}
|
|
500
|
+
</Button>
|
|
313
501
|
</div>
|
|
314
502
|
{/if}
|
|
315
503
|
|
|
316
|
-
<!--
|
|
317
|
-
{
|
|
318
|
-
|
|
319
|
-
|
|
504
|
+
<!-- Logo / Title (flex-1) -->
|
|
505
|
+
<div class={_classLogo}>
|
|
506
|
+
{#if logo}
|
|
507
|
+
{@render logo()}
|
|
508
|
+
{:else if projectName}
|
|
509
|
+
<span class={unstyled ? undefined : "stuic-header-project-name"}>
|
|
510
|
+
{projectName}
|
|
511
|
+
</span>
|
|
512
|
+
{/if}
|
|
513
|
+
</div>
|
|
320
514
|
|
|
321
515
|
<!-- Nav items (expanded mode) -->
|
|
322
516
|
{#if !_isCollapsed && items.length > 0}
|
|
@@ -350,15 +544,10 @@
|
|
|
350
544
|
</nav>
|
|
351
545
|
{/if}
|
|
352
546
|
|
|
353
|
-
<!--
|
|
354
|
-
{#if navAlign === "left"}
|
|
355
|
-
<div class={unstyled ? undefined : "stuic-header-spacer"}></div>
|
|
356
|
-
{/if}
|
|
357
|
-
|
|
358
|
-
<!-- End area: locale + avatar + hamburger -->
|
|
547
|
+
<!-- End area: locale + actions + avatar + trailing hamburger -->
|
|
359
548
|
<div class={_classEnd}>
|
|
360
|
-
<!-- Locale switcher (expanded mode
|
|
361
|
-
{#if
|
|
549
|
+
<!-- Locale switcher (shown when expanded, or in "hide" mode with keepLocaleOnCollapse) -->
|
|
550
|
+
{#if _showLocaleSwitcher}
|
|
362
551
|
<DropdownMenu
|
|
363
552
|
items={_localeDropdownItems}
|
|
364
553
|
position="bottom-span-right"
|
|
@@ -393,6 +582,46 @@
|
|
|
393
582
|
</DropdownMenu>
|
|
394
583
|
{/if}
|
|
395
584
|
|
|
585
|
+
<!-- Actions (icon buttons, always visible) -->
|
|
586
|
+
{#if actions.length > 0}
|
|
587
|
+
<div class={_classActions}>
|
|
588
|
+
{#each actions as action (action.id)}
|
|
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}
|
|
621
|
+
{/each}
|
|
622
|
+
</div>
|
|
623
|
+
{/if}
|
|
624
|
+
|
|
396
625
|
<!-- Avatar: hidden when collapsed + avatarOnClick (moves into dropdown) -->
|
|
397
626
|
{#if avatar && !(_isCollapsed && _avatarInDropdown)}
|
|
398
627
|
{#if avatarOnClick}
|
|
@@ -437,5 +666,6 @@
|
|
|
437
666
|
</DropdownMenu>
|
|
438
667
|
{/if}
|
|
439
668
|
</div>
|
|
669
|
+
</div>
|
|
440
670
|
{/if}
|
|
441
671
|
</header>
|