@squadbase/vantage 0.1.0 → 0.1.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 (46) hide show
  1. package/README.md +4 -1
  2. package/dist/{chunk-PRFBSQA4.js → chunk-5L2MH2NG.js} +9 -4
  3. package/dist/chunk-5L2MH2NG.js.map +1 -0
  4. package/dist/chunk-7IPAXPPY.js +51 -0
  5. package/dist/chunk-7IPAXPPY.js.map +1 -0
  6. package/dist/chunk-DTDVSFRY.js +29 -0
  7. package/dist/chunk-DTDVSFRY.js.map +1 -0
  8. package/dist/cli.js +1 -1
  9. package/dist/client/index.d.ts +2 -1
  10. package/dist/client/index.js +3 -1
  11. package/dist/client/index.js.map +1 -1
  12. package/dist/components/index.d.ts +23 -2
  13. package/dist/components/index.js +115 -1
  14. package/dist/components/index.js.map +1 -1
  15. package/dist/{define-page-B6y9TOfZ.d.ts → define-page-BfhrK99G.d.ts} +5 -0
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/query/index.d.ts +69 -2
  20. package/dist/query/index.js +85 -2
  21. package/dist/query/index.js.map +1 -1
  22. package/dist/router/index.d.ts +86 -1
  23. package/dist/router/index.js +71 -7
  24. package/dist/router/index.js.map +1 -1
  25. package/dist/vite/index.js +1 -1
  26. package/docs/en/components.md +2 -0
  27. package/docs/en/data-fetching.md +77 -13
  28. package/docs/en/pages-and-metadata.md +6 -3
  29. package/docs/en/parts/placeholder.md +38 -0
  30. package/docs/en/parts/sparkline.md +47 -0
  31. package/docs/en/routing.md +82 -0
  32. package/docs/index.json +50 -10
  33. package/docs/ja/components.md +2 -0
  34. package/docs/ja/data-fetching.md +76 -12
  35. package/docs/ja/pages-and-metadata.md +6 -3
  36. package/docs/ja/parts/placeholder.md +37 -0
  37. package/docs/ja/parts/sparkline.md +47 -0
  38. package/docs/ja/routing.md +80 -0
  39. package/package.json +1 -1
  40. package/skills/vantage-add-feature/SKILL.md +14 -11
  41. package/skills/vantage-app/SKILL.md +54 -21
  42. package/skills/vantage-pitfalls/SKILL.md +26 -2
  43. package/templates/AGENTS.md +62 -17
  44. package/dist/chunk-ATYZ45XL.js +0 -19
  45. package/dist/chunk-ATYZ45XL.js.map +0 -1
  46. package/dist/chunk-PRFBSQA4.js.map +0 -1
@@ -0,0 +1,38 @@
1
+ # Placeholder
2
+
3
+ > An inline marker for sample, not-yet-real data.
4
+
5
+ Marks a spot where real data isn't wired up yet. The muted colour plus `animate-pulse` says
6
+ **"this value is meant to be replaced"**, so a template never gets shared with sample numbers
7
+ mistaken for real ones.
8
+
9
+ ```tsx
10
+ import { Placeholder } from "@squadbase/vantage/components";
11
+ ```
12
+
13
+ | Prop | Type | Description |
14
+ | --- | --- | --- |
15
+ | `children` | `ReactNode` | The sample value — text or elements. |
16
+ | `className` | `string` | Override size and weight (e.g. `text-2xl font-medium`). |
17
+
18
+ Any remaining props go straight to the `<span>`.
19
+
20
+ ## Placeholder or Skeleton
21
+
22
+ | | Use for | Element |
23
+ | --- | --- | --- |
24
+ | [`Skeleton`](ui/skeleton) | **Loading** — it disappears when data arrives | block `<div>` |
25
+ | `Placeholder` | **A stand-in value** — it stays until you replace it | inline `<span>` |
26
+
27
+ Being inline, it is safe **inside a sentence** — in a `<p>` or a heading. `Skeleton` is a block
28
+ element and breaks the line if you mix it into running text.
29
+
30
+ ```tsx
31
+ <p>
32
+ Revenue this month was <Placeholder>$12,400</Placeholder>.
33
+ </p>
34
+ ```
35
+
36
+ > [!TIP]
37
+ > When you start from a template, searching for `Placeholder` gives you the checklist of everything
38
+ > still unwired.
@@ -0,0 +1,47 @@
1
+ # Sparkline
2
+
3
+ > An inline trend chart for table cells and KPI tiles.
4
+
5
+ Draws a sequence of numbers as a small line or bar chart — no axes, no legend. It is **a single
6
+ `<svg>`, not an `EChart`**, so dozens of them inside table cells or tiles stay cheap.
7
+
8
+ ```tsx
9
+ import { Sparkline } from "@squadbase/vantage/components";
10
+ import type { SparklineDataPoint, SparklineVariant } from "@squadbase/vantage/components";
11
+ ```
12
+
13
+ | Prop | Type | Default | Description |
14
+ | --- | --- | --- | --- |
15
+ | `data` | `SparklineDataPoint[]` | | An array of `{ value: number; label?: string }`. **Required.** An empty array draws nothing. |
16
+ | `variant` | `"line" \| "bar"` | `"line"` | Line or bars. |
17
+ | `height` | `number` | `40` | Height in px; the width fills the parent. |
18
+ | `color` | `string` | `"text-chart-1"` | A Tailwind **text-* class**; it drives stroke and fill through `currentColor`. |
19
+ | `area` | `boolean` | `false` | Fill under the line (`line` only). |
20
+ | `animate` | `boolean` | `false` | Draw in on mount. |
21
+ | `className` | `string` | | Classes to add. |
22
+
23
+ Any remaining props go straight to the `<svg>`.
24
+
25
+ > [!WARNING]
26
+ > `color` is a **class name**. Something like `color="#3b82f6"` does nothing — both the stroke and the
27
+ > fill read `currentColor`, so pass a utility such as `text-chart-2`. Change the theme tokens
28
+ > (`--chart-1` … `--chart-5`) and the colours follow.
29
+
30
+ ## Sizing
31
+
32
+ `preserveAspectRatio="none"` stretches the chart horizontally, so **the parent decides the width**.
33
+ Inside a table cell, constrain both.
34
+
35
+ ```tsx
36
+ <Sparkline data={points} height={16} className="w-20 shrink-0" />
37
+ ```
38
+
39
+ ## Sparkline or EChart
40
+
41
+ Reach for [`EChart`](parts/echart) when you need axes, a legend, tooltips or multiple
42
+ series. `Sparkline` conveys the *shape* of a series and nothing else — **no ticks, no numbers**.
43
+
44
+ > [!NOTE]
45
+ > It is `aria-hidden="true"`, so screen readers skip it. Put the actual number next to it as text —
46
+ > [`MetricValue`](parts/metric-value) or
47
+ > [`TrendIndicator`](parts/trend-indicator).
@@ -85,6 +85,88 @@ export default function RootLayout() {
85
85
 
86
86
  `_404.tsx` and `_error.tsx` are recognized at the **root only**.
87
87
 
88
+ ## Build a nav from the route list
89
+
90
+ `useRoutes()` returns every page route in the app. The file system is the source of truth, so adding
91
+ one page file adds one nav entry — there is no link array to maintain.
92
+
93
+ ```tsx
94
+ // _layout.tsx
95
+ import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router";
96
+
97
+ export default function RootLayout() {
98
+ // Dynamic routes (/sales/:customerId) have no single URL, so leave them out.
99
+ const routes = useRoutes().filter((route) => !route.dynamic);
100
+ const current = useCurrentRoute();
101
+
102
+ return (
103
+ <div>
104
+ <nav>
105
+ {routes.map((route) => (
106
+ <Link key={route.path} to={route.to} aria-current={route.path === current?.path}>
107
+ {route.label}
108
+ </Link>
109
+ ))}
110
+ </nav>
111
+ <Outlet />
112
+ </div>
113
+ );
114
+ }
115
+ ```
116
+
117
+ The order is Vantage's scan order — shallow before deep, static before dynamic, alphabetical — which
118
+ is the order a nav wants.
119
+
120
+ Each entry is a `RouteInfo`:
121
+
122
+ | Field | Type | Contents |
123
+ |---|---|---|
124
+ | `path` | `string` | Display path, `/sales/:customerId` |
125
+ | `to` | `string` | The form `Link`'s `to` expects, `/sales/$customerId` |
126
+ | `params` | `string[]` | Dynamic parameter names; a catch-all is `_splat` |
127
+ | `dynamic` | `boolean` | Whether the route has dynamic parameters |
128
+ | `index` | `boolean` | Whether it is a directory's index route |
129
+ | `label` | `string` | Display name: `navLabel`, else `title`, else `path` |
130
+ | `title` / `description` / `navLabel` | `string \| undefined` | Values from [`definePage`](pages-and-metadata) |
131
+
132
+ `useCurrentRoute()` returns the route being rendered, or `undefined` on the 404 route. Beyond the
133
+ "you are here" check above, it fits breadcrumbs and page headings.
134
+
135
+ > [!NOTE]
136
+ > `label` prefers `navLabel` because `title` drives `document.title` and therefore tends to carry the
137
+ > site name, which is usually too long for a nav.
138
+
139
+ ## Keep filter state in the URL
140
+
141
+ A dashboard's filters belong in the URL: the view survives a reload and can be pasted to a colleague.
142
+ `useSearchParam` reads and writes one search-param key with a `useState` shape.
143
+
144
+ ```tsx
145
+ import { useSearchParam } from "@squadbase/vantage/router";
146
+ import { SegmentedControl } from "@squadbase/vantage/components";
147
+
148
+ export default function Sales() {
149
+ const [region, setRegion] = useSearchParam("region", "all");
150
+
151
+ return <SegmentedControl options={REGIONS} value={region} onChange={setRegion} />;
152
+ }
153
+ ```
154
+
155
+ - The value is **always a string** (`?year=2024` parses as a number; this hook normalizes it back).
156
+ - Writing the **default value** (`"all"` above) or `null` removes the key from the URL.
157
+ - History defaults to `replace`, so filtering doesn't fill the back button. Pass `{ replace: false }`
158
+ to push instead.
159
+
160
+ For anything JSON-serializable — an array, an object — use `useSearchState`, which also accepts a
161
+ functional update.
162
+
163
+ ```tsx
164
+ import { useSearchState } from "@squadbase/vantage/router";
165
+
166
+ const [segments, setSegments] = useSearchState<string[]>("segments", []);
167
+ setSegments((prev) => [...prev, "enterprise"]);
168
+ ```
169
+
88
170
  ## HMR and route regeneration
89
171
 
90
172
  Adding or removing a `.tsx` page or a `server/api` file regenerates routes and triggers a full
package/docs/index.json CHANGED
@@ -592,6 +592,16 @@
592
592
  "file": "ja/parts/page-shell.md",
593
593
  "order": 9
594
594
  },
595
+ {
596
+ "slug": "parts/placeholder",
597
+ "lang": "ja",
598
+ "section": "components/parts",
599
+ "sectionTitle": "@squadbase/vantage/components",
600
+ "title": "Placeholder",
601
+ "description": "仮データであることを示す、インラインの目印。",
602
+ "file": "ja/parts/placeholder.md",
603
+ "order": 10
604
+ },
595
605
  {
596
606
  "slug": "parts/searchable-select",
597
607
  "lang": "ja",
@@ -600,7 +610,7 @@
600
610
  "title": "SearchableSelect",
601
611
  "description": "検索できる単一選択。候補が多いときに。",
602
612
  "file": "ja/parts/searchable-select.md",
603
- "order": 10
613
+ "order": 11
604
614
  },
605
615
  {
606
616
  "slug": "parts/section-header",
@@ -610,7 +620,7 @@
610
620
  "title": "SectionHeader",
611
621
  "description": "セクションの見出しとアクション。",
612
622
  "file": "ja/parts/section-header.md",
613
- "order": 11
623
+ "order": 12
614
624
  },
615
625
  {
616
626
  "slug": "parts/segmented-control",
@@ -620,7 +630,17 @@
620
630
  "title": "SegmentedControl",
621
631
  "description": "単一選択のセグメント。ToggleGroup の配列を隠す。",
622
632
  "file": "ja/parts/segmented-control.md",
623
- "order": 12
633
+ "order": 13
634
+ },
635
+ {
636
+ "slug": "parts/sparkline",
637
+ "lang": "ja",
638
+ "section": "components/parts",
639
+ "sectionTitle": "@squadbase/vantage/components",
640
+ "title": "Sparkline",
641
+ "description": "表のセルや KPI タイルに置く、インラインの推移グラフ。",
642
+ "file": "ja/parts/sparkline.md",
643
+ "order": 14
624
644
  },
625
645
  {
626
646
  "slug": "parts/status-badge",
@@ -630,7 +650,7 @@
630
650
  "title": "StatusBadge",
631
651
  "description": "ドット付きのステータスバッジ。色は名前で決まる。",
632
652
  "file": "ja/parts/status-badge.md",
633
- "order": 13
653
+ "order": 15
634
654
  },
635
655
  {
636
656
  "slug": "parts/trend-indicator",
@@ -640,7 +660,7 @@
640
660
  "title": "TrendIndicator",
641
661
  "description": "増減の矢印と変化率。良し悪しは positiveIsGood で決まる。",
642
662
  "file": "ja/parts/trend-indicator.md",
643
- "order": 14
663
+ "order": 16
644
664
  },
645
665
  {
646
666
  "slug": "markdown/markdown-renderer",
@@ -1242,6 +1262,16 @@
1242
1262
  "file": "en/parts/page-shell.md",
1243
1263
  "order": 9
1244
1264
  },
1265
+ {
1266
+ "slug": "parts/placeholder",
1267
+ "lang": "en",
1268
+ "section": "components/parts",
1269
+ "sectionTitle": "@squadbase/vantage/components",
1270
+ "title": "Placeholder",
1271
+ "description": "An inline marker for sample, not-yet-real data.",
1272
+ "file": "en/parts/placeholder.md",
1273
+ "order": 10
1274
+ },
1245
1275
  {
1246
1276
  "slug": "parts/searchable-select",
1247
1277
  "lang": "en",
@@ -1250,7 +1280,7 @@
1250
1280
  "title": "SearchableSelect",
1251
1281
  "description": "Single choice with search, for long option lists.",
1252
1282
  "file": "en/parts/searchable-select.md",
1253
- "order": 10
1283
+ "order": 11
1254
1284
  },
1255
1285
  {
1256
1286
  "slug": "parts/section-header",
@@ -1260,7 +1290,7 @@
1260
1290
  "title": "SectionHeader",
1261
1291
  "description": "A section heading with its actions.",
1262
1292
  "file": "en/parts/section-header.md",
1263
- "order": 11
1293
+ "order": 12
1264
1294
  },
1265
1295
  {
1266
1296
  "slug": "parts/segmented-control",
@@ -1270,7 +1300,17 @@
1270
1300
  "title": "SegmentedControl",
1271
1301
  "description": "Single-choice segments — it hides ToggleGroup's array.",
1272
1302
  "file": "en/parts/segmented-control.md",
1273
- "order": 12
1303
+ "order": 13
1304
+ },
1305
+ {
1306
+ "slug": "parts/sparkline",
1307
+ "lang": "en",
1308
+ "section": "components/parts",
1309
+ "sectionTitle": "@squadbase/vantage/components",
1310
+ "title": "Sparkline",
1311
+ "description": "An inline trend chart for table cells and KPI tiles.",
1312
+ "file": "en/parts/sparkline.md",
1313
+ "order": 14
1274
1314
  },
1275
1315
  {
1276
1316
  "slug": "parts/status-badge",
@@ -1280,7 +1320,7 @@
1280
1320
  "title": "StatusBadge",
1281
1321
  "description": "A dot-and-label status badge, coloured by name.",
1282
1322
  "file": "en/parts/status-badge.md",
1283
- "order": 13
1323
+ "order": 15
1284
1324
  },
1285
1325
  {
1286
1326
  "slug": "parts/trend-indicator",
@@ -1290,7 +1330,7 @@
1290
1330
  "title": "TrendIndicator",
1291
1331
  "description": "An arrow and a percentage; positiveIsGood decides the colour.",
1292
1332
  "file": "en/parts/trend-indicator.md",
1293
- "order": 14
1333
+ "order": 16
1294
1334
  },
1295
1335
  {
1296
1336
  "slug": "markdown/markdown-renderer",
@@ -124,6 +124,7 @@ shadcn/ui のプリミティブです。名前も props も shadcn/ui のもの
124
124
  | [`DataTable`](parts/data-table) | 並べ替え・検索・ページング・選択を備えた表 |
125
125
  | [`EChart`](parts/echart) | Apache ECharts のラッパー |
126
126
  | [`FunnelSteps`](parts/funnel-steps) | ファネルの棒表示 |
127
+ | [`Sparkline`](parts/sparkline) | セルやタイルに置くインラインの推移グラフ |
127
128
  | [`FilterBar`](parts/filter-bar) | フィルタ一式を 1 つの値で扱う行 |
128
129
  | [`DateRangePicker`](parts/date-range-picker) | プリセット付きの期間選択 |
129
130
  | [`SegmentedControl`](parts/segmented-control) | 単一選択のセグメント |
@@ -132,6 +133,7 @@ shadcn/ui のプリミティブです。名前も props も shadcn/ui のもの
132
133
  | [`MetricValue`](parts/metric-value) | KPI の数値と単位 |
133
134
  | [`TrendIndicator`](parts/trend-indicator) | 増減の矢印と変化率 |
134
135
  | [`StatusBadge`](parts/status-badge) | ドット付きステータスバッジ |
136
+ | [`Placeholder`](parts/placeholder) | 仮データであることを示すインラインの目印 |
135
137
 
136
138
  ## `@squadbase/vantage/markdown`
137
139
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  Vantage はサーバー状態のために **管理された TanStack Query** を同梱しています。`QueryClient` の
6
6
  セットアップは不要で、`@squadbase/vantage/query` から `useQuery` などを import するだけです。
7
+ 自分の `server/api` を呼ぶなら、後述の [`useApiQuery`](#自分の-api-を呼ぶ--useapiquery) が近道です。
7
8
 
8
9
  ```tsx
9
10
  import { useQuery } from "@squadbase/vantage/query";
@@ -39,6 +40,12 @@ Vantage が用意する `QueryClient` は 1 つだけで、次のデフォルト
39
40
  | `staleTime` | 30 秒 |
40
41
  | `retry` | 1 |
41
42
  | `refetchOnWindowFocus` | `false` |
43
+ | `networkMode` | `"always"`(queries / mutations とも) |
44
+
45
+ `networkMode: "always"` は、ブラウザのオフライン判定を無視して常にリクエストとリトライを行う
46
+ 設定です。既定の `"online"` だと、オフラインと判定された瞬間に失敗したクエリが *paused* に
47
+ なり、エラーにならないまま読み込み表示が残り続けます。ダッシュボードは自分のオリジン(または
48
+ 設定した API ベース)と話すので、その判定は当てになりません。
42
49
 
43
50
  カスタマイズは**クエリごとのオプションで上書き**します。`QueryClient` 丸ごとの差し替えは、
44
51
  管理されたスタックを保つため意図的に公開していません。
@@ -57,21 +64,78 @@ useQuery({
57
64
  > `@tanstack/react-query` を直接 import しないでください。`@squadbase/vantage/query` の
58
65
  > キュレートされた再エクスポートを使うことで、Vantage が下層ライブラリを安全に進化させられます。
59
66
 
60
- ## API との組み合わせ
67
+ ## 自分の API を呼ぶ — useApiQuery
68
+
69
+ `server/api` のルートを呼ぶときは `useApiQuery` を使います。`useQuery` に、毎回書いていた
70
+ 定型(ベース URL の解決・JSON パース・非 2xx のエラー化・クエリキー)を足したものです。
71
+
72
+ ```tsx
73
+ import { useApiQuery } from "@squadbase/vantage/query";
74
+
75
+ interface Monthly {
76
+ monthly: { month: string; revenue: number }[];
77
+ }
78
+
79
+ export default function MonthlyAnalysis() {
80
+ const { data, isPending, error } = useApiQuery<Monthly>("/api/monthly-analysis");
81
+
82
+ if (isPending) return <p>読み込み中…</p>;
83
+ if (error) return <p>{error.message}</p>;
84
+ return <Chart data={data.monthly} />;
85
+ }
86
+ ```
87
+
88
+ クエリ文字列は `search` で渡します。`undefined` の項目は落ちるので、未設定のフィルタは
89
+ URL にもクエリキーにも現れません。
90
+
91
+ ```tsx
92
+ const query = useApiQuery<Row[]>("/api/customers", {
93
+ search: { segment, from: range.from, to: range.to },
94
+ enabled: Boolean(segment), // useQuery のオプションはそのまま使える
95
+ });
96
+ ```
97
+
98
+ クエリキーは既定で `["api", <クエリ文字列込みの URL>]` です。したがって `search` が変われば
99
+ 再取得され、`invalidateQueries({ queryKey: ["api"] })` で API 由来のキャッシュを一括で捨てられます。
100
+
101
+ ### エラーは ApiError
102
+
103
+ 非 2xx のレスポンスは `ApiError` として throw されます。`message` は API が
104
+ [`HttpError`](api-and-server) で投げたメッセージ、それ以外は汎用のステータス文言です。
105
+
106
+ | プロパティ | 内容 |
107
+ |---|---|
108
+ | `message` | サーバーが `HttpError` で明示したメッセージ |
109
+ | `status` | HTTP ステータス |
110
+ | `body` | パース済みのレスポンスボディ |
111
+ | `requestId` | `x-vantage-request-id`。開発ターミナルのログと突き合わせられる |
112
+
113
+ ### 書き込みは useApiMutation
114
+
115
+ 変数はそのまま JSON ボディとして送られます。パスは関数でも渡せます。
116
+
117
+ ```tsx
118
+ import { useApiMutation, useQueryClient } from "@squadbase/vantage/query";
119
+
120
+ const queryClient = useQueryClient();
121
+ const save = useApiMutation<Customer, { id: string; name: string }>(
122
+ (vars) => `/api/customers/${vars.id}`,
123
+ {
124
+ method: "PUT",
125
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["api"] }),
126
+ },
127
+ );
128
+
129
+ save.mutate({ id: "1", name: "Northwind" });
130
+ ```
61
131
 
62
- `queryFn` の中で `/api/*` を呼べば、[オプションのサーバー](api-and-server)と自然につながります。
63
- `server/` を持たないクライアントのみのアプリでも、外部 API を `fetch` して同じように扱えます。
132
+ hook が使えない場所(イベントハンドラなど)では `apiJson(path, init)` を直接呼べます。
133
+ 生の `Response` が欲しい場合は `apiFetch` です。
64
134
 
65
135
  > [!TIP]
66
- > 組み込みの `/api` を呼ぶときは、`@squadbase/vantage/query` `apiFetch` を使うと、ベースURLを
67
- > env で切り替えられます(フロントとサーバーを別オリジンにデプロイする場合)。詳しくは
68
- > [ビルドとデプロイ](build-and-deploy)を参照してください。
69
- >
70
- > ```tsx
71
- > import { apiFetch } from "@squadbase/vantage/query";
72
- >
73
- > queryFn: () => apiFetch("/api/customers").then((r) => r.json());
74
- > ```
136
+ > 外部 API を呼ぶときは `useQuery` + `fetch` をそのまま使います。`useApiQuery` は
137
+ > `apiFetch` 経由なので、[`PUBLIC_API_BASE_URL`](build-and-deploy) を設定すれば
138
+ > フロントとサーバーを別オリジンにデプロイしても、ページのコードは変わりません。
75
139
 
76
140
  ## 次に読む
77
141
 
@@ -22,7 +22,8 @@ default export が無いページは `vantage check` が `MISSING_DEFAULT_EXPORT
22
22
  import { definePage } from "@squadbase/vantage";
23
23
 
24
24
  export const page = definePage({
25
- title: "月次分析",
25
+ title: "月次分析 · Acme Analytics",
26
+ navLabel: "月次分析",
26
27
  description: "売上と主要指標の月次サマリー",
27
28
  });
28
29
 
@@ -33,8 +34,10 @@ export default function MonthlyAnalysis() {
33
34
 
34
35
  - `title` は `document.title` を駆動します(末端のルートマッチのタイトルが使われ、無ければ
35
36
  ドキュメント初期の `<title>` にフォールバック)。
36
- - `title`/`description` は **生成時に静的に読まれます**。Vantage はページを実行して読むのではなく、
37
- ソースを静的に解析します。したがって値は**リテラル**にしてください。
37
+ - `navLabel` は [`useRoutes()`](routing#ルート一覧からナビを作る) で組むナビの表示名です。
38
+ 省略すると `title` が使われます。`title` にサイト名を含めるときに分けます。
39
+ - `title`/`description`/`navLabel` は **生成時に静的に読まれます**。Vantage はページを実行して
40
+ 読むのではなく、ソースを静的に解析します。したがって値は**リテラル**にしてください。
38
41
 
39
42
  > [!WARNING]
40
43
  > `export const page = definePage({...})` の**エクスポート名は `page` 固定**です。別名にすると
@@ -0,0 +1,37 @@
1
+ # Placeholder
2
+
3
+ > 仮データであることを示す、インラインの目印。
4
+
5
+ まだ本物のデータを繋いでいない箇所を「仮置き」として見せます。薄い色と `animate-pulse` が
6
+ **「ここは差し替える値だ」**という合図になり、実データを入れ忘れたまま共有してしまう事故を防ぎます。
7
+
8
+ ```tsx
9
+ import { Placeholder } from "@squadbase/vantage/components";
10
+ ```
11
+
12
+ | Prop | Type | 説明 |
13
+ | --- | --- | --- |
14
+ | `children` | `ReactNode` | 仮の値。文字列でも要素でもよい。 |
15
+ | `className` | `string` | 大きさ・太さなどの上書き(例: `text-2xl font-medium`)。 |
16
+
17
+ 残りの props は `<span>` にそのまま渡ります。
18
+
19
+ ## `Skeleton` との違い
20
+
21
+ | | 用途 | 要素 |
22
+ | --- | --- | --- |
23
+ | [`Skeleton`](ui/skeleton) | **読み込み中**。実データが来れば消える | ブロック `<div>` |
24
+ | `Placeholder` | **仮の値**。実装者が差し替えるまで残る | インライン `<span>` |
25
+
26
+ インライン要素なので、`<p>` や見出しの**文中に置けます**。`Skeleton` はブロック要素なので、
27
+ 文中に混ぜると行が崩れます。
28
+
29
+ ```tsx
30
+ <p>
31
+ 今月の売上は <Placeholder>¥1,240,000</Placeholder> でした。
32
+ </p>
33
+ ```
34
+
35
+ > [!TIP]
36
+ > テンプレートから作り始めたときは、`Placeholder` を検索して潰していけば「まだ繋いでいない箇所」の
37
+ > チェックリストになります。
@@ -0,0 +1,47 @@
1
+ # Sparkline
2
+
3
+ > 表のセルや KPI タイルに置く、インラインの推移グラフ。
4
+
5
+ 数値の並びを、軸も凡例もない小さな折れ線・棒で描きます。**`EChart` ではなく単一の `<svg>`**
6
+ なので、表のセルやタイルに何十個並べても軽いままです。
7
+
8
+ ```tsx
9
+ import { Sparkline } from "@squadbase/vantage/components";
10
+ import type { SparklineDataPoint, SparklineVariant } from "@squadbase/vantage/components";
11
+ ```
12
+
13
+ | Prop | Type | Default | 説明 |
14
+ | --- | --- | --- | --- |
15
+ | `data` | `SparklineDataPoint[]` | | `{ value: number; label?: string }` の配列。**必須**。空配列なら何も描かない。 |
16
+ | `variant` | `"line" \| "bar"` | `"line"` | 折れ線か棒か。 |
17
+ | `height` | `number` | `40` | 高さ(px)。幅は親要素いっぱい。 |
18
+ | `color` | `string` | `"text-chart-1"` | Tailwind の **text-* クラス**。`currentColor` 経由で線と塗りの両方に効く。 |
19
+ | `area` | `boolean` | `false` | 折れ線の下を薄く塗る(line のみ)。 |
20
+ | `animate` | `boolean` | `false` | マウント時に描画アニメーションする。 |
21
+ | `className` | `string` | | 足すクラス。 |
22
+
23
+ 残りの props は `<svg>` にそのまま渡ります。
24
+
25
+ > [!WARNING]
26
+ > `color` は**クラス名**です。`color="#3b82f6"` のような値は効きません — 線も塗りも
27
+ > `currentColor` を見ているため、`text-chart-2` のようなユーティリティを渡してください。
28
+ > テーマトークン(`--chart-1`〜`--chart-5`)を変えれば配色も一緒に変わります。
29
+
30
+ ## 大きさを決める
31
+
32
+ `preserveAspectRatio="none"` で横方向に引き伸ばすので、**幅は親要素が決めます**。表のセルに
33
+ 入れるときは幅と高さを絞ってください。
34
+
35
+ ```tsx
36
+ <Sparkline data={points} height={16} className="w-20 shrink-0" />
37
+ ```
38
+
39
+ ## EChart との使い分け
40
+
41
+ 軸・凡例・ツールチップ・複数系列が要るなら [`EChart`](parts/echart) を使います。
42
+ `Sparkline` が持つのは「値の並びの形」だけで、**目盛りも数値も出しません**。
43
+
44
+ > [!NOTE]
45
+ > `aria-hidden="true"` が付いているため、スクリーンリーダーには読まれません。実際の数値は
46
+ > [`MetricValue`](parts/metric-value) や
47
+ > [`TrendIndicator`](parts/trend-indicator) など、テキストとして隣に置いてください。
@@ -85,6 +85,86 @@ export default function RootLayout() {
85
85
 
86
86
  `_404.tsx` と `_error.tsx` は **ルート(プロジェクト直下)でのみ**認識されます。
87
87
 
88
+ ## ルート一覧からナビを作る
89
+
90
+ `useRoutes()` はアプリの全ページルートを返します。ファイルシステムが正本なので、
91
+ ページファイルを 1 つ足せばナビに 1 行増えます。手で管理するリンク配列は不要です。
92
+
93
+ ```tsx
94
+ // _layout.tsx
95
+ import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router";
96
+
97
+ export default function RootLayout() {
98
+ // 動的ルート(/sales/:customerId)は URL が 1 つに定まらないので外す
99
+ const routes = useRoutes().filter((route) => !route.dynamic);
100
+ const current = useCurrentRoute();
101
+
102
+ return (
103
+ <div>
104
+ <nav>
105
+ {routes.map((route) => (
106
+ <Link key={route.path} to={route.to} aria-current={route.path === current?.path}>
107
+ {route.label}
108
+ </Link>
109
+ ))}
110
+ </nav>
111
+ <Outlet />
112
+ </div>
113
+ );
114
+ }
115
+ ```
116
+
117
+ 並び順は Vantage のスキャン順(浅い順 → 静的が動的より先 → アルファベット順)で、
118
+ そのままナビに使える順序です。
119
+
120
+ 各要素は `RouteInfo` です。
121
+
122
+ | フィールド | 型 | 内容 |
123
+ |---|---|---|
124
+ | `path` | `string` | 表示パス。`/sales/:customerId` |
125
+ | `to` | `string` | `Link` の `to` に渡す形。`/sales/$customerId` |
126
+ | `params` | `string[]` | 動的パラメータ名。catch-all は `_splat` |
127
+ | `dynamic` | `boolean` | 動的パラメータを持つか |
128
+ | `index` | `boolean` | ディレクトリの index ルートか |
129
+ | `label` | `string` | `navLabel` → `title` → `path` の順で決まる表示名 |
130
+ | `title` / `description` / `navLabel` | `string \| undefined` | [`definePage`](pages-and-metadata) の値 |
131
+
132
+ `useCurrentRoute()` は今表示中のルートを返します(404 のときは `undefined`)。
133
+ 上の例のような「現在地」判定のほか、パンくずやページ見出しに使えます。
134
+
135
+ > [!NOTE]
136
+ > `label` は `navLabel` を優先します。`title` は `document.title` に使われるためサイト名を
137
+ > 含めがちで、ナビには長すぎることが多いためです。
138
+
139
+ ## URL にフィルタ状態を置く
140
+
141
+ ダッシュボードの絞り込みは URL に置くと、リロードで消えず、そのまま同僚に共有できます。
142
+ `useSearchParam` は `useState` と同じ形で search params を読み書きします。
143
+
144
+ ```tsx
145
+ import { useSearchParam } from "@squadbase/vantage/router";
146
+ import { SegmentedControl } from "@squadbase/vantage/components";
147
+
148
+ export default function Sales() {
149
+ const [region, setRegion] = useSearchParam("region", "all");
150
+
151
+ return <SegmentedControl options={REGIONS} value={region} onChange={setRegion} />;
152
+ }
153
+ ```
154
+
155
+ - 値は**常に文字列**です(`?year=2024` は数値としてパースされますが、この hook が文字列に戻します)。
156
+ - **デフォルト値**(上の例では `"all"`)や `null` を書き込むと、キーは URL から取り除かれます。
157
+ - 履歴は既定で `replace`(戻るボタンが埋まらない)。`{ replace: false }` で push に変えられます。
158
+
159
+ 配列やオブジェクトなど JSON になる値は `useSearchState` を使います。関数更新も渡せます。
160
+
161
+ ```tsx
162
+ import { useSearchState } from "@squadbase/vantage/router";
163
+
164
+ const [segments, setSegments] = useSearchState<string[]>("segments", []);
165
+ setSegments((prev) => [...prev, "enterprise"]);
166
+ ```
167
+
88
168
  ## HMR とルート再生成
89
169
 
90
170
  `.tsx` ページや `server/api` ファイルを追加・削除すると、開発サーバーがルートを再生成して
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squadbase/vantage",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Data dashboard framework for Squadbase Editor. Vite-powered, config-free, one-file first.",
5
5
  "type": "module",
6
6
  "license": "MIT",