@squadbase/vantage-template 0.1.1-dev1.57af5cd → 0.1.1-dev1.680772d

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 (34) hide show
  1. package/README.md +11 -1
  2. package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +9 -7
  3. package/base-template/.squadbase/skills/vantage-app/SKILL.md +10 -6
  4. package/base-template/.squadbase/skills/vantage-design/SKILL.md +36 -24
  5. package/base-template/.squadbase/skills/vantage-design/references/extract.md +16 -14
  6. package/base-template/.squadbase/skills/vantage-design/references/format.md +6 -5
  7. package/base-template/.squadbase/skills/vantage-design/references/presets.md +60 -110
  8. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +45 -1
  9. package/base-template/.squadbase/skills/vantage-slide/SKILL.md +299 -0
  10. package/base-template/.squadbase/skills/vantage-slide/references/morph.md +102 -0
  11. package/base-template/.squadbase/skills/vantage-slide/references/steps.md +57 -0
  12. package/base-template/.squadbase/skills/vantage-slide/references/tokens.md +87 -0
  13. package/base-template/.squadbase/skills/vantage-slide/references/transitions.md +196 -0
  14. package/base-template/AGENTS.md +113 -4
  15. package/base-template/DESIGN.md +7 -6
  16. package/base-template/agents/api.md +29 -0
  17. package/base-template/agents/csv.md +2 -1
  18. package/base-template/agents/doc.md +49 -3
  19. package/base-template/agents/page.md +54 -9
  20. package/base-template/agents/slide.md +61 -3
  21. package/base-template/agents/xlsx.md +2 -1
  22. package/base-template/bun.lock +78 -68
  23. package/base-template/bunfig.toml +5 -0
  24. package/base-template/package.json +6 -6
  25. package/base-template/src/_layout.tsx +9 -9
  26. package/base-template/src/components/build-recovery.tsx +37 -1
  27. package/base-template/src/components/user-card.tsx +10 -10
  28. package/base-template/src/index.tsx +7 -3
  29. package/base-template/src/styles.css +108 -12
  30. package/dist/index.js +956 -0
  31. package/package.json +20 -17
  32. package/ui-templates/CLAUDE.md +2 -0
  33. package/ui-templates/kpi-chart-simple/pages/index.tsx +48 -48
  34. package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +48 -48
@@ -0,0 +1,196 @@
1
+ # ページ間トランジション(`SlideTransition`)
2
+
3
+ スライドが切り替わるときの enter / exit アニメーションです。**既定はありません** ——
4
+ 宣言しない限りページはスナップで入れ替わり、そのスナップも十分に上品な既定です。
5
+ 動きが何かを足すときだけ opt-in してください。
6
+
7
+ `prefers-reduced-motion: reduce` は自動で尊重されます。フォールバックを書く必要はありません。
8
+
9
+ (2 枚にまたがる同一要素を動かす「Magic Move」は `morph.md`。この契約の上に載っています。)
10
+
11
+ ## 契約
12
+
13
+ トランジションは `<Slide transition={...}>` に付けます。**入ってくる側が勝ちます**:
14
+ A → B の移動では B の `transition` が使われ、その `exit` が A で、`enter` が B で再生されます。
15
+ B → A と戻るときは A の `transition` が使われるので、**両方向で動かしたいなら両方のスライドに
16
+ 同じオブジェクトを渡します**。
17
+
18
+ ```tsx
19
+ import { Slide, SlideDeck, type SlideTransition } from "@squadbase/vantage/slide"
20
+
21
+ const RISE: SlideTransition = { /* … */ }
22
+
23
+ <SlideDeck>
24
+ <Slide>…</Slide>
25
+ <Slide transition={RISE}>…</Slide>
26
+ </SlideDeck>
27
+ ```
28
+
29
+ ```ts
30
+ type TransitionPhase = {
31
+ keyframes: Keyframe[] | PropertyIndexedKeyframes // WAAPI のキーフレーム
32
+ duration?: number // ms(省略時は上位の duration)
33
+ easing?: string // CSS easing
34
+ delay?: number // ms —— exit と enter を重ねるのに使う
35
+ }
36
+ type SlideTransition = {
37
+ duration: number // 上位のフォールバック
38
+ easing?: string
39
+ enter?: TransitionPhase // 入ってくる側で再生
40
+ exit?: TransitionPhase // 出ていく側で再生
41
+ morph?: boolean | MorphTransition // 共有要素のモーフ → morph.md
42
+ }
43
+ ```
44
+
45
+ ラッパーには `--slide-dir`(前進 `1` / 後退 `-1`)と `data-slide-dir`(`"forward"` / `"backward"`)が
46
+ 出るので、JS のコールバック無しに 1 つのキーフレームで方向を反転できます。
47
+
48
+ ## 設計原則(ここは崩さない)
49
+
50
+ 「PowerPoint で作りました」と最も大きく告げるのは、1 つのデッキに 6 種類のトランジションがあることです。
51
+ 節度がリズムを作ります。
52
+
53
+ - **DNA を 1 つ選び、デッキ全体で保つ。** 同じ duration 帯、同じ easing の組、同じ out → in のずらし方。
54
+ 変えてよいのは**どのプロパティに小さな一押しを乗せるか**だけ —— Y、X、opacity、scale、blur
55
+ - **duration: 140–280ms。** exit 140–180ms、enter 200–280ms、enter を 80ms ほど遅らせて重ねる。
56
+ 350ms を超えると動画編集の領域で、本当の状態変化のために取っておく
57
+ - **振れ幅の上限: 12px か 3% スケール。** 6px の Y 上昇は「次の考え」に読める。1920px の translateX は
58
+ 「別の書類」に読める。上質なツールは、気づく分だけしか動かさない
59
+ - **opacity は常に混ぜる。** transform だけのトランジションは硬く、opacity だけのものは最も安全な既定
60
+ - **easing: exit は ease-in、enter は ease-out。** 出るとき `cubic-bezier(0.4, 0, 1, 1)`、
61
+ 入るとき `cubic-bezier(0, 0, 0.2, 1)`。`linear` は使わない(スライドショーに見える)。
62
+ 対称な ease-in-out は状態が繋がるモーフのために取っておく
63
+
64
+ ## 上品な family —— 6 つ、1 つの DNA
65
+
66
+ ここから選んでください。1 つをデッキの標準にし、必要ならヒーロー / 表紙用に 2 つ目、
67
+ 本当の章の切れ目に 3 つ目を取っておきます。
68
+
69
+ ```tsx
70
+ const EASE_OUT = "cubic-bezier(0, 0, 0.2, 1)"
71
+ const EASE_IN = "cubic-bezier(0.4, 0, 1, 1)"
72
+
73
+ // RISE —— 標準の静けさ。Y に 6px。デッキの既定にする
74
+ const RISE: SlideTransition = {
75
+ duration: 200,
76
+ exit: {
77
+ duration: 140, easing: EASE_IN,
78
+ keyframes: [
79
+ { opacity: 1, transform: "translateY(0)" },
80
+ { opacity: 0, transform: "translateY(-4px)" },
81
+ ],
82
+ },
83
+ enter: {
84
+ duration: 200, delay: 80, easing: EASE_OUT,
85
+ keyframes: [
86
+ { opacity: 0, transform: "translateY(6px)" },
87
+ { opacity: 1, transform: "translateY(0)" },
88
+ ],
89
+ },
90
+ }
91
+
92
+ // DISSOLVE —— opacity だけ。最も静か
93
+ const DISSOLVE: SlideTransition = {
94
+ duration: 240,
95
+ exit: { duration: 200, easing: EASE_IN, keyframes: [{ opacity: 1 }, { opacity: 0 }] },
96
+ enter: { duration: 240, delay: 40, easing: EASE_OUT, keyframes: [{ opacity: 0 }, { opacity: 1 }] },
97
+ }
98
+
99
+ // SETTLE —— 表紙向き。上昇 + enter だけにわずかな blur
100
+ const SETTLE: SlideTransition = {
101
+ duration: 280,
102
+ exit: {
103
+ duration: 160, easing: EASE_IN,
104
+ keyframes: [
105
+ { opacity: 1, transform: "translateY(0)" },
106
+ { opacity: 0, transform: "translateY(-6px)" },
107
+ ],
108
+ },
109
+ enter: {
110
+ duration: 280, delay: 100, easing: EASE_OUT,
111
+ keyframes: [
112
+ { opacity: 0, transform: "translateY(12px)", filter: "blur(4px)" },
113
+ { opacity: 1, transform: "translateY(0)", filter: "blur(0)" },
114
+ ],
115
+ },
116
+ }
117
+
118
+ // BLOOM —— scale 0.97 → 1、移動なし。その場で像を結ぶ
119
+ const BLOOM: SlideTransition = {
120
+ duration: 240,
121
+ exit: {
122
+ duration: 160, easing: EASE_IN,
123
+ keyframes: [
124
+ { opacity: 1, transform: "scale(1)" },
125
+ { opacity: 0, transform: "scale(1.01)" },
126
+ ],
127
+ },
128
+ enter: {
129
+ duration: 240, delay: 80, easing: EASE_OUT,
130
+ keyframes: [
131
+ { opacity: 0, transform: "scale(0.97)" },
132
+ { opacity: 1, transform: "scale(1)" },
133
+ ],
134
+ },
135
+ }
136
+
137
+ // FALL —— RISE の鏡像。上から降りてくる
138
+ const FALL: SlideTransition = {
139
+ duration: 200,
140
+ exit: {
141
+ duration: 140, easing: EASE_IN,
142
+ keyframes: [
143
+ { opacity: 1, transform: "translateY(0)" },
144
+ { opacity: 0, transform: "translateY(4px)" },
145
+ ],
146
+ },
147
+ enter: {
148
+ duration: 200, delay: 80, easing: EASE_OUT,
149
+ keyframes: [
150
+ { opacity: 0, transform: "translateY(-6px)" },
151
+ { opacity: 1, transform: "translateY(0)" },
152
+ ],
153
+ },
154
+ }
155
+
156
+ // BREATH —— 章の切れ目。完全に消してから 120ms 置いて入る
157
+ // 本当の区切りにだけ、デッキ 1 本で 1〜2 回まで
158
+ const BREATH: SlideTransition = {
159
+ duration: 460,
160
+ exit: { duration: 180, easing: EASE_IN, keyframes: [{ opacity: 1 }, { opacity: 0 }] },
161
+ enter: {
162
+ duration: 240, delay: 300, easing: EASE_OUT,
163
+ keyframes: [
164
+ { opacity: 0, transform: "translateY(8px)" },
165
+ { opacity: 1, transform: "translateY(0)" },
166
+ ],
167
+ },
168
+ }
169
+ ```
170
+
171
+ 6 つは同じ DNA を共有していて、小さな一押しをどのプロパティに乗せるかだけが違います。
172
+ 読み手には変化に見え、それでいて 1 つの手つきに読めます。
173
+
174
+ ## 方向を意識したキーフレーム(控えめに)
175
+
176
+ 上品なツールの多くは後退時に鏡像化しません。本当に必要なとき(横に流れるスライドを逆再生したい等)
177
+ だけ `--slide-dir` を `calc()` の中で使います:
178
+
179
+ ```tsx
180
+ { transform: "translateX(calc(var(--slide-dir, 1) * 8px))" },
181
+ { transform: "translateX(0)" },
182
+ ```
183
+
184
+ 毎回これに手が伸びるなら、たぶん設計しすぎです。前進 = 後退の方が洗練された既定。
185
+
186
+ ## アンチパターン
187
+
188
+ - ❌ 6 ページに 6 種類のトランジション —— 最も大きな「PowerPoint 感」の合図
189
+ - ❌ `translateX(100%)` の横スライド —— iOS のモーダルや PowerPoint の Push であって、スライドの切り替えではない
190
+ - ❌ 強いスケールのポップ(`0.85 → 1`)+ blur —— ライトボックスの語彙で、何かに「ズームイン」する意味になる
191
+ - ❌ `clip-path: inset(…)` のワイプ —— After Effects の語彙。芝居がかる
192
+ - ❌ 両方のレイヤーに同時に blur —— 視覚的な濁り。目が焦点を結べない
193
+ - ❌ 通常の切り替えで 350ms を超える duration —— 間延びする
194
+ - ❌ 12px / 3% を超える移動 —— 連続ではなく断絶に読める
195
+ - ❌ `linear` easing —— プロダクトではなくスライドショーに見える
196
+ - ❌ とりあえずトランジションを宣言する。**理由が無いなら書かない。** スナップで十分
@@ -93,13 +93,25 @@ root/
93
93
  | `@squadbase/vantage/router` | `Link`・`Outlet`・`useParams`・`useSearch`・`useNavigate`・`redirect`・`notFound`・`useRoutes`・`useCurrentRoute`・`useSearchParam`・`useSearchState` |
94
94
  | `@squadbase/vantage/query` | `useApiQuery`・`useApiMutation`・`apiJson`・`apiFetch`・`apiUrl`・`ApiError`、ほか `useQuery`/`useMutation` など TanStack Query の再エクスポート |
95
95
  | `@squadbase/vantage/ui` | shadcn/ui(Base UI バリアント)プリミティブ(`Button`・`Loading`・`ErrorState`・`Empty` ほか) |
96
- | `@squadbase/vantage/components` | 複合パーツ(`PageShell`・`DashboardCardPreset`・`DataTablePreset`・`EChart` ほか) |
96
+ | `@squadbase/vantage/components` | 複合パーツ(`PageShell`・`DashboardCardPreset`・`DataTablePreset`、KPI の `KpiCard`・`Delta`、型付きチャート `LineChart`・`BarChart`・`AreaChart`・`DonutChart`・`BarList` ほか、`EChart`、書式の `formatValue`) |
97
97
  | `@squadbase/vantage/table` | `FilePageView`・`TableViewer`(データファイルページの表ビューア) |
98
- | `@squadbase/vantage/slide` | `SlideDeck`・`Slide`・`Steps`・`Step`・`MorphElement`・`useSlideNumber`・`useIsActiveSlide`(スライド専用。1920×1080 の固定キャンバス) |
99
- | `@squadbase/vantage/doc` | `Document`(ドキュメント専用。中の素の HTML 要素に見出し・表などのスタイルが当たる) |
98
+ | `@squadbase/vantage/slide` | `SlideDeck`・`Slide`・`Steps`・`Step`・`MorphElement`・`useSlideNumber`・`useIsActiveSlide`、1 枚分の block `SlideCover`・`SlideExecSummary`・`SlideKpiHero`・`SlideKpiGrid`・`SlideChartInsight`・`SlideScorecardTable`・`SlideNextActions`(スライド専用。1920×1080 の固定キャンバス) |
99
+ | `@squadbase/vantage/doc` | `Document`、ドキュメント用の block `DocReportHeader`・`DocExecutiveSummary`・`DocKpiScorecard`・`DocFigure`・`DocDataTable`・`DocCallout`・`DocKeyFindings`・`DocActionItems`(ドキュメント専用。中の素の HTML 要素に見出し・表などのスタイルが当たる) |
100
100
  | `@squadbase/vantage/markdown` | `MarkdownRenderer`(Shiki を隔離するための専用サブパス) |
101
101
  | `@squadbase/vantage/server` | `ApiContext`・`ApiHandler`・`HttpError`・`isHttpError`・`json`(server/ 側でのみ使う) |
102
102
 
103
+ **KPI は `KpiCard`、増減は `Delta`(`comparison` と `polarity` を渡す)。** 向き・率・色は値から計算
104
+ されるので手で渡さない。`TrendIndicator` は非推奨(`direction` を手で渡すので値と食い違いうる)。props は
105
+ `vantage docs kpi-card` / `vantage docs delta` を引く。
106
+
107
+ ```tsx
108
+ import { KpiCard, Delta } from "@squadbase/vantage/components";
109
+
110
+ <KpiCard label="売上" value={48_200_000} format="currency"
111
+ comparison={{ previous: 42_900_000, vs: "prior-period" }} />
112
+ <Delta value={0.021} comparison={{ previous: 0.026, vs: "prior-period" }} polarity="lower-is-better" />
113
+ ```
114
+
103
115
  ## モードは 2 つ、切り替えるのは `server/` の有無だけ
104
116
 
105
117
  `server/` ディレクトリがあれば **fullstack**、無ければ **SPA**。`vantage-manifest.json` の
@@ -137,6 +149,15 @@ root/
137
149
  | `.xlsx` ページ | `agents/xlsx.md` |
138
150
  | `.csv` ページ | `agents/csv.md` |
139
151
 
152
+ **ページになるのは `.tsx` / `.jsx`(`.slide.tsx`・`.doc.tsx` を含む)・`.csv`・`.xlsx` だけ。**
153
+
154
+ - `.xls`・`.xlsm`・`.xlsb`・`.ods`・`.tsv`・`.numbers`・`.parquet` はルートにならず、404 にもならない。
155
+ `vantage check` の `UNSUPPORTED_PAGE_FILE` 警告だけが知らせる。`.xlsx` / `.csv` に変換して置く
156
+ - `.md`・`.json`・`.txt` は何も言わずに無視される。Markdown を見せたいときは `.tsx` のページで
157
+ `MarkdownRenderer`(`@squadbase/vantage/markdown`)に渡す
158
+ - `.csv` / `.xlsx` はブラウザでパースする。25MB を超えるとそのページにエラーが出る
159
+ (`FILE_PAGE_TOO_LARGE` 警告)。20 万行を超えるシートは先頭 20 万行だけが表示される
160
+
140
161
  ## 環境変数
141
162
 
142
163
  - **クライアントで読めるのは `import.meta.env.PUBLIC_*` と `MODE`/`DEV`/`PROD`/`SSR`/`BASE_URL`
@@ -224,7 +245,7 @@ vantage add skill --all --dir .claude/skills # 置き場所を指定(既定は
224
245
  > `vantage upgrade` はこのファイルをフレームワーク同梱の正本で丸ごと上書きするため、
225
246
  > このセクションは消えます。消えたら squadbase-monorepo の
226
247
  > `packages/vantage-template/agents/vantage/` から貼り直してください
227
- > (リポジトリ側では `npm run agents` が正本と結合して再生成します)。
248
+ > (リポジトリ側では `pnpm run agents` が正本と結合して再生成します)。
228
249
 
229
250
  ## Squadbase の Connection / Query からデータを読む
230
251
 
@@ -342,6 +363,10 @@ return Response.json((report.rows ?? []).map(toSessionsDaily))
342
363
  `components/build-recovery.tsx` の `BuildRecovery` が、その画面を DOM から検知して
343
364
  `Working on it…` 表示を被せ、dev では 1.2 秒待ってから自動でリロードして復帰させる
344
365
  (上限 3 回。使い切ったら「再読み込み」の案内に降格し、無事に描けたら残数が戻る)。
366
+ **ただし `window.__VANTAGE_DRAFT__?.active` が `true` の間(AI の書きかけのファイルを下書きとして
367
+ プレビューに出している間)は、覆わず・リロードもしない。** 下書き中の描画エラーはフレームワークが
368
+ 直前に通った版へ戻すので、ここで覆うとその版の表示まで隠れ、リロードすると下書きの表示ごと消える。
369
+ 下書きが終わった時点でエラー画面が残っていれば、通常どおり覆う。
345
370
  降格した画面にはエラー本文をそのまま出し、`Copy log` でコピーできるようにしてある —
346
371
  自力で直らないときに本文をそのまま貼り戻してもらうため。
347
372
  `_error.tsx` も同じ `RecoveringState` を描くので、ルートレイアウトが throw したときの
@@ -377,6 +402,9 @@ lockfile は `bun.lock`)。Squadbase のサンドボックスは bun と依存
377
402
  `bun install` を一度走らせて `bun.lock` を追従させ、両方を同じコミットに入れる。
378
403
  - スクリプトは `bun run <script>`(`bun run dev` / `bun run build` / `bun run check` /
379
404
  `bun run routes`)。`npm run <script>` でも動くが、install に npm を使わないこと。
405
+ - **公開から 48 時間未満の版は入らない。** `bunfig.toml` の `minimumReleaseAge`(サプライチェーン対策)で
406
+ 弾かれる。`bun add` / `bun install` が「バージョンが見つからない」で落ちたら、直近の版を避けて
407
+ 少し前の版を指定する。`bunfig.toml` の設定を緩めたり `@squadbase/*` 以外の除外を足したりしない。
380
408
 
381
409
  ## このアプリで作れるもの
382
410
 
@@ -394,9 +422,90 @@ lockfile は `bun.lock`)。Squadbase のサンドボックスは bun と依存
394
422
  - 作り方: `src/<slug>.slide.tsx` に `<SlideDeck>` を返すページを置く(`@squadbase/vantage/slide`)。
395
423
  書く前に `agents/slide.md` を読み、`vantage-slide` Skill が未配置なら
396
424
  `vantage add skill vantage-slide` で置いてから読む。
425
+ - **ドキュメント** — 報告書・調査のまとめ・説明資料のように、文章と図表を上から読む形で残し
426
+ 共有したいとき。
427
+ - 書き込む場所: `src/<slug>.doc.tsx`
428
+ - ページ: あり
429
+ - 作り方: `src/<slug>.doc.tsx` に `<Document>` を返すページを置く(`@squadbase/vantage/doc`。
430
+ ルートは `.doc` を除いた `/<slug>`)。数値はコードに直接書き、チャートは `@squadbase/vantage/components` のチャート部品を文中に置く。
431
+ 書く前に `agents/doc.md` を読む。
397
432
  - **オートメーション** — 決まった時刻に集計・通知・定期レポートを回したいとき、しきい値で
398
433
  アラートしたいとき。
399
434
  - 書き込む場所: `automations/<slug>/**`, `pipelines/**`
400
435
  - ページ: なし
401
436
  - 作り方: `automations/<名前>/automation.yml` に定義し、処理のコードは `pipelines/` に置く。
402
437
  書く前に Automation のガイド(`readAutomationGuide`)を読む。
438
+
439
+ CSV / Excel(`.csv` / `.xlsx`)は上の項目とは別で、書いて作るものではない。受け取ったファイルを
440
+ `src/` に置くだけで表として読めるページになる(`src/data/june.xlsx` → `/data/june`。置く前に
441
+ `agents/csv.md` / `agents/xlsx.md` を読む)。値だけが表示され、数式・書式は再現されない。25MB を
442
+ 超えるとエラー、20 万行を超えると先頭 20 万行だけが出る。`.xls`・`.ods`・`.tsv` などはページにならない
443
+ (`vantage check` の `UNSUPPORTED_PAGE_FILE` 警告)。
444
+
445
+ ## 表示領域に安定 ID を付ける(`data-sq-ref`)
446
+
447
+ Squadbase のチャットは、回答の根拠になったグラフ・表・KPI カードを
448
+ `squadbase://page/<path>#<ref>`(スライド内なら `?p=<N>#<ref>`)のリンクで指し、その領域を開く。
449
+ `<ref>` はページのソースに書かれた `data-sq-ref` の値で、過去の回答のリンクもこの値で今の画面を開く。
450
+
451
+ ```tsx
452
+ <DashboardCardPreset data-sq-ref="revenue-trend" title="売上の推移">
453
+ <EChart option={trendOption(data)} height="320px" />
454
+ </DashboardCardPreset>
455
+
456
+ {kpis.map((kpi) => (
457
+ <DashboardCard key={kpi.id} data-sq-ref={kpi.id}>...</DashboardCard>
458
+ ))}
459
+ ```
460
+
461
+ ### 追加の不変条件
462
+
463
+ - **意味のまとまった表示領域(グラフ・表・KPI カードなど)のルート要素に付ける。** 通常は
464
+ `DashboardCard` / `DashboardCardPreset` のように props を DOM に流す部品。`EChart` と ECharts で描く
465
+ チャート部品(`LineChart` / `BarChart` / `AreaChart` / `DonutChart`)・`DataTable` / `SectionHeader` は
466
+ 付けた属性を DOM に流さないので、それらを包むカード(または `div`)に付ける。
467
+ - **値の形式は `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`**(ASCII の英数字・ハイフン・アンダースコア、先頭は英数字、
468
+ 100 文字以内)。これ以外の値はリンクから開けない。推奨は意味を表す小文字 kebab-case の名前
469
+ (例: `revenue-trend`, `churn-by-segment`)。表示順・ソース上の位置・index・連番から作らない。
470
+ - **`.map` で描く領域は、データの安定したキー(`kpi.id` など)がこの形式を満たすときだけ `data-sq-ref={kpi.id}`
471
+ のように使う。** 日本語や空白を含むキーはそのまま使わず、変換もしない(変換規則が変わると値が変わる)。
472
+ その場合は静的な意味名を付けるか、付けない。
473
+ - **ページ内で一意にする。**
474
+ - **新しく作った・直した画面では付ける。更新では既存の値を変えない** — 領域の意味が変わったときだけ変える
475
+ (値を変えると、それを指していた過去のリンクが開けなくなる)。触っていない画面へ遡って付けることはしない。
476
+
477
+ ## ファイル内の書く順序(値 → 画面本体 → 子の部品)
478
+
479
+ ページ・部品のファイルは、上から次の順に書く。
480
+
481
+ 1. `import`(と `definePage(...)`)
482
+ 2. **値** — データ・定数・列定義・小さな関数。短く
483
+ 3. **画面本体** — `export default` のコンポーネント
484
+ 4. **子の部品** — 大文字始まりの部品。`function` 宣言で書く
485
+
486
+ 理由: ファイルは上から順に書かれ、書きかけでもプレビューに反映される。**本体が書かれた時点で画面が出る。**
487
+ 値を本体より後ろに書くと、本体はまだ無い値を読むので、その値が書かれるまで画面が出ない。
488
+ 子の部品は後ろでよい — 未定義の部品はプレビューが仮の枠で埋める。
489
+
490
+ ```tsx
491
+ import { definePage } from "@squadbase/vantage"
492
+
493
+ export const page = definePage({ title: "売上" })
494
+
495
+ const KPI_ITEMS: Kpi[] = [/* … */] // 値は本体より上
496
+ type Kpi = { label: string; value: number }
497
+
498
+ export default function SalesPage() { // 本体
499
+ return <KpiRow items={KPI_ITEMS} />
500
+ }
501
+
502
+ function KpiRow({ items }: { items: Kpi[] }) { /* … */ } // 子の部品は下
503
+ ```
504
+
505
+ ### 追加の不変条件
506
+
507
+ - **子の部品は `function` 宣言で書く。** 宣言は巻き上げられるので、本体より後ろでも安全。
508
+ `const Foo = () => …` のアロー関数は後ろに置くと、モジュール評価時に参照された瞬間に TDZ(初期化前参照)で落ちる。
509
+ - **値(定数・データ・列定義・小さな関数)は本体より上。** 小文字の関数・hook も値として上に置く。
510
+ - 型は消えるのでどこでもよい。値のそばに置く。
511
+ - 順序だけのために、既に並んでいるファイルを書き換えない。
@@ -26,7 +26,7 @@ DESIGN.md — このアプリの「見た目の意図」を書くファイルで
26
26
 
27
27
  | 役割 | トークン | 使う場所 |
28
28
  | --- | --- | --- |
29
- | 主色 (Primary) | `--primary` / `--primary-foreground` | 主要ボタン、選択中の状態。既定は無彩色 |
29
+ | 主色 (Primary) | `--primary` / `--primary-foreground` | 主要ボタン、選択中の状態。既定(テーマ `neutral`)は青 |
30
30
  | 強調 (Accent) | `--chart-1` | 1 つだけ目立たせたい数値・系列・リンク的な強調 |
31
31
  | 良い (Positive) | `--chart-2` | 前期比のプラスなど、良い変化の小さな印 |
32
32
  | 悪い (Negative) | `--destructive` | 前期比のマイナス、エラー、削除操作 |
@@ -43,20 +43,21 @@ DESIGN.md — このアプリの「見た目の意図」を書くファイルで
43
43
  ## チャート (Charts)
44
44
 
45
45
  - 系列の順番は `--chart-1` → `--chart-2` → `--chart-3` → `--chart-4` → `--chart-5`
46
- (現在はプリセット `blue`: 青 → ティール → フクシア → バイオレット → グレー)。
47
- - 1 系列だけを目立たせるときは、その系列を `--chart-1`、残りをすべて `--chart-5`(グレー)にする。
46
+ (現在はテーマ `neutral`: 青 → 黄緑 → 赤紫 → ティール → 紫)。
47
+ - 1 系列だけを目立たせるときは、その系列を `--chart-1`、残りをすべて `--chart-5` にする。
48
48
  - 良い / 悪いを色で塗り分けるときは `--chart-2` / `--destructive`。色だけに頼らず ▲▼ や符号も添える。
49
49
  - 系列は 5 つまで。それ以上はグループ化するか「その他」にまとめる。
50
50
  - 禁止: 同じチャートで `--destructive` を普通の系列色として使う(悪い値に見える)。
51
51
  - 禁止: 隣り合う系列に `--chart-3` と `--chart-4` を並べて細い線で描く(見分けにくい)。
52
- - 配色を丸ごと変えるときは `npx @squadbase/vantage-template chart <preset>` を使う
53
- (`blue` / `ocean` / `sunset` / `forest` / `berry` / `mono`)。
52
+ - 配色を丸ごと変えるときは `npx @squadbase/vantage-template theme <id>` を使う
53
+ (`neutral` / `ocean` / `forest` / `sunset` / `violet` / `rose` / `mono`)。チャートだけ別の配色にするときは
54
+ `chart <preset>`(`blue` / `ocean` / `sunset` / `forest` / `berry` / `mono`)。
54
55
 
55
56
  ## 書体 (Typography)
56
57
 
57
58
  | 用途 | 指定 | 備考 |
58
59
  | --- | --- | --- |
59
- | 見出し・本文 | システムフォント(`body` の `font-family`) | Web フォントは使わない |
60
+ | 見出し・本文 | 日本語を含むシステムフォント(`--font-sans`) | Web フォントは使わない |
60
61
  | 数字 | 同上 + `tabular-nums` | 桁がそろうように必ず付ける |
61
62
  | スライドの見出し | `--slide-font-display` | 既定は日本語を含むシステムフォント |
62
63
  | スライドの本文 | `--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`。
@@ -27,6 +27,7 @@
27
27
  ```
28
28
  - 数式・グラフ・セル書式といった概念は csv には無いので、値がそのまま読めます。ブラウザで
29
29
  パースするので 25MB を超えるファイルはそのページにエラーが出ます —— 大きいデータは
30
- `server/api/` から配って `DataTable` で描いてください
30
+ `server/api/` から配って `DataTable` で描いてください。20 万行を超えると先頭
31
+ 20 万行だけが表示されます(残りはダウンロードして見る)
31
32
  - **ビューアの無い拡張子(`.tsv` など)はルートになりません。** `vantage check` の
32
33
  `UNSUPPORTED_PAGE_FILE` 警告が知らせます
@@ -12,9 +12,16 @@
12
12
 
13
13
  ```tsx
14
14
  import { definePage } from "@squadbase/vantage"
15
- import { EChart } from "@squadbase/vantage/components"
15
+ import { BarChart } from "@squadbase/vantage/components"
16
16
  import { Document } from "@squadbase/vantage/doc"
17
17
 
18
+ const weekly = [
19
+ { week: "第1週", 売上: 1180 },
20
+ { week: "第2週", 売上: 1240 },
21
+ { week: "第3週", 売上: 1210 },
22
+ { week: "第4週", 売上: 1620 },
23
+ ]
24
+
18
25
  export const page = definePage({ title: "6月度サマリー", navLabel: "6月度サマリー" })
19
26
  export default function JuneSummary() {
20
27
  return (
@@ -23,7 +30,7 @@ export default function JuneSummary() {
23
30
  <p>6月の売上は <strong>5,250 万円</strong>で、目標を 5% 上回りました。</p>
24
31
  <h2>週次の売上</h2>
25
32
  <figure>
26
- <EChart option={option} height={280} ariaLabel="6月の週次売上" />
33
+ <BarChart data={weekly} index="week" categories={["売上"]} height={280} ariaLabel="6月の週次売上" />
27
34
  <figcaption>第4週はキャンペーン初週</figcaption>
28
35
  </figure>
29
36
  <table>…</table>
@@ -42,13 +49,35 @@ export default function JuneSummary() {
42
49
  見た目を変えたいときはユーティリティを並べず、下の `--doc-*` を変えます。
43
50
  - **数値はコードに直接書く。** ドキュメントは書いた時点の内容を残すページです。
44
51
  `useApiQuery` などで読み込むと、開くたびに値が変わります。
45
- - **チャートは `EChart`(`@squadbase/vantage/components`)を文中に置く。** `figure` で囲めば
52
+ - **チャートは型付きチャート(`BarChart`・`LineChart` など。`@squadbase/vantage/components`)を
53
+ 文中に置く**(選び方は `agents/page.md` の「グラフはまず型付きチャートで描く」)。`figure` で囲めば
46
54
  `figcaption` がキャプションになります。
47
55
  - **PDF の保存ボタンは作らない。** `<Document>` が右下に PDF 保存のボタンを出し、印刷時は
48
56
  本文だけをライトの配色で出します。
49
57
  - **ページ全体を包むのは `<Document>` だけ。** `PageShell` などのダッシュボード用の枠で包まない
50
58
  (本文の幅と余白は `<Document>` が決めます)。
51
59
 
60
+ ## まず block を探す
61
+
62
+ 1 節分の中身は、手で組む前に `@squadbase/vantage/doc` の block から探します(`<Document>` の直下に
63
+ 並べる。多くは 1 block = 1 節。`DocCallout`・`DocFigure`・`DocDataTable` は節ではなく、節の中(本文の間)に置く部品)。props は `vantage docs <name>`(例: `vantage docs doc-kpi-scorecard`)で引く。
64
+
65
+ | block | 用途 |
66
+ | --- | --- |
67
+ | `DocReportHeader` | レポートの冒頭(タイトル・サブタイトル・対象期間・データ時点・作成者・出典) |
68
+ | `DocExecutiveSummary` | 結論を先に述べる要約の節 |
69
+ | `DocKpiScorecard` | 主要指標をまとめたスコアカード |
70
+ | `DocFigure` | 図表 1 つとキャプション(チャートは children で渡す) |
71
+ | `DocDataTable` | 素の `table` による表 |
72
+ | `DocCallout` | 補足・示唆・注意・決定の囲み(`kind`: `note` / `insight` / `warning` / `decision`) |
73
+ | `DocKeyFindings` | 主要な発見の箇条 |
74
+ | `DocActionItems` | 次のアクション(担当・期限つき) |
75
+
76
+ **props の渡し方は block 間で共通。** 数値は `value: number | null` と `format`(整形は block がする。表示を上書きするときだけ `display`。`null` は「—」)。前期比などは `delta: { value, format? }` と `comparisonLabel`(例 `"前年同期比"`)、良し悪しは指標ごとの `betterWhen`(`"up"` が既定、コストなど下がるほど良いものは `"down"`。向きは block が符号から出す)。目標は `target: number`。出典は `source`、時点は `asOf` に**日付だけ**(例 `"2026-09-30"`。「データ時点」は block が付けるので「時点」を書かない)。時系列は、単一系列なら `{ x, y }[]`(`trend`)。
77
+
78
+ block が合わない**1 節だけ**を素の HTML 要素で手で書く。import 元は `@squadbase/vantage/doc` のままで、
79
+ block のための新しい import 元はありません。本文は文字列か ReactNode で渡し、Markdown 文字列は渡さない。
80
+
52
81
  ## 見た目を変える
53
82
 
54
83
  配色・書体・本文の幅は `--doc-*` の CSS 変数です。既定値はアプリのトークンへの別名なので、
@@ -73,3 +102,20 @@ export default function JuneSummary() {
73
102
  `.doc` を付けられるのはルートになるページモジュールだけで、`_layout.doc.tsx` などは
74
103
  `INVALID_PAGE_VARIANT` エラーになります。`june.doc.tsx` と `june.tsx` は同じ URL に解決するので
75
104
  `ROUTE_CONFLICT` です。
105
+
106
+ ## ファイル形式の規約
107
+
108
+ 置き場所・URL・default export・`definePage` の書き方は全ページ共通(`agents/page.md` の
109
+ 「ページファイルの規約」)。ここではドキュメント固有のものだけを挙げます。
110
+
111
+ **MUST**
112
+
113
+ - ファイル名は `<name>.doc.tsx`(`.jsx` も可)。サフィックスは小文字で 1 つだけ。`.doc.mdx` は
114
+ `INVALID_PAGE_VARIANT`、`.DOC.tsx`・二重サフィックスは黙って通常ページになる
115
+ - default export は `<Document>` をルートに返す。フレームワークは包まない
116
+ - ドキュメントの部品は `@squadbase/vantage/doc` から import する
117
+
118
+ **SHOULD**
119
+
120
+ - `PageShell` で包まない・PDF の保存ボタンを自作しない(上の「書き方」)
121
+ - 数値はコードに直接書き、いつ時点の値かを本文に書く
@@ -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
  ```
@@ -71,6 +71,7 @@ URL になる。各ページは**デフォルトエクスポートの React コ
71
71
  | `_404.tsx` | Not Found ページ |
72
72
  | `_error.tsx` | ルートが throw したときのエラーページ |
73
73
  | `monthly-report.slide.tsx` | `/monthly-report`(スライドデッキ。詳しくは `agents/slide.md`) |
74
+ | `june-summary.doc.tsx` | `/june-summary`(ドキュメント。詳しくは `agents/doc.md`) |
74
75
  | `sales-export.csv` | `/sales-export`(詳しくは `agents/csv.md`) |
75
76
  | `data/june.xlsx` | `/data/june`(詳しくは `agents/xlsx.md`) |
76
77
 
@@ -89,6 +90,26 @@ export default function MonthlyAnalysis() {
89
90
  }
90
91
  ```
91
92
 
93
+ ### ページファイルの規約(`.tsx` / `.slide.tsx` / `.doc.tsx` 共通)
94
+
95
+ **MUST**
96
+
97
+ - ページルートの配下に置く。`components/` などの走査対象外のディレクトリに置いたファイルはページに
98
+ ならず、エラーも出ない。`_` 始まりの名前は予約名(`_layout` / `_404` / `_error`)にだけ使う
99
+ (それ以外の `_` 始まりはページにならない。予約名にサフィックスを付けると `INVALID_PAGE_VARIANT`)
100
+ - URL はサフィックスと拡張子を除いたパスで決まる。同じ URL になる別ファイル(`june.tsx` /
101
+ `june.slide.tsx` / `june.doc.tsx` / `june.csv`)を並べない(`ROUTE_CONFLICT`)。テンプレートの
102
+ 未編集の `index.tsx` だけは例外で、`index.slide.tsx` / `index.doc.tsx` が `/` を取れる
103
+ - default export は React の関数コンポーネント(無いと `MISSING_DEFAULT_EXPORT`。関数かどうかは
104
+ 検査されない)
105
+ - `definePage({ ... })` にはオブジェクトリテラルを直接渡し、`title` / `description` / `navLabel` は
106
+ 文字列リテラルで書く。ビルド時に静的抽出されるので、変数・関数呼び出し・spread・shorthand・
107
+ `${}` を含むテンプレート文字列は**エラーにならず黙ってメタなし**になる
108
+
109
+ **SHOULD**
110
+
111
+ - `definePage` に `title` と `navLabel` を書く(ナビの表示名とブラウザのタブ名になる)
112
+
92
113
  ### ナビはルート一覧から組む
93
114
 
94
115
  `useRoutes()` がページルート一覧(スキャン順)を返すので、リンク配列を手で持たない。
@@ -109,7 +130,7 @@ routes.map((r) => (
109
130
  ```
110
131
 
111
132
  `RouteInfo`: `path`(`/sales/:id`)・`to`(`/sales/$id`)・`params`・`dynamic`・`index`・`label`・
112
- `title`・`description`・`navLabel`・`kind`(`"module"` | `"file"`)・`variant`(`"slide"`)。
133
+ `title`・`description`・`navLabel`・`kind`(`"module"` | `"file"`)・`variant`(`"slide"` | `"doc"`)。
113
134
  後ろの 2 つで、ナビをダッシュボード / スライド / データに分けたりアイコンを変えたりできます。
114
135
 
115
136
  ### フィルタ状態は URL に置く
@@ -164,17 +185,41 @@ refetchOnWindowFocus false・networkMode "always")。挙動を変えたいとき
164
185
  ## 組み込み UI / コンポーネントの入口
165
186
 
166
187
  - **プリミティブ**は `@squadbase/vantage/ui` から import する(`Button`・`Loading`・
167
- `ErrorState`・`Empty`・`Select`・`Checkbox` ほか)。
188
+ `ErrorState`・`Empty`・`Select`・`Checkbox`・`RadioGroup`・`Field`・`Combobox`・`Pagination`・
189
+ `Avatar`・`Kbd`・`HoverCard`・`AlertDialog`・`Drawer`・`Toaster` + `toast.add()` ほか)。
168
190
  - **複合パーツ**は `@squadbase/vantage/components` から(`PageShell`・`DashboardCardPreset`・
169
- `DataTablePreset`・`EChart` ほか)。
191
+ `DataTablePreset`・型付きチャート(下の「グラフ」)・`EChart` ほか)。
170
192
  - **どの名前が import 可能か / props の詳細は `vantage docs` で引く。** ガイドとコンポーネント
171
193
  リファレンスはパッケージに同梱されていて、オフラインでも読める(→ `AGENTS.md` の
172
194
  「ドキュメントを引く」)。**名前が分からないとき**はやりたいことで `vantage search` する
173
195
  (→ 同節)。
174
- - **UI は import して使うのが既定。取り出す(eject)必要はない。** `vantage add ui|block` は
175
- ソースを手元にコピーして**改造したいとき専用**のコマンドで、取り出せるのは
176
- `ui: data-table` と `block: sales-overview` の 2 つだけ。それ以外の名前は import 専用で
177
- `add` の対象ではない(未知名で実行すると候補が一覧表示される)。
196
+ - **UI は import して使うのが既定。取り出す(eject)必要はない。** `vantage add ui <name>` は
197
+ ソースを手元にコピーして**改造したいとき専用**のコマンド。`/ui` のプリミティブも
198
+ `/components` の複合パーツも全部取り出せる(名前は kebab-case: `add ui bar-chart`。
199
+ ブロックは `add block sales-overview`。未知名で実行すると候補が一覧表示される)。
200
+
201
+ ### 長い表はページングする(縦スクロール箱を作らない)
202
+
203
+ - 行が多い表は `DataTablePreset`(または `DataTable`)に `enablePagination` を付けて**ページング**する
204
+ (既定は 10 行 / ページ。ページングは既定で無効なので、付け忘れると全行が並ぶ)。素の `<table>` を
205
+ 手書きして全行を流し込まない。
206
+ - カードの中に `max-h-*` + `overflow-y-auto` の**縦スクロール領域を作らない**。ホイールやキー
207
+ (End / Space)がその箱に吸われ、ページ全体がスクロールしなくなる。高さは内容に任せる。
208
+ - 横に広い表の `overflow-x-auto` は構わない。`SheetGrid`(`.csv` / `.xlsx` のビューア)は
209
+ グリッドとして意図した入れ子スクロールなので対象外。
210
+
211
+ ### グラフはまず型付きチャートで描く
212
+
213
+ - 推移は `LineChart` / `AreaChart`、比較は `BarChart`、構成比は `DonutChart`。`data`(行の配列)・
214
+ `index`(x 軸の列)・`categories`(系列の列)を渡すだけで、option は書かない
215
+ (`DonutChart` は `category` / `value`)。
216
+ - 上位 N 件は `BarList`、判定帯や予算消化は `CategoryBar`、達成率は `ProgressCircle`、
217
+ 日ごとの状態は `Tracker`。
218
+ - `EChart` + `EChartsOption` は、二軸・散布図・ヒートマップ・注釈線など型付きチャートの props で
219
+ 表せないときだけ使う。
220
+ - 色は省略してテーマトークンに任せる。指定するなら `"chart-1"`〜`"chart-5"` か
221
+ `"var(--destructive)"` などの CSS 色。hex の直書きや Tailwind クラスは使わない。
222
+ - 数値の書式(`¥`・`%`・桁区切り)は `valueFormatter` に渡す。
178
223
 
179
224
  ### ダークモードは opt-in
180
225