レシピ · 5手 · 1プロンプト実装

デザイントークン層を追加する

既存イベント app に @event/design を配線し、src/design.ts の defineDesign 1 ファイルで配色・書体・角丸・余白を型付きで上書きできる状態にする

前提: new-event解説 ch.44module: デザイントークン層 + Worldview

このレシピは @event/design(実装済み・検証済み・非 React 純関数層)をイベント app に配線する完全手順。 対象 app を apps/<app> とする(例では apps/starter — starter には既に全配線が入っているので照合先にできる)。 上から順に実行すれば完了する。

この機能は feature flag 不要(全ページ共通の基盤層。event.config.ts の features には何も足さない。 nav も route も増えず、design.ts と layout.tsx の <head> だけが変わる)。

仕組みの要点(詳細は第44章):

1. 依存を追加

apps/<app>/package.json の dependencies に追加:

"@event/design": "*"

apps/<app>/next.config.tstranspilePackages 配列に "@event/design" を足す。

monorepo root で:

yarn install

2. src/design.ts — デザイン宣言(イベントごとに触る唯一のファイル)

apps/<app>/src/design.ts(新規):

import { defineDesign, type EventDesign } from "@event/design";

/**
 * <app> のデザイン宣言 — globals.css トークンの唯一の型付き上書き面。
 *
 * ここで宣言した値は @event/design が globals.css と同じ CSS 変数名
 * (--accent / --bg …)へ写像し、layout.tsx が <style> で <head> に注入して
 * 既定を「差分だけ」上書きする。
 *
 * ここではブランドアクセントだけ差し替えている(bg / ink / surface / line は
 * 未指定 = globals.css の既定色のまま)。defineDesign が未指定を既定で埋めるので、
 * 注入 CSS の他トークンは globals.css と同値になり、既存の見た目は壊れない。
 */
export const eventDesign: EventDesign = defineDesign({
  name: "<app>",
  tokens: {
    colors: {
      accent: "#0b6ea8",
      accentSoft: "rgba(11, 110, 168, 0.08)",
    },
  },
  dark: {
    colors: {
      accent: "#4bb6e8",
      accentSoft: "rgba(75, 182, 232, 0.12)",
    },
  },
});

dark の意味: 省略 = 既定ダークで OS 追従 / Partial = 既定ダークに浅くマージ / null = dark ブロックを出さない = ライト固定。)

3. layout.tsx — <head> に tokensToCssVars を注入

apps/<app>/src/app/layout.tsx に import 2 行と <head> ブロックを追加する。 最小の完全形(add-i18n 適用済みの app なら既存の LangProvider 配線はそのまま残し、 <head> ブロックだけ足せばよい — starter は両方入りが完成形):

import type { Metadata } from "next";
import type { ReactNode } from "react";
import { tokensToCssVars } from "@event/design";
import { eventDesign } from "@/design";
import "./globals.css";

export const metadata: Metadata = {
  title: "<app> — event site",
  description: "<イベントの説明>",
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
      <head>
        {/*
          design.ts のトークンを globals.css と同じ CSS 変数名へ写像して注入。
          globals.css の import 後に <head> で出すので、同名変数の「差分だけ」が上書きされる。
          (ダークも同構造で emit するため prefers-color-scheme 追従は維持される)
        */}
        <style dangerouslySetInnerHTML={{ __html: tokensToCssVars(eventDesign) }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

tokensToCssVars(eventDesign) は純関数で、globals.css と同じ構造の文字列を返す:

:root {
  --bg: #f1f3ef;
  --accent: #0b6ea8;
  /* … 全トークン(未指定は既定値なので見た目は変わらない) */
}
@media (prefers-color-scheme: dark) {
  :root { --bg: #0c110f; --accent: #4bb6e8; /* … */ }
}

4. globals.css 実変数との対応

写像テーブルは @event/designCOLOR_VARS / FONT_VARS / RADIUS_VARS / SPACE_VARS が単一真実源。 globals.css はこのレシピでは編集しない(既定値を持つ側。注入が差分上書きする)。

トークンCSS 変数既定(light)区分
colors.bg--bg#f1f3efglobals.css 既存
colors.surface--surface#fafbf9globals.css 既存
colors.ink--ink#1a211dglobals.css 既存
colors.ink2--ink-2#4c5852globals.css 既存
colors.ink3--ink-3#7e8882globals.css 既存
colors.line--line#d9ded8globals.css 既存
colors.accent--accent#0b7a6cglobals.css 既存
colors.accentSoft--accent-softrgba(11, 122, 108, 0.08)globals.css 既存
fonts.mono--mono"Cascadia Code", ui-monospace, Consolas, monospaceglobals.css 既存
fonts.sans--sansbody の font-family と同値注入で追加
fonts.display--displaysans と同値注入で追加
radius.sm/md/lg--radius-sm/md/lg6px / 10px / 16px注入で追加
space.xs/sm/md/lg/xl--space-xs/…/xl4px / 8px / 16px / 24px / 40px注入で追加

「注入で追加」の変数は globals.css には無いが、注入後は全ページで var(--radius-md) のように参照できる (tokensToCssVars は解決済み全トークンを emit するため)。dark の既定色は globals.css の @media (prefers-color-scheme: dark) ブロックと同値(--bg: #0c110f / --accent: #38bfab 等)。

5. プリセット / slots / dark 固定

プリセットへ丸ごと切り替える(1 行差し替えで全体が変わる。system / light / noir / warm):

import { defineDesign, DESIGN_PRESETS, type EventDesign } from "@event/design";

export const eventDesign: EventDesign = defineDesign(DESIGN_PRESETS.noir);

プリセットは部分入力のままなので、イベント色を更に上書き合成できる:

export const eventDesign: EventDesign = defineDesign({
  ...DESIGN_PRESETS.noir,
  name: "<app>",
});

セクション別上書き(slots) — selector に scope して指定トークンだけ差し替える:

export const eventDesign: EventDesign = defineDesign({
  name: "<app>",
  slots: [{ selector: "header", tokens: { colors: { surface: "#0e1512" } } }],
});

ライト固定 — OS がダークでも切り替えない: dark: null(プリセット light と同じ)。

6. 検証

yarn workspace <app> typecheck   # 型が通ること
yarn workspace <app> dev
  1. src/design.tstokens.colors.accent を目立つ色(例 "#c0492f")に変えて保存
  2. ブラウザでトップページを再読み込み → アクセント色の要素(見出しラベル等 var(--accent) 参照箇所)が変わる
  3. "#0b6ea8" に戻す → 元の色に戻る
  4. OS をダークモードに切り替え → dark.colors.accent の色が効く(prefers-color-scheme 追従の確認)

完了条件: typecheck が通り、design.ts の accent 変更だけで dev の表示色が変わり、ダーク追従が保たれている。