@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.
- package/package.json +1 -1
- package/src/app.css +244 -4
- package/src/components/AdminApp.svelte +9 -2
- package/src/components/AnimatedCounter.svelte +46 -0
- package/src/components/AutoTable.svelte +59 -7
- package/src/components/Breadcrumbs.svelte +3 -1
- package/src/components/DraggableHeader.svelte +112 -0
- package/src/components/InlineEdit.svelte +88 -0
- package/src/components/KeyboardShortcuts.svelte +61 -0
- package/src/components/Layout.svelte +11 -5
- package/src/components/Sidebar.svelte +28 -11
- package/src/components/SidebarItem.svelte +129 -0
- package/src/components/VirtualTable.svelte +76 -0
- package/src/components/charts/BarChart.svelte +69 -0
- package/src/components/charts/LineChart.svelte +115 -0
- package/src/components/charts/PieChart.svelte +95 -0
- package/src/components/charts/index.ts +3 -0
- package/src/components/stories/BarChart.stories.ts +43 -0
- package/src/components/stories/LineChart.stories.ts +48 -0
- package/src/components/stories/PieChart.stories.ts +41 -0
- package/src/index.ts +8 -0
|
@@ -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,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';
|