Repository navigation
Evaluation v0.4 and Roadmap
対象:
@yasmro/schattenv0.4.0 作成日: 2026-05-11 スコープ: デザインシステムとしての完成度 / 堅牢性 / AI 親和性 / ガバナンス
Schatten は トークン体系の洗練と日本の伝統色彩世界観 を中核価値とする独創的な DS。プリミティブ層 (lv1) は十分に揃い、CI/VRT も成熟している。一方で 合成層 (lv2) / ドキュメント / AI 親和性 / ガバナンス強制 が次の山であり、これらが整うまで「1.0」を名乗るには早い。
| 観点 | 評価 | 一言 |
|---|---|---|
| トークン体系 | ⭐⭐⭐⭐⭐ | 3層構造 + OKLCH + 4-token state shape は業界水準を超える |
| ブランドアイデンティティ | ⭐⭐⭐⭐⭐ | 墨・和紙・朱・24節気 — 唯一無二の世界観 |
| コンポーネント網羅性 | ⭐⭐⭐ | 18個。プリミティブ層は十分、ナビ系・データ表示系が空白 |
| 合成層 (lv2) | ⭐ | 空 stub。「いつ作るか」の規範がない |
| テーマ拡張性 | ⭐⭐⭐ | 季節テーマは独創的だが影響面が --color-primary-* だけで狭い。「特別テーマ」概念が未整理で将来追加に脆い (§2.4) |
| a11y | ⭐⭐⭐ | Radix + Field context は良い。自動検証はゼロ |
| ドキュメント | ⭐⭐⭐ | Color / Typography / Welcome のみ。基礎ページが不足 |
| 採用導線 | ⭐⭐⭐⭐ | マルチエントリ export で部分採用しやすい |
| ガバナンス | ⭐⭐ | changeset 未強制、ADR なし、貢献ガイド薄い |
| 配布・バージョニング | ⭐⭐⭐ | 0.4.0 のまま、1.0 への基準が未提示 |
| アーキテクチャ | ⭐⭐⭐⭐ | レイヤー依存はクリーン、ただし合成パターンと外部依存の境界が曖昧 |
| パフォーマンス | ⭐⭐ | lucide-react が external 未指定で bundle 肥大化リスク、sideEffects 未指定 |
| SSR 互換性 | ⭐ |
'use client' ゼロ件 → Next.js App Router で破綻 |
| TestID 規約 | ⭐⭐ |
...props で通るが規約未文書化、Portal 配下の経路も未定義 |
| Theme 切替 API | ⭐⭐ |
ThemeProvider / useTheme 未提供、利用者が毎回自作 |
| AI 親和性 | ⭐⭐⭐ | 型 export と CLAUDE.md は良いが、トークン autocomplete 不可、variant 語彙不統一 |
| コード堅牢性 | ⭐⭐⭐⭐ | strict TS / Biome / VRT 整備済。changeset 未強制と test 抜けが課題 |
-
三層トークン構造 + OKLCH —
destructiveとerrorを意味分離しつつ primitive 共有する設計は Material 3 や Polaris にもない洗練 (src/core/tokens/semantic.css) - 24節気ベース seasonal theme — OSS DS で類例がない強烈な差別化要素 (src/themes/seasonal/index.ts)
- VRT 運用の成熟 — 失敗時 artifact + PR コメント自動生成、成功で削除 (.github/workflows/vrt.yml)
-
Framework-agnostic 路線が実体化していない (phantom class) —
.btnは CSS 定義なし、現状の framework-agnostic は token レベルのみで成立。Option A (1.0 DoD として保証) を満たすには 18 コンポーネント分の@layer components書き起こしが必要 (§2.8.1) -
'use client'ディレクティブがゼロ件 — Next.js App Router でビルド失敗する。Schatten が SSR フレンドリーだと誤解されるリスク -
lucide-react が external 未指定 —
import { icons } from 'lucide-react'で全 1500+ アイコンが dist にバンドルされる可能性、サイズ計測も未実施 - lv2 が空 stub — DS としての「合成の正解」が存在しない (src/components/lv2/index.ts)
-
トークンが TS 型を持たない — AI も人間も autocomplete 不能、
bg-red-500の混入を構造的に防げない - 8 コンポーネントが unit test なし / 3 が VRT spec なし — テスト網羅率が読めない
強み:
- 三層(primitive → semantic → component)の責務分離が徹底
- OKLCH 採用 — sRGB の hex を引きずる多くの DS より将来性がある
- destructive と error の意味分離(同じ vermillion を共有しつつ semantic としては別レイヤー)
- inverted-foreground の muted/subtle 階層化 (src/core/tokens/semantic.css:54) — 飽和背景上のテキスト階層まで考えられている
-
4-token state shape(base / hover / foreground / subtle)が
error,success,warning,infoで統一
課題:
-
typography が composite tokens を分解して提供している(
--text-body-md-size/-leading/-weightの3変数)。consumer が3つ手で当てる必要があり摩擦がある -
shadow / radius / motion トークンが semantic 化されていない。
--shadow-card,--radius-control,--motion-instantのような意味層がない → ブランド変更時にコンポーネント側を書き換える必要が出る -
z-index 用途別 semantic がない (src/core/tokens/z-index.css) —
--z-tooltip,--z-modal,--z-toast等の意味付けがあると統治しやすい
sumi / alabaster / paper-white/warm/cream / vermillion / ink-black — 命名から世界観が立ち上がる。shadcn の neutral-* には絶対に出せない武器。
24節気ベースの seasonal theme は OSS DS では類例がない。
ただし生かしきれていない:
- 季節で動くのは
--color-primary-*だけ。background/accent/borderは固定 → 「primary ボタンの色だけ変わる」程度の visual payoff - Storybook での見せ方が
Color.stories.tsxのみ。コンポーネント実例での効果が見えない
提案: --color-accent-*、--color-surface-*、--shadow-card まで [data-season] で変動させる + Foundations / Seasonal Showcase ストーリーで8季節 × Button / Badge / Callout を並べる。
18個。プリミティブ層は十分だが、実プロダクト構築で詰まる箇所が見える:
| カテゴリ | 現状 | 不足している典型 |
|---|---|---|
| Form | Input, Textarea, Select, Radio, Checkbox, Switch, Field, FieldSet | Combobox / Autocomplete, DatePicker, Slider, FileUpload, Form (送信統括) |
| Disclosure | Dialog, Tooltip | Popover, Drawer, Accordion, Collapsible, HoverCard |
| Navigation | – | Tabs, Breadcrumb, NavigationMenu, Pagination, Menu/DropdownMenu |
| Data display | Badge, Callout, Text, Separator | Table, Card, Avatar, List, EmptyState |
| Feedback | Toast, Spinner | Skeleton, ProgressBar, Alert (vs Callout の使い分け要定義) |
| Layout | – |
Stack / HStack / VStack / Cluster — 不採用確定 (オーナー判断)。Tailwind 直書き + spacing-conventions.md で AI 制約 + Patterns / Layout ストーリーで運用 |
| Icon |
IconName = keyof typeof icons で lucide 直依存 |
<Icon> 共通コンポーネント — sizing / aria / lucide 差し替え抽象化なし |
特に痛い不在: Menu / Tabs / Card / Icon ラッパー。
src/components/lv2/index.ts は export {} のまま。これが構造的な弱点:
- 「Input + Label + Description + Error message」のような頻出セットがない → 利用者は毎回 Field + Input + label を組み合わせる
- 「Confirmation Dialog」「Form」のような典型パターンが lv1 から都度組まれ、プロダクト間の差異が増える
- 判定基準が文書化されていない — 何を lv2 にし、何を利用者プロダクトに残すか
提案: lv2/FormField, lv2/ConfirmDialog, lv2/PageHeader, lv2/EmptyState から始める。
src/docs/ は Color, Typography, Welcome のみ。必要なページが欠けている:
不足:
- Foundations / Spacing — 4px base scale の使い分け
- Foundations / Elevation — shadow トークンの使い分け
-
Foundations / Motion —
--transition-fast/normal/slowの意図 - Foundations / Iconography — lucide 採用、サイズ規則、a11y
-
Foundations / Accessibility — フォーカス可視性、
aria-*、コントラスト保証ライン - Patterns / Form Composition — Field / FieldSet のレシピ集
-
Patterns / Composition with
asChild— Radix Slot パターンの方針 -
Getting Started / For Astro consumers —
variantsエントリの意義 - Getting Started / Migration — 0.x 間の破壊的変更ガイド
- Contributing — コンポーネント追加プロセス、命名規約
Schatten は二層 DS である:
-
Layer A: Framework-agnostic CSS / Class DS — 素の HTML / Astro / Vue / Svelte で
<button class="btn btn-primary">の形で使える -
Layer B: React component DS — Layer A の上に乗る React ラッパー (
<Button variant="primary">)
これは shadcn 系 React DS との明確な差別化軸。「React に lock-in しない DS」というメッセージで打ち出せる強みであり、README の冒頭で第一に伝えるべき価値。
1.0 リリースは「両層が動く」状態で出す (Option A 確定)。現状の framework-agnostic は token レベルのみで、コンポーネントは React + Tailwind パイプライン依存 (§2.8.1 参照)。1.0 でこの実装ギャップを埋めることが Schatten の差別化を実体化する条件。
-
shadcn ベースなのに npm パッケージ配布という選択
- shadcn の根本価値は「コピペして自由に改変できる」こと。npm 配布にした瞬間その価値は消える
- しかし**「framework-agnostic な CSS クラス DS」という独自軸**を持つことで、単なる shadcn の劣化コピーではなく独立した価値命題を持つ
- README で「Schatten is inspired by shadcn/ui but ships as a framework-agnostic CSS + optional React layer」と明示
-
0.4.0 のまま — 1.0 リリースの基準が未提示
- 提案: 1.0 の DoD を明示する (詳細は §7 ロードマップ)
- Framework-agnostic 路線が確定したため、CSS クラス名と CSS 変数名も「公開 API」として 1.0 で固定対象になる
-
対象オーディエンス
- npm publish & GitHub Pages で Storybook 公開 → public 想定
- public なら ADR と「なぜ existing DS を選ばないか」のステートメントが欲しい
「堅牢性」が 個々の実装が壊れないか の評価なら、アーキテクチャは 構造が長期的に機能するか の評価。重複する論点もあるが、視点は別。
[Framework: React]
│
primitives.css ──→ semantic.css ──→ variants/*.ts ──→ components/lv1
(raw) (meaning) (CVA, no React) (React)
↑ ↑
│ │
themes/* (override semantic) contexts/* (perpendicular)
良い点:
- 単方向依存が守られている — primitive → semantic → variants → components の流れに逆流がない
-
src/variants/が React に依存しない — CVA 自体は framework-agnostic なので、Astro / Vue / Svelte への展開可能性を残した設計 (tsup.config.ts:18 で React 抜きエントリを切っているのと整合) - themes は semantic を上書きするだけ — components 側を変えずにブランド変更可能
懸念:
- contexts (FieldContext, FieldSetContext) はレイヤーに垂直に存在する。どの lv1 から消費して OK か / lv2 が context を消費するときの規範が暗黙
-
lib/utils.ts(cn) とlib/merge-refs.tsは共有ユーティリティだが、将来 lv2 が増えたとき lv2 が lv1 から import するのか直接 lib から import するのか未規定
package.json の exports は意図的にマルチエントリ:
| Entry | 内容 | 評価 |
|---|---|---|
. |
style: ./dist/schatten.css のみ |
import { Button } from '@yasmro/schatten' が動くようにすべき |
./schatten.css |
バンドル済み CSS | ◎ |
./core/tokens |
tokens CSS | ◎ 部分採用の窓口 |
./themes/default |
デフォルトテーマ CSS | ◎ |
./themes/seasonal |
TS (getCurrentSeason 等) |
◎ JS 実装が伴う唯一のテーマ |
./variants |
TS (CVA, React 抜き) | △ README に説明なし — 戦略的な隠し玉が利用者に伝わっていない |
./components/lv1 |
TS | ◎ 現状の主要 import 点 |
./components/lv2 |
TS (export {}) |
評価: マルチエントリは DS としての強み (段階的採用が可能)。ただし . の JS/types 未提供と空の lv2 で「API として未確定なものが混じっている」印象を与える。両方とも Phase 1 で解消する。
| コンポーネント | 形 | asChild | 備考 |
|---|---|---|---|
| Button | flat | ✅ | shadcn 標準 |
| Input | flat (内部に wrapper) | ? | |
| Select | compound (11 exports) | (Radix) | |
| Dialog | compound | (Radix) | |
| Tooltip | compound (Provider + Trigger + Content) | (Radix) | |
| Radio | hybrid (Radio + RadioGroup) |
? | |
| Field / FieldSet | context-only | n/a |
問題: 「いつ compound にするか」「いつ asChild を提供するか」の規範がない。新規追加するときに過去から類推するしかなく、AI も人間も毎回判断にコストがかかる。
提案: .claude/rules/component-architecture.md で明文化:
- Radix primitive をラップする場合は compound を踏襲
- 自前実装の場合は基本 flat。合成パターンが必要なら lv2 で
-
asChildは「他要素として renderable な必要があるもの」(Button / Link 系) のみ
並行する 2 つのテーマ機構:
-
Mode theme:
prefers-color-scheme+.darkclass (light/dark) -
Special theme:
[data-season="..."]属性 (seasonal の 8 種)
現状の課題(オーナー確認済の未解決事項): seasonal は「特別テーマ」の最初の例だが、特別テーマというカテゴリが概念として確立していない。将来、季節以外の特別テーマ(イベントテーマ / コラボテーマ / 色弱モード / 高コントラストモード)が追加される可能性が高く、その時に都度ルールを後付けすると破綻する。
整理が必要な論点:
| 論点 | 例 |
|---|---|
| 特別テーマが上書きしてよい token の範囲 | seasonal は --color-primary-* のみ? それとも --color-accent-*, --shadow-card も? |
| 特別テーマ同士の併存可否 |
spring-early と christmas-2026 を同時適用したらどうなる? |
| Mode theme との優先順位 | dark + spring-early のとき --color-primary-* は seasonal が勝つべきか dark が勝つべきか |
| 高コントラストモードの位置づけ | a11y 要件としてのモード? それとも特別テーマ? |
| 利用者が独自テーマを足せるか | yasmro 内製専用? OSS 公開時に第三者がテーマを追加可能? |
提案する整理モデル:
Theme Axis 1: Mode (排他) → light | dark | high-contrast
Theme Axis 2: Special (累積) → seasonal-{x} / event-{y} / custom-{z}
各 special は token allowlist を持つ
Cascade rule:
Special (最後勝ち) > Mode > base semantic
各特別テーマは 「自分が上書きする token 一覧」を宣言的に持つ。例:
export const springEarlyTheme = {
name: 'spring-early',
allowedTokens: ['--color-primary-*', '--color-accent-*'],
// Special テーマはこの allowlist 内のみ上書き可能。
// それ以外の token (foreground / background / border 等) は Mode テーマ (light/dark) が担当。
}Phase 1 で必要な対応:
-
.claude/rules/theme-architecture.md新設 — Mode / Special の二軸モデルと cascade ルールを明文化 - 「特別テーマは何を上書きしてよいか」の allowlist を CSS コメント or TS const で明示
-
Foundations / Theme Compositionストーリーで 8 seasonal × 2 mode = 16 パターンを視覚検証 - (将来) 利用者定義特別テーマを受け入れる API を検討
外部ライブラリの型・名前が Schatten の公開 API に直接漏れている箇所:
| 漏れているもの | 場所 | 影響 |
|---|---|---|
| lucide の icon 名集合 |
IconName = keyof typeof icons (Button.tsx:7) |
lucide のバージョン上げで Schatten の API が破壊的に変わる |
| Radix の Props 型 | 各 compound component (Select, Dialog, Tooltip) で間接 re-export | Radix のメジャー更新に追従が必要 |
| Lucide のアイコン Props |
<IconComponent className=...> を Button 内で展開 |
アイコンライブラリ移行が困難に |
影響: 「1.0 で API 固定」を宣言したくても、外部依存に縛られて固定できない。
対策 (5章 ROI 順 #4 と統合):
-
<Icon>ラッパー + 許可リスト型でアイコン境界を閉じる - Radix Props の公開は意図的なら明文化、そうでないなら型ガードを挟む
現状 18 個が flat 配置 (src/components/lv1/):
src/components/lv1/
├── Badge/ Button/ Callout/ Checkbox/ Dialog/ Field/ FieldSet/
├── Input/ Radio/ Select/ Separator/ Spinner/ Switch/ Text/
├── Textarea/ Toast/ Tooltip/ ← 計 18
50 個に増えたときの想定:
- フラット配置だとファイラーで一覧不能
-
index.tsの re-export が巨大化 - カテゴリ別の grep が困難
提案: lv1 内に category subdirectory を導入する閾値を決める。例: 25 個超えたら form/, disclosure/, navigation/, data/, feedback/, layout/ に再編成。あるいは Phase 3 の新規追加と同時に最初から category-based にする。
良い点:
-
ButtonProps extends ButtonHTMLAttributes, ButtonVariantsパターンが標準化 -
VariantProps<typeof buttonVariants>で variant 型が自動派生
未対応:
-
asChildを使うと 戻り値の DOM 型が変わる が、ButtonPropsはそれを表現していない (forwardRef<HTMLButtonElement>に固定) - polymorphic component pattern (
asprop) は未採用 — これは shadcn 慣習との整合で OK だが、「採用しない」を明文化しないと AI / 人間が迷う
tsup.config.ts は 3 つの独立ビルドを定義:
-
variants/index(React 抜き — CSS クラス生成のみ) -
components/{index,lv1,lv2}(React 必須) -
themes/seasonal/index+ CSS 複製
確定方針(オーナー確認済): Schatten は 「React コンポーネント DS」であると同時に「framework-agnostic な CSS / クラス DS」 という二層構造で位置づける。素の HTML / Astro / Vue / Svelte でも <button class="btn btn-primary"> の形で組み込み可能であることを一級市民として扱う。
この方針が DS 全体に与える含意:
| 領域 | 含意 |
|---|---|
| ポジショニング | shadcn 系 React DS とは差別化される独自軸。「React lock-in しない DS」というメッセージが立つ |
| API 安定性 | CSS クラス名 (.btn, .btn-primary, .input-wrapper 等) は公開 API として 1.0 で固定する必要がある |
| variants の型 |
buttonVariants({ variant: 'primary' }) で得られる class string も公開 API。CVA の出力形式に依存しない契約が必要 |
| トークン |
--color-primary-600 のような変数名も公開 API。現状の primitive 名 (vermillion, sumi, alabaster) も同様に固定対象
|
| ドキュメント | 「React 利用例」と「素の HTML 利用例」を並列で示すストーリーが必須 |
| バージョニング | クラス名のリネームは破壊的変更扱い |
Phase 1 で必要な対応:
- README の冒頭に「Schatten works without React. Use plain HTML + CSS or import the React components.」と明示
- Storybook に
Getting Started / Vanilla HTMLストーリー追加 -
Getting Started / For AstroGetting Started / For Vueも並列で(Vue/Svelte は class 利用例のみ) - CSS クラス名・variant 出力・CSS 変数名を**「公開 API」として CHANGELOG / migration guide の対象に含める**ことを
.claude/rules/api-stability.mdに明文化 - クラス名公開 API の棚卸し(下記チェックリスト)を実施
オーナー判断(確定): 1.0 リリースの DoD として棚卸しを完遂する (Option A)。1.0 を「真の framework-agnostic を保証する」リリースにする。Phase 1 で横断ルール決定 → Phase 3 で 18 コンポ実装 → Phase 4 で出口条件確認、という流れ。
前提となる現状認識:
.btnクラスは variants/button.ts:5 で参照されているが、CSS 定義が存在しない (phantom class)- src/schatten.css は CDN 利用を想定 (コメントあり) だが、現状 import しているのは token 4種 + デフォルトテーマ + Spinner.css + Toast.css のみ
- 現状の「素の HTML で使える」は token / 色レベルでのみ成立。コンポーネント styling は React + Tailwind パイプラインに依存している
- 1.0 で framework-agnostic を保証するには CVA 出力相当を
@layer componentsで CSS rule 化する必要がある
- 命名規約を 1 つに統一:
- 案 A: BEM スタイル (
.btn,.btn--primary,.btn--sm) - 案 B: チェーン (
.btn.btn-primary.btn-sm) - 案 C: data 属性 (
<button class="btn" data-variant="primary" data-size="sm">) - → 推奨は A (BEM) または C (data 属性)。Tailwind utility chain (
btn btn-primary btn-sm) は内部実装を漏らすので避ける
- 案 A: BEM スタイル (
- CSS layer 戦略: Tailwind v4
@layer componentsを採用するか、独自レイヤーか - Single source of truth: CVA 文字列か
@layer componentsか。両方持つなら生成側(コードジェネ)を決める - State 表現:
:disabled,[aria-invalid],[data-state="open"],.darkのうちどれを公式に採用するか - internal-only utility の扱い: animation / focus-visible / transition 等を public class に含めるか分離するか
- ダーク対応:
.dark .btn { ... }か[data-mode="dark"] .btnか:where(.dark) .btn
各コンポーネントについて以下を埋める:
- ルート semantic class 名を確定 (例:
.btn,.input,.input-wrapper,.toast,.toast-item) - variant modifier 一覧を列挙 (例: Button →
.btn--primary,.btn--secondary,.btn--tertiary,.btn--inverted,.btn--destructive,.btn--link) - size modifier 一覧を列挙
- state 表現を確定 (例:
.btn:disabled,.btn[data-loading="true"]) - CVA 出力 →
@layer componentsへの書き起こし (Tailwind utility chain を CSS property に展開) - vanilla HTML 利用サンプルを Storybook に追加
- public class 一覧を
.d.tsまたは manifest に export (AI / IDE 補完用)
| # | コンポーネント | 担当 | 状態 |
|---|---|---|---|
| 1 | Badge | – | ⬜ |
| 2 | Button | – | ⬜ |
| 3 | Callout | – | ⬜ |
| 4 | Checkbox | – | ⬜ |
| 5 | Dialog | – | ⬜ |
| 6 | Field | – | ⬜ |
| 7 | FieldSet | – | ⬜ |
| 8 | Input | – | ⬜ |
| 9 | Radio (+ RadioGroup) | – | ⬜ |
| 10 | Select | – | ⬜ |
| 11 | Separator | – | ⬜ |
| 12 | Spinner | – | ⬜ |
| 13 | Switch | – | ⬜ |
| 14 | Text | – | ⬜ |
| 15 | Textarea | – | ⬜ |
| 16 | Toast (+ Toaster) | – | ⬜ |
| 17 | Tooltip | – | ⬜ |
- 全 18 コンポーネントで
@layer componentsへの書き起こしが完了 -
dist/schatten.cssに全コンポーネントの CSS ルールが含まれている - Storybook で 「同じ見た目を React と vanilla HTML の両方で再現できる」検証ストーリーが動く
- public class 一覧が
dist/schatten.manifest.jsonに export されている -
.claude/rules/api-stability.mdでクラス命名規約と破壊的変更ポリシーが定義済み
DS 評価でもアーキテクチャ評価でも完全には括れない、**「利用者が実プロダクトに組み込んだ瞬間に痛みが出る」**4つの観点。
| 観点 | 現状 | 評価 |
|---|---|---|
| Multi-entry exports | 9 entries で部分採用可能 | ◎ |
| Tree-shaking external 設定 |
external: ['react', 'react-dom'] のみ (tsup.config.ts:25) |
|
package.json の sideEffects
|
未指定 | |
| lucide-react の取り扱い | external 未指定 + import { icons } from 'lucide-react' で 全 icon map を読み込み (Button.tsx:2) |
❌ 最重要懸念 |
| Radix の取り扱い | external 未指定 — dependencies 経由で重複インストールはないが、dist に同梱される |
|
| CSS バンドル |
lightningcss --minify --bundle で dist/schatten.css 1 ファイルに統合 |
◎ サイズ自体は小、ただし利用者が「Button だけ使う」時も全 CSS が読まれる |
| CI でのサイズ計測 | なし | ❌ |
最重要懸念の検証手順:
pnpm build && du -sh dist/components/lv1/index.js
# 想定外に大きければ (>200KB)、lucide が丸ごとバンドルされている対策:
-
lucide-reactをpeerDependenciesに移す + README で install を促す -
Iconラッパー化 (5.3 と統合) +import { ArrowRight, Trash2 } from 'lucide-react'の named import に統一して動的アクセスを排除 -
package.jsonに"sideEffects": ["*.css"]を追加 — CSS 以外は副作用なしを宣言 - CI に
size-limitかbundlewatch導入
| 観点 | 現状 | 評価 |
|---|---|---|
useMemo で context value 安定化 |
Field, FieldSet が実施 | ◎ |
| 外部 store によるレンダー分離 | Toast は use-toast.ts で分離 |
◎ |
| CSS-in-JS なし (CVA + Tailwind) | ランタイムコスト最小 | ◎ |
| Portal の遅延マウント | 制御なし |
| 観点 | 現状 | 評価 |
|---|---|---|
'use client' ディレクティブ |
ゼロ件 (src/ 全体) |
❌ |
| useId / useRef 使用 | 8 コンポーネント (Input, Radio, Checkbox, Switch, Field, FieldSet, Textarea, Select) | クライアント側で必要 |
| Radix Portal 使用 | Tooltip / Dialog / Select | クライアント側で必要 |
document / window 直接参照 |
なし (Radix が抽象化) | ◎ |
| README に SSR 注意書き | なし | ❌ |
結論: 現状 Next.js App Router の Server Components から直接 import するとビルド失敗する。Radix 自体は 'use client' 付きで配布されているため、Schatten もそれに揃えるのが業界標準。
対策(一つを選ぶ):
| 案 | 推奨度 | 説明 |
|---|---|---|
A. 全 lv1 ファイル先頭に 'use client' を追加 |
★★★ | tsup の banner で一括注入可能。利用者は何も考えず使える |
B. ./components/lv1/client 別エントリ |
★★ | server-safe な部分 (型 export 等) と分離できるが複雑 |
| C. README に「Client Components 内のみ」と明示 | ★ | 最低限の対応、DX は悪い |
推奨は A。lv1 全てが client 前提なので、tsup の banner: { js: "'use client';" } を該当ビルドに追加するだけ。VRT も Storybook も影響なし。
現状:
- 各コンポーネントは
...propsを forward しており、data-testidは通る (Separator.test.tsx:7, Callout.test.tsx:132) -
testIdのような専用 prop は採用していない (DOM 属性そのままパス) - 規約として明文化されていない
- Portal 配下の要素 (Dialog Content, Select Content, Tooltip Content) への testid 経路が未定義
問題: 利用プロダクトの E2E (Playwright / Cypress) で testid 命名が散らかる。特に Dialog 内部の要素を捕まえるとき:
// 利用者が書きうる N パターン
<Dialog>
<DialogContent data-testid="confirm-dialog-content"> ... </DialogContent>
</Dialog>
// vs
<Dialog data-testid="confirm-dialog"> ... </Dialog>
// vs (Schatten が内部で testid を生成する)
<Dialog testId="confirm-dialog"> // → root, content, overlay 全てに自動 prefix推奨方針: 現状の A 方式 (...props 直 pass) を維持しつつ、.claude/rules/component-testid-guideline.md で明文化:
- ルート要素には
...props経由でdata-testidを通せる - compound component では、各サブ component が独立して
data-testidを受ける - Schatten 内部では
data-testidを自動付与しない (ユーザの testid 戦略を侵食しない) - E2E 推奨パターンを Storybook の
Patterns / Testingストーリーに掲載
現状:
- React 側に
ThemeProvider/useThemeの export なし -
getCurrentSeason()(日付ベースの pure function) だけが src/themes/seasonal/index.ts から export - 実装例は Storybook の
preview.tsxにしかない — そこではdocument.documentElement.classList.toggle('dark')を手動で行う (.storybook/preview.tsx)
問題:
-
利用者は自前で全部書く —
localStorage永続化 / システム設定マッチ / FOUC 回避 / SSR hydration mismatch 回避 -
誤実装の温床 — 特に Next.js App Router で
'use client'を付け忘れる、または hydration 前に DOM を触る問題 -
DS としてのオピニオン不在 —
next-themes推奨か、自前 provider 提供か、未決定
選択肢:
| 案 | メリット | デメリット |
|---|---|---|
A. ThemeProvider を schatten が提供 |
利用者は1行で完結 | SSR / framework 依存性が DS に入り込む |
B. next-themes を peer dep として推奨し README に書く |
既存エコシステムに乗る | Next.js 寄りに見える |
C. 切替パターンを .claude/skills/setup-theme-toggle/ で各 framework 向けレシピ化 |
レシピが並列で出せる | コード自動化されない |
| D. 何もしない (現状) | DS が薄い | 利用者が毎回はまる |
推奨: A を Phase 2 で実装。framework-agnostic な ThemeProvider として作る:
<ThemeProvider
defaultMode="system" // 'light' | 'dark' | 'system'
seasonal={{ auto: true }} // 日付から自動判定
storageKey="schatten-theme"
>
{children}
</ThemeProvider>SSR ガード用の インラインスクリプト snippet を README に掲載し、FOUC を防ぐ。
- TS strict 有効 (tsconfig.json:12) + Biome + lefthook の pre-commit 二重ガード
- VRT の運用が成熟 (.github/workflows/vrt.yml)
-
tsupのマルチエントリ + CSS 複製でcomponents/lv1,variants,themes/seasonalを独立 import 可能 - Field / FieldSet の context 設計が文書化済み
| # | 問題 | 場所 | 影響 |
|---|---|---|---|
| 1 | Stories / Tests が typecheck から除外 |
tsconfig.json:24 "exclude": ["**/*.stories.tsx", "**/*.test.tsx"]
|
pnpm typecheck が story / test の型エラーを検出しない |
| 2 | 8 コンポーネントが unit test なし | Badge / Checkbox / Radio / Select / Spinner / Switch / Text / Textarea | テスト網羅率が読めない |
| 3 | 3 コンポーネントが VRT spec なし | Field / FieldSet / Tooltip | 視覚回帰のブラックホール |
| 4 | changeset 必須が CI で強制されていない |
.github/workflows/ci.yml に changeset status チェックなし |
バージョン bump 漏れ |
| 5 | a11y 自動テストなし | – |
@axe-core/playwright を VRT に同居可能 |
| 6 | Biome が recommended のみ |
biome.json:13 |
noUnusedImports 等で品質強制可能 |
| 7 | lv2 が空 stub のまま | src/components/lv2/index.ts | 判定基準が未定義 |
| 8 | bundle size 監視なし | – |
size-limit 等を CI 化推奨 |
| 9 | Storybook 公開ビルドの a11y check なし | – | storybook-addon-a11y |
| 10 | tsup の CSS 複製が onSuccess 紐付き |
tsup.config.ts:37 | エントリ順を変えるとサイレントに崩れる |
- Compound component(Select, Dialog, Tooltip)と単一 export(Button, Input)の使い分け方針が未文書化
-
lv1 内に
Toaster.tsx、use-toast.ts、Toast.cssのような付随ファイルが共存 — CSS ファイルを許容する基準が暗黙
「AI(Claude / Cursor / Copilot / v0 など)がこの DS をどれだけ正しく使い・拡張できるか」の評価。
-
Props 型を
index.tsから全部 export している — AI が.d.tsを読めば正しい prop が分かる - CVA で variant が型として現れる — 補完に出る
-
.claude/rules/が CLAUDE.md から参照済み — Claude Code はワークツリー起動時に CLAUDE.md を読む -
variants ディレクトリの集中 —
src/variants/を1パスで列挙できる - TS strict + Biome + CI — AI 出力への即フィードバックループ
- CSS 変数 + Tailwind ユーティリティのみ
- AI には「使える Tailwind クラスの全集合」が見えない →
bg-red-500,text-gray-700を書きがち - セマンティック名(
bg-errorvsbg-error-subtle)の使い分けがコード上で区別不能
-
IconName = keyof typeof icons— lucide のバージョン依存。AI は学習時点の名前を混在させる
- Button:
primary | secondary | tertiary | inverted | destructive | link - Badge/Callout/Toast は独自語彙
- AI が
<Badge variant="primary">と書く → CVA は未定義 variant でデフォルト fallback、silent fail
- Select は 11 個の sub-component を export
- 「Dialog は compound、Button は単一」というメンタルモデルがコード上に明示されていない
- Button にしか
asChildがない(Input, Select には?) - AI は全コンポにあると仮定して書きがち
- 「Field + Input + label + error 表示」を書かせると毎回少しずつ違う JSX
- 正解の参照点が存在しないため、AI 同士の出力が DS 採用前より散らかるという皮肉
-
render: () => ...形式で variant 列挙が宣言的でない - AI は「コードを読んで抽出」する必要があり、宣言的でない
- 直接
<input className="border rounded p-2">を書く誘惑 - CLAUDE.md でカバーされているが、Claude Code 外の AI(Copilot 等)には届かない
1. Props 型に JSDoc を全面付与
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, ButtonVariants {
/**
* Visual style of the button.
* - `primary`: solid fill, main CTA
* - `secondary`: outlined, secondary actions
* - `destructive`: for delete/remove actions (red)
* @default 'primary'
*/
variant?: ButtonVariants['variant']
}- 効果: hover tooltip / autocomplete に description が出る → Copilot 含む全 AI が文脈を獲得
- 現状の
argTypes.descriptionは Storybook 内専用で再利用されていない
2. ルートに AGENTS.md を新設
- 業界で
AGENTS.mdが AI 向けの README 標準として浮上中 - 内容: CLAUDE.md と重複してよい
3. src/tokens.ts で CSS 変数を TS の constant として再 export
export const tokens = {
color: {
error: 'var(--color-error)',
errorSubtle: 'var(--color-error-subtle)',
foreground: 'var(--color-foreground)',
},
spacing: { /* ... */ },
} as const
export type ColorToken = keyof typeof tokens.color4. Icon ラッパーコンポーネント + 許可リスト型
const ALLOWED_ICONS = ['ArrowRight', 'Trash2', 'Check'] as const
export type IconName = (typeof ALLOWED_ICONS)[number]5. variant 語彙の意図的な統一 ✅ Phase 1 に確定済み (オーナー判断)
- 提案: 全コンポで
variantの意味を「色トーン」に統一、形は別 prop (appearance) で分離 - Pre-1.0 のため破壊的変更を恐れず実施。以降の Phase が最終 API を前提に進められる
6. lv2 を最低3個作る
-
FormField/ConfirmDialog/EmptyState
7. .claude/skills/ で能動ワークフロー
-
add-lv1-component/audit-componentSkill
8. コンポーネントマニフェスト JSON を生成
-
dist/schatten.manifest.jsonを CVA + 型 + argTypes から自動生成 - v0, Cursor, MCP server からプログラマブルに DS を検査可能に
9. Storybook MCP サーバ化
- Storybook 9 系のロードマップ次第
10. ESLint / Biome custom rule
-
bg-red-*等の primitive 色を JSX で禁止 → state token 強制
現状: .claude/ には rules/ 4本と commands/review-pr.md のみ。Rules は読まれる前提なので網羅的だが、能動的なトリガーがない点が課題。
.claude/
├── rules/ # 既存: 受動的な参照ドキュメント
│ ├── component-architecture.md ★新設: lv1/lv2 判定基準・compound vs単一の方針
│ ├── component-api-conventions.md ★新設: isError/disabled/size の命名・型統一
│ ├── field-context-guideline.md (既存)
│ ├── state-token-guideline.md (既存)
│ ├── storybook-guideline.md (既存)
│ ├── vrt-spec-guideline.md (既存)
│ └── testing-guideline.md ★新設: 必須テストケース表 + 未実装の補完方針
│
├── skills/ # ★新設: 能動的に呼ばれるワークフロー
│ ├── add-lv1-component/SKILL.md "新しい lv1 コンポーネントを追加する" 起動
│ ├── add-vrt-spec/SKILL.md "VRT spec を追加" 起動
│ ├── audit-component/SKILL.md 1コンポーネントを全 rule で監査
│ └── prepare-release/SKILL.md changeset → version → publish 手順
│
├── commands/ # ユーザーが叩く slash 専用
│ ├── review-pr.md (既存)
│ └── audit-coverage.md ★新設: テスト/VRT 抜けスキャン
│
└── settings.json # ★新設: hook で機械的強制
├── PostToolUse(Edit) → lv1 配下なら test/vrt 存在チェック
└── Stop → CLAUDE.md がリストする lv1 件数 と index.ts の export 数を整合チェック
- Rule = 「そのトピックに触れる時に読むべきこと」(受動). 設計原則・命名規約・トークン解説など
- Skill = 「ユーザーがこういう依頼をした時に走るべき手順」(能動). 新規追加・監査・リリース等
-
Command = ユーザーが明示的に起動するもの(
/review-pr,/audit-coverage) - Hook = 機械的に強制すべきもの(コミット時のチェック等)
-
.github/workflows/ci.yml に
pnpm exec changeset status --since=origin/mainを追加 → changeset 強制 -
tsconfig.json の exclude から stories/test を外す(または
tsconfig.test.jsonを別建て) -
lefthook.yml の pre-push に
pnpm test:vrt --grep <changed-component>を追加(任意) -
@axe-core/playwrightを VRT spec に同居 →add-vrt-specSkill のテンプレに axe assertion を含める
Pre-1.0 の破壊的変更ポリシー (オーナー確認済): 現時点で実利用者がいないため、1.0 リリースまでは破壊的変更を恐れずに API 整理を進める。
api-stability.mdで定義する公開 API 安定化契約は 1.0 以降にのみ適用する。 結果として、各 Phase で「無破壊で漸進」を優先する必要はなく、最適な形に直接書き換える。例: typography の 3 変数 API は shorthand に置換、variant 語彙統一も同様。
バージョニング方針 (オーナー確認済):
0.x.0のxを1つずつ進める。Phase はコンセプト粒度として残し、各 Phase 内を 2-3 個の minor バージョン (1-2 週間スプリント想定) に分割する。x ≥ 10も許容 (v0.10, v0.11, ...)。各 minor は GitHub milestone と 1:1 で対応させる。
Versions: v0.5.0 → v0.6.0 → v0.7.0 → v0.8.0 目的: AI 親和性を ⭐⭐⭐ → ⭐⭐⭐⭐⭐ に底上げし、規範を明文化し、API 整理を実装し、production 採用の壁を除去する
テーマ: 「Schatten が IDE / AI で使いやすくなった」
- Props 型に JSDoc 全面付与(全 lv1 コンポーネント)
-
src/tokens.tsで TS 型付き token export を追加 - ルートに
AGENTS.md新設(Claude 以外の AI 向け) -
tsconfig.jsonの exclude から stories/test を外す - CI に
changeset statusチェックを追加 - Biome ルール強化(
noUnusedImports,useExhaustiveDependencies等) - 8 コンポーネントの unit test を補完
- 3 コンポーネントの VRT spec を補完
テーマ: 「設計原則と命名規約が文書化された」(以降のすべての実装の前提)
-
.claude/rules/component-api-conventions.md新設 — variant 語彙統一の方針 +isError/disabled/sizeの型・命名 (v0.7 での実装の前提) -
.claude/rules/component-architecture.md新設 — 以下を含む:- lv1/lv2 判定基準
- compound vs flat の選択基準
- asChild 採用基準
-
polymorphic component pattern (
asprop) を採用しないことを明文化 - contexts の消費規範 (どの lv1 が Field/FieldSet を消費して OK か、lv2 が context を消費する時の順序)
-
lib/utils.tsの import 規範 (lv2 は lib/ から直接 import、lv1 経由しない 等) - lv1 内に component CSS ファイル (Toast.css, Spinner.css) を置く許容基準 (アニメーション / Tailwind で表現困難なものに限定)
-
.claude/rules/spacing-conventions.md新設 — AI 向けに「gap / padding は DS スケールのみ使用」(Layout primitives 不採用判断の補完) -
.claude/rules/testing-guideline.md新設 — 必須テストケース表 -
.claude/rules/theme-architecture.md新設 — Mode × Special の二軸モデルと cascade ルール (Special > Mode > base semantic) を文書化 -
.claude/rules/api-stability.md新設 — CSS クラス名・variant 出力・CSS 変数名を「公開 API」として CHANGELOG 対象にする規約 (1.0 以降に発効) -
.claude/rules/component-testid-guideline.md新設 —...props直 pass 方式の明文化 + Portal 経路ルール -
.claude/settings.jsonの hook 整備 —PostToolUse(Edit): lv1 編集で test/vrt 存在チェック /Stop: lv1 件数と export 数の整合
テーマ: 「最終 API がほぼ確定。素の HTML で動く道筋が見えた」
- variant 語彙の意図的な統一の実装 — v0.6 の
component-api-conventions.mdに従って 18 コンポを refactor (「色トーン」と「形」を別 prop に分離) -
package.jsonの.exports に JS/types エントリを追加 —import { Button } from '@yasmro/schatten'を動かす -
./components/lv2の空 export を一旦削除 (lv2 実装後に復活) - README 冒頭に「Schatten works without React」を明示
- Storybook
Getting Started / Vanilla HTMLストーリー追加 - Storybook
Getting Started / For Astro/For Vueストーリー追加 (class 利用例のみ) - 各特別テーマの token allowlist を CSS コメント or TS const で実装
-
Foundations / Theme Compositionストーリーで 16 パターン視覚検証 (8 seasonal × 2 mode)
テーマ: 「Next.js App Router で動く。サイズ計測も入る」
-
'use client'を tsupbannerで全 lv1 ビルドに自動注入 — Next.js App Router 対応 (最優先) -
lucide-reactをpeerDependencies移動 +import { icons }を named import に置換 -
package.jsonに"sideEffects": ["*.css"]追加 - CI に
size-limit導入 (lv1 全体で 100KB を初期閾値) - README に SSR 利用ガイド追加 (Client Components 内のみで動作する旨)
-
tsupの CSS 複製をonSuccessから独立スクリプト化 (scripts/copy-css-assets.ts) - Biome custom rule で primitive 色クラス (
bg-red-*等) の JSX 内使用を禁止 — state token 強制
Versions: v0.9.0 → v0.10.0 → v0.11.0 目的: lv2 で「合成の正解」を示し、ドキュメント完備、トークン拡充で DS の価値を証明
テーマ: 「合成パターンの正解が見える」
-
lv2/FormField実装 -
lv2/ConfirmDialog実装 -
lv2/EmptyState実装 -
lv2/PageHeader実装 -
Icon共通コンポーネント + 許可リスト型 -
ThemeProvider/useTheme提供 (light/dark + seasonal を統括する framework-agnostic provider) - README に FOUC 回避用 SSR snippet 掲載
-
.claude/skills/add-lv1-component/新設(テスト・VRT・story テンプレを必ず展開) -
.claude/skills/add-vrt-spec/新設 (VRT spec 追加専用、axe assertion 込み) -
.claude/skills/audit-component/新設 (1 コンポを全 rule で監査) -
.claude/skills/prepare-release/新設 (changeset → version → publish 手順自動化) -
.claude/commands/audit-coverage.md新設 (テスト/VRT 抜けスキャンを slash command 化)
テーマ: 「学習リソースが揃った」
- Foundations ページ 5枚追加(Spacing / Elevation / Motion / Iconography / Accessibility)
-
Patterns / Form Compositionストーリー (Field/FieldSet レシピ集) -
Patterns / Composition with asChildストーリー (Radix Slot パターン方針) -
Patterns / Layoutストーリー (Tailwind 直書きで DS スケールを守るレシピ) -
Patterns / Testingストーリー (testid 規約と E2E 推奨パターン)
テーマ: 「微細な仕上げ」
- Typography を shorthand 一本化 —
@utility text-body-md等を 1 クラスで完結。既存の 3 変数 API は撤去 (破壊的変更可) - Shadow / radius / motion の semantic token 追加 —
--shadow-card,--radius-control,--motion-instant等 - Z-index 用途別 semantic token —
--z-tooltip,--z-modal,--z-toast等 -
@axe-core/playwrightを VRT に組み込み - Storybook
addon-a11y導入 (開発時の即時 a11y フィードバック) - 季節テーマの影響面を
--color-accent-*,--shadow-cardまで拡張 -
Foundations / Seasonal Showcaseストーリー追加 (8季節 × Button / Badge / Callout)
Versions: v0.12.0 → v0.13.0 → v0.14.0 目的: 「実プロダクトで詰まらない DS」へ + クラス名棚卸し実装
テーマ: 「ナビ系がない」を解消
- Menu / DropdownMenu
- Popover
- Tabs
テーマ: 「データを並べる」基盤
- Card
- Avatar
- Skeleton
- Table(最小機能)
テーマ: 「framework-agnostic を実体化、API を最終形へ」
- Alert vs Callout の使い分け定義 — Phase 1 で確定した variant 語彙のもとで、Alert を新規追加するか Callout に変種を追加するか決定
- クラス名公開 API 棚卸しの実装 (§2.8.1) — 18 コンポーネント × CVA →
@layer componentsへの書き起こし -
lv1を category-based subdirectory に再編 (form/,disclosure/,navigation/,data/,feedback/) - Radix Props の re-export 方針を明文化 (公開する/型ガードを挟む)
Versions: v0.15.0 → v1.0.0
テーマ: 「1.0 で固定する全ドキュメントを揃える」
- Migration guide 整備
- ADR ディレクトリ整備 (なぜ shadcn ベース / なぜ npm 配布 / なぜ framework-agnostic 等)
- AGENTS.md / CLAUDE.md / Contributing.md の三点セット完備
- Storybook Foundations / Patterns ページの最終チェック
- Framework-agnostic 動作確認 — Storybook で Vanilla HTML / Astro / Vue 利用例が動く
-
dist/schatten.manifest.jsonexport 動作確認 (public class 一覧)
1.0 の DoD (チェックのみ、新規実装ゼロ):
- a11y 自動テスト合格(全 lv1)
- lv1 主要 8 種に lv2 ラッパー存在
- Tabs / Menu / Popover / Card 追加完了
- Migration guide / ADR / 三点セット完備
- クラス名公開 API 棚卸し完了 (§2.8.1 出口条件をすべて満たす)
- Framework-agnostic 利用例が Storybook で動作確認済み (Vanilla HTML / Astro / Vue)
- CHANGELOG / アナウンス公開
- API stability commitment 開始 —
api-stability.mdに従い、CSS クラス名 / variant 出力 / CSS 変数名 を公開 API 扱いに切り替え
1.0 リリース後に取り組む方が良い未解決テーマ:
テーマ・a11y:
- 特別テーマの token allowlist 強制機構 (§2.4 関連)
-
lightningcssplugin で違反検出する方式の検証 - 利用者定義特別テーマを受け入れる API 設計
- 「event テーマ」「コラボテーマ」等の追加方針
-
- 高コントラストモードの位置づけ (§2.4 関連)
- Mode 軸 (light/dark/high-contrast) として追加するか、Special 軸として追加するか
-
prefers-contrast: moreメディアクエリ対応 - WCAG AAA レベルとの整合
- 色弱モード対応 (deuteranopia / protanopia / tritanopia)
追加コンポーネント (高機能 / 専門系):
- Form 系: Combobox / Autocomplete, DatePicker, Slider, FileUpload, Form 送信統括
- Disclosure 系: Drawer, Accordion / Collapsible, HoverCard
- Navigation 系: Breadcrumb, NavigationMenu, Pagination
- Data display 系: List, ProgressBar
- Layout primitives (Stack / HStack / VStack / Cluster) — 採用判断含む (現状は Tailwind 直書きで運用 +
spacing-conventions.mdで AI 制約)
ツール / エコシステム:
- Portal 遅延マウント検討 — 大規模ページでの Tooltip 早期マウント計測 → 必要なら lazy mount API
- Storybook MCP サーバ化 (§5.3 #9)
- Vue / Svelte 公式ラッパーコンポーネント (現状はクラス利用のみ提供、コンポーネントとしての提供は将来検討)
プロジェクト設定:
- CLAUDE.md
- README.md
- package.json
- tsconfig.json
- biome.json
- tsup.config.ts
- lefthook.yml
- .github/workflows/ci.yml
- .github/workflows/vrt.yml
- .storybook/preview.tsx
トークン / テーマ:
- src/core/tokens/primitives.css
- src/core/tokens/semantic.css
- src/core/tokens/typography.css
- src/themes/seasonal/index.ts
- src/themes/seasonal/themes.css
- src/schatten.css
- src/styles/globals.css
コンポーネント / バリアント:
- src/variants/button.ts
- src/components/lv1/Button/Button.tsx
- src/components/lv1/index.ts
- src/components/lv2/index.ts
- src/components/lv1/Toast/Toaster.tsx
- src/components/lv1/Toast/Toast.css
Claude rules / commands:
Foundation
Components
Development
Notes