@svadmin/ui 0.12.0 → 0.14.0

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.
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ interface DataPoint {
3
+ label: string;
4
+ value: number;
5
+ }
6
+
7
+ interface Props {
8
+ data: DataPoint[];
9
+ height?: number;
10
+ color?: string;
11
+ /** Show dots at data points */
12
+ showDots?: boolean;
13
+ /** Fill area under the line */
14
+ fill?: boolean;
15
+ class?: string;
16
+ }
17
+
18
+ let { data, height = 200, color = 'var(--primary)', showDots = true, fill = true, class: className = '' }: Props = $props();
19
+
20
+ const padding = { top: 20, right: 20, bottom: 30, left: 40 };
21
+ const chartWidth = $derived(Math.max(data.length * 60, 300));
22
+ const chartHeight = $derived(height - padding.top - padding.bottom);
23
+
24
+ const maxValue = $derived(Math.max(...data.map(d => d.value), 1));
25
+ const minValue = $derived(Math.min(...data.map(d => d.value), 0));
26
+ const range = $derived(maxValue - minValue || 1);
27
+
28
+ function getX(i: number): number {
29
+ if (data.length <= 1) return padding.left + chartWidth / 2;
30
+ return padding.left + (i / (data.length - 1)) * (chartWidth - padding.left - padding.right);
31
+ }
32
+
33
+ function getY(value: number): number {
34
+ return padding.top + chartHeight - ((value - minValue) / range) * chartHeight;
35
+ }
36
+
37
+ const linePath = $derived(
38
+ data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${getX(i)} ${getY(d.value)}`).join(' ')
39
+ );
40
+
41
+ const areaPath = $derived(
42
+ linePath + ` L ${getX(data.length - 1)} ${padding.top + chartHeight} L ${getX(0)} ${padding.top + chartHeight} Z`
43
+ );
44
+ </script>
45
+
46
+ <div class="w-full overflow-x-auto {className}">
47
+ <svg viewBox="0 0 {chartWidth} {height}" class="w-full" style="min-width: {Math.min(chartWidth, 300)}px;">
48
+ <!-- Grid lines -->
49
+ {#each [0, 0.25, 0.5, 0.75, 1] as ratio}
50
+ {@const y = padding.top + chartHeight * (1 - ratio)}
51
+ <line
52
+ x1={padding.left}
53
+ y1={y}
54
+ x2={chartWidth - padding.right}
55
+ y2={y}
56
+ stroke="var(--border)"
57
+ stroke-width="1"
58
+ stroke-dasharray={ratio === 0 ? 'none' : '4 4'}
59
+ opacity="0.5"
60
+ />
61
+ <text x={padding.left - 8} y={y + 4} text-anchor="end" fill="var(--muted-foreground)" font-size="10">
62
+ {Math.round(minValue + range * ratio)}
63
+ </text>
64
+ {/each}
65
+
66
+ <!-- Area fill -->
67
+ {#if fill && data.length > 1}
68
+ <path
69
+ d={areaPath}
70
+ fill={color}
71
+ opacity="0.08"
72
+ />
73
+ {/if}
74
+
75
+ <!-- Line -->
76
+ {#if data.length > 1}
77
+ <path
78
+ d={linePath}
79
+ fill="none"
80
+ stroke={color}
81
+ stroke-width="2"
82
+ stroke-linecap="round"
83
+ stroke-linejoin="round"
84
+ >
85
+ <animate attributeName="stroke-dashoffset" from="2000" to="0" dur="1s" fill="freeze" />
86
+ </path>
87
+ {/if}
88
+
89
+ <!-- Dots -->
90
+ {#if showDots}
91
+ {#each data as point, i}
92
+ <circle
93
+ cx={getX(i)}
94
+ cy={getY(point.value)}
95
+ r="4"
96
+ fill="var(--background)"
97
+ stroke={color}
98
+ stroke-width="2"
99
+ class="transition-all duration-200 hover:r-[6]"
100
+ />
101
+ {/each}
102
+ {/if}
103
+
104
+ <!-- X-axis labels -->
105
+ {#each data as point, i}
106
+ <text
107
+ x={getX(i)}
108
+ y={height - 4}
109
+ text-anchor="middle"
110
+ fill="var(--muted-foreground)"
111
+ font-size="10"
112
+ >{point.label}</text>
113
+ {/each}
114
+ </svg>
115
+ </div>
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ interface Slice {
3
+ label: string;
4
+ value: number;
5
+ color: string;
6
+ }
7
+
8
+ interface Props {
9
+ data: Slice[];
10
+ size?: number;
11
+ /** Donut hole ratio (0 = pie, 0.6 = donut) */
12
+ donut?: number;
13
+ class?: string;
14
+ }
15
+
16
+ let { data, size = 200, donut = 0.6, class: className = '' }: Props = $props();
17
+
18
+ const total = $derived(data.reduce((sum, d) => sum + d.value, 0) || 1);
19
+ const center = $derived(size / 2);
20
+ const radius = $derived(size / 2 - 8);
21
+ const innerRadius = $derived(radius * donut);
22
+
23
+ function getArcPath(startAngle: number, endAngle: number, outer: number, inner: number): string {
24
+ const startOuter = polarToCartesian(center, center, outer, endAngle);
25
+ const endOuter = polarToCartesian(center, center, outer, startAngle);
26
+ const startInner = polarToCartesian(center, center, inner, startAngle);
27
+ const endInner = polarToCartesian(center, center, inner, endAngle);
28
+ const largeArc = endAngle - startAngle > 180 ? 1 : 0;
29
+
30
+ return [
31
+ `M ${startOuter.x} ${startOuter.y}`,
32
+ `A ${outer} ${outer} 0 ${largeArc} 0 ${endOuter.x} ${endOuter.y}`,
33
+ inner > 0 ? `L ${startInner.x} ${startInner.y}` : `L ${center} ${center}`,
34
+ inner > 0 ? `A ${inner} ${inner} 0 ${largeArc} 1 ${endInner.x} ${endInner.y}` : '',
35
+ 'Z',
36
+ ].join(' ');
37
+ }
38
+
39
+ function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
40
+ const rad = ((angleDeg - 90) * Math.PI) / 180;
41
+ return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
42
+ }
43
+
44
+ const slices = $derived.by(() => {
45
+ let currentAngle = 0;
46
+ return data.map(d => {
47
+ const angle = (d.value / total) * 360;
48
+ const slice = {
49
+ ...d,
50
+ startAngle: currentAngle,
51
+ endAngle: currentAngle + angle,
52
+ percentage: ((d.value / total) * 100).toFixed(1),
53
+ };
54
+ currentAngle += angle;
55
+ return slice;
56
+ });
57
+ });
58
+ </script>
59
+
60
+ <div class="inline-flex flex-col items-center gap-3 {className}">
61
+ <svg viewBox="0 0 {size} {size}" width={size} height={size}>
62
+ {#each slices as slice, i}
63
+ <path
64
+ d={getArcPath(slice.startAngle, slice.endAngle, radius, innerRadius)}
65
+ fill={slice.color}
66
+ class="transition-all duration-200 hover:opacity-80"
67
+ stroke="var(--background)"
68
+ stroke-width="2"
69
+ >
70
+ <animate attributeName="opacity" from="0" to="1" dur="0.4s" begin="{i * 0.1}s" fill="freeze" />
71
+ </path>
72
+ {/each}
73
+
74
+ <!-- Center label (donut only) -->
75
+ {#if donut > 0}
76
+ <text x={center} y={center - 6} text-anchor="middle" fill="var(--foreground)" font-size="20" font-weight="600">
77
+ {total.toLocaleString()}
78
+ </text>
79
+ <text x={center} y={center + 12} text-anchor="middle" fill="var(--muted-foreground)" font-size="11">
80
+ Total
81
+ </text>
82
+ {/if}
83
+ </svg>
84
+
85
+ <!-- Legend -->
86
+ <div class="flex flex-wrap justify-center gap-x-4 gap-y-1">
87
+ {#each slices as slice}
88
+ <div class="flex items-center gap-1.5 text-xs">
89
+ <span class="h-2.5 w-2.5 rounded-full" style="background-color: {slice.color};"></span>
90
+ <span class="text-muted-foreground">{slice.label}</span>
91
+ <span class="font-medium text-foreground">{slice.percentage}%</span>
92
+ </div>
93
+ {/each}
94
+ </div>
95
+ </div>
@@ -0,0 +1,3 @@
1
+ export { default as BarChart } from './BarChart.svelte';
2
+ export { default as LineChart } from './LineChart.svelte';
3
+ export { default as PieChart } from './PieChart.svelte';
@@ -0,0 +1,43 @@
1
+ import type { Meta, StoryObj } from '@storybook/svelte';
2
+ import BarChart from '../charts/BarChart.svelte';
3
+
4
+ const meta = {
5
+ title: 'Charts/BarChart',
6
+ component: BarChart,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ height: { control: { type: 'number', min: 100, max: 500 } },
10
+ showValues: { control: 'boolean' },
11
+ },
12
+ } satisfies Meta<BarChart>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ data: [
20
+ { label: 'Jan', value: 120 },
21
+ { label: 'Feb', value: 180 },
22
+ { label: 'Mar', value: 95 },
23
+ { label: 'Apr', value: 210 },
24
+ { label: 'May', value: 165 },
25
+ { label: 'Jun', value: 245 },
26
+ ],
27
+ height: 200,
28
+ showValues: true,
29
+ },
30
+ };
31
+
32
+ export const CustomColors: Story = {
33
+ args: {
34
+ data: [
35
+ { label: 'Users', value: 1200, color: '#6366f1' },
36
+ { label: 'Orders', value: 850, color: '#f59e0b' },
37
+ { label: 'Revenue', value: 2100, color: '#10b981' },
38
+ { label: 'Returns', value: 120, color: '#ef4444' },
39
+ ],
40
+ height: 250,
41
+ showValues: true,
42
+ },
43
+ };
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from '@storybook/svelte';
2
+ import LineChart from '../charts/LineChart.svelte';
3
+
4
+ const meta = {
5
+ title: 'Charts/LineChart',
6
+ component: LineChart,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ height: { control: { type: 'number', min: 100, max: 500 } },
10
+ showDots: { control: 'boolean' },
11
+ fill: { control: 'boolean' },
12
+ },
13
+ } satisfies Meta<LineChart>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ data: [
21
+ { label: 'Mon', value: 120 },
22
+ { label: 'Tue', value: 180 },
23
+ { label: 'Wed', value: 150 },
24
+ { label: 'Thu', value: 210 },
25
+ { label: 'Fri', value: 190 },
26
+ { label: 'Sat', value: 250 },
27
+ { label: 'Sun', value: 220 },
28
+ ],
29
+ height: 200,
30
+ showDots: true,
31
+ fill: true,
32
+ },
33
+ };
34
+
35
+ export const NoFill: Story = {
36
+ args: {
37
+ data: [
38
+ { label: 'Q1', value: 3200 },
39
+ { label: 'Q2', value: 4100 },
40
+ { label: 'Q3', value: 3800 },
41
+ { label: 'Q4', value: 5200 },
42
+ ],
43
+ height: 250,
44
+ showDots: true,
45
+ fill: false,
46
+ color: '#10b981',
47
+ },
48
+ };
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from '@storybook/svelte';
2
+ import PieChart from '../charts/PieChart.svelte';
3
+
4
+ const meta = {
5
+ title: 'Charts/PieChart',
6
+ component: PieChart,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ size: { control: { type: 'number', min: 100, max: 400 } },
10
+ donut: { control: { type: 'number', min: 0, max: 0.8, step: 0.1 } },
11
+ },
12
+ } satisfies Meta<PieChart>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Donut: Story = {
18
+ args: {
19
+ data: [
20
+ { label: 'Desktop', value: 580, color: '#6366f1' },
21
+ { label: 'Mobile', value: 320, color: '#f59e0b' },
22
+ { label: 'Tablet', value: 100, color: '#10b981' },
23
+ ],
24
+ size: 200,
25
+ donut: 0.6,
26
+ },
27
+ };
28
+
29
+ export const Pie: Story = {
30
+ args: {
31
+ data: [
32
+ { label: 'Chrome', value: 64, color: '#4285f4' },
33
+ { label: 'Safari', value: 19, color: '#5ac8fa' },
34
+ { label: 'Firefox', value: 10, color: '#ff7139' },
35
+ { label: 'Edge', value: 4, color: '#0078d7' },
36
+ { label: 'Other', value: 3, color: '#a0a0a0' },
37
+ ],
38
+ size: 220,
39
+ donut: 0,
40
+ },
41
+ };
package/src/index.ts CHANGED
@@ -54,6 +54,14 @@ export { default as CopilotPanel } from "./components/CopilotPanel.svelte";
54
54
  export { default as InsightCard } from "./components/InsightCard.svelte";
55
55
  export { default as AnomalyBadge } from "./components/AnomalyBadge.svelte";
56
56
  export { default as VoiceInput } from "./components/VoiceInput.svelte";
57
+ export { default as AnimatedCounter } from "./components/AnimatedCounter.svelte";
58
+ export { default as KeyboardShortcuts } from "./components/KeyboardShortcuts.svelte";
59
+ export { default as InlineEdit } from "./components/InlineEdit.svelte";
60
+ export { default as VirtualTable } from "./components/VirtualTable.svelte";
61
+ export { default as DraggableHeader } from "./components/DraggableHeader.svelte";
62
+
63
+ // Dashboard Charts (zero-dependency SVG)
64
+ export { BarChart, LineChart, PieChart } from './components/charts/index.js';
57
65
  // Field display components
58
66
  export { default as NumberField } from './components/fields/NumberField.svelte';
59
67
  export { default as DateField } from './components/fields/DateField.svelte';