@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.
Files changed (33) hide show
  1. package/README.md +7 -7
  2. package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +9 -7
  3. package/base-template/.squadbase/skills/vantage-app/SKILL.md +4 -0
  4. package/base-template/.squadbase/skills/vantage-design/SKILL.md +3 -1
  5. package/base-template/.squadbase/skills/vantage-design/references/format.md +1 -1
  6. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +45 -1
  7. package/base-template/.squadbase/skills/vantage-slide/SKILL.md +281 -0
  8. package/base-template/.squadbase/skills/vantage-slide/references/morph.md +102 -0
  9. package/base-template/.squadbase/skills/vantage-slide/references/steps.md +57 -0
  10. package/base-template/.squadbase/skills/vantage-slide/references/tokens.md +87 -0
  11. package/base-template/.squadbase/skills/vantage-slide/references/transitions.md +196 -0
  12. package/base-template/AGENTS.md +164 -1
  13. package/base-template/DESIGN.md +1 -1
  14. package/base-template/agents/api.md +29 -0
  15. package/base-template/agents/csv.md +2 -1
  16. package/base-template/agents/doc.md +6 -3
  17. package/base-template/agents/page.md +22 -8
  18. package/base-template/agents/slide.md +13 -1
  19. package/base-template/agents/xlsx.md +2 -1
  20. package/base-template/bun.lock +917 -0
  21. package/base-template/package.json +6 -6
  22. package/base-template/squadbase.yml +1 -0
  23. package/base-template/src/_layout.tsx +12 -10
  24. package/base-template/src/components/build-recovery.tsx +37 -1
  25. package/base-template/src/components/user-card.tsx +10 -10
  26. package/base-template/src/index.tsx +8 -3
  27. package/base-template/src/styles.css +9 -0
  28. package/dist/index.js +15 -6
  29. package/package.json +2 -2
  30. package/ui-templates/CLAUDE.md +2 -0
  31. package/ui-templates/kpi-chart-simple/pages/index.tsx +48 -48
  32. package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +48 -48
  33. 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.1",
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.2.7",
20
- "react-dom": "^19.2.7"
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.2.0",
25
- "@types/react-dom": "^19.2.0",
24
+ "@types/react": "^19.3.0",
25
+ "@types/react-dom": "^19.3.0",
26
26
  "typescript": "~5.7.0"
27
27
  }
28
28
  }
@@ -3,3 +3,4 @@ version: "1"
3
3
  build:
4
4
  framework: squadbase-vantage
5
5
  runtime: node20
6
+ package_manager: bun
@@ -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
- return () => observer.disconnect()
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", " npm install");
1029
- log("dim", " npm run dev");
1029
+ log("dim", ` ${packageManager} install`);
1030
+ log("dim", ` ${packageManager} run dev`);
1030
1031
  return;
1031
1032
  }
1032
- log("green", "\nInstalling dependencies...");
1033
- const install = spawnSync("npm", ["install"], {
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", "Run 'npm install' manually in the project directory.");
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", " npm run dev");
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.0",
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": "npm --prefix template ci",
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",
@@ -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
+ }