@squadbase/vantage-template 0.0.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.
- package/README.md +112 -0
- package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +162 -0
- package/base-template/.squadbase/skills/vantage-app/SKILL.md +120 -0
- package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +137 -0
- package/base-template/AGENTS.md +339 -0
- package/base-template/_404.tsx +18 -0
- package/base-template/_error.tsx +14 -0
- package/base-template/_gitignore +5 -0
- package/base-template/_layout.tsx +60 -0
- package/base-template/components/user-card.tsx +132 -0
- package/base-template/index.tsx +59 -0
- package/base-template/package.json +27 -0
- package/base-template/server/api/health.ts +12 -0
- package/base-template/squadbase.yml +5 -0
- package/base-template/styles.css +18 -0
- package/base-template/tsconfig.json +28 -0
- package/chart-presets/berry.css +16 -0
- package/chart-presets/blue.css +16 -0
- package/chart-presets/forest.css +16 -0
- package/chart-presets/mono.css +16 -0
- package/chart-presets/ocean.css +16 -0
- package/chart-presets/sunset.css +16 -0
- package/dist/index.js +1157 -0
- package/package.json +55 -0
- package/ui-templates/CLAUDE.md +200 -0
- package/ui-templates/blank/manifest.json +8 -0
- package/ui-templates/blank/pages/index.tsx +33 -0
- package/ui-templates/blank-ja/manifest.json +8 -0
- package/ui-templates/blank-ja/pages/index.tsx +33 -0
- package/ui-templates/chart-grid/lib/mock-data.ts +139 -0
- package/ui-templates/chart-grid/lib/types.ts +31 -0
- package/ui-templates/chart-grid/manifest.json +10 -0
- package/ui-templates/chart-grid/pages/index.tsx +331 -0
- package/ui-templates/chart-grid/preview-square.png +0 -0
- package/ui-templates/chart-grid/preview-wide.png +0 -0
- package/ui-templates/chart-grid-ja/lib/mock-data.ts +139 -0
- package/ui-templates/chart-grid-ja/lib/types.ts +31 -0
- package/ui-templates/chart-grid-ja/manifest.json +10 -0
- package/ui-templates/chart-grid-ja/pages/index.tsx +331 -0
- package/ui-templates/chart-grid-ja/preview-square.png +0 -0
- package/ui-templates/chart-grid-ja/preview-wide.png +0 -0
- package/ui-templates/funnel/components/chart-helpers.ts +11 -0
- package/ui-templates/funnel/components/stage-table.tsx +63 -0
- package/ui-templates/funnel/lib/mock-data.ts +22 -0
- package/ui-templates/funnel/lib/types.ts +11 -0
- package/ui-templates/funnel/manifest.json +12 -0
- package/ui-templates/funnel/pages/index.tsx +252 -0
- package/ui-templates/funnel/preview-square.png +0 -0
- package/ui-templates/funnel/preview-wide.png +0 -0
- package/ui-templates/funnel-ja/components/chart-helpers.ts +11 -0
- package/ui-templates/funnel-ja/components/stage-table.tsx +63 -0
- package/ui-templates/funnel-ja/lib/mock-data.ts +22 -0
- package/ui-templates/funnel-ja/lib/types.ts +11 -0
- package/ui-templates/funnel-ja/manifest.json +12 -0
- package/ui-templates/funnel-ja/pages/index.tsx +251 -0
- package/ui-templates/funnel-ja/preview-square.png +0 -0
- package/ui-templates/funnel-ja/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-advanced/components/campaign-table.tsx +69 -0
- package/ui-templates/kpi-chart-advanced/lib/mock-data.ts +109 -0
- package/ui-templates/kpi-chart-advanced/lib/types.ts +38 -0
- package/ui-templates/kpi-chart-advanced/manifest.json +11 -0
- package/ui-templates/kpi-chart-advanced/pages/index.tsx +339 -0
- package/ui-templates/kpi-chart-advanced/preview-square.png +0 -0
- package/ui-templates/kpi-chart-advanced/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-advanced-ja/components/campaign-table.tsx +69 -0
- package/ui-templates/kpi-chart-advanced-ja/lib/mock-data.ts +109 -0
- package/ui-templates/kpi-chart-advanced-ja/lib/types.ts +38 -0
- package/ui-templates/kpi-chart-advanced-ja/manifest.json +11 -0
- package/ui-templates/kpi-chart-advanced-ja/pages/index.tsx +333 -0
- package/ui-templates/kpi-chart-advanced-ja/preview-square.png +0 -0
- package/ui-templates/kpi-chart-advanced-ja/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-simple/components/top-items-table.tsx +73 -0
- package/ui-templates/kpi-chart-simple/lib/types.ts +44 -0
- package/ui-templates/kpi-chart-simple/manifest.json +11 -0
- package/ui-templates/kpi-chart-simple/pages/index.tsx +200 -0
- package/ui-templates/kpi-chart-simple/preview-square.png +0 -0
- package/ui-templates/kpi-chart-simple/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-simple/server/kpi-summary.ts +141 -0
- package/ui-templates/kpi-chart-simple-ja/components/top-items-table.tsx +73 -0
- package/ui-templates/kpi-chart-simple-ja/lib/types.ts +44 -0
- package/ui-templates/kpi-chart-simple-ja/manifest.json +11 -0
- package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +200 -0
- package/ui-templates/kpi-chart-simple-ja/preview-square.png +0 -0
- package/ui-templates/kpi-chart-simple-ja/preview-wide.png +0 -0
- package/ui-templates/kpi-chart-simple-ja/server/kpi-summary.ts +141 -0
- package/ui-templates/table-focus/components/detail-table.tsx +88 -0
- package/ui-templates/table-focus/lib/mock-data.ts +69 -0
- package/ui-templates/table-focus/lib/types.ts +24 -0
- package/ui-templates/table-focus/manifest.json +11 -0
- package/ui-templates/table-focus/pages/index.tsx +180 -0
- package/ui-templates/table-focus/preview-square.png +0 -0
- package/ui-templates/table-focus/preview-wide.png +0 -0
- package/ui-templates/table-focus-ja/components/detail-table.tsx +95 -0
- package/ui-templates/table-focus-ja/lib/mock-data.ts +69 -0
- package/ui-templates/table-focus-ja/lib/types.ts +24 -0
- package/ui-templates/table-focus-ja/manifest.json +11 -0
- package/ui-templates/table-focus-ja/pages/index.tsx +180 -0
- package/ui-templates/table-focus-ja/preview-square.png +0 -0
- package/ui-templates/table-focus-ja/preview-wide.png +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# AGENTS.md — Vantage アプリの地図
|
|
2
|
+
|
|
3
|
+
このファイルは、**Vantage(`@squadbase/vantage`)アプリを作るコーディングエージェント**が
|
|
4
|
+
最初に読むための地図です。アプリ作者(= `index.tsx` を書く側)が守るべき規約・不変条件・
|
|
5
|
+
最小レシピを 1 ページに集約しています。ここに書かれた規約だけで、最小のダッシュボードアプリを
|
|
6
|
+
正しく組めます。
|
|
7
|
+
|
|
8
|
+
> このファイルは **Vantage フレームワークが所有する正本**のコピーです。手で書き換えても、
|
|
9
|
+
> `vantage upgrade` を実行するとフレームワーク同梱の正本から再同期されて上書きされます。
|
|
10
|
+
> アプリ固有のメモは別ファイル(例: `README.md`)に書いてください。
|
|
11
|
+
>
|
|
12
|
+
> 手順を伴う踏み込んだワークフローは、同梱の Claude Code Skill に分かれています。このファイルは
|
|
13
|
+
> 「地図」、Skill は「手順書」という役割分担です。**配置済みの Skill を先に探す**手順は
|
|
14
|
+
> 末尾の「さらに詳しく(同梱 Skill)」にあります。
|
|
15
|
+
|
|
16
|
+
## Vantage とは
|
|
17
|
+
|
|
18
|
+
設定ファイル不要(config-free)な React ダッシュボードフレームワーク。アプリ作者が書くのは
|
|
19
|
+
`index.tsx`(と任意の追加ファイル)だけで、Vite・ルーティング・TanStack Query・Tailwind・
|
|
20
|
+
UI キット・開発サーバー・API サーバー・ビルドはすべて Vantage が所有します。設定は
|
|
21
|
+
「ファイル名の規約」で表現します。
|
|
22
|
+
|
|
23
|
+
## 不変条件(破ると静かに壊れる)
|
|
24
|
+
|
|
25
|
+
- **設定ファイルを作らない。** `vite.config.*`・`tailwind.config.*`・`postcss.config.*`・
|
|
26
|
+
`components.json`・`vantage.config.*` はすべて禁止。存在すると `vantage check` がエラーにする
|
|
27
|
+
(`FORBIDDEN_FILE`)。テーマ調整は `styles.css` のトークンで行う。
|
|
28
|
+
- **import は必ず `@squadbase/vantage` のサブパス経由。** `@tanstack/*`・`@base-ui/react`・
|
|
29
|
+
`echarts`・`hono`・`vite`・`tailwindcss` を直接 import しない。`lucide-react` のアイコンだけは
|
|
30
|
+
直接 import してよい。
|
|
31
|
+
- **クライアントコードは `server/` を import してはならない。** 違反は `CLIENT_IMPORTS_SERVER`
|
|
32
|
+
エラー(静的にもビルド時にも弾かれる)。共有したいコードは `lib/` に置く。
|
|
33
|
+
- **クライアントに届く env は `PUBLIC_` 接頭辞のものだけ。** `import.meta.env.PUBLIC_FOO`。
|
|
34
|
+
それ以外を `import.meta.env` で読むと `PUBLIC_ENV_MISUSE` 警告。シークレットは
|
|
35
|
+
`ApiContext.env`(server/ 内)にだけ届く。
|
|
36
|
+
- **`.vantage/` と `dist/` は生成物。** 編集しない・読みにいかない(gitignore 済み)。任意の CLI
|
|
37
|
+
コマンド、または `vantage upgrade` で再生成される。
|
|
38
|
+
- **`definePage` の値はリテラルで書く。** title/description/navLabel はビルド時に静的抽出される
|
|
39
|
+
ため、変数・関数呼び出し・テンプレート補間は使わない。`page` エクスポートはランタイムでは
|
|
40
|
+
読まれない。
|
|
41
|
+
|
|
42
|
+
## import サブパスの地図
|
|
43
|
+
|
|
44
|
+
| import 元 | 提供するもの |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `@squadbase/vantage` | `definePage`(ページ設定) |
|
|
47
|
+
| `@squadbase/vantage/router` | `Link`・`Outlet`・`useParams`・`useSearch`・`useNavigate`・`redirect`・`notFound`・`useRoutes`・`useCurrentRoute`・`useSearchParam`・`useSearchState` |
|
|
48
|
+
| `@squadbase/vantage/query` | `useApiQuery`・`useApiMutation`・`apiJson`・`apiFetch`・`apiUrl`・`ApiError`、ほか `useQuery`/`useMutation` など TanStack Query の再エクスポート |
|
|
49
|
+
| `@squadbase/vantage/ui` | shadcn/ui(Base UI バリアント)プリミティブ(`Button`・`Loading`・`ErrorState`・`Empty` ほか) |
|
|
50
|
+
| `@squadbase/vantage/components` | 複合パーツ(`PageShell`・`DashboardCardPreset`・`DataTablePreset`・`EChart` ほか) |
|
|
51
|
+
| `@squadbase/vantage/markdown` | `MarkdownRenderer`(Shiki を隔離するための専用サブパス) |
|
|
52
|
+
| `@squadbase/vantage/server` | `ApiContext`・`ApiHandler`・`HttpError`・`isHttpError`・`json`(server/ 側でのみ使う) |
|
|
53
|
+
|
|
54
|
+
## モードは 2 つ、切り替えるのは `server/` の有無だけ
|
|
55
|
+
|
|
56
|
+
`server/` ディレクトリがあれば **fullstack**、無ければ **SPA**。`vantage-manifest.json` の
|
|
57
|
+
`mode` はここから自動で決まり、手で書く設定は無い。**SPA から始めて fullstack へ「昇格」する
|
|
58
|
+
必要はない** ― テンプレートから始めた場合は最初から `server/` があり、その時点で fullstack。
|
|
59
|
+
下の 2 節は段階ではなく、いま自分がどちらにいるかを確かめるための地図として読む。
|
|
60
|
+
|
|
61
|
+
- `server/` が**ある** → 「API を持つ(fullstack モード)」を見る。データは自前の `/api/*` から
|
|
62
|
+
`useApiQuery` で取る。
|
|
63
|
+
- `server/` が**無い** → 「最小アプリ(2 ファイル)」の構成。外部 API を直接叩くなら `useQuery`。
|
|
64
|
+
自前の API が要るようになったら `server/api/*.ts` を足すだけでよい(設定変更は不要)。
|
|
65
|
+
|
|
66
|
+
## 最小アプリ(2 ファイル)
|
|
67
|
+
|
|
68
|
+
`package.json` と `index.tsx` の 2 ファイルだけで動く。
|
|
69
|
+
|
|
70
|
+
`package.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"name": "my-dashboard",
|
|
75
|
+
"private": true,
|
|
76
|
+
"type": "module",
|
|
77
|
+
"scripts": {
|
|
78
|
+
"dev": "vantage dev",
|
|
79
|
+
"build": "vantage build",
|
|
80
|
+
"preview": "vantage preview",
|
|
81
|
+
"check": "vantage check",
|
|
82
|
+
"routes": "vantage routes"
|
|
83
|
+
},
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"@squadbase/vantage": "^0.2.0",
|
|
86
|
+
"react": "^19.2.7",
|
|
87
|
+
"react-dom": "^19.2.7"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`index.tsx`(ルートのデフォルトエクスポートが `/` ページになる):
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
export default function Dashboard() {
|
|
96
|
+
return (
|
|
97
|
+
<main className="p-6">
|
|
98
|
+
<h1 className="text-2xl font-semibold">Dashboard</h1>
|
|
99
|
+
</main>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm install
|
|
106
|
+
pnpm dev # → http://localhost:5173(HMR)
|
|
107
|
+
pnpm check # 静的診断(エラーがあれば exit 1)
|
|
108
|
+
pnpm routes # ページ/API の URL マップ
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`server/` ディレクトリが無いので、この構成は **SPA モード**。
|
|
112
|
+
|
|
113
|
+
## ルーティング規約(ファイル名 → ルート)
|
|
114
|
+
|
|
115
|
+
ルートディレクトリ直下の `.tsx`/`.jsx` がそのままページになる。各ページは**デフォルト
|
|
116
|
+
エクスポートの React コンポーネントが必須**(無いと `MISSING_DEFAULT_EXPORT`)。
|
|
117
|
+
|
|
118
|
+
| ファイル | ルート |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `index.tsx` | `/` |
|
|
121
|
+
| `monthly-analysis.tsx` | `/monthly-analysis` |
|
|
122
|
+
| `sales/index.tsx` | `/sales` |
|
|
123
|
+
| `sales/[customerId].tsx` | `/sales/:customerId`(動的パラメータ) |
|
|
124
|
+
| `_layout.tsx` | そのディレクトリ配下を包むネストレイアウト(`Outlet` で子を描く) |
|
|
125
|
+
| `_404.tsx` | Not Found ページ |
|
|
126
|
+
| `_error.tsx` | ルートが throw したときのエラーページ |
|
|
127
|
+
|
|
128
|
+
`components/`・`hooks/`・`lib/`・`server/`・`public/` はルート走査の対象外(ページにならない)。
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { definePage } from "@squadbase/vantage"
|
|
132
|
+
|
|
133
|
+
// title は document.title、navLabel は useRoutes() で組むナビの表示名(既定は title)
|
|
134
|
+
export const page = definePage({ title: "Monthly Analysis · Acme", navLabel: "Monthly" })
|
|
135
|
+
|
|
136
|
+
export default function MonthlyAnalysis() {
|
|
137
|
+
return <main className="p-6">…</main>
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### ナビはルート一覧から組む
|
|
142
|
+
|
|
143
|
+
`useRoutes()` がページルート一覧(スキャン順)を返すので、リンク配列を手で持たない。
|
|
144
|
+
`useCurrentRoute()` は現在のルート(404 なら `undefined`)。
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Link, useCurrentRoute, useRoutes } from "@squadbase/vantage/router"
|
|
148
|
+
|
|
149
|
+
// 動的ルートは URL が定まらないので外す。label は navLabel → title → path の順
|
|
150
|
+
const routes = useRoutes().filter((r) => !r.dynamic)
|
|
151
|
+
const current = useCurrentRoute()
|
|
152
|
+
|
|
153
|
+
routes.map((r) => (
|
|
154
|
+
<Link key={r.path} to={r.to} activeClassName="font-semibold">
|
|
155
|
+
{r.label}
|
|
156
|
+
</Link>
|
|
157
|
+
))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`RouteInfo`: `path`(`/sales/:id`)・`to`(`/sales/$id`)・`params`・`dynamic`・`index`・`label`・
|
|
161
|
+
`title`・`description`・`navLabel`。
|
|
162
|
+
|
|
163
|
+
### フィルタ状態は URL に置く
|
|
164
|
+
|
|
165
|
+
リロードで消えず、URL をそのまま共有できる。`useState` と同じ形。
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { useSearchParam, useSearchState } from "@squadbase/vantage/router"
|
|
169
|
+
|
|
170
|
+
const [region, setRegion] = useSearchParam("region", "all") // 常に string
|
|
171
|
+
const [segments, setSegments] = useSearchState<string[]>("segments", []) // JSON になる値
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
デフォルト値(または `null`)を書くとキーは URL から消える。履歴は既定で `replace`
|
|
175
|
+
(`{ replace: false }` で push)。
|
|
176
|
+
|
|
177
|
+
### 動的パラメータの「3 つの綴り」を同期させる
|
|
178
|
+
|
|
179
|
+
崩すと静かにマッチしなくなる不変条件:
|
|
180
|
+
|
|
181
|
+
- ファイル名 `sales/[customerId].tsx`
|
|
182
|
+
- 表示ルート `/sales/:customerId`
|
|
183
|
+
- リンクトークン `to="/sales/$customerId"` + `params={{ customerId }}`
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { Link, useParams } from "@squadbase/vantage/router"
|
|
187
|
+
|
|
188
|
+
<Link to="/sales/$customerId" params={{ customerId: row.id }}>{row.name}</Link>
|
|
189
|
+
|
|
190
|
+
// 詳細ページ側で取り出す
|
|
191
|
+
const { customerId } = useParams()
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## データ取得(server を持たない fetch)
|
|
195
|
+
|
|
196
|
+
外部 API を叩くだけなら `server/` は不要。`@squadbase/vantage/query` の `useQuery` を使う:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
import { useQuery } from "@squadbase/vantage/query"
|
|
200
|
+
import { Loading, ErrorState } from "@squadbase/vantage/ui"
|
|
201
|
+
|
|
202
|
+
const q = useQuery({ queryKey: ["stats"], queryFn: () => fetch("/…").then((r) => r.json()) })
|
|
203
|
+
if (q.isPending) return <Loading />
|
|
204
|
+
if (q.isError) return <ErrorState message={(q.error as Error).message} />
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`QueryClient` は Vantage が 1 つだけ管理する(staleTime 30s・retry 1・
|
|
208
|
+
refetchOnWindowFocus false・networkMode "always")。挙動を変えたいときはクエリ側のオプションで
|
|
209
|
+
上書きする。
|
|
210
|
+
自分の `server/api` を叩くときは `useQuery` ではなく `useApiQuery`(→「API を持つ(fullstack
|
|
211
|
+
モード)」)。
|
|
212
|
+
|
|
213
|
+
## API を持つ(fullstack モード)
|
|
214
|
+
|
|
215
|
+
**`server/` ディレクトリがあれば fullstack モード**(無いアプリに足せば、その時点で切り替わる)。
|
|
216
|
+
`server/api/**` の各ファイルが API ルートになる。
|
|
217
|
+
|
|
218
|
+
`server/api/monthly-analysis.ts` → `GET /api/monthly-analysis`:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import type { ApiContext } from "@squadbase/vantage/server"
|
|
222
|
+
import { HttpError } from "@squadbase/vantage/server"
|
|
223
|
+
|
|
224
|
+
export async function GET({ params, env }: ApiContext) {
|
|
225
|
+
if (!env.API_KEY) throw new HttpError(500, "API_KEY missing")
|
|
226
|
+
return Response.json({ ok: true })
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- API モジュールは**大文字の HTTP メソッド**(`GET`/`POST`/`PUT`/`PATCH`/`DELETE`/`OPTIONS`)を
|
|
231
|
+
エクスポートする。それ以外の名前は `INVALID_API_EXPORT` エラー。
|
|
232
|
+
- 動的 API も `[id].ts` 記法: `server/api/customers/[id].ts` → `GET /api/customers/:id`。
|
|
233
|
+
`params.id` で取り出す(`noUncheckedIndexedAccess` が効くので narrowing が要る)。
|
|
234
|
+
- **クライアントに見せたいエラーは `HttpError(status, msg)` を throw する。** それ以外の throw は
|
|
235
|
+
ログに記録され汎用の 500 に丸められる。
|
|
236
|
+
- **シークレットは `ApiContext.env` にだけ届く**(クライアントには決して届かない)。
|
|
237
|
+
|
|
238
|
+
クライアント側からは `useApiQuery` で `/api/*` を叩く。ベース URL の解決・JSON パース・
|
|
239
|
+
非 2xx の `ApiError` 化・クエリキー(`["api", url]`)が入っている:
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
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 の項目は落ちる
|
|
248
|
+
const rows = useApiQuery<Row[]>("/api/customers", { search: { segment } })
|
|
249
|
+
|
|
250
|
+
// 書き込み。変数がそのまま JSON ボディになる
|
|
251
|
+
const save = useApiMutation<Customer, Payload>("/api/customers", { method: "POST" })
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`ApiError` は `message`・`status`・`body`・`requestId` を持つ。hook が使えない場所では
|
|
255
|
+
`apiJson(path, init)`、生の `Response` が要るときは `apiFetch`。
|
|
256
|
+
|
|
257
|
+
## 環境変数
|
|
258
|
+
|
|
259
|
+
- **クライアントで読めるのは `import.meta.env.PUBLIC_*` と `MODE`/`DEV`/`PROD`/`SSR`/`BASE_URL`
|
|
260
|
+
のみ。** `VITE_` 系の公開 API は無い。
|
|
261
|
+
- サーバー側のシークレットは `ApiContext.env.MY_SECRET` で読む(`server/` 内のみ)。
|
|
262
|
+
|
|
263
|
+
## 組み込み UI / コンポーネントの入口
|
|
264
|
+
|
|
265
|
+
- **プリミティブ**は `@squadbase/vantage/ui` から import する(`Button`・`Loading`・
|
|
266
|
+
`ErrorState`・`Empty`・`Select`・`Checkbox` ほか)。
|
|
267
|
+
- **複合パーツ**は `@squadbase/vantage/components` から(`PageShell`・`DashboardCardPreset`・
|
|
268
|
+
`DataTablePreset`・`EChart` ほか)。
|
|
269
|
+
- **どの名前が import 可能か / props の詳細は `vantage docs` で引く。** ガイドとコンポーネント
|
|
270
|
+
リファレンスはパッケージに同梱されていて、オフラインでも読める(→「ドキュメントを引く」)。
|
|
271
|
+
**名前が分からないとき**はやりたいことで `vantage search` する(→ 同節)。
|
|
272
|
+
- **UI は import して使うのが既定。取り出す(eject)必要はない。** `vantage add ui|block` は
|
|
273
|
+
ソースを手元にコピーして**改造したいとき専用**のコマンドで、取り出せるのは
|
|
274
|
+
`ui: data-table` と `block: sales-overview` の 2 つだけ。それ以外の名前は import 専用で
|
|
275
|
+
`add` の対象ではない(未知名で実行すると候補が一覧表示される)。
|
|
276
|
+
|
|
277
|
+
## ドキュメントを引く(`vantage docs` / `vantage search`)
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
vantage docs # 全ページの一覧(ガイド + コンポーネント)
|
|
281
|
+
vantage docs button # 1 ページ表示(短縮名。ui/button に解決される)
|
|
282
|
+
vantage docs parts/data-table # 完全な slug でも引ける
|
|
283
|
+
vantage docs data-table --json # { slug, title, description, section, content }
|
|
284
|
+
vantage docs --all # 全ページを連結(まとめて読み込む用)
|
|
285
|
+
vantage docs routing --lang en # 英語版(既定は ja)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
props の表・使用例・落とし穴はコンポーネントのページに載っている。**推測で props を書く前に
|
|
289
|
+
`vantage docs <name>` を引くこと。** 本文中のリンクは slug なので、`[DataTable](parts/data-table)`
|
|
290
|
+
は `vantage docs parts/data-table` で開ける。
|
|
291
|
+
|
|
292
|
+
**名前が分からないときは検索する。** 対象は `vantage docs` と同じ同梱ドキュメント。
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
vantage search 期間を選ぶ UI が欲しい # 自然言語(BM25)。やりたいことで引く
|
|
296
|
+
vantage search "enable[A-Z]\w+" --regex # 正規表現で props を横断(行番号付き)
|
|
297
|
+
vantage search テーブル --limit 5 --json # 機械可読(slug + score + snippet)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
結果の slug はそのまま `vantage docs <slug>` に渡せる。
|
|
301
|
+
|
|
302
|
+
## 検証・ビルド・プレビュー
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
pnpm check # 静的診断(禁止ファイル・ルート衝突・境界・API export・env 誤用)
|
|
306
|
+
pnpm routes # ページ + API の URL マップ
|
|
307
|
+
pnpm build # dist/ に client(+ server)+ vantage-manifest.json
|
|
308
|
+
pnpm preview # 本番ビルドをローカル実行
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`vantage-manifest.json` の `mode`(`spa` / `fullstack`)は `server/` の有無から自動で決まる
|
|
312
|
+
(手で書かない)。`vantage routes` は `--pages` / `--apis` で片側だけに絞れ、`--detail` を足すと
|
|
313
|
+
各ルートの静的な仕様(ページ: パスパラメータ + `definePage` のメタ / API: メソッド・query・
|
|
314
|
+
body・レスポンスの status と形)まで出る。`vantage check --json` / `vantage routes --json` は
|
|
315
|
+
エージェント向けの機械可読出力。`console.*` とランタイムエラーは開発ターミナルに `[browser:…]` として転送される。
|
|
316
|
+
|
|
317
|
+
## さらに詳しく(同梱 Skill)
|
|
318
|
+
|
|
319
|
+
- **`vantage-app`** — アプリを一から作る / 構成を広げるときの進め方と検証の順序
|
|
320
|
+
- **`vantage-add-feature`** — 既存アプリに page / api / ui を 1 つ足す定型
|
|
321
|
+
- **`vantage-pitfalls`** — Base UI(≠ Radix)の癖など、静かに壊れる落とし穴のリファレンス
|
|
322
|
+
|
|
323
|
+
手順書の本体は `SKILL.md` というファイルで、置き場所はアプリによって違う(ルート直下・
|
|
324
|
+
`.claude/skills/`・`.squadbase/skills/` など)。**探す必要はない ― CLI が探す:**
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
vantage add skill # 同梱 skill の一覧と、それぞれの配置場所
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
配置済みならその場所が出るので、その `SKILL.md` を読む。`not placed` のものだけ配置する:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
vantage add skill --all # 配置済みはスキップ(場所を報告するだけ)
|
|
334
|
+
vantage add skill --all --dir .claude/skills # 置き場所を指定(既定はルート直下)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`add skill` はプロジェクト内を走査してから動くので、**再実行しても二重に配置されない**。
|
|
338
|
+
配置済みのコピーを新しいバージョンに更新したいときだけ `--force` を付ける(見つかった場所を
|
|
339
|
+
そのまま書き換える)。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Link } from "@squadbase/vantage/router"
|
|
2
|
+
import { Button, Empty } from "@squadbase/vantage/ui"
|
|
3
|
+
|
|
4
|
+
export default function NotFound() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex min-h-[50vh] items-center justify-center">
|
|
7
|
+
<Empty
|
|
8
|
+
title="404 — Page not found"
|
|
9
|
+
description="The page you are looking for doesn't exist or was moved."
|
|
10
|
+
action={
|
|
11
|
+
<Link to="/">
|
|
12
|
+
<Button>Go home</Button>
|
|
13
|
+
</Link>
|
|
14
|
+
}
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Button, ErrorState } from "@squadbase/vantage/ui"
|
|
2
|
+
|
|
3
|
+
export default function RouteError({ error }: { error: unknown }) {
|
|
4
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex min-h-[50vh] items-center justify-center">
|
|
7
|
+
<ErrorState
|
|
8
|
+
title="This page failed to render"
|
|
9
|
+
message={message}
|
|
10
|
+
action={<Button onClick={() => window.location.reload()}>Reload</Button>}
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ComponentProps } from "react"
|
|
2
|
+
import { SquadbaseProvider } from "@squadbase/react"
|
|
3
|
+
import { Link, Outlet, useCurrentRoute, useRoutes } from "@squadbase/vantage/router"
|
|
4
|
+
import { AppShell } from "@squadbase/vantage/components"
|
|
5
|
+
import type { NavGroup } from "@squadbase/vantage/components"
|
|
6
|
+
|
|
7
|
+
import { UserCard } from "./components/user-card"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `AppShell` is router-agnostic and renders nav entries through whatever
|
|
11
|
+
* component it is handed, using an `href` prop. Adapt it to the Vantage router
|
|
12
|
+
* so navigation stays client-side.
|
|
13
|
+
*/
|
|
14
|
+
function RouterLink({ href, ...props }: { href: string } & ComponentProps<"a">) {
|
|
15
|
+
return <Link to={href} {...props} />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The nav *is* the route list — adding a page file puts it in the nav, with no
|
|
20
|
+
* second list to keep in sync. Dynamic routes (`sales/[id].tsx`) have no single
|
|
21
|
+
* URL, so they are filtered out; `route.label` resolves `navLabel` → `title` →
|
|
22
|
+
* path, so shorten a long page title with `definePage({ navLabel })`.
|
|
23
|
+
*/
|
|
24
|
+
function useNavGroups(): NavGroup[] {
|
|
25
|
+
const routes = useRoutes()
|
|
26
|
+
const current = useCurrentRoute()
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
label: "Pages",
|
|
30
|
+
items: routes
|
|
31
|
+
.filter((route) => !route.dynamic)
|
|
32
|
+
.map((route) => ({
|
|
33
|
+
label: route.label,
|
|
34
|
+
href: route.to,
|
|
35
|
+
isActive: route.path === current?.path,
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The root layout is the only place an app-wide provider can go — Vantage owns
|
|
43
|
+
* the React root, so there is no `main.tsx` to wrap. `SquadbaseProvider` supplies
|
|
44
|
+
* the signed-in user to `useUser()`; the QueryClient provider is already
|
|
45
|
+
* installed by the runtime.
|
|
46
|
+
*/
|
|
47
|
+
export default function RootLayout() {
|
|
48
|
+
return (
|
|
49
|
+
<SquadbaseProvider>
|
|
50
|
+
<AppShell
|
|
51
|
+
variant="header"
|
|
52
|
+
groups={useNavGroups()}
|
|
53
|
+
actions={<UserCard />}
|
|
54
|
+
linkComponent={RouterLink}
|
|
55
|
+
>
|
|
56
|
+
<Outlet />
|
|
57
|
+
</AppShell>
|
|
58
|
+
</SquadbaseProvider>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useUser } from "@squadbase/react"
|
|
2
|
+
import type { User } from "@squadbase/react"
|
|
3
|
+
import { UserIcon } from "lucide-react"
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DialogTrigger,
|
|
11
|
+
cn,
|
|
12
|
+
} from "@squadbase/vantage/ui"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Squadbase-authenticated user chip for the app shell.
|
|
16
|
+
*
|
|
17
|
+
* The Vantage UI kit has no `Avatar`, so the initials bubble is inlined here —
|
|
18
|
+
* it is a handful of classes and only ever used by this component.
|
|
19
|
+
*/
|
|
20
|
+
function Avatar({ user, className }: { user: User; className?: string }) {
|
|
21
|
+
const fullName = `${user.firstName} ${user.lastName}`
|
|
22
|
+
const initials = `${user.firstName.charAt(0)}${user.lastName.charAt(0)}`
|
|
23
|
+
return (
|
|
24
|
+
<span
|
|
25
|
+
className={cn(
|
|
26
|
+
"flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted font-medium",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
{user.iconUrl ? (
|
|
31
|
+
<img src={user.iconUrl} alt={fullName} className="size-full object-cover" />
|
|
32
|
+
) : (
|
|
33
|
+
initials
|
|
34
|
+
)}
|
|
35
|
+
</span>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
<p className="text-sm text-muted-foreground">{label}</p>
|
|
43
|
+
<div className="font-medium">{children}</div>
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function UserInfo({ user }: { user: User }) {
|
|
49
|
+
const fullName = `${user.firstName} ${user.lastName}`
|
|
50
|
+
const rolesHref = navigator.language.startsWith("ja")
|
|
51
|
+
? "https://www.squadbase.dev/ja/docs/access-control#プロジェクトロール"
|
|
52
|
+
: "https://www.squadbase.dev/docs/access-control#project-roles"
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Dialog>
|
|
56
|
+
<DialogTrigger
|
|
57
|
+
render={
|
|
58
|
+
<button className="flex w-full items-center gap-3 rounded-md p-2 text-left hover:bg-sidebar-accent" />
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
<Avatar user={user} className="size-9 text-sm" />
|
|
62
|
+
<span className="flex flex-col overflow-hidden">
|
|
63
|
+
<span className="truncate text-sm font-medium">{fullName}</span>
|
|
64
|
+
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
|
65
|
+
</span>
|
|
66
|
+
</DialogTrigger>
|
|
67
|
+
<DialogContent>
|
|
68
|
+
<DialogHeader>
|
|
69
|
+
<DialogTitle>User Details</DialogTitle>
|
|
70
|
+
</DialogHeader>
|
|
71
|
+
<div className="flex flex-col items-center gap-4">
|
|
72
|
+
<Avatar user={user} className="size-20 text-2xl" />
|
|
73
|
+
<div className="w-full space-y-4">
|
|
74
|
+
<DetailRow label="Name">{fullName}</DetailRow>
|
|
75
|
+
<DetailRow label="Username">{user.username}</DetailRow>
|
|
76
|
+
<DetailRow label="Email">{user.email}</DetailRow>
|
|
77
|
+
<div>
|
|
78
|
+
<div className="flex items-center justify-between">
|
|
79
|
+
<p className="text-sm text-muted-foreground">Project Roles</p>
|
|
80
|
+
<a
|
|
81
|
+
href={rolesHref}
|
|
82
|
+
target="_blank"
|
|
83
|
+
rel="noopener noreferrer"
|
|
84
|
+
className="text-xs text-muted-foreground underline underline-offset-4 hover:text-primary/80"
|
|
85
|
+
>
|
|
86
|
+
Learn more
|
|
87
|
+
</a>
|
|
88
|
+
</div>
|
|
89
|
+
{user.roles.length > 0 ? (
|
|
90
|
+
<div className="mt-2 flex flex-wrap gap-1">
|
|
91
|
+
{user.roles.map((role) => (
|
|
92
|
+
<Badge key={role} variant="outline">
|
|
93
|
+
{role}
|
|
94
|
+
</Badge>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<p className="mt-2 text-sm text-muted-foreground">No roles assigned</p>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</DialogContent>
|
|
104
|
+
</Dialog>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function UserInfoFallback() {
|
|
109
|
+
return (
|
|
110
|
+
<div className="flex w-full items-center gap-3 rounded-md p-2">
|
|
111
|
+
<span className="flex size-9 shrink-0 items-center justify-center rounded-full bg-muted">
|
|
112
|
+
<UserIcon className="size-4 text-muted-foreground" />
|
|
113
|
+
</span>
|
|
114
|
+
<div className="flex flex-col overflow-hidden">
|
|
115
|
+
<span className="truncate text-sm font-medium text-muted-foreground">Guest</span>
|
|
116
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
117
|
+
Unable to retrieve user information
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { definePage } from "@squadbase/vantage"
|
|
2
|
+
import {
|
|
3
|
+
DashboardCardPreset,
|
|
4
|
+
PageShell,
|
|
5
|
+
PageShellContent,
|
|
6
|
+
PageShellDescription,
|
|
7
|
+
PageShellHeader,
|
|
8
|
+
PageShellHeading,
|
|
9
|
+
PageShellTitle,
|
|
10
|
+
Placeholder,
|
|
11
|
+
} from "@squadbase/vantage/components"
|
|
12
|
+
|
|
13
|
+
export const page = definePage({
|
|
14
|
+
title: "Dashboard",
|
|
15
|
+
description: "Sample data — replace with your own to get started.",
|
|
16
|
+
navLabel: "Home",
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const METRICS = [
|
|
20
|
+
{ label: "Metric 1", value: "1,234" },
|
|
21
|
+
{ label: "Metric 2", value: "5,678" },
|
|
22
|
+
{ label: "Metric 3", value: "90.1%" },
|
|
23
|
+
{ label: "Metric 4", value: "$12.3k" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const CHARTS = ["Chart 1", "Chart 2"]
|
|
27
|
+
|
|
28
|
+
export default function HomePage() {
|
|
29
|
+
return (
|
|
30
|
+
<PageShell>
|
|
31
|
+
<PageShellHeader>
|
|
32
|
+
<PageShellHeading>
|
|
33
|
+
<PageShellTitle>Dashboard</PageShellTitle>
|
|
34
|
+
<PageShellDescription>
|
|
35
|
+
Sample data — replace with your own to get started.
|
|
36
|
+
</PageShellDescription>
|
|
37
|
+
</PageShellHeading>
|
|
38
|
+
</PageShellHeader>
|
|
39
|
+
|
|
40
|
+
<PageShellContent>
|
|
41
|
+
<section className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
42
|
+
{METRICS.map((metric) => (
|
|
43
|
+
<DashboardCardPreset key={metric.label} title={metric.label}>
|
|
44
|
+
<Placeholder className="text-2xl font-bold">{metric.value}</Placeholder>
|
|
45
|
+
</DashboardCardPreset>
|
|
46
|
+
))}
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
50
|
+
{CHARTS.map((title) => (
|
|
51
|
+
<DashboardCardPreset key={title} title={title}>
|
|
52
|
+
<Placeholder className="h-64 w-full" />
|
|
53
|
+
</DashboardCardPreset>
|
|
54
|
+
))}
|
|
55
|
+
</section>
|
|
56
|
+
</PageShellContent>
|
|
57
|
+
</PageShell>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibe-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vantage dev",
|
|
7
|
+
"build": "vantage build",
|
|
8
|
+
"start": "node dist/server/index.mjs",
|
|
9
|
+
"preview": "vantage preview",
|
|
10
|
+
"check": "vantage check",
|
|
11
|
+
"routes": "vantage routes"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@squadbase/react": "^0.0.1",
|
|
15
|
+
"@squadbase/vantage": "^0.2.3",
|
|
16
|
+
"date-fns": "^4.1.0",
|
|
17
|
+
"lucide-react": "^0.468.0",
|
|
18
|
+
"react": "^19.2.7",
|
|
19
|
+
"react-dom": "^19.2.7"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.15.0",
|
|
23
|
+
"@types/react": "^19.2.0",
|
|
24
|
+
"@types/react-dom": "^19.2.0",
|
|
25
|
+
"typescript": "~5.7.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ApiContext } from "@squadbase/vantage/server"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `GET /api/health` — the seed endpoint that puts this app in fullstack mode.
|
|
5
|
+
*
|
|
6
|
+
* The presence of any `server/api/**` file is what makes Vantage build a server
|
|
7
|
+
* bundle; delete this directory and the app builds as a pure SPA instead.
|
|
8
|
+
* Secrets live on `ctx.env` and never reach the client bundle.
|
|
9
|
+
*/
|
|
10
|
+
export async function GET(_ctx: ApiContext) {
|
|
11
|
+
return Response.json({ status: "ok" })
|
|
12
|
+
}
|