@mrintel/villain-ui 0.2.0 → 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 +14 -7
- package/dist/index.css +0 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { createId } from '../../lib/internal/id.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
open?: boolean;
|
|
7
|
+
title?: string;
|
|
8
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
9
|
+
closeOnBackdrop?: boolean;
|
|
10
|
+
closeOnEscape?: boolean;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
footer?: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
open = $bindable(false),
|
|
17
|
+
title,
|
|
18
|
+
size = 'md',
|
|
19
|
+
closeOnBackdrop = true,
|
|
20
|
+
closeOnEscape = true,
|
|
21
|
+
children,
|
|
22
|
+
footer
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
let modalElement = $state<HTMLDivElement>();
|
|
26
|
+
let previousFocus = $state<HTMLElement | null>(null);
|
|
27
|
+
|
|
28
|
+
const sizeClasses = {
|
|
29
|
+
sm: 'max-w-[28rem]',
|
|
30
|
+
md: 'max-w-[36rem]',
|
|
31
|
+
lg: 'max-w-[48rem]',
|
|
32
|
+
xl: 'max-w-[64rem]'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const titleId = createId('modal-title');
|
|
36
|
+
|
|
37
|
+
function handleClose() {
|
|
38
|
+
open = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
42
|
+
if (closeOnBackdrop && event.target === event.currentTarget) {
|
|
43
|
+
handleClose();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleEscape(event: KeyboardEvent) {
|
|
48
|
+
if (closeOnEscape && event.key === 'Escape') {
|
|
49
|
+
handleClose();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
$effect(() => {
|
|
54
|
+
if (typeof document === 'undefined') return;
|
|
55
|
+
|
|
56
|
+
if (open) {
|
|
57
|
+
// Store previous focus
|
|
58
|
+
previousFocus = document.activeElement as HTMLElement;
|
|
59
|
+
|
|
60
|
+
// Prevent body scroll
|
|
61
|
+
document.body.style.overflow = 'hidden';
|
|
62
|
+
|
|
63
|
+
// Add event listeners
|
|
64
|
+
document.addEventListener('keydown', handleEscape);
|
|
65
|
+
|
|
66
|
+
// Focus first interactive element
|
|
67
|
+
requestAnimationFrame(() => {
|
|
68
|
+
const firstInteractive = modalElement?.querySelector<HTMLElement>(
|
|
69
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
70
|
+
);
|
|
71
|
+
firstInteractive?.focus();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
// Restore body scroll
|
|
76
|
+
document.body.style.overflow = '';
|
|
77
|
+
|
|
78
|
+
// Remove event listeners
|
|
79
|
+
document.removeEventListener('keydown', handleEscape);
|
|
80
|
+
|
|
81
|
+
// Restore previous focus
|
|
82
|
+
previousFocus?.focus();
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
{#if open}
|
|
89
|
+
<div
|
|
90
|
+
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-overlay backdrop-blur-md animate-[fade-in_0.2s_var(--ease-luxe)]"
|
|
91
|
+
onclick={handleBackdropClick}
|
|
92
|
+
role="presentation"
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
bind:this={modalElement}
|
|
96
|
+
class="glass-panel rounded-xl shadow-deep w-full {sizeClasses[size]} animate-[fade-up_0.3s_var(--ease-luxe)] flex flex-col max-h-[90vh]"
|
|
97
|
+
role="dialog"
|
|
98
|
+
aria-modal="true"
|
|
99
|
+
aria-labelledby={title ? titleId : undefined}
|
|
100
|
+
>
|
|
101
|
+
{#if title}
|
|
102
|
+
<div class="flex items-center justify-between p-6 border-b border-border">
|
|
103
|
+
<h2 id={titleId} class="text-xl font-semibold text-text">
|
|
104
|
+
{title}
|
|
105
|
+
</h2>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onclick={handleClose}
|
|
109
|
+
class="text-text-soft hover:text-text transition-colors"
|
|
110
|
+
aria-label="Close modal"
|
|
111
|
+
>
|
|
112
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
113
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
114
|
+
</svg>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
118
|
+
|
|
119
|
+
<div class="flex-1 overflow-y-auto p-6 max-h-[70vh]" style="scrollbar-width: thin; scrollbar-color: var(--color-accent) var(--color-base-3);">
|
|
120
|
+
{@render children?.()}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{#if footer}
|
|
124
|
+
<div class="flex items-center justify-end gap-3 p-6 border-t border-border">
|
|
125
|
+
{@render footer?.()}
|
|
126
|
+
</div>
|
|
127
|
+
{/if}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
title?: string;
|
|
5
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
6
|
+
closeOnBackdrop?: boolean;
|
|
7
|
+
closeOnEscape?: boolean;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
footer?: Snippet;
|
|
10
|
+
}
|
|
11
|
+
declare const Modal: import("svelte").Component<Props, {}, "open">;
|
|
12
|
+
type Modal = ReturnType<typeof Modal>;
|
|
13
|
+
export default Modal;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { createId } from '../../lib/internal/id.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
open?: boolean;
|
|
7
|
+
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'right';
|
|
8
|
+
closeOnClickOutside?: boolean;
|
|
9
|
+
trigger?: Snippet;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
open = $bindable(false),
|
|
15
|
+
placement = 'bottom',
|
|
16
|
+
closeOnClickOutside = true,
|
|
17
|
+
trigger,
|
|
18
|
+
children
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
let popoverElement = $state<HTMLDivElement>();
|
|
22
|
+
let wrapperElement = $state<HTMLDivElement>();
|
|
23
|
+
let actualPlacement = $state(placement);
|
|
24
|
+
|
|
25
|
+
const popoverId = createId('popover');
|
|
26
|
+
|
|
27
|
+
const placementClasses = {
|
|
28
|
+
'top': 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
|
29
|
+
'top-start': 'bottom-full left-0 mb-2',
|
|
30
|
+
'top-end': 'bottom-full right-0 mb-2',
|
|
31
|
+
'bottom': 'top-full left-1/2 -translate-x-1/2 mt-2',
|
|
32
|
+
'bottom-start': 'top-full left-0 mt-2',
|
|
33
|
+
'bottom-end': 'top-full right-0 mt-2',
|
|
34
|
+
'left': 'right-full top-1/2 -translate-y-1/2 mr-2',
|
|
35
|
+
'right': 'left-full top-1/2 -translate-y-1/2 ml-2'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const oppositePlacement = {
|
|
39
|
+
'top': 'bottom',
|
|
40
|
+
'top-start': 'bottom-start',
|
|
41
|
+
'top-end': 'bottom-end',
|
|
42
|
+
'bottom': 'top',
|
|
43
|
+
'bottom-start': 'top-start',
|
|
44
|
+
'bottom-end': 'top-end',
|
|
45
|
+
'left': 'right',
|
|
46
|
+
'right': 'left'
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
function toggleOpen() {
|
|
50
|
+
open = !open;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleClickOutside(event: MouseEvent) {
|
|
54
|
+
if (closeOnClickOutside && popoverElement && wrapperElement && !wrapperElement.contains(event.target as Node)) {
|
|
55
|
+
open = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleEscape(event: KeyboardEvent) {
|
|
60
|
+
if (event.key === 'Escape') {
|
|
61
|
+
open = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Reset actualPlacement when visibility changes
|
|
66
|
+
$effect(() => {
|
|
67
|
+
if (!open) {
|
|
68
|
+
actualPlacement = placement;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Check viewport bounds and flip placement if needed
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (typeof window === 'undefined') return;
|
|
75
|
+
|
|
76
|
+
if (open && popoverElement) {
|
|
77
|
+
const rect = popoverElement.getBoundingClientRect();
|
|
78
|
+
const viewportWidth = window.innerWidth;
|
|
79
|
+
const viewportHeight = window.innerHeight;
|
|
80
|
+
|
|
81
|
+
// Determine if current placement overflows and flip if needed
|
|
82
|
+
if (actualPlacement.startsWith('top') && rect.top < 0) {
|
|
83
|
+
actualPlacement = oppositePlacement[actualPlacement] || placement;
|
|
84
|
+
} else if (actualPlacement.startsWith('bottom') && rect.bottom > viewportHeight) {
|
|
85
|
+
actualPlacement = oppositePlacement[actualPlacement] || placement;
|
|
86
|
+
} else if (actualPlacement === 'left' && rect.left < 0) {
|
|
87
|
+
actualPlacement = 'right';
|
|
88
|
+
} else if (actualPlacement === 'right' && rect.right > viewportWidth) {
|
|
89
|
+
actualPlacement = 'left';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
$effect(() => {
|
|
95
|
+
if (typeof document === 'undefined') return;
|
|
96
|
+
|
|
97
|
+
if (open) {
|
|
98
|
+
document.addEventListener('click', handleClickOutside);
|
|
99
|
+
document.addEventListener('keydown', handleEscape);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
document.removeEventListener('click', handleClickOutside);
|
|
103
|
+
document.removeEventListener('keydown', handleEscape);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<div bind:this={wrapperElement} class="relative inline-block">
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
onclick={toggleOpen}
|
|
113
|
+
aria-haspopup="true"
|
|
114
|
+
aria-expanded={open}
|
|
115
|
+
aria-controls={open ? popoverId : undefined}
|
|
116
|
+
class="bg-transparent border-none p-0 cursor-pointer"
|
|
117
|
+
>
|
|
118
|
+
{@render trigger?.()}
|
|
119
|
+
</button>
|
|
120
|
+
|
|
121
|
+
{#if open}
|
|
122
|
+
<div
|
|
123
|
+
bind:this={popoverElement}
|
|
124
|
+
id={popoverId}
|
|
125
|
+
class="absolute {placementClasses[actualPlacement]} z-50 glass-panel rounded-lg shadow-deep animate-[fade-up_0.2s_var(--ease-luxe)]"
|
|
126
|
+
role="dialog"
|
|
127
|
+
>
|
|
128
|
+
{@render children?.()}
|
|
129
|
+
</div>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'right';
|
|
5
|
+
closeOnClickOutside?: boolean;
|
|
6
|
+
trigger?: Snippet;
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
}
|
|
9
|
+
declare const Popover: import("svelte").Component<Props, {}, "open">;
|
|
10
|
+
type Popover = ReturnType<typeof Popover>;
|
|
11
|
+
export default Popover;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
size?: 'sm' | 'md' | 'lg';
|
|
6
|
+
showLabel?: boolean;
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
value,
|
|
12
|
+
max = 100,
|
|
13
|
+
size = 'md',
|
|
14
|
+
showLabel = false,
|
|
15
|
+
label
|
|
16
|
+
}: Props = $props();
|
|
17
|
+
|
|
18
|
+
const percentage = $derived(Math.min(100, Math.max(0, (value / max) * 100)));
|
|
19
|
+
|
|
20
|
+
const heightClasses = {
|
|
21
|
+
sm: 'h-2',
|
|
22
|
+
md: 'h-3',
|
|
23
|
+
lg: 'h-4'
|
|
24
|
+
};
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div
|
|
28
|
+
class="relative bg-[var(--color-base-3)] border border-[var(--color-border)] rounded-[var(--radius-pill)] overflow-hidden shadow-[var(--shadow-deep)] {heightClasses[size]}"
|
|
29
|
+
role="progressbar"
|
|
30
|
+
aria-valuenow={value}
|
|
31
|
+
aria-valuemin="0"
|
|
32
|
+
aria-valuemax={max}
|
|
33
|
+
aria-label={label || `${percentage.toFixed(0)}% complete`}
|
|
34
|
+
>
|
|
35
|
+
<div
|
|
36
|
+
class="h-full bg-[var(--color-accent)] accent-glow transition-all duration-500 ease-[var(--ease-luxe)]"
|
|
37
|
+
style="width: {percentage}%"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
{#if showLabel}
|
|
41
|
+
<div class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-[var(--color-text)] text-glow">
|
|
42
|
+
{label || `${percentage.toFixed(0)}%`}
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
value: number;
|
|
3
|
+
max?: number;
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
showLabel?: boolean;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ProgressBar: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ProgressBar = ReturnType<typeof ProgressBar>;
|
|
10
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
variant?: 'text' | 'circular' | 'rectangular';
|
|
4
|
+
width?: string;
|
|
5
|
+
height?: string;
|
|
6
|
+
count?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
variant = 'rectangular',
|
|
11
|
+
width,
|
|
12
|
+
height,
|
|
13
|
+
count = 1
|
|
14
|
+
}: Props = $props();
|
|
15
|
+
|
|
16
|
+
const defaultDimensions = {
|
|
17
|
+
text: { width: '100%', height: '1rem' },
|
|
18
|
+
circular: { width: '3rem', height: '3rem' },
|
|
19
|
+
rectangular: { width: '100%', height: '8rem' }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const roundingClasses = {
|
|
23
|
+
text: 'rounded-[var(--radius-sm)]',
|
|
24
|
+
circular: 'rounded-[var(--radius-pill)]',
|
|
25
|
+
rectangular: 'rounded-[var(--radius-lg)]'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const finalWidth = width || defaultDimensions[variant].width;
|
|
29
|
+
const finalHeight = height || defaultDimensions[variant].height;
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div
|
|
33
|
+
aria-busy="true"
|
|
34
|
+
aria-live="polite"
|
|
35
|
+
aria-label="Loading content"
|
|
36
|
+
>
|
|
37
|
+
{#if variant === 'text'}
|
|
38
|
+
{#each Array(count) as _, i}
|
|
39
|
+
<div
|
|
40
|
+
class="bg-[var(--color-base-3)] border border-[var(--color-border)] {roundingClasses[variant]} mb-2 skeleton-shimmer"
|
|
41
|
+
style="width: {i === count - 1 ? '80%' : finalWidth}; height: {finalHeight};"
|
|
42
|
+
/>
|
|
43
|
+
{/each}
|
|
44
|
+
{:else}
|
|
45
|
+
<div
|
|
46
|
+
class="bg-[var(--color-base-3)] border border-[var(--color-border)] {roundingClasses[variant]} skeleton-shimmer"
|
|
47
|
+
style="width: {finalWidth}; height: {finalHeight};"
|
|
48
|
+
/>
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.skeleton-shimmer {
|
|
54
|
+
position: relative;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.skeleton-shimmer::before {
|
|
59
|
+
content: '';
|
|
60
|
+
position: absolute;
|
|
61
|
+
top: 0;
|
|
62
|
+
left: 0;
|
|
63
|
+
width: 200%;
|
|
64
|
+
height: 100%;
|
|
65
|
+
background: linear-gradient(
|
|
66
|
+
90deg,
|
|
67
|
+
transparent,
|
|
68
|
+
rgba(127, 61, 255, 0.1),
|
|
69
|
+
transparent
|
|
70
|
+
);
|
|
71
|
+
animation: shimmer 1.5s ease-in-out infinite;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@keyframes shimmer {
|
|
75
|
+
0% {
|
|
76
|
+
transform: translateX(-100%);
|
|
77
|
+
}
|
|
78
|
+
100% {
|
|
79
|
+
transform: translateX(100%);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
variant?: 'text' | 'circular' | 'rectangular';
|
|
3
|
+
width?: string;
|
|
4
|
+
height?: string;
|
|
5
|
+
count?: number;
|
|
6
|
+
}
|
|
7
|
+
declare const SkeletonLoader: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type SkeletonLoader = ReturnType<typeof SkeletonLoader>;
|
|
9
|
+
export default SkeletonLoader;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: 'sm' | 'md' | 'lg';
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
size = 'md',
|
|
9
|
+
label
|
|
10
|
+
}: Props = $props();
|
|
11
|
+
|
|
12
|
+
const sizeClasses = {
|
|
13
|
+
sm: 'w-4 h-4 border-2',
|
|
14
|
+
md: 'w-8 h-8 border-4',
|
|
15
|
+
lg: 'w-12 h-12 border-4'
|
|
16
|
+
};
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div
|
|
20
|
+
class="inline-flex items-center justify-center"
|
|
21
|
+
role="status"
|
|
22
|
+
aria-live="polite"
|
|
23
|
+
aria-label={label || 'Loading'}
|
|
24
|
+
>
|
|
25
|
+
<div
|
|
26
|
+
class="{sizeClasses[size]} border-[var(--color-base-3)] border-t-[var(--color-accent)] rounded-[var(--radius-pill)] accent-glow animate-spin"
|
|
27
|
+
/>
|
|
28
|
+
<span class="sr-only">{label || 'Loading'}</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
.sr-only {
|
|
33
|
+
position: absolute;
|
|
34
|
+
width: 1px;
|
|
35
|
+
height: 1px;
|
|
36
|
+
padding: 0;
|
|
37
|
+
margin: -1px;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
clip: rect(0, 0, 0, 0);
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
border-width: 0;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
message: string;
|
|
4
|
+
variant?: 'info' | 'success' | 'warning' | 'error';
|
|
5
|
+
duration?: number;
|
|
6
|
+
position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
7
|
+
dismissible?: boolean;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
/** Index in toast stack for vertical offset. Pass the toast's position in a list to enable stacking. */
|
|
10
|
+
index?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
message,
|
|
15
|
+
variant = 'info',
|
|
16
|
+
duration = 3000,
|
|
17
|
+
position = 'bottom-right',
|
|
18
|
+
dismissible = true,
|
|
19
|
+
onclose,
|
|
20
|
+
index = 0
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
|
|
23
|
+
let visible = $state(true);
|
|
24
|
+
let isExiting = $state(false);
|
|
25
|
+
|
|
26
|
+
const positionClasses = {
|
|
27
|
+
'top-left': 'top-4 left-4',
|
|
28
|
+
'top-center': 'top-4 left-1/2 -translate-x-1/2',
|
|
29
|
+
'top-right': 'top-4 right-4',
|
|
30
|
+
'bottom-left': 'bottom-4 left-4',
|
|
31
|
+
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
|
|
32
|
+
'bottom-right': 'bottom-4 right-4'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const variantBorderClasses = {
|
|
36
|
+
info: 'border-l-4 border-[var(--color-accent)]',
|
|
37
|
+
success: 'border-l-4 border-[var(--color-success)]',
|
|
38
|
+
warning: 'border-l-4 border-[var(--color-warning)]',
|
|
39
|
+
error: 'border-l-4 border-[var(--color-error)]'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const variantIconClasses = {
|
|
43
|
+
info: 'text-[var(--color-accent-soft)]',
|
|
44
|
+
success: 'text-[var(--color-success)]',
|
|
45
|
+
warning: 'text-[var(--color-warning)]',
|
|
46
|
+
error: 'text-[var(--color-error)]'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const variantIcons = {
|
|
50
|
+
info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
51
|
+
success: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
52
|
+
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',
|
|
53
|
+
error: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const roleMap = {
|
|
57
|
+
info: 'status',
|
|
58
|
+
success: 'status',
|
|
59
|
+
warning: 'alert',
|
|
60
|
+
error: 'alert'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const ariaLiveMap = {
|
|
64
|
+
info: 'polite',
|
|
65
|
+
success: 'polite',
|
|
66
|
+
warning: 'polite',
|
|
67
|
+
error: 'assertive'
|
|
68
|
+
} satisfies Record<'info' | 'success' | 'warning' | 'error', 'polite' | 'assertive'>;
|
|
69
|
+
|
|
70
|
+
const animationClasses = $derived(
|
|
71
|
+
position.startsWith('bottom')
|
|
72
|
+
? 'animate-[fade-up_0.3s_var(--ease-luxe)]'
|
|
73
|
+
: 'animate-[fade-in_0.3s_var(--ease-luxe)]'
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const stackOffset = $derived(
|
|
77
|
+
position.startsWith('bottom')
|
|
78
|
+
? `translateY(-${index * 5}rem)`
|
|
79
|
+
: `translateY(${index * 5}rem)`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
function handleClose() {
|
|
83
|
+
if (isExiting || !visible) return; // Prevent double-close
|
|
84
|
+
isExiting = true;
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
visible = false;
|
|
87
|
+
onclose?.();
|
|
88
|
+
}, 200); // Match fade-out animation duration
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$effect(() => {
|
|
92
|
+
// Only auto-dismiss if toast is visible and duration is set
|
|
93
|
+
if (visible && !isExiting && duration > 0) {
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
handleClose();
|
|
96
|
+
}, duration);
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
{#if visible}
|
|
106
|
+
<div
|
|
107
|
+
class="fixed z-[100] {positionClasses[position]} {isExiting ? 'animate-[fade-out_0.2s_var(--ease-sharp)]' : animationClasses}"
|
|
108
|
+
style="transform: {stackOffset}"
|
|
109
|
+
>
|
|
110
|
+
<div
|
|
111
|
+
class="glass-panel rounded-[var(--radius-lg)] p-4 min-w-[20rem] max-w-md flex gap-3 items-start {variantBorderClasses[variant]}"
|
|
112
|
+
role={roleMap[variant]}
|
|
113
|
+
aria-live={ariaLiveMap[variant]}
|
|
114
|
+
aria-atomic="true"
|
|
115
|
+
>
|
|
116
|
+
<div class="flex-shrink-0 {variantIconClasses[variant]}">
|
|
117
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
118
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={variantIcons[variant]} />
|
|
119
|
+
</svg>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="flex-1 text-sm text-[var(--color-text)]">
|
|
123
|
+
{message}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{#if dismissible}
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
onclick={handleClose}
|
|
130
|
+
class="flex-shrink-0 text-[var(--color-text-soft)] hover:text-[var(--color-text)] transition-colors"
|
|
131
|
+
aria-label="Dismiss notification"
|
|
132
|
+
>
|
|
133
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
134
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
135
|
+
</svg>
|
|
136
|
+
</button>
|
|
137
|
+
{/if}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
message: string;
|
|
3
|
+
variant?: 'info' | 'success' | 'warning' | 'error';
|
|
4
|
+
duration?: number;
|
|
5
|
+
position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
6
|
+
dismissible?: boolean;
|
|
7
|
+
onclose?: () => void;
|
|
8
|
+
/** Index in toast stack for vertical offset. Pass the toast's position in a list to enable stacking. */
|
|
9
|
+
index?: number;
|
|
10
|
+
}
|
|
11
|
+
declare const Toast: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type Toast = ReturnType<typeof Toast>;
|
|
13
|
+
export default Toast;
|