@mrintel/villain-ui 0.3.0 → 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.
Files changed (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3490 -1296
  3. package/dist/components/buttons/Button.svelte +27 -33
  4. package/dist/components/buttons/Button.svelte.d.ts +4 -1
  5. package/dist/components/buttons/ButtonGroup.svelte +17 -30
  6. package/dist/components/buttons/FloatingActionButton.svelte +20 -44
  7. package/dist/components/buttons/FloatingActionButton.svelte.d.ts +2 -1
  8. package/dist/components/buttons/IconButton.svelte +23 -53
  9. package/dist/components/buttons/IconButton.svelte.d.ts +2 -1
  10. package/dist/components/buttons/LinkButton.svelte +24 -37
  11. package/dist/components/buttons/LinkButton.svelte.d.ts +4 -1
  12. package/dist/components/buttons/buttonClasses.d.ts +5 -0
  13. package/dist/components/buttons/buttonClasses.js +8 -3
  14. package/dist/components/cards/Card.svelte +60 -46
  15. package/dist/components/cards/Card.svelte.d.ts +6 -2
  16. package/dist/components/cards/Container.svelte +17 -33
  17. package/dist/components/cards/Divider.svelte +36 -52
  18. package/dist/components/cards/Divider.svelte.d.ts +2 -0
  19. package/dist/components/cards/Grid.svelte +55 -44
  20. package/dist/components/cards/Panel.svelte +18 -32
  21. package/dist/components/cards/Panel.svelte.d.ts +2 -1
  22. package/dist/components/cards/SectionHeader.svelte +24 -38
  23. package/dist/components/cards/SectionHeader.svelte.d.ts +1 -0
  24. package/dist/components/data/Avatar.svelte +48 -67
  25. package/dist/components/data/Badge.svelte +45 -32
  26. package/dist/components/data/Badge.svelte.d.ts +7 -1
  27. package/dist/components/data/CalendarGrid.svelte +433 -0
  28. package/dist/components/data/CalendarGrid.svelte.d.ts +25 -0
  29. package/dist/components/data/CalendarGrid.types.d.ts +7 -0
  30. package/dist/components/data/CalendarGrid.types.js +1 -0
  31. package/dist/components/data/CodeBlock.svelte +119 -121
  32. package/dist/components/data/CodeBlock.svelte.d.ts +8 -0
  33. package/dist/components/data/List.svelte +87 -64
  34. package/dist/components/data/List.svelte.d.ts +7 -0
  35. package/dist/components/data/Pagination.svelte +121 -123
  36. package/dist/components/data/Pagination.svelte.d.ts +5 -0
  37. package/dist/components/data/Sparkline.svelte +117 -0
  38. package/dist/components/data/Sparkline.svelte.d.ts +43 -0
  39. package/dist/components/data/Stat.svelte +92 -103
  40. package/dist/components/data/Table.svelte +443 -76
  41. package/dist/components/data/Table.svelte.d.ts +23 -2
  42. package/dist/components/data/Table.types.d.ts +14 -0
  43. package/dist/components/data/Table.types.js +1 -0
  44. package/dist/components/data/Tag.svelte +51 -53
  45. package/dist/components/data/Tag.svelte.d.ts +5 -1
  46. package/dist/components/data/index.d.ts +4 -0
  47. package/dist/components/data/index.js +2 -0
  48. package/dist/components/forms/Checkbox.svelte +39 -51
  49. package/dist/components/forms/Checkbox.svelte.d.ts +3 -1
  50. package/dist/components/forms/DatePicker.svelte +61 -0
  51. package/dist/components/forms/DatePicker.svelte.d.ts +15 -0
  52. package/dist/components/forms/DateTimePicker.svelte +63 -0
  53. package/dist/components/forms/DateTimePicker.svelte.d.ts +16 -0
  54. package/dist/components/forms/FileUpload.svelte +136 -164
  55. package/dist/components/forms/FileUpload.svelte.d.ts +1 -0
  56. package/dist/components/forms/Input.svelte +282 -57
  57. package/dist/components/forms/Input.svelte.d.ts +9 -3
  58. package/dist/components/forms/InputGroup.svelte +7 -7
  59. package/dist/components/forms/RadioGroup.svelte +77 -87
  60. package/dist/components/forms/RadioGroup.svelte.d.ts +3 -1
  61. package/dist/components/forms/RangeSlider.svelte +90 -116
  62. package/dist/components/forms/Select.svelte +106 -71
  63. package/dist/components/forms/Select.svelte.d.ts +3 -1
  64. package/dist/components/forms/Switch.svelte +44 -56
  65. package/dist/components/forms/Switch.svelte.d.ts +3 -1
  66. package/dist/components/forms/Textarea.svelte +52 -57
  67. package/dist/components/forms/Textarea.svelte.d.ts +3 -1
  68. package/dist/components/forms/TimePicker.svelte +63 -0
  69. package/dist/components/forms/TimePicker.svelte.d.ts +16 -0
  70. package/dist/components/forms/formClasses.d.ts +3 -0
  71. package/dist/components/forms/formClasses.js +3 -0
  72. package/dist/components/forms/index.d.ts +3 -0
  73. package/dist/components/forms/index.js +3 -0
  74. package/dist/components/navigation/Breadcrumbs.svelte +56 -59
  75. package/dist/components/navigation/Breadcrumbs.svelte.d.ts +1 -0
  76. package/dist/components/navigation/ContextMenu.svelte +133 -83
  77. package/dist/components/navigation/ContextMenu.svelte.d.ts +8 -1
  78. package/dist/components/navigation/DropdownMenu.svelte +139 -80
  79. package/dist/components/navigation/DropdownMenu.svelte.d.ts +8 -1
  80. package/dist/components/navigation/Menu.svelte +72 -48
  81. package/dist/components/navigation/Navbar.svelte +111 -32
  82. package/dist/components/navigation/Navbar.svelte.d.ts +6 -0
  83. package/dist/components/navigation/Sidebar.svelte +236 -35
  84. package/dist/components/navigation/Sidebar.svelte.d.ts +2 -0
  85. package/dist/components/navigation/Tabs.svelte +86 -54
  86. package/dist/components/navigation/Tabs.svelte.d.ts +5 -1
  87. package/dist/components/overlays/Alert.svelte +81 -99
  88. package/dist/components/overlays/Alert.svelte.d.ts +5 -1
  89. package/dist/components/overlays/CommandPalette.svelte +182 -217
  90. package/dist/components/overlays/Drawer.svelte +158 -167
  91. package/dist/components/overlays/Drawer.svelte.d.ts +3 -1
  92. package/dist/components/overlays/Dropdown.svelte +62 -30
  93. package/dist/components/overlays/Dropdown.svelte.d.ts +2 -0
  94. package/dist/components/overlays/Modal.svelte +125 -130
  95. package/dist/components/overlays/Modal.svelte.d.ts +3 -1
  96. package/dist/components/overlays/Popover.svelte +106 -131
  97. package/dist/components/overlays/ProgressBar.svelte +29 -45
  98. package/dist/components/overlays/SkeletonLoader.svelte +66 -82
  99. package/dist/components/overlays/Spinner.svelte +33 -43
  100. package/dist/components/overlays/Toast.svelte +111 -140
  101. package/dist/components/overlays/Toast.svelte.d.ts +3 -0
  102. package/dist/components/overlays/Tooltip.svelte +94 -115
  103. package/dist/components/overlays/Tooltip.svelte.d.ts +3 -1
  104. package/dist/components/typography/Code.svelte +10 -14
  105. package/dist/components/typography/Heading.svelte +15 -22
  106. package/dist/components/typography/Heading.svelte.d.ts +1 -0
  107. package/dist/components/typography/Text.svelte +21 -24
  108. package/dist/components/typography/Text.svelte.d.ts +2 -1
  109. package/dist/components/utilities/Accordion.svelte +54 -67
  110. package/dist/components/utilities/Accordion.svelte.d.ts +4 -1
  111. package/dist/components/utilities/Carousel.svelte +124 -152
  112. package/dist/components/utilities/Collapse.svelte +46 -60
  113. package/dist/components/utilities/Hero.svelte +42 -0
  114. package/dist/components/utilities/Hero.svelte.d.ts +10 -0
  115. package/dist/components/utilities/Portal.svelte +47 -72
  116. package/dist/components/utilities/ScrollArea.svelte +33 -41
  117. package/dist/components/utilities/SystemConsole.svelte +310 -0
  118. package/dist/components/utilities/SystemConsole.svelte.d.ts +20 -0
  119. package/dist/components/utilities/SystemInterface.svelte +726 -0
  120. package/dist/components/utilities/SystemInterface.svelte.d.ts +19 -0
  121. package/dist/components/utilities/index.d.ts +4 -0
  122. package/dist/components/utilities/index.js +3 -0
  123. package/dist/components/utilities/utilities.types.d.ts +46 -0
  124. package/dist/components/utilities/utilities.types.js +4 -0
  125. package/dist/index.d.ts +49 -4
  126. package/dist/index.js +4 -4
  127. package/dist/theme.css +2821 -218
  128. package/package.json +83 -76
@@ -1,80 +1,139 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- interface Props {
5
- open?: boolean;
6
- placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
7
- trigger?: Snippet;
8
- children?: Snippet;
9
- }
10
-
11
- let {
12
- open = $bindable(false),
13
- placement = 'bottom-start',
14
- trigger,
15
- children
16
- }: Props = $props();
17
-
18
- let menuElement: HTMLDivElement;
19
- let wrapperElement: HTMLDivElement;
20
-
21
- const placementClasses = {
22
- 'bottom-start': 'top-full left-0 mt-2',
23
- 'bottom-end': 'top-full right-0 mt-2',
24
- 'top-start': 'bottom-full left-0 mb-2',
25
- 'top-end': 'bottom-full right-0 mb-2'
26
- };
27
-
28
- function toggleMenu() {
29
- open = !open;
30
- }
31
-
32
- function handleClickOutside(event: MouseEvent) {
33
- if (menuElement && !wrapperElement.contains(event.target as Node)) {
34
- open = false;
35
- }
36
- }
37
-
38
- function handleEscape(event: KeyboardEvent) {
39
- if (event.key === 'Escape') {
40
- open = false;
41
- }
42
- }
43
-
44
- $effect(() => {
45
- if (open) {
46
- document.addEventListener('click', handleClickOutside);
47
- document.addEventListener('keydown', handleEscape);
48
-
49
- return () => {
50
- document.removeEventListener('click', handleClickOutside);
51
- document.removeEventListener('keydown', handleEscape);
52
- };
53
- }
54
- });
55
- </script>
56
-
57
- <div bind:this={wrapperElement} class="relative">
58
- <button
59
- type="button"
60
- onclick={toggleMenu}
61
- onkeydown={(e) => {
62
- if (e.key === 'Enter' || e.key === ' ') {
63
- e.preventDefault();
64
- toggleMenu();
65
- }
66
- }}
67
- class="bg-transparent border-none p-0 cursor-pointer"
68
- >
69
- {@render trigger?.()}
70
- </button>
71
-
72
- {#if open}
73
- <div
74
- bind:this={menuElement}
75
- class="absolute {placementClasses[placement]} z-50 glass-panel rounded-[var(--radius-lg)] shadow-[var(--shadow-deep)] min-w-[12rem] animate-[fade-up_0.2s_var(--ease-luxe)]"
76
- >
77
- {@render children?.()}
78
- </div>
79
- {/if}
80
- </div>
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>
@@ -1,9 +1,16 @@
1
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
+ }
2
9
  interface Props {
10
+ items: MenuItem[];
3
11
  open?: boolean;
4
12
  placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
5
13
  trigger?: Snippet;
6
- children?: Snippet;
7
14
  }
8
15
  declare const DropdownMenu: import("svelte").Component<Props, {}, "open">;
9
16
  type DropdownMenu = ReturnType<typeof DropdownMenu>;
@@ -1,48 +1,72 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- interface MenuItem {
5
- id: string;
6
- label: string;
7
- icon?: Snippet;
8
- disabled?: boolean;
9
- onclick?: () => void;
10
- }
11
-
12
- interface Props {
13
- items?: MenuItem[];
14
- children?: Snippet;
15
- }
16
-
17
- let {
18
- items,
19
- children
20
- }: Props = $props();
21
-
22
- function handleItemClick(item: MenuItem) {
23
- if (!item.disabled && item.onclick) {
24
- item.onclick();
25
- }
26
- }
27
- </script>
28
-
29
- <div role="menu" class="glass-panel rounded-[var(--radius-lg)] p-2 shadow-[var(--shadow-deep)]">
30
- {#if children}
31
- {@render children()}
32
- {:else if items}
33
- {#each items as item}
34
- <button
35
- role="menuitem"
36
- aria-disabled={item.disabled}
37
- onclick={() => handleItemClick(item)}
38
- disabled={item.disabled}
39
- class="flex items-center gap-2 w-full px-3 py-2 rounded-[var(--radius-md)] text-[var(--color-text)] text-sm font-[var(--font-body)] hover:bg-[var(--color-base-3)] transition-all duration-200 ease-[var(--ease-luxe)] {item.disabled ? 'opacity-50 pointer-events-none' : 'cursor-pointer'}"
40
- >
41
- {#if item.icon}
42
- {@render item.icon()}
43
- {/if}
44
- <span>{item.label}</span>
45
- </button>
46
- {/each}
47
- {/if}
48
- </div>
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>
@@ -1,32 +1,111 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- interface Props {
5
- position?: 'sticky' | 'fixed';
6
- height?: 'sm' | 'md' | 'lg';
7
- children?: Snippet;
8
- }
9
-
10
- let {
11
- position = 'sticky',
12
- height = 'md',
13
- children
14
- }: Props = $props();
15
-
16
- const positionClasses = {
17
- sticky: 'sticky top-0',
18
- fixed: 'fixed top-0 left-0 right-0'
19
- };
20
-
21
- const heightClasses = {
22
- sm: 'h-14',
23
- md: 'h-16',
24
- lg: 'h-20'
25
- };
26
-
27
- const baseClasses = 'z-40 glass-panel flex items-center justify-between px-4 md:px-6 lg:px-8 transition-all duration-300 ease-[var(--ease-luxe)]';
28
- </script>
29
-
30
- <nav class="{baseClasses} {positionClasses[position]} {heightClasses[height]}">
31
- {@render children?.()}
32
- </nav>
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>
@@ -2,7 +2,13 @@ import type { Snippet } from 'svelte';
2
2
  interface Props {
3
3
  position?: 'sticky' | 'fixed';
4
4
  height?: 'sm' | 'md' | 'lg';
5
+ navigationAlign?: 'left' | 'center';
6
+ toggleButton?: Snippet;
7
+ logo?: Snippet;
8
+ navigation?: Snippet;
9
+ actions?: Snippet;
5
10
  children?: Snippet;
11
+ currentPath?: string;
6
12
  }
7
13
  declare const Navbar: import("svelte").Component<Props, {}, "">;
8
14
  type Navbar = ReturnType<typeof Navbar>;