@squadbase/vantage-template 0.1.1 → 0.2.0-dev1.0c136ab

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 (58) hide show
  1. package/README.md +19 -9
  2. package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +14 -7
  3. package/base-template/.squadbase/skills/vantage-app/SKILL.md +4 -0
  4. package/base-template/.squadbase/skills/vantage-design/SKILL.md +62 -52
  5. package/base-template/.squadbase/skills/vantage-design/references/extract.md +26 -27
  6. package/base-template/.squadbase/skills/vantage-design/references/format.md +12 -172
  7. package/base-template/.squadbase/skills/vantage-design/references/presets.md +50 -110
  8. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +50 -1
  9. package/base-template/.squadbase/skills/vantage-slide/SKILL.md +299 -0
  10. package/base-template/.squadbase/skills/vantage-slide/references/morph.md +102 -0
  11. package/base-template/.squadbase/skills/vantage-slide/references/steps.md +57 -0
  12. package/base-template/.squadbase/skills/vantage-slide/references/tokens.md +87 -0
  13. package/base-template/.squadbase/skills/vantage-slide/references/transitions.md +196 -0
  14. package/base-template/AGENTS.md +225 -18
  15. package/base-template/agents/api.md +36 -0
  16. package/base-template/agents/csv.md +2 -1
  17. package/base-template/agents/doc.md +52 -3
  18. package/base-template/agents/page.md +92 -10
  19. package/base-template/agents/slide.md +90 -4
  20. package/base-template/agents/xlsx.md +2 -1
  21. package/base-template/bun.lock +917 -0
  22. package/base-template/bunfig.toml +5 -0
  23. package/base-template/package.json +6 -6
  24. package/base-template/squadbase.yml +1 -0
  25. package/base-template/src/_layout.tsx +16 -7
  26. package/base-template/src/components/build-recovery.tsx +37 -1
  27. package/base-template/src/components/user-card.tsx +11 -11
  28. package/base-template/src/index.tsx +8 -3
  29. package/base-template/src/styles.css +108 -12
  30. package/dist/index.js +971 -6
  31. package/package.json +20 -17
  32. package/ui-templates/CLAUDE.md +14 -11
  33. package/ui-templates/chart-grid/lib/mock-data.ts +0 -3
  34. package/ui-templates/chart-grid/lib/types.ts +6 -6
  35. package/ui-templates/chart-grid/pages/index.tsx +11 -161
  36. package/ui-templates/chart-grid-ja/lib/mock-data.ts +0 -3
  37. package/ui-templates/chart-grid-ja/lib/types.ts +6 -6
  38. package/ui-templates/chart-grid-ja/pages/index.tsx +11 -161
  39. package/ui-templates/funnel/lib/types.ts +2 -2
  40. package/ui-templates/funnel/pages/index.tsx +10 -88
  41. package/ui-templates/funnel-ja/lib/types.ts +2 -2
  42. package/ui-templates/funnel-ja/pages/index.tsx +10 -87
  43. package/ui-templates/kpi-chart-advanced/lib/mock-data.ts +6 -6
  44. package/ui-templates/kpi-chart-advanced/lib/types.ts +6 -7
  45. package/ui-templates/kpi-chart-advanced/pages/index.tsx +6 -117
  46. package/ui-templates/kpi-chart-advanced-ja/lib/mock-data.ts +6 -6
  47. package/ui-templates/kpi-chart-advanced-ja/lib/types.ts +6 -7
  48. package/ui-templates/kpi-chart-advanced-ja/pages/index.tsx +6 -117
  49. package/ui-templates/kpi-chart-simple/lib/types.ts +4 -4
  50. package/ui-templates/kpi-chart-simple/pages/index.tsx +51 -89
  51. package/ui-templates/kpi-chart-simple-ja/lib/types.ts +4 -4
  52. package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +51 -87
  53. package/ui-templates/table-focus/lib/types.ts +3 -3
  54. package/ui-templates/table-focus/pages/index.tsx +3 -45
  55. package/ui-templates/table-focus-ja/lib/types.ts +3 -3
  56. package/ui-templates/table-focus-ja/pages/index.tsx +3 -45
  57. package/base-template/DESIGN.md +0 -108
  58. package/base-template/package-lock.json +0 -5841
@@ -20,7 +20,9 @@ import {
20
20
  DashboardCardPreset,
21
21
  DashboardCardTitle,
22
22
  DateRangePicker,
23
- EChart,
23
+ AreaChart,
24
+ BarChart,
25
+ DonutChart,
24
26
  PageShell,
25
27
  PageShellContent,
26
28
  PageShellDescription,
@@ -32,7 +34,6 @@ import {
32
34
  PageShellTitle,
33
35
  Placeholder,
34
36
  Sparkline,
35
- type EChartsOption,
36
37
  } from "@squadbase/vantage/components"
37
38
 
38
39
  import { CampaignTable } from "./components/kpi-chart-advanced/campaign-table"
@@ -44,10 +45,7 @@ import {
44
45
  trendSeries,
45
46
  } from "./components/kpi-chart-advanced/mock-data"
46
47
  import type {
47
- BreakdownSlice,
48
- ComparisonPoint,
49
48
  DashboardFilters,
50
- TrendPoint,
51
49
  } from "./components/kpi-chart-advanced/types"
52
50
 
53
51
  export const page = definePage({
@@ -56,115 +54,6 @@ export const page = definePage({
56
54
  "ヒーローKPI (大1+小4の2×2) ・棒+折れ線比較チャート・ドーナツ・日次トレンド・明細テーブルを搭載したKPI主役のダッシュボード構成",
57
55
  })
58
56
 
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
57
  // ── page ─────────────────────────────────────────────────────────────────────
169
58
 
170
59
  const today = new Date()
@@ -312,16 +201,16 @@ export default function HomePage() {
312
201
  <div className="grid gap-4 lg:grid-cols-3">
313
202
  <div className="lg:col-span-2">
314
203
  <DashboardCardPreset title="比較" description="今期 vs 前期">
315
- <EChart option={comparisonOption(comparisonSeries)} height="320px" />
204
+ <BarChart data={comparisonSeries} index="period" categories={["current", "previous"]} labels={{ current: "系列A", previous: "系列B" }} height="320px" />
316
205
  </DashboardCardPreset>
317
206
  </div>
318
207
  <DashboardCardPreset title="内訳" description="セグメント別シェア">
319
- <EChart option={breakdownOption(breakdownSlices)} height="320px" />
208
+ <DonutChart data={breakdownSlices} index="segment" categories={["value"]} height="320px" />
320
209
  </DashboardCardPreset>
321
210
  </div>
322
211
 
323
212
  <DashboardCardPreset title="トレンド" description="選択期間の値">
324
- <EChart option={trendOption(trendSeries)} height="280px" />
213
+ <AreaChart data={trendSeries} index="date" categories={["value"]} labels={{ value: "系列A" }} height="280px" />
325
214
  </DashboardCardPreset>
326
215
 
327
216
  <DashboardCardPreset title="キャンペーン" description="キャンペーン別パフォーマンス">
@@ -11,7 +11,7 @@ export type RangeKey = "7d" | "30d" | "90d"
11
11
  export type KpiId = "total-revenue" | "active-users" | "conversion-rate" | "aov"
12
12
 
13
13
  /** Raw numbers only — labels and formatting belong to the page. */
14
- export interface KpiItem {
14
+ export type KpiItem = {
15
15
  id: KpiId
16
16
  value: number
17
17
  /** Percent change against the preceding window of the same length. */
@@ -19,14 +19,14 @@ export interface KpiItem {
19
19
  sparklineData: number[]
20
20
  }
21
21
 
22
- export interface TrendPoint {
22
+ export type TrendPoint = {
23
23
  date: string
24
24
  revenue: number
25
25
  orders: number
26
26
  visitors: number
27
27
  }
28
28
 
29
- export interface TopItemRow {
29
+ export type TopItemRow = {
30
30
  id: string
31
31
  name: string
32
32
  category: string
@@ -36,7 +36,7 @@ export interface TopItemRow {
36
36
  }
37
37
 
38
38
  /** The payload `GET /api/kpi-summary` returns. */
39
- export interface KpiSummary {
39
+ export type KpiSummary = {
40
40
  range: RangeKey
41
41
  kpis: KpiItem[]
42
42
  trend: TrendPoint[]
@@ -7,7 +7,7 @@ import {
7
7
  DashboardCardHeader,
8
8
  DashboardCardPreset,
9
9
  DashboardCardTitle,
10
- EChart,
10
+ AreaChart,
11
11
  PageShell,
12
12
  PageShellContent,
13
13
  PageShellDescription,
@@ -19,13 +19,13 @@ import {
19
19
  SegmentedControl,
20
20
  Sparkline,
21
21
  } from "@squadbase/vantage/components"
22
- import type { EChartsOption, SegmentOption } from "@squadbase/vantage/components"
22
+ import type { SegmentOption } from "@squadbase/vantage/components"
23
23
  import { Button, ErrorState, Loading } from "@squadbase/vantage/ui"
24
24
  import { useApiQuery } from "@squadbase/vantage/query"
25
25
  import { useSearchParam } from "@squadbase/vantage/router"
26
26
 
27
27
  import { TopItemsTable } from "./components/kpi-chart-simple/top-items-table"
28
- import type { KpiId, KpiSummary, TrendPoint } from "./lib/kpi-chart-simple/types"
28
+ import type { KpiId, KpiSummary } from "./lib/kpi-chart-simple/types"
29
29
 
30
30
  export const page = definePage({
31
31
  title: "KPI + Chart (Simple)",
@@ -35,14 +35,6 @@ export const page = definePage({
35
35
 
36
36
  // ── formatting ───────────────────────────────────────────────────────────────
37
37
 
38
- function formatNumber(n: number): string {
39
- const sign = n < 0 ? "-" : ""
40
- const abs = Math.abs(n)
41
- if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M`
42
- if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(1)}K`
43
- return n.toLocaleString("en-US")
44
- }
45
-
46
38
  function formatCurrency(n: number): string {
47
39
  const sign = n < 0 ? "-" : ""
48
40
  const abs = Math.abs(n)
@@ -51,36 +43,6 @@ function formatCurrency(n: number): string {
51
43
  return `${sign}$${abs.toFixed(0)}`
52
44
  }
53
45
 
54
- // ── chart options ────────────────────────────────────────────────────────────
55
-
56
- function trendOption(data: TrendPoint[]): EChartsOption {
57
- return {
58
- tooltip: { trigger: "axis" },
59
- legend: { bottom: 0 },
60
- grid: { left: "3%", right: "4%", bottom: "10%", containLabel: true },
61
- xAxis: {
62
- type: "category",
63
- data: data.map((d) => d.date),
64
- boundaryGap: false,
65
- axisLabel: { formatter: (value: string) => value.slice(5) },
66
- },
67
- yAxis: {
68
- type: "value",
69
- axisLabel: { formatter: (v: number) => formatNumber(v) },
70
- },
71
- series: [
72
- {
73
- name: "Series A",
74
- type: "line",
75
- smooth: true,
76
- showSymbol: false,
77
- data: data.map((d) => d.revenue),
78
- areaStyle: { opacity: 0.18 },
79
- },
80
- ],
81
- }
82
- }
83
-
84
46
  // ── page ─────────────────────────────────────────────────────────────────────
85
47
 
86
48
  /** Display labels live here, not in the payload the API returns. */
@@ -104,54 +66,6 @@ const RANGE_OPTIONS: SegmentOption[] = [
104
66
  { label: "90 days", value: "90d" },
105
67
  ]
106
68
 
107
- function DashboardBody({ data }: { data: KpiSummary }) {
108
- return (
109
- <>
110
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
111
- {data.kpis.map((kpi) => {
112
- const { label, icon: Icon, format } = KPI_META[kpi.id]
113
- return (
114
- <DashboardCard key={kpi.id}>
115
- <DashboardCardHeader>
116
- <DashboardCardTitle className="text-muted-foreground">
117
- {label}
118
- </DashboardCardTitle>
119
- <DashboardCardAction>
120
- <Icon className="size-4 text-muted-foreground" />
121
- </DashboardCardAction>
122
- </DashboardCardHeader>
123
- <DashboardCardContent>
124
- <Placeholder className="text-2xl font-bold">
125
- {format(kpi.value)}
126
- </Placeholder>
127
- <div className="mt-2">
128
- <Placeholder className="text-sm font-medium">
129
- {kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
130
- </Placeholder>
131
- </div>
132
- <Sparkline
133
- data={kpi.sparklineData.map((v) => ({ value: v }))}
134
- height={32}
135
- area
136
- className="mt-3"
137
- />
138
- </DashboardCardContent>
139
- </DashboardCard>
140
- )
141
- })}
142
- </div>
143
-
144
- <DashboardCardPreset title="Trend" description="Values over the selected period">
145
- <EChart option={trendOption(data.trend)} height="320px" />
146
- </DashboardCardPreset>
147
-
148
- <DashboardCardPreset title="Top items" description="Ranked by value">
149
- <TopItemsTable data={data.topItems} />
150
- </DashboardCardPreset>
151
- </>
152
- )
153
- }
154
-
155
69
  export default function HomePage() {
156
70
  // Filter state belongs in the URL rather than `useState`: it survives a
157
71
  // reload, and a pasted link reproduces exactly what you were looking at.
@@ -198,3 +112,51 @@ export default function HomePage() {
198
112
  </PageShell>
199
113
  )
200
114
  }
115
+
116
+ function DashboardBody({ data }: { data: KpiSummary }) {
117
+ return (
118
+ <>
119
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
120
+ {data.kpis.map((kpi) => {
121
+ const { label, icon: Icon, format } = KPI_META[kpi.id]
122
+ return (
123
+ <DashboardCard key={kpi.id} data-sq-ref={kpi.id}>
124
+ <DashboardCardHeader>
125
+ <DashboardCardTitle className="text-muted-foreground">
126
+ {label}
127
+ </DashboardCardTitle>
128
+ <DashboardCardAction>
129
+ <Icon className="size-4 text-muted-foreground" />
130
+ </DashboardCardAction>
131
+ </DashboardCardHeader>
132
+ <DashboardCardContent>
133
+ <Placeholder className="text-2xl font-bold">
134
+ {format(kpi.value)}
135
+ </Placeholder>
136
+ <div className="mt-2">
137
+ <Placeholder className="text-sm font-medium">
138
+ {kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
139
+ </Placeholder>
140
+ </div>
141
+ <Sparkline
142
+ data={kpi.sparklineData.map((v) => ({ value: v }))}
143
+ height={32}
144
+ area
145
+ className="mt-3"
146
+ />
147
+ </DashboardCardContent>
148
+ </DashboardCard>
149
+ )
150
+ })}
151
+ </div>
152
+
153
+ <DashboardCardPreset data-sq-ref="trend" title="Trend" description="Values over the selected period">
154
+ <AreaChart data={data.trend} index="date" categories={["revenue"]} labels={{ revenue: "Series A" }} height="320px" />
155
+ </DashboardCardPreset>
156
+
157
+ <DashboardCardPreset data-sq-ref="top-items" title="Top items" description="Ranked by value">
158
+ <TopItemsTable data={data.topItems} />
159
+ </DashboardCardPreset>
160
+ </>
161
+ )
162
+ }
@@ -11,7 +11,7 @@ export type RangeKey = "7d" | "30d" | "90d"
11
11
  export type KpiId = "total-revenue" | "active-users" | "conversion-rate" | "aov"
12
12
 
13
13
  /** Raw numbers only — labels and formatting belong to the page. */
14
- export interface KpiItem {
14
+ export type KpiItem = {
15
15
  id: KpiId
16
16
  value: number
17
17
  /** Percent change against the preceding window of the same length. */
@@ -19,14 +19,14 @@ export interface KpiItem {
19
19
  sparklineData: number[]
20
20
  }
21
21
 
22
- export interface TrendPoint {
22
+ export type TrendPoint = {
23
23
  date: string
24
24
  revenue: number
25
25
  orders: number
26
26
  visitors: number
27
27
  }
28
28
 
29
- export interface TopItemRow {
29
+ export type TopItemRow = {
30
30
  id: string
31
31
  name: string
32
32
  category: string
@@ -36,7 +36,7 @@ export interface TopItemRow {
36
36
  }
37
37
 
38
38
  /** The payload `GET /api/kpi-summary` returns. */
39
- export interface KpiSummary {
39
+ export type KpiSummary = {
40
40
  range: RangeKey
41
41
  kpis: KpiItem[]
42
42
  trend: TrendPoint[]
@@ -7,7 +7,7 @@ import {
7
7
  DashboardCardHeader,
8
8
  DashboardCardPreset,
9
9
  DashboardCardTitle,
10
- EChart,
10
+ AreaChart,
11
11
  PageShell,
12
12
  PageShellContent,
13
13
  PageShellDescription,
@@ -19,13 +19,13 @@ import {
19
19
  SegmentedControl,
20
20
  Sparkline,
21
21
  } from "@squadbase/vantage/components"
22
- import type { EChartsOption, SegmentOption } from "@squadbase/vantage/components"
22
+ import type { SegmentOption } from "@squadbase/vantage/components"
23
23
  import { Button, ErrorState, Loading } from "@squadbase/vantage/ui"
24
24
  import { useApiQuery } from "@squadbase/vantage/query"
25
25
  import { useSearchParam } from "@squadbase/vantage/router"
26
26
 
27
27
  import { TopItemsTable } from "./components/kpi-chart-simple/top-items-table"
28
- import type { KpiId, KpiSummary, TrendPoint } from "./lib/kpi-chart-simple/types"
28
+ import type { KpiId, KpiSummary } from "./lib/kpi-chart-simple/types"
29
29
 
30
30
  export const page = definePage({
31
31
  title: "KPI + チャート (シンプル)",
@@ -35,14 +35,6 @@ export const page = definePage({
35
35
 
36
36
  // ── 表示フォーマット ─────────────────────────────────────────────────────────
37
37
 
38
- function formatNumber(n: number): string {
39
- const sign = n < 0 ? "-" : ""
40
- const abs = Math.abs(n)
41
- if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M`
42
- if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(1)}K`
43
- return n.toLocaleString("ja-JP")
44
- }
45
-
46
38
  function formatCurrency(n: number): string {
47
39
  const sign = n < 0 ? "-" : ""
48
40
  const abs = Math.abs(n)
@@ -53,34 +45,6 @@ function formatCurrency(n: number): string {
53
45
 
54
46
  // ── チャートオプション ───────────────────────────────────────────────────────
55
47
 
56
- function trendOption(data: TrendPoint[]): EChartsOption {
57
- return {
58
- tooltip: { trigger: "axis" },
59
- legend: { bottom: 0 },
60
- grid: { left: "3%", right: "4%", bottom: "10%", containLabel: true },
61
- xAxis: {
62
- type: "category",
63
- data: data.map((d) => d.date),
64
- boundaryGap: false,
65
- axisLabel: { formatter: (value: string) => value.slice(5) },
66
- },
67
- yAxis: {
68
- type: "value",
69
- axisLabel: { formatter: (v: number) => formatNumber(v) },
70
- },
71
- series: [
72
- {
73
- name: "系列A",
74
- type: "line",
75
- smooth: true,
76
- showSymbol: false,
77
- data: data.map((d) => d.revenue),
78
- areaStyle: { opacity: 0.18 },
79
- },
80
- ],
81
- }
82
- }
83
-
84
48
  // ── ページ ───────────────────────────────────────────────────────────────────
85
49
 
86
50
  /** 表示ラベルは API のレスポンスではなく描画側に置く。 */
@@ -104,54 +68,6 @@ const RANGE_OPTIONS: SegmentOption[] = [
104
68
  { label: "90日", value: "90d" },
105
69
  ]
106
70
 
107
- function DashboardBody({ data }: { data: KpiSummary }) {
108
- return (
109
- <>
110
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
111
- {data.kpis.map((kpi) => {
112
- const { label, icon: Icon, format } = KPI_META[kpi.id]
113
- return (
114
- <DashboardCard key={kpi.id}>
115
- <DashboardCardHeader>
116
- <DashboardCardTitle className="text-muted-foreground">
117
- {label}
118
- </DashboardCardTitle>
119
- <DashboardCardAction>
120
- <Icon className="size-4 text-muted-foreground" />
121
- </DashboardCardAction>
122
- </DashboardCardHeader>
123
- <DashboardCardContent>
124
- <Placeholder className="text-2xl font-bold">
125
- {format(kpi.value)}
126
- </Placeholder>
127
- <div className="mt-2">
128
- <Placeholder className="text-sm font-medium">
129
- {kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
130
- </Placeholder>
131
- </div>
132
- <Sparkline
133
- data={kpi.sparklineData.map((v) => ({ value: v }))}
134
- height={32}
135
- area
136
- className="mt-3"
137
- />
138
- </DashboardCardContent>
139
- </DashboardCard>
140
- )
141
- })}
142
- </div>
143
-
144
- <DashboardCardPreset title="トレンド" description="選択期間の値">
145
- <EChart option={trendOption(data.trend)} height="320px" />
146
- </DashboardCardPreset>
147
-
148
- <DashboardCardPreset title="上位項目" description="値で並べ替え">
149
- <TopItemsTable data={data.topItems} />
150
- </DashboardCardPreset>
151
- </>
152
- )
153
- }
154
-
155
71
  export default function HomePage() {
156
72
  // 絞り込み状態は useState ではなく URL に置く。リロードで消えず、URL を
157
73
  // そのまま共有すれば相手も同じ画面を見られる。
@@ -198,3 +114,51 @@ export default function HomePage() {
198
114
  </PageShell>
199
115
  )
200
116
  }
117
+
118
+ function DashboardBody({ data }: { data: KpiSummary }) {
119
+ return (
120
+ <>
121
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
122
+ {data.kpis.map((kpi) => {
123
+ const { label, icon: Icon, format } = KPI_META[kpi.id]
124
+ return (
125
+ <DashboardCard key={kpi.id} data-sq-ref={kpi.id}>
126
+ <DashboardCardHeader>
127
+ <DashboardCardTitle className="text-muted-foreground">
128
+ {label}
129
+ </DashboardCardTitle>
130
+ <DashboardCardAction>
131
+ <Icon className="size-4 text-muted-foreground" />
132
+ </DashboardCardAction>
133
+ </DashboardCardHeader>
134
+ <DashboardCardContent>
135
+ <Placeholder className="text-2xl font-bold">
136
+ {format(kpi.value)}
137
+ </Placeholder>
138
+ <div className="mt-2">
139
+ <Placeholder className="text-sm font-medium">
140
+ {kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
141
+ </Placeholder>
142
+ </div>
143
+ <Sparkline
144
+ data={kpi.sparklineData.map((v) => ({ value: v }))}
145
+ height={32}
146
+ area
147
+ className="mt-3"
148
+ />
149
+ </DashboardCardContent>
150
+ </DashboardCard>
151
+ )
152
+ })}
153
+ </div>
154
+
155
+ <DashboardCardPreset data-sq-ref="trend" title="トレンド" description="選択期間の値">
156
+ <AreaChart data={data.trend} index="date" categories={["revenue"]} labels={{ revenue: "系列A" }} height="320px" />
157
+ </DashboardCardPreset>
158
+
159
+ <DashboardCardPreset data-sq-ref="top-items" title="上位項目" description="値で並べ替え">
160
+ <TopItemsTable data={data.topItems} />
161
+ </DashboardCardPreset>
162
+ </>
163
+ )
164
+ }
@@ -1,4 +1,4 @@
1
- export interface DetailRow {
1
+ export type DetailRow = {
2
2
  id: string
3
3
  name: string
4
4
  owner: string
@@ -10,7 +10,7 @@ export interface DetailRow {
10
10
  updated: string
11
11
  }
12
12
 
13
- export interface SummaryRow {
13
+ export type SummaryRow = {
14
14
  segment: string
15
15
  count: number
16
16
  revenue: number
@@ -18,7 +18,7 @@ export interface SummaryRow {
18
18
  share: number
19
19
  }
20
20
 
21
- export interface TrendPoint {
21
+ export type TrendPoint = {
22
22
  date: string
23
23
  value: number
24
24
  }