@squadbase/vantage-template 0.1.1-dev1.470473f → 0.1.1-dev1.6b30e42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/base-template/.squadbase/skills/vantage-add-feature/SKILL.md +9 -7
- package/base-template/.squadbase/skills/vantage-app/SKILL.md +10 -6
- package/base-template/.squadbase/skills/vantage-design/SKILL.md +36 -24
- package/base-template/.squadbase/skills/vantage-design/references/extract.md +16 -14
- package/base-template/.squadbase/skills/vantage-design/references/format.md +6 -5
- package/base-template/.squadbase/skills/vantage-design/references/presets.md +60 -110
- package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +45 -1
- package/base-template/.squadbase/skills/vantage-slide/SKILL.md +281 -0
- package/base-template/.squadbase/skills/vantage-slide/references/morph.md +102 -0
- package/base-template/.squadbase/skills/vantage-slide/references/steps.md +57 -0
- package/base-template/.squadbase/skills/vantage-slide/references/tokens.md +87 -0
- package/base-template/.squadbase/skills/vantage-slide/references/transitions.md +196 -0
- package/base-template/AGENTS.md +162 -2
- package/base-template/DESIGN.md +7 -6
- package/base-template/agents/api.md +29 -0
- package/base-template/agents/csv.md +2 -1
- package/base-template/agents/doc.md +4 -3
- package/base-template/agents/page.md +32 -8
- package/base-template/agents/slide.md +6 -1
- package/base-template/agents/xlsx.md +2 -1
- package/base-template/bun.lock +78 -68
- package/base-template/bunfig.toml +5 -0
- package/base-template/package.json +6 -6
- package/base-template/src/_layout.tsx +9 -9
- package/base-template/src/components/build-recovery.tsx +37 -1
- package/base-template/src/components/user-card.tsx +10 -10
- package/base-template/src/index.tsx +7 -3
- package/base-template/src/styles.css +108 -12
- package/dist/index.js +956 -0
- package/package.json +20 -17
- package/ui-templates/CLAUDE.md +2 -0
- package/ui-templates/kpi-chart-simple/pages/index.tsx +48 -48
- package/ui-templates/kpi-chart-simple-ja/pages/index.tsx +48 -48
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
- **ルート `/` がすでに他のページ(`index.tsx` など)で使われている場合、スライドは `index.slide.tsx` ではなく、タイトルから作った slug でファイル名を付ける。** `index.slide.tsx` と `index.tsx` は同じ `/` に解決して `ROUTE_CONFLICT` になる。
|
|
53
|
+
- 例: "Label check" → `src/label-check.slide.tsx` → `/label-check`
|
|
54
|
+
- 例: "Monthly Report" → `src/monthly-report.slide.tsx` → `/monthly-report`
|
|
55
|
+
- **デッキファイル以外を作らない。** ヘルパーコンポーネントも定数も同じ `.slide.tsx` の中に書く。
|
|
56
|
+
スライド専用のコンポーネントを `components/` に切り出すと、デッキ 1 本を読むのに 2 ファイル
|
|
57
|
+
追わせることになります。
|
|
58
|
+
|
|
59
|
+
## import してよいもの
|
|
60
|
+
|
|
61
|
+
- `@squadbase/vantage/slide` — `SlideDeck` `Slide` `Steps` `Step` `MorphElement` `useSlideNumber`
|
|
62
|
+
`useIsActiveSlide` `SlideCanvas` と、その型
|
|
63
|
+
- `@squadbase/vantage` — `definePage`
|
|
64
|
+
- `@squadbase/vantage/components` の `EChart` — **チャートを載せるときはこれ**(→「チャート」)
|
|
65
|
+
- `lucide-react` のアイコン
|
|
66
|
+
|
|
67
|
+
**`@squadbase/vantage/ui` のプリミティブはスライドに使わない。** UI キットは rem と Tailwind で
|
|
68
|
+
ダッシュボード向けに組まれているので、1920×1080 を縮小投影すると `text-sm` のボタンは実効 8px 程度に
|
|
69
|
+
なって読めません。スライドは**絶対 px のインラインスタイル**で書きます。
|
|
70
|
+
|
|
71
|
+
## キャンバス
|
|
72
|
+
|
|
73
|
+
各スライドは **1920 × 1080** の固定キャンバスに描かれ、フレームワークが画面に合わせて縮小します。
|
|
74
|
+
ビューポートが文字どおり 1920×1080 だと思って設計してください。
|
|
75
|
+
|
|
76
|
+
- `font-size`・padding・位置は**絶対 px**。`rem`・`vw`/`vh`・文字サイズの `%` は使わない
|
|
77
|
+
- 各スライドのルート要素はキャンバスを埋める(`width: "100%"; height: "100%"`)
|
|
78
|
+
- インライン `style={{ … }}` を使う。読み込んだ CSS はグローバルです
|
|
79
|
+
|
|
80
|
+
### タイプスケール(ここから始めて調整する)
|
|
81
|
+
|
|
82
|
+
| 要素 | サイズ |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| ヒーロータイトル | 140–200px |
|
|
85
|
+
| セクション見出し | 80–120px |
|
|
86
|
+
| ページ見出し | 56–80px |
|
|
87
|
+
| 本文 | 32–44px |
|
|
88
|
+
| キャプション / ラベル | 22–28px |
|
|
89
|
+
|
|
90
|
+
### 余白
|
|
91
|
+
|
|
92
|
+
- コンテンツの padding: キャンバス端から **100–160px**。文字を端に触れさせない
|
|
93
|
+
- line-height: 見出し 1.2、本文 1.5–1.7
|
|
94
|
+
- 要素間: 32–64px
|
|
95
|
+
|
|
96
|
+
### 縦方向の予算 — 中身は 1080px に収まらなければならない
|
|
97
|
+
|
|
98
|
+
キャンバスは**スクロールしません**。1080px の下端を越えたものは黙って切れます。JSX を書く前に
|
|
99
|
+
計算して収まることを確かめること。**これがスライドが壊れる原因の第 1 位**で、確かめない限り
|
|
100
|
+
溢れていると思ってください。
|
|
101
|
+
|
|
102
|
+
**使える高さ** = `1080 − 上 padding − 下 padding`。上下 120px なら **840px**、160px なら **760px**。
|
|
103
|
+
先に padding を決めて、その予算の中で設計します。
|
|
104
|
+
|
|
105
|
+
**要素の高さ** = `文字サイズ × line-height × 行数`。2 行に折り返す箇条書きは 2 行と数えます。
|
|
106
|
+
次の要素を足す前に、その下の gap(32–64px)を加算してください。
|
|
107
|
+
|
|
108
|
+
**計算例 — 本文 1 枚、padding 120px(予算 840px):**
|
|
109
|
+
|
|
110
|
+
| 要素 | 高さ |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| 見出し: 80px × 1.2 × 1 行 | 96px |
|
|
113
|
+
| gap | 64px |
|
|
114
|
+
| 本文: 40px × 1.6 × 3 行 | 192px |
|
|
115
|
+
| gap | 48px |
|
|
116
|
+
| 箇条書き 5 行: 40px × 1.6 × 1 行 | 320px(5 × 64px) |
|
|
117
|
+
| 箇条書き間の gap 4 つ: 24px | 96px |
|
|
118
|
+
| **合計** | **816px ✅ 840 に収まる** |
|
|
119
|
+
|
|
120
|
+
見出しを 120px にするか箇条書きを 6 つにすると溢れます。**書く前に毎ページこの検算をすること。**
|
|
121
|
+
|
|
122
|
+
**ページ単位の規則:**
|
|
123
|
+
|
|
124
|
+
- 「見出し + 本文」か「見出し + 5 つ以内の短い箇条書き」のどちらか。両方は載せない
|
|
125
|
+
- 箇条書きは選んだ文字サイズで 1 行に収まること。折り返すなら短くするか別ページに割る
|
|
126
|
+
- ヒーロータイトル(140–200px)はタイトル + サブタイトル 1 行 + アイブロウまで。それ以外は載せない
|
|
127
|
+
- セクション見出し(80–120px)のページにはほとんど何も足さない
|
|
128
|
+
- 収めるために padding を上げる・スケールの下限より小さい文字にする・本文の line-height を 1.4 未満に
|
|
129
|
+
詰める、をやりたくなったら**ページを 2 枚に割る**。予算が厳しいときは分割が常に正解
|
|
130
|
+
|
|
131
|
+
**`overflow: auto/scroll`・負のマージン・transform で溢れを隠さないこと。** キャンバスは固定で、
|
|
132
|
+
切れた中身は消えています。
|
|
133
|
+
|
|
134
|
+
## ビジュアルの方向
|
|
135
|
+
|
|
136
|
+
一貫した見た目を選び、全ページで保つこと。
|
|
137
|
+
|
|
138
|
+
- **配色** — 背景 1・本文 1・アクセント 1・ミュート 1。前の 3 つは `<SlideDeck theme>` に入れ、
|
|
139
|
+
それ以外はプレーンな const にする(→ `references/tokens.md`)
|
|
140
|
+
- **書体** — ディスプレイ 1 + 本文 1。指定がなければシステムスタック。見出しは太く(800–900)、
|
|
141
|
+
本文は通常(400–500)
|
|
142
|
+
- **レイアウトグリッド** — padding を 1 つ決めて(例: 120px)貫く。左揃えは編集的、中央揃えは儀礼的
|
|
143
|
+
- **美学のコミットメント** — 1 つ選ぶ: ミニマル / マキシマル / エディトリアル / レトロ /
|
|
144
|
+
ブルータリスト / ソフト・パステル / ネオン / 紙・印刷。混ぜない
|
|
145
|
+
|
|
146
|
+
## 繰り返す要素は `map` ではなくコンポーネント
|
|
147
|
+
|
|
148
|
+
カード・ロゴの列・タイル・行など**視覚的に繰り返す**要素は、小さなコンポーネントを定義して
|
|
149
|
+
**1 件につき 1 回インスタンス化**します。データ配列を `map` して 1 つのテンプレートから描かない。
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
// ✅ 各カードが独自の JSX ノードを持つ
|
|
153
|
+
const Card = ({ label, value }: { label: string; value: string }) => (
|
|
154
|
+
<div style={{ flex: 1 }}>
|
|
155
|
+
<div style={{ fontSize: 28, opacity: 0.6 }}>{label}</div>
|
|
156
|
+
<div style={{ fontSize: 120, fontWeight: 800 }}>{value}</div>
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
<div style={{ display: "flex", gap: 96 }}>
|
|
161
|
+
<Card label="売上" value="¥5.2億" />
|
|
162
|
+
<Card label="解約率" value="1.4%" />
|
|
163
|
+
</div>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`map` の本体は**ソース上 1 箇所**なので、あとから 1 枚だけ文言や色を変えたいときに全部が一緒に
|
|
167
|
+
変わります。コンポーネント定義がレイアウトの単一の正本のまま、インスタンスごとの値だけが
|
|
168
|
+
呼び出し側に出る形が正しい。
|
|
169
|
+
|
|
170
|
+
これは**視覚要素が繰り返すとき**の話で、データが繰り返すときの話ではありません。純粋な
|
|
171
|
+
テキストの箇条書き(`<ul><li>`)は各 `<li>` が既に独立した JSX ノードなので、そのままで正しい。
|
|
172
|
+
|
|
173
|
+
## ページ番号
|
|
174
|
+
|
|
175
|
+
フッターに現在位置を出すなら `useSlideNumber()` から読む。**ハードコードしない。**
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
const { current, total } = useSlideNumber() // current は 1 始まり
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
スライドを差し込む・並べ替える・消すたびに全フッターを直す羽目になるのを避けられます。
|
|
182
|
+
|
|
183
|
+
## チャート
|
|
184
|
+
|
|
185
|
+
チャートは `@squadbase/vantage/components` の `EChart` を使います。**canvas に描かれるので、
|
|
186
|
+
pptx 書き出しのために描き終わりを待たせる必要があります**:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<div data-waitfor="canvas">
|
|
190
|
+
<EChart option={option} style={{ width: 1400, height: 600 }} />
|
|
191
|
+
</div>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`data-waitfor` を付けないと、書き出しがチャートの描画前にスクリーンショットを撮り、
|
|
195
|
+
**そのスライドだけチャートが白紙**で出ます。
|
|
196
|
+
|
|
197
|
+
チャートに色をハードコードしないこと(`option.color` / `itemStyle.color` / `theme`)。
|
|
198
|
+
`EChart` はテーマトークンに追従します。
|
|
199
|
+
|
|
200
|
+
## 説明と図表を同じデータ時点へ揃える
|
|
201
|
+
|
|
202
|
+
分析を共有するデッキでは、本文・数値・図表が**同じ対象期間と同じ取得時点の結果**を参照していることを
|
|
203
|
+
確かめます。
|
|
204
|
+
|
|
205
|
+
- 対象期間とデータ取得時点を、該当ページかフッターに短く表示する(例: 「2026年6月 / 7月1日 9:00 取得」)
|
|
206
|
+
- **固定した説明文に対して図表だけが最新値へ変わる構成を避ける。** ライブ更新を意図するなら、本文の
|
|
207
|
+
数値・結論も同じ取得結果から導いて、図表と一緒に変わる構成にする
|
|
208
|
+
- 同一時点の結果を保持・参照できない場合は、スナップショットとして保存できたとは報告しない。
|
|
209
|
+
必要な保持の仕組みと、現在保証できない範囲(例: 再表示すると図表だけ最新値になる)を伝える
|
|
210
|
+
|
|
211
|
+
## 実行時に無料で付いてくるもの
|
|
212
|
+
|
|
213
|
+
- `src/june.slide.tsx` → `/june`。ナビ(`useRoutes()`)に通常のページと同格で並ぶ
|
|
214
|
+
- `→ ↓ Space PageDown` で次、`← ↑ PageUp` で前、`Home` / `End` で端へ
|
|
215
|
+
- 画面の左右 30% のクリックでも送れる(中央は不活性。リンク・ボタンの上は素通し)
|
|
216
|
+
- `F` で全画面、`Escape` で全画面を抜ける / デッキから戻る
|
|
217
|
+
- `L` でスライド一覧(左のサムネイル列)を開閉。クリックでその枚へ飛ぶ
|
|
218
|
+
- 現在位置は URL の `?p=` に入る(履歴は汚れない)。`?p=4` のリンクをそのまま共有できる
|
|
219
|
+
- ツールバーから **pptx 書き出し**
|
|
220
|
+
- 画面サイズを変えても縮尺が変わるだけでレイアウトは崩れない
|
|
221
|
+
|
|
222
|
+
見え方は 3 通りで、切り替えはすべて実行時(コードでは決めない):
|
|
223
|
+
|
|
224
|
+
| 見え方 | 出し方 |
|
|
225
|
+
|---|---|
|
|
226
|
+
| 全画面 | `F` / ツールバー。スライドだけになり、一覧は退く |
|
|
227
|
+
| 一覧 + スライド(既定) | 左にサムネイル、右にスライド |
|
|
228
|
+
| スライドのみ | `L` / ツールバー で一覧を閉じる。表示領域いっぱいに 16:9 を描く |
|
|
229
|
+
|
|
230
|
+
一覧の開閉は `?rail=` にも入るので、閉じた状態のリンクも共有できます。
|
|
231
|
+
閉じた状態から始めたいデッキだけ `<SlideDeck rail={false}>` と書きます
|
|
232
|
+
(`embedded` のデッキは幅が無いので既定で閉じています)。サムネイルの配色はデッキ自身の
|
|
233
|
+
`--slide-bg` / `--slide-fg` の混色だけで作られていて、色味は持ちません。
|
|
234
|
+
|
|
235
|
+
## pptx 書き出しの前提
|
|
236
|
+
|
|
237
|
+
デッキは**画像として**書き出されます。1 スライド = 1 枚の PNG(3840×2160)。文字は入らないので
|
|
238
|
+
受け取り側にフォントが要らず、日本語も欠けません。代わりに**書き出した pptx の文字は検索・編集
|
|
239
|
+
できません**(ブラウザで見ている間は DOM なので選択できます)。
|
|
240
|
+
|
|
241
|
+
書き出しが壊れる典型は 2 つ:
|
|
242
|
+
|
|
243
|
+
1. **`data-waitfor` を付けないチャート**(→「チャート」)
|
|
244
|
+
2. **サブセットしていない日本語 webfont**(→ `references/tokens.md`)
|
|
245
|
+
|
|
246
|
+
段階表示は**全部開いた状態**で書き出されます(`<Steps>` はホストが無いと全開)。
|
|
247
|
+
|
|
248
|
+
## 仕上げ前のセルフレビュー
|
|
249
|
+
|
|
250
|
+
- [ ] `export default` が `<SlideDeck>` を返している
|
|
251
|
+
- [ ] `<Slide>` が `<SlideDeck>` の直下にある
|
|
252
|
+
- [ ] 各スライドのルート要素が `100% × 100%` を埋めている
|
|
253
|
+
- [ ] 文字が padding の内側にある(端に触れていない)
|
|
254
|
+
- [ ] **全ページで(文字サイズ × line-height × 行数)+ gap + padding×2 ≤ 1080px。** 際どいなら分割
|
|
255
|
+
- [ ] 箇条書きが 2 行目に折り返していない
|
|
256
|
+
- [ ] 配色とタイプスケールが全ページで一貫している
|
|
257
|
+
- [ ] 1 ページ 1 メッセージ
|
|
258
|
+
- [ ] 繰り返す視覚要素が `array.map` ではなく明示的な `<Component />` になっている
|
|
259
|
+
- [ ] ページ番号が `useSlideNumber()` から来ている(ハードコードしていない)
|
|
260
|
+
- [ ] チャートが `data-waitfor="canvas"` で包まれている
|
|
261
|
+
- [ ] 本文・数値・図表が同じ対象期間と取得時点の結果を参照し、その期間と時点が表示されている
|
|
262
|
+
- [ ] `<Steps>` を使ったページが、`?p=` で直接開いても完成した絵として読める
|
|
263
|
+
- [ ] トランジションを宣言したなら、全ページが 1 つの family に収まっている(→ `references/transitions.md`)
|
|
264
|
+
- [ ] `definePage` の値がリテラルである
|
|
265
|
+
- [ ] `.slide.tsx` 以外のファイルを作っていない
|
|
266
|
+
|
|
267
|
+
## アンチパターン
|
|
268
|
+
|
|
269
|
+
- ❌ 文字の壁。1 ページ 40 語を超えたら分割する
|
|
270
|
+
- ❌ キャンバス全面を本文で使う。padding 100–160px を守る
|
|
271
|
+
- ❌ 縦に 1080px を溢れさせる。切れた中身は見えない — 分割する
|
|
272
|
+
- ❌ `overflow: auto` / `scroll` / `hidden` で溢れを「隠す」。バグを隠しただけ
|
|
273
|
+
- ❌ 詰め込むためにスケールの下限より小さい文字にする・padding を 100px 未満にする
|
|
274
|
+
- ❌ 本文 28px 未満。プロジェクタで読めない
|
|
275
|
+
- ❌ `@squadbase/vantage/ui` のプリミティブをスライドに置く(縮小されて読めなくなる)
|
|
276
|
+
- ❌ 依存を追加する。使えるのは `@squadbase/vantage/*` と `lucide-react` と標準 Web API だけ
|
|
277
|
+
- ❌ スライド用のコンポーネントを別ファイルに切り出す
|
|
278
|
+
- ❌ チャートに `data-waitfor="canvas"` を付け忘れる(pptx が白紙になる)
|
|
279
|
+
- ❌ プリミティブを `references/*.md` を読まずに使う
|
|
280
|
+
- ❌ 説明文を固定したまま図表だけライブデータで描く(再表示すると本文と図表の時点がずれる)
|
|
281
|
+
- ❌ 同一時点の結果を保持できていないのに「スナップショットとして保存した」と報告する
|
|
@@ -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=` で入ると全開になることを前提に設計する
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# デザイントークンと書体
|
|
2
|
+
|
|
3
|
+
## トークンの解決順
|
|
4
|
+
|
|
5
|
+
スライドの色と書体は CSS カスタムプロパティ `--slide-*` から来ます。値は 3 段で解決されます:
|
|
6
|
+
|
|
7
|
+
1. **`theme.css`(フレームワーク)** —— `--slide-bg: var(--background)` のように、アプリ自身の
|
|
8
|
+
トークンへの**別名**が張ってあります。CSS 変数は遅延解決なので、これだけでデッキは
|
|
9
|
+
アプリのライト / ダークの切り替えに自動で追従します
|
|
10
|
+
2. **アプリの `styles.css`** —— アプリがトークンを上書きしていれば、デッキもそれに従います
|
|
11
|
+
3. **`<SlideDeck theme={{ … }}>`** —— そのデッキのルートにインラインで書かれ、上の 2 つに勝ちます
|
|
12
|
+
|
|
13
|
+
| プロパティ | CSS 変数 | 既定 |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `bg` | `--slide-bg` | `var(--background)` |
|
|
16
|
+
| `fg` | `--slide-fg` | `var(--foreground)` |
|
|
17
|
+
| `accent` | `--slide-accent` | `var(--primary)` |
|
|
18
|
+
| `muted` | `--slide-muted` | `var(--muted-foreground)` |
|
|
19
|
+
| `fontDisplay` | `--slide-font-display` | 日本語を含むシステムスタック |
|
|
20
|
+
| `fontBody` | `--slide-font-body` | `var(--slide-font-display)` |
|
|
21
|
+
| `sizeHero` | `--slide-size-hero` | `168px` |
|
|
22
|
+
| `sizeBody` | `--slide-size-body` | `36px` |
|
|
23
|
+
| `radius` | `--slide-radius` | `var(--radius)` |
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<SlideDeck theme={{ bg: "#0f172a", fg: "#f8fafc", accent: "#fbbf24" }}>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**使うときは変数で参照します** —— `color: "var(--slide-fg)"`、`fontSize: "var(--slide-size-hero)"`。
|
|
30
|
+
`theme` に渡した値を直接書かないこと。変数経由にしておけば、`theme` を 1 行変えるだけでデッキ全体が
|
|
31
|
+
追従します。
|
|
32
|
+
|
|
33
|
+
**`theme` に無い色はプレーンな const に置く**(`const muted = "#94a3b8"`)。`theme` は
|
|
34
|
+
「デッキの土台となる 3〜4 色」のためのもので、パレット全部を入れる場所ではありません。
|
|
35
|
+
|
|
36
|
+
## ダークモード
|
|
37
|
+
|
|
38
|
+
`theme` を渡さないデッキは、アプリのトークンをそのまま使います —— つまり `.dark` /
|
|
39
|
+
`[data-theme="dark"]` が祖先に付いていればダークで描かれます。**明暗の両方を自分で管理する必要は
|
|
40
|
+
ありません。**
|
|
41
|
+
|
|
42
|
+
`theme` に具体的な色(`bg: "#0f172a"`)を書いた瞬間、そのデッキは明暗に追従しなくなります。
|
|
43
|
+
これは意図的です —— 配色を決め打ちしたデッキは、どちらのモードでもその配色であるべきだからです。
|
|
44
|
+
追従させたいなら `theme` を渡さないこと。
|
|
45
|
+
|
|
46
|
+
## 書体
|
|
47
|
+
|
|
48
|
+
**既定はシステムスタックで、まずこれを使ってください。** 日本語を含むスタックが最初から入っています。
|
|
49
|
+
|
|
50
|
+
webfont が本当に必要なとき(ブランドフォント、あるいはシステムのカバーが薄い言語):
|
|
51
|
+
|
|
52
|
+
- **`<head>` に 1 回だけ、冪等に読み込む。** スライドコンポーネントの中でスタイルシートを描かないこと。
|
|
53
|
+
切り替え中は 2 枚のスライドが同時にマウントされ、pptx 書き出しでは全スライドが同時にマウントされます
|
|
54
|
+
—— コンポーネントの中で `<link>` を描くと `@font-face` の集合が**マウント数だけ**登録されます。
|
|
55
|
+
`.slide.tsx` のモジュールのトップレベルから 1 回だけ注入します:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
const FONT_HREF = "https://fonts.googleapis.com/css2?family=...&display=swap"
|
|
59
|
+
const FONT_LINK_ID = "vantage-slide-font-june" // デッキごとに違う id にする
|
|
60
|
+
if (typeof document !== "undefined" && !document.getElementById(FONT_LINK_ID)) {
|
|
61
|
+
const link = document.createElement("link")
|
|
62
|
+
link.id = FONT_LINK_ID
|
|
63
|
+
link.rel = "stylesheet"
|
|
64
|
+
link.href = FONT_HREF
|
|
65
|
+
document.head.appendChild(link)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- **実際に使うウェイトだけを列挙する。** 余分な 1 ウェイトが `@font-face` の数を倍にします。
|
|
70
|
+
|
|
71
|
+
## webfont と pptx 書き出し —— ここが壊れやすい
|
|
72
|
+
|
|
73
|
+
pptx 書き出しは、スライドを SVG の `foreignObject` の中でラスタライズします。この隔離された
|
|
74
|
+
描画コンテキストでは:
|
|
75
|
+
|
|
76
|
+
- **ローカルにインストールされたフォントはファミリ名で解決されます。** システムスタックなら
|
|
77
|
+
埋め込みは 1 バイトも要りません。既定のままなら何もしなくてよい、ということです
|
|
78
|
+
- **webfont は data URL としてインライン化されていなければ使えません。** 書き出しは
|
|
79
|
+
`getFontEmbedCSS` で自動的にインライン化しますが、それには 2 つの条件があります:
|
|
80
|
+
- **同一オリジンか CORS が有効**であること(Google Fonts は CORS 有効)
|
|
81
|
+
- **サブセットしてあること。** 日本語の webfont は致命的です —— Noto Sans JP のフルセットは
|
|
82
|
+
100 を超える `unicode-range` の face を持ち、全部を base64 化しようとして書き出しが
|
|
83
|
+
タイムアウトするか、文字が豆腐になります。デッキの文字が固定なら Google Fonts の URL に
|
|
84
|
+
**`&text=<使う文字>`** を付けて、単一 face の小さなサブセットを要求してください
|
|
85
|
+
|
|
86
|
+
**判断の目安**: 日本語のデッキで pptx を配る予定があるなら、**システムスタックのままにする**のが
|
|
87
|
+
最も確実です。ブランドフォントが要件なら `&text=` のサブセットにすること。
|