@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,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface MenuItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
onclick?: () => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
icon?: Snippet;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
items: MenuItem[];
|
|
11
|
+
open?: boolean;
|
|
12
|
+
x?: number;
|
|
13
|
+
y?: number;
|
|
14
|
+
trigger?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
declare const ContextMenu: import("svelte").Component<Props, {}, "open" | "x" | "y">;
|
|
17
|
+
type ContextMenu = ReturnType<typeof ContextMenu>;
|
|
18
|
+
export default ContextMenu;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">import { createId } from '../../lib/internal/id.js';
|
|
2
|
+
let { items, open = $bindable(false), placement = 'bottom-start', trigger } = $props();
|
|
3
|
+
let menuElement = $state();
|
|
4
|
+
let wrapperElement;
|
|
5
|
+
let selectedIndex = $state(0);
|
|
6
|
+
const menuId = createId('dropdown-menu');
|
|
7
|
+
const placementClasses = {
|
|
8
|
+
'bottom-start': 'top-full left-0 mt-2',
|
|
9
|
+
'bottom-end': 'top-full right-0 mt-2',
|
|
10
|
+
'top-start': 'bottom-full left-0 mb-2',
|
|
11
|
+
'top-end': 'bottom-full right-0 mb-2'
|
|
12
|
+
};
|
|
13
|
+
function toggleMenu() {
|
|
14
|
+
open = !open;
|
|
15
|
+
if (open) {
|
|
16
|
+
selectedIndex = 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function handleItemClick(item) {
|
|
20
|
+
if (item.disabled)
|
|
21
|
+
return;
|
|
22
|
+
item.onclick?.();
|
|
23
|
+
open = false;
|
|
24
|
+
}
|
|
25
|
+
function handleMenuKeyDown(event) {
|
|
26
|
+
// Derive current index from focused element to stay in sync
|
|
27
|
+
const focusedElement = document.activeElement;
|
|
28
|
+
const dataIndex = focusedElement?.getAttribute('data-index');
|
|
29
|
+
if (dataIndex !== null) {
|
|
30
|
+
const currentFocusedIndex = parseInt(dataIndex || '0', 10);
|
|
31
|
+
if (!isNaN(currentFocusedIndex) && currentFocusedIndex >= 0 && currentFocusedIndex < items.length) {
|
|
32
|
+
selectedIndex = currentFocusedIndex;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const enabledItems = items.filter(item => !item.disabled);
|
|
36
|
+
const currentEnabledIndex = enabledItems.findIndex(item => item.id === items[selectedIndex]?.id);
|
|
37
|
+
let nextIndex = currentEnabledIndex;
|
|
38
|
+
switch (event.key) {
|
|
39
|
+
case 'ArrowDown':
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
nextIndex = currentEnabledIndex < enabledItems.length - 1 ? currentEnabledIndex + 1 : 0;
|
|
42
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[nextIndex]?.id);
|
|
43
|
+
break;
|
|
44
|
+
case 'ArrowUp':
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
nextIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : enabledItems.length - 1;
|
|
47
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[nextIndex]?.id);
|
|
48
|
+
break;
|
|
49
|
+
case 'Home':
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[0]?.id);
|
|
52
|
+
break;
|
|
53
|
+
case 'End':
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[enabledItems.length - 1]?.id);
|
|
56
|
+
break;
|
|
57
|
+
case 'Enter':
|
|
58
|
+
case ' ':
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
if (items[selectedIndex] && !items[selectedIndex].disabled) {
|
|
61
|
+
handleItemClick(items[selectedIndex]);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case 'Escape':
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
open = false;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function handleClickOutside(event) {
|
|
71
|
+
if (menuElement && !wrapperElement.contains(event.target)) {
|
|
72
|
+
open = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
$effect(() => {
|
|
76
|
+
if (open) {
|
|
77
|
+
document.addEventListener('click', handleClickOutside);
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
const firstItem = menuElement?.querySelector('[role="menuitem"]');
|
|
80
|
+
if (firstItem) {
|
|
81
|
+
const dataIndex = firstItem.getAttribute('data-index');
|
|
82
|
+
if (dataIndex !== null) {
|
|
83
|
+
selectedIndex = parseInt(dataIndex, 10);
|
|
84
|
+
}
|
|
85
|
+
firstItem.focus();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return () => {
|
|
89
|
+
document.removeEventListener('click', handleClickOutside);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<div bind:this={wrapperElement} class="relative">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onclick={toggleMenu}
|
|
99
|
+
onkeydown={(e) => {
|
|
100
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
toggleMenu();
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
aria-haspopup="menu"
|
|
106
|
+
aria-expanded={open}
|
|
107
|
+
aria-controls={menuId}
|
|
108
|
+
class="bg-transparent border-none p-0 cursor-pointer"
|
|
109
|
+
>
|
|
110
|
+
{@render trigger?.()}
|
|
111
|
+
</button>
|
|
112
|
+
|
|
113
|
+
{#if open}
|
|
114
|
+
<div
|
|
115
|
+
bind:this={menuElement}
|
|
116
|
+
id={menuId}
|
|
117
|
+
role="menu"
|
|
118
|
+
tabindex="-1"
|
|
119
|
+
onkeydown={handleMenuKeyDown}
|
|
120
|
+
class="absolute {placementClasses[placement]} z-[var(--z-50)] glass-panel rounded-[var(--radius-lg)] shadow-[var(--shadow-deep)] min-w-[12rem] animate-[fade-up_0.2s_var(--ease-luxe)]"
|
|
121
|
+
>
|
|
122
|
+
{#each items as item, index}
|
|
123
|
+
<button
|
|
124
|
+
role="menuitem"
|
|
125
|
+
data-index={index}
|
|
126
|
+
tabindex={index === selectedIndex ? 0 : -1}
|
|
127
|
+
onclick={() => handleItemClick(item)}
|
|
128
|
+
disabled={item.disabled}
|
|
129
|
+
class="w-full text-left px-4 py-2 text-sm text-text hover:bg-base-3 transition-colors duration-200 flex items-center gap-2 {item.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
|
|
130
|
+
>
|
|
131
|
+
{#if item.icon}
|
|
132
|
+
{@render item.icon()}
|
|
133
|
+
{/if}
|
|
134
|
+
{item.label}
|
|
135
|
+
</button>
|
|
136
|
+
{/each}
|
|
137
|
+
</div>
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface MenuItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
onclick?: () => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
icon?: Snippet;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
items: MenuItem[];
|
|
11
|
+
open?: boolean;
|
|
12
|
+
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
13
|
+
trigger?: Snippet;
|
|
14
|
+
}
|
|
15
|
+
declare const DropdownMenu: import("svelte").Component<Props, {}, "open">;
|
|
16
|
+
type DropdownMenu = ReturnType<typeof DropdownMenu>;
|
|
17
|
+
export default DropdownMenu;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">let { items, children } = $props();
|
|
2
|
+
let selectedIndex = $state(0);
|
|
3
|
+
let menuContainer;
|
|
4
|
+
function handleItemClick(item) {
|
|
5
|
+
if (!item.disabled && item.onclick) {
|
|
6
|
+
item.onclick();
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function handleMenuKeyDown(event) {
|
|
10
|
+
if (!items)
|
|
11
|
+
return;
|
|
12
|
+
const enabledItems = items.filter(item => !item.disabled);
|
|
13
|
+
const currentEnabledIndex = enabledItems.findIndex(item => item.id === items[selectedIndex]?.id);
|
|
14
|
+
let nextIndex = currentEnabledIndex;
|
|
15
|
+
switch (event.key) {
|
|
16
|
+
case 'ArrowDown':
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
nextIndex = currentEnabledIndex < enabledItems.length - 1 ? currentEnabledIndex + 1 : 0;
|
|
19
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[nextIndex]?.id);
|
|
20
|
+
break;
|
|
21
|
+
case 'ArrowUp':
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
nextIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : enabledItems.length - 1;
|
|
24
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[nextIndex]?.id);
|
|
25
|
+
break;
|
|
26
|
+
case 'Home':
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[0]?.id);
|
|
29
|
+
break;
|
|
30
|
+
case 'End':
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
selectedIndex = items.findIndex(item => item.id === enabledItems[enabledItems.length - 1]?.id);
|
|
33
|
+
break;
|
|
34
|
+
case 'Enter':
|
|
35
|
+
case ' ':
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
if (items[selectedIndex] && !items[selectedIndex].disabled) {
|
|
38
|
+
handleItemClick(items[selectedIndex]);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
$effect(() => {
|
|
44
|
+
if (items && selectedIndex >= 0 && menuContainer) {
|
|
45
|
+
const selectedButton = menuContainer.querySelector(`[role="menuitem"][tabindex="0"]`);
|
|
46
|
+
selectedButton?.focus();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
export {};
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div bind:this={menuContainer} role="menu" tabindex="-1" onkeydown={handleMenuKeyDown} class="glass-panel rounded-[var(--radius-lg)] p-3 shadow-[var(--shadow-deep)]">
|
|
53
|
+
{#if children}
|
|
54
|
+
{@render children()}
|
|
55
|
+
{:else if items}
|
|
56
|
+
{#each items as item, index}
|
|
57
|
+
<button
|
|
58
|
+
role="menuitem"
|
|
59
|
+
aria-disabled={item.disabled}
|
|
60
|
+
tabindex={index === selectedIndex ? 0 : -1}
|
|
61
|
+
onclick={() => handleItemClick(item)}
|
|
62
|
+
disabled={item.disabled}
|
|
63
|
+
class="flex items-center gap-2 w-full px-4 py-3 rounded-[var(--radius-md)] text-[var(--color-text)] text-sm font-[var(--font-body)] hover:bg-[var(--color-base-3)] hover-lift transition-all duration-200 ease-[var(--ease-luxe)] {item.disabled ? 'opacity-50 pointer-events-none' : 'cursor-pointer'}"
|
|
64
|
+
>
|
|
65
|
+
{#if item.icon}
|
|
66
|
+
{@render item.icon()}
|
|
67
|
+
{/if}
|
|
68
|
+
<span>{item.label}</span>
|
|
69
|
+
</button>
|
|
70
|
+
{/each}
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface MenuItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: Snippet;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onclick?: () => void;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
items?: MenuItem[];
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
}
|
|
13
|
+
declare const Menu: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type Menu = ReturnType<typeof Menu>;
|
|
15
|
+
export default Menu;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">let { position = 'sticky', height = 'md', navigationAlign = 'center', toggleButton, logo, navigation, actions, children, currentPath } = $props();
|
|
2
|
+
const positionClasses = {
|
|
3
|
+
sticky: 'sticky top-0',
|
|
4
|
+
fixed: 'fixed top-0 left-0 right-0'
|
|
5
|
+
};
|
|
6
|
+
const heightClasses = {
|
|
7
|
+
sm: 'h-16',
|
|
8
|
+
md: 'h-18',
|
|
9
|
+
lg: 'h-24'
|
|
10
|
+
};
|
|
11
|
+
const baseClasses = 'z-[var(--z-50)] glass-panel flex items-center justify-between px-4 md:px-6 lg:px-8 transition-all duration-300 ease-[var(--ease-luxe)]';
|
|
12
|
+
// Track elements modified by the effect to preserve manual .active classes
|
|
13
|
+
let autoManagedElements = $state(new Set());
|
|
14
|
+
let rootElement = $state(null);
|
|
15
|
+
$effect(() => {
|
|
16
|
+
if (typeof document === 'undefined')
|
|
17
|
+
return;
|
|
18
|
+
if (!rootElement)
|
|
19
|
+
return;
|
|
20
|
+
// Clear auto-managed active classes when currentPath becomes falsy
|
|
21
|
+
if (!currentPath) {
|
|
22
|
+
autoManagedElements.forEach((element) => {
|
|
23
|
+
element.classList.remove('active');
|
|
24
|
+
});
|
|
25
|
+
autoManagedElements.clear();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const elements = rootElement.querySelectorAll('a, button');
|
|
29
|
+
elements.forEach((element) => {
|
|
30
|
+
const href = element.getAttribute('href');
|
|
31
|
+
const dataHref = element.getAttribute('data-href');
|
|
32
|
+
const targetPath = href || dataHref;
|
|
33
|
+
// Match exact path or nested routes (e.g., /buttons matches /buttons/icon-button)
|
|
34
|
+
const isActive = targetPath === currentPath ||
|
|
35
|
+
(targetPath && currentPath.startsWith(targetPath + '/'));
|
|
36
|
+
if (isActive) {
|
|
37
|
+
// Only add to autoManagedElements if we're adding the class ourselves
|
|
38
|
+
if (!element.classList.contains('active')) {
|
|
39
|
+
element.classList.add('active');
|
|
40
|
+
autoManagedElements.add(element);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (autoManagedElements.has(element)) {
|
|
44
|
+
element.classList.remove('active');
|
|
45
|
+
autoManagedElements.delete(element);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
export {};
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<nav bind:this={rootElement} data-navbar class="{baseClasses} {positionClasses[position]} {heightClasses[height]}">
|
|
53
|
+
<!-- Left Section: Toggle Button and Logo -->
|
|
54
|
+
<div class="flex items-center gap-3">
|
|
55
|
+
{#if toggleButton}
|
|
56
|
+
{@render toggleButton()}
|
|
57
|
+
{/if}
|
|
58
|
+
{#if logo}
|
|
59
|
+
{@render logo()}
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Center Section: Navigation Links -->
|
|
64
|
+
{#if navigation}
|
|
65
|
+
<div class="flex-1 flex items-center {navigationAlign === 'center' ? 'justify-center' : ''} gap-6 {logo ? 'ml-4' : ''}">
|
|
66
|
+
{@render navigation()}
|
|
67
|
+
</div>
|
|
68
|
+
{:else if children}
|
|
69
|
+
<div class="flex-1 flex items-center {navigationAlign === 'center' ? 'justify-center' : ''} gap-4 {logo ? 'ml-4' : ''}">
|
|
70
|
+
{@render children()}
|
|
71
|
+
</div>
|
|
72
|
+
{/if}
|
|
73
|
+
|
|
74
|
+
<!-- Right Section: Actions -->
|
|
75
|
+
{#if actions}
|
|
76
|
+
<div class="flex items-center gap-3">
|
|
77
|
+
{@render actions()}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
</nav>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
nav :global(.navbar-logo) {
|
|
84
|
+
color: var(--color-accent);
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
font-size: var(--text-lg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
nav :global(a.active),
|
|
90
|
+
nav :global(button.active) {
|
|
91
|
+
color: var(--color-accent);
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
position: relative;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
nav :global(a.active::after),
|
|
97
|
+
nav :global(button.active::after) {
|
|
98
|
+
content: '';
|
|
99
|
+
position: absolute;
|
|
100
|
+
bottom: -0.5rem;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
height: 2px;
|
|
104
|
+
background: var(--color-accent);
|
|
105
|
+
box-shadow: var(--shadow-accent-glow);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
nav :global(a:not(.active):hover),
|
|
109
|
+
nav :global(button:not(.active):hover) {
|
|
110
|
+
color: var(--color-accent-soft);
|
|
111
|
+
}</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
position?: 'sticky' | 'fixed';
|
|
4
|
+
height?: 'sm' | 'md' | 'lg';
|
|
5
|
+
navigationAlign?: 'left' | 'center';
|
|
6
|
+
toggleButton?: Snippet;
|
|
7
|
+
logo?: Snippet;
|
|
8
|
+
navigation?: Snippet;
|
|
9
|
+
actions?: Snippet;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
currentPath?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const Navbar: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type Navbar = ReturnType<typeof Navbar>;
|
|
15
|
+
export default Navbar;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<script lang="ts">let { open = $bindable(true), side = 'left', width = 'md', header, children, currentPath } = $props();
|
|
2
|
+
let navbarHeight = $state(0);
|
|
3
|
+
$effect(() => {
|
|
4
|
+
if (typeof document === 'undefined')
|
|
5
|
+
return;
|
|
6
|
+
const navbarElement = document.querySelector('[data-navbar]');
|
|
7
|
+
if (navbarElement) {
|
|
8
|
+
// Initial height
|
|
9
|
+
navbarHeight = navbarElement.offsetHeight;
|
|
10
|
+
// Watch for height changes (responsive behavior, window resize, etc.)
|
|
11
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
12
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
13
|
+
navbarHeight = navbarElement.offsetHeight;
|
|
14
|
+
});
|
|
15
|
+
resizeObserver.observe(navbarElement);
|
|
16
|
+
return () => {
|
|
17
|
+
resizeObserver.disconnect();
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
navbarHeight = 0;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
// Track elements modified by the effect to preserve manual .active classes
|
|
26
|
+
let autoManagedElements = $state(new Set());
|
|
27
|
+
let rootElement = $state(null);
|
|
28
|
+
$effect(() => {
|
|
29
|
+
if (typeof document === 'undefined')
|
|
30
|
+
return;
|
|
31
|
+
if (!rootElement)
|
|
32
|
+
return;
|
|
33
|
+
// Clear auto-managed active classes when currentPath becomes falsy
|
|
34
|
+
if (!currentPath) {
|
|
35
|
+
autoManagedElements.forEach((element) => {
|
|
36
|
+
element.classList.remove('active');
|
|
37
|
+
});
|
|
38
|
+
autoManagedElements.clear();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const elements = rootElement.querySelectorAll('a, button');
|
|
42
|
+
elements.forEach((element) => {
|
|
43
|
+
const href = element.getAttribute('href');
|
|
44
|
+
const dataHref = element.getAttribute('data-href');
|
|
45
|
+
const targetPath = href || dataHref;
|
|
46
|
+
// Match exact path or nested routes (e.g., /buttons matches /buttons/icon-button)
|
|
47
|
+
const isActive = targetPath === currentPath ||
|
|
48
|
+
(targetPath && currentPath.startsWith(targetPath + '/'));
|
|
49
|
+
if (isActive) {
|
|
50
|
+
// Only add to autoManagedElements if we're adding the class ourselves
|
|
51
|
+
if (!element.classList.contains('active')) {
|
|
52
|
+
element.classList.add('active');
|
|
53
|
+
autoManagedElements.add(element);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (autoManagedElements.has(element)) {
|
|
57
|
+
element.classList.remove('active');
|
|
58
|
+
autoManagedElements.delete(element);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// Collapsed state icon/text detection
|
|
63
|
+
$effect(() => {
|
|
64
|
+
if (typeof document === 'undefined')
|
|
65
|
+
return;
|
|
66
|
+
if (!rootElement)
|
|
67
|
+
return;
|
|
68
|
+
// Reference open to make effect reactive to collapse/expand state changes
|
|
69
|
+
// This ensures detection runs when sidebar toggles
|
|
70
|
+
if (open === undefined)
|
|
71
|
+
return;
|
|
72
|
+
const elements = rootElement.querySelectorAll('a, button');
|
|
73
|
+
elements.forEach((element) => {
|
|
74
|
+
// Check for icon presence using documented .sidebar-item-icon class
|
|
75
|
+
const hasIcon = element.querySelector('.sidebar-item-icon');
|
|
76
|
+
if (hasIcon) {
|
|
77
|
+
element.setAttribute('data-sidebar-has-icon', 'true');
|
|
78
|
+
element.removeAttribute('data-sidebar-first-letter');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Extract first letter from .sidebar-item-label or fallback to text content
|
|
82
|
+
const label = element.querySelector('.sidebar-item-label');
|
|
83
|
+
const textContent = (label?.textContent || element.textContent)?.trim();
|
|
84
|
+
const firstChar = textContent?.[0];
|
|
85
|
+
// Only set first-letter if it's alphanumeric
|
|
86
|
+
if (firstChar && /[a-zA-Z0-9]/.test(firstChar)) {
|
|
87
|
+
element.setAttribute('data-sidebar-first-letter', firstChar.toUpperCase());
|
|
88
|
+
element.removeAttribute('data-sidebar-has-icon');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// No valid first letter - don't show anything in collapsed mode
|
|
92
|
+
element.removeAttribute('data-sidebar-first-letter');
|
|
93
|
+
element.removeAttribute('data-sidebar-has-icon');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const widthClasses = {
|
|
99
|
+
sm: { open: 'w-56', collapsed: 'w-14' },
|
|
100
|
+
md: { open: 'w-64', collapsed: 'w-16' },
|
|
101
|
+
lg: { open: 'w-80', collapsed: 'w-20' }
|
|
102
|
+
};
|
|
103
|
+
const sideClasses = {
|
|
104
|
+
left: 'left-0',
|
|
105
|
+
right: 'right-0'
|
|
106
|
+
};
|
|
107
|
+
const currentWidth = $derived(open ? widthClasses[width].open : widthClasses[width].collapsed);
|
|
108
|
+
const baseClasses = 'fixed bottom-0 z-[var(--z-40)] glass-panel overflow-y-auto overflow-x-hidden transition-all duration-300 ease-[var(--ease-luxe)]';
|
|
109
|
+
export {};
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<aside bind:this={rootElement} data-sidebar class="{baseClasses} {sideClasses[side]} {currentWidth}" style="top: {navbarHeight}px">
|
|
113
|
+
{#if header && open}
|
|
114
|
+
{@render header()}
|
|
115
|
+
{/if}
|
|
116
|
+
{@render children?.()}
|
|
117
|
+
</aside>
|
|
118
|
+
|
|
119
|
+
<style>
|
|
120
|
+
/* Header styling */
|
|
121
|
+
aside :global(.sidebar-header) {
|
|
122
|
+
padding: 1rem;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
color: var(--color-accent);
|
|
125
|
+
font-size: var(--text-sm);
|
|
126
|
+
text-transform: uppercase;
|
|
127
|
+
letter-spacing: 0.05em;
|
|
128
|
+
transition: all 0.2s var(--ease-luxe);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Collapsed state - center icons and hide text */
|
|
132
|
+
aside[class*="w-14"] :global(a[data-sidebar-has-icon]),
|
|
133
|
+
aside[class*="w-16"] :global(a[data-sidebar-has-icon]),
|
|
134
|
+
aside[class*="w-20"] :global(a[data-sidebar-has-icon]),
|
|
135
|
+
aside[class*="w-14"] :global(button[data-sidebar-has-icon]),
|
|
136
|
+
aside[class*="w-16"] :global(button[data-sidebar-has-icon]),
|
|
137
|
+
aside[class*="w-20"] :global(button[data-sidebar-has-icon]) {
|
|
138
|
+
justify-content: center;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
aside[class*="w-14"] :global(a[data-sidebar-has-icon] .sidebar-item-label),
|
|
142
|
+
aside[class*="w-16"] :global(a[data-sidebar-has-icon] .sidebar-item-label),
|
|
143
|
+
aside[class*="w-20"] :global(a[data-sidebar-has-icon] .sidebar-item-label),
|
|
144
|
+
aside[class*="w-14"] :global(button[data-sidebar-has-icon] .sidebar-item-label),
|
|
145
|
+
aside[class*="w-16"] :global(button[data-sidebar-has-icon] .sidebar-item-label),
|
|
146
|
+
aside[class*="w-20"] :global(button[data-sidebar-has-icon] .sidebar-item-label) {
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
aside[class*="w-14"] :global(a[data-sidebar-has-icon] .sidebar-item-icon),
|
|
151
|
+
aside[class*="w-16"] :global(a[data-sidebar-has-icon] .sidebar-item-icon),
|
|
152
|
+
aside[class*="w-20"] :global(a[data-sidebar-has-icon] .sidebar-item-icon),
|
|
153
|
+
aside[class*="w-14"] :global(button[data-sidebar-has-icon] .sidebar-item-icon),
|
|
154
|
+
aside[class*="w-16"] :global(button[data-sidebar-has-icon] .sidebar-item-icon),
|
|
155
|
+
aside[class*="w-20"] :global(button[data-sidebar-has-icon] .sidebar-item-icon) {
|
|
156
|
+
font-size: var(--text-2xl);
|
|
157
|
+
width: 1.5rem;
|
|
158
|
+
height: 1.5rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Collapsed state - first letter circle for items without icons */
|
|
162
|
+
aside[class*="w-14"] :global(a:not([data-sidebar-has-icon]))::before,
|
|
163
|
+
aside[class*="w-16"] :global(a:not([data-sidebar-has-icon]))::before,
|
|
164
|
+
aside[class*="w-20"] :global(a:not([data-sidebar-has-icon]))::before,
|
|
165
|
+
aside[class*="w-14"] :global(button:not([data-sidebar-has-icon]))::before,
|
|
166
|
+
aside[class*="w-16"] :global(button:not([data-sidebar-has-icon]))::before,
|
|
167
|
+
aside[class*="w-20"] :global(button:not([data-sidebar-has-icon]))::before {
|
|
168
|
+
content: attr(data-sidebar-first-letter);
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
width: 2.5rem;
|
|
173
|
+
height: 2.5rem;
|
|
174
|
+
border-radius: var(--radius-pill);
|
|
175
|
+
background: var(--color-accent-overlay-20);
|
|
176
|
+
color: var(--color-accent);
|
|
177
|
+
font-weight: 600;
|
|
178
|
+
font-size: var(--text-lg);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
aside[class*="w-14"] :global(a:not([data-sidebar-has-icon])),
|
|
182
|
+
aside[class*="w-16"] :global(a:not([data-sidebar-has-icon])),
|
|
183
|
+
aside[class*="w-20"] :global(a:not([data-sidebar-has-icon])),
|
|
184
|
+
aside[class*="w-14"] :global(button:not([data-sidebar-has-icon])),
|
|
185
|
+
aside[class*="w-16"] :global(button:not([data-sidebar-has-icon])),
|
|
186
|
+
aside[class*="w-20"] :global(button:not([data-sidebar-has-icon])) {
|
|
187
|
+
justify-content: center;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
aside[class*="w-14"] :global(a:not([data-sidebar-has-icon]) .sidebar-item-label),
|
|
191
|
+
aside[class*="w-16"] :global(a:not([data-sidebar-has-icon]) .sidebar-item-label),
|
|
192
|
+
aside[class*="w-20"] :global(a:not([data-sidebar-has-icon]) .sidebar-item-label),
|
|
193
|
+
aside[class*="w-14"] :global(button:not([data-sidebar-has-icon]) .sidebar-item-label),
|
|
194
|
+
aside[class*="w-16"] :global(button:not([data-sidebar-has-icon]) .sidebar-item-label),
|
|
195
|
+
aside[class*="w-20"] :global(button:not([data-sidebar-has-icon]) .sidebar-item-label) {
|
|
196
|
+
display: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
aside :global(a.active),
|
|
200
|
+
aside :global(button.active) {
|
|
201
|
+
background: var(--color-accent-overlay-15);
|
|
202
|
+
color: var(--color-accent);
|
|
203
|
+
border-left: 3px solid var(--color-accent);
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
aside :global(a.active),
|
|
208
|
+
aside :global(button.active) {
|
|
209
|
+
box-shadow: var(--shadow-accent-glow);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
aside :global(a:not(.active):hover),
|
|
213
|
+
aside :global(button:not(.active):hover) {
|
|
214
|
+
background: var(--color-base-3);
|
|
215
|
+
color: var(--color-accent-soft);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
aside :global(a),
|
|
219
|
+
aside :global(button) {
|
|
220
|
+
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
gap: 0.75rem;
|
|
223
|
+
padding: 0.75rem 1rem;
|
|
224
|
+
margin: 0.25rem 0.5rem;
|
|
225
|
+
border-radius: var(--radius-md);
|
|
226
|
+
color: var(--color-text-soft);
|
|
227
|
+
text-decoration: none;
|
|
228
|
+
transition: all 0.2s var(--ease-luxe);
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
border: none;
|
|
231
|
+
background: transparent;
|
|
232
|
+
width: calc(100% - 1rem);
|
|
233
|
+
text-align: left;
|
|
234
|
+
font-family: var(--font-body);
|
|
235
|
+
font-size: 0.875rem;
|
|
236
|
+
}</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
side?: 'left' | 'right';
|
|
5
|
+
width?: 'sm' | 'md' | 'lg';
|
|
6
|
+
header?: Snippet;
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
currentPath?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const Sidebar: import("svelte").Component<Props, {}, "open">;
|
|
11
|
+
type Sidebar = ReturnType<typeof Sidebar>;
|
|
12
|
+
export default Sidebar;
|