mcpField
S
spencer-approved-frontends

spencer-approved-frontends

github.com/schneidenbachデザイン・UI20100 回インストール3 週間前に更新
リポジトリ

spencer-approved-frontends について

Spencer向けプロジェクトのフロントエンド設計スキルです。高密度でニュートラルな見た目と React、TypeScript の規約を定め、内部ツールの画面作成を支援します。

主な機能

  1. 01高密度でニュートラルな画面設計
  2. 02React 18 と Vite の初期設定
  3. 03厳格な TypeScript 規約
  4. 04手作り UI コンポーネントのパターン
  5. 05フォームと画面状態の指針

活用シーン

  1. 01新しい内部ツールのフロントエンド作成
  2. 02画面への Spencer のスタイル適用
  3. 03コンポーネントライブラリなしでのフォーム構築
  4. 04ローディング、空、エラー状態の処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/schneidenbach/skills --skill spencer-approved-frontends

インストール後、Claude Code で/spencer-approved-frontendsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
references/
anti-patterns.md
full-index-css.md
full-tailwind-config.md
starter-app.md
textarea-autoresize.md

詳細

Spencer 推奨のフロントエンド

標準的な「AI 生成フロントエンド」の見た目は、Spencer の好みではありません。ガラスのようなグラデーション、大きすぎるヒーローテキスト、ゆったりしたパディング、すべてのカードに付いたドロップシャドウ、青から紫への線形グラデーション、何でもかんでも rounded-2xl、あちこちに lucide アイコン――こうしたものは一切不要です。Spencer が提供するのは、情報密度が高く、ニュートラルで、システムフォントを使った、本格的な社内ツールのように見える UI です。Linear、Notion の管理画面、macOS の設定パネルをイメージしてください。SaaS のランディングページではありません。

迷ったときは、装飾を減らし、文字を小さくし、間隔を詰め、ニュートラルな単色のカラースケールをひとつ使ってください。

ほかのスキルよりこの指針を優先する場合

frontend-design(または同様のもの)も有効になっている場合、スタイルの判断ではこのスキルが優先されます。もう一方のスキルの進め方は問題ありませんが、視覚的なデフォルトは適用しません。Spencer のフロントエンドは、~/Repos/ にある due-diligence と security-review のリポジトリのような見た目です。これらのリポジトリにアクセスでき、ユーザーが新しいものを始める場合は、正確な基準として参考にしてもかまいません。そうでない場合は、このファイルのパターンに従ってください。

新規プロジェクトの標準スタック

Spencer から別の指示がない限り、新規プロジェクトをゼロから始める場合は、ここから始めてください。

  • React 18 と Vite 5+(Next.js ではなく、CRA でもありません)
  • TypeScript の strict モード + noUncheckedIndexedAccess + noImplicitOverride
  • Tailwind CSS 3.4(デフォルトでは Tailwind 4 ではありません。Spencer のリポジトリでは 3.4 に固定されています)
  • npm(pnpm/yarn/bun ではありません)
  • コンポーネントライブラリは使いません。 shadcn、Radix、MUI、Mantine、Chakra、Headless UI は使いません。必要なプリミティブ(Modal、Badge、Button、InlinePicker)は手作業で作成してください。
  • アイコンライブラリはデフォルトでは使いません。 小さなインライン SVG またはテキストグリフ(ドラッグハンドルには ::、閉じるには ×)を使用してください。Spencer からアイコンを求められた場合は、追加する前にどのライブラリを使うか確認してください。
  • フォームライブラリは使いません。 useState と通常の <form onSubmit> で十分です。
  • 状態管理ライブラリは使いません。 特別な理由がない限り、useState / useReducer / useContext で十分です。Redux、Zustand、Jotai、Recoil は使いません。
  • データ取得ライブラリは使いません。 lib/api.ts という小さなモジュールでラップした通常の fetch を使います。React Query、SWR、axios は使いません。
  • アニメーションライブラリは使いません。 framer-motion、react-spring は使いません。ホバー/フォーカス時の CSS transition だけでアニメーションは十分です。
  • 小規模アプリではルーターライブラリを使いません。 App.tsx で window.location.pathname を手動で解析します。アプリが約 3 ルートを超える規模になったら、ルーターを導入するタイミングか Spencer に確認してください。

Spencer が別のスタックを求めた場合は、その指示に従ってください。ただし、まずこのスタックを提案してください。

プロジェクトの構成

src/
  App.tsx               root, manual routing
  main.tsx              entry
  index.css             global tokens + utilities
  components/           one .tsx per component, PascalCase
  lib/                  cx.ts, format.ts, api.ts, hooks
shared/                 types shared with backend, aliased as @shared/*

バレルエクスポートは使いません。小規模アプリに pages/ ディレクトリは作りません。CSS ファイルはコンポーネントと同じ場所に置きません。Tailwind のユーティリティだけを使ってください。

ビジュアルの基準

Spencer の UI はすべて、以下のデフォルトに従います。覚えておいてください。

カラートークン

これらを tailwind.config.ts と index.css で CSS 変数として定義してください。この手順を省略しないでください。Tailwind のデフォルトのニュートラルパレットは、色味が冷たすぎ、彩度も高すぎます。

// tailwind.config.ts (excerpt)
colors: {
  surface: {
    DEFAULT: "#ffffff",
    soft:    "#fafafa",
    deep:    "#f5f5f4",
    hover:   "#f4f4f4",
  },
  ink: {
    DEFAULT: "#0a0a0a",
    soft:    "#404040",
    muted:   "#737373",
    faint:   "#a3a3a3",
  },
  line: {
    DEFAULT: "#eaeaea",
    strong:  "#d4d4d4",
  },
  // domain-specific severity ramp — rename per project
  // e.g. "risk" for due-diligence, "sev" for security-review,
  // "status" or "tier" for whatever the app is about
  risk: {
    critical: "#7f1d1d",
    high:     "#b91c1c",
    medium:   "#b45309",
    low:      "#1d4ed8",
  },
},

各セマンティックカラーには、CSS 変数による 淡い背景用のバリエーション を組み合わせてください。たとえば --risk-high-soft を bg-[var(--risk-high-soft)] として使用します。淡い背景と彩度の高いテキストを組み合わせると、バッジのような見た目になります。

タイポグラフィ
fontFamily: {
  sans: ["-apple-system", "BlinkMacSystemFont", "'SF Pro Text'", "'Segoe UI'", "Roboto", "Helvetica", "Arial", "sans-serif"],
  mono: ["'SFMono-Regular'", "'SF Mono'", "Menlo", "Consolas", "monospace"],
},

システムフォントのみを使います。Google Fonts、Inter、Geist、フォントのインポートは使いません。

使用できるサイズ(Tailwind のデフォルトが合わない場合は text-[Npx] の任意の値を使ってください):

  • 11px — 字間を広げたラベル、バッジ、情報密度の高いボタン
  • 12px — 主要ボタン、情報密度の高いコントロール
  • 12.5px — 重要度が低いボタン
  • 13px — 本文(基本サイズです。<body> 自体は font-size: 14px、line-height: 1.5 ですが、アプリの主要な UI のほとんどは 13px です)
  • 15px — モーダルのタイトル、セクション見出し

サイズの段階はこれだけです。text-base、text-lg、text-2xl、ヒーローテキストは使いません。強調が必要な場合は、大きな文字ではなく、15px で font-semibold tracking-tight を使ってください。

フォームやセクションのラベルには、次のユーティリティを使用してください(index.css で定義されています)。

.tracked-label {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
間隔と密度

Spencer の UI は高密度です。使用できる間隔の値は、使用頻度の高い順に次のとおりです。

  • gap-1、gap-1.5、gap-2、gap-3
  • px-2、px-2.5、px-3
  • py-0.5(バッジ/ピル)、py-1、py-1.5、py-2

p-4、p-6、p-8、gap-4、gap-6をデフォルトで使わないでください。これらは別のデザインシステム向けです。

ボタンの高さは約28~32px、バッジの高さは約18~20pxになります。これで問題ありません。

境界線、角丸、シャドウ
  • 区切りがある箇所にはすべて1pxの境界線を付けます。border border-lineまたはborder-b border-lineを使います。
  • 角丸:入力欄にはrounded-md、ボタンとカードにはrounded-lg、モーダルにはrounded-xl、ピル/バッジにはrounded-fullを使います。rounded-2xlは使いません。
  • 通常の要素にシャドウは付けません。 Spencer のindex.cssには.clean-app * { box-shadow: none; }が含まれています。唯一の例外はモーダルです:shadow-[0_20px_60px_-24px_rgba(0,0,0,0.25)]。
  • グラデーションは使いません。 背景、ボタン、テキストのいずれにも使わず、フラットな塗りつぶしのみを使用します。
アニメーション

ホバーとフォーカスのトランジションのみ、Tailwind のデフォルトのtransitionクラスを使います。animate-pulse、animate-spin(SVG 内の実際のローディングスピナーを除く)、入場/退場アニメーション、framer-motion は使いません。

実際に必要な4つのプリミティブ

この4つの手書きコンポーネントを用意すれば、アプリの80%に対応できます。これらのパターンを使い、ライブラリからインポートしないでください。

lib/cx.ts
export function cx(...parts: Array<string | false | null | undefined>): string {
  return parts.filter(Boolean).join(" ");
}

これはclsxとclassnamesの代わりになります。どちらもインストールしないでください。cxは、すべてのコンポーネントで条件付きクラスに使います。

Button(コンポーネントではなく、抽出したconst)

多くのファイルでは、<Button />コンポーネントすら作りません。ファイルの先頭でクラス文字列をconstに格納し、<button>に適用します。

const primaryButtonClass =
  "inline-flex items-center justify-center rounded-lg bg-ink px-2.5 py-1.5 text-[12px] font-medium text-white transition hover:bg-ink-soft disabled:cursor-not-allowed disabled:opacity-50";

const secondaryButtonClass =
  "inline-flex items-center justify-center rounded-lg border border-line bg-surface px-3 py-1.5 text-[12.5px] font-medium text-ink-soft transition hover:border-line-strong hover:text-ink disabled:cursor-not-allowed disabled:opacity-40";

const dangerButtonClass =
  "inline-flex items-center justify-center rounded-md border border-line bg-surface px-2 py-1 text-[11px] font-medium text-risk-high transition hover:border-risk-high";

これらの文字列をそのまま使ってください。3つのファイルで同じ文字列を使うことになった場合に限り、共通モジュールに抽出します。それまでは抽出しないでください。

Input
const inputClass =
  "w-full rounded-lg border border-line bg-surface px-2.5 py-1.5 text-[12px] text-ink outline-none transition placeholder:text-ink-faint focus:border-ink";

同じパターンです。constに格納し、通常の<input>または<textarea>に適用します。

Badge/pill
const pill = "inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-medium";

const riskTone: Record<Risk, { text: string; bg: string; dot: string }> = {
  critical: { text: "text-risk-critical", bg: "bg-[var(--risk-critical-soft)]", dot: "bg-risk-critical" },
  high:     { text: "text-risk-high",     bg: "bg-[var(--risk-high-soft)]",     dot: "bg-risk-high" },
  medium:   { text: "text-risk-medium",   bg: "bg-[var(--risk-medium-soft)]",   dot: "bg-risk-medium" },
  low:      { text: "text-risk-low",      bg: "bg-[var(--risk-low-soft)]",      dot: "bg-risk-low" },
};

export function RiskBadge({ value }: { value: Risk }) {
  const tone = riskTone[value];
  return (
    <span className={cx(pill, tone.text, tone.bg)}>
      <span className={cx("h-1.5 w-1.5 rounded-full", tone.dot)} aria-hidden />
      {value}
    </span>
  );
}

1.5pxのドット、柔らかな背景、彩度の高いテキスト、先頭を大文字にした値。これが Spencer のバッジです。

Modal
export function Modal({
  open, title, children, onClose, width = "max-w-3xl",
}: {
  open: boolean;
  title: string;
  children: ReactNode;
  onClose: () => void;
  width?: string;
}) {
  if (!open) return null;
  return (
    <div
      className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/30 px-4 py-10"
      onClick={onClose}
    >
      <div
        className={`w-full ${width} overflow-hidden rounded-xl border border-line bg-surface shadow-[0_20px_60px_-24px_rgba(0,0,0,0.25)]`}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center justify-between border-b border-line px-6 py-4">
          <h2 className="text-[15px] font-semibold tracking-tight text-ink">{title}</h2>
          <button type="button" onClick={onClose} className="text-sm text-ink-muted transition hover:text-ink">
            Close
          </button>
        </div>
        <div className="max-h-[78vh] overflow-y-auto scrollbar-thin">{children}</div>
      </div>
    </div>
  );
}

必要なモーダルはこれだけです。

App shell
export function App() {
  return (
    <div className="clean-app flex h-screen flex-col bg-surface pt-7 text-[13px] text-ink">
      {bannerError && <ErrorBanner message={bannerError} onDismiss={() => setBannerError(null)} />}
      <div className="min-h-0 flex-1">
        {/* routed content */}
      </div>
    </div>
  );
}

スクロールコンテナのmin-h-0 flex-1は必須です。これがないと、ネストされたフレックス列をスクロールできません。

コンポーネント作成の規約

  • 1ファイルにつき1コンポーネントとし、ファイル名はPascalCaseにします。バレルファイルやindex.tsからの再エクスポートは使いません。

  • 関数コンポーネントのみを使います。React.FCは使いません。実際にrefが必要な場合を除き、forwardRefも使いません。

  • propsには別のinterfaceではなく、インラインのオブジェクト型を使います。

    // do
    export function Badge({ value }: { value: Risk }) { ... }
    // not
    interface BadgeProps { value: Risk }
    export function Badge({ value }: BadgeProps) { ... }
    

    例外:propsを再利用する場合や、型がジェネリックの場合(下記のInlinePicker<T>パターンなど)は、interfaceに名前を付けます。

  • 条件付きクラスにはcx()を使います。テンプレートリテラルの連結でクラスを組み立てないでください。

  • anyは使いません。Spencer の TypeScript 設定は厳格で、noUncheckedIndexedAccessも有効になっています。配列のインデックス参照で返されるT | undefinedを適切に処理してください。

  • インポートの順序:React → @shared/*の型 → lib/ユーティリティ → 相対パスのコンポーネント。

  • コメントでは、何をするかではなくなぜそうするかを説明します。JSDocは使いません。実際の理由なしに// TODO: refactorと書かないでください。

ジェネリックなInlinePickerパターン

ステータス、重大度、リスク、ティアなど、意味に応じた色を持つセレクト形式のコントロールには、このジェネリックを使います。それぞれに個別の<select>を書かないでください。

interface InlinePickerProps<T extends string> {
  value: T;
  disabled?: boolean;
  saving?: boolean;
  onChange: (next: T) => void;
  options: readonly T[];
  toneClass: (value: T) => string;
  ariaLabel: string;
  label?: (value: T) => string;
}

function InlinePicker<T extends string>({
  value, disabled, saving, onChange, options, toneClass, ariaLabel, label = (v) => v,
}: InlinePickerProps<T>) {
  return (
    <label className="relative inline-flex">
      <span className="sr-only">{ariaLabel}</span>
      <select
        value={value}
        disabled={disabled || saving}
        onChange={(e) => onChange(e.target.value as T)}
        className={cx(
          "cursor-pointer appearance-none rounded-full border border-transparent bg-no-repeat py-0.5 pl-2.5 pr-6 text-[11px] font-medium capitalize",
          toneClass(value),
          saving && "opacity-60",
          disabled && "cursor-not-allowed opacity-50",
        )}
      >
        {options.map((o) => <option key={o} value={o}>{label(o)}</option>)}
      </select>
    </label>
  );
}

次に、具体的な型を指定します。

export function RiskPicker(props: Omit<InlinePickerProps<Risk>, "options" | "toneClass" | "ariaLabel">) {
  return <InlinePicker {...props} options={risks} toneClass={(v) => `${riskTone[v].text} ${riskTone[v].bg}`} ariaLabel="Risk" />;
}

フォーム

  • 通常の<form onSubmit={...}>を使います。event.preventDefault()を呼び出してください。react-hook-formやformikは使いません。
  • フォームごとにsavingのbooleanを1つ用意します。trueの間は送信ボタンを無効にしてください。
  • エラーは、画面上部のErrorBannerか、親に渡すsetErrorコールバックで表示します。トーストライブラリは使いません。
  • バリデーションは送信ハンドラー内に記述します。大きくなったら、同じファイル内でvalidate()関数に切り出してください。Spencer から指示がない限り、zodは使わないでください。
  • useEffectでscrollHeightを使う方法により、テキストエリアを自動リサイズします(references/textarea-autoresize.mdを参照)。

読み込み中、空、エラーの状態

3 つのルール:

  1. 読み込み中は、画面全体を覆うスピナーではなく、小さなインラインマーカーにします。フォームを無効にし、ボタンに Saving… というテキストを表示します。リストでは、行の高さに合わせたスケルトン行(1px の境界線、同じパディング)を表示します。脈打つグラデーションの塊は使いません。
  2. 空の状態では、text-ink-muted のテキストを 1 行と、必要なら 2 次ボタンを 1 つだけ表示します。イラスト、クリップアート、大きな「始めましょう」カードは使いません。
  3. エラー状態には、ErrorBanner パターンで bg-[var(--risk-high-soft)] text-risk-high を使用します。必ず閉じるボタンを含めます。

Spencer の UI に絶対に含めないもの

以下のいずれかを使おうとしていることに気づいたら、やめてください。

  • グラデーションの背景、グラデーションのテキスト、グラデーションのボタン。
  • ガラス風の効果、ぼかし、backdrop-filter 効果。
  • カード、ボタン、入力欄のドロップシャドウ(モーダルだけは例外)。
  • rounded-2xl 以上。rounded-xl より大きいものは、マーケティングサイトのように見えます。
  • ヒーローセクション、大きく中央揃えされた見出し、「ようこそ、___」のスプラッシュ画面。
  • ストックイラスト、undraw 風の SVG、マスコット。
  • 「見た目をよくするため」にラベルすべてに Lucide アイコンを散りばめること。
  • アニメーション付きの登場、ページ遷移、スクロール連動アニメーション。
  • 色は、ink ランプ、surface ランプ、line ランプ、そして 1 つのセマンティックランプ(risk/sev/status)以外では使わないこと。
  • Tailwind の gray-*、slate-*、zinc-*、neutral-* クラス。一貫したパレットを保つため、代わりにカスタムの ink と line トークンを使用してください。
  • Tailwind の blue-500、indigo-600、purple-*、pink-* のアクセント。
  • 「モダンな SaaS」向けのフォントとして選ばれる Inter、Geist、Plus Jakarta Sans、Manrope。システムフォントのみを使用してください。
  • Spencer がダークモードを明示的に求めない限り、dark: バリアント。

既存のリポジトリで作業する場合

Spencer が、shadcn を使った Next.js や Mantine アプリなど、すでに異なるスタックを採用しているリポジトリを編集している場合、このスキルに合わせて書き換えてはいけません。リポジトリの規約に従ってください。ただし、このスキルのデザイン上の基本方針は適用します。文字を小さめにし、余白を詰め、中立的なパレットを使い、不要なグラデーションやシャドウを避けてください。既存のコンポーネントを置き換えるのではなく、その方向に調整してください。

リファレンスファイル

このファイルだけでは情報が足りない場合は、次のファイルを参照してください。

  • references/full-tailwind-config.md — CSS 変数を含む、完全な tailwind.config.ts。
  • references/full-index-css.md — .clean-app、.tracked-label、.scrollbar-thin、.select-ink を含むグローバル CSS。
  • references/starter-app.md — 新規プロジェクト向けの最小限の App.tsx、main.tsx、index.html。
  • references/anti-patterns.md — 具体的な悪い例と良い例のコードを組み合わせた、避けるべきパターンの詳しい説明。既存の AI 生成フロントエンドを Spencer のスタイルに合わせてレビューまたはリファクタリングする際に役立ちます。

コメント