@squadbase/vantage-template 0.1.1-dev1.2be5c44 → 0.1.1-dev1.39049f4
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 +12 -2
- 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 +62 -52
- package/base-template/.squadbase/skills/vantage-design/references/extract.md +26 -27
- package/base-template/.squadbase/skills/vantage-design/references/format.md +12 -172
- package/base-template/.squadbase/skills/vantage-design/references/presets.md +50 -110
- package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +50 -1
- package/base-template/.squadbase/skills/vantage-slide/SKILL.md +299 -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 +202 -16
- package/base-template/agents/api.md +29 -0
- package/base-template/agents/csv.md +2 -1
- package/base-template/agents/doc.md +52 -3
- package/base-template/agents/page.md +55 -10
- package/base-template/agents/slide.md +90 -4
- 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 +13 -6
- 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 +8 -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
- package/base-template/DESIGN.md +0 -108
|
@@ -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
|
+
- ❌ とりあえずトランジションを宣言する。**理由が無いなら書かない。** スナップで十分
|
package/base-template/AGENTS.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
> このファイルと `agents/*.md` は、いずれも **Vantage フレームワークが所有する正本**の
|
|
8
8
|
> コピーです。手で書き換えても、`vantage upgrade` を実行するとフレームワーク同梱の正本から
|
|
9
9
|
> 再同期されて上書きされます。アプリ固有のメモは別ファイル(例: `README.md`)に書いてください。
|
|
10
|
-
>
|
|
11
|
-
> `vantage add agents`
|
|
10
|
+
> 例外はプロジェクトルートの任意の `DESIGN.md` で、これは**ユーザーが所有**します(`vantage upgrade` も
|
|
11
|
+
> `vantage add agents` も触らない。→「見た目の規約」)。
|
|
12
12
|
>
|
|
13
13
|
> 手順を伴う踏み込んだワークフローは、同梱の Claude Code Skill に分かれています。このファイルは
|
|
14
14
|
> 「地図」、`agents/*.md` は「種類ごとの静的リファレンス」、Skill は「手順書」という役割分担です。
|
|
@@ -47,6 +47,8 @@ UI キット・開発サーバー・API サーバー・ビルドはすべて Van
|
|
|
47
47
|
- **`definePage` の値はリテラルで書く。** title/description/navLabel はビルド時に静的抽出される
|
|
48
48
|
ため、変数・関数呼び出し・テンプレート補間は使わない。`page` エクスポートはランタイムでは
|
|
49
49
|
読まれない。
|
|
50
|
+
- **アプリの表示言語は、ルート `_layout.tsx` の `export const lang = "ja"`(リテラル)で指定する。**
|
|
51
|
+
`<html lang>` になり、日本語の文節改行とスライドの文言切り替えに効く。未指定は `"en"`。
|
|
50
52
|
|
|
51
53
|
## ディレクトリ構造(ページをどこに置くか)
|
|
52
54
|
|
|
@@ -93,13 +95,25 @@ root/
|
|
|
93
95
|
| `@squadbase/vantage/router` | `Link`・`Outlet`・`useParams`・`useSearch`・`useNavigate`・`redirect`・`notFound`・`useRoutes`・`useCurrentRoute`・`useSearchParam`・`useSearchState` |
|
|
94
96
|
| `@squadbase/vantage/query` | `useApiQuery`・`useApiMutation`・`apiJson`・`apiFetch`・`apiUrl`・`ApiError`、ほか `useQuery`/`useMutation` など TanStack Query の再エクスポート |
|
|
95
97
|
| `@squadbase/vantage/ui` | shadcn/ui(Base UI バリアント)プリミティブ(`Button`・`Loading`・`ErrorState`・`Empty` ほか) |
|
|
96
|
-
| `@squadbase/vantage/components` | 複合パーツ(`PageShell`・`DashboardCardPreset`・`DataTablePreset`・`EChart`
|
|
98
|
+
| `@squadbase/vantage/components` | 複合パーツ(`PageShell`・`DashboardCardPreset`・`DataTablePreset`、KPI の `KpiCard`・`Delta`、型付きチャート `LineChart`・`BarChart`・`AreaChart`・`DonutChart`・`BarList` ほか、`EChart`、書式の `formatValue`) |
|
|
97
99
|
| `@squadbase/vantage/table` | `FilePageView`・`TableViewer`(データファイルページの表ビューア) |
|
|
98
|
-
| `@squadbase/vantage/slide` | `SlideDeck`・`Slide`・`Steps`・`Step`・`MorphElement`・`useSlideNumber`・`useIsActiveSlide`(スライド専用。1920×1080 の固定キャンバス) |
|
|
99
|
-
| `@squadbase/vantage/doc` | `Document`(ドキュメント専用。中の素の HTML 要素に見出し・表などのスタイルが当たる) |
|
|
100
|
+
| `@squadbase/vantage/slide` | `SlideDeck`・`Slide`・`Steps`・`Step`・`MorphElement`・`useSlideNumber`・`useIsActiveSlide`、1 枚分の block `SlideCover`・`SlideExecSummary`・`SlideKpiHero`・`SlideKpiGrid`・`SlideChartInsight`・`SlideScorecardTable`・`SlideNextActions`・`SlideSection`・`SlideComparison`・`SlideChartFull`、手書きの 1 枚に使う部品 `SlideHeading`・`SlideSourceLine`・`SlideColumns`・`SlideColumn`・`DeltaText`(スライド専用。1920×1080 の固定キャンバス) |
|
|
101
|
+
| `@squadbase/vantage/doc` | `Document`、ドキュメント用の block `DocReportHeader`・`DocExecutiveSummary`・`DocKpiScorecard`・`DocFigure`・`DocDataTable`・`DocCallout`・`DocKeyFindings`・`DocActionItems`・`DocNotesAndSources`、部品 `DeltaText`(ドキュメント専用。中の素の HTML 要素に見出し・表などのスタイルが当たる) |
|
|
100
102
|
| `@squadbase/vantage/markdown` | `MarkdownRenderer`(Shiki を隔離するための専用サブパス) |
|
|
101
103
|
| `@squadbase/vantage/server` | `ApiContext`・`ApiHandler`・`HttpError`・`isHttpError`・`json`(server/ 側でのみ使う) |
|
|
102
104
|
|
|
105
|
+
**KPI は `KpiCard`、増減は `Delta`(`comparison` を渡し、下がって良い指標は `betterWhen="down"`)。** 向き・率・色は値から計算
|
|
106
|
+
されるので手で渡さない。`TrendIndicator` は非推奨(`direction` を手で渡すので値と食い違いうる)。props は
|
|
107
|
+
`vantage docs kpi-card` / `vantage docs delta` を引く。
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { KpiCard, Delta } from "@squadbase/vantage/components";
|
|
111
|
+
|
|
112
|
+
<KpiCard label="売上" value={48_200_000} format="currency"
|
|
113
|
+
comparison={{ previous: 42_900_000, vs: "prior-period" }} />
|
|
114
|
+
<Delta value={0.021} comparison={{ previous: 0.026, vs: "prior-period" }} polarity="lower-is-better" />
|
|
115
|
+
```
|
|
116
|
+
|
|
103
117
|
## モードは 2 つ、切り替えるのは `server/` の有無だけ
|
|
104
118
|
|
|
105
119
|
`server/` ディレクトリがあれば **fullstack**、無ければ **SPA**。`vantage-manifest.json` の
|
|
@@ -111,17 +125,30 @@ root/
|
|
|
111
125
|
- `server/` が**無い** → 外部 API を直接叩くなら `useQuery`(→ `agents/page.md`)。自前の API が
|
|
112
126
|
要るようになったら `server/api/*.ts` を足すだけでよい(設定変更は不要)。
|
|
113
127
|
|
|
114
|
-
##
|
|
128
|
+
## 見た目の規約
|
|
129
|
+
|
|
130
|
+
色・書体・角丸などの**値と色の割り当ては、テーマ(`styles.css` の `/* theme: <name> */` マーカー以降の
|
|
131
|
+
上書き)だけが持つ。** ここには値を書かず、使い分けの規則だけを置く。UI(ページ・スライド・
|
|
132
|
+
コンポーネント)はこの規則に従い、色は必ずトークンで指定する(色コード・素のパレットクラスは書かない)。
|
|
115
133
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
- **意味トークン:** 良い = `positive`、悪い = `negative`、注意 = `warning`、情報 = `info`、
|
|
135
|
+
中立 = `neutral`。系列色は `chart-1`〜`chart-5` を番号順に使う(5 系列まで。超えるなら「その他」に
|
|
136
|
+
まとめる)。増減の良否は指標の性質(コスト減・解約率減は良い)で決めて
|
|
137
|
+
positive / negative に分け(`KpiCard` / `Delta` の `betterWhen`。下がって良い指標は `betterWhen="down"`)、向きは ▲▼ や符号で表す。
|
|
138
|
+
- **レイアウト:** 内容はカードで包む。順序は KPI → チャート → 表。値が 1 つなら KPI にする
|
|
139
|
+
(1 本の棒グラフにしない)。強調は 1 画面(スライドは 1 枚)に 1 か所。数値には単位と比較対象を添える。
|
|
140
|
+
- **Do:** トークンで色を指定する / 色の意味をアプリ全体でそろえる / チャートのタイトルに結論を書く。
|
|
141
|
+
- **Don't:** 色コードや素のパレットクラスを書く / `dark:` を並べる / 3D・装飾目的のグラデーション・
|
|
142
|
+
円グラフの多用 / 同じチャートで `negative` を普通の系列色に使う。
|
|
143
|
+
- スライド固有の規則は `agents/slide.md`。
|
|
119
144
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
**任意の `DESIGN.md`:** プロジェクトルートにあれば読んで従う(密度・スライドの見出し方針・
|
|
146
|
+
ブランド素材などユーザー固有の意図)。AGENTS.md の規則に加えて従うが、**色の割り当てはテーマを優先**
|
|
147
|
+
する。新規には作らない。ユーザーの持ち物で、`vantage upgrade` は書き換えも削除もしない。
|
|
148
|
+
|
|
149
|
+
**デザイン変更の依頼**(「もっと落ち着いた色に」「ブランドに合わせて」など)は、ファイルごとに
|
|
150
|
+
直さず、テーマの切り替えかスタイルシートの上書きで完結させる。`DESIGN.md` には記録しない。今のテーマ名は
|
|
151
|
+
`styles.css` の `/* theme: <name> */` マーカーから読む。手順は Skill `vantage-design`。
|
|
125
152
|
|
|
126
153
|
## 何を書くか → どのファイルを読むか
|
|
127
154
|
|
|
@@ -137,6 +164,15 @@ root/
|
|
|
137
164
|
| `.xlsx` ページ | `agents/xlsx.md` |
|
|
138
165
|
| `.csv` ページ | `agents/csv.md` |
|
|
139
166
|
|
|
167
|
+
**ページになるのは `.tsx` / `.jsx`(`.slide.tsx`・`.doc.tsx` を含む)・`.csv`・`.xlsx` だけ。**
|
|
168
|
+
|
|
169
|
+
- `.xls`・`.xlsm`・`.xlsb`・`.ods`・`.tsv`・`.numbers`・`.parquet` はルートにならず、404 にもならない。
|
|
170
|
+
`vantage check` の `UNSUPPORTED_PAGE_FILE` 警告だけが知らせる。`.xlsx` / `.csv` に変換して置く
|
|
171
|
+
- `.md`・`.json`・`.txt` は何も言わずに無視される。Markdown を見せたいときは `.tsx` のページで
|
|
172
|
+
`MarkdownRenderer`(`@squadbase/vantage/markdown`)に渡す
|
|
173
|
+
- `.csv` / `.xlsx` はブラウザでパースする。25MB を超えるとそのページにエラーが出る
|
|
174
|
+
(`FILE_PAGE_TOO_LARGE` 警告)。20 万行を超えるシートは先頭 20 万行だけが表示される
|
|
175
|
+
|
|
140
176
|
## 環境変数
|
|
141
177
|
|
|
142
178
|
- **クライアントで読めるのは `import.meta.env.PUBLIC_*` と `MODE`/`DEV`/`PROD`/`SSR`/`BASE_URL`
|
|
@@ -189,7 +225,7 @@ body・レスポンスの status と形)まで出る。`vantage check --json` /
|
|
|
189
225
|
- **`vantage-app`** — アプリを一から作る / 構成を広げるときの進め方と検証の順序
|
|
190
226
|
- **`vantage-add-feature`** — 既存アプリに page / api / ui を 1 つ足す定型
|
|
191
227
|
- **`vantage-slide`** — `.slide.tsx` でスライドデッキを書く(キャンバスの予算・段階表示・pptx 書き出し)
|
|
192
|
-
- **`vantage-design`** —
|
|
228
|
+
- **`vantage-design`** — テーマの切り替え・`styles.css` の上書きで見た目を直す(配色・書体・ブランド合わせ)
|
|
193
229
|
- **`vantage-pitfalls`** — Base UI(≠ Radix)の癖など、静かに壊れる落とし穴のリファレンス
|
|
194
230
|
- **`project-storage`** — Squadbase の Project Storage(アップロード済みファイル)をブラウザから読み書きする
|
|
195
231
|
|
|
@@ -224,7 +260,7 @@ vantage add skill --all --dir .claude/skills # 置き場所を指定(既定は
|
|
|
224
260
|
> `vantage upgrade` はこのファイルをフレームワーク同梱の正本で丸ごと上書きするため、
|
|
225
261
|
> このセクションは消えます。消えたら squadbase-monorepo の
|
|
226
262
|
> `packages/vantage-template/agents/vantage/` から貼り直してください
|
|
227
|
-
> (リポジトリ側では `
|
|
263
|
+
> (リポジトリ側では `pnpm run agents` が正本と結合して再生成します)。
|
|
228
264
|
|
|
229
265
|
## Squadbase の Connection / Query からデータを読む
|
|
230
266
|
|
|
@@ -302,6 +338,34 @@ const res = await fetch(`${baseUrl}/queries/${QUERY_ID}?since=2026-01-01`, {
|
|
|
302
338
|
})
|
|
303
339
|
```
|
|
304
340
|
|
|
341
|
+
### 生の応答から行を取り出す(`noUncheckedIndexedAccess`)
|
|
342
|
+
|
|
343
|
+
テンプレートの `tsconfig.json` は `noUncheckedIndexedAccess: true`。**配列の添字はすべて
|
|
344
|
+
`T | undefined`** なので、HTTP Query が通す上流の生の応答(GA4 の `rows[].metricValues[]` など)を
|
|
345
|
+
`row.metricValues[0].value` と書くと `Object is possibly 'undefined'.` で落ちる。書く前に添字
|
|
346
|
+
アクセスを頭の中で型検査し、応答の型と行の変換関数を先に決めて `?.` と `??` で取り出す:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
type Ga4Row = {
|
|
350
|
+
dimensionValues?: { value?: string }[]
|
|
351
|
+
metricValues?: { value?: string }[]
|
|
352
|
+
}
|
|
353
|
+
type SessionsDaily = { date: string; sessions: number; pageViews: number }
|
|
354
|
+
|
|
355
|
+
const toSessionsDaily = (row: Ga4Row): SessionsDaily => ({
|
|
356
|
+
date: row.dimensionValues?.[0]?.value ?? "",
|
|
357
|
+
sessions: Number(row.metricValues?.[0]?.value ?? "0"),
|
|
358
|
+
pageViews: Number(row.metricValues?.[1]?.value ?? "0"),
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const report = (await res.json()) as { rows?: Ga4Row[] }
|
|
362
|
+
return Response.json((report.rows ?? []).map(toSessionsDaily))
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- 値が欠けた行を落としたいときは `if (!date) throw new HttpError(502, …)` ではなく、変換後に
|
|
366
|
+
`.filter((r) => r.date !== "")` で除く。`date!` のような非 null 断言で黙らせない。
|
|
367
|
+
- SQL Query の `rows` も同じ。`rows[0]` を使うなら `const first = rows[0]; if (!first) …` で先に絞る。
|
|
368
|
+
|
|
305
369
|
## 構築中の一時的なエラーを「進行中」に見せる(`components/build-recovery.tsx`)
|
|
306
370
|
|
|
307
371
|
ページを書いている途中は、ファイルが書き終わる順番の都合で一瞬だけページが throw する
|
|
@@ -314,6 +378,10 @@ const res = await fetch(`${baseUrl}/queries/${QUERY_ID}?since=2026-01-01`, {
|
|
|
314
378
|
`components/build-recovery.tsx` の `BuildRecovery` が、その画面を DOM から検知して
|
|
315
379
|
`Working on it…` 表示を被せ、dev では 1.2 秒待ってから自動でリロードして復帰させる
|
|
316
380
|
(上限 3 回。使い切ったら「再読み込み」の案内に降格し、無事に描けたら残数が戻る)。
|
|
381
|
+
**ただし `window.__VANTAGE_DRAFT__?.active` が `true` の間(AI の書きかけのファイルを下書きとして
|
|
382
|
+
プレビューに出している間)は、覆わず・リロードもしない。** 下書き中の描画エラーはフレームワークが
|
|
383
|
+
直前に通った版へ戻すので、ここで覆うとその版の表示まで隠れ、リロードすると下書きの表示ごと消える。
|
|
384
|
+
下書きが終わった時点でエラー画面が残っていれば、通常どおり覆う。
|
|
317
385
|
降格した画面にはエラー本文をそのまま出し、`Copy log` でコピーできるようにしてある —
|
|
318
386
|
自力で直らないときに本文をそのまま貼り戻してもらうため。
|
|
319
387
|
`_error.tsx` も同じ `RecoveringState` を描くので、ルートレイアウトが throw したときの
|
|
@@ -349,3 +417,121 @@ lockfile は `bun.lock`)。Squadbase のサンドボックスは bun と依存
|
|
|
349
417
|
`bun install` を一度走らせて `bun.lock` を追従させ、両方を同じコミットに入れる。
|
|
350
418
|
- スクリプトは `bun run <script>`(`bun run dev` / `bun run build` / `bun run check` /
|
|
351
419
|
`bun run routes`)。`npm run <script>` でも動くが、install に npm を使わないこと。
|
|
420
|
+
- **公開から 48 時間未満の版は入らない。** `bunfig.toml` の `minimumReleaseAge`(サプライチェーン対策)で
|
|
421
|
+
弾かれる。`bun add` / `bun install` が「バージョンが見つからない」で落ちたら、直近の版を避けて
|
|
422
|
+
少し前の版を指定する。`bunfig.toml` の設定を緩めたり `@squadbase/*` 以外の除外を足したりしない。
|
|
423
|
+
|
|
424
|
+
## このアプリで作れるもの
|
|
425
|
+
|
|
426
|
+
ユーザーの目的に合わせて、どの形にするかを選ぶ。ここに書かれていない形は約束しない。
|
|
427
|
+
|
|
428
|
+
- **ダッシュボード** — 数字を繰り返し見に来て、変化に気づきたいとき。指標と推移・内訳を 1 画面に
|
|
429
|
+
置き、`server/api/` で実データを取る。
|
|
430
|
+
- 書き込む場所: `src/<slug>.tsx`, `server/api/<slug>/**`
|
|
431
|
+
- ページ: あり
|
|
432
|
+
- 作り方: `src/` にページ(`.tsx`)を置く。書く前に `agents/page.md`(API は `agents/api.md`)を読む。
|
|
433
|
+
- **スライド** — 分析の示唆をまとめて誰かに伝えるとき(調査結果の報告、会議資料、施策の提案)。
|
|
434
|
+
結論 → 根拠 → 次の一手の順にまとめる。
|
|
435
|
+
- 書き込む場所: `src/<slug>.slide.tsx`, `vantage-slide/**`
|
|
436
|
+
- ページ: あり
|
|
437
|
+
- 作り方: `src/<slug>.slide.tsx` に `<SlideDeck>` を返すページを置く(`@squadbase/vantage/slide`)。
|
|
438
|
+
書く前に `agents/slide.md` を読み、`vantage-slide` Skill が未配置なら
|
|
439
|
+
`vantage add skill vantage-slide` で置いてから読む。
|
|
440
|
+
- **ドキュメント** — 報告書・調査のまとめ・説明資料のように、文章と図表を上から読む形で残し
|
|
441
|
+
共有したいとき。
|
|
442
|
+
- 書き込む場所: `src/<slug>.doc.tsx`
|
|
443
|
+
- ページ: あり
|
|
444
|
+
- 作り方: `src/<slug>.doc.tsx` に `<Document>` を返すページを置く(`@squadbase/vantage/doc`。
|
|
445
|
+
ルートは `.doc` を除いた `/<slug>`)。数値はコードに直接書き、チャートは `@squadbase/vantage/components` のチャート部品を文中に置く。
|
|
446
|
+
書く前に `agents/doc.md` を読む。
|
|
447
|
+
- **オートメーション** — 決まった時刻に集計・通知・定期レポートを回したいとき、しきい値で
|
|
448
|
+
アラートしたいとき。
|
|
449
|
+
- 書き込む場所: `automations/<slug>/**`, `pipelines/**`
|
|
450
|
+
- ページ: なし
|
|
451
|
+
- 作り方: `automations/<名前>/automation.yml` に定義し、処理のコードは `pipelines/` に置く。
|
|
452
|
+
書く前に Automation のガイド(`readAutomationGuide`)を読む。
|
|
453
|
+
|
|
454
|
+
CSV / Excel(`.csv` / `.xlsx`)は上の項目とは別で、書いて作るものではない。受け取ったファイルを
|
|
455
|
+
`src/` に置くだけで表として読めるページになる(`src/data/june.xlsx` → `/data/june`。置く前に
|
|
456
|
+
`agents/csv.md` / `agents/xlsx.md` を読む)。値だけが表示され、数式・書式は再現されない。25MB を
|
|
457
|
+
超えるとエラー、20 万行を超えると先頭 20 万行だけが出る。`.xls`・`.ods`・`.tsv` などはページにならない
|
|
458
|
+
(`vantage check` の `UNSUPPORTED_PAGE_FILE` 警告)。
|
|
459
|
+
|
|
460
|
+
## 表示領域に安定 ID を付ける(`data-sq-ref`)
|
|
461
|
+
|
|
462
|
+
Squadbase のチャットは、回答の根拠になったグラフ・表・KPI カードを
|
|
463
|
+
`squadbase://page/<path>#<ref>`(スライド内なら `?p=<N>#<ref>`)のリンクで指し、その領域を開く。
|
|
464
|
+
`<ref>` はページのソースに書かれた `data-sq-ref` の値で、過去の回答のリンクもこの値で今の画面を開く。
|
|
465
|
+
|
|
466
|
+
```tsx
|
|
467
|
+
<DashboardCardPreset data-sq-ref="revenue-trend" title="売上の推移">
|
|
468
|
+
<EChart option={trendOption(data)} height="320px" />
|
|
469
|
+
</DashboardCardPreset>
|
|
470
|
+
|
|
471
|
+
{kpis.map((kpi) => (
|
|
472
|
+
<DashboardCard key={kpi.id} data-sq-ref={kpi.id}>...</DashboardCard>
|
|
473
|
+
))}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### 追加の不変条件
|
|
477
|
+
|
|
478
|
+
- **意味のまとまった表示領域(グラフ・表・KPI カードなど)のルート要素に付ける。** 通常は
|
|
479
|
+
`DashboardCard` / `DashboardCardPreset` のように props を DOM に流す部品。`EChart` と ECharts で描く
|
|
480
|
+
チャート部品(`LineChart` / `BarChart` / `AreaChart` / `DonutChart`)・`DataTable` / `SectionHeader` は
|
|
481
|
+
付けた属性を DOM に流さないので、それらを包むカード(または `div`)に付ける。
|
|
482
|
+
- **値の形式は `^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$`**(ASCII の英数字・ハイフン・アンダースコア、先頭は英数字、
|
|
483
|
+
100 文字以内)。これ以外の値はリンクから開けない。推奨は意味を表す小文字 kebab-case の名前
|
|
484
|
+
(例: `revenue-trend`, `churn-by-segment`)。表示順・ソース上の位置・index・連番から作らない。
|
|
485
|
+
- **`.map` で描く領域は、データの安定したキー(`kpi.id` など)がこの形式を満たすときだけ `data-sq-ref={kpi.id}`
|
|
486
|
+
のように使う。** 日本語や空白を含むキーはそのまま使わず、変換もしない(変換規則が変わると値が変わる)。
|
|
487
|
+
その場合は静的な意味名を付けるか、付けない。
|
|
488
|
+
- **ページ内で一意にする。**
|
|
489
|
+
- **新しく作った・直した画面では付ける。更新では既存の値を変えない** — 領域の意味が変わったときだけ変える
|
|
490
|
+
(値を変えると、それを指していた過去のリンクが開けなくなる)。触っていない画面へ遡って付けることはしない。
|
|
491
|
+
|
|
492
|
+
## ファイル内の書く順序(値 → 画面本体 → 子の部品)
|
|
493
|
+
|
|
494
|
+
ページ・部品のファイルは、上から次の順に書く。
|
|
495
|
+
|
|
496
|
+
1. `import`(と `definePage(...)`)
|
|
497
|
+
2. **値** — データ・定数・列定義・小さな関数。短く
|
|
498
|
+
3. **画面本体** — `export default` のコンポーネント
|
|
499
|
+
4. **子の部品** — 大文字始まりの部品。`function` 宣言で書く
|
|
500
|
+
|
|
501
|
+
理由: ファイルは上から順に書かれ、書きかけでもプレビューに反映される。**本体が書かれた時点で画面が出る。**
|
|
502
|
+
値を本体より後ろに書くと、本体はまだ無い値を読むので、その値が書かれるまで画面が出ない。
|
|
503
|
+
子の部品は後ろでよい — 未定義の部品はプレビューが仮の枠で埋める。
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
import { definePage } from "@squadbase/vantage"
|
|
507
|
+
|
|
508
|
+
export const page = definePage({ title: "売上" })
|
|
509
|
+
|
|
510
|
+
const KPI_ITEMS: Kpi[] = [/* … */] // 値は本体より上
|
|
511
|
+
type Kpi = { label: string; value: number }
|
|
512
|
+
|
|
513
|
+
export default function SalesPage() { // 本体
|
|
514
|
+
return <KpiRow items={KPI_ITEMS} />
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function KpiRow({ items }: { items: Kpi[] }) { /* … */ } // 子の部品は下
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### 追加の不変条件
|
|
521
|
+
|
|
522
|
+
- **子の部品は `function` 宣言で書く。** 宣言は巻き上げられるので、本体より後ろでも安全。
|
|
523
|
+
`const Foo = () => …` のアロー関数は後ろに置くと、モジュール評価時に参照された瞬間に TDZ(初期化前参照)で落ちる。
|
|
524
|
+
- **値(定数・データ・列定義・小さな関数)は本体より上。** 小文字の関数・hook も値として上に置く。
|
|
525
|
+
- 型は消えるのでどこでもよい。値のそばに置く。
|
|
526
|
+
- 順序だけのために、既に並んでいるファイルを書き換えない。
|
|
527
|
+
|
|
528
|
+
## アプリの表示言語(`lang`)
|
|
529
|
+
|
|
530
|
+
ルートの `src/_layout.tsx` の `export const lang = "en"` が `<html lang>` になる。**アプリの内容の言語に合わせて書き換える**
|
|
531
|
+
(ユーザーが日本語で依頼している・画面の文言が日本語なら `"ja"`)。
|
|
532
|
+
|
|
533
|
+
- `lang` は**リテラル文字列**(BCP 47。`"ja"` / `"en"` / `"ja-JP"`)。変数・テンプレート補間は読まれない。
|
|
534
|
+
- 読まれるのは**ルートの `src/_layout.tsx` だけ**。入れ子の `_layout.tsx` やページには書かない。
|
|
535
|
+
- 日本語アプリで `"en"` のままだと、スライドの文節改行(`word-break: auto-phrase`)が効かず、見出しが単語の途中で折れる。
|
|
536
|
+
スライドのツールバー文言も `lang` で切り替わる(`"ja"` なら日本語、`en*` なら英語)。
|
|
537
|
+
- 値は消さない。指定が無いと `"en"` 扱いになる。
|