@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,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: "セグメント1", value: 412_000 },
53
+ { segment: "セグメント2", value: 318_000 },
54
+ { segment: "セグメント3", value: 287_000 },
55
+ { segment: "セグメント4", value: 164_000 },
56
+ { segment: "セグメント5", value: 132_000 },
57
+ ]
58
+
59
+ export const headerKpis: [KpiItem, KpiItem, KpiItem, KpiItem, KpiItem] = [
60
+ {
61
+ id: "revenue",
62
+ value: "¥104万",
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: "項目1", channel: "グループA", spend: 48_200, conversions: 612, roi: 4.8 },
104
+ { id: "c-02", name: "項目2", channel: "グループB", spend: 34_600, conversions: 421, roi: 3.4 },
105
+ { id: "c-03", name: "項目3", channel: "グループC", spend: 12_800, conversions: 308, roi: 6.9 },
106
+ { id: "c-04", name: "項目4", channel: "グループD", spend: 22_100, conversions: 254, roi: 4.1 },
107
+ { id: "c-05", name: "項目5", channel: "グループE", spend: 18_400, conversions: 196, roi: 3.2 },
108
+ { id: "c-06", name: "項目6", channel: "グループ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-ja",
3
+ "description": "[日本語] KPI + チャート (アドバンスト) — ヒーローKPI (大1+小4の2×2) ・棒+折れ線比較チャート・ドーナツ・日次トレンド・明細テーブルを搭載したKPI主役のダッシュボード構成",
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,333 @@
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 + チャート (アドバンスト)",
55
+ description:
56
+ "ヒーローKPI (大1+小4の2×2) ・棒+折れ線比較チャート・ドーナツ・日次トレンド・明細テーブルを搭載したKPI主役のダッシュボード構成",
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 >= 100_000_000) return `${sign}${(abs / 100_000_000).toFixed(1)}億`
73
+ if (abs >= 10_000) return `${sign}${(abs / 10_000).toFixed(0)}万`
74
+ return n.toLocaleString("ja-JP")
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: "系列A",
102
+ type: "bar",
103
+ data: data.map((d) => d.current),
104
+ barMaxWidth: 28,
105
+ },
106
+ {
107
+ name: "系列B",
108
+ type: "bar",
109
+ data: data.map((d) => d.previous),
110
+ barMaxWidth: 28,
111
+ },
112
+ {
113
+ name: "系列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: "系列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: "指標2", icon: Users, kpi: headerKpis[1] },
180
+ { label: "指標3", icon: MousePointer, kpi: headerKpis[2] },
181
+ { label: "指標4", icon: Activity, kpi: headerKpis[3] },
182
+ { label: "指標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>[テンプレート] パフォーマンスダッシュボード</PageShellTitle>
194
+ <PageShellDescription>
195
+ 選択期間の主要KPI・比較・詳細。
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">成長</p>
212
+ <p className="text-xs text-muted-foreground">
213
+ 全体のパフォーマンスは前期比で上昇傾向です。
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">構成</p>
221
+ <p className="text-xs text-muted-foreground">
222
+ 単一セグメントが全体で最大のシェアを占めています。
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">勢い</p>
230
+ <p className="text-xs text-muted-foreground">
231
+ 直近の動きはトレンドの好転を示しています。
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
+ 指標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">指標A</p>
270
+ <Placeholder className="text-sm font-medium">¥92万</Placeholder>
271
+ </div>
272
+ <div className="space-y-1.5">
273
+ <p className="text-xs text-muted-foreground">指標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 title="比較" description="今期 vs 前期">
315
+ <EChart option={comparisonOption(comparisonSeries)} height="320px" />
316
+ </DashboardCardPreset>
317
+ </div>
318
+ <DashboardCardPreset title="内訳" description="セグメント別シェア">
319
+ <EChart option={breakdownOption(breakdownSlices)} height="320px" />
320
+ </DashboardCardPreset>
321
+ </div>
322
+
323
+ <DashboardCardPreset title="トレンド" description="選択期間の値">
324
+ <EChart option={trendOption(trendSeries)} height="280px" />
325
+ </DashboardCardPreset>
326
+
327
+ <DashboardCardPreset title="キャンペーン" description="キャンペーン別パフォーマンス">
328
+ <CampaignTable data={campaignRows} />
329
+ </DashboardCardPreset>
330
+ </PageShellContent>
331
+ </PageShell>
332
+ )
333
+ }
@@ -0,0 +1,73 @@
1
+ import { useMemo } from "react"
2
+ import { DataTablePreset, Placeholder } from "@squadbase/vantage/components"
3
+ import type { ColumnDef } from "@squadbase/vantage/components"
4
+
5
+ import type { TopItemRow } from "../../lib/kpi-chart-simple/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
+ function formatPercent(value: number, digits = 1): string {
19
+ return `${value.toFixed(digits)}%`
20
+ }
21
+
22
+ interface TopItemsTableProps {
23
+ data: TopItemRow[]
24
+ }
25
+
26
+ export function TopItemsTable({ data }: TopItemsTableProps) {
27
+ const columns = useMemo<ColumnDef<TopItemRow>[]>(
28
+ () => [
29
+ {
30
+ accessorKey: "name",
31
+ header: "Column 1",
32
+ },
33
+ {
34
+ accessorKey: "category",
35
+ header: "Column 2",
36
+ },
37
+ {
38
+ accessorKey: "revenue",
39
+ header: "Column 3",
40
+ cell: ({ row }) => (
41
+ <div className="flex justify-end">
42
+ <Placeholder>
43
+ {formatCurrency(row.original.revenue, { short: true })}
44
+ </Placeholder>
45
+ </div>
46
+ ),
47
+ },
48
+ {
49
+ accessorKey: "units",
50
+ header: "Column 4",
51
+ cell: ({ row }) => (
52
+ <div className="flex justify-end">
53
+ <Placeholder>
54
+ {row.original.units.toLocaleString("en-US")}
55
+ </Placeholder>
56
+ </div>
57
+ ),
58
+ },
59
+ {
60
+ accessorKey: "share",
61
+ header: "Column 5",
62
+ cell: ({ row }) => (
63
+ <div className="flex justify-end">
64
+ <Placeholder>{formatPercent(row.original.share * 100)}</Placeholder>
65
+ </div>
66
+ ),
67
+ },
68
+ ],
69
+ [],
70
+ )
71
+
72
+ return <DataTablePreset columns={columns} data={data} enableSorting />
73
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared between the page and `server/api/kpi-summary.ts`.
3
+ *
4
+ * These live in `lib/` rather than `components/` because the client may never
5
+ * import from `server/` (`vantage check` reports `CLIENT_IMPORTS_SERVER`), so
6
+ * anything both sides need has to sit in neutral ground.
7
+ */
8
+
9
+ export type RangeKey = "7d" | "30d" | "90d"
10
+
11
+ export type KpiId = "total-revenue" | "active-users" | "conversion-rate" | "aov"
12
+
13
+ /** Raw numbers only — labels and formatting belong to the page. */
14
+ export interface KpiItem {
15
+ id: KpiId
16
+ value: number
17
+ /** Percent change against the preceding window of the same length. */
18
+ change: number
19
+ sparklineData: number[]
20
+ }
21
+
22
+ export interface TrendPoint {
23
+ date: string
24
+ revenue: number
25
+ orders: number
26
+ visitors: number
27
+ }
28
+
29
+ export interface TopItemRow {
30
+ id: string
31
+ name: string
32
+ category: string
33
+ revenue: number
34
+ units: number
35
+ share: number
36
+ }
37
+
38
+ /** The payload `GET /api/kpi-summary` returns. */
39
+ export interface KpiSummary {
40
+ range: RangeKey
41
+ kpis: KpiItem[]
42
+ trend: TrendPoint[]
43
+ topItems: TopItemRow[]
44
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "kpi-chart-simple",
3
+ "description": "KPI + Chart (Simple) — 4 KPI cards, a single trend line, and a ranked-items table, served by server/api and filtered through the URL. The minimal headline-metrics dashboard layout.",
4
+ "version": "0.2.0",
5
+ "files": [
6
+ { "src": "pages/index.tsx", "dest": "index.tsx", "action": "replace" },
7
+ { "src": "components/top-items-table.tsx", "dest": "components/kpi-chart-simple/top-items-table.tsx", "action": "add" },
8
+ { "src": "lib/types.ts", "dest": "lib/kpi-chart-simple/types.ts", "action": "add", "relabel": false },
9
+ { "src": "server/kpi-summary.ts", "dest": "server/api/kpi-summary.ts", "action": "add", "relabel": false }
10
+ ]
11
+ }