diff --git a/.claude/skills/design-review/SKILL.md b/.claude/skills/design-review/SKILL.md index c7530626b..8725e240a 100644 --- a/.claude/skills/design-review/SKILL.md +++ b/.claude/skills/design-review/SKILL.md @@ -14,16 +14,4 @@ description: 画面デザインやモックアップをレビューする。UI ルールのギャップに気づいたら `../design-principles/feedback.md` の手順でユーザーに伝える。 -## レビュー時の追加確認 - -- コメントが日本語かどうか -- 不具合になりそうな部分はないか -- リファクタできそうな部分がないか(変更差分の範囲内のみ) -- 修正が必要な場合は提案のみ行い、修正は実行しないこと -- インラインコメントや PR コメントで補足した方が良い箇所がないか - -(フロントエンドの場合) - -- 不必要なメモ化(useMemo / useCallback)がないか -- 過剰な抽象化によって可読性が低下していないか -- リンクは next/link を利用しているか +修正が必要な箇所は提案だけを行い、ユーザーに頼まれない限り修正はしない。 diff --git a/.claude/skills/ds-factory/finish.md b/.claude/skills/ds-factory/finish.md index 5724e7d14..9fb2e3a09 100644 --- a/.claude/skills/ds-factory/finish.md +++ b/.claude/skills/ds-factory/finish.md @@ -2,7 +2,7 @@ 以降は生成先のディレクトリで行う。 -1. **依存とトークン**: `git init && make setup`(道具の版は `.tool-versions`) → `make palette` → `make tokens` → `design-md` → `tw-merge` → `make registry` +1. **依存とトークン**: `git init && make setup`(道具の版は `.tool-versions`) → `make palette` → `make generate` 2. **コントラスト**: `make check`。不足があれば `semantic/light.tokens.json` / `dark.tokens.json` の段階番号を調整する。`contrast.rules.json` の基準は緩めない。AAA を選んだ場合は、文字の基準を 7:1 に上げる 3. **見た目の性格**: 生成時に表示された箇所を、新しいキーカラーと性格で書き換える(DESIGN.md の冒頭、`docs/rulebook/01-principles.md` の「見た目の性格」、README の冒頭と特長、README のバナー `docs/assets/banner-*.svg` の色)。ロゴとアイコン(`docs/assets/logo*`・`icon-*`、`apps/storybook/public/` の favicon・アイコン・`logo.svg`)は 8341-ui 固有なので、新しいシステムのロゴに差し替えるか、削除して `.storybook/manager.ts` の `brandImage` と `manager-head.html` のリンクを外す(`docs/assets/README.md`)。ひな形の ADR(0002 など)は「引き継いだ判断」として残し、変えた判断は新しい ADR に書く 4. **プロダクトの前提**: diff --git a/.claude/skills/ds-improve/SKILL.md b/.claude/skills/ds-improve/SKILL.md index b6561df31..09f2f741b 100644 --- a/.claude/skills/ds-improve/SKILL.md +++ b/.claude/skills/ds-improve/SKILL.md @@ -17,13 +17,9 @@ description: このデザインシステム自体を改善するときに使う - 1 PR 1 改善。大きいものは分割してバックログに戻す 3. **直す** - 関連するルールブックの章、DESIGN.md、該当コンポーネントのソースとストーリーを先に読む - - トークンを変えるなら `packages/tokens/src/*.tokens.json` を編集し、`make tokens` と `design-md`・`tw-merge` を実行する(生成物を手で編集しない) + - トークンを変えるなら `packages/tokens/src/{primitive,semantic}/*.tokens.json` を編集し、`make generate` で生成物を更新する(生成物を手で編集しない) - 値や方針を変えるなら `docs/decisions/` に ADR を追加する -4. **再発を防ぐ(省略しない)** - - コントラスト → `tokens/contrast.rules.json` に組み合わせを追加 - - クラスの書き方 → `packages/ui/scripts/audit-classes.ts`、依存の向き → `.dependency-cruiser.cjs`、lint → ルートの `vite.config.ts` の `lint` にルールを追加 - - 振る舞い → テストかストーリーの play 関数を追加 - - 判断基準 → ルールブックに追記 +4. **再発を防ぐ(省略しない)**: 問題の種類に合うものを `docs/rulebook/06-improvement-loop.md` の「4. 再発を防ぐ」の表から選んで追加する 5. **確かめる** - `make verify` が通ること。見た目を変えたら `make test-vrt` も - 見た目を変えたら Storybook の「並べて表示」でライト / ダークを確認する diff --git a/.github/workflows/branch-cleanup.yml b/.github/workflows/branch-cleanup.yml index 4eb218609..4f0995358 100644 --- a/.github/workflows/branch-cleanup.yml +++ b/.github/workflows/branch-cleanup.yml @@ -4,7 +4,7 @@ name: Branch cleanup # - 対象: main・gh-pages 以外で、開いている PR が無く、次のどれかに当たるブランチ # マージ済み・閉じた PR の元ブランチ / main に取り込み済み / ci-debug で始まる一時ブランチ # - plan ジョブが候補を一覧にし、delete ジョブは環境 branch-cleanup の承認を待ってから消す -# (承認者は Settings > Environments > branch-cleanup の Required reviewers で設定する。手順は README) +# (承認者は Settings > Environments > branch-cleanup の Required reviewers で設定する。手順は docs/operations.md の「ブランチの片付け」) # - 候補が無ければ delete ジョブは動かず、承認の依頼も出ない on: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ccb93d06..9d3f7e1aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: # VRT と e2e は手動・都度実行のため CI では回さない - run: make test - # リリースは CI では行わない。権限のあるアカウント保有者が手元で pnpm release:version / release:publish を実行する + # リリースは CI では行わない。権限のあるアカウント保有者が手元で make release-version / release-publish を実行する # (リポジトリにトークンやシークレットを置かない。docs/releasing.md・ADR-0014) # main の Storybook と最新のレジストリ(/r/*.json)を gh-pages ブランチの直下に置く。 diff --git a/.github/workflows/storybook-preview-cleanup.yml b/.github/workflows/storybook-preview-cleanup.yml index 04e57a757..77e729d2f 100644 --- a/.github/workflows/storybook-preview-cleanup.yml +++ b/.github/workflows/storybook-preview-cleanup.yml @@ -1,7 +1,7 @@ name: Storybook preview cleanup # PR を閉じたら、ローカルから公開した Storybook のプレビュー(gh-pages の pr-preview/pr-<番号>/)を消す。 -# 公開はローカルで行う(pnpm storybook:preview。手順は .claude/skills/ds-storybook-preview/SKILL.md)。 +# 公開はローカルで行う(make storybook-preview。手順は .claude/skills/ds-storybook-preview/SKILL.md)。 # ここではビルドしない。 on: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 356fbbb94..6996ac21f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ gh auth login | --- | --- | | `make setup` | 初回の準備(`make tools` と `make install`) | | `make dev` | Storybook を起動する | -| `make check` | fmt・lint(型情報つき)・型、生成物の鮮度、コントラスト、クラス監査、依存の向き | +| `make check` | fmt・lint・型・生成物の鮮度(DESIGN.md・プロダクト用 DESIGN.md の原則・tw-merge・registry.json)・コントラスト・クラス監査・依存の向き・スライドとスキルの検査 | | `make test` | 単体テスト(happy-dom)と、全ストーリーの実ブラウザ描画・axe | | `make verify` | `check` と `test` をまとめて実行する(PR を出す前に) | | `make test-vrt` | 見た目の回帰テスト(手動) | @@ -168,10 +168,8 @@ main へは squash マージするため、**PR のタイトルが main のコ ```bash # 値を変える: packages/tokens/src/{primitive,semantic}/*.tokens.json # キーカラーを変える: packages/tokens/src/palette.config.json → 次で primitive を再生成 -make palette -make tokens # CSS 変数・Tailwind テーマ・Figma JSON -make design-md # DESIGN.md の表 -make tw-merge # クラス統合の設定 +make palette # キーカラーを変えたときだけ +make generate # CSS 変数・Tailwind テーマ・DESIGN.md の表・クラス統合の設定・レジストリ make check ``` @@ -180,20 +178,7 @@ make check ### コンポーネントを足す・変える -`packages/ui/src/components/ui//`(ディレクトリ名は PascalCase)に次をそろえる。構造と import の規則は [コンポーネント構造規則](./packages/ui/src/components/COMPONENT_STRUCTURE.md)(`make check` が検査する)。 - -| ファイル | 中身 | -| --- | --- | -| `index.tsx` | 本体(そのまま公開 API)。Base UI の部品と `tv`(`@/lib/tv`)で書く。フックを使うなら先頭に `'use client'` | -| `index.test.tsx` | 振る舞いの単体テスト(happy-dom) | -| `index.stories.tsx` | ストーリー。操作があるなら play 関数で振る舞いを検査する | -| `index.figma.ts` | Code Connect(Figma のコンポーネントがある場合) | -| `useXxx.ts`・`/` | そのコンポーネント専用のフック・子コンポーネント(必要なら) | - -- semantic トークンの Tailwind クラスだけを使う(任意値では `var(--8341-*)`) -- ほかのコンポーネントは `@/components/ui//index` で import する(相対パスにしない) -- 追加・削除したら `make registry` でレジストリを更新する -- 使い方の規約は [docs/rulebook/04-components.md](./docs/rulebook/04-components.md)、アクセシビリティは [02-accessibility.md](./docs/rulebook/02-accessibility.md) +置き場所・ファイルの種類・import の規則は [コンポーネント構造規則](./packages/ui/src/components/COMPONENT_STRUCTURE.md)、実装の規約と完了の定義は [docs/rulebook/04-components.md](./docs/rulebook/04-components.md)、アクセシビリティは [02-accessibility.md](./docs/rulebook/02-accessibility.md) を読む。構造は `make check` が検査する。 ### 画面設計のルールを変える diff --git a/DESIGN.md b/DESIGN.md index 6d40c4fcf..9eaa508a5 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -182,14 +182,10 @@ DTCG では `{"colorSpace": "oklch", "components": [L, C, H], "hex": "#..."}` ### 共通規約 -- コンポーネントは Base UI の部品を `tv()`(`@/lib/tv`)のスタイルで包む。`tailwind-variants` の `tv` を直接使わない(独自のテーマ名を登録した `tv` でないと `text-fg text-body-md` の文字色が消える) - import パスは shadcn 方式: `@/components/ui/{Name}/index`(ディレクトリ名は PascalCase)、`@/lib/{name}` -- 状態は Base UI の data 属性で装飾する: `data-[checked]:`、`data-[invalid]:`、`data-[disabled]:`、`data-[highlighted]:`、`data-[panel-open]:`、`data-[starting-style]:` -- **フォーカスは `focus-ring` クラス**(Base UI は `data-focus-visible` を付けないため `:focus-visible` で描く)。`outline-none` だけで消さない -- 見た目が 44px 未満の操作対象には `tap-target` クラスでタップ領域を足す -- `hover:` は Tailwind v4 では自動で `@media (hover: hover)` に入る。無効状態に効かせないよう `enabled:hover:` を使う -- 強制カラーモードは `forced-colors:` で補う -- `'use client'` はフック・コンテキスト・イベントハンドラを作るエントリだけに付ける(button / form-field / text-field / textarea / select / truncate) +- 独自のスタイルは `tv()` を `@/lib/tv` から import して書く。`tailwind-variants` の `tv` を直接使わない(独自のテーマ名を登録した `tv` でないと `text-fg text-body-md` の文字色が消える) +- **フォーカスは `focus-ring` クラス**。見た目が 44px 未満の操作対象には `tap-target` +- コンポーネント自体を作る・直すときの規約(data 属性、`enabled:hover:`、`forced-colors:`、`'use client'` など)は [docs/rulebook/04-components.md](./docs/rulebook/04-components.md) ### Button @@ -411,19 +407,10 @@ Breakpoint: md: のみ(768px) Variants: tv() from '@/lib/tv'(tailwind-variants v3) ``` -### 作業前に必ず読むもの +### 作業の前と後 -0. 画面を作る場合は、プロダクトの DESIGN.md(UI の方向性とサービス種別) -1. このファイル -2. `docs/rulebook/02-accessibility.md`(必須ルール) -3. 該当コンポーネントのソースとストーリー(`packages/ui/src/components/*/`) - -### 作業後に必ず実行するもの - -```bash -make check # fmt・lint(型情報つき)・型・トークン・コントラスト・DESIGN.md・レジストリ・クラス監査・アーキテクチャ -make test # 単体テスト + 全ストーリーの a11y テスト -``` +- 前: このファイルは全文ではなく、作業に要る節だけを読む。読む節と順序は `.claude/skills/ds-build-ui/SKILL.md` の「作る前に読む」 +- 後: `make verify`(中身は `make` で出る一覧) ### プロンプト例 diff --git a/Makefile b/Makefile index 5119986de..3a42bf326 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # 8341-ui の開発・運用コマンド。`make` で一覧を表示する。 -# 中身は pnpm のスクリプト(package.json)を呼ぶだけ。手順の説明は CONTRIBUTING.md・docs/releasing.md。 +# 中身は pnpm のスクリプト(package.json)か、リポジトリのスクリプト(node scripts/…)を呼ぶだけ。手順の説明は CONTRIBUTING.md・docs/releasing.md。 .DEFAULT_GOAL := help SHELL := /bin/bash @@ -52,7 +52,7 @@ build-storybook: ## Storybook とレジストリをビルドする ##@ 検査・テスト -check: ## fmt・lint・型・生成物の鮮度・コントラスト・クラス監査・依存の向き・スライドとスキルの検査 +check: ## fmt・lint・型・生成物の鮮度(DESIGN.md・プロダクト用 DESIGN.md の原則・tw-merge・registry.json)・コントラスト・クラス監査・依存の向き・スライドとスキルの検査 pnpm check test: ## 単体テストと、全ストーリーの描画・axe @@ -90,7 +90,7 @@ registry: ## shadcn の registry.json を更新する registry-build: ## レジストリを配信用にビルドする(apps/storybook/public/r) pnpm --filter $(UI) registry:build -generate: tokens design-md tw-merge registry ## 生成物をまとめて更新する(トークンやコンポーネントを変えた後に) +generate: tokens design-md tw-merge registry ## 生成物をまとめて更新する(トークン・コンポーネント・原則を変えた後に) audit: ## 健康診断レポートを作る(reports/audit.md) pnpm audit:report @@ -141,4 +141,4 @@ factory: ## 新しいデザインシステムを生成する(CONFIG=設定フ ##@ 後片付け clean: ## ビルドの成果物を消す(依存は残す) - rm -rf packages/*/dist apps/storybook/storybook-static apps/storybook/public/r packages/ui/registry-assets reports + rm -rf dist packages/*/dist apps/storybook/storybook-static apps/storybook/public/r packages/ui/registry-assets reports diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 71eb56624..3ddc185c7 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -7,8 +7,7 @@ "dev": "storybook dev -p 6006", "build": "pnpm --filter 8341-ui registry:build && storybook build", "test": "vp test --project storybook", - "test:e2e": "playwright test", - "preview:publish": "node scripts/publish-preview.ts" + "test:e2e": "playwright test" }, "dependencies": { "8341-ui": "workspace:*", diff --git a/apps/storybook/scripts/publish-preview.ts b/apps/storybook/scripts/publish-preview.ts index 0ba9ba514..fd25e5bab 100644 --- a/apps/storybook/scripts/publish-preview.ts +++ b/apps/storybook/scripts/publish-preview.ts @@ -5,7 +5,7 @@ * * 使い方(リポジトリのルートで) * make storybook-preview # ビルドして、今のブランチの PR に公開する - * make storybook-preview PR=12 # ビルド済みのものを PR #12 に公開する + * make storybook-preview PR=12 # ビルドして、PR #12 に公開する * make storybook-preview-remove PR=12 # PR #12 のプレビューを消す * * - PR 番号を省くと、今のブランチから開いている PR を探す(gh の REST API を使う) diff --git a/docs/factory.md b/docs/factory.md index 253815300..38ec1932f 100644 --- a/docs/factory.md +++ b/docs/factory.md @@ -5,7 +5,7 @@ このリポジトリには二つの使い方がある。 1. **デザインシステムとして使う** - 8341-ui は、そのままプロダクトで使えるデザインシステムである。トークン(W3C DTCG)、コンポーネント(Base UI + Tailwind CSS v4)、Storybook、ルールブック、DESIGN.md、改善ループを持つ。shadcn のレジストリでコードを取り込む([README](../README.md) の「使う」)。 + 8341-ui は、そのままプロダクトで使えるデザインシステムである。トークン(W3C DTCG)、コンポーネント(Base UI + Tailwind CSS v4)、Storybook、ルールブック、DESIGN.md、改善ループを持つ。shadcn のレジストリでコードを取り込む([README](../README.md) の「クイックスタート」)。 2. **デザインシステムファクトリーとして使う** 画面設計のルール([design-principles](./design-principles/README.md))とユーザーへのヒアリングの結果から、**新しいデザインシステムのリポジトリ**を作る。8341-ui はそのひな形になる。ファクトリーは AI エージェントのスキル(`ds-factory`)として動く。 @@ -35,18 +35,9 @@ docs/design-principles/ ──(ルール)──┬──▶ 8341-ui(この ### 流れ -1. **ルールを読む**: `design-principles` スキルの「新規作成」モード(`mode-1-init.md`)に従う -2. **ヒアリング**: [hearing.md](./design-principles/hearing.md) の項目に、ファクトリー固有の項目(共有の基盤か専用か、名前、接頭辞、キーカラー、フォント、アクセシビリティ目標、テーマ、リポジトリ、配布方法)を足して、一度にまとめて聞く -3. **設定ファイル**: 回答を JSON にまとめる -4. **生成**: `node scripts/factory/create.mjs --config <設定> --out <出力先>` - - git で追跡しているファイルを複製し、名前・接頭辞・キーカラーの系統名・リポジトリの URL を置き換える - - キーカラーの色相(hex から変換も可)を `palette.config.json` に入れる - - ファクトリー専用のファイルと、文書中の `` 〜 `` を取り除く - - 改善ログを空にし、生成の記録を ADR に残す -5. **仕上げ**(生成先で): 依存の導入、パレットとトークンの再生成、コントラストの確認、見た目の性格(キーカラーの名前・説明)の書き換え、プロダクトの前提を DESIGN.md に記入、`make verify` -6. **公開**: ユーザーの承認を得てから GitHub にリポジトリを作る +ルールを読む(`design-principles` の新規作成モード)→ ヒアリング → 設定ファイル(名前・接頭辞・キーカラー・リポジトリ)→ `make factory CONFIG=<設定> OUT=<出力先>` で生成 → 生成先で仕上げ(トークンの再生成・コントラスト・見た目の性格・DESIGN.md)→ ユーザーの承認を得て公開。 -詳しい手順は [`.claude/skills/ds-factory/SKILL.md`](../.claude/skills/ds-factory/SKILL.md)。 +生成では、git で追跡しているファイルを複製し、名前・接頭辞・キーカラーの系統名・リポジトリの URL を置き換え、ファクトリー専用のファイルと `` 〜 `` を取り除く。手順の詳細、ヒアリングの項目、設定ファイルの形は [`ds-factory` スキル](../.claude/skills/ds-factory/SKILL.md)。たとえば接頭辞を `acme` にすると、パッケージは `acme-ui` と `acme-ui-tokens`、CSS 変数は `--acme-*`、レジストリは `@acme/button` になる。 ### 生成されるもの・されないもの @@ -56,19 +47,6 @@ docs/design-principles/ ──(ルール)──┬──▶ 8341-ui(この | design-principles とそのスキル | 8341-ui の改善ログ(空から始める) | | ルールブック・ADR(引き継いだ判断として残る) | ロックファイル(生成先で `make install` して作る) | -### 設定ファイルの例 - -```json -{ - "name": "acme-ui", - "prefix": "acme", - "repository": { "owner": "acme-inc", "name": "acme-design-system" }, - "keyColor": { "hex": "#2f7d5b", "family": "wakatake", "label": "若竹 Wakatake — キーカラー" } -} -``` - -この設定で生成すると、パッケージは `acme-ui` と `acme-ui-tokens`、CSS 変数は `--acme-*`、レジストリは `@acme/button` のようになる。 - ### ファクトリーを直す - 置き換え漏れや、生成後の手作業が多い手順を見つけたら、`scripts/factory/create.mjs` か `ds-factory` スキルを直す diff --git a/docs/improvement/log.md b/docs/improvement/log.md index 0c42788dc..586ed45ad 100644 --- a/docs/improvement/log.md +++ b/docs/improvement/log.md @@ -4,6 +4,7 @@ | 日付 | 種別 | 内容 | 再発防止 | | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| 2026-10-23 | 保守 | 全体のリファクタリング。古い参照(存在しない make ターゲット・パス・README の節名)を直し、トークンを変えた後の手順を `make generate` に統一。Makefile と重複していた pnpm スクリプト、design-review に混ざっていたコードレビュー・Next.js の項目、使われていない関数と export を削除。DESIGN.md・ルールブック 04・06・CONTRIBUTING・factory.md の重複を正本へのリンクにし、トークンの CSS 化の規則を `packages/tokens/scripts/lib/css.ts` に集めて build.ts とスライドで共有 | 規則を 1 か所にしたので、tokens.css とスライドの値の書き方がずれない。重複していた手順は正本(Makefile・COMPONENT_STRUCTURE.md・06 章・ds-factory)だけに置いた | | 2026-10-23 | 機能 | 8341-ui を使うプロダクトに、DESIGN.md と design-principles を配るレジストリ item `@8341/design-md` を追加。DESIGN.md はコードから読み取れない考え方(プロダクトの前提・画面領域ごとの方向性・大事にすること・外した必須)と原則の必須の要約だけにし、トークンや部品の一覧は載せない | ADR-0019。原則の要約は `scripts/sync-product-design.ts` が docs/design-principles から生成し、`make check` で鮮度を検査。配布する原則のファイル一覧も registry.json の --check で追従 | | 2026-10-23 | 文書 | design-principles の原則「削る」に、部品を組み合わせたページで不要なセカンダリラベルや説明を避け、関係をレイアウトで伝えるルールを追加(必須 3・推奨 1・レビュー 3)。WCAG 3.3.2・1.3.1 を緩めないよう、見えるラベルと構造は残す条件を明記。ルールブック 01・05 と DESIGN.md の Don't に反映 | 原則のレビュー項目に追加(design-review で確認される)。05 文言で `supportText` をラベルの言い換えに使わないと明記 | | 2026-10-21 | 開発体験 | スキルの一覧 `docs/skills.md` を追加し、README・AGENTS.md・CONTRIBUTING・ai-agents.md からリンク(重複していた表は一覧へのリンクに置き換え)。文書を必要なときだけ読み込むよう、大きいスキル(design-principles・ds-factory・ds-slides・ds-storybook-preview)を本体と段階ごとの文書に分け、AGENTS.md に読み込みの方針を追加 | ADR-0018。`scripts/check-skills.ts`(name・description・一覧の網羅・本体 2,000 字以内・リンク切れ・参照されない文書)を `make check` に追加 | diff --git a/docs/operations.md b/docs/operations.md index c217148ec..a43e8a5f0 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -6,7 +6,7 @@ | ワークフロー | きっかけ | やること | | --- | --- | --- | -| `CI`(`ci.yml`) | PR、main への push | `check`(fmt・lint・型・生成物の鮮度・コントラスト・クラス監査・依存の向き)、`test`(単体テスト、全ストーリーの実ブラウザ描画と axe)。main では `storybook`(Pages への公開)も動く。リリースは CI では行わない([releasing.md](./releasing.md)) | +| `CI`(`ci.yml`) | PR、main への push | `check`(中身は Makefile の `make check` の説明)、`test`(単体テスト、全ストーリーの実ブラウザ描画と axe)。main では `storybook`(Pages への公開)も動く。リリースは CI では行わない([releasing.md](./releasing.md)) | | `Storybook preview cleanup` | PR を閉じたとき | その PR のプレビュー(`pr-preview/pr-<番号>/`)を消す | | `Branch cleanup` | main への push、手動 | マージ済み・閉じた PR の元ブランチと `ci-debug*` を、環境 `branch-cleanup` の承認後に消す | | `Weekly audit` | 毎週月曜、手動 | 健康診断レポート(warning の蓄積、コントラストの余裕、未使用トークン、ストーリーの欠け)を Issue に起票する | @@ -89,5 +89,5 @@ gh api -X PUT repos/tkc310/8341-ui/environments/branch-cleanup \ | --- | --- | | Pages が古い内容のまま | 配信のキャッシュ。`gh-pages` ブランチが新しければ数分で反映される | | `make install-ci`(`pnpm install --frozen-lockfile`)が失敗する | 依存を変えたのにロックファイルを更新していない。手元で `make install` してコミットする | -| `check` が生成物のずれで失敗する | トークンやコンポーネントを変えたら、`design-md`・`tw-merge`・`registry` を実行して生成物を更新する | +| `check` が生成物のずれで失敗する | トークン・コンポーネント・原則を変えたら、`make generate` で生成物を更新する | | Windows で `CLAUDE.md` やスキルが読めない | シンボリックリンクが取り出せていない([ai-agents.md](./ai-agents.md) の「Windows で使う場合」) | diff --git a/docs/rulebook/03-tokens.md b/docs/rulebook/03-tokens.md index 9f43b4f69..593f67ebb 100644 --- a/docs/rulebook/03-tokens.md +++ b/docs/rulebook/03-tokens.md @@ -70,14 +70,14 @@ DTCG の仕様から外している点と理由は [ADR-0009](../decisions/0009- | 800 | 約 8 | ライトの文字色(リンク・エラー) | | 1000〜1200 | 13〜17 | 本文・ダークの面 | -色相を変えるときは `palette.config.json` の `hue` を変え、`palette` → `build` → `check:contrast` の順に実行する。 +色相を変えるときは `palette.config.json` の `hue` を変え、`make palette` → `make generate` → `make check` の順に実行する。 ### 新しい semantic カラーを足すとき 1. `semantic/light.tokens.json` と `semantic/dark.tokens.json` の **両方** に同じキーで追加する 2. `src/contrast.rules.json` に、そのトークンが置かれうる組み合わせを追加する 3. 用途を `$description` に書く(DESIGN.md・Storybook・Figma に反映される) -4. `make tokens && make check` +4. `make generate && make check` ## タイポグラフィ・余白・サイズ・角丸 diff --git a/docs/rulebook/04-components.md b/docs/rulebook/04-components.md index 0a58a49bf..f3d5d4279 100644 --- a/docs/rulebook/04-components.md +++ b/docs/rulebook/04-components.md @@ -11,15 +11,7 @@ ## ファイル構成 -``` -packages/ui/src/components/ui/Button/ -├── index.tsx # 本体=公開 API(他のコンポーネントはここ経由で参照する) -├── index.test.tsx # 単体テスト(happy-dom) -├── index.stories.tsx # Storybook(状態ごとのストーリーと play 関数) -└── index.figma.ts # Figma Code Connect(テンプレート形式) -``` - -ディレクトリ名は PascalCase。専用のフック(`useXxx.ts`)と子コンポーネント(入れ子のディレクトリ)も同じ場所に置く。詳しくは [コンポーネント構造規則](../../packages/ui/src/components/COMPONENT_STRUCTURE.md)。VRT はコンポーネントを横断するため `src/components/gallery.vrt.test.tsx` に集約している。 +`packages/ui/src/components/ui//index.tsx` に本体を置き、テスト・ストーリー・Code Connect・専用のフックと子コンポーネントを同じディレクトリに置く。置いてよいファイルと import の規則は [コンポーネント構造規則](../../packages/ui/src/components/COMPONENT_STRUCTURE.md)(`make check` が検査する)。VRT はコンポーネントを横断するため `src/components/gallery.vrt.test.tsx` に集約している。 ## 実装の規約 @@ -42,7 +34,7 @@ packages/ui/src/components/ui/Button/ ### スタイル - `tv()` の `slots` / `variants` / `compoundVariants` で書き、利用者の `className` は最後に渡して上書きできるようにする -- 状態は Base UI の data 属性で装飾する(`data-[checked]:`、`data-[invalid]:`、`data-[disabled]:`、`data-[highlighted]:`、`data-[panel-open]:`) +- 状態は Base UI の data 属性で装飾する(`data-[checked]:`、`data-[invalid]:`、`data-[disabled]:`、`data-[highlighted]:`、`data-[panel-open]:`、`data-[starting-style]:`) - **フォーカスは `focus-ring`**(`@/lib/focus` の `focusRing`)。Base UI は `data-focus-visible` を付けないため `:focus-visible` で描く - 見た目が 44px 未満の操作対象には `tap-target` - 無効状態に hover を効かせないよう `enabled:hover:` を使う(Tailwind v4 の `hover:` は自動で `@media (hover: hover)` に入る) @@ -59,7 +51,7 @@ packages/ui/src/components/ui/Button/ - [ ] キーボード操作を play 関数かテストで検証している - [ ] [02 アクセシビリティ](./02-accessibility.md) の手動確認項目を実施した - [ ] 主たる関数の JSDoc の 1 文目に「何をする部品か」を書いた(レジストリの説明になる) -- [ ] `src/index.ts` から export し、`make registry` でレジストリを更新した +- [ ] [コンポーネント構造規則](../../packages/ui/src/components/COMPONENT_STRUCTURE.md) のチェックリストを満たした(`src/index.ts` からの export と `make registry` を含む) - [ ] 振る舞いのテストを `index.test.tsx` に書いた - [ ] Figma のコンポーネントがあれば `index.figma.ts` を書いた - [ ] changeset を追加した(`make changeset`) diff --git a/docs/rulebook/06-improvement-loop.md b/docs/rulebook/06-improvement-loop.md index aae46b7fb..b5ea90b68 100644 --- a/docs/rulebook/06-improvement-loop.md +++ b/docs/rulebook/06-improvement-loop.md @@ -24,7 +24,7 @@ | 仕組み | タイミング | 何を見つけるか | | ----------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `make check`(CI) | PR ごと | fmt・lint(oxlint、型情報つき)・型、コントラスト不足、テーマ間のキーずれ、DESIGN.md / tw-merge 設定 / registry.json のずれ、クラス監査、アーキテクチャ違反(dependency-cruiser) | +| `make check`(CI) | PR ごと | fmt・lint(oxlint、型情報つき)・型、コントラスト不足、テーマ間のキーずれ、生成物のずれ、クラス監査、アーキテクチャ違反(dependency-cruiser)、スライドとスキルの規約違反 | | `make test`(Vitest 5) | PR ごと | 振る舞いの退行(happy-dom)、全ストーリーの play 関数と axe による WCAG 違反(実ブラウザ) | | VRT・e2e | 手動・都度 | 見た目の退行(`make test-vrt`)、320px・200%・強制カラーでの崩れ(`make test-e2e`) | | 週次監査ワークフロー | 毎週月曜 | warning の蓄積、コントラストの余裕が 5% 未満の組み合わせ、未使用トークン、ストーリー・Code Connect の欠け。結果を Issue に起票 | @@ -49,7 +49,7 @@ - 修正は PR で行い、PR テンプレートのチェックリスト(完了の定義)を満たす - 見た目を変えたら、ローカルで `make storybook-preview` を実行して PR ごとの Storybook を GitHub Pages に公開し、変わったストーリーへのリンクを PR に添える(`.claude/skills/ds-storybook-preview/SKILL.md`) - 値や方針を変えたら ADR を追加する(`docs/decisions/NNNN-*.md`) -- トークンを変えたら `make tokens` と `design-md` で CSS と DESIGN.md を更新する(CI がずれを検出する) +- トークンを変えたら `make generate` で生成物を更新する(CI がずれを検出する) ## 4. 再発を防ぐ(いちばん大事) @@ -71,10 +71,4 @@ ## AI エージェントが回す場合 -`.claude/skills/ds-improve/SKILL.md` に手順をまとめています。エージェントは次の順で動きます。 - -1. `make audit` で現状のレポート(`reports/audit.md`)を作る -2. `docs/improvement/backlog.md` と未解決の Issue を読み、優先度の高いものを 1 つ選ぶ -3. 修正し、上の「再発を防ぐ」に従ってチェックを追加する -4. `make verify` が通ることを確認し、changeset と log.md を追加して PR を出す -5. 見た目を変えたら `ds-storybook-preview` スキルでプレビューを公開し、PR 本文にリンクを書く +手順は `ds-improve` スキル(`.claude/skills/ds-improve/SKILL.md`)。この章の 1〜5 を、エージェントが 1 件ずつ回す形にしたもの。 diff --git a/docs/skills.md b/docs/skills.md index c99eff5d3..3e1a8a26f 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -49,7 +49,7 @@ AI エージェント(Claude Code・Cursor・Codex)がこのリポジトリ - `AGENTS.md` から文書を自動で読み込む書き方(`@path/to/file.md` の取り込み)は使わない。表で場所を示し、必要になったら読む - 大きい文書(DESIGN.md・`principles.md`・`wcag-for-designers.md` など)は、全文ではなく見出しで探して該当する節だけを読む -`make check` が、スキルの `name` とディレクトリ名の一致、`description` の有無、この一覧にすべてのスキルが載っていること、本体の字数、スキルの中の相対リンクの切れを検査します(`scripts/check-skills.ts`)。 +`make check` が、スキルの `name` とディレクトリ名の一致、`description` の有無、この一覧にすべてのスキルが載っていること、本体の字数、スキルの中の相対リンクの切れ、どこからも参照されない段階ごとの文書を検査します(`scripts/check-skills.ts`)。 ## スキルを足す・直すとき diff --git a/package.json b/package.json index 65f6c26a9..181103e7a 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,12 @@ "dev": "pnpm build:tokens && pnpm --filter 8341-ui-storybook dev", "build": "pnpm build:tokens && pnpm --filter 8341-ui build", "build:storybook": "pnpm build:tokens && pnpm --filter 8341-ui-storybook build", - "storybook:preview": "pnpm build:storybook && pnpm --filter 8341-ui-storybook preview:publish", "check": "pnpm build:tokens && vp check && vp run -r check && depcruise packages apps && node scripts/slides/check.ts && node scripts/check-skills.ts && node scripts/sync-product-design.ts --check", "test": "pnpm build:tokens && vp run -r test", "test:vrt": "pnpm build:tokens && pnpm --filter 8341-ui test:vrt", "test:e2e": "pnpm build:tokens && pnpm --filter 8341-ui-storybook test:e2e", "audit:report": "pnpm --filter 8341-ui-tokens audit", - "changeset": "changeset", - "factory:create": "node scripts/factory/create.mjs", - "release:version": "node scripts/release.mjs version", - "release:publish": "node scripts/release.mjs publish" + "changeset": "changeset" }, "devDependencies": { "@changesets/cli": "catalog:", diff --git a/packages/tokens/scripts/audit-report.ts b/packages/tokens/scripts/audit-report.ts index d4fb9615a..ad19a4dce 100644 --- a/packages/tokens/scripts/audit-report.ts +++ b/packages/tokens/scripts/audit-report.ts @@ -7,7 +7,7 @@ * 1. コントラスト不足と、余裕が 5% 未満の組み合わせ * 2. クラス監査(packages/ui)の error / warning * 3. どこからも参照されていない semantic トークン - * 4. ストーリー・テスト・Code Connect が無いコンポーネント + * 4. ストーリー・Code Connect が無いコンポーネント * 5. 改善バックログの未着手件数 */ import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'; diff --git a/packages/tokens/scripts/build.ts b/packages/tokens/scripts/build.ts index 1af2b43d6..6992325b1 100644 --- a/packages/tokens/scripts/build.ts +++ b/packages/tokens/scripts/build.ts @@ -18,18 +18,14 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import StyleDictionary, { type Config, type TransformedToken } from 'style-dictionary'; import { fileHeader, formattedVariables } from 'style-dictionary/utils'; +import { THEME_SELECTOR, dimensionToCss, durationToCss, letterSpacingToCss, shadowToCss } from './lib/css.ts'; import { tailwindName } from './lib/tailwind.ts'; -import { PREFIX, ROOT, THEMES, resolvedTokens, sourceFiles } from './lib/tokens.ts'; +import { type DtcgDimension, PREFIX, ROOT, THEMES, cssVarName, resolvedTokens, sourceFiles } from './lib/tokens.ts'; const DIST = resolve(ROOT, 'dist'); const CSS_DIR = resolve(DIST, 'css'); const PARTS = resolve(DIST, '.parts'); -const LIGHT_SELECTOR = ':root, .light, [data-theme="light"]'; -const DARK_SELECTOR = '.dark, [data-theme="dark"]'; -const DARK_MEDIA = '(prefers-color-scheme: dark)'; -const DARK_MEDIA_SELECTOR = ':root:not(.light):not([data-theme="light"])'; - // --------------------------------------------------------------------------- // Transforms // --------------------------------------------------------------------------- @@ -44,11 +40,7 @@ StyleDictionary.registerTransform({ filter: (t) => typeOf(t) === 'dimension', transform: (t) => { const v = valueOf(t); - if (typeof v === 'string') return v; - const { value, unit } = v as { value: number; unit: 'px' | 'rem' }; - if (unit === 'rem') return `${value}rem`; - if (value <= 3 || value >= 9999 || t.path[0] === 'breakpoint') return `${value}px`; - return `${Number((value / 16).toFixed(4))}rem`; + return typeof v === 'string' ? v : dimensionToCss(v as DtcgDimension, t.path); }, }); @@ -60,7 +52,7 @@ StyleDictionary.registerTransform({ filter: (t) => t.path.includes('letter-spacing'), transform: (t) => { const v = valueOf(t); - return typeof v === 'string' ? v : `${v as number}em`; + return typeof v === 'string' ? v : letterSpacingToCss(v as number); }, }); @@ -72,16 +64,11 @@ StyleDictionary.registerTransform({ filter: (t) => typeOf(t) === 'duration', transform: (t) => { const v = valueOf(t); - if (typeof v === 'string') return v; - const { value, unit } = v as { value: number; unit: 'ms' | 's' }; - return `${value}${unit}`; + return typeof v === 'string' ? v : durationToCss(v as { value: number; unit: 'ms' | 's' }); }, }); -/** - * 影(DTCG の shadow: 1 つまたは配列。色はオブジェクト、寸法は { value, unit })を CSS の box-shadow に。 - * 入れ子の値は他の transform が届かないため、ここでまとめて変換する。 - */ +/** 影(DTCG の shadow)を CSS の box-shadow に */ StyleDictionary.registerTransform({ name: '8341/shadow/css', type: 'value', @@ -89,27 +76,7 @@ StyleDictionary.registerTransform({ filter: (t) => typeOf(t) === 'shadow', transform: (t) => { const v = valueOf(t); - if (typeof v === 'string') return v; - const layers = (Array.isArray(v) ? v : [v]) as { - color: { hex: string; alpha?: number } | string; - offsetX: { value: number; unit: string }; - offsetY: { value: number; unit: string }; - blur: { value: number; unit: string }; - spread: { value: number; unit: string }; - inset?: boolean; - }[]; - const len = (d: { value: number; unit: string }) => `${d.value}${d.unit}`; - const color = (c: { hex: string; alpha?: number } | string) => { - if (typeof c === 'string') return c; - const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(c.hex.slice(i, i + 2), 16)); - return c.alpha === undefined || c.alpha === 1 ? c.hex : `rgb(${r} ${g} ${b} / ${c.alpha})`; - }; - return layers - .map( - (l) => - `${l.inset ? 'inset ' : ''}${len(l.offsetX)} ${len(l.offsetY)} ${len(l.blur)} ${len(l.spread)} ${color(l.color)}`, - ) - .join(', '); + return typeof v === 'string' ? v : shadowToCss(v as Parameters[0]); }, }); @@ -268,13 +235,17 @@ async function build() { // 3. テーマごとの semantic for (const theme of THEMES) { const themed = rel(sourceFiles({ theme }).filter((f) => f.kind === 'modifier')); - const variants = + const variants: { destination: string; selector: string; media?: string }[] = theme === 'dark' ? [ - { destination: '4-dark.css', selector: DARK_SELECTOR }, - { destination: '5-dark-media.css', selector: DARK_MEDIA_SELECTOR, media: DARK_MEDIA }, + { destination: '4-dark.css', selector: THEME_SELECTOR.dark }, + { + destination: '5-dark-media.css', + selector: THEME_SELECTOR.darkMediaSelector, + media: THEME_SELECTOR.darkMedia, + }, ] - : [{ destination: '3-light.css', selector: LIGHT_SELECTOR }]; + : [{ destination: '3-light.css', selector: THEME_SELECTOR.light }]; for (const v of variants) { await new StyleDictionary( config({ @@ -317,7 +288,7 @@ async function build() { theme, resolvedTokens(theme).map(({ name, type, layer, themed, resolved, value, description }) => ({ name, - cssVar: `--${PREFIX}-${name.replace(/\./g, '-')}`, + cssVar: cssVarName(name), tailwind: tailwindName(name.split('.')), type, layer, diff --git a/packages/tokens/scripts/check-contrast.ts b/packages/tokens/scripts/check-contrast.ts index d9b815c2d..4f3e7bbd1 100644 --- a/packages/tokens/scripts/check-contrast.ts +++ b/packages/tokens/scripts/check-contrast.ts @@ -3,7 +3,7 @@ * 1. テーマ(light / dark)で semantic のキーが一致しているか * 2. すべての参照が解決できるか * 3. src/contrast.rules.json の組み合わせが、両テーマで基準を満たすか - * 実行: make check:contrast(--json で機械可読な結果) + * 実行: make check(単体では pnpm --filter 8341-ui-tokens check:contrast。--json で機械可読な結果) */ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; @@ -19,7 +19,7 @@ interface Rule { wcag: string; } -export interface ContrastResult { +interface ContrastResult { theme: string; rule: string; fg: string; diff --git a/packages/tokens/scripts/lib/color.ts b/packages/tokens/scripts/lib/color.ts index e49e7f4b3..4ad026c10 100644 --- a/packages/tokens/scripts/lib/color.ts +++ b/packages/tokens/scripts/lib/color.ts @@ -43,12 +43,7 @@ export function gamutMapOklch(L: number, C: number, h: number) { return { components: [round(L, 4), round(c, 4), c === 0 ? 0 : round(h, 2)] as [number, number, number], hex }; } -/** OKLCH を sRGB の hex に変換する(色域外は彩度を下げる) */ -export function oklchToHex(L: number, C: number, h: number): string { - return gamutMapOklch(L, C, h).hex; -} - -export function parseColor(input: string): { rgb: RGB; alpha: number } { +function parseColor(input: string): { rgb: RGB; alpha: number } { const hex = input.trim().toLowerCase(); const m = hex.match(/^#([0-9a-f]{6})([0-9a-f]{2})?$/); if (m) { @@ -74,7 +69,7 @@ export function parseColor(input: string): { rgb: RGB; alpha: number } { } /** 半透明色を背景に合成した不透明色を返す */ -export function composite(fg: string, bg: string): RGB { +function composite(fg: string, bg: string): RGB { const f = parseColor(fg); const b = parseColor(bg); return f.rgb.map((v, i) => v * f.alpha + (b.rgb[i] ?? 0) * (1 - f.alpha)) as RGB; diff --git a/packages/tokens/scripts/lib/css.ts b/packages/tokens/scripts/lib/css.ts new file mode 100644 index 000000000..f4cf00464 --- /dev/null +++ b/packages/tokens/scripts/lib/css.ts @@ -0,0 +1,50 @@ +/** + * DTCG の値を CSS にする規則と、テーマのセレクタ。 + * tokens.css を作る build.ts(Style Dictionary の transform)と、スライドのデッキ(scripts/slides/lib.ts)が同じ規則を使う。 + */ +import type { DtcgDimension } from './tokens.ts'; + +/** テーマの切り替え。明示が無ければ OS の設定に従う */ +export const THEME_SELECTOR = { + light: ':root, .light, [data-theme="light"]', + dark: '.dark, [data-theme="dark"]', + darkMedia: '(prefers-color-scheme: dark)', + darkMediaSelector: ':root:not(.light):not([data-theme="light"])', +} as const; + +/** px の寸法は rem に(ユーザーの文字サイズ設定に追従させる)。罫線など 3px 以下、9999px、ブレークポイントは px のまま */ +export function dimensionToCss({ value, unit }: DtcgDimension, path: string[]): string { + if (unit === 'rem') return `${value}rem`; + if (value <= 3 || value >= 9999 || path[0] === 'breakpoint') return `${value}px`; + return `${Number((value / 16).toFixed(4))}rem`; +} + +/** 字間(文字サイズに対する比率)を em に */ +export const letterSpacingToCss = (ratio: number) => `${ratio}em`; + +export const durationToCss = ({ value, unit }: { value: number; unit: 'ms' | 's' }) => `${value}${unit}`; + +interface ShadowLayer { + color: { hex: string; alpha?: number } | string; + offsetX: DtcgDimension; + offsetY: DtcgDimension; + blur: DtcgDimension; + spread: DtcgDimension; + inset?: boolean; +} + +/** 影(1 つまたは配列)を box-shadow に。入れ子の値は Style Dictionary の他の transform が届かないため、ここでまとめて変換する */ +export function shadowToCss(v: ShadowLayer | ShadowLayer[]): string { + const len = (d: DtcgDimension) => `${d.value}${d.unit}`; + const color = (c: ShadowLayer['color']) => { + if (typeof c === 'string') return c; + const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(c.hex.slice(i, i + 2), 16)); + return c.alpha === undefined || c.alpha === 1 ? c.hex : `rgb(${r} ${g} ${b} / ${c.alpha})`; + }; + return (Array.isArray(v) ? v : [v]) + .map( + (l) => + `${l.inset ? 'inset ' : ''}${len(l.offsetX)} ${len(l.offsetY)} ${len(l.blur)} ${len(l.spread)} ${color(l.color)}`, + ) + .join(', '); +} diff --git a/packages/tokens/scripts/lib/tokens.ts b/packages/tokens/scripts/lib/tokens.ts index 357caddcd..12c458338 100644 --- a/packages/tokens/scripts/lib/tokens.ts +++ b/packages/tokens/scripts/lib/tokens.ts @@ -8,13 +8,13 @@ * コントラスト検査・DESIGN.md 同期・Figma 書き出しもこのモジュールの解決結果を使う。 */ import { readFileSync } from 'node:fs'; -import { dirname, relative, resolve } from 'node:path'; +import { relative, resolve } from 'node:path'; export const ROOT = resolve(import.meta.dirname, '../..'); export const SRC = resolve(ROOT, 'src'); export const REPO_ROOT = resolve(ROOT, '../..'); export const PREFIX = '8341'; -export const RESOLVER = resolve(SRC, '8341-ui.resolver.json'); +const RESOLVER = resolve(SRC, '8341-ui.resolver.json'); type Json = Record; @@ -29,7 +29,7 @@ export interface DtcgDimension { unit: 'px' | 'rem'; } -export interface FlatToken { +interface FlatToken { path: string[]; /** ドット区切りの名前(例: color.text.default) */ name: string; @@ -55,7 +55,7 @@ interface ResolverDoc { const readJson = (p: string): Json => JSON.parse(readFileSync(p, 'utf8')) as Json; -export const resolverDoc = readJson(RESOLVER) as unknown as ResolverDoc; +const resolverDoc = readJson(RESOLVER) as unknown as ResolverDoc; /** modifier `theme` の context 名(light / dark) */ export const THEMES = Object.keys(resolverDoc.modifiers.theme?.contexts ?? {}); @@ -123,11 +123,11 @@ export function loadTokens(theme: Theme = resolverDoc.modifiers.theme?.default ? } const REF = /^\{([^}]+)\}$/; -export const isRef = (v: unknown): v is string => typeof v === 'string' && REF.test(v); -export const refName = (v: string) => (v.match(REF) as RegExpMatchArray)[1] as string; +const isRef = (v: unknown): v is string => typeof v === 'string' && REF.test(v); +const refName = (v: string) => (v.match(REF) as RegExpMatchArray)[1] as string; /** 参照を最終値まで解決する(循環・未定義はエラー) */ -export function createResolver(all: FlatToken[]) { +function createResolver(all: FlatToken[]) { const map = new Map(all.map((t) => [t.name, t])); const resolveValue = (v: unknown, seen: string[] = []): unknown => { if (isRef(v)) { @@ -155,7 +155,7 @@ export function resolvedTokens(theme: Theme) { export const cssVarName = (name: string) => `--${PREFIX}-${name.replace(/\./g, '-')}`; /** DTCG 色オブジェクトを #rrggbb / rgba() へ(コントラスト計算・Figma 用) */ -export function colorToHex(c: DtcgColor): string { +function colorToHex(c: DtcgColor): string { if (!c.hex) throw new Error(`hex の無い色はこのツールでは扱えません: ${JSON.stringify(c)}`); return c.hex; } @@ -166,10 +166,3 @@ export function colorToCssRgba(c: DtcgColor): string { const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16)); return `rgba(${r}, ${g}, ${b}, ${c.alpha})`; } - -export function dimensionToString(d: DtcgDimension): string { - return `${d.value}${d.unit}`; -} - -export const relFromRepo = (p: string) => relative(REPO_ROOT, p); -export const srcDir = (p: string) => dirname(p); diff --git a/packages/tokens/src/contrast.rules.json b/packages/tokens/src/contrast.rules.json index eef11c5b7..702fce9ff 100644 --- a/packages/tokens/src/contrast.rules.json +++ b/packages/tokens/src/contrast.rules.json @@ -1,5 +1,5 @@ { - "$description": "semantic カラーの組み合わせごとに必要なコントラスト比。両テーマで検証される(pnpm --filter 8341-ui-tokens check:contrast)。新しい semantic カラーを足したら必ずここにも組み合わせを足す。", + "$description": "semantic カラーの組み合わせごとに必要なコントラスト比。両テーマで検証される(make check)。新しい semantic カラーを足したら必ずここにも組み合わせを足す。", "rules": [ { "id": "text-on-surfaces", diff --git a/packages/tokens/src/palette.config.json b/packages/tokens/src/palette.config.json index d5631e509..c50c8c2bb 100644 --- a/packages/tokens/src/palette.config.json +++ b/packages/tokens/src/palette.config.json @@ -1,5 +1,5 @@ { - "$description": "プリミティブカラーの生成パラメータ。色を変えたいときはここを編集し `pnpm --filter 8341-ui-tokens palette` を実行する。全色相で同じ明度カーブを共有するため、同じ段階番号なら色相が違ってもコントラストがほぼ揃う。", + "$description": "プリミティブカラーの生成パラメータ。色を変えたいときはここを編集し `make palette` を実行する。全色相で同じ明度カーブを共有するため、同じ段階番号なら色相が違ってもコントラストがほぼ揃う。", "steps": [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200], "lightness": [0.975, 0.945, 0.89, 0.82, 0.74, 0.65, 0.57, 0.5, 0.44, 0.38, 0.32, 0.27, 0.22], "chroma": [0.02, 0.04, 0.07, 0.1, 0.13, 0.15, 0.155, 0.15, 0.135, 0.12, 0.1, 0.085, 0.07], diff --git a/scripts/factory/create.mjs b/scripts/factory/create.mjs index 3252de27d..79e022473 100644 --- a/scripts/factory/create.mjs +++ b/scripts/factory/create.mjs @@ -27,6 +27,7 @@ import { readFileSync, readdirSync, readlinkSync, + rmSync, symlinkSync, writeFileSync, } from 'node:fs'; @@ -122,10 +123,12 @@ mkdirSync(out, { recursive: true }); for (const file of files) { const src = join(TEMPLATE, file); // 作業ツリーで消したファイルは持ち込まない - if (!lstatSync(src, { throwIfNoEntry: false })) continue; + const stat = lstatSync(src, { throwIfNoEntry: false }); + if (!stat) continue; const dest = join(out, transform(file)); mkdirSync(dirname(dest), { recursive: true }); - if (lstatSync(src).isSymbolicLink()) { + if (stat.isSymbolicLink()) { + rmSync(dest, { force: true }); // --force で上書きするとき、既にあるリンクで EEXIST にしない symlinkSync(readlinkSync(src), dest); continue; } @@ -144,12 +147,6 @@ palette.families[family] = { palette.families.gray = { ...palette.families.gray, hue, label: '中立色(わずかにキーカラーへ寄せる)' }; writeFileSync(palettePath, `${JSON.stringify(palette, null, 2)}\n`); -// ファクトリーを呼ぶスクリプトは生成先では使えないので消す -const rootPkgPath = join(out, 'package.json'); -const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf8')); -delete rootPkg.scripts['factory:create']; -writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`); - // 改善ログを空にする const today = new Date().toISOString().slice(0, 10); writeFileSync( diff --git a/scripts/slides/lib.ts b/scripts/slides/lib.ts index e4067d9ed..0f08bcff8 100644 --- a/scripts/slides/lib.ts +++ b/scripts/slides/lib.ts @@ -8,6 +8,13 @@ */ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; +import { + THEME_SELECTOR, + dimensionToCss, + durationToCss, + letterSpacingToCss, + shadowToCss, +} from '../../packages/tokens/scripts/lib/css.ts'; import { PREFIX, THEMES, @@ -24,7 +31,7 @@ export const REPO_ROOT = resolve(SLIDES_DIR, '../..'); // ルールの定数(docs/rulebook/07-slides.md と合わせる) // --------------------------------------------------------------------------- -export const LAYOUTS = [ +const LAYOUTS = [ 'title', 'section', 'statement', @@ -36,10 +43,9 @@ export const LAYOUTS = [ 'table', 'summary', ] as const; -export type Layout = (typeof LAYOUTS)[number]; /** ソースで使えるクラス(S9)。増やすときは deck.css と 07-slides.md も直す */ -export const ALLOWED_CLASSES = new Set([ +const ALLOWED_CLASSES = new Set([ 'slide', 'subtitle', 'meta', @@ -72,7 +78,7 @@ export const ALLOWED_CLASSES = new Set([ 'visually-hidden', ]); -export const LIMITS = { +const LIMITS = { listItems: 5, listDepth: 2, slideChars: 150, @@ -92,51 +98,24 @@ const FORBIDDEN_TAGS = new Set(['style', 'script', 'link', 'font', 'center', 'ma // トークン → CSS 変数 // --------------------------------------------------------------------------- -const LIGHT_SELECTOR = ':root, .light, [data-theme="light"]'; -const DARK_SELECTOR = '.dark, [data-theme="dark"]'; -const DARK_MEDIA_SELECTOR = ':root:not(.light):not([data-theme="light"])'; - -interface ShadowLayer { - color: DtcgColor; - offsetX: DtcgDimension; - offsetY: DtcgDimension; - blur: DtcgDimension; - spread: DtcgDimension; - inset?: boolean; -} - -/** build.ts(Style Dictionary)と同じ規則で値を CSS にする: 寸法は rem(3px 以下と 9999px は px)、字間は em */ -function cssValue(name: string, type: string, v: unknown): string { +/** build.ts(tokens.css)と同じ規則(packages/tokens/scripts/lib/css.ts)で値を CSS にする */ +function cssValue(name: string, type: string, path: string[], v: unknown): string { if (typeof v === 'string') return v; switch (type) { case 'color': return colorToCssRgba(v as DtcgColor); - case 'dimension': { - const { value, unit } = v as DtcgDimension; - if (unit === 'rem') return `${value}rem`; - if (value <= 3 || value >= 9999) return `${value}px`; - return `${Number((value / 16).toFixed(4))}rem`; - } + case 'dimension': + return dimensionToCss(v as DtcgDimension, path); case 'fontFamily': return (Array.isArray(v) ? v : [v]).map((f) => (/[\s\d]/.test(String(f)) ? `"${f}"` : String(f))).join(', '); - case 'duration': { - const d = v as { value: number; unit: string }; - return `${d.value}${d.unit}`; - } + case 'duration': + return durationToCss(v as Parameters[0]); case 'cubicBezier': return `cubic-bezier(${(v as number[]).join(', ')})`; - case 'shadow': { - const layers = (Array.isArray(v) ? v : [v]) as ShadowLayer[]; - const len = (d: DtcgDimension) => `${d.value}${d.unit}`; - return layers - .map( - (l) => - `${l.inset ? 'inset ' : ''}${len(l.offsetX)} ${len(l.offsetY)} ${len(l.blur)} ${len(l.spread)} ${colorToCssRgba(l.color)}`, - ) - .join(', '); - } + case 'shadow': + return shadowToCss(v as Parameters[0]); default: - if (typeof v === 'number' && name.endsWith('.letter-spacing')) return `${v}em`; + if (typeof v === 'number' && name.endsWith('.letter-spacing')) return letterSpacingToCss(v); return String(v); } } @@ -158,7 +137,7 @@ export function tokenCss(): { css: string; names: Set } { themed[theme] = []; for (const t of resolvedTokens(theme)) { if (t.layer !== 'semantic') continue; - const line = `${cssVarName(t.name)}: ${cssValue(t.name, t.type, t.resolved)};`; + const line = `${cssVarName(t.name)}: ${cssValue(t.name, t.type, t.path, t.resolved)};`; names.add(cssVarName(t.name)); if (t.themed) themed[theme]?.push(line); else if (theme === THEMES[0]) base.push(line); @@ -166,13 +145,13 @@ export function tokenCss(): { css: string; names: Set } { } const light = themed.light ?? []; const dark = themed.dark ?? []; - const media = block(DARK_MEDIA_SELECTOR, dark, 'dark').replace(/^/gm, ' ').trimEnd(); + const media = block(THEME_SELECTOR.darkMediaSelector, dark, 'dark').replace(/^/gm, ' ').trimEnd(); const css = [ `/* ${PREFIX}-ui のトークン(semantic のみ。scripts/slides/lib.ts が packages/tokens/src から生成) */`, block(':root', base), - block(LIGHT_SELECTOR, light, 'light'), - block(DARK_SELECTOR, dark, 'dark'), - `@media (prefers-color-scheme: dark) {\n${media}\n}\n`, + block(THEME_SELECTOR.light, light, 'light'), + block(THEME_SELECTOR.dark, dark, 'dark'), + `@media ${THEME_SELECTOR.darkMedia} {\n${media}\n}\n`, ].join('\n'); return { css, names }; } @@ -189,11 +168,11 @@ export interface El { line: number; parent?: El; } -export interface Text { +interface Text { type: 'text'; value: string; } -export interface Comment { +interface Comment { type: 'comment'; value: string; } @@ -216,7 +195,7 @@ const VOID = new Set([ ]); const RAW_TEXT = new Set(['script', 'style']); -export class ParseError extends Error { +class ParseError extends Error { line: number; constructor(message: string, line: number) { super(message); @@ -293,7 +272,7 @@ export function parseHtml(html: string): El { } export const attr = (el: El, name: string): string | null | undefined => el.attrs.find(([k]) => k === name)?.[1]; -export const hasAttr = (el: El, name: string) => el.attrs.some(([k]) => k === name); +const hasAttr = (el: El, name: string) => el.attrs.some(([k]) => k === name); export function setAttr(el: El, name: string, value: string | null) { const found = el.attrs.find(([k]) => k === name); if (found) found[1] = value; @@ -303,7 +282,7 @@ export const classes = (el: El) => (attr(el, 'class') ?? '').split(/\s+/).filter export const hasClass = (el: El, c: string) => classes(el).includes(c); export const elements = (n: El) => n.children.filter((c): c is El => c.type === 'el'); -export function walk(n: El, fn: (el: El) => void) { +function walk(n: El, fn: (el: El) => void) { for (const c of elements(n)) { fn(c); walk(c, fn); @@ -319,7 +298,7 @@ export function find(n: El, pred: (el: El) => boolean): El[] { export const findTag = (n: El, tag: string) => find(n, (el) => el.tag === tag); const ENTITIES: Record = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }; -export const decode = (s: string) => +const decode = (s: string) => s.replace(/&(#x[\da-f]+|#\d+|\w+);/gi, (all, e: string) => { if (e[0] === '#') return String.fromCodePoint(e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1))); @@ -337,7 +316,7 @@ export function textOf(n: Node, skip: (el: El) => boolean = () => false): string } export const compact = (s: string) => s.replace(/\s+/g, ' ').trim(); /** 空白を除いた字数(和文の分量の目安) */ -export const charCount = (s: string) => s.replace(/\s+/g, '').length; +const charCount = (s: string) => s.replace(/\s+/g, '').length; export function serialize(n: Node): string { if (n.type === 'text') return n.value; @@ -353,7 +332,7 @@ export function serialize(n: Node): string { // デッキの読み込み // --------------------------------------------------------------------------- -export interface Deck { +interface Deck { root: El; html?: El; body?: El;