@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,252 @@
1
+ import { useState } from "react"
2
+ import { subDays } from "date-fns"
3
+ import { TrendingDown, TrendingUp, Users, Target } from "lucide-react"
4
+ import { definePage } from "@squadbase/vantage"
5
+ import {
6
+ DashboardCard,
7
+ DashboardCardHeader,
8
+ DashboardCardTitle,
9
+ DashboardCardAction,
10
+ DashboardCardContent,
11
+ DashboardCardPreset,
12
+ DateRangePicker,
13
+ EChart,
14
+ PageShell,
15
+ PageShellHeader,
16
+ PageShellHeading,
17
+ PageShellTitle,
18
+ PageShellDescription,
19
+ PageShellHeaderEnd,
20
+ PageShellContent,
21
+ Placeholder,
22
+ } from "@squadbase/vantage/components"
23
+ import type { DateRange, EChartsOption } from "@squadbase/vantage/components"
24
+ import { ToggleGroup, ToggleGroupItem } from "@squadbase/vantage/ui"
25
+
26
+ import { StageTable } from "./components/funnel/stage-table"
27
+ import { funnelStages, stageRows } from "./components/funnel/mock-data"
28
+ import type { FunnelStage } from "./components/funnel/types"
29
+
30
+ export const page = definePage({
31
+ title: "Funnel",
32
+ description:
33
+ "Pipeline-centric layout: summary KPIs, a theme-coloured funnel chart with a per-stage sidebar, and a stage-performance table.",
34
+ })
35
+
36
+ const today = new Date()
37
+
38
+ const STAGE_NAMES = ["Stage 1", "Stage 2", "Stage 3", "Stage 4", "Stage 5"]
39
+
40
+ type DisplayMode = "count" | "percent"
41
+
42
+ function funnelOption(
43
+ data: FunnelStage[],
44
+ labels: string[],
45
+ display: DisplayMode,
46
+ ): EChartsOption {
47
+ const max = data[0]?.value ?? 0
48
+
49
+ return {
50
+ tooltip: { trigger: "item" },
51
+ series: [
52
+ {
53
+ type: "funnel",
54
+ orient: "horizontal",
55
+ funnelAlign: "center",
56
+ left: "2%",
57
+ right: "2%",
58
+ top: 16,
59
+ bottom: 64,
60
+ min: 0,
61
+ max,
62
+ sort: "descending",
63
+ gap: 4,
64
+ label: {
65
+ show: true,
66
+ position: "bottom",
67
+ formatter: (p: unknown) => {
68
+ const params = p as {
69
+ name: string
70
+ value: number
71
+ data: { conversionRate: number; share: number }
72
+ }
73
+ const main =
74
+ display === "percent"
75
+ ? `${params.data.share.toFixed(1)}%`
76
+ : params.value.toLocaleString("en-US")
77
+ return `{name|${params.name}}\n{value|${main}}`
78
+ },
79
+ rich: {
80
+ name: { fontSize: 12, fontWeight: 600, lineHeight: 16, align: "center" },
81
+ value: { fontSize: 11, opacity: 0.6, lineHeight: 14, align: "center" },
82
+ },
83
+ },
84
+ labelLine: { show: false },
85
+ // No `itemStyle.color` and no `option.color`: the stages take
86
+ // `--chart-1..5`, and the labels the foreground token, so the chart
87
+ // rebrands with the theme (v0.2.1+).
88
+ data: data.map((d, i) => ({
89
+ name: labels[i] ?? "",
90
+ value: d.value,
91
+ conversionRate: d.conversionRate,
92
+ share: (d.value / Math.max(1, max)) * 100,
93
+ })),
94
+ },
95
+ ] as EChartsOption["series"],
96
+ }
97
+ }
98
+
99
+ const summaryCards = [
100
+ {
101
+ label: "Entered",
102
+ value: "124,500",
103
+ sub: "Total entering the funnel",
104
+ icon: <Users className="size-4 text-muted-foreground" />,
105
+ },
106
+ {
107
+ label: "Converted",
108
+ value: "1,186",
109
+ sub: "Completed conversions",
110
+ icon: <Target className="size-4 text-muted-foreground" />,
111
+ },
112
+ {
113
+ label: "Best step",
114
+ value: "31.9%",
115
+ sub: "Highest step conversion",
116
+ icon: <TrendingUp className="size-4 text-emerald-600" />,
117
+ },
118
+ {
119
+ label: "Biggest drop",
120
+ value: "68.2%",
121
+ sub: "Largest step drop-off",
122
+ icon: <TrendingDown className="size-4 text-rose-600" />,
123
+ },
124
+ ]
125
+
126
+ export default function HomePage() {
127
+ const [dateRange, setDateRange] = useState<DateRange>({
128
+ from: subDays(today, 29),
129
+ to: today,
130
+ })
131
+ const [display, setDisplay] = useState<DisplayMode>("count")
132
+
133
+ return (
134
+ <PageShell>
135
+ <PageShellHeader>
136
+ <PageShellHeading>
137
+ <PageShellTitle>[Template] Conversion funnel</PageShellTitle>
138
+ <PageShellDescription>
139
+ Stage-by-stage conversion for the selected period.
140
+ </PageShellDescription>
141
+ </PageShellHeading>
142
+ <PageShellHeaderEnd className="flex-row items-center gap-2">
143
+ {/* Base UI toggle groups are always multi-value — keep the array at length 1. */}
144
+ <ToggleGroup
145
+ value={[display]}
146
+ onValueChange={(value) => {
147
+ const next = value[0]
148
+ if (next) setDisplay(next as DisplayMode)
149
+ }}
150
+ variant="outline"
151
+ size="sm"
152
+ >
153
+ <ToggleGroupItem value="count">Option A</ToggleGroupItem>
154
+ <ToggleGroupItem value="percent">Option B</ToggleGroupItem>
155
+ </ToggleGroup>
156
+ <DateRangePicker
157
+ value={dateRange}
158
+ onChange={setDateRange}
159
+ maxDate={today}
160
+ />
161
+ </PageShellHeaderEnd>
162
+ </PageShellHeader>
163
+
164
+ <PageShellContent className="space-y-6">
165
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
166
+ {summaryCards.map(({ label, value, sub, icon }) => (
167
+ <DashboardCard key={label}>
168
+ <DashboardCardHeader>
169
+ <DashboardCardTitle className="text-muted-foreground">
170
+ {label}
171
+ </DashboardCardTitle>
172
+ <DashboardCardAction>{icon}</DashboardCardAction>
173
+ </DashboardCardHeader>
174
+ <DashboardCardContent>
175
+ <Placeholder className="text-2xl font-bold">{value}</Placeholder>
176
+ <p className="mt-2 text-xs text-muted-foreground">{sub}</p>
177
+ </DashboardCardContent>
178
+ </DashboardCard>
179
+ ))}
180
+ </div>
181
+
182
+ <div className="grid gap-4 lg:grid-cols-3">
183
+ <div className="lg:col-span-2">
184
+ <DashboardCardPreset
185
+ title="Conversion funnel"
186
+ description="Stage-by-stage conversion"
187
+ >
188
+ <EChart
189
+ option={funnelOption(funnelStages, STAGE_NAMES, display)}
190
+ height="440px"
191
+ />
192
+ </DashboardCardPreset>
193
+ </div>
194
+ <DashboardCardPreset title="Stages" description="Conversion by stage">
195
+ <StagesSidebar />
196
+ </DashboardCardPreset>
197
+ </div>
198
+
199
+ <DashboardCardPreset
200
+ title="Stage breakdown"
201
+ description="Conversion between stages"
202
+ >
203
+ <StageTable data={stageRows} />
204
+ </DashboardCardPreset>
205
+ </PageShellContent>
206
+ </PageShell>
207
+ )
208
+ }
209
+
210
+ function StagesSidebar() {
211
+ return (
212
+ <ol className="space-y-4">
213
+ {funnelStages.map((s, i) => {
214
+ const prev = i > 0 ? funnelStages[i - 1] : null
215
+ const dropoffPct = prev ? ((prev.value - s.value) / prev.value) * 100 : 0
216
+ return (
217
+ <li key={i} className="space-y-2">
218
+ <div className="flex items-baseline justify-between gap-2">
219
+ <div className="flex items-baseline gap-2 min-w-0">
220
+ <span className="text-xs font-semibold text-muted-foreground tabular-nums">
221
+ {String(i + 1).padStart(2, "0")}
222
+ </span>
223
+ <span className="truncate text-sm font-medium">{STAGE_NAMES[i]}</span>
224
+ </div>
225
+ <Placeholder className="text-sm font-medium">
226
+ {s.value.toLocaleString("en-US")}
227
+ </Placeholder>
228
+ </div>
229
+ <div className="grid grid-cols-2 gap-3 text-xs text-muted-foreground">
230
+ <span>Conversion rate</span>
231
+ <div className="flex justify-end">
232
+ <Placeholder>{`${s.conversionRate.toFixed(1)}%`}</Placeholder>
233
+ </div>
234
+ </div>
235
+ {prev && (
236
+ <div className="flex h-1.5 overflow-hidden rounded-full bg-muted">
237
+ <div
238
+ className="h-full bg-chart-1"
239
+ style={{ width: `${s.conversionRate.toFixed(1)}%` }}
240
+ />
241
+ <div
242
+ className="h-full bg-muted-foreground/30"
243
+ style={{ width: `${dropoffPct.toFixed(1)}%` }}
244
+ />
245
+ </div>
246
+ )}
247
+ </li>
248
+ )
249
+ })}
250
+ </ol>
251
+ )
252
+ }
@@ -0,0 +1,11 @@
1
+ export function formatNumber(n: number): string {
2
+ const sign = n < 0 ? "-" : ""
3
+ const abs = Math.abs(n)
4
+ if (abs >= 100_000_000) return `${sign}${(abs / 100_000_000).toFixed(1)}億`
5
+ if (abs >= 10_000) return `${sign}${(abs / 10_000).toFixed(0)}万`
6
+ return n.toLocaleString("ja-JP")
7
+ }
8
+
9
+ export function formatPercent(value: number, digits = 1): string {
10
+ return `${value.toFixed(digits)}%`
11
+ }
@@ -0,0 +1,63 @@
1
+ import { useMemo } from "react"
2
+ import { DataTablePreset, Placeholder } from "@squadbase/vantage/components"
3
+ import type { ColumnDef } from "@squadbase/vantage/components"
4
+
5
+ import { formatNumber, formatPercent } from "./chart-helpers"
6
+ import type { StageRow } from "./types"
7
+
8
+ const STEP_NAMES = ["ステージ1 → 2", "ステージ2 → 3", "ステージ3 → 4", "ステージ4 → 5"]
9
+
10
+ interface StageTableProps {
11
+ data: StageRow[]
12
+ }
13
+
14
+ export function StageTable({ data }: StageTableProps) {
15
+ const columns = useMemo<ColumnDef<StageRow>[]>(
16
+ () => [
17
+ {
18
+ id: "step",
19
+ header: "列1",
20
+ cell: ({ row }) => STEP_NAMES[row.index],
21
+ },
22
+ {
23
+ accessorKey: "count",
24
+ header: "列2",
25
+ cell: ({ row }) => (
26
+ <div className="flex justify-end">
27
+ <Placeholder>{formatNumber(row.original.count)}</Placeholder>
28
+ </div>
29
+ ),
30
+ },
31
+ {
32
+ accessorKey: "conversionRate",
33
+ header: "列3",
34
+ cell: ({ row }) => (
35
+ <div className="flex justify-end">
36
+ <Placeholder>{formatPercent(row.original.conversionRate)}</Placeholder>
37
+ </div>
38
+ ),
39
+ },
40
+ {
41
+ accessorKey: "avgDays",
42
+ header: "列4",
43
+ cell: ({ row }) => (
44
+ <div className="flex justify-end">
45
+ <Placeholder>{`${row.original.avgDays.toFixed(1)}日`}</Placeholder>
46
+ </div>
47
+ ),
48
+ },
49
+ {
50
+ accessorKey: "dropoff",
51
+ header: "列5",
52
+ cell: ({ row }) => (
53
+ <div className="flex justify-end">
54
+ <Placeholder>{formatNumber(row.original.dropoff)}</Placeholder>
55
+ </div>
56
+ ),
57
+ },
58
+ ],
59
+ [],
60
+ )
61
+
62
+ return <DataTablePreset columns={columns} data={data} enableSorting />
63
+ }
@@ -0,0 +1,22 @@
1
+ import type { FunnelStage, StageRow } from "./types"
2
+
3
+ const VISITORS = 124_500
4
+ const LEADS = 38_240
5
+ const QUALIFIED = 12_180
6
+ const OPPORTUNITIES = 4_520
7
+ const CONVERSIONS = 1_186
8
+
9
+ export const funnelStages: FunnelStage[] = [
10
+ { value: VISITORS, conversionRate: 100 },
11
+ { value: LEADS, conversionRate: (LEADS / VISITORS) * 100 },
12
+ { value: QUALIFIED, conversionRate: (QUALIFIED / LEADS) * 100 },
13
+ { value: OPPORTUNITIES, conversionRate: (OPPORTUNITIES / QUALIFIED) * 100 },
14
+ { value: CONVERSIONS, conversionRate: (CONVERSIONS / OPPORTUNITIES) * 100 },
15
+ ]
16
+
17
+ export const stageRows: StageRow[] = [
18
+ { count: LEADS, conversionRate: (LEADS / VISITORS) * 100, avgDays: 0.6, dropoff: VISITORS - LEADS },
19
+ { count: QUALIFIED, conversionRate: (QUALIFIED / LEADS) * 100, avgDays: 3.2, dropoff: LEADS - QUALIFIED },
20
+ { count: OPPORTUNITIES, conversionRate: (OPPORTUNITIES / QUALIFIED) * 100, avgDays: 7.4, dropoff: QUALIFIED - OPPORTUNITIES },
21
+ { count: CONVERSIONS, conversionRate: (CONVERSIONS / OPPORTUNITIES) * 100, avgDays: 12.8, dropoff: OPPORTUNITIES - CONVERSIONS },
22
+ ]
@@ -0,0 +1,11 @@
1
+ export interface FunnelStage {
2
+ value: number
3
+ conversionRate: number
4
+ }
5
+
6
+ export interface StageRow {
7
+ count: number
8
+ conversionRate: number
9
+ avgDays: number
10
+ dropoff: number
11
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "funnel-ja",
3
+ "description": "[日本語] ファネル — 上部に 4 つのサマリ KPI (ファネル流入 / エンドツーエンド転換率 / 最良ステージ / ボトルネック)、その下に左揃えでテーマ配色 (--chart-1..5) のファネルとステージ別「通過率 + 離脱」サイドバー、最下段にステージ別パフォーマンステーブル。コンバージョン/パイプライン分析向け",
4
+ "version": "0.1.0",
5
+ "files": [
6
+ { "src": "pages/index.tsx", "dest": "index.tsx", "action": "replace" },
7
+ { "src": "components/stage-table.tsx", "dest": "components/funnel/stage-table.tsx", "action": "add" },
8
+ { "src": "components/chart-helpers.ts", "dest": "components/funnel/chart-helpers.ts", "action": "add" },
9
+ { "src": "lib/mock-data.ts", "dest": "components/funnel/mock-data.ts", "action": "add", "relabel": false },
10
+ { "src": "lib/types.ts", "dest": "components/funnel/types.ts", "action": "add", "relabel": false }
11
+ ]
12
+ }
@@ -0,0 +1,251 @@
1
+ import { useState } from "react"
2
+ import { subDays } from "date-fns"
3
+ import { TrendingDown, TrendingUp, Users, Target } from "lucide-react"
4
+ import { definePage } from "@squadbase/vantage"
5
+ import {
6
+ DashboardCard,
7
+ DashboardCardHeader,
8
+ DashboardCardTitle,
9
+ DashboardCardAction,
10
+ DashboardCardContent,
11
+ DashboardCardPreset,
12
+ DateRangePicker,
13
+ EChart,
14
+ PageShell,
15
+ PageShellHeader,
16
+ PageShellHeading,
17
+ PageShellTitle,
18
+ PageShellDescription,
19
+ PageShellHeaderEnd,
20
+ PageShellContent,
21
+ Placeholder,
22
+ } from "@squadbase/vantage/components"
23
+ import type { DateRange, EChartsOption } from "@squadbase/vantage/components"
24
+ import { ToggleGroup, ToggleGroupItem } from "@squadbase/vantage/ui"
25
+
26
+ import { StageTable } from "./components/funnel/stage-table"
27
+ import { funnelStages, stageRows } from "./components/funnel/mock-data"
28
+ import type { FunnelStage } from "./components/funnel/types"
29
+
30
+ export const page = definePage({
31
+ title: "ファネル",
32
+ description:
33
+ "パイプライン中心のレイアウト。サマリ KPI、テーマ配色のファネルとステージ別サイドバー、ステージ別パフォーマンステーブル。",
34
+ })
35
+
36
+ const today = new Date()
37
+
38
+ const STAGE_NAMES = ["ステージ1", "ステージ2", "ステージ3", "ステージ4", "ステージ5"]
39
+
40
+ type DisplayMode = "count" | "percent"
41
+
42
+ function funnelOption(
43
+ data: FunnelStage[],
44
+ labels: string[],
45
+ display: DisplayMode,
46
+ ): EChartsOption {
47
+ const max = data[0]?.value ?? 0
48
+
49
+ return {
50
+ tooltip: { trigger: "item" },
51
+ series: [
52
+ {
53
+ type: "funnel",
54
+ orient: "horizontal",
55
+ funnelAlign: "center",
56
+ left: "2%",
57
+ right: "2%",
58
+ top: 16,
59
+ bottom: 64,
60
+ min: 0,
61
+ max,
62
+ sort: "descending",
63
+ gap: 4,
64
+ label: {
65
+ show: true,
66
+ position: "bottom",
67
+ formatter: (p: unknown) => {
68
+ const params = p as {
69
+ name: string
70
+ value: number
71
+ data: { conversionRate: number; share: number }
72
+ }
73
+ const main =
74
+ display === "percent"
75
+ ? `${params.data.share.toFixed(1)}%`
76
+ : params.value.toLocaleString("ja-JP")
77
+ return `{name|${params.name}}\n{value|${main}}`
78
+ },
79
+ rich: {
80
+ name: { fontSize: 12, fontWeight: 600, lineHeight: 16, align: "center" },
81
+ value: { fontSize: 11, opacity: 0.6, lineHeight: 14, align: "center" },
82
+ },
83
+ },
84
+ labelLine: { show: false },
85
+ // itemStyle.color も option.color も持たせない: ステージは --chart-1..5、
86
+ // ラベルは文字色トークンで描かれ、テーマ差し替えに追従する (v0.2.1〜)。
87
+ data: data.map((d, i) => ({
88
+ name: labels[i] ?? "",
89
+ value: d.value,
90
+ conversionRate: d.conversionRate,
91
+ share: (d.value / Math.max(1, max)) * 100,
92
+ })),
93
+ },
94
+ ] as EChartsOption["series"],
95
+ }
96
+ }
97
+
98
+ const summaryCards = [
99
+ {
100
+ label: "流入数",
101
+ value: "124,500",
102
+ sub: "ファネルへの総流入数",
103
+ icon: <Users className="size-4 text-muted-foreground" />,
104
+ },
105
+ {
106
+ label: "コンバージョン数",
107
+ value: "1,186",
108
+ sub: "完了したコンバージョン",
109
+ icon: <Target className="size-4 text-muted-foreground" />,
110
+ },
111
+ {
112
+ label: "最良ステップ",
113
+ value: "31.9%",
114
+ sub: "最も高いステップ転換率",
115
+ icon: <TrendingUp className="size-4 text-emerald-600" />,
116
+ },
117
+ {
118
+ label: "最大の離脱",
119
+ value: "68.2%",
120
+ sub: "最大のステップ離脱率",
121
+ icon: <TrendingDown className="size-4 text-rose-600" />,
122
+ },
123
+ ]
124
+
125
+ export default function HomePage() {
126
+ const [dateRange, setDateRange] = useState<DateRange>({
127
+ from: subDays(today, 29),
128
+ to: today,
129
+ })
130
+ const [display, setDisplay] = useState<DisplayMode>("count")
131
+
132
+ return (
133
+ <PageShell>
134
+ <PageShellHeader>
135
+ <PageShellHeading>
136
+ <PageShellTitle>[テンプレート] コンバージョンファネル</PageShellTitle>
137
+ <PageShellDescription>
138
+ 選択期間のステージ別コンバージョン。
139
+ </PageShellDescription>
140
+ </PageShellHeading>
141
+ <PageShellHeaderEnd className="flex-row items-center gap-2">
142
+ {/* Base UI の ToggleGroup は常に配列。単一選択は長さ 1 に保つ。 */}
143
+ <ToggleGroup
144
+ value={[display]}
145
+ onValueChange={(value) => {
146
+ const next = value[0]
147
+ if (next) setDisplay(next as DisplayMode)
148
+ }}
149
+ variant="outline"
150
+ size="sm"
151
+ >
152
+ <ToggleGroupItem value="count">選択肢A</ToggleGroupItem>
153
+ <ToggleGroupItem value="percent">選択肢B</ToggleGroupItem>
154
+ </ToggleGroup>
155
+ <DateRangePicker
156
+ value={dateRange}
157
+ onChange={setDateRange}
158
+ maxDate={today}
159
+ />
160
+ </PageShellHeaderEnd>
161
+ </PageShellHeader>
162
+
163
+ <PageShellContent className="space-y-6">
164
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
165
+ {summaryCards.map(({ label, value, sub, icon }) => (
166
+ <DashboardCard key={label}>
167
+ <DashboardCardHeader>
168
+ <DashboardCardTitle className="text-muted-foreground">
169
+ {label}
170
+ </DashboardCardTitle>
171
+ <DashboardCardAction>{icon}</DashboardCardAction>
172
+ </DashboardCardHeader>
173
+ <DashboardCardContent>
174
+ <Placeholder className="text-2xl font-bold">{value}</Placeholder>
175
+ <p className="mt-2 text-xs text-muted-foreground">{sub}</p>
176
+ </DashboardCardContent>
177
+ </DashboardCard>
178
+ ))}
179
+ </div>
180
+
181
+ <div className="grid gap-4 lg:grid-cols-3">
182
+ <div className="lg:col-span-2">
183
+ <DashboardCardPreset
184
+ title="コンバージョンファネル"
185
+ description="ステージ別コンバージョン"
186
+ >
187
+ <EChart
188
+ option={funnelOption(funnelStages, STAGE_NAMES, display)}
189
+ height="440px"
190
+ />
191
+ </DashboardCardPreset>
192
+ </div>
193
+ <DashboardCardPreset title="ステージ" description="ステージ別コンバージョン">
194
+ <StagesSidebar />
195
+ </DashboardCardPreset>
196
+ </div>
197
+
198
+ <DashboardCardPreset
199
+ title="ステージ内訳"
200
+ description="ステージ間のコンバージョン"
201
+ >
202
+ <StageTable data={stageRows} />
203
+ </DashboardCardPreset>
204
+ </PageShellContent>
205
+ </PageShell>
206
+ )
207
+ }
208
+
209
+ function StagesSidebar() {
210
+ return (
211
+ <ol className="space-y-4">
212
+ {funnelStages.map((s, i) => {
213
+ const prev = i > 0 ? funnelStages[i - 1] : null
214
+ const dropoffPct = prev ? ((prev.value - s.value) / prev.value) * 100 : 0
215
+ return (
216
+ <li key={i} className="space-y-2">
217
+ <div className="flex items-baseline justify-between gap-2">
218
+ <div className="flex items-baseline gap-2 min-w-0">
219
+ <span className="text-xs font-semibold text-muted-foreground tabular-nums">
220
+ {String(i + 1).padStart(2, "0")}
221
+ </span>
222
+ <span className="truncate text-sm font-medium">{STAGE_NAMES[i]}</span>
223
+ </div>
224
+ <Placeholder className="text-sm font-medium">
225
+ {s.value.toLocaleString("ja-JP")}
226
+ </Placeholder>
227
+ </div>
228
+ <div className="grid grid-cols-2 gap-3 text-xs text-muted-foreground">
229
+ <span>転換率</span>
230
+ <div className="flex justify-end">
231
+ <Placeholder>{`${s.conversionRate.toFixed(1)}%`}</Placeholder>
232
+ </div>
233
+ </div>
234
+ {prev && (
235
+ <div className="flex h-1.5 overflow-hidden rounded-full bg-muted">
236
+ <div
237
+ className="h-full bg-chart-1"
238
+ style={{ width: `${s.conversionRate.toFixed(1)}%` }}
239
+ />
240
+ <div
241
+ className="h-full bg-muted-foreground/30"
242
+ style={{ width: `${dropoffPct.toFixed(1)}%` }}
243
+ />
244
+ </div>
245
+ )}
246
+ </li>
247
+ )
248
+ })}
249
+ </ol>
250
+ )
251
+ }