Repository navigation
Storybook を導入し、デザイントークンのカタログと a11y の自動検査を足す (ADR-0022) - #35
Merged
Merged
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.storybook/ で Storybook v10.6.0 を @storybook/tanstack-react framework で起動できるようにする。story はまだ置かない (Task 5 以降)。 - tsconfig.json の include に .storybook/**/*.ts(x) を追加。TypeScript の ** glob はドットディレクトリを暗黙に除外するため、追加しないと vite-plus/client の ambient module 宣言 (declare module "*.css") が 適用されず、preview.tsx の side-effect import が TS2882 で失敗する Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
styles.css が SSOT なので story 側に値を写さず、getComputedStyle で解決後の値を 読む。実装中に brief の前提コードでは対応できない挙動を 3 点実測で発見し、 本 PR 内で対応した。 - styles.css の色は oklch() で定義されているが contrastRatio (src/lib/contrast.ts) は rgb() しか解析できないため、コントラスト比が全行 null になる。Canvas 2D の fillStyle で rgb() へ正規化する toRgb() を追加した - ColorTokens は getComputedStyle を mount 時に 1 回読むだけで、 withThemeByClassName によるテーマ切り替え (document.documentElement への直接の class 書き換え、React の state/props を経由しない) を検知できず、dark へ切り替え ても light の値のまま固まる。useSyncExternalStore で <html> の class を購読する useHtmlClass() を追加した - 色見本の <span aria-hidden> だけでは jsx-a11y/control-has-associated-label が td 自体 (th と同様に対象) をラベル無しの control とみなして落ちる。sr-only の テキストを添えた Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Critical 1 件 (readRootTokens が @layer を辿らずトークンを取りこぼす)、 Important 3 件に対応した。 - styles.css の @theme inline を @theme static inline にし、未使用トークン (--font-heading 等) も出力させる (readRootTokens で static 無し 70 件 → static あり 109 件、自分で分離実測) - readRootTokens を CSSGroupingRule 再帰に直し、@layer theme の中の :root も辿る - ColorTokens の振り分けを denylist から isColor() (色として解析できたものだけ) に 変更。振り分け用の静かな述語 isColor と、解析失敗時に warn する toRgb を src/lib/css-color.ts へ切り出し、単体テスト (5 ケース) を追加した - 各 story に 0 件検出の warn を追加 - TYPOGRAPHY_SAMPLES のページ見出し行を pageTitle() 呼び出しに変更 (page-title.tsx との二重管理を解消) - 色見本を「解決後の値」セルに同居させ、sr-only の重複読み上げと jsx-a11y/control-has-associated-label 違反を解消 (列を 4 → 3 に変更) - pageTitle 経由の class-variance-authority / cn 依存追加に伴い、 vitest.storybook.config.ts の optimizeDeps.include へ追加 (事前バンドル漏れで 3 story 全滅を実測) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- styles.css の static/inline コメントを事実と合う内容に短く書き直した (spacing/text/tracking トークンが見えたのは @layer 再帰の効果で、static とは 無関係だった)。内訳・使用箇所・バイト数は書かず ADR-0022 (Task 6) に譲る - Tokens/Colors の --color-X (Tailwind の @theme inline が生成する var(--X) の 別名) を、同じ集合に生トークン --X が在るときだけ落とす filter を追加。 --color-black/white は対応する生トークンが無いため残る。Colors が 68 → 35 行になった - tokens.stories.tsx のコメントの「63 件」を自分で実測し直し 27 件に修正 (denylistKept 95 / isColorKept 68 / leaked 27) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- --color-X の除外を dropRedundantColorAliases として src/lib/ へ切り出し、境界条件の 単体テストを付ける (coding-discipline.md「純粋関数の切り出し」) - 別名と生トークンの値が食い違うときは落とさず warn する。落としてよい根拠は var() 経由で 値が同じことであって名前の綴りではなく、黙って消すと一覧自身が drift を隠す - コメントから動く数値 (27 件 / 34 件 / 109 件) を外し、機序だけ残す。実測値は ADR-0022 が 日付付きで持つ (doc-hygiene.md「数値はその数値が動く要因で判断する」) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
spec の取りこぼし 4 件: - Decision 1 へ server-only 依存の __mocks__ 遮断 - Decision 2 へ variant を代表値 + argTypes で表す歯止め - 検討した選択肢へトークン専用 addon の却下 - Consequences へ storybook/test の expect が持たない matcher がある事実 事実誤り 4 件: - 節 6 の「static で 4 件が新たに出力される」は誤り。--font-heading / --radius-sm / --radius-lg / --radius-xl は static の有無によらず出る - 同じく CSS のバイト数が実測と合わない。出所のビルドも書かれていなかった - 件数表を削除。値は Tailwind の版・utility の使われ方・story の集合で動き、 過去の測定が 2026-09-20 に再現しなかった。機序と 1 例と測り方に置き換える - 節 9 の段階の内訳が節 2 の対象の層と食い違っていた (1 段目 / 2 段目) 3 文以上の段落 2 つを分割した (節 4 / 節 5)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
f88d5fc で ADR-0022 の件数表を削除したが、tokens.stories.tsx の 2 箇所が 「件数の実測は ADR-0022」と指したままだった。指す先が無くなっている (doc-hygiene.md「陳腐化したポインタは注記を足さず、文ごと削除する」)。 件数への言及を外し、出典キーとしての ADR-0022 だけを残す。 削除前に grep -rn "ADR-0022" src/ を通していれば検出できた (grep-before-edit.md 5「削除 / リネームは全参照更新」)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9 箇所が対象。Context と節 6 は箇条書き、節 9 の段階の内訳は表にし、 残りは 2 文以下の段落へ分けた。決定の内容は変えていない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
contrast / css-color / theme-tokens はアプリから 1 箇所も import されて
おらず、tokens.stories.tsx と自分のテストだけが使っていた。src/lib/ は
プロダクトコードの置き場所なので、読んだ人はアプリのコードだと受け取る。
今は bundle に入っていないが、それを止めるものが無かった。
置き場所と命名は既存の *.test-helpers.ts に合わせた。Storybook 自身も
同じ形を使っている (code/core/src/manager/components/sidebar/
Filter.story-helpers.tsx)。Storybook 公式の推奨は無い (storybookjs/
storybook#13244 は回答なしで not planned)。
- src/lib/{contrast,css-color,theme-tokens}.ts -> src/components/
{...}.story-helpers.ts。テストも同じ名前で移動
- no-restricted-imports の regex を \.(test|story)-helpers$ へ拡張し、
story 自身を excludeFiles へ足す (tokens.stories.tsx が helper を引く)
- coverage からも除外する
破壊テスト: アプリのコード (live-regions.tsx) から story-helper を import
すると lint が落ちることを確認した。excludeFiles への story-helpers の
追加は、今日は 1 件も効いていない (消しても違反 0 件)。同じ位置の
test-helpers は 3 ファイルで効いており、story-helpers も helper が
@/test/ を引いた時点で要る。対称性のために残す。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
自前の contrast は rgb しか読めず、トークンが oklch なので Canvas で
ブラウザの色パーサを借りる css-color が要り、そのために純粋な計算まで
Chromium で走らせていた。起点は自前実装だったので、そこから畳む。
同じ入力で 6 案を測り culori を選んだ (比較は ADR-0022)。決め手は精度では
なく非色トークンの扱いで、culori だけが解析できない値へ undefined を返す。
colord は oklch を解析できないまま 1.000 を返すため採らない。
比較の相手も直す。全トークンを --background 一律と比べていたが、画面上で
重ならない組み合わせの比は閾値の判断に使えない。styles.css の命名が示す
前景・背景の対だけを Tokens/Contrast へ並べる。
- src/components/{contrast,css-color}.story-helpers.ts とテスト 12 case を削除
- theme-tokens.story-helpers.ts へ foregroundPairs を足す (5 case)
- テーマ切り替え時は読み取り要素を key で remount する。値は React の依存に
現れないため再 render だけでは React Compiler のメモ化が残り、light の値で
止まる (light 4.35 / dark 5.57 に分かれることで確認)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tokens/Contrast の story が検出した。既存の欠陥で、値は初期 template コミット (fb3433a) から変わっていない。この PR が持ち込んだものではない。 light の --muted-foreground は shadcn 既定の L 0.554 (slate-500 相当) で、 --muted の上で 4.35:1 と WCAG 1.4.3 (4.5:1) を割る。--muted と text-muted-foreground は 11 ファイルが同じファイル内で使う。 palette の ramp を 1 段下げて slate-600 の L にした。--muted 上 6.90:1、 白背景 7.56:1。閾値を跨ぐ最小の値 (L 0.545 で 4.52:1) は採らない。通ること 以外に根拠が無く、以降の token 調整で静かに割り直す。 彩度は registry 既定より低い既存の値を保つ。dark 側は slate-400 の L で 5.57:1 を満たすため触らない。 残る 2 対 (--destructive-foreground / --sidebar-primary-foreground) は このアプリが描画していないため別で扱う。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
culori へ移した際、色の判定を parse(value) !== undefined にしたが、culori の
parse は # の無い hex を受ける (parse("700") が #700 を返す、2026-09-20 実測)。
--font-weight-bold: 700 などの数値トークンが色として Colors に並んでいた
(実機で 39 件、正しくは 35 件)。
hex 数字だけの値を先に落とす。CSS の色は必ず # か関数か色名で書く。
判定は theme-tokens.story-helpers.ts へ isColorValue として置き、境界条件の
テストを付けた (+3 case)。
撤去した Canvas 版の isColor はブラウザの色パーサを借りていたためこの値を
落としていた。移行で失った性質はこれだけだった。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 つの形を試してどちらも目的を果たさなかった。 - 全トークンを --background と比べる形は、画面上で重ならない組み合わせの比が 並び、閾値を割ったかどうかの判断に使えない - 命名から対を導く形は、命名が「画面で重なること」の代理になっていない。 使われていない対 (--destructive-foreground / --sidebar-primary-foreground) で 不合格を出し、styles.css が値を調整した実在の対を 1 行も出さなかった JS で比を計算する形そのものにも無理がある。ブラウザは sRGB 外の oklch() を clip し alpha を持つ背景は下地と合成するため、描画されない値を計算しても 見え方と一致しない (culori の wcagContrast は clamp しない)。 実ペアを実テキストで描いて axe に判定させる形は成立し、bg-destructive/20 と text-destructive が 3.74:1 で不合格になることを検出した。破壊ボタンの hover の 実在の組み合わせだが、扱いに --destructive の再検討が要るため別で扱う。 culori と @types/culori を外す。src/components/css-color.story-helpers.ts は Canvas でブラウザの色パーサを借りる isColor だけを残す (culori の parse は color-mix() と relative color syntax を解析できず、# 無しの hex を受ける)。 比の計算に使っていた toRgb は落とす。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
909b3e9 を戻す。値そのものは正しく、--muted の上で 4.35:1 と WCAG 1.4.3 を 割っていたのを 6.90:1 にした。戻す理由は 2 つ。 - 検出した Tokens/Contrast の story を 739dbeb で外したため、この PR に根拠の 見えないトークン値の変更だけが残る - 同じ palette の --destructive も割っており (bg-destructive/20 で 3.74:1)、 トークンの値はまとめて決めるほうが判断しやすい 経緯は ADR-0022 の節 6-1 が持つ。 実機で確認した副作用も残しておく。placeholder (#6b7380 -> #4d5561) が入力済みの 値 (#020618) へ近づき、階層の差が縮む。見分けは付くが、比の改善と引き換えに なっている。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## silent failure (4 件) - css-color: sentinel を 2 つにする。1 つ固定だと、その値そのものの色トークンを 「変わらなかった」と読んで無警告で一覧から落とす - css-color: 2D context が取れないときに warn する。全件が非色になり、表に出る のは「0 件」の warn だけで原因が canvas だと分からなかった - css-color: 誰も使わない fillRect と getImageData を落とし、canvas を使い回す。 toRgb の撤去で読み戻しの消費者が居なくなっていた - tokens.stories: テーマ再読込を preview.tsx の decorator へ移す。story ごとに 書き手が覚える形だと、値を読む story を足した人が忘れて静かに止まる ## 設定の食い違い (5 件) - registry-baseline と ADR-0006 の付随ファイルへ *.story-helpers.* を足す。 段階 3 で ui/ に helper を置いた時点で網羅検査が落ちる - no-restricted-imports の regex へ \.stories$ を足す。helper は止まっていたが story 自身の import は素通りで、引く helper ごと bundle に入る経路が残っていた - *.stories.ts を lint の除外と coverage の除外へ。main.ts の glob は .ts も拾う のに 2 箇所が .tsx しか列挙しておらず、3 箇所で食い違っていた - SOURCE_ROOTS へ .storybook を足す。追跡されたのに lint の可視範囲の検査外だった ## 構造 (2 件) - CSSOM の走査を css-rules.story-helpers.ts へ切り出し、境界条件を 9 件で固定 した。selectorText を "," で分ける形をやめ matches() に委ねる。:is(:root, .x) の取りこぼしが消える。ただし root に当たるだけでは Tailwind の "*, ::before, ::after, ::backdrop" まで拾うため (実測 4 件)、任意の要素にも 当たる rule は除く - getComputedStyle を一覧の組み立てごとに 1 つへ畳む ## 規範とツール (3 件) - styling.md が @theme inline のままだった。static を外すとカタログから消える - mise の storybook タスクの port を worktree ごとに導出する。6006 固定では 2 つ目の worktree が bind に失敗する - ADR-0022 の選択肢表へ、static の代替 3 案を却下理由つきで足す。いずれも公式に 文書化されていない経路に乗る。未参照の宣言を全利用者が払う点も明記した Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 重大 1 件: decorator が play の状態を捨てる dee9d3b で入れた preview.tsx の global decorator を撤去する。 withThemeByClassName は STORY_RENDERED 後の useEffect で class を当てるため、 class の変化は必ず play より後に来る。そこで key による remount を掛けると、 play が開いたダイアログやメニューが閉じた状態で終わる (dark で実測)。 key での remount をやめ、theme-snapshot.story-helpers.ts の外部ストアへ移す。 テーマの class をキーに読み取り結果を覚え、useSyncExternalStore から読む。 remount が起きないので play の状態を壊さない。境界条件 5 件のテストを付けた。 vitest の storybook project は themes: { light: "" } で class を動かさないため、 この経路は 367 件の green を一度も通っていなかった。 ## 重要 2 件 - derive-dev-port.sh が linked worktree で base を無視していた。STORYBOOK_PORT が DEV_PORT と同値になり、同じ worktree で serve と storybook を同時に起動できなく なっていた。base 相対 (hash % 999 + base + 1) にし、worktree で 3638 / 6644 に 分かれることを実測。この壊れ方を検出するテストを足した - static を styles.css から外し、.storybook/preview.css が同じファイルを theme(static) 付きで読み直す形にする。@import に theme() を付ける形は公式に 文書化されている (tailwindcss.com/docs/preflight「Disabling Preflight」)。 「公式 docs に無い」と書いた ADR の却下理由は誤りだった 本番 CSS は 140,064 から 138,889 バイトへ減り、未参照の --color-* 別名は 0 件に なる。この template から作られる全プロジェクトが払っていた分が消える。代償は Tailwind 既定の未定義トークンがカタログに混ざること (Radius 2 件 / Typography 6 件。Colors は --color-*: initial が効いていて増えない) ## 軽微 4 件 - styles.css と styling.md の記述を新しい形に揃える - css-rules の「実測で 4 件」を、測り方 (--tw で始まる名前が並ばないこと) へ - isRootScoped の catch にテストを足す。root の型を必要な形へ絞り、型アサーション なしでダブルを渡せるようにした - css-color の context 取得失敗を覚える。??= は null をキャッシュせず、失敗のたびに canvas を作り直して warn がトークンの数だけ出ていた。warn も 1 度だけにする Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 重要 3 件 (いずれも ADR-0022 の記述が実装・実測と合っていない) - 節 6 が撤去済みの key remount を指示したままだった。外部ストアの記述へ。 段階 2 / 3 で story を書く人が読むのはここなので、残すと同じ形が再発する - 「1,175 バイト」が再現しない (clean な A/B では 1,469)。2 つの build が static 以外も違っていた。数値を外し、測り方へ置き換える - 例に挙げた変数が static 無しでも本番 CSS に残っていた。原因は下記 ## source scan がドキュメントを読んでいた Tailwind の source scan は Markdown も読む。ADR と rules に書いた class 名・変数名 がそのまま本番 CSS へ出ていた。ADR 自身が、ADR の挙げた「出ない例」を出力させて いたことになる。 styles.css へ @source not で docs/ と .claude/ を除く。本番 CSS は 138,851 から 136,164 バイトへ減る (2026-09-20 実測、2,687 バイト)。docs/registry-baseline/ の .tsx も対象で、あれは上流の記録であって描画されない。 ブラウザテストが算出スタイルを固定しているため、utility の欠落は検出される。 377 件すべて通る。 ## 軽微 3 件 - css-color に createColorParser の注入点を作り、失敗経路を 3 件のテストで固定 した。context 取得は 1 度だけ / warn も 1 度だけ / 全件を非色として返す。 module 級の singleton のままでは機械で守れなかった - README に Storybook を足す。port の節も base 相対の記述へ。ADR-0002 の [env] の例にも STORYBOOK_PORT を足す - styling.md の paths へ .storybook/** を足す。新設した preview.css を触るときに 規約がロードされなかった Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 重要 2 件 - 3 巡目 #1 (ADR の節 6 が撤去済みの key remount を指示したまま) が未対応だった。 c838437 で「書き換えた」と報告したが、置換スクリプトが別の箇所で例外を投げて 書き込みに到達していなかった。確認せずに完了として報告した - source scan の絞り込みを denylist から公式の allowlist へ替える。@source not の 列挙では .claude/skills -> .agents の symlink を取りこぼしており、実体はまだ scan されていた。vite.config.ts のコメントや CI の yaml の字面も残っていた @import "tailwindcss" source("../src") にすると 126,040 バイト。絞る前の 138,851 から 12,811 バイト減る。落ちる selector は全件 src/ の外に由来する (レビュアーが 43 件を全数突き合わせ) ## 軽微 3 件 - ADR の「2687 バイト」が @source not 単体では再現しない (単体なら 2,648)。 同じ commit で消した変数名の 39 バイトが混ざっていた。数値を測り方へ置き換える。 再現しない数値を書くのは 3 巡目に続いて 2 度目 - テストが console.warn を mock しておらず、実行のたびに warn が出力へ混ざっていた - import 時に document を触らないことがテストで固定されていなかった ## 測ったが採らなかったもの src/**/*.stories.tsx を scan から外すと 171 バイト減る。denylist の取りこぼしを 理由に allowlist へ替えた直後に @source not を 1 行戻すことになるため採らない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
source("../src") は .storybook/ を対象から外す。decorator や preview に class を
書いても utility が生成されず、スタイルが当たらないことに書いた人が気付けない。
前回「明示しても拾われない」と報告したが誤り。grep のパターンをシェルの引用符で
壊しており (p-\[13px\] が p-[13px] として文字クラス扱いになっていた)、0 件という
結果は grep の誤りだった。対照を src 側に置いて測り直すと、@source を足せば
両方とも生成される。
本番 CSS は 126,040 バイトのまま変わらない。.storybook/ に class を書いた
ファイルが現状ないため。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
トークン一覧から非色 (spacing / font / animation 等) を選り分ける述語を、canvas の
fillStyle へ既知の値を代入して変化を見る自作実装から、標準の
CSS.supports("color", value) へ置き換える。
実トークン 77 件で両者の判定は完全に一致した (2026-09-20、chromium 上で
getComputedStyle の解決後の値を両実装へ通して比較)。computed 値に inherit や var()
が残るケースは 0 件で、CSS 全体キーワードによる false positive は起きない。
canvas の 2D context を取得できない失敗経路と、その warn-once、テストからの注入点
ContextFactory、失敗経路専用のテスト 4 ケースは CSS.supports には存在しないため
消える。
ADR-0022 の却下リストにこの選択肢は無く、標準 API を確かめる前に canvas へ行っていた。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
種別ごとに store を持つのをやめ、全トークンを読む store 1 つに寄せる。 collectRootCustomProperties の prefix は集めた名前の絞り込みにしか使われず、どの rule を 辿るかを変えない。種別ごとに store を分けると、テーマを切り替えるたびに同じ CSSOM 走査と getComputedStyle を種別の数だけ繰り返すことになる。 あわせて 3 箇所へ写していた「store を購読して warnIfEmpty を呼ぶ」2 行を useTokenList へ 集約し、readTokenNames / rootStyle / resolvedWith / readTokens の 4 つを readAllTokens 1 つへ 畳む。useThemeTokens が引数の型を ReturnType<typeof createThemeSnapshotStore<...>> で 再導出していたのも、store が 1 つになって引数ごと不要になった。 件数は変えていない。Colors 35 / Radius 9 / Typography 13 (全 241 トークン中、 2026-09-20 に vp test run --project storybook で実測)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
source() の絞り込みと @theme inline に static を付けない判断について、styles.css の コメントと ADR-0022 がほぼ同文を持っていた。片方だけを直すと説明が食い違う。 styles.css には、そのファイルを編集する人が破りうる規範だけを残す。source() の外で utility を書く場所は個別に足すこと、static をここへ付けないこと。根拠と効果の測り方は ADR-0022 が持つ。 styles.css にしか無かった出典 (Detecting classes in source files) は ADR-0022 の参考へ移す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dev server と Storybook の port は git から worktree 名を取って導出するため、 1 回あたり git のサブプロセスを 4 つ起動する。これを .mise.toml の [env] に 2 本置くと、 mise が env を解決するたび (シェル hook の下ではディレクトリへ入るたび) に 8 つ走る。 読み手は serve と storybook のタスクしかいないので、タスクの run の中で導出する。 2026-09-20 の実測で mise hook-env は 70ms から 28ms になった。副次的に、シェル hook を 入れていない手元でも port が決まるようになる。 ADR-0002 の「値の置き場所を 3 つに分ける」に、解決コストのかかる値を [env] へ置かない 判断を足し、[env] の例から port を外す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
「アプリのコードと同じディレクトリに置くが、アプリのコードではないファイル」の種別が 4 箇所に別々の方言で手書きされていた。lint の適用外 (vite.config.ts) と coverage の除外 (vitest.config.ts) は glob、registry baseline の突き合わせ (registry-baseline.test.ts) は regex で、いずれも同じ集合を指す。 scripts/lib/companion-files.ts に種別の一覧を 1 つ置き、3 箇所をそこから導出する。 種別を足すときに 1 箇所を書き忘れる事故そのものが起きなくなる。 検査を足す形は採らない。vitest.config.ts の coverage.exclude は今どの検査からも見られて いないが、現状の値に違反は無く、coding-discipline.md の「1 回走らせて違反件数を数える。 0 件なら作らない」に当たる。ズレを検出するより、ズレる経路を消す。 lint-config.test.ts は導出しない。lint の解決結果と突き合わせる期待値なので、同じ定数から 両辺を作ると入力どうしの比較になり常に通る。vite.config.ts の DESIGN_SYSTEM_LAYERS と 同じ扱い (設定側は共有定数、検査側は手書き)。 導出した glob が従来のリテラルと同一であることは、書き換え前の lint-config.test.ts の toEqual(EXPECTED_OVERRIDES) が通ることで確かめた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AGENTS.md に決定・理由・実測値をまとめて書いていたが、agents-md-maintenance.md の 「書くべきでないもの」に「実測値の詳細。判断を決める閾値だけ書き、根拠の実測は ADR へ 置く (ADR-0001)」とある。AGENTS.md は毎セッション context に載る媒体なので、そちらの 方針に従う。 ADR-0023 が持つもの: CLI と MCP の比較、決め手 (MCP の登録は URL を 1 つしか持てず、 worktree ごとに変わる port へ配れない)、起動の要否の実測、find-by-component が未起動だと 空を返す落とし穴、上流 skill と ADR-0022 の食い違い。 AGENTS.md に残すもの: storybook skills を起点にすること、--help に出ないこと、props は docs tools で答えること、find-by-component は起動してから指すこと、ADR-0023 への参照。 手書き行数は 47 から 35 になった (目標 50 / 上限 100)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/code-review が挙げた 2 件を直す。どちらも silent failure で、実測で再現した。 1. .mise.toml の port run の引数に $(...) を書いていたため、script が実行できないと空文字が渡り、Vite の 既定 port で起動して worktree の分離が無言で外れていた (chmod 000 で PORT=[] を実測)。 mise のタスクは set -e 付きで走る (公式 docs) が、置換の失敗は引数の中では コマンドの exit status にならない。 mise 公式のタスク単位の env へ移す。exec の template がそのまま使え、script が失敗すると タスクごと止まる。トップレベルの [env] へ戻すわけではないので、env 解決のコストも 増えない (mise hook-env は 28ms のまま)。3 点とも実測した。 2. lint-config.test.ts の付随ファイルの除外 excludeFiles から companionGlobs の分を無条件に差し引いていたため、glob を 1 本消す 壊し方が無検出だった (**/*.test-helpers.tsx を消しても 18 passed)。差し引きは「付いて いるか」を見ないので、別の override へ付ける壊し方と同じ検査で受け持つ形にし、全種類 そろっていることを固定する。 mutant 3 種で確認した。glob を 1 本消す / 層へ glob を足す / ルールを off は、いずれも Tests 1 failed | 17 passed (18) で落ちる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/code-review の 2 件。どちらも silent failure で、実測で再現した。 1. a11y 検査が light だけだった defaultTheme が "light" で globals の matrix が無く、dark の contrast は一度も検査されて いなかった。parameters.a11y.test: "error" が保証しているように見えて、していない。 addon-vitest の公式パターン (型定義に「define one Vitest project per theme」と明記) に 従い、storybookTest へ initialGlobals を渡す project を theme ごとに作る。project 名は storybook-light / storybook-dark。追跡対象に --project storybook の参照は無い。 dark の --muted-foreground を背景へ寄せる mutant で確かめた。storybook-dark だけが color-contrast で落ち、storybook-light は 3 passed のまま通る。 2. カタログが空でも story が通った 一覧の中身は styles.css と CSSOM の走査で決まるが、story は描くだけで assertion が無く、 0 件でも a11y 違反ゼロで緑になっていた。warnIfEmpty の warn はテストを落とさない。 3 story に play を足し、トークン名 (-- で始まる文字列) が 1 つ以上描かれることを見る。 走査を空配列へ差し替える mutant で 3 story とも落ちることを確かめた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/code-review の 3 件と、telemetry の設定。 1. @import が指す stylesheet を辿っていなかった CSSImportRule は CSSGroupingRule を継承しないため、走査から漏れていた。漏れた宣言は 「トークンが無い」のと区別が付かない。辿るようにし、テストを 1 件足す。 2. try/catch が走査全体を覆っていた 深い位置の失敗でその stylesheet の残りが丸ごと落ち、warn は最上位の href を指していた。 catch を cssRules の取得だけに絞り、捨てていた error も warn へ載せる。読めない @import が あっても同じ stylesheet の他の宣言が残ることをテストで固定する。 あわせて ReadableStyleSheet.cssRules を Iterable<CSSRule> へ広げた。テスト側が CSSRuleList を型アサーションで作る必要がなくなる (typing.md はアサーションを禁じている)。 3. CSS の HMR でカタログが古いまま残った stylesheet の中身だけが差し替わると <html> の class は動かないので、MutationObserver では 拾えない。Vite 公式の HMR API (vite:afterUpdate) を購読する。本番では import.meta.hot が 無く、ガードごと落ちる。 4. telemetry を切る 既定で有効で、実行したコマンド・バージョン・addon 一覧・story とコンポーネントの件数を 送る。このテンプレートから作られる全プロジェクトへ配られる設定なので、envDir: false や disable_tools と同じく明示で潰す側に揃える。理由は ADR-0022 が持つ。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/code-review の 4 件。 11 / 13) console.warn と絞り込みが render 中に走っていた useTokenList が render のたびに select を呼び、dropRedundantColorAliases の warn が 再 render ごとに出ていた。種別ごとの絞り込みを store の read へ移し、テーマが変わった ときだけ走らせる。0 件の警告も read 側に 1 つだけ置く。個別 story の欠落は play の assertion が受け持つ。 12) storybook project が browser.viewport を持たなかった vitest.browser.config.ts は「既定 viewport の SSOT」と宣言しているのに、storybook 側は Playwright の既定へ落ちていた。2 つの browser project が別の baseline で走る。 寸法だけを src/test/viewport-sizes.ts へ切り出し、両 config と src/test/viewport.ts が そこから読む形にする。src/test/viewport.ts を直接 config から import すると 「vitest/browser can be imported only inside the Browser Mode」で config の読み込みごと 落ちる (2026-09-20 実測)。リテラルとコメントで対にしてあったのはこのためだった。 9) routeFileIgnorePattern が付随ファイルの 4 つ目の消費者だった companion-files.ts の docstring は「種別を足すときはそこだけを直す」と書いているのに、 vite.config.ts の routeFileIgnorePattern だけが \.test(-helpers)?\.tsx?$ を手書きして いた。story を route の隣へ置くと「Route を export していない」で毎回ビルドが落ちる。 companionFilePattern() を足して導出する。単体テストも足した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/code-review の残り 2 件。 1) stories glob と規範が食い違っていた .storybook/main.ts の stories は src/components/** だけを見るのに、 directory-structure.md は「部品と同じディレクトリに置く」としか書いていなかった。 features/ や routes/**/-components/ へ置くと、Storybook も vitest の project も拾わず、 a11y 検査ごと無言で外れる。 glob を広げず規範を絞る。範囲は spec で「/components 配下だけ」と決めてあり、広げると 段階 3 の対象も変わる。広げるかどうかは、そこへ story を書きたくなった時点で決める。 14) TYPOGRAPHY_SAMPLES が styling.md の表と二重管理だった story 側のコメント自身が「表を変えたらここも合わせる」と書いていた。 directory-structure.md は cva から argTypes への複製に型強制を要求しているが、markdown と code を突き合わせる手段は無く、同じ形にできない。 トークンの story は CSS 変数の値を見せる場所で、class の規範は styling.md が持つ。 複製を消して出処を 1 つにする。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@source は書いた stylesheet からの相対で解決し、scan の対象は CSS entry ごとに 閉じる (tailwindcss 4.3.3 の dist で確認)。本番 entry の src/styles.css に @source "../.storybook" を置くと、.storybook に class を書いた時点でそれが 出荷 CSS へ乗る。3 行目の source("../src") で絞った目的と逆を向くため、 .storybook/preview.css へ @source "." として移す。 移設で挙動は変わらない。本番 CSS は md5 150c1af97e75a1f0f1a4029535860f73 で 移設前と一致し、Storybook 側の候補集合も 2783 件で双方向の差分が 0 件だった。 現時点の .storybook/ 3 ファイルに className は 0 件で、scan が拾っていた 15 件は main.ts の設定キーと addon のパッケージ名だけだった。 preview.css の出典も補う。theme() が import 単位で効くことの出典は Preflight の 「Disabling Preflight」で正しいが、参照された変数だけを出力する既定と static の 出典が欠けていた。ADR-0022 の出典一覧にある /docs/theme を併記する。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
afterEach を describe("collectRootCustomProperties") の中に置いていたため、
兄弟の describe("解釈できない selector") が張った spy が restore されず、
後続のテストへ残っていた。vitest 4.1.11 の既定は clearMocks / restoreMocks /
mockReset とも false (node_modules/vitest/dist/chunks/defaults.9aQKnqFk.js:51-53)
なので、config 側が拾うこともない。
漏れは probe で実測した。afterEach を持たない describe で spy を張ると、後続の
describe で vi.isMockFunction(console.warn) が true のままになる。
afterEach をファイル冒頭へ移す。複数の top-level describe を持つテストでは
src/components/parts/form-fields.test.tsx:20 が同じ形を採っている。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vite:afterUpdate を購読していたが、getSnapshot のキャッシュキーが className だけ だったため、stylesheet の差し替えではキーが動かず同じ参照が返っていた。React は Object.is で snapshot を比べて再 render の要否を決める (react-dom-client.development.js の checkIfSnapshotChanged) ので、購読していても 再 render に至らず、カタログはトークンを編集しても古いまま残っていた。 キーに世代を混ぜる。React 公式は「How you determine whether mutable data has changed depends on your mutable store」と書いており、変更の検知手段は store 側が 定義するものなので、class だけを代理に使う形をやめる。vite:afterUpdate は CSS の load 完了後に発火する (vitejs/vite#9810) ため、この時点で読み直せば新しい値が取れる。 変更の契機は store に 1 つだけ持ち、そこから購読者へ配る。購読者ごとに観測すると 1 回の変更で購読者の数だけ世代が進み、その数だけ read が走る。tokens.stories.tsx が 「走査と警告はテーマが変わったときだけ起こしたい」と書いている意図に反するため、 購読者 2 件で読み直しが 1 回に収まることをテストで固定した。 hot を第 4 引数に出した。Vitest は server.hmr: false で vite:afterUpdate を配らず、 差し替え口が無いと HMR の経路をテストで固定できない。第 3 引数の root と同じ流儀。 テストは 5 件から 8 件へ。既存 5 件の書き換えは無し。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ColorSwatches は解決後の値の文字列で塗り、RadiusTokens だけ var(--name) で塗って いた。同じファイルの中で 2 つの流儀が並ぶ状態だったので、値で塗る側へ揃える。 var() 側へ揃えない理由は 2 つある。Storybook 同梱の ColorItem (v10.6.0) と UX-and-I/storybook-design-token は、どちらも表示している値の文字列で塗っており、 トークンの種類をまたいで流儀を 1 つにしている。もう 1 つは、var() で塗ると 「塗りは新しいがラベルは古い」状態が起こりうることで、これは目で気付けない。 見た目は変わらない。getPropertyValue は var() を解決済みの文字列を返すため、 そのまま borderRadius へ渡しても同じ寸法になる (chromium で実測)。 --radius-sm="calc(0.625rem - 4px)" 適用後="6px" --radius-2xl="calc(0.625rem * 1.8)" 適用後="18px" Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rule.styleSheet?.cssRules ?? [] は、styleSheet が null のときに空の rule 列を返して いた。readRules は例外を拾って warn するが、null は例外ではないので通り抜ける。 結果として、その stylesheet のトークンが 1 件も無いのか、読めなかったのかを 区別できないまま一覧が出ていた。 null のときに warn する。ただし無条件には出さない。@import url(...) supports(...) の条件が成立しないときは取得自体が起きず、null が正常な結果になる (w3c/csswg-drafts#8608)。supportsText を見て、条件が成立しない場合は黙って飛ばす。 styleSheet の getter は cross-origin の stylesheet で例外を投げるため、読み取りごと readRules の中に入れた。外に出すと、これまで warn して続行できていた経路で走査 全体が止まる。 テストは 12 件から 14 件へ (+2 case)。null で warn すること、supports() 不成立では warn しないことをそれぞれ固定した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1b4dbde が「[env] だと env 解決のたびに git のサブプロセスが走る」を書き、 3d11c51 が実装を exec() テンプレートへ変えた際に「$(...) だと script の失敗時に 空文字が渡る」を別段落として足したため、同じ結論を別の理由で述べた段落が 2 つ 並んでいた。片方を編集したときにもう片方が取り残される形になっている。 両方の独自情報 (worktree の bind 失敗、シェル hook 下での env 解決、$(...) の silent failure、fail-closed) を残して 1 段落にまとめる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
59ace6a が DEFAULT_VIEWPORT と TABLET_VIEWPORT を viewport-sizes.ts へ移し、 SHORT_VIEWPORT と NARROW_VIEWPORT は viewport.ts に残した。分割の理由は viewport-sizes.ts の docstring が書くとおり「config が読むため」だが、config が 実際に import しているのは DEFAULT_VIEWPORT だけで、TABLET_VIEWPORT は他の 2 つと 同じくテストからしか使われていない。基準と実態が食い違うため、次に寸法を足す人が どちらへ置くか判断できない。 寸法は 4 つとも viewport-sizes.ts へ置き、viewport.ts は browser mode の操作と アサーション専任にして寸法を re-export する。docstring も、config が読むのは DEFAULT_VIEWPORT であると具体的に書き直す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.storybook/** は lint 可視性の検査 (SOURCE_ROOTS) と .claude/rules/styling.md の paths には入ったが、shadcn/no-restyle と shadcn/require-static-classes の override は files: ["src/**"] のままだった。同じ違反が src/ では止まり .storybook/ では素通り する状態で、2 つの設定が「.storybook を規約の対象とみなすか」で食い違っていた。 両方の壊し方で確認した。design system component へ動的な className を渡すコードを .storybook/preview.tsx へ置くと、files が src/** のときは診断ゼロ、.storybook/** を 足すと src/ に置いたときと同じ診断が出る。 .storybook/preview.tsx:25:47: error shadcn(require-static-classes): Dynamically built className on <Button> cannot be checked. ADR-0020 は「適用範囲の決定はこの ADR が持つ」と書いているため、.storybook/ を 消費側として扱う旨を Decision へ追記した。lint-config.test.ts の期待値も同期する (files 配列を厳密比較しているため、同期しないと検査が落ちる)。 no-restricted-imports の override (files: ["src/**", "scripts/**"]) は広げない。 story 専用コードの import を止める規則で、.storybook/ はその参照が正当な唯一の 置き場になる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
コメントは「class-variance-authority / cn は tokens.stories.tsx が page-title
(pageTitle) を経由して依存する」と書いていたが、909f65d が tokens story から
pageTitle の import を外しており、3361b5b 時点の story グラフから到達しない。
@tanstack/react-query も到達せず、@tanstack/react-form はコメント自身が「後続 Task で
story が付く見込み」と先取りであることを認めていた。
到達しない名前を include に書いても Vite は解決できてしまうため警告が出ず、
configHash の入力にだけ残る。無関係な変更で deps キャッシュ全体が無効になる。
axe-core だけ残す。addon-a11y の preview が import("axe-core") で読むため
(dist/_browser-chunks/chunk-P5J2FJ2Z.js:71)、静的な走査に出ない。story から辿れる
依存は Storybook 10.6 が story と preview annotation を optimizeDeps.entries へ積む
(storybookjs/storybook#33875) ので、手で並べる必要がない。
PR #35 時点の story は tokens.stories.tsx の 1 件で、その import は
@storybook/tanstack-react / react / storybook/test とローカルの *.story-helpers 3 件
だけであることを実測した。後続の段で story が増えたら、その段で必要な依存を足す。
storybook-light / storybook-dark とも 6 件 pass。再最適化は config 変更直後の 1 回
だけで、以降は "Vite unexpectedly reloaded a test" も React 二重解決も出ない。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
299553e で「supports() の条件が偽なら null が正常なので warn しない」分岐を入れたが、 実ブラウザでその状態を作れないことが分かった。chromium 153 で確かめたところ、 - 構文が不正な条件 (supports(display: nonsense-value)) の @import は rule ごと消える - 条件が偽の複合条件 (supports((display: grid) and (display: nonsense-value))) は rule として残るが styleSheet は非 null。取得している つまり w3c/csswg-drafts#8608 の議論はまだ実装に入っておらず、この分岐は通らない。 守っていたテストも Object.create で作った、実ブラウザでは起こりえない状態を 固定していた。 分岐を落として null なら常に warn する。supportsText は payload に残すので、 将来 null を返す実装が出ても読み手が切り分けられる。 テストは 14 件から 13 件へ。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
観測を張るかどうかを listeners.size === 1 で判定していた。listeners は Set なので 同じ関数を 2 回渡すと重複が落ちて size が 1 のままになり、条件が再び真になって MutationObserver と vite:afterUpdate の listener が二重に張られる。解除しても listeners.delete が 1 回で size 0 になるため、片方が観測を続けたまま残る。 observer の有無で判定する形に変える。 「購読者が複数でも読み直しは 1 回」のテストも直した。同じ関数を 2 回渡していたため Set が重複を落とし、購読者が 1 件しか作られていなかった。主張していた性質を 検証できていない。別の関数を 2 つ渡す形にする。 二重登録は Set の hot スタブでは見えないので、配列で受けるテストを足した。 React は hook ごとに別の関数を渡すため、実アプリでこの経路は通らない。 テストは 8 件から 9 件へ。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
299553e のコメントは「styleSheet の getter は cross-origin の stylesheet で投げる」と 書いていたが、出典を確認していなかった。MDN の CSSImportRule.styleSheet に例外の 記述はなく、SecurityError を投げると書かれているのは cssRules のほうである (CSSOM「If the origin-clean flag is unset, throw a SecurityError exception.」)。 テストも getter が投げる形で模擬しており、仕様に書かれた経路 (非 null の sheet を 返して cssRules が投げる) を固定するテストが 1 件も無かった。b34c196 で「実ブラウザ では起こりえない状態の固定」を理由にテストを落としたので、基準を揃える。 実測もした (chromium 153、2026-09-20)。 data: URL (origin-clean): styleSheet=非null / cssRules 取得OK 取得に失敗した相対 URL: styleSheet=null null になるのは取得の失敗で、getter は投げなかった。null の警告を残す根拠は この実測にある。実装は読み取りを readRules の中に置いたままなので、getter と cssRules のどちらが投げても走査は止まらない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5e29afb で 2 段落を 1 つにまとめた際、統合前の「タスク側なら起動するときだけ走り、 シェル hook の有無にも依存しない」の後半が落ちていた。env の解決がシェル hook の 設定に左右されない点は、タスクの env を選ぶ理由の 1 つなので戻す。 3409c9c で寸法を viewport-sizes.ts へ移したあと、viewport.ts の docstring が 「viewport 定数・操作・アサーション」のままで、定数を自前で持つように読める。 操作とアサーションを持ち、寸法は再 export であることを書く。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2cb2d98 で「story グラフから到達しないから不要」として 5 件を axe-core だけに 絞ったが、判断の前提が誤っていた。あの一覧は story が import する依存の列挙では なく、2 つの project が 1 つの deps キャッシュを共有する構成を成り立たせていた。 storybook-light と storybook-dark は同じ configDir を渡すため、addon-vitest が configDir のハッシュから cacheDir を導く結果 (vitest-plugin の oneWayHash(configDir))、deps キャッシュが 1 つになる。事前宣言が足りないと、 両者が実行中に別々の依存を見つけて互いのキャッシュを無効化し合う。 story 53 件の段 (storybook-ui へ取り込んだ状態) で実測した。 この一覧なし + 2 project : Test Files 62 failed | 44 passed / reloaded 8 この一覧あり + 2 project : Test Files 106 passed / reloaded 0 どちらでも 1 project なら : Test Files 53 passed / reloaded 0 PR #35 の段では story が 1 件しかなく、両 project の依存集合が同じになるため どちらでも通る。到達性だけを見て減らすと、後続の段で落ちる。 コメントも書き直した。2cb2d98 以前の「tokens.stories.tsx が page-title 経由で cva に依存する」という記述は 909f65d の時点で成り立っておらず、そこの指摘自体は 正しかった。理由を実際のもの (project 間の deps キャッシュ共有) へ差し替える。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shnakatani
added a commit
that referenced
this pull request
Sep 20, 2026
PR #35 側で書いた docstring は vitest.browser.config.ts と vitest.storybook.config.ts の 2 つが DEFAULT_VIEWPORT を読むと書いていたが、 storybook-ui は vitest.storybook.config.ts から viewport 指定を外している (addon-vitest の setViewport が story ごとに page.viewport() を呼ぶため)。 取り込んだ結果、読み手が 1 つになったので記述を合わせる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shnakatani
added a commit
that referenced
this pull request
Sep 20, 2026
PR #35 が squash で main へ入ったため、同じ内容を別のコミット列で持つ storybook-parts との間で 3 件衝突した。いずれも storybook-parts 側を採った。 判断の根拠は次のとおり。 docs/decisions/0022-storybook-as-state-catalog.md storybook-parts が完全な上位集合で、main にしかない行は 0 件。 .claude/rules/directory-structure.md main の 25 行目「story は部品と同じディレクトリに置く。src/components/ui/ も 同じで、baseline の対象外になる (ADR-0006 / ADR-0022)」を、6ef804a が title を 書かない規範へ書き換えた際に落としている。落ちた 2 つの事実は別の行が持つので 復元しない。ui/ にも置くことは 26 行目「story を置けるのは src/components/ 配下 だけ」が、baseline の対象外であることは 64 行目「付随ファイルの種別は companion-files.ts が唯一の定義で、registry baseline の突き合わせはそこから 導出する」が持つ。 vitest.storybook.config.ts main は PR #35 時点の形で、cacheDir を project ごとに分ける修正を持たない。 storybook-parts 側が新しい。 docs/decisions/README.md は自動マージされ、#39 が足した記述は残っている。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shnakatani
added a commit
that referenced
this pull request
Sep 20, 2026
PR #35 側で書いた docstring は vitest.browser.config.ts と vitest.storybook.config.ts の 2 つが DEFAULT_VIEWPORT を読むと書いていたが、 storybook-ui は vitest.storybook.config.ts から viewport 指定を外している (addon-vitest の setViewport が story ごとに page.viewport() を呼ぶため)。 取り込んだ結果、読み手が 1 つになったので記述を合わせる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shnakatani
added a commit
that referenced
this pull request
Sep 20, 2026
PR #35 側で書いた docstring は vitest.browser.config.ts と vitest.storybook.config.ts の 2 つが DEFAULT_VIEWPORT を読むと書いていたが、 storybook-ui は vitest.storybook.config.ts から viewport 指定を外している (addon-vitest の setViewport が story ごとに page.viewport() を呼ぶため)。 取り込んだ結果、読み手が 1 つになったので記述を合わせる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shnakatani
added a commit
that referenced
this pull request
Sep 20, 2026
* chore: registry の baseline 検査から付随ファイルを除く Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: Storybook の基盤を置く .storybook/ で Storybook v10.6.0 を @storybook/tanstack-react framework で起動できるようにする。story はまだ置かない (Task 5 以降)。 - tsconfig.json の include に .storybook/**/*.ts(x) を追加。TypeScript の ** glob はドットディレクトリを暗黙に除外するため、追加しないと vite-plus/client の ambient module 宣言 (declare module "*.css") が 適用されず、preview.tsx の side-effect import が TS2882 で失敗する Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: story を Vitest の project として走らせる Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: vitest.storybook.config.ts に optimizeDeps.include を足す (レビュー指摘対応) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: コントラスト比の計算を切り出す Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test: コントラスト比のテストに係数とガンマ補正の検証ケースを追加 (レビュー指摘対応) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: デザイントークンの story を足す styles.css が SSOT なので story 側に値を写さず、getComputedStyle で解決後の値を 読む。実装中に brief の前提コードでは対応できない挙動を 3 点実測で発見し、 本 PR 内で対応した。 - styles.css の色は oklch() で定義されているが contrastRatio (src/lib/contrast.ts) は rgb() しか解析できないため、コントラスト比が全行 null になる。Canvas 2D の fillStyle で rgb() へ正規化する toRgb() を追加した - ColorTokens は getComputedStyle を mount 時に 1 回読むだけで、 withThemeByClassName によるテーマ切り替え (document.documentElement への直接の class 書き換え、React の state/props を経由しない) を検知できず、dark へ切り替え ても light の値のまま固まる。useSyncExternalStore で <html> の class を購読する useHtmlClass() を追加した - 色見本の <span aria-hidden> だけでは jsx-a11y/control-has-associated-label が td 自体 (th と同様に対象) をラベル無しの control とみなして落ちる。sr-only の テキストを添えた Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore: Task 5 のレビュー指摘 4 件を本対応する Critical 1 件 (readRootTokens が @layer を辿らずトークンを取りこぼす)、 Important 3 件に対応した。 - styles.css の @theme inline を @theme static inline にし、未使用トークン (--font-heading 等) も出力させる (readRootTokens で static 無し 70 件 → static あり 109 件、自分で分離実測) - readRootTokens を CSSGroupingRule 再帰に直し、@layer theme の中の :root も辿る - ColorTokens の振り分けを denylist から isColor() (色として解析できたものだけ) に 変更。振り分け用の静かな述語 isColor と、解析失敗時に warn する toRgb を src/lib/css-color.ts へ切り出し、単体テスト (5 ケース) を追加した - 各 story に 0 件検出の warn を追加 - TYPOGRAPHY_SAMPLES のページ見出し行を pageTitle() 呼び出しに変更 (page-title.tsx との二重管理を解消) - 色見本を「解決後の値」セルに同居させ、sr-only の重複読み上げと jsx-a11y/control-has-associated-label 違反を解消 (列を 4 → 3 に変更) - pageTitle 経由の class-variance-authority / cn 依存追加に伴い、 vitest.storybook.config.ts の optimizeDeps.include へ追加 (事前バンドル漏れで 3 story 全滅を実測) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore: PR #? タスク5 re-review 指摘 3 件を本対応 - styles.css の static/inline コメントを事実と合う内容に短く書き直した (spacing/text/tracking トークンが見えたのは @layer 再帰の効果で、static とは 無関係だった)。内訳・使用箇所・バイト数は書かず ADR-0022 (Task 6) に譲る - Tokens/Colors の --color-X (Tailwind の @theme inline が生成する var(--X) の 別名) を、同じ集合に生トークン --X が在るときだけ落とす filter を追加。 --color-black/white は対応する生トークンが無いため残る。Colors が 68 → 35 行になった - tokens.stories.tsx のコメントの「63 件」を自分で実測し直し 27 件に修正 (denylistKept 95 / isColorKept 68 / leaked 27) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore: Task 5 の re-review 指摘 3 件を本対応する - --color-X の除外を dropRedundantColorAliases として src/lib/ へ切り出し、境界条件の 単体テストを付ける (coding-discipline.md「純粋関数の切り出し」) - 別名と生トークンの値が食い違うときは落とさず warn する。落としてよい根拠は var() 経由で 値が同じことであって名前の綴りではなく、黙って消すと一覧自身が drift を隠す - コメントから動く数値 (27 件 / 34 件 / 109 件) を外し、機序だけ残す。実測値は ADR-0022 が 日付付きで持つ (doc-hygiene.md「数値はその数値が動く要因で判断する」) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: Storybook の決定を ADR-0022 として記録する Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * chore: Task 6 のレビュー指摘 8 件を本対応する spec の取りこぼし 4 件: - Decision 1 へ server-only 依存の __mocks__ 遮断 - Decision 2 へ variant を代表値 + argTypes で表す歯止め - 検討した選択肢へトークン専用 addon の却下 - Consequences へ storybook/test の expect が持たない matcher がある事実 事実誤り 4 件: - 節 6 の「static で 4 件が新たに出力される」は誤り。--font-heading / --radius-sm / --radius-lg / --radius-xl は static の有無によらず出る - 同じく CSS のバイト数が実測と合わない。出所のビルドも書かれていなかった - 件数表を削除。値は Tailwind の版・utility の使われ方・story の集合で動き、 過去の測定が 2026-09-20 に再現しなかった。機序と 1 例と測り方に置き換える - 節 9 の段階の内訳が節 2 の対象の層と食い違っていた (1 段目 / 2 段目) 3 文以上の段落 2 つを分割した (節 4 / 節 5)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: ADR-0022 から件数表を消した際の参照漏れを直す f88d5fc で ADR-0022 の件数表を削除したが、tokens.stories.tsx の 2 箇所が 「件数の実測は ADR-0022」と指したままだった。指す先が無くなっている (doc-hygiene.md「陳腐化したポインタは注記を足さず、文ごと削除する」)。 件数への言及を外し、出典キーとしての ADR-0022 だけを残す。 削除前に grep -rn "ADR-0022" src/ を通していれば検出できた (grep-before-edit.md 5「削除 / リネームは全参照更新」)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: ADR-0022 の 3 文以上の段落を箇条書きと表へ割る 9 箇所が対象。Context と節 6 は箇条書き、節 9 の段階の内訳は表にし、 残りは 2 文以下の段落へ分けた。決定の内容は変えていない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: story だけが使うロジックを src/lib から出し、隔離する contrast / css-color / theme-tokens はアプリから 1 箇所も import されて おらず、tokens.stories.tsx と自分のテストだけが使っていた。src/lib/ は プロダクトコードの置き場所なので、読んだ人はアプリのコードだと受け取る。 今は bundle に入っていないが、それを止めるものが無かった。 置き場所と命名は既存の *.test-helpers.ts に合わせた。Storybook 自身も 同じ形を使っている (code/core/src/manager/components/sidebar/ Filter.story-helpers.tsx)。Storybook 公式の推奨は無い (storybookjs/ storybook#13244 は回答なしで not planned)。 - src/lib/{contrast,css-color,theme-tokens}.ts -> src/components/ {...}.story-helpers.ts。テストも同じ名前で移動 - no-restricted-imports の regex を \.(test|story)-helpers$ へ拡張し、 story 自身を excludeFiles へ足す (tokens.stories.tsx が helper を引く) - coverage からも除外する 破壊テスト: アプリのコード (live-regions.tsx) から story-helper を import すると lint が落ちることを確認した。excludeFiles への story-helpers の 追加は、今日は 1 件も効いていない (消しても違反 0 件)。同じ位置の test-helpers は 3 ファイルで効いており、story-helpers も helper が @/test/ を引いた時点で要る。対称性のために残す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: コントラスト計算を culori へ移し、対で出す story にする 自前の contrast は rgb しか読めず、トークンが oklch なので Canvas で ブラウザの色パーサを借りる css-color が要り、そのために純粋な計算まで Chromium で走らせていた。起点は自前実装だったので、そこから畳む。 同じ入力で 6 案を測り culori を選んだ (比較は ADR-0022)。決め手は精度では なく非色トークンの扱いで、culori だけが解析できない値へ undefined を返す。 colord は oklch を解析できないまま 1.000 を返すため採らない。 比較の相手も直す。全トークンを --background 一律と比べていたが、画面上で 重ならない組み合わせの比は閾値の判断に使えない。styles.css の命名が示す 前景・背景の対だけを Tokens/Contrast へ並べる。 - src/components/{contrast,css-color}.story-helpers.ts とテスト 12 case を削除 - theme-tokens.story-helpers.ts へ foregroundPairs を足す (5 case) - テーマ切り替え時は読み取り要素を key で remount する。値は React の依存に 現れないため再 render だけでは React Compiler のメモ化が残り、light の値で 止まる (light 4.35 / dark 5.57 に分かれることで確認) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: --muted-foreground が --muted の上で 4.5:1 を割るのを直す Tokens/Contrast の story が検出した。既存の欠陥で、値は初期 template コミット (fb3433a) から変わっていない。この PR が持ち込んだものではない。 light の --muted-foreground は shadcn 既定の L 0.554 (slate-500 相当) で、 --muted の上で 4.35:1 と WCAG 1.4.3 (4.5:1) を割る。--muted と text-muted-foreground は 11 ファイルが同じファイル内で使う。 palette の ramp を 1 段下げて slate-600 の L にした。--muted 上 6.90:1、 白背景 7.56:1。閾値を跨ぐ最小の値 (L 0.545 で 4.52:1) は採らない。通ること 以外に根拠が無く、以降の token 調整で静かに割り直す。 彩度は registry 既定より低い既存の値を保つ。dark 側は slate-400 の L で 5.57:1 を満たすため触らない。 残る 2 対 (--destructive-foreground / --sidebar-primary-foreground) は このアプリが描画していないため別で扱う。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: culori の parse が # 無しの hex を受けるため色の判定を厳しくする culori へ移した際、色の判定を parse(value) !== undefined にしたが、culori の parse は # の無い hex を受ける (parse("700") が #700 を返す、2026-09-20 実測)。 --font-weight-bold: 700 などの数値トークンが色として Colors に並んでいた (実機で 39 件、正しくは 35 件)。 hex 数字だけの値を先に落とす。CSS の色は必ず # か関数か色名で書く。 判定は theme-tokens.story-helpers.ts へ isColorValue として置き、境界条件の テストを付けた (+3 case)。 撤去した Canvas 版の isColor はブラウザの色パーサを借りていたためこの値を 落としていた。移行で失った性質はこれだけだった。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * revert: コントラストの検算を tokens story から外す 2 つの形を試してどちらも目的を果たさなかった。 - 全トークンを --background と比べる形は、画面上で重ならない組み合わせの比が 並び、閾値を割ったかどうかの判断に使えない - 命名から対を導く形は、命名が「画面で重なること」の代理になっていない。 使われていない対 (--destructive-foreground / --sidebar-primary-foreground) で 不合格を出し、styles.css が値を調整した実在の対を 1 行も出さなかった JS で比を計算する形そのものにも無理がある。ブラウザは sRGB 外の oklch() を clip し alpha を持つ背景は下地と合成するため、描画されない値を計算しても 見え方と一致しない (culori の wcagContrast は clamp しない)。 実ペアを実テキストで描いて axe に判定させる形は成立し、bg-destructive/20 と text-destructive が 3.74:1 で不合格になることを検出した。破壊ボタンの hover の 実在の組み合わせだが、扱いに --destructive の再検討が要るため別で扱う。 culori と @types/culori を外す。src/components/css-color.story-helpers.ts は Canvas でブラウザの色パーサを借りる isColor だけを残す (culori の parse は color-mix() と relative color syntax を解析できず、# 無しの hex を受ける)。 比の計算に使っていた toRgb は落とす。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * revert: --muted-foreground の修正を戻し、トークンの値はまとめて決める 909b3e9 を戻す。値そのものは正しく、--muted の上で 4.35:1 と WCAG 1.4.3 を 割っていたのを 6.90:1 にした。戻す理由は 2 つ。 - 検出した Tokens/Contrast の story を 739dbeb で外したため、この PR に根拠の 見えないトークン値の変更だけが残る - 同じ palette の --destructive も割っており (bg-destructive/20 で 3.74:1)、 トークンの値はまとめて決めるほうが判断しやすい 経緯は ADR-0022 の節 6-1 が持つ。 実機で確認した副作用も残しておく。placeholder (#6b7380 -> #4d5561) が入力済みの 値 (#020618) へ近づき、階層の差が縮む。見分けは付くが、比の改善と引き換えに なっている。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #35 レビュー指摘 14 件を本対応する ## silent failure (4 件) - css-color: sentinel を 2 つにする。1 つ固定だと、その値そのものの色トークンを 「変わらなかった」と読んで無警告で一覧から落とす - css-color: 2D context が取れないときに warn する。全件が非色になり、表に出る のは「0 件」の warn だけで原因が canvas だと分からなかった - css-color: 誰も使わない fillRect と getImageData を落とし、canvas を使い回す。 toRgb の撤去で読み戻しの消費者が居なくなっていた - tokens.stories: テーマ再読込を preview.tsx の decorator へ移す。story ごとに 書き手が覚える形だと、値を読む story を足した人が忘れて静かに止まる ## 設定の食い違い (5 件) - registry-baseline と ADR-0006 の付随ファイルへ *.story-helpers.* を足す。 段階 3 で ui/ に helper を置いた時点で網羅検査が落ちる - no-restricted-imports の regex へ \.stories$ を足す。helper は止まっていたが story 自身の import は素通りで、引く helper ごと bundle に入る経路が残っていた - *.stories.ts を lint の除外と coverage の除外へ。main.ts の glob は .ts も拾う のに 2 箇所が .tsx しか列挙しておらず、3 箇所で食い違っていた - SOURCE_ROOTS へ .storybook を足す。追跡されたのに lint の可視範囲の検査外だった ## 構造 (2 件) - CSSOM の走査を css-rules.story-helpers.ts へ切り出し、境界条件を 9 件で固定 した。selectorText を "," で分ける形をやめ matches() に委ねる。:is(:root, .x) の取りこぼしが消える。ただし root に当たるだけでは Tailwind の "*, ::before, ::after, ::backdrop" まで拾うため (実測 4 件)、任意の要素にも 当たる rule は除く - getComputedStyle を一覧の組み立てごとに 1 つへ畳む ## 規範とツール (3 件) - styling.md が @theme inline のままだった。static を外すとカタログから消える - mise の storybook タスクの port を worktree ごとに導出する。6006 固定では 2 つ目の worktree が bind に失敗する - ADR-0022 の選択肢表へ、static の代替 3 案を却下理由つきで足す。いずれも公式に 文書化されていない経路に乗る。未参照の宣言を全利用者が払う点も明記した Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #35 再レビュー指摘 7 件を本対応する ## 重大 1 件: decorator が play の状態を捨てる dee9d3b で入れた preview.tsx の global decorator を撤去する。 withThemeByClassName は STORY_RENDERED 後の useEffect で class を当てるため、 class の変化は必ず play より後に来る。そこで key による remount を掛けると、 play が開いたダイアログやメニューが閉じた状態で終わる (dark で実測)。 key での remount をやめ、theme-snapshot.story-helpers.ts の外部ストアへ移す。 テーマの class をキーに読み取り結果を覚え、useSyncExternalStore から読む。 remount が起きないので play の状態を壊さない。境界条件 5 件のテストを付けた。 vitest の storybook project は themes: { light: "" } で class を動かさないため、 この経路は 367 件の green を一度も通っていなかった。 ## 重要 2 件 - derive-dev-port.sh が linked worktree で base を無視していた。STORYBOOK_PORT が DEV_PORT と同値になり、同じ worktree で serve と storybook を同時に起動できなく なっていた。base 相対 (hash % 999 + base + 1) にし、worktree で 3638 / 6644 に 分かれることを実測。この壊れ方を検出するテストを足した - static を styles.css から外し、.storybook/preview.css が同じファイルを theme(static) 付きで読み直す形にする。@import に theme() を付ける形は公式に 文書化されている (tailwindcss.com/docs/preflight「Disabling Preflight」)。 「公式 docs に無い」と書いた ADR の却下理由は誤りだった 本番 CSS は 140,064 から 138,889 バイトへ減り、未参照の --color-* 別名は 0 件に なる。この template から作られる全プロジェクトが払っていた分が消える。代償は Tailwind 既定の未定義トークンがカタログに混ざること (Radius 2 件 / Typography 6 件。Colors は --color-*: initial が効いていて増えない) ## 軽微 4 件 - styles.css と styling.md の記述を新しい形に揃える - css-rules の「実測で 4 件」を、測り方 (--tw で始まる名前が並ばないこと) へ - isRootScoped の catch にテストを足す。root の型を必要な形へ絞り、型アサーション なしでダブルを渡せるようにした - css-color の context 取得失敗を覚える。??= は null をキャッシュせず、失敗のたびに canvas を作り直して warn がトークンの数だけ出ていた。warn も 1 度だけにする Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #35 3 巡目のレビュー指摘 6 件を本対応する ## 重要 3 件 (いずれも ADR-0022 の記述が実装・実測と合っていない) - 節 6 が撤去済みの key remount を指示したままだった。外部ストアの記述へ。 段階 2 / 3 で story を書く人が読むのはここなので、残すと同じ形が再発する - 「1,175 バイト」が再現しない (clean な A/B では 1,469)。2 つの build が static 以外も違っていた。数値を外し、測り方へ置き換える - 例に挙げた変数が static 無しでも本番 CSS に残っていた。原因は下記 ## source scan がドキュメントを読んでいた Tailwind の source scan は Markdown も読む。ADR と rules に書いた class 名・変数名 がそのまま本番 CSS へ出ていた。ADR 自身が、ADR の挙げた「出ない例」を出力させて いたことになる。 styles.css へ @source not で docs/ と .claude/ を除く。本番 CSS は 138,851 から 136,164 バイトへ減る (2026-09-20 実測、2,687 バイト)。docs/registry-baseline/ の .tsx も対象で、あれは上流の記録であって描画されない。 ブラウザテストが算出スタイルを固定しているため、utility の欠落は検出される。 377 件すべて通る。 ## 軽微 3 件 - css-color に createColorParser の注入点を作り、失敗経路を 3 件のテストで固定 した。context 取得は 1 度だけ / warn も 1 度だけ / 全件を非色として返す。 module 級の singleton のままでは機械で守れなかった - README に Storybook を足す。port の節も base 相対の記述へ。ADR-0002 の [env] の例にも STORYBOOK_PORT を足す - styling.md の paths へ .storybook/** を足す。新設した preview.css を触るときに 規約がロードされなかった Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #35 4 巡目のレビュー指摘 5 件を本対応する ## 重要 2 件 - 3 巡目 #1 (ADR の節 6 が撤去済みの key remount を指示したまま) が未対応だった。 c838437 で「書き換えた」と報告したが、置換スクリプトが別の箇所で例外を投げて 書き込みに到達していなかった。確認せずに完了として報告した - source scan の絞り込みを denylist から公式の allowlist へ替える。@source not の 列挙では .claude/skills -> .agents の symlink を取りこぼしており、実体はまだ scan されていた。vite.config.ts のコメントや CI の yaml の字面も残っていた @import "tailwindcss" source("../src") にすると 126,040 バイト。絞る前の 138,851 から 12,811 バイト減る。落ちる selector は全件 src/ の外に由来する (レビュアーが 43 件を全数突き合わせ) ## 軽微 3 件 - ADR の「2687 バイト」が @source not 単体では再現しない (単体なら 2,648)。 同じ commit で消した変数名の 39 バイトが混ざっていた。数値を測り方へ置き換える。 再現しない数値を書くのは 3 巡目に続いて 2 度目 - テストが console.warn を mock しておらず、実行のたびに warn が出力へ混ざっていた - import 時に document を触らないことがテストで固定されていなかった ## 測ったが採らなかったもの src/**/*.stories.tsx を scan から外すと 171 バイト減る。denylist の取りこぼしを 理由に allowlist へ替えた直後に @source not を 1 行戻すことになるため採らない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: .storybook/ を source scan へ明示的に足す source("../src") は .storybook/ を対象から外す。decorator や preview に class を 書いても utility が生成されず、スタイルが当たらないことに書いた人が気付けない。 前回「明示しても拾われない」と報告したが誤り。grep のパターンをシェルの引用符で 壊しており (p-\[13px\] が p-[13px] として文字クラス扱いになっていた)、0 件という 結果は grep の誤りだった。対照を src 側に置いて測り直すと、@source を足せば 両方とも生成される。 本番 CSS は 126,040 バイトのまま変わらない。.storybook/ に class を書いた ファイルが現状ないため。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: 色の判定を CSS.supports へ寄せ、canvas の自作実装を消す トークン一覧から非色 (spacing / font / animation 等) を選り分ける述語を、canvas の fillStyle へ既知の値を代入して変化を見る自作実装から、標準の CSS.supports("color", value) へ置き換える。 実トークン 77 件で両者の判定は完全に一致した (2026-09-20、chromium 上で getComputedStyle の解決後の値を両実装へ通して比較)。computed 値に inherit や var() が残るケースは 0 件で、CSS 全体キーワードによる false positive は起きない。 canvas の 2D context を取得できない失敗経路と、その warn-once、テストからの注入点 ContextFactory、失敗経路専用のテスト 4 ケースは CSS.supports には存在しないため 消える。 ADR-0022 の却下リストにこの選択肢は無く、標準 API を確かめる前に canvas へ行っていた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: トークンの走査を 1 回にまとめ、種別ごとの一覧は絞り込みで作る 種別ごとに store を持つのをやめ、全トークンを読む store 1 つに寄せる。 collectRootCustomProperties の prefix は集めた名前の絞り込みにしか使われず、どの rule を 辿るかを変えない。種別ごとに store を分けると、テーマを切り替えるたびに同じ CSSOM 走査と getComputedStyle を種別の数だけ繰り返すことになる。 あわせて 3 箇所へ写していた「store を購読して warnIfEmpty を呼ぶ」2 行を useTokenList へ 集約し、readTokenNames / rootStyle / resolvedWith / readTokens の 4 つを readAllTokens 1 つへ 畳む。useThemeTokens が引数の型を ReturnType<typeof createThemeSnapshotStore<...>> で 再導出していたのも、store が 1 つになって引数ごと不要になった。 件数は変えていない。Colors 35 / Radius 9 / Typography 13 (全 241 トークン中、 2026-09-20 に vp test run --project storybook で実測)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: styles.css の説明を ADR-0022 へのポインタに縮める source() の絞り込みと @theme inline に static を付けない判断について、styles.css の コメントと ADR-0022 がほぼ同文を持っていた。片方だけを直すと説明が食い違う。 styles.css には、そのファイルを編集する人が破りうる規範だけを残す。source() の外で utility を書く場所は個別に足すこと、static をここへ付けないこと。根拠と効果の測り方は ADR-0022 が持つ。 styles.css にしか無かった出典 (Detecting classes in source files) は ADR-0022 の参考へ移す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: port の導出を [env] からタスクへ移す dev server と Storybook の port は git から worktree 名を取って導出するため、 1 回あたり git のサブプロセスを 4 つ起動する。これを .mise.toml の [env] に 2 本置くと、 mise が env を解決するたび (シェル hook の下ではディレクトリへ入るたび) に 8 つ走る。 読み手は serve と storybook のタスクしかいないので、タスクの run の中で導出する。 2026-09-20 の実測で mise hook-env は 70ms から 28ms になった。副次的に、シェル hook を 入れていない手元でも port が決まるようになる。 ADR-0002 の「値の置き場所を 3 つに分ける」に、解決コストのかかる値を [env] へ置かない 判断を足し、[env] の例から port を外す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: 付随ファイルの列挙を共有定数へ畳む 「アプリのコードと同じディレクトリに置くが、アプリのコードではないファイル」の種別が 4 箇所に別々の方言で手書きされていた。lint の適用外 (vite.config.ts) と coverage の除外 (vitest.config.ts) は glob、registry baseline の突き合わせ (registry-baseline.test.ts) は regex で、いずれも同じ集合を指す。 scripts/lib/companion-files.ts に種別の一覧を 1 つ置き、3 箇所をそこから導出する。 種別を足すときに 1 箇所を書き忘れる事故そのものが起きなくなる。 検査を足す形は採らない。vitest.config.ts の coverage.exclude は今どの検査からも見られて いないが、現状の値に違反は無く、coding-discipline.md の「1 回走らせて違反件数を数える。 0 件なら作らない」に当たる。ズレを検出するより、ズレる経路を消す。 lint-config.test.ts は導出しない。lint の解決結果と突き合わせる期待値なので、同じ定数から 両辺を作ると入力どうしの比較になり常に通る。vite.config.ts の DESIGN_SYSTEM_LAYERS と 同じ扱い (設定側は共有定数、検査側は手書き)。 導出した glob が従来のリテラルと同一であることは、書き換え前の lint-config.test.ts の toEqual(EXPECTED_OVERRIDES) が通ることで確かめた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: lint 設定の検査から付随ファイルの写経を外す lint-config.test.ts はリポジトリ全 56 コミットのうち 7 つで触られており、うち保守だけの 5 件は全て EXPECTED_OVERRIDES の excludeFiles にある。他の 5 検査は 6c20cfb 以降 1 度も 触られていない。 excludeFiles から companionGlobs の分を差し引いてから突き合わせる。付随ファイルの種別は scripts/lib/companion-files.ts が唯一の定義で単体テストを持つので、ここへ写すと種別を足す たびに同じ変更を 2 度書くだけの手順が増える。差し引いた残り (手書きの層の一覧と src/test/**) は従来どおり写す。 検出力は mutant で確認した (2026-09-20)。ルールを消す / off へ差し替える / excludeFiles を 無関係な glob で広げる / 層の excludeFiles を広げる、の 4 つはいずれも Tests 1 failed | 16 passed (17) で落ちる。種別を足す操作だけが 17 passed (17) で通り、 そちらは companion-files.test.ts が 2 件で落ちる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 付随ファイルの定義の所在を rules に書く coverage の除外先を vitest.config.ts と書いていた 2 項目を、唯一の定義である scripts/lib/companion-files.ts を指す 1 項目へ統合する。種別を足すときにどこを直すかが、 規範を読んだ側で決まるようにする。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #35 品質改善の再レビュー指摘 7 件を本対応する 重要 1: lint-config.test.ts の excludeFiles の差し引きがどの override にも効くため、 付随ファイルの除外を層の override へ足す壊し方が無検出だった。足すとテストと story で no-restyle と require-static-classes が外れる。「付随ファイルの除外を持つ override は 1 つ だけ」を別の検査で固定する。 重要 2: 3cdfd7a が styles.css から消した「除外を並べる形は symlink と新しい置き場を 取りこぼす」の移し先が無く、styles.css のポインタが ADR-0022 に無い内容を指していた。 ADR-0022 へ却下理由を書く。 重要 3: isCompanionFile の stem ガード (fileName.length > suffix.length) を外す。効くのは `.test.ts` のような先頭ドットの入力だけで、そこだけ旧 regex と挙動が食い違っていた。 消しても全テストが通る未検査の分岐でもあった。10 入力で旧 regex との差分が 0 件になった。 軽微: .mise.toml 末尾の余分な空行を戻す / companion-files.ts の docstring の消費者の 数え方と lint-config.test.ts の扱いを実態へ直す / companionGlobs の prefix は `/` で 終える契約を docstring に書く / directory-structure.md から落ちた「include に一致せず テストとして収集されない」を戻す。 検出力は mutant 6 種で確認した (2026-09-20)。ルール削除 / ルールを off / excludeFiles へ 無関係な glob / 層の excludeFiles 拡大 / 層へ companion glob 追加 は Tests 1 failed | 41 passed (42)、COMPANION_KINDS への種別追加は Tests 2 failed | 40 passed (42) で落ちる。 なお ae6bf62 の本文にある「実トークン 77 件」は、Storybook ではなく styles.css の宣言を 写した CSS で測った件数である。Storybook 上の実測では root のカスタムプロパティ 241 件、 CSS.supports が色と判定するのは 68 件で、canvas 実装との判定差は 0 件だった。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: tokens story の title を落とし、見出しをファイルパスへ一本化する サイドバーの見出しの出処を 1 つにする。title を書くと、部品名やファイル名を変えたときに 片方が古いまま残る。段階 2 で parts/ の 5 ファイルからも落とす。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: Storybook の skill と tools の引き方を AGENTS.md に書く storybook@10.6.0 は agent skill を 3 つ (stories / write-story / setup) と、それを支える ツール群を CLI で配っている。ただし vp exec storybook --help のコマンド一覧に skills と tools が出ないため、探さないと見つからない。 stories skill が「部品の props・API・使い方は documentation tools で答える。ソースや型定義 から答えない」と定めているので、story を書く手順の起点になる。 MCP (@storybook/addon-mcp) を採らない理由も併記する。全ツールが起動中の Storybook を 要求し、登録 URL が localhost:6006 固定である一方、この repo の Storybook の port は worktree ごとに変わる。CLI は docs list / docs show / stories changed / find-by-component / test run が起動不要 (2026-09-20 実測)。 intent が生成する <!-- intent-skills --> ブロックの外へ置く。中に書くと再生成で消える。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: storybook tools の逆依存グラフと、CLI を選んだ代償を AGENTS.md に足す stories find-by-component が逆依存グラフを引くこと (変更ファイルから影響する story を 距離つきで返す) を書く。grep では出せない情報で、CLI を選ぶ理由の 1 つだった。 MCP が優る点も書く。MCP はツールの説明がエージェントに常時見えるが、CLI は AGENTS.md に 書いても読み飛ばせば使われない。実際に 2026-09-20 のセッションで、一度調べた skills の 存在を書き残さずに失い、同じ調査を繰り返した。ここに書いてあること自体がその対策である、 と明記する。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: AGENTS.md の storybook tools の記述を実測に合わせる 3 点を直す。 1. stories find-by-component を「起動不要」の一覧から外す。起動なしでも走るが、逆依存 グラフを持つのが dev server 側なので結果が空で返り、story が無いのと区別が付かない。 tool の help も「If a component has no matches here, it has no stories yet」と書いて おり、読んだ側は「story が無い」と報告する。silent failure の経路だった。 起動して --port で指すと distance 1 で 3 story が返ることを実測した。 2. 「登録 URL が localhost:6006 固定」は言い過ぎ。mcp-add の --url は引数で任意の値を 渡せる。問題は登録が URL を 1 つ持つ形であることと、この repo の port が worktree ごとに変わることの組み合わせなので、そう書き直す。 3. MCP の優位点を結論の後ろへ移す。 起動不要の 4 つ (docs list / docs show / stories changed / test run) は、6006 の Storybook を止めて --no-attach を付けた状態で 1 つずつ実行して確かめ直した。前回の確認は subagent が残した 6006 の Storybook が動いている状態で行っており、auto-attach していた 可能性があった。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: agent 向けツールの選定を ADR-0023 として切り出す AGENTS.md に決定・理由・実測値をまとめて書いていたが、agents-md-maintenance.md の 「書くべきでないもの」に「実測値の詳細。判断を決める閾値だけ書き、根拠の実測は ADR へ 置く (ADR-0001)」とある。AGENTS.md は毎セッション context に載る媒体なので、そちらの 方針に従う。 ADR-0023 が持つもの: CLI と MCP の比較、決め手 (MCP の登録は URL を 1 つしか持てず、 worktree ごとに変わる port へ配れない)、起動の要否の実測、find-by-component が未起動だと 空を返す落とし穴、上流 skill と ADR-0022 の食い違い。 AGENTS.md に残すもの: storybook skills を起点にすること、--help に出ないこと、props は docs tools で答えること、find-by-component は起動してから指すこと、ADR-0023 への参照。 手書き行数は 47 から 35 になった (目標 50 / 上限 100)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: delete-confirm-dialog の story を書き、検証をブラウザテストから移す 9 story。既存の delete-confirm-dialog.test.tsx の 9 case をすべて play と 初期状態へ移し、test ファイルを削除する (ADR-0022)。同じ振る舞いを 2 箇所で 固定しない。 Task 7 の 1 つ目として story の形を決める。 - 終了状態が他の story と同じになる 4 つ (Confirmed / Cancelled / Pending / NotCalledTwice) は検証専用として tags: ["!dev"] でサイドバーから外す - pending は決着する Promise で書く。決着しない Promise は後続 story の Transition を止める - 閉じる play は unmount を waitFor してから終える。待たないと play 後の a11y 検査が animate-out の窓に入る delete-confirm-dialog.test-helpers.ts は残す。routes/notes のテストが同じ locator と文言を引いている。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: parts/ の args だけで決まる部品に story を足す Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * revert: TableSkeleton の th へ sr-only を足す修正を戻す 5753ab1 で axe の empty-table-header を解消するため各 TableHead に sr-only テキストを足したが、実機の accessibility tree を測ると害のほうが 大きかった。 table は role="status" を載せているため table ではなくなり、th は columnheader ではなく generic に落ちている。そこへ名前を足すと、 live region (aria-label="読み込み中") の中に同じ文言が列数ぶん増え、 4 列なら 5 回読まれる。検査は黙るが体験は悪化する。 本体の扱い (role="status" が table を潰す / notes/index.test.tsx:133 が 露出しない性質を固定している / empty-table-header をどう扱うか) は #36 で 決める。それまで story 単位でこのルールだけ止める。 破壊テスト: 抑制を外すと Default story が empty-table-header で落ちる ことを確認した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: ActionButton の story を書き、検証をブラウザテストから移す action/button.test.tsx の 7 case をすべて story へ移し、test ファイルを削除する (ADR-0022)。 - 6 case は ActionButton の play へ移した。pending の遷移、決着前の再操作、 決着後の再操作、form 内で submit しないこと、aria-label、reject が Error Boundary へ届くこと - 「pending 中の描画に a11y 違反が無い」は ActionButtonShell の story が isPending を args で受けて持つ。ActionButton が Shell へ渡すだけの構造なので 描画は同じで、決着しない Promise を置かずに済む 終了状態が Default と同じになる 5 story は tags: ["!dev"] でサイドバーから外す。 reject の story は React が境界へ渡す前に出す console.error を play で黙らせる (元のテストの silenceConsoleError と同じ理由)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: ActionForm の story を書き、検証をブラウザテストから移す action/form.test.tsx の 6 case のうち 5 case を story の play へ移す (ADR-0022)。 6 story (Default / Settles / NotCalledTwice / PlainSubmitNotCalledTwice / RejectReachesErrorBoundary / SubmitOutsideForm)。 - submit の pending 遷移、決着前の再 submit、素の submit ボタンでの再 submit、 reject が Error Boundary へ届くこと、ActionForm の外で使うと throw することを play へ移した - 「pending 中の描画に a11y 違反が無い」は移さず form.test.tsx に残す。 Storybook の a11y 検査は play の完了後に走るため、pending を保ったまま検査 させるには決着しない Promise が要り、ADR-0022 節 4 がそれを禁じている 終了状態が Default と変わらない、または検証専用の 5 story は tags: ["!dev"] で サイドバーから外す。レンダー中の throw を捕まえる 2 story は beforeEach で console.error を黙らせる (描画より前に出るため play では間に合わない)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: ChoiceCard の story を書き、検証をブラウザテストから移す parts/choice-card.test.tsx の 6 case のうち 2 case を story の play へ移す (ADR-0022)。5 story (Default / Checked / Disabled / TogglesOnRowClick / GeneratedId)。 - 行のクリックでトグルすること、id を渡さないときの htmlFor の紐づきと行どうしの 非連動を play へ移した - 残す 4 case のうち 3 つは寸法を getBoundingClientRect で固定する回帰 (trailing の位置 / 行間 8px / tap target 44px) で、ADR-0022 節 7 のとおり ブラウザテストが持ち続ける - 「disabled の行はクリックしてもトグルせず、押せると主張しない」も残す。 Playwright に弾かれる要素へ dispatchNativeClick で実イベントを送る検証で、 ADR-0022 節 5 が実イベントの規律をブラウザテスト側に置いている。cursor の computed style 固定も節 7 の対象 disabled と選択済みは args だけで決まる状態なので、検証ではなくカタログとして Disabled / Checked の story に出す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: SegmentedRadioGroup の story を書き、検証をブラウザテストから移す parts/segmented-radio-group.test.tsx の 10 case のうち 3 case を story へ移す (ADR-0022)。5 story (Default / Selecting / ReclickKeepsSelection / Disabled / Invalid)。 - ロールの確認は Default の初期状態へ、選択の通知と再クリックで空選択に ならないことは play へ移した - 残す 7 case は寸法と色の回帰が 6 つ (等幅 / トラック 36px / hover で文字色を 奪われない / aria-invalid の枠色 / focus outline の実描画 / className のマージ) で、ADR-0022 節 7 のとおりブラウザテストが持ち続ける - 「disabled でクリックを受け付けず、無効表示が効く」も残す。dispatchNativeClick で 実イベントを送る検証と opacity / pointer-events の computed style 固定が一体で、 ADR-0022 節 5 が実イベントの規律をブラウザテスト側に置いている disabled と aria-invalid は args だけで決まる状態なので、検証ではなくカタログとして Disabled / Invalid の story に出す。story の harness は onValueChange を受けて 自身の state も更新するため、サイドバーから触っても選択が動く。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: ActionForm の pending a11y case を ActionButtonShell の story へ寄せる form.test.tsx に 1 case だけ残っていた「pending 中の描画に a11y 違反が無い」を 削除し、ファイルごと落とす。 ActionFormSubmit は isPending を ActionButtonShell へ渡すだけで (form.tsx:56)、pending 中の DOM は button-shell.stories.tsx の Pending と type 属性 (submit / button) 以外すべて一致する (2026-09-20 実測)。axe に type を 入力にするルールは無い。 決着しない Promise を置かないと play 後の a11y 検査を pending の状態に当てられ ないという制約 (ADR-0022 節 4) は変わらない。Shell 側の story が args だけで その状態を作れることで解消している。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: DialogScrollBody の story を書く (溢れ有無のカタログ) 溢れている状態と溢れていない状態を並べ、a11y 検査の対象に載せる。 dialog-scroll-body.test.tsx からブラウザテストへ移した case はゼロ。11 case は すべて getComputedStyle / getBoundingClientRect で寸法・色・レイアウトを固定する 回帰で、ADR-0022 の節 7 が既存テストの役割として残すと定めている。 play は story が名乗る状態 (data-has-overflow-y の有無) だけを確かめる。区切り線の 色そのものは従来どおりテスト側が持つ。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: FormFields の story を書き、検証をブラウザテストから移す 4 種のフィールド (text / number / select / checkbox) を 1 つのフォームへ並べた カタログを置き、配線の検証を play へ移す。story は 15 件 (サイドバーに出るのは Default / Invalid / Disabled の 3 件、残り 12 件は検証専用で tags: ["!dev"])。 form-fields.test.tsx は 21 case から 3 case へ。残したのは次のとおり。 - fieldValue の型契約 (expectTypeOf は play へ移せない) - ラベルの色 2 case (getComputedStyle で色を固定する回帰。ADR-0022 の節 7) 「検証エラーでラベルが destructive 色になり、aria-describedby がエラー要素と一致する」 は 2 つの性質が混ざっていたため分割した。data-invalid と aria-describedby の配線は Invalid story の play へ移し、テスト側は色の assertion だけに絞った。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: validationMode の control が効くようにする useAppForm は mount 時の validationLogic を握るため、render に key が無いと control で validationMode を変えても再描画されるだけで効かない。動かない knob が サイドバーに残る。 key を付けて remount させる。disabled / options / validateCheckbox は毎描画 props として渡るので元から効いていた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 段階 2 のレビュー指摘 10 件を本対応する 重大 1: ADR-0022 節 5 は「実イベントでの発火の規律はブラウザテスト側がそのまま持つ」と 決めているのに、その規律を持つ 3 ファイルを消していた。代替の有無で線を引き直す。 ActionButton の二重発火は src/routes/notes/index.test.tsx が AlertDialogActionButton (ActionButton に data-slot を足すだけのラッパー) 経由で実イベント + deferMock + animate-out の窓で持つ。DeleteConfirmDialog の確定 / キャンセル / Enter 2 連射も同ファイルが持つ。 代替が無い ActionForm と ActionFormSubmit の 2 case だけ form.test.tsx として復活させ、 どのテストが何を持つかを ADR-0022 節 5 の表に書く。 重要 1: pending の決着を setTimeout(50ms) に預けていた。testing.md「optimistic update テストは遅延 rejection で中間状態を観測」が否定している形で、決着が 2 発目の操作より先に 届いた回は偽 red になる。Promise.withResolvers にして play が決着を握り、play の終わりで 必ず決着させる (決着を待たずに次の story へ移る 3 件も同時に解消)。 重要 2: サイドバーの見出しが PascalCase の明示 title 5 件と kebab の autotitle に割れて いた。title を落としてファイルパスを唯一の出処にする。1 ファイルが複数の部品を export するときは story ファイルを分ける規範を directory-structure.md へ書く (CSF の meta は 1 ファイルに 1 つで component もそこに紐づくため、まとめると別の部品の meta 配下に並ぶ)。 重要 3: WithoutPayload が render の中でコンポーネントを定義し、useState の初期化子で queueMicrotask を投げていた。module scope へ出し、mount 時の open は useEffect へ移す。 重要 4: WithoutPayload に tags: ["!dev"] が無くカタログに出ていた。終了状態は Opened と ほぼ同じ見た目 (payload なしでは説明文が空になるだけ) なので外す。 軽微: OtherEntity の同語反復 assertion を toBeInTheDocument へ / NotCalledTwice の発火を 元と同じ 3 発火へ戻す / console を黙らせる線引き (描画中は beforeEach、操作由来は play 内) を 1 か所に書いて揃える / DialogScrollBody の見えない DialogTrigger を消す / ActionButtonShell の Idle と Pending を対で出す意図を書く。 重要 5 (到達不能な SHA を参照するコミットメッセージ) は push 済みのため履歴を書き換えず、 PR 本文で扱う。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: story のアサートを属性から semantic matcher へ寄せる story の assertion が属性を直に見る形に寄っていた。Storybook / Testing Library / jest-dom / Playwright のいずれも、ユーザーから見た状態を見るアサートを勧めており、 属性の突き合わせは低位の手段と位置づけている。 置き換えたもの: - aria-invalid の 8 件を toBeInvalid へ。jest-dom は aria-invalid を判定に含む - aria-checked / data-checked の 6 件を toBeChecked へ。jest-dom 6.9.1 の実装で aria-checked 対応を確認した - native disabled の 3 件を toBeDisabled / toBeEnabled へ - 手製の describedByText を toHaveAccessibleDescription へ。手製版は getElementById(aria-describedby) で、値が複数 ID のときに解決できなかった 属性のまま残したもの: - aria-disabled 10 件。jest-dom の toBeDisabled は aria-disabled を見ない (dist の matchers を grep して 0 件であることを確認。README にも明記がある) - aria-busy 7 件。相当の matcher が無い。getByRole の busy オプションにはできるが、 失敗時のメッセージは属性アサートの方が読める - data-invalid / data-disabled / data-checked / data-has-overflow-y。Base UI の styling hook で、見た目を駆動する属性そのものを固定している - Spinner の aria-hidden。一度 toHaveAccessibleName へ置き換えたが、 ActionButtonShell は aria-labelledby で名前を固定するため aria-hidden を外しても accessibility tree に差が出ず、mutant が 7 passed で素通りした (2026-09-20 実測)。 元の形へ戻し、要素を直に掴む理由をコメントに書いた 検出力は mutant で確認した (2026-09-20)。aria-describedby を潰すと 3 story、 aria-invalid を潰すと 4 story、Spinner の aria-hidden を外すと 4 story が落ちる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 状態のアサートは semantic matcher を先に探す規範を足す 段階 3 で ui/ の story が 30 件以上増えるので、属性を直に見るアサートが複製される前に 規範として置く。どの状態にどの matcher を使うか、相当が無くて属性で見てよいのはどれかを 表にする。 置き換えで検出力が落ちることがある点も併せて書く (ActionButtonShell の Spinner で実測)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 段階 2 再レビューの重大 1 件と重要 3 件を本対応する 重大 1: ADR-0022 節 5 の表が「ActionButton の二重発火は src/routes/notes/index.test.tsx が 持つ」と書いていたが偽だった。mutant で確かめると、ActionButtonShell の disabled={isPending} を disabled={false} にしても browser project は Tests 180 passed (180) で 1 件も落ちない。confirmDelete は close() のあと void runAction(...) と同期に返るので Transition が即終了し、2 発目の時点で isPending は false になる。あのテストが固定して いるのは画面側の queryClient.isMutating による dedupe である。 薄いラッパー (AlertDialogActionButton) を経由することを根拠に、guard を通ると推論して 表に書いていた。経路が通ることは guard を通ることを意味しない。 src/components/action/button.test.tsx を実イベントの二重発火 1 case で復活させ、表を 実測に合わせて書き直す。復活させた case は mutant で Tests 1 failed (1) になることを 確認した。 重要 1: settle が「最後に作った Promise 1 本」しか握らず、play が途中で落ちると未決着の まま残って後続 story を巻き添えにしていた。aria-busy={false} の mutant で Tests 8 failed | 63 passed (71) のうち 5 件が巻き添えだった。決着待ちの resolver を配列で 持ち、beforeEach の teardown で全決着させる。同じ mutant で Tests 3 failed | 68 passed (71) になり、落ちるのは実際の退行 3 件だけになった。 重要 2: directory-structure.md に書いた「複数 export は story ファイルを分ける」を、同じ コミットの ActionFormSubmit が満たしていなかった。親を要求する部品は単独の meta を 持てないので、適用範囲を「単独で描画できる部品」に限る。 重要 3: PlainSubmitNotCalledTwice の waitFor が直前の assertion と同じ条件で、何も待って いなかった。素の submit ボタンは aria-disabled にならないので、決着後にもう一度撃って 2 回目が通ることで観測する。ActionForm の if (isPending) return が解けることも同時に固定 できる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 段階 2 再レビューの軽微 3 件を本対応する 軽微 1: 決着を握る device が 3 ファイルに同型で重複し、リセットの作法も揃っていなかった。 mock ライブラリに依存しない形で src/test/settling-action.ts へ切り出し、単体テスト 5 件を 付ける。呼び出し側は fn(settling.impl) で包み、meta.beforeEach に settling.beforeEach を 渡す。 あわせて beforeEach の mockClear() を落とす。Storybook の既定 loader が story ごとに restoreAllMocks() を走らせるため冗長で、落としても storybook project は Tests 71 passed (71) のまま通る。 軽微 2: WithoutTrigger が Omit<StoryArgs, "target"> を名乗りながら render 側の spread で target を受け取っていた。JSX の spread には超過プロパティ検査が効かないので型は通るが、 実体は渡っている。同ファイルの WithTrigger と同じく分割代入で外す。 軽微 3: button-shell.stories.tsx のコメントが「別部品なら同じ見た目でも残してよい」を ADR-0022 として引いていたが、ADR-0022 節 3 にその例外は書かれていなかった。節 3 へ 1 行足し、コメントは節番号で指す。 巻き添えの抑制は factory 化のあとも保たれている。aria-busy={false} の mutant で Tests 3 failed | 68 passed (71)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 作業文書にしか無い決定 4 件を移し、argTypes の複製を型で守る docs/superpowers/{plans,specs} は .git/info/exclude で未追跡なので、そこにしか無い決定は 残らない。段階 2 の範囲に当たる 4 件を移す。 ADR-0022 へ 2 件: - story を書かない部品のテストは触らない。移せない case は残して理由を JSDoc に書く。 移せないのはレイアウトと配色の実測・型契約・CDP 実イベントで、src/components/ui/ の 既存テストは 26 case 中 25 case がこれに当たる (節 5) - a11y 違反は抑制せず直す。story 単位の parameters.a11y は global の "error" より強く、 書けば黙る。抑制するなら理由と issue 番号をその場に書く (節 7) directory-structure.md へ 2 件: - argTypes の options は cva 定義と突き合わせてから写す - story から部品へ className を渡さない。decorator で外側の要素に当てる 後ろ 2 件は機械で止まらないことを実測した。cva に variant を足しても vp check は 198 ファイルで警告ゼロ、story も 71 件通る。parts/ の story が部品へ className="p-8 shadow-lg" を渡しても警告ゼロ (vite.config.ts の excludeFiles が ui/ action/ parts/ を外すため)。 あわせて button-link.stories.tsx の argTypes を型で守る形へ直す。variant 8 値と size 8 値をリテラルで複製していた。satisfies Record<ButtonVariant, null> に通すと、 cva へ足したときに TS2741 で落ちる (実測)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 200 字を超えた規範 3 項目から根拠を ADR-0022 へ移す rules-maintenance.md は「1 項目は 200 字以下。超えたら根拠が混ざっている (ADR-0001)」と 定めている。directory-structure.md の story の項目が 204 字と 218 字で超えていた。 ADR-0022 へ移した根拠: - CSF の meta は 1 ファイルに 1 つで component もそこに紐づく (節 8) - argTypes の options は readonly any[] で中身を検査しない。satisfies Record<Variant, null> のオブジェクトを出処にして Object.keys で渡すと、cva に足した側が型エラーになる (節 2) - story は no-restyle / require-static-classes の適用外。vite.config.ts の override が src/components/{ui,action,parts}/** を excludeFiles で外しており、story もそこに置く。 部品へ className を直接渡しても lint は鳴らない (節 8) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: port の導出を fail-closed にし、付随ファイルの除外の欠落を検出する /code-review が挙げた 2 件を直す。どちらも silent failure で、実測で再現した。 1. .mise.toml の port run の引数に $(...) を書いていたため、script が実行できないと空文字が渡り、Vite の 既定 port で起動して worktree の分離が無言で外れていた (chmod 000 で PORT=[] を実測)。 mise のタスクは set -e 付きで走る (公式 docs) が、置換の失敗は引数の中では コマンドの exit status にならない。 mise 公式のタスク単位の env へ移す。exec の template がそのまま使え、script が失敗すると タスクごと止まる。トップレベルの [env] へ戻すわけではないので、env 解決のコストも 増えない (mise hook-env は 28ms のまま)。3 点とも実測した。 2. lint-config.test.ts の付随ファイルの除外 excludeFiles から companionGlobs の分を無条件に差し引いていたため、glob を 1 本消す 壊し方が無検出だった (**/*.test-helpers.tsx を消しても 18 passed)。差し引きは「付いて いるか」を見ないので、別の override へ付ける壊し方と同じ検査で受け持つ形にし、全種類 そろっていることを固定する。 mutant 3 種で確認した。glob を 1 本消す / 層へ glob を足す / ルールを off は、いずれも Tests 1 failed | 17 passed (18) で落ちる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: a11y 検査を dark でも走らせ、空のカタログを落とす /code-review の 2 件。どちらも silent failure で、実測で再現した。 1. a11y 検査が light だけだった defaultTheme が "light" で globals の matrix が無く、dark の contrast は一度も検査されて いなかった。parameters.a11y.test: "error" が保証しているように見えて、していない。 addon-vitest の公式パターン (型定義に「define one Vitest project per theme」と明記) に 従い、storybookTest へ initialGlobals を渡す project を theme ごとに作る。project 名は storybook-light / storybook-dark。追跡対象に --project storybook の参照は無い。 dark の --muted-foreground を背景へ寄せる mutant で確かめた。storybook-dark だけが color-contrast で落ち、storybook-light は 3 passed のまま通る。 2. カタログが空でも story が通った 一覧の中身は styles.css と CSSOM の走査で決まるが、story は描くだけで assertion が無く、 0 件でも a11y 違反ゼロで緑になっていた。warnIfEmpty の warn はテストを落とさない。 3 story に play を足し、トークン名 (-- で始まる文字列) が 1 つ以上描かれることを見る。 走査を空配列へ差し替える mutant で 3 story とも落ちることを確かめた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: CSSOM の走査を @import へ広げ、CSS の HMR でカタログを読み直す /code-review の 3 件と、telemetry の設定。 1. @import が指す stylesheet を辿っていなかった CSSImportRule は CSSGroupingRule を継承しないため、走査から漏れていた。漏れた宣言は 「トークンが無い」のと区別が付かない。辿るようにし、テストを 1 件足す。 2. try/catch が走査全体を覆っていた 深い位置の失敗でその stylesheet の残りが丸ごと落ち、warn は最上位の href を指していた。 catch を cssRules の取得だけに絞り、捨てていた error も warn へ載せる。読めない @import が あっても同じ stylesheet の他の宣言が残ることをテストで固定する。 あわせて ReadableStyleSheet.cssRules を Iterable<CSSRule> へ広げた。テスト側が CSSRuleList を型アサーションで作る必要がなくなる (typing.md はアサーションを禁じている)。 3. CSS の HMR でカタログが古いまま残った stylesheet の中身だけが差し替わると <html> の class は動かないので、MutationObserver では 拾えない。Vite 公式の HMR API (vite:afterUpdate) を購読する。本番では import.meta.hot が 無く、ガードごと落ちる。 4. telemetry を切る 既定で有効で、実行したコマンド・バージョン・addon 一覧・story とコンポーネントの件数を 送る。このテンプレートから作られる全プロジェクトへ配られる設定なので、envDir: false や disable_tools と同じく明示で潰す側に揃える。理由は ADR-0022 が持つ。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: 走査と警告を store へ寄せ、viewport と route の除外を共有定義から導く /code-review …
shnakatani
added a commit
that referenced
this pull request
Sep 20, 2026
* docs: 段階 3 の決定を ADR-0022 へ移す 作業文書 (docs/superpowers、未追跡) にしか無かった段階 3 の決定 3 件を移す。Task 8-9 は 未着手なので、plan が消えると失われる。 - 段階 3 の対象は import されている部品に限る。0 件には書かない。未使用の先行導入は ADR-0006 が許容しており、使い始めるときに足せばよい (節 9) - ui/ の play は「開く」まで。開いた先の操作は書かない。registry 部品の振る舞いは上流が 持っていて、こちらで固定すると上流の更新のたびに落ちる (節 2) - animation 無効化の判断の母数は accordion 1 件。要るときの入れ先は vitest.storybook.config.ts の setupFiles で、preview ではない。preview へ入れると storybook dev でも animation が消える (節 5) 前 2 件は上限を書かないと上流の既定値が入る。AGENTS.md が実装者に vp exec storybook skills を読ませており、write-story skill は 「ALWAYS write a Storybook story for any component written」と 「Simulate key user flows: clicking, typing, focus/blur, keyboard nav, form submit, ...」 を持つ。ADR に上限が無い項目はそちらへ倒れる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: button の variant 一覧を story-helpers へ寄せる Button を包む部品の story も同じ選択肢を出すため、`cva` の variant と size の 写しが 2 箇所に増える。写しが増えても `satisfies Record<..., null>` が型で 落とすので silent には壊れないが、選択肢の出処が 2 つになる。 `src/components/ui/button.story-helpers.ts` を唯一の出処にして、 `button-link.stories.tsx` はそこから引く。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: ui/ の args で状態が決まる部品 11 件に story を置く 対象は「消費側から直接使う」かつ「args だけで状態が決まる」部品。 消費者の数は import から数え、story とテストからの参照は除いた。 - 消費者 0 件の部品 (badge / progress / alert / calendar など) は置かない。 未使用の先行導入は ADR-0006 が許容しており、カタログに出す対象ではない - skeleton は単体で寸法を持たず、描くには部品へ className を渡すことになる。 実際の見え方は table-skeleton.stories.tsx が持つので対象から外す - 操作で状態が変わる部品 (dialog / select / checkbox など) は次の段で扱う subcomponent の variant (EmptyMedia の variant、InputGroupAddon の align) は argTypes に出せないため、網羅を型で守る定数を置かず story を分けて覆う。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: 操作で状態が変わる registry 部品 9 件に story を足す play function は「開く」「押す」までにし、開いた先の操作 (選択、送信、閉じる) は 書かない。振る舞いの検証は既存のブラウザテストが持つ (ADR-0022)。 - Base UI の animation は play を妨げなかった。開くのは findBy 系で待てるため、 vitest.storybook.config.ts へ animation 無効化の setupFiles は足していない - portal へ出る部品 (dialog / alert-dialog / sheet / select / tooltip / toast) は canvas ではなく screen から取る - tooltip は hover ではなく focus で開く。pointer の hover は base-ui の開閉 ヒューリスティクスに委ねられ、自動操作では安定して開かない - handle と toast manager は story ごとに作る。module 変数にすると、1 つの React root へ story を描き替える Storybook の vitest 実行で状態が持ち越される - 消費者 0 件の combobox / dropdown-menu / popover / sidebar は書かない - scroll-area は単体で高さを持たず、描くには部品へ className を渡すことになる。 実際の見え方は dialog-scroll-body.stories.tsx が持つ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: checkbox に indeterminate の指標を足し、toast の種別を型で縛る 段階 3 の story が registry 部品の穴を 2 つ踏んだため、registry 側を直す。 どちらも上流に未解決のまま残っている。 checkbox: base-ui は checked と indeterminate のどちらでも Indicator を描くが、 registry はチェックマークしか持たないため両者が同じ絵になる (shadcn-ui/ui#9357)。 Indicator へ MinusIcon を足し、base-ui が出す data-indeterminate で出し分ける。 上流 PR #9373 は Radix 互換の checked="indeterminate" を受ける形だが、base-ui は indeterminate を独立 prop に持つので互換シムは取り込まない。 toast: ToastIcon の 5 種別が if 連鎖にしかなく、増減しても型も lint も鳴らない。 対応表 TOAST_ICONS を唯一の出処にして ToastIconType を export し、story が satisfies Record<ToastIconType, string> で網羅を強制する。base-ui の type は string のままにして、対応表に無い type は icon なしで通す (独自 type を塞がない)。 ガード: checkbox は checkbox.test.tsx (上流の形へ戻すと expected 'check' to be 'minus' で落ちることを実測)、toast は story の satisfies で vp check が落ちる (種別を 1 つ減らすと TS2353 / TS2322)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 段階 3 のレビュー指摘 17 件を本対応する HIGH 2 件 - tooltip: base-ui の tooltip popup は role を持たず `queryByRole("tooltip")` が 開閉によらず null になるため、Closed の否定が常に真だった。Opened と同じ テキストの経路へ揃えた - alert-dialog: 「Esc では閉じない」は誤り。`disablePointerDismissal` は外側 クリックだけを止め、Esc は `escapeKey: isTopmost` で通る (実測で確認) MEDIUM 5 件 - alert-dialog: 「閉じる手段はフッターの 2 つ」は誤り。`AlertDialogAction` は 素の Button で閉じない。閉じるのは Cancel と Esc - alert-dialog: 「media は見出しの上」は sm 未満のみ。既定 viewport (1280) では左 - alert-dialog: X を持たないことを件数で検証する。名前 "Close" での否定は alert-dialog に該当要素が無く常に真になる - checkbox: Indeterminate は registry へ指標を足したことで Checked と別の絵になった - field: WithContent の `id` に受け手が無く、可視テキストを `aria-label` で 二重に持っていた。`FieldLabel htmlFor` で結ぶ形へ LOW 10 件 - dialog: X 無しを勧める docstring を、registry の prop の紹介に留めた - dialog / alert-dialog / toast: 未実測だった「持ち越される」を実測の範囲へ収めた - checkbox: aria-invalid はリングも変える - accordion: canvas と within(canvasElement) の混在を canvas へ寄せた - label: 器を decorator へ移し separator と流儀を揃えた - rules 参照 6 箇所に見出し語を付けた - input-group: 型で縛れない align の網羅主張を落とした - sheet: component を SheetContent にして docs show から Props が消えないようにした - card / checkbox / input に parts への誘導を足した (5/8 だった) - ADR-0022 の animation 判断の母数を段階 3 の実測へ更新した Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 段階 3 の対象を registry 部品すべてに広げる (ADR-0022) 「import 0 件の部品には story を書かない」を撤回する。2026-09-20 の実測で 3 点が成立しないことが分かった。 - README がデモアプリの削除を案内しており、消費者数の母数が捨てられる前提のもの - 基準が推移的に閉じない。sheet / tooltip は sidebar からのみ、textarea / input-group は combobox からのみ参照され、参照元自体に消費者がいない。 ui/ の外で数えるか否かで同じ状態の部品が両側へ分かれる - story も test も持たない部品は axe が一度も当たらないまま利用者へ配られる あわせて、撤回した基準が根拠に挙げていた ADR-0006 の記述が存在しないことも 確認した (実際の出典は .claude/rules/directory-structure.md の shadcn 導入チェック で、そこは「vendor してよい」としか言っていない)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: 残る registry 部品 16 件に story を書き、ui/ を全件カタログ化する alert / badge / breadcrumb / button-group / calendar / combobox / dropdown-menu / item / popover / progress / radio-group / scroll-area / sidebar / skeleton / tabs / toggle-group。これで src/components/ui/ の 36 部品すべてに story が付く。 寸法を持たない部品 (skeleton / scroll-area) は decorator の器から寸法を与える。 story から部品へ className は渡さない。 toggle の variant / size は ToggleGroup と共有するため toggle.story-helpers.ts へ 寄せた (button と同じ形)。 カタログ化で a11y 違反が 3 件見つかった。いずれも上流 registry も同じ形で、 該当 issue は無い (gh search issues で確認)。正しい値は消費側しか知らないため registry 側では既定値を持てず、罠として .claude/rules/base-ui.md へ記録した。 - Combobox: popup 内に入力欄を置くと base-ui が role="dialog" を付けるため ComboboxContent に aria-label が要る (aria-dialog-name) - ItemGroup: role="list" を持つが Item は listitem にならないので消費側で渡す (aria-required-children)。ItemSeparator はこの中に置けない - Progress: role="progressbar" は名前が要る。ProgressLabel を置かない形では aria-label で与える (aria-progressbar-name) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 段階 3 の再レビュー指摘 9 件を本対応する HIGH 2 件 - progress: meta の render が children を渡していたため registry のフォールバックが 無効になり、Indicator が一度も描かれていなかった。全 story が同じ空トラックに 見える状態で、カタログの目的を果たしていない。render を外し、WithLabel だけ Track と Indicator を明示する (value=60 で幅 720px が出ることを実測) - sidebar: Collapsed の説明が実物と逆だった。collapsible の既定は offcanvas で 帯ごと画面外へ出る。説明を直し、アイコン列と tooltip が見える collapsible="icon" の story を足した MEDIUM 4 件 - item: 「Item は render prop を持たない」は誤り。useRender.ComponentProps<"div"> を受ける。render={<li />} へ寄せて role="listitem" と lint 抑制 2 件を外し、 .claude/rules/base-ui.md の記述も直した。誤った根拠で抑制を勧める規範を テンプレート利用者へ配るのを避ける - calendar: defaultMonth だけでは today modifier と年ドロップダウンの範囲が 実時計を読み続ける。today / startMonth / endMonth を固定した (today を 5 日へ変えると強調が動くことを実測) - dropdown-menu: Grouped が getByText でしか見ておらず、Label を素の span へ 変えても通った。getByRole("group", { name }) へ替えて検出力を持たせた - サブコンポーネントの variant がカタログに出ていなかった。TabsList の line、 ItemMedia の image、Sidebar の floating / inset / 右側を足した LOW 3 件 - checkbox.test.tsx: 見えているアイコンを 1 つ目で打ち切っており、両方が同時に 見える壊れ方 (片側の hidden だけが落ちた状態) を検出できなかった。配列で固定する - ADR-0022: animation 判断の母数は story を足すたびに動くので、列挙をやめて 測り方だけ残した - vitest.storybook.config.ts: browser.viewport は効いていなかった。 @storybook/addon-vitest の setViewport が story ごとに page.viewport() を呼び、 同 addon の既定 1200x900 へ固定する (browser project は 1280x720)。 死んだ設定を外し、実測を注記に残した Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: 段階 3 の再レビュー指摘 4 件を本対応する - badge: Link が `span` のままでリンクを描いていなかった。hover の指定が `[a]:` 限定なので、リンクとしての見た目がカタログに一度も出ていない。`render` で `a` へ差し替えた (A/href=/notes/下書き を実測) - sidebar: `React.ReactNode` を import なしの UMD グローバルで引いていた。 他の story と同じ `import type { ReactNode }` へ揃えた - alert / button-group / item: `render: () => ...` が meta の args を捨てており、 同じファイルの中で control が効く story と効かない story が混ざっていた。 `render: (args) =>` で通すようにした - breadcrumb: 「`aria-current="page"` を持つリンクでない要素」は不正確。実物は `a href` を持たない `role="link"` + `aria-disabled="true"` で、「link 相当だが 遷移しない」を伝える形 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: /simplify #40 の指摘 20 件を本対応する 「registry を直さず消費側へ作法を強いる」判断が支えきれていなかった。必要なのは 既定値ではなく型での必須化で、同じ stack の toast が既にその形を採っている。 - combobox: `ComboboxContent` のアクセシブル名を型必須にした。書き忘れが `vp check` で落ちる。`ComboboxTrigger` / `ComboboxClear` には既定の `aria-label` を入れた (上流 shadcn-ui/ui#11589)。story も内部ボタンを 実際に描く形にし、`showTrigger={false}` で迂回するのをやめた - item: `ItemGroup` に `render` を通し、`render={<ul />}` + `Item render={<li />}` で組めるようにした。既定の `role="list"` の div は子に `role="listitem"` を 要求するが `<li>` は ul/ol/menu の中でしか置けず HTML が破綻していた。 `ItemSeparator` は base-ui の `Separator` から外して装飾の線にし、 list の中へ戻した (対で export される部品が組めない状態を解消) - progress: `children` を渡さない形でアクセシブル名を型必須にした。 `<Progress value={60} />` が最短の書き方で、そこが必ず axe に落ちていた - scroll-area: 自前の乖離 prop `viewportClassName` の story を足した。 消費側 3/3 はこちらを使うのに、カタログに一度も出ていなかった - skeleton: 寸法の API が `className` しかない部品なので、器を作る形をやめて 消費側と同じ形で描くようにした。`directory-structure.md` に例外を明記 - ItemMedia の variant 網羅が抜けていた (`default` が 0 件)。`argTypes` に 出せない variant も、対応表を型で押さえて 1 つの story に並べれば守れる - alert-dialog: `BODY` 定数と `WithMedia` が同じ本文を 2 度持っていた。 media スロットへ畳んだ - toast: 5 story が `raise(TYPE_LABELS.<type>)` だけの play を反復し、 `args.type` と同じ値を 2 度書いていた。meta の play へ寄せた - sidebar: `<SidebarProvider {...args}>` のラッパが 4 箇所で複製されていた。 args へ上げて各 story を 1 行にした - field: `Responsive` が meta.render の写しだった。decorator へ寄せた - 「1 つの React root へ描き替える」の実測説明を 3 ファイルへ写していた。 正本は ADR-0022 と `src/test/settling-action.ts`。規範と出典だけ残した - `argTypes` の出処コメントが出典 2 系統 × 検出力の説明 2 通りに割れていた。 `satisfies` は両方向を見るので、1 行の同一文言へ揃えた - 中間定数 `*_MEMBERS` を story-helpers と同じ `Object.keys({...})` の形へ畳んだ - combobox の `NoItems` が 249ms 空転していた。base-ui が live region へ word joiner (U+2060) を 200ms 入れ、`findByText` の normalizer は空白しか 畳まないため厳密一致がタイマー明けまで poll していた。部分一致にして 55ms Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: viewport-sizes の docstring から storybook config を外す PR #35 側で書いた docstring は vitest.browser.config.ts と vitest.storybook.config.ts の 2 つが DEFAULT_VIEWPORT を読むと書いていたが、 storybook-ui は vitest.storybook.config.ts から viewport 指定を外している (addon-vitest の setViewport が story ごとに page.viewport() を呼ぶため)。 取り込んだ結果、読み手が 1 つになったので記述を合わせる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #40 レビュー指摘 12 件を本対応する a11y の型と検査 - ComboboxContent の型からアクセシブル名の必須を外した。base-ui は popup の role を inputInsidePopup ? "dialog" : "presentation" で決める。presentation は name prohibited で aria-label は WAI-ARIA 1.2 §5.2.8.6 の MUST NOT にあたり、判定が実行時の state である以上、型で必須にすると入力欄を popup の外へ置く構成が違反を書かされていた。 InlineWithTrigger と InlineWithClear から aria-label を外す (入力欄が中にある ComboboxExample の 1 箇所は dialog なので残す) - 上の 2 story の play で popup を開く。開かないと Portal の中が mount されず、 axe の aria-prohibited-attr が見張れない。開くと base-ui が外側へ aria-hidden を 付けつつ tab 順から外さない上流バグ (mui/base-ui#5528、open) で aria-hidden-focus が 出るので、ADR-0022 節 7 の作法どおり issue 番号を添えてこの story でだけ止めた - progress.stories.tsx の WithLabel から meta の aria-label を外し、名前が ProgressLabel 由来であることを play で固定した。ProgressLabel があると aria-labelledby が優先されるため、残すと命名の経路が壊れても story が緑のまま通る silent failure - toast.tsx の ToastIcon が対応表に無い type を無言で落としていた。通す方針は変えず、 type が未指定 (正常) と対応表に無い (綴り違いや上流の rename) を分けて warn する story が catalog として壊れていた箇所 - scroll-area.stories.tsx の SizedByViewportClassName に置いた decorator は no-op だった。Storybook は story の decorator を meta のものと合成するので、meta へ置いた 固定寸法の器を story 側から外せない。器を必要とする 3 story へ移した - skeleton.stories.tsx の className から rounded-md を外した。@shadcn/lint の分類で rounded-* は shape で layout ではなく、directory-structure.md の「layout に限る」に 反する。Skeleton 本体が既定で当てており重複でもあった - item.stories.tsx の MEDIA_VARIANTS を対応表から導いた。リテラルで並べると variant を 足したとき satisfies は落ちるのに配列は古いまま通り、story が網羅を失う 重複と保守 - Object.keys({...} satisfies Record<T, null>) の idiom と同一の docstring が 8 つの story へ写されていた。variant-options.story-helpers.ts へ variantOptions として 切り出し、8 ファイル 9 箇所と button / toggle の helper 4 箇所を機械的に置き換えた。 欠けたキーと余分なキーの両方が型エラーになることは単体テストで固定した (+3 case) その他 - sidebar.stories.tsx の querySelector の結果を instanceof で絞ってから使う (前例: dialog-scroll-body.stories.tsx) - input.stories.tsx の story 名 Date がグローバルを覆っていたので DateInput へ - ADR-0006 の 3 行を実態に合わせた。progress は children ありの穴が型では塞がらず axe の aria-progressbar-name が捕まえること、item は orientation prop も落ちること、 combobox は上記の型の変更 progress.tsx と item.tsx のコードは変えていない。progress は型で ProgressLabel の 存在を強制できず axe が二重の網になっているため、item は orientation の消費者が 0 件で復活は縦罫との分岐が要るためで、どちらも ADR への記録で足りると判断した。 mise run verify は exit 0、Tests 799 passed。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #40 再レビュー指摘 6 件を本対応する 規範と実装の食い違いを、直した先で作っていた - .claude/rules/base-ui.md の「ComboboxContent はアクセシブル名を型で要求する。 書き忘れは vp check が止める」が 9e4b870 で偽になっていた。ADR-0006 だけ直して rule を残したため。同ファイルの frontmatter は paths: src/** で src の作業に自動で ロードされるので、読んだ側が「型が止めるはず」と検証を省く。実態へ書き直した 型として効いていなかった記述 - combobox.tsx の & Partial<{"aria-label"; "aria-labelledby"}> は no-op だった。 Popup.Props は div の props 経由で React.AriaAttributes を含み、両方すでに任意で 受かる。外しても <ComboboxContent aria-label="x" /> がエラー 0 で通ることを実測して 確認し、削除した。契約が残っているように読める記述を消す - variant-options.story-helpers.ts の型引数は推論されるため、 variantOptions({ a: null }) と書けば網羅の検査が無言で消えていた。旧 idiom の satisfies Record<T, null> は型名の明記が必須だったので、検査が弱くなっていた。 helper は Record<string, null> を受ける形にして satisfies を呼び出し側へ戻す (13 箇所)。単体テストも satisfies 形へ 抑制の非対称に理由が無かった - combobox.stories.tsx で InlineWithTrigger だけ aria-hidden-focus を止めている理由を 書いていなかった。実測 (2026-09-21) すると InlineWithClear の addon に残る操作子は combobox-clear だけで tabindex="-1" を持ち tab 順に入らないため、axe が落とさない (trigger は clear があると CSS で消える)。抑制を外して確かめると落ちるのは InlineWithTrigger だけだった。理由をコメントに残した 撤回 - toast.tsx の console.warn を撤回した。同ファイルの doc が「対応表に無い type は icon なしで通す。狭めると base-ui が許す独自の type を塞ぐ」と独自 type を正常系と して宣言しており、その正常系で毎レンダー鳴る。綴り違いと独自 type を区別する手も 無い (base-ui の type は string で突き合わせる定義が無い)。テンプレートとして配る 以上、独自 type を使う側で鳴り続ける。silent failure ではないことを doc に明記した - item.stories.tsx の filter は実行時には恒真だが、Object.keys の string[] を型 アサーション無しで絞る手が型述語しか無いため残す。理由をコメントに書いた mise run verify は browser project で screenshot timeout の flake が 3 件出たが、 --project=browser 単独では 191 件すべて通過する (Test Files 37 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: registry 逸脱の抑制に上流の追跡先を書く `ItemGroup` の role="list" と `BreadcrumbPage` の role="link" は、どちらも抑制の理由を その場に書いていたが追跡先が無く、上流を見張れない状態だった。調べたところ両方とも 上流に issue があった。 shadcn-ui/ui#11532 (OPEN) は ItemGroup の欠陥そのもので、本文が逸脱の理由と一致する。 An explicit role="list" makes ARIA, rather than the DOM, responsible for the children. A <div role="list"> containing <div>s has no list items, so screen readers announce "list, 0 items". ADR-0006 の該当 3 行 (ItemGroup の useRender 化 / ItemSeparator の Separator 外し / prefer-tag-over-role の抑制) と item.tsx のコメントへ番号を入れた。撤去条件が 「上流が list の意味をネイティブのタグで出したら」なので、見張る先が要る。 shadcn-ui/ui#7639 は BreadcrumbPage の role="link" + aria-disabled が linter に 指摘された報告で、コメント無しの NOT_PLANNED で閉じている。上流が直す見込みが無いこと 自体が判断材料なので、撤去を待つ対象ではないと書き添えた。 button-group / field / input-group の prefer-tag-over-role は 8 通りのクエリで該当 issue が見つからなかった。fieldset の既定意匠を避ける設計判断で、上流の欠陥ではない。 #8475 (InputGroup + FormControl の label focus) はヒットするが別件である。 ロジックの差分は無く、コメントと ADR 本文だけを足した。 mise run verify は exit 0、Tests 800 passed。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
shnakatani
added a commit
that referenced
this pull request
Sep 21, 2026
review.md の判断軸 (本 PR の差分が直接の原因か) を当て直したところ、 起票が妥当なのは PR #35 由来の 1 件だけだった。 - contrast.stories.tsx の 19 箇所のボイラープレートを Row 部品へ畳む。 棄却理由「配列にすると静的解析から見えなくなる」は事実として誤りで、 読めなくなるのは .map() のコールバック引数だけだった。架空クラスを Row 経由で入れて no-raw-colors が捕捉することを確認した - ADR-0025 に、example-placeholder への置換で消費側の上書きが cn で dedupe されなくなったことを残す。勝敗が CSS のソース順に依存する - ADR-0024 の節 5 に、Storybook の a11y ゲートが violations だけを見て incomplete を見ないことを残す。背景が解決できなくなると story の検算は 緑のまま何も見なくなり、src/test/a11y.ts とは基準が揃っていない - ADR-0024 の Consequences に、文書とコメントの比率が人の書き写しで 追随しないことを残す。本ブランチでも 3 箇所が古いまま残っていた Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shnakatani
added a commit
that referenced
this pull request
Sep 21, 2026
* デザイントークンを上流生成物から作り直し、コントラストを段で解く (ADR-0024) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR レビュー指摘 2 件を本対応 (検算 story の参照先と網羅) - 実在しないファイルへの参照を docstring から外し、理由を本文で述べる - 未検査だった bg-muted/50 / bg-secondary/80 / bg-input/30 / bg-input/50 に行を足す Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * toast の成功アイコンへ --success を割り当てる Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * a11y helper の 1.4.11 の注記へ実測値と上流 issue を添える Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR レビュー指摘 1 件を本対応 (1.4.11 の注記から未検証可能な比率を除去) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * --muted-foreground からプレースホルダー色を --placeholder へ分離する --muted-foreground は --muted 上の本文 (mist-600) とプレースホルダー例示テキスト (要 mist-500) で要求されるコントラストが競合していたため、input / textarea の ::placeholder 疑似要素だけを --placeholder トークンへ切り出す。select の data-placeholder は疑似要素ではなく実テキストで WCAG 1.4.3 (4.5:1) が掛かり、 dark で 3.93 に落ちて違反するため対象外とし --muted-foreground のまま残す。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR レビュー指摘 1 件を本対応 (dark の --placeholder にスコープ注記への導線を足す) light の --placeholder 直上に置いたスコープコメント (select を対象外にする理由) が dark の宣言からは約 40 行離れて見えず、dark だけを触る編集者に伝わらなかった。 --destructive-surface / --success が light/dark 双方に短い注記を持つ既存の書式に 揃え、dark 側の値が light と意図的に同値であることと、詳しい理由は :root の コメントを見ることを 1 行で示す。3 行コメントの複製はしない (複製すると片方の 編集で内容がずれる)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * トークンの決め方と placeholder の扱いを ADR へ記録する (ADR-0024 / ADR-0025) - ADR-0024 を新設。上流生成物を土台に置き、乖離を WCAG の実測と palette の段で 決める。有彩色アクセントの light / dark 反転、両立しない 2 役のトークン分割、 リポジトリが持つ検算の範囲を含む - ADR-0025 を新設。placeholder には例示だけを置き、色を専用トークンへ切る。 axe が `::placeholder` を読まないため、この値はどの検査にも映らない - ADR-0006 を改訂。`src/styles.css` を生成時 baseline の対象に加え、 styles.css の乖離とコードの乖離の許容リストへ行を足した - ADR-0022 の節 6-1 を、実ペアを描く story を採った結果へ書き換えた - `.claude/rules/implementation.md` の「色とコントラスト」へ段に乗せる規律を追加 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: /simplify の指摘 2 件を本対応 (hover を採らない理由の誤帰属と却下範囲) - `contrast.stories.tsx` の docstring が、ポインタ退避を a11y helper の仕組みと 書いていた。実体は `src/test/park-mouse.ts` で、理由も transition ではなく 全体 run で前のファイルの click 位置が残ること - ADR-0024 の節 5 の却下を、ポインタを当てる形だけでなく擬似クラスを強制する形へ 広げた。Storybook の test 実行は animation を止めないため (ADR-0022)、強制しても 途中の色が残る Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: /simplify の指摘 2 件を本対応 (トークンの露出範囲と probe の重複) - `--placeholder` を `@theme inline` から外し、`@utility example-placeholder` だけで 露出させた。通していると `data-placeholder:text-placeholder` まで生成され、select の 実テキストへ当てられる。公式も utility を生やさない値は `@theme` でなく `:root` へ 置くとしている (tailwindcss.com/docs/theme)。到達できないことが制約になり、 コメントで守る必要が消えた - `placeholder-` は Tailwind の色 utility の名前空間で `shadcn/no-raw-colors` が 未宣言の色として落とすため、utility 名は `example-placeholder` にした - probe 要素で CSS 変数を算出色へ解決する技法が `sidebar.test.tsx` と `form-fields.test.tsx` に二重にあったので、`src/test/resolve-color-token.ts` へ 出して両方から呼ぶ。scope を受け取れるようにし、境界条件の単体テストを併設した Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 レビュー指摘 9 件を本対応 - resolveColorToken を未定義トークンで fail-closed にする。var() の置換に失敗した color は継承色へ落ち、綴り違いやリネームの取りこぼしが緑のまま通っていた - 同テストが pin していた --destructive の百分率表記を、テスト所有のトークンへ置き換える - registry baseline の網羅検査に styles.css を登録する - .claude/rules/styling.md に露出の 2 経路 (@theme inline / :root + @Utility) を足す - contrast.stories.tsx の数え直しコマンドから漏れる scrim と、片テーマ専用の対を もう一方で描いたときの余裕 (最小 4.81) を明記する - ADR-0024 に --destructive-surface と --placeholder で露出を分けた理由を残す - ADR-0006 の --success 行から、存在しない消費者 (本文) を根拠にした記述を外す - .storybook/preview.css の未定義トークン一覧から --radius-3xl を外す (本ブランチで定義済み) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 再レビュー指摘 4 件を本対応 - resolveColorToken のガードを CSS.supports へ広げる。空判定だけでは --radius のような 色でないトークンが通り抜け、色と同じく継承色へ落ちていた - registry baseline の残骸検出から .css が外れていた。間接の壊し方 (src/styles.css の rename) で落ちないため、拡張子の絞りに外部エントリを通す - .storybook/preview.css の Typography 側も実測へ揃える。--font-mono が抜け件数も 合っていなかった。日付も再実測の 2026-09-21 へ - contrast.stories.tsx の 4.81 を、axe が読む丸め後の 4.79 へ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 2 回目のレビュー指摘 8 件を本対応 - ADR-0025 の dark の比は --background の上でしか測っていなかった。フォームの多くが 出る Dialog / Sheet / Popover (--popover) では 3.93 でなく 3.34 になる - ADR-0006 の「styles.css の取得漏れは機械では鳴らない」を、本ブランチで鳴るように した事実へ合わせる。--success 行の宛先の無い ADR-0024 参照も外す - styling.md の露出の判定を、ADR-0024 が実際に採った軸 (誤用を誘う既存の形の有無) へ 直す。「当ててはいけない場所を持つ色は通さない」は --destructive-surface と矛盾する - 本ブランチの --foreground / --muted の変更で動いた比率コメントを再計算する (segmented-radio-group 5.27→5.05 / 20.17→19.71、data-table 3.81→3.70) - contrast.stories.tsx の数え直しコマンドが桁付きトークン (chart-1 等) を拾えなかった。 併せて field.tsx と select.tsx の面と文字色の対 3 つを描く - .dark の --placeholder は :root と同値の重複だったので宣言を消す。dark でも mist-500 のまま解決することを実機で確認した - meta の layout: "padded" は Storybook の既定で、リポジトリで唯一の宣言だった Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 棄却の再検証で覆った 2 件を本対応 公式 docs と上流ソースへ当て直した結果、棄却の理由が事実として成立しない ものが出たため直す。 - registry-baseline.test.ts の baselineFiles() から絞り込みを外し、全件を 分類して落ちないものを失敗させる。EXTERNAL_REGISTRY_FILES が期待値と フィルタを兼ねており、登録を 1 件消すと走査対象からも外れて無言で緑に なっていた。同じ構造の禁止は scripts/lib/companion-files.ts が明文で持ち、 deny-by-default の先例は同ファイルの「ui 直下に想定外のサブディレクトリが 無い」にある。登録削除の mutant が落ちることを確認した - resolveColorToken の判定を CSS.supports から sentinel 親へ変える。 CSS.supports("color", "currentColor") は true を返すが継承色へ解決される ので、測りたい値ではない。親を既知の色で塗ると、置換に失敗した経路が すべてそこへ集まる。tokens.stories.tsx との判定の重複も消える (あちらは分類器、こちらは guard で役割が違う) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: resolveColorToken に @Property を採らない理由を残す 不正値が parse 時ではなく算出時に無効となり initial-value へ戻るため、 登録すると綴り違いもリネームの取りこぼしも既定の色として通る。 検出を強める提案として再提案されやすいので、docstring に理由を置く。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: ADR-0025 の規格の扱いを調査結果に合わせて直す 規格原文と上流 issue へ当て直した結果、3 箇所が実態と食い違っていた。 - 「例示に 1.4.3 が掛かるかは未決着」は範囲が広すぎた。Understanding SC 1.4.3 の Intent は "including placeholder text" と名指しで含めており、w3c/wcag#4343 が 問うているのは「可視ラベルの文言を繰り返すだけの placeholder」に限られる - Carbon を低コントラスト許容の根拠に引いていた。carbon#19553 の立場は Intent と 正面から食い違うので根拠から外し、参照を 3 分類の枠組みだけに絞る。割る側を 選んだのは入力値との区別を優先したためで、正当化ではないと明記する - 「axe は ::placeholder を読まない」は不正確。空の入力欄にもマッチし、要素自身の color で判定して placeholder を評価したかのような違反を出す (axe-core#4260) - 面ごとに測る根拠を Note 3/4/6 で補い、W3C 自身の #767676 が白を仮定した値で あることを例に足す Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: ADR-0025 の「面ごとに測る」を、条文でなく定義からの導出として書き直す 「面を列挙せよ」と書いた条文は Understanding / Techniques / ACT に無く (2026-09-21 に検索して不在を確認)、Note 3/4 の背景の定義と Note 6 の 複数形から導く形になる。要求と書くと、根拠を確かめた人が条文を探して 見つけられない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 起票へ回していた 4 件のうち本 PR 案件を引き取る review.md の判断軸 (本 PR の差分が直接の原因か) を当て直したところ、 起票が妥当なのは PR #35 由来の 1 件だけだった。 - contrast.stories.tsx の 19 箇所のボイラープレートを Row 部品へ畳む。 棄却理由「配列にすると静的解析から見えなくなる」は事実として誤りで、 読めなくなるのは .map() のコールバック引数だけだった。架空クラスを Row 経由で入れて no-raw-colors が捕捉することを確認した - ADR-0025 に、example-placeholder への置換で消費側の上書きが cn で dedupe されなくなったことを残す。勝敗が CSS のソース順に依存する - ADR-0024 の節 5 に、Storybook の a11y ゲートが violations だけを見て incomplete を見ないことを残す。背景が解決できなくなると story の検算は 緑のまま何も見なくなり、src/test/a11y.ts とは基準が揃っていない - ADR-0024 の Consequences に、文書とコメントの比率が人の書き写しで 追随しないことを残す。本ブランチでも 3 箇所が古いまま残っていた Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: contrast.stories.tsx に args 形を採らない理由も残す CSF3 の args へ className を移すと .map() の引数と同じく解決されず、 クラス名が lint の対象から外れる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: tokens.stories.tsx の getComputedStyle の理由付けを削る 「呼ぶたびに root のスタイル解決を起こす」は成り立たない。DOM を汚して いなければ再計算は起きない (Paul Irish "What forces layout/reflow": "Reflow only has a cost if the document has changed and invalidated the style or layout.")。readAllTokens は DOM を変更しないので、使い回しは 性能のためではない。 誤った理由を別の値へ差し替えず、文ごと削る。使い回すこと自体は自明で、 説明が要らない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 再レビュー (コード) の指摘 8 件を本対応 - registry-baseline の一覧を readdirSync から git ls-files へ。全件分類に したことで gitignore 済みの .DS_Store まで未分類として落ちる退行を 入れていた。併せてサブディレクトリの孤児も相対パスで落ちるようになる (先例: lint-config.test.ts の「追跡しているソースが lint の対象に入っている」) - resolveColorToken のエラーへ宣言の字面を戻す。「定義が無い」と「値が色で ない」がメッセージから区別できなくなっていた - Typed OM の記述を実測へ合わせる。computedStyleMap は getComputedStyle と 同じ展開済みの値を返す。展開しないのは指定値のパーサである CSSStyleValue.parse だけ - @Property が殺す範囲を限定する。死ぬのは「色でない値」の検出で、未登録の 名前を引く綴り違いは var() の置換に失敗して今までどおり落ちる - contrast.stories.tsx から bg-primary/10 + text-primary の行を落とす。 bg-primary/10 は field.tsx の ChoiceCard の dark にしか無く、button.tsx の dashed は dark の上書きを持たないので、この対は画面に存在しない - lint が読める形の説明を実測へ合わせる。.map() では配列の中身ごと見えない。 読まれるのは className 直下の文字列リテラルと cn() の引数の配列リテラル - sentinel 衝突が機械で守られていないことを注記する Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 再レビュー (文書) の指摘を本対応し、規格の帰属と面の記述を直す - ADR-0025 の「--background の上で 3.93 / --popover の上で 3.34」は面の 記述が誤り。どちらも bg-input/30 を載せた入力欄の面の値で、素の面なら 4.27 と 3.76 になる。表の行見出しとも食い違っていた - Note 3 / 4 / 6 は Understanding ではなく WCAG 2.2 勧告本体にある。 帰属を直し、根拠を規範側へ置く。丸めるなの note は逆に Understanding にしか無いので ADR-0024 側をそう書き直す - Note 6 の引用の当て方を直す。免除の対象は "unusual presentations" で、 UA の色変更はその例示。"except where caused by authors' code" は免除の 例外側の句であり「免除されるのは UA 由来だけ」の根拠にならない。テーマや 面の切り替えは typical presentation の側に直接入る - w3c/wcag#4343 の射程を issue の字面へ戻す。「情報を足さない placeholder」 であって字面の繰り返しに限らない。detlevhfischer の placeholder="Mary" は 例示にコントラスト要件を課さない見解で、本 ADR の立場を支持する材料だった。 philljenkins の 3:1 + イタリック案も決着していないので併記する Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: 再レビュー (文書) の残り 5 件を本対応する - ADR-0024 の節 2 で「light の /80 が 4.6 を下回る hue」を orange / emerald / teal の 3 色と書いていたが、実測すると 9 色 (amber / yellow / lime / green / cyan / sky が漏れていた)。amber を選んだ人は規則に従っても 4.44 で割る。9 色すべてと、<hue>-900 へ下げた後の値を表で持つ - 同 ADR の「--border / --input は約 1.25」に dark の --input が入らない。 白 15% で 1.48 なので範囲で書く - ADR-0025 の grep が主張を支えていなかった。'placeholder:' は React の prop も select の class も返す。'placeholder:text-' で 1 件に絞る - Carbon の出典ラベルが逆だった。3 分類は #7515 ではなく #4799 のレビューに あり、#7515 は例示と書式の 2 分けでそこへリンクしている - segmented-radio-group の 5.05 / 19.71 は light の値。dark を併記する Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: ADR-0024 の上流実測の行を再現できる書き方へ直す 「dark 1.95〜2.78 / lime と yellow は light 1.54・1.57」は正しいが圧縮 しすぎていた。dark の範囲が 17 テーマ全部に掛かると読め、正しい変換器を 持つレビュアーでも再現できなかった。lime と yellow は dark が 10.08・ 10.30 で通り light 側で割る、という対比を書き下す。 併せて、この表が上流の生成物の値で、節 2 以降で決める本リポジトリの段では ないことを明記する。レビュアーは本リポジトリの規則 (hue-800 / hue-50) で 計算して再現できなかった。 sweep-out の生成物 25 テーマ分から有彩色 17 を抽出して再測し、表の値と 一致することを確認した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 3 巡目レビューの指摘 2 件を本対応 - registry-baseline の一覧を git ls-files --cached から「disk の再帰走査 − gitignore 済み」へ戻す。.DS_Store を通す目的で index に切り替えたが、 目的は「無視対象を除く」であって「未追跡を除く」ではなかった。git add 前の 綴り違いの baseline が走査から消えて無言で緑になっていた - gitPaths に -z を付ける。既定の git は非 ASCII を八進エスケープで引用し、 readdirSync の実名と一致しないため、無視対象の非 ASCII が差し引かれずに 孤児として報告される。.DS_Store と同じ穴を別の入口から作っていた - contrast.stories.tsx の「読まれるのは 2 つだけ」を削る。8 形態で実測すると 読まれないのは .map() のコールバック引数と args の 2 つで、cn() の文字列は 読まれる。同ファイル自身が cn("rounded px-3 py-2 text-sm", className) を 使っており、コメントが自分のコードと矛盾していた 4 方向を実測: 無視対象 (ASCII / 非 ASCII) は通り、未追跡の孤児・サブ ディレクトリの孤児・登録削除の mutant は落ちる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 3 巡目レビューの指摘 5 件を本対応 文書側 (rr2-docs): - ADR-0024 の base color の件数が誤り。CLI が受け付けるのは 7 色 (neutral / zinc / stone / mauve / olive / mist / taupe) で、gray と slate は registry が配信するが選択肢に無い。「8 色中 7 色」→「7 色中 6 色」 - 同じ表の --border / --input の範囲 1.25〜1.48 は mist 単体の振れ幅だった。 7 色 28 値で測ると 1.23〜1.49 (両端とも olive) - 節 2 の表の列見出しが 1 列ずれ、hue 名の列が「<hue>-800 のまま」と読めて 本文と逆の意味になっていた - ADR-0025 の「2 つの note」が Note 3 / 4 / 6 の 3 つを指していた コード側 (rr2-code): - resolveColorToken のメッセージから「未定義」の断定を外す。chromium 153 は 未定義にも空値 (--x: ;) にも "" を返し区別できない (実測)。到達しない 分岐を作らず「未定義か空値」と書く - メッセージ本体にアサートが無く、宣言の字面を消しても 6 件すべて緑だった。 2 本を字面まで見る形にし、消すと落ちることを確認した Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 3 巡目レビューの残り 7 件を本対応 - ADR-0025 の「上表は…bg-input/30 に置いた値で」は dark 行にしか当たらない。 light 行は素の --background 上で 4.61 (bg-input/30 を載せると 4.33)。 前回の修正で、誤りが dark 側から light 側へ移っただけになっていた。 light に bg-input/30 が無い理由 (input/textarea が dark: 限定で付ける) も添える - 同「Understanding 側の再掲ではない」は不正確。Understanding の Key Terms にも 逐語で載っている。規範の出所が勧告本体の glossary だと書き直す - 同、前回落とした検索範囲 (勧告本体 / Understanding / Techniques / ACT) を戻す - 同、w3c/wcag#4343 の軸を原文へ合わせる。免除されるのは「例示かどうか」ではなく 「ラベルが名指していない情報を足さないか」で、detlevhfischer は同じコメントで placeholder="Mary Smith" を適合しない側に置いている。Decision もその軸へ直す - 同、支持材料だけを引いて mbgower の反対 (冗長性は関係なく例外は及ばない) を 落としていたので足す - ADR-0024 節 2 の表に測り方を書く。下地・文字の段・丸めない旨。orange だけが 丸めない 4.5873 と 8bit 4.5955 で 2 桁表示の判定が割れる - segmented-radio-group の「dark は切替経路を用意していない」が実態と違う。 アプリに切替 UI は無いが Storybook は両テーマで描く。だから dark の比が測れる Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 4 巡目レビューの指摘 3 件を本対応 Decision を「ラベルが名指していない情報を足さない例示だけ」へ狭めたのに、 古い軸 (例示かどうか) のまま残っていた 3 箇所を揃える。 - ADR-0025 の「例示と指示で変わる分け方は Carbon と #4343 が共通して採る」は 同 ADR の直前の行と逆。#4343 の軸は「ラベルが名指していない情報を足すか」で、 Carbon のカテゴリとは別。結論が同じところまでを書く - 同、運用手順の「例示かどうかを確かめる」を軸へ合わせる。例示であることは 条件を満たさない (placeholder="Mary Smith" は例示だが対象外) - docs/decisions/README.md の要約が「例示は --placeholder」のままで、索引だけ 読むと ADR と逆の結論になる タイトルは据え置く。ADR-0006 / ADR-0024 / styles.css などが番号と名前で参照 しており、限定は索引の要約と本文が持つ。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 4 巡目レビューの指摘 4 件を本対応 前巡で Decision を「ラベルが名指していない情報を足さない例示だけ」へ狭めた のは誤りだった。2 つの別の問いを 1 つの規則に混ぜていた。 - 何を置くか (Carbon #7515 の分類。例示か指示か) - その色が 1.4.3 から免除されるか (w3c/wcag#4343 の軸。ラベルが名指して いない情報を足すか) この 2 つは一致しない。混ぜた結果、(a) 同じ ADR の中で軸が食い違い、 (b) 運用手順が Decision に追随せず、(c) リポジトリ自身が Decision を破る 状態になった (placeholder="name@example.com" は正当な例示だが免除には 入らない)。 - Decision を content の規則へ戻し、免除の軸は別の段落として持たせる - Consequences に、確かめる 2 点と、(2) を満たさない 2 件 (name@example.com と 0、どちらも story のカタログ、dark 3.93) を実測で記録する - README の索引の要約も 2 つの問いが分かれた形へ - Context の「軸は」を「免除の軸は」へ限定する placeholder の実数は 22 件 13 種 (python で全数走査)。うち免除に入らないのは 上の 2 件で、残る 11 種はラベルの言い換えか値の例示。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: PR #44 4 巡目レビュー (コード) の指摘 4 件を本対応 - registry-baseline の照合を NFC へ揃える。-z が止めるのは引用だけで、macOS の git は core.precomposeunicode で readdir の NFD を NFC へ直して出力する。 濁点を分解した名前の gitignore 済みファイルが孤児として報告されていた。 .DS_Store → git ls-files → -z と 3 回塞いだ穴の 4 つ目の入口 - 同、symlink を走査へ入れる。isFile() だけだと無言で消え、docstring の 「どの分類にも落ちないものを失敗させる」が成立しない - resolve-color-token.test.tsx が --radius の値 (styles.css) を pin していた。 同ファイル 5-6 行が禁じている形そのもので、角丸を変えると helper の不具合の 顔で落ちる。テストが所有する probe トークンへ移す - contrast.stories.tsx の「8 形態」「読まれなくなるのは 2 つだけ」を実測へ。 11 形態で測ると読まれないのは 3 つ (map の引数 / 補間ありテンプレート / args) 3 方向を実測: 無視対象 (NFD) は通り、symlink と未追跡の孤児は落ちる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Storybook を導入し、デザイントークンのカタログと a11y の自動検査を足す。3 段の stack の 1 段目で、基盤とトークンの story までを持つ。部品の story は 2 段目以降。
@storybook/tanstack-react。tanstackStart()plugin と Storybook の Vite builder が衝突する (storybookjs/storybook#33747) ため、標準の Vite builder は採らないvitest.storybook.config.tsを足し、story をvp test runの project として走らせる。parameters.a11y.testは"error"src/components/tokens.stories.tsxに 3 story (Colors/Radius/Typography)。値はstyles.cssからgetComputedStyleと CSSOM で実測し、story 側に書き写さないuseSyncExternalStoreで購読する。keyによる remount は採らない。テーマの class はSTORY_RENDERED後に当たるため、remount は play function より後に起きて play が作った状態を捨てるCSS.supports("color", value)に任せる。ブラウザ自身のパーサなのでoklch()もcolor-mix()も relative color syntax も通るsrc/components/*.story-helpers.tsへ置き、no-restricted-importsでアプリからの import を止める (*.test-helpers.tsと同じ形。Storybook 自身もFilter.story-helpers.tsxを使う)*.test.*/*.test-helpers.*/*.story-helpers.*/*.stories.*) の種別をscripts/lib/companion-files.tsに 1 つ置き、lint の適用外・coverage の除外・registry baseline の突き合わせをそこから導出する.claude/rules/を更新するmise run storybookを足すtokens.stories.tsxのtitleを落とし、サイドバーの見出しをファイルパス由来へ一本化するAGENTS.mdに引き方を書く本番 CSS が 12,811 バイト減る
Storybook のカタログを作る過程で、本番 CSS に不要なものが載っていることが分かった。どちらもこの template から作られる全プロジェクトが払っていた分になる。
1. Tailwind の source scan がリポジトリ全体を読んでいた。 scan は Markdown も対象にするため、ADR や rules に書いた class 名・変数名、
docs/registry-baseline/の上流コピー、.claudeの skill、CI の YAML のキーまで utility や@propertyとして出力されていた。@import "tailwindcss" source("../src")で対象をアプリのソースへ絞る。落ちるセレクタ 43 件はすべてsrc/の外に由来する。2.
@theme staticを本番に置かない。inlineは utility へ値を直接埋め込むため、実際に使っているトークンでも変数が出力されず、カタログから消える。staticは.storybook/preview.cssがsrc/styles.cssをtheme(static)付きで読み直して掛ける。theme()は import 単位で効くので本番は払わない。代償としてカタログに Tailwind 既定の未定義トークンが混ざる (Radius 2 件 / Typography 6 件。Colors は--color-*: initialが効いていて増えない)。差の測り方は
source()とstaticをそれぞれ外してvp buildを 2 回回す。agent 向けツールは同梱の CLI を使う (ADR-0023)
storybook@10.6.0は agent skill 3 つ (stories/write-story/setup) とツール群を本体同梱の CLI で配っている。vp exec storybook --helpのコマンド一覧にskillsとtoolsが出ないため、書き残さないと見落とす。@storybook/addon-mcpは入れない。決め手は、MCP の登録がエージェント側の設定に URL を 1 つ持つ形である一方、この repo の Storybook の port は worktree ごとに変わるので collaborator へ同じ登録を配れないこと。起動の要否は 6006 の Storybook を止めて
--no-attachで 1 つずつ測った。docs list/docs show/stories changed/test runは起動なしで動く。stories find-by-componentは起動なしでも走って結果が空で返るため、story が無いのと区別が付かない。この落とし穴も含めて ADR-0023 が持つ。副次的な挙動の差分
.mise.tomlの[env]ではなくタスクのrunで導出する。env の解決ごとに git のサブプロセスが走らなくなり、mise hook-envは 70ms から 28ms になった (2026-09-20 実測)。シェル hook を入れていない手元でも port が決まるderive-dev-port.shの算出lint-config.test.tsの検出範囲excludeFilesから付随ファイルぶんを差し引いてから突き合わせる。種別を足したときの写経が消え、代わりに「付随ファイルの除外を持つ override は 1 つだけ」を固定する検査が増えるtokensstory の見出しtitle: "Tokens"を落としてファイルパス由来のtokensになるtsconfig.jsonincludeに.storybook/**を足した扱わないもの
esbuild@0.18.20の Dependabot 警告 (medium) は main に既存で、本ブランチは同じバージョン集合のまま変えていないコミット履歴について
コントラストの検算を入れる試みを 2 つ行って撤去したため、自己相殺するコミットが含まれる。積み直さずに残してある。
レビューは 4 巡し、指摘 33 件をすべて本対応した。その後に再利用・簡潔さ・効率・対処の深さの 4 観点で品質改善を行い (7 コミット)、その差分をもう 1 度レビューして指摘 10 件のうち 7 件を本対応した。棄却 2 件は
select()の毎 render 実行 (ADR-0009 の決定 5「予防的なメモ化は入れない」) と、単独では恒真だが同ファイルの別テストが字面を pin しているテスト。さらに 1 巡レビューし、指摘 14 件のうち 11 件を本対応した。見送り 3 件は次のとおり。
companionGlobsの末尾/を実行時に強制する/終わりで違反 0 件。coding-discipline.md「0 件なら作らない」matches()の呼び出しを間引く--tw-*を宣言し、--の有無では間引けない。走査はテーマ切替と HMR 時のみcacheDirを共有するconfigDirを 1 つ共有する形は Storybook 公式のマルチテーマ例と同じ (storybookjs/storybook#35226)。cacheDirを消して 3 回実行しても再最適化による失敗は出ないレビュー指摘のうち 2 件は、調査の結果その指摘自体が誤っていた。
theme(static)の出典は Preflight で正しく、欠けていたのは「参照された変数だけを出力する」側の/docs/themeだった。swatch の塗り方はvar()ではなく値へ揃えた (Storybook 同梱のColorItemとstorybook-design-tokenが値で塗る)。ae6bf62の本文にある「実トークン 77 件」はstyles.cssの宣言を写した CSS で測った件数で、Storybook 上の実測ではない。正しくは root のカスタムプロパティ 241 件中 68 件が色で、削除した canvas 実装との判定差は 0 件。Test plan
vp check—Found no warnings, lint errors, or type errors in 182 filesmise run verify—Tests 390 passed (390)/[security-headers] 6 件すべてが応答に乗っているcss-rules13 /companion-files9 /theme-snapshot9 /theme-tokens5 /tokens.stories.tsx3 story /derive-dev-port+1 /lint-config+1)Colors35 件で非色トークンの混入なし、テーマ切り替えで 32 件の値が変わるstaticが漏れていないことを確認 (:root,:hostの宣言が 75 → 38 件)CSS.supportsと削除した canvas 実装の判定が全 241 トークンで一致することを Storybook 上で確認 (差分 0 件)lint-config.test.tsの検出力を mutant 6 種で確認。ルール削除 / ルールを off /excludeFilesへ無関係な glob / 層のexcludeFiles拡大 / 層へ付随 glob 追加 はTests 1 failed | 41 passed (42)、COMPANION_KINDSへの種別追加はTests 2 failed | 40 passed (42)🤖 Generated with Claude Code