@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.
- package/README.md +5 -2
- package/dist/{chunk-YLAB6UQS.js → chunk-46QI6GFC.js} +2 -2
- package/dist/{chunk-YLAB6UQS.js.map → chunk-46QI6GFC.js.map} +1 -1
- package/dist/{chunk-WQZYXXQW.js → chunk-DIABD3KZ.js} +2 -2
- package/dist/chunk-DIABD3KZ.js.map +1 -0
- package/dist/chunk-DTDVSFRY.js +29 -0
- package/dist/chunk-DTDVSFRY.js.map +1 -0
- package/dist/chunk-EHPJXDJU.js +51 -0
- package/dist/chunk-EHPJXDJU.js.map +1 -0
- package/dist/{chunk-PRFBSQA4.js → chunk-UHZ7XSAN.js} +10 -5
- package/dist/chunk-UHZ7XSAN.js.map +1 -0
- package/dist/cli.js +19 -11
- package/dist/cli.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +23 -2
- package/dist/components/index.js +116 -2
- package/dist/components/index.js.map +1 -1
- package/dist/{define-page-B6y9TOfZ.d.ts → define-page-BfhrK99G.d.ts} +5 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/markdown/index.js.map +1 -1
- package/dist/query/index.d.ts +69 -2
- package/dist/query/index.js +85 -2
- package/dist/query/index.js.map +1 -1
- package/dist/router/index.d.ts +86 -1
- package/dist/router/index.js +71 -7
- package/dist/router/index.js.map +1 -1
- package/dist/server/node.js +1 -1
- package/dist/server/node.js.map +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/vite/index.js +2 -2
- package/docs/en/agent-skills.md +12 -8
- package/docs/en/changelog.md +98 -0
- package/docs/en/cli-reference.md +1 -1
- package/docs/en/components.md +2 -0
- package/docs/en/data-fetching.md +77 -13
- package/docs/en/pages-and-metadata.md +6 -3
- package/docs/en/parts/placeholder.md +38 -0
- package/docs/en/parts/sparkline.md +47 -0
- package/docs/en/routing.md +82 -4
- package/docs/index.json +75 -13
- package/docs/ja/agent-skills.md +13 -9
- package/docs/ja/changelog.md +89 -0
- package/docs/ja/cli-reference.md +1 -1
- package/docs/ja/components.md +2 -0
- package/docs/ja/data-fetching.md +76 -12
- package/docs/ja/pages-and-metadata.md +6 -3
- package/docs/ja/parts/placeholder.md +37 -0
- package/docs/ja/parts/sparkline.md +47 -0
- package/docs/ja/routing.md +80 -4
- package/package.json +1 -1
- package/skills/vantage-add-feature/SKILL.md +14 -12
- package/skills/vantage-app/SKILL.md +55 -24
- package/skills/vantage-pitfalls/SKILL.md +27 -13
- package/templates/AGENTS.md +65 -23
- package/dist/chunk-ATYZ45XL.js +0 -19
- package/dist/chunk-ATYZ45XL.js.map +0 -1
- package/dist/chunk-PRFBSQA4.js.map +0 -1
- 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) など、テキストとして隣に置いてください。
|
package/docs/ja/routing.md
CHANGED
|
@@ -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
|
@@ -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
|
-
クライアント側から叩くときは `
|
|
93
|
+
クライアント側から叩くときは `useApiQuery`(ベース URL 解決・JSON パース・非 2xx の `ApiError`
|
|
94
|
+
化・クエリキー `["api", url]` が入っている):
|
|
92
95
|
|
|
93
96
|
```tsx
|
|
94
|
-
import {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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` | `
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
クライアント側からは `
|
|
238
|
+
クライアント側からは `useApiQuery` で `/api/*` を叩く(ベース URL 解決・JSON パース・非 2xx の
|
|
239
|
+
`ApiError` 化・クエリキー `["api", url]` が入っている):
|
|
212
240
|
|
|
213
241
|
```tsx
|
|
214
|
-
import {
|
|
215
|
-
|
|
216
|
-
const q =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/templates/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
> アプリ固有のメモは別ファイル(例: `README.md`)に書いてください。
|
|
11
11
|
>
|
|
12
12
|
> 手順を伴う踏み込んだワークフローは、同梱の Claude Code Skill に分かれています
|
|
13
|
-
> (`vantage add skill --all
|
|
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
|
-
|
|
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` | `
|
|
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.
|
|
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
|
-
|
|
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
|
-
クライアント側からは `
|
|
227
|
+
クライアント側からは `useApiQuery` で `/api/*` を叩く。ベース URL の解決・JSON パース・
|
|
228
|
+
非 2xx の `ApiError` 化・クエリキー(`["api", url]`)が入っている:
|
|
190
229
|
|
|
191
230
|
```tsx
|
|
192
|
-
import {
|
|
193
|
-
|
|
194
|
-
const q =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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 つ足す定型
|
package/dist/chunk-ATYZ45XL.js
DELETED
|
@@ -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"]}
|