@classic-homes/theme-svelte 0.1.3 → 0.1.5
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/lib/components/Combobox.svelte +187 -0
- package/dist/lib/components/Combobox.svelte.d.ts +38 -0
- package/dist/lib/components/DateTimePicker.svelte +415 -0
- package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
- package/dist/lib/components/MultiSelect.svelte +244 -0
- package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
- package/dist/lib/components/NumberInput.svelte +205 -0
- package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
- package/dist/lib/components/OTPInput.svelte +213 -0
- package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
- package/dist/lib/components/RadioGroup.svelte +124 -0
- package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
- package/dist/lib/components/Signature.svelte +1070 -0
- package/dist/lib/components/Signature.svelte.d.ts +74 -0
- package/dist/lib/components/Slider.svelte +136 -0
- package/dist/lib/components/Slider.svelte.d.ts +30 -0
- package/dist/lib/components/layout/AppShell.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
- package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
- package/dist/lib/components/layout/QuickLinks.svelte +49 -29
- package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
- package/dist/lib/components/layout/Sidebar.svelte +345 -86
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
- package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
- package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
- package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
- package/dist/lib/components/layout/sidebar/index.js +10 -0
- package/dist/lib/index.d.ts +9 -1
- package/dist/lib/index.js +8 -0
- package/dist/lib/schemas/auth.d.ts +6 -6
- package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
- package/dist/lib/stores/sidebar.svelte.js +171 -1
- package/dist/lib/types/components.d.ts +105 -0
- package/dist/lib/types/layout.d.ts +32 -2
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SidebarFlyout - Flyout menu for collapsed sidebar items
|
|
4
|
+
*
|
|
5
|
+
* Features entrance animation and keyboard navigation
|
|
6
|
+
*/
|
|
7
|
+
import { fly } from 'svelte/transition';
|
|
8
|
+
import type { NavItem } from '../../../types/layout.js';
|
|
9
|
+
import { cn } from '../../../utils.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** The item to show in the flyout */
|
|
13
|
+
item: NavItem;
|
|
14
|
+
/** Top position in pixels */
|
|
15
|
+
top: number;
|
|
16
|
+
/** Whether using light variant */
|
|
17
|
+
isLight?: boolean;
|
|
18
|
+
/** Callback when a navigation item is clicked */
|
|
19
|
+
onNavigate?: (item: NavItem) => void;
|
|
20
|
+
/** Callback when mouse enters flyout */
|
|
21
|
+
onMouseEnter?: () => void;
|
|
22
|
+
/** Callback when mouse leaves flyout */
|
|
23
|
+
onMouseLeave?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { item, top, isLight = true, onNavigate, onMouseEnter, onMouseLeave }: Props = $props();
|
|
27
|
+
|
|
28
|
+
let flyoutElement = $state<HTMLElement | null>(null);
|
|
29
|
+
|
|
30
|
+
// Get all focusable elements within the flyout
|
|
31
|
+
function getFocusableElements(): HTMLElement[] {
|
|
32
|
+
if (!flyoutElement) return [];
|
|
33
|
+
const elements = flyoutElement.querySelectorAll<HTMLElement>(
|
|
34
|
+
'a[href]:not([disabled]):not([tabindex="-1"]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
35
|
+
);
|
|
36
|
+
return Array.from(elements);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
40
|
+
if (event.key === 'Escape') {
|
|
41
|
+
onMouseLeave?.();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Focus trap - cycle through focusable elements
|
|
46
|
+
if (event.key === 'Tab') {
|
|
47
|
+
const focusable = getFocusableElements();
|
|
48
|
+
if (focusable.length === 0) return;
|
|
49
|
+
|
|
50
|
+
const firstElement = focusable[0];
|
|
51
|
+
const lastElement = focusable[focusable.length - 1];
|
|
52
|
+
|
|
53
|
+
if (event.shiftKey) {
|
|
54
|
+
// Shift+Tab: go backwards
|
|
55
|
+
if (document.activeElement === firstElement || document.activeElement === flyoutElement) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
lastElement.focus();
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// Tab: go forwards
|
|
61
|
+
if (document.activeElement === lastElement) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
firstElement.focus();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Arrow key navigation within flyout
|
|
70
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
const items = flyoutElement?.querySelectorAll('[role="menuitem"]');
|
|
73
|
+
if (!items || items.length === 0) return;
|
|
74
|
+
|
|
75
|
+
const currentIndex = Array.from(items).findIndex((el) => el === document.activeElement);
|
|
76
|
+
|
|
77
|
+
if (event.key === 'ArrowDown') {
|
|
78
|
+
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
79
|
+
(items[nextIndex] as HTMLElement).focus();
|
|
80
|
+
} else {
|
|
81
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
82
|
+
(items[prevIndex] as HTMLElement).focus();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
{#snippet navBadge(badge: string | number | undefined)}
|
|
89
|
+
{#if badge !== undefined}
|
|
90
|
+
<span
|
|
91
|
+
class="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground"
|
|
92
|
+
>
|
|
93
|
+
{badge}
|
|
94
|
+
</span>
|
|
95
|
+
{/if}
|
|
96
|
+
{/snippet}
|
|
97
|
+
|
|
98
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
99
|
+
<div
|
|
100
|
+
bind:this={flyoutElement}
|
|
101
|
+
class={cn(
|
|
102
|
+
'fixed left-16 min-w-48 rounded-md border shadow-lg z-[calc(var(--z-sidebar,50)+10)]',
|
|
103
|
+
'motion-reduce:transition-none',
|
|
104
|
+
isLight ? 'bg-background border-border' : 'bg-sidebar border-sidebar-foreground/20'
|
|
105
|
+
)}
|
|
106
|
+
style="top: {top}px;"
|
|
107
|
+
role="menu"
|
|
108
|
+
tabindex="0"
|
|
109
|
+
onmouseenter={onMouseEnter}
|
|
110
|
+
onmouseleave={onMouseLeave}
|
|
111
|
+
onkeydown={handleKeydown}
|
|
112
|
+
transition:fly={{ x: -10, duration: 150 }}
|
|
113
|
+
>
|
|
114
|
+
<div class="p-2">
|
|
115
|
+
<!-- Item name as header -->
|
|
116
|
+
<div
|
|
117
|
+
class={cn(
|
|
118
|
+
'px-3 py-2 text-sm font-medium',
|
|
119
|
+
isLight ? 'text-foreground' : 'text-sidebar-foreground'
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{#if item.href && !item.disabled}
|
|
123
|
+
<a
|
|
124
|
+
href={item.href}
|
|
125
|
+
class="hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 rounded"
|
|
126
|
+
target={item.external ? '_blank' : undefined}
|
|
127
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
128
|
+
onclick={() => onNavigate?.(item)}
|
|
129
|
+
>
|
|
130
|
+
{item.name}
|
|
131
|
+
{#if item.external}
|
|
132
|
+
<span class="text-xs opacity-50">(opens in new tab)</span>
|
|
133
|
+
{/if}
|
|
134
|
+
</a>
|
|
135
|
+
{:else}
|
|
136
|
+
<span class={item.disabled ? 'opacity-50' : ''}>
|
|
137
|
+
{item.name}
|
|
138
|
+
</span>
|
|
139
|
+
{/if}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Children if present -->
|
|
143
|
+
{#if item.children && item.children.length > 0}
|
|
144
|
+
<div
|
|
145
|
+
class={cn('border-t mt-1 pt-1', isLight ? 'border-border' : 'border-sidebar-foreground/20')}
|
|
146
|
+
>
|
|
147
|
+
{#each item.children as child}
|
|
148
|
+
<a
|
|
149
|
+
href={child.disabled ? undefined : child.href}
|
|
150
|
+
class={cn(
|
|
151
|
+
'flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors ml-2',
|
|
152
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
153
|
+
isLight
|
|
154
|
+
? 'text-foreground hover:bg-accent hover:text-accent-foreground'
|
|
155
|
+
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
156
|
+
child.active &&
|
|
157
|
+
(isLight
|
|
158
|
+
? 'bg-primary/10 text-primary'
|
|
159
|
+
: 'bg-sidebar-primary text-sidebar-primary-foreground'),
|
|
160
|
+
child.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
|
|
161
|
+
)}
|
|
162
|
+
target={child.external ? '_blank' : undefined}
|
|
163
|
+
rel={child.external ? 'noopener noreferrer' : undefined}
|
|
164
|
+
aria-disabled={child.disabled || undefined}
|
|
165
|
+
tabindex={child.disabled ? -1 : undefined}
|
|
166
|
+
onclick={(e) => {
|
|
167
|
+
if (child.disabled) {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
onNavigate?.(child);
|
|
172
|
+
}}
|
|
173
|
+
role="menuitem"
|
|
174
|
+
>
|
|
175
|
+
{child.name}
|
|
176
|
+
{@render navBadge(child.badge)}
|
|
177
|
+
</a>
|
|
178
|
+
{/each}
|
|
179
|
+
</div>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NavItem } from '../../../types/layout.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The item to show in the flyout */
|
|
4
|
+
item: NavItem;
|
|
5
|
+
/** Top position in pixels */
|
|
6
|
+
top: number;
|
|
7
|
+
/** Whether using light variant */
|
|
8
|
+
isLight?: boolean;
|
|
9
|
+
/** Callback when a navigation item is clicked */
|
|
10
|
+
onNavigate?: (item: NavItem) => void;
|
|
11
|
+
/** Callback when mouse enters flyout */
|
|
12
|
+
onMouseEnter?: () => void;
|
|
13
|
+
/** Callback when mouse leaves flyout */
|
|
14
|
+
onMouseLeave?: () => void;
|
|
15
|
+
}
|
|
16
|
+
declare const SidebarFlyout: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type SidebarFlyout = ReturnType<typeof SidebarFlyout>;
|
|
18
|
+
export default SidebarFlyout;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SidebarNavItem - Individual navigation item component
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Items with children (expandable submenu)
|
|
7
|
+
* - Items without children (simple links/buttons)
|
|
8
|
+
* - Collapsed mode (icons only with flyout)
|
|
9
|
+
* - Disabled and loading states
|
|
10
|
+
*/
|
|
11
|
+
import { slide } from 'svelte/transition';
|
|
12
|
+
import type { Snippet } from 'svelte';
|
|
13
|
+
import type { NavItem } from '../../../types/layout.js';
|
|
14
|
+
import { cn } from '../../../utils.js';
|
|
15
|
+
import { sidebarStore } from '../../../stores/sidebar.svelte.js';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** The navigation item to render */
|
|
19
|
+
item: NavItem;
|
|
20
|
+
/** Whether using light variant */
|
|
21
|
+
isLight?: boolean;
|
|
22
|
+
/** Whether sidebar is collapsed */
|
|
23
|
+
collapsed?: boolean;
|
|
24
|
+
/** Whether currently on mobile */
|
|
25
|
+
isMobile?: boolean;
|
|
26
|
+
/** Custom icon renderer */
|
|
27
|
+
icon?: Snippet<[NavItem]>;
|
|
28
|
+
/** Search query for text highlighting */
|
|
29
|
+
searchQuery?: string;
|
|
30
|
+
/** Callback when item is clicked */
|
|
31
|
+
onNavigate?: (item: NavItem) => void;
|
|
32
|
+
/** Callback when mouse enters item (for flyout) */
|
|
33
|
+
onMouseEnter?: (item: NavItem, event: MouseEvent) => void;
|
|
34
|
+
/** Callback when mouse leaves item (for flyout) */
|
|
35
|
+
onMouseLeave?: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
item,
|
|
40
|
+
isLight = true,
|
|
41
|
+
collapsed = false,
|
|
42
|
+
isMobile = false,
|
|
43
|
+
icon,
|
|
44
|
+
searchQuery = '',
|
|
45
|
+
onNavigate,
|
|
46
|
+
onMouseEnter,
|
|
47
|
+
onMouseLeave,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
// Highlight matching text in item names
|
|
51
|
+
function highlightText(
|
|
52
|
+
text: string,
|
|
53
|
+
query: string
|
|
54
|
+
): { before: string; match: string; after: string } | null {
|
|
55
|
+
if (!query.trim()) return null;
|
|
56
|
+
const lowerText = text.toLowerCase();
|
|
57
|
+
const lowerQuery = query.toLowerCase();
|
|
58
|
+
const index = lowerText.indexOf(lowerQuery);
|
|
59
|
+
if (index === -1) return null;
|
|
60
|
+
return {
|
|
61
|
+
before: text.slice(0, index),
|
|
62
|
+
match: text.slice(index, index + query.length),
|
|
63
|
+
after: text.slice(index + query.length),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const itemHighlight = $derived(highlightText(item.name, searchQuery));
|
|
68
|
+
|
|
69
|
+
const hasChildren = $derived(item.children && item.children.length > 0);
|
|
70
|
+
const isExpanded = $derived(sidebarStore.isItemExpanded(item.id));
|
|
71
|
+
|
|
72
|
+
function toggleItem() {
|
|
73
|
+
sidebarStore.toggleItem(item.id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getItemStateClasses(active?: boolean, disabled?: boolean): string {
|
|
77
|
+
return cn(
|
|
78
|
+
isLight && !active && 'text-foreground hover:bg-accent hover:text-accent-foreground',
|
|
79
|
+
isLight && active && 'bg-primary/10 text-primary',
|
|
80
|
+
!isLight && !active && 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
|
81
|
+
!isLight && active && 'bg-sidebar-primary text-sidebar-primary-foreground',
|
|
82
|
+
disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleClick(e: MouseEvent) {
|
|
87
|
+
if (item.disabled) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
onNavigate?.(item);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleMouseEnter(e: MouseEvent) {
|
|
95
|
+
onMouseEnter?.(item, e);
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<!-- Reusable snippets -->
|
|
100
|
+
{#snippet navIcon(navItem: NavItem)}
|
|
101
|
+
{#if navItem.loading}
|
|
102
|
+
<span class="h-5 w-5 shrink-0 flex items-center justify-center">
|
|
103
|
+
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
|
104
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
105
|
+
<path
|
|
106
|
+
class="opacity-75"
|
|
107
|
+
fill="currentColor"
|
|
108
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
109
|
+
/>
|
|
110
|
+
</svg>
|
|
111
|
+
</span>
|
|
112
|
+
{:else if icon}
|
|
113
|
+
<span class="h-5 w-5 shrink-0">
|
|
114
|
+
{@render icon(navItem)}
|
|
115
|
+
</span>
|
|
116
|
+
{:else if navItem.icon}
|
|
117
|
+
<span class="h-5 w-5 shrink-0 flex items-center justify-center">
|
|
118
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
119
|
+
<circle cx="12" cy="12" r="10" stroke-width="2" />
|
|
120
|
+
</svg>
|
|
121
|
+
</span>
|
|
122
|
+
{/if}
|
|
123
|
+
{/snippet}
|
|
124
|
+
|
|
125
|
+
{#snippet navBadge(badge: string | number | undefined)}
|
|
126
|
+
{#if badge !== undefined}
|
|
127
|
+
<span class="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground">
|
|
128
|
+
{badge}
|
|
129
|
+
</span>
|
|
130
|
+
{/if}
|
|
131
|
+
{/snippet}
|
|
132
|
+
|
|
133
|
+
{#snippet externalIndicator()}
|
|
134
|
+
<svg class="h-4 w-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
135
|
+
<path
|
|
136
|
+
stroke-linecap="round"
|
|
137
|
+
stroke-linejoin="round"
|
|
138
|
+
stroke-width="2"
|
|
139
|
+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
140
|
+
/>
|
|
141
|
+
</svg>
|
|
142
|
+
{/snippet}
|
|
143
|
+
|
|
144
|
+
{#snippet highlightedName(
|
|
145
|
+
name: string,
|
|
146
|
+
highlight: { before: string; match: string; after: string } | null
|
|
147
|
+
)}
|
|
148
|
+
{#if highlight}
|
|
149
|
+
<span class="flex-1 truncate">
|
|
150
|
+
{highlight.before}<mark class="bg-yellow-200 dark:bg-yellow-900 text-inherit rounded px-0.5"
|
|
151
|
+
>{highlight.match}</mark
|
|
152
|
+
>{highlight.after}
|
|
153
|
+
</span>
|
|
154
|
+
{:else}
|
|
155
|
+
<span class="flex-1 truncate">{name}</span>
|
|
156
|
+
{/if}
|
|
157
|
+
{/snippet}
|
|
158
|
+
|
|
159
|
+
{#if hasChildren && !collapsed}
|
|
160
|
+
<!-- Item with children - expandable submenu -->
|
|
161
|
+
<div>
|
|
162
|
+
<div
|
|
163
|
+
class={cn(
|
|
164
|
+
'flex w-full items-center rounded-md transition-colors overflow-hidden',
|
|
165
|
+
isLight && !item.active && 'hover:bg-accent',
|
|
166
|
+
isLight && item.active && 'bg-primary/10',
|
|
167
|
+
!isLight && !item.active && 'hover:bg-sidebar-accent',
|
|
168
|
+
!isLight && item.active && 'bg-sidebar-primary'
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{#if item.href}
|
|
172
|
+
<!-- Navigable parent: link for main content -->
|
|
173
|
+
<a
|
|
174
|
+
href={item.disabled ? undefined : item.href}
|
|
175
|
+
class={cn(
|
|
176
|
+
'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
|
|
177
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
178
|
+
isLight && !item.active && 'text-foreground hover:text-accent-foreground',
|
|
179
|
+
isLight && item.active && 'text-primary',
|
|
180
|
+
!isLight && !item.active && 'hover:text-sidebar-accent-foreground',
|
|
181
|
+
!isLight && item.active && 'text-sidebar-primary-foreground',
|
|
182
|
+
item.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
|
|
183
|
+
)}
|
|
184
|
+
target={item.external ? '_blank' : undefined}
|
|
185
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
186
|
+
aria-current={item.active ? 'page' : undefined}
|
|
187
|
+
aria-disabled={item.disabled || undefined}
|
|
188
|
+
tabindex={item.disabled ? -1 : undefined}
|
|
189
|
+
onclick={handleClick}
|
|
190
|
+
>
|
|
191
|
+
{@render navIcon(item)}
|
|
192
|
+
{@render highlightedName(item.name, itemHighlight)}
|
|
193
|
+
{@render navBadge(item.badge)}
|
|
194
|
+
{#if item.external}{@render externalIndicator()}{/if}
|
|
195
|
+
</a>
|
|
196
|
+
{:else}
|
|
197
|
+
<!-- Non-navigable parent: button for entire main area -->
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
class={cn(
|
|
201
|
+
'flex flex-1 items-center gap-3 px-3 py-2 text-sm font-medium transition-colors overflow-hidden text-left',
|
|
202
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
203
|
+
isLight && !item.active && 'text-foreground',
|
|
204
|
+
isLight && item.active && 'text-primary',
|
|
205
|
+
!isLight && !item.active && '',
|
|
206
|
+
!isLight && item.active && 'text-sidebar-primary-foreground',
|
|
207
|
+
item.disabled && 'opacity-50 pointer-events-none cursor-not-allowed'
|
|
208
|
+
)}
|
|
209
|
+
onclick={() => !item.disabled && toggleItem()}
|
|
210
|
+
aria-expanded={isExpanded}
|
|
211
|
+
aria-disabled={item.disabled || undefined}
|
|
212
|
+
disabled={item.disabled}
|
|
213
|
+
>
|
|
214
|
+
{@render navIcon(item)}
|
|
215
|
+
{@render highlightedName(item.name, itemHighlight)}
|
|
216
|
+
{@render navBadge(item.badge)}
|
|
217
|
+
</button>
|
|
218
|
+
{/if}
|
|
219
|
+
|
|
220
|
+
<!-- Separate expand/collapse toggle button -->
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
class={cn(
|
|
224
|
+
'flex items-center justify-center p-2 transition-colors rounded-md',
|
|
225
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
226
|
+
isLight ? 'hover:bg-accent/50' : 'hover:bg-sidebar-accent/50'
|
|
227
|
+
)}
|
|
228
|
+
onclick={(e) => {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
e.stopPropagation();
|
|
231
|
+
toggleItem();
|
|
232
|
+
}}
|
|
233
|
+
aria-expanded={isExpanded}
|
|
234
|
+
aria-label={isExpanded ? `Collapse ${item.name}` : `Expand ${item.name}`}
|
|
235
|
+
>
|
|
236
|
+
<svg
|
|
237
|
+
class={cn(
|
|
238
|
+
'h-4 w-4 transition-transform duration-200',
|
|
239
|
+
isExpanded ? 'rotate-0' : '-rotate-90'
|
|
240
|
+
)}
|
|
241
|
+
fill="none"
|
|
242
|
+
viewBox="0 0 24 24"
|
|
243
|
+
stroke="currentColor"
|
|
244
|
+
>
|
|
245
|
+
<path
|
|
246
|
+
stroke-linecap="round"
|
|
247
|
+
stroke-linejoin="round"
|
|
248
|
+
stroke-width="2"
|
|
249
|
+
d="M19 9l-7 7-7-7"
|
|
250
|
+
/>
|
|
251
|
+
</svg>
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{#if isExpanded}
|
|
256
|
+
<ul
|
|
257
|
+
class="mt-1 space-y-1 pl-4 motion-reduce:transition-none overflow-hidden"
|
|
258
|
+
transition:slide={{ duration: 200 }}
|
|
259
|
+
>
|
|
260
|
+
{#each item.children as child}
|
|
261
|
+
<li>
|
|
262
|
+
<a
|
|
263
|
+
href={child.disabled ? undefined : child.href}
|
|
264
|
+
class={cn(
|
|
265
|
+
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden',
|
|
266
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
267
|
+
getItemStateClasses(child.active, child.disabled)
|
|
268
|
+
)}
|
|
269
|
+
target={child.external ? '_blank' : undefined}
|
|
270
|
+
rel={child.external ? 'noopener noreferrer' : undefined}
|
|
271
|
+
aria-current={child.active ? 'page' : undefined}
|
|
272
|
+
aria-disabled={child.disabled || undefined}
|
|
273
|
+
tabindex={child.disabled ? -1 : undefined}
|
|
274
|
+
onclick={(e) => {
|
|
275
|
+
if (child.disabled) {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
onNavigate?.(child);
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{@render navIcon(child)}
|
|
283
|
+
{@render highlightedName(child.name, highlightText(child.name, searchQuery))}
|
|
284
|
+
{@render navBadge(child.badge)}
|
|
285
|
+
{#if child.external}{@render externalIndicator()}{/if}
|
|
286
|
+
</a>
|
|
287
|
+
</li>
|
|
288
|
+
{/each}
|
|
289
|
+
</ul>
|
|
290
|
+
{/if}
|
|
291
|
+
</div>
|
|
292
|
+
{:else}
|
|
293
|
+
<!-- Regular item or collapsed item -->
|
|
294
|
+
{#if item.href}
|
|
295
|
+
<!-- Item with href - render as link -->
|
|
296
|
+
<a
|
|
297
|
+
href={item.disabled ? undefined : item.href}
|
|
298
|
+
class={cn(
|
|
299
|
+
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden relative',
|
|
300
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
301
|
+
getItemStateClasses(item.active, item.disabled),
|
|
302
|
+
collapsed && !isMobile && 'justify-center px-2'
|
|
303
|
+
)}
|
|
304
|
+
target={item.external ? '_blank' : undefined}
|
|
305
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
306
|
+
aria-current={item.active ? 'page' : undefined}
|
|
307
|
+
aria-disabled={item.disabled || undefined}
|
|
308
|
+
tabindex={item.disabled ? -1 : undefined}
|
|
309
|
+
title={collapsed && !isMobile
|
|
310
|
+
? item.external
|
|
311
|
+
? `${item.name} (opens in new tab)`
|
|
312
|
+
: item.name
|
|
313
|
+
: undefined}
|
|
314
|
+
onclick={handleClick}
|
|
315
|
+
onmouseenter={handleMouseEnter}
|
|
316
|
+
onmouseleave={onMouseLeave}
|
|
317
|
+
>
|
|
318
|
+
{@render navIcon(item)}
|
|
319
|
+
|
|
320
|
+
{#if !collapsed || isMobile}
|
|
321
|
+
{@render highlightedName(item.name, itemHighlight)}
|
|
322
|
+
{@render navBadge(item.badge)}
|
|
323
|
+
{#if item.external}{@render externalIndicator()}{/if}
|
|
324
|
+
{/if}
|
|
325
|
+
|
|
326
|
+
<!-- Visual indicator for collapsed items with children -->
|
|
327
|
+
{#if collapsed && !isMobile && hasChildren}
|
|
328
|
+
<span class="absolute bottom-0.5 right-0.5">
|
|
329
|
+
<svg class="h-2 w-2 opacity-50" fill="currentColor" viewBox="0 0 8 8">
|
|
330
|
+
<path d="M0 0 L8 4 L0 8 Z" />
|
|
331
|
+
</svg>
|
|
332
|
+
</span>
|
|
333
|
+
{/if}
|
|
334
|
+
</a>
|
|
335
|
+
{:else}
|
|
336
|
+
<!-- Item without href (parent with children only, or placeholder) - render as button -->
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
class={cn(
|
|
340
|
+
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors overflow-hidden w-full text-left relative',
|
|
341
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
|
342
|
+
getItemStateClasses(item.active, item.disabled),
|
|
343
|
+
collapsed && !isMobile && 'justify-center px-2'
|
|
344
|
+
)}
|
|
345
|
+
title={collapsed && !isMobile ? item.name : undefined}
|
|
346
|
+
aria-haspopup={hasChildren ? 'menu' : undefined}
|
|
347
|
+
aria-disabled={item.disabled || undefined}
|
|
348
|
+
disabled={item.disabled}
|
|
349
|
+
onmouseenter={handleMouseEnter}
|
|
350
|
+
onmouseleave={onMouseLeave}
|
|
351
|
+
>
|
|
352
|
+
{@render navIcon(item)}
|
|
353
|
+
|
|
354
|
+
{#if !collapsed || isMobile}
|
|
355
|
+
{@render highlightedName(item.name, itemHighlight)}
|
|
356
|
+
{@render navBadge(item.badge)}
|
|
357
|
+
{/if}
|
|
358
|
+
|
|
359
|
+
<!-- Visual indicator for collapsed items with children -->
|
|
360
|
+
{#if collapsed && !isMobile && hasChildren}
|
|
361
|
+
<span class="absolute bottom-0.5 right-0.5">
|
|
362
|
+
<svg class="h-2 w-2 opacity-50" fill="currentColor" viewBox="0 0 8 8">
|
|
363
|
+
<path d="M0 0 L8 4 L0 8 Z" />
|
|
364
|
+
</svg>
|
|
365
|
+
</span>
|
|
366
|
+
{/if}
|
|
367
|
+
</button>
|
|
368
|
+
{/if}
|
|
369
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { NavItem } from '../../../types/layout.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The navigation item to render */
|
|
5
|
+
item: NavItem;
|
|
6
|
+
/** Whether using light variant */
|
|
7
|
+
isLight?: boolean;
|
|
8
|
+
/** Whether sidebar is collapsed */
|
|
9
|
+
collapsed?: boolean;
|
|
10
|
+
/** Whether currently on mobile */
|
|
11
|
+
isMobile?: boolean;
|
|
12
|
+
/** Custom icon renderer */
|
|
13
|
+
icon?: Snippet<[NavItem]>;
|
|
14
|
+
/** Search query for text highlighting */
|
|
15
|
+
searchQuery?: string;
|
|
16
|
+
/** Callback when item is clicked */
|
|
17
|
+
onNavigate?: (item: NavItem) => void;
|
|
18
|
+
/** Callback when mouse enters item (for flyout) */
|
|
19
|
+
onMouseEnter?: (item: NavItem, event: MouseEvent) => void;
|
|
20
|
+
/** Callback when mouse leaves item (for flyout) */
|
|
21
|
+
onMouseLeave?: () => void;
|
|
22
|
+
}
|
|
23
|
+
declare const SidebarNavItem: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type SidebarNavItem = ReturnType<typeof SidebarNavItem>;
|
|
25
|
+
export default SidebarNavItem;
|