@squadbase/vantage 0.5.3-dev.71d2349 → 0.6.0-dev.3cc6138
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/NOTICE +73 -0
- package/README.md +3 -3
- package/dist/add-S62E5MSP.js +6 -0
- package/dist/add-S62E5MSP.js.map +1 -0
- package/dist/{build-756WIEVI.js → build-XXKKX6KB.js} +10 -10
- package/dist/{build-756WIEVI.js.map → build-XXKKX6KB.js.map} +1 -1
- package/dist/{check-KZ5PHCA6.js → check-X2TDOILH.js} +8 -8
- package/dist/{check-KZ5PHCA6.js.map → check-X2TDOILH.js.map} +1 -1
- package/dist/{chunk-L3TW4FNA.js → chunk-46PPUW6Y.js} +34 -13
- package/dist/chunk-46PPUW6Y.js.map +1 -0
- package/dist/chunk-5ZOP4SQR.js +47 -0
- package/dist/chunk-5ZOP4SQR.js.map +1 -0
- package/dist/{chunk-BAM33YZ6.js → chunk-B45WAO5Q.js} +6 -5
- package/dist/chunk-B45WAO5Q.js.map +1 -0
- package/dist/{chunk-HQDEYCDP.js → chunk-CIJWVFJD.js} +3 -3
- package/dist/{chunk-HQDEYCDP.js.map → chunk-CIJWVFJD.js.map} +1 -1
- package/dist/chunk-FVWU275X.js +91 -0
- package/dist/chunk-FVWU275X.js.map +1 -0
- package/dist/{chunk-VXAV2435.js → chunk-KLEIVDUV.js} +30 -7
- package/dist/chunk-KLEIVDUV.js.map +1 -0
- package/dist/{chunk-RA6Z3OX3.js → chunk-LCN36MYX.js} +61 -9
- package/dist/chunk-LCN36MYX.js.map +1 -0
- package/dist/{chunk-JC6MT5UU.js → chunk-MXI3PZFT.js} +3 -3
- package/dist/{chunk-JC6MT5UU.js.map → chunk-MXI3PZFT.js.map} +1 -1
- package/dist/chunk-PLST5Q7T.js +2221 -0
- package/dist/chunk-PLST5Q7T.js.map +1 -0
- package/dist/chunk-PVTHHV7X.js +105 -0
- package/dist/chunk-PVTHHV7X.js.map +1 -0
- package/dist/{chunk-EHPJXDJU.js → chunk-SVNOWE5U.js} +4 -2
- package/dist/chunk-SVNOWE5U.js.map +1 -0
- package/dist/{chunk-C4MQRWXS.js → chunk-TDUIW7LD.js} +17 -3
- package/dist/chunk-TDUIW7LD.js.map +1 -0
- package/dist/{chunk-X44ZTKID.js → chunk-V2FV5OJ6.js} +61 -9
- package/dist/chunk-V2FV5OJ6.js.map +1 -0
- package/dist/{chunk-PSEYFYAP.js → chunk-VVOXG6ZU.js} +68 -8
- package/dist/chunk-VVOXG6ZU.js.map +1 -0
- package/dist/{chunk-D4Y5RWJA.js → chunk-YE4CPCXX.js} +81 -21
- package/dist/chunk-YE4CPCXX.js.map +1 -0
- package/dist/cli.js +15 -14
- package/dist/cli.js.map +1 -1
- package/dist/client/index.d.ts +5 -35
- package/dist/client/index.js +1 -1
- package/dist/components/index.js +3 -2212
- package/dist/components/index.js.map +1 -1
- package/dist/{dev-EILX2VTF.js → dev-4MQQDHA4.js} +10 -10
- package/dist/{dev-EILX2VTF.js.map → dev-4MQQDHA4.js.map} +1 -1
- package/dist/{docs-GP2ZHIQF.js → docs-OTFWTCPW.js} +4 -4
- package/dist/{docs-GP2ZHIQF.js.map → docs-OTFWTCPW.js.map} +1 -1
- package/dist/{doctor-FXZLMGC3.js → doctor-QXA6DURZ.js} +8 -8
- package/dist/{doctor-FXZLMGC3.js.map → doctor-QXA6DURZ.js.map} +1 -1
- package/dist/manifest-ttGMFi-Z.d.ts +43 -0
- package/dist/{preview-WBQPCTTT.js → preview-HVOM27FS.js} +4 -4
- package/dist/{preview-WBQPCTTT.js.map → preview-HVOM27FS.js.map} +1 -1
- package/dist/query/index.js +1 -102
- package/dist/query/index.js.map +1 -1
- package/dist/router/index.d.ts +10 -0
- package/dist/router/index.js +2 -89
- package/dist/router/index.js.map +1 -1
- package/dist/{routes-WCN4KSZY.js → routes-2QRAKMGK.js} +17 -8
- package/dist/routes-2QRAKMGK.js.map +1 -0
- package/dist/{search-GLKCJXSI.js → search-7A43AZ7M.js} +4 -4
- package/dist/{search-GLKCJXSI.js.map → search-7A43AZ7M.js.map} +1 -1
- package/dist/server/node.d.ts +1 -1
- package/dist/server/node.js +6 -1
- package/dist/server/node.js.map +1 -1
- package/dist/slide/index.d.ts +230 -0
- package/dist/slide/index.js +1571 -0
- package/dist/slide/index.js.map +1 -0
- package/dist/table/index.d.ts +107 -0
- package/dist/table/index.js +680 -0
- package/dist/table/index.js.map +1 -0
- package/dist/types-DQjLpLCs.d.ts +121 -0
- package/dist/ui/index.js +3 -41
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-S64IXL42.js → upgrade-2QCP7UQQ.js} +9 -9
- package/dist/upgrade-2QCP7UQQ.js.map +1 -0
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +5 -5
- package/docs/en/agent-skills.md +19 -12
- package/docs/en/changelog.md +77 -0
- package/docs/en/cli-reference.md +8 -4
- package/docs/en/pages-and-metadata.md +17 -0
- package/docs/en/routing.md +75 -3
- package/docs/en/ui-and-theming.md +76 -0
- package/docs/index.json +2 -2
- package/docs/ja/agent-skills.md +18 -11
- package/docs/ja/changelog.md +75 -0
- package/docs/ja/cli-reference.md +8 -4
- package/docs/ja/pages-and-metadata.md +17 -0
- package/docs/ja/routing.md +71 -3
- package/docs/ja/ui-and-theming.md +74 -0
- package/package.json +17 -3
- package/skills/vantage-app/SKILL.md +4 -0
- package/skills/vantage-pitfalls/SKILL.md +41 -1
- package/skills/vantage-slide/SKILL.md +264 -0
- package/skills/vantage-slide/references/morph.md +102 -0
- package/skills/vantage-slide/references/steps.md +57 -0
- package/skills/vantage-slide/references/tokens.md +87 -0
- package/skills/vantage-slide/references/transitions.md +196 -0
- package/templates/AGENTS.md +28 -230
- package/templates/agents/api.md +49 -0
- package/templates/agents/csv.md +32 -0
- package/templates/agents/page.md +185 -0
- package/templates/agents/slide.md +34 -0
- package/templates/agents/xlsx.md +33 -0
- package/theme.css +28 -0
- package/dist/add-PPCBMWEN.js +0 -6
- package/dist/add-PPCBMWEN.js.map +0 -1
- package/dist/chunk-BAM33YZ6.js.map +0 -1
- package/dist/chunk-C4MQRWXS.js.map +0 -1
- package/dist/chunk-D4Y5RWJA.js.map +0 -1
- package/dist/chunk-EHPJXDJU.js.map +0 -1
- package/dist/chunk-L3TW4FNA.js.map +0 -1
- package/dist/chunk-PSEYFYAP.js.map +0 -1
- package/dist/chunk-RA6Z3OX3.js.map +0 -1
- package/dist/chunk-VXAV2435.js.map +0 -1
- package/dist/chunk-X44ZTKID.js.map +0 -1
- package/dist/routes-WCN4KSZY.js.map +0 -1
- package/dist/types-De3mo_hv.d.ts +0 -59
- package/dist/upgrade-S64IXL42.js.map +0 -1
|
@@ -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=` のサブセットにすること。
|
|
@@ -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
|
+
- ❌ とりあえずトランジションを宣言する。**理由が無いなら書かない。** スナップで十分
|