このレシピは @event/design(実装済み・検証済み・非 React 純関数層)をイベント app に配線する完全手順。 対象 app を apps/<app> とする(例では apps/starter — starter には既に全配線が入っているので照合先にできる)。 上から順に実行すれば完了する。
この機能は feature flag 不要(全ページ共通の基盤層。event.config.ts の features には何も足さない。 nav も route も増えず、design.ts と layout.tsx の <head> だけが変わる)。
仕組みの要点(詳細は第44章):
- イベントの見た目は
globals.cssの CSS 変数 1 組(--bg/--accent…)で決まる defineDesign(部分入力)が未指定を既定(= globals.css の実値)で埋めてEventDesignを返すtokensToCssVars(design)が globals.css と同じ変数名で:root{}+ dark media query の CSS 文字列を吐く- layout の
<head>に<style>で注入 → globals.css の既定を「書いた差分だけ」上書き(globals.css は触らない)
1. 依存を追加
apps/<app>/package.json の dependencies に追加:
"@event/design": "*"
apps/<app>/next.config.ts の transpilePackages 配列に "@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/design の COLOR_VARS / FONT_VARS / RADIUS_VARS / SPACE_VARS が単一真実源。 globals.css はこのレシピでは編集しない(既定値を持つ側。注入が差分上書きする)。
| トークン | CSS 変数 | 既定(light) | 区分 |
|---|---|---|---|
colors.bg | --bg | #f1f3ef | globals.css 既存 |
colors.surface | --surface | #fafbf9 | globals.css 既存 |
colors.ink | --ink | #1a211d | globals.css 既存 |
colors.ink2 | --ink-2 | #4c5852 | globals.css 既存 |
colors.ink3 | --ink-3 | #7e8882 | globals.css 既存 |
colors.line | --line | #d9ded8 | globals.css 既存 |
colors.accent | --accent | #0b7a6c | globals.css 既存 |
colors.accentSoft | --accent-soft | rgba(11, 122, 108, 0.08) | globals.css 既存 |
fonts.mono | --mono | "Cascadia Code", ui-monospace, Consolas, monospace | globals.css 既存 |
fonts.sans | --sans | body の font-family と同値 | 注入で追加 |
fonts.display | --display | sans と同値 | 注入で追加 |
radius.sm/md/lg | --radius-sm/md/lg | 6px / 10px / 16px | 注入で追加 |
space.xs/sm/md/lg/xl | --space-xs/…/xl | 4px / 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
src/design.tsのtokens.colors.accentを目立つ色(例"#c0492f")に変えて保存- ブラウザでトップページを再読み込み → アクセント色の要素(見出しラベル等
var(--accent)参照箇所)が変わる "#0b6ea8"に戻す → 元の色に戻る- OS をダークモードに切り替え →
dark.colors.accentの色が効く(prefers-color-scheme 追従の確認)
完了条件: typecheck が通り、design.ts の accent 変更だけで dev の表示色が変わり、ダーク追従が保たれている。