@mrintel/villain-ui 0.2.2 → 0.3.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/buttons/Button.svelte +33 -0
- package/dist/components/buttons/Button.svelte.d.ts +11 -0
- package/dist/components/buttons/ButtonGroup.svelte +30 -0
- package/dist/components/buttons/ButtonGroup.svelte.d.ts +8 -0
- package/dist/components/buttons/FloatingActionButton.svelte +44 -0
- package/dist/components/buttons/FloatingActionButton.svelte.d.ts +11 -0
- package/dist/components/buttons/IconButton.svelte +53 -0
- package/dist/components/buttons/IconButton.svelte.d.ts +13 -0
- package/dist/components/buttons/LinkButton.svelte +37 -0
- package/dist/components/buttons/LinkButton.svelte.d.ts +12 -0
- package/dist/components/buttons/buttonClasses.d.ts +10 -0
- package/dist/components/buttons/buttonClasses.js +10 -0
- package/dist/components/buttons/index.d.ts +5 -0
- package/dist/components/buttons/index.js +5 -0
- package/dist/components/cards/Card.svelte +46 -0
- package/dist/components/cards/Card.svelte.d.ts +11 -0
- package/dist/components/cards/Container.svelte +33 -0
- package/dist/components/cards/Container.svelte.d.ts +10 -0
- package/dist/components/cards/Divider.svelte +52 -0
- package/dist/components/cards/Divider.svelte.d.ts +9 -0
- package/dist/components/cards/Grid.svelte +44 -0
- package/dist/components/cards/Grid.svelte.d.ts +10 -0
- package/dist/components/cards/Panel.svelte +32 -0
- package/dist/components/cards/Panel.svelte.d.ts +10 -0
- package/dist/components/cards/SectionHeader.svelte +38 -0
- package/dist/components/cards/SectionHeader.svelte.d.ts +11 -0
- package/dist/components/cards/index.d.ts +6 -0
- package/dist/components/cards/index.js +6 -0
- package/dist/components/data/Avatar.svelte +67 -0
- package/dist/components/data/Avatar.svelte.d.ts +10 -0
- package/dist/components/data/Badge.svelte +32 -0
- package/dist/components/data/Badge.svelte.d.ts +8 -0
- package/dist/components/data/CodeBlock.svelte +121 -0
- package/dist/components/data/CodeBlock.svelte.d.ts +32 -0
- package/dist/components/data/List.svelte +64 -0
- package/dist/components/data/List.svelte.d.ts +8 -0
- package/dist/components/data/Pagination.svelte +123 -0
- package/dist/components/data/Pagination.svelte.d.ts +9 -0
- package/dist/components/data/Stat.svelte +103 -0
- package/dist/components/data/Stat.svelte.d.ts +11 -0
- package/dist/components/data/Table.svelte +76 -0
- package/dist/components/data/Table.svelte.d.ts +9 -0
- package/dist/components/data/Tag.svelte +53 -0
- package/dist/components/data/Tag.svelte.d.ts +9 -0
- package/dist/components/data/index.d.ts +8 -0
- package/dist/components/data/index.js +8 -0
- package/dist/components/forms/Checkbox.svelte +51 -0
- package/dist/components/forms/Checkbox.svelte.d.ts +10 -0
- package/dist/components/forms/FileUpload.svelte +164 -0
- package/dist/components/forms/FileUpload.svelte.d.ts +22 -0
- package/dist/components/forms/Input.svelte +57 -0
- package/dist/components/forms/Input.svelte.d.ts +13 -0
- package/dist/components/forms/InputGroup.svelte +7 -0
- package/dist/components/forms/InputGroup.svelte.d.ts +20 -0
- package/dist/components/forms/RadioGroup.svelte +87 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts +15 -0
- package/dist/components/forms/RangeSlider.svelte +116 -0
- package/dist/components/forms/RangeSlider.svelte.d.ts +14 -0
- package/dist/components/forms/Select.svelte +71 -0
- package/dist/components/forms/Select.svelte.d.ts +16 -0
- package/dist/components/forms/Switch.svelte +56 -0
- package/dist/components/forms/Switch.svelte.d.ts +10 -0
- package/dist/components/forms/Textarea.svelte +57 -0
- package/dist/components/forms/Textarea.svelte.d.ts +13 -0
- package/dist/components/forms/index.d.ts +9 -0
- package/dist/components/forms/index.js +9 -0
- package/dist/components/navigation/Breadcrumbs.svelte +59 -0
- package/dist/components/navigation/Breadcrumbs.svelte.d.ts +14 -0
- package/dist/components/navigation/ContextMenu.svelte +83 -0
- package/dist/components/navigation/ContextMenu.svelte.d.ts +11 -0
- package/dist/components/navigation/DropdownMenu.svelte +80 -0
- package/dist/components/navigation/DropdownMenu.svelte.d.ts +10 -0
- package/dist/components/navigation/Menu.svelte +48 -0
- package/dist/components/navigation/Menu.svelte.d.ts +15 -0
- package/dist/components/navigation/Navbar.svelte +32 -0
- package/dist/components/navigation/Navbar.svelte.d.ts +9 -0
- package/dist/components/navigation/Sidebar.svelte +35 -0
- package/dist/components/navigation/Sidebar.svelte.d.ts +10 -0
- package/dist/components/navigation/Tabs.svelte +54 -0
- package/dist/components/navigation/Tabs.svelte.d.ts +15 -0
- package/dist/components/navigation/index.d.ts +7 -0
- package/dist/components/navigation/index.js +7 -0
- package/dist/components/overlays/Alert.svelte +99 -0
- package/dist/components/overlays/Alert.svelte.d.ts +11 -0
- package/dist/components/overlays/CommandPalette.svelte +217 -0
- package/dist/components/overlays/CommandPalette.svelte.d.ts +16 -0
- package/dist/components/overlays/Drawer.svelte +167 -0
- package/dist/components/overlays/Drawer.svelte.d.ts +14 -0
- package/dist/components/overlays/Dropdown.svelte +30 -0
- package/dist/components/overlays/Dropdown.svelte.d.ts +9 -0
- package/dist/components/overlays/Modal.svelte +130 -0
- package/dist/components/overlays/Modal.svelte.d.ts +13 -0
- package/dist/components/overlays/Popover.svelte +131 -0
- package/dist/components/overlays/Popover.svelte.d.ts +11 -0
- package/dist/components/overlays/ProgressBar.svelte +45 -0
- package/dist/components/overlays/ProgressBar.svelte.d.ts +10 -0
- package/dist/components/overlays/SkeletonLoader.svelte +82 -0
- package/dist/components/overlays/SkeletonLoader.svelte.d.ts +9 -0
- package/dist/components/overlays/Spinner.svelte +43 -0
- package/dist/components/overlays/Spinner.svelte.d.ts +7 -0
- package/dist/components/overlays/Toast.svelte +140 -0
- package/dist/components/overlays/Toast.svelte.d.ts +13 -0
- package/dist/components/overlays/Tooltip.svelte +115 -0
- package/dist/components/overlays/Tooltip.svelte.d.ts +10 -0
- package/dist/components/overlays/index.d.ts +11 -0
- package/dist/components/overlays/index.js +11 -0
- package/dist/components/typography/Code.svelte +14 -0
- package/dist/components/typography/Code.svelte.d.ts +6 -0
- package/dist/components/typography/Heading.svelte +22 -0
- package/dist/components/typography/Heading.svelte.d.ts +9 -0
- package/dist/components/typography/Text.svelte +24 -0
- package/dist/components/typography/Text.svelte.d.ts +9 -0
- package/dist/components/typography/index.d.ts +3 -0
- package/dist/components/typography/index.js +3 -0
- package/dist/components/utilities/Accordion.svelte +67 -0
- package/dist/components/utilities/Accordion.svelte.d.ts +14 -0
- package/dist/components/utilities/Carousel.svelte +152 -0
- package/dist/components/utilities/Carousel.svelte.d.ts +16 -0
- package/dist/components/utilities/Collapse.svelte +60 -0
- package/dist/components/utilities/Collapse.svelte.d.ts +10 -0
- package/dist/components/utilities/Portal.svelte +72 -0
- package/dist/components/utilities/Portal.svelte.d.ts +21 -0
- package/dist/components/utilities/ScrollArea.svelte +41 -0
- package/dist/components/utilities/ScrollArea.svelte.d.ts +8 -0
- package/dist/components/utilities/index.d.ts +5 -0
- package/dist/components/utilities/index.js +5 -0
- package/dist/index.d.ts +15 -175
- package/dist/index.js +24 -4560
- package/dist/lib/internal/id.d.ts +12 -0
- package/dist/lib/internal/id.js +15 -0
- package/dist/theme.css +218 -0
- package/package.json +6 -5
- package/dist/index.css +0 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
|
|
3
|
+
interface Tab {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
tabs: Tab[];
|
|
11
|
+
activeTab?: string;
|
|
12
|
+
orientation?: 'horizontal' | 'vertical';
|
|
13
|
+
variant?: 'default' | 'pills';
|
|
14
|
+
onTabChange?: (tabId: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
tabs,
|
|
19
|
+
activeTab = $bindable(''),
|
|
20
|
+
orientation = 'horizontal',
|
|
21
|
+
variant = 'default',
|
|
22
|
+
onTabChange
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
function handleTabClick(tabId: string, disabled?: boolean) {
|
|
26
|
+
if (disabled) return;
|
|
27
|
+
activeTab = tabId;
|
|
28
|
+
if (onTabChange) {
|
|
29
|
+
onTabChange(tabId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const orientationClasses = {
|
|
34
|
+
horizontal: 'flex flex-row',
|
|
35
|
+
vertical: 'flex flex-col'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const radiusClass = variant === 'pills' ? 'rounded-pill' : 'rounded-md';
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div role="tablist" class="{orientationClasses[orientation]} gap-1 p-1">
|
|
42
|
+
{#each tabs as tab}
|
|
43
|
+
<button
|
|
44
|
+
role="tab"
|
|
45
|
+
aria-selected={activeTab === tab.id}
|
|
46
|
+
aria-disabled={tab.disabled}
|
|
47
|
+
onclick={() => handleTabClick(tab.id, tab.disabled)}
|
|
48
|
+
disabled={tab.disabled}
|
|
49
|
+
class="px-4 py-2 {radiusClass} font-body text-sm transition-all duration-300 ease-luxe {activeTab === tab.id ? 'bg-accent text-text accent-glow' : 'text-text-soft hover:bg-base-3'} {tab.disabled ? 'opacity-50 pointer-events-none' : 'cursor-pointer'}"
|
|
50
|
+
>
|
|
51
|
+
{tab.label}
|
|
52
|
+
</button>
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface Tab {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface Props {
|
|
7
|
+
tabs: Tab[];
|
|
8
|
+
activeTab?: string;
|
|
9
|
+
orientation?: 'horizontal' | 'vertical';
|
|
10
|
+
variant?: 'default' | 'pills';
|
|
11
|
+
onTabChange?: (tabId: string) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const Tabs: import("svelte").Component<Props, {}, "activeTab">;
|
|
14
|
+
type Tabs = ReturnType<typeof Tabs>;
|
|
15
|
+
export default Tabs;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Navbar } from './Navbar.svelte';
|
|
2
|
+
export { default as Sidebar } from './Sidebar.svelte';
|
|
3
|
+
export { default as Tabs } from './Tabs.svelte';
|
|
4
|
+
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
|
5
|
+
export { default as Menu } from './Menu.svelte';
|
|
6
|
+
export { default as DropdownMenu } from './DropdownMenu.svelte';
|
|
7
|
+
export { default as ContextMenu } from './ContextMenu.svelte';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Navbar } from './Navbar.svelte';
|
|
2
|
+
export { default as Sidebar } from './Sidebar.svelte';
|
|
3
|
+
export { default as Tabs } from './Tabs.svelte';
|
|
4
|
+
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
|
5
|
+
export { default as Menu } from './Menu.svelte';
|
|
6
|
+
export { default as DropdownMenu } from './DropdownMenu.svelte';
|
|
7
|
+
export { default as ContextMenu } from './ContextMenu.svelte';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
variant?: 'info' | 'success' | 'warning' | 'error';
|
|
6
|
+
title?: string;
|
|
7
|
+
dismissible?: boolean;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
variant = 'info',
|
|
14
|
+
title,
|
|
15
|
+
dismissible = false,
|
|
16
|
+
onclose,
|
|
17
|
+
children
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
let visible = $state(true);
|
|
21
|
+
|
|
22
|
+
const variantClasses = {
|
|
23
|
+
info: 'border-l-4 border-accent',
|
|
24
|
+
success: 'border-l-4 border-success',
|
|
25
|
+
warning: 'border-l-4 border-warning',
|
|
26
|
+
error: 'border-l-4 border-error'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const variantTextClasses = {
|
|
30
|
+
info: 'text-accent-soft',
|
|
31
|
+
success: 'text-success',
|
|
32
|
+
warning: 'text-warning',
|
|
33
|
+
error: 'text-error'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const variantIcons = {
|
|
37
|
+
info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
38
|
+
success: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
39
|
+
warning: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
|
|
40
|
+
error: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const roleMap = {
|
|
44
|
+
info: 'status',
|
|
45
|
+
success: 'status',
|
|
46
|
+
warning: 'alert',
|
|
47
|
+
error: 'alert'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ariaLiveMap = {
|
|
51
|
+
info: 'polite',
|
|
52
|
+
success: 'polite',
|
|
53
|
+
warning: 'polite',
|
|
54
|
+
error: 'assertive'
|
|
55
|
+
} satisfies Record<'info' | 'success' | 'warning' | 'error', 'polite' | 'assertive'>;
|
|
56
|
+
|
|
57
|
+
function handleClose() {
|
|
58
|
+
visible = false;
|
|
59
|
+
onclose?.();
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
{#if visible}
|
|
64
|
+
<div
|
|
65
|
+
class="glass-panel rounded-lg p-4 flex gap-3 {variantClasses[variant]} animate-[fade-in_0.2s_var(--ease-luxe)]"
|
|
66
|
+
role={roleMap[variant]}
|
|
67
|
+
aria-live={ariaLiveMap[variant]}
|
|
68
|
+
>
|
|
69
|
+
<div class="shrink-0 {variantTextClasses[variant]}">
|
|
70
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
71
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={variantIcons[variant]} />
|
|
72
|
+
</svg>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="flex-1 min-w-0">
|
|
76
|
+
{#if title}
|
|
77
|
+
<h4 class="font-semibold text-text mb-1">
|
|
78
|
+
{title}
|
|
79
|
+
</h4>
|
|
80
|
+
{/if}
|
|
81
|
+
<div class="text-sm text-text-soft">
|
|
82
|
+
{@render children?.()}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{#if dismissible}
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onclick={handleClose}
|
|
90
|
+
class="shrink-0 text-text-soft hover:text-text transition-colors"
|
|
91
|
+
aria-label="Dismiss alert"
|
|
92
|
+
>
|
|
93
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
94
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
95
|
+
</svg>
|
|
96
|
+
</button>
|
|
97
|
+
{/if}
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
variant?: 'info' | 'success' | 'warning' | 'error';
|
|
4
|
+
title?: string;
|
|
5
|
+
dismissible?: boolean;
|
|
6
|
+
onclose?: () => void;
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
}
|
|
9
|
+
declare const Alert: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Alert = ReturnType<typeof Alert>;
|
|
11
|
+
export default Alert;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { createId } from '../../lib/internal/id.js';
|
|
4
|
+
|
|
5
|
+
interface Command {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: Snippet;
|
|
9
|
+
onSelect: () => void;
|
|
10
|
+
keywords?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
open?: boolean;
|
|
15
|
+
commands: Command[];
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
open = $bindable(false),
|
|
21
|
+
commands,
|
|
22
|
+
placeholder = 'Search commands...'
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
let query = $state('');
|
|
26
|
+
let selectedIndex = $state(0);
|
|
27
|
+
let inputElement = $state<HTMLInputElement>();
|
|
28
|
+
let previousFocus = $state<HTMLElement | null>(null);
|
|
29
|
+
|
|
30
|
+
const paletteId = createId('command-palette');
|
|
31
|
+
|
|
32
|
+
// Inline fuzzy matching function
|
|
33
|
+
function fuzzyMatch(text: string, search: string): boolean {
|
|
34
|
+
const searchChars = search.toLowerCase().split('');
|
|
35
|
+
const textLower = text.toLowerCase();
|
|
36
|
+
let searchIndex = 0;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < textLower.length && searchIndex < searchChars.length; i++) {
|
|
39
|
+
if (textLower[i] === searchChars[searchIndex]) {
|
|
40
|
+
searchIndex++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return searchIndex === searchChars.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let filteredCommands = $derived(
|
|
48
|
+
query.trim() === ''
|
|
49
|
+
? commands
|
|
50
|
+
: commands.filter(cmd => {
|
|
51
|
+
// Match against label
|
|
52
|
+
if (fuzzyMatch(cmd.label, query)) return true;
|
|
53
|
+
|
|
54
|
+
// Match against keywords
|
|
55
|
+
if (cmd.keywords) {
|
|
56
|
+
return cmd.keywords.some(keyword => fuzzyMatch(keyword, query));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return false;
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
function handleClose() {
|
|
64
|
+
open = false;
|
|
65
|
+
query = '';
|
|
66
|
+
selectedIndex = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
70
|
+
if (event.target === event.currentTarget) {
|
|
71
|
+
handleClose();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleSelectCommand(command: Command) {
|
|
76
|
+
command.onSelect();
|
|
77
|
+
handleClose();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
81
|
+
if (event.key === 'ArrowDown') {
|
|
82
|
+
if (!filteredCommands.length) return;
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
selectedIndex = (selectedIndex + 1) % filteredCommands.length;
|
|
85
|
+
} else if (event.key === 'ArrowUp') {
|
|
86
|
+
if (!filteredCommands.length) return;
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
selectedIndex = (selectedIndex - 1 + filteredCommands.length) % filteredCommands.length;
|
|
89
|
+
} else if (event.key === 'Enter') {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
if (filteredCommands[selectedIndex]) {
|
|
92
|
+
handleSelectCommand(filteredCommands[selectedIndex]);
|
|
93
|
+
} else if (filteredCommands.length) {
|
|
94
|
+
selectedIndex = 0;
|
|
95
|
+
}
|
|
96
|
+
} else if (event.key === 'Escape') {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
handleClose();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Reset and clamp selected index when filtered commands change
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (filteredCommands.length === 0) {
|
|
105
|
+
selectedIndex = -1;
|
|
106
|
+
} else if (selectedIndex >= filteredCommands.length || selectedIndex < 0) {
|
|
107
|
+
selectedIndex = 0;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
$effect(() => {
|
|
112
|
+
if (typeof document === 'undefined') return;
|
|
113
|
+
|
|
114
|
+
if (open) {
|
|
115
|
+
// Store previous focus
|
|
116
|
+
previousFocus = document.activeElement as HTMLElement;
|
|
117
|
+
|
|
118
|
+
// Prevent body scroll
|
|
119
|
+
document.body.style.overflow = 'hidden';
|
|
120
|
+
|
|
121
|
+
// Focus input element
|
|
122
|
+
requestAnimationFrame(() => {
|
|
123
|
+
inputElement?.focus();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
// Restore body scroll
|
|
128
|
+
document.body.style.overflow = '';
|
|
129
|
+
|
|
130
|
+
// Restore previous focus
|
|
131
|
+
previousFocus?.focus();
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
{#if open}
|
|
138
|
+
<div
|
|
139
|
+
class="fixed inset-0 z-50 flex items-start justify-center pt-[20vh] p-4 bg-overlay backdrop-blur-md animate-[fade-in_0.2s_var(--ease-luxe)]"
|
|
140
|
+
onclick={handleBackdropClick}
|
|
141
|
+
role="presentation"
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
class="glass-panel rounded-xl shadow-deep w-full max-w-[36rem] animate-[fade-up_0.3s_var(--ease-luxe)]"
|
|
145
|
+
role="combobox"
|
|
146
|
+
aria-expanded="true"
|
|
147
|
+
aria-haspopup="listbox"
|
|
148
|
+
aria-controls={paletteId}
|
|
149
|
+
aria-activedescendant={selectedIndex >= 0 && selectedIndex < filteredCommands.length && filteredCommands[selectedIndex] ? `cmd-${filteredCommands[selectedIndex].id}` : undefined}
|
|
150
|
+
tabindex="0"
|
|
151
|
+
>
|
|
152
|
+
<div class="p-4 border-b border-border">
|
|
153
|
+
<input
|
|
154
|
+
bind:this={inputElement}
|
|
155
|
+
bind:value={query}
|
|
156
|
+
type="text"
|
|
157
|
+
placeholder={placeholder}
|
|
158
|
+
onkeydown={handleKeyDown}
|
|
159
|
+
class="w-full bg-transparent border-none outline-none text-text text-lg placeholder:text-text-muted"
|
|
160
|
+
aria-autocomplete="list"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div
|
|
165
|
+
id={paletteId}
|
|
166
|
+
role="listbox"
|
|
167
|
+
class="max-h-[400px] overflow-y-auto"
|
|
168
|
+
style="scrollbar-width: thin; scrollbar-color: var(--color-accent) var(--color-base-3);"
|
|
169
|
+
>
|
|
170
|
+
{#if filteredCommands.length === 0}
|
|
171
|
+
<div class="p-8 text-center text-text-muted">
|
|
172
|
+
No commands found
|
|
173
|
+
</div>
|
|
174
|
+
{:else}
|
|
175
|
+
{#each filteredCommands as command, index (command.id)}
|
|
176
|
+
<button
|
|
177
|
+
id="cmd-{command.id}"
|
|
178
|
+
type="button"
|
|
179
|
+
role="option"
|
|
180
|
+
aria-selected={index === selectedIndex}
|
|
181
|
+
tabindex={index === selectedIndex ? 0 : -1}
|
|
182
|
+
onclick={() => handleSelectCommand(command)}
|
|
183
|
+
class="w-full flex items-center gap-3 p-4 text-left text-text hover:bg-base-3 transition-colors {index === selectedIndex ? 'accent-glow bg-base-3' : ''}"
|
|
184
|
+
>
|
|
185
|
+
{#if command.icon}
|
|
186
|
+
<div class="shrink-0">
|
|
187
|
+
{@render command.icon()}
|
|
188
|
+
</div>
|
|
189
|
+
{/if}
|
|
190
|
+
<span>{command.label}</span>
|
|
191
|
+
</button>
|
|
192
|
+
{/each}
|
|
193
|
+
{/if}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
{/if}
|
|
198
|
+
|
|
199
|
+
<style>
|
|
200
|
+
div[role="listbox"]::-webkit-scrollbar {
|
|
201
|
+
width: 8px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
div[role="listbox"]::-webkit-scrollbar-track {
|
|
205
|
+
background: var(--color-base-3);
|
|
206
|
+
border-radius: var(--radius-sm);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
div[role="listbox"]::-webkit-scrollbar-thumb {
|
|
210
|
+
background: var(--color-accent);
|
|
211
|
+
border-radius: var(--radius-sm);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
div[role="listbox"]::-webkit-scrollbar-thumb:hover {
|
|
215
|
+
background: var(--color-accent-soft);
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Command {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: Snippet;
|
|
6
|
+
onSelect: () => void;
|
|
7
|
+
keywords?: string[];
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
open?: boolean;
|
|
11
|
+
commands: Command[];
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const CommandPalette: import("svelte").Component<Props, {}, "open">;
|
|
15
|
+
type CommandPalette = ReturnType<typeof CommandPalette>;
|
|
16
|
+
export default CommandPalette;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { createId } from '../../lib/internal/id.js';
|
|
4
|
+
import ScrollArea from '../utilities/ScrollArea.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
open?: boolean;
|
|
8
|
+
side?: 'left' | 'right' | 'top' | 'bottom';
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
title?: string;
|
|
11
|
+
closeOnBackdrop?: boolean;
|
|
12
|
+
closeOnEscape?: boolean;
|
|
13
|
+
children?: Snippet;
|
|
14
|
+
footer?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
open = $bindable(false),
|
|
19
|
+
side = 'right',
|
|
20
|
+
size = 'md',
|
|
21
|
+
title,
|
|
22
|
+
closeOnBackdrop = true,
|
|
23
|
+
closeOnEscape = true,
|
|
24
|
+
children,
|
|
25
|
+
footer
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let drawerElement = $state<HTMLDivElement>();
|
|
29
|
+
let previousFocus = $state<HTMLElement | null>(null);
|
|
30
|
+
|
|
31
|
+
const titleId = createId('drawer-title');
|
|
32
|
+
|
|
33
|
+
const sizeClasses = {
|
|
34
|
+
left: {
|
|
35
|
+
sm: 'max-w-[20rem]',
|
|
36
|
+
md: 'max-w-[28rem]',
|
|
37
|
+
lg: 'max-w-[36rem]'
|
|
38
|
+
},
|
|
39
|
+
right: {
|
|
40
|
+
sm: 'max-w-[20rem]',
|
|
41
|
+
md: 'max-w-[28rem]',
|
|
42
|
+
lg: 'max-w-[36rem]'
|
|
43
|
+
},
|
|
44
|
+
top: {
|
|
45
|
+
sm: 'max-h-[20rem]',
|
|
46
|
+
md: 'max-h-[28rem]',
|
|
47
|
+
lg: 'max-h-[36rem]'
|
|
48
|
+
},
|
|
49
|
+
bottom: {
|
|
50
|
+
sm: 'max-h-[20rem]',
|
|
51
|
+
md: 'max-h-[28rem]',
|
|
52
|
+
lg: 'max-h-[36rem]'
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const positionClasses = {
|
|
57
|
+
left: 'left-0 top-0 h-full',
|
|
58
|
+
right: 'right-0 top-0 h-full',
|
|
59
|
+
top: 'top-0 left-0 w-full',
|
|
60
|
+
bottom: 'bottom-0 left-0 w-full'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const animationClasses = {
|
|
64
|
+
left: 'animate-[slide-in-left_0.3s_var(--ease-luxe)]',
|
|
65
|
+
right: 'animate-[slide-in-right_0.3s_var(--ease-luxe)]',
|
|
66
|
+
top: 'animate-[slide-in-top_0.3s_var(--ease-luxe)]',
|
|
67
|
+
bottom: 'animate-[slide-in-bottom_0.3s_var(--ease-luxe)]'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function handleClose() {
|
|
71
|
+
open = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
75
|
+
if (closeOnBackdrop && event.target === event.currentTarget) {
|
|
76
|
+
handleClose();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleEscape(event: KeyboardEvent) {
|
|
81
|
+
if (closeOnEscape && event.key === 'Escape') {
|
|
82
|
+
handleClose();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
$effect(() => {
|
|
87
|
+
if (typeof document === 'undefined') return;
|
|
88
|
+
|
|
89
|
+
if (open) {
|
|
90
|
+
// Store previous focus
|
|
91
|
+
previousFocus = document.activeElement as HTMLElement;
|
|
92
|
+
|
|
93
|
+
// Prevent body scroll
|
|
94
|
+
document.body.style.overflow = 'hidden';
|
|
95
|
+
|
|
96
|
+
// Add event listeners
|
|
97
|
+
document.addEventListener('keydown', handleEscape);
|
|
98
|
+
|
|
99
|
+
// Focus first interactive element
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
const firstInteractive = drawerElement?.querySelector<HTMLElement>(
|
|
102
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
103
|
+
);
|
|
104
|
+
firstInteractive?.focus();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
// Restore body scroll
|
|
109
|
+
document.body.style.overflow = '';
|
|
110
|
+
|
|
111
|
+
// Remove event listeners
|
|
112
|
+
document.removeEventListener('keydown', handleEscape);
|
|
113
|
+
|
|
114
|
+
// Restore previous focus
|
|
115
|
+
previousFocus?.focus();
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
{#if open}
|
|
122
|
+
<div
|
|
123
|
+
class="fixed inset-0 z-50 bg-overlay backdrop-blur-md animate-[fade-in_0.2s_var(--ease-luxe)]"
|
|
124
|
+
onclick={handleBackdropClick}
|
|
125
|
+
role="presentation"
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
bind:this={drawerElement}
|
|
129
|
+
class="glass-panel shadow-deep fixed {positionClasses[side]} {sizeClasses[side][size]} {animationClasses[side]} flex flex-col"
|
|
130
|
+
role="dialog"
|
|
131
|
+
aria-modal="true"
|
|
132
|
+
aria-labelledby={title ? titleId : undefined}
|
|
133
|
+
>
|
|
134
|
+
{#if title}
|
|
135
|
+
<div class="flex items-center justify-between p-6 border-b border-border">
|
|
136
|
+
<h2 id={titleId} class="text-xl font-semibold text-text">
|
|
137
|
+
{title}
|
|
138
|
+
</h2>
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onclick={handleClose}
|
|
142
|
+
class="text-text-soft hover:text-text transition-colors"
|
|
143
|
+
aria-label="Close drawer"
|
|
144
|
+
>
|
|
145
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
146
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
147
|
+
</svg>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
{/if}
|
|
151
|
+
|
|
152
|
+
<div class="flex-1 overflow-hidden">
|
|
153
|
+
<ScrollArea height="100%">
|
|
154
|
+
<div class="p-6">
|
|
155
|
+
{@render children?.()}
|
|
156
|
+
</div>
|
|
157
|
+
</ScrollArea>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{#if footer}
|
|
161
|
+
<div class="flex items-center justify-end gap-3 p-6 border-t border-border">
|
|
162
|
+
{@render footer?.()}
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
{/if}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
side?: 'left' | 'right' | 'top' | 'bottom';
|
|
5
|
+
size?: 'sm' | 'md' | 'lg';
|
|
6
|
+
title?: string;
|
|
7
|
+
closeOnBackdrop?: boolean;
|
|
8
|
+
closeOnEscape?: boolean;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
footer?: Snippet;
|
|
11
|
+
}
|
|
12
|
+
declare const Drawer: import("svelte").Component<Props, {}, "open">;
|
|
13
|
+
type Drawer = ReturnType<typeof Drawer>;
|
|
14
|
+
export default Drawer;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
open = $bindable(false),
|
|
12
|
+
placement = 'bottom-start',
|
|
13
|
+
children
|
|
14
|
+
}: Props = $props();
|
|
15
|
+
|
|
16
|
+
const placementClasses = {
|
|
17
|
+
'bottom-start': 'top-full left-0 mt-2',
|
|
18
|
+
'bottom-end': 'top-full right-0 mt-2',
|
|
19
|
+
'top-start': 'bottom-full left-0 mb-2',
|
|
20
|
+
'top-end': 'bottom-full right-0 mb-2'
|
|
21
|
+
};
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if open}
|
|
25
|
+
<div
|
|
26
|
+
class="absolute {placementClasses[placement]} z-50 glass-panel rounded-[var(--radius-lg)] shadow-[var(--shadow-deep)] min-w-[12rem] animate-[fade-up_0.2s_var(--ease-luxe)]"
|
|
27
|
+
>
|
|
28
|
+
{@render children?.()}
|
|
29
|
+
</div>
|
|
30
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
}
|
|
7
|
+
declare const Dropdown: import("svelte").Component<Props, {}, "open">;
|
|
8
|
+
type Dropdown = ReturnType<typeof Dropdown>;
|
|
9
|
+
export default Dropdown;
|