@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
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from "svelte";
3
3
  import type { HTMLInputAttributes } from "svelte/elements";
4
+ import type { TypeaheadOptions } from "../../actions/typeahead.svelte.js";
4
5
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
5
6
  import type { THC } from "../Thc/Thc.svelte";
6
7
 
@@ -17,6 +18,7 @@
17
18
  tabindex?: number;
18
19
  renderSize?: "sm" | "md" | "lg" | string;
19
20
  useTrim?: boolean;
21
+ useTypeahead?: boolean | Omit<TypeaheadOptions, "enabled">;
20
22
  required?: boolean;
21
23
  disabled?: boolean;
22
24
  validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
@@ -42,6 +44,7 @@
42
44
 
43
45
  <script lang="ts">
44
46
  import { trim } from "../../actions/trim.svelte.js";
47
+ import { typeahead } from "../../actions/typeahead.svelte.js";
45
48
  import {
46
49
  validate as validateAction,
47
50
  type ValidationResult,
@@ -61,6 +64,7 @@
61
64
  class: classProp,
62
65
  renderSize = "md",
63
66
  useTrim = true,
67
+ useTypeahead,
64
68
  //
65
69
  required = false,
66
70
  disabled = false,
@@ -126,11 +130,15 @@
126
130
  bind:this={input}
127
131
  {type}
128
132
  {id}
129
- class={twMerge("form-input", renderSize, classInput)}
133
+ class={twMerge(classInput)}
130
134
  use:trim={() => ({
131
135
  enabled: useTrim,
132
136
  setValue: (v: string) => useTrim && (value = v),
133
137
  })}
138
+ use:typeahead={() => ({
139
+ enabled: !!useTypeahead,
140
+ ...(typeof useTypeahead === "boolean" ? {} : useTypeahead),
141
+ })}
134
142
  use:validateAction={() => ({
135
143
  enabled: !!validate,
136
144
  ...(typeof validate === "boolean" ? {} : validate),
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { HTMLInputAttributes } from "svelte/elements";
3
+ import type { TypeaheadOptions } from "../../actions/typeahead.svelte.js";
3
4
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
5
  import type { THC } from "../Thc/Thc.svelte";
5
6
  type SnippetWithId = Snippet<[{
@@ -16,6 +17,7 @@ export interface Props extends HTMLInputAttributes, Record<string, any> {
16
17
  tabindex?: number;
17
18
  renderSize?: "sm" | "md" | "lg" | string;
18
19
  useTrim?: boolean;
20
+ useTypeahead?: boolean | Omit<TypeaheadOptions, "enabled">;
19
21
  required?: boolean;
20
22
  disabled?: boolean;
21
23
  validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
@@ -228,10 +228,13 @@
228
228
  }
229
229
 
230
230
  function getIconThc(isSelected: boolean): { html: string } {
231
+ const size = 19;
231
232
  if (isMultiple) {
232
- return { html: isSelected ? iconCheckboxCheck() : iconCheckboxEmpty() };
233
+ return {
234
+ html: isSelected ? iconCheckboxCheck({ size }) : iconCheckboxEmpty({ size }),
235
+ };
233
236
  }
234
- return { html: isSelected ? iconRadioCheck() : iconRadioEmpty() };
237
+ return { html: isSelected ? iconRadioCheck({ size }) : iconRadioEmpty({ size }) };
235
238
  }
236
239
 
237
240
  function sortFn(a: Item, b: Item) {
@@ -542,7 +545,7 @@
542
545
  let extra = '';
543
546
  if (vals.length > limit) {
544
547
  vals = vals.slice(0, limit);
545
- extra = `, ... <span class="text-sm opacity-75">(+${(origLength - limit)})</span>`;
548
+ extra = `, ... <span class="text-sm stuic-field-options-muted">(+${(origLength - limit)})</span>`;
546
549
  }
547
550
  return vals.filter(v => v != null).map(_renderOptionLabel).join(", ") + extra;
548
551
  } catch (e) {
@@ -558,7 +561,7 @@
558
561
  bind:this={modalDialog}
559
562
  preEscapeClose={escape}
560
563
  classDialog="items-start"
561
- class="w-full max-w-2xl bg-transparent pointer-events-none"
564
+ class={twMerge("w-full max-w-2xl bg-transparent! shadow-none! pointer-events-none")}
562
565
  ariaLabelledby={id}
563
566
  {noScrollLock}
564
567
  >
@@ -569,8 +572,8 @@
569
572
  class={twMerge("m-2 mb-12 shadow-xl", classModalField)}
570
573
  classInputBoxWrap={twMerge(
571
574
  // always look like focused
572
- `border border-input-accent dark:border-input-accent-dark`,
573
- `ring-input-accent/20 dark:ring-input-accent-dark/20 ring-4`
575
+ `border border-(--stuic-input-accent)`,
576
+ `ring-(--stuic-input-accent)/20 ring-4`
574
577
  )}
575
578
  {id}
576
579
  {required}
@@ -580,7 +583,7 @@
580
583
  bind:this={input}
581
584
  {type}
582
585
  {id}
583
- class={twMerge("form-input", renderSize, classInput)}
586
+ class={twMerge(renderSize, classInput)}
584
587
  tabindex={1}
585
588
  {required}
586
589
  {disabled}
@@ -599,16 +602,16 @@
599
602
  />
600
603
 
601
604
  {#snippet inputBelow()}
602
- <div class="h-full border-t p-2 border-black/20">
605
+ <div
606
+ class="h-full border-t p-2"
607
+ style="border-color: var(--stuic-field-options-divider);"
608
+ >
603
609
  <div class="text-sm -mt-1 flex items-center">
604
610
  {#if isMultiple}
605
611
  <button
606
612
  type="button"
607
613
  onclick={() => _selectedColl.addMany(options.items)}
608
- class={twMerge(
609
- "control flex items-center p-1 m-1 text-sm opacity-75 underline rounded",
610
- "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
611
- )}
614
+ class="control flex items-center p-1 m-1 text-sm underline rounded stuic-field-options-control"
612
615
  tabindex={4}
613
616
  disabled={!options.size}
614
617
  >
@@ -621,11 +624,8 @@
621
624
  _selectedColl.clear();
622
625
  input?.focus();
623
626
  }}
624
- class={twMerge(
625
- "control flex items-center p-1 m-1 text-sm opacity-75 underline rounded",
626
- "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
627
- )}
628
- class:opacity-20={!selected.items.length}
627
+ class="control flex items-center p-1 m-1 text-sm underline rounded stuic-field-options-control"
628
+ data-disabled={!selected.items.length || undefined}
629
629
  tabindex={5}
630
630
  disabled={!selected.items.length}
631
631
  >
@@ -634,7 +634,7 @@
634
634
 
635
635
  <span class="p-1 m-1 text-sm">&nbsp;</span>
636
636
  <span
637
- class="flex-1 block justify-end opacity-75 text-right text-xs p-1 pr-2"
637
+ class="flex-1 block justify-end text-right text-xs p-1 pr-2 stuic-field-options-muted"
638
638
  >
639
639
  {selected.items.length}
640
640
  {#if cardinality > 0 && cardinality < Infinity}
@@ -655,11 +655,15 @@
655
655
  tabindex="-1"
656
656
  >
657
657
  {#if isFetching && !options.items.length}
658
- <div class="flex opacity-50 text-sm h-full items-center justify-center">
658
+ <div
659
+ class="flex text-sm h-full items-center justify-center stuic-field-options-placeholder"
660
+ >
659
661
  <Spinner class="w-4" />
660
662
  </div>
661
663
  {:else if !options.items.length && !allowUnknown}
662
- <div class="flex opacity-50 text-sm h-full items-center justify-center">
664
+ <div
665
+ class="flex text-sm h-full items-center justify-center stuic-field-options-placeholder"
666
+ >
663
667
  {@html t("no_results")}
664
668
  </div>
665
669
  {/if}
@@ -682,8 +686,7 @@
682
686
  {#if _optgroup}
683
687
  <div
684
688
  class={twMerge(
685
- "mb-1 p-1 text-xs font-semibold uppercase tracking-wide",
686
- "text-neutral-500 dark:text-neutral-400",
689
+ "mb-1 p-1 text-xs font-semibold uppercase tracking-wide stuic-field-options-optgroup",
687
690
  classOptgroup
688
691
  )}
689
692
  >
@@ -717,7 +720,9 @@
717
720
  active={isSelected}
718
721
  focused={active}
719
722
  contentBefore={showIcons ? getIconThc(isSelected) : undefined}
720
- classContentBefore={isSelected ? "opacity-100" : "opacity-50"}
723
+ classContentBefore={isSelected
724
+ ? "stuic-field-options-icon stuic-field-options-icon--selected"
725
+ : "stuic-field-options-icon"}
721
726
  class={classOption}
722
727
  classActive={classOptionActive}
723
728
  classFocused={classOptionActive}
@@ -734,7 +739,7 @@
734
739
  </div>
735
740
  <!-- {/if} -->
736
741
  <div class="p-2 px-3 flex items-end justify-between">
737
- <div class="text-xs opacity-75">
742
+ <div class="text-xs stuic-field-options-muted">
738
743
  <!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
739
744
  {#if allowUnknown}
740
745
  {@html t("unknown_allowed")}
@@ -746,7 +751,7 @@
746
751
  <Button
747
752
  class="control"
748
753
  type="button"
749
- variant="primary"
754
+ intent="primary"
750
755
  onclick={async (e) => {
751
756
  e.preventDefault();
752
757
  try_submit(true);
@@ -761,22 +766,20 @@
761
766
  {/snippet}
762
767
 
763
768
  {#snippet inputAfter()}
764
- <div class="flex pl-2 items-center justify-center opacity-50">
769
+ <div
770
+ class="flex pl-2 items-center justify-center stuic-field-options-placeholder"
771
+ >
765
772
  {#if isFetching}
766
773
  <Spinner class="w-4" />
767
774
  {/if}
768
775
  </div>
769
776
  <div class="flex pl-2 pr-1 items-center justify-center">
770
- <button
777
+ <Button
778
+ x
779
+ variant="ghost"
780
+ roundedFull
771
781
  type="button"
772
- class={twMerge(
773
- "opacity-75 rounded",
774
- "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
775
- "focus-visible:opacity-100 focus-visible:outline-0",
776
- "focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
777
- )}
778
- use:tooltip
779
- aria-label={t("x_close")}
782
+ tooltip={t("x_close")}
780
783
  onclick={(e) => {
781
784
  e.preventDefault();
782
785
  if (innerValue.trim() == "") {
@@ -787,14 +790,14 @@
787
790
  input?.focus();
788
791
  }}
789
792
  tabindex={2}
790
- >
791
- <X class="m-2 size-6" />
792
- </button>
793
+ />
793
794
  </div>
794
795
  {/snippet}
795
796
 
796
797
  {#snippet inputBefore()}
797
- <div class="flex flex-col items-center justify-center pl-3 opacity-75">
798
+ <div
799
+ class="flex flex-col items-center justify-center pl-3 stuic-field-options-muted"
800
+ >
798
801
  {@html iconSearch({ size: 19, strokeWidth: 3 })}
799
802
  </div>
800
803
  {/snippet}
@@ -63,30 +63,23 @@
63
63
  let validation = $state<ValidationResult | undefined>();
64
64
  let invalid = $derived(validation && !validation?.valid);
65
65
 
66
- //
67
- let _classCommon = $derived(
68
- [invalid && "invalid", disabled && "disabled", required && "required", renderSize]
69
- .filter(Boolean)
70
- .join(" ")
71
- );
72
66
  // $inspect(value);
73
67
  </script>
74
68
 
75
69
  {#if _options.length}
76
- <div class={twMerge("stuic-radios", _classCommon)} {style}>
70
+ <div
71
+ class={twMerge("stuic-radios", invalid && "invalid", disabled && "disabled")}
72
+ data-size={renderSize}
73
+ {style}
74
+ >
77
75
  <div
78
76
  class={twMerge(
79
77
  "radios-box",
80
- _classCommon,
81
- "gap-y-2 grid rounded-md p-2 mb-8",
82
- "border border-neutral-300 dark:border-neutral-600",
83
- "bg-neutral-100 dark:bg-neutral-700",
84
- invalid && "border-input-accent-invalid dark:border-input-accent-invalid-dark",
78
+ "gap-y-2 grid p-2 mb-8",
85
79
  classProp
86
80
  )}
87
81
  >
88
82
  {#each _options as o, i}
89
- <!-- value={o.value || o.label} -->
90
83
  <FieldRadioInternal
91
84
  {name}
92
85
  bind:group={value}
@@ -114,9 +107,7 @@
114
107
  transition:slide={{ duration: 150 }}
115
108
  class={twMerge(
116
109
  "validation-box",
117
- _classCommon,
118
- `mt-1 px-2 text-sm text-input-accent dark:text-input-accent-dark
119
- tracking-tight`,
110
+ "mt-1 px-2 text-sm tracking-tight",
120
111
  classValidationBox
121
112
  )}
122
113
  >
@@ -137,7 +137,7 @@
137
137
  bind:value
138
138
  bind:this={input}
139
139
  {id}
140
- class={twMerge("form-input", renderSize, classInput)}
140
+ class={twMerge(classInput)}
141
141
  use:validateAction={() => ({
142
142
  enabled: !!validate,
143
143
  ...(typeof validate === "boolean" ? {} : validate),
@@ -111,14 +111,10 @@
111
111
  {classLabel}
112
112
  {classLabelBox}
113
113
  {classInputBox}
114
- classInputBoxWrap={twMerge(
115
- "border-0 focus-within:border-0 focus-within:dark:border-0 focus-within:ring-0",
116
- "bg-transparent dark:bg-transparent",
117
- classInputBoxWrap
118
- )}
119
114
  {classDescBox}
120
115
  {classBelowBox}
121
116
  {validation}
117
+ classInputBoxWrap={twMerge("input-wrap-transparent", classInputBoxWrap)}
122
118
  {style}
123
119
  >
124
120
  <Switch bind:checked {name} {required} {disabled} {validate} {setValidationResult} />
@@ -125,7 +125,7 @@
125
125
  bind:value
126
126
  bind:this={input}
127
127
  {id}
128
- class={twMerge("form-input min-h-16", renderSize, classInput)}
128
+ class={twMerge("min-h-16", classInput)}
129
129
  use:trim={() => ({
130
130
  enabled: useTrim,
131
131
  setValue: (v: string) => (value = v),
@@ -232,3 +232,197 @@ Validation is handled by the `validate` action. Pass `validate={true}` for defau
232
232
  }}
233
233
  />
234
234
  ```
235
+
236
+ ## CSS Variables
237
+
238
+ ### Component Tokens
239
+
240
+ Override globally in `:root` or locally via `style` prop:
241
+
242
+ | Variable | Default | Description |
243
+ |----------|---------|-------------|
244
+ | `--stuic-input-radius` | `--radius-md` | Border radius |
245
+ | `--stuic-input-font-family` | `--font-sans` | Font family |
246
+ | `--stuic-input-transition` | `150ms` | Transition duration |
247
+ | `--stuic-input-ring-width` | `4px` | Focus ring width |
248
+ | `--stuic-input-ring-color` | `--stuic-color-ring` | Focus ring color |
249
+ | `--stuic-input-accent` | `--stuic-color-primary` | Accent color |
250
+ | `--stuic-input-accent-error` | `--stuic-color-destructive` | Error/validation color |
251
+
252
+ ### Color Tokens
253
+
254
+ | Variable | Default | Description |
255
+ |----------|---------|-------------|
256
+ | `--stuic-input-bg` | `--stuic-color-input` | Background color |
257
+ | `--stuic-input-border` | `--stuic-color-border` | Border color |
258
+ | `--stuic-input-border-focus` | `--stuic-input-accent` | Border color on focus |
259
+ | `--stuic-input-text` | `--stuic-color-foreground` | Text color |
260
+ | `--stuic-input-placeholder` | `--stuic-color-muted-foreground` | Placeholder color |
261
+
262
+ ### Size Tokens
263
+
264
+ Each size (sm, md, lg) has corresponding tokens:
265
+
266
+ | Variable | sm | md | lg |
267
+ |----------|----|----|-----|
268
+ | `--stuic-input-padding-x-{size}` | `calc(--spacing * 2.5)` | `calc(--spacing * 3)` | `calc(--spacing * 4)` |
269
+ | `--stuic-input-padding-y-{size}` | `calc(--spacing * 2)` | `calc(--spacing * 2.5)` | `calc(--spacing * 3)` |
270
+ | `--stuic-input-font-size-{size}` | `--text-sm` | `--text-base` | `--text-lg` |
271
+ | `--stuic-input-min-height-{size}` | `2.5rem` | `2.75rem` | `3rem` |
272
+
273
+ ### Checkbox/Radio Tokens
274
+
275
+ | Variable | Default | Description |
276
+ |----------|---------|-------------|
277
+ | `--stuic-checkbox-size` | `1.25rem` | Checkbox size |
278
+ | `--stuic-checkbox-radius` | `--radius-sm` | Checkbox border radius |
279
+ | `--stuic-radio-size` | `1rem` | Radio button size |
280
+ | `--stuic-checkbox-bg` | `--stuic-color-muted` | Unchecked background |
281
+ | `--stuic-checkbox-border` | `--stuic-color-border` | Unchecked border |
282
+ | `--stuic-checkbox-checked-bg` | `--stuic-input-accent` | Checked background |
283
+ | `--stuic-checkbox-checked-border` | `--stuic-input-accent` | Checked border |
284
+
285
+ ### Range Input Tokens
286
+
287
+ | Variable | Default | Description |
288
+ |----------|---------|-------------|
289
+ | `--stuic-input-range-thumb-size` | `18px` | Slider thumb size |
290
+ | `--stuic-input-range-track-height` | `4px` | Track height |
291
+ | `--stuic-input-range-track-radius` | `18px` | Track border radius |
292
+ | `--stuic-input-range-track-bg` | `--stuic-color-muted` | Track background |
293
+
294
+ ### Customization Examples
295
+
296
+ ```css
297
+ /* Global: Make all inputs have pill shape */
298
+ :root {
299
+ --stuic-input-radius: 9999px;
300
+ }
301
+
302
+ /* Global: Custom accent color */
303
+ :root {
304
+ --stuic-input-accent: var(--color-violet-600);
305
+ }
306
+ ```
307
+
308
+ ```svelte
309
+ <!-- Local: Custom radius for one input -->
310
+ <FieldInput
311
+ label="Search"
312
+ style="--stuic-input-radius: 9999px;"
313
+ />
314
+ ```
315
+
316
+ ## Data Attributes
317
+
318
+ Components use data attributes for CSS styling:
319
+
320
+ - `data-size` - The size value ("sm", "md", "lg")
321
+
322
+ ## Class Props
323
+
324
+ | Prop | Target |
325
+ |------|--------|
326
+ | `class` | Root container |
327
+ | `classInput` | Input element |
328
+ | `classLabel` | Label element |
329
+ | `classLabelBox` | Label container |
330
+ | `classInputBox` | Input container (outer) |
331
+ | `classInputBoxWrap` | Input wrapper (inner, has border) |
332
+ | `classInputBoxWrapInvalid` | Input wrapper when invalid |
333
+ | `classDescBox` | Description container |
334
+ | `classBelowBox` | Below slot container |
335
+
336
+ ---
337
+
338
+ ## FieldOptions
339
+
340
+ A modal-based multi-select/single-select component with search functionality, typeahead support, and option grouping.
341
+
342
+ ### Props
343
+
344
+ | Prop | Type | Default | Description |
345
+ |------|------|---------|-------------|
346
+ | `value` | `string` | `"[]"` | JSON array of selected items (bindable) |
347
+ | `name` | `string` | - | Form field name |
348
+ | `getOptions` | `(q: string, current: Item[]) => Promise<{found: Item[]}>` | - | Async function to fetch options |
349
+ | `cardinality` | `number` | `Infinity` | Max selections (-1 for unlimited) |
350
+ | `allowUnknown` | `boolean` | `false` | Allow typing custom values |
351
+ | `renderOptionLabel` | `(item: Item) => string` | - | Custom option label renderer |
352
+ | `renderOptionGroup` | `(s: string) => string` | - | Custom optgroup label renderer |
353
+ | `renderValue` | `(stringifiedItems: string) => string` | - | Custom value display renderer |
354
+ | `showIconsCheckbox` | `boolean` | `true` | Show checkbox icons in multi-select |
355
+ | `showIconsRadio` | `boolean` | `false` | Show radio icons in single-select |
356
+ | `searchPlaceholder` | `string` | - | Custom search placeholder |
357
+ | `itemIdPropName` | `string` | `"id"` | Property name for item ID |
358
+ | `notifications` | `NotificationsStack` | - | Notification handler for errors |
359
+
360
+ ### Class Props
361
+
362
+ | Prop | Target |
363
+ |------|--------|
364
+ | `classOption` | Option item (ListItemButton) |
365
+ | `classOptionActive` | Active/selected option |
366
+ | `classOptgroup` | Option group label |
367
+ | `classModalField` | Modal field wrapper |
368
+
369
+ ### CSS Variables
370
+
371
+ #### Component Tokens
372
+
373
+ | Variable | Default | Description |
374
+ |----------|---------|-------------|
375
+ | `--stuic-field-options-divider` | `--stuic-color-border` | Divider/separator color |
376
+ | `--stuic-field-options-control-text` | `--stuic-color-muted-foreground` | Control button text color |
377
+ | `--stuic-field-options-control-text-hover` | `--stuic-color-foreground` | Control button hover text color |
378
+ | `--stuic-field-options-control-ring` | `--stuic-color-ring` | Control button focus ring |
379
+ | `--stuic-field-options-muted-text` | `--stuic-color-muted-foreground` | Muted/secondary text color |
380
+ | `--stuic-field-options-optgroup-text` | `--stuic-color-muted-foreground` | Option group label color |
381
+
382
+ ### Usage
383
+
384
+ ```svelte
385
+ <script lang="ts">
386
+ import { FieldOptions } from 'stuic';
387
+
388
+ let value = $state('[]');
389
+
390
+ async function getOptions(query: string, current: any[]) {
391
+ const response = await fetch(`/api/search?q=${query}`);
392
+ const data = await response.json();
393
+ return { found: data.items };
394
+ }
395
+ </script>
396
+
397
+ <FieldOptions
398
+ label="Select Tags"
399
+ name="tags"
400
+ bind:value
401
+ {getOptions}
402
+ cardinality={5}
403
+ allowUnknown
404
+ />
405
+ ```
406
+
407
+ ### Customization Examples
408
+
409
+ ```css
410
+ /* Make optgroup labels more prominent */
411
+ :root {
412
+ --stuic-field-options-optgroup-text: var(--stuic-color-primary);
413
+ }
414
+
415
+ /* Custom control button colors */
416
+ :root {
417
+ --stuic-field-options-control-text: var(--stuic-color-primary);
418
+ --stuic-field-options-control-text-hover: var(--stuic-color-primary-hover);
419
+ }
420
+ ```
421
+
422
+ ```svelte
423
+ <!-- Local customization -->
424
+ <FieldOptions
425
+ style="--stuic-field-options-divider: var(--color-red-500);"
426
+ ...
427
+ />
428
+ ```
@@ -69,21 +69,7 @@
69
69
  let id = getId();
70
70
  let idDesc = getId();
71
71
 
72
- //
73
- let _classCommon = $derived(
74
- [invalid && "invalid", disabled && "disabled", renderSize].filter(Boolean).join(" ")
75
- );
76
72
 
77
- const _preset = {
78
- labelBox: {
79
- label: {
80
- size: {
81
- sm: "text-sm mt-0.5",
82
- lg: "font-semibold",
83
- } as any,
84
- },
85
- },
86
- };
87
73
  </script>
88
74
 
89
75
  {#snippet snippetOrThc({ id, value }: { id: string; value?: SnippetWithId | THC })}
@@ -97,7 +83,6 @@
97
83
  <label
98
84
  class={twMerge(
99
85
  "radio-box",
100
- _classCommon,
101
86
  "flex items-start cursor-pointer pr-1",
102
87
  disabled && "cursor-not-allowed",
103
88
  classRadioBox
@@ -106,7 +91,6 @@
106
91
  <div
107
92
  class={twMerge(
108
93
  "input-box",
109
- _classCommon,
110
94
  "flex h-6 items-center ml-1",
111
95
  classInputBox
112
96
  )}
@@ -118,26 +102,7 @@
118
102
  bind:group
119
103
  {value}
120
104
  {name}
121
- class={twMerge(
122
- _classCommon,
123
- `size-4 rounded-full
124
- bg-neutral-100
125
- border-neutral-300
126
- text-input-accent dark:text-input-accent-dark
127
- cursor-pointer
128
-
129
- checked:border-input-accent checked:bg-input-accent
130
- checked:dark:border-input-accent-dark checked:dark:bg-input-accent-dark
131
-
132
- focus:border-input-accent
133
- focus:ring-4
134
- focus:ring-offset-0
135
- focus:ring-input-accent/20 focus:dark:ring-input-accent-dark/20
136
-
137
- disabled:cursor-not-allowed`,
138
- disabled && "cursor-not-allowed",
139
- classInput
140
- )}
105
+ class={twMerge(classInput)}
141
106
  aria-describedby={description ? idDesc : undefined}
142
107
  use:validateAction={() => ({
143
108
  enabled: !!validate,
@@ -149,14 +114,12 @@
149
114
  {...rest}
150
115
  />
151
116
  </div>
152
- <div class={twMerge("label-box", _classCommon, "ml-3 w-full", classLabelBox)}>
117
+ <div class={twMerge("label-box", "ml-3 w-full", classLabelBox)}>
153
118
  {#if label}
154
119
  <div
155
120
  class={twMerge(
156
121
  "label",
157
- _classCommon,
158
122
  "block w-full cursor-pointer",
159
- renderSize && _preset.labelBox.label.size[renderSize],
160
123
  required && "after:content-['*'] after:opacity-40 after:pl-1",
161
124
  classLabel
162
125
  )}
@@ -173,7 +136,6 @@
173
136
  id={idDesc}
174
137
  class={twMerge(
175
138
  "desc-box",
176
- _classCommon,
177
139
  "text-sm opacity-50 cursor-pointer font-normal",
178
140
  disabled && "cursor-not-allowed",
179
141
  classDescBox