@marianmeres/stuic 3.115.0 → 3.116.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.
Files changed (86) hide show
  1. package/API.md +297 -304
  2. package/dist/actions/dim-behind/index.css +4 -1
  3. package/dist/actions/focus-trap.js +3 -1
  4. package/dist/components/Accordion/README.md +17 -17
  5. package/dist/components/Accordion/index.css +4 -2
  6. package/dist/components/AssetsPreview/README.md +7 -7
  7. package/dist/components/AssetsPreview/_internal/assets-preview-types.d.ts +1 -2
  8. package/dist/components/AssetsPreview/_internal/assets-preview-utils.d.ts +1 -1
  9. package/dist/components/AssetsPreview/_internal/assets-preview-utils.js +9 -3
  10. package/dist/components/Avatar/Avatar.svelte +1 -3
  11. package/dist/components/Avatar/README.md +33 -27
  12. package/dist/components/Book/Book.svelte +6 -1
  13. package/dist/components/Book/README.md +22 -20
  14. package/dist/components/Book/index.css +4 -2
  15. package/dist/components/Button/README.md +17 -17
  16. package/dist/components/Card/Card.svelte +25 -8
  17. package/dist/components/Card/README.md +52 -56
  18. package/dist/components/Card/index.css +2 -1
  19. package/dist/components/Carousel/Carousel.svelte +1 -3
  20. package/dist/components/Carousel/README.md +28 -28
  21. package/dist/components/Cart/Cart.svelte +2 -1
  22. package/dist/components/Cart/README.md +25 -25
  23. package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +8 -3
  24. package/dist/components/Checkout/CheckoutShippingStep.svelte +1 -2
  25. package/dist/components/Checkout/README.md +143 -130
  26. package/dist/components/CronInput/CronInput.svelte +64 -60
  27. package/dist/components/CronInput/README.md +46 -46
  28. package/dist/components/DataTable/DataTable.svelte +5 -1
  29. package/dist/components/DataTable/README.md +78 -63
  30. package/dist/components/DropdownMenu/DropdownMenu.svelte +6 -2
  31. package/dist/components/DropdownMenu/README.md +33 -27
  32. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +2 -9
  33. package/dist/components/EmailVerifyForm/README.md +30 -30
  34. package/dist/components/Header/Header.svelte +161 -165
  35. package/dist/components/Header/README.md +7 -7
  36. package/dist/components/IconSwap/README.md +20 -15
  37. package/dist/components/IconSwap/index.css +2 -1
  38. package/dist/components/ImageCycler/ImageCycler.svelte +19 -5
  39. package/dist/components/ImageCycler/ImageCycler.svelte.d.ts +14 -10
  40. package/dist/components/ImageCycler/README.md +15 -15
  41. package/dist/components/ImageCycler/index.css +26 -20
  42. package/dist/components/Input/FieldFile.svelte +1 -3
  43. package/dist/components/Input/FieldInput.svelte +1 -3
  44. package/dist/components/Input/FieldKeyValues.svelte +2 -6
  45. package/dist/components/Input/FieldObject.svelte +2 -1
  46. package/dist/components/Input/README.md +11 -11
  47. package/dist/components/KbdShortcut/index.css +2 -1
  48. package/dist/components/LoginForm/LoginForm.svelte +1 -7
  49. package/dist/components/LoginForm/README.md +46 -46
  50. package/dist/components/ModalDialog/index.css +2 -1
  51. package/dist/components/Notifications/index.css +24 -6
  52. package/dist/components/OtpInput/OtpInput.svelte +0 -0
  53. package/dist/components/OtpInput/README.md +15 -19
  54. package/dist/components/OtpInput/index.css +1 -4
  55. package/dist/components/OtpInput/index.d.ts +1 -1
  56. package/dist/components/OtpInput/index.js +1 -1
  57. package/dist/components/Pill/README.md +41 -40
  58. package/dist/components/Pill/index.css +3 -6
  59. package/dist/components/PricingTable/README.md +86 -86
  60. package/dist/components/PricingTable/index.css +20 -35
  61. package/dist/components/RegisterForm/README.md +60 -60
  62. package/dist/components/RegisterForm/RegisterForm.svelte +1 -7
  63. package/dist/components/Separator/README.md +7 -7
  64. package/dist/components/TabbedMenu/index.css +6 -3
  65. package/dist/components/Tree/README.md +67 -67
  66. package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +1 -5
  67. package/dist/components/WithSidePanel/index.css +4 -4
  68. package/dist/index.css +12 -8
  69. package/dist/utils/design-tokens.d.ts +1 -1
  70. package/dist/utils/design-tokens.js +1 -1
  71. package/docs/architecture.md +7 -7
  72. package/docs/component-testing/00-overview-and-roadmap.md +19 -19
  73. package/docs/component-testing/01-framework-setup.md +6 -6
  74. package/docs/component-testing/02-test-conventions.md +6 -5
  75. package/docs/component-testing/03-component-coverage-roadmap.md +27 -27
  76. package/docs/component-testing/04-hard-cases-and-e2e.md +8 -8
  77. package/docs/component-testing/05-ci.md +3 -3
  78. package/docs/component-testing/PROGRESS.md +30 -26
  79. package/docs/component-testing/README.md +8 -8
  80. package/docs/conventions.md +25 -25
  81. package/docs/domains/components.md +386 -385
  82. package/docs/domains/theming.md +24 -24
  83. package/docs/domains/utils.md +22 -25
  84. package/docs/testing.md +2 -2
  85. package/docs/upgrading.md +32 -28
  86. package/package.json +2 -1
@@ -4,32 +4,32 @@ A feature-rich dropdown menu component with CSS Anchor Positioning (with fallbac
4
4
 
5
5
  ## Props
6
6
 
7
- | Prop | Type | Default | Description |
8
- | ------------------------ | ---------------------- | -------------------- | ---------------------------------------- |
9
- | `items` | `DropdownMenuItem[]` | - | Menu items to display |
10
- | `isOpen` | `boolean` | `false` | Controlled open state (bindable) |
11
- | `position` | `DropdownMenuPosition` | `"bottom-span-right"` | Popover position relative to trigger |
12
- | `offset` | `string` | `"0.25rem"` | Offset from trigger element (CSS value) |
13
- | `maxHeight` | `string` | `"300px"` | Max height of dropdown |
14
- | `closeOnSelect` | `boolean` | `true` | Close menu when action item is selected |
15
- | `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
16
- | `closeOnEscape` | `boolean` | `true` | Close on Escape key |
17
- | `forceFallback` | `boolean` | `false` | Force fallback positioning (for testing) |
18
- | `search` | `boolean \| DropdownMenuSearchConfig` | - | Enable search/filter input (see [Search](#search)) |
19
- | `unstyled` | `boolean` | `false` | Opt out of stuic base classes |
20
- | `class` | `string` | - | Classes for wrapper element |
21
- | `classTrigger` | `string` | - | Classes for trigger button |
22
- | `classDropdown` | `string` | - | Classes for dropdown container |
23
- | `classItem` | `string` | - | Classes for action items |
24
- | `classItemActive` | `string` | - | Classes for active/focused item |
25
- | `classItemDisabled` | `string` | - | Classes for disabled items |
26
- | `classDivider` | `string` | - | Classes for dividers |
27
- | `classHeader` | `string` | - | Classes for header items |
28
- | `classExpandable` | `string` | - | Classes for expandable section header |
29
- | `classExpandableContent` | `string` | - | Classes for expandable section content |
30
- | `el` | `HTMLDivElement` | - | Wrapper element reference (bindable) |
31
- | `triggerEl` | `HTMLButtonElement` | - | Trigger element reference (bindable) |
32
- | `dropdownEl` | `HTMLDivElement` | - | Dropdown element reference (bindable) |
7
+ | Prop | Type | Default | Description |
8
+ | ------------------------ | ------------------------------------- | --------------------- | -------------------------------------------------- |
9
+ | `items` | `DropdownMenuItem[]` | - | Menu items to display |
10
+ | `isOpen` | `boolean` | `false` | Controlled open state (bindable) |
11
+ | `position` | `DropdownMenuPosition` | `"bottom-span-right"` | Popover position relative to trigger |
12
+ | `offset` | `string` | `"0.25rem"` | Offset from trigger element (CSS value) |
13
+ | `maxHeight` | `string` | `"300px"` | Max height of dropdown |
14
+ | `closeOnSelect` | `boolean` | `true` | Close menu when action item is selected |
15
+ | `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
16
+ | `closeOnEscape` | `boolean` | `true` | Close on Escape key |
17
+ | `forceFallback` | `boolean` | `false` | Force fallback positioning (for testing) |
18
+ | `search` | `boolean \| DropdownMenuSearchConfig` | - | Enable search/filter input (see [Search](#search)) |
19
+ | `unstyled` | `boolean` | `false` | Opt out of stuic base classes |
20
+ | `class` | `string` | - | Classes for wrapper element |
21
+ | `classTrigger` | `string` | - | Classes for trigger button |
22
+ | `classDropdown` | `string` | - | Classes for dropdown container |
23
+ | `classItem` | `string` | - | Classes for action items |
24
+ | `classItemActive` | `string` | - | Classes for active/focused item |
25
+ | `classItemDisabled` | `string` | - | Classes for disabled items |
26
+ | `classDivider` | `string` | - | Classes for dividers |
27
+ | `classHeader` | `string` | - | Classes for header items |
28
+ | `classExpandable` | `string` | - | Classes for expandable section header |
29
+ | `classExpandableContent` | `string` | - | Classes for expandable section content |
30
+ | `el` | `HTMLDivElement` | - | Wrapper element reference (bindable) |
31
+ | `triggerEl` | `HTMLButtonElement` | - | Trigger element reference (bindable) |
32
+ | `dropdownEl` | `HTMLDivElement` | - | Dropdown element reference (bindable) |
33
33
 
34
34
  ## Snippets
35
35
 
@@ -199,7 +199,13 @@ interface DropdownMenuSearchConfig {
199
199
  <DropdownMenu
200
200
  items={[
201
201
  { type: "action", id: "docs", label: "Documentation", href: "/docs" },
202
- { type: "action", id: "github", label: "GitHub", href: "https://github.com/...", target: "_blank" },
202
+ {
203
+ type: "action",
204
+ id: "github",
205
+ label: "GitHub",
206
+ href: "https://github.com/...",
207
+ target: "_blank",
208
+ },
203
209
  { type: "divider" },
204
210
  { type: "action", id: "logout", label: "Logout", onSelect: () => handleLogout() },
205
211
  ]}
@@ -265,15 +265,8 @@
265
265
  {@render submitButton({ isSubmitting, disabled: submitDisabled })}
266
266
  {:else}
267
267
  <div class={unstyled ? undefined : "stuic-email-verify-form-submit"}>
268
- <Button
269
- intent="primary"
270
- type="submit"
271
- disabled={submitDisabled}
272
- class="w-full"
273
- >
274
- {isSubmitting
275
- ? t("email_verify_form.submitting")
276
- : t("email_verify_form.submit")}
268
+ <Button intent="primary" type="submit" disabled={submitDisabled} class="w-full">
269
+ {isSubmitting ? t("email_verify_form.submitting") : t("email_verify_form.submit")}
277
270
  </Button>
278
271
  </div>
279
272
  {/if}
@@ -36,39 +36,39 @@ The form owns:
36
36
 
37
37
  ## Props
38
38
 
39
- | Prop | Type | Default | Description |
40
- | ----------------------- | ------------------------------------------ | -------- | -------------------------------------------------------- |
41
- | `email` | `string` | required | Email address shown in the subhead |
42
- | `onSubmit` | `(code: string) => void` | required | Called with the entered code (auto on complete + manual) |
43
- | `onResend` | `() => Promise<void> \| void` | — | When set, renders a resend control |
44
- | `resendCooldownSeconds` | `number` | `30` | Cooldown after a successful resend |
45
- | `isSubmitting` | `boolean` | `false` | Disables submit + OtpInput |
46
- | `error` | `string` | — | General error (renders alert + applies error styling) |
47
- | `attemptsRemaining` | `number` | — | Inline hint, e.g. "3 attempts remaining" |
48
- | `codeLength` | `number` | `6` | Forwarded to OtpInput |
49
- | `otpInputProps` | `Partial<OtpInputProps>` | — | Pass-through props for the inner OtpInput |
50
- | `notifications` | `NotificationsStack` | — | Route errors to notification system |
51
- | `submitButton` | `Snippet` | — | Override submit section |
52
- | `footer` | `Snippet` | — | Content below the resend control |
53
- | `heading` | `string \| false` | — | Override heading text, or `false` to suppress entirely |
54
- | `t` | `TranslateFn` | built-in | Translation function |
55
- | `unstyled` | `boolean` | `false` | Skip default styling |
56
- | `class` | `string` | — | Additional CSS classes on the form root |
57
- | `el` | `HTMLFormElement` | — | Bindable form element |
39
+ | Prop | Type | Default | Description |
40
+ | ----------------------- | ----------------------------- | -------- | -------------------------------------------------------- |
41
+ | `email` | `string` | required | Email address shown in the subhead |
42
+ | `onSubmit` | `(code: string) => void` | required | Called with the entered code (auto on complete + manual) |
43
+ | `onResend` | `() => Promise<void> \| void` | — | When set, renders a resend control |
44
+ | `resendCooldownSeconds` | `number` | `30` | Cooldown after a successful resend |
45
+ | `isSubmitting` | `boolean` | `false` | Disables submit + OtpInput |
46
+ | `error` | `string` | — | General error (renders alert + applies error styling) |
47
+ | `attemptsRemaining` | `number` | — | Inline hint, e.g. "3 attempts remaining" |
48
+ | `codeLength` | `number` | `6` | Forwarded to OtpInput |
49
+ | `otpInputProps` | `Partial<OtpInputProps>` | — | Pass-through props for the inner OtpInput |
50
+ | `notifications` | `NotificationsStack` | — | Route errors to notification system |
51
+ | `submitButton` | `Snippet` | — | Override submit section |
52
+ | `footer` | `Snippet` | — | Content below the resend control |
53
+ | `heading` | `string \| false` | — | Override heading text, or `false` to suppress entirely |
54
+ | `t` | `TranslateFn` | built-in | Translation function |
55
+ | `unstyled` | `boolean` | `false` | Skip default styling |
56
+ | `class` | `string` | — | Additional CSS classes on the form root |
57
+ | `el` | `HTMLFormElement` | — | Bindable form element |
58
58
 
59
59
  ## i18n keys
60
60
 
61
- | Key | Default |
62
- | ----------------------------------------- | ------------------------------------------ |
63
- | `email_verify_form.heading` | `Check your email` |
64
- | `email_verify_form.subheading` | `We sent a 6-digit code to {email}` |
65
- | `email_verify_form.submit` | `Verify` |
66
- | `email_verify_form.submitting` | `Verifying...` |
67
- | `email_verify_form.resend_prompt` | `Didn't receive it?` |
68
- | `email_verify_form.resend` | `Resend code` |
69
- | `email_verify_form.resend_cooldown` | `Resend available in {seconds}s` |
70
- | `email_verify_form.resent` | `New code sent` |
71
- | `email_verify_form.attempts_remaining` | `{count} attempts remaining` |
61
+ | Key | Default |
62
+ | -------------------------------------- | ----------------------------------- |
63
+ | `email_verify_form.heading` | `Check your email` |
64
+ | `email_verify_form.subheading` | `We sent a 6-digit code to {email}` |
65
+ | `email_verify_form.submit` | `Verify` |
66
+ | `email_verify_form.submitting` | `Verifying...` |
67
+ | `email_verify_form.resend_prompt` | `Didn't receive it?` |
68
+ | `email_verify_form.resend` | `Resend code` |
69
+ | `email_verify_form.resend_cooldown` | `Resend available in {seconds}s` |
70
+ | `email_verify_form.resent` | `New code sent` |
71
+ | `email_verify_form.attempts_remaining` | `{count} attempts remaining` |
72
72
 
73
73
  ## CSS Tokens
74
74
 
@@ -331,8 +331,7 @@
331
331
  // trailing dropdown when collapsed). In "hide" mode: visible when not
332
332
  // collapsed, or when collapsed and `keepLocaleOnCollapse` is set.
333
333
  let _showLocaleSwitcher = $derived(
334
- _hasLocales &&
335
- (!_isCollapsed || (collapseMode === "hide" && keepLocaleOnCollapse))
334
+ _hasLocales && (!_isCollapsed || (collapseMode === "hide" && keepLocaleOnCollapse))
336
335
  );
337
336
 
338
337
  // Active locale object (for trigger label); fallback to first
@@ -473,199 +472,196 @@
473
472
  })}
474
473
  {:else}
475
474
  <div bind:offsetWidth={_innerWidth} class={_classContent} style={_styleContent}>
476
- <!-- Leading slot (left-side) -->
477
- {#if leading}
478
- <div class={_classLeading}>
479
- {@render leading({ isCollapsed: _isCollapsed })}
480
- </div>
481
- {:else if _showLeadingHamburger}
482
- <div class={_classLeading}>
483
- <Button
484
- variant="ghost"
485
- iconButton
486
- size="sm"
487
- {unstyled}
488
- class={twMerge(
489
- !unstyled && HEADER_LEADING_HAMBURGER_CLASSES,
490
- classLeadingHamburger
491
- )}
492
- onclick={onLeadingHamburger}
493
- aria-label={leadingHamburgerLabel}
494
- >
495
- {#if leadingHamburgerIcon}
496
- <Thc thc={leadingHamburgerIcon} />
497
- {:else}
498
- {@html iconMenu({ size: iconSize })}
499
- {/if}
500
- </Button>
501
- </div>
502
- {/if}
503
-
504
- <!-- Logo / Title (flex-1) -->
505
- <div class={_classLogo}>
506
- {#if logo}
507
- {@render logo()}
508
- {:else if projectName}
509
- <span class={unstyled ? undefined : "stuic-header-project-name"}>
510
- {projectName}
511
- </span>
512
- {/if}
513
- </div>
514
-
515
- <!-- Nav items (expanded mode) -->
516
- {#if !_isCollapsed && items.length > 0}
517
- <nav class={_classNav}>
518
- {#each items as item (item.id)}
475
+ <!-- Leading slot (left-side) -->
476
+ {#if leading}
477
+ <div class={_classLeading}>
478
+ {@render leading({ isCollapsed: _isCollapsed })}
479
+ </div>
480
+ {:else if _showLeadingHamburger}
481
+ <div class={_classLeading}>
519
482
  <Button
520
- variant={navVariant}
483
+ variant="ghost"
484
+ iconButton
521
485
  size="sm"
522
- href={item.href}
523
- target={item.target}
524
- disabled={item.disabled}
525
486
  {unstyled}
526
487
  class={twMerge(
527
- !unstyled && HEADER_NAV_ITEM_CLASSES,
528
- !unstyled && item.active && classNavItemActive,
529
- classNavItem,
530
- item.class
488
+ !unstyled && HEADER_LEADING_HAMBURGER_CLASSES,
489
+ classLeadingHamburger
531
490
  )}
532
- data-active={!unstyled && item.active ? "" : undefined}
533
- aria-current={item.active ? "page" : undefined}
534
- onclick={() => handleItemClick(item)}
491
+ onclick={onLeadingHamburger}
492
+ aria-label={leadingHamburgerLabel}
535
493
  >
536
- {#if item.icon}
537
- <span class={unstyled ? undefined : "stuic-header-nav-icon"}>
538
- <Thc thc={item.icon} />
539
- </span>
494
+ {#if leadingHamburgerIcon}
495
+ <Thc thc={leadingHamburgerIcon} />
496
+ {:else}
497
+ {@html iconMenu({ size: iconSize })}
540
498
  {/if}
541
- <Thc thc={item.label} />
542
499
  </Button>
543
- {/each}
544
- </nav>
545
- {/if}
546
-
547
- <!-- End area: locale + actions + avatar + trailing hamburger -->
548
- <div class={_classEnd}>
549
- <!-- Locale switcher (shown when expanded, or in "hide" mode with keepLocaleOnCollapse) -->
550
- {#if _showLocaleSwitcher}
551
- <DropdownMenu
552
- items={_localeDropdownItems}
553
- position="bottom-span-right"
554
- class={_classLocale}
555
- >
556
- {#snippet trigger({ isOpen, toggle, triggerProps })}
500
+ </div>
501
+ {/if}
502
+
503
+ <!-- Logo / Title (flex-1) -->
504
+ <div class={_classLogo}>
505
+ {#if logo}
506
+ {@render logo()}
507
+ {:else if projectName}
508
+ <span class={unstyled ? undefined : "stuic-header-project-name"}>
509
+ {projectName}
510
+ </span>
511
+ {/if}
512
+ </div>
513
+
514
+ <!-- Nav items (expanded mode) -->
515
+ {#if !_isCollapsed && items.length > 0}
516
+ <nav class={_classNav}>
517
+ {#each items as item (item.id)}
557
518
  <Button
558
519
  variant={navVariant}
559
520
  size="sm"
521
+ href={item.href}
522
+ target={item.target}
523
+ disabled={item.disabled}
560
524
  {unstyled}
561
525
  class={twMerge(
562
- !unstyled && "stuic-header-locale-trigger",
563
- classLocale
526
+ !unstyled && HEADER_NAV_ITEM_CLASSES,
527
+ !unstyled && item.active && classNavItemActive,
528
+ classNavItem,
529
+ item.class
564
530
  )}
565
- onclick={toggle}
566
- aria-label="Change language"
567
- {...triggerProps}
531
+ data-active={!unstyled && item.active ? "" : undefined}
532
+ aria-current={item.active ? "page" : undefined}
533
+ onclick={() => handleItemClick(item)}
568
534
  >
569
- {#if _activeLocale}
570
- <Thc thc={_activeLocale.label} />
535
+ {#if item.icon}
536
+ <span class={unstyled ? undefined : "stuic-header-nav-icon"}>
537
+ <Thc thc={item.icon} />
538
+ </span>
571
539
  {/if}
572
- <span
573
- class={twMerge(
574
- !unstyled && "stuic-header-locale-chevron",
575
- isOpen && !unstyled && "stuic-header-locale-chevron-open"
576
- )}
577
- >
578
- {@html iconChevronDown({ size: 14 })}
579
- </span>
540
+ <Thc thc={item.label} />
580
541
  </Button>
581
- {/snippet}
582
- </DropdownMenu>
542
+ {/each}
543
+ </nav>
583
544
  {/if}
584
545
 
585
- <!-- Actions (icon buttons, always visible) -->
586
- {#if actions.length > 0}
587
- <div class={_classActions}>
588
- {#each actions as action (action.id)}
589
- {@const actionClass = twMerge(
590
- !unstyled && HEADER_ACTION_CLASSES,
591
- !unstyled && action.active && classActionActive,
592
- classAction,
593
- action.class
594
- )}
595
- {#if action.render}
596
- {@render action.render({
597
- action,
598
- class: actionClass,
599
- isCollapsed: _isCollapsed,
600
- onclick: () => handleActionClick(action),
601
- })}
602
- {:else}
546
+ <!-- End area: locale + actions + avatar + trailing hamburger -->
547
+ <div class={_classEnd}>
548
+ <!-- Locale switcher (shown when expanded, or in "hide" mode with keepLocaleOnCollapse) -->
549
+ {#if _showLocaleSwitcher}
550
+ <DropdownMenu
551
+ items={_localeDropdownItems}
552
+ position="bottom-span-right"
553
+ class={_classLocale}
554
+ >
555
+ {#snippet trigger({ isOpen, toggle, triggerProps })}
603
556
  <Button
604
- variant="ghost"
605
- iconButton
557
+ variant={navVariant}
606
558
  size="sm"
607
- href={action.href}
608
- target={action.target}
609
- disabled={action.disabled}
610
559
  {unstyled}
611
- class={actionClass}
612
- data-active={!unstyled && action.active ? "" : undefined}
613
- aria-label={typeof action.label === "string" ? action.label : undefined}
614
- onclick={() => handleActionClick(action)}
560
+ class={twMerge(!unstyled && "stuic-header-locale-trigger", classLocale)}
561
+ onclick={toggle}
562
+ aria-label="Change language"
563
+ {...triggerProps}
615
564
  >
616
- {#if action.icon !== undefined}
617
- <Thc thc={action.icon} />
565
+ {#if _activeLocale}
566
+ <Thc thc={_activeLocale.label} />
618
567
  {/if}
568
+ <span
569
+ class={twMerge(
570
+ !unstyled && "stuic-header-locale-chevron",
571
+ isOpen && !unstyled && "stuic-header-locale-chevron-open"
572
+ )}
573
+ >
574
+ {@html iconChevronDown({ size: 14 })}
575
+ </span>
619
576
  </Button>
620
- {/if}
621
- {/each}
622
- </div>
623
- {/if}
577
+ {/snippet}
578
+ </DropdownMenu>
579
+ {/if}
624
580
 
625
- <!-- Avatar: hidden when collapsed + avatarOnClick (moves into dropdown) -->
626
- {#if avatar && !(_isCollapsed && _avatarInDropdown)}
627
- {#if avatarOnClick}
628
- <button
629
- type="button"
630
- class={twMerge(!unstyled && "stuic-header-avatar", classAvatar)}
631
- onclick={avatarOnClick}
632
- >
633
- {@render avatar()}
634
- </button>
635
- {:else}
636
- <div class={twMerge(!unstyled && "stuic-header-avatar", classAvatar)}>
637
- {@render avatar()}
581
+ <!-- Actions (icon buttons, always visible) -->
582
+ {#if actions.length > 0}
583
+ <div class={_classActions}>
584
+ {#each actions as action (action.id)}
585
+ {@const actionClass = twMerge(
586
+ !unstyled && HEADER_ACTION_CLASSES,
587
+ !unstyled && action.active && classActionActive,
588
+ classAction,
589
+ action.class
590
+ )}
591
+ {#if action.render}
592
+ {@render action.render({
593
+ action,
594
+ class: actionClass,
595
+ isCollapsed: _isCollapsed,
596
+ onclick: () => handleActionClick(action),
597
+ })}
598
+ {:else}
599
+ <Button
600
+ variant="ghost"
601
+ iconButton
602
+ size="sm"
603
+ href={action.href}
604
+ target={action.target}
605
+ disabled={action.disabled}
606
+ {unstyled}
607
+ class={actionClass}
608
+ data-active={!unstyled && action.active ? "" : undefined}
609
+ aria-label={typeof action.label === "string" ? action.label : undefined}
610
+ onclick={() => handleActionClick(action)}
611
+ >
612
+ {#if action.icon !== undefined}
613
+ <Thc thc={action.icon} />
614
+ {/if}
615
+ </Button>
616
+ {/if}
617
+ {/each}
638
618
  </div>
639
619
  {/if}
640
- {/if}
641
620
 
642
- {#if _isCollapsed && _dropdownItems.length > 0}
643
- <DropdownMenu
644
- items={_dropdownItems}
645
- bind:isOpen={isMenuOpen}
646
- position={dropdownPosition}
647
- class={classDropdown}
648
- >
649
- {#snippet trigger({ isOpen, toggle, triggerProps })}
650
- <Button
651
- variant="ghost"
652
- iconButton
653
- size="sm"
654
- {unstyled}
655
- class={twMerge(!unstyled && HEADER_HAMBURGER_CLASSES, classHamburger)}
656
- onclick={toggle}
657
- aria-label={isOpen ? "Close menu" : "Open menu"}
658
- {...triggerProps}
621
+ <!-- Avatar: hidden when collapsed + avatarOnClick (moves into dropdown) -->
622
+ {#if avatar && !(_isCollapsed && _avatarInDropdown)}
623
+ {#if avatarOnClick}
624
+ <button
625
+ type="button"
626
+ class={twMerge(!unstyled && "stuic-header-avatar", classAvatar)}
627
+ onclick={avatarOnClick}
659
628
  >
660
- <IconSwap
661
- active={isOpen ? 1 : 0}
662
- states={[iconMenu({ size: iconSize }), iconX({ size: iconSize })]}
663
- />
664
- </Button>
665
- {/snippet}
666
- </DropdownMenu>
667
- {/if}
668
- </div>
629
+ {@render avatar()}
630
+ </button>
631
+ {:else}
632
+ <div class={twMerge(!unstyled && "stuic-header-avatar", classAvatar)}>
633
+ {@render avatar()}
634
+ </div>
635
+ {/if}
636
+ {/if}
637
+
638
+ {#if _isCollapsed && _dropdownItems.length > 0}
639
+ <DropdownMenu
640
+ items={_dropdownItems}
641
+ bind:isOpen={isMenuOpen}
642
+ position={dropdownPosition}
643
+ class={classDropdown}
644
+ >
645
+ {#snippet trigger({ isOpen, toggle, triggerProps })}
646
+ <Button
647
+ variant="ghost"
648
+ iconButton
649
+ size="sm"
650
+ {unstyled}
651
+ class={twMerge(!unstyled && HEADER_HAMBURGER_CLASSES, classHamburger)}
652
+ onclick={toggle}
653
+ aria-label={isOpen ? "Close menu" : "Open menu"}
654
+ {...triggerProps}
655
+ >
656
+ <IconSwap
657
+ active={isOpen ? 1 : 0}
658
+ states={[iconMenu({ size: iconSize }), iconX({ size: iconSize })]}
659
+ />
660
+ </Button>
661
+ {/snippet}
662
+ </DropdownMenu>
663
+ {/if}
664
+ </div>
669
665
  </div>
670
666
  {/if}
671
667
  </header>
@@ -8,13 +8,13 @@ Top-bar component with leading slot, project logo, nav items, locale switcher, o
8
8
 
9
9
  Common "app shell" pattern: when the header collapses below `collapseThreshold`, the avatar and a few key actions (search, notifications, cart…) remain visible, the trailing hamburger is NOT shown, and the nav items + locale switcher are hidden entirely (the nav typically lives in a drawer triggered by the leading hamburger instead).
10
10
 
11
- | Requirement | Where it's handled | How |
12
- | --- | --- | --- |
13
- | **Avatar stays visible** | [Header.svelte:626](./Header.svelte#L626) — `{#if avatar && !(_isCollapsed && _avatarInDropdown)}` | `_avatarInDropdown` requires `collapseMode === "hamburger"`. In `"hide"` mode it's always `false`, so the avatar always renders. |
14
- | **Action buttons stay visible** | [Header.svelte:585](./Header.svelte#L585) — `<!-- Actions (icon buttons, always visible) -->` | The actions loop has no collapse gating — items render in both modes. |
15
- | **No trailing hamburger** | [Header.svelte:642](./Header.svelte#L642) — `{#if _isCollapsed && _dropdownItems.length > 0}` | In `"hide"` mode, `_dropdownItems` short-circuits to `[]`, so the `{#if}` is false → no trailing hamburger. |
16
- | **Nav items hidden** | [Header.svelte:516](./Header.svelte#L516) — `{#if !_isCollapsed && items.length > 0}` | Inline nav requires `!_isCollapsed`; combined with the empty `_dropdownItems` above, items don't reappear in a dropdown either. |
17
- | **Locale hidden** | [Header.svelte:335](./Header.svelte#L335) — `!_isCollapsed \|\| (collapseMode === "hide" && keepLocaleOnCollapse)` | Default `keepLocaleOnCollapse={false}` hides the locale switcher in collapsed mode. |
11
+ | Requirement | Where it's handled | How |
12
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
13
+ | **Avatar stays visible** | [Header.svelte:626](./Header.svelte#L626) — `{#if avatar && !(_isCollapsed && _avatarInDropdown)}` | `_avatarInDropdown` requires `collapseMode === "hamburger"`. In `"hide"` mode it's always `false`, so the avatar always renders. |
14
+ | **Action buttons stay visible** | [Header.svelte:585](./Header.svelte#L585) — `<!-- Actions (icon buttons, always visible) -->` | The actions loop has no collapse gating — items render in both modes. |
15
+ | **No trailing hamburger** | [Header.svelte:642](./Header.svelte#L642) — `{#if _isCollapsed && _dropdownItems.length > 0}` | In `"hide"` mode, `_dropdownItems` short-circuits to `[]`, so the `{#if}` is false → no trailing hamburger. |
16
+ | **Nav items hidden** | [Header.svelte:516](./Header.svelte#L516) — `{#if !_isCollapsed && items.length > 0}` | Inline nav requires `!_isCollapsed`; combined with the empty `_dropdownItems` above, items don't reappear in a dropdown either. |
17
+ | **Locale hidden** | [Header.svelte:335](./Header.svelte#L335) — `!_isCollapsed \|\| (collapseMode === "hide" && keepLocaleOnCollapse)` | Default `keepLocaleOnCollapse={false}` hides the locale switcher in collapsed mode. |
18
18
 
19
19
  Minimal config:
20
20
 
@@ -4,16 +4,16 @@ Cross-fades between N visual states (HTML strings or Snippets) at a single posit
4
4
 
5
5
  ## Props
6
6
 
7
- | Prop | Type | Default | Description |
8
- | ------------ | -------------------------- | -------- | -------------------------------------------------------------------------- |
9
- | `states` | `Array<string \| Snippet>` | required | The visual states to swap between. Strings are rendered with `{@html}`. |
10
- | `active` | `number` | `0` | Bindable index of the currently visible state. |
11
- | `duration` | `number` | `300` | Transition duration in ms. Set `0` to disable. |
12
- | `easing` | `string` | `"ease"` | CSS `transition-timing-function`. |
13
- | `unstyled` | `boolean` | `false` | Skip default styling. |
14
- | `class` | `string` | - | Additional CSS classes for the root `<span>`. |
15
- | `stateClass` | `string` | - | Additional CSS classes for each state wrapper. |
16
- | `el` | `HTMLSpanElement` | - | Bindable root element. |
7
+ | Prop | Type | Default | Description |
8
+ | ------------ | -------------------------- | -------- | ----------------------------------------------------------------------- |
9
+ | `states` | `Array<string \| Snippet>` | required | The visual states to swap between. Strings are rendered with `{@html}`. |
10
+ | `active` | `number` | `0` | Bindable index of the currently visible state. |
11
+ | `duration` | `number` | `300` | Transition duration in ms. Set `0` to disable. |
12
+ | `easing` | `string` | `"ease"` | CSS `transition-timing-function`. |
13
+ | `unstyled` | `boolean` | `false` | Skip default styling. |
14
+ | `class` | `string` | - | Additional CSS classes for the root `<span>`. |
15
+ | `stateClass` | `string` | - | Additional CSS classes for each state wrapper. |
16
+ | `el` | `HTMLSpanElement` | - | Bindable root element. |
17
17
 
18
18
  ## Usage
19
19
 
@@ -47,15 +47,20 @@ Cross-fades between N visual states (HTML strings or Snippets) at a single posit
47
47
  ### Custom easing/duration
48
48
 
49
49
  ```svelte
50
- <IconSwap states={frames} active={i} duration={500} easing="cubic-bezier(0.4, 0, 0.2, 1)" />
50
+ <IconSwap
51
+ states={frames}
52
+ active={i}
53
+ duration={500}
54
+ easing="cubic-bezier(0.4, 0, 0.2, 1)"
55
+ />
51
56
  ```
52
57
 
53
58
  ## CSS Variables
54
59
 
55
- | Variable | Default | Description |
56
- | ----------------------------- | ------- | -------------------------- |
57
- | `--stuic-icon-swap-duration` | (prop) | Transition duration |
58
- | `--stuic-icon-swap-easing` | (prop) | Transition timing function |
60
+ | Variable | Default | Description |
61
+ | ---------------------------- | ------- | -------------------------- |
62
+ | `--stuic-icon-swap-duration` | (prop) | Transition duration |
63
+ | `--stuic-icon-swap-easing` | (prop) | Transition timing function |
59
64
 
60
65
  ## Data Attributes
61
66
 
@@ -22,7 +22,8 @@
22
22
  justify-content: center;
23
23
  opacity: 0;
24
24
  pointer-events: none;
25
- transition: opacity var(--stuic-icon-swap-duration, var(--stuic-transition)) var(--stuic-icon-swap-easing);
25
+ transition: opacity var(--stuic-icon-swap-duration, var(--stuic-transition))
26
+ var(--stuic-icon-swap-easing);
26
27
  }
27
28
 
28
29
  .stuic-icon-swap > .stuic-icon-swap-state[data-visible="true"] {