@squadbase/vantage-template 0.1.0 → 0.1.1-dev1.3f269fd

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 CHANGED
@@ -19,20 +19,20 @@ Initialize a new Vantage project in the current directory. Copies the base templ
19
19
  ```bash
20
20
  npx @squadbase/vantage-template init
21
21
  npx @squadbase/vantage-template init --force # Overwrite existing files
22
- npx @squadbase/vantage-template init --skip-install # Skip npm install
22
+ npx @squadbase/vantage-template init --skip-install # Skip dependency install (bun, or npm if bun is absent)
23
23
  npx @squadbase/vantage-template init --chart sunset # Apply a chart preset during init
24
24
  ```
25
25
 
26
- After initialization:
26
+ After initialization (the template ships `bun.lock` and declares `package_manager: bun` in `squadbase.yml`; `npm run` works too once dependencies are installed):
27
27
 
28
28
  ```bash
29
- npm run dev # vantage dev --no-overlay — dev server + API on :5173
30
- npm run build # vantage build → dist/
31
- npm run check # vantage check — static diagnostics
32
- npm run routes # vantage routes — page/API route map
29
+ bun run dev # vantage dev --no-overlay — dev server + API on :5173
30
+ bun run build # vantage build → dist/
31
+ bun run check # vantage check — static diagnostics
32
+ bun run routes # vantage routes — page/API route map
33
33
  ```
34
34
 
35
- There is no `vite.config.ts`, no `main.tsx`, and no route table: adding a file under `src/` adds a route (`src/index.tsx` → `/`, `src/sales/[id].tsx` → `/sales/:id`), and `src/_layout.tsx` builds the nav from `useRoutes()`.
35
+ There is no `vite.config.ts`, no `main.tsx`, and no route table: adding a file under `src/` adds a route (`src/index.tsx` → `/`, `src/sales/[id].tsx` → `/sales/:id`), and `src/_layout.tsx` builds the nav from `useRoutes()`. The layout passes Vantage's `useNavigate()` to the header page selector so selecting a page updates the route without reloading the app.
36
36
 
37
37
  Since `@squadbase/vantage` v0.3.0, `src/` is the page-scan root and does not appear in URLs. `server/` and `public/` stay at the project root — `src/server/` is never scanned.
38
38
 
@@ -14,7 +14,7 @@ description: Vantage(@squadbase/vantage)ダッシュボードアプリを一か
14
14
 
15
15
  ## 全体の流れ
16
16
 
17
- 1. **骨組み** — `pnpm dev` が上がるところまで
17
+ 1. **骨組み** — `bun run dev` が上がるところまで
18
18
  2. **ルートを決める** — ページファイルを置き、`vantage routes` で URL を確認
19
19
  3. **データを繋ぐ** — 外部 API か、自前の `server/api` か
20
20
  4. **仕上げ** — `check` → `build` → `preview`
@@ -52,7 +52,7 @@ exit 1 になる。
52
52
  ```
53
53
 
54
54
  `index.tsx` はデフォルトエクスポートの React コンポーネント 1 つ(これが `/` になる)。あとは
55
- `pnpm install && pnpm dev` で http://localhost:5173 が上がる。
55
+ `bun install && bun run dev` で http://localhost:5173 が上がる(このプロジェクトの package manager は bun。`bun.lock` を持つ)。
56
56
 
57
57
  **既存アプリに合流したときは、作る前に現状を読む:**
58
58
 
@@ -105,10 +105,10 @@ vantage add skill # 配置済みの skill(このファイルの仲間)と、そ
105
105
  ## Step 4 — 仕上げ
106
106
 
107
107
  ```bash
108
- pnpm check # エラーが残っていないか(exit 1 なら残っている)
109
- pnpm routes # 公開される URL の最終確認
110
- pnpm build # dist/ に client(+ server)+ vantage-manifest.json
111
- pnpm preview # 本番ビルドをローカルで動かす
108
+ bun run check # エラーが残っていないか(exit 1 なら残っている)
109
+ bun run routes # 公開される URL の最終確認
110
+ bun run build # dist/ に client(+ server)+ vantage-manifest.json
111
+ bun run preview # 本番ビルドをローカルで動かす
112
112
  ```
113
113
 
114
114
  `vantage-manifest.json` の `mode` は `server/` の有無から自動で決まる(手で書かない)。
@@ -77,7 +77,7 @@ Vantage アプリの見た目は 2 つのファイルで決まる。
77
77
 
78
78
  ```css
79
79
  body {
80
- font-family: "<Font>", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
80
+ font-family: "<Font>", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Noto Sans JP", "Yu Gothic UI", Meiryo, sans-serif;
81
81
  }
82
82
  :root {
83
83
  --slide-font-display: "<Font>", system-ui, -apple-system, "Hiragino Kaku Gothic ProN", "Hiragino Sans", "Yu Gothic", Meiryo, sans-serif;
@@ -85,6 +85,8 @@ Vantage アプリの見た目は 2 つのファイルで決まる。
85
85
  }
86
86
  ```
87
87
 
88
+ 欧文の Web フォントだけを先頭に足すときも、日本語の system face(`"Hiragino Sans"` 〜 `Meiryo`)は
89
+ 必ず残す。消すとカナ・漢字がブラウザ任せのフォールバックになり、細く表示される。
88
90
  日本語の Web フォントは face が 100 を超えるので原則使わない(pptx 書き出しも壊れる。
89
91
  `vantage-slide` の `references/tokens.md` 参照)。
90
92
 
@@ -119,7 +119,7 @@ DESIGN.md — このアプリの「見た目の意図」を書くファイルで
119
119
 
120
120
  | 用途 | 指定 | 備考 |
121
121
  | --- | --- | --- |
122
- | 見出し・本文 | システムフォント(`body` の `font-family`) | Web フォントは使わない |
122
+ | 見出し・本文 | 日本語を含むシステムフォント(`body` の `font-family`) | Web フォントは使わない |
123
123
  | 数字 | 同上 + `tabular-nums` | 桁がそろうように必ず付ける |
124
124
  | スライドの見出し | `--slide-font-display` | 既定は日本語を含むシステムフォント |
125
125
  | スライドの本文 | `--slide-font-body` | 既定は見出しと同じ |
@@ -302,6 +302,34 @@ const res = await fetch(`${baseUrl}/queries/${QUERY_ID}?since=2026-01-01`, {
302
302
  })
303
303
  ```
304
304
 
305
+ ### 生の応答から行を取り出す(`noUncheckedIndexedAccess`)
306
+
307
+ テンプレートの `tsconfig.json` は `noUncheckedIndexedAccess: true`。**配列の添字はすべて
308
+ `T | undefined`** なので、HTTP Query が通す上流の生の応答(GA4 の `rows[].metricValues[]` など)を
309
+ `row.metricValues[0].value` と書くと `Object is possibly 'undefined'.` で落ちる。書く前に添字
310
+ アクセスを頭の中で型検査し、応答の型と行の変換関数を先に決めて `?.` と `??` で取り出す:
311
+
312
+ ```ts
313
+ type Ga4Row = {
314
+ dimensionValues?: { value?: string }[]
315
+ metricValues?: { value?: string }[]
316
+ }
317
+ type SessionsDaily = { date: string; sessions: number; pageViews: number }
318
+
319
+ const toSessionsDaily = (row: Ga4Row): SessionsDaily => ({
320
+ date: row.dimensionValues?.[0]?.value ?? "",
321
+ sessions: Number(row.metricValues?.[0]?.value ?? "0"),
322
+ pageViews: Number(row.metricValues?.[1]?.value ?? "0"),
323
+ })
324
+
325
+ const report = (await res.json()) as { rows?: Ga4Row[] }
326
+ return Response.json((report.rows ?? []).map(toSessionsDaily))
327
+ ```
328
+
329
+ - 値が欠けた行を落としたいときは `if (!date) throw new HttpError(502, …)` ではなく、変換後に
330
+ `.filter((r) => r.date !== "")` で除く。`date!` のような非 null 断言で黙らせない。
331
+ - SQL Query の `rows` も同じ。`rows[0]` を使うなら `const first = rows[0]; if (!first) …` で先に絞る。
332
+
305
333
  ## 構築中の一時的なエラーを「進行中」に見せる(`components/build-recovery.tsx`)
306
334
 
307
335
  ページを書いている途中は、ファイルが書き終わる順番の都合で一瞬だけページが throw する
@@ -331,3 +359,82 @@ const res = await fetch(`${baseUrl}/queries/${QUERY_ID}?since=2026-01-01`, {
331
359
  - **データ取得の失敗は各ページが `ErrorState` で出す。** `BuildRecovery` が拾うのは
332
360
  「レンダリングが throw した」場合だけで、`useApiQuery` の `isError` はページの責任
333
361
  (→ `agents/page.md` の「データ取得」)。
362
+
363
+ ## 依存の追加とスクリプトの実行は bun で
364
+
365
+ このプロジェクトの package manager は **bun** です(`squadbase.yml` の `package_manager: bun`、
366
+ lockfile は `bun.lock`)。Squadbase のサンドボックスは bun と依存のキャッシュを持っており、
367
+ `bun install` はキャッシュだけで終わります。上のフレームワーク規約に出てくる
368
+ `pnpm install` / `pnpm dev` / `pnpm check` は、そのまま `bun install` / `bun run dev` /
369
+ `bun run check` に読み替えてください。
370
+
371
+ ### 追加の不変条件
372
+
373
+ - **依存を足すときは `bun add <pkg>`(開発用は `bun add -d <pkg>`)。** `npm install` や
374
+ `pnpm add` を使わない — `package-lock.json` / `pnpm-lock.yaml` が生えて `bun.lock` と別の断面に
375
+ なり、次回起動時の install が lockfile を作り直す。
376
+ - **`bun.lock` を消さない・手で編集しない。** `package.json` の依存を直接書き換えたときは
377
+ `bun install` を一度走らせて `bun.lock` を追従させ、両方を同じコミットに入れる。
378
+ - スクリプトは `bun run <script>`(`bun run dev` / `bun run build` / `bun run check` /
379
+ `bun run routes`)。`npm run <script>` でも動くが、install に npm を使わないこと。
380
+
381
+ ## このアプリで作れるもの
382
+
383
+ ユーザーの目的に合わせて、どの形にするかを選ぶ。ここに書かれていない形は約束しない。
384
+
385
+ - **ダッシュボード** — 数字を繰り返し見に来て、変化に気づきたいとき。指標と推移・内訳を 1 画面に
386
+ 置き、`server/api/` で実データを取る。
387
+ - 書き込む場所: `src/<slug>.tsx`, `server/api/<slug>/**`
388
+ - ページ: あり
389
+ - 作り方: `src/` にページ(`.tsx`)を置く。書く前に `agents/page.md`(API は `agents/api.md`)を読む。
390
+ - **スライド** — 分析の示唆をまとめて誰かに伝えるとき(調査結果の報告、会議資料、施策の提案)。
391
+ 結論 → 根拠 → 次の一手の順にまとめる。
392
+ - 書き込む場所: `src/<slug>.slide.tsx`, `vantage-slide/**`
393
+ - ページ: あり
394
+ - 作り方: `src/<slug>.slide.tsx` に `<SlideDeck>` を返すページを置く(`@squadbase/vantage/slide`)。
395
+ 書く前に `agents/slide.md` を読み、`vantage-slide` Skill が未配置なら
396
+ `vantage add skill vantage-slide` で置いてから読む。
397
+ - **ドキュメント** — 報告書・調査のまとめ・説明資料のように、文章と図表を上から読む形で残し
398
+ 共有したいとき。
399
+ - 書き込む場所: `src/<slug>.doc.tsx`
400
+ - ページ: あり
401
+ - 作り方: `src/<slug>.doc.tsx` に `<Document>` を返すページを置く(`@squadbase/vantage/doc`。
402
+ ルートは `.doc` を除いた `/<slug>`)。数値はコードに直接書き、チャートは `EChart` を文中に置く。
403
+ 書く前に `agents/doc.md` を読む。
404
+ - **オートメーション** — 決まった時刻に集計・通知・定期レポートを回したいとき、しきい値で
405
+ アラートしたいとき。
406
+ - 書き込む場所: `automations/<slug>/**`, `pipelines/**`
407
+ - ページ: なし
408
+ - 作り方: `automations/<名前>/automation.yml` に定義し、処理のコードは `pipelines/` に置く。
409
+ 書く前に Automation のガイド(`readAutomationGuide`)を読む。
410
+
411
+ ## 表示領域に安定 ID を付ける(`data-sq-ref`)
412
+
413
+ Squadbase のチャットは、回答の根拠になったグラフ・表・KPI カードを
414
+ `squadbase://page/<path>#<ref>`(スライド内なら `?p=<N>#<ref>`)のリンクで指し、その領域を開く。
415
+ `<ref>` はページのソースに書かれた `data-sq-ref` の値で、過去の回答のリンクもこの値で今の画面を開く。
416
+
417
+ ```tsx
418
+ <DashboardCardPreset data-sq-ref="revenue-trend" title="売上の推移">
419
+ <EChart option={trendOption(data)} height="320px" />
420
+ </DashboardCardPreset>
421
+
422
+ {kpis.map((kpi) => (
423
+ <DashboardCard key={kpi.id} data-sq-ref={kpi.id}>...</DashboardCard>
424
+ ))}
425
+ ```
426
+
427
+ ### 追加の不変条件
428
+
429
+ - **意味のまとまった表示領域(グラフ・表・KPI カードなど)のルート要素に付ける。** 通常は
430
+ `DashboardCard` / `DashboardCardPreset` のように props を DOM に流す部品。`EChart` / `DataTable` /
431
+ `SectionHeader` は付けた属性を DOM に流さないので、それらを包むカード(または `div`)に付ける。
432
+ - **値の形式は `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`**(ASCII の英数字・ハイフン・アンダースコア、先頭は英数字、
433
+ 100 文字以内)。これ以外の値はリンクから開けない。推奨は意味を表す小文字 kebab-case の名前
434
+ (例: `revenue-trend`, `churn-by-segment`)。表示順・ソース上の位置・index・連番から作らない。
435
+ - **`.map` で描く領域は、データの安定したキー(`kpi.id` など)がこの形式を満たすときだけ `data-sq-ref={kpi.id}`
436
+ のように使う。** 日本語や空白を含むキーはそのまま使わず、変換もしない(変換規則が変わると値が変わる)。
437
+ その場合は静的な意味名を付けるか、付けない。
438
+ - **ページ内で一意にする。**
439
+ - **新しく作った・直した画面では付ける。更新では既存の値を変えない** — 領域の意味が変わったときだけ変える
440
+ (値を変えると、それを指していた過去のリンクが開けなくなる)。触っていない画面へ遡って付けることはしない。
@@ -56,7 +56,7 @@ DESIGN.md — このアプリの「見た目の意図」を書くファイルで
56
56
 
57
57
  | 用途 | 指定 | 備考 |
58
58
  | --- | --- | --- |
59
- | 見出し・本文 | システムフォント(`body` の `font-family`) | Web フォントは使わない |
59
+ | 見出し・本文 | 日本語を含むシステムフォント(`body` の `font-family`) | Web フォントは使わない |
60
60
  | 数字 | 同上 + `tabular-nums` | 桁がそろうように必ず付ける |
61
61
  | スライドの見出し | `--slide-font-display` | 既定は日本語を含むシステムフォント |
62
62
  | スライドの本文 | `--slide-font-body` | 既定は見出しと同じ |
@@ -25,6 +25,11 @@ export async function GET({ params, env }: ApiContext) {
25
25
  エクスポートする。それ以外の名前は `INVALID_API_EXPORT` エラー。
26
26
  - 動的 API も `[id].ts` 記法: `server/api/customers/[id].ts` → `GET /api/customers/:id`。
27
27
  `params.id` で取り出す(`noUncheckedIndexedAccess` が効くので narrowing が要る)。
28
+ - **`noUncheckedIndexedAccess` は配列・レコードの添字すべてに効く。** `arr[0]` も `obj[key]` も
29
+ `T | undefined` になる。最初に書く前に、添字アクセスと `useApiQuery` の `data` が
30
+ undefined を扱っているかを頭の中で型検査してから書く。上流の生の応答の値は
31
+ `Number(row.metricValues?.[0]?.value ?? "0")` のように `?.` と `??` で取り出す
32
+ (`!` で黙らせない)。
28
33
  - **クライアントに見せたいエラーは `HttpError(status, msg)` を throw する。** それ以外の throw は
29
34
  ログに記録され汎用の 500 に丸められる。
30
35
  - **シークレットは `ApiContext.env` にだけ届く**(クライアントには決して届かない)。
@@ -34,9 +39,12 @@ export async function GET({ params, env }: ApiContext) {
34
39
 
35
40
  ```tsx
36
41
  import { useApiQuery, useApiMutation } from "@squadbase/vantage/query"
42
+ import { Loading, ErrorState } from "@squadbase/vantage/ui"
37
43
 
38
44
  const q = useApiQuery<Customer>(`/api/customers/${customerId}`)
45
+ if (q.isPending) return <Loading />
39
46
  if (q.isError) return <ErrorState message={q.error.message} /> // HttpError のメッセージ
47
+ q.data // ここで初めて Customer(undefined ではない)
40
48
 
41
49
  // クエリ文字列は search で。undefined の項目は落ちる
42
50
  const rows = useApiQuery<Row[]>("/api/customers", { search: { segment } })
@@ -45,5 +53,26 @@ const rows = useApiQuery<Row[]>("/api/customers", { search: { segment } })
45
53
  const save = useApiMutation<Customer, Payload>("/api/customers", { method: "POST" })
46
54
  ```
47
55
 
56
+ **`data` は、同じ結果オブジェクトで `isPending` と `isError` を判定したあとでしか
57
+ narrowing されない。** 判定前や、配列の `.find` などでまとめた判定のあとでは `T | undefined` のまま。
58
+ 複数のクエリは 1 本ずつ判定してから `data` を使う(`data!` で黙らせない):
59
+
60
+ ```tsx
61
+ const sessions = useApiQuery<SessionsDaily[]>("/api/sessions-daily")
62
+ const metrics = useApiQuery<MetricsDaily[]>("/api/metrics-daily")
63
+
64
+ if (sessions.isPending || metrics.isPending) return <Loading />
65
+ if (sessions.isError) return <ErrorState message={sessions.error.message} />
66
+ if (metrics.isError) return <ErrorState message={metrics.error.message} />
67
+ // ここから sessions.data: SessionsDaily[] / metrics.data: MetricsDaily[]
68
+ return <Chart sessions={sessions.data} metrics={metrics.data} />
69
+
70
+ // NG: const failed = [sessions, metrics].find((q) => q.isError) のように配列でまとめて判定すると
71
+ // 各 q.data は narrowing されず、`T[] | undefined` を T[] の props に渡して型エラーになる
72
+ ```
73
+
74
+ カードごとに別々に読み込み中・エラーを出すなら、`data` を渡す側で `q.data ?? []` にするか、
75
+ カードのコンポーネントの中で上と同じ判定をする。
76
+
48
77
  `ApiError` は `message`・`status`・`body`・`requestId` を持つ。hook が使えない場所では
49
78
  `apiJson(path, init)`、生の `Response` が要るときは `apiFetch`。
@@ -44,6 +44,8 @@ export default function JuneSummary() {
44
44
  `useApiQuery` などで読み込むと、開くたびに値が変わります。
45
45
  - **チャートは `EChart`(`@squadbase/vantage/components`)を文中に置く。** `figure` で囲めば
46
46
  `figcaption` がキャプションになります。
47
+ - **PDF の保存ボタンは作らない。** `<Document>` が右下に PDF 保存のボタンを出し、印刷時は
48
+ 本文だけをライトの配色で出します。
47
49
  - **ページ全体を包むのは `<Document>` だけ。** `PageShell` などのダッシュボード用の枠で包まない
48
50
  (本文の幅と余白は `<Document>` が決めます)。
49
51
 
@@ -26,8 +26,8 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@squadbase/vantage": "^0.5.0",
29
- "react": "^19.2.7",
30
- "react-dom": "^19.2.7"
29
+ "react": "^19.3.1",
30
+ "react-dom": "^19.3.1"
31
31
  }
32
32
  }
33
33
  ```
@@ -11,6 +11,13 @@
11
11
  `src/monthly-report.slide.tsx` → `/monthly-report`。中身は `<SlideDeck>` を返す普通の React
12
12
  コンポーネントで、`definePage` もナビも通常のページと同じように効きます。
13
13
 
14
+ **ファイル名の決め方:**
15
+
16
+ - **ルート `/` がすでに他のページ(`index.tsx`など)で使われている場合は、スライドのタイトルから slug を作ってファイル名にする。** `index.slide.tsx` は `index.tsx` と同じ `/` に解決するので `ROUTE_CONFLICT` になる。
17
+ - 例: "Label check" というスライド → `src/label-check.slide.tsx` → `/label-check`
18
+ - 例: "Monthly Report" というスライド → `src/monthly-report.slide.tsx` → `/monthly-report`
19
+ - 新規プロジェクトでまだ `/` が空いている場合のみ、`index.slide.tsx` を使ってよい。
20
+
14
21
  ```tsx
15
22
  import { Slide, SlideDeck } from "@squadbase/vantage/slide"
16
23
 
@@ -34,7 +41,12 @@ export default function Deck() {
34
41
  送れ、左のスライド一覧(サムネイル列)は `L` で開閉、`F` で全画面、ツールバーから pptx を
35
42
  書き出せます。**書き方の作法は同梱 Skill の
36
43
  `vantage-slide`** にあります(キャンバスの縦方向の予算、段階表示、トランジション、モーフ、
37
- pptx が壊れる条件)。
44
+ pptx が壊れる条件、説明と図表のデータ時点の揃え方)。
45
+
46
+ **分析を共有するデッキでは、本文・数値・図表を同じ対象期間と同じ取得時点の結果に揃え、その期間と
47
+ 時点を短く表示する。** 説明文を固定したまま図表だけが最新値へ変わる構成にしない(ライブ更新を
48
+ 意図するなら本文も同じ取得結果から導く)。同一時点の結果を保持できない場合は、スナップショットとして
49
+ 保存できたとは報告せず、必要な保持の仕組みと保証できない範囲を伝える。
38
50
 
39
51
  `.slide` を付けられるのはルートになるページモジュールだけで、`_layout.slide.tsx` などは
40
52
  `INVALID_PAGE_VARIANT` エラーになります。`june.slide.tsx` と `june.tsx` は同じ URL に解決するので