@squadbase/vantage-template 0.1.0 → 0.1.1-dev1.0eaa1f0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +9 -7
- package/base-template/.squadbase/skills/vantage-app/SKILL.md +4 -0
- package/base-template/.squadbase/skills/vantage-design/SKILL.md +3 -1
- package/base-template/.squadbase/skills/vantage-design/references/format.md +1 -1
- package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +45 -1
- package/base-template/.squadbase/skills/vantage-slide/SKILL.md +281 -0
- package/base-template/.squadbase/skills/vantage-slide/references/morph.md +102 -0
- package/base-template/.squadbase/skills/vantage-slide/references/steps.md +57 -0
- package/base-template/.squadbase/skills/vantage-slide/references/tokens.md +87 -0
- package/base-template/.squadbase/skills/vantage-slide/references/transitions.md +196 -0
- package/base-template/AGENTS.md +164 -1
- package/base-template/DESIGN.md +1 -1
- package/base-template/agents/api.md +29 -0
- package/base-template/agents/csv.md +2 -1
- package/base-template/agents/doc.md +6 -3
- package/base-template/agents/page.md +22 -8
- package/base-template/agents/slide.md +13 -1
- package/base-template/agents/xlsx.md +2 -1
- package/base-template/bun.lock +917 -0
- package/base-template/package.json +6 -6
- package/base-template/squadbase.yml +1 -0
- package/base-template/src/_layout.tsx +12 -10
- package/base-template/src/components/build-recovery.tsx +37 -1
- package/base-template/src/components/user-card.tsx +10 -10
- package/base-template/src/index.tsx +8 -3
- package/base-template/src/styles.css +9 -0
- package/dist/index.js +15 -6
- package/package.json +2 -2
- package/ui-templates/CLAUDE.md +2 -0
- package/ui-templates/kpi-chart-simple/pages/index.tsx +48 -48
- package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +48 -48
- package/base-template/package-lock.json +0 -5841
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@squadbase/react": "^0.1.0",
|
|
15
|
-
"@squadbase/vantage": "0.6.
|
|
16
|
-
"@squadbase/vantage-sdk": "0.1.1",
|
|
15
|
+
"@squadbase/vantage": "0.6.2-dev1.42c4e48",
|
|
16
|
+
"@squadbase/vantage-sdk": "0.1.1-dev1.28afad5",
|
|
17
17
|
"date-fns": "^4.1.0",
|
|
18
18
|
"lucide-react": "^0.468.0",
|
|
19
|
-
"react": "^19.
|
|
20
|
-
"react-dom": "^19.
|
|
19
|
+
"react": "^19.3.1",
|
|
20
|
+
"react-dom": "^19.3.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^22.15.0",
|
|
24
|
-
"@types/react": "^19.
|
|
25
|
-
"@types/react-dom": "^19.
|
|
24
|
+
"@types/react": "^19.3.0",
|
|
25
|
+
"@types/react-dom": "^19.3.0",
|
|
26
26
|
"typescript": "~5.7.0"
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import type { ComponentProps } from "react"
|
|
2
2
|
import { SquadbaseProvider } from "@squadbase/react"
|
|
3
|
-
import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router"
|
|
3
|
+
import { Link, Outlet, useCurrentRoute, useNavigate, useRoutes } from "@squadbase/vantage/router"
|
|
4
4
|
import { AppShell } from "@squadbase/vantage/components"
|
|
5
5
|
import type { NavGroup } from "@squadbase/vantage/components"
|
|
6
6
|
|
|
7
7
|
import { BuildRecovery } from "./components/build-recovery"
|
|
8
8
|
import { UserCard } from "./components/user-card"
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* `AppShell` is router-agnostic and renders nav entries through whatever
|
|
12
|
-
* component it is handed, using an `href` prop. Adapt it to the Vantage router
|
|
13
|
-
* so navigation stays client-side.
|
|
14
|
-
*/
|
|
15
|
-
function RouterLink({ href, ...props }: { href: string } & ComponentProps<"a">) {
|
|
16
|
-
return <Link to={href} {...props} />
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
/**
|
|
20
11
|
* The nav *is* the route list — adding a page file puts it in the nav, with no
|
|
21
12
|
* second list to keep in sync. Dynamic routes (`sales/[id].tsx`) have no single
|
|
@@ -50,6 +41,7 @@ function useNavGroups(): NavGroup[] {
|
|
|
50
41
|
* 「Something went wrong」のままリロードするまで残る。
|
|
51
42
|
*/
|
|
52
43
|
export default function RootLayout() {
|
|
44
|
+
const navigate = useNavigate()
|
|
53
45
|
return (
|
|
54
46
|
<SquadbaseProvider>
|
|
55
47
|
<AppShell
|
|
@@ -57,6 +49,7 @@ export default function RootLayout() {
|
|
|
57
49
|
groups={useNavGroups()}
|
|
58
50
|
actions={<UserCard />}
|
|
59
51
|
linkComponent={RouterLink}
|
|
52
|
+
onNavigate={(href) => navigate({ to: href })}
|
|
60
53
|
>
|
|
61
54
|
<BuildRecovery>
|
|
62
55
|
<Outlet />
|
|
@@ -65,3 +58,12 @@ export default function RootLayout() {
|
|
|
65
58
|
</SquadbaseProvider>
|
|
66
59
|
)
|
|
67
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `AppShell` is router-agnostic and renders nav entries through whatever
|
|
64
|
+
* component it is handed, using an `href` prop. Adapt it to the Vantage router
|
|
65
|
+
* so navigation stays client-side.
|
|
66
|
+
*/
|
|
67
|
+
function RouterLink({ href, ...props }: { href: string } & ComponentProps<"a">) {
|
|
68
|
+
return <Link to={href} {...props} />
|
|
69
|
+
}
|
|
@@ -29,6 +29,12 @@ import { Button, Spinner } from "@squadbase/vantage/ui"
|
|
|
29
29
|
*/
|
|
30
30
|
const FRAMEWORK_ERROR_SELECTOR = '[role="alert"] h2'
|
|
31
31
|
|
|
32
|
+
/** `import.meta.hot` のうち、ここで使う分だけ。 */
|
|
33
|
+
interface ViteHot {
|
|
34
|
+
on(event: string, cb: () => void): void
|
|
35
|
+
off?(event: string, cb: () => void): void
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
/** 書き込みの連鎖が落ち着くのを待ってからリロードする。 */
|
|
33
39
|
const RELOAD_DELAY_MS = 1200
|
|
34
40
|
|
|
@@ -43,6 +49,21 @@ const SETTLE_MS = 5000
|
|
|
43
49
|
|
|
44
50
|
const RELOAD_COUNT_KEY = "vantage:build-recovery-reloads"
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 書きかけのファイルを下書きとして表示している間、Vantage のクライアントが立てる目印。
|
|
54
|
+
* 下書き中の描画エラーはフレームワークが直前に通った版へ戻すので、ここでは覆わず、
|
|
55
|
+
* リロードもしない(リロードすると下書きの表示ごと消える)。
|
|
56
|
+
*/
|
|
57
|
+
declare global {
|
|
58
|
+
interface Window {
|
|
59
|
+
__VANTAGE_DRAFT__?: { active: boolean }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isDraftActive(): boolean {
|
|
64
|
+
return window.__VANTAGE_DRAFT__?.active === true
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
/** コピーしたことが伝わればいいので、確認表示はすぐ元に戻す。 */
|
|
47
68
|
const COPIED_FEEDBACK_MS = 2000
|
|
48
69
|
|
|
@@ -90,6 +111,7 @@ export function RecoveringState({ error }: { error?: unknown }) {
|
|
|
90
111
|
const attempts = readReloadCount()
|
|
91
112
|
if (attempts < MAX_RELOADS) {
|
|
92
113
|
const timer = setTimeout(() => {
|
|
114
|
+
if (isDraftActive()) return
|
|
93
115
|
writeReloadCount(attempts + 1)
|
|
94
116
|
window.location.reload()
|
|
95
117
|
}, RELOAD_DELAY_MS)
|
|
@@ -181,6 +203,11 @@ export function BuildRecovery({ children }: { children: ReactNode }) {
|
|
|
181
203
|
const host = hostRef.current
|
|
182
204
|
if (!host) return
|
|
183
205
|
const sync = () => {
|
|
206
|
+
// 下書きの表示中は覆わない。終わった時点でエラー画面が残っていれば、次の sync で覆う。
|
|
207
|
+
if (isDraftActive()) {
|
|
208
|
+
setMessage(null)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
184
211
|
const heading = host.querySelector(FRAMEWORK_ERROR_SELECTOR)
|
|
185
212
|
// 覆って見えなくする以上、支援技術にも読ませない。`role="alert"` のままだと
|
|
186
213
|
// 「Something went wrong」がそのまま読み上げられてしまう。属性は observe の
|
|
@@ -192,7 +219,16 @@ export function BuildRecovery({ children }: { children: ReactNode }) {
|
|
|
192
219
|
sync()
|
|
193
220
|
const observer = new MutationObserver(sync)
|
|
194
221
|
observer.observe(host, { childList: true, subtree: true })
|
|
195
|
-
|
|
222
|
+
// 下書きの有無が変わったときに見直す(目印の更新を待つため 1 tick 遅らせる)。
|
|
223
|
+
const onDraftState = () => setTimeout(sync, 0)
|
|
224
|
+
// 生成される `ImportMeta` は `env` しか宣言しない(HMR の型を入れるとアプリが vite/client を
|
|
225
|
+
// 入れたときに二重宣言になる)ので、ここだけ局所キャストで読む。
|
|
226
|
+
const hot = (import.meta as unknown as { hot?: ViteHot }).hot
|
|
227
|
+
hot?.on("vantage:draft-state", onDraftState)
|
|
228
|
+
return () => {
|
|
229
|
+
observer.disconnect()
|
|
230
|
+
hot?.off?.("vantage:draft-state", onDraftState)
|
|
231
|
+
}
|
|
196
232
|
}, [])
|
|
197
233
|
|
|
198
234
|
const crashed = message !== null
|
|
@@ -11,6 +11,16 @@ import {
|
|
|
11
11
|
cn,
|
|
12
12
|
} from "@squadbase/vantage/ui"
|
|
13
13
|
|
|
14
|
+
export function UserCard() {
|
|
15
|
+
const userState = useUser()
|
|
16
|
+
|
|
17
|
+
if (userState.status === "success") {
|
|
18
|
+
return <UserInfo user={userState.data} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <UserInfoFallback />
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* Squadbase-authenticated user chip for the app shell.
|
|
16
26
|
*
|
|
@@ -120,13 +130,3 @@ function UserInfoFallback() {
|
|
|
120
130
|
</div>
|
|
121
131
|
)
|
|
122
132
|
}
|
|
123
|
-
|
|
124
|
-
export function UserCard() {
|
|
125
|
-
const userState = useUser()
|
|
126
|
-
|
|
127
|
-
if (userState.status === "success") {
|
|
128
|
-
return <UserInfo user={userState.data} />
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return <UserInfoFallback />
|
|
132
|
-
}
|
|
@@ -17,6 +17,7 @@ export const page = definePage({
|
|
|
17
17
|
title: "Dashboard",
|
|
18
18
|
description: "Sample data — replace with your own to get started.",
|
|
19
19
|
navLabel: "Home",
|
|
20
|
+
placeholder: true,
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
const METRICS = [
|
|
@@ -65,14 +66,18 @@ const CHARTS = [
|
|
|
65
66
|
]
|
|
66
67
|
|
|
67
68
|
export default function HomePage() {
|
|
69
|
+
// `data-vantage-sample` marks this page as the untouched template sample (the coding
|
|
70
|
+
// agent looks for it). Remove it together with the sample content.
|
|
71
|
+
const sampleNote = navigator.language.startsWith("ja")
|
|
72
|
+
? "サンプルデータです。お手元のデータに置き換えて始めましょう。"
|
|
73
|
+
: "Sample data — replace with your own to get started."
|
|
74
|
+
|
|
68
75
|
return (
|
|
69
76
|
<PageShell>
|
|
70
77
|
<PageShellHeader>
|
|
71
78
|
<PageShellHeading>
|
|
72
79
|
<PageShellTitle>Dashboard</PageShellTitle>
|
|
73
|
-
<PageShellDescription>
|
|
74
|
-
Sample data — replace with your own to get started.
|
|
75
|
-
</PageShellDescription>
|
|
80
|
+
<PageShellDescription data-vantage-sample="home">{sampleNote}</PageShellDescription>
|
|
76
81
|
</PageShellHeading>
|
|
77
82
|
</PageShellHeader>
|
|
78
83
|
|
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
/* --radius: 0.625rem; */
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/* Page typeface. Latin system faces first, then Japanese system faces so kana and
|
|
21
|
+
kanji do not fall back to a thin locale default. Put a Latin webfont in front if
|
|
22
|
+
you add one, but keep the Japanese families. */
|
|
23
|
+
body {
|
|
24
|
+
font-family:
|
|
25
|
+
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Hiragino Sans",
|
|
26
|
+
"Hiragino Kaku Gothic ProN", "Noto Sans JP", "Yu Gothic UI", Meiryo, sans-serif;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/* vantage-template:chart-preset — regenerated by `chart`; edits here are lost */
|
|
21
30
|
:root {
|
|
22
31
|
--chart-1: var(--color-blue-600);
|
package/dist/index.js
CHANGED
|
@@ -1022,27 +1022,36 @@ function initProject(options) {
|
|
|
1022
1022
|
applyChartPreset(targetDir, options.chart);
|
|
1023
1023
|
log("cyan", ` chart preset: ${options.chart}`);
|
|
1024
1024
|
}
|
|
1025
|
+
const packageManager = isBunAvailable() ? "bun" : "npm";
|
|
1025
1026
|
if (options.skipInstall) {
|
|
1026
1027
|
log("green", "\nProject initialized successfully!");
|
|
1027
1028
|
log("dim", "\nNext steps:");
|
|
1028
|
-
log("dim",
|
|
1029
|
-
log("dim",
|
|
1029
|
+
log("dim", ` ${packageManager} install`);
|
|
1030
|
+
log("dim", ` ${packageManager} run dev`);
|
|
1030
1031
|
return;
|
|
1031
1032
|
}
|
|
1032
|
-
log("green",
|
|
1033
|
-
|
|
1033
|
+
log("green", `
|
|
1034
|
+
Installing dependencies with ${packageManager}...`);
|
|
1035
|
+
const install = spawnSync(packageManager, ["install"], {
|
|
1034
1036
|
cwd: targetDir,
|
|
1035
1037
|
stdio: "inherit",
|
|
1036
1038
|
shell: process.platform === "win32"
|
|
1037
1039
|
});
|
|
1038
1040
|
if (install.error || install.status !== 0) {
|
|
1039
1041
|
log("red", "\nDependency installation failed.");
|
|
1040
|
-
log("yellow",
|
|
1042
|
+
log("yellow", `Run '${packageManager} install' manually in the project directory.`);
|
|
1041
1043
|
process.exit(1);
|
|
1042
1044
|
}
|
|
1043
1045
|
log("green", "\nProject initialized successfully!");
|
|
1044
1046
|
log("dim", "\nNext steps:");
|
|
1045
|
-
log("dim",
|
|
1047
|
+
log("dim", ` ${packageManager} run dev`);
|
|
1048
|
+
}
|
|
1049
|
+
function isBunAvailable() {
|
|
1050
|
+
const probe = spawnSync("bun", ["--version"], {
|
|
1051
|
+
stdio: "ignore",
|
|
1052
|
+
shell: process.platform === "win32"
|
|
1053
|
+
});
|
|
1054
|
+
return !probe.error && probe.status === 0;
|
|
1046
1055
|
}
|
|
1047
1056
|
|
|
1048
1057
|
// src/commands/list.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squadbase/vantage-template",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-dev1.0eaa1f0",
|
|
4
4
|
"description": "CLI tool for initializing and customizing Squadbase Vantage projects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"squadbase",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"type-check": "tsc",
|
|
43
43
|
"agents": "node scripts/build-agents.cjs",
|
|
44
44
|
"agents:check": "node scripts/build-agents.cjs --check",
|
|
45
|
-
"template:install": "
|
|
45
|
+
"template:install": "bun install --cwd template --frozen-lockfile",
|
|
46
46
|
"template:build": "npm --prefix template run build",
|
|
47
47
|
"prepublishOnly": "npm run agents:check && npm run build",
|
|
48
48
|
"release": "npm publish --@squadbase:registry=https://registry.npmjs.org",
|
package/ui-templates/CLAUDE.md
CHANGED
|
@@ -96,6 +96,8 @@ server/api/<endpoint>.ts # ★ サンプルデータ + GET ハ
|
|
|
96
96
|
| --- | --- |
|
|
97
97
|
| `PageShell*` / `DashboardCard*` / `EChart` / `EChartsOption` / `DataTable*` / `ColumnDef` / `DateRangePicker` / `FilterBar*` / `FunnelSteps` / `MetricValue` / `TrendIndicator` / `StatusBadge` / `SegmentedControl` / `MultiSelect` / `SearchableSelect` / `SectionHeader` / `AppShell` / `Placeholder` / `Sparkline` | `@squadbase/vantage/components` |
|
|
98
98
|
| `Button` / `Input` / `Badge` / `Tabs*` / `ToggleGroup*` / `Table*` / `Select*` / `Dialog*` / `cn` ほか shadcn プリミティブ | `@squadbase/vantage/ui` |
|
|
99
|
+
|
|
100
|
+
型付きチャート(`LineChart` / `AreaChart` / `BarChart` / `DonutChart` / `BarList` / `CategoryBar` / `ProgressCircle` / `Tracker`、`@squadbase/vantage/components`)と `Avatar` / `Pagination` / `RadioGroup` / `AlertDialog` / `Drawer` / `Toaster` / `Field` / `Combobox` / `HoverCard` / `Kbd`(`@squadbase/vantage/ui`)は 0.6.3 で入った。雛形の依存が `^0.5.3` の間は使わない(上の EChart の書き方のまま)。
|
|
99
101
|
| `definePage` | `@squadbase/vantage` |
|
|
100
102
|
| `Link` / `Outlet` / `useParams` / `useSearch` / `useNavigate` / `useRoutes` / `useCurrentRoute` / `useSearchParam` / `useSearchState` | `@squadbase/vantage/router` |
|
|
101
103
|
| `useApiQuery` / `useApiMutation` / `useQuery` / `useMutation` / `apiFetch` | `@squadbase/vantage/query` |
|
|
@@ -104,54 +104,6 @@ const RANGE_OPTIONS: SegmentOption[] = [
|
|
|
104
104
|
{ label: "90 days", value: "90d" },
|
|
105
105
|
]
|
|
106
106
|
|
|
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
107
|
export default function HomePage() {
|
|
156
108
|
// Filter state belongs in the URL rather than `useState`: it survives a
|
|
157
109
|
// reload, and a pasted link reproduces exactly what you were looking at.
|
|
@@ -198,3 +150,51 @@ export default function HomePage() {
|
|
|
198
150
|
</PageShell>
|
|
199
151
|
)
|
|
200
152
|
}
|
|
153
|
+
|
|
154
|
+
function DashboardBody({ data }: { data: KpiSummary }) {
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
158
|
+
{data.kpis.map((kpi) => {
|
|
159
|
+
const { label, icon: Icon, format } = KPI_META[kpi.id]
|
|
160
|
+
return (
|
|
161
|
+
<DashboardCard key={kpi.id} data-sq-ref={kpi.id}>
|
|
162
|
+
<DashboardCardHeader>
|
|
163
|
+
<DashboardCardTitle className="text-muted-foreground">
|
|
164
|
+
{label}
|
|
165
|
+
</DashboardCardTitle>
|
|
166
|
+
<DashboardCardAction>
|
|
167
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
168
|
+
</DashboardCardAction>
|
|
169
|
+
</DashboardCardHeader>
|
|
170
|
+
<DashboardCardContent>
|
|
171
|
+
<Placeholder className="text-2xl font-bold">
|
|
172
|
+
{format(kpi.value)}
|
|
173
|
+
</Placeholder>
|
|
174
|
+
<div className="mt-2">
|
|
175
|
+
<Placeholder className="text-sm font-medium">
|
|
176
|
+
{kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
|
|
177
|
+
</Placeholder>
|
|
178
|
+
</div>
|
|
179
|
+
<Sparkline
|
|
180
|
+
data={kpi.sparklineData.map((v) => ({ value: v }))}
|
|
181
|
+
height={32}
|
|
182
|
+
area
|
|
183
|
+
className="mt-3"
|
|
184
|
+
/>
|
|
185
|
+
</DashboardCardContent>
|
|
186
|
+
</DashboardCard>
|
|
187
|
+
)
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<DashboardCardPreset data-sq-ref="trend" title="Trend" description="Values over the selected period">
|
|
192
|
+
<EChart option={trendOption(data.trend)} height="320px" />
|
|
193
|
+
</DashboardCardPreset>
|
|
194
|
+
|
|
195
|
+
<DashboardCardPreset data-sq-ref="top-items" title="Top items" description="Ranked by value">
|
|
196
|
+
<TopItemsTable data={data.topItems} />
|
|
197
|
+
</DashboardCardPreset>
|
|
198
|
+
</>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
@@ -104,54 +104,6 @@ const RANGE_OPTIONS: SegmentOption[] = [
|
|
|
104
104
|
{ label: "90日", value: "90d" },
|
|
105
105
|
]
|
|
106
106
|
|
|
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
107
|
export default function HomePage() {
|
|
156
108
|
// 絞り込み状態は useState ではなく URL に置く。リロードで消えず、URL を
|
|
157
109
|
// そのまま共有すれば相手も同じ画面を見られる。
|
|
@@ -198,3 +150,51 @@ export default function HomePage() {
|
|
|
198
150
|
</PageShell>
|
|
199
151
|
)
|
|
200
152
|
}
|
|
153
|
+
|
|
154
|
+
function DashboardBody({ data }: { data: KpiSummary }) {
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
158
|
+
{data.kpis.map((kpi) => {
|
|
159
|
+
const { label, icon: Icon, format } = KPI_META[kpi.id]
|
|
160
|
+
return (
|
|
161
|
+
<DashboardCard key={kpi.id} data-sq-ref={kpi.id}>
|
|
162
|
+
<DashboardCardHeader>
|
|
163
|
+
<DashboardCardTitle className="text-muted-foreground">
|
|
164
|
+
{label}
|
|
165
|
+
</DashboardCardTitle>
|
|
166
|
+
<DashboardCardAction>
|
|
167
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
168
|
+
</DashboardCardAction>
|
|
169
|
+
</DashboardCardHeader>
|
|
170
|
+
<DashboardCardContent>
|
|
171
|
+
<Placeholder className="text-2xl font-bold">
|
|
172
|
+
{format(kpi.value)}
|
|
173
|
+
</Placeholder>
|
|
174
|
+
<div className="mt-2">
|
|
175
|
+
<Placeholder className="text-sm font-medium">
|
|
176
|
+
{kpi.change >= 0 ? `+${kpi.change}%` : `${kpi.change}%`}
|
|
177
|
+
</Placeholder>
|
|
178
|
+
</div>
|
|
179
|
+
<Sparkline
|
|
180
|
+
data={kpi.sparklineData.map((v) => ({ value: v }))}
|
|
181
|
+
height={32}
|
|
182
|
+
area
|
|
183
|
+
className="mt-3"
|
|
184
|
+
/>
|
|
185
|
+
</DashboardCardContent>
|
|
186
|
+
</DashboardCard>
|
|
187
|
+
)
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<DashboardCardPreset data-sq-ref="trend" title="トレンド" description="選択期間の値">
|
|
192
|
+
<EChart option={trendOption(data.trend)} height="320px" />
|
|
193
|
+
</DashboardCardPreset>
|
|
194
|
+
|
|
195
|
+
<DashboardCardPreset data-sq-ref="top-items" title="上位項目" description="値で並べ替え">
|
|
196
|
+
<TopItemsTable data={data.topItems} />
|
|
197
|
+
</DashboardCardPreset>
|
|
198
|
+
</>
|
|
199
|
+
)
|
|
200
|
+
}
|