@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.
- package/dist/components/CommandMenu/CommandMenu.svelte +19 -23
- package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +1 -0
- package/dist/components/CommandMenu/index.css +3 -0
- package/dist/components/DropdownMenu/DropdownMenu.svelte +46 -106
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +10 -6
- package/dist/components/DropdownMenu/index.css +10 -2
- package/dist/components/DropdownMenu/index.d.ts +1 -1
- package/dist/components/DropdownMenu/index.js +1 -1
- package/dist/components/Input/FieldOptions.svelte +36 -56
- package/dist/components/Input/FieldOptions.svelte.d.ts +2 -1
- package/dist/components/ListItemButton/ListItemButton.svelte +170 -0
- package/dist/components/ListItemButton/ListItemButton.svelte.d.ts +47 -0
- package/dist/components/ListItemButton/README.md +180 -0
- package/dist/components/ListItemButton/index.css +58 -0
- package/dist/components/ListItemButton/index.d.ts +1 -0
- package/dist/components/ListItemButton/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
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=
|
|
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
|
-
<
|
|
329
|
-
|
|
330
|
-
type="button"
|
|
337
|
+
<ListItemButton
|
|
338
|
+
id={btn_id(item[itemIdPropName])}
|
|
331
339
|
role="option"
|
|
332
340
|
aria-selected={active}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
</
|
|
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;
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
type: "action";
|
|
38
38
|
/** Label displayed - supports THC for icons, HTML, etc. */
|
|
39
39
|
label: THC;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
|
|
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
|
|
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
|
-
<
|
|
695
|
+
<ListItemButton
|
|
702
696
|
id={itemId(item.id)}
|
|
703
697
|
role="menuitem"
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
707
|
+
disabled={item.disabled}
|
|
716
708
|
tabindex={-1}
|
|
717
|
-
type="button"
|
|
718
709
|
>
|
|
719
|
-
{
|
|
720
|
-
|
|
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
|
-
<
|
|
735
|
+
<ListItemButton
|
|
759
736
|
id={expandableHeaderId(item.id)}
|
|
760
737
|
role="menuitem"
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
753
|
+
disabled={item.disabled}
|
|
780
754
|
tabindex={-1}
|
|
781
|
-
type="button"
|
|
782
755
|
>
|
|
783
|
-
{
|
|
784
|
-
|
|
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
|
-
<
|
|
771
|
+
<ListItemButton
|
|
814
772
|
id={itemId(childItem.id)}
|
|
815
773
|
role="menuitem"
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
783
|
+
disabled={childItem.disabled}
|
|
829
784
|
tabindex={-1}
|
|
830
|
-
type="button"
|
|
831
785
|
>
|
|
832
|
-
{
|
|
833
|
-
|
|
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
|
-
/**
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
|
|
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
|
|
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-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
<
|
|
666
|
-
|
|
667
|
-
bind:this={addNewBtn}
|
|
669
|
+
<ListItemButton
|
|
670
|
+
bind:el={addNewBtn}
|
|
668
671
|
onclick={add_new}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
isAddNewBtnActive && classOptionActive
|
|
673
|
-
)}
|
|
672
|
+
focused={isAddNewBtnActive}
|
|
673
|
+
class={classOption}
|
|
674
|
+
classFocused={classOptionActive}
|
|
674
675
|
>
|
|
675
676
|
{t("add_new", { value: innerValue })}
|
|
676
|
-
</
|
|
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
|
-
"
|
|
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-
|
|
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
|
-
<
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
{
|
|
730
|
-
|
|
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";
|