@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.
Files changed (41) hide show
  1. package/dist/lib/components/Combobox.svelte +187 -0
  2. package/dist/lib/components/Combobox.svelte.d.ts +38 -0
  3. package/dist/lib/components/DateTimePicker.svelte +415 -0
  4. package/dist/lib/components/DateTimePicker.svelte.d.ts +31 -0
  5. package/dist/lib/components/MultiSelect.svelte +244 -0
  6. package/dist/lib/components/MultiSelect.svelte.d.ts +40 -0
  7. package/dist/lib/components/NumberInput.svelte +205 -0
  8. package/dist/lib/components/NumberInput.svelte.d.ts +33 -0
  9. package/dist/lib/components/OTPInput.svelte +213 -0
  10. package/dist/lib/components/OTPInput.svelte.d.ts +23 -0
  11. package/dist/lib/components/RadioGroup.svelte +124 -0
  12. package/dist/lib/components/RadioGroup.svelte.d.ts +31 -0
  13. package/dist/lib/components/Signature.svelte +1070 -0
  14. package/dist/lib/components/Signature.svelte.d.ts +74 -0
  15. package/dist/lib/components/Slider.svelte +136 -0
  16. package/dist/lib/components/Slider.svelte.d.ts +30 -0
  17. package/dist/lib/components/layout/AppShell.svelte +1 -1
  18. package/dist/lib/components/layout/DashboardLayout.svelte +63 -16
  19. package/dist/lib/components/layout/DashboardLayout.svelte.d.ts +12 -10
  20. package/dist/lib/components/layout/QuickLinks.svelte +49 -29
  21. package/dist/lib/components/layout/QuickLinks.svelte.d.ts +4 -2
  22. package/dist/lib/components/layout/Sidebar.svelte +345 -86
  23. package/dist/lib/components/layout/Sidebar.svelte.d.ts +12 -0
  24. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +182 -0
  25. package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte.d.ts +18 -0
  26. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +369 -0
  27. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +25 -0
  28. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte +121 -0
  29. package/dist/lib/components/layout/sidebar/SidebarSearch.svelte.d.ts +17 -0
  30. package/dist/lib/components/layout/sidebar/SidebarSection.svelte +144 -0
  31. package/dist/lib/components/layout/sidebar/SidebarSection.svelte.d.ts +25 -0
  32. package/dist/lib/components/layout/sidebar/index.d.ts +10 -0
  33. package/dist/lib/components/layout/sidebar/index.js +10 -0
  34. package/dist/lib/index.d.ts +9 -1
  35. package/dist/lib/index.js +8 -0
  36. package/dist/lib/schemas/auth.d.ts +6 -6
  37. package/dist/lib/stores/sidebar.svelte.d.ts +54 -0
  38. package/dist/lib/stores/sidebar.svelte.js +171 -1
  39. package/dist/lib/types/components.d.ts +105 -0
  40. package/dist/lib/types/layout.d.ts +32 -2
  41. package/package.json +1 -1
@@ -0,0 +1,74 @@
1
+ import type { SignatureData, SignatureFont, SignatureValidationResult } from '../types/components.js';
2
+ interface Props {
3
+ /** Signature data containing image output and metadata */
4
+ value?: SignatureData | null;
5
+ /** Callback when signature changes */
6
+ onValueChange?: (data: SignatureData | null) => void;
7
+ /** Callback when signature is completed (meets validation) */
8
+ onComplete?: (data: SignatureData) => void;
9
+ /** Callback when consent state changes */
10
+ onConsentChange?: (consented: boolean) => void;
11
+ /** Input mode: draw with stylus/mouse or type with fonts */
12
+ mode?: 'draw' | 'type';
13
+ /** Whether to show mode toggle */
14
+ showModeToggle?: boolean;
15
+ /** Default typed name for type mode */
16
+ typedName?: string;
17
+ /** Stroke color (default: currentColor) */
18
+ strokeColor?: string;
19
+ /** Stroke width in pixels */
20
+ strokeWidth?: number;
21
+ /** Minimum stroke width for pressure */
22
+ minStrokeWidth?: number;
23
+ /** Maximum stroke width for pressure */
24
+ maxStrokeWidth?: number;
25
+ /** Whether to show stroke customization */
26
+ showStrokeCustomization?: boolean;
27
+ /** Available fonts for typed signatures */
28
+ fonts?: SignatureFont[];
29
+ /** Selected font family */
30
+ selectedFont?: string;
31
+ /** Preferred output format */
32
+ outputFormat?: 'png' | 'svg' | 'dataUrl';
33
+ /** PNG/image quality (0-1) */
34
+ imageQuality?: number;
35
+ /** Background color */
36
+ backgroundColor?: 'transparent' | 'white';
37
+ /** Minimum stroke points required (draw mode) */
38
+ minStrokePoints?: number;
39
+ /** Minimum stroke length in pixels (draw mode) */
40
+ minStrokeLength?: number;
41
+ /** Minimum characters for typed signature */
42
+ minTypedLength?: number;
43
+ /** Custom validation function */
44
+ validate?: (data: SignatureData) => SignatureValidationResult;
45
+ /** Whether to show consent checkbox */
46
+ showConsent?: boolean;
47
+ /** Consent checkbox text */
48
+ consentText?: string;
49
+ /** Whether consent is required */
50
+ requireConsent?: boolean;
51
+ /** Current consent state */
52
+ consented?: boolean;
53
+ /** Input ID */
54
+ id?: string;
55
+ /** Name attribute for form submission */
56
+ name?: string;
57
+ /** Whether disabled */
58
+ disabled?: boolean;
59
+ /** Whether required */
60
+ required?: boolean;
61
+ /** Error message */
62
+ error?: string;
63
+ /** Hint text */
64
+ hint?: string;
65
+ /** Label text */
66
+ label?: string;
67
+ /** Canvas height (CSS value) */
68
+ height?: string;
69
+ /** Additional class */
70
+ class?: string;
71
+ }
72
+ declare const Signature: import("svelte").Component<Props, {}, "value" | "consented">;
73
+ type Signature = ReturnType<typeof Signature>;
74
+ export default Signature;
@@ -0,0 +1,136 @@
1
+ <script lang="ts">
2
+ import { Slider as SliderPrimitive } from 'bits-ui';
3
+ import { cn } from '../utils.js';
4
+ import { tv, type VariantProps } from 'tailwind-variants';
5
+
6
+ const sliderVariants = tv({
7
+ slots: {
8
+ root: 'relative flex w-full touch-none select-none items-center',
9
+ range: 'absolute h-full bg-primary rounded-full',
10
+ thumb:
11
+ 'block rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
12
+ },
13
+ variants: {
14
+ size: {
15
+ sm: {
16
+ root: 'h-1.5',
17
+ thumb: 'h-4 w-4',
18
+ },
19
+ md: {
20
+ root: 'h-2',
21
+ thumb: 'h-5 w-5',
22
+ },
23
+ lg: {
24
+ root: 'h-3',
25
+ thumb: 'h-6 w-6',
26
+ },
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ size: 'md',
31
+ },
32
+ });
33
+
34
+ type SliderVariants = VariantProps<typeof sliderVariants>;
35
+
36
+ interface Props {
37
+ /** Current value(s) - array for range slider */
38
+ value?: number[];
39
+ /** Minimum value */
40
+ min?: number;
41
+ /** Maximum value */
42
+ max?: number;
43
+ /** Step increment */
44
+ step?: number;
45
+ /** Whether the slider is disabled */
46
+ disabled?: boolean;
47
+ /** Layout orientation */
48
+ orientation?: 'horizontal' | 'vertical';
49
+ /** Callback when value changes during drag */
50
+ onValueChange?: (value: number[]) => void;
51
+ /** Callback when value is committed (drag ends) */
52
+ onValueCommit?: (value: number[]) => void;
53
+ /** Size variant */
54
+ size?: SliderVariants['size'];
55
+ /** Optional label */
56
+ label?: string;
57
+ /** Whether to show the current value */
58
+ showValue?: boolean;
59
+ /** Format function for displayed value */
60
+ formatValue?: (value: number) => string;
61
+ /** Additional class for the container */
62
+ class?: string;
63
+ }
64
+
65
+ let {
66
+ value = $bindable([50]),
67
+ min = 0,
68
+ max = 100,
69
+ step = 1,
70
+ disabled = false,
71
+ orientation = 'horizontal',
72
+ onValueChange,
73
+ onValueCommit,
74
+ size = 'md',
75
+ label,
76
+ showValue = false,
77
+ formatValue = (v) => String(v),
78
+ class: className,
79
+ }: Props = $props();
80
+
81
+ const styles = $derived(sliderVariants({ size }));
82
+
83
+ function handleValueChange(newValue: number[]) {
84
+ value = newValue;
85
+ onValueChange?.(newValue);
86
+ }
87
+
88
+ function handleValueCommit(newValue: number[]) {
89
+ onValueCommit?.(newValue);
90
+ }
91
+
92
+ const displayValue = $derived(
93
+ value.length === 1
94
+ ? formatValue(value[0])
95
+ : `${formatValue(value[0])} - ${formatValue(value[value.length - 1])}`
96
+ );
97
+ </script>
98
+
99
+ <div class={cn('w-full', className)}>
100
+ {#if label || showValue}
101
+ <div class="flex items-center justify-between mb-2">
102
+ {#if label}
103
+ <span class="text-sm font-medium leading-none">
104
+ {label}
105
+ </span>
106
+ {/if}
107
+ {#if showValue}
108
+ <span class="text-sm text-muted-foreground tabular-nums">
109
+ {displayValue}
110
+ </span>
111
+ {/if}
112
+ </div>
113
+ {/if}
114
+
115
+ <SliderPrimitive.Root
116
+ type="multiple"
117
+ {value}
118
+ {min}
119
+ {max}
120
+ {step}
121
+ {disabled}
122
+ {orientation}
123
+ onValueChange={handleValueChange}
124
+ onValueCommit={handleValueCommit}
125
+ class={cn(styles.root(), 'rounded-full bg-secondary')}
126
+ >
127
+ <SliderPrimitive.Range class={styles.range()} />
128
+ {#each value as _, i}
129
+ <SliderPrimitive.Thumb
130
+ index={i}
131
+ class={styles.thumb()}
132
+ aria-label={value.length > 1 ? `Thumb ${i + 1}` : label || 'Slider'}
133
+ />
134
+ {/each}
135
+ </SliderPrimitive.Root>
136
+ </div>
@@ -0,0 +1,30 @@
1
+ declare const Slider: import("svelte").Component<{
2
+ /** Current value(s) - array for range slider */
3
+ value?: number[];
4
+ /** Minimum value */
5
+ min?: number;
6
+ /** Maximum value */
7
+ max?: number;
8
+ /** Step increment */
9
+ step?: number;
10
+ /** Whether the slider is disabled */
11
+ disabled?: boolean;
12
+ /** Layout orientation */
13
+ orientation?: "horizontal" | "vertical";
14
+ /** Callback when value changes during drag */
15
+ onValueChange?: (value: number[]) => void;
16
+ /** Callback when value is committed (drag ends) */
17
+ onValueCommit?: (value: number[]) => void;
18
+ /** Size variant */
19
+ size?: "sm" | "md" | "lg" | undefined;
20
+ /** Optional label */
21
+ label?: string;
22
+ /** Whether to show the current value */
23
+ showValue?: boolean;
24
+ /** Format function for displayed value */
25
+ formatValue?: (value: number) => string;
26
+ /** Additional class for the container */
27
+ class?: string;
28
+ }, {}, "value">;
29
+ type Slider = ReturnType<typeof Slider>;
30
+ export default Slider;
@@ -52,7 +52,7 @@
52
52
  let {
53
53
  skipToId = 'main-content',
54
54
  skipToText = 'Skip to main content',
55
- toastPosition = 'top-right',
55
+ toastPosition = 'bottom-right',
56
56
  children,
57
57
  }: Props = $props();
58
58
  </script>
@@ -11,21 +11,12 @@
11
11
  * - Integrated with sidebar store for state management
12
12
  */
13
13
  import type { Snippet } from 'svelte';
14
- import type { NavSection, NavItem, User, QuickLink } from '../../types/layout.js';
14
+ import type { NavSection, NavItem, User, QuickLink, BackLink } from '../../types/layout.js';
15
15
  import { cn } from '../../utils.js';
16
16
  import { sidebarStore } from '../../stores/sidebar.svelte.js';
17
17
  import AppShell from './AppShell.svelte';
18
18
  import Sidebar from './Sidebar.svelte';
19
19
 
20
- interface BackLink {
21
- /** Link label (e.g., "Back to Dashboard") */
22
- label: string;
23
- /** Link URL */
24
- href: string;
25
- /** Icon name for the icon snippet */
26
- icon?: string;
27
- }
28
-
29
20
  interface Props {
30
21
  /** Navigation sections for sidebar */
31
22
  navigation: NavSection[];
@@ -49,10 +40,12 @@
49
40
  icon?: Snippet<[NavItem]>;
50
41
  /** Quick links displayed at bottom of sidebar */
51
42
  quickLinks?: QuickLink[];
52
- /** Quick links display mode: 'list' for stacked with labels, 'icons' for horizontal icons only */
43
+ /** Quick links display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
53
44
  quickLinksDisplay?: 'list' | 'icons';
54
45
  /** Custom icon renderer for quick links */
55
46
  quickLinkIcon?: Snippet<[QuickLink]>;
47
+ /** Callback when a navigation item is clicked */
48
+ onNavigate?: (item: NavItem) => void;
56
49
  /** Custom content at start of header (after toggle button) */
57
50
  headerStart?: Snippet;
58
51
  /** Custom content at end of header */
@@ -61,6 +54,14 @@
61
54
  userMenu?: Snippet<[User]>;
62
55
  /** Sidebar footer content */
63
56
  sidebarFooter?: Snippet;
57
+ /** Sidebar width when expanded in pixels (default: 256) */
58
+ expandedWidth?: number;
59
+ /** Sidebar width when collapsed in pixels (default: 64) */
60
+ collapsedWidth?: number;
61
+ /** Enable search/filter input for navigation */
62
+ searchable?: boolean;
63
+ /** Placeholder text for search input */
64
+ searchPlaceholder?: string;
64
65
  /** Main content */
65
66
  children: Snippet;
66
67
  }
@@ -79,10 +80,15 @@
79
80
  quickLinks,
80
81
  quickLinksDisplay = 'list',
81
82
  quickLinkIcon,
83
+ onNavigate,
82
84
  headerStart,
83
85
  headerEnd,
84
86
  userMenu,
85
87
  sidebarFooter,
88
+ expandedWidth = 256,
89
+ collapsedWidth = 64,
90
+ searchable = false,
91
+ searchPlaceholder = 'Search...',
86
92
  children,
87
93
  }: Props = $props();
88
94
 
@@ -125,36 +131,77 @@
125
131
  }
126
132
  });
127
133
 
128
- // Initialize sidebar state
134
+ // Load persisted sidebar state and apply prop-based defaults
129
135
  $effect(() => {
130
- if (!sidebarCollapsed && !isMobile) {
131
- sidebarStore.open();
136
+ sidebarStore.initialize();
137
+
138
+ // Only apply prop-based initial state if no persisted state exists
139
+ if (!sidebarStore.hasPersistedState && !isMobile) {
140
+ if (sidebarCollapsed) {
141
+ sidebarStore.close();
142
+ } else {
143
+ sidebarStore.open();
144
+ }
132
145
  }
133
146
  });
134
147
 
135
- const sidebarWidth = $derived(isMobile ? 0 : sidebarStore.isOpen ? 256 : 64);
148
+ // Keyboard shortcuts
149
+ $effect(() => {
150
+ if (typeof window === 'undefined') return;
151
+
152
+ const handleKeydown = (e: KeyboardEvent) => {
153
+ // Toggle sidebar (Ctrl+\ or Cmd+\)
154
+ if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
155
+ e.preventDefault();
156
+ sidebarStore.toggle();
157
+ return;
158
+ }
159
+
160
+ // Focus search with / key (when not in input/textarea)
161
+ if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName || '')) {
162
+ e.preventDefault();
163
+ const searchInput = document.querySelector<HTMLInputElement>('[data-sidebar-search]');
164
+ if (searchInput) {
165
+ searchInput.focus();
166
+ }
167
+ }
168
+ };
169
+
170
+ window.addEventListener('keydown', handleKeydown);
171
+ return () => window.removeEventListener('keydown', handleKeydown);
172
+ });
173
+
174
+ const sidebarWidth = $derived(
175
+ isMobile ? 0 : sidebarStore.isOpen ? expandedWidth : collapsedWidth
176
+ );
136
177
  </script>
137
178
 
138
179
  <AppShell>
139
180
  <!-- Sidebar -->
140
181
  <Sidebar
141
182
  navigation={effectiveNavigation}
183
+ userRoles={user?.roles}
142
184
  variant={sidebarVariant}
143
185
  collapsed={!sidebarStore.isOpen && !isMobile}
144
186
  {isMobile}
145
187
  mobileOpen={isMobile && sidebarStore.isOpen}
146
188
  onClose={() => sidebarStore.close()}
189
+ {onNavigate}
147
190
  {logo}
148
191
  {icon}
149
192
  {quickLinks}
150
193
  {quickLinksDisplay}
151
194
  {quickLinkIcon}
152
195
  footer={sidebarFooter}
196
+ {expandedWidth}
197
+ {collapsedWidth}
198
+ {searchable}
199
+ {searchPlaceholder}
153
200
  />
154
201
 
155
202
  <!-- Main Content Area -->
156
203
  <div
157
- class="flex flex-1 flex-col transition-all duration-300"
204
+ class="flex flex-1 flex-col transition-all duration-300 motion-reduce:transition-none"
158
205
  style="margin-left: {sidebarWidth}px;"
159
206
  >
160
207
  {#if showHeader}
@@ -10,15 +10,7 @@
10
10
  * - Integrated with sidebar store for state management
11
11
  */
12
12
  import type { Snippet } from 'svelte';
13
- import type { NavSection, NavItem, User, QuickLink } from '../../types/layout.js';
14
- interface BackLink {
15
- /** Link label (e.g., "Back to Dashboard") */
16
- label: string;
17
- /** Link URL */
18
- href: string;
19
- /** Icon name for the icon snippet */
20
- icon?: string;
21
- }
13
+ import type { NavSection, NavItem, User, QuickLink, BackLink } from '../../types/layout.js';
22
14
  interface Props {
23
15
  /** Navigation sections for sidebar */
24
16
  navigation: NavSection[];
@@ -42,10 +34,12 @@ interface Props {
42
34
  icon?: Snippet<[NavItem]>;
43
35
  /** Quick links displayed at bottom of sidebar */
44
36
  quickLinks?: QuickLink[];
45
- /** Quick links display mode: 'list' for stacked with labels, 'icons' for horizontal icons only */
37
+ /** Quick links display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
46
38
  quickLinksDisplay?: 'list' | 'icons';
47
39
  /** Custom icon renderer for quick links */
48
40
  quickLinkIcon?: Snippet<[QuickLink]>;
41
+ /** Callback when a navigation item is clicked */
42
+ onNavigate?: (item: NavItem) => void;
49
43
  /** Custom content at start of header (after toggle button) */
50
44
  headerStart?: Snippet;
51
45
  /** Custom content at end of header */
@@ -54,6 +48,14 @@ interface Props {
54
48
  userMenu?: Snippet<[User]>;
55
49
  /** Sidebar footer content */
56
50
  sidebarFooter?: Snippet;
51
+ /** Sidebar width when expanded in pixels (default: 256) */
52
+ expandedWidth?: number;
53
+ /** Sidebar width when collapsed in pixels (default: 64) */
54
+ collapsedWidth?: number;
55
+ /** Enable search/filter input for navigation */
56
+ searchable?: boolean;
57
+ /** Placeholder text for search input */
58
+ searchPlaceholder?: string;
57
59
  /** Main content */
58
60
  children: Snippet;
59
61
  }
@@ -3,7 +3,7 @@
3
3
  * QuickLinks - Quick navigation links section for sidebar
4
4
  *
5
5
  * Features:
6
- * - Two display modes: 'list' (stacked with labels) and 'icons' (horizontal icons only)
6
+ * - Two display modes: 'list' (stacked with labels) and 'icons' (stacked centered icons only)
7
7
  * - Light/dark variant support
8
8
  * - External link indicator
9
9
  * - Custom icon renderer support
@@ -16,37 +16,41 @@
16
16
  interface Props {
17
17
  /** Array of quick link items */
18
18
  links: QuickLink[];
19
- /** Display mode: 'list' for stacked with labels, 'icons' for horizontal icons only */
19
+ /** Display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
20
20
  display?: 'list' | 'icons';
21
21
  /** Visual variant - light (default) or dark */
22
22
  variant?: 'light' | 'dark';
23
23
  /** Custom icon renderer */
24
24
  icon?: Snippet<[QuickLink]>;
25
+ /** Accessible label for the navigation region */
26
+ ariaLabel?: string;
25
27
  /** Additional classes */
26
28
  class?: string;
27
29
  }
28
30
 
29
- let { links, display = 'list', variant = 'light', icon, class: className }: Props = $props();
31
+ let {
32
+ links,
33
+ display = 'list',
34
+ variant = 'light',
35
+ icon,
36
+ ariaLabel = 'Quick links',
37
+ class: className,
38
+ }: Props = $props();
30
39
 
31
40
  const isLight = $derived(variant === 'light');
32
41
  </script>
33
42
 
34
43
  {#if links.length > 0}
35
44
  <div
36
- class={cn(
37
- display === 'list'
38
- ? 'flex flex-col gap-1'
39
- : 'flex flex-row items-center justify-center gap-2',
40
- className
41
- )}
45
+ class={cn('flex flex-col gap-1', display === 'icons' && 'items-center', className)}
42
46
  role="navigation"
43
- aria-label="Quick links"
47
+ aria-label={ariaLabel}
44
48
  >
45
49
  {#each links as link}
46
50
  <a
47
51
  href={link.href}
48
52
  class={cn(
49
- 'flex items-center transition-colors',
53
+ 'flex items-center transition-all duration-300 motion-reduce:transition-none overflow-hidden',
50
54
  // List mode styles
51
55
  display === 'list' && 'gap-3 rounded-md px-3 py-2 text-sm',
52
56
  display === 'list' &&
@@ -56,7 +60,7 @@
56
60
  !isLight &&
57
61
  'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
58
62
  // Icon mode styles
59
- display === 'icons' && 'rounded-md p-2',
63
+ display === 'icons' && 'rounded-md p-2 gap-0',
60
64
  display === 'icons' &&
61
65
  isLight &&
62
66
  'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
@@ -87,24 +91,40 @@
87
91
  </span>
88
92
  {/if}
89
93
 
90
- {#if display === 'list'}
91
- <span class="flex-1 truncate">{link.label}</span>
94
+ <span
95
+ class={cn(
96
+ 'truncate whitespace-nowrap transition-all duration-300 motion-reduce:transition-none',
97
+ display === 'icons' ? 'w-0 opacity-0 flex-none' : 'flex-1 opacity-100'
98
+ )}
99
+ >
100
+ {link.label}
101
+ </span>
92
102
 
93
- {#if link.external}
94
- <svg
95
- class="h-4 w-4 shrink-0 opacity-50"
96
- fill="none"
97
- viewBox="0 0 24 24"
98
- stroke="currentColor"
99
- >
100
- <path
101
- stroke-linecap="round"
102
- stroke-linejoin="round"
103
- stroke-width="2"
104
- d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
105
- />
106
- </svg>
107
- {/if}
103
+ {#if link.badge !== undefined && display === 'list'}
104
+ <span
105
+ class="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground whitespace-nowrap"
106
+ >
107
+ {link.badge}
108
+ </span>
109
+ {/if}
110
+
111
+ {#if link.external}
112
+ <svg
113
+ class={cn(
114
+ 'h-4 w-4 shrink-0 transition-all duration-300',
115
+ display === 'icons' ? 'hidden' : 'opacity-50'
116
+ )}
117
+ fill="none"
118
+ viewBox="0 0 24 24"
119
+ stroke="currentColor"
120
+ >
121
+ <path
122
+ stroke-linecap="round"
123
+ stroke-linejoin="round"
124
+ stroke-width="2"
125
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
126
+ />
127
+ </svg>
108
128
  {/if}
109
129
  </a>
110
130
  {/each}
@@ -2,7 +2,7 @@
2
2
  * QuickLinks - Quick navigation links section for sidebar
3
3
  *
4
4
  * Features:
5
- * - Two display modes: 'list' (stacked with labels) and 'icons' (horizontal icons only)
5
+ * - Two display modes: 'list' (stacked with labels) and 'icons' (stacked centered icons only)
6
6
  * - Light/dark variant support
7
7
  * - External link indicator
8
8
  * - Custom icon renderer support
@@ -13,12 +13,14 @@ import type { QuickLink } from '../../types/layout.js';
13
13
  interface Props {
14
14
  /** Array of quick link items */
15
15
  links: QuickLink[];
16
- /** Display mode: 'list' for stacked with labels, 'icons' for horizontal icons only */
16
+ /** Display mode: 'list' for stacked with labels, 'icons' for stacked centered icons only */
17
17
  display?: 'list' | 'icons';
18
18
  /** Visual variant - light (default) or dark */
19
19
  variant?: 'light' | 'dark';
20
20
  /** Custom icon renderer */
21
21
  icon?: Snippet<[QuickLink]>;
22
+ /** Accessible label for the navigation region */
23
+ ariaLabel?: string;
22
24
  /** Additional classes */
23
25
  class?: string;
24
26
  }