@marianmeres/stuic 3.184.0 → 3.187.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/API.md CHANGED
@@ -153,38 +153,38 @@ Navigation wrapper component.
153
153
 
154
154
  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>`.
155
155
 
156
- | Prop | Type | Default | Description |
157
- | ----------------------- | ------------------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
158
- | `leading` | `Snippet<[{ isCollapsed }]>` | — | Leading (left-side) slot. Overrides `leadingHamburger`. |
159
- | `leadingHamburger` | `boolean \| "collapsed"` | `false` | Built-in hamburger in the leading slot (`"collapsed"` = only below threshold). Ignored when `leading` is provided. |
160
- | `onLeadingHamburger` | `() => void` | — | Click handler for the built-in leading hamburger (typically opens a drawer). |
161
- | `leadingHamburgerIcon` | `THC` | menu icon | Icon override for the built-in leading hamburger. |
162
- | `leadingHamburgerLabel` | `string` | `"Open menu"` | Aria-label for the leading hamburger. |
163
- | `logo` | `Snippet` | — | Logo/brand snippet. |
164
- | `projectName` | `string` | — | Simple text logo alternative. |
165
- | `navVariant` | `ButtonVariant` | `"ghost"` | Button variant for nav items and the locale switcher trigger. |
166
- | `items` | `HeaderNavItem[]` | `[]` | Navigation items — inline when expanded, dropdown when collapsed (hamburger mode). |
167
- | `actions` | `HeaderActionItem[]` | `[]` | Action icon buttons between the locale switcher and the avatar. Always visible — never fold into the dropdown. |
168
- | `onActionSelect` | `(action) => void` | — | Called after the per-item `onclick`. |
169
- | `avatar` | `Snippet` | — | Avatar snippet (far right). |
170
- | `avatarOnClick` | `() => void` | — | Makes the avatar interactive. In `"hamburger"` collapse mode it moves into the dropdown. |
171
- | `avatarLabel` | `THC` | `"Account"` | Label for the avatar entry inside the collapsed dropdown. |
172
- | `locales` | `HeaderLocaleItem[]` | `[]` | Locale items. Switcher only renders when 2+. |
173
- | `activeLocale` | `string` | — | Current locale id. |
174
- | `onLocaleChange` | `(localeId) => void` | — | Locale selection callback. |
175
- | `localeLabel` | `THC` | `"Language"` | Section header inside the collapsed dropdown. |
176
- | `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`. |
177
- | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables. |
178
- | `collapseMode` | `"hamburger" \| "hide"` | `"hamburger"` | Collapse behavior. `"hide"` keeps avatar/actions visible and renders no trailing hamburger (app-shell pattern). |
179
- | `keepLocaleOnCollapse` | `boolean` | `false` | Keep the locale switcher visible in collapsed mode (only `collapseMode === "hide"`). |
180
- | `fixed` | `boolean` | `false` | Fixed positioning at the top. |
181
- | `safeArea` | `boolean` | `false` | PWA: when installed/standalone, offset the **top app bar** below the device safe-area insets (top + side notch). No-op in a browser tab. Set only on the top bar — not in-page/detail/drawer headers. See Header README. |
182
- | `isCollapsed` | `boolean` | — | Bindable: collapsed state. |
183
- | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open. |
184
- | `dropdownPosition` | `DropdownMenuPosition` | `"bottom-span-right"` | Position of the collapsed dropdown. |
185
- | `iconSize` | `number` | `24` | Hamburger/X icon size in px. |
186
- | `onSelect` | `(item) => void` | — | Item selection callback (both modes). |
187
- | `children` | `Snippet<[{ isCollapsed, items, offsetWidth }]>` | — | Escape hatch: override the entire inner layout. |
156
+ | Prop | Type | Default | Description |
157
+ | ----------------------- | ------------------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
+ | `leading` | `Snippet<[{ isCollapsed }]>` | — | Leading (left-side) slot. Overrides `leadingHamburger`. |
159
+ | `leadingHamburger` | `boolean \| "collapsed"` | `false` | Built-in hamburger in the leading slot (`"collapsed"` = only below threshold). Ignored when `leading` is provided. |
160
+ | `onLeadingHamburger` | `() => void` | — | Click handler for the built-in leading hamburger (typically opens a drawer). |
161
+ | `leadingHamburgerIcon` | `THC` | menu icon | Icon override for the built-in leading hamburger. |
162
+ | `leadingHamburgerLabel` | `string` | `"Open menu"` | Aria-label for the leading hamburger. |
163
+ | `logo` | `Snippet` | — | Logo/brand snippet. |
164
+ | `projectName` | `string` | — | Simple text logo alternative. |
165
+ | `navVariant` | `ButtonVariant` | `"ghost"` | Button variant for nav items and the locale switcher trigger. |
166
+ | `items` | `HeaderNavItem[]` | `[]` | Navigation items — inline when expanded, dropdown when collapsed (hamburger mode). |
167
+ | `actions` | `HeaderActionItem[]` | `[]` | Action icon buttons between the locale switcher and the avatar. Always visible — never fold into the dropdown. |
168
+ | `onActionSelect` | `(action) => void` | — | Called after the per-item `onclick`. |
169
+ | `avatar` | `Snippet` | — | Avatar snippet (far right). |
170
+ | `avatarOnClick` | `() => void` | — | Makes the avatar interactive. In `"hamburger"` collapse mode it moves into the dropdown. |
171
+ | `avatarLabel` | `THC` | `"Account"` | Label for the avatar entry inside the collapsed dropdown. |
172
+ | `locales` | `HeaderLocaleItem[]` | `[]` | Locale items. Switcher only renders when 2+. |
173
+ | `activeLocale` | `string` | — | Current locale id. |
174
+ | `onLocaleChange` | `(localeId) => void` | — | Locale selection callback. |
175
+ | `localeLabel` | `THC` | `"Language"` | Section header inside the collapsed dropdown. |
176
+ | `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`. |
177
+ | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables. |
178
+ | `collapseMode` | `"hamburger" \| "hide"` | `"hamburger"` | Collapse behavior. `"hide"` keeps avatar/actions visible and renders no trailing hamburger (app-shell pattern). |
179
+ | `keepLocaleOnCollapse` | `boolean` | `true` | Keep the locale switcher inline (next to the hamburger) when collapsed — in **both** collapse modes, and without duplicating it inside the dropdown. `false` folds it into the dropdown (`"hamburger"`) or hides it (`"hide"`). |
180
+ | `fixed` | `boolean` | `false` | Fixed positioning at the top. |
181
+ | `safeArea` | `boolean` | `false` | PWA: when installed/standalone, offset the **top app bar** below the device safe-area insets (top + side notch). No-op in a browser tab. Set only on the top bar — not in-page/detail/drawer headers. See Header README. |
182
+ | `isCollapsed` | `boolean` | — | Bindable: collapsed state. |
183
+ | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open. |
184
+ | `dropdownPosition` | `DropdownMenuPosition` | `"bottom-span-right"` | Position of the collapsed dropdown. |
185
+ | `iconSize` | `number` | `24` | Hamburger/X icon size in px. |
186
+ | `onSelect` | `(item) => void` | — | Item selection callback (both modes). |
187
+ | `children` | `Snippet<[{ isCollapsed, items, offsetWidth }]>` | — | Escape hatch: override the entire inner layout. |
188
188
 
189
189
  Class slots: `class`, `classContent`, `classLeading`, `classLeadingHamburger`, `classLogo`, `classNav`, `classNavItem`, `classNavItemActive`, `classActions`, `classAction`, `classActionActive`, `classEnd`, `classAvatar`, `classLocale`, `classHamburger`, `classDropdown`.
190
190
 
@@ -223,9 +223,9 @@ Class slots: `class`, `classContent`, `classLeading`, `classLeadingHamburger`, `
223
223
 
224
224
  **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).
225
225
 
226
- **HeaderLocaleItem:** `{ id, label }`
226
+ **HeaderLocaleItem:** `{ id, label, shortLabel? }` — `shortLabel` replaces `label` on the inline trigger **in collapsed mode only** (keeps a long name like "Slovenčina" from crowding a narrow header); the dropdown list always shows the full `label`.
227
227
 
228
- 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`.
228
+ CSS tokens: `--stuic-header-padding-x`, `--stuic-header-padding-y`, `--stuic-header-gap`, `--stuic-header-end-gap-collapsed`, `--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`.
229
229
 
230
230
  ---
231
231
 
@@ -35,6 +35,12 @@
35
35
  id: string;
36
36
  /** Display label — supports THC (string, html, component, snippet) */
37
37
  label: THC;
38
+ /** Compact label for the inline trigger in COLLAPSED mode only.
39
+ * Falls back to `label`. Use it when the full label ("Slovenčina")
40
+ * would crowd the narrow header next to the hamburger — the dropdown
41
+ * list always shows the full `label`, so the long form stays
42
+ * available where there is room to read it. */
43
+ shortLabel?: THC;
38
44
  }
39
45
 
40
46
  export interface HeaderActionItem {
@@ -86,11 +92,13 @@
86
92
  }
87
93
 
88
94
  /** 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.
95
+ * - "hamburger": nav items fold into a trailing dropdown along with an
96
+ * interactive avatar.
91
97
  * - "hide": nav items are hidden entirely. No trailing hamburger renders.
92
- * Avatar stays visible. Locale visibility is controlled by
93
- * `keepLocaleOnCollapse`. */
98
+ * Avatar stays visible.
99
+ *
100
+ * In BOTH modes the locale switcher stays inline next to the hamburger
101
+ * by default — see `keepLocaleOnCollapse`. */
94
102
  export type HeaderCollapseMode = "hamburger" | "hide";
95
103
 
96
104
  /** Visibility for the built-in leading hamburger button:
@@ -152,9 +160,27 @@
152
160
  collapseThreshold?: number;
153
161
  /** Collapse behavior when below threshold (defaults to "hamburger") */
154
162
  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). */
163
+ /** Keep the locale switcher visible — inline, next to the hamburger —
164
+ * when the header is collapsed. Applies to BOTH collapse modes.
165
+ * Defaults to `true`.
166
+ *
167
+ * Why it defaults on: the person who most needs this control is the
168
+ * one who landed in a language they cannot read. For them a visible
169
+ * "EN ▾" trigger is the only self-describing widget in the bar, while
170
+ * a locale entry folded into the hamburger — listed last, under a
171
+ * "Language" heading they also cannot read, often below the fold of a
172
+ * long menu — is effectively invisible.
173
+ *
174
+ * When `true` in `"hamburger"` mode the locale section is NOT also
175
+ * appended to the trailing dropdown, so it is never reachable twice.
176
+ *
177
+ * Set `false` for the alternative: folds into the trailing dropdown
178
+ * in `"hamburger"` mode, hidden entirely in `"hide"` mode (this was
179
+ * the behavior before the prop applied to both modes). Worth doing
180
+ * when the end area is already crowded with actions, or when the
181
+ * app's own drawer owns the language switch. See also
182
+ * `HeaderLocaleItem.shortLabel` for keeping the inline trigger
183
+ * narrow. */
158
184
  keepLocaleOnCollapse?: boolean;
159
185
  /** Fixed positioning (top of viewport) */
160
186
  fixed?: boolean;
@@ -263,7 +289,7 @@
263
289
  contentMaxWidth,
264
290
  collapseThreshold = 768,
265
291
  collapseMode = "hamburger",
266
- keepLocaleOnCollapse = false,
292
+ keepLocaleOnCollapse = true,
267
293
  fixed = false,
268
294
  safeArea = false,
269
295
  isCollapsed = $bindable(false),
@@ -336,17 +362,28 @@
336
362
  // Locale switcher: only render when 2+ locales
337
363
  let _hasLocales = $derived(locales.length > 1);
338
364
 
339
- // Visibility of the inline (expanded-form) locale switcher.
340
- // In "hamburger" mode: visible only when not collapsed (it folds into the
341
- // trailing dropdown when collapsed). In "hide" mode: visible when not
342
- // collapsed, or when collapsed and `keepLocaleOnCollapse` is set.
365
+ // Visibility of the inline (expanded-form) locale switcher. Always visible
366
+ // when expanded; when collapsed it stays inline unless explicitly opted out
367
+ // — in BOTH collapse modes. Deliberately mode-agnostic: a user who cannot
368
+ // read the current language must be able to find the switcher without
369
+ // opening a menu whose trigger tells them nothing.
343
370
  let _showLocaleSwitcher = $derived(
344
- _hasLocales && (!_isCollapsed || (collapseMode === "hide" && keepLocaleOnCollapse))
371
+ _hasLocales && (!_isCollapsed || keepLocaleOnCollapse)
345
372
  );
346
373
 
347
374
  // Active locale object (for trigger label); fallback to first
348
375
  let _activeLocale = $derived(locales.find((l) => l.id === activeLocale) ?? locales[0]);
349
376
 
377
+ // Inline trigger label. `shortLabel` (when provided) is used ONLY in
378
+ // collapsed mode, where horizontal space next to the hamburger is scarce;
379
+ // expanded mode and the dropdown list always show the full `label`.
380
+ let _localeTriggerLabel = $derived.by((): THC | undefined => {
381
+ if (!_activeLocale) return undefined;
382
+ return _isCollapsed
383
+ ? (_activeLocale.shortLabel ?? _activeLocale.label)
384
+ : _activeLocale.label;
385
+ });
386
+
350
387
  // Locale items for the expanded-mode DropdownMenu
351
388
  let _localeDropdownItems = $derived.by((): DropdownMenuItem[] => {
352
389
  return locales.map(
@@ -389,8 +426,10 @@
389
426
  }) satisfies DropdownMenuActionItem
390
427
  );
391
428
 
392
- // Append locale section when locales are available
393
- if (_hasLocales) {
429
+ // Append the locale section ONLY when the inline switcher is hidden.
430
+ // With the inline trigger visible this would be a second, worse route
431
+ // to the same setting — and the one nobody finds.
432
+ if (_hasLocales && !_showLocaleSwitcher) {
394
433
  if (navItems.length > 0) {
395
434
  navItems.push({ type: "divider" });
396
435
  }
@@ -556,7 +595,9 @@
556
595
 
557
596
  <!-- End area: locale + actions + avatar + trailing hamburger -->
558
597
  <div class={_classEnd}>
559
- <!-- Locale switcher (shown when expanded, or in "hide" mode with keepLocaleOnCollapse) -->
598
+ <!-- Locale switcher — inline when expanded and, unless
599
+ `keepLocaleOnCollapse={false}`, still inline when collapsed
600
+ (both collapse modes) rather than folded into the dropdown -->
560
601
  {#if _showLocaleSwitcher}
561
602
  <DropdownMenu
562
603
  items={_localeDropdownItems}
@@ -573,8 +614,8 @@
573
614
  aria-label="Change language"
574
615
  {...triggerProps}
575
616
  >
576
- {#if _activeLocale}
577
- <Thc thc={_activeLocale.label} />
617
+ {#if _localeTriggerLabel !== undefined}
618
+ <Thc thc={_localeTriggerLabel} />
578
619
  {/if}
579
620
  <span
580
621
  class={twMerge(
@@ -28,6 +28,12 @@ export interface HeaderLocaleItem {
28
28
  id: string;
29
29
  /** Display label — supports THC (string, html, component, snippet) */
30
30
  label: THC;
31
+ /** Compact label for the inline trigger in COLLAPSED mode only.
32
+ * Falls back to `label`. Use it when the full label ("Slovenčina")
33
+ * would crowd the narrow header next to the hamburger — the dropdown
34
+ * list always shows the full `label`, so the long form stays
35
+ * available where there is room to read it. */
36
+ shortLabel?: THC;
31
37
  }
32
38
  export interface HeaderActionItem {
33
39
  /** Unique identifier */
@@ -75,11 +81,13 @@ export interface HeaderActionItem {
75
81
  ]>;
76
82
  }
77
83
  /** Collapse behavior when the header drops below `collapseThreshold`:
78
- * - "hamburger": nav items fold into a trailing dropdown along with the
79
- * locale switcher and an interactive avatar.
84
+ * - "hamburger": nav items fold into a trailing dropdown along with an
85
+ * interactive avatar.
80
86
  * - "hide": nav items are hidden entirely. No trailing hamburger renders.
81
- * Avatar stays visible. Locale visibility is controlled by
82
- * `keepLocaleOnCollapse`. */
87
+ * Avatar stays visible.
88
+ *
89
+ * In BOTH modes the locale switcher stays inline next to the hamburger
90
+ * by default — see `keepLocaleOnCollapse`. */
83
91
  export type HeaderCollapseMode = "hamburger" | "hide";
84
92
  /** Visibility for the built-in leading hamburger button:
85
93
  * - false/undefined: not rendered
@@ -141,9 +149,27 @@ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children"> {
141
149
  collapseThreshold?: number;
142
150
  /** Collapse behavior when below threshold (defaults to "hamburger") */
143
151
  collapseMode?: HeaderCollapseMode;
144
- /** When `collapseMode === "hide"`, keep the locale switcher visible in
145
- * collapsed mode. No effect when `collapseMode === "hamburger"`
146
- * (locale already folds into the trailing dropdown there). */
152
+ /** Keep the locale switcher visible — inline, next to the hamburger —
153
+ * when the header is collapsed. Applies to BOTH collapse modes.
154
+ * Defaults to `true`.
155
+ *
156
+ * Why it defaults on: the person who most needs this control is the
157
+ * one who landed in a language they cannot read. For them a visible
158
+ * "EN ▾" trigger is the only self-describing widget in the bar, while
159
+ * a locale entry folded into the hamburger — listed last, under a
160
+ * "Language" heading they also cannot read, often below the fold of a
161
+ * long menu — is effectively invisible.
162
+ *
163
+ * When `true` in `"hamburger"` mode the locale section is NOT also
164
+ * appended to the trailing dropdown, so it is never reachable twice.
165
+ *
166
+ * Set `false` for the alternative: folds into the trailing dropdown
167
+ * in `"hamburger"` mode, hidden entirely in `"hide"` mode (this was
168
+ * the behavior before the prop applied to both modes). Worth doing
169
+ * when the end area is already crowded with actions, or when the
170
+ * app's own drawer owns the language switch. See also
171
+ * `HeaderLocaleItem.shortLabel` for keeping the inline trigger
172
+ * narrow. */
147
173
  keepLocaleOnCollapse?: boolean;
148
174
  /** Fixed positioning (top of viewport) */
149
175
  fixed?: boolean;
@@ -4,17 +4,54 @@ Top-bar component with leading slot, project logo, nav items, locale switcher, o
4
4
 
5
5
  ## Examples
6
6
 
7
+ ### The locale switcher stays visible when collapsed
8
+
9
+ When the header collapses, the locale switcher does **not** fold into the trailing hamburger. It stays inline, immediately before the actions / avatar / hamburger, in **both** collapse modes. Nothing to configure — it is the default.
10
+
11
+ ```svelte
12
+ <Header
13
+ projectName="App"
14
+ items={navItems}
15
+ {locales}
16
+ {activeLocale}
17
+ onLocaleChange={(id) => (activeLocale = id)}
18
+ />
19
+ <!-- collapsed: [App] [EN ▾] [☰] -->
20
+ ```
21
+
22
+ This is the one control `Header` deliberately treats differently from a nav item, and the reason is worth stating because it looks like an inconsistency:
23
+
24
+ > The person who most needs the language switch is the person who landed in a language they cannot read. A visible `EN ▾` trigger is self-describing to them — it shows the current language and it is obviously a control. An entry inside the hamburger is not: the trigger is an unlabeled icon, the section heading says "Language" in a language they do not speak, and past a handful of nav items it sits below the fold of a menu they have to scroll with a finger. It is reliably never found.
25
+
26
+ What follows from it:
27
+
28
+ | Behavior | Why |
29
+ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
30
+ | The trailing dropdown has **no** locale section while the inline trigger is visible. | Two routes to one setting, one of which nobody finds. `_dropdownItems` appends the locale section only when the inline switcher is hidden. |
31
+ | A header whose only collapsible content was the locale switcher renders **no hamburger at all**. | `_dropdownItems` comes back empty, and the trailing `DropdownMenu` is gated on it being non-empty. |
32
+ | The inline trigger shows `HeaderLocaleItem.shortLabel` (when set) **only** while collapsed. | `"Slovenčina"` next to a hamburger on a 360px phone is what pushed the switcher into the menu in the first place. The dropdown list always shows the full `label`. |
33
+ | `keepLocaleOnCollapse={false}` restores the alternative: folds into the dropdown in `"hamburger"` mode, hidden entirely in `"hide"` mode. | For an end area already crowded with actions, or an app whose own drawer owns the language switch. |
34
+
35
+ ```ts
36
+ // Long locale names on a narrow header: short form on the inline trigger,
37
+ // full form in the dropdown list.
38
+ const locales: HeaderLocaleItem[] = [
39
+ { id: "en", label: "English", shortLabel: "EN" },
40
+ { id: "sk", label: "Slovenčina", shortLabel: "SK" },
41
+ ];
42
+ ```
43
+
7
44
  ### App-like collapse: avatar + actions visible, everything else hidden
8
45
 
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).
46
+ Common "app shell" pattern: when the header collapses below `collapseThreshold`, the avatar, the locale switcher and a few key actions (search, notifications, cart…) remain visible, the trailing hamburger is NOT shown, and the nav items are hidden entirely (the nav typically lives in a drawer triggered by the leading hamburger instead).
10
47
 
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. |
48
+ | Requirement | Where it's handled | How |
49
+ | ------------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | **Avatar stays visible** | [Header.svelte:673](./Header.svelte#L673) — `{#if avatar && !(_isCollapsed && _avatarInDropdown)}` | `_avatarInDropdown` requires `collapseMode === "hamburger"`. In `"hide"` mode it's always `false`, so the avatar always renders. |
51
+ | **Action buttons stay visible** | [Header.svelte:632](./Header.svelte#L632) — `<!-- Actions (icon buttons, always visible) -->` | The actions loop has no collapse gating — items render in both modes. |
52
+ | **No trailing hamburger** | [Header.svelte:689](./Header.svelte#L689) — `{#if _isCollapsed && _dropdownItems.length > 0}` | In `"hide"` mode, `_dropdownItems` short-circuits to `[]`, so the `{#if}` is false → no trailing hamburger. |
53
+ | **Nav items hidden** | [Header.svelte:564](./Header.svelte#L564) — `{#if !_isCollapsed && items.length > 0}` | Inline nav requires `!_isCollapsed`; combined with the empty `_dropdownItems` above, items don't reappear in a dropdown either. |
54
+ | **Locale stays visible** | [Header.svelte:369](./Header.svelte#L369) — `!_isCollapsed \|\| keepLocaleOnCollapse` | `keepLocaleOnCollapse` defaults to `true` in both modes (see the section above). Pass `false` to hide it, e.g. when the drawer owns the language switch. |
18
55
 
19
56
  Minimal config:
20
57
 
@@ -26,7 +63,7 @@ Minimal config:
26
63
  collapseMode="hide" <!-- no trailing hamburger; avatar stays -->
27
64
  leadingHamburger <!-- optional: drives a drawer for the hidden nav -->
28
65
  onLeadingHamburger={() => (drawerOpen = true)}
29
- {locales} {activeLocale} <!-- hidden in collapsed (keepLocaleOnCollapse defaults to false) -->
66
+ {locales} {activeLocale} <!-- stays visible in collapsed (keepLocaleOnCollapse defaults to true) -->
30
67
  onLocaleChange={(id) => (activeLocale = id)}
31
68
  avatarOnClick={() => alert("Profile")} <!-- safe in "hide" mode — won't move into dropdown -->
32
69
  >
@@ -21,6 +21,14 @@
21
21
  /* Actions (icon buttons in the end region) */
22
22
  --stuic-header-actions-gap: 0.25rem;
23
23
 
24
+ /* Gap between the end-area regions (locale / actions / avatar / hamburger)
25
+ while collapsed. Expanded uses --stuic-header-gap; collapsed is tighter
26
+ because the locale switcher stays inline there instead of folding into
27
+ the hamburger, and at 320px the full gap pushes a locale + 3 actions +
28
+ hamburger row past the content edge. Set to var(--stuic-header-gap) to
29
+ keep both states equal. */
30
+ --stuic-header-end-gap-collapsed: 0.5rem;
31
+
24
32
  /* Hamburger offsets — outdent the built-in hamburger buttons on both
25
33
  inline sides so the *icon* (not the invisible iconButton hit area)
26
34
  sits flush with the header's content padding on one side and tightens
@@ -126,6 +134,14 @@
126
134
  font-size: var(--stuic-header-project-name-font-size, var(--text-lg, 1.125rem));
127
135
  font-weight: var(--stuic-header-project-name-font-weight);
128
136
  white-space: nowrap;
137
+ /* Truncate instead of overflowing the row. `min-width: 0` lets this
138
+ flex item shrink below its content width (auto minimum size would
139
+ otherwise pin it), and `overflow: hidden` is what makes
140
+ `text-overflow` apply. Matters most in collapsed mode, where the
141
+ name shares the row with the locale trigger + hamburger. */
142
+ min-width: 0;
143
+ overflow: hidden;
144
+ text-overflow: ellipsis;
129
145
  }
130
146
 
131
147
  /* ============================================================================
@@ -193,6 +209,10 @@
193
209
  flex-shrink: 0;
194
210
  }
195
211
 
212
+ .stuic-header[data-collapsed] .stuic-header-end {
213
+ gap: var(--stuic-header-end-gap-collapsed);
214
+ }
215
+
196
216
  .stuic-header-avatar {
197
217
  display: flex;
198
218
  align-items: center;
@@ -177,6 +177,14 @@
177
177
  placeholder?: THC;
178
178
  /** Hide the "Preview" action (the `AssetsPreview` lightbox). */
179
179
  noPreview?: boolean;
180
+ /**
181
+ * Never show the asset's `name` to the user — the tile's filename line, the
182
+ * lightbox caption, the "… removed" / "… uploaded" announcements and toasts all
183
+ * fall back to name-less wording (`replace_file_short`, `removed_short`, ...).
184
+ * For stores that hand back a content-hash filename, which carries no meaning
185
+ * worth reading. The type / size line (`metaText`) is unaffected.
186
+ */
187
+ noFilename?: boolean;
180
188
  /** Hide the lightbox's Download button. */
181
189
  noDownload?: boolean;
182
190
  /** See `AssetsPreview.onDownload`: replaces the default download of `url.original`. */
@@ -288,6 +296,7 @@
288
296
  placeholder,
289
297
  noPreview = false,
290
298
  noDownload = false,
299
+ noFilename = false,
291
300
  onDownload,
292
301
  onChange,
293
302
  }: Props = $props();
@@ -356,9 +365,13 @@
356
365
  let tileActionText = $derived(
357
366
  canUpload
358
367
  ? shown
359
- ? t("replace_file", { name: shown.name })
368
+ ? noFilename
369
+ ? t("replace_file_short")
370
+ : t("replace_file", { name: shown.name })
360
371
  : t("pick_file")
361
- : (shown?.name ?? "")
372
+ : noFilename
373
+ ? ""
374
+ : (shown?.name ?? "")
362
375
  );
363
376
  let metaText = $derived.by(() => {
364
377
  if (!shown) return "";
@@ -540,7 +553,7 @@
540
553
  assetsPreview?.close?.();
541
554
  pending = { asset: optimistic, file, progress: 0, error: null };
542
555
  const seq = ++uploadSeq;
543
- announce(t("uploading", { name: file.name }));
556
+ announce(noFilename ? t("uploading_short") : t("uploading", { name: file.name }));
544
557
 
545
558
  const onProgress = (p: number) => {
546
559
  if (seq === uploadSeq && pending && !pending.error) {
@@ -564,14 +577,20 @@
564
577
  }
565
578
  pending = null;
566
579
  commit(uploaded);
567
- announce(t("uploaded", { name: uploaded.name ?? file.name }));
580
+ announce(
581
+ noFilename
582
+ ? t("uploaded_short")
583
+ : t("uploaded", { name: uploaded.name ?? file.name })
584
+ );
568
585
  })
569
586
  .catch((e) => {
570
587
  if (seq !== uploadSeq) return;
571
588
  const error = `${e?.message ?? e}`;
572
589
  clog.error(error);
573
590
  if (pending) pending.error = error;
574
- const msg = t("upload_failed_named", { name: file.name, error });
591
+ const msg = noFilename
592
+ ? t("upload_failed_error", { error })
593
+ : t("upload_failed_named", { name: file.name, error });
575
594
  announce(msg);
576
595
  // the consumer may own the report (a quota panel it already renders):
577
596
  // `false` skips only the toast, the tile's Retry / Discard stay
@@ -609,7 +628,7 @@
609
628
  }
610
629
  assetsPreview?.close?.();
611
630
  commit(null);
612
- announce(t("removed", { name: current.name }));
631
+ announce(noFilename ? t("removed_short") : t("removed", { name: current.name }));
613
632
  if (undoTtl > 0) {
614
633
  removed = current;
615
634
  undoTimer = setTimeout(() => clear_removed(), undoTtl);
@@ -622,7 +641,7 @@
622
641
  if (!back) return;
623
642
  clear_removed();
624
643
  commit(back);
625
- announce(t("restored", { name: back.name }));
644
+ announce(noFilename ? t("restored_short") : t("restored", { name: back.name }));
626
645
  focus_tile();
627
646
  }
628
647
 
@@ -823,13 +842,17 @@
823
842
  {#if isLoading}
824
843
  <Skeleton variant="text" lines={2} width="60%" />
825
844
  {:else if shown}
826
- <div class="truncate font-medium" title={shown.name}>{shown.name}</div>
845
+ {#if !noFilename}
846
+ <div class="truncate font-medium" title={shown.name}>{shown.name}</div>
847
+ {/if}
827
848
  {#if metaText}
828
849
  <div class="text-xs stuic-field-single-asset-meta">{metaText}</div>
829
850
  {/if}
830
851
  {:else if undoOffer}
831
852
  <div class="stuic-field-single-asset-meta">
832
- <span>{t("removed", { name: undoOffer.name })}</span>
853
+ <span>
854
+ {noFilename ? t("removed_short") : t("removed", { name: undoOffer.name })}
855
+ </span>
833
856
  <button type="button" class="stuic-field-single-asset-undo" onclick={undo}>
834
857
  {@html iconUndo({ size: 14 })}
835
858
  <span>{t("undo")}</span>
@@ -925,6 +948,7 @@
925
948
  noPrevNext
926
949
  noDots
927
950
  noCurrentOfTotal
951
+ noName={noFilename}
928
952
  {noDownload}
929
953
  onDelete={(_, _index, controls) => {
930
954
  controls.close();
@@ -133,6 +133,14 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
133
133
  placeholder?: THC;
134
134
  /** Hide the "Preview" action (the `AssetsPreview` lightbox). */
135
135
  noPreview?: boolean;
136
+ /**
137
+ * Never show the asset's `name` to the user — the tile's filename line, the
138
+ * lightbox caption, the "… removed" / "… uploaded" announcements and toasts all
139
+ * fall back to name-less wording (`replace_file_short`, `removed_short`, ...).
140
+ * For stores that hand back a content-hash filename, which carries no meaning
141
+ * worth reading. The type / size line (`metaText`) is unaffected.
142
+ */
143
+ noFilename?: boolean;
136
144
  /** Hide the lightbox's Download button. */
137
145
  noDownload?: boolean;
138
146
  /** See `AssetsPreview.onDownload`: replaces the default download of `url.original`. */
@@ -937,6 +937,7 @@ a dialog and resolves with the cropped file.
937
937
  | `size` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Tile height: preset or CSS length |
938
938
  | `placeholder` | `THC` | - | Empty-tile content (e.g. an `Avatar` with initials) |
939
939
  | `noPreview`, `noDownload` | `boolean` | `false` | Hide the Preview action / the lightbox's Download |
940
+ | `noFilename` | `boolean` | `false` | Never show the asset's name (hash filenames); name-less wording instead |
940
941
  | `onDownload` | `(asset) => void \| Promise<void>` | - | Replaces the lightbox's default download (auth-gated bytes) |
941
942
  | `onChange` | `(asset \| null) => void` | - | After every user-driven change of `value` |
942
943
  | `parseValue`, `serializeValue` | see Value | JSON | Custom `value` shape |
@@ -21,12 +21,15 @@ export const FIELD_SINGLE_ASSET_MESSAGES_SK = {
21
21
  field_req_att: "Toto pole vyžaduje pozornosť. Skontrolujte ho a skúste to znova.",
22
22
  pick_file: "Vybrať súbor",
23
23
  replace_file: "Nahradiť {{name}}",
24
+ replace_file_short: "Nahradiť súbor",
24
25
  empty_hint: "Presuňte sem súbor alebo kliknite a vyberte ho",
25
26
  remove: "Odstrániť",
26
27
  removing_short: "Odstraňuje sa…",
27
28
  removed: "Súbor {{name}} bol odstránený",
29
+ removed_short: "Súbor bol odstránený",
28
30
  undo: "Vrátiť späť",
29
31
  restored: "Súbor {{name}} bol obnovený",
32
+ restored_short: "Súbor bol obnovený",
30
33
  preview: "Náhľad",
31
34
  retry: "Skúsiť nahrať znova",
32
35
  discard: "Zahodiť",
@@ -35,8 +38,10 @@ export const FIELD_SINGLE_ASSET_MESSAGES_SK = {
35
38
  uploading_short: "Nahráva sa…",
36
39
  uploading_progress: "Nahráva sa… {{percent}} %",
37
40
  uploaded: "Súbor {{name}} bol nahraný",
41
+ uploaded_short: "Súbor bol nahraný",
38
42
  upload_failed: "Nahrávanie zlyhalo",
39
43
  upload_failed_named: "Nahrávanie súboru {{name}} zlyhalo: {{error}}",
44
+ upload_failed_error: "Nahrávanie zlyhalo: {{error}}",
40
45
  invalid_type: "Tento typ súboru nie je podporovaný. Povolené: „{{accept}}“.",
41
46
  too_large: "Súbor je príliš veľký ({{size}}). Maximum je {{max}}.",
42
47
  single_only: "Sem je možné umiestniť iba jeden súbor. Presuňte alebo prilepte jeden súbor.",
@@ -10,12 +10,15 @@ export declare const FIELD_SINGLE_ASSET_MESSAGES_EN: {
10
10
  field_req_att: string;
11
11
  pick_file: string;
12
12
  replace_file: string;
13
+ replace_file_short: string;
13
14
  empty_hint: string;
14
15
  remove: string;
15
16
  removing_short: string;
16
17
  removed: string;
18
+ removed_short: string;
17
19
  undo: string;
18
20
  restored: string;
21
+ restored_short: string;
19
22
  preview: string;
20
23
  retry: string;
21
24
  discard: string;
@@ -24,8 +27,10 @@ export declare const FIELD_SINGLE_ASSET_MESSAGES_EN: {
24
27
  uploading_short: string;
25
28
  uploading_progress: string;
26
29
  uploaded: string;
30
+ uploaded_short: string;
27
31
  upload_failed: string;
28
32
  upload_failed_named: string;
33
+ upload_failed_error: string;
29
34
  invalid_type: string;
30
35
  too_large: string;
31
36
  single_only: string;
@@ -11,12 +11,15 @@ export const FIELD_SINGLE_ASSET_MESSAGES_EN = {
11
11
  field_req_att: "This field requires attention. Please review and try again.",
12
12
  pick_file: "Choose a file",
13
13
  replace_file: "Replace {{name}}",
14
+ replace_file_short: "Replace file",
14
15
  empty_hint: "Drop a file here or click to browse",
15
16
  remove: "Remove",
16
17
  removing_short: "Removing…",
17
18
  removed: "{{name}} removed",
19
+ removed_short: "File removed",
18
20
  undo: "Undo",
19
21
  restored: "{{name}} restored",
22
+ restored_short: "File restored",
20
23
  preview: "Preview",
21
24
  retry: "Retry upload",
22
25
  discard: "Discard",
@@ -25,8 +28,10 @@ export const FIELD_SINGLE_ASSET_MESSAGES_EN = {
25
28
  uploading_short: "Uploading…",
26
29
  uploading_progress: "Uploading… {{percent}}%",
27
30
  uploaded: "{{name}} uploaded",
31
+ uploaded_short: "File uploaded",
28
32
  upload_failed: "Upload failed",
29
33
  upload_failed_named: "Upload of {{name}} failed: {{error}}",
34
+ upload_failed_error: "Upload failed: {{error}}",
30
35
  invalid_type: 'This file type is not supported. Allowed: "{{accept}}".',
31
36
  too_large: "The file is too large ({{size}}). The maximum is {{max}}.",
32
37
  single_only: "Only one file can be placed here. Drop or paste a single file.",
@@ -1515,6 +1515,18 @@ interface HeaderActionItem {
1515
1515
  }
1516
1516
  ```
1517
1517
 
1518
+ ### HeaderLocaleItem
1519
+
1520
+ ```ts
1521
+ interface HeaderLocaleItem {
1522
+ id: string;
1523
+ label: THC;
1524
+ /** Compact label for the inline trigger in COLLAPSED mode only
1525
+ * (falls back to `label`); the dropdown list always shows `label`. */
1526
+ shortLabel?: THC;
1527
+ }
1528
+ ```
1529
+
1518
1530
  ### Key Props
1519
1531
 
1520
1532
  | Prop | Type | Default | Description |
@@ -1540,7 +1552,7 @@ interface HeaderActionItem {
1540
1552
  | `contentMaxWidth` | `string \| number` | — | Inner content row max-width (outer header stays 100%). |
1541
1553
  | `collapseThreshold` | `number` | `768` | Width (px) to collapse; 0 disables. |
1542
1554
  | `collapseMode` | `"hamburger" \| "hide"` | `"hamburger"` | Collapse behavior. See top of section. |
1543
- | `keepLocaleOnCollapse` | `boolean` | `false` | Keep locale switcher visible when collapsed (only `collapseMode === "hide"`). |
1555
+ | `keepLocaleOnCollapse` | `boolean` | `true` | Keep the locale switcher inline when collapsed (both modes). See below. |
1544
1556
  | `fixed` | `boolean` | `false` | Fixed positioning at top. |
1545
1557
  | `isCollapsed` | `boolean` | — | Bindable: collapsed state. |
1546
1558
  | `isMenuOpen` | `boolean` | — | Bindable: hamburger menu open. |
@@ -1575,11 +1587,24 @@ Common pattern for app interfaces: the leading hamburger opens a side drawer for
1575
1587
 
1576
1588
  See [Header/README.md](../../src/lib/components/Header/README.md) for the breakdown of which markup branch handles each requirement of this pattern.
1577
1589
 
1590
+ ### The locale switcher never folds into the hamburger
1591
+
1592
+ By default (`keepLocaleOnCollapse`) the locale switcher stays **inline** when the header collapses, in both collapse modes — it sits just before the actions/avatar/hamburger instead of becoming the last section of the dropdown.
1593
+
1594
+ This is the one control `Header` deliberately treats differently from nav items. The user who most needs the language switch is the one who landed in a language they cannot read, and for them a visible `EN ▾` trigger is self-describing while a hamburger entry is not: the trigger is an unlabeled icon, the section heading says "Language" in a language they do not speak, and with more than a few nav items it sits below the fold of a menu they must scroll.
1595
+
1596
+ Consequences worth knowing:
1597
+
1598
+ - The dropdown never carries a duplicate locale section while the inline trigger is visible.
1599
+ - If the locales were the only thing the trailing dropdown would have held, no hamburger renders at all.
1600
+ - `HeaderLocaleItem.shortLabel` keeps the inline trigger narrow on phones (`"Slovenčina"` → `"SK"`) without shortening the dropdown entries.
1601
+ - Set `keepLocaleOnCollapse={false}` when the end area is already crowded with actions, or when the app's own drawer owns the language switch.
1602
+
1578
1603
  ### CSS Tokens
1579
1604
 
1580
1605
  Prefix: `--stuic-header-*`
1581
1606
 
1582
- `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`
1607
+ `padding-x`, `padding-y`, `gap`, `end-gap-collapsed`, `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`
1583
1608
 
1584
1609
  ---
1585
1610
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.184.0",
3
+ "version": "3.187.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",