Color mode

テーマ作成の詳細

このページは、Riebeckite Theme を実際に設計・実装するときの詳細ガイドです。

初めて Theme を作る場合は、先に はじめてのテーマ作成 を読んでください。

このページでは、その先に必要になる、

  • Theme の責務
  • defineTheme
  • Color Mode
  • Typography
  • Layout
  • Design Token
  • Stable CSS Hook
  • Theme 固有 Option
  • CSS Cascade
  • Package としての配布

までをまとめて扱います。

各型やフィールドの完全な定義を確認したい場合は Theme API を参照してください。

1. Theme にするべき変更

Theme は Presentation Layer です。

Site の機能や Content の意味は変更せず、見た目だけを変更します。

Theme でできる Theme ではしない
Token の上書き・追加 Component Replacement
CSS Rule の定義 JSX の注入
Theme 固有 data-* Attribute Route の追加
Color Mode の宣言 Plugin の追加・削除
Typography の宣言 Client Script の実行
Layout Preset の宣言 DOM Transformation
Stable Hook の Styling Island の登録
userCss による最終上書き Filesystem / ContentManager へのアクセス

迷った場合は、次のように判断します。

Diagram source
text
flowchart TD
    Q{"何を変更したい?"}
 
    Q -->|"見た目"| Theme["Theme"]
    Q -->|"再利用可能な機能"| Plugin["Plugin"]
    Q -->|"Site固有Route / Layout構造"| App["Application"]
    Q -->|"Framework共通Model"| Core["Core"]

基本的には、

text
機能
  → Plugin
 
見た目
  → Theme
 
Site 固有 Route
  → Application

です。

見た目を変えるためだけに Plugin を作ったり、機能を追加するために Theme を拡張したりしないでください。

2. 最小の Theme

Theme は defineTheme() で定義します。

ts
import { defineTheme } from "@riebeckite/core";
 
export function exampleTheme() {
  return defineTheme({
    name: "example",
 
    styles: [
      {
        moduleSpecifier:
          "@riebeckite/theme-example/style.css",
      },
    ],
  });
}

Site では theme に指定します。

ts
export default defineConfig({
  theme: exampleTheme(),
});

これが最小構成です。

3. defineTheme の Contract

Theme が扱う主な Contract は次のとおりです。

領域 内容
Identity name
Theme 固有設定 options
CSS styles[].moduleSpecifier
共通設定 colorMode, typography, articleLayout, tokens, userCss
Attributes 安全な data-* Attribute

たとえば、

ts
import { defineTheme } from "@riebeckite/core";
 
export function exampleTheme() {
  return defineTheme({
    name: "example",
 
    options: {
      // Theme 固有 Option
    },
 
    styles: [
      {
        moduleSpecifier:
          "@riebeckite/theme-example/style.css",
      },
    ],
 
    attributes: {
      "data-example-flag": "on",
    },
  });
}

のように定義できます。

styles[].moduleSpecifier は Host Bundler が解決する Module Specifier です。

CSS File を Application Directory へコピーするための Path ではありません。

4. Site 内だけで使う Theme

Theme は npm Package として公開しなくても利用できます。

たとえば、

text
site/
└─ extensions/
   ├─ local-theme.ts
   └─ theme.css

のように Site 内へ置けます。

ts
// site/extensions/local-theme.ts
 
import { defineTheme } from "@riebeckite/core";
 
export function localTheme() {
  return defineTheme({
    name: "site-local",
 
    styles: [
      {
        moduleSpecifier:
          "/extensions/theme.css",
      },
    ],
 
    attributes: {
      "data-site-local": "on",
    },
  });
}

Site-local Theme の、

  • name
  • styles
  • attributes
  • tokens

も Published Theme と同じ resolveThemeConfig の経路で解決・sanitize・適用されます。

5. Color Mode

Theme は3種類の Color Mode を扱えます。

ts
type ThemeColorMode =
  | "light"
  | "dark"
  | "system";
Mode 動作
light Light 配色
dark Dark 配色
system OS の設定に追従

Theme は data-theme と Semantic Token を使って配色を切り替えます。

個々の Component に Light / Dark の色を直接埋め込まないでください。

6. Color Mode の CSS

基本となる CSS は次の形です。

css
/* Light */
:is(:root, .rb-theme-root)
[data-theme-name="<name>"] {
  /* ... */
}
 
/* Dark */
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]
[data-theme="dark"] {
  /* ... */
}
 
/* System */
@media (prefers-color-scheme: dark) {
  :is(:root, .rb-theme-root)
  [data-theme-name="<name>"]
  :not([data-theme]) {
    /* ... */
  }
}

Server は colorMode が "system" 以外なら <html> に data-theme を出力します。

html
<html
  data-theme-name="example"
  data-theme="dark"
>

"system" の場合は data-theme を出力しません。

html
<html data-theme-name="example">

この違いは重要です。

7. system では Attribute を削除する

実行時に Color Mode を変更する場合は、

text
light
  → data-theme="light"
 
dark
  → data-theme="dark"
 
system
  → data-theme を削除

とします。

たとえば、

ts
document.documentElement.dataset.theme =
  "dark";

から System へ戻す場合は、

ts
delete document.documentElement.dataset.theme;

とします。

次のように空文字へ変更してはいけません。

ts
document.documentElement.dataset.theme = "";

これは、

html
<html data-theme="">

となり、依然として [data-theme] Selector に一致するためです。

その結果、

css
:not([data-theme])

が成立せず、System Mode の Media Query が機能しません。

@riebeckite/plugin-color-mode がこの Contract の参照実装です。

8. Typography

Theme は Typography Preset を指定できます。

ts
type ThemeTypographyPreset =
  | "system"
  | "serif"
  | "sans";

Preset は、

  • Body
  • Heading
  • Code

などの Semantic Font Token に反映されます。

Font を個々の Component に直接指定するのではなく、Semantic Token を通して Site 全体の Typography を統一します。

9. Article Layout

Theme は Article Layout Preset を指定できます。

ts
type ThemeArticleLayoutPreset =
  | "article"
  | "sidebar"
  | "full-width";

Theme が決めるのは Layout の Presentation です。

Route や Component Tree そのものを Theme が差し替えるわけではありません。

Diagram source
text
flowchart LR
    App["Application<br/>Component Structure"]
    Hooks["Stable Layout Hooks"]
    Theme["Theme<br/>Presentation"]
 
    App --> Hooks
    Theme --> Hooks

10. Design Tokens

Theme の中心となるのが Semantic Design Token です。

Component や Plugin は、

text
このThemeの黒
このThemeの灰色

のような Theme 固有の値を参照するのではなく、

text
本文色
背景色
Accent
Border

という意味を参照します。

Diagram source
text
flowchart LR
    UI["Component / Plugin"]
    Token["--rb-color-ink"]
    ThemeA["Theme A<br/>#202020"]
    ThemeB["Theme B<br/>#d8dee9"]
 
    UI --> Token
    ThemeA --> Token
    ThemeB --> Token

これによって Theme を交換しても Component を変更する必要がありません。

11. Color Tokens

主な Color Token は次のとおりです。

Token CSS Variable
paper --rb-color-paper
ink --rb-color-ink
muted --rb-color-muted
accent --rb-color-accent
border --rb-color-border
borderStrong --rb-color-border-strong
surface --rb-color-surface
surfaceHover --rb-color-surface-hover
overlay --rb-color-overlay
danger --rb-color-danger
success --rb-color-success
codeBackground --rb-color-code-background

12. Typography Tokens

Token CSS Variable
bodyFont --rb-font-body
headingFont --rb-font-heading
monoFont --rb-font-mono

13. Layout Tokens

Token CSS Variable
pageMaxWidth --rb-layout-page-max
articleMaxWidth --rb-layout-article-max
sidebarWidth --rb-layout-sidebar
contentGap --rb-layout-gap

CSS では次のように定義します。

css
@layer base {
  :is(:root, .rb-theme-root)
  [data-theme-name="example"] {
    --rb-color-paper: #fafafa;
    --rb-color-ink: #202020;
    --rb-color-accent: #555;
 
    --rb-font-body:
      system-ui, sans-serif;
 
    --rb-layout-article-max: 48rem;
  }
}

Token 名と CSS Variable 名が完全に同じとは限りません。

たとえば、

text
borderStrong
  → --rb-color-border-strong
 
surfaceHover
  → --rb-color-surface-hover
 
codeBackground
  → --rb-color-code-background

のように kebab-case へ変換されます。

14. Semantic Token を使う

Plugin や Component でも Semantic Token を利用してください。

css
/* Good */
 
.rr-example {
  color:
    var(--rb-color-ink);
 
  background:
    var(--rb-color-surface);
}

次のように Theme 固有の色を直接指定することは避けます。

css
/* Avoid */
 
.rr-example {
  color: #171717;
  background: #f6efe2;
}

後者では Theme を交換しても Plugin の色が変わりません。

Plugin 固有の意味を持つ Token が必要なら、

text
--rr-*

を Plugin 側で定義できます。

必要に応じて、

css
--rr-example-background:
  var(--rb-color-surface);

のように --rb-* を fallback として利用できます。

15. Theme Root

Theme CSS は Document 全体へ無条件に適用しません。

基本 Selector は、

css
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]

です。

<name> には Theme の Identity Name が入ります。

組み込み Theme では、たとえば、

text
riebeckite
minimal
gruvbox
sakura
tokyonight
rerurate

があります。

16. Theme Root が必要な理由

通常の Site では Application が <html> に Theme 名を付けます。

html
<html data-theme-name="minimal">

この場合は :root が Theme Root になります。

一方、Theme Gallery では、

html
<div
  class="rb-theme-root"
  data-theme-name="minimal"
>
  ...
</div>
 
<div
  class="rb-theme-root"
  data-theme-name="gruvbox"
>
  ...
</div>

のように、同じ Document 内で複数 Theme を表示できます。

Diagram source
text
flowchart TD
    CSS["同じTheme CSS"]
 
    CSS --> Site["実Site<br/>:root"]
    CSS --> PreviewA["Preview<br/>.rb-theme-root"]
    CSS --> PreviewB["別Theme Preview<br/>.rb-theme-root"]

そのため Theme CSS を裸の、

css
:root {
  /* ... */
}

として定義しないでください。

17. Theme Rule の Scope

Color Mode、Typography、Theme Option、Element Rule も同じ Theme Root に閉じ込めます。

css
/* Light */
:is(:root, .rb-theme-root)
[data-theme-name="<name>"] {
  /* ... */
}
 
/* Dark */
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]
[data-theme="dark"] {
  /* ... */
}
 
/* System */
@media (prefers-color-scheme: dark) {
  :is(:root, .rb-theme-root)
  [data-theme-name="<name>"]
  :not([data-theme]) {
    /* ... */
  }
}
 
/* Typography */
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]
[data-typography="serif"] {
  /* ... */
}
 
/* Theme Option */
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]
[data-example-option="on"] {
  /* ... */
}
 
/* Elements */
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]
:focus-visible {
  /* ... */
}

data-theme-name は Application が出力します。

Preview では .rb-theme-root と同じ Theme Name を指定します。

18. Theme 固有 Attributes

Theme 固有 Option を CSS へ渡したい場合は、安全な data-* Attribute を利用します。

ts
return defineTheme({
  name: "newspaper",
 
  attributes: {
    "data-newspaper-density":
      "compact",
  },
});

CSS では、

css
:is(:root, .rb-theme-root)
[data-theme-name="newspaper"]
[data-newspaper-density="compact"] {
  /* ... */
}

のように利用できます。

Theme API から、

text
class
style
id
lang

などを自由に変更する設計にはしません。

Framework が所有する Attribute と Theme 固有 Attribute を分離してください。

ThemeRoot と themeRootAttributes

Framework は ThemeRoot という UI primitive を提供し、<html> 要素への theme 属性の付与を担当します。

tsx
import { ThemeRoot } from "@riebeckite/honox/ui";
 
<ThemeRoot
  theme={config.theme}
  lang={c.get("htmlLanguage") ?? config.site.locale}
>
  {children}
</ThemeRoot>

ThemeRoot は次のように <html> 要素を描画します。

html
<html
  lang="ja"
  data-theme-name="minimal"
  data-theme="dark"
  data-typography="system"
  data-article-layout="article"
>

Framework は themeRootAttributes(theme) を使って、theme の attributes に加え、以下の予約属性を <html> へ出力します。

  • data-theme: Color Mode の状態 ("light" / "dark" / 未設定の場合は削除)
  • data-theme-name: Theme の識別子
  • data-typography: Typography Preset の値
  • data-article-layout: Article Layout Preset の値

独自の <html> 属性を追加したい場合は、ThemeRoot の代わりに themeRootAttributes を直接使えます。

tsx
import { themeRootAttributes } from "@riebeckite/honox/ui";
 
<html
  lang={c.get("htmlLanguage") ?? config.site.locale}
  {...themeRootAttributes(config.theme)}
  data-custom-attr="..."
>
  ...
</html>

ただし、Framework が予約する data-theme, data-theme-name, data-typography, data-article-layout は theme 側の attributes では上書きされません。

Plugin や Theme が独自に themeAttributes() を実装していた場合は、Framework が提供する ThemeRoot / themeRootAttributes() への移行を検討してください。Framework が所有する attribute namespace と Theme 固有の namespace を明確に分離できます。

19. Theme Factory Options

Theme 固有の機能は Factory Option として定義します。

ts
type NewspaperOptions = {
  density?:
    | "compact"
    | "comfortable";
};
 
export function newspaperTheme(
  options: NewspaperOptions = {},
) {
  return defineTheme({
    name: "newspaper",
 
    options,
 
    attributes: {
      "data-newspaper-density":
        options.density
        ?? "comfortable",
    },
 
    styles: [
      {
        moduleSpecifier:
          "@riebeckite/theme-newspaper/style.css",
      },
    ],
  });
}

この Option は Core の ThemeConfig に追加しません。

text
newspaper の density
  → newspaperTheme が所有
 
tokyonight の neon
  → tokyonightTheme が所有

Theme 固有の概念は、その Theme Package 内で完結させます。

20. Stable CSS Hooks

Theme は Application や Plugin の内部 Markup ではなく、公開された Stable CSS Hook を対象にします。

Hook には大きく2つの Namespace があります。

Namespace 所有者 用途
rb-* Framework Site の構造
rr-* Plugin / Feature Plugin UI

Framework が提供する代表的な Hook は、

text
.rb-theme-root
.rb-site
.rb-article
.rb-article-layout
.rb-article-header
.rb-article-body
.rb-article-content
.rb-article-meta
.rb-article-footer
.rb-sidebar

です。

レンダリングされた Markdown 本文は .rb-article-content に包まれ、Markdown のセマンティックなベースライン(リストマーカーと字下げ、見出し、段落とブロックの余白、表、図、定義リスト、インラインコード、整形済みブロック)はこの wrapper が持ちます。.rb-article-body は header、metadata、本文、plugin slot を含む article body のシェルです。ベースラインはレイヤー化されているため、テーマは構造を再宣言する必要はなく、--rb-* トークンとレイヤー外のキャラクター規則で見た目を表現します。

Plugin は、

text
.rr-search
.rr-callout
.rr-table-of-contents
.rr-backlinks
.rr-local-graph
.rr-code
.rr-code-tabs
.rr-lightbox
.rr-excalidraw
.rr-mermaid
.rr-query
.rr-cardlink
.rr-diff-history
.rr-attachment
.rr-media
.rr-recent-posts
.rr-garden-explorer

などの Root Hook を提供できます。

Theme はこれらの Stable Hook を対象にします。

21. Plugin の内部 Class

Plugin は内部で、

text
.rr-search
.rr-search__input
.rr-search__result
.rr-search--loading

のような BEM Class を使う場合があります。

基本的に Public Hook は、

text
.rr-search

です。

text
__input
__result
--loading

などは、Plugin が明示的に Public Hook として文書化していない限り内部実装として扱います。

.sr-only のような一般的な Helper Class も Plugin Hook ではありません。

後方互換性のため旧 Class と .rr-* が同じ要素に存在する場合でも、Theme は .rr-* を利用してください。

22. Character Layer

Theme は Token を変更するだけでなく、Stable Hook を直接 Style して視覚的な個性を与えられます。

たとえば、

css
:is(:root, .rb-theme-root)
[data-theme-name="example"]
.rb-article-header {
  border-bottom:
    var(--rb-rule-width)
    solid
    var(--rb-color-border);
}

のような変更です。

対象にできるのは Stable Hook です。

Theme 側で新しい、

text
rb-*
rr-*

Class を発明して Framework Contract のように扱わないでください。

Character Layer も Presentation 専用です。

Content、Structure、Behavior を変更してはいけません。

23. CSS Layer

Token Definition は @layer base に置きます。

css
@layer base {
  :is(:root, .rb-theme-root)
  [data-theme-name="example"] {
    --rb-color-accent: #b45309;
  }
}

一方、Stable Hook に対する Character Rule は unlayered にします。

css
:is(:root, .rb-theme-root)
[data-theme-name="example"]
.rb-article-header {
  border-bottom:
    var(--rb-rule-width)
    solid
    var(--rb-color-border);
}

Application の Structural CSS と Plugin CSS も unlayered です。

Theme CSS はそれらより後に読み込まれるため、通常は !important を使わなくても上書きできます。

!important に依存しないでください。

24. Web Font

Theme Package は Self-hosted Web Font を含めることができます。

たとえば、

text
styles/
├─ theme.css
└─ fonts/
   └─ example-serif-latin.woff2

のように配置します。

CSS では相対 URL を使います。

css
@font-face {
  font-family: "Example Serif";
 
  src:
    url("./fonts/example-serif-latin.woff2")
    format("woff2");
 
  font-weight: 400 700;
  font-display: swap;
}

Font を同梱する場合は、その Font の License File も Package に含めてください。

Latin Subset のような比較的小さい Font は同梱できます。

日本語などの CJK Font は File Size が大きいため、基本的には System Font Stack へ fallback します。

25. userCss

userCss は Site 利用者が Theme の上から最終調整するための CSS です。

ts
theme: defaultTheme({
  userCss: [
    "/extensions/custom.css",
  ],
}),

Theme の Stylesheet より後に読み込まれるため、userCss が最終的な Override になります。

Theme Package 側で userCss より強い Selector や !important を多用しないでください。

26. CSS Cascade

Riebeckite では CSS の読み込み順も Contract の一部です。

Diagram source
text
flowchart TD
    Base["Base / Application<br/>Structural CSS"]
    Plugin["Plugin Default CSS"]
    Theme["Theme CSS"]
    Token["Config Token<br/>Inline Style"]
    User["userCss"]
 
    Base --> Plugin
    Plugin --> Theme
    Theme --> Token
    Token --> User

順番は、

text
Framework Structural CSS
        ↓
Base / Application CSS
        ↓
Plugin Default CSS
        ↓
Theme CSS
        ↓
Config Token Inline Style
        ↓
userCss

です。

この順番は偶然ではなく、Presentation Extension の Contract として保証されます。

@riebeckite/honox は、

text
.riebeckite/framework-styles.css
.riebeckite/plugin-styles.css
.riebeckite/theme-styles.css

を生成します。

Site は Framework Stylesheet を Plugin Stylesheet より先に、Plugin Stylesheet を Theme Stylesheet より先に読み込みます。

そのため、

text
Plugin
  → 標準の見た目
 
Theme
  → Plugin の見た目を変更
 
userCss
  → Site 利用者が最終調整

という関係になります。

生成された Stylesheet を直接編集したり、Import 順を変更したりしないでください。

27. Package として配布する

公開 Theme は、たとえば次の構成にできます。

text
packages/themes/example/
├─ src/
│  └─ index.ts
├─ styles/
│  ├─ theme.css
│  └─ fonts/          # 必要な場合のみ
├─ package.json
├─ README_ja.md
└─ README.md

Riebeckite Repository 内では、

text
packages/themes/minimal

が雛形になります。

src/index.ts では Theme Factory を公開します。

ts
import { defineTheme } from "@riebeckite/core";
 
export function exampleTheme() {
  return defineTheme({
    name: "example",
 
    styles: [
      {
        moduleSpecifier:
          "@riebeckite/theme-example/style.css",
      },
    ],
  });
}

package.json では Stylesheet を、

text
./style.css

として Export します。

28. Repository 外で Theme を配布する

外部 Theme Package は Riebeckite monorepo の内部構造へ依存させません。

基本的には、

text
@riebeckite/core

の Public API だけを利用します。

次のような Internal Import は避けてください。

ts
import {
  something,
} from "@riebeckite/core/src/...";

また、

text
../../../../packages/core/...

のような monorepo 内部 Path にも依存しません。

Theme の Stylesheet も Package 自身の Export として公開します。

29. Theme を検証する

Theme を作成・変更したら、次の順番で確認します。

Diagram source
text
flowchart LR
    Check["check"]
    Inspect["inspect config"]
    Dev["dev"]
    Build["build"]
 
    Check --> Inspect
    Inspect --> Dev
    Dev --> Build

まず Configuration を確認します。

sh
pnpm exec riebeckite check

次に解決された Theme 設定を確認します。

sh
pnpm exec riebeckite inspect config

実際の表示を確認する場合は、

sh
pnpm exec riebeckite dev

を使います。

最後に生成物まで確認します。

sh
pnpm exec riebeckite build

check、doctor、inspect は Build Output を変更しません。

Theme を交換しても、

  • Route
  • Manifest
  • Content Graph
  • Client Behavior

は変わらないことが基本です。

30. 見た目がおかしい場合

Theme が期待どおりに適用されない場合は、まず次の順番で確認します。

Diagram source
text
flowchart TD
    Start["Themeが適用されない"]
 
    Start --> Name{"data-theme-name は正しい?"}
    Name -->|No| FixName["Theme nameを確認"]
    Name -->|Yes| Hook{"正しいHookを対象にしている?"}
 
    Hook -->|No| FixHook["rb-* / rr-* を確認"]
    Hook -->|Yes| Cascade{"Cascadeは正しい?"}
 
    Cascade -->|No| FixCascade["Plugin → Theme → userCssを確認"]
    Cascade -->|Yes| Mode{"Color Mode条件は正しい?"}
 
    Mode -->|No| FixMode["data-theme / systemを確認"]
    Mode -->|Yes| CSS["Selector / CSSを確認"]

特に確認するのは、

  1. data-theme-name が Theme の name と一致しているか
  2. .rb-* / .rr-* の正しい Stable Hook を対象にしているか
  3. Plugin CSS → Theme CSS → userCss の順になっているか
  4. system なのに data-theme="" が残っていないか
  5. Theme Root の外へ Selector が漏れていないか

です。

31. Theme を作るときの基本方針

Theme の実装では、最終的に次の境界を維持することが重要です。

Diagram source
text
flowchart LR
    App["Application"]
    Plugin["Plugin"]
 
    App --> Hooks["Stable Hooks"]
    Plugin --> Hooks
 
    Core["Core"] --> Tokens["Semantic Tokens"]
 
    Hooks --> Contract["Presentation Contract"]
    Tokens --> Contract
 
    Theme["Theme"] --> Contract
 
    Contract --> Site["Final Site"]

Theme は Application や Plugin の内部構造を所有しません。

Framework と Plugin が公開した、

text
Stable CSS Hooks
Semantic Design Tokens
Theme Attributes
CSS Cascade

という Presentation Contract を利用します。

Theme 固有の設定は Theme Package 内に閉じ込め、Core へ漏らしません。

そして、Theme の変更によって、

text
Content
Route
Manifest
Content Graph
Plugin Behavior
Client Behavior

が変化しない状態を維持してください。

機能は Plugin、構造は Framework / Application、見た目は Theme

という境界を守ることで、Theme を交換しても同じ Site と Plugin をそのまま利用できます。

関連資料

History

1 changesCollapseExpand
1 + # テーマ作成の詳細
2 +
3 + このページは、Riebeckite Theme を実際に設計・実装するときの詳細ガイドです。
4 +
5 + 初めて Theme を作る場合は、先に [はじめてのテーマ作成](../themes/writing-a-theme.md) を読んでください。
6 +
7 + このページでは、その先に必要になる、
8 +
9 + - Theme の責務
10 + - `defineTheme`
11 + - Color Mode
12 + - Typography
13 + - Layout
14 + - Design Token
15 + - Stable CSS Hook
16 + - Theme 固有 Option
17 + - CSS Cascade
18 + - Package としての配布
19 +
20 + までをまとめて扱います。
21 +
22 + 各型やフィールドの完全な定義を確認したい場合は [Theme API](../reference/theme-api.md) を参照してください。
23 +
24 + # 1. Theme にするべき変更
25 +
26 + Theme は **Presentation Layer** です。
27 +
28 + Site の機能や Content の意味は変更せず、見た目だけを変更します。
29 +
30 + | Theme でできる | Theme ではしない |
31 + | --- | --- |
32 + | Token の上書き・追加 | Component Replacement |
33 + | CSS Rule の定義 | JSX の注入 |
34 + | Theme 固有 `data-*` Attribute | Route の追加 |
35 + | Color Mode の宣言 | Plugin の追加・削除 |
36 + | Typography の宣言 | Client Script の実行 |
37 + | Layout Preset の宣言 | DOM Transformation |
38 + | Stable Hook の Styling | Island の登録 |
39 + | `userCss` による最終上書き | Filesystem / ContentManager へのアクセス |
40 +
41 + 迷った場合は、次のように判断します。
42 +
43 + ```mermaid id="5c6pd8"
44 + flowchart TD
45 + Q{"何を変更したい?"}
46 +
47 + Q -->|"見た目"| Theme["Theme"]
48 + Q -->|"再利用可能な機能"| Plugin["Plugin"]
49 + Q -->|"Site固有Route / Layout構造"| App["Application"]
50 + Q -->|"Framework共通Model"| Core["Core"]
51 + ```
52 +
53 + 基本的には、
54 +
55 + ```text id="kl9kkm"
56 + 機能
57 + → Plugin
58 +
59 + 見た目
60 + → Theme
61 +
62 + Site 固有 Route
63 + → Application
64 + ```
65 +
66 + です。
67 +
68 + 見た目を変えるためだけに Plugin を作ったり、機能を追加するために Theme を拡張したりしないでください。
69 +
70 + # 2. 最小の Theme
71 +
72 + Theme は `defineTheme()` で定義します。
73 +
74 + ```ts id="yplspq"
75 + import { defineTheme } from "@riebeckite/core";
76 +
77 + export function exampleTheme() {
78 + return defineTheme({
79 + name: "example",
80 +
81 + styles: [
82 + {
83 + moduleSpecifier:
84 + "@riebeckite/theme-example/style.css",
85 + },
86 + ],
87 + });
88 + }
89 + ```
90 +
91 + Site では `theme` に指定します。
92 +
93 + ```ts id="ij1yvm"
94 + export default defineConfig({
95 + theme: exampleTheme(),
96 + });
97 + ```
98 +
99 + これが最小構成です。
100 +
101 + # 3. `defineTheme` の Contract
102 +
103 + Theme が扱う主な Contract は次のとおりです。
104 +
105 + | 領域 | 内容 |
106 + | --- | --- |
107 + | Identity | `name` |
108 + | Theme 固有設定 | `options` |
109 + | CSS | `styles[].moduleSpecifier` |
110 + | 共通設定 | `colorMode`, `typography`, `articleLayout`, `tokens`, `userCss` |
111 + | Attributes | 安全な `data-*` Attribute |
112 +
113 + たとえば、
114 +
115 + ```ts id="k0nh8m"
116 + import { defineTheme } from "@riebeckite/core";
117 +
118 + export function exampleTheme() {
119 + return defineTheme({
120 + name: "example",
121 +
122 + options: {
123 + // Theme 固有 Option
124 + },
125 +
126 + styles: [
127 + {
128 + moduleSpecifier:
129 + "@riebeckite/theme-example/style.css",
130 + },
131 + ],
132 +
133 + attributes: {
134 + "data-example-flag": "on",
135 + },
136 + });
137 + }
138 + ```
139 +
140 + のように定義できます。
141 +
142 + `styles[].moduleSpecifier` は Host Bundler が解決する Module Specifier です。
143 +
144 + CSS File を Application Directory へコピーするための Path ではありません。
145 +
146 + # 4. Site 内だけで使う Theme
147 +
148 + Theme は npm Package として公開しなくても利用できます。
149 +
150 + たとえば、
151 +
152 + ```text id="10b70z"
153 + site/
154 + └─ extensions/
155 + ├─ local-theme.ts
156 + └─ theme.css
157 + ```
158 +
159 + のように Site 内へ置けます。
160 +
161 + ```ts id="31fzcq"
162 + // site/extensions/local-theme.ts
163 +
164 + import { defineTheme } from "@riebeckite/core";
165 +
166 + export function localTheme() {
167 + return defineTheme({
168 + name: "site-local",
169 +
170 + styles: [
171 + {
172 + moduleSpecifier:
173 + "/extensions/theme.css",
174 + },
175 + ],
176 +
177 + attributes: {
178 + "data-site-local": "on",
179 + },
180 + });
181 + }
182 + ```
183 +
184 + Site-local Theme の、
185 +
186 + - `name`
187 + - `styles`
188 + - `attributes`
189 + - `tokens`
190 +
191 + も Published Theme と同じ `resolveThemeConfig` の経路で解決・sanitize・適用されます。
192 +
193 + # 5. Color Mode
194 +
195 + Theme は3種類の Color Mode を扱えます。
196 +
197 + ```ts id="9qfwqg"
198 + type ThemeColorMode =
199 + | "light"
200 + | "dark"
201 + | "system";
202 + ```
203 +
204 + | Mode | 動作 |
205 + | --- | --- |
206 + | `light` | Light 配色 |
207 + | `dark` | Dark 配色 |
208 + | `system` | OS の設定に追従 |
209 +
210 + Theme は `data-theme` と Semantic Token を使って配色を切り替えます。
211 +
212 + 個々の Component に Light / Dark の色を直接埋め込まないでください。
213 +
214 + # 6. Color Mode の CSS
215 +
216 + 基本となる CSS は次の形です。
217 +
218 + ```css id="m87m0s"
219 + /* Light */
220 + :is(:root, .rb-theme-root)
221 + [data-theme-name="<name>"] {
222 + /* ... */
223 + }
224 +
225 + /* Dark */
226 + :is(:root, .rb-theme-root)
227 + [data-theme-name="<name>"]
228 + [data-theme="dark"] {
229 + /* ... */
230 + }
231 +
232 + /* System */
233 + @media (prefers-color-scheme: dark) {
234 + :is(:root, .rb-theme-root)
235 + [data-theme-name="<name>"]
236 + :not([data-theme]) {
237 + /* ... */
238 + }
239 + }
240 + ```
241 +
242 + Server は `colorMode` が `"system"` 以外なら `<html>` に `data-theme` を出力します。
243 +
244 + ```html id="s6hs50"
245 + <html
246 + data-theme-name="example"
247 + data-theme="dark"
248 + >
249 + ```
250 +
251 + `"system"` の場合は `data-theme` を出力しません。
252 +
253 + ```html id="k3dd13"
254 + <html data-theme-name="example">
255 + ```
256 +
257 + この違いは重要です。
258 +
259 + # 7. `system` では Attribute を削除する
260 +
261 + 実行時に Color Mode を変更する場合は、
262 +
263 + ```text id="h03gwl"
264 + light
265 + → data-theme="light"
266 +
267 + dark
268 + → data-theme="dark"
269 +
270 + system
271 + → data-theme を削除
272 + ```
273 +
274 + とします。
275 +
276 + たとえば、
277 +
278 + ```ts id="iyf9ao"
279 + document.documentElement.dataset.theme =
280 + "dark";
281 + ```
282 +
283 + から System へ戻す場合は、
284 +
285 + ```ts id="6t0p0r"
286 + delete document.documentElement.dataset.theme;
287 + ```
288 +
289 + とします。
290 +
291 + 次のように空文字へ変更してはいけません。
292 +
293 + ```ts id="o2i8bp"
294 + document.documentElement.dataset.theme = "";
295 + ```
296 +
297 + これは、
298 +
299 + ```html id="25ohm9"
300 + <html data-theme="">
301 + ```
302 +
303 + となり、依然として `[data-theme]` Selector に一致するためです。
304 +
305 + その結果、
306 +
307 + ```css id="qfrb4j"
308 + :not([data-theme])
309 + ```
310 +
311 + が成立せず、System Mode の Media Query が機能しません。
312 +
313 + `@riebeckite/plugin-color-mode` がこの Contract の参照実装です。
314 +
315 + # 8. Typography
316 +
317 + Theme は Typography Preset を指定できます。
318 +
319 + ```ts id="6q5em7"
320 + type ThemeTypographyPreset =
321 + | "system"
322 + | "serif"
323 + | "sans";
324 + ```
325 +
326 + Preset は、
327 +
328 + - Body
329 + - Heading
330 + - Code
331 +
332 + などの Semantic Font Token に反映されます。
333 +
334 + Font を個々の Component に直接指定するのではなく、Semantic Token を通して Site 全体の Typography を統一します。
335 +
336 + # 9. Article Layout
337 +
338 + Theme は Article Layout Preset を指定できます。
339 +
340 + ```ts id="l0kwo9"
341 + type ThemeArticleLayoutPreset =
342 + | "article"
343 + | "sidebar"
344 + | "full-width";
345 + ```
346 +
347 + Theme が決めるのは Layout の **Presentation** です。
348 +
349 + Route や Component Tree そのものを Theme が差し替えるわけではありません。
350 +
351 + ```mermaid id="6oqsru"
352 + flowchart LR
353 + App["Application<br/>Component Structure"]
354 + Hooks["Stable Layout Hooks"]
355 + Theme["Theme<br/>Presentation"]
356 +
357 + App --> Hooks
358 + Theme --> Hooks
359 + ```
360 +
361 + # 10. Design Tokens
362 +
363 + Theme の中心となるのが Semantic Design Token です。
364 +
365 + Component や Plugin は、
366 +
367 + ```text id="9vwkwf"
368 + このThemeの黒
369 + このThemeの灰色
370 + ```
371 +
372 + のような Theme 固有の値を参照するのではなく、
373 +
374 + ```text id="zfg9dh"
375 + 本文色
376 + 背景色
377 + Accent
378 + Border
379 + ```
380 +
381 + という**意味**を参照します。
382 +
383 + ```mermaid id="e6x8ou"
384 + flowchart LR
385 + UI["Component / Plugin"]
386 + Token["--rb-color-ink"]
387 + ThemeA["Theme A<br/>#202020"]
388 + ThemeB["Theme B<br/>#d8dee9"]
389 +
390 + UI --> Token
391 + ThemeA --> Token
392 + ThemeB --> Token
393 + ```
394 +
395 + これによって Theme を交換しても Component を変更する必要がありません。
396 +
397 + # 11. Color Tokens
398 +
399 + 主な Color Token は次のとおりです。
400 +
401 + | Token | CSS Variable |
402 + | --- | --- |
403 + | `paper` | `--rb-color-paper` |
404 + | `ink` | `--rb-color-ink` |
405 + | `muted` | `--rb-color-muted` |
406 + | `accent` | `--rb-color-accent` |
407 + | `border` | `--rb-color-border` |
408 + | `borderStrong` | `--rb-color-border-strong` |
409 + | `surface` | `--rb-color-surface` |
410 + | `surfaceHover` | `--rb-color-surface-hover` |
411 + | `overlay` | `--rb-color-overlay` |
412 + | `danger` | `--rb-color-danger` |
413 + | `success` | `--rb-color-success` |
414 + | `codeBackground` | `--rb-color-code-background` |
415 +
416 + # 12. Typography Tokens
417 +
418 + | Token | CSS Variable |
419 + | --- | --- |
420 + | `bodyFont` | `--rb-font-body` |
421 + | `headingFont` | `--rb-font-heading` |
422 + | `monoFont` | `--rb-font-mono` |
423 +
424 + # 13. Layout Tokens
425 +
426 + | Token | CSS Variable |
427 + | --- | --- |
428 + | `pageMaxWidth` | `--rb-layout-page-max` |
429 + | `articleMaxWidth` | `--rb-layout-article-max` |
430 + | `sidebarWidth` | `--rb-layout-sidebar` |
431 + | `contentGap` | `--rb-layout-gap` |
432 +
433 + CSS では次のように定義します。
434 +
435 + ```css id="u6csj3"
436 + @layer base {
437 + :is(:root, .rb-theme-root)
438 + [data-theme-name="example"] {
439 + --rb-color-paper: #fafafa;
440 + --rb-color-ink: #202020;
441 + --rb-color-accent: #555;
442 +
443 + --rb-font-body:
444 + system-ui, sans-serif;
445 +
446 + --rb-layout-article-max: 48rem;
447 + }
448 + }
449 + ```
450 +
451 + Token 名と CSS Variable 名が完全に同じとは限りません。
452 +
453 + たとえば、
454 +
455 + ```text id="avjyrb"
456 + borderStrong
457 + → --rb-color-border-strong
458 +
459 + surfaceHover
460 + → --rb-color-surface-hover
461 +
462 + codeBackground
463 + → --rb-color-code-background
464 + ```
465 +
466 + のように kebab-case へ変換されます。
467 +
468 + # 14. Semantic Token を使う
469 +
470 + Plugin や Component でも Semantic Token を利用してください。
471 +
472 + ```css id="0ld7xq"
473 + /* Good */
474 +
475 + .rr-example {
476 + color:
477 + var(--rb-color-ink);
478 +
479 + background:
480 + var(--rb-color-surface);
481 + }
482 + ```
483 +
484 + 次のように Theme 固有の色を直接指定することは避けます。
485 +
486 + ```css id="5tkz4j"
487 + /* Avoid */
488 +
489 + .rr-example {
490 + color: #171717;
491 + background: #f6efe2;
492 + }
493 + ```
494 +
495 + 後者では Theme を交換しても Plugin の色が変わりません。
496 +
497 + Plugin 固有の意味を持つ Token が必要なら、
498 +
499 + ```text id="fbfuw6"
500 + --rr-*
501 + ```
502 +
503 + を Plugin 側で定義できます。
504 +
505 + 必要に応じて、
506 +
507 + ```css id="8qr5yn"
508 + --rr-example-background:
509 + var(--rb-color-surface);
510 + ```
511 +
512 + のように `--rb-*` を fallback として利用できます。
513 +
514 + # 15. Theme Root
515 +
516 + Theme CSS は Document 全体へ無条件に適用しません。
517 +
518 + 基本 Selector は、
519 +
520 + ```css id="36j35i"
521 + :is(:root, .rb-theme-root)
522 + [data-theme-name="<name>"]
523 + ```
524 +
525 + です。
526 +
527 + `<name>` には Theme の Identity Name が入ります。
528 +
529 + 組み込み Theme では、たとえば、
530 +
531 + ```text id="l0yyse"
532 + riebeckite
533 + minimal
534 + gruvbox
535 + sakura
536 + tokyonight
537 + rerurate
538 + ```
539 +
540 + があります。
541 +
542 + # 16. Theme Root が必要な理由
543 +
544 + 通常の Site では Application が `<html>` に Theme 名を付けます。
545 +
546 + ```html id="18amdy"
547 + <html data-theme-name="minimal">
548 + ```
549 +
550 + この場合は `:root` が Theme Root になります。
551 +
552 + 一方、Theme Gallery では、
553 +
554 + ```html id="3ynnb9"
555 + <div
556 + class="rb-theme-root"
557 + data-theme-name="minimal"
558 + >
559 + ...
560 + </div>
561 +
562 + <div
563 + class="rb-theme-root"
564 + data-theme-name="gruvbox"
565 + >
566 + ...
567 + </div>
568 + ```
569 +
570 + のように、同じ Document 内で複数 Theme を表示できます。
571 +
572 + ```mermaid id="5jjg45"
573 + flowchart TD
574 + CSS["同じTheme CSS"]
575 +
576 + CSS --> Site["実Site<br/>:root"]
577 + CSS --> PreviewA["Preview<br/>.rb-theme-root"]
578 + CSS --> PreviewB["別Theme Preview<br/>.rb-theme-root"]
579 + ```
580 +
581 + そのため Theme CSS を裸の、
582 +
583 + ```css id="69pszy"
584 + :root {
585 + /* ... */
586 + }
587 + ```
588 +
589 + として定義しないでください。
590 +
591 + # 17. Theme Rule の Scope
592 +
593 + Color Mode、Typography、Theme Option、Element Rule も同じ Theme Root に閉じ込めます。
594 +
595 + ```css id="bsov79"
596 + /* Light */
597 + :is(:root, .rb-theme-root)
598 + [data-theme-name="<name>"] {
599 + /* ... */
600 + }
601 +
602 + /* Dark */
603 + :is(:root, .rb-theme-root)
604 + [data-theme-name="<name>"]
605 + [data-theme="dark"] {
606 + /* ... */
607 + }
608 +
609 + /* System */
610 + @media (prefers-color-scheme: dark) {
611 + :is(:root, .rb-theme-root)
612 + [data-theme-name="<name>"]
613 + :not([data-theme]) {
614 + /* ... */
615 + }
616 + }
617 +
618 + /* Typography */
619 + :is(:root, .rb-theme-root)
620 + [data-theme-name="<name>"]
621 + [data-typography="serif"] {
622 + /* ... */
623 + }
624 +
625 + /* Theme Option */
626 + :is(:root, .rb-theme-root)
627 + [data-theme-name="<name>"]
628 + [data-example-option="on"] {
629 + /* ... */
630 + }
631 +
632 + /* Elements */
633 + :is(:root, .rb-theme-root)
634 + [data-theme-name="<name>"]
635 + :focus-visible {
636 + /* ... */
637 + }
638 + ```
639 +
640 + `data-theme-name` は Application が出力します。
641 +
642 + Preview では `.rb-theme-root` と同じ Theme Name を指定します。
643 +
644 + # 18. Theme 固有 Attributes
645 +
646 + Theme 固有 Option を CSS へ渡したい場合は、安全な `data-*` Attribute を利用します。
647 +
648 + ```ts id="ylx4jp"
649 + return defineTheme({
650 + name: "newspaper",
651 +
652 + attributes: {
653 + "data-newspaper-density":
654 + "compact",
655 + },
656 + });
657 + ```
658 +
659 + CSS では、
660 +
661 + ```css id="4evhcs"
662 + :is(:root, .rb-theme-root)
663 + [data-theme-name="newspaper"]
664 + [data-newspaper-density="compact"] {
665 + /* ... */
666 + }
667 + ```
668 +
669 + のように利用できます。
670 +
671 + Theme API から、
672 +
673 + ```text id="cn9sza"
674 + class
675 + style
676 + id
677 + lang
678 + ```
679 +
680 + などを自由に変更する設計にはしません。
681 +
682 + Framework が所有する Attribute と Theme 固有 Attribute を分離してください。
683 +
684 + ## ThemeRoot と themeRootAttributes
685 +
686 + Framework は `ThemeRoot` という UI primitive を提供し、`<html>` 要素への theme 属性の付与を担当します。
687 +
688 + ```tsx
689 + import { ThemeRoot } from "@riebeckite/honox/ui";
690 +
691 + <ThemeRoot
692 + theme={config.theme}
693 + lang={c.get("htmlLanguage") ?? config.site.locale}
694 + >
695 + {children}
696 + </ThemeRoot>
697 + ```
698 +
699 + `ThemeRoot` は次のように `<html>` 要素を描画します。
700 +
701 + ```html
702 + <html
703 + lang="ja"
704 + data-theme-name="minimal"
705 + data-theme="dark"
706 + data-typography="system"
707 + data-article-layout="article"
708 + >
709 + ```
710 +
711 + Framework は `themeRootAttributes(theme)` を使って、theme の `attributes` に加え、以下の予約属性を `<html>` へ出力します。
712 +
713 + - `data-theme`: Color Mode の状態 (`"light"` / `"dark"` / 未設定の場合は削除)
714 + - `data-theme-name`: Theme の識別子
715 + - `data-typography`: Typography Preset の値
716 + - `data-article-layout`: Article Layout Preset の値
717 +
718 + 独自の `<html>` 属性を追加したい場合は、`ThemeRoot` の代わりに `themeRootAttributes` を直接使えます。
719 +
720 + ```tsx
721 + import { themeRootAttributes } from "@riebeckite/honox/ui";
722 +
723 + <html
724 + lang={c.get("htmlLanguage") ?? config.site.locale}
725 + {...themeRootAttributes(config.theme)}
726 + data-custom-attr="..."
727 + >
728 + ...
729 + </html>
730 + ```
731 +
732 + ただし、Framework が予約する `data-theme`, `data-theme-name`, `data-typography`, `data-article-layout` は theme 側の `attributes` では上書きされません。
733 +
734 + Plugin や Theme が独自に `themeAttributes()` を実装していた場合は、Framework が提供する `ThemeRoot` / `themeRootAttributes()` への移行を検討してください。Framework が所有する attribute namespace と Theme 固有の namespace を明確に分離できます。
735 +
736 + # 19. Theme Factory Options
737 +
738 + Theme 固有の機能は Factory Option として定義します。
739 +
740 + ```ts id="94pd1f"
741 + type NewspaperOptions = {
742 + density?:
743 + | "compact"
744 + | "comfortable";
745 + };
746 +
747 + export function newspaperTheme(
748 + options: NewspaperOptions = {},
749 + ) {
750 + return defineTheme({
751 + name: "newspaper",
752 +
753 + options,
754 +
755 + attributes: {
756 + "data-newspaper-density":
757 + options.density
758 + ?? "comfortable",
759 + },
760 +
761 + styles: [
762 + {
763 + moduleSpecifier:
764 + "@riebeckite/theme-newspaper/style.css",
765 + },
766 + ],
767 + });
768 + }
769 + ```
770 +
771 + この Option は Core の `ThemeConfig` に追加しません。
772 +
773 + ```text id="2cqt02"
774 + newspaper の density
775 + → newspaperTheme が所有
776 +
777 + tokyonight の neon
778 + → tokyonightTheme が所有
779 + ```
780 +
781 + Theme 固有の概念は、その Theme Package 内で完結させます。
782 +
783 + # 20. Stable CSS Hooks
784 +
785 + Theme は Application や Plugin の内部 Markup ではなく、公開された Stable CSS Hook を対象にします。
786 +
787 + Hook には大きく2つの Namespace があります。
788 +
789 + | Namespace | 所有者 | 用途 |
790 + | --- | --- | --- |
791 + | `rb-*` | Framework | Site の構造 |
792 + | `rr-*` | Plugin / Feature | Plugin UI |
793 +
794 + Framework が提供する代表的な Hook は、
795 +
796 + ```text id="etblkj"
797 + .rb-theme-root
798 + .rb-site
799 + .rb-article
800 + .rb-article-layout
801 + .rb-article-header
802 + .rb-article-body
803 + .rb-article-content
804 + .rb-article-meta
805 + .rb-article-footer
806 + .rb-sidebar
807 + ```
808 +
809 + です。
810 +
811 + レンダリングされた Markdown 本文は `.rb-article-content` に包まれ、Markdown のセマンティックなベースライン(リストマーカーと字下げ、見出し、段落とブロックの余白、表、図、定義リスト、インラインコード、整形済みブロック)はこの wrapper が持ちます。`.rb-article-body` は header、metadata、本文、plugin slot を含む article body のシェルです。ベースラインはレイヤー化されているため、テーマは構造を再宣言する必要はなく、`--rb-*` トークンとレイヤー外のキャラクター規則で見た目を表現します。
812 +
813 + Plugin は、
814 +
815 + ```text id="rt44pe"
816 + .rr-search
817 + .rr-callout
818 + .rr-table-of-contents
819 + .rr-backlinks
820 + .rr-local-graph
821 + .rr-code
822 + .rr-code-tabs
823 + .rr-lightbox
824 + .rr-excalidraw
825 + .rr-mermaid
826 + .rr-query
827 + .rr-cardlink
828 + .rr-diff-history
829 + .rr-attachment
830 + .rr-media
831 + .rr-recent-posts
832 + .rr-garden-explorer
833 + ```
834 +
835 + などの Root Hook を提供できます。
836 +
837 + Theme はこれらの Stable Hook を対象にします。
838 +
839 + # 21. Plugin の内部 Class
840 +
841 + Plugin は内部で、
842 +
843 + ```text id="u7bmkr"
844 + .rr-search
845 + .rr-search__input
846 + .rr-search__result
847 + .rr-search--loading
848 + ```
849 +
850 + のような BEM Class を使う場合があります。
851 +
852 + 基本的に Public Hook は、
853 +
854 + ```text id="9k6dgy"
855 + .rr-search
856 + ```
857 +
858 + です。
859 +
860 + ```text id="1ggxg1"
861 + __input
862 + __result
863 + --loading
864 + ```
865 +
866 + などは、Plugin が明示的に Public Hook として文書化していない限り内部実装として扱います。
867 +
868 + `.sr-only` のような一般的な Helper Class も Plugin Hook ではありません。
869 +
870 + 後方互換性のため旧 Class と `.rr-*` が同じ要素に存在する場合でも、Theme は `.rr-*` を利用してください。
871 +
872 + # 22. Character Layer
873 +
874 + Theme は Token を変更するだけでなく、Stable Hook を直接 Style して視覚的な個性を与えられます。
875 +
876 + たとえば、
877 +
878 + ```css id="n72uhc"
879 + :is(:root, .rb-theme-root)
880 + [data-theme-name="example"]
881 + .rb-article-header {
882 + border-bottom:
883 + var(--rb-rule-width)
884 + solid
885 + var(--rb-color-border);
886 + }
887 + ```
888 +
889 + のような変更です。
890 +
891 + 対象にできるのは Stable Hook です。
892 +
893 + Theme 側で新しい、
894 +
895 + ```text id="mkmfyh"
896 + rb-*
897 + rr-*
898 + ```
899 +
900 + Class を発明して Framework Contract のように扱わないでください。
901 +
902 + Character Layer も Presentation 専用です。
903 +
904 + Content、Structure、Behavior を変更してはいけません。
905 +
906 + # 23. CSS Layer
907 +
908 + Token Definition は `@layer base` に置きます。
909 +
910 + ```css id="43rbsh"
911 + @layer base {
912 + :is(:root, .rb-theme-root)
913 + [data-theme-name="example"] {
914 + --rb-color-accent: #b45309;
915 + }
916 + }
917 + ```
918 +
919 + 一方、Stable Hook に対する Character Rule は **unlayered** にします。
920 +
921 + ```css id="s2xwqm"
922 + :is(:root, .rb-theme-root)
923 + [data-theme-name="example"]
924 + .rb-article-header {
925 + border-bottom:
926 + var(--rb-rule-width)
927 + solid
928 + var(--rb-color-border);
929 + }
930 + ```
931 +
932 + Application の Structural CSS と Plugin CSS も unlayered です。
933 +
934 + Theme CSS はそれらより後に読み込まれるため、通常は `!important` を使わなくても上書きできます。
935 +
936 + `!important` に依存しないでください。
937 +
938 + # 24. Web Font
939 +
940 + Theme Package は Self-hosted Web Font を含めることができます。
941 +
942 + たとえば、
943 +
944 + ```text id="8g43c5"
945 + styles/
946 + ├─ theme.css
947 + └─ fonts/
948 + └─ example-serif-latin.woff2
949 + ```
950 +
951 + のように配置します。
952 +
953 + CSS では相対 URL を使います。
954 +
955 + ```css id="66ktv6"
956 + @font-face {
957 + font-family: "Example Serif";
958 +
959 + src:
960 + url("./fonts/example-serif-latin.woff2")
961 + format("woff2");
962 +
963 + font-weight: 400 700;
964 + font-display: swap;
965 + }
966 + ```
967 +
968 + Font を同梱する場合は、その Font の License File も Package に含めてください。
969 +
970 + Latin Subset のような比較的小さい Font は同梱できます。
971 +
972 + 日本語などの CJK Font は File Size が大きいため、基本的には System Font Stack へ fallback します。
973 +
974 + # 25. `userCss`
975 +
976 + `userCss` は Site 利用者が Theme の上から最終調整するための CSS です。
977 +
978 + ```ts id="mphtk3"
979 + theme: defaultTheme({
980 + userCss: [
981 + "/extensions/custom.css",
982 + ],
983 + }),
984 + ```
985 +
986 + Theme の Stylesheet より後に読み込まれるため、`userCss` が最終的な Override になります。
987 +
988 + Theme Package 側で `userCss` より強い Selector や `!important` を多用しないでください。
989 +
990 + # 26. CSS Cascade
991 +
992 + Riebeckite では CSS の読み込み順も Contract の一部です。
993 +
994 + ```mermaid id="e5j94x"
995 + flowchart TD
996 + Base["Base / Application<br/>Structural CSS"]
997 + Plugin["Plugin Default CSS"]
998 + Theme["Theme CSS"]
999 + Token["Config Token<br/>Inline Style"]
1000 + User["userCss"]
1001 +
1002 + Base --> Plugin
1003 + Plugin --> Theme
1004 + Theme --> Token
1005 + Token --> User
1006 + ```
1007 +
1008 + 順番は、
1009 +
1010 + ```text id="jz92ku"
1011 + Framework Structural CSS
1012 + ↓
1013 + Base / Application CSS
1014 + ↓
1015 + Plugin Default CSS
1016 + ↓
1017 + Theme CSS
1018 + ↓
1019 + Config Token Inline Style
1020 + ↓
1021 + userCss
1022 + ```
1023 +
1024 + です。
1025 +
1026 + この順番は偶然ではなく、Presentation Extension の Contract として保証されます。
1027 +
1028 + `@riebeckite/honox` は、
1029 +
1030 + ```text id="i2y97j"
1031 + .riebeckite/framework-styles.css
1032 + .riebeckite/plugin-styles.css
1033 + .riebeckite/theme-styles.css
1034 + ```
1035 +
1036 + を生成します。
1037 +
1038 + Site は Framework Stylesheet を Plugin Stylesheet より先に、Plugin Stylesheet を Theme Stylesheet より先に読み込みます。
1039 +
1040 + そのため、
1041 +
1042 + ```text id="4w8d9d"
1043 + Plugin
1044 + → 標準の見た目
1045 +
1046 + Theme
1047 + → Plugin の見た目を変更
1048 +
1049 + userCss
1050 + → Site 利用者が最終調整
1051 + ```
1052 +
1053 + という関係になります。
1054 +
1055 + 生成された Stylesheet を直接編集したり、Import 順を変更したりしないでください。
1056 +
1057 + # 27. Package として配布する
1058 +
1059 + 公開 Theme は、たとえば次の構成にできます。
1060 +
1061 + ```text id="c4cx40"
1062 + packages/themes/example/
1063 + ├─ src/
1064 + │ └─ index.ts
1065 + ├─ styles/
1066 + │ ├─ theme.css
1067 + │ └─ fonts/ # 必要な場合のみ
1068 + ├─ package.json
1069 + ├─ README_ja.md
1070 + └─ README.md
1071 + ```
1072 +
1073 + Riebeckite Repository 内では、
1074 +
1075 + ```text id="7s39yd"
1076 + packages/themes/minimal
1077 + ```
1078 +
1079 + が雛形になります。
1080 +
1081 + `src/index.ts` では Theme Factory を公開します。
1082 +
1083 + ```ts id="lhhz91"
1084 + import { defineTheme } from "@riebeckite/core";
1085 +
1086 + export function exampleTheme() {
1087 + return defineTheme({
1088 + name: "example",
1089 +
1090 + styles: [
1091 + {
1092 + moduleSpecifier:
1093 + "@riebeckite/theme-example/style.css",
1094 + },
1095 + ],
1096 + });
1097 + }
1098 + ```
1099 +
1100 + `package.json` では Stylesheet を、
1101 +
1102 + ```text id="8md3uw"
1103 + ./style.css
1104 + ```
1105 +
1106 + として Export します。
1107 +
1108 + # 28. Repository 外で Theme を配布する
1109 +
1110 + 外部 Theme Package は Riebeckite monorepo の内部構造へ依存させません。
1111 +
1112 + 基本的には、
1113 +
1114 + ```text id="a8r4we"
1115 + @riebeckite/core
1116 + ```
1117 +
1118 + の Public API だけを利用します。
1119 +
1120 + 次のような Internal Import は避けてください。
1121 +
1122 + ```ts id="0q5wfe"
1123 + import {
1124 + something,
1125 + } from "@riebeckite/core/src/...";
1126 + ```
1127 +
1128 + また、
1129 +
1130 + ```text id="mpy5js"
1131 + ../../../../packages/core/...
1132 + ```
1133 +
1134 + のような monorepo 内部 Path にも依存しません。
1135 +
1136 + Theme の Stylesheet も Package 自身の Export として公開します。
1137 +
1138 + # 29. Theme を検証する
1139 +
1140 + Theme を作成・変更したら、次の順番で確認します。
1141 +
1142 + ```mermaid id="muvx0f"
1143 + flowchart LR
1144 + Check["check"]
1145 + Inspect["inspect config"]
1146 + Dev["dev"]
1147 + Build["build"]
1148 +
1149 + Check --> Inspect
1150 + Inspect --> Dev
1151 + Dev --> Build
1152 + ```
1153 +
1154 + まず Configuration を確認します。
1155 +
1156 + ```sh id="cb8y30"
1157 + pnpm exec riebeckite check
1158 + ```
1159 +
1160 + 次に解決された Theme 設定を確認します。
1161 +
1162 + ```sh id="56lmdw"
1163 + pnpm exec riebeckite inspect config
1164 + ```
1165 +
1166 + 実際の表示を確認する場合は、
1167 +
1168 + ```sh id="i9vjkb"
1169 + pnpm exec riebeckite dev
1170 + ```
1171 +
1172 + を使います。
1173 +
1174 + 最後に生成物まで確認します。
1175 +
1176 + ```sh id="tsbdjv"
1177 + pnpm exec riebeckite build
1178 + ```
1179 +
1180 + `check`、`doctor`、`inspect` は Build Output を変更しません。
1181 +
1182 + Theme を交換しても、
1183 +
1184 + - Route
1185 + - Manifest
1186 + - Content Graph
1187 + - Client Behavior
1188 +
1189 + は変わらないことが基本です。
1190 +
1191 + # 30. 見た目がおかしい場合
1192 +
1193 + Theme が期待どおりに適用されない場合は、まず次の順番で確認します。
1194 +
1195 + ```mermaid id="pm3ukv"
1196 + flowchart TD
1197 + Start["Themeが適用されない"]
1198 +
1199 + Start --> Name{"data-theme-name は正しい?"}
1200 + Name -->|No| FixName["Theme nameを確認"]
1201 + Name -->|Yes| Hook{"正しいHookを対象にしている?"}
1202 +
1203 + Hook -->|No| FixHook["rb-* / rr-* を確認"]
1204 + Hook -->|Yes| Cascade{"Cascadeは正しい?"}
1205 +
1206 + Cascade -->|No| FixCascade["Plugin → Theme → userCssを確認"]
1207 + Cascade -->|Yes| Mode{"Color Mode条件は正しい?"}
1208 +
1209 + Mode -->|No| FixMode["data-theme / systemを確認"]
1210 + Mode -->|Yes| CSS["Selector / CSSを確認"]
1211 + ```
1212 +
1213 + 特に確認するのは、
1214 +
1215 + 1. `data-theme-name` が Theme の `name` と一致しているか
1216 + 2. `.rb-*` / `.rr-*` の正しい Stable Hook を対象にしているか
1217 + 3. Plugin CSS → Theme CSS → `userCss` の順になっているか
1218 + 4. `system` なのに `data-theme=""` が残っていないか
1219 + 5. Theme Root の外へ Selector が漏れていないか
1220 +
1221 + です。
1222 +
1223 + # 31. Theme を作るときの基本方針
1224 +
1225 + Theme の実装では、最終的に次の境界を維持することが重要です。
1226 +
1227 + ```mermaid id="sdf2dm"
1228 + flowchart LR
1229 + App["Application"]
1230 + Plugin["Plugin"]
1231 +
1232 + App --> Hooks["Stable Hooks"]
1233 + Plugin --> Hooks
1234 +
1235 + Core["Core"] --> Tokens["Semantic Tokens"]
1236 +
1237 + Hooks --> Contract["Presentation Contract"]
1238 + Tokens --> Contract
1239 +
1240 + Theme["Theme"] --> Contract
1241 +
1242 + Contract --> Site["Final Site"]
1243 + ```
1244 +
1245 + Theme は Application や Plugin の内部構造を所有しません。
1246 +
1247 + Framework と Plugin が公開した、
1248 +
1249 + ```text id="ysb8qa"
1250 + Stable CSS Hooks
1251 + Semantic Design Tokens
1252 + Theme Attributes
1253 + CSS Cascade
1254 + ```
1255 +
1256 + という Presentation Contract を利用します。
1257 +
1258 + Theme 固有の設定は Theme Package 内に閉じ込め、Core へ漏らしません。
1259 +
1260 + そして、Theme の変更によって、
1261 +
1262 + ```text id="e6c5sk"
1263 + Content
1264 + Route
1265 + Manifest
1266 + Content Graph
1267 + Plugin Behavior
1268 + Client Behavior
1269 + ```
1270 +
1271 + が変化しない状態を維持してください。
1272 +
1273 + **機能は Plugin、構造は Framework / Application、見た目は Theme**
1274 +
1275 + という境界を守ることで、Theme を交換しても同じ Site と Plugin をそのまま利用できます。
1276 +
1277 + ## 関連資料
1278 +
1279 + - [はじめてのテーマ作成](../themes/writing-a-theme.md) — 最初の Theme を作る
1280 + - [Theme System](./theme-system.md) — Theme System 全体の考え方
1281 + - [Plugin System](./plugin-system.md) — Plugin との責務の違い
1282 + - [Framework Reference](../reference/README.md) — `defineTheme` などの Public API
1283 +