@marianmeres/stuic 2.63.0 → 2.65.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.
@@ -9,6 +9,7 @@
9
9
  import { Debounced, watch } from "runed";
10
10
  import { NotificationsStack } from "../Notifications/index.js";
11
11
  import { Spinner } from "../Spinner/index.js";
12
+ import { ListItemButton } from "../ListItemButton/index.js";
12
13
  import { strHash } from "../../utils/str-hash.js";
13
14
  import { qsa } from "../../utils/qsa.js";
14
15
  import { replaceMap } from "../../utils/index.js";
@@ -61,6 +62,8 @@
61
62
  </script>
62
63
 
63
64
  <script lang="ts">
65
+ import "./index.css";
66
+
64
67
  const clog = createClog("CommandMenu");
65
68
 
66
69
  let {
@@ -242,7 +245,9 @@
242
245
  autocomplete="off"
243
246
  aria-autocomplete="list"
244
247
  aria-controls={options.size ? listId : undefined}
245
- aria-activedescendant={options.active ? btn_id(options.active[itemIdPropName]) : undefined}
248
+ aria-activedescendant={options.active
249
+ ? btn_id(options.active[itemIdPropName])
250
+ : undefined}
246
251
  placeholder={searchPlaceholder ?? t("search_placeholder")}
247
252
  classInputBoxWrap={twMerge(
248
253
  // always look like focused
@@ -298,6 +303,7 @@
298
303
  {#if options.size}
299
304
  <div
300
305
  class={twMerge(
306
+ "stuic-command-menu-options",
301
307
  "options block space-y-1 p-1",
302
308
  "overflow-y-auto overflow-x-hidden mb-1",
303
309
  "border-t border-black/20",
@@ -314,43 +320,33 @@
314
320
  <div class="p-1">
315
321
  {#if _optgroup}
316
322
  <div
317
- class="text-sm capitalize opacity-50 border-b border-black/10 mb-1 p-1"
323
+ class={[
324
+ "mb-1 p-1 text-xs font-semibold uppercase tracking-wide",
325
+ "text-neutral-500 dark:text-neutral-400",
326
+ ]}
318
327
  >
319
328
  {_optgroup}
320
329
  </div>
321
330
  {/if}
322
- <ul role="presentation">
331
+ <ul role="presentation" class="space-y-1">
323
332
  {#each _opts as item (item[itemIdPropName])}
324
333
  {@const active =
325
334
  item[itemIdPropName] === options.active?.[itemIdPropName]}
326
335
  <!-- {@const isSelected = false} -->
327
336
  <li class:active role="presentation">
328
- <button
329
- class:active
330
- type="button"
337
+ <ListItemButton
338
+ id={btn_id(item[itemIdPropName])}
331
339
  role="option"
332
340
  aria-selected={active}
333
- class={twMerge(
334
- "no-focus-visible",
335
- "text-left rounded-md py-2 px-2.5",
336
- "min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap",
337
- "border border-transparent",
338
- "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
339
- "focus-visible:outline-0 focus-visible:ring-0",
340
- "hover:border-neutral-400 dark:hover:border-neutral-500",
341
- active && "bg-neutral-200 dark:bg-neutral-800",
342
- classOption,
343
- active && classOptionActive
344
- )}
345
- id={btn_id(item[itemIdPropName])}
346
- tabindex="-1"
341
+ {active}
342
+ tabindex={-1}
343
+ class={classOption}
344
+ classActive={classOptionActive}
347
345
  onclick={() => {
348
346
  _optionsColl.setActive(item);
349
347
  submit();
350
348
  }}
351
349
  onkeydown={(e) => {
352
- // need to handle tab here, because the tabindex="-1" is ignored
353
- // in the focus-trap selectors... so, on Tab, manually focusin input
354
350
  if (e.key === "Tab") {
355
351
  e.preventDefault();
356
352
  input?.focus();
@@ -358,7 +354,7 @@
358
354
  }}
359
355
  >
360
356
  {_renderOptionLabel(item)}
361
- </button>
357
+ </ListItemButton>
362
358
  </li>
363
359
  {/each}
364
360
  </ul>
@@ -17,6 +17,7 @@ export interface Props {
17
17
  classOptionActive?: string;
18
18
  showAllOnEmptyQ?: boolean;
19
19
  }
20
+ import "./index.css";
20
21
  declare const CommandMenu: import("svelte").Component<Props, {
21
22
  close: () => void;
22
23
  open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
@@ -0,0 +1,3 @@
1
+ /* Override ListItemButton defaults for CommandMenu context */
2
+ .stuic-command-menu-options .stuic-list-item-button {
3
+ }
@@ -37,10 +37,10 @@
37
37
  type: "action";
38
38
  /** Label displayed - supports THC for icons, HTML, etc. */
39
39
  label: THC;
40
- /** Shortcut hint displayed on the right (e.g., "Cmd+K") */
41
- shortcut?: string;
42
- /** Icon displayed before label - supports THC */
43
- icon?: THC;
40
+ /** Content displayed before label (e.g., icon) - supports THC */
41
+ contentBefore?: THC;
42
+ /** Content displayed after label (e.g., shortcut hint, badge) - supports THC */
43
+ contentAfter?: THC;
44
44
  /** Callback when item is selected */
45
45
  onSelect?: () => void | boolean;
46
46
  }
@@ -74,7 +74,8 @@
74
74
  type: "expandable";
75
75
  /** Label for the expandable header */
76
76
  label: THC;
77
- icon?: THC;
77
+ /** Content displayed before label (e.g., icon) - supports THC */
78
+ contentBefore?: THC;
78
79
  /** Nested items (single level only - no nested expandables) */
79
80
  items: DropdownMenuFlatItem[];
80
81
  /** Whether section starts expanded */
@@ -130,6 +131,10 @@
130
131
  classItemActive?: string;
131
132
  /** Classes for disabled items */
132
133
  classItemDisabled?: string;
134
+ /** Classes for content before label (contentBefore slot) */
135
+ classItemBefore?: string;
136
+ /** Classes for content after label (contentAfter slot) */
137
+ classItemAfter?: string;
133
138
  /** Classes for dividers */
134
139
  classDivider?: string;
135
140
  /** Classes for header items */
@@ -226,20 +231,6 @@
226
231
  min-w-48
227
232
  `;
228
233
 
229
- export const DROPDOWN_MENU_ITEM_CLASSES = `
230
- w-full
231
- flex items-center gap-2
232
- px-3 py-1.5
233
- min-h-[44px]
234
- text-left
235
- rounded-md
236
- cursor-pointer
237
- touch-action-manipulation
238
- hover:bg-neutral-100 dark:hover:bg-neutral-700
239
- focus:outline-none
240
- focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600
241
- `;
242
-
243
234
  export const DROPDOWN_MENU_DIVIDER_CLASSES = `
244
235
  h-px my-1
245
236
  bg-neutral-200 dark:bg-neutral-700
@@ -269,6 +260,7 @@
269
260
  import { slide, fade } from "svelte/transition";
270
261
  import { untrack } from "svelte";
271
262
  import Thc from "../Thc/Thc.svelte";
263
+ import ListItemButton from "../ListItemButton/ListItemButton.svelte";
272
264
  import "./index.css";
273
265
  import { BodyScroll } from "../../utils/body-scroll-locker.js";
274
266
  import { waitForTwoRepaints } from "../../utils/paint.js";
@@ -289,6 +281,8 @@
289
281
  classItem,
290
282
  classItemActive,
291
283
  classItemDisabled,
284
+ classItemBefore,
285
+ classItemAfter,
292
286
  classDivider,
293
287
  classHeader,
294
288
  classExpandable,
@@ -698,40 +692,23 @@
698
692
  {#each items as item}
699
693
  {#if item.type === "action"}
700
694
  {@const isActive = _navItems.active?.id === item.id}
701
- <button
695
+ <ListItemButton
702
696
  id={itemId(item.id)}
703
697
  role="menuitem"
704
- class={twMerge(
705
- DROPDOWN_MENU_ITEM_CLASSES,
706
- classItem,
707
- item.class,
708
- isActive && "bg-neutral-200 dark:bg-neutral-600",
709
- isActive && classItemActive,
710
- item.disabled && "opacity-50 cursor-not-allowed pointer-events-none",
711
- item.disabled && classItemDisabled
712
- )}
698
+ focused={isActive}
699
+ contentBefore={item.contentBefore}
700
+ contentAfter={item.contentAfter}
701
+ class={twMerge(classItem, item.class)}
702
+ classFocused={classItemActive}
703
+ classContentBefore={classItemBefore}
704
+ classContentAfter={classItemAfter}
713
705
  onclick={() => selectItem(item)}
714
706
  onmouseenter={() => navItems.setActive(item)}
715
- aria-disabled={item.disabled || undefined}
707
+ disabled={item.disabled}
716
708
  tabindex={-1}
717
- type="button"
718
709
  >
719
- {#if item.icon}
720
- <span class="shrink-0">
721
- <Thc thc={item.icon} />
722
- </span>
723
- {/if}
724
- <span class="flex-1">
725
- <Thc thc={item.label} />
726
- </span>
727
- {#if item.shortcut}
728
- <span
729
- class="text-xs text-dropdown-header dark:text-dropdown-header-dark ml-auto"
730
- >
731
- {item.shortcut}
732
- </span>
733
- {/if}
734
- </button>
710
+ <Thc thc={item.label} />
711
+ </ListItemButton>
735
712
  {:else if item.type === "divider"}
736
713
  <div
737
714
  role="separator"
@@ -755,19 +732,16 @@
755
732
  _navItems.active?.id === item.id}
756
733
  <div role="group" aria-labelledby={expandableHeaderId(item.id)}>
757
734
  <!-- Expandable header -->
758
- <button
735
+ <ListItemButton
759
736
  id={expandableHeaderId(item.id)}
760
737
  role="menuitem"
761
- class={twMerge(
762
- DROPDOWN_MENU_ITEM_CLASSES,
763
- "font-medium",
764
- classExpandable,
765
- item.class,
766
- isExpandableActive && "bg-neutral-200 dark:bg-neutral-600",
767
- isExpandableActive && classItemActive,
768
- item.disabled && "opacity-50 cursor-not-allowed pointer-events-none",
769
- item.disabled && classItemDisabled
770
- )}
738
+ focused={isExpandableActive}
739
+ contentBefore={item.contentBefore}
740
+ contentAfter={{ html: iconChevronRight({ size: 16 }) }}
741
+ class={twMerge("font-medium", classExpandable, item.class)}
742
+ classFocused={classItemActive}
743
+ classContentBefore={classItemBefore}
744
+ classContentAfter={twMerge("transition-transform", isExpanded && "rotate-90")}
771
745
  onclick={() => toggleExpanded(item.id)}
772
746
  onmouseenter={() =>
773
747
  navItems.setActive({
@@ -776,27 +750,11 @@
776
750
  expandableItem: item,
777
751
  })}
778
752
  aria-expanded={isExpanded}
779
- aria-disabled={item.disabled || undefined}
753
+ disabled={item.disabled}
780
754
  tabindex={-1}
781
- type="button"
782
755
  >
783
- {#if item.icon}
784
- <span class="shrink-0">
785
- <Thc thc={item.icon} />
786
- </span>
787
- {/if}
788
- <span class="flex-1">
789
- <Thc thc={item.label} />
790
- </span>
791
- <span
792
- class={twMerge(
793
- "transition-transform inline-block",
794
- isExpanded && "rotate-90"
795
- )}
796
- >
797
- {@html iconChevronRight({ size: 16 })}
798
- </span>
799
- </button>
756
+ <Thc thc={item.label} />
757
+ </ListItemButton>
800
758
 
801
759
  <!-- Expandable content -->
802
760
  {#if isExpanded}
@@ -810,41 +768,23 @@
810
768
  {#each item.items as childItem}
811
769
  {#if childItem.type === "action"}
812
770
  {@const isChildActive = _navItems.active?.id === childItem.id}
813
- <button
771
+ <ListItemButton
814
772
  id={itemId(childItem.id)}
815
773
  role="menuitem"
816
- class={twMerge(
817
- DROPDOWN_MENU_ITEM_CLASSES,
818
- classItem,
819
- childItem.class,
820
- isChildActive && "bg-neutral-200 dark:bg-neutral-600",
821
- isChildActive && classItemActive,
822
- childItem.disabled &&
823
- "opacity-50 cursor-not-allowed pointer-events-none",
824
- childItem.disabled && classItemDisabled
825
- )}
774
+ focused={isChildActive}
775
+ contentBefore={childItem.contentBefore}
776
+ contentAfter={childItem.contentAfter}
777
+ class={twMerge(classItem, childItem.class)}
778
+ classFocused={classItemActive}
779
+ classContentBefore={classItemBefore}
780
+ classContentAfter={classItemAfter}
826
781
  onclick={() => selectItem(childItem)}
827
782
  onmouseenter={() => navItems.setActive(childItem)}
828
- aria-disabled={childItem.disabled || undefined}
783
+ disabled={childItem.disabled}
829
784
  tabindex={-1}
830
- type="button"
831
785
  >
832
- {#if childItem.icon}
833
- <span class="shrink-0">
834
- <Thc thc={childItem.icon} />
835
- </span>
836
- {/if}
837
- <span class="flex-1">
838
- <Thc thc={childItem.label} />
839
- </span>
840
- {#if childItem.shortcut}
841
- <span
842
- class="text-xs text-dropdown-header dark:text-dropdown-header-dark ml-auto"
843
- >
844
- {childItem.shortcut}
845
- </span>
846
- {/if}
847
- </button>
786
+ <Thc thc={childItem.label} />
787
+ </ListItemButton>
848
788
  {:else if childItem.type === "divider"}
849
789
  <div
850
790
  role="separator"
@@ -21,10 +21,10 @@ export interface DropdownMenuActionItem extends DropdownMenuItemBase {
21
21
  type: "action";
22
22
  /** Label displayed - supports THC for icons, HTML, etc. */
23
23
  label: THC;
24
- /** Shortcut hint displayed on the right (e.g., "Cmd+K") */
25
- shortcut?: string;
26
- /** Icon displayed before label - supports THC */
27
- icon?: THC;
24
+ /** Content displayed before label (e.g., icon) - supports THC */
25
+ contentBefore?: THC;
26
+ /** Content displayed after label (e.g., shortcut hint, badge) - supports THC */
27
+ contentAfter?: THC;
28
28
  /** Callback when item is selected */
29
29
  onSelect?: () => void | boolean;
30
30
  }
@@ -54,7 +54,8 @@ export interface DropdownMenuExpandableItem extends DropdownMenuItemBase {
54
54
  type: "expandable";
55
55
  /** Label for the expandable header */
56
56
  label: THC;
57
- icon?: THC;
57
+ /** Content displayed before label (e.g., icon) - supports THC */
58
+ contentBefore?: THC;
58
59
  /** Nested items (single level only - no nested expandables) */
59
60
  items: DropdownMenuFlatItem[];
60
61
  /** Whether section starts expanded */
@@ -102,6 +103,10 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
102
103
  classItemActive?: string;
103
104
  /** Classes for disabled items */
104
105
  classItemDisabled?: string;
106
+ /** Classes for content before label (contentBefore slot) */
107
+ classItemBefore?: string;
108
+ /** Classes for content after label (contentAfter slot) */
109
+ classItemAfter?: string;
105
110
  /** Classes for dividers */
106
111
  classDivider?: string;
107
112
  /** Classes for header items */
@@ -145,7 +150,6 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
145
150
  export declare const DROPDOWN_MENU_BASE_CLASSES = "stuic-dropdown-menu relative inline-block";
146
151
  export declare const DROPDOWN_MENU_TRIGGER_CLASSES = "\n\t\tinline-flex items-center justify-center gap-2\n\t\tpx-3 py-2\n\t\trounded-md border\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder-neutral-200 dark:border-neutral-700\n\t\thover:brightness-95 dark:hover:brightness-110\n\t\tcursor-pointer\n\t\t";
147
152
  export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "\n\t\tstuic-dropdown-menu-dropdown\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder border-neutral-200 dark:border-neutral-700\n\t\trounded-md shadow-sm\n\t\tp-1\n\t\toverflow-y-auto\n\t\tz-50\n\t\tmin-w-48\n\t";
148
- export declare const DROPDOWN_MENU_ITEM_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\tpx-3 py-1.5\n\t\tmin-h-[44px]\n\t\ttext-left \n\t\trounded-md\n\t\tcursor-pointer\n\t\ttouch-action-manipulation\n\t\thover:bg-neutral-100 dark:hover:bg-neutral-700\n\t\tfocus:outline-none\n\t\tfocus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600\n\t";
149
153
  export declare const DROPDOWN_MENU_DIVIDER_CLASSES = "\n\t\th-px my-1\n\t\tbg-neutral-200 dark:bg-neutral-700\n\t";
150
154
  export declare const DROPDOWN_MENU_HEADER_CLASSES = "\n\t\tpx-2 py-1.5\n\t\ttext-xs font-semibold uppercase tracking-wide\n\t\ttext-neutral-500 dark:text-neutral-400\n\t\tselect-none\n\t";
151
155
  export declare const DROPDOWN_MENU_BACKDROP_CLASSES = "\n\t\tstuic-dropdown-menu-backdrop\n\t\tfixed inset-0 bg-black/25\n\t\tz-40\n\t";
@@ -3,6 +3,14 @@
3
3
  scrollbar-width: thin;
4
4
  }
5
5
 
6
+ /* Override ListItemButton defaults for dropdown context */
7
+ .stuic-dropdown-menu-dropdown .stuic-list-item-button {
8
+ --color-lib-bg: transparent;
9
+ --color-lib-bg-dark: transparent;
10
+ --color-lib-border: transparent;
11
+ --color-lib-border-dark: transparent;
12
+ }
13
+
6
14
  @position-try --pop-top {
7
15
  position-area: top; /* above, centered */
8
16
  }
@@ -39,8 +47,8 @@
39
47
  /* order: try other bottom positions first, then top, then left/right */
40
48
  position-try-fallbacks:
41
49
  flip-inline, --pop-bottom-span-right, --pop-bottom-span-left, --pop-bottom,
42
- flip-block, --pop-top-span-right, --pop-top-span-left, --pop-top,
43
- --pop-left, --pop-right;
50
+ flip-block, --pop-top-span-right, --pop-top-span-left, --pop-top, --pop-left,
51
+ --pop-right;
44
52
  }
45
53
  }
46
54
 
@@ -1 +1 @@
1
- export { default as DropdownMenu, type Props as DropdownMenuProps, type DropdownMenuItem, type DropdownMenuActionItem, type DropdownMenuDividerItem, type DropdownMenuHeaderItem, type DropdownMenuCustomItem, type DropdownMenuExpandableItem, type DropdownMenuFlatItem, type DropdownMenuPosition, type NavigableItem, type NavigableExpandable, DROPDOWN_MENU_BASE_CLASSES, DROPDOWN_MENU_TRIGGER_CLASSES, DROPDOWN_MENU_DROPDOWN_CLASSES, DROPDOWN_MENU_ITEM_CLASSES, DROPDOWN_MENU_DIVIDER_CLASSES, DROPDOWN_MENU_HEADER_CLASSES, } from "./DropdownMenu.svelte";
1
+ export { default as DropdownMenu, type Props as DropdownMenuProps, type DropdownMenuItem, type DropdownMenuActionItem, type DropdownMenuDividerItem, type DropdownMenuHeaderItem, type DropdownMenuCustomItem, type DropdownMenuExpandableItem, type DropdownMenuFlatItem, type DropdownMenuPosition, type NavigableItem, type NavigableExpandable, DROPDOWN_MENU_BASE_CLASSES, DROPDOWN_MENU_TRIGGER_CLASSES, DROPDOWN_MENU_DROPDOWN_CLASSES, DROPDOWN_MENU_DIVIDER_CLASSES, DROPDOWN_MENU_HEADER_CLASSES, } from "./DropdownMenu.svelte";
@@ -1 +1 @@
1
- export { default as DropdownMenu, DROPDOWN_MENU_BASE_CLASSES, DROPDOWN_MENU_TRIGGER_CLASSES, DROPDOWN_MENU_DROPDOWN_CLASSES, DROPDOWN_MENU_ITEM_CLASSES, DROPDOWN_MENU_DIVIDER_CLASSES, DROPDOWN_MENU_HEADER_CLASSES, } from "./DropdownMenu.svelte";
1
+ export { default as DropdownMenu, DROPDOWN_MENU_BASE_CLASSES, DROPDOWN_MENU_TRIGGER_CLASSES, DROPDOWN_MENU_DROPDOWN_CLASSES, DROPDOWN_MENU_DIVIDER_CLASSES, DROPDOWN_MENU_HEADER_CLASSES, } from "./DropdownMenu.svelte";
@@ -23,6 +23,7 @@
23
23
  import X from "../X/X.svelte";
24
24
  import InputWrap from "./_internal/InputWrap.svelte";
25
25
  import FieldLikeButton from "./FieldLikeButton.svelte";
26
+ import ListItemButton from "../ListItemButton/ListItemButton.svelte";
26
27
 
27
28
  export interface Option {
28
29
  label: string;
@@ -33,6 +34,7 @@
33
34
 
34
35
  export interface Props extends Record<string, any> {
35
36
  trigger?: Snippet<[{ value: string; modal: ModalDialog }]>;
37
+ modal?: ModalDialog;
36
38
  input?: HTMLInputElement;
37
39
  value: string;
38
40
  label?: SnippetWithId | THC;
@@ -96,7 +98,7 @@
96
98
  clear_all: "Clear selected",
97
99
  clear: "Clear",
98
100
  search_placeholder: "Type to search...",
99
- search_submit_placeholder: "Type to search and/or submit...",
101
+ search_submit_placeholder: "Type to search and submit...",
100
102
  cardinality_full: "Max selection reached",
101
103
  select_from_list: "Please select from the list only",
102
104
  x_close: "Clear input or close [esc]",
@@ -123,6 +125,7 @@
123
125
 
124
126
  let {
125
127
  trigger,
128
+ modal = $bindable(),
126
129
  input = $bindable(),
127
130
  value = $bindable(), //
128
131
  label = "",
@@ -181,6 +184,10 @@
181
184
  }: Props = $props();
182
185
 
183
186
  let modalDialog: ModalDialog = $state()!;
187
+ // Sync internal modal state to bindable prop for external access
188
+ $effect(() => {
189
+ modal = modalDialog;
190
+ });
184
191
  let innerValue = $state("");
185
192
  let isFetching = $state(false);
186
193
  let isUnmounted = false;
@@ -220,6 +227,13 @@
220
227
  return renderOptionLabel?.(item) || `${item[itemIdPropName]}`;
221
228
  }
222
229
 
230
+ function getIconThc(isSelected: boolean): { html: string } {
231
+ if (isMultiple) {
232
+ return { html: isSelected ? iconCheckboxCheck() : iconCheckboxEmpty() };
233
+ }
234
+ return { html: isSelected ? iconRadioCheck() : iconRadioEmpty() };
235
+ }
236
+
223
237
  function sortFn(a: Item, b: Item) {
224
238
  const withOptGroup = (i: Item) => `${i.optgroup || ""}__${_renderOptionLabel(i)}`;
225
239
  return withOptGroup(a).localeCompare(withOptGroup(b), undefined, {
@@ -418,16 +432,6 @@
418
432
 
419
433
  let groupedOptions = $derived(_normalize_and_group_options(options.items));
420
434
 
421
- const BTN_CLS = [
422
- "no-focus-visible",
423
- "text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
424
- "w-full",
425
- "border border-transparent",
426
- "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
427
- "focus-visible:outline-0 focus-visible:ring-0",
428
- "hover:border-neutral-400 dark:hover:border-neutral-500",
429
- ];
430
-
431
435
  // add new dance
432
436
  $effect(() => {
433
437
  if (addNewBtn && isAddNewBtnActive) {
@@ -662,18 +666,15 @@
662
666
 
663
667
  {#if !isFetching && allowUnknown && innerValue && !have_option_label_like(options.items, innerValue)}
664
668
  <div class="px-1">
665
- <button
666
- type="button"
667
- bind:this={addNewBtn}
669
+ <ListItemButton
670
+ bind:el={addNewBtn}
668
671
  onclick={add_new}
669
- class={twMerge(
670
- BTN_CLS,
671
- classOption,
672
- isAddNewBtnActive && classOptionActive
673
- )}
672
+ focused={isAddNewBtnActive}
673
+ class={classOption}
674
+ classFocused={classOptionActive}
674
675
  >
675
676
  {t("add_new", { value: innerValue })}
676
- </button>
677
+ </ListItemButton>
677
678
  </div>
678
679
  {/if}
679
680
 
@@ -681,23 +682,23 @@
681
682
  {#if _optgroup}
682
683
  <div
683
684
  class={twMerge(
684
- "text-sm capitalize opacity-50 border-b border-black/10 mb-0.5 p-1 mx-1",
685
+ "mb-1 p-1 text-xs font-semibold uppercase tracking-wide",
686
+ "text-neutral-500 dark:text-neutral-400",
685
687
  classOptgroup
686
688
  )}
687
689
  >
688
690
  {_optgroup}
689
691
  </div>
690
692
  {/if}
691
- <ul class="space-y-0.5">
693
+ <ul role="presentation" class="space-y-1">
692
694
  <!-- {#each options.items as item} -->
693
695
  {#each _opts as item (item[itemIdPropName])}
694
696
  {@const active =
695
697
  item[itemIdPropName] === options.active?.[itemIdPropName]}
696
698
  {@const isSelected =
697
699
  selected.items && _selectedColl.exists(item[itemIdPropName])}
698
- <li class:active class="px-1">
699
- <button
700
- type="button"
700
+ <li class:active role="presentation" class="px-1">
701
+ <ListItemButton
701
702
  id={btn_id(item[itemIdPropName])}
702
703
  onclick={() => {
703
704
  if (isMultiple) {
@@ -713,40 +714,19 @@
713
714
  submit();
714
715
  }
715
716
  }}
716
- class:active
717
- class:selected={isSelected}
718
- class={twMerge(
719
- BTN_CLS,
720
- isSelected && "bg-neutral-200 dark:bg-neutral-800",
721
- classOption,
722
- // active && "border-neutral-400",
723
- active && classOptionActive
724
- )}
725
- tabindex="-1"
717
+ active={isSelected}
718
+ focused={active}
719
+ contentBefore={showIcons ? getIconThc(isSelected) : undefined}
720
+ classContentBefore={isSelected ? "opacity-100" : "opacity-50"}
721
+ class={classOption}
722
+ classActive={classOptionActive}
723
+ classFocused={classOptionActive}
724
+ tabindex={-1}
726
725
  role={isMultiple ? "checkbox" : "radio"}
727
726
  aria-checked={isSelected}
728
727
  >
729
- {#if showIcons}
730
- <span class={isSelected ? "opacity-100" : "opacity-25"}>
731
- {#if isMultiple}
732
- {#if isSelected}
733
- {@html iconCheckboxCheck()}
734
- {:else}
735
- {@html iconCheckboxEmpty()}
736
- {/if}
737
- {:else if isSelected}
738
- {@html iconRadioCheck()}
739
- {:else}
740
- {@html iconRadioEmpty()}
741
- {/if}
742
- </span>
743
- {/if}
744
- <span
745
- class={twMerge(
746
- "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
747
- )}>{_renderOptionLabel(item)}</span
748
- >
749
- </button>
728
+ {_renderOptionLabel(item)}
729
+ </ListItemButton>
750
730
  </li>
751
731
  {/each}
752
732
  </ul>
@@ -17,6 +17,7 @@ export interface Props extends Record<string, any> {
17
17
  value: string;
18
18
  modal: ModalDialog;
19
19
  }]>;
20
+ modal?: ModalDialog;
20
21
  input?: HTMLInputElement;
21
22
  value: string;
22
23
  label?: SnippetWithId | THC;
@@ -65,6 +66,6 @@ export interface Props extends Record<string, any> {
65
66
  itemIdPropName?: string;
66
67
  onChange?: (value: string) => void;
67
68
  }
68
- declare const FieldOptions: import("svelte").Component<Props, {}, "value" | "input">;
69
+ declare const FieldOptions: import("svelte").Component<Props, {}, "value" | "input" | "modal">;
69
70
  type FieldOptions = ReturnType<typeof FieldOptions>;
70
71
  export default FieldOptions;
@@ -0,0 +1,170 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLButtonAttributes } from "svelte/elements";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+
6
+ export interface Props extends Omit<HTMLButtonAttributes, "children" | "class"> {
7
+ /** Content displayed in the button */
8
+ children?: Snippet;
9
+ /** Whether this item is currently active/selected (visual state) */
10
+ active?: boolean;
11
+ /** Whether this item is currently focused via keyboard navigation */
12
+ focused?: boolean;
13
+ /** Size preset affecting padding and min-height (sm, md, lg) */
14
+ size?: "sm" | "md" | "lg" | string;
15
+ /** Skip all default styling, use only custom classes */
16
+ unstyled?: boolean;
17
+ /** Enable touch-friendly sizing (larger tap targets). "auto" detects coarse pointer. */
18
+ touchFriendly?: boolean | "auto";
19
+ /** Icon/content displayed before the main content */
20
+ contentBefore?: THC;
21
+ /** Icon/content displayed after the main content */
22
+ contentAfter?: THC;
23
+ /** Render as anchor tag instead of button */
24
+ href?: string;
25
+ /** CSS classes for the button element */
26
+ class?: string;
27
+ /** CSS classes for the icon before slot */
28
+ classContentBefore?: string;
29
+ /** CSS classes for the icon after slot */
30
+ classContentAfter?: string;
31
+ /** CSS classes applied when active */
32
+ classActive?: string;
33
+ /** CSS classes applied when focused */
34
+ classFocused?: string;
35
+ /** Bindable element reference */
36
+ el?: HTMLButtonElement | HTMLAnchorElement;
37
+ }
38
+
39
+ export interface ListItemButtonPresetClasses {
40
+ size: Record<string, string>;
41
+ touchFriendly: string;
42
+ }
43
+
44
+ export const LIST_ITEM_BUTTON_STUIC_BASE_CLASSES = `
45
+ w-full
46
+ flex items-center gap-2
47
+ text-left
48
+ rounded-[var(--lib-radius)]
49
+ cursor-pointer
50
+ touch-action-manipulation
51
+
52
+ bg-lib-bg dark:bg-lib-bg-dark
53
+ text-lib-text dark:text-lib-text-dark
54
+
55
+ border border-lib-border dark:border-lib-border-dark
56
+
57
+ hover:bg-lib-hover-bg dark:hover:bg-lib-hover-bg-dark
58
+ hover:text-lib-hover-text dark:hover:text-lib-hover-text-dark
59
+ hover:border-lib-hover-border dark:hover:border-lib-hover-border-dark
60
+
61
+ focus:outline-none
62
+ focus-visible:bg-lib-focus-bg dark:focus-visible:bg-lib-focus-bg-dark
63
+ focus-visible:text-lib-focus-text dark:focus-visible:text-lib-focus-text-dark
64
+ focus-visible:border-lib-focus-border dark:focus-visible:border-lib-focus-border-dark
65
+
66
+ disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none
67
+ `;
68
+
69
+ export const LIST_ITEM_BUTTON_STUIC_PRESET_CLASSES: ListItemButtonPresetClasses = {
70
+ size: {
71
+ sm: `px-2 py-1.5 text-sm min-h-[36px]`,
72
+ md: `px-2.5 py-2 text-base min-h-[40px]`,
73
+ lg: `px-3 py-2.5 text-base min-h-[44px]`,
74
+ },
75
+ touchFriendly: `min-h-[44px] py-2.5`,
76
+ };
77
+
78
+ export const LIST_ITEM_BUTTON_ACTIVE_CLASSES = `
79
+ bg-lib-active-bg dark:bg-lib-active-bg-dark
80
+ text-lib-active-text dark:text-lib-active-text-dark
81
+ border-lib-active-border dark:border-lib-active-border-dark
82
+ `;
83
+
84
+ export const LIST_ITEM_BUTTON_FOCUSED_CLASSES = `
85
+ bg-lib-focus-bg dark:bg-lib-focus-bg-dark
86
+ text-lib-focus-text dark:text-lib-focus-text-dark
87
+ border-lib-focus-border dark:border-lib-focus-border-dark
88
+ `;
89
+ </script>
90
+
91
+ <script lang="ts">
92
+ import { twMerge } from "../../utils/tw-merge.js";
93
+ import { DevicePointer } from "../../utils/device-pointer.svelte.js";
94
+ import Thc from "../Thc/Thc.svelte";
95
+ import "./index.css";
96
+
97
+ let {
98
+ children,
99
+ active = false,
100
+ focused = false,
101
+ size = "md",
102
+ unstyled = false,
103
+ touchFriendly = false,
104
+ contentBefore,
105
+ contentAfter,
106
+ href,
107
+ class: classProp,
108
+ classContentBefore,
109
+ classContentAfter,
110
+ classActive,
111
+ classFocused,
112
+ el = $bindable(),
113
+ ...rest
114
+ }: Props = $props();
115
+
116
+ const devicePointer = new DevicePointer();
117
+
118
+ const _base = LIST_ITEM_BUTTON_STUIC_BASE_CLASSES;
119
+ const _preset = LIST_ITEM_BUTTON_STUIC_PRESET_CLASSES;
120
+
121
+ let _touchClasses = $derived.by(() => {
122
+ if (touchFriendly === true) return _preset.touchFriendly;
123
+ if (touchFriendly === "auto" && devicePointer.isCoarse) return _preset.touchFriendly;
124
+ return "";
125
+ });
126
+
127
+ let _class = $derived(
128
+ [
129
+ "stuic-list-item-button",
130
+ size,
131
+ active && "active",
132
+ focused && "focused",
133
+ !unstyled && _base,
134
+ !unstyled && size && _preset.size[size],
135
+ !unstyled && _touchClasses,
136
+ !unstyled && active && LIST_ITEM_BUTTON_ACTIVE_CLASSES,
137
+ !unstyled && focused && !active && LIST_ITEM_BUTTON_FOCUSED_CLASSES,
138
+ active && classActive,
139
+ focused && !active && classFocused,
140
+ ]
141
+ .filter(Boolean)
142
+ .join(" ")
143
+ );
144
+ </script>
145
+
146
+ {#snippet content()}
147
+ {#if contentBefore}
148
+ <span class={twMerge("shrink-0", classContentBefore)}>
149
+ <Thc thc={contentBefore} />
150
+ </span>
151
+ {/if}
152
+ <span class="flex-1 min-w-0">
153
+ {@render children?.()}
154
+ </span>
155
+ {#if contentAfter}
156
+ <span class={twMerge("shrink-0", classContentAfter)}>
157
+ <Thc thc={contentAfter} />
158
+ </span>
159
+ {/if}
160
+ {/snippet}
161
+
162
+ {#if href}
163
+ <a {href} bind:this={el} class={twMerge(_class, classProp)} {...rest as any}>
164
+ {@render content()}
165
+ </a>
166
+ {:else}
167
+ <button bind:this={el} class={twMerge(_class, classProp)} type="button" {...rest}>
168
+ {@render content()}
169
+ </button>
170
+ {/if}
@@ -0,0 +1,47 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLButtonAttributes } from "svelte/elements";
3
+ import type { THC } from "../Thc/Thc.svelte";
4
+ export interface Props extends Omit<HTMLButtonAttributes, "children" | "class"> {
5
+ /** Content displayed in the button */
6
+ children?: Snippet;
7
+ /** Whether this item is currently active/selected (visual state) */
8
+ active?: boolean;
9
+ /** Whether this item is currently focused via keyboard navigation */
10
+ focused?: boolean;
11
+ /** Size preset affecting padding and min-height (sm, md, lg) */
12
+ size?: "sm" | "md" | "lg" | string;
13
+ /** Skip all default styling, use only custom classes */
14
+ unstyled?: boolean;
15
+ /** Enable touch-friendly sizing (larger tap targets). "auto" detects coarse pointer. */
16
+ touchFriendly?: boolean | "auto";
17
+ /** Icon/content displayed before the main content */
18
+ contentBefore?: THC;
19
+ /** Icon/content displayed after the main content */
20
+ contentAfter?: THC;
21
+ /** Render as anchor tag instead of button */
22
+ href?: string;
23
+ /** CSS classes for the button element */
24
+ class?: string;
25
+ /** CSS classes for the icon before slot */
26
+ classContentBefore?: string;
27
+ /** CSS classes for the icon after slot */
28
+ classContentAfter?: string;
29
+ /** CSS classes applied when active */
30
+ classActive?: string;
31
+ /** CSS classes applied when focused */
32
+ classFocused?: string;
33
+ /** Bindable element reference */
34
+ el?: HTMLButtonElement | HTMLAnchorElement;
35
+ }
36
+ export interface ListItemButtonPresetClasses {
37
+ size: Record<string, string>;
38
+ touchFriendly: string;
39
+ }
40
+ export declare const LIST_ITEM_BUTTON_STUIC_BASE_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\ttext-left\n\t\trounded-[var(--lib-radius)]\n\t\tcursor-pointer\n\t\ttouch-action-manipulation\n\n\t\tbg-lib-bg dark:bg-lib-bg-dark\n\t\ttext-lib-text dark:text-lib-text-dark\n\n\t\tborder border-lib-border dark:border-lib-border-dark\n\n\t\thover:bg-lib-hover-bg dark:hover:bg-lib-hover-bg-dark\n\t\thover:text-lib-hover-text dark:hover:text-lib-hover-text-dark\n\t\thover:border-lib-hover-border dark:hover:border-lib-hover-border-dark\n\n\t\tfocus:outline-none\n\t\tfocus-visible:bg-lib-focus-bg dark:focus-visible:bg-lib-focus-bg-dark\n\t\tfocus-visible:text-lib-focus-text dark:focus-visible:text-lib-focus-text-dark\n\t\tfocus-visible:border-lib-focus-border dark:focus-visible:border-lib-focus-border-dark\n\n\t\tdisabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none\n\t";
41
+ export declare const LIST_ITEM_BUTTON_STUIC_PRESET_CLASSES: ListItemButtonPresetClasses;
42
+ export declare const LIST_ITEM_BUTTON_ACTIVE_CLASSES = "\n\t\tbg-lib-active-bg dark:bg-lib-active-bg-dark\n\t\ttext-lib-active-text dark:text-lib-active-text-dark\n\t\tborder-lib-active-border dark:border-lib-active-border-dark\n\t";
43
+ export declare const LIST_ITEM_BUTTON_FOCUSED_CLASSES = "\n\t\tbg-lib-focus-bg dark:bg-lib-focus-bg-dark\n\t\ttext-lib-focus-text dark:text-lib-focus-text-dark\n\t\tborder-lib-focus-border dark:border-lib-focus-border-dark\n\t";
44
+ import "./index.css";
45
+ declare const ListItemButton: import("svelte").Component<Props, {}, "el">;
46
+ type ListItemButton = ReturnType<typeof ListItemButton>;
47
+ export default ListItemButton;
@@ -0,0 +1,180 @@
1
+ # ListItemButton
2
+
3
+ A versatile button component for list-like contexts such as dropdown menus, command palettes, and option lists. Supports multiple visual states, touch-friendly sizing, and full CSS variable customization.
4
+
5
+ ## Usage
6
+
7
+ ```svelte
8
+ <script>
9
+ import { ListItemButton } from "stuic";
10
+ </script>
11
+
12
+ <ListItemButton>Click me</ListItemButton>
13
+ ```
14
+
15
+ ## Props
16
+
17
+ | Prop | Type | Default | Description |
18
+ |------|------|---------|-------------|
19
+ | `children` | `Snippet` | - | Content displayed in the button |
20
+ | `active` | `boolean` | `false` | Whether this item is currently active/selected |
21
+ | `focused` | `boolean` | `false` | Whether this item is focused via keyboard navigation |
22
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size preset affecting padding and min-height |
23
+ | `unstyled` | `boolean` | `false` | Skip all default styling, use only custom classes |
24
+ | `touchFriendly` | `boolean \| "auto"` | `false` | Enable touch-friendly sizing. `"auto"` detects coarse pointer. |
25
+ | `contentBefore` | `THC` | - | Icon/content displayed before the main content |
26
+ | `contentAfter` | `THC` | - | Icon/content displayed after the main content |
27
+ | `href` | `string` | - | Render as anchor tag instead of button |
28
+ | `class` | `string` | - | CSS classes for the button element |
29
+ | `classContentBefore` | `string` | - | CSS classes for the icon before slot |
30
+ | `classContentAfter` | `string` | - | CSS classes for the icon after slot |
31
+ | `classActive` | `string` | - | CSS classes applied when active |
32
+ | `classFocused` | `string` | - | CSS classes applied when focused |
33
+ | `el` | `HTMLButtonElement \| HTMLAnchorElement` | - | Bindable element reference |
34
+
35
+ ## Examples
36
+
37
+ ### Basic States
38
+
39
+ ```svelte
40
+ <ListItemButton>Default</ListItemButton>
41
+ <ListItemButton active>Active (selected)</ListItemButton>
42
+ <ListItemButton focused>Focused (keyboard nav)</ListItemButton>
43
+ <ListItemButton disabled>Disabled</ListItemButton>
44
+ ```
45
+
46
+ ### Sizes
47
+
48
+ ```svelte
49
+ <ListItemButton size="sm">Small</ListItemButton>
50
+ <ListItemButton size="md">Medium (default)</ListItemButton>
51
+ <ListItemButton size="lg">Large</ListItemButton>
52
+ ```
53
+
54
+ ### Touch Friendly
55
+
56
+ ```svelte
57
+ <!-- Always use touch-friendly sizing (min-height 44px) -->
58
+ <ListItemButton touchFriendly>Touch friendly</ListItemButton>
59
+
60
+ <!-- Auto-detect coarse pointer (touch screens) -->
61
+ <ListItemButton touchFriendly="auto">Auto detect</ListItemButton>
62
+ ```
63
+
64
+ ### With Icons
65
+
66
+ ```svelte
67
+ <script>
68
+ import { ListItemButton, iconUser, iconChevronRight } from "stuic";
69
+ </script>
70
+
71
+ <ListItemButton contentBefore={{ html: iconUser({}) }}>
72
+ User Profile
73
+ </ListItemButton>
74
+
75
+ <ListItemButton contentAfter={{ html: iconChevronRight({}) }}>
76
+ Settings
77
+ </ListItemButton>
78
+
79
+ <ListItemButton
80
+ contentBefore={{ html: iconUser({}) }}
81
+ contentAfter={{ html: iconChevronRight({}) }}
82
+ >
83
+ Both icons
84
+ </ListItemButton>
85
+ ```
86
+
87
+ ### As Link
88
+
89
+ ```svelte
90
+ <ListItemButton href="/settings">Settings</ListItemButton>
91
+ ```
92
+
93
+ ### Unstyled
94
+
95
+ ```svelte
96
+ <ListItemButton unstyled class="my-custom-classes">
97
+ Custom styled
98
+ </ListItemButton>
99
+ ```
100
+
101
+ ## CSS Variables
102
+
103
+ All colors support customization via CSS variables. Define them on a parent element to override defaults.
104
+
105
+ ### Border Radius
106
+
107
+ | Variable | Default | Description |
108
+ |----------|---------|-------------|
109
+ | `--lib-radius` | `var(--radius-md)` | Border radius |
110
+
111
+ ### Base State
112
+
113
+ | Variable | Default | Description |
114
+ |----------|---------|-------------|
115
+ | `--color-lib-bg` | `neutral-200` | Background color |
116
+ | `--color-lib-bg-dark` | `neutral-600` | Background color (dark mode) |
117
+ | `--color-lib-text` | `black` | Text color |
118
+ | `--color-lib-text-dark` | `neutral-100` | Text color (dark mode) |
119
+ | `--color-lib-border` | `transparent` | Border color |
120
+ | `--color-lib-border-dark` | `transparent` | Border color (dark mode) |
121
+
122
+ ### Hover State
123
+
124
+ | Variable | Default | Description |
125
+ |----------|---------|-------------|
126
+ | `--color-lib-hover-bg` | `neutral-500` | Hover background |
127
+ | `--color-lib-hover-bg-dark` | `neutral-200` | Hover background (dark mode) |
128
+ | `--color-lib-hover-text` | `white` | Hover text color |
129
+ | `--color-lib-hover-text-dark` | `neutral-900` | Hover text color (dark mode) |
130
+ | `--color-lib-hover-border` | `transparent` | Hover border color |
131
+ | `--color-lib-hover-border-dark` | `transparent` | Hover border color (dark mode) |
132
+
133
+ ### Active State
134
+
135
+ | Variable | Default | Description |
136
+ |----------|---------|-------------|
137
+ | `--color-lib-active-bg` | `neutral-500` | Active background |
138
+ | `--color-lib-active-bg-dark` | `neutral-200` | Active background (dark mode) |
139
+ | `--color-lib-active-text` | `white` | Active text color |
140
+ | `--color-lib-active-text-dark` | `neutral-900` | Active text color (dark mode) |
141
+ | `--color-lib-active-border` | `transparent` | Active border color |
142
+ | `--color-lib-active-border-dark` | `transparent` | Active border color (dark mode) |
143
+
144
+ ### Focus State
145
+
146
+ | Variable | Default | Description |
147
+ |----------|---------|-------------|
148
+ | `--color-lib-focus-bg` | `neutral-500` | Focus background |
149
+ | `--color-lib-focus-bg-dark` | `neutral-200` | Focus background (dark mode) |
150
+ | `--color-lib-focus-text` | `white` | Focus text color |
151
+ | `--color-lib-focus-text-dark` | `neutral-900` | Focus text color (dark mode) |
152
+ | `--color-lib-focus-border` | `transparent` | Focus border color |
153
+ | `--color-lib-focus-border-dark` | `transparent` | Focus border color (dark mode) |
154
+
155
+ ### Custom Theme Example
156
+
157
+ ```svelte
158
+ <div style="
159
+ --color-lib-hover-bg: var(--color-blue-500);
160
+ --color-lib-hover-bg-dark: var(--color-blue-600);
161
+ --color-lib-active-bg: var(--color-blue-600);
162
+ --color-lib-active-bg-dark: var(--color-blue-500);
163
+ ">
164
+ <ListItemButton>Blue theme</ListItemButton>
165
+ <ListItemButton active>Active blue</ListItemButton>
166
+ </div>
167
+ ```
168
+
169
+ ## Exported Constants
170
+
171
+ The component exports several class constants for advanced customization:
172
+
173
+ ```typescript
174
+ import {
175
+ LIST_ITEM_BUTTON_STUIC_BASE_CLASSES,
176
+ LIST_ITEM_BUTTON_STUIC_PRESET_CLASSES,
177
+ LIST_ITEM_BUTTON_ACTIVE_CLASSES,
178
+ LIST_ITEM_BUTTON_FOCUSED_CLASSES,
179
+ } from "stuic";
180
+ ```
@@ -0,0 +1,58 @@
1
+ /* Border radius (0.375rem = rounded-md) */
2
+ :root {
3
+ --lib-radius: var(--radius-md);
4
+ }
5
+
6
+ /* prettier-ignore */
7
+ @theme inline {
8
+
9
+ /* "lib" -> list item button */
10
+
11
+ /* Private defaults (for human readability) */
12
+ --_lib-bg: var(--color-neutral-200);
13
+ --_lib-bg-hi: var(--color-neutral-500);
14
+ --_lib-bg-dark: var(--color-neutral-600);
15
+ --_lib-bg-hi-dark: var(--color-neutral-200);
16
+
17
+ --_lib-text: var(--color-black);
18
+ --_lib-text-hi: var(--color-white);
19
+ --_lib-text-dark: var(--color-neutral-100);
20
+ --_lib-text-hi-dark: var(--color-neutral-900);
21
+
22
+ --_lib-border: transparent;
23
+ --_lib-border-hi: transparent;
24
+ --_lib-border-dark: transparent;
25
+ --_lib-border-hi-dark: transparent;
26
+
27
+ /* Base state */
28
+ --color-lib-bg: var(--color-lib-bg, var(--_lib-bg));
29
+ --color-lib-bg-dark: var(--color-lib-bg-dark, var(--_lib-bg-dark));
30
+ --color-lib-text: var(--color-lib-text, var(--_lib-text));
31
+ --color-lib-text-dark: var(--color-lib-text-dark, var(--_lib-text-dark));
32
+ --color-lib-border: var(--color-lib-border, var(--_lib-border));
33
+ --color-lib-border-dark: var(--color-lib-border-dark, var(--_lib-border-dark));
34
+
35
+ /* Hover state */
36
+ --color-lib-hover-bg: var(--color-lib-hover-bg, var(--_lib-bg-hi));
37
+ --color-lib-hover-bg-dark: var(--color-lib-hover-bg-dark, var(--_lib-bg-hi-dark));
38
+ --color-lib-hover-text: var(--color-lib-hover-text, var(--_lib-text-hi));
39
+ --color-lib-hover-text-dark: var(--color-lib-hover-text-dark, var(--_lib-text-hi-dark));
40
+ --color-lib-hover-border: var(--color-lib-hover-border, var(--_lib-border-hi));
41
+ --color-lib-hover-border-dark: var(--color-lib-hover-border-dark, var(--_lib-border-hi-dark));
42
+
43
+ /* Active/Selected state */
44
+ --color-lib-active-bg: var(--color-lib-active-bg, var(--_lib-bg-hi));
45
+ --color-lib-active-bg-dark: var(--color-lib-active-bg-dark, var(--_lib-bg-hi-dark));
46
+ --color-lib-active-text: var(--color-lib-active-text, var(--_lib-text-hi));
47
+ --color-lib-active-text-dark: var(--color-lib-active-text-dark, var(--_lib-text-hi-dark));
48
+ --color-lib-active-border: var(--color-lib-active-border, var(--_lib-border-hi));
49
+ --color-lib-active-border-dark: var(--color-lib-active-border-dark, var(--_lib-border-hi-dark));
50
+
51
+ /* Focus-visible state (keyboard focus) */
52
+ --color-lib-focus-bg: var(--color-lib-focus-bg, var(--_lib-bg-hi));
53
+ --color-lib-focus-bg-dark: var(--color-lib-focus-bg-dark, var(--_lib-bg-hi-dark));
54
+ --color-lib-focus-text: var(--color-lib-focus-text, var(--_lib-text-hi));
55
+ --color-lib-focus-text-dark: var(--color-lib-focus-text-dark, var(--_lib-text-hi-dark));
56
+ --color-lib-focus-border: var(--color-lib-focus-border, var(--_lib-border-hi));
57
+ --color-lib-focus-border-dark: var(--color-lib-focus-border-dark, var(--_lib-border-hi-dark));
58
+ }
@@ -0,0 +1 @@
1
+ export { default as ListItemButton, type Props as ListItemButtonProps, type ListItemButtonPresetClasses, LIST_ITEM_BUTTON_STUIC_BASE_CLASSES, LIST_ITEM_BUTTON_STUIC_PRESET_CLASSES, LIST_ITEM_BUTTON_ACTIVE_CLASSES, LIST_ITEM_BUTTON_FOCUSED_CLASSES, } from "./ListItemButton.svelte";
@@ -0,0 +1 @@
1
+ export { default as ListItemButton, LIST_ITEM_BUTTON_STUIC_BASE_CLASSES, LIST_ITEM_BUTTON_STUIC_PRESET_CLASSES, LIST_ITEM_BUTTON_ACTIVE_CLASSES, LIST_ITEM_BUTTON_FOCUSED_CLASSES, } from "./ListItemButton.svelte";
package/dist/index.css CHANGED
@@ -20,6 +20,7 @@ so, since we need to override, sticking with that */
20
20
  @import "./components/ButtonGroupRadio/index.css";
21
21
  @import "./components/DismissibleMessage/index.css";
22
22
  @import "./components/Input/index.css";
23
+ @import "./components/ListItemButton/index.css";
23
24
  @import "./components/Notifications/index.css";
24
25
  @import "./components/Progress/index.css";
25
26
  @import "./components/Switch/index.css";
package/dist/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export * from "./components/DropdownMenu/index.js";
38
38
  export * from "./components/HoverExpandableWidth/index.js";
39
39
  export * from "./components/Input/index.js";
40
40
  export * from "./components/KbdShortcut/index.js";
41
+ export * from "./components/ListItemButton/index.js";
41
42
  export * from "./components/Modal/index.js";
42
43
  export * from "./components/ModalDialog/index.js";
43
44
  export * from "./components/Notifications/index.js";
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ export * from "./components/DropdownMenu/index.js";
39
39
  export * from "./components/HoverExpandableWidth/index.js";
40
40
  export * from "./components/Input/index.js";
41
41
  export * from "./components/KbdShortcut/index.js";
42
+ export * from "./components/ListItemButton/index.js";
42
43
  export * from "./components/Modal/index.js";
43
44
  export * from "./components/ModalDialog/index.js";
44
45
  export * from "./components/Notifications/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.63.0",
3
+ "version": "2.65.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",