@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.
@@ -0,0 +1,170 @@
1
+ # LoginOrRegisterForm
2
+
3
+ Composite form that toggles between [`LoginForm`](../LoginForm/README.md), [`RegisterForm`](../RegisterForm/README.md), and (since 3.71) [`EmailVerifyForm`](../EmailVerifyForm/). The built-in mode switcher only exposes **login/register** tabs — `"verify"` is an outcome state entered programmatically (typically after a `requiresVerification` register response).
4
+
5
+ `LoginOrRegisterForm` is the form-only component; `LoginOrRegisterFormModal` wraps it in a `Modal` with an opener trigger and a mode-aware title.
6
+
7
+ ## Exports
8
+
9
+ | Export | Kind | Description |
10
+ | --------------------------------- | --------- | ------------------------------------------------- |
11
+ | `LoginOrRegisterForm` | component | Composite form |
12
+ | `LoginOrRegisterFormModal` | component | Modal-wrapped composite form |
13
+ | `LoginOrRegisterFormProps` | type | Props for `LoginOrRegisterForm` |
14
+ | `LoginOrRegisterFormModalProps` | type | Props for `LoginOrRegisterFormModal` |
15
+ | `LoginOrRegisterFormMode` | type | `"login" \| "register" \| "verify"` |
16
+
17
+ ## Mode behavior
18
+
19
+ - **login** — renders `LoginForm` + the shared `socialLogins` block.
20
+ - **register** — renders `RegisterForm` + the shared `socialLogins` block.
21
+ - **verify** — renders `EmailVerifyForm`. Social logins are hidden. The mode switcher is hidden (consumers transition in/out programmatically).
22
+
23
+ When the active mode changes, the relevant email is **one-shot copied** to the destination form's `email` field (login ↔ register ↔ verify) so the user does not have to re-type it. No continuous sync effect — flips are explicit.
24
+
25
+ ## LoginOrRegisterForm — Props
26
+
27
+ | Prop | Type | Default | Description |
28
+ | --------------------- | ------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------ |
29
+ | `mode` | `LoginOrRegisterFormMode` | `"login"` | Bindable active mode. |
30
+ | `loginData` | `LoginFormData` | empty | Bindable login form data (forwarded to `LoginForm`). |
31
+ | `registerData` | `RegisterFormData` | empty | Bindable register form data (forwarded to `RegisterForm`). |
32
+ | `verifyEmail` | `string` | `""` | Bindable email used by `EmailVerifyForm` (auto-seeded on transitions). |
33
+ | `onLogin` | `(data: LoginFormData) => void` | required | Login submit callback. |
34
+ | `onRegister` | `(data: RegisterFormData) => void` | required | Register submit callback. |
35
+ | `onVerify` | `(code: string) => void` | - | Verify submit callback (required only when using verify mode). |
36
+ | `onResendCode` | `() => Promise<void> \| void` | - | Resend handler — when set, `EmailVerifyForm` renders the resend control. |
37
+ | `isSubmitting` | `boolean` | `false` | Forwarded to all three forms. |
38
+ | `onForgotPassword` | `() => void` | - | Forgot-password handler for login mode. |
39
+ | `loginProps` | `Partial<LoginFormProps>` | - | Pass-through to the inner `LoginForm`. |
40
+ | `registerProps` | `Partial<RegisterFormProps>` | - | Pass-through to the inner `RegisterForm`. |
41
+ | `verifyProps` | `Partial<EmailVerifyFormProps>` | - | Pass-through to the inner `EmailVerifyForm` (e.g., `error`, `attemptsRemaining`). |
42
+ | `modeSwitcher` | `Snippet<[{ mode, setMode, t }]>` | - | Override the built-in `ButtonGroupRadio` switcher. |
43
+ | `loginModeLabel` | `string` | i18n | Override the "Log in" tab label. |
44
+ | `registerModeLabel` | `string` | i18n | Override the "Sign up" tab label. |
45
+ | `socialLogins` | `Snippet` | - | Shared OAuth buttons rendered once below the active form (hidden in verify mode). |
46
+ | `socialDividerLabel` | `string \| false` | i18n | Override (or hide with `false`) the divider above social buttons. |
47
+ | `footer` | `Snippet<[{ mode, setMode }]>` | - | Mode-aware footer. |
48
+ | `notifications` | `NotificationsStack` | - | Forwarded to inner forms. |
49
+ | `onModeChange` | `(next, prev) => void` | - | Called when the active mode changes. Use to clear parent-owned mode-specific state. |
50
+ | `t` | `TranslateFn` | English | i18n function. |
51
+ | `unstyled` / `class` | - | - | Standard styling escape hatches. |
52
+
53
+ ## LoginOrRegisterFormModal — extra props
54
+
55
+ Inherits all `LoginOrRegisterForm` props, plus:
56
+
57
+ | Prop | Type | Default | Description |
58
+ | --------------------- | ------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
59
+ | `title` | `string` | mode-aware ("Log In" / "Create account" / "Verify your email") | Modal title. |
60
+ | `visible` | `boolean` | `false` | Bindable modal visibility. |
61
+ | `trigger` | `Snippet<[{ open }]>` | - | Optional trigger element rendered outside the modal. |
62
+ | `classModal` | `string` | - | Class for the Modal box. |
63
+ | `classInner` | `string` | - | Class for the Modal inner width container. |
64
+ | `classForm` | `string` | - | Class forwarded to the inner `LoginOrRegisterForm`. |
65
+ | `noXClose` | `boolean` | `false` | Hide the close (X) button. |
66
+ | `onClose` | `() => false \| void` | - | Pre-close hook. Return `false` to prevent close. |
67
+ | `noClickOutsideClose` | `boolean` | `true` | Disable backdrop-click close (default-on to protect typed credentials). |
68
+
69
+ **Methods:** `open(openerOrEvent?)`, `close()` — exposed via `bind:this`.
70
+
71
+ ## Usage
72
+
73
+ ### Basic toggle
74
+
75
+ ```svelte
76
+ <script lang="ts">
77
+ import { LoginOrRegisterForm } from "@marianmeres/stuic";
78
+ </script>
79
+
80
+ <LoginOrRegisterForm
81
+ onLogin={(data) => api.login(data)}
82
+ onRegister={(data) => api.register(data)}
83
+ onForgotPassword={() => goto("/forgot-password")}
84
+ />
85
+ ```
86
+
87
+ ### Programmatic verify flow
88
+
89
+ ```svelte
90
+ <script lang="ts">
91
+ import { LoginOrRegisterForm } from "@marianmeres/stuic";
92
+
93
+ let mode = $state<"login" | "register" | "verify">("login");
94
+ let verifyEmail = $state("");
95
+
96
+ async function handleRegister(data) {
97
+ const { requiresVerification } = await api.register(data);
98
+ if (requiresVerification) {
99
+ verifyEmail = data.email;
100
+ mode = "verify";
101
+ }
102
+ }
103
+
104
+ async function handleVerify(code: string) {
105
+ await api.verifyEmail({ email: verifyEmail, code });
106
+ // success — go back to login (or directly into the app)
107
+ mode = "login";
108
+ }
109
+ </script>
110
+
111
+ <LoginOrRegisterForm
112
+ bind:mode
113
+ bind:verifyEmail
114
+ onLogin={api.login}
115
+ onRegister={handleRegister}
116
+ onVerify={handleVerify}
117
+ onResendCode={() => api.resendCode({ email: verifyEmail })}
118
+ />
119
+ ```
120
+
121
+ ### Modal with trigger + shared OAuth
122
+
123
+ ```svelte
124
+ <LoginOrRegisterFormModal
125
+ bind:mode
126
+ onLogin={api.login}
127
+ onRegister={api.register}
128
+ >
129
+ {#snippet trigger({ open })}
130
+ <Button onclick={open}>Sign in / Sign up</Button>
131
+ {/snippet}
132
+ {#snippet socialLogins()}
133
+ <Button variant="outline" onclick={loginWithGoogle}>Google</Button>
134
+ <Button variant="outline" onclick={loginWithGitHub}>GitHub</Button>
135
+ {/snippet}
136
+ </LoginOrRegisterFormModal>
137
+ ```
138
+
139
+ ## CSS Variables
140
+
141
+ Prefix: `--stuic-login-or-register-form-*`
142
+
143
+ | Variable | Purpose |
144
+ | -------------------------------------------------------------- | ---------------------------------- |
145
+ | `--stuic-login-or-register-form-gap` | Vertical gap |
146
+ | `--stuic-login-or-register-form-switcher-margin-bottom` | Spacing below the mode switcher |
147
+ | `--stuic-login-or-register-form-social-margin-top` | Margin above social block |
148
+ | `--stuic-login-or-register-form-social-gap` | Gap between social buttons |
149
+ | `--stuic-login-or-register-form-social-divider-color` | Divider text color |
150
+ | `--stuic-login-or-register-form-social-divider-line-color` | Divider line color |
151
+ | `--stuic-login-or-register-form-social-divider-font-size` | Divider text size |
152
+ | `--stuic-login-or-register-form-social-divider-margin-bottom` | Divider bottom margin |
153
+
154
+ ## i18n keys
155
+
156
+ | Key | English default |
157
+ | ------------------------------------------------ | -------------------- |
158
+ | `login_or_register_form.mode_login` | `Log in` |
159
+ | `login_or_register_form.mode_register` | `Sign up` |
160
+ | `login_or_register_form.mode_verify` | `Verify` |
161
+ | `login_or_register_form.modal_title_login` | `Log In` |
162
+ | `login_or_register_form.modal_title_register` | `Create account` |
163
+ | `login_or_register_form.modal_title_verify` | `Verify your email` |
164
+ | `login_or_register_form.social_divider` | `or continue with` |
165
+
166
+ ## See also
167
+
168
+ - [LoginForm](../LoginForm/README.md) — inner form for login mode.
169
+ - [RegisterForm](../RegisterForm/README.md) — inner form for register mode.
170
+ - [EmailVerifyForm](../EmailVerifyForm/) — inner form for verify mode.
@@ -0,0 +1,183 @@
1
+ # RegisterForm
2
+
3
+ Standalone registration form. Same conventions as [`LoginForm`](../LoginForm/README.md): `formData`, `onSubmit`, internal + server validation, i18n, optional `notifications`, social-logins snippet. Adds **declarative extra fields** (top/bottom positioning, custom validators) and an **`extraFieldsSlot` escape hatch** for non-FieldInput extras (e.g., a terms-of-service checkbox).
4
+
5
+ `RegisterForm` is the form-only component; `RegisterFormModal` wraps it in a `Modal` with an opener trigger.
6
+
7
+ ## Exports
8
+
9
+ | Export | Kind | Description |
10
+ | ------------------------------- | --------- | ------------------------------------------------- |
11
+ | `RegisterForm` | component | Form component |
12
+ | `RegisterFormModal` | component | Modal-wrapped form with optional trigger snippet |
13
+ | `RegisterFormProps` | type | Props for `RegisterForm` |
14
+ | `RegisterFormModalProps` | type | Props for `RegisterFormModal` |
15
+ | `RegisterFormData` | type | `{ email, password, passwordConfirm, extra }` |
16
+ | `RegisterFormValidationError` | type | `{ field, message }` |
17
+ | `RegisterFieldConfig` | type | Declarative extra-field descriptor |
18
+ | `createEmptyRegisterFormData` | function | Factory for an empty `RegisterFormData` |
19
+ | `validateRegisterForm` | function | `(data, t, extraFields, opts) => Error[]` |
20
+
21
+ ## `RegisterFieldConfig`
22
+
23
+ ```ts
24
+ interface RegisterFieldConfig {
25
+ name: string; // unique key under formData.extra
26
+ label: string; // already-translated
27
+ type?: "text" | "email" | "tel" | "url" | "password" | "number";
28
+ placeholder?: string;
29
+ required?: boolean;
30
+ autocomplete?: HTMLInputAttributes["autocomplete"];
31
+ initialValue?: unknown;
32
+ validate?: (value: unknown, data: RegisterFormData) => string | undefined;
33
+ position?: "top" | "bottom"; // default "bottom"
34
+ props?: Record<string, unknown>; // passthrough to FieldInput
35
+ }
36
+ ```
37
+
38
+ ## RegisterForm — Props
39
+
40
+ | Prop | Type | Default | Description |
41
+ | --------------------- | ------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
42
+ | `formData` | `RegisterFormData` | empty | Bindable form data. |
43
+ | `onSubmit` | `(data: RegisterFormData) => void` | required | Called after client-side validation passes. |
44
+ | `isSubmitting` | `boolean` | `false` | Disables the CTA during submission. |
45
+ | `errors` | `RegisterFormValidationError[]` | `[]` | Field-specific server errors (merged with internal validation). |
46
+ | `error` | `string` | - | General error rendered as a `DismissibleMessage` above the form. |
47
+ | `showPasswordConfirm` | `boolean` | `true` | Render the password-confirm field. |
48
+ | `passwordMinLength` | `number` | `8` | Minimum password length (fed into both the FieldInput attribute and the validator). |
49
+ | `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields. Rendered as `FieldInput`s positioned top or bottom. |
50
+ | `extraFieldsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Escape hatch for non-FieldInput extras. Rendered after declarative bottom fields. |
51
+ | `submitLabel` | `string` | i18n | Override the CTA label. |
52
+ | `submittingLabel` | `string` | i18n | Override the CTA label while submitting. |
53
+ | `submitButton` | `Snippet<[{ isSubmitting, disabled }]>` | - | Override the entire CTA section. |
54
+ | `socialLogins` | `Snippet` | - | Social/OAuth buttons. A divider is shown above when set. |
55
+ | `socialDividerLabel` | `string \| false` | i18n | Override (or hide with `false`) the divider above social buttons. |
56
+ | `footer` | `Snippet` | - | Content below the form (e.g., "Already have an account? Log in"). |
57
+ | `notifications` | `NotificationsStack` | - | When set, general errors are also pushed via `notifications.error()`. |
58
+ | `t` | `TranslateFn` | English | i18n function. |
59
+ | `unstyled` / `class` | - | - | Standard styling escape hatches. |
60
+ | `el` | `HTMLFormElement` | - | Bindable form element. |
61
+
62
+ ### Imperative methods (via `bind:this`)
63
+
64
+ | Method | Returns | Purpose |
65
+ | ------------------------------ | --------- | ------------------------------------------------------------------------ |
66
+ | `validate()` | `boolean` | Forces every inner field's validator to run. `true` if all valid. |
67
+ | `scrollToFirstError(opts?)` | `boolean` | Scrolls + focuses the first invalid field. Call after `validate()`. |
68
+
69
+ ## RegisterFormModal — extra props
70
+
71
+ Inherits all `RegisterForm` props, plus:
72
+
73
+ | Prop | Type | Default | Description |
74
+ | ------------- | ------------------------------- | ------------------------------------ | ---------------------------------------------------------- |
75
+ | `title` | `string` | `"Create account"` (i18n) | Modal title. |
76
+ | `visible` | `boolean` | `false` | Bindable modal visibility. |
77
+ | `trigger` | `Snippet<[{ open }]>` | - | Optional trigger element rendered outside the modal. |
78
+ | `classModal` | `string` | - | Class for the Modal box. |
79
+ | `classInner` | `string` | - | Class for the Modal inner width container. |
80
+ | `classForm` | `string` | - | Class forwarded to the inner `RegisterForm`. |
81
+ | `noXClose` | `boolean` | `false` | Hide the close (X) button. |
82
+ | `onClose` | `() => false \| void` | - | Pre-close hook. Return `false` to prevent close. |
83
+
84
+ **Methods:** `open(openerOrEvent?)`, `close()` — exposed via `bind:this`.
85
+
86
+ ## Usage
87
+
88
+ ### Basic
89
+
90
+ ```svelte
91
+ <script lang="ts">
92
+ import { RegisterForm } from "@marianmeres/stuic";
93
+ </script>
94
+
95
+ <RegisterForm onSubmit={(data) => signup(data)} />
96
+ ```
97
+
98
+ ### With declarative extra fields
99
+
100
+ ```svelte
101
+ <RegisterForm
102
+ onSubmit={signup}
103
+ extraFields={[
104
+ {
105
+ name: "fullName",
106
+ label: "Full name",
107
+ required: true,
108
+ position: "top",
109
+ autocomplete: "name",
110
+ },
111
+ {
112
+ name: "company",
113
+ label: "Company",
114
+ position: "bottom",
115
+ },
116
+ ]}
117
+ />
118
+ ```
119
+
120
+ ### With a terms-of-service checkbox (extraFieldsSlot)
121
+
122
+ ```svelte
123
+ <script lang="ts">
124
+ import { RegisterForm, FieldCheckbox } from "@marianmeres/stuic";
125
+
126
+ let formData = $state({
127
+ email: "",
128
+ password: "",
129
+ passwordConfirm: "",
130
+ extra: { tos: false },
131
+ });
132
+
133
+ function validateTos(value: unknown) {
134
+ return value ? undefined : "You must accept the terms.";
135
+ }
136
+ </script>
137
+
138
+ <RegisterForm bind:formData onSubmit={signup}>
139
+ {#snippet extraFieldsSlot({ formData, fieldError })}
140
+ <FieldCheckbox
141
+ bind:checked={formData.extra.tos}
142
+ label="I accept the terms of service"
143
+ error={fieldError("tos")}
144
+ validate={{ customValidator: () => validateTos(formData.extra.tos) }}
145
+ />
146
+ {/snippet}
147
+ </RegisterForm>
148
+ ```
149
+
150
+ ### Modal with trigger
151
+
152
+ ```svelte
153
+ <RegisterFormModal onSubmit={signup}>
154
+ {#snippet trigger({ open })}
155
+ <Button onclick={open}>Sign up</Button>
156
+ {/snippet}
157
+ </RegisterFormModal>
158
+ ```
159
+
160
+ ## CSS Variables
161
+
162
+ Prefix: `--stuic-register-form-*`
163
+
164
+ | Variable | Purpose |
165
+ | ----------------------------------------------------- | ----------------------------- |
166
+ | `--stuic-register-form-gap` | Vertical gap between sections |
167
+ | `--stuic-register-form-gap-row` | Gap inside multi-column rows |
168
+ | `--stuic-register-form-social-margin-top` | Margin above social block |
169
+ | `--stuic-register-form-social-gap` | Gap between social buttons |
170
+ | `--stuic-register-form-social-divider-color` | Divider text color |
171
+ | `--stuic-register-form-social-divider-line-color` | Divider line color |
172
+ | `--stuic-register-form-social-divider-font-size` | Divider text size |
173
+ | `--stuic-register-form-social-divider-margin-bottom` | Divider bottom margin |
174
+
175
+ ## i18n keys
176
+
177
+ Under the `register_form.*` namespace. See `_internal/register-form-i18n-defaults.ts` for the full list and English defaults.
178
+
179
+ ## See also
180
+
181
+ - [LoginForm](../LoginForm/README.md) — login counterpart.
182
+ - [LoginOrRegisterForm](../LoginOrRegisterForm/README.md) — composite that toggles between login, register, and verify modes.
183
+ - [EmailVerifyForm](../EmailVerifyForm/) — post-registration verify code form, often used in tandem.
@@ -0,0 +1,44 @@
1
+ # Separator
2
+
3
+ A horizontal or vertical separator line. Renders an accessible `role="separator"` element by default; pass `decorative` for a purely visual divider (`aria-hidden`).
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ | ------------- | ---------------------------- | -------------- | -------------------------------------------------------- |
9
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction. Also published as `data-orientation`. |
10
+ | `decorative` | `boolean` | `false` | Drop `role` + `aria-orientation`, set `aria-hidden="true"`. |
11
+ | `unstyled` | `boolean` | `false` | Skip all default styling. |
12
+ | `class` | `string` | - | Additional CSS classes. |
13
+ | `style` | `string` | - | Inline style. |
14
+ | `el` | `HTMLDivElement` | - | Bindable root element. |
15
+
16
+ ## Usage
17
+
18
+ ```svelte
19
+ <script lang="ts">
20
+ import { Separator } from "@marianmeres/stuic";
21
+ </script>
22
+
23
+ <p>Above</p>
24
+ <Separator />
25
+ <p>Below</p>
26
+
27
+ <div style="display: flex; align-items: center; gap: 0.5rem; height: 1.5rem;">
28
+ <span>Left</span>
29
+ <Separator orientation="vertical" />
30
+ <span>Right</span>
31
+ </div>
32
+
33
+ <!-- Purely decorative — removed from the a11y tree -->
34
+ <Separator decorative />
35
+ ```
36
+
37
+ ## Data Attributes
38
+
39
+ - `data-orientation` — `"horizontal"` or `"vertical"`. Use for CSS targeting.
40
+
41
+ ## Notes
42
+
43
+ - Vertical separators need a non-zero height from the surrounding layout (the component does not impose one).
44
+ - For semantic separation between landmark sections, prefer `<hr>` or native sectioning elements over a `Separator`.
@@ -10,21 +10,25 @@ STUIC is a Svelte 5 component library with centralized CSS theming via custom pr
10
10
 
11
11
  ---
12
12
 
13
- ## Three-Layer Styling System
13
+ ## Four-Layer Styling System
14
14
 
15
15
  ```
16
- Layer 1: Theme Tokens (--stuic-color-*)
16
+ Layer 1: Theme Tokens (--stuic-color-*)
17
17
 
18
- Layer 2: Component Tokens (--stuic-{component}-*)
18
+ Layer 2: Structural Tokens (--stuic-radius, --stuic-shadow, --stuic-border-width, --stuic-transition)
19
+ ↓ (used as fallbacks)
20
+ Layer 3: Component Tokens (--stuic-{component}-*)
19
21
 
20
- Layer 3: Internal Vars (--_bg, --_text, --_border)
22
+ Layer 4: Internal Vars (--_bg, --_text, --_border)
21
23
  ```
22
24
 
23
- **Layer 1 - Theme Tokens:** Global design tokens defining colors, provided by `@marianmeres/design-tokens/css/`.
25
+ **Layer 1 - Theme Tokens:** Global design tokens defining colors, provided by `@marianmeres/design-tokens/css/`. Includes intent colors (`primary`, `accent`, `destructive`, `warning`, `success`), surface intents (15%/30% color-mix tints), and role colors (`background`, `surface`, `muted`, `foreground`, `border`, `input`, `ring`).
24
26
 
25
- **Layer 2 - Component Tokens:** Component-specific customization points, defined in component `index.css` files.
27
+ **Layer 2 - Structural Tokens:** Shared cross-component visuals defined in `src/lib/index.css`. Override these to reshape the entire library at once (e.g., a brutalist "no radius / no shadow / no border" pass). See [Conventions § Shared Structural Tokens](./conventions.md).
26
28
 
27
- **Layer 3 - Internal Vars:** Private variables set by intent/variant selectors, used in base styles.
29
+ **Layer 3 - Component Tokens:** Component-specific customization points, defined in component `index.css` files. Reference structural tokens as fallbacks at usage sites (NOT at `:root`) so per-instance scoped overrides remain possible.
30
+
31
+ **Layer 4 - Internal Vars:** Private variables (`--_*`) set by intent/variant/size selectors and consumed in base styles.
28
32
 
29
33
  ---
30
34
 
@@ -32,7 +36,7 @@ Layer 3: Internal Vars (--_bg, --_text, --_border)
32
36
 
33
37
  ```
34
38
  src/lib/
35
- ├── components/ # 46 UI components
39
+ ├── components/ # 57 component directories
36
40
  │ └── {Name}/
37
41
  │ ├── {Name}.svelte # Main component
38
42
  │ ├── index.ts # Exports
@@ -44,12 +48,12 @@ src/lib/
44
48
  │ ├── *.ts # Traditional actions
45
49
  │ └── index.ts # Barrel export
46
50
 
47
- ├── utils/ # 43 utility modules
51
+ ├── utils/ # 44 utility modules
48
52
  │ ├── *.svelte.ts # Reactive utilities
49
53
  │ ├── *.ts # Pure functions
50
54
  │ └── index.ts # Barrel export
51
55
 
52
- ├── icons/ # Icon re-exports
56
+ ├── icons/ # Icon re-exports from @marianmeres/icons-fns
53
57
 
54
58
  ├── index.css # CENTRALIZED CSS imports
55
59
  └── index.ts # Main barrel export
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- 56 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
5
+ 57 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
6
6
 
7
7
  ## Component Categories
8
8
 
@@ -798,21 +798,34 @@ Prefix: `--stuic-cron-input-*`
798
798
 
799
799
  ## Header
800
800
 
801
- Responsive navigation header with logo, nav items, avatar, and automatic hamburger collapse.
801
+ Responsive navigation header with leading slot, logo, nav items, locale switcher, action icon buttons, avatar, and configurable responsive collapse. Two collapse modes:
802
+
803
+ - `"hamburger"` (default): nav items fold into a trailing dropdown along with the locale switcher and (if interactive) the avatar.
804
+ - `"hide"`: nav items are hidden entirely — no trailing hamburger. Avatar + actions stay visible. Common "app shell" pattern where a leading hamburger opens a side drawer.
802
805
 
803
806
  ### Exports
804
807
 
805
- | Export | Kind | Description |
806
- | -------------------------- | -------- | ------------------------- |
807
- | `Header` | component | Main header component |
808
- | `HeaderProps` | type | Props type |
809
- | `HeaderNavItem` | type | Nav item interface |
810
- | `HEADER_BASE_CLASSES` | constant | `"stuic-header"` |
811
- | `HEADER_LOGO_CLASSES` | constant | `"stuic-header-logo"` |
812
- | `HEADER_NAV_CLASSES` | constant | `"stuic-header-nav"` |
813
- | `HEADER_NAV_ITEM_CLASSES` | constant | `"stuic-header-nav-item"` |
814
- | `HEADER_END_CLASSES` | constant | `"stuic-header-end"` |
815
- | `HEADER_HAMBURGER_CLASSES` | constant | `"stuic-header-hamburger"` |
808
+ | Export | Kind | Description |
809
+ | ----------------------------------- | --------- | ---------------------------------------- |
810
+ | `Header` | component | Main header component |
811
+ | `HeaderProps` | type | Props type |
812
+ | `HeaderNavItem` | type | Nav item interface |
813
+ | `HeaderActionItem` | type | Action icon-button interface |
814
+ | `HeaderLocaleItem` | type | Locale switcher item |
815
+ | `HeaderCollapseMode` | type | `"hamburger" \| "hide"` |
816
+ | `HeaderLeadingHamburger` | type | `boolean \| "collapsed"` |
817
+ | `HEADER_BASE_CLASSES` | constant | `"stuic-header"` |
818
+ | `HEADER_CONTENT_CLASSES` | constant | `"stuic-header-content"` |
819
+ | `HEADER_LEADING_CLASSES` | constant | `"stuic-header-leading"` |
820
+ | `HEADER_LEADING_HAMBURGER_CLASSES` | constant | `"stuic-header-leading-hamburger"` |
821
+ | `HEADER_LOGO_CLASSES` | constant | `"stuic-header-logo"` |
822
+ | `HEADER_NAV_CLASSES` | constant | `"stuic-header-nav"` |
823
+ | `HEADER_NAV_ITEM_CLASSES` | constant | `"stuic-header-nav-item"` |
824
+ | `HEADER_ACTIONS_CLASSES` | constant | `"stuic-header-actions"` |
825
+ | `HEADER_ACTION_CLASSES` | constant | `"stuic-header-action"` |
826
+ | `HEADER_END_CLASSES` | constant | `"stuic-header-end"` |
827
+ | `HEADER_HAMBURGER_CLASSES` | constant | `"stuic-header-hamburger"` |
828
+ | `HEADER_LOCALE_CLASSES` | constant | `"stuic-header-locale"` |
816
829
 
817
830
  ### HeaderNavItem
818
831
 
@@ -821,6 +834,7 @@ interface HeaderNavItem {
821
834
  id: string | number;
822
835
  label: THC;
823
836
  href?: string;
837
+ target?: string;
824
838
  onclick?: () => void;
825
839
  icon?: THC;
826
840
  active?: boolean;
@@ -829,28 +843,98 @@ interface HeaderNavItem {
829
843
  }
830
844
  ```
831
845
 
846
+ ### HeaderActionItem
847
+
848
+ ```ts
849
+ interface HeaderActionItem {
850
+ id: string | number;
851
+ icon?: THC; // visible content (ignored when render is provided)
852
+ label: THC; // aria-label
853
+ onclick?: () => void;
854
+ href?: string;
855
+ target?: string;
856
+ active?: boolean;
857
+ disabled?: boolean;
858
+ class?: string;
859
+ /** Optional custom renderer — replaces the default <Button> while
860
+ * keeping Header-owned positioning + collapse behavior. */
861
+ render?: Snippet<[{
862
+ action: HeaderActionItem;
863
+ class: string;
864
+ isCollapsed: boolean;
865
+ onclick: () => void;
866
+ }]>;
867
+ }
868
+ ```
869
+
832
870
  ### Key Props
833
871
 
834
- | Prop | Type | Default | Description |
835
- | ------------------- | --------------------- | ----------------------- | ------------------------------------ |
836
- | `logo` | `Snippet` | — | Logo/brand snippet |
837
- | `projectName` | `string` | | Simple text logo alternative |
838
- | `items` | `HeaderNavItem[]` | `[]` | Navigation items |
839
- | `avatar` | `Snippet` | | Avatar snippet (far right) |
840
- | `avatarOnClick` | `() => void` | — | Avatar click handler |
841
- | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables |
842
- | `fixed` | `boolean` | `false` | Fixed positioning at top |
843
- | `isCollapsed` | `boolean` | | Bindable: collapsed state |
844
- | `isMenuOpen` | `boolean` | | Bindable: hamburger menu open |
845
- | `onSelect` | `(item) => void` | | Item selection callback |
846
-
847
- Snippets: `logo`, `avatar`, `children({ isCollapsed, items, offsetWidth })`.
872
+ | Prop | Type | Default | Description |
873
+ | ----------------------- | ------------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------ |
874
+ | `leading` | `Snippet<[{ isCollapsed }]>` | — | Leading (left) slot. Overrides `leadingHamburger`. |
875
+ | `leadingHamburger` | `boolean \| "collapsed"` | `false` | Built-in leading hamburger (`"collapsed"` = only below threshold). |
876
+ | `onLeadingHamburger` | `() => void` | | Click handler for the leading hamburger (typically opens a drawer). |
877
+ | `leadingHamburgerIcon` | `THC` | menu icon | Icon override for the leading hamburger. |
878
+ | `leadingHamburgerLabel` | `string` | `"Open menu"` | Aria-label for the leading hamburger. |
879
+ | `logo` | `Snippet` | | Logo/brand snippet. |
880
+ | `projectName` | `string` | | Simple text logo alternative. |
881
+ | `navVariant` | `ButtonVariant` | `"ghost"` | Button variant for nav items + locale trigger. |
882
+ | `items` | `HeaderNavItem[]` | `[]` | Nav items — inline expanded, dropdown collapsed (hamburger mode). |
883
+ | `actions` | `HeaderActionItem[]` | `[]` | Action icon buttons. Always visible. |
884
+ | `onActionSelect` | `(action) => void` | — | Called after per-item `onclick`. |
885
+ | `avatar` | `Snippet` | — | Avatar snippet (far right). |
886
+ | `avatarOnClick` | `() => void` | — | Makes avatar interactive. In `"hamburger"` mode it folds into the dropdown when collapsed. |
887
+ | `avatarLabel` | `THC` | `"Account"` | Label for the avatar entry inside the collapsed dropdown. |
888
+ | `locales` | `HeaderLocaleItem[]` | `[]` | Locale items. Switcher only renders when 2+. |
889
+ | `activeLocale` | `string` | — | Current locale id. |
890
+ | `onLocaleChange` | `(localeId) => void` | — | Locale selection callback. |
891
+ | `localeLabel` | `THC` | `"Language"` | Section header inside the collapsed dropdown. |
892
+ | `contentMaxWidth` | `string \| number` | — | Inner content row max-width (outer header stays 100%). |
893
+ | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables. |
894
+ | `collapseMode` | `"hamburger" \| "hide"` | `"hamburger"` | Collapse behavior. See top of section. |
895
+ | `keepLocaleOnCollapse` | `boolean` | `false` | Keep locale switcher visible when collapsed (only `collapseMode === "hide"`). |
896
+ | `fixed` | `boolean` | `false` | Fixed positioning at top. |
897
+ | `isCollapsed` | `boolean` | — | Bindable: collapsed state. |
898
+ | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open. |
899
+ | `dropdownPosition` | `DropdownMenuPosition` | `"bottom-span-right"` | Position of the collapsed dropdown. |
900
+ | `iconSize` | `number` | `24` | Hamburger/X icon size in px. |
901
+ | `onSelect` | `(item) => void` | — | Item selection callback (both modes). |
902
+
903
+ Snippets: `leading({ isCollapsed })`, `logo`, `avatar`, `children({ isCollapsed, items, offsetWidth })`. `HeaderActionItem.render` is a per-action snippet.
904
+
905
+ Class slots: `class`, `classContent`, `classLeading`, `classLeadingHamburger`, `classLogo`, `classNav`, `classNavItem`, `classNavItemActive`, `classActions`, `classAction`, `classActionActive`, `classEnd`, `classAvatar`, `classLocale`, `classHamburger`, `classDropdown`.
906
+
907
+ ### App-shell pattern (`collapseMode="hide"`)
908
+
909
+ Common pattern for app interfaces: the leading hamburger opens a side drawer for navigation, while avatar + action buttons (search, notifications, cart…) remain accessible in the top bar even on narrow viewports.
910
+
911
+ ```svelte
912
+ <Header
913
+ projectName="App"
914
+ items={navItems} <!-- inline expanded, hidden collapsed -->
915
+ actions={[
916
+ { id: "search", icon: { html: iconSearch() }, label: "Search", onclick: openSearch },
917
+ { id: "cart", icon: { html: iconCart() }, label: "Cart", onclick: openCart },
918
+ ]}
919
+ collapseMode="hide"
920
+ leadingHamburger="collapsed"
921
+ onLeadingHamburger={() => (drawerOpen = true)}
922
+ {locales}
923
+ {activeLocale}
924
+ onLocaleChange={(id) => (activeLocale = id)}
925
+ avatarOnClick={() => goto("/me")}
926
+ >
927
+ {#snippet avatar()}<Avatar initials="MM" autoColor />{/snippet}
928
+ </Header>
929
+ ```
930
+
931
+ See [Header/README.md](../../src/lib/components/Header/README.md) for the breakdown of which markup branch handles each requirement of this pattern.
848
932
 
849
933
  ### CSS Tokens
850
934
 
851
935
  Prefix: `--stuic-header-*`
852
936
 
853
- `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`
937
+ `padding-x`, `padding-y`, `gap`, `min-height`, `nav-gap`, `content-max-width`, `project-name-font-weight`, `z-index`, `bg`, `text`, `border-width`, `border-color`, `nav-item-bg-active`, `nav-item-text-active`
854
938
 
855
939
  ---
856
940