@squadbase/vantage 0.5.3 → 0.6.1-dev.2d25e94

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 (134) hide show
  1. package/NOTICE +73 -0
  2. package/README.md +6 -5
  3. package/dist/add-DY4AXANK.js +6 -0
  4. package/dist/add-DY4AXANK.js.map +1 -0
  5. package/dist/{build-756WIEVI.js → build-JB4VOMUI.js} +10 -10
  6. package/dist/{build-756WIEVI.js.map → build-JB4VOMUI.js.map} +1 -1
  7. package/dist/{check-KZ5PHCA6.js → check-ZJYK6RLG.js} +8 -8
  8. package/dist/{check-KZ5PHCA6.js.map → check-ZJYK6RLG.js.map} +1 -1
  9. package/dist/chunk-5ZOP4SQR.js +47 -0
  10. package/dist/chunk-5ZOP4SQR.js.map +1 -0
  11. package/dist/{chunk-EHPJXDJU.js → chunk-6S4BAIGV.js} +4 -2
  12. package/dist/chunk-6S4BAIGV.js.map +1 -0
  13. package/dist/chunk-BCE22JSM.js +510 -0
  14. package/dist/chunk-BCE22JSM.js.map +1 -0
  15. package/dist/{chunk-C4MQRWXS.js → chunk-C2CX6GLX.js} +17 -3
  16. package/dist/chunk-C2CX6GLX.js.map +1 -0
  17. package/dist/{chunk-RA6Z3OX3.js → chunk-EP6JHXSR.js} +61 -9
  18. package/dist/chunk-EP6JHXSR.js.map +1 -0
  19. package/dist/{chunk-BAM33YZ6.js → chunk-JGRMRXXZ.js} +45 -6
  20. package/dist/chunk-JGRMRXXZ.js.map +1 -0
  21. package/dist/{chunk-VXAV2435.js → chunk-KLEIVDUV.js} +30 -7
  22. package/dist/chunk-KLEIVDUV.js.map +1 -0
  23. package/dist/{chunk-JC6MT5UU.js → chunk-MXI3PZFT.js} +3 -3
  24. package/dist/{chunk-JC6MT5UU.js.map → chunk-MXI3PZFT.js.map} +1 -1
  25. package/dist/chunk-PLST5Q7T.js +2221 -0
  26. package/dist/chunk-PLST5Q7T.js.map +1 -0
  27. package/dist/chunk-PVTHHV7X.js +105 -0
  28. package/dist/chunk-PVTHHV7X.js.map +1 -0
  29. package/dist/{chunk-HQDEYCDP.js → chunk-QI3XQKHS.js} +40 -4
  30. package/dist/chunk-QI3XQKHS.js.map +1 -0
  31. package/dist/{chunk-L3TW4FNA.js → chunk-T3FLAGI6.js} +34 -13
  32. package/dist/chunk-T3FLAGI6.js.map +1 -0
  33. package/dist/{chunk-X44ZTKID.js → chunk-U2M6HGKQ.js} +61 -9
  34. package/dist/chunk-U2M6HGKQ.js.map +1 -0
  35. package/dist/chunk-ZO62C6T4.js +91 -0
  36. package/dist/chunk-ZO62C6T4.js.map +1 -0
  37. package/dist/{chunk-D4Y5RWJA.js → chunk-ZVANIULZ.js} +81 -21
  38. package/dist/chunk-ZVANIULZ.js.map +1 -0
  39. package/dist/cli.js +18 -14
  40. package/dist/cli.js.map +1 -1
  41. package/dist/client/index.d.ts +5 -35
  42. package/dist/client/index.js +1 -1
  43. package/dist/components/index.js +3 -2212
  44. package/dist/components/index.js.map +1 -1
  45. package/dist/{dev-EILX2VTF.js → dev-FCEC3UKQ.js} +24 -13
  46. package/dist/dev-FCEC3UKQ.js.map +1 -0
  47. package/dist/doc/index.d.ts +39 -0
  48. package/dist/doc/index.js +38 -0
  49. package/dist/doc/index.js.map +1 -0
  50. package/dist/{docs-GP2ZHIQF.js → docs-EJJ3BECT.js} +4 -4
  51. package/dist/{docs-GP2ZHIQF.js.map → docs-EJJ3BECT.js.map} +1 -1
  52. package/dist/{doctor-FXZLMGC3.js → doctor-WHZXPERA.js} +9 -9
  53. package/dist/doctor-WHZXPERA.js.map +1 -0
  54. package/dist/manifest-DiOjyinq.d.ts +43 -0
  55. package/dist/{preview-WBQPCTTT.js → preview-OGMCNQUO.js} +13 -5
  56. package/dist/preview-OGMCNQUO.js.map +1 -0
  57. package/dist/query/index.js +1 -102
  58. package/dist/query/index.js.map +1 -1
  59. package/dist/router/index.d.ts +10 -0
  60. package/dist/router/index.js +2 -89
  61. package/dist/router/index.js.map +1 -1
  62. package/dist/{routes-WCN4KSZY.js → routes-GTFO5PUF.js} +17 -8
  63. package/dist/routes-GTFO5PUF.js.map +1 -0
  64. package/dist/{search-GLKCJXSI.js → search-6Y422SV6.js} +4 -4
  65. package/dist/{search-GLKCJXSI.js.map → search-6Y422SV6.js.map} +1 -1
  66. package/dist/server/node.d.ts +1 -1
  67. package/dist/server/node.js +6 -1
  68. package/dist/server/node.js.map +1 -1
  69. package/dist/slide/index.d.ts +230 -0
  70. package/dist/slide/index.js +1571 -0
  71. package/dist/slide/index.js.map +1 -0
  72. package/dist/table/index.d.ts +107 -0
  73. package/dist/table/index.js +680 -0
  74. package/dist/table/index.js.map +1 -0
  75. package/dist/types-CXFlMl8S.d.ts +121 -0
  76. package/dist/ui/index.js +3 -41
  77. package/dist/ui/index.js.map +1 -1
  78. package/dist/{upgrade-S64IXL42.js → upgrade-AL4KZZR4.js} +9 -9
  79. package/dist/upgrade-AL4KZZR4.js.map +1 -0
  80. package/dist/vite/index.d.ts +1 -1
  81. package/dist/vite/index.js +5 -5
  82. package/docs/en/agent-skills.md +22 -12
  83. package/docs/en/changelog.md +107 -0
  84. package/docs/en/cli-reference.md +42 -4
  85. package/docs/en/pages-and-metadata.md +17 -0
  86. package/docs/en/routing.md +102 -3
  87. package/docs/en/ui-and-theming.md +102 -0
  88. package/docs/index.json +2 -2
  89. package/docs/ja/agent-skills.md +21 -11
  90. package/docs/ja/changelog.md +105 -0
  91. package/docs/ja/cli-reference.md +41 -4
  92. package/docs/ja/pages-and-metadata.md +17 -0
  93. package/docs/ja/routing.md +98 -3
  94. package/docs/ja/ui-and-theming.md +100 -0
  95. package/package.json +33 -9
  96. package/skills/vantage-app/SKILL.md +4 -0
  97. package/skills/vantage-design/SKILL.md +137 -0
  98. package/skills/vantage-design/references/extract.md +110 -0
  99. package/skills/vantage-design/references/format.md +172 -0
  100. package/skills/vantage-design/references/presets.md +110 -0
  101. package/skills/vantage-design/scripts/contrast.mjs +166 -0
  102. package/skills/vantage-pitfalls/SKILL.md +41 -1
  103. package/skills/vantage-slide/SKILL.md +264 -0
  104. package/skills/vantage-slide/references/morph.md +102 -0
  105. package/skills/vantage-slide/references/steps.md +57 -0
  106. package/skills/vantage-slide/references/tokens.md +87 -0
  107. package/skills/vantage-slide/references/transitions.md +196 -0
  108. package/templates/AGENTS.md +43 -228
  109. package/templates/agents/api.md +49 -0
  110. package/templates/agents/csv.md +32 -0
  111. package/templates/agents/doc.md +73 -0
  112. package/templates/agents/page.md +187 -0
  113. package/templates/agents/slide.md +41 -0
  114. package/templates/agents/xlsx.md +33 -0
  115. package/theme.css +169 -0
  116. package/dist/add-PPCBMWEN.js +0 -6
  117. package/dist/add-PPCBMWEN.js.map +0 -1
  118. package/dist/chunk-BAM33YZ6.js.map +0 -1
  119. package/dist/chunk-C4MQRWXS.js.map +0 -1
  120. package/dist/chunk-D4Y5RWJA.js.map +0 -1
  121. package/dist/chunk-EHPJXDJU.js.map +0 -1
  122. package/dist/chunk-HQDEYCDP.js.map +0 -1
  123. package/dist/chunk-L3TW4FNA.js.map +0 -1
  124. package/dist/chunk-PSEYFYAP.js +0 -276
  125. package/dist/chunk-PSEYFYAP.js.map +0 -1
  126. package/dist/chunk-RA6Z3OX3.js.map +0 -1
  127. package/dist/chunk-VXAV2435.js.map +0 -1
  128. package/dist/chunk-X44ZTKID.js.map +0 -1
  129. package/dist/dev-EILX2VTF.js.map +0 -1
  130. package/dist/doctor-FXZLMGC3.js.map +0 -1
  131. package/dist/preview-WBQPCTTT.js.map +0 -1
  132. package/dist/routes-WCN4KSZY.js.map +0 -1
  133. package/dist/types-De3mo_hv.d.ts +0 -59
  134. package/dist/upgrade-S64IXL42.js.map +0 -1
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ // WCAG 2.x contrast ratio between two CSS colours. No dependencies.
3
+ //
4
+ // node contrast.mjs <foreground> <background>
5
+ // node contrast.mjs '#ffffff' 'oklch(0.205 0 0)'
6
+ // node contrast.mjs 'oklch(0.985 0 0)' 'var(--color-sky-700)'
7
+ //
8
+ // Accepts: #rgb / #rgba / #rrggbb / #rrggbbaa, rgb()/rgba(), oklch(), and
9
+ // Tailwind palette references `var(--color-<name>-<step>)` (resolved from
10
+ // node_modules/tailwindcss/theme.css, searched upward from the cwd).
11
+ // A translucent foreground is composited over the background first; the
12
+ // background is treated as opaque.
13
+
14
+ import { existsSync, readFileSync } from "node:fs"
15
+ import { dirname, join } from "node:path"
16
+
17
+ function usage(message) {
18
+ if (message) console.error(`error: ${message}`)
19
+ console.error("usage: node contrast.mjs <foreground> <background>")
20
+ console.error(" colours: #hex, rgb(), oklch(), var(--color-<name>-<step>)")
21
+ process.exit(2)
22
+ }
23
+
24
+ // --- parsing ---------------------------------------------------------------
25
+
26
+ function parseNumber(token, percentScale = 1) {
27
+ const t = token.trim()
28
+ if (t === "none") return 0
29
+ if (t.endsWith("%")) return (Number.parseFloat(t) / 100) * percentScale
30
+ const n = Number.parseFloat(t)
31
+ if (Number.isNaN(n)) throw new Error(`not a number: ${token}`)
32
+ return n
33
+ }
34
+
35
+ function splitArgs(body) {
36
+ const [main, alpha] = body.split("/")
37
+ const parts = main.trim().split(/[\s,]+/).filter(Boolean)
38
+ return { parts, alpha: alpha === undefined ? 1 : parseNumber(alpha, 1) }
39
+ }
40
+
41
+ function srgbToLinear(c) {
42
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
43
+ }
44
+
45
+ function linearToSrgb(c) {
46
+ const v = Math.min(1, Math.max(0, c))
47
+ return v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055
48
+ }
49
+
50
+ // Returns gamma-encoded sRGB channels in [0, 1] plus alpha.
51
+ function parseHex(input) {
52
+ let hex = input.slice(1)
53
+ if (![3, 4, 6, 8].includes(hex.length) || /[^0-9a-f]/i.test(hex)) {
54
+ throw new Error(`bad hex colour: ${input}`)
55
+ }
56
+ if (hex.length <= 4) hex = [...hex].map((c) => c + c).join("")
57
+ const n = (i) => Number.parseInt(hex.slice(i, i + 2), 16) / 255
58
+ return { r: n(0), g: n(2), b: n(4), a: hex.length === 8 ? n(6) : 1 }
59
+ }
60
+
61
+ function parseRgb(body) {
62
+ const { parts, alpha } = splitArgs(body)
63
+ if (parts.length === 4) return parseRgb(`${parts.slice(0, 3).join(" ")} / ${parts[3]}`)
64
+ if (parts.length !== 3) throw new Error(`bad rgb(): ${body}`)
65
+ const [r, g, b] = parts.map((p) => parseNumber(p, 255) / 255)
66
+ return { r, g, b, a: alpha }
67
+ }
68
+
69
+ function parseOklch(body) {
70
+ const { parts, alpha } = splitArgs(body)
71
+ if (parts.length !== 3) throw new Error(`bad oklch(): ${body}`)
72
+ const L = parseNumber(parts[0], 1)
73
+ const C = parseNumber(parts[1], 0.4)
74
+ const H = (Number.parseFloat(parts[2]) || 0) * (Math.PI / 180)
75
+ const a = C * Math.cos(H)
76
+ const bb = C * Math.sin(H)
77
+ const l_ = (L + 0.3963377774 * a + 0.2158037573 * bb) ** 3
78
+ const m_ = (L - 0.1055613458 * a - 0.0638541728 * bb) ** 3
79
+ const s_ = (L - 0.0894841775 * a - 1.291485548 * bb) ** 3
80
+ const r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_
81
+ const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_
82
+ const b = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_
83
+ return { r: linearToSrgb(r), g: linearToSrgb(g), b: linearToSrgb(b), a: alpha }
84
+ }
85
+
86
+ let tailwindTheme
87
+ function findTailwindTheme() {
88
+ if (tailwindTheme !== undefined) return tailwindTheme
89
+ tailwindTheme = null
90
+ let dir = process.cwd()
91
+ for (;;) {
92
+ for (const rel of [
93
+ "node_modules/tailwindcss/theme.css",
94
+ "node_modules/@squadbase/vantage/node_modules/tailwindcss/theme.css",
95
+ ]) {
96
+ const file = join(dir, rel)
97
+ if (existsSync(file)) {
98
+ tailwindTheme = readFileSync(file, "utf8")
99
+ return tailwindTheme
100
+ }
101
+ }
102
+ const parent = dirname(dir)
103
+ if (parent === dir) return tailwindTheme
104
+ dir = parent
105
+ }
106
+ }
107
+
108
+ function resolveVar(input) {
109
+ const name = input.match(/^var\(\s*(--color-[a-z]+-\d+)\s*\)$/)?.[1]
110
+ if (!name) throw new Error(`only var(--color-<name>-<step>) can be resolved: ${input}`)
111
+ const theme = findTailwindTheme()
112
+ if (!theme) throw new Error(`tailwindcss/theme.css not found from ${process.cwd()}; pass the value instead`)
113
+ const value = theme.match(new RegExp(`${name}:\\s*([^;]+);`))?.[1]
114
+ if (!value) throw new Error(`${name} is not in tailwindcss/theme.css`)
115
+ return value.trim()
116
+ }
117
+
118
+ function parseColor(raw) {
119
+ const input = raw.trim().toLowerCase()
120
+ if (input.startsWith("#")) return parseHex(input)
121
+ if (input.startsWith("var(")) return parseColor(resolveVar(input))
122
+ const fn = input.match(/^([a-z]+)\((.*)\)$/)
123
+ if (fn) {
124
+ if (fn[1] === "rgb" || fn[1] === "rgba") return parseRgb(fn[2])
125
+ if (fn[1] === "oklch") return parseOklch(fn[2])
126
+ }
127
+ throw new Error(`unsupported colour: ${raw}`)
128
+ }
129
+
130
+ // --- contrast --------------------------------------------------------------
131
+
132
+ function composite(fg, bg) {
133
+ const mix = (f, b) => f * fg.a + b * (1 - fg.a)
134
+ return { r: mix(fg.r, bg.r), g: mix(fg.g, bg.g), b: mix(fg.b, bg.b), a: 1 }
135
+ }
136
+
137
+ function luminance({ r, g, b }) {
138
+ return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b)
139
+ }
140
+
141
+ function toHex({ r, g, b }) {
142
+ const h = (c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")
143
+ return `#${h(r)}${h(g)}${h(b)}`
144
+ }
145
+
146
+ const [fgArg, bgArg] = process.argv.slice(2)
147
+ if (!fgArg || !bgArg) usage()
148
+
149
+ let fg
150
+ let bg
151
+ try {
152
+ bg = { ...parseColor(bgArg), a: 1 }
153
+ fg = composite(parseColor(fgArg), bg)
154
+ } catch (error) {
155
+ usage(error.message)
156
+ }
157
+
158
+ const [hi, lo] = [luminance(fg), luminance(bg)].sort((x, y) => y - x)
159
+ const ratio = (hi + 0.05) / (lo + 0.05)
160
+ const verdict = (min) => (ratio >= min ? "PASS" : "FAIL")
161
+
162
+ console.log(`foreground ${fgArg} (${toHex(fg)})`)
163
+ console.log(`background ${bgArg} (${toHex(bg)})`)
164
+ console.log(`contrast ${ratio.toFixed(2)}:1`)
165
+ console.log(`AA normal text (>= 4.5:1): ${verdict(4.5)}`)
166
+ console.log(`AA large text / UI (>= 3:1): ${verdict(3)}`)
@@ -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 アプリの落とし穴リファレンス
@@ -153,6 +153,46 @@ import { type ColumnDef } from "@squadbase/vantage/components"
153
153
  const columns: ColumnDef<Row>[] = [ … ] // 注釈しないと accessorKey が string に広がり弾かれる
154
154
  ```
155
155
 
156
+ ## データファイルページ:同名の tsx と衝突する / ブラウザでパースされる
157
+
158
+ `src/` に置いた `.csv` / `.xlsx` はページになる。ここで静かに壊れるのは 3 つ。
159
+
160
+ - **`sales.csv` と `sales.tsx` はどちらも `/sales`** に解決するので `ROUTE_CONFLICT` エラー。
161
+ `.slide.tsx` も同じ(`june.slide.tsx` と `june.tsx`)。
162
+ - **パースはブラウザで走る。** 25MB を超えるファイルはそのページがエラー表示になる
163
+ (`vantage check` が `FILE_PAGE_TOO_LARGE` で先に警告する)。大きいデータは `server/api/` から
164
+ 配って `DataTable` で描く。
165
+ - **ビューアの無い拡張子(`.xls`・`.ods`・`.tsv` など)はルートにならない。** 404 ですらなく、
166
+ ナビにも `vantage routes` にも出ない。`vantage check` の `UNSUPPORTED_PAGE_FILE` 警告が
167
+ 唯一の告知点なので、「置いたのに出てこない」ときはまず `check` を見る。
168
+
169
+ ビューアは**表計算ソフトの見た目の読み取り専用グリッド**(列は A / B / C、左に行番号、1 行目は
170
+ 固定、シートのタブは下端)。**ソート・フィルタ・ページネーションは持たない** —— 並べ替えや
171
+ 絞り込みが要るなら `server/api/` から配って `DataTable` で描く。数式・グラフ・セル書式は
172
+ 再現されない(値として読めれば十分、という設計)。
173
+
174
+ ## スライド:`data-waitfor` を忘れるとチャートが白紙で書き出される
175
+
176
+ `.slide.tsx` の pptx 書き出しはスライドを画像として焼く。**canvas に描くもの(`EChart`)は
177
+ 描き終わりを待たせないと、描画前に撮られてそのスライドだけ白紙になる**:
178
+
179
+ ```tsx
180
+ <div data-waitfor="canvas">
181
+ <EChart option={option} style={{ width: 1400, height: 600 }} />
182
+ </div>
183
+ ```
184
+
185
+ ほかに 2 つ。
186
+
187
+ - **`@squadbase/vantage/ui` のプリミティブをスライドに置かない。** UI キットは rem と Tailwind で
188
+ 組まれているので、1920×1080 を縮小投影すると `text-sm` のボタンが実効 8px 程度になって読めない。
189
+ スライドは絶対 px のインラインスタイルで書く。
190
+ - **dev サーバーを起動したあとに最初の `.slide.tsx` を新規作成した場合、初回の pptx 書き出しで
191
+ 1 回だけリロードが入る。** 書き出し用の依存はプロジェクトがデッキを持つときだけ宣言される仕組みで、
192
+ その判定は dev の起動時に 1 度しか走らないため。`vantage dev` を再起動すれば解消する。
193
+
194
+ 書き方の作法(キャンバスの縦方向の予算、段階表示、トランジション、モーフ)は `vantage-slide` スキル。
195
+
156
196
  ## 生成物:`.vantage/` と `dist/` は触らない
157
197
 
158
198
  どちらも gitignore 済みの生成物。編集しても次のコマンドで上書きされる。`.vantage/` は任意の CLI
@@ -0,0 +1,264 @@
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
+ - **スライドは `<SlideDeck>` の直下の `<Slide>`。** 深くネストした `<Slide>` は拾われません。
47
+ - `definePage` の `title` / `navLabel` はビルド時に静的抽出されるので**リテラルで書く**(変数・
48
+ テンプレート補間は不可)。ナビの表示名はこれで決まります。
49
+ - **`.slide` を付けられるのはルートになるページモジュールだけ。** `_layout.slide.tsx` や
50
+ `deck.slide.ts` は `INVALID_PAGE_VARIANT` エラーになります。
51
+ - **`june.slide.tsx` と `june.tsx` は同じ URL** になるので `ROUTE_CONFLICT` です。どちらかを消すこと。
52
+ - **デッキファイル以外を作らない。** ヘルパーコンポーネントも定数も同じ `.slide.tsx` の中に書く。
53
+ スライド専用のコンポーネントを `components/` に切り出すと、デッキ 1 本を読むのに 2 ファイル
54
+ 追わせることになります。
55
+
56
+ ## import してよいもの
57
+
58
+ - `@squadbase/vantage/slide` — `SlideDeck` `Slide` `Steps` `Step` `MorphElement` `useSlideNumber`
59
+ `useIsActiveSlide` `SlideCanvas` と、その型
60
+ - `@squadbase/vantage` — `definePage`
61
+ - `@squadbase/vantage/components` の `EChart` — **チャートを載せるときはこれ**(→「チャート」)
62
+ - `lucide-react` のアイコン
63
+
64
+ **`@squadbase/vantage/ui` のプリミティブはスライドに使わない。** UI キットは rem と Tailwind で
65
+ ダッシュボード向けに組まれているので、1920×1080 を縮小投影すると `text-sm` のボタンは実効 8px 程度に
66
+ なって読めません。スライドは**絶対 px のインラインスタイル**で書きます。
67
+
68
+ ## キャンバス
69
+
70
+ 各スライドは **1920 × 1080** の固定キャンバスに描かれ、フレームワークが画面に合わせて縮小します。
71
+ ビューポートが文字どおり 1920×1080 だと思って設計してください。
72
+
73
+ - `font-size`・padding・位置は**絶対 px**。`rem`・`vw`/`vh`・文字サイズの `%` は使わない
74
+ - 各スライドのルート要素はキャンバスを埋める(`width: "100%"; height: "100%"`)
75
+ - インライン `style={{ … }}` を使う。読み込んだ CSS はグローバルです
76
+
77
+ ### タイプスケール(ここから始めて調整する)
78
+
79
+ | 要素 | サイズ |
80
+ | --- | --- |
81
+ | ヒーロータイトル | 140–200px |
82
+ | セクション見出し | 80–120px |
83
+ | ページ見出し | 56–80px |
84
+ | 本文 | 32–44px |
85
+ | キャプション / ラベル | 22–28px |
86
+
87
+ ### 余白
88
+
89
+ - コンテンツの padding: キャンバス端から **100–160px**。文字を端に触れさせない
90
+ - line-height: 見出し 1.2、本文 1.5–1.7
91
+ - 要素間: 32–64px
92
+
93
+ ### 縦方向の予算 — 中身は 1080px に収まらなければならない
94
+
95
+ キャンバスは**スクロールしません**。1080px の下端を越えたものは黙って切れます。JSX を書く前に
96
+ 計算して収まることを確かめること。**これがスライドが壊れる原因の第 1 位**で、確かめない限り
97
+ 溢れていると思ってください。
98
+
99
+ **使える高さ** = `1080 − 上 padding − 下 padding`。上下 120px なら **840px**、160px なら **760px**。
100
+ 先に padding を決めて、その予算の中で設計します。
101
+
102
+ **要素の高さ** = `文字サイズ × line-height × 行数`。2 行に折り返す箇条書きは 2 行と数えます。
103
+ 次の要素を足す前に、その下の gap(32–64px)を加算してください。
104
+
105
+ **計算例 — 本文 1 枚、padding 120px(予算 840px):**
106
+
107
+ | 要素 | 高さ |
108
+ | --- | --- |
109
+ | 見出し: 80px × 1.2 × 1 行 | 96px |
110
+ | gap | 64px |
111
+ | 本文: 40px × 1.6 × 3 行 | 192px |
112
+ | gap | 48px |
113
+ | 箇条書き 5 行: 40px × 1.6 × 1 行 | 320px(5 × 64px) |
114
+ | 箇条書き間の gap 4 つ: 24px | 96px |
115
+ | **合計** | **816px ✅ 840 に収まる** |
116
+
117
+ 見出しを 120px にするか箇条書きを 6 つにすると溢れます。**書く前に毎ページこの検算をすること。**
118
+
119
+ **ページ単位の規則:**
120
+
121
+ - 「見出し + 本文」か「見出し + 5 つ以内の短い箇条書き」のどちらか。両方は載せない
122
+ - 箇条書きは選んだ文字サイズで 1 行に収まること。折り返すなら短くするか別ページに割る
123
+ - ヒーロータイトル(140–200px)はタイトル + サブタイトル 1 行 + アイブロウまで。それ以外は載せない
124
+ - セクション見出し(80–120px)のページにはほとんど何も足さない
125
+ - 収めるために padding を上げる・スケールの下限より小さい文字にする・本文の line-height を 1.4 未満に
126
+ 詰める、をやりたくなったら**ページを 2 枚に割る**。予算が厳しいときは分割が常に正解
127
+
128
+ **`overflow: auto/scroll`・負のマージン・transform で溢れを隠さないこと。** キャンバスは固定で、
129
+ 切れた中身は消えています。
130
+
131
+ ## ビジュアルの方向
132
+
133
+ 一貫した見た目を選び、全ページで保つこと。
134
+
135
+ - **配色** — 背景 1・本文 1・アクセント 1・ミュート 1。前の 3 つは `<SlideDeck theme>` に入れ、
136
+ それ以外はプレーンな const にする(→ `references/tokens.md`)
137
+ - **書体** — ディスプレイ 1 + 本文 1。指定がなければシステムスタック。見出しは太く(800–900)、
138
+ 本文は通常(400–500)
139
+ - **レイアウトグリッド** — padding を 1 つ決めて(例: 120px)貫く。左揃えは編集的、中央揃えは儀礼的
140
+ - **美学のコミットメント** — 1 つ選ぶ: ミニマル / マキシマル / エディトリアル / レトロ /
141
+ ブルータリスト / ソフト・パステル / ネオン / 紙・印刷。混ぜない
142
+
143
+ ## 繰り返す要素は `map` ではなくコンポーネント
144
+
145
+ カード・ロゴの列・タイル・行など**視覚的に繰り返す**要素は、小さなコンポーネントを定義して
146
+ **1 件につき 1 回インスタンス化**します。データ配列を `map` して 1 つのテンプレートから描かない。
147
+
148
+ ```tsx
149
+ // ✅ 各カードが独自の JSX ノードを持つ
150
+ const Card = ({ label, value }: { label: string; value: string }) => (
151
+ <div style={{ flex: 1 }}>
152
+ <div style={{ fontSize: 28, opacity: 0.6 }}>{label}</div>
153
+ <div style={{ fontSize: 120, fontWeight: 800 }}>{value}</div>
154
+ </div>
155
+ )
156
+
157
+ <div style={{ display: "flex", gap: 96 }}>
158
+ <Card label="売上" value="¥5.2億" />
159
+ <Card label="解約率" value="1.4%" />
160
+ </div>
161
+ ```
162
+
163
+ `map` の本体は**ソース上 1 箇所**なので、あとから 1 枚だけ文言や色を変えたいときに全部が一緒に
164
+ 変わります。コンポーネント定義がレイアウトの単一の正本のまま、インスタンスごとの値だけが
165
+ 呼び出し側に出る形が正しい。
166
+
167
+ これは**視覚要素が繰り返すとき**の話で、データが繰り返すときの話ではありません。純粋な
168
+ テキストの箇条書き(`<ul><li>`)は各 `<li>` が既に独立した JSX ノードなので、そのままで正しい。
169
+
170
+ ## ページ番号
171
+
172
+ フッターに現在位置を出すなら `useSlideNumber()` から読む。**ハードコードしない。**
173
+
174
+ ```tsx
175
+ const { current, total } = useSlideNumber() // current は 1 始まり
176
+ ```
177
+
178
+ スライドを差し込む・並べ替える・消すたびに全フッターを直す羽目になるのを避けられます。
179
+
180
+ ## チャート
181
+
182
+ チャートは `@squadbase/vantage/components` の `EChart` を使います。**canvas に描かれるので、
183
+ pptx 書き出しのために描き終わりを待たせる必要があります**:
184
+
185
+ ```tsx
186
+ <div data-waitfor="canvas">
187
+ <EChart option={option} style={{ width: 1400, height: 600 }} />
188
+ </div>
189
+ ```
190
+
191
+ `data-waitfor` を付けないと、書き出しがチャートの描画前にスクリーンショットを撮り、
192
+ **そのスライドだけチャートが白紙**で出ます。
193
+
194
+ チャートに色をハードコードしないこと(`option.color` / `itemStyle.color` / `theme`)。
195
+ `EChart` はテーマトークンに追従します。
196
+
197
+ ## 実行時に無料で付いてくるもの
198
+
199
+ - `src/june.slide.tsx` → `/june`。ナビ(`useRoutes()`)に通常のページと同格で並ぶ
200
+ - `→ ↓ Space PageDown` で次、`← ↑ PageUp` で前、`Home` / `End` で端へ
201
+ - 画面の左右 30% のクリックでも送れる(中央は不活性。リンク・ボタンの上は素通し)
202
+ - `F` で全画面、`Escape` で全画面を抜ける / デッキから戻る
203
+ - `L` でスライド一覧(左のサムネイル列)を開閉。クリックでその枚へ飛ぶ
204
+ - 現在位置は URL の `?p=` に入る(履歴は汚れない)。`?p=4` のリンクをそのまま共有できる
205
+ - ツールバーから **pptx 書き出し**
206
+ - 画面サイズを変えても縮尺が変わるだけでレイアウトは崩れない
207
+
208
+ 見え方は 3 通りで、切り替えはすべて実行時(コードでは決めない):
209
+
210
+ | 見え方 | 出し方 |
211
+ |---|---|
212
+ | 全画面 | `F` / ツールバー。スライドだけになり、一覧は退く |
213
+ | 一覧 + スライド(既定) | 左にサムネイル、右にスライド |
214
+ | スライドのみ | `L` / ツールバー で一覧を閉じる。表示領域いっぱいに 16:9 を描く |
215
+
216
+ 一覧の開閉は `?rail=` にも入るので、閉じた状態のリンクも共有できます。
217
+ 閉じた状態から始めたいデッキだけ `<SlideDeck rail={false}>` と書きます
218
+ (`embedded` のデッキは幅が無いので既定で閉じています)。サムネイルの配色はデッキ自身の
219
+ `--slide-bg` / `--slide-fg` の混色だけで作られていて、色味は持ちません。
220
+
221
+ ## pptx 書き出しの前提
222
+
223
+ デッキは**画像として**書き出されます。1 スライド = 1 枚の PNG(3840×2160)。文字は入らないので
224
+ 受け取り側にフォントが要らず、日本語も欠けません。代わりに**書き出した pptx の文字は検索・編集
225
+ できません**(ブラウザで見ている間は DOM なので選択できます)。
226
+
227
+ 書き出しが壊れる典型は 2 つ:
228
+
229
+ 1. **`data-waitfor` を付けないチャート**(→「チャート」)
230
+ 2. **サブセットしていない日本語 webfont**(→ `references/tokens.md`)
231
+
232
+ 段階表示は**全部開いた状態**で書き出されます(`<Steps>` はホストが無いと全開)。
233
+
234
+ ## 仕上げ前のセルフレビュー
235
+
236
+ - [ ] `export default` が `<SlideDeck>` を返している
237
+ - [ ] `<Slide>` が `<SlideDeck>` の直下にある
238
+ - [ ] 各スライドのルート要素が `100% × 100%` を埋めている
239
+ - [ ] 文字が padding の内側にある(端に触れていない)
240
+ - [ ] **全ページで(文字サイズ × line-height × 行数)+ gap + padding×2 ≤ 1080px。** 際どいなら分割
241
+ - [ ] 箇条書きが 2 行目に折り返していない
242
+ - [ ] 配色とタイプスケールが全ページで一貫している
243
+ - [ ] 1 ページ 1 メッセージ
244
+ - [ ] 繰り返す視覚要素が `array.map` ではなく明示的な `<Component />` になっている
245
+ - [ ] ページ番号が `useSlideNumber()` から来ている(ハードコードしていない)
246
+ - [ ] チャートが `data-waitfor="canvas"` で包まれている
247
+ - [ ] `<Steps>` を使ったページが、`?p=` で直接開いても完成した絵として読める
248
+ - [ ] トランジションを宣言したなら、全ページが 1 つの family に収まっている(→ `references/transitions.md`)
249
+ - [ ] `definePage` の値がリテラルである
250
+ - [ ] `.slide.tsx` 以外のファイルを作っていない
251
+
252
+ ## アンチパターン
253
+
254
+ - ❌ 文字の壁。1 ページ 40 語を超えたら分割する
255
+ - ❌ キャンバス全面を本文で使う。padding 100–160px を守る
256
+ - ❌ 縦に 1080px を溢れさせる。切れた中身は見えない — 分割する
257
+ - ❌ `overflow: auto` / `scroll` / `hidden` で溢れを「隠す」。バグを隠しただけ
258
+ - ❌ 詰め込むためにスケールの下限より小さい文字にする・padding を 100px 未満にする
259
+ - ❌ 本文 28px 未満。プロジェクタで読めない
260
+ - ❌ `@squadbase/vantage/ui` のプリミティブをスライドに置く(縮小されて読めなくなる)
261
+ - ❌ 依存を追加する。使えるのは `@squadbase/vantage/*` と `lucide-react` と標準 Web API だけ
262
+ - ❌ スライド用のコンポーネントを別ファイルに切り出す
263
+ - ❌ チャートに `data-waitfor="canvas"` を付け忘れる(pptx が白紙になる)
264
+ - ❌ プリミティブを `references/*.md` を読まずに使う
@@ -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
+ 周りのページは静かにカットかフェードしているべきです。
@@ -0,0 +1,57 @@
1
+ # 段階表示(`<Steps>` / `<Step>`)
2
+
3
+ 一度に全部見せる代わりに、1 拍ずつ開いていきます。後から出す部分を `<Step>` で、そのまとまりを
4
+ `<Steps>` で包む。`→` を押すたびに次の `<Step>` が現れ、最後の 1 つの後の `→` で次のスライドへ
5
+ 進みます。`←` は直前の 1 つを畳みます。**順序そのものが主張になっている**ときに使う道具です
6
+ —— 前提 → 帰結 → 転換のように、聴衆が話し手の速さで読むようにするためのもの。
7
+
8
+ ```tsx
9
+ import { Step, Steps } from "@squadbase/vantage/slide"
10
+
11
+ <Steps>
12
+ <Step><p style={ROW}>聴衆は話し手より速く読む。</p></Step>
13
+ <Step><p style={ROW}>全部を一度に出すと先読みされる。</p></Step>
14
+ <Step><p style={ROW}>時間差で出すと注意を配れる。</p></Step>
15
+ </Steps>
16
+ ```
17
+
18
+ ## 規則
19
+
20
+ - **`<Step>` は `<Steps>` の直接の子でなければならない。** 深くネストした(あるいは `<Steps>` の
21
+ 外に置いた)`<Step>` は**最初から全部表示された状態**で描かれ、何も遅延しません。静かに効かない
22
+ ので気づきにくい。
23
+ - **`<Step>` でない子はすぐ表示される。** 見出しや導入の段落を `<Steps>` の中にプレーンな要素として
24
+ 置けば最初から見えます。「見出しは常に、本文は順に」のパターン:
25
+
26
+ ```tsx
27
+ <Steps>
28
+ <h2>全部が待つ必要はない。</h2>{/* 最初から見える */}
29
+ <Step><p>まず状況を置いて…</p></Step>
30
+ <Step><p>次に帰結を重ねる…</p></Step>
31
+ </Steps>
32
+ ```
33
+
34
+ - **1 ページに複数の `<Steps>` があるとマウント順に連なる。** 最初のまとまりが全部開いてから
35
+ 次が始まり、`←` は逆順に畳みます。左の列 → 右の列の順に組み上げる 2 カラムは、各列が自分の
36
+ `<Steps>` を持てばそのまま実現できます。
37
+ - **入り方で初期状態が変わる —— 同じ中身、2 つのリズム。** 前のスライドから `→` で進んで入ると
38
+ 空から積み上がります。`?p=` のリンクで直接入る、あるいは後ろのスライドから戻ってくると、
39
+ **全部開いた完成形**で表示されます。**どちらでも読めるように設計すること** —— 直リンクで開いた
40
+ ときに白紙に見えるページは失敗です。
41
+ - **`<Step>` は `duration` ミリ秒(既定 180)でフェードインする。** `<Step duration={...}>` で変えられます。
42
+ `prefers-reduced-motion: reduce` は自動で即時カットになるので、フォールバックを書く必要はありません。
43
+ - **pptx 書き出しでは全部開いた状態で出ます。** 書き出しは `<Steps>` のホストを持たないインスタンスで
44
+ 描かれるため。段階表示は画面での演出であって、配布物の状態ではありません。
45
+
46
+ ## いつ使うか
47
+
48
+ 順序が主張になっているとき —— 最後の項目が落ちどころになる箇条書き、結論への積み上げ、
49
+ before / after。**全ページの中身を反射的に `<Step>` で包まないこと**: 一目で受け取ってほしい
50
+ ページ(ヒーロータイトル、引用 1 つ、図)は、丸ごと見せた方が強い。段階表示は演出ではなく
51
+ タイミングの道具で、トランジションと同じ節度が要ります。
52
+
53
+ ## アンチパターン
54
+
55
+ - ❌ 全ページの中身を反射的に `<Step>` で包む。一目で伝わるページは丸ごと見せる方が強い
56
+ - ❌ `<Steps>` の直接の子でない `<Step>`(深いネスト、`<Steps>` 無し)。全開で描かれ、何も遅延しない
57
+ - ❌ 直リンクで開くと白紙に見えるページ。`?p=` で入ると全開になることを前提に設計する