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