@marianmeres/stuic 2.66.0 → 3.0.1

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 (208) hide show
  1. package/README.md +292 -4
  2. package/dist/README.md +41 -18
  3. package/dist/actions/index.d.ts +1 -0
  4. package/dist/actions/index.js +1 -0
  5. package/dist/actions/popover/README.md +19 -0
  6. package/dist/actions/popover/index.css +6 -9
  7. package/dist/actions/popover/popover.svelte.js +2 -2
  8. package/dist/actions/tooltip/README.md +18 -0
  9. package/dist/actions/tooltip/index.css +5 -8
  10. package/dist/actions/tooltip/tooltip.svelte.js +1 -1
  11. package/dist/actions/typeahead.svelte.d.ts +53 -0
  12. package/dist/actions/typeahead.svelte.js +328 -0
  13. package/dist/base.css +17 -0
  14. package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte +10 -10
  15. package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte.d.ts +4 -3
  16. package/dist/components/AlertConfirmPrompt/Current.svelte +15 -18
  17. package/dist/components/AlertConfirmPrompt/Current.svelte.d.ts +4 -3
  18. package/dist/components/AlertConfirmPrompt/acp-icons.js +5 -4
  19. package/dist/components/AlertConfirmPrompt/index.css +66 -0
  20. package/dist/components/AssetsPreview/AssetsPreview.svelte +91 -73
  21. package/dist/components/AssetsPreview/index.css +61 -0
  22. package/dist/components/Avatar/Avatar.svelte +31 -18
  23. package/dist/components/Avatar/README.md +166 -0
  24. package/dist/components/Avatar/index.css +130 -0
  25. package/dist/components/Backdrop/Backdrop.svelte +7 -2
  26. package/dist/components/Backdrop/README.md +71 -6
  27. package/dist/components/Backdrop/index.css +31 -0
  28. package/dist/components/Button/Button.svelte +116 -124
  29. package/dist/components/Button/Button.svelte.d.ts +35 -24
  30. package/dist/components/Button/README.md +87 -21
  31. package/dist/components/Button/index.css +475 -9
  32. package/dist/components/Button/index.d.ts +1 -1
  33. package/dist/components/Button/index.js +1 -1
  34. package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +7 -39
  35. package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +0 -1
  36. package/dist/components/ButtonGroupRadio/README.md +82 -4
  37. package/dist/components/ButtonGroupRadio/index.css +158 -14
  38. package/dist/components/Collapsible/Collapsible.svelte +7 -7
  39. package/dist/components/Collapsible/Collapsible.svelte.d.ts +2 -2
  40. package/dist/components/Collapsible/README.md +34 -2
  41. package/dist/components/Collapsible/index.css +40 -0
  42. package/dist/components/CommandMenu/CommandMenu.svelte +18 -26
  43. package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +0 -1
  44. package/dist/components/CommandMenu/README.md +39 -0
  45. package/dist/components/CommandMenu/index.css +47 -2
  46. package/dist/components/DismissibleMessage/DismissibleMessage.svelte +53 -51
  47. package/dist/components/DismissibleMessage/DismissibleMessage.svelte.d.ts +6 -6
  48. package/dist/components/DismissibleMessage/README.md +93 -11
  49. package/dist/components/DismissibleMessage/index.css +128 -8
  50. package/dist/components/DismissibleMessage/index.d.ts +1 -1
  51. package/dist/components/DropdownMenu/DropdownMenu.svelte +14 -51
  52. package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +6 -7
  53. package/dist/components/DropdownMenu/README.md +132 -0
  54. package/dist/components/DropdownMenu/index.css +258 -52
  55. package/dist/components/Input/FieldAssets.svelte +8 -5
  56. package/dist/components/Input/FieldCheckbox.svelte +7 -44
  57. package/dist/components/Input/FieldFile.svelte +1 -6
  58. package/dist/components/Input/FieldInput.svelte +9 -1
  59. package/dist/components/Input/FieldInput.svelte.d.ts +2 -0
  60. package/dist/components/Input/FieldOptions.svelte +42 -39
  61. package/dist/components/Input/FieldRadios.svelte +7 -16
  62. package/dist/components/Input/FieldSelect.svelte +1 -1
  63. package/dist/components/Input/FieldSwitch.svelte +1 -5
  64. package/dist/components/Input/FieldTextarea.svelte +1 -1
  65. package/dist/components/Input/README.md +194 -0
  66. package/dist/components/Input/_internal/FieldRadioInternal.svelte +2 -40
  67. package/dist/components/Input/_internal/InputWrap.svelte +8 -48
  68. package/dist/components/Input/index.css +524 -116
  69. package/dist/components/KbdShortcut/KbdShortcut.svelte +4 -12
  70. package/dist/components/KbdShortcut/README.md +34 -0
  71. package/dist/components/KbdShortcut/index.css +55 -0
  72. package/dist/components/ListItemButton/ListItemButton.svelte +37 -74
  73. package/dist/components/ListItemButton/ListItemButton.svelte.d.ts +1 -10
  74. package/dist/components/ListItemButton/README.md +100 -45
  75. package/dist/components/ListItemButton/index.css +173 -52
  76. package/dist/components/ListItemButton/index.d.ts +1 -1
  77. package/dist/components/ListItemButton/index.js +1 -1
  78. package/dist/components/Modal/Modal.svelte +1 -8
  79. package/dist/components/Modal/README.md +29 -0
  80. package/dist/components/Modal/index.css +38 -0
  81. package/dist/components/ModalDialog/ModalDialog.svelte +2 -21
  82. package/dist/components/ModalDialog/README.md +35 -0
  83. package/dist/components/ModalDialog/index.css +59 -0
  84. package/dist/components/Nav/Nav.svelte +732 -0
  85. package/dist/components/Nav/Nav.svelte.d.ts +110 -0
  86. package/dist/components/Nav/README.md +334 -0
  87. package/dist/components/Nav/index.css +318 -0
  88. package/dist/components/Nav/index.d.ts +1 -0
  89. package/dist/components/Nav/index.js +1 -0
  90. package/dist/components/Notifications/Notifications.svelte +44 -129
  91. package/dist/components/Notifications/Notifications.svelte.d.ts +9 -18
  92. package/dist/components/Notifications/README.md +186 -70
  93. package/dist/components/Notifications/index.css +212 -15
  94. package/dist/components/Notifications/notifications-stack.svelte.d.ts +4 -0
  95. package/dist/components/Notifications/notifications-stack.svelte.js +8 -0
  96. package/dist/components/Progress/Progress.svelte +4 -2
  97. package/dist/components/Progress/Progress.svelte.d.ts +1 -0
  98. package/dist/components/Progress/README.md +97 -11
  99. package/dist/components/Progress/_internal/Bar.svelte +4 -15
  100. package/dist/components/Progress/_internal/Bar.svelte.d.ts +1 -1
  101. package/dist/components/Progress/_internal/Circle.svelte +30 -2
  102. package/dist/components/Progress/_internal/Circle.svelte.d.ts +1 -0
  103. package/dist/components/Progress/index.css +50 -4
  104. package/dist/components/Skeleton/README.md +152 -0
  105. package/dist/components/Skeleton/Skeleton.svelte +9 -9
  106. package/dist/components/Skeleton/Skeleton.svelte.d.ts +0 -1
  107. package/dist/components/Skeleton/index.css +72 -45
  108. package/dist/components/Spinner/README.md +149 -37
  109. package/dist/components/Spinner/Spinner.svelte +14 -38
  110. package/dist/components/Spinner/Spinner.svelte.d.ts +2 -1
  111. package/dist/components/Spinner/SpinnerCircle.svelte +6 -34
  112. package/dist/components/Spinner/SpinnerCircle.svelte.d.ts +1 -0
  113. package/dist/components/Spinner/SpinnerCircleOscillate.svelte +10 -5
  114. package/dist/components/Spinner/SpinnerUnicode.svelte +3 -1
  115. package/dist/components/Spinner/SpinnerUnicode.svelte.d.ts +1 -0
  116. package/dist/components/Spinner/index.css +104 -0
  117. package/dist/components/Switch/README.md +45 -14
  118. package/dist/components/Switch/Switch.svelte +23 -48
  119. package/dist/components/Switch/Switch.svelte.d.ts +4 -2
  120. package/dist/components/Switch/index.css +121 -4
  121. package/dist/components/Switch/index.d.ts +1 -2
  122. package/dist/components/Switch/index.js +1 -2
  123. package/dist/components/TabbedMenu/README.md +37 -21
  124. package/dist/components/TabbedMenu/TabbedMenu.svelte +5 -46
  125. package/dist/components/TabbedMenu/TabbedMenu.svelte.d.ts +0 -1
  126. package/dist/components/TabbedMenu/index.css +84 -17
  127. package/dist/components/ThemePreview/README.md +289 -0
  128. package/dist/components/ThemePreview/ThemePreview.svelte +394 -0
  129. package/dist/components/ThemePreview/ThemePreview.svelte.d.ts +35 -0
  130. package/dist/components/ThemePreview/index.css +509 -0
  131. package/dist/components/ThemePreview/index.d.ts +1 -0
  132. package/dist/components/ThemePreview/index.js +1 -0
  133. package/dist/components/TwCheck/README.md +32 -13
  134. package/dist/components/TwCheck/TwCheck.svelte +11 -9
  135. package/dist/components/TwCheck/TwCheck.svelte.d.ts +0 -1
  136. package/dist/components/TwCheck/index.css +17 -2
  137. package/dist/components/TypeaheadInput/TypeaheadInput.svelte +20 -188
  138. package/dist/components/TypeaheadInput/TypeaheadInput.svelte.d.ts +4 -2
  139. package/dist/components/X/X.svelte +12 -5
  140. package/dist/components/X/X.svelte.d.ts +1 -0
  141. package/dist/icons/index.d.ts +1 -0
  142. package/dist/icons/index.js +1 -0
  143. package/dist/index.css +46 -26
  144. package/dist/index.d.ts +2 -0
  145. package/dist/index.js +2 -0
  146. package/dist/themes/blue-orange.css +217 -0
  147. package/dist/themes/blue-orange.d.ts +6 -0
  148. package/dist/themes/blue-orange.js +175 -0
  149. package/dist/themes/cyan-red.css +217 -0
  150. package/dist/themes/cyan-red.d.ts +6 -0
  151. package/dist/themes/cyan-red.js +175 -0
  152. package/dist/themes/cyan-slate.css +217 -0
  153. package/dist/themes/cyan-slate.d.ts +6 -0
  154. package/dist/themes/cyan-slate.js +175 -0
  155. package/dist/themes/emerald-pink.css +217 -0
  156. package/dist/themes/emerald-pink.d.ts +6 -0
  157. package/dist/themes/emerald-pink.js +175 -0
  158. package/dist/themes/fuchsia-emerald.css +217 -0
  159. package/dist/themes/fuchsia-emerald.d.ts +6 -0
  160. package/dist/themes/fuchsia-emerald.js +175 -0
  161. package/dist/themes/gray.css +217 -0
  162. package/dist/themes/gray.d.ts +6 -0
  163. package/dist/themes/gray.js +175 -0
  164. package/dist/themes/indigo-amber.css +217 -0
  165. package/dist/themes/indigo-amber.d.ts +6 -0
  166. package/dist/themes/indigo-amber.js +175 -0
  167. package/dist/themes/neutral.css +217 -0
  168. package/dist/themes/neutral.d.ts +6 -0
  169. package/dist/themes/neutral.js +175 -0
  170. package/dist/themes/pink-emerald.css +217 -0
  171. package/dist/themes/pink-emerald.d.ts +6 -0
  172. package/dist/themes/pink-emerald.js +175 -0
  173. package/dist/themes/purple-yellow.css +217 -0
  174. package/dist/themes/purple-yellow.d.ts +6 -0
  175. package/dist/themes/purple-yellow.js +175 -0
  176. package/dist/themes/rainbow.css +217 -0
  177. package/dist/themes/rainbow.d.ts +6 -0
  178. package/dist/themes/rainbow.js +180 -0
  179. package/dist/themes/red-blue.css +217 -0
  180. package/dist/themes/red-blue.d.ts +6 -0
  181. package/dist/themes/red-blue.js +175 -0
  182. package/dist/themes/red-cyan.css +217 -0
  183. package/dist/themes/red-cyan.d.ts +6 -0
  184. package/dist/themes/red-cyan.js +175 -0
  185. package/dist/themes/rose-teal.css +217 -0
  186. package/dist/themes/rose-teal.d.ts +6 -0
  187. package/dist/themes/rose-teal.js +175 -0
  188. package/dist/themes/sky-amber.css +217 -0
  189. package/dist/themes/sky-amber.d.ts +6 -0
  190. package/dist/themes/sky-amber.js +175 -0
  191. package/dist/themes/slate-cyan.css +217 -0
  192. package/dist/themes/slate-cyan.d.ts +6 -0
  193. package/dist/themes/slate-cyan.js +175 -0
  194. package/dist/themes/tailwind-color-pairs.md +31 -0
  195. package/dist/themes/teal-rose.css +217 -0
  196. package/dist/themes/teal-rose.d.ts +6 -0
  197. package/dist/themes/teal-rose.js +175 -0
  198. package/dist/themes/violet-lime.css +217 -0
  199. package/dist/themes/violet-lime.d.ts +6 -0
  200. package/dist/themes/violet-lime.js +175 -0
  201. package/dist/utils/design-tokens.d.ts +43 -0
  202. package/dist/utils/design-tokens.js +127 -0
  203. package/dist/utils/index.d.ts +1 -0
  204. package/dist/utils/index.js +1 -0
  205. package/dist/utils/storage-abstraction.js +1 -1
  206. package/package.json +14 -11
  207. package/dist/components/Switch/SwitchButton.svelte +0 -135
  208. package/dist/components/Switch/SwitchButton.svelte.d.ts +0 -21
@@ -171,6 +171,7 @@
171
171
  </script>
172
172
 
173
173
  <script lang="ts">
174
+ import Button from "../Button/Button.svelte";
174
175
  const clog = createClog("AssetsPreview", { color: "auto" });
175
176
 
176
177
  let {
@@ -219,7 +220,13 @@
219
220
  let imgEl: HTMLImageElement | null = null;
220
221
  let containerEl: HTMLDivElement | null = $state(null);
221
222
 
222
- const TOP_BUTTON_CLS = "rounded bg-white hover:bg-neutral-200 p-1";
223
+ const BUTTON_CLS = "stuic-assets-preview-control pointer-events-auto p-0!";
224
+
225
+ const BUTTON_PROPS = {
226
+ aspect1: true,
227
+ variant: "soft",
228
+ roundedFull: true,
229
+ };
223
230
 
224
231
  $effect(() => {
225
232
  const visible = modal?.visibility().visible;
@@ -457,6 +464,7 @@
457
464
  previewIdx = idx % assets.length;
458
465
  }
459
466
 
467
+ const ICON_SIZE = 24;
460
468
  // $inspect(assets).with(clog);
461
469
  </script>
462
470
 
@@ -513,7 +521,9 @@
513
521
  class: "mx-auto",
514
522
  })}
515
523
  </div>
516
- <div class="opacity-50 mt-4">{t("unable_to_preview")}</div>
524
+ <div class="text-(--stuic-color-muted-foreground) mt-4">
525
+ {t("unable_to_preview")}
526
+ </div>
517
527
  </div>
518
528
  {/if}
519
529
 
@@ -521,95 +531,103 @@
521
531
  <div
522
532
  class="absolute inset-0 flex items-center justify-between pointer-events-none"
523
533
  >
524
- <button
525
- class={twMerge("p-4 pointer-events-auto", classControls)}
534
+ <!-- class={twMerge("p-4 aspect-square pointer-events-auto", classControls)} -->
535
+ <Button
536
+ class={twMerge(BUTTON_CLS, "ml-4", classControls)}
526
537
  onclick={preview_previous}
527
538
  type="button"
539
+ {...BUTTON_PROPS}
528
540
  >
529
- <span class="bg-white rounded-full p-3 block">
530
- {@html iconPrevious()}
531
- </span>
532
- </button>
533
-
534
- <button
535
- class={twMerge("p-4 pointer-events-auto", classControls)}
541
+ <!-- <span class="stuic-assets-preview-control-nav p-3 block"> -->
542
+ {@html iconPrevious({ size: ICON_SIZE })}
543
+ <!-- </span> -->
544
+ </Button>
545
+
546
+ <!-- class={twMerge("p-4 aspect-square pointer-events-auto", classControls)} -->
547
+ <Button
548
+ class={twMerge(BUTTON_CLS, "mr-4", classControls)}
536
549
  onclick={preview_next}
537
550
  type="button"
551
+ {...BUTTON_PROPS}
538
552
  >
539
- <span class="bg-white rounded-full p-3 block">
540
- {@html iconNext()}
541
- </span>
542
- </button>
553
+ <!-- <span class="stuic-assets-preview-control-nav p-3 block"> -->
554
+ {@html iconNext({ size: ICON_SIZE })}
555
+ <!-- </span> -->
556
+ </Button>
543
557
  </div>
544
558
  {/if}
545
559
 
546
560
  <div class="absolute top-4 left-4 right-4 flex items-center justify-between gap-3">
547
561
  {#if !noName && previewAsset?.name}
548
- <span class="truncate bg-white px-1 rounded">
562
+ <span class="stuic-assets-preview-label truncate px-1">
549
563
  {previewAsset?.name}
550
564
  </span>
551
565
  {:else}
552
566
  <span></span>
553
567
  {/if}
554
568
  <div class="flex items-center space-x-3 shrink-0">
555
- {#if previewAsset.isImage}
556
- <button
557
- class={twMerge(TOP_BUTTON_CLS, classControls)}
558
- type="button"
559
- onclick={zoomOut}
560
- disabled={zoomLevelIdx === 0}
561
- aria-label={t("zoom_out")}
562
- use:tooltip={() => ({ content: t("zoom_out") })}
563
- >
564
- {@html iconZoomOut({ class: "size-6" })}
565
- </button>
566
-
567
- <button
568
- class={twMerge(TOP_BUTTON_CLS, classControls)}
569
+ {#if previewAsset.isImage}
570
+ <Button
571
+ class={twMerge(BUTTON_CLS, classControls)}
572
+ type="button"
573
+ onclick={zoomOut}
574
+ disabled={zoomLevelIdx === 0}
575
+ aria-label={t("zoom_out")}
576
+ tooltip={t("zoom_out")}
577
+ {...BUTTON_PROPS}
578
+ >
579
+ {@html iconZoomOut({ size: ICON_SIZE })}
580
+ </Button>
581
+
582
+ <Button
583
+ class={twMerge(BUTTON_CLS, classControls)}
584
+ type="button"
585
+ onclick={zoomIn}
586
+ disabled={zoomLevelIdx === ZOOM_LEVELS.length - 1}
587
+ aria-label={t("zoom_in")}
588
+ tooltip={t("zoom_in")}
589
+ {...BUTTON_PROPS}
590
+ >
591
+ {@html iconZoomIn({ size: ICON_SIZE })}
592
+ </Button>
593
+ {/if}
594
+
595
+ {#if typeof onDelete === "function"}
596
+ <Button
597
+ class={twMerge(BUTTON_CLS, classControls)}
598
+ type="button"
599
+ onclick={() => onDelete(previewAsset, previewIdx, { close })}
600
+ aria-label={t("delete")}
601
+ tooltip={t("delete")}
602
+ {...BUTTON_PROPS}
603
+ >
604
+ {@html iconDelete({ size: ICON_SIZE })}
605
+ </Button>
606
+ {/if}
607
+
608
+ <Button
609
+ class={twMerge(BUTTON_CLS, classControls)}
569
610
  type="button"
570
- onclick={zoomIn}
571
- disabled={zoomLevelIdx === ZOOM_LEVELS.length - 1}
572
- aria-label={t("zoom_in")}
573
- use:tooltip={() => ({ content: t("zoom_in") })}
611
+ onclick={(e) => {
612
+ e.preventDefault();
613
+ forceDownload(String(previewAsset.url.original), previewAsset?.name || "");
614
+ }}
615
+ aria-label={t("download")}
616
+ tooltip={t("download")}
617
+ {...BUTTON_PROPS}
574
618
  >
575
- {@html iconZoomIn({ class: "size-6" })}
576
- </button>
577
- {/if}
619
+ {@html iconDownload({ size: ICON_SIZE })}
620
+ </Button>
578
621
 
579
- {#if typeof onDelete === "function"}
580
- <button
581
- class={twMerge(TOP_BUTTON_CLS, classControls)}
622
+ <Button
623
+ class={twMerge(BUTTON_CLS, classControls)}
624
+ onclick={modal?.close}
625
+ aria-label={t("close")}
582
626
  type="button"
583
- onclick={() => onDelete(previewAsset, previewIdx, { close })}
584
- aria-label={t("delete")}
585
- use:tooltip
586
- >
587
- {@html iconDelete({ class: "size-6" })}
588
- </button>
589
- {/if}
590
-
591
- <button
592
- class={twMerge(TOP_BUTTON_CLS, classControls)}
593
- type="button"
594
- onclick={(e) => {
595
- e.preventDefault();
596
- forceDownload(String(previewAsset.url.original), previewAsset?.name || "");
597
- }}
598
- aria-label={t("download")}
599
- use:tooltip
600
- >
601
- {@html iconDownload({ class: "size-6" })}
602
- </button>
603
-
604
- <button
605
- class={twMerge(TOP_BUTTON_CLS, classControls)}
606
- onclick={modal?.close}
607
- aria-label={t("close")}
608
- type="button"
609
- use:tooltip
610
- >
611
- <X />
612
- </button>
627
+ tooltip={t("close")}
628
+ {...BUTTON_PROPS}
629
+ x
630
+ />
613
631
  </div>
614
632
  </div>
615
633
 
@@ -619,7 +637,7 @@
619
637
  class="absolute bottom-10 left-0 right-0 text-center"
620
638
  transition:fade={{ duration: 100 }}
621
639
  >
622
- <span class="bg-white p-1 rounded opacity/75">
640
+ <span class="stuic-assets-preview-label p-1">
623
641
  {dotTooltip}
624
642
  </span>
625
643
  </div>
@@ -630,8 +648,8 @@
630
648
  <button
631
649
  type="button"
632
650
  class={twMerge(
633
- "size-3 rounded-full transition-colors border border-black/50",
634
- i === previewIdx ? "bg-white" : "bg-white/50 hover:bg-neutral-200"
651
+ "stuic-assets-preview-dot",
652
+ i === previewIdx ? "active" : ""
635
653
  )}
636
654
  onclick={() => {
637
655
  previewIdx = i;
@@ -0,0 +1,61 @@
1
+ /* ============================================================================
2
+ ASSETS PREVIEW COMPONENT TOKENS
3
+ Override globally: :root { --stuic-assets-preview-control-bg: var(--stuic-color-muted); }
4
+ Override locally: <AssetsPreview style="--stuic-assets-preview-control-bg: red;">
5
+ ============================================================================ */
6
+
7
+ :root {
8
+ /* Control buttons (zoom, download, close, prev/next) */
9
+
10
+ /* Labels (filename, tooltip) */
11
+ --stuic-assets-preview-label-bg: var(--stuic-color-surface);
12
+ --stuic-assets-preview-label-text: var(--stuic-color-surface-foreground);
13
+ --stuic-assets-preview-label-radius: var(--radius-sm);
14
+
15
+ /* Dot indicators */
16
+ --stuic-assets-preview-dot-size: 0.75rem;
17
+ --stuic-assets-preview-dot-bg: color-mix(
18
+ in srgb,
19
+ var(--stuic-color-surface) 50%,
20
+ transparent
21
+ );
22
+ --stuic-assets-preview-dot-bg-active: var(--stuic-color-surface);
23
+ --stuic-assets-preview-dot-bg-hover: var(--stuic-color-surface-hover);
24
+ --stuic-assets-preview-dot-border: var(--stuic-color-border);
25
+
26
+ /* Transition */
27
+ --stuic-assets-preview-transition: 150ms;
28
+ }
29
+
30
+ @layer components {
31
+ /* ============================================================================
32
+ LABELS (filename display, tooltip)
33
+ ============================================================================ */
34
+
35
+ .stuic-assets-preview-label {
36
+ background: var(--stuic-assets-preview-label-bg);
37
+ color: var(--stuic-assets-preview-label-text);
38
+ border-radius: var(--stuic-assets-preview-label-radius);
39
+ }
40
+
41
+ /* ============================================================================
42
+ DOT INDICATORS (pagination dots)
43
+ ============================================================================ */
44
+
45
+ .stuic-assets-preview-dot {
46
+ width: var(--stuic-assets-preview-dot-size);
47
+ height: var(--stuic-assets-preview-dot-size);
48
+ border-radius: 9999px;
49
+ background: var(--stuic-assets-preview-dot-bg);
50
+ border: 1px solid var(--stuic-assets-preview-dot-border);
51
+ transition: background var(--stuic-assets-preview-transition);
52
+ }
53
+
54
+ .stuic-assets-preview-dot:hover {
55
+ background: var(--stuic-assets-preview-dot-bg-hover);
56
+ }
57
+
58
+ .stuic-assets-preview-dot.active {
59
+ background: var(--stuic-assets-preview-dot-bg-active);
60
+ }
61
+ }
@@ -60,14 +60,19 @@
60
60
  el = $bindable(),
61
61
  }: Props = $props();
62
62
 
63
- const SIZE_PRESETS: Record<string, { container: string; icon: number }> = {
64
- sm: { container: "size-8 text-xs", icon: 16 },
65
- md: { container: "size-10 text-base", icon: 20 },
66
- lg: { container: "size-12 text-lg", icon: 28 },
67
- xl: { container: "size-14 text-xl", icon: 32 },
68
- "2xl": { container: "size-16 text-2xl", icon: 36 },
63
+ // Icon sizes for preset sizes (visual sizes are handled by CSS)
64
+ const ICON_SIZES: Record<string, number> = {
65
+ sm: 16,
66
+ md: 20,
67
+ lg: 28,
68
+ xl: 32,
69
+ "2xl": 36,
69
70
  };
70
71
 
72
+ // Check if size is a known preset
73
+ const isPresetSize = (s: string): s is "sm" | "md" | "lg" | "xl" | "2xl" =>
74
+ s in ICON_SIZES;
75
+
71
76
  // Extract initials from input string (email, name, or raw initials)
72
77
  function extractInitials(input: string, length: number): string {
73
78
  let _input = (input || "").trim();
@@ -145,8 +150,7 @@
145
150
 
146
151
  // Get icon size based on preset or custom size
147
152
  let iconSize = $derived.by(() => {
148
- const preset = SIZE_PRESETS[size];
149
- if (preset) return preset.icon;
153
+ if (isPresetSize(size)) return ICON_SIZES[size];
150
154
 
151
155
  // For custom sizes, try to parse size-N pattern
152
156
  const match = size?.match(/size-(\d+)/);
@@ -162,25 +166,21 @@
162
166
  autoColor ? generateAvatarColors(hashSource || initialsProp || "") : null
163
167
  );
164
168
 
165
- let sizeClass = $derived(SIZE_PRESETS[size]?.container || size);
166
-
167
169
  let style = $derived(
168
170
  autoColor && colors
169
171
  ? `background-color: ${colors.bg}; color: ${colors.text};`
170
172
  : undefined
171
173
  );
172
174
 
175
+ // Build class string - base class for CSS targeting, allow user overrides via classProp
173
176
  let baseClass = $derived(
174
177
  twMerge(
175
178
  "stuic-avatar",
176
- "inline-flex items-center justify-center",
177
- "rounded-full font-medium shrink-0 overflow-hidden",
178
- !autoColor &&
179
- "bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200",
180
- sizeClass,
179
+ // Custom size classes when not using preset (preset sizes handled by data-size in CSS)
180
+ !isPresetSize(size) && size,
181
+ // User-provided Tailwind overrides for colors (only when not using autoColor)
181
182
  !autoColor && bg,
182
183
  !autoColor && textColor,
183
- onclick && "select-none cursor-pointer",
184
184
  classProp
185
185
  )
186
186
  );
@@ -191,7 +191,15 @@
191
191
  </script>
192
192
 
193
193
  {#if onclick}
194
- <button bind:this={el} type="button" class={baseClass} {style} {onclick}>
194
+ <button
195
+ bind:this={el}
196
+ type="button"
197
+ class={baseClass}
198
+ {style}
199
+ {onclick}
200
+ data-size={isPresetSize(size) ? size : undefined}
201
+ data-interactive="true"
202
+ >
195
203
  {#if renderMode === "photo"}
196
204
  <img {src} {alt} class="size-full object-cover" onerror={handleImageError} />
197
205
  {:else if renderMode === "initials"}
@@ -201,7 +209,12 @@
201
209
  {/if}
202
210
  </button>
203
211
  {:else}
204
- <div bind:this={el} class={baseClass} {style}>
212
+ <div
213
+ bind:this={el}
214
+ class={baseClass}
215
+ {style}
216
+ data-size={isPresetSize(size) ? size : undefined}
217
+ >
205
218
  {#if renderMode === "photo"}
206
219
  <img {src} {alt} class="size-full object-cover" onerror={handleImageError} />
207
220
  {:else if renderMode === "initials"}
@@ -0,0 +1,166 @@
1
+ # Avatar
2
+
3
+ A flexible avatar component that displays user photos, initials, or icons with automatic fallback handling and optional deterministic color generation.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `src` | `string` | - | Photo URL - when provided, renders in photo mode |
10
+ | `alt` | `string` | - | Alt text for photo mode |
11
+ | `initials` | `string` | - | String to extract initials from. Supports: "AB", "John Doe", or "john.doe@example.com" |
12
+ | `initialsLength` | `number` | `2` | Maximum length of extracted initials |
13
+ | `icon` | `IconFn` | - | Icon function to display - when provided alone, renders in icon mode |
14
+ | `fallback` | `AvatarFallback` | `"icon"` | Fallback when photo fails to load |
15
+ | `hashSource` | `string` | - | String for color hash calculation (e.g., email, user ID). Falls back to `initials` |
16
+ | `size` | `"sm" \| "md" \| "lg" \| "xl" \| "2xl" \| string` | `"md"` | Size preset or custom Tailwind size class |
17
+ | `onclick` | `(event: MouseEvent) => void` | - | Click handler - when provided, renders as a button |
18
+ | `bg` | `string` | - | Background color (Tailwind class). Ignored if `autoColor=true` |
19
+ | `textColor` | `string` | - | Text color (Tailwind class). Ignored if `autoColor=true` |
20
+ | `autoColor` | `boolean` | `false` | Generate deterministic pastel colors from hashSource/initials |
21
+ | `class` | `string` | - | Additional CSS classes |
22
+ | `el` | `HTMLElement` | - | Bindable element reference |
23
+
24
+ ## Usage
25
+
26
+ ### Basic
27
+
28
+ ```svelte
29
+ <script lang="ts">
30
+ import { Avatar } from '@marianmeres/stuic';
31
+ </script>
32
+
33
+ <!-- Photo -->
34
+ <Avatar src="/path/to/photo.jpg" alt="John Doe" />
35
+
36
+ <!-- Initials -->
37
+ <Avatar initials="John Doe" />
38
+
39
+ <!-- Icon (default fallback) -->
40
+ <Avatar />
41
+ ```
42
+
43
+ ### Sizes
44
+
45
+ ```svelte
46
+ <Avatar initials="AB" size="sm" />
47
+ <Avatar initials="AB" size="md" />
48
+ <Avatar initials="AB" size="lg" />
49
+ <Avatar initials="AB" size="xl" />
50
+ <Avatar initials="AB" size="2xl" />
51
+
52
+ <!-- Custom size with Tailwind -->
53
+ <Avatar initials="AB" size="size-20" />
54
+ ```
55
+
56
+ ### Auto Color
57
+
58
+ Generate deterministic pastel colors based on a hash source:
59
+
60
+ ```svelte
61
+ <!-- Same input always produces same color -->
62
+ <Avatar initials="john@example.com" autoColor />
63
+ <Avatar initials="Jane Smith" autoColor hashSource="user-123" />
64
+ ```
65
+
66
+ ### Photo with Fallback
67
+
68
+ ```svelte
69
+ <!-- Falls back to icon on error -->
70
+ <Avatar src="/maybe-broken.jpg" fallback="icon" />
71
+
72
+ <!-- Falls back to initials on error -->
73
+ <Avatar src="/maybe-broken.jpg" fallback="initials" initials="JD" />
74
+
75
+ <!-- Falls back to specific initials -->
76
+ <Avatar src="/maybe-broken.jpg" fallback={{ initials: "AB" }} />
77
+ ```
78
+
79
+ ### As Button
80
+
81
+ ```svelte
82
+ <Avatar
83
+ src="/photo.jpg"
84
+ onclick={() => console.log('clicked')}
85
+ />
86
+ ```
87
+
88
+ ### Custom Colors
89
+
90
+ ```svelte
91
+ <!-- Using Tailwind classes -->
92
+ <Avatar initials="AB" bg="bg-blue-500" textColor="text-white" />
93
+
94
+ <!-- Using inline style for component tokens -->
95
+ <Avatar
96
+ initials="AB"
97
+ style="--stuic-avatar-bg: #3b82f6; --stuic-avatar-fg: white;"
98
+ />
99
+ ```
100
+
101
+ ## CSS Variables
102
+
103
+ ### Component Tokens
104
+
105
+ Override globally in `:root` or locally via `style` prop:
106
+
107
+ | Variable | Default | Description |
108
+ |----------|---------|-------------|
109
+ | `--stuic-avatar-radius` | `9999px` | Border radius (circle by default) |
110
+ | `--stuic-avatar-font-weight` | `--font-weight-medium` | Font weight for initials |
111
+ | `--stuic-avatar-transition` | `150ms` | Transition duration |
112
+ | `--stuic-avatar-bg` | `--stuic-color-muted` | Default background color |
113
+ | `--stuic-avatar-fg` | `--stuic-color-muted-foreground` | Default text/icon color |
114
+ | `--stuic-avatar-ring-width` | `3px` | Focus ring width (button mode) |
115
+ | `--stuic-avatar-ring-color` | `--stuic-color-ring` | Focus ring color |
116
+
117
+ ### Size Tokens
118
+
119
+ Each size preset has corresponding tokens (font sizes use Tailwind CSS variables):
120
+
121
+ | Size | Width/Height | Font Size |
122
+ |------|--------------|-----------|
123
+ | `sm` | `--stuic-avatar-size-sm` (2rem) | `--stuic-avatar-font-size-sm` (`--text-xs`) |
124
+ | `md` | `--stuic-avatar-size-md` (2.5rem) | `--stuic-avatar-font-size-md` (`--text-base`) |
125
+ | `lg` | `--stuic-avatar-size-lg` (3rem) | `--stuic-avatar-font-size-lg` (`--text-lg`) |
126
+ | `xl` | `--stuic-avatar-size-xl` (3.5rem) | `--stuic-avatar-font-size-xl` (`--text-xl`) |
127
+ | `2xl` | `--stuic-avatar-size-2xl` (4rem) | `--stuic-avatar-font-size-2xl` (`--text-2xl`) |
128
+
129
+ ### Customization Examples
130
+
131
+ ```css
132
+ /* Global override - square avatars */
133
+ :root {
134
+ --stuic-avatar-radius: 0.5rem;
135
+ }
136
+
137
+ /* Global override - larger default size */
138
+ :root {
139
+ --stuic-avatar-size-md: 3rem;
140
+ --stuic-avatar-font-size-md: 1.25rem;
141
+ }
142
+ ```
143
+
144
+ ```svelte
145
+ <!-- Local override - square avatar -->
146
+ <Avatar initials="AB" style="--stuic-avatar-radius: 0.25rem;" />
147
+ ```
148
+
149
+ ## Data Attributes
150
+
151
+ The component uses data attributes for CSS styling:
152
+
153
+ | Attribute | Values | Description |
154
+ |-----------|--------|-------------|
155
+ | `data-size` | `sm`, `md`, `lg`, `xl`, `2xl` | Size preset (only for preset sizes) |
156
+ | `data-interactive` | `true` | Present when `onclick` is provided |
157
+
158
+ ## Theming
159
+
160
+ The Avatar component uses theme tokens for default colors:
161
+
162
+ - Background: `--stuic-color-muted`
163
+ - Foreground: `--stuic-color-muted-foreground`
164
+ - Focus ring: `--stuic-color-ring`
165
+
166
+ These automatically adapt to light/dark mode when using the theming system.
@@ -0,0 +1,130 @@
1
+ /* ============================================================================
2
+ AVATAR COMPONENT TOKENS
3
+ Override globally: :root { --stuic-avatar-radius: 0.5rem; }
4
+ Override locally: <Avatar style="--stuic-avatar-radius: 0;">
5
+ ============================================================================ */
6
+
7
+ :root {
8
+ /* Component-level customization tokens */
9
+ --stuic-avatar-radius: 9999px;
10
+ --stuic-avatar-font-weight: var(--font-weight-medium);
11
+ --stuic-avatar-transition: 150ms;
12
+
13
+ /* Default colors (uses muted role colors) */
14
+ --stuic-avatar-bg: var(--stuic-color-muted);
15
+ --stuic-avatar-fg: var(--stuic-color-muted-foreground);
16
+
17
+ /* Focus ring (for interactive/button mode) */
18
+ --stuic-avatar-ring-width: 3px;
19
+ --stuic-avatar-ring-offset: 0px;
20
+ --stuic-avatar-ring-color: var(--stuic-color-ring);
21
+
22
+ /* Size: sm */
23
+ --stuic-avatar-size-sm: calc(var(--spacing) * 8);
24
+ --stuic-avatar-font-size-sm: var(--text-xs);
25
+
26
+ /* Size: md */
27
+ --stuic-avatar-size-md: calc(var(--spacing) * 10);
28
+ --stuic-avatar-font-size-md: var(--text-base);
29
+
30
+ /* Size: lg */
31
+ --stuic-avatar-size-lg: calc(var(--spacing) * 12);
32
+ --stuic-avatar-font-size-lg: var(--text-lg);
33
+
34
+ /* Size: xl */
35
+ --stuic-avatar-size-xl: calc(var(--spacing) * 14);
36
+ --stuic-avatar-font-size-xl: var(--text-xl);
37
+
38
+ /* Size: 2xl */
39
+ --stuic-avatar-size-2xl: calc(var(--spacing) * 16);
40
+ --stuic-avatar-font-size-2xl: var(--text-2xl);
41
+ }
42
+
43
+ @layer components {
44
+ /* ============================================================================
45
+ BASE STYLES
46
+ ============================================================================ */
47
+
48
+ .stuic-avatar {
49
+ /* Layout */
50
+ display: inline-flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ flex-shrink: 0;
54
+ overflow: hidden;
55
+
56
+ /* Typography */
57
+ font-weight: var(--stuic-avatar-font-weight);
58
+
59
+ /* Box model */
60
+ border-radius: var(--stuic-avatar-radius);
61
+
62
+ /* Colors (can be overridden by autoColor inline styles) */
63
+ background: var(--stuic-avatar-bg);
64
+ color: var(--stuic-avatar-fg);
65
+
66
+ /* Transition for interactive mode */
67
+ transition:
68
+ background var(--stuic-avatar-transition),
69
+ color var(--stuic-avatar-transition),
70
+ opacity var(--stuic-avatar-transition);
71
+ }
72
+
73
+ /* ============================================================================
74
+ SIZE VARIANTS
75
+ ============================================================================ */
76
+
77
+ .stuic-avatar[data-size="sm"] {
78
+ width: var(--stuic-avatar-size-sm);
79
+ height: var(--stuic-avatar-size-sm);
80
+ font-size: var(--stuic-avatar-font-size-sm);
81
+ }
82
+
83
+ .stuic-avatar[data-size="md"] {
84
+ width: var(--stuic-avatar-size-md);
85
+ height: var(--stuic-avatar-size-md);
86
+ font-size: var(--stuic-avatar-font-size-md);
87
+ }
88
+
89
+ .stuic-avatar[data-size="lg"] {
90
+ width: var(--stuic-avatar-size-lg);
91
+ height: var(--stuic-avatar-size-lg);
92
+ font-size: var(--stuic-avatar-font-size-lg);
93
+ }
94
+
95
+ .stuic-avatar[data-size="xl"] {
96
+ width: var(--stuic-avatar-size-xl);
97
+ height: var(--stuic-avatar-size-xl);
98
+ font-size: var(--stuic-avatar-font-size-xl);
99
+ }
100
+
101
+ .stuic-avatar[data-size="2xl"] {
102
+ width: var(--stuic-avatar-size-2xl);
103
+ height: var(--stuic-avatar-size-2xl);
104
+ font-size: var(--stuic-avatar-font-size-2xl);
105
+ }
106
+
107
+ /* ============================================================================
108
+ INTERACTIVE MODE (button)
109
+ ============================================================================ */
110
+
111
+ .stuic-avatar[data-interactive] {
112
+ cursor: pointer;
113
+ user-select: none;
114
+ -webkit-tap-highlight-color: transparent;
115
+ }
116
+
117
+ .stuic-avatar[data-interactive]:hover {
118
+ opacity: 0.85;
119
+ }
120
+
121
+ .stuic-avatar[data-interactive]:active {
122
+ opacity: 0.75;
123
+ }
124
+
125
+ /* Focus styles */
126
+ .stuic-avatar[data-interactive]:focus-visible {
127
+ outline: var(--stuic-avatar-ring-width) solid var(--stuic-avatar-ring-color);
128
+ outline-offset: var(--stuic-avatar-ring-offset);
129
+ }
130
+ }