@squadbase/vantage 0.1.0 → 0.2.0

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 (63) hide show
  1. package/README.md +5 -2
  2. package/dist/{chunk-YLAB6UQS.js → chunk-46QI6GFC.js} +2 -2
  3. package/dist/{chunk-YLAB6UQS.js.map → chunk-46QI6GFC.js.map} +1 -1
  4. package/dist/{chunk-WQZYXXQW.js → chunk-DIABD3KZ.js} +2 -2
  5. package/dist/chunk-DIABD3KZ.js.map +1 -0
  6. package/dist/chunk-DTDVSFRY.js +29 -0
  7. package/dist/chunk-DTDVSFRY.js.map +1 -0
  8. package/dist/chunk-EHPJXDJU.js +51 -0
  9. package/dist/chunk-EHPJXDJU.js.map +1 -0
  10. package/dist/{chunk-PRFBSQA4.js → chunk-UHZ7XSAN.js} +10 -5
  11. package/dist/chunk-UHZ7XSAN.js.map +1 -0
  12. package/dist/cli.js +19 -11
  13. package/dist/cli.js.map +1 -1
  14. package/dist/client/index.d.ts +2 -1
  15. package/dist/client/index.js +3 -1
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/components/index.d.ts +23 -2
  18. package/dist/components/index.js +116 -2
  19. package/dist/components/index.js.map +1 -1
  20. package/dist/{define-page-B6y9TOfZ.d.ts → define-page-BfhrK99G.d.ts} +5 -0
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/markdown/index.js.map +1 -1
  25. package/dist/query/index.d.ts +69 -2
  26. package/dist/query/index.js +85 -2
  27. package/dist/query/index.js.map +1 -1
  28. package/dist/router/index.d.ts +86 -1
  29. package/dist/router/index.js +71 -7
  30. package/dist/router/index.js.map +1 -1
  31. package/dist/server/node.js +1 -1
  32. package/dist/server/node.js.map +1 -1
  33. package/dist/ui/index.js +1 -1
  34. package/dist/ui/index.js.map +1 -1
  35. package/dist/vite/index.js +2 -2
  36. package/docs/en/agent-skills.md +12 -8
  37. package/docs/en/changelog.md +98 -0
  38. package/docs/en/cli-reference.md +1 -1
  39. package/docs/en/components.md +2 -0
  40. package/docs/en/data-fetching.md +77 -13
  41. package/docs/en/pages-and-metadata.md +6 -3
  42. package/docs/en/parts/placeholder.md +38 -0
  43. package/docs/en/parts/sparkline.md +47 -0
  44. package/docs/en/routing.md +82 -4
  45. package/docs/index.json +75 -13
  46. package/docs/ja/agent-skills.md +13 -9
  47. package/docs/ja/changelog.md +89 -0
  48. package/docs/ja/cli-reference.md +1 -1
  49. package/docs/ja/components.md +2 -0
  50. package/docs/ja/data-fetching.md +76 -12
  51. package/docs/ja/pages-and-metadata.md +6 -3
  52. package/docs/ja/parts/placeholder.md +37 -0
  53. package/docs/ja/parts/sparkline.md +47 -0
  54. package/docs/ja/routing.md +80 -4
  55. package/package.json +1 -1
  56. package/skills/vantage-add-feature/SKILL.md +14 -12
  57. package/skills/vantage-app/SKILL.md +55 -24
  58. package/skills/vantage-pitfalls/SKILL.md +27 -13
  59. package/templates/AGENTS.md +65 -23
  60. package/dist/chunk-ATYZ45XL.js +0 -19
  61. package/dist/chunk-ATYZ45XL.js.map +0 -1
  62. package/dist/chunk-PRFBSQA4.js.map +0 -1
  63. package/dist/chunk-WQZYXXQW.js.map +0 -1
@@ -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) など、テキストとして隣に置いてください。
@@ -33,10 +33,6 @@ server/ API(存在するとサーバーが有効になる)
33
33
  public/ 静的アセット
34
34
  ```
35
35
 
36
- > [!NOTE]
37
- > ファイル間の import は **ランタイムの `.js` 指定子**を使います(例: `./components/revenue-chart.js`)。
38
- > ESM の Node 解決に必要です。
39
-
40
36
  ## 動的パラメータの 3 つの綴り
41
37
 
42
38
  同じパラメータが、場所によって 3 つの綴りで現れます。**必ず一致させます。**
@@ -85,6 +81,86 @@ export default function RootLayout() {
85
81
 
86
82
  `_404.tsx` と `_error.tsx` は **ルート(プロジェクト直下)でのみ**認識されます。
87
83
 
84
+ ## ルート一覧からナビを作る
85
+
86
+ `useRoutes()` はアプリの全ページルートを返します。ファイルシステムが正本なので、
87
+ ページファイルを 1 つ足せばナビに 1 行増えます。手で管理するリンク配列は不要です。
88
+
89
+ ```tsx
90
+ // _layout.tsx
91
+ import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router";
92
+
93
+ export default function RootLayout() {
94
+ // 動的ルート(/sales/:customerId)は URL が 1 つに定まらないので外す
95
+ const routes = useRoutes().filter((route) => !route.dynamic);
96
+ const current = useCurrentRoute();
97
+
98
+ return (
99
+ <div>
100
+ <nav>
101
+ {routes.map((route) => (
102
+ <Link key={route.path} to={route.to} aria-current={route.path === current?.path}>
103
+ {route.label}
104
+ </Link>
105
+ ))}
106
+ </nav>
107
+ <Outlet />
108
+ </div>
109
+ );
110
+ }
111
+ ```
112
+
113
+ 並び順は Vantage のスキャン順(浅い順 → 静的が動的より先 → アルファベット順)で、
114
+ そのままナビに使える順序です。
115
+
116
+ 各要素は `RouteInfo` です。
117
+
118
+ | フィールド | 型 | 内容 |
119
+ |---|---|---|
120
+ | `path` | `string` | 表示パス。`/sales/:customerId` |
121
+ | `to` | `string` | `Link` の `to` に渡す形。`/sales/$customerId` |
122
+ | `params` | `string[]` | 動的パラメータ名。catch-all は `_splat` |
123
+ | `dynamic` | `boolean` | 動的パラメータを持つか |
124
+ | `index` | `boolean` | ディレクトリの index ルートか |
125
+ | `label` | `string` | `navLabel` → `title` → `path` の順で決まる表示名 |
126
+ | `title` / `description` / `navLabel` | `string \| undefined` | [`definePage`](pages-and-metadata) の値 |
127
+
128
+ `useCurrentRoute()` は今表示中のルートを返します(404 のときは `undefined`)。
129
+ 上の例のような「現在地」判定のほか、パンくずやページ見出しに使えます。
130
+
131
+ > [!NOTE]
132
+ > `label` は `navLabel` を優先します。`title` は `document.title` に使われるためサイト名を
133
+ > 含めがちで、ナビには長すぎることが多いためです。
134
+
135
+ ## URL にフィルタ状態を置く
136
+
137
+ ダッシュボードの絞り込みは URL に置くと、リロードで消えず、そのまま同僚に共有できます。
138
+ `useSearchParam` は `useState` と同じ形で search params を読み書きします。
139
+
140
+ ```tsx
141
+ import { useSearchParam } from "@squadbase/vantage/router";
142
+ import { SegmentedControl } from "@squadbase/vantage/components";
143
+
144
+ export default function Sales() {
145
+ const [region, setRegion] = useSearchParam("region", "all");
146
+
147
+ return <SegmentedControl options={REGIONS} value={region} onChange={setRegion} />;
148
+ }
149
+ ```
150
+
151
+ - 値は**常に文字列**です(`?year=2024` は数値としてパースされますが、この hook が文字列に戻します)。
152
+ - **デフォルト値**(上の例では `"all"`)や `null` を書き込むと、キーは URL から取り除かれます。
153
+ - 履歴は既定で `replace`(戻るボタンが埋まらない)。`{ replace: false }` で push に変えられます。
154
+
155
+ 配列やオブジェクトなど JSON になる値は `useSearchState` を使います。関数更新も渡せます。
156
+
157
+ ```tsx
158
+ import { useSearchState } from "@squadbase/vantage/router";
159
+
160
+ const [segments, setSegments] = useSearchState<string[]>("segments", []);
161
+ setSegments((prev) => [...prev, "enterprise"]);
162
+ ```
163
+
88
164
  ## HMR とルート再生成
89
165
 
90
166
  `.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.2.0",
4
4
  "description": "Data dashboard framework for Squadbase Editor. Vite-powered, config-free, one-file first.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,9 @@ vantage add page "sales/[customerId]" # → 動的ルート /sales/:customerI
43
43
  チェックリスト:
44
44
 
45
45
  1. **デフォルトエクスポートは必須**(無いと `MISSING_DEFAULT_EXPORT` エラー)。
46
- 2. `definePage({ title })` の値は**リテラル**で書く(ビルド時に静的抽出されるため)
46
+ 2. `definePage({ title })` の値は**リテラル**で書く(ビルド時に静的抽出されるため)。ナビに出す
47
+ 短い名前が要るなら `navLabel` も足す(`useRoutes()` が読む。既定は `title`)。
48
+ `_layout.tsx` のナビを `useRoutes()` で組んでいれば、ページを足すだけでリンクも増える。
47
49
  3. 動的ページなら **3つの綴りを同期**:
48
50
  - ファイル `foo/[id].tsx`
49
51
  - 表示ルート `/foo/:id`
@@ -88,20 +90,21 @@ export async function GET({ request, params }: ApiContext) {
88
90
  6. サーバー専用の共有コードは `server/utils.ts` 等に置く。**クライアントから `server/` を
89
91
  import しない**(`CLIENT_IMPORTS_SERVER` エラー。共有は `lib/` に置く)。
90
92
 
91
- クライアント側から叩くときは `apiFetch`:
93
+ クライアント側から叩くときは `useApiQuery`(ベース URL 解決・JSON パース・非 2xx の `ApiError`
94
+ 化・クエリキー `["api", url]` が入っている):
92
95
 
93
96
  ```tsx
94
- import { apiFetch, useQuery } from "@squadbase/vantage/query"
95
- const q = useQuery({
96
- queryKey: ["customer", id],
97
- queryFn: async () => {
98
- const res = await apiFetch(`/api/customers/${id}`)
99
- if (!res.ok) throw new Error("not found")
100
- return res.json()
101
- },
102
- })
97
+ import { useApiQuery } from "@squadbase/vantage/query"
98
+
99
+ const q = useApiQuery<Customer>(`/api/customers/${id}`)
100
+ if (q.isError) return <ErrorState message={q.error.message} /> // HttpError のメッセージ
101
+
102
+ // クエリ文字列は search (undefined の項目は落ちる)
103
+ const rows = useApiQuery<Row[]>("/api/customers", { search: { segment } })
103
104
  ```
104
105
 
106
+ 書き込みは `useApiMutation(path, { method: "POST" })` — 変数がそのまま JSON ボディになる。
107
+
105
108
  ## UI コンポーネント / ブロックを追加する
106
109
 
107
110
  ```bash
@@ -118,7 +121,6 @@ vantage add block sales-overview # → components/blocks/sales-overview.tsx
118
121
  import { Button, Loading, ErrorState } from "@squadbase/vantage/ui"
119
122
  import { DashboardCardPreset, DataTablePreset, EChart } from "@squadbase/vantage/components"
120
123
  ```
121
- - コピーしたソースの相対 import は**ランタイムの `.js` 指定子**で書く(`./cn.js` など)。
122
124
  - **props は `vantage docs <name>` で確認してから書く**(`vantage docs button`・
123
125
  `vantage docs parts/data-table`。名前が分からなければ `vantage search <やりたいこと>` で
124
126
  探してから `vantage docs` に渡す)。
@@ -22,16 +22,14 @@ Tailwind・UI キット・開発サーバー・API サーバー・ビルドは
22
22
  `echarts`・`hono`・`vite`・`tailwindcss` を直接 import しない。`lucide-react` のアイコンだけは
23
23
  直接 import してよい。
24
24
  - **`.vantage/` と `dist/` は生成物。** 編集しない・読みにいかない(gitignore 済み)。
25
- - **モジュール間 import はランタイムの `.js` 指定子を使う**(例: `./components/revenue-chart.js`)。
26
- ソースは `.tsx`/`.ts` でも、相対 import の拡張子は `.js` と書く。ESM の Node 解決に必要。
27
25
 
28
26
  ## サブパスの地図
29
27
 
30
28
  | import 元 | 提供するもの |
31
29
  | --- | --- |
32
30
  | `@squadbase/vantage` | `definePage`(ページ設定) |
33
- | `@squadbase/vantage/router` | `Link`・`Outlet`・`useParams`・`useSearch`・`useNavigate`・`redirect`・`notFound` |
34
- | `@squadbase/vantage/query` | `useQuery`・`useMutation`・`apiFetch`・`apiUrl` ほか TanStack Query の再エクスポート |
31
+ | `@squadbase/vantage/router` | `Link`・`Outlet`・`useParams`・`useSearch`・`useNavigate`・`redirect`・`notFound`・`useRoutes`・`useCurrentRoute`・`useSearchParam`・`useSearchState` |
32
+ | `@squadbase/vantage/query` | `useApiQuery`・`useApiMutation`・`apiJson`・`apiFetch`・`apiUrl`・`ApiError`、ほか `useQuery`/`useMutation` など TanStack Query の再エクスポート |
35
33
  | `@squadbase/vantage/ui` | shadcn/ui 系プリミティブ(`Button`・`Loading`・`ErrorState`・`Empty` ほか) |
36
34
  | `@squadbase/vantage/components` | 複合パーツ(`PageShell`・`DashboardCardPreset`・`DataTablePreset`・`EChart` ほか) |
37
35
  | `@squadbase/vantage/markdown` | `MarkdownRenderer`(Shiki を隔離するため専用サブパス) |
@@ -62,7 +60,7 @@ Tailwind・UI キット・開発サーバー・API サーバー・ビルドは
62
60
  "routes": "vantage routes"
63
61
  },
64
62
  "dependencies": {
65
- "@squadbase/vantage": "^0.1.0",
63
+ "@squadbase/vantage": "^0.2.0",
66
64
  "react": "^19.2.7",
67
65
  "react-dom": "^19.2.7"
68
66
  }
@@ -114,27 +112,39 @@ pnpm routes # ページ/API の URL マップ
114
112
  ```tsx
115
113
  import { definePage } from "@squadbase/vantage"
116
114
 
117
- export const page = definePage({ title: "Monthly Analysis" })
115
+ // navLabel useRoutes() で組むナビの表示名(省略時は title)
116
+ export const page = definePage({ title: "Monthly Analysis · Acme", navLabel: "Monthly" })
118
117
 
119
118
  export default function MonthlyAnalysis() {
120
119
  return <main className="p-6">…</main>
121
120
  }
122
121
  ```
123
122
 
124
- > `page` エクスポートはランタイムでは読まれない。title/description はビルド時に**静的抽出**される
125
- > ので、値はリテラルで書く(変数や関数呼び出しにしない)。
123
+ > `page` エクスポートはランタイムでは読まれない。title/description/navLabel はビルド時に
124
+ > **静的抽出**されるので、値はリテラルで書く(変数や関数呼び出しにしない)。
126
125
 
127
126
  ### ネストレイアウトと特殊ページ
128
127
 
129
- `_layout.tsx` は `Outlet` で子ルートを描く:
128
+ `_layout.tsx` は `Outlet` で子ルートを描く。ナビは `useRoutes()` から組むと、ページファイルを
129
+ 足すだけでリンクが増える(手で持つリンク配列を作らない):
130
130
 
131
131
  ```tsx
132
- import { Outlet } from "@squadbase/vantage/router"
132
+ import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router"
133
133
 
134
134
  export default function RootLayout() {
135
+ // 動的ルート(/sales/:id)は URL が定まらないので外す。label は navLabel → title → path
136
+ const routes = useRoutes().filter((r) => !r.dynamic)
137
+ const current = useCurrentRoute()
138
+
135
139
  return (
136
140
  <div className="min-h-screen">
137
- <header>…</header>
141
+ <header>
142
+ {routes.map((r) => (
143
+ <Link key={r.path} to={r.to} activeClassName="font-semibold">
144
+ {r.label}
145
+ </Link>
146
+ ))}
147
+ </header>
138
148
  <Outlet />
139
149
  </div>
140
150
  )
@@ -182,8 +192,25 @@ if (q.isPending) return <Loading />
182
192
  if (q.isError) return <ErrorState message={(q.error as Error).message} />
183
193
  ```
184
194
 
185
- `QueryClient` は Vantage が1つだけ管理する(staleTime 30s・retry 1・refetchOnWindowFocus false)。
186
- 挙動を変えたいときはクエリ側のオプションで上書きする(クライアントごと差し替える口は無い)
195
+ `QueryClient` は Vantage が1つだけ管理する(staleTime 30s・retry 1・refetchOnWindowFocus false
196
+ networkMode "always" = ブラウザのオフライン判定に従わず必ず投げる)。挙動を変えたいときは
197
+ クエリ側のオプションで上書きする(クライアントごと差し替える口は無い)。
198
+ 自分の `server/api` を叩くときは `useQuery` ではなく `useApiQuery`(→ Step 4)。
199
+
200
+ ### フィルタ状態は URL に置く
201
+
202
+ ダッシュボードの絞り込みは `useState` ではなく `useSearchParam` にする。リロードで消えず、
203
+ URL をそのまま共有できる:
204
+
205
+ ```tsx
206
+ import { useSearchParam, useSearchState } from "@squadbase/vantage/router"
207
+
208
+ const [region, setRegion] = useSearchParam("region", "all") // 値は常に string
209
+ const [segments, setSegments] = useSearchState<string[]>("segments", []) // 配列・オブジェクト
210
+ ```
211
+
212
+ デフォルト値(または `null`)を書き込むとキーは URL から消える。履歴は既定で `replace`
213
+ (`{ replace: false }` で push)。動的ルートの上でもパスパラメータは保たれる。
187
214
 
188
215
  ## Step 4 — fullstack へ拡張(server/api を足す)
189
216
 
@@ -208,21 +235,25 @@ export async function GET(_ctx: ApiContext) {
208
235
  ログに記録され汎用の 500 になる。
209
236
  - **シークレットは `ApiContext.env` にだけ届く**(クライアントには決して届かない)。
210
237
 
211
- クライアント側からは `apiFetch` で同一オリジンの `/api/*` を叩く:
238
+ クライアント側からは `useApiQuery` `/api/*` を叩く(ベース URL 解決・JSON パース・非 2xx の
239
+ `ApiError` 化・クエリキー `["api", url]` が入っている):
212
240
 
213
241
  ```tsx
214
- import { apiFetch, useQuery } from "@squadbase/vantage/query"
215
-
216
- const q = useQuery({
217
- queryKey: ["customer", customerId],
218
- queryFn: async () => {
219
- const res = await apiFetch(`/api/customers/${customerId}`)
220
- if (!res.ok) throw new Error(`Customer ${customerId} not found`)
221
- return res.json()
222
- },
223
- })
242
+ import { useApiQuery, useApiMutation } from "@squadbase/vantage/query"
243
+
244
+ const q = useApiQuery<Customer>(`/api/customers/${customerId}`)
245
+ if (q.isError) return <ErrorState message={q.error.message} /> // HttpError のメッセージが入る
246
+
247
+ // クエリ文字列は search で渡す(undefined の項目は落ちる → URL にもキーにも出ない)
248
+ const rows = useApiQuery<Row[]>("/api/customers", { search: { segment } })
249
+
250
+ // 書き込み。変数がそのまま JSON ボディになる
251
+ const save = useApiMutation<Customer, Payload>("/api/customers", { method: "POST" })
224
252
  ```
225
253
 
254
+ `ApiError` は `message`・`status`・`body`・`requestId`(dev ターミナルのログと突き合わせられる)を
255
+ 持つ。hook が使えない場所では `apiJson(path, init)`、生の `Response` が要るときは `apiFetch`。
256
+
226
257
  ### server/ とクライアントの境界(絶対に守る)
227
258
 
228
259
  - **クライアントコードは `server/` を import してはならない。** 共有したいコードは `lib/` に置く。
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vantage-pitfalls
3
- description: Vantage(@squadbase/vantage)アプリを書くときに静かに壊れる落とし穴のリファレンス。Base UI は Radix ではない(asChild 無し・render プロップ)、SelectValue は value を描く、クライアントから server/ を import しない、EChart はテーマ非追従、相対 import は .js 指定子、動的パラメータの3綴り同期、PUBLIC_ env、HttpError など。Vantage の UI コンポーネントやルーティングが思った通りに動かないとき、ビルドやスタイルが静かに欠落するときに参照する。
3
+ description: Vantage(@squadbase/vantage)アプリを書くときに静かに壊れる落とし穴のリファレンス。Base UI は Radix ではない(asChild 無し・render プロップ)、SelectValue は value を描く、クライアントから server/ を import しない、EChart はテーマ非追従、動的パラメータの3綴り同期、PUBLIC_ env、HttpError など。Vantage の UI コンポーネントやルーティングが思った通りに動かないとき、ビルドやスタイルが静かに欠落するときに参照する。
4
4
  ---
5
5
 
6
6
  # Vantage アプリの落とし穴リファレンス
@@ -52,16 +52,6 @@ Vantage の UI キットは shadcn/ui の **Base UI バリアント**。Radix
52
52
  それ以外を `import.meta.env` で読むと `PUBLIC_ENV_MISUSE` 警告。
53
53
  - `VITE_` 系の公開 API は無い。サーバーのシークレットは `ctx.env` で読む。
54
54
 
55
- ## import:相対 import はランタイムの `.js` 指定子で書く
56
-
57
- ソースが `.tsx`/`.ts` でも、**モジュール間の相対 import は `.js`** と書く(ESM の Node 解決に必要):
58
-
59
- ```tsx
60
- import { RevenueChart } from "./components/revenue-chart.js" // ← .tsx ではなく .js
61
- ```
62
-
63
- `@squadbase/vantage` のサブパス import はこの限りではない(`@squadbase/vantage/ui` 等)。
64
-
65
55
  ## ルーティング:動的パラメータは「3つの綴り」を同期させる
66
56
 
67
57
  ズレると静かにマッチしなくなる:
@@ -72,10 +62,34 @@ import { RevenueChart } from "./components/revenue-chart.js" // ← .tsx では
72
62
 
73
63
  ## ページ:`definePage` の値はリテラルで書く
74
64
 
75
- title/description は**ビルド時に静的抽出**される。変数・関数呼び出し・テンプレート補間を使うと
76
- 抽出できない。`export const page = definePage({ title: "…" })` の右辺はリテラルにする。
65
+ title/description/navLabel は**ビルド時に静的抽出**される。変数・関数呼び出し・テンプレート補間を
66
+ 使うと抽出できない。`export const page = definePage({ title: "…" })` の右辺はリテラルにする。
77
67
  なお `page` エクスポートはランタイムでは読まれない(抽出専用)。
78
68
 
69
+ ## ナビ:`useRoutes()` は動的ルートも返す
70
+
71
+ `/sales/:customerId` のような動的ルートは URL が 1 つに定まらない。ナビに出すなら
72
+ `useRoutes().filter((r) => !r.dynamic)` で外す。リンク先は `r.path`(`:id` 形式)ではなく
73
+ **`r.to`**(`$id` 形式)を `Link` に渡す。表示名は `r.label`(`navLabel` → `title` → `path` の順)。
74
+
75
+ `useCurrentRoute()` は 404 のとき `undefined` を返す。動的ルートを開いているときに親を
76
+ アクティブにしたいなら `current?.path.startsWith("/sales")` のように前方一致で判定する。
77
+
78
+ ## URL 状態:`useSearchParam` の値は常に string、既定値はURLから消える
79
+
80
+ - TanStack は `?year=2024` を数値としてパースするが、`useSearchParam` は必ず `string` に戻す
81
+ (数値が欲しければ自分で `Number(...)`)。配列やオブジェクトは `useSearchState` を使う。
82
+ - **デフォルト値または `null` を書き込むとキーが URL から消える**。「明示的に既定値を選んだ」
83
+ 状態を URL に残すことはできない。
84
+ - 履歴は既定で `replace`。戻るボタンで1手ずつ戻したいときだけ `{ replace: false }`。
85
+
86
+ ## API 呼び出し:`useApiQuery` のエラーは `ApiError`
87
+
88
+ `(q.error as Error)` のキャストは要らない。`q.error` は `ApiError | null` で、`message` には
89
+ サーバーが `HttpError` で明示したメッセージが入る(それ以外は汎用のステータス文言)。
90
+ `status`・`body`・`requestId` も持つ。クエリキーは `["api", url]` なので、
91
+ `invalidateQueries({ queryKey: ["api"] })` で API 由来のキャッシュを一括で捨てられる。
92
+
79
93
  ## API:見せたいエラーは `HttpError` を throw する
80
94
 
81
95
  ```ts
@@ -10,7 +10,7 @@
10
10
  > アプリ固有のメモは別ファイル(例: `README.md`)に書いてください。
11
11
  >
12
12
  > 手順を伴う踏み込んだワークフローは、同梱の Claude Code Skill に分かれています
13
- > (`vantage add skill --all` `.claude/skills/` に配置)。このファイルは「地図」、Skill は
13
+ > (`vantage add skill --all --dir .claude/skills` で配置)。このファイルは「地図」、Skill は
14
14
  > 「手順書」という役割分担です。詰まったら `vantage-app` / `vantage-add-feature` /
15
15
  > `vantage-pitfalls` を参照してください。
16
16
 
@@ -29,9 +29,6 @@ UI キット・開発サーバー・API サーバー・ビルドはすべて Van
29
29
  - **import は必ず `@squadbase/vantage` のサブパス経由。** `@tanstack/*`・`@base-ui/react`・
30
30
  `echarts`・`hono`・`vite`・`tailwindcss` を直接 import しない。`lucide-react` のアイコンだけは
31
31
  直接 import してよい。
32
- - **モジュール間の相対 import はランタイムの `.js` 指定子で書く**(例:
33
- `./components/revenue-chart.js`)。ソースが `.tsx`/`.ts` でも拡張子は `.js` と書く。
34
- ESM の Node 解決に必要。`@squadbase/vantage` のサブパスはこの限りではない。
35
32
  - **クライアントコードは `server/` を import してはならない。** 違反は `CLIENT_IMPORTS_SERVER`
36
33
  エラー(静的にもビルド時にも弾かれる)。共有したいコードは `lib/` に置く。
37
34
  - **クライアントに届く env は `PUBLIC_` 接頭辞のものだけ。** `import.meta.env.PUBLIC_FOO`。
@@ -39,16 +36,17 @@ UI キット・開発サーバー・API サーバー・ビルドはすべて Van
39
36
  `ApiContext.env`(server/ 内)にだけ届く。
40
37
  - **`.vantage/` と `dist/` は生成物。** 編集しない・読みにいかない(gitignore 済み)。任意の CLI
41
38
  コマンド、または `vantage upgrade` で再生成される。
42
- - **`definePage` の値はリテラルで書く。** title/description はビルド時に静的抽出されるため、
43
- 変数・関数呼び出し・テンプレート補間は使わない。`page` エクスポートはランタイムでは読まれない。
39
+ - **`definePage` の値はリテラルで書く。** title/description/navLabel はビルド時に静的抽出される
40
+ ため、変数・関数呼び出し・テンプレート補間は使わない。`page` エクスポートはランタイムでは
41
+ 読まれない。
44
42
 
45
43
  ## import サブパスの地図
46
44
 
47
45
  | import 元 | 提供するもの |
48
46
  | --- | --- |
49
47
  | `@squadbase/vantage` | `definePage`(ページ設定) |
50
- | `@squadbase/vantage/router` | `Link`・`Outlet`・`useParams`・`useSearch`・`useNavigate`・`redirect`・`notFound` |
51
- | `@squadbase/vantage/query` | `useQuery`・`useMutation`・`apiFetch`・`apiUrl` ほか TanStack Query の再エクスポート |
48
+ | `@squadbase/vantage/router` | `Link`・`Outlet`・`useParams`・`useSearch`・`useNavigate`・`redirect`・`notFound`・`useRoutes`・`useCurrentRoute`・`useSearchParam`・`useSearchState` |
49
+ | `@squadbase/vantage/query` | `useApiQuery`・`useApiMutation`・`apiJson`・`apiFetch`・`apiUrl`・`ApiError`、ほか `useQuery`/`useMutation` など TanStack Query の再エクスポート |
52
50
  | `@squadbase/vantage/ui` | shadcn/ui(Base UI バリアント)プリミティブ(`Button`・`Loading`・`ErrorState`・`Empty` ほか) |
53
51
  | `@squadbase/vantage/components` | 複合パーツ(`PageShell`・`DashboardCardPreset`・`DataTablePreset`・`EChart` ほか) |
54
52
  | `@squadbase/vantage/markdown` | `MarkdownRenderer`(Shiki を隔離するための専用サブパス) |
@@ -73,7 +71,7 @@ UI キット・開発サーバー・API サーバー・ビルドはすべて Van
73
71
  "routes": "vantage routes"
74
72
  },
75
73
  "dependencies": {
76
- "@squadbase/vantage": "^0.1.0",
74
+ "@squadbase/vantage": "^0.2.0",
77
75
  "react": "^19.2.7",
78
76
  "react-dom": "^19.2.7"
79
77
  }
@@ -121,13 +119,50 @@ pnpm routes # ページ/API の URL マップ
121
119
  ```tsx
122
120
  import { definePage } from "@squadbase/vantage"
123
121
 
124
- export const page = definePage({ title: "Monthly Analysis" })
122
+ // title document.title、navLabel は useRoutes() で組むナビの表示名(既定は title)
123
+ export const page = definePage({ title: "Monthly Analysis · Acme", navLabel: "Monthly" })
125
124
 
126
125
  export default function MonthlyAnalysis() {
127
126
  return <main className="p-6">…</main>
128
127
  }
129
128
  ```
130
129
 
130
+ ### ナビはルート一覧から組む
131
+
132
+ `useRoutes()` がページルート一覧(スキャン順)を返すので、リンク配列を手で持たない。
133
+ `useCurrentRoute()` は現在のルート(404 なら `undefined`)。
134
+
135
+ ```tsx
136
+ import { Link, useCurrentRoute, useRoutes } from "@squadbase/vantage/router"
137
+
138
+ // 動的ルートは URL が定まらないので外す。label は navLabel → title → path の順
139
+ const routes = useRoutes().filter((r) => !r.dynamic)
140
+ const current = useCurrentRoute()
141
+
142
+ routes.map((r) => (
143
+ <Link key={r.path} to={r.to} activeClassName="font-semibold">
144
+ {r.label}
145
+ </Link>
146
+ ))
147
+ ```
148
+
149
+ `RouteInfo`: `path`(`/sales/:id`)・`to`(`/sales/$id`)・`params`・`dynamic`・`index`・`label`・
150
+ `title`・`description`・`navLabel`。
151
+
152
+ ### フィルタ状態は URL に置く
153
+
154
+ リロードで消えず、URL をそのまま共有できる。`useState` と同じ形。
155
+
156
+ ```tsx
157
+ import { useSearchParam, useSearchState } from "@squadbase/vantage/router"
158
+
159
+ const [region, setRegion] = useSearchParam("region", "all") // 常に string
160
+ const [segments, setSegments] = useSearchState<string[]>("segments", []) // JSON になる値
161
+ ```
162
+
163
+ デフォルト値(または `null`)を書くとキーは URL から消える。履歴は既定で `replace`
164
+ (`{ replace: false }` で push)。
165
+
131
166
  ### 動的パラメータの「3 つの綴り」を同期させる
132
167
 
133
168
  崩すと静かにマッチしなくなる不変条件:
@@ -159,7 +194,10 @@ if (q.isError) return <ErrorState message={(q.error as Error).message} />
159
194
  ```
160
195
 
161
196
  `QueryClient` は Vantage が 1 つだけ管理する(staleTime 30s・retry 1・
162
- refetchOnWindowFocus false)。挙動を変えたいときはクエリ側のオプションで上書きする。
197
+ refetchOnWindowFocus false・networkMode "always")。挙動を変えたいときはクエリ側のオプションで
198
+ 上書きする。
199
+ 自分の `server/api` を叩くときは `useQuery` ではなく `useApiQuery`(→「API を足して fullstack に
200
+ する」)。
163
201
 
164
202
  ## API を足して fullstack にする
165
203
 
@@ -186,21 +224,25 @@ export async function GET({ params, env }: ApiContext) {
186
224
  ログに記録され汎用の 500 に丸められる。
187
225
  - **シークレットは `ApiContext.env` にだけ届く**(クライアントには決して届かない)。
188
226
 
189
- クライアント側からは `apiFetch` で同一オリジンの `/api/*` を叩く:
227
+ クライアント側からは `useApiQuery` `/api/*` を叩く。ベース URL の解決・JSON パース・
228
+ 非 2xx の `ApiError` 化・クエリキー(`["api", url]`)が入っている:
190
229
 
191
230
  ```tsx
192
- import { apiFetch, useQuery } from "@squadbase/vantage/query"
193
-
194
- const q = useQuery({
195
- queryKey: ["customer", customerId],
196
- queryFn: async () => {
197
- const res = await apiFetch(`/api/customers/${customerId}`)
198
- if (!res.ok) throw new Error(`Customer ${customerId} not found`)
199
- return res.json()
200
- },
201
- })
231
+ import { useApiQuery, useApiMutation } from "@squadbase/vantage/query"
232
+
233
+ const q = useApiQuery<Customer>(`/api/customers/${customerId}`)
234
+ if (q.isError) return <ErrorState message={q.error.message} /> // HttpError のメッセージ
235
+
236
+ // クエリ文字列は search で。undefined の項目は落ちる
237
+ const rows = useApiQuery<Row[]>("/api/customers", { search: { segment } })
238
+
239
+ // 書き込み。変数がそのまま JSON ボディになる
240
+ const save = useApiMutation<Customer, Payload>("/api/customers", { method: "POST" })
202
241
  ```
203
242
 
243
+ `ApiError` は `message`・`status`・`body`・`requestId` を持つ。hook が使えない場所では
244
+ `apiJson(path, init)`、生の `Response` が要るときは `apiFetch`。
245
+
204
246
  ## 環境変数
205
247
 
206
248
  - **クライアントで読めるのは `import.meta.env.PUBLIC_*` と `MODE`/`DEV`/`PROD`/`SSR`/`BASE_URL`
@@ -260,7 +302,7 @@ pnpm preview # 本番ビルドをローカル実行
260
302
 
261
303
  ## さらに詳しく(同梱 Skill)
262
304
 
263
- `vantage add skill --all` `.claude/skills/` に配置される:
305
+ `vantage add skill --all --dir .claude/skills` で配置される(`--dir` を省くとルート直下):
264
306
 
265
307
  - **`vantage-app`** — アプリを一から作る / SPA を fullstack に広げる手順
266
308
  - **`vantage-add-feature`** — 既存アプリに page / api / ui / block を 1 つ足す定型
@@ -1,19 +0,0 @@
1
- import { QueryClient } from '@tanstack/react-query';
2
-
3
- // @squadbase/vantage — generated build. Do not edit.
4
-
5
- function createQueryClient() {
6
- return new QueryClient({
7
- defaultOptions: {
8
- queries: {
9
- staleTime: 3e4,
10
- retry: 1,
11
- refetchOnWindowFocus: false
12
- }
13
- }
14
- });
15
- }
16
-
17
- export { createQueryClient };
18
- //# sourceMappingURL=chunk-ATYZ45XL.js.map
19
- //# sourceMappingURL=chunk-ATYZ45XL.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/query/client.ts"],"names":[],"mappings":";;;;AAOO,SAAS,iBAAA,GAAiC;AAC/C,EAAA,OAAO,IAAI,WAAA,CAAY;AAAA,IACrB,cAAA,EAAgB;AAAA,MACd,OAAA,EAAS;AAAA,QACP,SAAA,EAAW,GAAA;AAAA,QACX,KAAA,EAAO,CAAA;AAAA,QACP,oBAAA,EAAsB;AAAA;AACxB;AACF,GACD,CAAA;AACH","file":"chunk-ATYZ45XL.js","sourcesContent":["import { QueryClient } from \"@tanstack/react-query\"\n\n/**\n * Create the QueryClient with Vantage defaults. Individual queries may override\n * these, but replacing the QueryClient wholesale is intentionally not exposed —\n * Squadbase relies on a single managed client for auth, logging and telemetry.\n */\nexport function createQueryClient(): QueryClient {\n return new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 30_000,\n retry: 1,\n refetchOnWindowFocus: false,\n },\n },\n })\n}\n"]}