@squadbase/vantage-template 0.0.1

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 (99) hide show
  1. package/README.md +112 -0
  2. package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +162 -0
  3. package/base-template/.squadbase/skills/vantage-app/SKILL.md +120 -0
  4. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +137 -0
  5. package/base-template/AGENTS.md +339 -0
  6. package/base-template/_404.tsx +18 -0
  7. package/base-template/_error.tsx +14 -0
  8. package/base-template/_gitignore +5 -0
  9. package/base-template/_layout.tsx +60 -0
  10. package/base-template/components/user-card.tsx +132 -0
  11. package/base-template/index.tsx +59 -0
  12. package/base-template/package.json +27 -0
  13. package/base-template/server/api/health.ts +12 -0
  14. package/base-template/squadbase.yml +5 -0
  15. package/base-template/styles.css +18 -0
  16. package/base-template/tsconfig.json +28 -0
  17. package/chart-presets/berry.css +16 -0
  18. package/chart-presets/blue.css +16 -0
  19. package/chart-presets/forest.css +16 -0
  20. package/chart-presets/mono.css +16 -0
  21. package/chart-presets/ocean.css +16 -0
  22. package/chart-presets/sunset.css +16 -0
  23. package/dist/index.js +1157 -0
  24. package/package.json +55 -0
  25. package/ui-templates/CLAUDE.md +200 -0
  26. package/ui-templates/blank/manifest.json +8 -0
  27. package/ui-templates/blank/pages/index.tsx +33 -0
  28. package/ui-templates/blank-ja/manifest.json +8 -0
  29. package/ui-templates/blank-ja/pages/index.tsx +33 -0
  30. package/ui-templates/chart-grid/lib/mock-data.ts +139 -0
  31. package/ui-templates/chart-grid/lib/types.ts +31 -0
  32. package/ui-templates/chart-grid/manifest.json +10 -0
  33. package/ui-templates/chart-grid/pages/index.tsx +331 -0
  34. package/ui-templates/chart-grid/preview-square.png +0 -0
  35. package/ui-templates/chart-grid/preview-wide.png +0 -0
  36. package/ui-templates/chart-grid-ja/lib/mock-data.ts +139 -0
  37. package/ui-templates/chart-grid-ja/lib/types.ts +31 -0
  38. package/ui-templates/chart-grid-ja/manifest.json +10 -0
  39. package/ui-templates/chart-grid-ja/pages/index.tsx +331 -0
  40. package/ui-templates/chart-grid-ja/preview-square.png +0 -0
  41. package/ui-templates/chart-grid-ja/preview-wide.png +0 -0
  42. package/ui-templates/funnel/components/chart-helpers.ts +11 -0
  43. package/ui-templates/funnel/components/stage-table.tsx +63 -0
  44. package/ui-templates/funnel/lib/mock-data.ts +22 -0
  45. package/ui-templates/funnel/lib/types.ts +11 -0
  46. package/ui-templates/funnel/manifest.json +12 -0
  47. package/ui-templates/funnel/pages/index.tsx +252 -0
  48. package/ui-templates/funnel/preview-square.png +0 -0
  49. package/ui-templates/funnel/preview-wide.png +0 -0
  50. package/ui-templates/funnel-ja/components/chart-helpers.ts +11 -0
  51. package/ui-templates/funnel-ja/components/stage-table.tsx +63 -0
  52. package/ui-templates/funnel-ja/lib/mock-data.ts +22 -0
  53. package/ui-templates/funnel-ja/lib/types.ts +11 -0
  54. package/ui-templates/funnel-ja/manifest.json +12 -0
  55. package/ui-templates/funnel-ja/pages/index.tsx +251 -0
  56. package/ui-templates/funnel-ja/preview-square.png +0 -0
  57. package/ui-templates/funnel-ja/preview-wide.png +0 -0
  58. package/ui-templates/kpi-chart-advanced/components/campaign-table.tsx +69 -0
  59. package/ui-templates/kpi-chart-advanced/lib/mock-data.ts +109 -0
  60. package/ui-templates/kpi-chart-advanced/lib/types.ts +38 -0
  61. package/ui-templates/kpi-chart-advanced/manifest.json +11 -0
  62. package/ui-templates/kpi-chart-advanced/pages/index.tsx +339 -0
  63. package/ui-templates/kpi-chart-advanced/preview-square.png +0 -0
  64. package/ui-templates/kpi-chart-advanced/preview-wide.png +0 -0
  65. package/ui-templates/kpi-chart-advanced-ja/components/campaign-table.tsx +69 -0
  66. package/ui-templates/kpi-chart-advanced-ja/lib/mock-data.ts +109 -0
  67. package/ui-templates/kpi-chart-advanced-ja/lib/types.ts +38 -0
  68. package/ui-templates/kpi-chart-advanced-ja/manifest.json +11 -0
  69. package/ui-templates/kpi-chart-advanced-ja/pages/index.tsx +333 -0
  70. package/ui-templates/kpi-chart-advanced-ja/preview-square.png +0 -0
  71. package/ui-templates/kpi-chart-advanced-ja/preview-wide.png +0 -0
  72. package/ui-templates/kpi-chart-simple/components/top-items-table.tsx +73 -0
  73. package/ui-templates/kpi-chart-simple/lib/types.ts +44 -0
  74. package/ui-templates/kpi-chart-simple/manifest.json +11 -0
  75. package/ui-templates/kpi-chart-simple/pages/index.tsx +200 -0
  76. package/ui-templates/kpi-chart-simple/preview-square.png +0 -0
  77. package/ui-templates/kpi-chart-simple/preview-wide.png +0 -0
  78. package/ui-templates/kpi-chart-simple/server/kpi-summary.ts +141 -0
  79. package/ui-templates/kpi-chart-simple-ja/components/top-items-table.tsx +73 -0
  80. package/ui-templates/kpi-chart-simple-ja/lib/types.ts +44 -0
  81. package/ui-templates/kpi-chart-simple-ja/manifest.json +11 -0
  82. package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +200 -0
  83. package/ui-templates/kpi-chart-simple-ja/preview-square.png +0 -0
  84. package/ui-templates/kpi-chart-simple-ja/preview-wide.png +0 -0
  85. package/ui-templates/kpi-chart-simple-ja/server/kpi-summary.ts +141 -0
  86. package/ui-templates/table-focus/components/detail-table.tsx +88 -0
  87. package/ui-templates/table-focus/lib/mock-data.ts +69 -0
  88. package/ui-templates/table-focus/lib/types.ts +24 -0
  89. package/ui-templates/table-focus/manifest.json +11 -0
  90. package/ui-templates/table-focus/pages/index.tsx +180 -0
  91. package/ui-templates/table-focus/preview-square.png +0 -0
  92. package/ui-templates/table-focus/preview-wide.png +0 -0
  93. package/ui-templates/table-focus-ja/components/detail-table.tsx +95 -0
  94. package/ui-templates/table-focus-ja/lib/mock-data.ts +69 -0
  95. package/ui-templates/table-focus-ja/lib/types.ts +24 -0
  96. package/ui-templates/table-focus-ja/manifest.json +11 -0
  97. package/ui-templates/table-focus-ja/pages/index.tsx +180 -0
  98. package/ui-templates/table-focus-ja/preview-square.png +0 -0
  99. package/ui-templates/table-focus-ja/preview-wide.png +0 -0
@@ -0,0 +1,69 @@
1
+ import { useMemo } from "react"
2
+
3
+ import { DataTablePreset, Placeholder, type ColumnDef } from "@squadbase/vantage/components"
4
+
5
+ import type { CampaignRow } from "./types"
6
+
7
+ function formatCurrency(n: number, opts: { short?: boolean } = {}): string {
8
+ const sign = n < 0 ? "-" : ""
9
+ const abs = Math.abs(n)
10
+ if (opts.short) {
11
+ if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(2)}M`
12
+ if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(1)}K`
13
+ return `${sign}$${abs.toLocaleString("en-US")}`
14
+ }
15
+ return `${sign}$${Math.round(abs).toLocaleString("en-US")}`
16
+ }
17
+
18
+ interface CampaignTableProps {
19
+ data: CampaignRow[]
20
+ }
21
+
22
+ export function CampaignTable({ data }: CampaignTableProps) {
23
+ const columns = useMemo<ColumnDef<CampaignRow>[]>(
24
+ () => [
25
+ {
26
+ accessorKey: "name",
27
+ header: "Column 1",
28
+ },
29
+ {
30
+ accessorKey: "channel",
31
+ header: "Column 2",
32
+ },
33
+ {
34
+ accessorKey: "spend",
35
+ header: "Column 3",
36
+ cell: ({ row }) => (
37
+ <div className="flex justify-end">
38
+ <Placeholder>
39
+ {formatCurrency(row.original.spend, { short: true })}
40
+ </Placeholder>
41
+ </div>
42
+ ),
43
+ },
44
+ {
45
+ accessorKey: "conversions",
46
+ header: "Column 4",
47
+ cell: ({ row }) => (
48
+ <div className="flex justify-end">
49
+ <Placeholder>
50
+ {row.original.conversions.toLocaleString("en-US")}
51
+ </Placeholder>
52
+ </div>
53
+ ),
54
+ },
55
+ {
56
+ accessorKey: "roi",
57
+ header: "Column 5",
58
+ cell: ({ row }) => (
59
+ <div className="flex justify-end">
60
+ <Placeholder>{`${row.original.roi.toFixed(1)}x`}</Placeholder>
61
+ </div>
62
+ ),
63
+ },
64
+ ],
65
+ [],
66
+ )
67
+
68
+ return <DataTablePreset columns={columns} data={data} enableSorting />
69
+ }
@@ -0,0 +1,109 @@
1
+ import type {
2
+ KpiItem,
3
+ ComparisonPoint,
4
+ BreakdownSlice,
5
+ TrendPoint,
6
+ CampaignRow,
7
+ } from "./types"
8
+
9
+ export const trendSeries: TrendPoint[] = [
10
+ { date: "2024-03-02", value: 25537 },
11
+ { date: "2024-03-03", value: 25443 },
12
+ { date: "2024-03-04", value: 31199 },
13
+ { date: "2024-03-05", value: 27676 },
14
+ { date: "2024-03-06", value: 31689 },
15
+ { date: "2024-03-07", value: 31329 },
16
+ { date: "2024-03-08", value: 29802 },
17
+ { date: "2024-03-09", value: 24254 },
18
+ { date: "2024-03-10", value: 23112 },
19
+ { date: "2024-03-11", value: 32913 },
20
+ { date: "2024-03-12", value: 29638 },
21
+ { date: "2024-03-13", value: 28547 },
22
+ { date: "2024-03-14", value: 33511 },
23
+ { date: "2024-03-15", value: 30842 },
24
+ { date: "2024-03-16", value: 26136 },
25
+ { date: "2024-03-17", value: 29594 },
26
+ { date: "2024-03-18", value: 34866 },
27
+ { date: "2024-03-19", value: 30377 },
28
+ { date: "2024-03-20", value: 31164 },
29
+ { date: "2024-03-21", value: 32209 },
30
+ { date: "2024-03-22", value: 36270 },
31
+ { date: "2024-03-23", value: 28720 },
32
+ { date: "2024-03-24", value: 26871 },
33
+ { date: "2024-03-25", value: 32817 },
34
+ { date: "2024-03-26", value: 36762 },
35
+ { date: "2024-03-27", value: 34853 },
36
+ { date: "2024-03-28", value: 34516 },
37
+ { date: "2024-03-29", value: 38290 },
38
+ { date: "2024-03-30", value: 28654 },
39
+ { date: "2024-03-31", value: 33368 },
40
+ ]
41
+
42
+ export const comparisonSeries: ComparisonPoint[] = [
43
+ { period: "2023-10", current: 308820, previous: 285196, growth: 8.28 },
44
+ { period: "2023-11", current: 360871, previous: 300245, growth: 20.19 },
45
+ { period: "2023-12", current: 369038, previous: 344274, growth: 7.19 },
46
+ { period: "2024-01", current: 402455, previous: 338646, growth: 18.84 },
47
+ { period: "2024-02", current: 443741, previous: 396096, growth: 12.03 },
48
+ { period: "2024-03", current: 445883, previous: 387812, growth: 14.97 },
49
+ ]
50
+
51
+ export const breakdownSlices: BreakdownSlice[] = [
52
+ { segment: "Segment 1", value: 412_000 },
53
+ { segment: "Segment 2", value: 318_000 },
54
+ { segment: "Segment 3", value: 287_000 },
55
+ { segment: "Segment 4", value: 164_000 },
56
+ { segment: "Segment 5", value: 132_000 },
57
+ ]
58
+
59
+ export const headerKpis: [KpiItem, KpiItem, KpiItem, KpiItem, KpiItem] = [
60
+ {
61
+ id: "revenue",
62
+ value: "$1.04M",
63
+ change: 14.2,
64
+ changeLabel: "",
65
+ positiveIsGood: true,
66
+ sparklineData: [29802, 23112, 29638, 33511, 26136, 34866, 31164, 36270, 26871, 36762, 34516, 28654],
67
+ },
68
+ {
69
+ id: "users",
70
+ value: "82,400",
71
+ change: 9.8,
72
+ changeLabel: "",
73
+ positiveIsGood: true,
74
+ sparklineData: [4967, 3852, 4940, 5585, 4356, 5811, 5194, 6045, 4479, 6127, 5753, 4776],
75
+ },
76
+ {
77
+ id: "sessions",
78
+ value: "132,900",
79
+ change: 6.5,
80
+ changeLabel: "",
81
+ positiveIsGood: true,
82
+ sparklineData: [7451, 5778, 7410, 8378, 6534, 8717, 7791, 9068, 6718, 9191, 8629, 7164],
83
+ },
84
+ {
85
+ id: "conversion",
86
+ value: "4.18%",
87
+ change: -0.4,
88
+ changeLabel: "",
89
+ positiveIsGood: true,
90
+ sparklineData: [4.8, 4.66, 4.79, 4.87, 4.72, 4.9, 4.82, 4.93, 4.74, 4.94, 4.89, 4.77],
91
+ },
92
+ {
93
+ id: "aov",
94
+ value: "$12.62",
95
+ change: 5.1,
96
+ changeLabel: "",
97
+ positiveIsGood: true,
98
+ sparklineData: [1064.36, 825.43, 1058.5, 1196.82, 933.43, 1245.21, 1113, 1295.36, 959.68, 1312.93, 1232.71, 1023.36],
99
+ },
100
+ ]
101
+
102
+ export const campaignRows: CampaignRow[] = [
103
+ { id: "c-01", name: "Item 1", channel: "Group A", spend: 48_200, conversions: 612, roi: 4.8 },
104
+ { id: "c-02", name: "Item 2", channel: "Group B", spend: 34_600, conversions: 421, roi: 3.4 },
105
+ { id: "c-03", name: "Item 3", channel: "Group C", spend: 12_800, conversions: 308, roi: 6.9 },
106
+ { id: "c-04", name: "Item 4", channel: "Group D", spend: 22_100, conversions: 254, roi: 4.1 },
107
+ { id: "c-05", name: "Item 5", channel: "Group E", spend: 18_400, conversions: 196, roi: 3.2 },
108
+ { id: "c-06", name: "Item 6", channel: "Group B", spend: 14_300, conversions: 124, roi: 2.5 },
109
+ ]
@@ -0,0 +1,38 @@
1
+ export interface KpiItem {
2
+ id: "revenue" | "users" | "conversion" | "aov" | "sessions"
3
+ value: string
4
+ change: number
5
+ changeLabel: string
6
+ positiveIsGood: boolean
7
+ sparklineData: number[]
8
+ }
9
+
10
+ export interface ComparisonPoint {
11
+ period: string
12
+ current: number
13
+ previous: number
14
+ growth: number
15
+ }
16
+
17
+ export interface BreakdownSlice {
18
+ segment: string
19
+ value: number
20
+ }
21
+
22
+ export interface TrendPoint {
23
+ date: string
24
+ value: number
25
+ }
26
+
27
+ export interface CampaignRow {
28
+ id: string
29
+ name: string
30
+ channel: string
31
+ spend: number
32
+ conversions: number
33
+ roi: number
34
+ }
35
+
36
+ export interface DashboardFilters {
37
+ dateRange: { from: Date | undefined; to: Date | undefined }
38
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "kpi-chart-advanced",
3
+ "description": "KPI + Chart (Advanced) — Hero KPI (1 large + 4 small in 2×2 grid), bar+line comparison, donut breakdown, daily trend, and a detail table. KPI-led analytics dashboard layout.",
4
+ "version": "0.1.0",
5
+ "files": [
6
+ { "src": "pages/index.tsx", "dest": "index.tsx", "action": "replace" },
7
+ { "src": "components/campaign-table.tsx", "dest": "components/kpi-chart-advanced/campaign-table.tsx", "action": "add" },
8
+ { "src": "lib/mock-data.ts", "dest": "components/kpi-chart-advanced/mock-data.ts", "action": "add", "relabel": false },
9
+ { "src": "lib/types.ts", "dest": "components/kpi-chart-advanced/types.ts", "action": "add", "relabel": false }
10
+ ]
11
+ }
@@ -0,0 +1,339 @@
1
+ import { useState } from "react"
2
+
3
+ import { subDays } from "date-fns"
4
+ import {
5
+ Activity,
6
+ DollarSign,
7
+ MousePointer,
8
+ PieChart,
9
+ ShoppingCart,
10
+ TrendingUp,
11
+ Users,
12
+ } from "lucide-react"
13
+
14
+ import { definePage } from "@squadbase/vantage"
15
+ import {
16
+ DashboardCard,
17
+ DashboardCardAction,
18
+ DashboardCardContent,
19
+ DashboardCardHeader,
20
+ DashboardCardPreset,
21
+ DashboardCardTitle,
22
+ DateRangePicker,
23
+ EChart,
24
+ PageShell,
25
+ PageShellContent,
26
+ PageShellDescription,
27
+ PageShellHeader,
28
+ PageShellHeaderEnd,
29
+ PageShellHeading,
30
+ PageShellSummary,
31
+ PageShellSummaryCard,
32
+ PageShellTitle,
33
+ Placeholder,
34
+ Sparkline,
35
+ type EChartsOption,
36
+ } from "@squadbase/vantage/components"
37
+
38
+ import { CampaignTable } from "./components/kpi-chart-advanced/campaign-table"
39
+ import {
40
+ breakdownSlices,
41
+ campaignRows,
42
+ comparisonSeries,
43
+ headerKpis,
44
+ trendSeries,
45
+ } from "./components/kpi-chart-advanced/mock-data"
46
+ import type {
47
+ BreakdownSlice,
48
+ ComparisonPoint,
49
+ DashboardFilters,
50
+ TrendPoint,
51
+ } from "./components/kpi-chart-advanced/types"
52
+
53
+ export const page = definePage({
54
+ title: "KPI + Chart (Advanced)",
55
+ description:
56
+ "Hero KPI (1 large + 4 small in 2×2 grid), bar+line comparison, donut breakdown, daily trend, and a detail table. KPI-led analytics dashboard layout.",
57
+ })
58
+
59
+ // ── chart helpers ───────────────────────────────────────────────────────────
60
+
61
+ function getBaseGrid() {
62
+ return { left: "3%", right: "4%", bottom: "12%", containLabel: true }
63
+ }
64
+
65
+ function getDualAxisGrid() {
66
+ return { left: "3%", right: "6%", bottom: "12%", containLabel: true }
67
+ }
68
+
69
+ function formatNumber(n: number): string {
70
+ const sign = n < 0 ? "-" : ""
71
+ const abs = Math.abs(n)
72
+ if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M`
73
+ if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(1)}K`
74
+ return n.toLocaleString("en-US")
75
+ }
76
+
77
+ // ── chart options ────────────────────────────────────────────────────────────
78
+
79
+ function comparisonOption(data: ComparisonPoint[]): EChartsOption {
80
+ return {
81
+ tooltip: { trigger: "axis" },
82
+ legend: { bottom: 0 },
83
+ grid: getDualAxisGrid(),
84
+ xAxis: {
85
+ type: "category",
86
+ data: data.map((d) => d.period),
87
+ },
88
+ yAxis: [
89
+ {
90
+ type: "value",
91
+ axisLabel: { formatter: (v: number) => formatNumber(v) },
92
+ },
93
+ {
94
+ type: "value",
95
+ axisLabel: { formatter: (v: number) => `${v.toFixed(0)}%` },
96
+ splitLine: { show: false },
97
+ },
98
+ ],
99
+ series: [
100
+ {
101
+ name: "Series A",
102
+ type: "bar",
103
+ data: data.map((d) => d.current),
104
+ barMaxWidth: 28,
105
+ },
106
+ {
107
+ name: "Series B",
108
+ type: "bar",
109
+ data: data.map((d) => d.previous),
110
+ barMaxWidth: 28,
111
+ },
112
+ {
113
+ name: "Series C",
114
+ type: "line",
115
+ yAxisIndex: 1,
116
+ smooth: true,
117
+ data: data.map((d) => d.growth),
118
+ },
119
+ ],
120
+ }
121
+ }
122
+
123
+ function breakdownOption(data: BreakdownSlice[]): EChartsOption {
124
+ return {
125
+ tooltip: { trigger: "item" },
126
+ legend: { bottom: 0 },
127
+ series: [
128
+ {
129
+ type: "pie",
130
+ radius: ["52%", "78%"],
131
+ avoidLabelOverlap: true,
132
+ itemStyle: { borderRadius: 6, borderColor: "transparent", borderWidth: 2 },
133
+ label: { show: false },
134
+ data: data.map((d) => ({ name: d.segment, value: d.value })),
135
+ },
136
+ ],
137
+ }
138
+ }
139
+
140
+ function trendOption(data: TrendPoint[]): EChartsOption {
141
+ return {
142
+ tooltip: { trigger: "axis" },
143
+ legend: { bottom: 0 },
144
+ grid: getBaseGrid(),
145
+ xAxis: {
146
+ type: "category",
147
+ data: data.map((d) => d.date),
148
+ boundaryGap: false,
149
+ axisLabel: { formatter: (v: string) => v.slice(5) },
150
+ },
151
+ yAxis: {
152
+ type: "value",
153
+ axisLabel: { formatter: (v: number) => formatNumber(v) },
154
+ },
155
+ series: [
156
+ {
157
+ name: "Series A",
158
+ type: "line",
159
+ smooth: true,
160
+ showSymbol: false,
161
+ data: data.map((d) => d.value),
162
+ areaStyle: { opacity: 0.18 },
163
+ },
164
+ ],
165
+ }
166
+ }
167
+
168
+ // ── page ─────────────────────────────────────────────────────────────────────
169
+
170
+ const today = new Date()
171
+ const initialFilters: DashboardFilters = {
172
+ dateRange: {
173
+ from: subDays(today, 29),
174
+ to: today,
175
+ },
176
+ }
177
+
178
+ const smallKpiCards = [
179
+ { label: "Metric 2", icon: Users, kpi: headerKpis[1] },
180
+ { label: "Metric 3", icon: MousePointer, kpi: headerKpis[2] },
181
+ { label: "Metric 4", icon: Activity, kpi: headerKpis[3] },
182
+ { label: "Metric 5", icon: ShoppingCart, kpi: headerKpis[4] },
183
+ ]
184
+
185
+ export default function HomePage() {
186
+ const [filters, setFilters] = useState<DashboardFilters>(initialFilters)
187
+ const heroKpi = headerKpis[0]
188
+
189
+ return (
190
+ <PageShell>
191
+ <PageShellHeader>
192
+ <PageShellHeading>
193
+ <PageShellTitle>[Template] Performance dashboard</PageShellTitle>
194
+ <PageShellDescription>
195
+ Headline KPIs, comparisons, and detail for the selected period.
196
+ </PageShellDescription>
197
+ </PageShellHeading>
198
+ <PageShellHeaderEnd>
199
+ <DateRangePicker
200
+ value={filters.dateRange}
201
+ onChange={(range) =>
202
+ setFilters((prev) => ({ ...prev, dateRange: range }))
203
+ }
204
+ maxDate={today}
205
+ />
206
+ </PageShellHeaderEnd>
207
+ <PageShellSummary>
208
+ <PageShellSummaryCard accent="accent">
209
+ <TrendingUp />
210
+ <div className="min-w-0 flex-1 space-y-1">
211
+ <p className="text-sm font-semibold">Growth</p>
212
+ <p className="text-xs text-muted-foreground">
213
+ Overall performance is trending up versus the previous period.
214
+ </p>
215
+ </div>
216
+ </PageShellSummaryCard>
217
+ <PageShellSummaryCard accent="default">
218
+ <PieChart />
219
+ <div className="min-w-0 flex-1 space-y-1">
220
+ <p className="text-sm font-semibold">Mix</p>
221
+ <p className="text-xs text-muted-foreground">
222
+ A single segment drives the largest share of the total.
223
+ </p>
224
+ </div>
225
+ </PageShellSummaryCard>
226
+ <PageShellSummaryCard accent="accent">
227
+ <Activity />
228
+ <div className="min-w-0 flex-1 space-y-1">
229
+ <p className="text-sm font-semibold">Momentum</p>
230
+ <p className="text-xs text-muted-foreground">
231
+ Recent activity points to a positive shift in the trend.
232
+ </p>
233
+ </div>
234
+ </PageShellSummaryCard>
235
+ </PageShellSummary>
236
+ </PageShellHeader>
237
+
238
+ <PageShellContent className="space-y-6">
239
+ <div className="grid gap-4 lg:grid-cols-3">
240
+ <DashboardCard className="h-full">
241
+ <DashboardCardHeader>
242
+ <DashboardCardTitle className="text-muted-foreground">
243
+ Metric 1
244
+ </DashboardCardTitle>
245
+ <DashboardCardAction>
246
+ <DollarSign className="size-5 text-muted-foreground" />
247
+ </DashboardCardAction>
248
+ </DashboardCardHeader>
249
+ <DashboardCardContent className="flex flex-1 flex-col gap-5">
250
+ <div>
251
+ <Placeholder className="text-4xl font-bold">
252
+ {heroKpi.value}
253
+ </Placeholder>
254
+ <div className="mt-3">
255
+ <Placeholder className="text-sm font-medium">
256
+ {heroKpi.change >= 0
257
+ ? `+${heroKpi.change}%`
258
+ : `${heroKpi.change}%`}
259
+ </Placeholder>
260
+ </div>
261
+ </div>
262
+ <Sparkline
263
+ data={heroKpi.sparklineData.map((v) => ({ value: v }))}
264
+ height={88}
265
+ area
266
+ />
267
+ <div className="grid grid-cols-2 gap-3 border-t pt-3">
268
+ <div className="space-y-1.5">
269
+ <p className="text-xs text-muted-foreground">Metric A</p>
270
+ <Placeholder className="text-sm font-medium">$0.92M</Placeholder>
271
+ </div>
272
+ <div className="space-y-1.5">
273
+ <p className="text-xs text-muted-foreground">Metric B</p>
274
+ <Placeholder className="text-sm font-medium">+8.4%</Placeholder>
275
+ </div>
276
+ </div>
277
+ </DashboardCardContent>
278
+ </DashboardCard>
279
+
280
+ <div className="grid gap-4 sm:grid-cols-2 lg:col-span-2">
281
+ {smallKpiCards.map(({ label, icon: Icon, kpi }) => (
282
+ <DashboardCard key={kpi.id}>
283
+ <DashboardCardHeader>
284
+ <DashboardCardTitle className="text-muted-foreground">
285
+ {label}
286
+ </DashboardCardTitle>
287
+ <DashboardCardAction>
288
+ <Icon className="size-4 text-muted-foreground" />
289
+ </DashboardCardAction>
290
+ </DashboardCardHeader>
291
+ <DashboardCardContent>
292
+ <Placeholder className="text-2xl font-bold">
293
+ {kpi.value}
294
+ </Placeholder>
295
+ <div className="mt-2">
296
+ <Placeholder className="text-sm font-medium">
297
+ {kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
298
+ </Placeholder>
299
+ </div>
300
+ <Sparkline
301
+ data={kpi.sparklineData.map((v) => ({ value: v }))}
302
+ height={32}
303
+ area
304
+ className="mt-3"
305
+ />
306
+ </DashboardCardContent>
307
+ </DashboardCard>
308
+ ))}
309
+ </div>
310
+ </div>
311
+
312
+ <div className="grid gap-4 lg:grid-cols-3">
313
+ <div className="lg:col-span-2">
314
+ <DashboardCardPreset
315
+ title="Comparison"
316
+ description="Current vs previous period"
317
+ >
318
+ <EChart option={comparisonOption(comparisonSeries)} height="320px" />
319
+ </DashboardCardPreset>
320
+ </div>
321
+ <DashboardCardPreset title="Breakdown" description="Share by segment">
322
+ <EChart option={breakdownOption(breakdownSlices)} height="320px" />
323
+ </DashboardCardPreset>
324
+ </div>
325
+
326
+ <DashboardCardPreset
327
+ title="Trend"
328
+ description="Values over the selected period"
329
+ >
330
+ <EChart option={trendOption(trendSeries)} height="280px" />
331
+ </DashboardCardPreset>
332
+
333
+ <DashboardCardPreset title="Campaigns" description="Performance by campaign">
334
+ <CampaignTable data={campaignRows} />
335
+ </DashboardCardPreset>
336
+ </PageShellContent>
337
+ </PageShell>
338
+ )
339
+ }
@@ -0,0 +1,69 @@
1
+ import { useMemo } from "react"
2
+
3
+ import { DataTablePreset, Placeholder, type ColumnDef } from "@squadbase/vantage/components"
4
+
5
+ import type { CampaignRow } from "./types"
6
+
7
+ function formatCurrency(n: number, opts: { short?: boolean } = {}): string {
8
+ const sign = n < 0 ? "-" : ""
9
+ const abs = Math.abs(n)
10
+ if (opts.short) {
11
+ if (abs >= 100_000_000) return `${sign}${(abs / 100_000_000).toFixed(2)}億円`
12
+ if (abs >= 10_000) return `${sign}${(abs / 10_000).toFixed(0)}万円`
13
+ return `${sign}¥${abs.toLocaleString("ja-JP")}`
14
+ }
15
+ return `${sign}¥${Math.round(abs).toLocaleString("ja-JP")}`
16
+ }
17
+
18
+ interface CampaignTableProps {
19
+ data: CampaignRow[]
20
+ }
21
+
22
+ export function CampaignTable({ data }: CampaignTableProps) {
23
+ const columns = useMemo<ColumnDef<CampaignRow>[]>(
24
+ () => [
25
+ {
26
+ accessorKey: "name",
27
+ header: "列1",
28
+ },
29
+ {
30
+ accessorKey: "channel",
31
+ header: "列2",
32
+ },
33
+ {
34
+ accessorKey: "spend",
35
+ header: "列3",
36
+ cell: ({ row }) => (
37
+ <div className="flex justify-end">
38
+ <Placeholder>
39
+ {formatCurrency(row.original.spend, { short: true })}
40
+ </Placeholder>
41
+ </div>
42
+ ),
43
+ },
44
+ {
45
+ accessorKey: "conversions",
46
+ header: "列4",
47
+ cell: ({ row }) => (
48
+ <div className="flex justify-end">
49
+ <Placeholder>
50
+ {row.original.conversions.toLocaleString("ja-JP")}
51
+ </Placeholder>
52
+ </div>
53
+ ),
54
+ },
55
+ {
56
+ accessorKey: "roi",
57
+ header: "列5",
58
+ cell: ({ row }) => (
59
+ <div className="flex justify-end">
60
+ <Placeholder>{`${row.original.roi.toFixed(1)}倍`}</Placeholder>
61
+ </div>
62
+ ),
63
+ },
64
+ ],
65
+ [],
66
+ )
67
+
68
+ return <DataTablePreset columns={columns} data={data} enableSorting />
69
+ }