data-to-ui について
data-to-uiは、静的データから型安全なReact UIを作る開発者向けスキルです。JSONからTypeScript、Reactへ変換するパターンに加え、意味に基づくマッピング、書式設定、集計を扱います。
主な機能
- 01JSON構造からTypeScript型を作成
- 02UIコンポーネント向け派生型
- 03意味に基づく色とアイコンの対応付け
- 04数値と日付の書式設定ユーティリティ
- 05型安全なグループ化と集計
活用シーン
- 01JSONデータからReactコンポーネントを作成
- 02重要度に応じた色とアイコンの統一
- 03表示用の数値と日付の書式設定
- 04レコードの分類と集計値の計算
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dralgorhythm/ claude-agentic-framework --skill data-to-uiインストール後、Claude Code で/data-to-uiを入力して読み込まれたか確認します。
詳細
データから UI へ
概要
静的データを型安全な React コンポーネントに変換するためのパターンです。このスキルでは、意味に基づくカラーシステム、派生型、フォーマットユーティリティに重点を置いた JSON → TypeScript → React のパイプラインを扱います。
ワークフロー
1. JSON スキーマ → TypeScript 型
- JSON スキーマ/データ構造を読み取る
- JSON の形状に一致する基本的な TypeScript インターフェースを作成する
- enum 用のユニオン型をエクスポートする(例:
type Severity = 'safety_hazard' | 'repair_needed') - null または欠損するフィールドにはオプショナルプロパティ(
?)を使用する - 複雑な型には JSDoc コメントを追加する
2. UI 用の派生型
-
extendsを使って基本型を拡張した合成型を作成する - コンポーネントの props には
Pick<T, K>とOmit<T, K>を使用する -
&を使って、結合したデータの交差型を構築する(例:FindingWithAsset) - 統計や要約用の集約インターフェースを作成する
3. カラーマッピングシステム
- 意味に基づく色には
Record<EnumType, ColorValue>を定義する - バッジ、背景、テキスト、枠線、ドットなど、複数のカラー形式を用意する
- Tailwind のユーティリティクラスを使用する(例:
'bg-red-500 text-red-600') - アクセサー関数をエクスポートする(例:
getSeverityColors()) - コメントで色の選択理由を説明する
4. アイコンマッピング
- lucide-react のアイコン名に対応する
Record<EnumType, string>を作成する - PascalCase のアイコン名を使用する(例:
'AlertTriangle'、'Thermometer') - アクセサー関数をエクスポートする(例:
getSeverityIcon())
5. フォーマットユーティリティ
- 通貨:小数点以下の桁数を指定せず、USD を使って
Intl.NumberFormatを使用する - 日付:月を短縮形式にして
toLocaleDateStringを使用する - 計算:年数、割合、耐用年数のヘルパーを作成する
- ラベル:人が読みやすいラベルのマップを作成する
6. 集約とグループ化
- reduce または forEach を使用した
groupByパターンを実装する - 重要度/優先度の順序を使ったカスタム比較関数でソートする
- 要約統計量(最小値、最大値、平均値、件数)を計算する
- 型を厳密に指定した集約結果を返す
リファレンス実装
カラーマッピングシステム
// Single source of truth for semantic colors
export interface SeverityColors {
badge: string; // 'text-red-600 bg-red-100'
bg: string; // 'bg-red-500'
text: string; // 'text-red-600'
border: string; // 'border-red-500'
dot: string; // 'bg-red-500'
}
const SEVERITY_COLOR_MAP: Record<Severity, SeverityColors> = {
safety_hazard: {
badge: 'text-red-600 bg-red-100',
bg: 'bg-red-500',
text: 'text-red-600',
dot: 'bg-red-500',
border: 'border-red-500',
},
// ... other severities
};
export function getSeverityColors(severity: Severity): SeverityColors {
return SEVERITY_COLOR_MAP[severity];
}
アイコンマッピング
export function getSeverityIcon(severity: Severity): string {
const icons: Record<Severity, string> = {
safety_hazard: 'AlertTriangle',
repair_needed: 'Wrench',
maintenance_item: 'Settings',
monitor: 'Eye',
informational: 'Info'
};
return icons[severity];
}
派生型
// Base type
export interface Finding {
id: string;
assetId?: string | null;
severity: Severity;
title: string;
}
// Derived type with relationship
export interface FindingWithAsset extends Finding {
asset?: Asset;
}
// Aggregate type
export interface PropertyWithDetails {
property: Property;
inspectionReport: InspectionReport;
findings: Finding[];
assets: Asset[];
}
フォーマットユーティリティ
export function formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value);
}
export function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
export function yearsSince(dateString: string): number {
const date = new Date(dateString);
const now = new Date();
return Math.floor((now.getTime() - date.getTime()) / (365.25 * 24 * 60 * 60 * 1000));
}
集約パターン
// Group by enum value
export function groupFindingsBySeverity(findings: Finding[]): Record<Severity, Finding[]> {
const grouped: Record<Severity, Finding[]> = {
safety_hazard: [],
repair_needed: [],
maintenance_item: [],
monitor: [],
informational: []
};
findings.forEach(f => grouped[f.severity].push(f));
return grouped;
}
// Sort by priority
export function sortFindingsBySeverity(findings: Finding[]): Finding[] {
const severityOrder: Record<Severity, number> = {
safety_hazard: 0,
repair_needed: 1,
maintenance_item: 2,
monitor: 3,
informational: 4
};
return [...findings].sort((a, b) =>
severityOrder[a.severity] - severityOrder[b.severity]
);
}
ベストプラクティス
- 信頼できる唯一の情報源:すべての色/アイコンのマッピングを一か所にまとめ、アクセサー関数を用意する
- 複数形式の色:柔軟に使えるよう、バッジ、背景、テキスト、枠線、ドットのバリエーションを用意する
- 型安全性:プレーンオブジェクトではなく
Record<EnumType, Value>を使用する - Intl API:ローカライズには
Intl.NumberFormatとIntl.DateTimeFormatを使用する - 不変性:配列のソート/フィルタリングにはスプレッド演算子を使用する
- ドキュメント:色の選択理由とデータ構造を説明する JSDoc コメントを追加する
- ユーティリティの同じ場所への配置:インポートしやすいよう、型とユーティリティを同じファイルにまとめる
アンチパターン
- 使用しないでください:意味を持たない汎用的な色名を使う(例:
'safety_hazard'ではなく'red') - 使用しないでください:コンポーネント内に色クラスを直接記述する。必ずマッピング関数を使用する
- 使用しないでください:
any型を使う。unknownと型ガードを優先する - 使用しないでください:ソート/フィルター関数で入力配列を変更する。必ずコピーを作成する
- 使用しないでください:日付形式をハードコードする。一貫性を保つため
Intl.DateTimeFormatを使用する - 使用しないでください:単純なユーティリティを別ファイルに分ける。型と同じ場所にまとめる
- 忘れないでください:オプショナルフィールドの null/undefined を処理する
- 使用しないでください:TypeScript ファイル名に snake_case や kebab-case を使う。camelCase を使用する
フィードバックループ
- 型チェック:
tsc --noEmitを実行して型を検証する - ランタイム検証:実行時の JSON スキーマ検証には Zod の使用を検討する
- ビジュアルテスト:カラーシステムを検証する Storybook ストーリーを作成する
- データの整合性:集約した統計値をソースデータの件数と比較する
- インポートの確認:すべてのユーティリティがエクスポートされ、インポート可能であることを確認する
関連スキル
interface-design- React コンポーネントでカラーシステムを使用するrefactoring-code- 重複する色/フォーマットのロジックを統合する
コメント
Markdown 対応