@squadbase/vantage-template 0.1.0-dev2.8dce0c9 → 0.1.1-dev1.02cc4b9

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 (36) hide show
  1. package/README.md +19 -9
  2. package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +14 -7
  3. package/base-template/.squadbase/skills/vantage-app/SKILL.md +4 -0
  4. package/base-template/.squadbase/skills/vantage-design/SKILL.md +62 -52
  5. package/base-template/.squadbase/skills/vantage-design/references/extract.md +26 -27
  6. package/base-template/.squadbase/skills/vantage-design/references/format.md +12 -172
  7. package/base-template/.squadbase/skills/vantage-design/references/presets.md +50 -110
  8. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +50 -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 +224 -17
  15. package/base-template/agents/api.md +36 -0
  16. package/base-template/agents/csv.md +2 -1
  17. package/base-template/agents/doc.md +54 -3
  18. package/base-template/agents/page.md +91 -10
  19. package/base-template/agents/slide.md +90 -4
  20. package/base-template/agents/xlsx.md +2 -1
  21. package/base-template/bun.lock +917 -0
  22. package/base-template/bunfig.toml +5 -0
  23. package/base-template/package.json +6 -6
  24. package/base-template/squadbase.yml +1 -0
  25. package/base-template/src/_layout.tsx +16 -7
  26. package/base-template/src/components/build-recovery.tsx +37 -1
  27. package/base-template/src/components/user-card.tsx +11 -11
  28. package/base-template/src/index.tsx +8 -3
  29. package/base-template/src/styles.css +108 -12
  30. package/dist/index.js +971 -6
  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
  35. package/base-template/DESIGN.md +0 -108
  36. package/base-template/package-lock.json +0 -5841
@@ -1,110 +1,50 @@
1
- # 初期デザインのプリセット
2
-
3
- プロジェクト作成時に選ぶ 4 つの出発点。**id(`default` / `calm` / `vivid` / `mono`)はプロジェクト
4
- 作成のワークフローが参照するので変えないこと。** どれも「チャートのプリセット 1 つ + `--primary` +
5
- `DESIGN.md` の差分」でできている。**`--primary` はどのプリセットでも必ず書く**(無彩色のプリセットも
6
- 値を明示する)。チャートだけ変えるとホームのような色の少ない画面では見た目が変わらず、別のプリセットから
7
- 切り替えたときに前の `--primary` が残るため。
8
-
9
- 適用の手順は共通:
10
-
11
- 1. `npx @squadbase/vantage-template chart <チャートのプリセット>` で `styles.css` のマーカーブロックを
12
- 差し替える。**マーカーブロックの中(`--chart-1..5`)は手で編集しない** ― コマンドの生成物で、
13
- 次の適用で消える。
14
- 2. 各プリセットの `--primary` / `--primary-foreground` を、`styles.css` のマーカーの**外**(先頭の
15
- `:root` ブロック)に `:root` と `.dark, [data-theme="dark"]` の両方で書く。既にある `--primary` は置き換える。
16
- 3. `DESIGN.md` を `format.md` の雛形から作り、下の「DESIGN.md の差分」の行だけを書き換える。
17
- 4. `scripts/contrast.mjs` で `--primary-foreground` / `--primary` をライトとダークで確かめる。
18
-
19
- | id | トーン | チャートのプリセット | `--primary` |
20
- | --- | --- | --- | --- |
21
- | `default` | 無彩色のベース。色を持つのはデータ(`blue` チャート)だけ | `blue` | 無彩色(`theme.css` の既定値を明示) |
22
- | `calm` | 落ち着いた寒色。報告書・経営向け | `ocean` | 深い青(スカイ 700 / 300) |
23
- | `vivid` | 明るい暖色。営業・マーケティング向け | `sunset` | 濃いオレンジ(オレンジ 700 / 400) |
24
- | `mono` | モノクロ。印刷・資料への貼り付け向け | `mono` | ジンク 900 / 200 |
25
-
26
- ## `default`
27
-
28
- - トーン: 落ち着いた無彩色のベースに、データだけが色を持つ業務ダッシュボード。装飾より読みやすさ。
29
- - チャート: `blue`(青 → ティール → フクシア → バイオレット → グレー)。
30
- - `--primary`(フレームワークの既定と同じ無彩色。他のプリセットから戻すときのために明示する):
31
-
32
- ```css
33
- :root {
34
- --primary: oklch(0.205 0 0);
35
- --primary-foreground: oklch(0.985 0 0);
36
- }
37
- .dark,
38
- [data-theme="dark"] {
39
- --primary: oklch(0.922 0 0);
40
- --primary-foreground: oklch(0.205 0 0);
41
- }
42
- ```
43
-
44
- - `DESIGN.md`: 雛形そのまま(`format.md` の雛形は `default` を説明している)。
45
-
46
- ## `calm`
47
-
48
- - トーン: 寒色でまとめた静かな画面。強調は控えめにし、数字と罫線で読ませる。
49
- - チャート: `ocean`(スカイ → シアン → インディゴ → ティール → スレート)。
50
- - `--primary`(チャートのスカイに揃えた深い青):
51
-
52
- ```css
53
- :root {
54
- --primary: var(--color-sky-700);
55
- --primary-foreground: oklch(0.985 0 0);
56
- }
57
- .dark,
58
- [data-theme="dark"] {
59
- --primary: var(--color-sky-300);
60
- --primary-foreground: oklch(0.205 0 0);
61
- }
62
- ```
63
-
64
- - `DESIGN.md` の差分: 「全体のトーン」を上の文に。チャートの色味を「スカイ → シアン → インディゴ →
65
- ティール → スレート」に。良い = `--chart-4`(ティール)。1 系列の強調は `--chart-1` + 残り `--chart-5`。
66
- 禁止: `--chart-1` と `--chart-2` を隣り合う細い線で並べる(どちらも明るい青系)。
67
-
68
- ## `vivid`
69
-
70
- - トーン: 明るく勢いのある暖色。KPI とトレンドを大きく見せ、強調色をはっきり使う。
71
- - チャート: `sunset`(オレンジ → アンバー → ローズ → イエロー → ストーン)。
72
- - `--primary`(オレンジ 600 は白文字で 4.5:1 に届かないので 700 を使う):
73
-
74
- ```css
75
- :root {
76
- --primary: var(--color-orange-700);
77
- --primary-foreground: oklch(0.985 0 0);
78
- }
79
- .dark,
80
- [data-theme="dark"] {
81
- --primary: var(--color-orange-400);
82
- --primary-foreground: oklch(0.205 0 0);
83
- }
84
- ```
85
-
86
- - `DESIGN.md` の差分: トーンを上の文に。主色の行を「濃いオレンジ」に。良い = 専用色なし(▲ と
87
- `--foreground`)、悪い = `--destructive`。禁止: `--chart-3`(ローズ)と `--destructive` を同じ
88
- チャートに置く(悪い値と区別できない)。1 系列の強調は `--chart-1` + 残り `--chart-5`。
89
-
90
- ## `mono`
91
-
92
- - トーン: 白黒と灰色だけ。色で意味を運ばず、太さ・順番・ラベルで読ませる。
93
- - チャート: `mono`(ジンク 900 → 200。ダークでは反転)。
94
- - `--primary`(チャートの両端と同じジンク):
95
-
96
- ```css
97
- :root {
98
- --primary: var(--color-zinc-900);
99
- --primary-foreground: oklch(0.985 0 0);
100
- }
101
- .dark,
102
- [data-theme="dark"] {
103
- --primary: var(--color-zinc-200);
104
- --primary-foreground: oklch(0.205 0 0);
105
- }
106
- ```
107
-
108
- - `DESIGN.md` の差分: トーンを上の文に。強調 = `--chart-1`(最も濃い灰)。良い = 専用色なし(▲ と
109
- 符号)、悪い = `--destructive`(**画面で唯一の有彩色**として使う)。チャートは系列を 3 つまでにし、
110
- それ以上は直接ラベルを付ける。禁止: 隣り合う `--chart-4` と `--chart-5`(明度差が小さい)。
1
+ # 初期デザイン(テーマ)
2
+
3
+ プロジェクトの見た目の出発点は、SquadKit の 7 テーマから選ぶ。**テーマ id はプロジェクト作成の
4
+ ワークフローが参照するので変えないこと。** テーマは主色・背景・文字・境界・チャート 5 色・角丸・書体を
5
+ まとめて持つ(ライトとダークの両方)。
6
+
7
+ | id | トーン | 向く場面 |
8
+ | --- | --- | --- |
9
+ | `neutral` | 既定。Vantage の素の見た目(青の主色と無彩色の面) | 迷ったとき・おまかせ |
10
+ | `ocean` | 青を基調にした落ち着いた信頼感。角丸はやや小さめ | 報告書・経営向け |
11
+ | `forest` | 緑を基調にした自然な印象。丸ゴシック系の書体、角丸はやや小さめ | 環境・ヘルスケア・自然 |
12
+ | `sunset` | 橙を基調にした温かみ。角丸はやや大きめ | 営業・マーケティング |
13
+ | `violet` | 紫を基調にしたやわらかい印象。角丸は大きめ | 華やかさ・ポップ |
14
+ | `rose` | 赤紫からピンクの落ち着いた色。角丸は大きめ(`--destructive` の赤とは別の色) | 華やかさ・ポップ |
15
+ | `mono` | 無彩色寄り。チャートも彩度を抑え、角丸は最小。等幅フォント | 印刷・資料への貼り付け |
16
+
17
+ ## 当てる
18
+
19
+ プロジェクトルートで次を実行する。`--dry-run` で書き込まずに結果を見られる。
20
+
21
+ ```bash
22
+ npx @squadbase/vantage-template theme <id>
23
+ npx @squadbase/vantage-template theme ocean --reset-chart # chart コマンドで選んだ配色を捨て、テーマのチャート色に揃える
24
+ ```
25
+
26
+ - `styles.css` の ``/* squadkit-theme — regenerated by `theme`; edits here are lost */`` 〜
27
+ `/* squadkit-theme:end */` の範囲(テーマ範囲)だけを差し替える。**範囲の中は手で編集しない**。
28
+ マーカーが無いファイルには自動で足される。同じ id を再実行しても変わらない。
29
+ - テーマ範囲はファイル先頭の `:root` より後ろに置かれ、同じ詳細度なら後ろが勝つ。
30
+ 先頭の `:root` に手書きした `--primary` などはテーマに負ける。
31
+ - `chart <preset>` の範囲(`--chart-*` だけ)はテーマ範囲の**後ろ**に置かれ、テーマのチャート色に勝つ。
32
+ チャートの配色もテーマに任せるなら `--reset-chart` で外す。
33
+
34
+ ## 旧プリセット id の読み替え
35
+
36
+ 古い呼び出しの `default` / `calm` / `vivid` / `mono` は、`theme` コマンドがそのまま受けて次の id として扱う。
37
+
38
+ | 旧 | 新 |
39
+ | --- | --- |
40
+ | `default` | `neutral` |
41
+ | `calm` | `ocean` |
42
+ | `vivid` | `sunset` |
43
+ | `mono` | `mono` |
44
+
45
+ ## テーマに無い値(ブランド色など)
46
+
47
+ テーマの `--primary` を変えたい、特定の色だけブランドに合わせたい、というときは、テーマを当てたうえで
48
+ **`/* squadkit-theme:end */`(チャート範囲があればその終端)より後ろ**に、変えたいトークンだけ
49
+ `:root` と `.dark, [data-theme="dark"]` の**両方**に書く。前に書くとテーマに負ける。
50
+ `scripts/contrast.mjs` で `--primary-foreground` / `--primary` をライトとダークで確かめる。
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vantage-pitfalls
3
- description: Vantage(@squadbase/vantage)アプリを書くときに静かに壊れる落とし穴のリファレンス。Base UI は Radix ではない(asChild 無し・render プロップ)、Select の value/label、クライアントから server/ を import しない、EChart の配色と高さ、動的パラメータの3綴り同期、PUBLIC_ env、HttpError など。Vantage の UI コンポーネントやルーティングが思った通りに動かないとき、ビルドやスタイルが静かに欠落するときに参照する。
3
+ description: Vantage(@squadbase/vantage)アプリを書くときに静かに壊れる落とし穴のリファレンス。Base UI は Radix ではない(asChild 無し・render プロップ)、Select の value/label、クライアントから server/ を import しない、チャートの色指定と EChart の配色・高さ、動的パラメータの3綴り同期、PUBLIC_ env、HttpError、csv/xlsx ページの URL 衝突とサイズ上限、スライドの data-waitfor など。Vantage の UI コンポーネントやルーティングが思った通りに動かないとき、ビルドやスタイルが静かに欠落するときに参照する。
4
4
  ---
5
5
 
6
6
  # Vantage アプリの落とし穴リファレンス
@@ -59,6 +59,10 @@ Base UI の `SelectValue` は選択中の **value をそのまま描き**、`Sel
59
59
 
60
60
  ## チャート:`EChart` の配色はトークン追従。上書きは `option.color`
61
61
 
62
+ ふつうの推移・比較・構成比は型付きチャート(`LineChart`・`BarChart` など。→ `agents/page.md`)で
63
+ 描く。その `colors` に渡すのはトークン名 `"chart-1"`〜`"chart-5"` か CSS 色で、Tailwind クラス
64
+ (`"bg-blue-500"` など)は効かない。以下は `EChart` を直接使うときの話。
65
+
62
66
  系列色は `--chart-1..5`、軸・凡例・ツールチップは文字色/境界色のトークンから組まれる
63
67
  (init 時にトークンの実値を解決している)。ライト / ダークの切り替えにも、`styles.css` の
64
68
  書き換えやテーマプリセットの適用にも追従する。
@@ -90,6 +94,11 @@ Base UI の `SelectValue` は選択中の **value をそのまま描き**、`Sel
90
94
  - 表示ルート `/sales/:customerId`
91
95
  - リンク `to="/sales/$customerId"` + `params={{ customerId }}` / 取り出し `useParams().customerId`
92
96
 
97
+ ## 日本語アプリは `lang` を指定する
98
+
99
+ ルート `src/_layout.tsx` に `export const lang = "ja"`(リテラル)が無いと `<html lang="en">` のままで、
100
+ スライドの見出しが文節の途中で折れる。内容が日本語なら必ず指定する。
101
+
93
102
  ## ページ:`definePage` の値はリテラルで書く
94
103
 
95
104
  title/description/navLabel は**ビルド時に静的抽出**される。変数・関数呼び出し・テンプレート補間を
@@ -153,6 +162,46 @@ import { type ColumnDef } from "@squadbase/vantage/components"
153
162
  const columns: ColumnDef<Row>[] = [ … ] // 注釈しないと accessorKey が string に広がり弾かれる
154
163
  ```
155
164
 
165
+ ## データファイルページ:同名の tsx と衝突する / ブラウザでパースされる
166
+
167
+ `src/` に置いた `.csv` / `.xlsx` はページになる。ここで静かに壊れるのは 3 つ。
168
+
169
+ - **`sales.csv` と `sales.tsx` はどちらも `/sales`** に解決するので `ROUTE_CONFLICT` エラー。
170
+ `.slide.tsx` も同じ(`june.slide.tsx` と `june.tsx`)。
171
+ - **パースはブラウザで走る。** 25MB を超えるファイルはそのページがエラー表示になる
172
+ (`vantage check` が `FILE_PAGE_TOO_LARGE` で先に警告する)。大きいデータは `server/api/` から
173
+ 配って `DataTable` で描く。
174
+ - **ビューアの無い拡張子(`.xls`・`.ods`・`.tsv` など)はルートにならない。** 404 ですらなく、
175
+ ナビにも `vantage routes` にも出ない。`vantage check` の `UNSUPPORTED_PAGE_FILE` 警告が
176
+ 唯一の告知点なので、「置いたのに出てこない」ときはまず `check` を見る。
177
+
178
+ ビューアは**表計算ソフトの見た目の読み取り専用グリッド**(列は A / B / C、左に行番号、1 行目は
179
+ 固定、シートのタブは下端)。**ソート・フィルタ・ページネーションは持たない** —— 並べ替えや
180
+ 絞り込みが要るなら `server/api/` から配って `DataTable` で描く。数式・グラフ・セル書式は
181
+ 再現されない(値として読めれば十分、という設計)。
182
+
183
+ ## スライド:`data-waitfor` を忘れるとチャートが白紙で書き出される
184
+
185
+ `.slide.tsx` の pptx 書き出しはスライドを画像として焼く。**canvas に描くもの(`EChart`)は
186
+ 描き終わりを待たせないと、描画前に撮られてそのスライドだけ白紙になる**:
187
+
188
+ ```tsx
189
+ <div data-waitfor="canvas">
190
+ <EChart option={option} style={{ width: 1400, height: 600 }} />
191
+ </div>
192
+ ```
193
+
194
+ ほかに 2 つ。
195
+
196
+ - **`@squadbase/vantage/ui` のプリミティブをスライドに置かない。** UI キットは rem と Tailwind で
197
+ 組まれているので、1920×1080 を縮小投影すると `text-sm` のボタンが実効 8px 程度になって読めない。
198
+ スライドは絶対 px のインラインスタイルで書く。
199
+ - **dev サーバーを起動したあとに最初の `.slide.tsx` を新規作成した場合、初回の pptx 書き出しで
200
+ 1 回だけリロードが入る。** 書き出し用の依存はプロジェクトがデッキを持つときだけ宣言される仕組みで、
201
+ その判定は dev の起動時に 1 度しか走らないため。`vantage dev` を再起動すれば解消する。
202
+
203
+ 書き方の作法(キャンバスの縦方向の予算、段階表示、トランジション、モーフ)は `vantage-slide` スキル。
204
+
156
205
  ## 生成物:`.vantage/` と `dist/` は触らない
157
206
 
158
207
  どちらも gitignore 済みの生成物。編集しても次のコマンドで上書きされる。`.vantage/` は任意の CLI
@@ -0,0 +1,299 @@
1
+ ---
2
+ name: vantage-slide
3
+ description: Vantage(@squadbase/vantage)で `.slide.tsx` のスライドデッキを書く / 直すときの技術リファレンス。ファイル契約、1920×1080 固定キャンバスと縦方向の予算、タイプスケールとレイアウト、デザイントークン、段階表示(Steps/Step)、ページ間トランジションとモーフ、pptx 書き出しの前提条件を扱う。`src/**/*.slide.tsx` を書く・触る前に読む。「スライドを作って」「デッキを1本」「この頁のレイアウトを直して」「1つずつ出したい」「pptx で出したい」で起動。
4
+ ---
5
+
6
+ # `.slide.tsx` でスライドを書く
7
+
8
+ このスキルは**技術リファレンス**です。Vantage アプリの一般規約(ページの置き場所・import サブパス・
9
+ 禁止ファイル)は `AGENTS.md` が正本で、ここでは繰り返しません。スライド固有のことだけを扱います。
10
+
11
+ 関連: 新しいページ / API / UI を 1 つ足す定型は `vantage-add-feature`、静かに壊れる落とし穴の
12
+ 一覧は `vantage-pitfalls`。
13
+
14
+ ## プリミティブのリファレンス
15
+
16
+ 各プリミティブには `references/` に個別のファイルがあります。**使う前にそのファイルを読むこと。**
17
+
18
+ | プリミティブ | 読むべき場面 | ファイル |
19
+ | --- | --- | --- |
20
+ | `<Steps>` / `<Step>` | 段階表示を作るとき | `references/steps.md` |
21
+ | `SlideTransition` | enter / exit のアニメーションを書くとき | `references/transitions.md` |
22
+ | `MorphElement` + `morph` | 2 枚にまたがる同一要素を動かすとき | `references/morph.md` |
23
+ | `<SlideDeck theme>` / `var(--slide-*)` / webfont | 配色・書体を決めるとき、pptx を出すとき | `references/tokens.md` |
24
+
25
+ ## ファイル契約
26
+
27
+ ```tsx
28
+ // src/june.slide.tsx → /june(`.slide` は URL に出ない)
29
+ import { definePage } from "@squadbase/vantage"
30
+ import { Slide, SlideDeck, Step, Steps, useSlideNumber } from "@squadbase/vantage/slide"
31
+
32
+ export const page = definePage({ title: "6月度レビュー", navLabel: "6月度" })
33
+
34
+ export default function JuneDeck() {
35
+ return (
36
+ <SlideDeck theme={{ bg: "#0f172a", fg: "#f8fafc", accent: "#fbbf24" }}>
37
+ <Slide>…</Slide>
38
+ <Slide transition={RISE}>…</Slide>
39
+ </SlideDeck>
40
+ )
41
+ }
42
+ ```
43
+
44
+ - **1 ファイル = 1 デッキ = 1 ルート。** `export default` は `<SlideDeck>` を返す React コンポーネント
45
+ (通常のページと同じ契約なので、`vantage check` も `vantage routes` も特別扱いをしません)。
46
+ - **ファイル形式の MUST / SHOULD(`<Slide>` は `<SlideDeck>` の直下、`<Step>` は `<Steps>` の直下、
47
+ `definePage` はリテラル など)は `agents/slide.md` と `agents/page.md` が正本**です。ここでは繰り返しません。
48
+ - **`.slide` を付けられるのはルートになるページモジュールだけ。** `_layout.slide.tsx` や
49
+ `deck.slide.ts` は `INVALID_PAGE_VARIANT` エラーになります。
50
+ - **`june.slide.tsx` と `june.tsx` は同じ URL** になるので `ROUTE_CONFLICT` です。どちらかを消すこと。
51
+ - **ルート `/` がすでに他のページ(`index.tsx` など)で使われている場合、スライドは `index.slide.tsx` ではなく、タイトルから作った slug でファイル名を付ける。** `index.slide.tsx` と `index.tsx` は同じ `/` に解決して `ROUTE_CONFLICT` になる。
52
+ - 例: "Label check" → `src/label-check.slide.tsx` → `/label-check`
53
+ - 例: "Monthly Report" → `src/monthly-report.slide.tsx` → `/monthly-report`
54
+ - **デッキファイル以外を作らない。** ヘルパーコンポーネントも定数も同じ `.slide.tsx` の中に書く。
55
+ スライド専用のコンポーネントを `components/` に切り出すと、デッキ 1 本を読むのに 2 ファイル
56
+ 追わせることになります。
57
+
58
+ ## まず block を探す
59
+
60
+ 1 枚分の中身は、手で組む前に `@squadbase/vantage/slide` の block(`SlideCover` `SlideExecSummary`
61
+ `SlideKpiHero` `SlideKpiGrid` `SlideChartInsight` `SlideScorecardTable` `SlideNextActions` `SlideSection` `SlideComparison` `SlideChartFull`)に
62
+ 合うものがないか探します。`<Slide>` の**中**に置き、props は `vantage docs <name>`(例:
63
+ `vantage docs slide-cover`)で引く。一覧は `agents/slide.md` が正本です。
64
+ - props は block 間で共通: `value: number | null` + `format`(上書きだけ `display`)、`delta: { value, format? }` + `betterWhen` + `comparisonLabel`、`target: number`、`source` は文字列、`asOf` は日付だけ(「時点」は block が付ける)。
65
+
66
+ - block は `@squadbase/vantage/slide` から import する。**新しい import 元は増えない**(下の
67
+ 「import してよいもの」と矛盾しない)。`vantage add` でコピーするものでもない。
68
+ - block が合わない**1 枚だけ**を、この下の規約で手書きする。デッキ全体を手書きに切り替えない。
69
+ - デッキ以外のファイルは作らない。block を使っても、デッキは 1 ファイルのままで、独自の
70
+ ヘルパーコンポーネントは同じ `.slide.tsx` に書く。
71
+
72
+ ## import してよいもの
73
+
74
+ - `@squadbase/vantage/slide` — `SlideDeck` `Slide` `Steps` `Step` `MorphElement` `useSlideNumber`
75
+ `useIsActiveSlide` `SlideCanvas`、1 枚分の block(`SlideCover` ほか上の 10 個)、手書きの 1 枚に使う部品(`SlideHeading` `SlideSourceLine` `SlideColumns` `SlideColumn` `DeltaText`)と、その型
76
+ - `@squadbase/vantage` — `definePage`
77
+ - `@squadbase/vantage/components` の `EChart` — **チャートを載せるときはこれ**(→「チャート」)
78
+ - `lucide-react` のアイコン
79
+
80
+ **`@squadbase/vantage/ui` のプリミティブはスライドに使わない。** UI キットは rem と Tailwind で
81
+ ダッシュボード向けに組まれているので、1920×1080 を縮小投影すると `text-sm` のボタンは実効 8px 程度に
82
+ なって読めません。スライドは**絶対 px のインラインスタイル**で書きます。
83
+
84
+ ## キャンバス
85
+
86
+ 各スライドは **1920 × 1080** の固定キャンバスに描かれ、フレームワークが画面に合わせて縮小します。
87
+ ビューポートが文字どおり 1920×1080 だと思って設計してください。
88
+
89
+ - `font-size`・padding・位置は**絶対 px**。`rem`・`vw`/`vh`・文字サイズの `%` は使わない
90
+ - 各スライドのルート要素はキャンバスを埋める(`width: "100%"; height: "100%"`)
91
+ - インライン `style={{ … }}` を使う。読み込んだ CSS はグローバルです
92
+
93
+ 以下の値がスライドの規約です。ヒーローと本文は `var(--slide-size-hero)` /
94
+ `var(--slide-size-body)` としても読めます(→ `references/tokens.md`)。
95
+
96
+ ### タイプスケール
97
+
98
+ | 要素 | サイズ |
99
+ | --- | --- |
100
+ | ヒーロータイトル | 168px |
101
+ | セクション見出し | 88px |
102
+ | ページ見出し | 64px |
103
+ | 本文 | 36px |
104
+ | キャプション / ラベル | 24px |
105
+
106
+ ### 余白とグリッド
107
+
108
+ - セーフエリア: キャンバス端から **左右 160px・上下 120px**。中身はこの内側の **1600 × 840px** に置き、文字を端に触れさせない
109
+ - グリッド: 内容幅 1600px を **12 列**(列幅 104px・溝 32px)に割る
110
+ - line-height: 見出し 1.2、本文 1.6
111
+ - 要素間: 32 / 48 / 64px のどれか
112
+
113
+ ### 縦方向の予算 — 中身は 1080px に収まらなければならない
114
+
115
+ キャンバスは**スクロールしません**。1080px の下端を越えたものは黙って切れます。JSX を書く前に
116
+ 計算して収まることを確かめること。**これがスライドが壊れる原因の第 1 位**で、確かめない限り
117
+ 溢れていると思ってください。
118
+
119
+ **使える高さ** = `1080 − 上下のセーフエリア(120px × 2)` = **840px**。この予算の中で設計します。
120
+
121
+ **要素の高さ** = `文字サイズ × line-height × 行数`。2 行に折り返す箇条書きは 2 行と数えます。
122
+ 次の要素を足す前に、その下の gap(32 / 48 / 64px)を加算してください。
123
+
124
+ **計算例 — 本文 1 枚(予算 840px):**
125
+
126
+ | 要素 | 高さ |
127
+ | --- | --- |
128
+ | ページ見出し: 64px × 1.2 × 1 行 | 76.8px |
129
+ | gap | 64px |
130
+ | 本文: 36px × 1.6 × 3 行 | 172.8px |
131
+ | gap | 48px |
132
+ | 箇条書き 5 行: 36px × 1.6 × 1 行 | 288px(5 × 57.6px) |
133
+ | 箇条書き間の gap 4 つ: 32px | 128px |
134
+ | **合計** | **777.6px ✅ 840 に収まる** |
135
+
136
+ 箇条書きを 6 つにすると 867.2px で溢れます。**書く前に毎ページこの検算をすること。**
137
+
138
+ **ページ単位の規則:**
139
+
140
+ - 「見出し + 本文」か「見出し + 5 つ以内の短い箇条書き」のどちらか。両方は載せない
141
+ - 箇条書きは選んだ文字サイズで 1 行に収まること。折り返すなら短くするか別ページに割る
142
+ - ヒーロータイトル(168px)はタイトル + サブタイトル 1 行 + アイブロウまで。それ以外は載せない
143
+ - セクション見出し(88px)のページにはほとんど何も足さない
144
+ - 収めるためにセーフエリアを削る・タイプスケールより小さい文字にする・本文の line-height を 1.6 未満に
145
+ 詰める、をやりたくなったら**ページを 2 枚に割る**。予算が厳しいときは分割が常に正解
146
+
147
+ **`overflow: auto/scroll`・負のマージン・transform で溢れを隠さないこと。** キャンバスは固定で、
148
+ 切れた中身は消えています。
149
+
150
+ ## ビジュアルの方向
151
+
152
+ 一貫した見た目を選び、全ページで保つこと。
153
+
154
+ - **配色** — 背景 1・本文 1・アクセント 1・ミュート 1。前の 3 つは `<SlideDeck theme>` に入れ、
155
+ それ以外はプレーンな const にする(→ `references/tokens.md`)
156
+ - **書体** — ディスプレイ 1 + 本文 1。指定がなければシステムスタック。見出しは太く(800–900)、
157
+ 本文は通常(400–500)
158
+ - **レイアウトグリッド** — セーフエリアと 12 列グリッド(→「余白とグリッド」)を全ページで貫く。左揃えは編集的、中央揃えは儀礼的
159
+ - **美学のコミットメント** — 1 つ選ぶ: ミニマル / マキシマル / エディトリアル / レトロ /
160
+ ブルータリスト / ソフト・パステル / ネオン / 紙・印刷。混ぜない
161
+
162
+ ## 繰り返す要素は `map` ではなくコンポーネント
163
+
164
+ カード・ロゴの列・タイル・行など**視覚的に繰り返す**要素は、小さなコンポーネントを定義して
165
+ **1 件につき 1 回インスタンス化**します。データ配列を `map` して 1 つのテンプレートから描かない。
166
+
167
+ ```tsx
168
+ // ✅ 各カードが独自の JSX ノードを持つ
169
+ const Card = ({ label, value }: { label: string; value: string }) => (
170
+ <div style={{ flex: 1 }}>
171
+ <div style={{ fontSize: 24, opacity: 0.6 }}>{label}</div>
172
+ <div style={{ fontSize: 88, fontWeight: 800 }}>{value}</div>
173
+ </div>
174
+ )
175
+
176
+ <div style={{ display: "flex", gap: 64 }}>
177
+ <Card label="売上" value="¥5.2億" />
178
+ <Card label="解約率" value="1.4%" />
179
+ </div>
180
+ ```
181
+
182
+ `map` の本体は**ソース上 1 箇所**なので、あとから 1 枚だけ文言や色を変えたいときに全部が一緒に
183
+ 変わります。コンポーネント定義がレイアウトの単一の正本のまま、インスタンスごとの値だけが
184
+ 呼び出し側に出る形が正しい。
185
+
186
+ これは**視覚要素が繰り返すとき**の話で、データが繰り返すときの話ではありません。純粋な
187
+ テキストの箇条書き(`<ul><li>`)は各 `<li>` が既に独立した JSX ノードなので、そのままで正しい。
188
+ `<SlideDeck>` の直下に `<Slide>` を配列で渡すこと自体はファイル形式として可能です(`agents/slide.md`)が、
189
+ 1 枚ごとの文言・図を変えるスライドは 1 枚ずつ書きます。
190
+
191
+ ## ページ番号
192
+
193
+ フッターに現在位置を出すなら `useSlideNumber()` から読む。**ハードコードしない。**
194
+
195
+ ```tsx
196
+ const { current, total } = useSlideNumber() // current は 1 始まり
197
+ ```
198
+
199
+ スライドを差し込む・並べ替える・消すたびに全フッターを直す羽目になるのを避けられます。
200
+
201
+ ## チャート
202
+
203
+ チャートは `@squadbase/vantage/components` の `EChart` を使います。**canvas に描かれるので、
204
+ pptx 書き出しのために描き終わりを待たせる必要があります**:
205
+
206
+ ```tsx
207
+ <div data-waitfor="canvas">
208
+ <EChart option={option} style={{ width: 1400, height: 600 }} />
209
+ </div>
210
+ ```
211
+
212
+ `data-waitfor` を付けないと、書き出しがチャートの描画前にスクリーンショットを撮り、
213
+ **そのスライドだけチャートが白紙**で出ます。
214
+
215
+ チャートに色をハードコードしないこと(`option.color` / `itemStyle.color` / `theme`)。
216
+ `EChart` はテーマトークンに追従します。
217
+
218
+ ## 説明と図表を同じデータ時点へ揃える
219
+
220
+ 分析を共有するデッキでは、本文・数値・図表が**同じ対象期間と同じ取得時点の結果**を参照していることを
221
+ 確かめます。
222
+
223
+ - 対象期間とデータ取得時点を、該当ページかフッターに短く表示する(例: 「2026年6月 / 7月1日 9:00 取得」)
224
+ - **固定した説明文に対して図表だけが最新値へ変わる構成を避ける。** ライブ更新を意図するなら、本文の
225
+ 数値・結論も同じ取得結果から導いて、図表と一緒に変わる構成にする
226
+ - 同一時点の結果を保持・参照できない場合は、スナップショットとして保存できたとは報告しない。
227
+ 必要な保持の仕組みと、現在保証できない範囲(例: 再表示すると図表だけ最新値になる)を伝える
228
+
229
+ ## 実行時に無料で付いてくるもの
230
+
231
+ - `src/june.slide.tsx` → `/june`。ナビ(`useRoutes()`)に通常のページと同格で並ぶ
232
+ - `→ ↓ Space PageDown` で次、`← ↑ PageUp` で前、`Home` / `End` で端へ
233
+ - 画面の左右 30% のクリックでも送れる(中央は不活性。リンク・ボタンの上は素通し)
234
+ - `F` で全画面、`Escape` で全画面を抜ける / デッキから戻る
235
+ - `L` でスライド一覧(左のサムネイル列)を開閉。クリックでその枚へ飛ぶ
236
+ - 現在位置は URL の `?p=` に入る(履歴は汚れない)。`?p=4` のリンクをそのまま共有できる
237
+ - ツールバーから **pptx 書き出し**
238
+ - 画面サイズを変えても縮尺が変わるだけでレイアウトは崩れない
239
+
240
+ 見え方は 3 通りで、切り替えはすべて実行時(コードでは決めない):
241
+
242
+ | 見え方 | 出し方 |
243
+ |---|---|
244
+ | 全画面 | `F` / ツールバー。スライドだけになり、一覧は退く |
245
+ | 一覧 + スライド(既定) | 左にサムネイル、右にスライド |
246
+ | スライドのみ | `L` / ツールバー で一覧を閉じる。表示領域いっぱいに 16:9 を描く |
247
+
248
+ 一覧の開閉は `?rail=` にも入るので、閉じた状態のリンクも共有できます。
249
+ 閉じた状態から始めたいデッキだけ `<SlideDeck rail={false}>` と書きます
250
+ (`embedded` のデッキは幅が無いので既定で閉じています)。サムネイルの配色はデッキ自身の
251
+ `--slide-bg` / `--slide-fg` の混色だけで作られていて、色味は持ちません。
252
+
253
+ ## pptx 書き出しの前提
254
+
255
+ デッキは**画像として**書き出されます。1 スライド = 1 枚の PNG(3840×2160)。文字は入らないので
256
+ 受け取り側にフォントが要らず、日本語も欠けません。代わりに**書き出した pptx の文字は検索・編集
257
+ できません**(ブラウザで見ている間は DOM なので選択できます)。
258
+
259
+ 書き出しが壊れる典型は 2 つ:
260
+
261
+ 1. **`data-waitfor` を付けないチャート**(→「チャート」)
262
+ 2. **サブセットしていない日本語 webfont**(→ `references/tokens.md`)
263
+
264
+ 段階表示は**全部開いた状態**で書き出されます(`<Steps>` はホストが無いと全開)。
265
+
266
+ ## 仕上げ前のセルフレビュー
267
+
268
+ - [ ] `export default` が `<SlideDeck>` を返している
269
+ - [ ] `<Slide>` が `<SlideDeck>` の直下にある
270
+ - [ ] 各スライドのルート要素が `100% × 100%` を埋めている
271
+ - [ ] 文字がセーフエリア(左右 160px・上下 120px)の内側にある
272
+ - [ ] **全ページで(文字サイズ × line-height × 行数)+ gap ≤ 840px。** 際どいなら分割
273
+ - [ ] 箇条書きが 2 行目に折り返していない
274
+ - [ ] 配色とタイプスケールが全ページで一貫している
275
+ - [ ] 1 ページ 1 メッセージ
276
+ - [ ] 繰り返す視覚要素が `array.map` ではなく明示的な `<Component />` になっている
277
+ - [ ] ページ番号が `useSlideNumber()` から来ている(ハードコードしていない)
278
+ - [ ] チャートが `data-waitfor="canvas"` で包まれている
279
+ - [ ] 本文・数値・図表が同じ対象期間と取得時点の結果を参照し、その期間と時点が表示されている
280
+ - [ ] `<Steps>` を使ったページが、`?p=` で直接開いても完成した絵として読める
281
+ - [ ] トランジションを宣言したなら、全ページが 1 つの family に収まっている(→ `references/transitions.md`)
282
+ - [ ] `definePage` の値がリテラルである
283
+ - [ ] `.slide.tsx` 以外のファイルを作っていない
284
+
285
+ ## アンチパターン
286
+
287
+ - ❌ 文字の壁。1 ページ 40 語を超えたら分割する
288
+ - ❌ キャンバス全面を本文で使う。セーフエリア(左右 160px・上下 120px)を守る
289
+ - ❌ 縦に 1080px を溢れさせる。切れた中身は見えない — 分割する
290
+ - ❌ `overflow: auto` / `scroll` / `hidden` で溢れを「隠す」。バグを隠しただけ
291
+ - ❌ 詰め込むためにタイプスケールより小さい文字にする・セーフエリアを削る
292
+ - ❌ 本文を 36px 未満にする。プロジェクタで読めない
293
+ - ❌ `@squadbase/vantage/ui` のプリミティブをスライドに置く(縮小されて読めなくなる)
294
+ - ❌ 依存を追加する。使えるのは `@squadbase/vantage/*` と `lucide-react` と標準 Web API だけ
295
+ - ❌ スライド用のコンポーネントを別ファイルに切り出す
296
+ - ❌ チャートに `data-waitfor="canvas"` を付け忘れる(pptx が白紙になる)
297
+ - ❌ プリミティブを `references/*.md` を読まずに使う
298
+ - ❌ 説明文を固定したまま図表だけライブデータで描く(再表示すると本文と図表の時点がずれる)
299
+ - ❌ 同一時点の結果を保持できていないのに「スナップショットとして保存した」と報告する
@@ -0,0 +1,102 @@
1
+ # モーフトランジション(`MorphElement`)
2
+
3
+ > モーフは `SlideTransition` の契約の上に載っています。まだなら `transitions.md` を先に読んでください。
4
+
5
+ **同じ視覚的対象**が隣り合う 2 枚に存在するとき、切り替えの間それを連続して動かせます ——
6
+ 位置・大きさ・角丸・色が 1 つの動きとして補間されます(Keynote の「マジックムーブ」)。
7
+ フェードアウトして入り直すのではなく。opt-in は 2 段階:
8
+
9
+ 1. **両方の**スライドで対象を `MorphElement` に包み、**同じ `id`** を付ける
10
+ 2. **入ってくる側**のスライドの `transition` で `morph` を有効にする
11
+
12
+ ```tsx
13
+ import { MorphElement, Slide, SlideDeck, type SlideTransition } from "@squadbase/vantage/slide"
14
+
15
+ // enter / exit を opacity だけにして、動きを全部クローンに預ける(→ 規則 1)
16
+ const MORPH: SlideTransition = {
17
+ duration: 280,
18
+ exit: { duration: 224, easing: "cubic-bezier(0.4, 0, 1, 1)", keyframes: [{ opacity: 1 }, { opacity: 0 }] },
19
+ enter: { duration: 308, delay: 112, easing: "cubic-bezier(0, 0, 0.2, 1)", keyframes: [{ opacity: 0 }, { opacity: 1 }] },
20
+ morph: { duration: 868, easing: "cubic-bezier(0.4, 0, 0.2, 1)" },
21
+ }
22
+
23
+ const STAGE = { width: "100%", height: "100%", position: "relative", background: "#000" } as const
24
+
25
+ <SlideDeck>
26
+ <Slide transition={MORPH}>
27
+ <div style={STAGE}>
28
+ <MorphElement id="pill">
29
+ <div style={{ position: "absolute", left: 842, top: 479, width: 236, height: 122, borderRadius: 999, background: "#fff" }} />
30
+ </MorphElement>
31
+ </div>
32
+ </Slide>
33
+ <Slide transition={MORPH}>
34
+ <div style={STAGE}>
35
+ <MorphElement id="pill">
36
+ <div style={{ position: "absolute", left: 721, top: 479, width: 478, height: 122, borderRadius: 999, background: "#0a0a0a" }} />
37
+ </MorphElement>
38
+ </div>
39
+ </Slide>
40
+ </SlideDeck>
41
+ ```
42
+
43
+ ## 契約
44
+
45
+ - `morph: true` は上位の `duration` / `easing` を継承します。`{ duration?, easing?, delay? }` を渡すと
46
+ ページのクロスフェードと独立に時間を決められます。**モーフはフェードの 2〜4 倍長い方がよく読めます**
47
+ (例: フェード 280ms、モーフ 868ms)。モーフするページのフェードは `transitions.md` の 140–280ms の帯を
48
+ 超えてよい —— 長いモーフに合わせて時間を取っているからで、帯は非モーフのページを縛り続けます。
49
+ - 要素は `id` で対応付けられます。対になったものは FLIP でモーフします: 両方の矩形を測り、出ていく側の
50
+ 要素をクローンして両ページの上のオーバーレイに置き、元を隠して、transform・角丸・色をアニメートする
51
+ (枠線の太さは別のフレームに乗るので、箱と一緒に伸びません)。**`id` は 1 ページ内で一意にすること。**
52
+ `MorphElement` を入れ子にしないこと。
53
+ - **片側にしかない `id` はその場でフェードイン / アウトします。** 「列に 3 つ目の箱が加わる」が
54
+ こう読めます —— 引き継がれた箱は新しい位置へ滑り、新しい箱はその場で像を結ぶ。
55
+ - **モーフノードとその子孫の色も補間されます。** 途中で黒 → 白に反転するラベルもそのまま動きます。
56
+ - トランジションは**入ってくる側**に付きます。戻るときも動かしたいなら、上の例のように**両方の
57
+ スライドに同じオブジェクトを渡す**こと。
58
+
59
+ ## 規則 —— どれも実際のデッキで学んだもの
60
+
61
+ 1. **モーフするページの enter / exit は opacity だけにする。** どの family でも正しく合成されます
62
+ (共有矩形はフェーズが始まる前に測られる)が、クローンが滑っている間に transform を持つ enter が
63
+ ページ全体も動かすと、2 つの動きが競合します。**動きを全部クローンに預けて残りをフェードさせる**のが、
64
+ モーフを 1 つの確信ある動作に見せる条件です。
65
+ 2. **モーフの座標はマウント時点で確定していること。** 矩形は切り替えの瞬間に 1 度だけ記録されます。
66
+ モーフ対象は**ピクセル定数で配置**し、マウント後に effect で測った値で置かないこと(再描画が
67
+ 目標をずらし、動きが飛びます)。中の文字が時間とともに伸びる(タイプライタ等)なら、
68
+ 測る箱が変わらないように**全幅の不可視スペーサ**を置きます。
69
+ 3. **モーフノード自身に `transform` を置かない。** パーセント指定の translate
70
+ (`translate(-50%, -50%)`)はモーフのスケールで誤って拡大され、クローンが数十 px ずれて着地します。
71
+ 中央寄せ等の transform は `MorphElement` を**包む**プレーンな `<div>` に置くこと。
72
+ 4. **`MorphElement` は `className` / `style` を単一のホスト子要素にマージします**(子が単独の DOM 要素の
73
+ ときはラッパーを足しません)。したがって切り抜き用の箱(`overflow: hidden` + 固定幅)には
74
+ 明示的な入れ子の `<div>` が要ります —— さもないと箱が中の `<img>` に潰れて画像が歪みます。
75
+ 5. **入場アニメーションは `useIsActiveSlide()` で門番すること。** 切り替えの間、ランタイムは出ていく
76
+ ページの**新しいインスタンス**をマウントして exit の状態を記録します(pptx 書き出しも全スライドを
77
+ オフスクリーンでマウントします)。そこで入場が再生されると、記録される状態がアニメーションの途中に
78
+ なり、モーフがおかしな位置から始まります。このフックは**聴衆が見ているインスタンスでだけ true** で、
79
+ それ以外(出ていく側の記録用、書き出し用)は完成した静止状態を描くべきです。
80
+
81
+ ```tsx
82
+ import { useIsActiveSlide } from "@squadbase/vantage/slide"
83
+
84
+ const animate = useIsActiveSlide()
85
+ ```
86
+
87
+ 6. **クローンは全ての上を通ります。** モーフのオーバーレイは両ページの上にあるので、クローンが
88
+ 「着地したとき」に現れてほしい要素は、そのままだと早く出てしまいます。そういう出現には
89
+ モーフと同じだけの遅延を与えること(`animation: "slide-fade 0.63s ease-out 0.868s both"`。
90
+ `0.868s` は `morph.duration`。`slide-fade` はフレームワークが用意しないので
91
+ `@keyframes slide-fade { from { opacity: 0 } to { opacity: 1 } }` を自分で書く)。
92
+ その数値は共有の const に 1 つ置いて、ずれないようにします。
93
+ 7. **「そのまま留める」ページは別のスライドにする。** トランジションは入ってくる側に付くので、
94
+ モーフの後に同じ中身をカットで見せたいなら、`transition` を持たない `<Slide>` をもう 1 枚置きます。
95
+
96
+ ## いつ使うか
97
+
98
+ モーフは**状態の連続**のための道具です: トグルのつまみが次の選択肢へ滑る、カードが詳細ビューへ広がる、
99
+ ロゴが中央から次のページの図の中へ移る、列に箱が 1 つ増える。**その対象が切り替えをまたぐ物語そのもの**
100
+ であるとき。装飾をモーフさせないこと、「念のため」でタグを付けないこと —— モーフの id はどれも
101
+ 「これは同じ物です」という聴衆への約束です。1 章につきモーフする流れは 1〜2 回で十分で、
102
+ 周りのページは静かにカットかフェードしているべきです。