@marianmeres/stuic 3.99.0 → 3.101.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.
@@ -6,6 +6,17 @@
6
6
  export type ButtonVariant = "solid" | "outline" | "ghost" | "soft" | "link";
7
7
  export type ButtonSize = "sm" | "md" | "lg" | "xl";
8
8
 
9
+ export type ButtonNavDirection = "prev" | "next";
10
+
11
+ export interface ButtonNavProps {
12
+ /** Which direction this button represents */
13
+ direction: ButtonNavDirection;
14
+ /** Override the default arrow. Pass any iconLucide* SVG string or Snippet. */
15
+ icon?: string | Snippet;
16
+ /** Extra classes merged onto the rendered icon wrapper */
17
+ class?: string;
18
+ }
19
+
9
20
  export interface Props extends Omit<HTMLButtonAttributes, "children"> {
10
21
  /** Color intent (semantic meaning) */
11
22
  intent?: IntentColorKey;
@@ -51,6 +62,13 @@
51
62
  * For that look, use: `<Button x unstyled class="stuic-close-button" />`
52
63
  */
53
64
  x?: boolean | XProps;
65
+ /**
66
+ * Render as a normalized prev/next navigation icon button. Implies iconButton
67
+ * (square, fully-rounded). Default icon is an arrow in the correct direction;
68
+ * pass the object form with `icon` to override (e.g. chevron). If both `x` and
69
+ * `nav` are set, `x` takes precedence.
70
+ */
71
+ nav?: ButtonNavDirection | ButtonNavProps;
54
72
  /** Two icon states for swap animation (implies iconButton). Uses `checked` for active state. */
55
73
  iconSwap?: [string | Snippet, string | Snippet];
56
74
  /** Optional out-of-the-box spinner support */
@@ -67,6 +85,10 @@
67
85
  import Thc, { type THC } from "../Thc/Thc.svelte";
68
86
  import Spinner from "../Spinner/Spinner.svelte";
69
87
  import { IconSwap } from "../IconSwap/index.js";
88
+ import {
89
+ iconArrowLeft as iconPrev,
90
+ iconArrowRight as iconNext,
91
+ } from "../../icons/index.js";
70
92
  let {
71
93
  class: classProp,
72
94
  intent,
@@ -86,6 +108,7 @@
86
108
  iconButton = false,
87
109
  tooltip: _tooltip,
88
110
  x,
111
+ nav,
89
112
  iconSwap,
90
113
  spinner,
91
114
  spinnerOnly,
@@ -120,8 +143,13 @@
120
143
  }
121
144
  });
122
145
 
123
- // "x" and "iconSwap" are semantically icon buttons
124
- let _isIconButton = $derived(iconButton || !!_xProps || !!iconSwap);
146
+ let _navProps: undefined | ButtonNavProps = $derived.by(() => {
147
+ if (!nav) return;
148
+ return typeof nav === "string" ? { direction: nav } : { ...nav };
149
+ });
150
+
151
+ // "x", "nav" and "iconSwap" are semantically icon buttons
152
+ let _isIconButton = $derived(iconButton || !!_xProps || !!_navProps || !!iconSwap);
125
153
 
126
154
  // icon buttons implicitly set aspect1
127
155
  let _isAspect1 = $derived(aspect1 || _isIconButton);
@@ -143,11 +171,22 @@
143
171
  data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
144
172
  data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
145
173
  data-x={!unstyled && !!_xProps ? "true" : undefined}
174
+ data-nav={!unstyled && _navProps ? _navProps.direction : undefined}
146
175
  use:tooltip={_tooltipConfig}
147
176
  {...rest as HTMLAnchorAttributes}
148
177
  >
149
178
  {#if _xProps}
150
179
  <X {..._xProps} />
180
+ {:else if _navProps}
181
+ {#if typeof _navProps.icon === "string"}
182
+ <span class={_navProps.class}>{@html _navProps.icon}</span>
183
+ {:else if _navProps.icon}
184
+ {@render _navProps.icon()}
185
+ {:else if _navProps.direction === "prev"}
186
+ <span class={_navProps.class}>{@html iconPrev({ size: 24 })}</span>
187
+ {:else}
188
+ <span class={_navProps.class}>{@html iconNext({ size: 24 })}</span>
189
+ {/if}
151
190
  {:else if iconSwap}
152
191
  <IconSwap states={iconSwap} active={checked ? 1 : 0} />
153
192
  {:else}
@@ -177,11 +216,22 @@
177
216
  data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
178
217
  data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
179
218
  data-x={!unstyled && !!_xProps ? "true" : undefined}
219
+ data-nav={!unstyled && _navProps ? _navProps.direction : undefined}
180
220
  use:tooltip={_tooltipConfig}
181
221
  {...rest}
182
222
  >
183
223
  {#if _xProps}
184
224
  <X {..._xProps} />
225
+ {:else if _navProps}
226
+ {#if typeof _navProps.icon === "string"}
227
+ <span class={_navProps.class}>{@html _navProps.icon}</span>
228
+ {:else if _navProps.icon}
229
+ {@render _navProps.icon()}
230
+ {:else if _navProps.direction === "prev"}
231
+ <span class={_navProps.class}>{@html iconPrev({ size: 24 })}</span>
232
+ {:else}
233
+ <span class={_navProps.class}>{@html iconNext({ size: 24 })}</span>
234
+ {/if}
185
235
  {:else if iconSwap}
186
236
  <IconSwap states={iconSwap} active={checked ? 1 : 0} />
187
237
  {:else}
@@ -3,6 +3,15 @@ import type { Snippet } from "svelte";
3
3
  import type { IntentColorKey } from "../../utils/design-tokens.js";
4
4
  export type ButtonVariant = "solid" | "outline" | "ghost" | "soft" | "link";
5
5
  export type ButtonSize = "sm" | "md" | "lg" | "xl";
6
+ export type ButtonNavDirection = "prev" | "next";
7
+ export interface ButtonNavProps {
8
+ /** Which direction this button represents */
9
+ direction: ButtonNavDirection;
10
+ /** Override the default arrow. Pass any iconLucide* SVG string or Snippet. */
11
+ icon?: string | Snippet;
12
+ /** Extra classes merged onto the rendered icon wrapper */
13
+ class?: string;
14
+ }
6
15
  export interface Props extends Omit<HTMLButtonAttributes, "children"> {
7
16
  /** Color intent (semantic meaning) */
8
17
  intent?: IntentColorKey;
@@ -50,6 +59,13 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
50
59
  * For that look, use: `<Button x unstyled class="stuic-close-button" />`
51
60
  */
52
61
  x?: boolean | XProps;
62
+ /**
63
+ * Render as a normalized prev/next navigation icon button. Implies iconButton
64
+ * (square, fully-rounded). Default icon is an arrow in the correct direction;
65
+ * pass the object form with `icon` to override (e.g. chevron). If both `x` and
66
+ * `nav` are set, `x` takes precedence.
67
+ */
68
+ nav?: ButtonNavDirection | ButtonNavProps;
53
69
  /** Two icon states for swap animation (implies iconButton). Uses `checked` for active state. */
54
70
  iconSwap?: [string | Snippet, string | Snippet];
55
71
  /** Optional out-of-the-box spinner support */
@@ -18,6 +18,8 @@ A flexible button component with semantic intents, visual variants, sizes, and o
18
18
  | `el` | `HTMLElement` | - | Element reference (bindable) |
19
19
  | `iconButton` | `boolean` | `false` | Icon-only button (implies aspect1, adds CSS hook) |
20
20
  | `iconSwap` | `[string \| Snippet, string \| Snippet]` | - | Two icon states with swap animation (implies iconButton) |
21
+ | `x` | `boolean \| XProps` | - | Normalized "X" icon button shortcut (close/dismiss) |
22
+ | `nav` | `"prev" \| "next" \| ButtonNavProps` | - | Normalized prev/next icon button shortcut (arrow by default; `x` wins on conflict) |
21
23
  | `class` | `string` | - | Additional CSS classes |
22
24
 
23
25
  ## Snippet Props
@@ -97,6 +99,33 @@ The `children` snippet receives `{ checked }` when `roleSwitch` is enabled.
97
99
  <Button iconSwap={[iconPlus(), iconMinus()]} roleSwitch bind:checked roundedFull />
98
100
  ```
99
101
 
102
+ ### Nav (prev / next)
103
+
104
+ Shortcut for a normalized prev/next navigation icon button (square, fully
105
+ rounded, arrow icon). Mirrors the `x` shortcut pattern — single `nav`
106
+ prop carries the direction so contradictory props are impossible.
107
+
108
+ ```svelte
109
+ <!-- Shorthand: default arrow in the correct direction -->
110
+ <Button nav="prev" />
111
+ <Button nav="next" />
112
+
113
+ <!-- Cascades intent / variant / size like `x` -->
114
+ <Button nav="prev" intent="primary" size="sm" />
115
+ <Button nav="next" variant="ghost" />
116
+
117
+ <!-- Override the default arrow (e.g. chevron) via object form -->
118
+ <Button nav={{ direction: "prev", icon: iconChevronLeft() }} />
119
+ <Button nav={{ direction: "next", icon: iconChevronRight({ size: 18 }) }} />
120
+ ```
121
+
122
+ Notes:
123
+
124
+ - `nav` implies `iconButton` (square + fully-rounded).
125
+ - `ghost + nav + roundedFull` auto-applies the same neutral overlay hover
126
+ used by the rounded X — handy for carousels/lightboxes on imagery.
127
+ - If both `x` and `nav` are set on the same Button, `x` takes precedence.
128
+
100
129
  Global CSS targeting for all icon buttons:
101
130
 
102
131
  ```css
@@ -170,5 +199,7 @@ The component uses data attributes for styling:
170
199
  - `data-raised` - Present when raised
171
200
  - `data-checked` - Present when roleSwitch is enabled and checked
172
201
  - `data-rounded-full` - Present when roundedFull
173
- - `data-aspect1` - Present when aspect1 (or iconButton, or x)
174
- - `data-icon-button` - Present when iconButton (or x)
202
+ - `data-aspect1` - Present when aspect1 (or iconButton, or x, or nav)
203
+ - `data-icon-button` - Present when iconButton (or x, or nav)
204
+ - `data-x` - Present when x is set
205
+ - `data-nav` - Set to `"prev"` or `"next"` when nav is set
@@ -434,4 +434,14 @@
434
434
  --_bg-hover: var(--stuic-button-x-bg-hover);
435
435
  --_bg-active: var(--stuic-button-x-bg-hover);
436
436
  }
437
+
438
+ /* ============================================================================
439
+ PURE ROUNDED NAV (prev/next)
440
+ Same treatment as the rounded X — useful for carousel/lightbox overlays
441
+ where the prev/next button sits on imagery.
442
+ ============================================================================ */
443
+ .stuic-button[data-variant="ghost"][data-nav][data-rounded-full] {
444
+ --_bg-hover: var(--stuic-button-x-bg-hover);
445
+ --_bg-active: var(--stuic-button-x-bg-hover);
446
+ }
437
447
  }
@@ -1 +1 @@
1
- export { default as Button, type Props as ButtonProps, type ButtonVariant, type ButtonSize, } from "./Button.svelte";
1
+ export { default as Button, type Props as ButtonProps, type ButtonVariant, type ButtonSize, type ButtonNavDirection, type ButtonNavProps, } from "./Button.svelte";
@@ -119,6 +119,15 @@
119
119
 
120
120
  /** Storage key prefix for localStorage (default: 'stuic-nav') */
121
121
  storageKeyPrefix?: string;
122
+
123
+ /** When true, the section title becomes a button that collapses/expands all groups beneath it. Default: false (non-interactive, today's behavior). */
124
+ collapsibleTitle?: boolean;
125
+
126
+ /** Initial expanded state when `collapsibleTitle` is true. Default: true. Overridden by localStorage when `persistState` is on. */
127
+ defaultTitleExpanded?: boolean;
128
+
129
+ /** Callback when section title is toggled. */
130
+ onTitleToggle?: (isExpanded: boolean) => void;
122
131
  }
123
132
 
124
133
  export const NAV_BASE_CLASSES = "stuic-nav";
@@ -169,11 +178,16 @@
169
178
  el = $bindable(),
170
179
  persistState = true,
171
180
  storageKeyPrefix = "stuic-nav",
181
+ collapsibleTitle = false,
182
+ defaultTitleExpanded = true,
183
+ onTitleToggle,
172
184
  ...rest
173
185
  }: Props = $props();
174
186
 
175
187
  // Unique IDs for accessibility
176
188
  const navId = getId("nav-");
189
+ const sectionTitleId = `${navId}-section-title`;
190
+ const sectionContentId = `${navId}-section-content`;
177
191
 
178
192
  // Device detection for touch-friendly sizing
179
193
  const devicePointer = new DevicePointer();
@@ -198,6 +212,10 @@
198
212
  return `${storageKeyPrefix}-item-${itemId}`;
199
213
  }
200
214
 
215
+ function getSectionStorageKey(): string {
216
+ return `${storageKeyPrefix}-section`;
217
+ }
218
+
201
219
  function loadGroupState(groupId: string): boolean | undefined {
202
220
  if (!persistState) return undefined;
203
221
  return localStorageValue<boolean | undefined>(
@@ -224,6 +242,19 @@
224
242
  localStorageValue(getItemStorageKey(itemId), expanded).set(expanded);
225
243
  }
226
244
 
245
+ function loadSectionState(): boolean | undefined {
246
+ if (!persistState) return undefined;
247
+ return localStorageValue<boolean | undefined>(
248
+ getSectionStorageKey(),
249
+ undefined
250
+ ).get();
251
+ }
252
+
253
+ function saveSectionState(expanded: boolean): void {
254
+ if (!persistState) return;
255
+ localStorageValue(getSectionStorageKey(), expanded).set(expanded);
256
+ }
257
+
227
258
  // Check if an item matches active state (used during initialization)
228
259
  function checkItemActive(item: NavItem): boolean {
229
260
  if (isActive) return isActive(item);
@@ -270,6 +301,26 @@
270
301
  // Initialize state synchronously from props (not via effect)
271
302
  let groupExpandedStates = $state<boolean[]>(computeGroupStates());
272
303
 
304
+ // Compute initial section expanded state synchronously
305
+ function computeSectionState(): boolean {
306
+ // First priority: localStorage (if persistence enabled)
307
+ if (persistState) {
308
+ const stored = loadSectionState();
309
+ if (stored != null) return stored;
310
+ }
311
+ // Default: use defaultTitleExpanded prop (defaults to true)
312
+ return defaultTitleExpanded;
313
+ }
314
+
315
+ let sectionExpanded = $state<boolean>(computeSectionState());
316
+
317
+ function toggleSectionTitle() {
318
+ const newState = !sectionExpanded;
319
+ sectionExpanded = newState;
320
+ saveSectionState(newState);
321
+ onTitleToggle?.(newState);
322
+ }
323
+
273
324
  // Re-sync if groups array length changes
274
325
  $effect.pre(() => {
275
326
  if (groupExpandedStates.length !== groups.length) {
@@ -469,304 +520,337 @@
469
520
  data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
470
521
  {...rest}
471
522
  >
472
- <!-- Section title (optional, non-interactive) -->
523
+ <!-- Section title (optional). Non-interactive by default; opt-in collapsible via `collapsibleTitle`. -->
473
524
  {#if title}
474
- <span
475
- class={twMerge(!unstyled && NAV_SECTION_TITLE_CLASSES, "uppercase", classTitle)}
476
- data-collapsed={!unstyled && isCollapsed ? "" : undefined}
477
- >
478
- {resolveLabel(title)}
479
- </span>
525
+ {#if collapsibleTitle}
526
+ <button
527
+ type="button"
528
+ id={sectionTitleId}
529
+ class={twMerge(!unstyled && NAV_SECTION_TITLE_CLASSES, "uppercase", classTitle)}
530
+ data-collapsed={!unstyled && isCollapsed ? "" : undefined}
531
+ data-interactive=""
532
+ data-expanded={!unstyled && sectionExpanded ? "" : undefined}
533
+ onclick={toggleSectionTitle}
534
+ aria-expanded={sectionExpanded}
535
+ aria-controls={sectionContentId}
536
+ >
537
+ <span
538
+ class={twMerge(
539
+ "inline-block shrink-0 transition-transform duration-150",
540
+ sectionExpanded && "rotate-90",
541
+ classChevron
542
+ )}
543
+ >
544
+ {@html iconChevronRight({ size: 12 })}
545
+ </span>
546
+ <span>{resolveLabel(title)}</span>
547
+ </button>
548
+ {:else}
549
+ <span
550
+ class={twMerge(!unstyled && NAV_SECTION_TITLE_CLASSES, "uppercase", classTitle)}
551
+ data-collapsed={!unstyled && isCollapsed ? "" : undefined}
552
+ >
553
+ {resolveLabel(title)}
554
+ </span>
555
+ {/if}
480
556
  {/if}
481
557
 
482
- <!-- Render each group -->
483
- {#each groups as group, groupIndex}
484
- {@const hasItems = groupHasItems(group)}
485
- {@const expanded = isGroupExpanded(groupIndex)}
486
- {@const groupActive = !hasItems && isGroupItemActive(group)}
487
-
488
- {#if hasItems}
489
- <!-- Group with items: show expandable header -->
490
- {#if !isCollapsed}
491
- <button
492
- type="button"
493
- id={groupElId(groupIndex)}
494
- class={twMerge(!unstyled && NAV_GROUP_TITLE_CLASSES, classGroupTitle)}
495
- onclick={() => toggleGroup(groupIndex)}
496
- aria-expanded={expanded}
497
- data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
498
- >
499
- <span
500
- class={twMerge(
501
- "inline-block shrink-0 transition-transform duration-150",
502
- expanded && "rotate-90",
503
- classChevron
504
- )}
505
- >
506
- {@html iconChevronRight({ size: isTouchFriendly ? 18 : 16 })}
507
- </span>
508
- {#if group.icon}
509
- <span class={twMerge("shrink-0", classIcon)}>
510
- <Thc thc={group.icon} />
511
- </span>
558
+ {#if !collapsibleTitle || sectionExpanded}
559
+ <div
560
+ id={sectionContentId}
561
+ class="flex flex-col gap-(--stuic-nav-children-gap)"
562
+ transition:slide={{ duration: transitionDuration }}
563
+ >
564
+ <!-- Render each group -->
565
+ {#each groups as group, groupIndex}
566
+ {@const hasItems = groupHasItems(group)}
567
+ {@const expanded = isGroupExpanded(groupIndex)}
568
+ {@const groupActive = !hasItems && isGroupItemActive(group)}
569
+
570
+ {#if hasItems}
571
+ <!-- Group with items: show expandable header -->
572
+ {#if !isCollapsed}
573
+ <button
574
+ type="button"
575
+ id={groupElId(groupIndex)}
576
+ class={twMerge(!unstyled && NAV_GROUP_TITLE_CLASSES, classGroupTitle)}
577
+ onclick={() => toggleGroup(groupIndex)}
578
+ aria-expanded={expanded}
579
+ data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
580
+ >
581
+ <span
582
+ class={twMerge(
583
+ "inline-block shrink-0 transition-transform duration-150",
584
+ expanded && "rotate-90",
585
+ classChevron
586
+ )}
587
+ >
588
+ {@html iconChevronRight({ size: isTouchFriendly ? 18 : 16 })}
589
+ </span>
590
+ {#if group.icon}
591
+ <span class={twMerge("shrink-0", classIcon)}>
592
+ <Thc thc={group.icon} />
593
+ </span>
594
+ {/if}
595
+ <span class={twMerge(classLabel)}>{resolveLabel(group.title)}</span>
596
+ </button>
597
+ {:else}
598
+ <!-- Collapsed mode: show only chevron -->
599
+ <button
600
+ type="button"
601
+ class={twMerge(!unstyled && NAV_ITEM_CLASSES, classItemCollapsed)}
602
+ onclick={() => toggleGroup(groupIndex)}
603
+ data-collapsed=""
604
+ data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
605
+ use:tooltip={() => ({
606
+ enabled: isCollapsed,
607
+ content: resolveLabel(group.title),
608
+ position: "right",
609
+ })}
610
+ >
611
+ <span
612
+ class={twMerge(
613
+ "inline-block shrink-0 transition-transform duration-150",
614
+ expanded && "rotate-90",
615
+ classChevron
616
+ )}
617
+ >
618
+ {@html iconChevronRight({ size: 16 })}
619
+ </span>
620
+ </button>
512
621
  {/if}
513
- <span class={twMerge(classLabel)}>{resolveLabel(group.title)}</span>
514
- </button>
515
- {:else}
516
- <!-- Collapsed mode: show only chevron -->
517
- <button
518
- type="button"
519
- class={twMerge(!unstyled && NAV_ITEM_CLASSES, classItemCollapsed)}
520
- onclick={() => toggleGroup(groupIndex)}
521
- data-collapsed=""
522
- data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
523
- use:tooltip={() => ({
524
- enabled: isCollapsed,
525
- content: resolveLabel(group.title),
526
- position: "right",
527
- })}
528
- >
529
- <span
530
- class={twMerge(
531
- "inline-block shrink-0 transition-transform duration-150",
532
- expanded && "rotate-90",
533
- classChevron
534
- )}
535
- >
536
- {@html iconChevronRight({ size: 16 })}
537
- </span>
538
- </button>
539
- {/if}
540
-
541
- <!-- Items -->
542
- {#if expanded}
543
- <ul
544
- class={twMerge(!unstyled && NAV_CHILDREN_CLASSES, classChildren)}
545
- aria-labelledby={groupElId(groupIndex)}
546
- transition:slide={{ duration: transitionDuration }}
547
- >
548
- {#snippet renderItem(item: NavItem, depth: number)}
549
- {@const hasChildren = itemHasChildren(item)}
550
- {@const itemExpanded = hasChildren && isItemExpanded(item.id)}
551
- {@const active = isItemActive(item)}
552
- {@const label = resolveLabel(item.label)}
553
- <li>
554
- {#if hasChildren}
555
- <!-- Parent with children: render as toggle button -->
556
- <button
557
- type="button"
558
- id={itemElId(groupIndex, item.id)}
559
- class={twMerge(
560
- !unstyled && NAV_ITEM_CLASSES,
561
- isCollapsed && classItemCollapsed,
562
- active && classItemActive,
563
- item.disabled && classItemDisabled,
564
- item.class,
565
- classItem
566
- )}
567
- onclick={() => toggleItem(item.id)}
568
- disabled={item.disabled}
569
- data-active={!unstyled && active ? "" : undefined}
570
- data-collapsed={!unstyled && isCollapsed ? "" : undefined}
571
- data-has-children=""
572
- data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
573
- aria-expanded={itemExpanded}
574
- use:tooltip={() => ({
575
- enabled: isCollapsed,
576
- content: label,
577
- position: "right",
578
- })}
579
- >
580
- <!-- Chevron indicator -->
581
- <span
582
- class={twMerge(
583
- "inline-block shrink-0 transition-transform duration-150",
584
- itemExpanded && "rotate-90",
585
- classChevron
586
- )}
587
- >
588
- {@html iconChevronRight({ size: isTouchFriendly ? 18 : 16 })}
589
- </span>
590
- {#if item.icon && !isCollapsed}
591
- <span class={twMerge("shrink-0", classIcon)}>
592
- <Thc thc={item.icon} />
593
- </span>
594
- {/if}
595
- {#if !isCollapsed}
596
- <span class={classLabel}>{label}</span>
597
- {/if}
598
- </button>
599
-
600
- <!-- Children (only shown when expanded) -->
601
- {#if itemExpanded}
602
- <ul
603
- class={twMerge(!unstyled && NAV_CHILDREN_CLASSES, classChildren)}
604
- transition:slide={{ duration: transitionDuration }}
605
- >
606
- {#each item.children ?? [] as child (child.id)}
607
- {@render renderItem(child, depth + 1)}
608
- {/each}
609
- </ul>
610
- {/if}
611
- {:else if item.href}
612
- <!-- Leaf item with href -->
613
- <a
614
- id={itemElId(groupIndex, item.id)}
615
- href={item.href}
616
- class={twMerge(
617
- !unstyled && NAV_ITEM_CLASSES,
618
- isCollapsed && classItemCollapsed,
619
- active && classItemActive,
620
- item.disabled && classItemDisabled,
621
- item.class,
622
- classItem
623
- )}
624
- onclick={() => handleItemSelect(item)}
625
- data-active={!unstyled && active ? "" : undefined}
626
- data-collapsed={!unstyled && isCollapsed ? "" : undefined}
627
- data-expanding={!unstyled && isExpanding ? "" : undefined}
628
- data-disabled={!unstyled && item.disabled ? "" : undefined}
629
- data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
630
- aria-current={active ? "page" : undefined}
631
- aria-disabled={item.disabled}
632
- tabindex={item.disabled ? -1 : 0}
633
- use:tooltip={() => ({
634
- enabled: isCollapsed,
635
- content: label,
636
- position: "right",
637
- })}
638
- >
639
- {#if item.icon}
640
- <span class={twMerge("shrink-0", classIcon)}>
641
- <Thc thc={item.icon} />
642
- </span>
643
- {:else if isCollapsed}
644
- <span class={twMerge("shrink-0 font-medium", classIcon)}
645
- >{getFirstLetter(label)}</span
622
+
623
+ <!-- Items -->
624
+ {#if expanded}
625
+ <ul
626
+ class={twMerge(!unstyled && NAV_CHILDREN_CLASSES, classChildren)}
627
+ aria-labelledby={groupElId(groupIndex)}
628
+ transition:slide={{ duration: transitionDuration }}
629
+ >
630
+ {#snippet renderItem(item: NavItem, depth: number)}
631
+ {@const hasChildren = itemHasChildren(item)}
632
+ {@const itemExpanded = hasChildren && isItemExpanded(item.id)}
633
+ {@const active = isItemActive(item)}
634
+ {@const label = resolveLabel(item.label)}
635
+ <li>
636
+ {#if hasChildren}
637
+ <!-- Parent with children: render as toggle button -->
638
+ <button
639
+ type="button"
640
+ id={itemElId(groupIndex, item.id)}
641
+ class={twMerge(
642
+ !unstyled && NAV_ITEM_CLASSES,
643
+ isCollapsed && classItemCollapsed,
644
+ active && classItemActive,
645
+ item.disabled && classItemDisabled,
646
+ item.class,
647
+ classItem
648
+ )}
649
+ onclick={() => toggleItem(item.id)}
650
+ disabled={item.disabled}
651
+ data-active={!unstyled && active ? "" : undefined}
652
+ data-collapsed={!unstyled && isCollapsed ? "" : undefined}
653
+ data-has-children=""
654
+ data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
655
+ aria-expanded={itemExpanded}
656
+ use:tooltip={() => ({
657
+ enabled: isCollapsed,
658
+ content: label,
659
+ position: "right",
660
+ })}
646
661
  >
647
- {/if}
648
- {#if !isCollapsed}
649
- <span class={classLabel}>{label}</span>
650
- {/if}
651
- </a>
652
- {:else}
653
- <!-- Leaf item with onClick only -->
654
- <button
655
- type="button"
656
- id={itemElId(groupIndex, item.id)}
657
- class={twMerge(
658
- !unstyled && NAV_ITEM_CLASSES,
659
- isCollapsed && classItemCollapsed,
660
- active && classItemActive,
661
- item.disabled && classItemDisabled,
662
- item.class,
663
- classItem
664
- )}
665
- onclick={() => handleItemSelect(item)}
666
- disabled={item.disabled}
667
- data-active={!unstyled && active ? "" : undefined}
668
- data-collapsed={!unstyled && isCollapsed ? "" : undefined}
669
- data-expanding={!unstyled && isExpanding ? "" : undefined}
670
- data-disabled={!unstyled && item.disabled ? "" : undefined}
671
- data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
672
- use:tooltip={() => ({
673
- enabled: isCollapsed,
674
- content: label,
675
- position: "right",
676
- })}
677
- >
678
- {#if item.icon}
679
- <span class={twMerge("shrink-0", classIcon)}>
680
- <Thc thc={item.icon} />
681
- </span>
682
- {:else if isCollapsed}
683
- <span class={twMerge("shrink-0 font-medium", classIcon)}
684
- >{getFirstLetter(label)}</span
662
+ <!-- Chevron indicator -->
663
+ <span
664
+ class={twMerge(
665
+ "inline-block shrink-0 transition-transform duration-150",
666
+ itemExpanded && "rotate-90",
667
+ classChevron
668
+ )}
669
+ >
670
+ {@html iconChevronRight({ size: isTouchFriendly ? 18 : 16 })}
671
+ </span>
672
+ {#if item.icon && !isCollapsed}
673
+ <span class={twMerge("shrink-0", classIcon)}>
674
+ <Thc thc={item.icon} />
675
+ </span>
676
+ {/if}
677
+ {#if !isCollapsed}
678
+ <span class={classLabel}>{label}</span>
679
+ {/if}
680
+ </button>
681
+
682
+ <!-- Children (only shown when expanded) -->
683
+ {#if itemExpanded}
684
+ <ul
685
+ class={twMerge(!unstyled && NAV_CHILDREN_CLASSES, classChildren)}
686
+ transition:slide={{ duration: transitionDuration }}
687
+ >
688
+ {#each item.children ?? [] as child (child.id)}
689
+ {@render renderItem(child, depth + 1)}
690
+ {/each}
691
+ </ul>
692
+ {/if}
693
+ {:else if item.href}
694
+ <!-- Leaf item with href -->
695
+ <a
696
+ id={itemElId(groupIndex, item.id)}
697
+ href={item.href}
698
+ class={twMerge(
699
+ !unstyled && NAV_ITEM_CLASSES,
700
+ isCollapsed && classItemCollapsed,
701
+ active && classItemActive,
702
+ item.disabled && classItemDisabled,
703
+ item.class,
704
+ classItem
705
+ )}
706
+ onclick={() => handleItemSelect(item)}
707
+ data-active={!unstyled && active ? "" : undefined}
708
+ data-collapsed={!unstyled && isCollapsed ? "" : undefined}
709
+ data-expanding={!unstyled && isExpanding ? "" : undefined}
710
+ data-disabled={!unstyled && item.disabled ? "" : undefined}
711
+ data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
712
+ aria-current={active ? "page" : undefined}
713
+ aria-disabled={item.disabled}
714
+ tabindex={item.disabled ? -1 : 0}
715
+ use:tooltip={() => ({
716
+ enabled: isCollapsed,
717
+ content: label,
718
+ position: "right",
719
+ })}
685
720
  >
721
+ {#if item.icon}
722
+ <span class={twMerge("shrink-0", classIcon)}>
723
+ <Thc thc={item.icon} />
724
+ </span>
725
+ {:else if isCollapsed}
726
+ <span class={twMerge("shrink-0 font-medium", classIcon)}
727
+ >{getFirstLetter(label)}</span
728
+ >
729
+ {/if}
730
+ {#if !isCollapsed}
731
+ <span class={classLabel}>{label}</span>
732
+ {/if}
733
+ </a>
734
+ {:else}
735
+ <!-- Leaf item with onClick only -->
736
+ <button
737
+ type="button"
738
+ id={itemElId(groupIndex, item.id)}
739
+ class={twMerge(
740
+ !unstyled && NAV_ITEM_CLASSES,
741
+ isCollapsed && classItemCollapsed,
742
+ active && classItemActive,
743
+ item.disabled && classItemDisabled,
744
+ item.class,
745
+ classItem
746
+ )}
747
+ onclick={() => handleItemSelect(item)}
748
+ disabled={item.disabled}
749
+ data-active={!unstyled && active ? "" : undefined}
750
+ data-collapsed={!unstyled && isCollapsed ? "" : undefined}
751
+ data-expanding={!unstyled && isExpanding ? "" : undefined}
752
+ data-disabled={!unstyled && item.disabled ? "" : undefined}
753
+ data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
754
+ use:tooltip={() => ({
755
+ enabled: isCollapsed,
756
+ content: label,
757
+ position: "right",
758
+ })}
759
+ >
760
+ {#if item.icon}
761
+ <span class={twMerge("shrink-0", classIcon)}>
762
+ <Thc thc={item.icon} />
763
+ </span>
764
+ {:else if isCollapsed}
765
+ <span class={twMerge("shrink-0 font-medium", classIcon)}
766
+ >{getFirstLetter(label)}</span
767
+ >
768
+ {/if}
769
+ {#if !isCollapsed}
770
+ <span class={classLabel}>{label}</span>
771
+ {/if}
772
+ </button>
686
773
  {/if}
687
- {#if !isCollapsed}
688
- <span class={classLabel}>{label}</span>
689
- {/if}
690
- </button>
691
- {/if}
692
- </li>
693
- {/snippet}
694
-
695
- {#each group.items ?? [] as item (item.id)}
696
- {@render renderItem(item, 0)}
697
- {/each}
698
- </ul>
699
- {/if}
700
- {:else}
701
- <!-- Group without items: render as a simple nav item (no chevron) -->
702
- {@const label = resolveLabel(group.title)}
703
- {#if group.href}
704
- <a
705
- href={group.href}
706
- class={twMerge(
707
- !unstyled && NAV_ITEM_CLASSES,
708
- isCollapsed && classItemCollapsed,
709
- groupActive && classItemActive,
710
- classItem
711
- )}
712
- onclick={() => handleGroupSelect(group)}
713
- data-active={!unstyled && groupActive ? "" : undefined}
714
- data-collapsed={!unstyled && isCollapsed ? "" : undefined}
715
- data-expanding={!unstyled && isExpanding ? "" : undefined}
716
- data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
717
- use:tooltip={() => ({
718
- enabled: isCollapsed,
719
- content: label,
720
- position: "right",
721
- })}
722
- >
723
- {#if group.icon}
724
- <span class={twMerge("shrink-0", classIcon)}>
725
- <Thc thc={group.icon} />
726
- </span>
727
- {:else if isCollapsed}
728
- <span class={twMerge("shrink-0 font-medium", classIcon)}
729
- >{getFirstLetter(label)}</span
730
- >
731
- {/if}
732
- {#if !isCollapsed}
733
- <span class={classLabel}>{label}</span>
774
+ </li>
775
+ {/snippet}
776
+
777
+ {#each group.items ?? [] as item (item.id)}
778
+ {@render renderItem(item, 0)}
779
+ {/each}
780
+ </ul>
734
781
  {/if}
735
- </a>
736
- {:else}
737
- <button
738
- type="button"
739
- class={twMerge(
740
- !unstyled && NAV_ITEM_CLASSES,
741
- isCollapsed && classItemCollapsed,
742
- groupActive && classItemActive,
743
- classItem
744
- )}
745
- onclick={() => handleGroupSelect(group)}
746
- data-active={!unstyled && groupActive ? "" : undefined}
747
- data-collapsed={!unstyled && isCollapsed ? "" : undefined}
748
- data-expanding={!unstyled && isExpanding ? "" : undefined}
749
- data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
750
- use:tooltip={() => ({
751
- enabled: isCollapsed,
752
- content: label,
753
- position: "right",
754
- })}
755
- >
756
- {#if group.icon}
757
- <span class={twMerge("shrink-0", classIcon)}>
758
- <Thc thc={group.icon} />
759
- </span>
760
- {:else if isCollapsed}
761
- <span class={twMerge("shrink-0 font-medium", classIcon)}
762
- >{getFirstLetter(label)}</span
782
+ {:else}
783
+ <!-- Group without items: render as a simple nav item (no chevron) -->
784
+ {@const label = resolveLabel(group.title)}
785
+ {#if group.href}
786
+ <a
787
+ href={group.href}
788
+ class={twMerge(
789
+ !unstyled && NAV_ITEM_CLASSES,
790
+ isCollapsed && classItemCollapsed,
791
+ groupActive && classItemActive,
792
+ classItem
793
+ )}
794
+ onclick={() => handleGroupSelect(group)}
795
+ data-active={!unstyled && groupActive ? "" : undefined}
796
+ data-collapsed={!unstyled && isCollapsed ? "" : undefined}
797
+ data-expanding={!unstyled && isExpanding ? "" : undefined}
798
+ data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
799
+ use:tooltip={() => ({
800
+ enabled: isCollapsed,
801
+ content: label,
802
+ position: "right",
803
+ })}
763
804
  >
805
+ {#if group.icon}
806
+ <span class={twMerge("shrink-0", classIcon)}>
807
+ <Thc thc={group.icon} />
808
+ </span>
809
+ {:else if isCollapsed}
810
+ <span class={twMerge("shrink-0 font-medium", classIcon)}
811
+ >{getFirstLetter(label)}</span
812
+ >
813
+ {/if}
814
+ {#if !isCollapsed}
815
+ <span class={classLabel}>{label}</span>
816
+ {/if}
817
+ </a>
818
+ {:else}
819
+ <button
820
+ type="button"
821
+ class={twMerge(
822
+ !unstyled && NAV_ITEM_CLASSES,
823
+ isCollapsed && classItemCollapsed,
824
+ groupActive && classItemActive,
825
+ classItem
826
+ )}
827
+ onclick={() => handleGroupSelect(group)}
828
+ data-active={!unstyled && groupActive ? "" : undefined}
829
+ data-collapsed={!unstyled && isCollapsed ? "" : undefined}
830
+ data-expanding={!unstyled && isExpanding ? "" : undefined}
831
+ data-touch-friendly={!unstyled && isTouchFriendly ? "" : undefined}
832
+ use:tooltip={() => ({
833
+ enabled: isCollapsed,
834
+ content: label,
835
+ position: "right",
836
+ })}
837
+ >
838
+ {#if group.icon}
839
+ <span class={twMerge("shrink-0", classIcon)}>
840
+ <Thc thc={group.icon} />
841
+ </span>
842
+ {:else if isCollapsed}
843
+ <span class={twMerge("shrink-0 font-medium", classIcon)}
844
+ >{getFirstLetter(label)}</span
845
+ >
846
+ {/if}
847
+ {#if !isCollapsed}
848
+ <span class={classLabel}>{label}</span>
849
+ {/if}
850
+ </button>
764
851
  {/if}
765
- {#if !isCollapsed}
766
- <span class={classLabel}>{label}</span>
767
- {/if}
768
- </button>
769
- {/if}
770
- {/if}
771
- {/each}
852
+ {/if}
853
+ {/each}
854
+ </div>
855
+ {/if}
772
856
  </nav>
@@ -99,6 +99,12 @@ export interface Props extends Omit<HTMLAttributes<HTMLElement>, "children" | "t
99
99
  persistState?: boolean;
100
100
  /** Storage key prefix for localStorage (default: 'stuic-nav') */
101
101
  storageKeyPrefix?: string;
102
+ /** When true, the section title becomes a button that collapses/expands all groups beneath it. Default: false (non-interactive, today's behavior). */
103
+ collapsibleTitle?: boolean;
104
+ /** Initial expanded state when `collapsibleTitle` is true. Default: true. Overridden by localStorage when `persistState` is on. */
105
+ defaultTitleExpanded?: boolean;
106
+ /** Callback when section title is toggled. */
107
+ onTitleToggle?: (isExpanded: boolean) => void;
102
108
  }
103
109
  export declare const NAV_BASE_CLASSES = "stuic-nav";
104
110
  export declare const NAV_SECTION_TITLE_CLASSES = "stuic-nav-section-title";
@@ -4,35 +4,38 @@ A navigation component for sidebars with support for groups, nested items, expan
4
4
 
5
5
  ## Props
6
6
 
7
- | Prop | Type | Default | Description |
8
- | -------------------- | --------------------------------------------------- | ------------- | --------------------------------------------------------- |
9
- | `groups` | `NavGroup[]` | - | Navigation groups to render |
10
- | `title` | `MaybeLocalized` | - | Section title above groups (uppercase, non-interactive) |
11
- | `locale` | `string` | - | Current locale for localized labels |
12
- | `isCollapsed` | `boolean` | `false` | Collapsed mode (icon-only) |
13
- | `isExpanding` | `boolean` | `false` | Transitioning from collapsed to expanded |
14
- | `activeId` | `string` | - | Active item ID for highlighting |
15
- | `isActive` | `(item: NavItem) => boolean` | - | Custom active check callback |
16
- | `isGroupActive` | `(group: NavGroup) => boolean` | - | Custom group active check callback |
17
- | `onSelect` | `(item: NavItem) => void` | - | Item selection callback |
18
- | `onGroupSelect` | `(group: NavGroup) => void` | - | Group selection callback (groups without items) |
19
- | `onGroupToggle` | `(groupIndex: number, isExpanded: boolean) => void` | - | Group expand/collapse callback |
20
- | `touchFriendly` | `boolean \| "auto"` | `false` | Touch-friendly sizing mode |
21
- | `persistState` | `boolean` | `true` | Enable localStorage persistence for expand/collapse state |
22
- | `storageKeyPrefix` | `string` | `"stuic-nav"` | Storage key prefix for localStorage |
23
- | `class` | `string` | - | Classes for wrapper element |
24
- | `classTitle` | `string` | - | Classes for section title |
25
- | `classGroupTitle` | `string` | - | Classes for group title/header |
26
- | `classItem` | `string` | - | Classes for individual items |
27
- | `classItemActive` | `string` | - | Classes for active items |
28
- | `classItemCollapsed` | `string` | - | Classes for collapsed mode items |
29
- | `classItemDisabled` | `string` | - | Classes for disabled items |
30
- | `classIcon` | `string` | - | Classes for icons |
31
- | `classLabel` | `string` | - | Classes for labels |
32
- | `classChildren` | `string` | - | Classes for children container |
33
- | `classChevron` | `string` | - | Classes for chevron icon |
34
- | `unstyled` | `boolean` | `false` | Skip all default styling |
35
- | `el` | `HTMLElement` | - | Element reference (bindable) |
7
+ | Prop | Type | Default | Description |
8
+ | ---------------------- | --------------------------------------------------- | ------------- | --------------------------------------------------------- |
9
+ | `groups` | `NavGroup[]` | - | Navigation groups to render |
10
+ | `title` | `MaybeLocalized` | - | Section title above groups (uppercase, non-interactive) |
11
+ | `locale` | `string` | - | Current locale for localized labels |
12
+ | `isCollapsed` | `boolean` | `false` | Collapsed mode (icon-only) |
13
+ | `isExpanding` | `boolean` | `false` | Transitioning from collapsed to expanded |
14
+ | `activeId` | `string` | - | Active item ID for highlighting |
15
+ | `isActive` | `(item: NavItem) => boolean` | - | Custom active check callback |
16
+ | `isGroupActive` | `(group: NavGroup) => boolean` | - | Custom group active check callback |
17
+ | `onSelect` | `(item: NavItem) => void` | - | Item selection callback |
18
+ | `onGroupSelect` | `(group: NavGroup) => void` | - | Group selection callback (groups without items) |
19
+ | `onGroupToggle` | `(groupIndex: number, isExpanded: boolean) => void` | - | Group expand/collapse callback |
20
+ | `touchFriendly` | `boolean \| "auto"` | `false` | Touch-friendly sizing mode |
21
+ | `persistState` | `boolean` | `true` | Enable localStorage persistence for expand/collapse state |
22
+ | `storageKeyPrefix` | `string` | `"stuic-nav"` | Storage key prefix for localStorage |
23
+ | `collapsibleTitle` | `boolean` | `false` | Make the section title clickable to collapse all groups |
24
+ | `defaultTitleExpanded` | `boolean` | `true` | Initial expanded state when `collapsibleTitle` is true |
25
+ | `onTitleToggle` | `(isExpanded: boolean) => void` | - | Callback when the section title is toggled |
26
+ | `class` | `string` | - | Classes for wrapper element |
27
+ | `classTitle` | `string` | - | Classes for section title |
28
+ | `classGroupTitle` | `string` | - | Classes for group title/header |
29
+ | `classItem` | `string` | - | Classes for individual items |
30
+ | `classItemActive` | `string` | - | Classes for active items |
31
+ | `classItemCollapsed` | `string` | - | Classes for collapsed mode items |
32
+ | `classItemDisabled` | `string` | - | Classes for disabled items |
33
+ | `classIcon` | `string` | - | Classes for icons |
34
+ | `classLabel` | `string` | - | Classes for labels |
35
+ | `classChildren` | `string` | - | Classes for children container |
36
+ | `classChevron` | `string` | - | Classes for chevron icon |
37
+ | `unstyled` | `boolean` | `false` | Skip all default styling |
38
+ | `el` | `HTMLElement` | - | Element reference (bindable) |
36
39
 
37
40
  ## Interfaces
38
41
 
@@ -249,6 +252,28 @@ Storage keys follow the pattern:
249
252
 
250
253
  - Groups: `{prefix}-group-{groupId}`
251
254
  - Items: `{prefix}-item-{itemId}`
255
+ - Section title (when `collapsibleTitle`): `{prefix}-section`
256
+
257
+ ### Collapsible Section Title
258
+
259
+ Opt in with `collapsibleTitle` to make the section title clickable. Clicking it expands or collapses all groups beneath it as a unit. The expanded state is persisted (under `{storageKeyPrefix}-section`) unless `persistState={false}`.
260
+
261
+ ```svelte
262
+ <Nav title="Main" {groups} collapsibleTitle />
263
+
264
+ <!-- Start collapsed -->
265
+ <Nav title="Main" {groups} collapsibleTitle defaultTitleExpanded={false} />
266
+
267
+ <!-- React to toggle -->
268
+ <Nav
269
+ title="Main"
270
+ {groups}
271
+ collapsibleTitle
272
+ onTitleToggle={(expanded) => console.log("section expanded:", expanded)}
273
+ />
274
+ ```
275
+
276
+ Note: in icon-only sidebar mode (`isCollapsed`), the section title is visually hidden but the saved collapsed state is still respected — if a user collapsed the section in expanded-sidebar mode, its groups will stay hidden in icon-only mode too.
252
277
 
253
278
  ### Custom Active Check
254
279
 
@@ -326,11 +351,13 @@ The component applies these base classes (when `unstyled` is false):
326
351
 
327
352
  ### Data Attributes
328
353
 
329
- | Attribute | Applied When |
330
- | --------------------- | ---------------------------------------- |
331
- | `data-collapsed` | Sidebar is in collapsed mode |
332
- | `data-expanding` | Transitioning from collapsed to expanded |
333
- | `data-active` | Item/group is currently active |
334
- | `data-touch-friendly` | Touch-friendly mode is active |
335
- | `data-has-children` | Item has nested children |
336
- | `data-disabled` | Item is disabled |
354
+ | Attribute | Applied When |
355
+ | --------------------- | ----------------------------------------------------- |
356
+ | `data-collapsed` | Sidebar is in collapsed mode |
357
+ | `data-expanding` | Transitioning from collapsed to expanded |
358
+ | `data-active` | Item/group is currently active |
359
+ | `data-touch-friendly` | Touch-friendly mode is active |
360
+ | `data-has-children` | Item has nested children |
361
+ | `data-disabled` | Item is disabled |
362
+ | `data-interactive` | Section title is clickable (`collapsibleTitle` is on) |
363
+ | `data-expanded` | Section title's content is currently expanded |
@@ -117,6 +117,36 @@
117
117
  visibility: hidden;
118
118
  }
119
119
 
120
+ /* Interactive (collapsible) variant: opt-in via `collapsibleTitle` prop */
121
+ .stuic-nav-section-title[data-interactive] {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: var(--stuic-nav-gap);
125
+ background: transparent;
126
+ border: none;
127
+ cursor: pointer;
128
+ text-align: left;
129
+ -webkit-tap-highlight-color: transparent;
130
+ border-radius: var(--stuic-nav-radius, var(--stuic-radius));
131
+ }
132
+
133
+ .stuic-nav-section-title[data-interactive]:hover {
134
+ background: var(--stuic-nav-item-bg-hover);
135
+ }
136
+
137
+ .stuic-nav-section-title[data-interactive]:focus {
138
+ outline: none;
139
+ }
140
+
141
+ .stuic-nav-section-title[data-interactive]:focus-visible {
142
+ background: var(--stuic-nav-item-bg-hover);
143
+ }
144
+
145
+ /* Chevron opacity to match group chevrons */
146
+ .stuic-nav-section-title[data-interactive] > span:first-child {
147
+ opacity: var(--stuic-nav-chevron-opacity);
148
+ }
149
+
120
150
  /* =============================================================================
121
151
  GROUP TITLE/HEADER (collapsible)
122
152
  ============================================================================= */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.99.0",
3
+ "version": "3.101.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",
@@ -81,7 +81,7 @@
81
81
  "dependencies": {
82
82
  "@marianmeres/clog": "^3.21.0",
83
83
  "@marianmeres/cron": "^2.0.1",
84
- "@marianmeres/design-tokens": "^1.6.0",
84
+ "@marianmeres/design-tokens": "^1.7.0",
85
85
  "@marianmeres/icons-fns": "^5.0.0",
86
86
  "@marianmeres/item-collection": "^1.4.2",
87
87
  "@marianmeres/paging-store": "^2.1.1",