@lanrenbang/basecoat-ultra-svelte 0.1.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/Accordion.svelte +64 -0
- package/dist/components/Accordion.svelte.d.ts +14 -0
- package/dist/components/Alert.svelte +51 -0
- package/dist/components/Alert.svelte.d.ts +12 -0
- package/dist/components/Avatar.svelte +57 -0
- package/dist/components/Avatar.svelte.d.ts +13 -0
- package/dist/components/Badge.svelte +30 -0
- package/dist/components/Badge.svelte.d.ts +10 -0
- package/dist/components/Breadcrumb.svelte +54 -0
- package/dist/components/Breadcrumb.svelte.d.ts +14 -0
- package/dist/components/Button.svelte +38 -0
- package/dist/components/Button.svelte.d.ts +12 -0
- package/dist/components/ButtonGroup.svelte +26 -0
- package/dist/components/ButtonGroup.svelte.d.ts +10 -0
- package/dist/components/Card.svelte +67 -0
- package/dist/components/Card.svelte.d.ts +13 -0
- package/dist/components/Carousel.svelte +142 -0
- package/dist/components/Carousel.svelte.d.ts +11 -0
- package/dist/components/CatppuccinThemeSwitcher.svelte +132 -0
- package/dist/components/CatppuccinThemeSwitcher.svelte.d.ts +3 -0
- package/dist/components/Checkbox.svelte +20 -0
- package/dist/components/Checkbox.svelte.d.ts +8 -0
- package/dist/components/Collapsible.svelte +39 -0
- package/dist/components/Collapsible.svelte.d.ts +13 -0
- package/dist/components/Command.svelte +78 -0
- package/dist/components/Command.svelte.d.ts +12 -0
- package/dist/components/DatePicker.svelte +172 -0
- package/dist/components/DatePicker.svelte.d.ts +13 -0
- package/dist/components/Dialog.svelte +91 -0
- package/dist/components/Dialog.svelte.d.ts +14 -0
- package/dist/components/Drawer.svelte +127 -0
- package/dist/components/Drawer.svelte.d.ts +12 -0
- package/dist/components/DropdownMenu.svelte +62 -0
- package/dist/components/DropdownMenu.svelte.d.ts +14 -0
- package/dist/components/Empty.svelte +58 -0
- package/dist/components/Empty.svelte.d.ts +12 -0
- package/dist/components/Input.svelte +22 -0
- package/dist/components/Input.svelte.d.ts +9 -0
- package/dist/components/InputOTP.svelte +189 -0
- package/dist/components/InputOTP.svelte.d.ts +12 -0
- package/dist/components/Item.svelte +64 -0
- package/dist/components/Item.svelte.d.ts +14 -0
- package/dist/components/Kbd.svelte +28 -0
- package/dist/components/Kbd.svelte.d.ts +9 -0
- package/dist/components/Label.svelte +30 -0
- package/dist/components/Label.svelte.d.ts +10 -0
- package/dist/components/Pagination.svelte +120 -0
- package/dist/components/Pagination.svelte.d.ts +35 -0
- package/dist/components/Popover.svelte +68 -0
- package/dist/components/Popover.svelte.d.ts +16 -0
- package/dist/components/Progress.svelte +26 -0
- package/dist/components/Progress.svelte.d.ts +9 -0
- package/dist/components/Radio.svelte +22 -0
- package/dist/components/Radio.svelte.d.ts +9 -0
- package/dist/components/Resizable.svelte +66 -0
- package/dist/components/Resizable.svelte.d.ts +13 -0
- package/dist/components/Select.svelte +183 -0
- package/dist/components/Select.svelte.d.ts +16 -0
- package/dist/components/Separator.svelte +19 -0
- package/dist/components/Separator.svelte.d.ts +8 -0
- package/dist/components/Sheet.svelte +182 -0
- package/dist/components/Sheet.svelte.d.ts +13 -0
- package/dist/components/Skeleton.svelte +27 -0
- package/dist/components/Skeleton.svelte.d.ts +8 -0
- package/dist/components/Slider.svelte +38 -0
- package/dist/components/Slider.svelte.d.ts +11 -0
- package/dist/components/Spinner.svelte +28 -0
- package/dist/components/Spinner.svelte.d.ts +8 -0
- package/dist/components/Switch.svelte +20 -0
- package/dist/components/Switch.svelte.d.ts +8 -0
- package/dist/components/Table.svelte +61 -0
- package/dist/components/Table.svelte.d.ts +13 -0
- package/dist/components/Tabs.svelte +97 -0
- package/dist/components/Tabs.svelte.d.ts +15 -0
- package/dist/components/Textarea.svelte +22 -0
- package/dist/components/Textarea.svelte.d.ts +9 -0
- package/dist/components/Toast.svelte +73 -0
- package/dist/components/Toast.svelte.d.ts +3 -0
- package/dist/components/Toggle.svelte +69 -0
- package/dist/components/Toggle.svelte.d.ts +13 -0
- package/dist/components/ToggleGroup.svelte +69 -0
- package/dist/components/ToggleGroup.svelte.d.ts +12 -0
- package/dist/components/Tooltip.svelte +32 -0
- package/dist/components/Tooltip.svelte.d.ts +11 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +47 -0
- package/dist/reference.css +2 -0
- package/package.json +70 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, createEventDispatcher } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
total = 1,
|
|
7
|
+
current = $bindable(1),
|
|
8
|
+
onPageChange,
|
|
9
|
+
class: className = '',
|
|
10
|
+
prevIcon,
|
|
11
|
+
nextIcon,
|
|
12
|
+
...rest
|
|
13
|
+
}: {
|
|
14
|
+
total: number;
|
|
15
|
+
current: number;
|
|
16
|
+
onPageChange?: (page: number) => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
prevIcon?: Snippet;
|
|
19
|
+
nextIcon?: Snippet;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
} = $props();
|
|
22
|
+
|
|
23
|
+
const dispatch = createEventDispatcher<{
|
|
24
|
+
change: { page: number; previousPage: number; totalPages: number };
|
|
25
|
+
'basecoat:initialized': void;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
let containerRef: HTMLElement;
|
|
29
|
+
|
|
30
|
+
const handlePageClick = (page: number) => {
|
|
31
|
+
if (page < 1 || page > total || page === current) return;
|
|
32
|
+
const previousPage = current;
|
|
33
|
+
current = page;
|
|
34
|
+
onPageChange?.(page);
|
|
35
|
+
dispatch('change', { page: current, previousPage, totalPages: total });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Keyboard navigation
|
|
39
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
40
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
handlePageClick(current - 1);
|
|
43
|
+
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
handlePageClick(current + 1);
|
|
46
|
+
} else if (e.key === 'Home') {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
handlePageClick(1);
|
|
49
|
+
} else if (e.key === 'End') {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
handlePageClick(total);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Expose API methods via element
|
|
56
|
+
onMount(() => {
|
|
57
|
+
if (containerRef) {
|
|
58
|
+
(containerRef as any).goToPage = (page: number) => handlePageClick(page);
|
|
59
|
+
(containerRef as any).getCurrentPage = () => current;
|
|
60
|
+
(containerRef as any).getTotalPages = () => total;
|
|
61
|
+
}
|
|
62
|
+
dispatch('basecoat:initialized');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const finalClass = $derived(['pagination flex items-center gap-1', className].filter(Boolean).join(' '));
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<nav
|
|
69
|
+
bind:this={containerRef}
|
|
70
|
+
class={finalClass}
|
|
71
|
+
onkeydown={handleKeydown}
|
|
72
|
+
{...rest}
|
|
73
|
+
>
|
|
74
|
+
<!-- Previous -->
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
class="btn btn-ghost btn-icon"
|
|
78
|
+
disabled={current <= 1}
|
|
79
|
+
aria-disabled={current <= 1 ? 'true' : undefined}
|
|
80
|
+
onclick={() => handlePageClick(current - 1)}
|
|
81
|
+
aria-label="Previous page"
|
|
82
|
+
data-pagination-prev
|
|
83
|
+
>
|
|
84
|
+
{#if prevIcon}
|
|
85
|
+
{@render prevIcon()}
|
|
86
|
+
{:else}
|
|
87
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
88
|
+
{/if}
|
|
89
|
+
</button>
|
|
90
|
+
|
|
91
|
+
<!-- Pages -->
|
|
92
|
+
{#each Array.from({ length: total }, (_, i) => i + 1) as page}
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class={['btn w-9', current === page ? 'btn-outline' : 'btn-ghost'].join(' ')}
|
|
96
|
+
onclick={() => handlePageClick(page)}
|
|
97
|
+
aria-current={current === page ? 'page' : undefined}
|
|
98
|
+
data-pagination-page={page}
|
|
99
|
+
>
|
|
100
|
+
{page}
|
|
101
|
+
</button>
|
|
102
|
+
{/each}
|
|
103
|
+
|
|
104
|
+
<!-- Next -->
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
class="btn btn-ghost btn-icon"
|
|
108
|
+
disabled={current >= total}
|
|
109
|
+
aria-disabled={current >= total ? 'true' : undefined}
|
|
110
|
+
onclick={() => handlePageClick(current + 1)}
|
|
111
|
+
aria-label="Next page"
|
|
112
|
+
data-pagination-next
|
|
113
|
+
>
|
|
114
|
+
{#if nextIcon}
|
|
115
|
+
{@render nextIcon()}
|
|
116
|
+
{:else}
|
|
117
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
|
118
|
+
{/if}
|
|
119
|
+
</button>
|
|
120
|
+
</nav>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
total: number;
|
|
4
|
+
current: number;
|
|
5
|
+
onPageChange?: (page: number) => void;
|
|
6
|
+
class?: string;
|
|
7
|
+
prevIcon?: Snippet;
|
|
8
|
+
nextIcon?: Snippet;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
};
|
|
11
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
12
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
13
|
+
$$bindings?: Bindings;
|
|
14
|
+
} & Exports;
|
|
15
|
+
(internal: unknown, props: Props & {
|
|
16
|
+
$$events?: Events;
|
|
17
|
+
$$slots?: Slots;
|
|
18
|
+
}): Exports & {
|
|
19
|
+
$set?: any;
|
|
20
|
+
$on?: any;
|
|
21
|
+
};
|
|
22
|
+
z_$$bindings?: Bindings;
|
|
23
|
+
}
|
|
24
|
+
declare const Pagination: $$__sveltets_2_IsomorphicComponent<$$ComponentProps, {
|
|
25
|
+
change: CustomEvent<{
|
|
26
|
+
page: number;
|
|
27
|
+
previousPage: number;
|
|
28
|
+
totalPages: number;
|
|
29
|
+
}>;
|
|
30
|
+
'basecoat:initialized': CustomEvent<void>;
|
|
31
|
+
} & {
|
|
32
|
+
[evt: string]: CustomEvent<any>;
|
|
33
|
+
}, {}, {}, "current">;
|
|
34
|
+
type Pagination = InstanceType<typeof Pagination>;
|
|
35
|
+
export default Pagination;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
open = $bindable(false),
|
|
7
|
+
trigger,
|
|
8
|
+
children,
|
|
9
|
+
side = 'bottom',
|
|
10
|
+
align = 'start',
|
|
11
|
+
class: className = '',
|
|
12
|
+
contentClass = '',
|
|
13
|
+
...rest
|
|
14
|
+
}: {
|
|
15
|
+
open?: boolean;
|
|
16
|
+
trigger: Snippet<{ open: boolean }>;
|
|
17
|
+
children: Snippet;
|
|
18
|
+
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
19
|
+
align?: 'start' | 'center' | 'end';
|
|
20
|
+
class?: string;
|
|
21
|
+
contentClass?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
let container: HTMLDivElement;
|
|
26
|
+
|
|
27
|
+
// 点击外部关闭
|
|
28
|
+
function handleClickOutside(event: MouseEvent) {
|
|
29
|
+
if (open && container && !container.contains(event.target as Node)) {
|
|
30
|
+
open = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onMount(() => {
|
|
35
|
+
document.addEventListener('click', handleClickOutside);
|
|
36
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div bind:this={container} class={['popover relative', className].filter(Boolean).join(' ')} {...rest}>
|
|
41
|
+
<div
|
|
42
|
+
role="button"
|
|
43
|
+
tabindex="0"
|
|
44
|
+
aria-expanded={open}
|
|
45
|
+
onclick={(e) => { e.stopPropagation(); open = !open; }}
|
|
46
|
+
onkeydown={(e) => e.key === 'Enter' && (open = !open)}
|
|
47
|
+
class="inline-block"
|
|
48
|
+
>
|
|
49
|
+
{@render trigger({ open })}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{#if open}
|
|
53
|
+
<section
|
|
54
|
+
data-popover
|
|
55
|
+
aria-hidden={!open}
|
|
56
|
+
data-side={side}
|
|
57
|
+
data-align={align}
|
|
58
|
+
class={['absolute z-50 p-4 bg-popover text-popover-foreground rounded-md border shadow-md outline-none animate-in fade-in-0 zoom-in-95', contentClass].filter(Boolean).join(' ')}
|
|
59
|
+
>
|
|
60
|
+
{@render children()}
|
|
61
|
+
</section>
|
|
62
|
+
{/if}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
@import "../../../../ultra/src/css/parts/components/popover.css";
|
|
67
|
+
@reference "../reference.css";
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
trigger: Snippet<{
|
|
5
|
+
open: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
9
|
+
align?: 'start' | 'center' | 'end';
|
|
10
|
+
class?: string;
|
|
11
|
+
contentClass?: string;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
};
|
|
14
|
+
declare const Popover: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
15
|
+
type Popover = ReturnType<typeof Popover>;
|
|
16
|
+
export default Popover;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
value = 0,
|
|
4
|
+
max = 100,
|
|
5
|
+
class: className = '',
|
|
6
|
+
...rest
|
|
7
|
+
}: {
|
|
8
|
+
value?: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
class?: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
} = $props();
|
|
13
|
+
|
|
14
|
+
const percentage = $derived(Math.min(Math.max((value / max) * 100, 0), 100));
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class={['progress h-2 bg-muted rounded-full overflow-hidden w-full', className].filter(Boolean).join(' ')} {...rest}>
|
|
18
|
+
<div
|
|
19
|
+
class="bg-primary h-full transition-all duration-300 ease-in-out"
|
|
20
|
+
role="progressbar"
|
|
21
|
+
aria-valuenow={value}
|
|
22
|
+
aria-valuemin="0"
|
|
23
|
+
aria-valuemax={max}
|
|
24
|
+
style="width: {percentage}%"
|
|
25
|
+
></div>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
group = $bindable(),
|
|
4
|
+
value,
|
|
5
|
+
class: className = '',
|
|
6
|
+
...rest
|
|
7
|
+
}: {
|
|
8
|
+
group?: any;
|
|
9
|
+
value?: any;
|
|
10
|
+
class?: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
} = $props();
|
|
13
|
+
|
|
14
|
+
const finalClass = $derived(['input', className].filter(Boolean).join(' '));
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<input type="radio" class={finalClass} bind:group {value} {...rest} />
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
@import "../../../../ultra/src/css/parts/components/radio.css";
|
|
21
|
+
@reference "../reference.css";
|
|
22
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import Split from 'split.js';
|
|
4
|
+
import type { Instance } from 'split.js';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
children,
|
|
9
|
+
direction = 'horizontal',
|
|
10
|
+
sizes,
|
|
11
|
+
minSize = 100,
|
|
12
|
+
gutterSize = 10,
|
|
13
|
+
class: className = '',
|
|
14
|
+
...rest
|
|
15
|
+
}: {
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
direction?: 'horizontal' | 'vertical';
|
|
18
|
+
sizes?: number[];
|
|
19
|
+
minSize?: number | number[];
|
|
20
|
+
gutterSize?: number;
|
|
21
|
+
class?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
let container: HTMLDivElement;
|
|
26
|
+
let splitInstance: Instance;
|
|
27
|
+
|
|
28
|
+
const finalClass = $derived([
|
|
29
|
+
'resizable-group h-full flex',
|
|
30
|
+
direction === 'vertical' ? 'flex-col' : 'flex-row',
|
|
31
|
+
className
|
|
32
|
+
].filter(Boolean).join(' '));
|
|
33
|
+
|
|
34
|
+
onMount(() => {
|
|
35
|
+
const elements = Array.from(container.children).filter(el => !el.classList.contains('gutter')) as HTMLElement[];
|
|
36
|
+
if (elements.length < 2) return;
|
|
37
|
+
|
|
38
|
+
splitInstance = Split(elements, {
|
|
39
|
+
direction,
|
|
40
|
+
sizes,
|
|
41
|
+
minSize,
|
|
42
|
+
gutterSize,
|
|
43
|
+
gutter: (index, direction) => {
|
|
44
|
+
const gutter = document.createElement('div');
|
|
45
|
+
gutter.className = `gutter gutter-${direction}`;
|
|
46
|
+
return gutter;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
onDestroy(() => {
|
|
52
|
+
splitInstance?.destroy();
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
bind:this={container}
|
|
58
|
+
class={finalClass}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
61
|
+
{@render children?.()}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
@reference "../reference.css";
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
children?: Snippet;
|
|
4
|
+
direction?: 'horizontal' | 'vertical';
|
|
5
|
+
sizes?: number[];
|
|
6
|
+
minSize?: number | number[];
|
|
7
|
+
gutterSize?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
};
|
|
11
|
+
declare const Resizable: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type Resizable = ReturnType<typeof Resizable>;
|
|
13
|
+
export default Resizable;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, tick } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(),
|
|
6
|
+
options = [],
|
|
7
|
+
placeholder = 'Select an option',
|
|
8
|
+
searchable = false,
|
|
9
|
+
searchPlaceholder = 'Search...',
|
|
10
|
+
class: className = '',
|
|
11
|
+
...rest
|
|
12
|
+
}: {
|
|
13
|
+
value?: any;
|
|
14
|
+
options: Array<{ value: any; label: string; disabled?: boolean }>;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
searchable?: boolean;
|
|
17
|
+
searchPlaceholder?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let open = $state(false);
|
|
23
|
+
let searchTerm = $state('');
|
|
24
|
+
let activeIndex = $state(-1);
|
|
25
|
+
let container = $state<HTMLDivElement>();
|
|
26
|
+
let filterInput = $state<HTMLInputElement>();
|
|
27
|
+
|
|
28
|
+
const filteredOptions = $derived(
|
|
29
|
+
options.filter(opt =>
|
|
30
|
+
!searchTerm || opt.label.toLowerCase().includes(searchTerm.toLowerCase())
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const selectedLabel = $derived(
|
|
35
|
+
options.find(opt => opt.value === value)?.label || placeholder
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
async function toggleOpen() {
|
|
39
|
+
open = !open;
|
|
40
|
+
if (open) {
|
|
41
|
+
activeIndex = filteredOptions.findIndex(opt => opt.value === value);
|
|
42
|
+
await tick();
|
|
43
|
+
if (searchable) filterInput?.focus();
|
|
44
|
+
|
|
45
|
+
// 自动滚动到选中项
|
|
46
|
+
const selectedEl = container.querySelector('[aria-selected="true"]');
|
|
47
|
+
selectedEl?.scrollIntoView({ block: 'nearest' });
|
|
48
|
+
} else {
|
|
49
|
+
searchTerm = '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleSelect(option: typeof options[0]) {
|
|
54
|
+
if (option.disabled) return;
|
|
55
|
+
value = option.value;
|
|
56
|
+
open = false;
|
|
57
|
+
searchTerm = '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
61
|
+
if (!open) {
|
|
62
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
toggleOpen();
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (event.key) {
|
|
70
|
+
case 'Escape':
|
|
71
|
+
open = false;
|
|
72
|
+
break;
|
|
73
|
+
case 'Enter':
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
if (activeIndex >= 0 && activeIndex < filteredOptions.length) {
|
|
76
|
+
handleSelect(filteredOptions[activeIndex]);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case 'ArrowDown':
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
activeIndex = (activeIndex + 1) % filteredOptions.length;
|
|
82
|
+
scrollToActive();
|
|
83
|
+
break;
|
|
84
|
+
case 'ArrowUp':
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
activeIndex = (activeIndex - 1 + filteredOptions.length) % filteredOptions.length;
|
|
87
|
+
scrollToActive();
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function scrollToActive() {
|
|
93
|
+
tick().then(() => {
|
|
94
|
+
const activeEl = container.querySelector('.active-option');
|
|
95
|
+
activeEl?.scrollIntoView({ block: 'nearest' });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleClickOutside(event: MouseEvent) {
|
|
100
|
+
if (open && container && !container.contains(event.target as Node)) {
|
|
101
|
+
open = false;
|
|
102
|
+
searchTerm = '';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onMount(() => {
|
|
107
|
+
document.addEventListener('click', handleClickOutside);
|
|
108
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
109
|
+
});
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<div bind:this={container} class={['select relative', className].filter(Boolean).join(' ')} {...rest}>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
class="btn btn-outline w-full justify-between"
|
|
116
|
+
aria-expanded={open}
|
|
117
|
+
aria-haspopup="listbox"
|
|
118
|
+
onclick={(e) => { e.stopPropagation(); toggleOpen(); }}
|
|
119
|
+
onkeydown={handleKeyDown}
|
|
120
|
+
>
|
|
121
|
+
<span class="truncate">{selectedLabel}</span>
|
|
122
|
+
<svg class="opacity-50 shrink-0" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
|
123
|
+
</button>
|
|
124
|
+
|
|
125
|
+
{#if open}
|
|
126
|
+
<section
|
|
127
|
+
data-popover
|
|
128
|
+
class="absolute z-50 w-full mt-1 bg-popover border rounded-md shadow-md animate-in fade-in-0 zoom-in-95 overflow-hidden"
|
|
129
|
+
>
|
|
130
|
+
{#if searchable}
|
|
131
|
+
<header class="flex items-center border-b px-3">
|
|
132
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-50 mr-2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
133
|
+
<input
|
|
134
|
+
bind:this={filterInput}
|
|
135
|
+
bind:value={searchTerm}
|
|
136
|
+
type="text"
|
|
137
|
+
placeholder={searchPlaceholder}
|
|
138
|
+
class="flex h-9 w-full bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground border-none focus:ring-0"
|
|
139
|
+
onkeydown={handleKeyDown}
|
|
140
|
+
/>
|
|
141
|
+
</header>
|
|
142
|
+
{/if}
|
|
143
|
+
|
|
144
|
+
<div role="listbox" class="p-1 max-h-48 overflow-y-auto scrollbar">
|
|
145
|
+
{#if filteredOptions.length === 0}
|
|
146
|
+
<div class="px-2 py-4 text-center text-sm text-muted-foreground">
|
|
147
|
+
No results found.
|
|
148
|
+
</div>
|
|
149
|
+
{:else}
|
|
150
|
+
{#each filteredOptions as option, i}
|
|
151
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
152
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
153
|
+
<div
|
|
154
|
+
role="option"
|
|
155
|
+
tabindex="-1"
|
|
156
|
+
data-value={option.value}
|
|
157
|
+
aria-selected={value === option.value}
|
|
158
|
+
aria-disabled={option.disabled}
|
|
159
|
+
class={[
|
|
160
|
+
'relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none',
|
|
161
|
+
activeIndex === i ? 'bg-accent text-accent-foreground active-option' : '',
|
|
162
|
+
value === option.value ? 'bg-accent/50' : ''
|
|
163
|
+
].join(' ')}
|
|
164
|
+
data-disabled={option.disabled}
|
|
165
|
+
onclick={() => handleSelect(option)}
|
|
166
|
+
onmouseenter={() => activeIndex = i}
|
|
167
|
+
>
|
|
168
|
+
{option.label}
|
|
169
|
+
{#if value === option.value}
|
|
170
|
+
<svg class="ml-auto size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
{/each}
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
</section>
|
|
177
|
+
{/if}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<style>
|
|
181
|
+
@import "../../../../ultra/src/css/parts/components/select.css";
|
|
182
|
+
@reference "../reference.css";
|
|
183
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: any;
|
|
3
|
+
options: Array<{
|
|
4
|
+
value: any;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
searchable?: boolean;
|
|
10
|
+
searchPlaceholder?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
};
|
|
14
|
+
declare const Select: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
15
|
+
type Select = ReturnType<typeof Select>;
|
|
16
|
+
export default Select;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
orientation = 'horizontal',
|
|
4
|
+
class: className = '',
|
|
5
|
+
...rest
|
|
6
|
+
}: {
|
|
7
|
+
orientation?: 'horizontal' | 'vertical';
|
|
8
|
+
class?: string;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
} = $props();
|
|
11
|
+
|
|
12
|
+
const finalClass = $derived([
|
|
13
|
+
'shrink-0 bg-border',
|
|
14
|
+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
|
15
|
+
className
|
|
16
|
+
].filter(Boolean).join(' '));
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<hr class={finalClass} aria-orientation={orientation} {...rest} />
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
orientation?: 'horizontal' | 'vertical';
|
|
3
|
+
class?: string;
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
};
|
|
6
|
+
declare const Separator: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type Separator = ReturnType<typeof Separator>;
|
|
8
|
+
export default Separator;
|