svelora 3.0.7 → 3.0.8

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.
@@ -1,47 +1,48 @@
1
- <script lang="ts">import { twMerge } from "tailwind-merge";
2
- import { onMount } from "svelte";
3
- import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, PointElement, LineElement, ArcElement, RadialLinearScale, Filler } from "chart.js";
4
- import { Bar, Line, Pie, Doughnut, Radar, PolarArea, Scatter, Bubble } from "svelte-chartjs";
5
- import { chartVariants } from "./chart.variants.js";
6
- // Register all common elements
7
- ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, PointElement, LineElement, ArcElement, RadialLinearScale, Filler);
8
- // Set default styling to match Tailwind/Svelora theme
9
- ChartJS.defaults.color = "currentColor";
10
- ChartJS.defaults.font.family = "inherit";
11
- ChartJS.defaults.plugins.tooltip.backgroundColor = "rgba(0, 0, 0, 0.8)";
12
- ChartJS.defaults.plugins.tooltip.titleColor = "#ffffff";
13
- ChartJS.defaults.plugins.tooltip.bodyColor = "#ffffff";
14
- ChartJS.defaults.plugins.tooltip.cornerRadius = 6;
15
- ChartJS.defaults.plugins.tooltip.padding = 10;
16
- let { type, data, options = {}, class: className, ...restProps } = $props();
17
- let styles = $derived(chartVariants());
18
- // Map string types to components
19
- const components = {
20
- bar: Bar,
21
- line: Line,
22
- pie: Pie,
23
- doughnut: Doughnut,
24
- radar: Radar,
25
- polarArea: PolarArea,
26
- scatter: Scatter,
27
- bubble: Bubble
28
- };
29
- // Default responsive options
30
- const defaultOptions = {
31
- responsive: true,
32
- maintainAspectRatio: false
33
- };
34
- let mergedOptions = $derived({
35
- ...defaultOptions,
36
- ...options
1
+ <script lang="ts">import { onMount, onDestroy } from "svelte";
2
+ import { twMerge } from "tailwind-merge";
3
+ let { options, series, class: className } = $props();
4
+ let chartContainer;
5
+ let chartInstance = null;
6
+ // Helper to dynamically load apexcharts only on the client
7
+ onMount(async () => {
8
+ // Dynamic import so it doesn't break SSR
9
+ const ApexCharts = (await import("apexcharts")).default;
10
+ // Inject series if provided separately
11
+ const initialOptions = series ? {
12
+ ...options,
13
+ series
14
+ } : options;
15
+ // Default styling to match theme
16
+ const mergedOptions = {
17
+ chart: {
18
+ background: "transparent",
19
+ toolbar: { show: false },
20
+ fontFamily: "inherit",
21
+ ...initialOptions.chart
22
+ },
23
+ theme: { ...initialOptions.theme },
24
+ tooltip: {
25
+ theme: "dark",
26
+ ...initialOptions.tooltip
27
+ },
28
+ ...initialOptions
29
+ };
30
+ chartInstance = new ApexCharts(chartContainer, mergedOptions);
31
+ chartInstance.render();
32
+ });
33
+ onDestroy(() => {
34
+ if (chartInstance) {
35
+ chartInstance.destroy();
36
+ }
37
+ });
38
+ // React to options/series changes
39
+ $effect(() => {
40
+ if (chartInstance) {
41
+ if (series) {
42
+ chartInstance.updateSeries(series);
43
+ }
44
+ }
37
45
  });
38
- // We must use "as any" or "as typeof SvelteComponent" because svelte-chartjs types
39
- // are sometimes tricky with svelte:component in Svelte 5.
40
- const ChartComponent = $derived(components[type]);
41
46
  </script>
42
47
 
43
- <div class={twMerge(styles, className)} {...restProps}>
44
- {#if ChartComponent}
45
- <ChartComponent data={data as any} options={mergedOptions as any} />
46
- {/if}
47
- </div>
48
+ <div class={twMerge('w-full', className)} bind:this={chartContainer}></div>
@@ -1,20 +1,16 @@
1
- import type { HTMLAttributes } from 'svelte/elements';
2
- import type { ChartData, ChartOptions, ChartType } from 'chart.js';
3
- export interface ChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'type'> {
1
+ import type { ApexOptions } from 'apexcharts';
2
+ import type { ClassNameValue } from 'tailwind-merge';
3
+ export interface ChartProps {
4
4
  /**
5
- * Type of the chart.
5
+ * Configuration options for ApexCharts.
6
6
  */
7
- type: ChartType;
7
+ options: ApexOptions;
8
8
  /**
9
- * The data object for the chart.
9
+ * Series data. If provided, overrides options.series.
10
10
  */
11
- data: ChartData<ChartType, unknown, unknown>;
11
+ series?: ApexOptions['series'];
12
12
  /**
13
- * Configuration options for the chart.
13
+ * Tailwind class to apply to the chart container wrapper.
14
14
  */
15
- options?: ChartOptions<ChartType>;
16
- /**
17
- * Additional CSS classes.
18
- */
19
- class?: string;
15
+ class?: ClassNameValue;
20
16
  }
@@ -1,4 +1,2 @@
1
- import Chart from './Chart.svelte';
2
- export { Chart };
1
+ export { default as Chart } from './Chart.svelte';
3
2
  export * from './chart.types.js';
4
- export * from './chart.variants.js';
@@ -1,4 +1,2 @@
1
- import Chart from './Chart.svelte';
2
- export { Chart };
1
+ export { default as Chart } from './Chart.svelte';
3
2
  export * from './chart.types.js';
4
- export * from './chart.variants.js';
@@ -0,0 +1,212 @@
1
+ <script lang="ts">import { untrack } from "svelte";
2
+ import { twMerge } from "tailwind-merge";
3
+ import Icon from "../Icon/Icon.svelte";
4
+ import Badge from "../Badge/Badge.svelte";
5
+ import Kbd from "../Kbd/Kbd.svelte";
6
+ import DropdownMenu from "../DropdownMenu/DropdownMenu.svelte";
7
+ import { navigationMenuVariants } from "./navigation-menu.variants.js";
8
+ let { items = [], variant = "default", orientation = "horizontal", accordion = false, class: className, ui, children } = $props();
9
+ let styles = $derived(navigationMenuVariants({
10
+ variant,
11
+ orientation
12
+ }));
13
+ let normalizedItems = $derived.by(() => {
14
+ if (!items || items.length === 0) return [];
15
+ if (Array.isArray(items[0])) {
16
+ return items;
17
+ }
18
+ return [items];
19
+ });
20
+ // --- Accordion State (for vertical) ---
21
+ function getInitialOpenGroups() {
22
+ const opened = [];
23
+ for (const group of normalizedItems) {
24
+ for (const item of group) {
25
+ if (item.defaultOpen && item.label) opened.push(item.label);
26
+ }
27
+ }
28
+ return opened;
29
+ }
30
+ let openGroupLabel = $state(untrack(() => accordion ? getInitialOpenGroups()[0] ?? null : null));
31
+ let openGroups = $state(untrack(() => !accordion ? getInitialOpenGroups() : []));
32
+ function handleGroupToggle(item) {
33
+ if (!item.label) return;
34
+ if (accordion) {
35
+ openGroupLabel = openGroupLabel === item.label ? null : item.label;
36
+ } else {
37
+ if (openGroups.includes(item.label)) {
38
+ openGroups = openGroups.filter((l) => l !== item.label);
39
+ } else {
40
+ openGroups = [...openGroups, item.label];
41
+ }
42
+ }
43
+ }
44
+ function isGroupOpen(item) {
45
+ if (accordion) return openGroupLabel === item.label;
46
+ if (!item.label) return false;
47
+ return openGroups.includes(item.label);
48
+ }
49
+ // --- Auto Active State ---
50
+ let currentPath = $state("");
51
+ $effect(() => {
52
+ if (typeof window !== "undefined") {
53
+ currentPath = window.location.pathname;
54
+ const onPopState = () => {
55
+ currentPath = window.location.pathname;
56
+ };
57
+ window.addEventListener("popstate", onPopState);
58
+ return () => window.removeEventListener("popstate", onPopState);
59
+ }
60
+ });
61
+ function isActive(item) {
62
+ if (item.active) return true;
63
+ if (item.href) {
64
+ return currentPath === item.href || currentPath.startsWith(item.href + "/");
65
+ }
66
+ return false;
67
+ }
68
+ // Helper for keyboard shortcuts
69
+ function getKbds(item) {
70
+ return item.kbds || item.shortcut || [];
71
+ }
72
+ </script>
73
+
74
+ {#snippet renderItem(item: NavigationMenuItemType)}
75
+ <li class={orientation === 'vertical' ? 'w-full' : ''}>
76
+ {#if item.type === 'separator'}
77
+ <!-- SEPARATOR -->
78
+ <div class={orientation === 'horizontal' ? 'w-px h-6 bg-outline-variant mx-1' : 'h-px w-full bg-outline-variant my-1'}></div>
79
+ {:else if (item.items && item.items.length > 0) || (item.children && item.children.length > 0)}
80
+ {@const subItems = item.items || item.children || []}
81
+
82
+ {#if orientation === 'horizontal'}
83
+ <!-- HORIZONTAL: Dropdown -->
84
+ <DropdownMenu
85
+ items={subItems as any}
86
+ side="bottom"
87
+ align="start"
88
+ sideOffset={4}
89
+ >
90
+ {#snippet children({ props, open })}
91
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
92
+ <button
93
+ type="button"
94
+ {...props}
95
+ class={twMerge(styles.item({ active: isActive(item) }), item.class)}
96
+ disabled={item.disabled}
97
+ >
98
+ {#if item.icon}
99
+ <Icon name={item.icon} class={styles.icon({ active: isActive(item) })} />
100
+ {/if}
101
+ <span>{item.label}</span>
102
+ {#if item.badge !== undefined}
103
+ <Badge label={String(item.badge)} color={item.badgeColor || 'primary'} size="sm" class="ml-1" />
104
+ {/if}
105
+ <span class="ml-auto flex items-center">
106
+ <Icon
107
+ name="lucide:chevron-down"
108
+ class={twMerge(styles.chevron(), open ? 'rotate-180' : '')}
109
+ />
110
+ </span>
111
+ </button>
112
+ {/snippet}
113
+ </DropdownMenu>
114
+ {:else}
115
+ <!-- VERTICAL: Accordion -->
116
+ <button
117
+ type="button"
118
+ class={twMerge(
119
+ styles.accordionTrigger(),
120
+ isGroupOpen(item) ? styles.item({ active: true }) : '',
121
+ item.class
122
+ )}
123
+ disabled={item.disabled}
124
+ onclick={() => handleGroupToggle(item)}
125
+ >
126
+ <div class="flex flex-1 items-center truncate">
127
+ {#if item.icon}
128
+ <Icon name={item.icon} class={styles.icon({ active: isActive(item) })} />
129
+ {/if}
130
+ <span class="truncate">{item.label}</span>
131
+ </div>
132
+ <div class="flex items-center gap-2">
133
+ {#if item.badge !== undefined || getKbds(item).length > 0}
134
+ <span class="flex items-center gap-2">
135
+ {#if item.badge !== undefined}
136
+ <Badge label={String(item.badge)} color={item.badgeColor || 'primary'} size="sm" />
137
+ {/if}
138
+ {#if getKbds(item).length > 0}
139
+ <span class="flex items-center gap-0.5">
140
+ {#each getKbds(item) as key}
141
+ <Kbd value={typeof key === 'string' ? key : key.value} size="sm" />
142
+ {/each}
143
+ </span>
144
+ {/if}
145
+ </span>
146
+ {/if}
147
+ <Icon
148
+ name="lucide:chevron-down"
149
+ class={twMerge(styles.chevron(), isGroupOpen(item) ? 'rotate-180' : '')}
150
+ />
151
+ </div>
152
+ </button>
153
+ {#if isGroupOpen(item)}
154
+ <ul class={styles.accordionGroupContent()}>
155
+ {#each subItems as subItem}
156
+ {@render renderItem(subItem)}
157
+ {/each}
158
+ </ul>
159
+ {/if}
160
+ {/if}
161
+ {:else}
162
+ <!-- STANDARD LINK -->
163
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
164
+ <svelte:element
165
+ this={item.href ? 'a' : 'button'}
166
+ href={item.href}
167
+ target={item.target}
168
+ type={!item.href ? 'button' : undefined}
169
+ class={twMerge(styles.item({ active: isActive(item) }), item.class)}
170
+ disabled={item.disabled}
171
+ onclick={item.onClick}
172
+ >
173
+ <div class="flex flex-1 items-center truncate gap-1.5">
174
+ {#if item.icon}
175
+ <Icon name={item.icon} class={styles.icon({ active: isActive(item) })} />
176
+ {/if}
177
+ {#if item.label}
178
+ <span class="truncate">{item.label}</span>
179
+ {/if}
180
+ </div>
181
+ {#if item.badge !== undefined || getKbds(item).length > 0}
182
+ <span class="ml-auto flex items-center gap-2">
183
+ {#if item.badge !== undefined}
184
+ <Badge label={String(item.badge)} color={item.badgeColor || 'primary'} size="sm" />
185
+ {/if}
186
+ {#if getKbds(item).length > 0}
187
+ <span class="flex items-center gap-0.5">
188
+ {#each getKbds(item) as key}
189
+ <Kbd value={typeof key === 'string' ? key : key.value} size="sm" />
190
+ {/each}
191
+ </span>
192
+ {/if}
193
+ </span>
194
+ {/if}
195
+ </svelte:element>
196
+ {/if}
197
+ </li>
198
+ {/snippet}
199
+
200
+ <nav class={twMerge(styles.base(), className)}>
201
+ {#if children}
202
+ {@render children()}
203
+ {:else}
204
+ {#each normalizedItems as group}
205
+ <ul class={styles.group()}>
206
+ {#each group as item}
207
+ {@render renderItem(item)}
208
+ {/each}
209
+ </ul>
210
+ {/each}
211
+ {/if}
212
+ </nav>
@@ -0,0 +1,4 @@
1
+ import type { NavigationMenuProps } from './navigation-menu.types.js';
2
+ declare const NavigationMenu: import("svelte").Component<NavigationMenuProps, {}, "">;
3
+ type NavigationMenu = ReturnType<typeof NavigationMenu>;
4
+ export default NavigationMenu;
@@ -0,0 +1,3 @@
1
+ export { default as NavigationMenu } from './NavigationMenu.svelte';
2
+ export * from './navigation-menu.types.js';
3
+ export * from './navigation-menu.variants.js';
@@ -0,0 +1,3 @@
1
+ export { default as NavigationMenu } from './NavigationMenu.svelte';
2
+ export * from './navigation-menu.types.js';
3
+ export * from './navigation-menu.variants.js';
@@ -0,0 +1,104 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ClassNameValue } from 'tailwind-merge';
3
+ import type { BadgeProps } from '../Badge/badge.types.js';
4
+ import type { KbdProps } from '../Kbd/kbd.types.js';
5
+ import type { NavigationMenuVariantProps, NavigationMenuSlots } from './navigation-menu.variants.js';
6
+ export type NavigationMenuItemType = {
7
+ /**
8
+ * Item type for compatibility with DropdownMenu.
9
+ */
10
+ type?: 'item' | 'label' | 'separator' | 'sub';
11
+ /**
12
+ * The visible text displayed in the menu item.
13
+ */
14
+ label?: string;
15
+ /**
16
+ * Optional icon to display before the label.
17
+ */
18
+ icon?: string;
19
+ /**
20
+ * Optional badge text/number to display after the label.
21
+ */
22
+ badge?: string | number;
23
+ /**
24
+ * Badge color (e.g. 'primary', 'error', 'success').
25
+ */
26
+ badgeColor?: BadgeProps['color'];
27
+ /**
28
+ * URL to navigate to when clicked.
29
+ */
30
+ href?: string;
31
+ /**
32
+ * Target attribute for the link (e.g., '_blank').
33
+ */
34
+ target?: string;
35
+ /**
36
+ * Force the active state styling.
37
+ */
38
+ active?: boolean;
39
+ /**
40
+ * Disable interaction for this item.
41
+ */
42
+ disabled?: boolean;
43
+ /**
44
+ * Whether this group should be open by default (when orientation="vertical").
45
+ */
46
+ defaultOpen?: boolean;
47
+ /**
48
+ * Keyboard shortcuts to display.
49
+ */
50
+ shortcut?: string[];
51
+ kbds?: (string | Pick<KbdProps, 'value' | 'size' | 'variant' | 'color'>)[];
52
+ /**
53
+ * Sub-items for this menu item (Svelora standard).
54
+ * If provided, the item renders as a dropdown trigger or accordion.
55
+ */
56
+ items?: NavigationMenuItemType[];
57
+ /**
58
+ * Alias for sub-items (Nuxt UI style compatibility).
59
+ */
60
+ children?: NavigationMenuItemType[];
61
+ /**
62
+ * Custom click handler for standard items.
63
+ */
64
+ onClick?: () => void;
65
+ /**
66
+ * Custom class for this specific item.
67
+ */
68
+ class?: ClassNameValue;
69
+ };
70
+ export type NavigationMenuProps = {
71
+ /**
72
+ * Array of navigation items. Supports 1D or 2D arrays for grouping.
73
+ */
74
+ items?: NavigationMenuItemType[] | NavigationMenuItemType[][];
75
+ /**
76
+ * Visual variant of the navigation menu.
77
+ * @default 'default'
78
+ */
79
+ variant?: NonNullable<NavigationMenuVariantProps['variant']>;
80
+ /**
81
+ * Orientation of the navigation menu.
82
+ * @default 'horizontal'
83
+ */
84
+ orientation?: NonNullable<NavigationMenuVariantProps['orientation']>;
85
+ /**
86
+ * Enable accordion mode (only affects orientation="vertical").
87
+ * If true, only one group can be open at a time.
88
+ * @default false
89
+ */
90
+ accordion?: boolean;
91
+ /**
92
+ * Additional CSS classes.
93
+ */
94
+ class?: ClassNameValue;
95
+ /**
96
+ * Override specific slot classes.
97
+ */
98
+ ui?: Partial<Record<NavigationMenuSlots, ClassNameValue>>;
99
+ /**
100
+ * Custom snippet for rendering the entire list.
101
+ * Overrides `items` if provided.
102
+ */
103
+ children?: Snippet;
104
+ };
@@ -0,0 +1,121 @@
1
+ import { type VariantProps } from 'tailwind-variants';
2
+ export declare const navigationMenuVariants: import("tailwind-variants").TVReturnType<{
3
+ orientation: {
4
+ horizontal: {
5
+ base: string;
6
+ group: string;
7
+ item: string;
8
+ };
9
+ vertical: {
10
+ base: string;
11
+ group: string;
12
+ item: string;
13
+ };
14
+ };
15
+ variant: {
16
+ default: {
17
+ item: string[];
18
+ icon: string;
19
+ };
20
+ ghost: {
21
+ item: string[];
22
+ icon: string;
23
+ };
24
+ };
25
+ active: {
26
+ true: {
27
+ item: string;
28
+ icon: string;
29
+ accordionTrigger: string;
30
+ };
31
+ false: {};
32
+ };
33
+ }, {
34
+ base: string;
35
+ group: string;
36
+ item: string[];
37
+ icon: string;
38
+ chevron: string;
39
+ accordionGroupContent: string;
40
+ accordionTrigger: string[];
41
+ }, undefined, {
42
+ orientation: {
43
+ horizontal: {
44
+ base: string;
45
+ group: string;
46
+ item: string;
47
+ };
48
+ vertical: {
49
+ base: string;
50
+ group: string;
51
+ item: string;
52
+ };
53
+ };
54
+ variant: {
55
+ default: {
56
+ item: string[];
57
+ icon: string;
58
+ };
59
+ ghost: {
60
+ item: string[];
61
+ icon: string;
62
+ };
63
+ };
64
+ active: {
65
+ true: {
66
+ item: string;
67
+ icon: string;
68
+ accordionTrigger: string;
69
+ };
70
+ false: {};
71
+ };
72
+ }, {
73
+ base: string;
74
+ group: string;
75
+ item: string[];
76
+ icon: string;
77
+ chevron: string;
78
+ accordionGroupContent: string;
79
+ accordionTrigger: string[];
80
+ }, import("tailwind-variants").TVReturnType<{
81
+ orientation: {
82
+ horizontal: {
83
+ base: string;
84
+ group: string;
85
+ item: string;
86
+ };
87
+ vertical: {
88
+ base: string;
89
+ group: string;
90
+ item: string;
91
+ };
92
+ };
93
+ variant: {
94
+ default: {
95
+ item: string[];
96
+ icon: string;
97
+ };
98
+ ghost: {
99
+ item: string[];
100
+ icon: string;
101
+ };
102
+ };
103
+ active: {
104
+ true: {
105
+ item: string;
106
+ icon: string;
107
+ accordionTrigger: string;
108
+ };
109
+ false: {};
110
+ };
111
+ }, {
112
+ base: string;
113
+ group: string;
114
+ item: string[];
115
+ icon: string;
116
+ chevron: string;
117
+ accordionGroupContent: string;
118
+ accordionTrigger: string[];
119
+ }, undefined, unknown, unknown, undefined>>;
120
+ export type NavigationMenuVariantProps = VariantProps<typeof navigationMenuVariants>;
121
+ export type NavigationMenuSlots = keyof ReturnType<typeof navigationMenuVariants>;
@@ -0,0 +1,64 @@
1
+ import { tv } from 'tailwind-variants';
2
+ export const navigationMenuVariants = tv({
3
+ slots: {
4
+ base: 'flex',
5
+ group: 'flex',
6
+ item: [
7
+ 'group relative flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
8
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
9
+ 'disabled:pointer-events-none disabled:opacity-50'
10
+ ],
11
+ icon: 'text-lg shrink-0 transition-colors',
12
+ chevron: 'text-base opacity-50 shrink-0 transition-transform group-data-[state=open]:rotate-180',
13
+ accordionGroupContent: 'flex flex-col gap-1 pl-4 mt-1',
14
+ accordionTrigger: [
15
+ 'flex items-center justify-between w-full px-3 py-2 text-sm font-medium rounded-md',
16
+ 'transition-colors duration-200 outline-none focus-visible:ring-2 focus-visible:ring-outline',
17
+ 'text-on-surface hover:bg-surface-container-highest',
18
+ 'disabled:opacity-50 disabled:cursor-not-allowed'
19
+ ]
20
+ },
21
+ variants: {
22
+ orientation: {
23
+ horizontal: {
24
+ base: 'flex-row items-center justify-between w-full gap-4',
25
+ group: 'flex-row items-center gap-1',
26
+ item: ''
27
+ },
28
+ vertical: {
29
+ base: 'flex-col w-full gap-4',
30
+ group: 'flex-col gap-1 w-full',
31
+ item: 'w-full justify-start'
32
+ }
33
+ },
34
+ variant: {
35
+ default: {
36
+ item: [
37
+ 'text-on-surface-variant hover:bg-surface-container hover:text-on-surface',
38
+ 'data-[state=open]:bg-surface-container data-[state=open]:text-on-surface'
39
+ ],
40
+ icon: 'text-on-surface-variant group-hover:text-on-surface group-data-[state=open]:text-on-surface'
41
+ },
42
+ ghost: {
43
+ item: [
44
+ 'text-on-surface hover:bg-primary/10 hover:text-primary',
45
+ 'data-[state=open]:bg-primary/10 data-[state=open]:text-primary'
46
+ ],
47
+ icon: 'text-on-surface-variant group-hover:text-primary group-data-[state=open]:text-primary'
48
+ }
49
+ },
50
+ active: {
51
+ true: {
52
+ item: 'bg-primary/10 text-primary font-semibold',
53
+ icon: 'text-primary',
54
+ accordionTrigger: 'bg-primary-container text-on-primary-container'
55
+ },
56
+ false: {}
57
+ }
58
+ },
59
+ defaultVariants: {
60
+ variant: 'default',
61
+ active: false,
62
+ orientation: 'horizontal'
63
+ }
64
+ });