From bfa8f26570c039572c4707be0859cb04c8f27d62 Mon Sep 17 00:00:00 2001 From: tkc310 Date: Fri, 23 Oct 2026 11:25:36 +0000 Subject: [PATCH 1/4] =?UTF-8?q?docs(design-principles):=20=E4=B8=8D?= =?UTF-8?q?=E8=A6=81=E3=81=AA=E3=82=BB=E3=82=AB=E3=83=B3=E3=83=80=E3=83=AA?= =?UTF-8?q?=E3=83=A9=E3=83=99=E3=83=AB=E3=82=84=E8=AA=AC=E6=98=8E=E3=82=92?= =?UTF-8?q?=E9=81=BF=E3=81=91=E3=80=81=E3=83=AC=E3=82=A4=E3=82=A2=E3=82=A6?= =?UTF-8?q?=E3=83=88=E3=81=AB=E8=AA=AC=E6=98=8E=E3=81=95=E3=81=9B=E3=82=8B?= =?UTF-8?q?=E3=83=AB=E3=83=BC=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原則「削る」に、部品を組み合わせたページで適用するルールを追加した。 - 必須: 見出し・配置・まとまりで伝わることを、小見出し・補助ラベル・説明文で繰り返さない - 必須: 項目どうしの関係(グループ・主従)はレイアウトで示し、説明文で補わない - 必須: 削るのは重複した表示だけ。入力欄の見えるラベル(WCAG 3.3.2)と構造(1.3.1)、アイコンだけの操作の名前は残す - 推奨: 補足文が要りそうなら、先に配置・見出し・ラベルの語を直す - 出典カタログ(ゲシュタルトの近接・共通地域)、ルールブック 01・05、DESIGN.md の Don't に反映 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb44feYKDNj2ukQeT2j8Ks --- DESIGN.md | 1 + docs/design-principles/principles.md | 20 +++++++++++++++----- docs/improvement/log.md | 1 + docs/rulebook/01-principles.md | 2 +- docs/rulebook/05-writing.md | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 97a3ec7b..8b9a81f0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -377,6 +377,7 @@ DTCG では `{"colorSpace": "oklch", "components": [L, C, H], "hex": "#..."}` - 14px 未満の文字、純黒の本文、中間ウェイト(500 / 600) - hover で太さ・大きさを変える - プレースホルダーをラベル代わりにする +- 見出しや配置で分かることを、補助ラベルや説明文で繰り返す(ラベルの言い換えだけの `supportText`、見出しを言い直す導入文)。関係はレイアウトで示す - アイコンだけのボタン(閉じるボタンなど慣習的なもの以外)。使う場合は `aria-label` - `outline-none` でフォーカス表示を消す(代わりに `focus-ring`) - `div` に `role="button"` を付けて作り直す diff --git a/docs/design-principles/principles.md b/docs/design-principles/principles.md index 425e49f0..1250230a 100644 --- a/docs/design-principles/principles.md +++ b/docs/design-principles/principles.md @@ -25,7 +25,7 @@ | 誤りと撤回 | 失敗を防ぐか。失敗の内容と戻り方を示すか。操作を中断できるか | | 再認 | 思い出させず、見せて選ばせるか | | 文言 | ラベルと文が、操作の結果と入力の意味を言うか | -| 削る | その画面の主タスクに不要な要素を置かないか | +| 削る | その画面の主タスクに不要な要素を置かないか。レイアウトで伝わることを文字で繰り返さないか | | 助けの場所 | ヘルプと検索を、サイト内の同じ場所に置くか | | 短い経路 | 繰り返す操作に、より短い別経路があるか | | 自律的な処理の範囲 | 人の操作を待たずに進む処理の境界が分かるか | @@ -321,19 +321,29 @@ ### 削る -その画面の主タスクに不要な要素を置かないかを決める。選択肢の個数は「一度に出す数」、残した要素の文言は「文言」である。 +その画面の主タスクに不要な要素を置かないかを決める。部品を組み合わせたページでは、配置とまとまりで伝わることを、ラベルや説明で繰り返さないかも決める。選択肢の個数は「一度に出す数」、グループの見せ方は「まとまり」、残した要素の文言は「文言」である。 **必須** - その画面の主タスク(利用者がその画面で終えたい作業)に使わない装飾、説明、動く要素を置かない。 +- 見出し、配置、まとまりで既に伝わることを、セカンダリラベル(小見出し、見出しの上の補助ラベル、項目名の前置き)や説明文で繰り返さない。例: 入力欄のラベルを言い換えただけの補足文、ページの見出しを言い直すだけの導入文、カードの中の「詳細」「情報」だけの小見出し、表の列見出しと同じ語を各セルに付けた前置き。 +- 項目どうしの関係(どれが同じグループか、どれが主でどれが従か)は、レイアウト(近さ、並び、枠や背景、大きさの差)で示す。関係を説明する文を足して補わない。 +- 削ってよいのは重複している表示だけである。入力欄の見えるラベル(WCAG 3.3.2)、見出しや表の見出しセルなど構造を表す要素(1.3.1)、アイコンだけの操作の名前は残す。見た目で示したグループには、読み上げでも取り出せる名前(`fieldset` と `legend`、区画の見出しなど)を付ける。 - 主タスクを遮るポップアップは、利用者の判断が要る場合(未保存の変更の破棄など)に限る。それ以外の通知は、主タスクを遮らない位置に出す。 - 自動で再生・移動する要素は、主タスクの妨げになる場合は初期状態で動かさない。 -**出典**: ニールセン「最小限で美しいデザイン」、情報提示の原則「注意を逸らさないようにする」「簡潔にする」、対話の原則「タスクへの適合性」 +**推奨** + +- 補足文が要りそうに見えたら、先に配置、見出しの段、ラベルの語を直して、補足なしで伝わらないか確かめる。補足は、形式や制約のように見た目から分からないことに使う。 + +**出典**: ニールセン「最小限で美しいデザイン」、情報提示の原則「注意を逸らさないようにする」「簡潔にする」、対話の原則「タスクへの適合性」、ゲシュタルトの法則(近接、共通地域) **レビュー** - [ ] 各要素が、その画面の主タスクに必要か +- [ ] 見出しや配置で分かることを、小見出し、補助ラベル、説明文で繰り返していないか +- [ ] グループや主従の関係を、文ではなくレイアウトで示しているか +- [ ] 削った結果、入力欄の見えるラベルやグループの名前まで消えていないか - [ ] 動く要素や通知が、主タスクから注意をずらしていないか - [ ] スクロールしないと見えない場所に、主タスクの完了手段が隠れていないか @@ -479,9 +489,9 @@ Jakob Nielsen がインタラクティブシステムの評価用に整理した | 法則 | 内容 | 吸収先 | | --- | --- | --- | -| 近接 | 近い要素は一つのグループに見える | まとまり | +| 近接 | 近い要素は一つのグループに見える | まとまり、削る(関係をレイアウトで伝える) | | 類同 | 色、形、サイズが似た要素は一つのグループに見える | まとまり | -| 共通地域 | 枠や背景で囲まれた要素は一つのグループに見える | まとまり | +| 共通地域 | 枠や背景で囲まれた要素は一つのグループに見える | まとまり、削る(関係をレイアウトで伝える) | | 連続 | 線や並びに沿った要素は一つの連なりに見える | まとまり | ### 視線のパターン diff --git a/docs/improvement/log.md b/docs/improvement/log.md index e34fbae5..99fcdb3b 100644 --- a/docs/improvement/log.md +++ b/docs/improvement/log.md @@ -4,6 +4,7 @@ | 日付 | 種別 | 内容 | 再発防止 | | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| 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` に追加 | | 2026-10-20 | 機能 | 発表用のスライドを作るスキル `ds-slides` を追加。HTML のデッキを semantic トークンだけで組み(`scripts/slides/`)、design-principles の原則をスライドに落としたルールを 07 章にまとめた | ADR-0017。`make slides-check`(ソースの S1〜S20 の検査と deck.css の監査)を `make check` に組み込み、見本 `scripts/slides/examples/sample.html` を毎回検査 | | 2026-10-20 | 不具合 | Checkbox のチェックのアイコンが枠の中心から下にずれていた。Indicator(span)が本文の行の高さを引き継いだ行ボックスになり、`align-middle` の SVG がベースライン基準で置かれていたため。Indicator を flex にして行ボックスを作らない | 単体テストでアイコンが flex の中心に置かれていることを検査 | diff --git a/docs/rulebook/01-principles.md b/docs/rulebook/01-principles.md index 625641c7..bb94d428 100644 --- a/docs/rulebook/01-principles.md +++ b/docs/rulebook/01-principles.md @@ -32,7 +32,7 @@ UI の方向性とサービス種別は、8341-ui を使う各プロダクトの | 誤りと撤回 | FormField(Base UI の Field)の補足とエラー、Base UI の Form による送信時の検証とフォーカス移動、`validationMode="onBlur"`、送信ボタンを無効化しない規約、danger ボタン | 確認ダイアログ(Base UI の AlertDialog)、エラー一覧(バックログ)。入力用の Dialog は提供済み | | 再認 | Select、RadioGroup、アイコン単体利用時の `label` 必須、Truncate(省略した全文をホバーとフォーカスで出す) | コンボボックス・汎用のツールチップ(Base UI にあり、バックログ) | | 文言 | [05 文言](./05-writing.md)、`label` / `legend` を必須 props にする契約 | — | -| 削る | 影は 3 段階・ボタンに使わない、`prefers-reduced-motion` で動きを止める | 画面ごとの要素の取捨 | +| 削る | 影は 3 段階・ボタンに使わない、`prefers-reduced-motion` で動きを止める。FormField の `supportText` はラベルの言い換えに使わない(05 文言)。グループは Stack・Card・RadioGroup の `legend` で見せ、関係を文で説明しない | 画面ごとの要素の取捨、ページで重複したラベルや説明を削ること | | 助けの場所 | — | ヘッダーの配置(プロダクト側) | | 短い経路 | ネイティブ要素でキーボード操作を保証 | ショートカット、一括操作(プロダクト側) | | 自律的な処理の範囲 | — | 該当する処理があるプロダクト側で決める | diff --git a/docs/rulebook/05-writing.md b/docs/rulebook/05-writing.md index 9abcf2ca..33739c95 100644 --- a/docs/rulebook/05-writing.md +++ b/docs/rulebook/05-writing.md @@ -28,7 +28,7 @@ design-principles の原則「文言」「誤りと撤回」「一貫性」の | 要素 | 書き方 | 例 | | ------------------- | -------------------------------------------- | ----------------------------- | | ラベル | 名詞で短く | 氏名、メールアドレス | -| 補足(supportText) | 入力形式・例・制約 | 半角数字 7 桁(ハイフンなし) | +| 補足(supportText) | 入力形式・例・制約。ラベルの言い換えは書かない(無ければ付けない) | 半角数字 7 桁(ハイフンなし) | | プレースホルダー | 入力例のみ。ラベルや説明の代わりにしない | 例: 1000001 | | 必須 | 「必須」バッジ。任意が少数なら「任意」を表示 | — | From 284ca4bf0e21d87e4accbb1ab8f12b4bf56feaae Mon Sep 17 00:00:00 2001 From: tkc310 Date: Fri, 23 Oct 2026 11:31:14 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat(registry):=208341-ui=20=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E3=81=86=E3=83=97=E3=83=AD=E3=83=80=E3=82=AF=E3=83=88?= =?UTF-8?q?=E3=81=AB=20DESIGN.md=20=E3=81=A8=20design-principles=20?= =?UTF-8?q?=E3=82=92=E9=85=8D=E3=82=8B=20@8341/design-md=20=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - レジストリ item `design-md`: リポジトリ直下に DESIGN.md、docs/design-principles/ に原則の全文を置く - プロダクト用 DESIGN.md(templates/PRODUCT-DESIGN.md)を、コードから読み取れない考え方だけの構成に書き直した(プロダクトの前提・画面領域ごとの方向性・大事にすること・外した必須)。トークンや部品の一覧は載せない - 原則の要約(判断の順序・ルールの強さ・方向性と各原則の必須)は scripts/sync-product-design.ts が docs/design-principles から生成し、make check で鮮度を検査 - build-registry-css.ts を build-registry-assets.ts に改名し、DESIGN.md と原則(ファクトリーの部分を除く)も registry-assets/ に作る - README のクイックスタート、ds-build-ui、DESIGN.md、ルールブック 01、design-principles/README を更新。ADR-0019、changeset、改善ログ Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb44feYKDNj2ukQeT2j8Ks --- .changeset/product-design-md.md | 5 + .claude/skills/ds-build-ui/SKILL.md | 4 +- DESIGN.md | 2 +- Makefile | 3 +- README.md | 8 + docs/decisions/0019-product-design-md.md | 24 ++ docs/decisions/README.md | 1 + docs/design-principles/README.md | 8 +- docs/improvement/log.md | 1 + docs/rulebook/01-principles.md | 2 +- package.json | 2 +- packages/ui/package.json | 2 +- packages/ui/registry.json | 88 +++++++ ...gistry-css.ts => build-registry-assets.ts} | 24 +- packages/ui/scripts/generate-registry.ts | 26 +- scripts/sync-product-design.ts | 113 +++++++++ templates/PRODUCT-DESIGN.md | 233 +++++++++++++++--- 17 files changed, 501 insertions(+), 45 deletions(-) create mode 100644 .changeset/product-design-md.md create mode 100644 docs/decisions/0019-product-design-md.md rename packages/ui/scripts/{build-registry-css.ts => build-registry-assets.ts} (57%) create mode 100644 scripts/sync-product-design.ts diff --git a/.changeset/product-design-md.md b/.changeset/product-design-md.md new file mode 100644 index 00000000..bf6b514c --- /dev/null +++ b/.changeset/product-design-md.md @@ -0,0 +1,5 @@ +--- +'8341-ui': patch +--- + +レジストリに `@8341/design-md` を追加しました。`npx shadcn@latest add @8341/design-md` で、リポジトリ直下にプロダクト用の DESIGN.md、`docs/design-principles/` にデザイン原則の全文が置かれます。DESIGN.md には、コードから読み取れない設計の考え方(プロダクトの前提、画面領域ごとの UI の方向性とサービス種別、大事にすること、外した必須)と、原則の必須の要約だけを書きます。 diff --git a/.claude/skills/ds-build-ui/SKILL.md b/.claude/skills/ds-build-ui/SKILL.md index c17e30b0..cc3364b3 100644 --- a/.claude/skills/ds-build-ui/SKILL.md +++ b/.claude/skills/ds-build-ui/SKILL.md @@ -9,8 +9,8 @@ description: このデザインシステムを使って画面やフォームを 文書は大きいので、**全文ではなく必要な節だけ** を読む(見出しで探して、その節を読む)。 -0. プロダクトの DESIGN.md。無ければ `templates/PRODUCT-DESIGN.md` から作り、UI の方向性とサービス種別を決める。`design-principles` スキルの手順(ヒアリング → 方向性 → 特化ルール → 原則 → WCAG)に従う -1. `DESIGN.md` は次の節だけ: 「9. Agent Prompt Guide」のクイックリファレンス、「7. Do's and Don'ts」、「4. Component Stylings」の一覧と、使うコンポーネントの節。色や余白を決めるときだけ「2. Color Palette & Roles」「5. Layout Principles」 +0. プロダクトの DESIGN.md の「プロダクトの前提」〜「採用したルールと外したもの」。無ければ `npx shadcn@latest add @8341/design-md` で置き(このリポジトリの中なら雛形は `templates/PRODUCT-DESIGN.md`)、( )のままの欄を `docs/design-principles/hearing.md` の項目で聞いて埋める +1. 8341-ui の `DESIGN.md`(このリポジトリ直下)は次の節だけ: 「9. Agent Prompt Guide」のクイックリファレンス、「7. Do's and Don'ts」、「4. Component Stylings」の一覧と、使うコンポーネントの節。色や余白を決めるときだけ「2. Color Palette & Roles」「5. Layout Principles」 2. `docs/rulebook/02-accessibility.md` の「必須ルール」の表(A1〜A16) 3. フォームやボタンの文言を書くときだけ `docs/rulebook/05-writing.md` 4. 使うコンポーネントのストーリー(`packages/ui/src/components/ui//index.stories.tsx`) diff --git a/DESIGN.md b/DESIGN.md index 8b9a81f0..6d40c4fc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -19,7 +19,7 @@ - **基盤のデザインシステム**: 特定のプロダクトに属さず、複数のプロダクトで使い回す - **画面設計のルール**: [design-principles](./docs/design-principles/README.md) を採用する。適用順は UI の方向性 → サービス別の特化ルール → 原則 → WCAG 2.2 -- **UI の方向性とサービス種別**: 基盤では決めない。各プロダクトの DESIGN.md で決める(雛形: `templates/PRODUCT-DESIGN.md`) +- **UI の方向性とサービス種別**: 基盤では決めない。各プロダクトの DESIGN.md で決める(`npx shadcn@latest add @8341/design-md` で置く。雛形は `templates/PRODUCT-DESIGN.md`) - **原則とトークン・コンポーネントの対応**: `docs/rulebook/01-principles.md` - **必須ルールを外した項目**: なし。タップ領域 44px、文字のコントラストを一律 4.5:1、リンクの下線必須、二重のフォーカス線は、design-principles の必須より厳しくしている diff --git a/Makefile b/Makefile index 442b1d01..5119986d 100644 --- a/Makefile +++ b/Makefile @@ -74,8 +74,9 @@ tokens: ## トークンの CSS・Tailwind テーマ・Figma JSON を生成する palette: ## palette.config.json から primitive カラーを生成し直す pnpm --filter $(TOKENS) palette -design-md: ## DESIGN.md の表をトークンから更新する +design-md: ## DESIGN.md の表をトークンから、プロダクト用の雛形の原則を docs/design-principles から更新する pnpm --filter $(TOKENS) design-md + node scripts/sync-product-design.ts tw-merge: ## クラス統合の設定(tw-merge-config.ts)を更新する pnpm --filter $(TOKENS) tw-merge diff --git a/README.md b/README.md index 16f7cf19..b380f9d8 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,14 @@ export function SignUp() { } ``` +**4. DESIGN.md を置く** — 画面を作る人とエージェントが、コードから読み取れない設計の考え方(プロダクトの前提と、[デザイン原則](./docs/design-principles/README.md)の必須)を共有するためのファイルです。 + +```bash +npx shadcn@latest add @8341/design-md # リポジトリ直下に DESIGN.md、docs/design-principles/ に原則の全文 +``` + +DESIGN.md の「プロダクトの前提」「画面領域ごとの方向性」を埋めてから画面を作ります(エージェントに頼むと、足りない項目を聞きながら埋めます)。トークンや部品の一覧は書きません。それはコードが正です。 +
npm パッケージとして使う(公開した場合) diff --git a/docs/decisions/0019-product-design-md.md b/docs/decisions/0019-product-design-md.md new file mode 100644 index 00000000..29006f63 --- /dev/null +++ b/docs/decisions/0019-product-design-md.md @@ -0,0 +1,24 @@ +# 0019 8341-ui を使うプロダクトに DESIGN.md と design-principles をレジストリで配る + +- 日付: 2026-10-23 +- 状態: 採用 + +## 背景 +UI の方向性とサービス種別は各プロダクトの DESIGN.md で決める(ADR-0007)。しかし 8341-ui の部品を shadcn で取り込んだプロダクトには、原則もプロダクト用 DESIGN.md も届かなかった。雛形 `templates/PRODUCT-DESIGN.md` を手でコピーし、原則は GitHub のリンクで読む前提で、プロダクト側のエージェントは原則を参照できなかった。ファクトリーで作ったデザインシステムには原則が入るのに、部品を使うだけのプロダクトには入らない差があった。 + +## 決定 +- shadcn のレジストリに `@8341/design-md`(`registry:file`)を足す。`npx shadcn@latest add @8341/design-md` で、リポジトリ直下に `DESIGN.md`、`docs/design-principles/` に原則の全文が置かれる(`target` の `~` はプロジェクトのルート) +- プロダクト用 DESIGN.md には **コードから読み取れない設計の考え方だけ** を書く。プロダクトの前提、画面領域ごとの UI の方向性とサービス種別、このプロダクトで大事にすること、外した必須とその理由、8341-ui に足したものの理由。トークンの値や部品の一覧は書かない(コードが正) +- DESIGN.md の「デザイン原則」は `` にし、`scripts/sync-product-design.ts` が `docs/design-principles/` から判断の順序・ルールの強さ・UI の方向性と各原則の「必須」を生成する。`make check` で鮮度を検査する +- 配るファイルは `packages/ui/scripts/build-registry-assets.ts`(旧 `build-registry-css.ts`)が `registry-assets/` に作る。原則の文書からは `` 〜 `` を取り除く(利用側はファクトリーではない) +- 配る原則のファイル一覧は `generate-registry.ts` が `docs/design-principles/` から作り、`registry.json` の `--check` で追従させる + +## 理由 +- 部品の取り込みと同じ shadcn の仕組みに乗せれば、プロダクト側の手順が 1 行増えるだけで済み、版を固定したレジストリ(`r/v<版>/`)で原則の版もそろう +- トークンや部品の仕様を DESIGN.md に写すと、コードと二重管理になり古くなる。コードから読み取れない判断の軸だけを置けば、DESIGN.md は短く保て、エージェントが毎回読んでも負担が小さい(ADR-0018) +- 原則の必須の要約を生成にすれば、原則を直したときにプロダクト用の雛形が古くならない + +## 影響・トレードオフ +- 原則の更新を取り込むと DESIGN.md が上書きになる。手順(作業用のブランチで取り込み、`auto` の範囲と `docs/design-principles/` だけを採用する)を DESIGN.md の末尾に書いた +- npm パッケージで使う場合は、この仕組みは使えない(shadcn のレジストリだけ)。必要になったら別の配り方を検討する +- 原則の必須は要約で、推奨・レビュー項目・理由は `docs/design-principles/` の全文を読む diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 75ba1497..f9744a6f 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -22,6 +22,7 @@ | [0016](./0016-component-structure.md) | コンポーネントのディレクトリ構造を PascalCase とコロケーションにする | 採用 | | [0017](./0017-slides.md) | スライドを HTML のデッキとしてトークンから組む(ds-slides) | 採用 | | [0018](./0018-progressive-disclosure.md) | エージェント向けの文書を必要なときだけ読み込む構成にする | 採用 | +| [0019](./0019-product-design-md.md) | 8341-ui を使うプロダクトに DESIGN.md と design-principles をレジストリで配る | 採用 | ## テンプレート diff --git a/docs/design-principles/README.md b/docs/design-principles/README.md index cf38a4e3..90e64ea5 100644 --- a/docs/design-principles/README.md +++ b/docs/design-principles/README.md @@ -97,7 +97,7 @@ WCAG 2.2 の Level A と AA の達成基準のうち、配色、文字、レイ #### スキルとして使う(Claude Code・Cursor・Codex) -このディレクトリのルールは、このリポジトリのスキルとして使える。スキルの置き場所とエージェントごとの読み込み方は [`docs/ai-agents.md`](../ai-agents.md) に書いている。 +8341-ui のリポジトリでは、このディレクトリのルールをスキルとして使える。スキルの置き場所とエージェントごとの読み込み方は [8341-ui の docs/ai-agents.md](https://github.com/tkc310/8341-ui/blob/main/docs/ai-agents.md) に書いている。 **スキル**: `design-principles`(`.claude/skills/design-principles/SKILL.md`) - デザイン作業時に読み込まれる手順とルーター @@ -114,7 +114,11 @@ WCAG 2.2 の Level A と AA の達成基準のうち、配色、文字、レイ **新しいデザインシステムを丸ごと作る**: `ds-factory` スキル(デザインシステムファクトリー)が、このルールとヒアリングの結果から、8341-ui をひな形に別リポジトリのデザインシステムを生成する([`docs/factory.md`](../factory.md))。 -**ルールへのフィードバック**: 対話中にルールの改善点(ギャップ、曖昧さ、適合しないルール、繰り返される例外)が見つかった場合、エージェントがユーザーに伝え、このリポジトリの `docs/design-principles/` への編集を提案する。プロダクト固有の例外は、そのプロダクトの DESIGN.md に記録する。 +#### 8341-ui を使うプロダクトで使う + +`npx shadcn@latest add @8341/design-md` を実行すると、このディレクトリがプロダクトの `docs/design-principles/` に、各ルールの必須をまとめた DESIGN.md がリポジトリ直下に置かれる。プロダクトの前提(UI の方向性、サービス種別、外した必須)は、その DESIGN.md に書く。配られたこのディレクトリは直接編集しない。 + +**ルールへのフィードバック**: 対話中にルールの改善点(ギャップ、曖昧さ、適合しないルール、繰り返される例外)が見つかった場合、エージェントがユーザーに伝え、[8341-ui](https://github.com/tkc310/8341-ui) の `docs/design-principles/` への編集を提案する。プロダクト固有の例外は、そのプロダクトの DESIGN.md に記録する。 ## 注意事項 diff --git a/docs/improvement/log.md b/docs/improvement/log.md index 99fcdb3b..0c42788d 100644 --- a/docs/improvement/log.md +++ b/docs/improvement/log.md @@ -4,6 +4,7 @@ | 日付 | 種別 | 内容 | 再発防止 | | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| 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` に追加 | | 2026-10-20 | 機能 | 発表用のスライドを作るスキル `ds-slides` を追加。HTML のデッキを semantic トークンだけで組み(`scripts/slides/`)、design-principles の原則をスライドに落としたルールを 07 章にまとめた | ADR-0017。`make slides-check`(ソースの S1〜S20 の検査と deck.css の監査)を `make check` に組み込み、見本 `scripts/slides/examples/sample.html` を毎回検査 | diff --git a/docs/rulebook/01-principles.md b/docs/rulebook/01-principles.md index bb94d428..64b73a50 100644 --- a/docs/rulebook/01-principles.md +++ b/docs/rulebook/01-principles.md @@ -14,7 +14,7 @@ design-principles のルールは、次の順で適用する。後のルール 4. **WCAG 2.2 の達成基準**(必須はどの段階からも緩めない) 8341-ui は複数のプロダクトで共有する基盤なので、1 と 2 は決めない。 -UI の方向性とサービス種別は、8341-ui を使う各プロダクトの DESIGN.md で決める(雛形: [`templates/PRODUCT-DESIGN.md`](../../templates/PRODUCT-DESIGN.md))。 +UI の方向性とサービス種別は、8341-ui を使う各プロダクトの DESIGN.md で決める。プロダクトには `npx shadcn@latest add @8341/design-md` で、DESIGN.md(雛形: [`templates/PRODUCT-DESIGN.md`](../../templates/PRODUCT-DESIGN.md)。コードから読み取れない考え方と原則の必須)と `docs/design-principles/` を配る(ADR-0019)。 8341-ui が受け持つのは 3 と 4 であり、どのサービス種別でも組み立てられる部品とトークンを用意する。 ## ウェブデザインの原則との対応 diff --git a/package.json b/package.json index 35f38a89..65f6c26a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "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", + "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", diff --git a/packages/ui/package.json b/packages/ui/package.json index aa42c73a..31c82086 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -52,7 +52,7 @@ "test:vrt": "vp test --project vrt", "figma:publish": "figma connect publish", "registry": "node scripts/generate-registry.ts", - "registry:build": "node scripts/build-registry-css.ts && shadcn build ./registry.json -o ../../apps/storybook/public/r" + "registry:build": "node scripts/build-registry-assets.ts && shadcn build ./registry.json -o ../../apps/storybook/public/r" }, "dependencies": { "8341-ui-tokens": "workspace:*", diff --git a/packages/ui/registry.json b/packages/ui/registry.json index f892a27d..b38134cd 100644 --- a/packages/ui/registry.json +++ b/packages/ui/registry.json @@ -61,6 +61,94 @@ } ] }, + { + "name": "design-md", + "type": "registry:file", + "title": "DESIGN.md", + "description": "プロダクト用の DESIGN.md(コードから読み取れない設計の考え方)と design-principles。リポジトリ直下に DESIGN.md、docs/design-principles/ に原則の全文を置く", + "files": [ + { + "path": "registry-assets/DESIGN.md", + "type": "registry:file", + "target": "~/DESIGN.md" + }, + { + "path": "registry-assets/design-principles/README.md", + "type": "registry:file", + "target": "~/docs/design-principles/README.md" + }, + { + "path": "registry-assets/design-principles/hearing.md", + "type": "registry:file", + "target": "~/docs/design-principles/hearing.md" + }, + { + "path": "registry-assets/design-principles/principles.md", + "type": "registry:file", + "target": "~/docs/design-principles/principles.md" + }, + { + "path": "registry-assets/design-principles/services/README.md", + "type": "registry:file", + "target": "~/docs/design-principles/services/README.md" + }, + { + "path": "registry-assets/design-principles/services/admin.md", + "type": "registry:file", + "target": "~/docs/design-principles/services/admin.md" + }, + { + "path": "registry-assets/design-principles/services/consumer.md", + "type": "registry:file", + "target": "~/docs/design-principles/services/consumer.md" + }, + { + "path": "registry-assets/design-principles/services/internal.md", + "type": "registry:file", + "target": "~/docs/design-principles/services/internal.md" + }, + { + "path": "registry-assets/design-principles/services/saas.md", + "type": "registry:file", + "target": "~/docs/design-principles/services/saas.md" + }, + { + "path": "registry-assets/design-principles/sources/README.md", + "type": "registry:file", + "target": "~/docs/design-principles/sources/README.md" + }, + { + "path": "registry-assets/design-principles/sources/digital-agency-service-design.md", + "type": "registry:file", + "target": "~/docs/design-principles/sources/digital-agency-service-design.md" + }, + { + "path": "registry-assets/design-principles/sources/jakobs-law.md", + "type": "registry:file", + "target": "~/docs/design-principles/sources/jakobs-law.md" + }, + { + "path": "registry-assets/design-principles/sources/nngroup-f-shaped-pattern.md", + "type": "registry:file", + "target": "~/docs/design-principles/sources/nngroup-f-shaped-pattern.md" + }, + { + "path": "registry-assets/design-principles/sources/nngroup-ten-usability-heuristics.md", + "type": "registry:file", + "target": "~/docs/design-principles/sources/nngroup-ten-usability-heuristics.md" + }, + { + "path": "registry-assets/design-principles/ui-orientation.md", + "type": "registry:file", + "target": "~/docs/design-principles/ui-orientation.md" + }, + { + "path": "registry-assets/design-principles/wcag-for-designers.md", + "type": "registry:file", + "target": "~/docs/design-principles/wcag-for-designers.md" + } + ] + }, { "name": "accordion", "type": "registry:ui", diff --git a/packages/ui/scripts/build-registry-css.ts b/packages/ui/scripts/build-registry-assets.ts similarity index 57% rename from packages/ui/scripts/build-registry-css.ts rename to packages/ui/scripts/build-registry-assets.ts index 657d8ea1..90ddd9d5 100644 --- a/packages/ui/scripts/build-registry-css.ts +++ b/packages/ui/scripts/build-registry-assets.ts @@ -1,13 +1,19 @@ /** - * shadcn のレジストリで配るテーマ CSS(registry-assets/8341-ui.css)を作る。 + * shadcn のレジストリで配るファイルを registry-assets/ に作る。 + * + * 1. テーマ CSS(8341-ui.css) * * src/styles/theme.css は npm パッケージ 8341-ui-tokens の CSS を @import している。 * レジストリだけで導入した利用者はそのパッケージを持たないので、@import をビルド済みのトークン CSS の中身に置き換え、 * 1 ファイルで完結させる(ADR-0012)。 * + * 2. プロダクト用の DESIGN.md と design-principles(@8341/design-md。ADR-0019) + * templates/PRODUCT-DESIGN.md を DESIGN.md として、docs/design-principles/ を置く。 + * 利用側はファクトリーではないので、 〜 は取り除く。 + * * 実行: registry:build の中で自動で呼ばれる。先に 8341-ui-tokens のビルドが要る */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; const ROOT = resolve(import.meta.dirname, '..'); @@ -42,4 +48,16 @@ const header = [ mkdirSync(OUT_DIR, { recursive: true }); writeFileSync(resolve(OUT_DIR, '8341-ui.css'), header + css.replace(/^\/\*[\s\S]*?\*\/\n/, '')); -console.log('✔ registry-assets/8341-ui.css'); + +const REPO = resolve(ROOT, '../..'); +cpSync(resolve(REPO, 'templates/PRODUCT-DESIGN.md'), resolve(OUT_DIR, 'DESIGN.md')); +const principlesDir = resolve(OUT_DIR, 'design-principles'); +rmSync(principlesDir, { recursive: true, force: true }); +cpSync(resolve(REPO, 'docs/design-principles'), principlesDir, { recursive: true }); +for (const d of readdirSync(principlesDir, { recursive: true, withFileTypes: true })) { + if (!d.isFile() || !d.name.endsWith('.md')) continue; + const file = resolve(d.parentPath, d.name); + const md = readFileSync(file, 'utf8'); + writeFileSync(file, md.replace(/[\s\S]*?\n?/g, '')); +} +console.log('✔ registry-assets/(8341-ui.css・DESIGN.md・design-principles/)'); diff --git a/packages/ui/scripts/generate-registry.ts b/packages/ui/scripts/generate-registry.ts index 1a3a2b25..370c73df 100644 --- a/packages/ui/scripts/generate-registry.ts +++ b/packages/ui/scripts/generate-registry.ts @@ -4,7 +4,8 @@ * * - registry:base 8341-base … Base UI を固定した土台。利用側の `shadcn init` で読み込む * - registry:lib tv … tv / cn / フォーカス表示のクラス - * - registry:file theme … Tailwind のテーマ。トークンの CSS を同梱した 1 ファイル(scripts/build-registry-css.ts が作る) + * - registry:file theme … Tailwind のテーマ。トークンの CSS を同梱した 1 ファイル(scripts/build-registry-assets.ts が作る) + * - registry:file design-md … プロダクト用の DESIGN.md と docs/design-principles/(同上。ADR-0019) * - registry:ui 各コンポーネント * * 実行: make registry(--check で差分検出)。公開は shadcn build(registry:build) @@ -34,6 +35,13 @@ interface Item { [k: string]: unknown; } +/** docs/design-principles/ のファイル(design-md で利用側の docs/design-principles/ に置く) */ +const PRINCIPLES = resolve(ROOT, '../../docs/design-principles'); +const principleFiles = readdirSync(PRINCIPLES, { recursive: true, withFileTypes: true }) + .filter((d) => d.isFile() && d.name.endsWith('.md')) + .map((d) => relative(PRINCIPLES, resolve(d.parentPath, d.name))) + .sort(); + /** コンポーネントのディレクトリ(PascalCase。src/components/ui/Button など) */ const components = readdirSync(resolve(ROOT, 'src/components/ui'), { withFileTypes: true }) .filter((d) => d.isDirectory()) @@ -126,6 +134,22 @@ const registry = { // npm の 8341-ui-tokens に依存させず、レジストリだけで完結させる(ADR-0012) files: [{ path: 'registry-assets/8341-ui.css', type: 'registry:file', target: 'styles/8341-ui.css' }], }, + { + name: 'design-md', + type: 'registry:file', + title: 'DESIGN.md', + description: + 'プロダクト用の DESIGN.md(コードから読み取れない設計の考え方)と design-principles。リポジトリ直下に DESIGN.md、docs/design-principles/ に原則の全文を置く', + // ~ は利用側のプロジェクトのルート + files: [ + { path: 'registry-assets/DESIGN.md', type: 'registry:file', target: '~/DESIGN.md' }, + ...principleFiles.map((f) => ({ + path: `registry-assets/design-principles/${f}`, + type: 'registry:file', + target: `~/docs/design-principles/${f}`, + })), + ], + }, ...uiItems, ], }; diff --git a/scripts/sync-product-design.ts b/scripts/sync-product-design.ts new file mode 100644 index 00000000..585ae20d --- /dev/null +++ b/scripts/sync-product-design.ts @@ -0,0 +1,113 @@ +/** + * プロダクト用 DESIGN.md の雛形(templates/PRODUCT-DESIGN.md)の を、 + * docs/design-principles/ から生成する。載せるのは判断の順序・ルールの強さ・各ルールの「必須」だけ(コードから読み取れない考え方)。 + * 雛形は shadcn のレジストリ(@8341/design-md)で、利用側のリポジトリ直下に DESIGN.md として置かれる。 + * + * 実行: make design-md(--check で差分検出。make check から呼ぶ) + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, normalize, resolve } from 'node:path'; + +const ROOT = resolve(import.meta.dirname, '..'); +const DP = 'docs/design-principles'; +const TEMPLATE = resolve(ROOT, 'templates/PRODUCT-DESIGN.md'); +const CHECK = process.argv.includes('--check'); + +const read = (rel: string) => readFileSync(resolve(ROOT, DP, rel), 'utf8'); + +/** 見出し(# の数と文言が一致)から、同じか上の段の次の見出しまで */ +function section(md: string, level: number, title: string): string { + const lines = md.split('\n'); + const start = lines.findIndex((l) => l === `${'#'.repeat(level)} ${title}`); + if (start < 0) throw new Error(`見出し「${title}」が見つかりません`); + const end = lines.findIndex((l, i) => i > start && new RegExp(`^#{1,${level}} `).test(l)); + return lines.slice(start + 1, end < 0 ? undefined : end).join('\n'); +} + +/** 「**必須**」の直後の箇条書き(空行を挟まずに続くもの) */ +function required(text: string): string[] { + const lines = text.split('\n'); + const start = lines.indexOf('**必須**'); + if (start < 0) return []; + const out: string[] = []; + for (const line of lines.slice(start + 1)) { + if (line.startsWith('- ')) out.push(line); + else if (line.trim() || out.length) break; + } + return out; +} + +/** 最初の表(見出し行を含む) */ +function table(text: string): string { + return text + .split('\n') + .filter((l) => l.startsWith('|')) + .join('\n'); +} + +/** 相対リンクを、利用側のリポジトリでのパス(docs/design-principles/…)の表記にする */ +function relink(text: string, from: string): string { + return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (all, label: string, href: string) => { + if (/^[a-z]+:/i.test(href) || href.startsWith('#')) return all; + const path = normalize(join(DP, dirname(from), href.split('#')[0] ?? '')); + return `${label}(\`${path}\`)`; + }); +} + +function build(): string { + const readme = read('README.md'); + const orientation = read('ui-orientation.md'); + const services = read('services/README.md'); + const principles = read('principles.md'); + + const rules = table(section(principles, 2, '適用ルール')) + .split('\n') + .slice(2) + .map((row) => row.split('|').map((c) => c.trim())) + .map(([, name = '', decides = '']) => ({ name, decides })); + + const out = [ + `8341-ui の design-principles から生成した要約(手で直さない)。各ルールの「必須」だけを載せる。推奨・レビュー項目・理由は \`${DP}/\` の該当する節を読む。`, + '', + '### 判断の順序とルールの強さ', + '', + `1. UI の方向性(\`${DP}/ui-orientation.md\`)`, + `2. サービス別の特化ルール(\`${DP}/services/\`)。原則と食い違う判断は特化ルールを採る`, + `3. ウェブデザインの原則(\`${DP}/principles.md\`)。1 と 2 が書いていない判断に使う`, + `4. WCAG 2.2 の達成基準(\`${DP}/wcag-for-designers.md\`)。必須は 1〜3 のどれからも緩めない`, + '', + table(section(readme, 2, 'ルールの強さ')), + '', + '### UI の方向性', + '', + ...required(section(orientation, 3, 'ドメインとユーザーによる判断')).map((l) => relink(l, 'ui-orientation.md')), + '', + '### サービス別の特化ルール', + '', + relink(table(section(services, 2, 'どの文書を使うか')), 'services/README.md'), + '', + '### ウェブデザインの原則(必須)', + ]; + for (const { name, decides } of rules) { + const bullets = required(section(principles, 3, name)); + if (!bullets.length) throw new Error(`原則「${name}」に必須がありません`); + out.push('', `#### ${name}`, '', `${decides}。`, '', ...bullets.map((l) => relink(l, 'principles.md'))); + } + return out.join('\n'); +} + +const current = readFileSync(TEMPLATE, 'utf8'); +const re = /()[\s\S]*?()/; +if (!re.test(current)) throw new Error('templates/PRODUCT-DESIGN.md に がありません'); +const next = current.replace(re, `$1\n${build()}\n$2`); + +if (CHECK) { + if (next !== current) { + console.error('✖ templates/PRODUCT-DESIGN.md の原則が古くなっています。make design-md を実行してください'); + process.exit(1); + } + console.log('✔ templates/PRODUCT-DESIGN.md の原則は docs/design-principles と一致しています'); +} else { + writeFileSync(TEMPLATE, next); + console.log('✔ templates/PRODUCT-DESIGN.md(auto:principles)'); +} diff --git a/templates/PRODUCT-DESIGN.md b/templates/PRODUCT-DESIGN.md index 713f677f..5d8617d4 100644 --- a/templates/PRODUCT-DESIGN.md +++ b/templates/PRODUCT-DESIGN.md @@ -1,52 +1,221 @@ # DESIGN.md — (プロダクト名) -> 8341-ui を使うプロダクトの DESIGN.md の雛形。プロダクトのリポジトリ直下にコピーして埋める。 -> 8341-ui 側の仕様(トークン・コンポーネント・Do / Don't)は 8341-ui リポジトリの DESIGN.md を参照し、ここには写さない。 -> 部品は shadcn 方式(`npx shadcn@latest add @8341/{name}`)か npm の `8341-ui` で取り込む。 -> 判断の順序は [design-principles](https://github.com/tkc310/8341-ui/tree/main/docs/design-principles) に従う: UI の方向性 → サービス別の特化ルール → 原則 → WCAG。 +> 8341-ui を使うプロダクトの DESIGN.md。**コードから読み取れない設計の考え方だけ** を書く。 +> トークンの値、コンポーネントの一覧と使い方は、コード(8341-ui の部品とテーマ)が正なので、ここには写さない。 +> 置き方: `npx shadcn@latest add @8341/design-md`(このファイルと `docs/design-principles/` が入る)。 +> `` の範囲は 8341-ui から配られる部分。手で直さない(下の「更新のしかた」)。 -## プロダクト概要 +## エージェントへ -- 対象ユーザー: (職種、習熟度、利用頻度) +- 画面を作る・直す前に、「1. プロダクトの前提」〜「4. 採用したルールと外したもの」を読む。( )のままの欄があれば、`docs/design-principles/hearing.md` の項目で利用者に聞いて埋めてから作る +- 判断の順序は、画面の方向性 → サービス別の特化ルール → 原則 → WCAG 2.2。迷ったら「5. デザイン原則」の該当ルールを見て、詳しくは `docs/design-principles/` の該当する節だけを読む(全文を読まない) +- 原則に足りない点を見つけたら、`docs/design-principles/` を直さずに、この DESIGN.md の「4.」に例外として書き、8341-ui に改善を提案する + +## 1. プロダクトの前提 + +- 誰が使うか: (職種、習熟度、利用頻度) +- 何をするためのものか: (利用者がこのプロダクトで終えたい主な作業) - ドメイン: (業務領域。専門的か日常的か) +- 使う場面と機器: (デスクトップ / タブレット / スマートフォン。主と補助。静かな事務所か、移動中か) - アクセシビリティ目標: WCAG 2.2 Level AA(8341-ui の基準。これより緩めない) -## 対象デバイス +## 2. 画面領域ごとの方向性とサービス種別 + +一つのプロダクトに複数の種類の画面があるときは、領域ごとに書く。選び方は `docs/design-principles/ui-orientation.md` と `docs/design-principles/services/README.md`。 + +| 画面領域 | UI の方向性 | 理由 | サービス種別 | +| --- | --- | --- | --- | +| (例: 請求書の管理) | オブジェクト指向 | 経理担当者が対象を熟知している | 管理画面 | +| (例: 初回セットアップ) | タスク指向 | 初回のみで手順を知らない | SaaS | + +## 3. このプロダクトで大事にすること + +部品やトークンでは決まらない、このプロダクト固有の判断の軸を書く。画面ごとに迷ったときに立ち返る基準になる。 + +- 体験の性格: (例: 速さより確かさ。取り消せない操作の前には必ず確認する) +- 言葉づかい: (例: 利用者の業務用語を使う。「案件」と呼び、「プロジェクト」と書かない) +- 画面の構成: (グローバルナビゲーションに対象名を並べるか作業名を並べるか、ヘルプと検索の位置、空状態の文面) +- 避けること: (例: 1 画面に主操作を 2 つ置かない) + +## 4. 採用したルールと外したもの + +- 特化ルール: (`docs/design-principles/services/` のどれを、どの画面領域に採るか) +- 必須を外した項目と理由(なければ「なし」): + +| ルール | 外した理由 | +| --- | --- | +| (なし) | | + +- 8341-ui に無いものを足した理由(部品やトークンそのものはコードにある。ここには「なぜ」だけを書く。ほかのプロダクトでも使えそうなら 8341-ui に提案する): + +| 足したもの | 理由 | +| --- | --- | +| (なし) | | + +## 5. デザイン原則 + + +8341-ui の design-principles から生成した要約(手で直さない)。各ルールの「必須」だけを載せる。推奨・レビュー項目・理由は `docs/design-principles/` の該当する節を読む。 + +### 判断の順序とルールの強さ + +1. UI の方向性(`docs/design-principles/ui-orientation.md`) +2. サービス別の特化ルール(`docs/design-principles/services/`)。原則と食い違う判断は特化ルールを採る +3. ウェブデザインの原則(`docs/design-principles/principles.md`)。1 と 2 が書いていない判断に使う +4. WCAG 2.2 の達成基準(`docs/design-principles/wcag-for-designers.md`)。必須は 1〜3 のどれからも緩めない + +| ルール | DESIGN.md での扱い | +| --- | --- | +| WCAG の必須 | 外せない。緩めることもできない | +| 特化ルールと原則の必須 | 原則として採る。外すときは、外す理由を DESIGN.md に書く | +| 推奨 | プロダクトごとに採るかどうかを決める | + +### UI の方向性 + +- ドメインが専門的であり、対象ユーザーがそのドメインに習熟している場合は、オブジェクト指向 UI を採用する。専門家は対象と操作の関係をすでに知っているため、対象から操作を選ぶほうが速い。 +- ドメインが日常的である場合は、オブジェクト指向 UI を採用する。利用者はドメインの対象を日常の知識から理解できる。 +- ドメインが専門的であり、対象ユーザーがそのドメインに習熟していない場合は、タスク指向 UI を採用する。専門的な対象を前提知識なしに操作させるには、典型的な手順をシステムが示す必要がある。 + +### サービス別の特化ルール + +| 画面 | 文書 | +| --- | --- | +| レコードを繰り返し検索・更新・監査する | 管理画面(`docs/design-principles/services/admin.md`) | +| 自社の社員が、自分の業務手続きを完了する。顧客には開かない | 社内向けツール(`docs/design-principles/services/internal.md`) | +| 契約した組織が、継続して日常業務に使う | SaaS(`docs/design-principles/services/saas.md`) | +| 個人が、自分のために使う | toCサービス(`docs/design-principles/services/consumer.md`) | + +### ウェブデザインの原則(必須) + +#### 読み順と優先度 + +どれを先に気づかせ、どの順で読ませるか。 + +- その画面で最初に伝える要素を、サイズ、位置、色のいずれかで他より先に気づく状態にする。 +- 見出しの段(大見出し、中見出し、小見出し)ごとに、見た目の差を付ける。同じ段には同じ見た目を使う。 +- 一つだけ強調する要素は、周囲と違う見た目にする。強調は画面につき少数にとどめる。 + +#### まとまり + +同一画面の中で、どれとどれを一つのグループに見せるか。 + +- 関係する要素(ラベルと入力欄、見出しと本文)の間隔を、関係しない要素の間隔より狭くする。 +- グループの境は、余白、枠、背景のいずれかか、その組み合わせで分かるようにする。 +- 同じグループに属する要素は、色、形、サイズを揃える。違うグループは、そのいずれかで区別する。 +- 手順や時系列は、線や並びに沿って一つの連なりに見えるようにする。 + +#### 文字と図の判別 + +文字と、情報を伝える図が判別できるか。 + +- 通常サイズの文字と背景のコントラスト比は 4.5:1 以上、大きい文字は 3:1 以上にする。プレースホルダーも文字である。 +- 情報を伝えるアイコン、グラフ、入力欄の境界と、隣接する色とのコントラスト比は 3:1 以上にする。 +- 色だけで状態や系列を区別しない。形、ラベル、パターンのいずれかを併用する。 +- 本文の文字サイズと行間は、デザインシステムに一つの初期値として定め、画面ごとに縮めない。 +- コンテナの内側には余白を置き、文字が枠に接触しないようにする。 + +#### 配置の秩序 + +位置が基準線に乗り、画面の視覚的な重さが極端に偏らないか。 + +- 関係する要素群を、左、中央、右のいずれかの基準線で揃える。 +- 列の幅と列間は、画面内で同じグリッドに乗せる。 +- 主内容と副内容を並べるとき、幅の比を画面内で一定にする。 + +#### 操作の幾何 + +押せるものの大きさと、隣との距離。 + +- ポインタで操作する対象は、一辺 24 CSS ピクセル以上にする。満たさない対象は、各対象のバウンディングボックスの中心に置いた直径 24 CSS ピクセルの円が、他の対象やその円と交差しない位置に置く。 +- 同じ画面で続けて押す操作は、直前に操作する要素(入力欄、選択した行など)の近くに置く。 + +#### 一度に出す数 + +同時に選ばせる個数。 + +- 一つの決定に、画面いっぱいの並列選択肢を出さない。分類、段階的な開示、既定値のいずれかで個数を減らす。 +- 長い一覧は、見出し、ページ分割、絞り込みのいずれかでまとまりに分ける。 + +#### 一貫性 + +画面をまたいで同じ機能を同じものに見せるか。ウェブで広く使われている配置を崩さないか。 + +- 保存、取消、削除のように同じ機能のボタンは、全画面で同じ見た目とラベルにする。 +- 色の意味(リンク、エラー、成功)を画面ごとに変えない。 +- ナビゲーションの位置と項目の順を、全ページで変えない。 +- ロゴの位置、戻る操作、主要なメニューの置き方は、他のウェブサイトで広く使われている配置から外れる理由があるときだけ変える。利用者は他サイトで過ごす時間のほうが長く、そちらの配置を前提に操作する、という観察がジャコブの法則と呼ばれる。 + +#### 状態と応答 + +今何が起きているか。待ちが長いか。 + +- 操作を受け付けたこと、処理中であること、完了または失敗したことを、それぞれ画面に表示する。 +- 応答がすぐ終わらない処理には、待っていることが分かる表示を出す。 +- ナビゲーションでは、現在のページがどこかを表示する。 +- 自動で動くカルーセルや通知は、利用者が止められるようにする。条件は wcag-for-designers.md(`docs/design-principles/wcag-for-designers.md`) の 2.2.2 に従う。 + +#### 誤りと撤回 + +失敗を防ぐか。失敗の内容と戻り方を示すか。操作を中断できるか。 + +- 削除や送信など取り消せない操作の前に、内容を確認する段階を置く。 +- 入力の形式と制約(必須か、文字数、使える文字)を、入力する前に示す。 +- エラーが起きた項目と、利用者が自分で直せる方法を示す。番号だけのエラーにしない。 +- モーダルと複数段の入力には、閉じる、中断する、下書きを残すのいずれかを置く。 +- 時間制限を置く場合は、解除、調整、延長のいずれかを利用者ができるようにする。条件と例外は wcag-for-designers.md(`docs/design-principles/wcag-for-designers.md`) の 2.2.1 に従う。 + +#### 再認 + +思い出させず、見せて選ばせるか。 + +- 入力値の候補があるときは、自由入力だけにせず候補を出す。 +- 直前までに入力した値を、後の段で再入力させない。セキュリティ上必要な再入力と、無効になった値は除く。 +- アイコンだけの操作には、ラベルかツールチップで名前を添える。 + +#### 文言 + +ラベルと文が、操作の結果と入力の意味を言うか。 + +- ボタンとリンクのラベルに、移動先か動作が入るようにする。「こちら」「詳細」「もっと見る」だけにしない。 +- 入力欄には、何を入れるかのラベルを付ける。形式の指定(日付の並びなど)は、エラーのあとではなく前に書く。 +- エラー文には、何が違うかと、どう直すかを書く。 +- 利用者の作業用語を使い、画面の実装用語をラベルに出さない。 + +#### 削る -- (デスクトップ / タブレット / スマートフォン。主と補助) +その画面の主タスクに不要な要素を置かないか。レイアウトで伝わることを文字で繰り返さないか。 -## 画面領域ごとの方向性とサービス種別 +- その画面の主タスク(利用者がその画面で終えたい作業)に使わない装飾、説明、動く要素を置かない。 +- 見出し、配置、まとまりで既に伝わることを、セカンダリラベル(小見出し、見出しの上の補助ラベル、項目名の前置き)や説明文で繰り返さない。例: 入力欄のラベルを言い換えただけの補足文、ページの見出しを言い直すだけの導入文、カードの中の「詳細」「情報」だけの小見出し、表の列見出しと同じ語を各セルに付けた前置き。 +- 項目どうしの関係(どれが同じグループか、どれが主でどれが従か)は、レイアウト(近さ、並び、枠や背景、大きさの差)で示す。関係を説明する文を足して補わない。 +- 削ってよいのは重複している表示だけである。入力欄の見えるラベル(WCAG 3.3.2)、見出しや表の見出しセルなど構造を表す要素(1.3.1)、アイコンだけの操作の名前は残す。見た目で示したグループには、読み上げでも取り出せる名前(`fieldset` と `legend`、区画の見出しなど)を付ける。 +- 主タスクを遮るポップアップは、利用者の判断が要る場合(未保存の変更の破棄など)に限る。それ以外の通知は、主タスクを遮らない位置に出す。 +- 自動で再生・移動する要素は、主タスクの妨げになる場合は初期状態で動かさない。 -一つのプロダクトに複数の種類の画面があるときは、領域ごとに書く。 +#### 助けの場所 -| 画面領域 | UI の方向性 | 理由 | サービス種別 | -| ------------------------ | ---------------- | ------------------------------ | ------------ | -| (例: 請求書の管理) | オブジェクト指向 | 経理担当者が対象を熟知している | 管理画面 | -| (例: 初回セットアップ) | タスク指向 | 初回のみで手順を知らない | SaaS | +ヘルプと検索を、サイト内の同じ場所に置くか。 -## 採用したルール +- ヘルプ、検索、問い合わせを置くときは、全ページで同じ位置に置く。WCAG 3.2.6 が求めるのは、ほかの要素に対する相対的な順序を変えないことである。 +- ヘルプの項目は、操作の名前で探せるようにする。 +- 目的のページへ到達する手段を、ナビゲーション以外にもう一つ置く。検索、サイトマップ、一覧のいずれでもよい。手続きの途中の段や、手続きの結果として表示するページは除く。手段の組み合わせは wcag-for-designers.md(`docs/design-principles/wcag-for-designers.md`) の 2.4.5 に従う。 -1. UI の方向性: 上の表のとおり -2. 特化ルール: (services/ のどれを採るか) -3. 原則: design-principles の適用ルール(8341-ui の対応表: `docs/rulebook/01-principles.md`) -4. WCAG 2.2 AA: 8341-ui のトークンとコンポーネントで担保。画面ごとの確認項目は `docs/rulebook/02-accessibility.md` +#### 短い経路 -## 必須ルールを外した項目 +繰り返す操作に、より短い別経路があるか。 -| ルール | 外した理由 | -| -------------------- | ---------- | -| (なければ「なし」) | | +- 基本の経路(ボタンを順に押す)を残したうえで、繰り返す操作にショートカット、一括操作、検索のいずれかがある。 +- キーボードだけで基本の経路を完了できるようにする。詳細は wcag-for-designers.md(`docs/design-principles/wcag-for-designers.md`) の 2.1.1 に従う。 -## 8341-ui からの拡張 +#### 自律的な処理の範囲 -プロダクト固有に足したトークンやコンポーネント。他のプロダクトでも使えそうなら 8341-ui への追加を Issue で提案する。 +人の操作を待たずに進む処理の境界が分かるか。 -| 名前 | 種類 | 理由 | -| ---- | ---- | ---- | -| | | | +- 自動で進んだ処理について、何を根拠に何をしたかを画面上で示す。 +- 利用者が止められる地点と、結果を取り消せる地点を置く。 +- 人とシステムのどちらが最終決定を持つかを、その画面に書く。 + -## ナビゲーションと画面構成 +## 更新のしかた -- グローバルナビゲーション: (対象名を並べるか、作業名を並べるか) -- ヘルプと検索の位置: (全ページで同じ位置) -- 空状態の文面: (「対象がまだありません」か「作業を始めましょう」か) +8341-ui の新しい版の原則を取り込むときは、`npx shadcn@latest add @8341/design-md` を作業用のブランチで実行する。上書きの確認が出たら受け入れ、`docs/design-principles/` と、この DESIGN.md の `` の範囲だけを残して、1.〜4. は元に戻す(git の差分で選ぶ)。 From 78760a6908ac42df5d5393ebd64d78952b3654d8 Mon Sep 17 00:00:00 2001 From: tkc310 Date: Fri, 23 Oct 2026 11:37:46 +0000 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=E5=8F=A4=E3=81=84=E5=8F=82?= =?UTF-8?q?=E7=85=A7=E3=83=BB=E9=87=8D=E8=A4=87=E3=83=BB=E4=BD=BF=E3=82=8F?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=82=B3=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=82=92=E6=95=B4=E7=90=86=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 古い参照を修正: 存在しない make ターゲット(check:contrast)、ds-improve のトークンと contrast.rules.json のパス、factory.md の README の節名、ワークフローのコメント(pnpm → make、手順の置き場所)、publish-preview.ts の使い方 - トークンを変えた後の手順を make generate に統一(CONTRIBUTING・03・06・operations・ds-improve・ds-factory) - Makefile と重複していた pnpm スクリプト(storybook:preview・factory:create・release:*・preview:publish)と、create.mjs のその削除処理を外した - design-review から、コードレビュー・Next.js 専用の項目を外した(画面デザインのレビューに限る) - DESIGN.md の「作業前に必ず読むもの」(全文を読む指示・古いパス)と、古い名前の 'use client' の一覧を直した - 重複を正本へのリンクに: DESIGN.md の共通規約とルールブック 04、ファイル構成のツリー(COMPONENT_STRUCTURE.md)、06 章と ds-improve、factory.md と ds-factory、make check の説明 - トークンの CSS 化の規則を packages/tokens/scripts/lib/css.ts に集め、build.ts とスライドで共有。build.ts の cssVar も cssVarName を使う - 使われていない関数(dimensionToString・relFromRepo・srcDir・oklchToHex・Layout)と、内部でしか使わない export を削除 - create.mjs: lstat の二重呼び出しをやめ、--force で既にあるシンボリックリンクがあっても落ちないようにした - make clean でスライドの出力(dist/)も消す Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb44feYKDNj2ukQeT2j8Ks --- .claude/skills/design-review/SKILL.md | 14 +-- .claude/skills/ds-factory/finish.md | 2 +- .claude/skills/ds-improve/SKILL.md | 8 +- .github/workflows/branch-cleanup.yml | 2 +- .github/workflows/ci.yml | 2 +- .../workflows/storybook-preview-cleanup.yml | 2 +- CONTRIBUTING.md | 23 +---- DESIGN.md | 25 ++---- Makefile | 8 +- apps/storybook/package.json | 3 +- apps/storybook/scripts/publish-preview.ts | 2 +- docs/factory.md | 28 +----- docs/improvement/log.md | 1 + docs/operations.md | 4 +- docs/rulebook/03-tokens.md | 4 +- docs/rulebook/04-components.md | 14 +-- docs/rulebook/06-improvement-loop.md | 12 +-- docs/skills.md | 2 +- package.json | 6 +- packages/tokens/scripts/audit-report.ts | 2 +- packages/tokens/scripts/build.ts | 59 ++++--------- packages/tokens/scripts/check-contrast.ts | 4 +- packages/tokens/scripts/lib/color.ts | 9 +- packages/tokens/scripts/lib/css.ts | 50 +++++++++++ packages/tokens/scripts/lib/tokens.ts | 23 ++--- packages/tokens/src/contrast.rules.json | 2 +- packages/tokens/src/palette.config.json | 2 +- scripts/factory/create.mjs | 13 ++- scripts/slides/lib.ts | 85 +++++++------------ 29 files changed, 156 insertions(+), 255 deletions(-) create mode 100644 packages/tokens/scripts/lib/css.ts diff --git a/.claude/skills/design-review/SKILL.md b/.claude/skills/design-review/SKILL.md index c7530626..8725e240 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 5724e7d1..9fb2e3a0 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 b6561df3..09f2f741 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 4eb21860..4f099535 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 5ccb93d0..9d3f7e1a 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 04e57a75..77e729d2 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 356fbbb9..6996ac21 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 6d40c4fc..9eaa508a 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 5119986d..3a42bf32 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 71eb5662..3ddc185c 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 0ba9ba51..fd25e5ba 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 25381530..38ec1932 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 0c42788d..586ed45a 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 c217148e..a43e8a5f 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 9f43b4f6..593f67eb 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 0a58a49b..f3d5d427 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 aae46b7f..b5ea90b6 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 c99eff5d..3e1a8a26 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 65f6c26a..181103e7 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 d4fb9615..ad19a4dc 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 1af2b43d..951ace61 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]); }, }); @@ -271,10 +238,14 @@ async function build() { const variants = 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 d9b815c2..4f3e7bbd 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 e49e7f4b..4ad026c1 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 00000000..f4cf0046 --- /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 357caddc..12c45833 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 eef11c5b..702fce9f 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 d5631e50..c50c8c2b 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 3252de27..79e02247 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 e4067d9e..0f08bcff 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; From b5f923e925b940e441a14272fa6d8b1a63c41da6 Mon Sep 17 00:00:00 2001 From: tkc310 Date: Fri, 23 Oct 2026 11:42:40 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix(tokens):=20=E3=83=86=E3=83=BC=E3=83=9E?= =?UTF-8?q?=E3=81=AE=E3=82=BB=E3=83=AC=E3=82=AF=E3=82=BF=E3=82=92=E5=AE=9A?= =?UTF-8?q?=E6=95=B0=E3=81=AB=E3=81=97=E3=81=9F=E3=81=93=E3=81=A8=E3=81=A7?= =?UTF-8?q?=E5=87=BA=E3=81=9F=20build.ts=20=E3=81=AE=E5=9E=8B=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb44feYKDNj2ukQeT2j8Ks --- packages/tokens/scripts/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tokens/scripts/build.ts b/packages/tokens/scripts/build.ts index 951ace61..6992325b 100644 --- a/packages/tokens/scripts/build.ts +++ b/packages/tokens/scripts/build.ts @@ -235,7 +235,7 @@ 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: THEME_SELECTOR.dark },