@squadbase/vantage-template 0.0.2 → 0.1.0-dev.3f759a0

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 (53) hide show
  1. package/README.md +9 -7
  2. package/base-template/.squadbase/skills/project-storage/SKILL.md +157 -0
  3. package/base-template/.squadbase/skills/vantage-app/SKILL.md +6 -6
  4. package/base-template/.squadbase/skills/vantage-design/SKILL.md +137 -0
  5. package/base-template/.squadbase/skills/vantage-design/references/extract.md +110 -0
  6. package/base-template/.squadbase/skills/vantage-design/references/format.md +172 -0
  7. package/base-template/.squadbase/skills/vantage-design/references/presets.md +110 -0
  8. package/base-template/.squadbase/skills/vantage-design/scripts/contrast.mjs +166 -0
  9. package/base-template/.squadbase/skills/vantage-pitfalls/SKILL.md +22 -0
  10. package/base-template/AGENTS.md +98 -218
  11. package/base-template/DESIGN.md +108 -0
  12. package/base-template/agents/api.md +49 -0
  13. package/base-template/agents/csv.md +32 -0
  14. package/base-template/agents/doc.md +73 -0
  15. package/base-template/agents/page.md +187 -0
  16. package/base-template/agents/slide.md +41 -0
  17. package/base-template/agents/xlsx.md +33 -0
  18. package/base-template/automations/.gitkeep +0 -0
  19. package/base-template/bun.lock +907 -0
  20. package/base-template/package.json +3 -3
  21. package/base-template/pipelines/pyproject.toml +5 -0
  22. package/base-template/squadbase.yml +1 -0
  23. package/base-template/src/_error.tsx +7 -11
  24. package/base-template/src/_layout.tsx +8 -1
  25. package/base-template/src/components/build-recovery.tsx +219 -0
  26. package/base-template/src/index.tsx +53 -8
  27. package/base-template/src/styles.css +19 -0
  28. package/dist/index.js +16 -7
  29. package/package.json +18 -12
  30. package/ui-templates/chart-grid/pages/index.tsx +1 -1
  31. package/ui-templates/chart-grid/preview-square.png +0 -0
  32. package/ui-templates/chart-grid/preview-wide.png +0 -0
  33. package/ui-templates/chart-grid-ja/pages/index.tsx +1 -1
  34. package/ui-templates/chart-grid-ja/preview-square.png +0 -0
  35. package/ui-templates/chart-grid-ja/preview-wide.png +0 -0
  36. package/ui-templates/funnel/pages/index.tsx +2 -2
  37. package/ui-templates/funnel/preview-square.png +0 -0
  38. package/ui-templates/funnel/preview-wide.png +0 -0
  39. package/ui-templates/funnel-ja/pages/index.tsx +2 -2
  40. package/ui-templates/funnel-ja/preview-square.png +0 -0
  41. package/ui-templates/funnel-ja/preview-wide.png +0 -0
  42. package/ui-templates/kpi-chart-advanced/preview-square.png +0 -0
  43. package/ui-templates/kpi-chart-advanced/preview-wide.png +0 -0
  44. package/ui-templates/kpi-chart-advanced-ja/preview-square.png +0 -0
  45. package/ui-templates/kpi-chart-advanced-ja/preview-wide.png +0 -0
  46. package/ui-templates/kpi-chart-simple/preview-square.png +0 -0
  47. package/ui-templates/kpi-chart-simple/preview-wide.png +0 -0
  48. package/ui-templates/kpi-chart-simple-ja/preview-square.png +0 -0
  49. package/ui-templates/kpi-chart-simple-ja/preview-wide.png +0 -0
  50. package/ui-templates/table-focus/preview-square.png +0 -0
  51. package/ui-templates/table-focus/preview-wide.png +0 -0
  52. package/ui-templates/table-focus-ja/preview-square.png +0 -0
  53. package/ui-templates/table-focus-ja/preview-wide.png +0 -0
@@ -0,0 +1,110 @@
1
+ # 初期デザインのプリセット
2
+
3
+ プロジェクト作成時に選ぶ 4 つの出発点。**id(`default` / `calm` / `vivid` / `mono`)はプロジェクト
4
+ 作成のワークフローが参照するので変えないこと。** どれも「チャートのプリセット 1 つ + `--primary` +
5
+ `DESIGN.md` の差分」でできている。**`--primary` はどのプリセットでも必ず書く**(無彩色のプリセットも
6
+ 値を明示する)。チャートだけ変えるとホームのような色の少ない画面では見た目が変わらず、別のプリセットから
7
+ 切り替えたときに前の `--primary` が残るため。
8
+
9
+ 適用の手順は共通:
10
+
11
+ 1. `npx @squadbase/vantage-template chart <チャートのプリセット>` で `styles.css` のマーカーブロックを
12
+ 差し替える。**マーカーブロックの中(`--chart-1..5`)は手で編集しない** ― コマンドの生成物で、
13
+ 次の適用で消える。
14
+ 2. 各プリセットの `--primary` / `--primary-foreground` を、`styles.css` のマーカーの**外**(先頭の
15
+ `:root` ブロック)に `:root` と `.dark, [data-theme="dark"]` の両方で書く。既にある `--primary` は置き換える。
16
+ 3. `DESIGN.md` を `format.md` の雛形から作り、下の「DESIGN.md の差分」の行だけを書き換える。
17
+ 4. `scripts/contrast.mjs` で `--primary-foreground` / `--primary` をライトとダークで確かめる。
18
+
19
+ | id | トーン | チャートのプリセット | `--primary` |
20
+ | --- | --- | --- | --- |
21
+ | `default` | 無彩色のベース。色を持つのはデータ(`blue` チャート)だけ | `blue` | 無彩色(`theme.css` の既定値を明示) |
22
+ | `calm` | 落ち着いた寒色。報告書・経営向け | `ocean` | 深い青(スカイ 700 / 300) |
23
+ | `vivid` | 明るい暖色。営業・マーケティング向け | `sunset` | 濃いオレンジ(オレンジ 700 / 400) |
24
+ | `mono` | モノクロ。印刷・資料への貼り付け向け | `mono` | ジンク 900 / 200 |
25
+
26
+ ## `default`
27
+
28
+ - トーン: 落ち着いた無彩色のベースに、データだけが色を持つ業務ダッシュボード。装飾より読みやすさ。
29
+ - チャート: `blue`(青 → ティール → フクシア → バイオレット → グレー)。
30
+ - `--primary`(フレームワークの既定と同じ無彩色。他のプリセットから戻すときのために明示する):
31
+
32
+ ```css
33
+ :root {
34
+ --primary: oklch(0.205 0 0);
35
+ --primary-foreground: oklch(0.985 0 0);
36
+ }
37
+ .dark,
38
+ [data-theme="dark"] {
39
+ --primary: oklch(0.922 0 0);
40
+ --primary-foreground: oklch(0.205 0 0);
41
+ }
42
+ ```
43
+
44
+ - `DESIGN.md`: 雛形そのまま(`format.md` の雛形は `default` を説明している)。
45
+
46
+ ## `calm`
47
+
48
+ - トーン: 寒色でまとめた静かな画面。強調は控えめにし、数字と罫線で読ませる。
49
+ - チャート: `ocean`(スカイ → シアン → インディゴ → ティール → スレート)。
50
+ - `--primary`(チャートのスカイに揃えた深い青):
51
+
52
+ ```css
53
+ :root {
54
+ --primary: var(--color-sky-700);
55
+ --primary-foreground: oklch(0.985 0 0);
56
+ }
57
+ .dark,
58
+ [data-theme="dark"] {
59
+ --primary: var(--color-sky-300);
60
+ --primary-foreground: oklch(0.205 0 0);
61
+ }
62
+ ```
63
+
64
+ - `DESIGN.md` の差分: 「全体のトーン」を上の文に。チャートの色味を「スカイ → シアン → インディゴ →
65
+ ティール → スレート」に。良い = `--chart-4`(ティール)。1 系列の強調は `--chart-1` + 残り `--chart-5`。
66
+ 禁止: `--chart-1` と `--chart-2` を隣り合う細い線で並べる(どちらも明るい青系)。
67
+
68
+ ## `vivid`
69
+
70
+ - トーン: 明るく勢いのある暖色。KPI とトレンドを大きく見せ、強調色をはっきり使う。
71
+ - チャート: `sunset`(オレンジ → アンバー → ローズ → イエロー → ストーン)。
72
+ - `--primary`(オレンジ 600 は白文字で 4.5:1 に届かないので 700 を使う):
73
+
74
+ ```css
75
+ :root {
76
+ --primary: var(--color-orange-700);
77
+ --primary-foreground: oklch(0.985 0 0);
78
+ }
79
+ .dark,
80
+ [data-theme="dark"] {
81
+ --primary: var(--color-orange-400);
82
+ --primary-foreground: oklch(0.205 0 0);
83
+ }
84
+ ```
85
+
86
+ - `DESIGN.md` の差分: トーンを上の文に。主色の行を「濃いオレンジ」に。良い = 専用色なし(▲ と
87
+ `--foreground`)、悪い = `--destructive`。禁止: `--chart-3`(ローズ)と `--destructive` を同じ
88
+ チャートに置く(悪い値と区別できない)。1 系列の強調は `--chart-1` + 残り `--chart-5`。
89
+
90
+ ## `mono`
91
+
92
+ - トーン: 白黒と灰色だけ。色で意味を運ばず、太さ・順番・ラベルで読ませる。
93
+ - チャート: `mono`(ジンク 900 → 200。ダークでは反転)。
94
+ - `--primary`(チャートの両端と同じジンク):
95
+
96
+ ```css
97
+ :root {
98
+ --primary: var(--color-zinc-900);
99
+ --primary-foreground: oklch(0.985 0 0);
100
+ }
101
+ .dark,
102
+ [data-theme="dark"] {
103
+ --primary: var(--color-zinc-200);
104
+ --primary-foreground: oklch(0.205 0 0);
105
+ }
106
+ ```
107
+
108
+ - `DESIGN.md` の差分: トーンを上の文に。強調 = `--chart-1`(最も濃い灰)。良い = 専用色なし(▲ と
109
+ 符号)、悪い = `--destructive`(**画面で唯一の有彩色**として使う)。チャートは系列を 3 つまでにし、
110
+ それ以上は直接ラベルを付ける。禁止: 隣り合う `--chart-4` と `--chart-5`(明度差が小さい)。
@@ -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)}`)
@@ -35,6 +35,28 @@ Base UI の `SelectValue` は選択中の **value をそのまま描き**、`Sel
35
35
 
36
36
  トリガーに `kanto` や `/sales` と生の値が出たら、まずこれを疑う。
37
37
 
38
+ ## 配色:素のパレット + `dark:` ではなく、セマンティックトークンで書く
39
+
40
+ 色は `text-foreground` / `text-muted-foreground` / `bg-card` / `border-border` などの
41
+ トークンで書く。トークンはライト / ダークの両方を持っているので、`dark:` を並べる必要が無い。
42
+
43
+ `text-slate-900 dark:text-slate-100` のような素の Tailwind パレット + `dark:` の組は、
44
+ **トークンで塗られた周囲と食い違う**。ダークトークンが効くのは祖先に `.dark` /
45
+ `[data-theme="dark"]` が付いているときだけで、`dark:` ユーティリティも同じスイッチに
46
+ 繋がっている(`theme.css` の `@custom-variant dark`)。つまり自分でクラスを付けない限り
47
+ アプリは常にライトで動くのに、素のパレットを使うとその 1 か所だけ手動で両モードを
48
+ 管理することになる。
49
+
50
+ ```tsx
51
+ // ✗ 手動で 2 モードを管理することになる
52
+ <div className="font-medium text-slate-900 dark:text-slate-100">{name}</div>
53
+ // ✓ トークンなら 1 つで済む
54
+ <div className="font-medium text-foreground">{name}</div>
55
+ ```
56
+
57
+ `--muted` / `--accent` / `--secondary` / `--border` は**背景・境界用**で、ライトではほぼ白。
58
+ 文字色に使うと読めなくなる ― 弱い文字色は `text-muted-foreground`。
59
+
38
60
  ## チャート:`EChart` の配色はトークン追従。上書きは `option.color`
39
61
 
40
62
  系列色は `--chart-1..5`、軸・凡例・ツールチップは文字色/境界色のトークンから組まれる