Color mode

Theme System

Riebeckite Theme は、Site や Plugin の見た目を変更するための仕組みです。

Theme は機能そのものを差し替えるものではありません。

たとえば Theme では、

  • 色
  • フォント
  • 余白
  • レイアウトの見た目
  • Article や Sidebar の装飾
  • Plugin UI の見た目

などを変更できます。

一方で、

  • Content の意味
  • Route
  • Component の構造
  • Browser の動作
  • Plugin の機能

は変更しません。

Diagram source
text
flowchart LR
    Content["Content / Features"]
    Hooks["Stable Hooks<br/>Semantic Tokens"]
    Theme["Theme"]
    Result["Presentation"]
 
    Content --> Hooks
    Theme --> Hooks
    Hooks --> Result

この境界によって、Application や Plugin の機能を変更せずに Theme を交換できます。

Theme と Plugin の違い

迷った場合は、まず「機能を変えたいのか、見た目を変えたいのか」を考えます。

Diagram source
text
flowchart TD
    Q{"何を変更したい?"}
 
    Q -->|"見た目"| Theme["Theme"]
    Q -->|"機能"| Plugin["Plugin"]
 
    Theme --> T1["色"]
    Theme --> T2["Font"]
    Theme --> T3["Spacing"]
    Theme --> T4["Visual Layout"]
 
    Plugin --> P1["Content変換"]
    Plugin --> P2["Renderer"]
    Plugin --> P3["Browser Behavior"]
    Plugin --> P4["Endpoint"]

見た目だけを変更するために Plugin を作らず、機能を追加するために Theme を拡張しません。

最小の Theme

Theme は defineTheme() で作成します。

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

Site では theme に指定します。

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

これだけで Theme の stylesheet が Site に組み込まれます。

Theme が扱えるもの

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

分類 主な設定
Identity name
Theme 固有設定 options
CSS styles[].moduleSpecifier
Color Mode colorMode
Typography typography
Article Layout articleLayout
Design Token tokens
User CSS userCss
Theme 固有属性 data-* attributes

Theme はこれらを使って Presentation を変更します。

Design Tokens

Riebeckite では、Component や Plugin が特定 Theme の色を直接参照するのではなく、Semantic Design Token を利用します。

たとえば、

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

のように書きます。

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

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

Theme が変わったときに Plugin 側まで変更する必要が出てしまうためです。

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

Component は「文字色」という意味だけを参照し、実際の色は Theme が決めます。

Token の種類

Core の ThemeDesignTokens には、大きく3種類の Token があります。

Color

text
paper
ink
muted
accent
border
borderStrong
surface
surfaceHover
overlay
danger
success
codeBackground

Typography

text
bodyFont
headingFont
monoFont

Layout

text
pageMaxWidth
articleMaxWidth
sidebarWidth
contentGap

CSS では --rb-* Custom Property として利用します。

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

--rb-* は Framework が提供する Semantic Token です。

Plugin 固有の意味を持つ Token は --rr-* として Plugin 側が所有し、必要に応じて --rb-* を fallback として利用できます。

Color Mode

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

ts
type ThemeColorMode =
  | "light"
  | "dark"
  | "system";
Mode 動作
light Light Theme を使用
dark Dark Theme を使用
system OS の設定に従う

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

個別 Component に Light / Dark の色を直接 hardcode しないでください。

Color Mode の仕組み

Light、Dark、System は 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]) {
    /* ... */
  }
}

system の場合、data-theme を付けないことが重要です。

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

空文字を設定するのとは異なります。

html
<html data-theme="">

では [data-theme] に一致するため、System 用の Media Query が正しく機能しません。

実行時に system へ戻す場合も、

ts
delete document.documentElement.dataset.theme;

のように属性そのものを削除します。

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

Dark を表す CSS 機構はこの 2 つだけです。Theme Root 上の明示的な Dark である [data-theme="dark"] と、data-theme が無いときの @media (prefers-color-scheme: dark)(System)です。Framework は .dark class を付与しないため、.dark に依存した selector は Contract 外です。どちらの状態でも同じ --rb-* Semantic Token が解決されるため、Semantic Token だけを参照する Component は明示 Dark と System Dark を区別する必要がありません。

Typography

Theme は Typography Preset を提供できます。

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

Preset は、

  • Body
  • Heading
  • Code

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

Theme は個々の Component に Font を直接設定するのではなく、可能な限り Semantic Token を通して Typography を統一します。

Article Layout

Theme は Article Layout の Presentation を変更できます。

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

ただし Theme が変更するのはレイアウトの見た目です。

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

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

Theme Root

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

各 Theme は Theme Root の内側だけを対象にします。

基本 selector は次の形です。

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

<name> は Theme の name です。

たとえば、

text
riebeckite
minimal
gruvbox
sakura
tokyonight
rerurate

などです。

なぜ Theme Root が必要なのか

通常の Site では Application が <html> に Theme 名を設定します。

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

この場合は :root が一致します。

一方、Theme Gallery のように1ページで複数 Theme を表示したい場合があります。

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

同じ stylesheet を Preview 内でも利用できます。

Diagram source
text
flowchart TD
    CSS["Theme Stylesheet"]
 
    CSS --> Site["Real Site<br/>:root"]
    CSS --> PreviewA["Preview<br/>.rb-theme-root minimal"]
    CSS --> PreviewB["Preview<br/>.rb-theme-root gruvbox"]

このため Theme CSS を裸の :root に書かないことが重要です。

Theme Selector

Theme が定義するルールは 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"] {
  /* ... */
}
 
/* 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-tokyonight-neon="on"] {
  /* ... */
}

子要素や擬似要素も同じ Root に閉じ込めます。

css
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]
:focus-visible {
  /* ... */
}
 
:is(:root, .rb-theme-root)
[data-theme-name="<name>"]
::selection {
  /* ... */
}

これにより、Theme Preview の CSS がページの他の部分へ漏れることを防ぎます。

Styles

Theme の stylesheet は Host Bundler が解決できる Module Specifier として宣言します。

ts
styles: [
  {
    moduleSpecifier:
      "@riebeckite/theme-example/style.css",
  },
]

これは「CSS ファイルを Application directory へコピーする」という Contract ではありません。

Integration が Module Specifier を解決し、Site の stylesheet として組み込みます。

Site 内 Theme

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

Site 内だけで使う Theme も作れます。

ts
// site/extensions/local-theme.ts
 
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・適用されます。

Theme Attributes

Theme 固有の設定を 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 の namespace を分離します。

Theme Root と 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 に加えて次の予約属性を出力します。

  • data-theme: Color Mode の状態("light" / "dark" / "system" では省略)
  • 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>

ただし、予約済みの data-theme, data-theme-name, data-typography, data-article-layout は theme 側の attributes では上書きできません。

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

Theme 固有 Options

Theme 固有の機能は 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",
      },
    ],
  });
}

Theme 固有の概念は Core の ThemeConfig へ追加しません。

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

Core は個別 Theme の機能を知りません。

Stable CSS Hooks

Theme は Application や Plugin の内部 Markup に依存するのではなく、文書化された Stable CSS Hook を利用します。

Riebeckite では主に2つの namespace を使います。

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

Framework の代表的な Stable 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
.rb-site-header
.rb-nav
.rb-nav__list
.rb-nav__item
.rb-nav__link
.rb-nav__link--active
.rb-nav__children
.rb-nav__mobile
.rb-nav__toggle
.rb-site-footer

です。

.rb-article-content はレンダリングされた Markdown 本文の wrapper で、Markdown のセマンティックなベースライン(リストマーカーと字下げ、見出し、段落とブロックの余白、表、図、定義リスト、インラインコード、整形済みブロック)を持ちます。.rb-article-body はその wrapper を含む article body のシェルです。.rb-article-content を出力するのは公開 primitive の ArticleBody です。このベースラインはレイヤー化されているため、テーマが見た目を再宣言するのではなく、--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 を提供できます。

Diagram source
text
flowchart TD
    Theme["Theme CSS"]
 
    Theme --> Framework["rb-*<br/>Framework Hooks"]
    Theme --> Plugin["rr-*<br/>Plugin Hooks"]
 
    Framework --> Site["Site Presentation"]
    Plugin --> Site

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

Plugin 内部の Class

Plugin が BEM を使って、

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

のような Class を持つ場合があります。

基本的には、

text
.rr-search

が Theme 向けの Public Hook です。

__input や --loading のような内部 Class は、Plugin が明示的に文書化していない限り Implementation Detail として扱います。

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

Character Layer

Theme は Token の値を変更するだけではありません。

Stable Hook を利用して、Site に視覚的な個性を与えることもできます。

たとえば、

  • Article の Border
  • Header の装飾
  • Sidebar の背景
  • Code Block の形
  • Plugin Card の見た目

などです。

css
[data-theme-name="example"]
.rb-article {
  /* visual character */
}
 
[data-theme-name="example"]
.rr-callout {
  /* visual character */
}

ただし対象にするのは Stable Hook だけです。

新しい rb-* や rr-* Class を Theme 側で勝手に定義して、Framework Contract のように扱わないでください。

Character Layer も Presentation 専用です。

Content、Structure、Behavior を変更するものではありません。

CSS Layer

Token の定義は @layer base に置きます。

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

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

Application の Structural CSS や Plugin CSS も unlayered であるため、Theme CSS の読み込み順によって適切に上書きできます。

通常は !important を使用しません。

Font

Theme package は必要に応じて Self-hosted Web Font を含められます。

たとえば、

text
theme/
└─ styles/
   └─ fonts/

のように Theme package 内へ配置し、CSS の相対 url() で参照できます。

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

Latin subset のような比較的小さい Web Font は同梱できますが、日本語などの CJK Font はサイズが大きいため、基本的には System Font Stack へ fallback します。

CSS Cascade

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

Diagram source
text
flowchart TD
    Base["Base / Application<br/>Structural CSS"]
    Plugin["Plugin Default CSS"]
    Theme["Theme CSS"]
    Tokens["Config Token<br/>Inline Style"]
    User["userCss"]
 
    Base --> Plugin
    Plugin --> Theme
    Theme --> Tokens
    Tokens --> 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 順を入れ替えたりしないでください。

通常は !important に依存せず、この Cascade で Override します。

Theme がしてはいけないこと

Theme の責務は Presentation です。

そのため、次の処理は Theme に置きません。

  • Component Replacement
  • JSX Injection
  • Route の追加
  • Plugin の追加・削除
  • Client Script の実行
  • DOM Transformation
  • Island の登録
  • Filesystem Access
  • ContentManager Access
Diagram source
text
flowchart TD
    Feature{"Themeに置いてよい?"}
 
    Feature -->|"CSS / Token / Visual"| Yes["Theme"]
    Feature -->|"Content処理"| Plugin["Plugin / Core"]
    Feature -->|"Browser Behavior"| Client["Plugin / Application"]
    Feature -->|"Route / Structure"| App["Application"]
    Feature -->|"Filesystem / Content"| Core["Core / Content System"]

見た目を実現するために JavaScript や DOM 操作が必要になった場合、その部分は Theme ではなく Plugin または Application の責務です。

Theme Package の例

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

text
packages/themes/example/
├─ index.ts
├─ package.json
├─ style.css
├─ README_ja.md
└─ README.md

Theme が Font などを持つ場合は必要に応じて追加します。

text
packages/themes/example/
├─ index.ts
├─ package.json
├─ style.css
├─ styles/
│  └─ fonts/
├─ README_ja.md
└─ README.md

Repository 外で Theme を配布する

外部 Theme は Riebeckite monorepo 内部へ依存させません。

基本的には、

text
@riebeckite/core

の Public API だけに依存し、defineTheme() を使います。

Stylesheet は Theme package 自身の Export として公開します。

たとえば、

text
./style.css

を package.json の exports へ定義します。

monorepo 内部の path や、

text
@riebeckite/core/src/**

のような Internal API を参照しないでください。

Theme を交換できる理由

Theme System の最も重要な目的は、Application Logic を変更せずに Theme を交換できることです。

Diagram source
text
flowchart LR
    App["Application"]
    Contract["Stable Hooks<br/>Semantic Tokens"]
 
    ThemeA["Minimal"]
    ThemeB["Gruvbox"]
    ThemeC["Tokyo Night"]
 
    App --> Contract
 
    ThemeA --> Contract
    ThemeB --> Contract
    ThemeC --> Contract

Application と Plugin は、

text
Stable CSS Hooks
Semantic Design Tokens

という共通 Contract を提供します。

Theme はその Contract に対して CSS を適用します。

そのため Theme が変わっても、

  • Route
  • Content
  • Manifest
  • Content Graph
  • Plugin Behavior
  • Client Behavior

を変更する必要はありません。

Theme 固有 Option も、その Theme を選択したときだけ意味を持ち、Core や他の Theme には漏れません。

Theme System の基本

Theme System 全体は次のようになります。

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

Theme は Site の機能を所有するのではなく、Framework と Plugin が公開した Presentation Contract に対して見た目を与えます。

基本原則は、

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

です。

この境界を維持することで、Theme を自由に交換しながら、同じ Content、Plugin、Route、Application Logic をそのまま利用できます。

関連

History

1 changesCollapseExpand
1 + # Theme System
2 +
3 + Riebeckite Theme は、Site や Plugin の**見た目を変更するための仕組み**です。
4 +
5 + Theme は機能そのものを差し替えるものではありません。
6 +
7 + たとえば Theme では、
8 +
9 + - 色
10 + - フォント
11 + - 余白
12 + - レイアウトの見た目
13 + - Article や Sidebar の装飾
14 + - Plugin UI の見た目
15 +
16 + などを変更できます。
17 +
18 + 一方で、
19 +
20 + - Content の意味
21 + - Route
22 + - Component の構造
23 + - Browser の動作
24 + - Plugin の機能
25 +
26 + は変更しません。
27 +
28 + ```mermaid id="w4tgad"
29 + flowchart LR
30 + Content["Content / Features"]
31 + Hooks["Stable Hooks<br/>Semantic Tokens"]
32 + Theme["Theme"]
33 + Result["Presentation"]
34 +
35 + Content --> Hooks
36 + Theme --> Hooks
37 + Hooks --> Result
38 + ```
39 +
40 + この境界によって、Application や Plugin の機能を変更せずに Theme を交換できます。
41 +
42 + # Theme と Plugin の違い
43 +
44 + 迷った場合は、まず「機能を変えたいのか、見た目を変えたいのか」を考えます。
45 +
46 + ```mermaid id="8e1m3z"
47 + flowchart TD
48 + Q{"何を変更したい?"}
49 +
50 + Q -->|"見た目"| Theme["Theme"]
51 + Q -->|"機能"| Plugin["Plugin"]
52 +
53 + Theme --> T1["色"]
54 + Theme --> T2["Font"]
55 + Theme --> T3["Spacing"]
56 + Theme --> T4["Visual Layout"]
57 +
58 + Plugin --> P1["Content変換"]
59 + Plugin --> P2["Renderer"]
60 + Plugin --> P3["Browser Behavior"]
61 + Plugin --> P4["Endpoint"]
62 + ```
63 +
64 + **見た目だけを変更するために Plugin を作らず、機能を追加するために Theme を拡張しません。**
65 +
66 + # 最小の Theme
67 +
68 + Theme は `defineTheme()` で作成します。
69 +
70 + ```ts id="z0z78e"
71 + import { defineTheme } from "@riebeckite/core";
72 +
73 + export function minimalTheme() {
74 + return defineTheme({
75 + name: "minimal",
76 +
77 + styles: [
78 + {
79 + moduleSpecifier:
80 + "@riebeckite/theme-minimal/style.css",
81 + },
82 + ],
83 + });
84 + }
85 + ```
86 +
87 + Site では `theme` に指定します。
88 +
89 + ```ts id="ps7zv1"
90 + export default defineConfig({
91 + theme: minimalTheme(),
92 + });
93 + ```
94 +
95 + これだけで Theme の stylesheet が Site に組み込まれます。
96 +
97 + # Theme が扱えるもの
98 +
99 + Theme の主な Contract は次のとおりです。
100 +
101 + | 分類 | 主な設定 |
102 + | --- | --- |
103 + | Identity | `name` |
104 + | Theme 固有設定 | `options` |
105 + | CSS | `styles[].moduleSpecifier` |
106 + | Color Mode | `colorMode` |
107 + | Typography | `typography` |
108 + | Article Layout | `articleLayout` |
109 + | Design Token | `tokens` |
110 + | User CSS | `userCss` |
111 + | Theme 固有属性 | `data-*` attributes |
112 +
113 + Theme はこれらを使って Presentation を変更します。
114 +
115 + # Design Tokens
116 +
117 + Riebeckite では、Component や Plugin が特定 Theme の色を直接参照するのではなく、**Semantic Design Token** を利用します。
118 +
119 + たとえば、
120 +
121 + ```css id="vp9a0g"
122 + .rr-example {
123 + color: var(--rb-color-ink);
124 + background: var(--rb-color-surface);
125 + }
126 + ```
127 +
128 + のように書きます。
129 +
130 + 次のように Theme 固有の色を直接書くことは避けます。
131 +
132 + ```css id="ucsgx9"
133 + .rr-example {
134 + color: #171717;
135 + background: #f6efe2;
136 + }
137 + ```
138 +
139 + Theme が変わったときに Plugin 側まで変更する必要が出てしまうためです。
140 +
141 + ```mermaid id="n1td48"
142 + flowchart LR
143 + Component["Component / Plugin"]
144 + Token["Semantic Token<br/>--rb-color-ink"]
145 + ThemeA["Theme A<br/>#202020"]
146 + ThemeB["Theme B<br/>#d8dee9"]
147 +
148 + Component --> Token
149 + ThemeA --> Token
150 + ThemeB --> Token
151 + ```
152 +
153 + Component は「文字色」という意味だけを参照し、実際の色は Theme が決めます。
154 +
155 + # Token の種類
156 +
157 + Core の `ThemeDesignTokens` には、大きく3種類の Token があります。
158 +
159 + ## Color
160 +
161 + ```text id="hwz8k3"
162 + paper
163 + ink
164 + muted
165 + accent
166 + border
167 + borderStrong
168 + surface
169 + surfaceHover
170 + overlay
171 + danger
172 + success
173 + codeBackground
174 + ```
175 +
176 + ## Typography
177 +
178 + ```text id="8en3hl"
179 + bodyFont
180 + headingFont
181 + monoFont
182 + ```
183 +
184 + ## Layout
185 +
186 + ```text id="pwn80n"
187 + pageMaxWidth
188 + articleMaxWidth
189 + sidebarWidth
190 + contentGap
191 + ```
192 +
193 + CSS では `--rb-*` Custom Property として利用します。
194 +
195 + ```css id="m3xggn"
196 + @layer base {
197 + :is(:root, .rb-theme-root)
198 + [data-theme-name="minimal"] {
199 + --rb-color-paper: #fafafa;
200 + --rb-color-ink: #202020;
201 + --rb-color-accent: #555;
202 +
203 + --rb-font-body:
204 + system-ui, sans-serif;
205 +
206 + --rb-layout-article-max: 48rem;
207 + }
208 + }
209 + ```
210 +
211 + `--rb-*` は Framework が提供する Semantic Token です。
212 +
213 + Plugin 固有の意味を持つ Token は `--rr-*` として Plugin 側が所有し、必要に応じて `--rb-*` を fallback として利用できます。
214 +
215 + # Color Mode
216 +
217 + Theme は3種類の Color Mode を扱えます。
218 +
219 + ```ts id="csm5c6"
220 + type ThemeColorMode =
221 + | "light"
222 + | "dark"
223 + | "system";
224 + ```
225 +
226 + | Mode | 動作 |
227 + | --- | --- |
228 + | `light` | Light Theme を使用 |
229 + | `dark` | Dark Theme を使用 |
230 + | `system` | OS の設定に従う |
231 +
232 + Theme は `data-theme` と Semantic Token を使って配色を切り替えます。
233 +
234 + 個別 Component に Light / Dark の色を直接 hardcode しないでください。
235 +
236 + # Color Mode の仕組み
237 +
238 + Light、Dark、System は CSS 上では次の状態として扱います。
239 +
240 + ```css id="nw1xrv"
241 + /* Light */
242 + :is(:root, .rb-theme-root)
243 + [data-theme-name="<name>"] {
244 + /* ... */
245 + }
246 +
247 + /* Dark */
248 + :is(:root, .rb-theme-root)
249 + [data-theme-name="<name>"]
250 + [data-theme="dark"] {
251 + /* ... */
252 + }
253 +
254 + /* System */
255 + @media (prefers-color-scheme: dark) {
256 + :is(:root, .rb-theme-root)
257 + [data-theme-name="<name>"]
258 + :not([data-theme]) {
259 + /* ... */
260 + }
261 + }
262 + ```
263 +
264 + `system` の場合、`data-theme` を付けないことが重要です。
265 +
266 + ```text id="em0o5z"
267 + light
268 + → data-theme="light"
269 +
270 + dark
271 + → data-theme="dark"
272 +
273 + system
274 + → data-theme を削除
275 + ```
276 +
277 + 空文字を設定するのとは異なります。
278 +
279 + ```html id="7jpx2k"
280 + <html data-theme="">
281 + ```
282 +
283 + では `[data-theme]` に一致するため、System 用の Media Query が正しく機能しません。
284 +
285 + 実行時に `system` へ戻す場合も、
286 +
287 + ```ts id="0tw3aa"
288 + delete document.documentElement.dataset.theme;
289 + ```
290 +
291 + のように属性そのものを削除します。
292 +
293 + `@riebeckite/plugin-color-mode` がこの Contract の参照実装です。
294 +
295 + Dark を表す CSS 機構はこの 2 つだけです。Theme Root 上の明示的な Dark である `[data-theme="dark"]` と、`data-theme` が無いときの `@media (prefers-color-scheme: dark)`(System)です。Framework は `.dark` class を付与しないため、`.dark` に依存した selector は Contract 外です。どちらの状態でも同じ `--rb-*` Semantic Token が解決されるため、Semantic Token だけを参照する Component は明示 Dark と System Dark を区別する必要がありません。
296 +
297 + # Typography
298 +
299 + Theme は Typography Preset を提供できます。
300 +
301 + ```ts id="kyyomq"
302 + type ThemeTypographyPreset =
303 + | "system"
304 + | "serif"
305 + | "sans";
306 + ```
307 +
308 + Preset は、
309 +
310 + - Body
311 + - Heading
312 + - Code
313 +
314 + などの Semantic Font Token に反映されます。
315 +
316 + Theme は個々の Component に Font を直接設定するのではなく、可能な限り Semantic Token を通して Typography を統一します。
317 +
318 + # Article Layout
319 +
320 + Theme は Article Layout の Presentation を変更できます。
321 +
322 + ```ts id="c69dlz"
323 + type ThemeArticleLayoutPreset =
324 + | "article"
325 + | "sidebar"
326 + | "full-width";
327 + ```
328 +
329 + ただし Theme が変更するのは**レイアウトの見た目**です。
330 +
331 + Route や Component Tree 自体を Theme が差し替えるわけではありません。
332 +
333 + ```mermaid id="9k8dq8"
334 + flowchart LR
335 + App["Application<br/>Component Structure"]
336 + Hook["Stable Layout Hooks"]
337 + Theme["Theme<br/>Layout Presentation"]
338 +
339 + App --> Hook
340 + Theme --> Hook
341 + ```
342 +
343 + # Theme Root
344 +
345 + Theme の CSS は、Document 全体へ無条件に適用しません。
346 +
347 + 各 Theme は **Theme Root** の内側だけを対象にします。
348 +
349 + 基本 selector は次の形です。
350 +
351 + ```css id="l84p42"
352 + :is(:root, .rb-theme-root)
353 + [data-theme-name="<name>"]
354 + ```
355 +
356 + `<name>` は Theme の `name` です。
357 +
358 + たとえば、
359 +
360 + ```text id="09cf2y"
361 + riebeckite
362 + minimal
363 + gruvbox
364 + sakura
365 + tokyonight
366 + rerurate
367 + ```
368 +
369 + などです。
370 +
371 + # なぜ Theme Root が必要なのか
372 +
373 + 通常の Site では Application が `<html>` に Theme 名を設定します。
374 +
375 + ```html id="4v4fpo"
376 + <html data-theme-name="minimal">
377 + ```
378 +
379 + この場合は `:root` が一致します。
380 +
381 + 一方、Theme Gallery のように1ページで複数 Theme を表示したい場合があります。
382 +
383 + ```html id="5yyh67"
384 + <div
385 + class="rb-theme-root"
386 + data-theme-name="minimal"
387 + >
388 + ...
389 + </div>
390 +
391 + <div
392 + class="rb-theme-root"
393 + data-theme-name="gruvbox"
394 + >
395 + ...
396 + </div>
397 + ```
398 +
399 + 同じ stylesheet を Preview 内でも利用できます。
400 +
401 + ```mermaid id="vmzpsr"
402 + flowchart TD
403 + CSS["Theme Stylesheet"]
404 +
405 + CSS --> Site["Real Site<br/>:root"]
406 + CSS --> PreviewA["Preview<br/>.rb-theme-root minimal"]
407 + CSS --> PreviewB["Preview<br/>.rb-theme-root gruvbox"]
408 + ```
409 +
410 + このため Theme CSS を裸の `:root` に書かないことが重要です。
411 +
412 + # Theme Selector
413 +
414 + Theme が定義するルールは Theme Root の内側に限定します。
415 +
416 + ```css id="33dppk"
417 + /* Light */
418 + :is(:root, .rb-theme-root)
419 + [data-theme-name="<name>"] {
420 + /* ... */
421 + }
422 +
423 + /* Dark */
424 + :is(:root, .rb-theme-root)
425 + [data-theme-name="<name>"]
426 + [data-theme="dark"] {
427 + /* ... */
428 + }
429 +
430 + /* Typography */
431 + :is(:root, .rb-theme-root)
432 + [data-theme-name="<name>"]
433 + [data-typography="serif"] {
434 + /* ... */
435 + }
436 +
437 + /* Theme option */
438 + :is(:root, .rb-theme-root)
439 + [data-theme-name="<name>"]
440 + [data-tokyonight-neon="on"] {
441 + /* ... */
442 + }
443 + ```
444 +
445 + 子要素や擬似要素も同じ Root に閉じ込めます。
446 +
447 + ```css id="fepgnc"
448 + :is(:root, .rb-theme-root)
449 + [data-theme-name="<name>"]
450 + :focus-visible {
451 + /* ... */
452 + }
453 +
454 + :is(:root, .rb-theme-root)
455 + [data-theme-name="<name>"]
456 + ::selection {
457 + /* ... */
458 + }
459 + ```
460 +
461 + これにより、Theme Preview の CSS がページの他の部分へ漏れることを防ぎます。
462 +
463 + # Styles
464 +
465 + Theme の stylesheet は Host Bundler が解決できる Module Specifier として宣言します。
466 +
467 + ```ts id="kz2hpi"
468 + styles: [
469 + {
470 + moduleSpecifier:
471 + "@riebeckite/theme-example/style.css",
472 + },
473 + ]
474 + ```
475 +
476 + これは「CSS ファイルを Application directory へコピーする」という Contract ではありません。
477 +
478 + Integration が Module Specifier を解決し、Site の stylesheet として組み込みます。
479 +
480 + # Site 内 Theme
481 +
482 + Theme は npm package として公開しなくても利用できます。
483 +
484 + Site 内だけで使う Theme も作れます。
485 +
486 + ```ts id="pzvj0n"
487 + // site/extensions/local-theme.ts
488 +
489 + return defineTheme({
490 + name: "site-local",
491 +
492 + styles: [
493 + {
494 + moduleSpecifier:
495 + "/extensions/theme.css",
496 + },
497 + ],
498 +
499 + attributes: {
500 + "data-site-local": "on",
501 + },
502 + });
503 + ```
504 +
505 + Site-local Theme でも、
506 +
507 + - `name`
508 + - `styles`
509 + - `attributes`
510 + - `tokens`
511 +
512 + は Published Theme と同じ `resolveThemeConfig` の仕組みで解決・sanitize・適用されます。
513 +
514 + # Theme Attributes
515 +
516 + Theme 固有の設定を CSS へ渡す場合は、安全な `data-*` Attribute を利用できます。
517 +
518 + ```ts id="dhy9s4"
519 + return defineTheme({
520 + name: "newspaper",
521 +
522 + attributes: {
523 + "data-newspaper-density":
524 + "compact",
525 + },
526 + });
527 + ```
528 +
529 + CSS では、
530 +
531 + ```css id="ym84dm"
532 + :is(:root, .rb-theme-root)
533 + [data-theme-name="newspaper"]
534 + [data-newspaper-density="compact"] {
535 + /* ... */
536 + }
537 + ```
538 +
539 + のように利用できます。
540 +
541 + Theme API から、
542 +
543 + ```text id="5vgpr4"
544 + class
545 + style
546 + id
547 + lang
548 + ```
549 +
550 + などを任意に変更する設計にはしません。
551 +
552 + Framework が所有する Attribute と Theme 固有 Attribute の namespace を分離します。
553 +
554 + # Theme Root と themeRootAttributes
555 +
556 + Framework は `ThemeRoot` という UI primitive を提供し、`<html>` 要素への theme 属性の付与を担当します。
557 +
558 + ```tsx
559 + import { ThemeRoot } from "@riebeckite/honox/ui";
560 +
561 + <ThemeRoot
562 + theme={config.theme}
563 + lang={c.get("htmlLanguage") ?? config.site.locale}
564 + >
565 + {children}
566 + </ThemeRoot>
567 + ```
568 +
569 + `ThemeRoot` は次の属性を持つ `<html>` 要素を描画します。
570 +
571 + ```html
572 + <html
573 + lang="ja"
574 + data-theme-name="minimal"
575 + data-theme="dark"
576 + data-typography="system"
577 + data-article-layout="article"
578 + >
579 + ```
580 +
581 + Framework は `themeRootAttributes(theme)` を使い、theme 自身の `attributes` に加えて次の予約属性を出力します。
582 +
583 + - `data-theme`: Color Mode の状態(`"light"` / `"dark"` / `"system"` では省略)
584 + - `data-theme-name`: Theme の識別子
585 + - `data-typography`: Typography Preset の値
586 + - `data-article-layout`: Article Layout Preset の値
587 +
588 + 独自の `<html>` 属性を追加したい場合は、`ThemeRoot` の代わりに `themeRootAttributes` を直接使えます。
589 +
590 + ```tsx
591 + import { themeRootAttributes } from "@riebeckite/honox/ui";
592 +
593 + <html
594 + lang={c.get("htmlLanguage") ?? config.site.locale}
595 + {...themeRootAttributes(config.theme)}
596 + data-custom-attr="..."
597 + >
598 + ...
599 + </html>
600 + ```
601 +
602 + ただし、予約済みの `data-theme`, `data-theme-name`, `data-typography`, `data-article-layout` は theme 側の `attributes` では上書きできません。
603 +
604 + plugin や theme が独自に `themeAttributes()` を実装していた場合は、Framework が提供する `ThemeRoot` / `themeRootAttributes()` への移行を検討してください。Framework が所有する namespace と theme 固有の namespace を明確に分離できます。
605 +
606 + # Theme 固有 Options
607 +
608 + Theme 固有の機能は Theme Factory の Option として定義します。
609 +
610 + ```ts id="i7yem2"
611 + type NewspaperOptions = {
612 + density?:
613 + | "compact"
614 + | "comfortable";
615 + };
616 +
617 + export function newspaperTheme(
618 + options: NewspaperOptions = {},
619 + ) {
620 + return defineTheme({
621 + name: "newspaper",
622 +
623 + options,
624 +
625 + attributes: {
626 + "data-newspaper-density":
627 + options.density ?? "comfortable",
628 + },
629 +
630 + styles: [
631 + {
632 + moduleSpecifier:
633 + "@riebeckite/theme-newspaper/style.css",
634 + },
635 + ],
636 + });
637 + }
638 + ```
639 +
640 + Theme 固有の概念は Core の `ThemeConfig` へ追加しません。
641 +
642 + ```text id="1etjdh"
643 + newspaperTheme の density
644 + → newspaperTheme が所有
645 +
646 + tokyonightTheme の neon
647 + → tokyonightTheme が所有
648 + ```
649 +
650 + Core は個別 Theme の機能を知りません。
651 +
652 + # Stable CSS Hooks
653 +
654 + Theme は Application や Plugin の内部 Markup に依存するのではなく、文書化された **Stable CSS Hook** を利用します。
655 +
656 + Riebeckite では主に2つの namespace を使います。
657 +
658 + | Namespace | 所有者 | 用途 |
659 + | --- | --- | --- |
660 + | `rb-*` | Framework | Site の構造 |
661 + | `rr-*` | Plugin / Feature | Plugin UI |
662 +
663 + Framework の代表的な Stable Hook は、
664 +
665 + ```text id="fahf95"
666 + .rb-theme-root
667 + .rb-site
668 + .rb-article
669 + .rb-article-layout
670 + .rb-article-header
671 + .rb-article-body
672 + .rb-article-content
673 + .rb-article-meta
674 + .rb-article-footer
675 + .rb-sidebar
676 + .rb-site-header
677 + .rb-nav
678 + .rb-nav__list
679 + .rb-nav__item
680 + .rb-nav__link
681 + .rb-nav__link--active
682 + .rb-nav__children
683 + .rb-nav__mobile
684 + .rb-nav__toggle
685 + .rb-site-footer
686 + ```
687 +
688 + です。
689 +
690 + `.rb-article-content` はレンダリングされた Markdown 本文の wrapper で、Markdown のセマンティックなベースライン(リストマーカーと字下げ、見出し、段落とブロックの余白、表、図、定義リスト、インラインコード、整形済みブロック)を持ちます。`.rb-article-body` はその wrapper を含む article body のシェルです。`.rb-article-content` を出力するのは公開 primitive の `ArticleBody` です。このベースラインはレイヤー化されているため、テーマが見た目を再宣言するのではなく、`--rb-*` トークンとレイヤー外のキャラクター規則で表現します。
691 +
692 + Plugin では、
693 +
694 + ```text id="jvm00r"
695 + .rr-search
696 + .rr-callout
697 + .rr-table-of-contents
698 + .rr-backlinks
699 + .rr-local-graph
700 + .rr-code
701 + .rr-code-tabs
702 + .rr-lightbox
703 + .rr-excalidraw
704 + .rr-mermaid
705 + .rr-query
706 + .rr-cardlink
707 + .rr-diff-history
708 + .rr-attachment
709 + .rr-media
710 + .rr-recent-posts
711 + .rr-garden-explorer
712 + ```
713 +
714 + などの Root Hook を提供できます。
715 +
716 + ```mermaid id="k4vqej"
717 + flowchart TD
718 + Theme["Theme CSS"]
719 +
720 + Theme --> Framework["rb-*<br/>Framework Hooks"]
721 + Theme --> Plugin["rr-*<br/>Plugin Hooks"]
722 +
723 + Framework --> Site["Site Presentation"]
724 + Plugin --> Site
725 + ```
726 +
727 + Theme はこの Stable Hook を対象にします。
728 +
729 + # Plugin 内部の Class
730 +
731 + Plugin が BEM を使って、
732 +
733 + ```text id="cf1uz0"
734 + .rr-search
735 + .rr-search__input
736 + .rr-search__result
737 + .rr-search--loading
738 + ```
739 +
740 + のような Class を持つ場合があります。
741 +
742 + 基本的には、
743 +
744 + ```text id="r2p6sm"
745 + .rr-search
746 + ```
747 +
748 + が Theme 向けの Public Hook です。
749 +
750 + `__input` や `--loading` のような内部 Class は、Plugin が明示的に文書化していない限り Implementation Detail として扱います。
751 +
752 + `.sr-only` のような一般的な Helper Class も Plugin Hook ではありません。
753 +
754 + # Character Layer
755 +
756 + Theme は Token の値を変更するだけではありません。
757 +
758 + Stable Hook を利用して、Site に視覚的な個性を与えることもできます。
759 +
760 + たとえば、
761 +
762 + - Article の Border
763 + - Header の装飾
764 + - Sidebar の背景
765 + - Code Block の形
766 + - Plugin Card の見た目
767 +
768 + などです。
769 +
770 + ```css id="0r1pvg"
771 + [data-theme-name="example"]
772 + .rb-article {
773 + /* visual character */
774 + }
775 +
776 + [data-theme-name="example"]
777 + .rr-callout {
778 + /* visual character */
779 + }
780 + ```
781 +
782 + ただし対象にするのは Stable Hook だけです。
783 +
784 + 新しい `rb-*` や `rr-*` Class を Theme 側で勝手に定義して、Framework Contract のように扱わないでください。
785 +
786 + Character Layer も Presentation 専用です。
787 +
788 + Content、Structure、Behavior を変更するものではありません。
789 +
790 + # CSS Layer
791 +
792 + Token の定義は `@layer base` に置きます。
793 +
794 + ```css id="yzyq6g"
795 + @layer base {
796 + :is(:root, .rb-theme-root)
797 + [data-theme-name="example"] {
798 + --rb-color-paper: #fff;
799 + --rb-color-ink: #111;
800 + }
801 + }
802 + ```
803 +
804 + 一方、Stable Hook に対する Theme の Character Rule は **unlayered** にします。
805 +
806 + Application の Structural CSS や Plugin CSS も unlayered であるため、Theme CSS の読み込み順によって適切に上書きできます。
807 +
808 + 通常は `!important` を使用しません。
809 +
810 + # Font
811 +
812 + Theme package は必要に応じて Self-hosted Web Font を含められます。
813 +
814 + たとえば、
815 +
816 + ```text id="mtpf4x"
817 + theme/
818 + └─ styles/
819 + └─ fonts/
820 + ```
821 +
822 + のように Theme package 内へ配置し、CSS の相対 `url()` で参照できます。
823 +
824 + Font を同梱する場合は License File も含めてください。
825 +
826 + Latin subset のような比較的小さい Web Font は同梱できますが、日本語などの CJK Font はサイズが大きいため、基本的には System Font Stack へ fallback します。
827 +
828 + # CSS Cascade
829 +
830 + Riebeckite では CSS の読み込み順も Contract の一部です。
831 +
832 + ```mermaid id="13bftb"
833 + flowchart TD
834 + Base["Base / Application<br/>Structural CSS"]
835 + Plugin["Plugin Default CSS"]
836 + Theme["Theme CSS"]
837 + Tokens["Config Token<br/>Inline Style"]
838 + User["userCss"]
839 +
840 + Base --> Plugin
841 + Plugin --> Theme
842 + Theme --> Tokens
843 + Tokens --> User
844 + ```
845 +
846 + 優先順は、
847 +
848 + ```text id="qq3vby"
849 + Framework Structural CSS
850 + ↓
851 + Base / Application CSS
852 + ↓
853 + Plugin Default CSS
854 + ↓
855 + Theme CSS
856 + ↓
857 + Config Token Inline Style
858 + ↓
859 + userCss
860 + ```
861 +
862 + です。
863 +
864 + これは偶然の読み込み順ではなく、Presentation Extension の Contract です。
865 +
866 + `@riebeckite/honox` は、
867 +
868 + ```text id="49ck0i"
869 + .riebeckite/framework-styles.css
870 + .riebeckite/plugin-styles.css
871 + .riebeckite/theme-styles.css
872 + ```
873 +
874 + を生成します。
875 +
876 + Site は Framework Stylesheet を Plugin Stylesheet より先に、Plugin Stylesheet を Theme Stylesheet より先に読み込みます。
877 +
878 + そのため、
879 +
880 + ```text id="qzwp1u"
881 + Plugin
882 + → 標準の見た目
883 +
884 + Theme
885 + → Plugin の見た目を変更
886 +
887 + userCss
888 + → Site 利用者が最終調整
889 + ```
890 +
891 + という関係になります。
892 +
893 + 生成された stylesheet を直接編集したり、Import 順を入れ替えたりしないでください。
894 +
895 + 通常は `!important` に依存せず、この Cascade で Override します。
896 +
897 + # Theme がしてはいけないこと
898 +
899 + Theme の責務は Presentation です。
900 +
901 + そのため、次の処理は Theme に置きません。
902 +
903 + - Component Replacement
904 + - JSX Injection
905 + - Route の追加
906 + - Plugin の追加・削除
907 + - Client Script の実行
908 + - DOM Transformation
909 + - Island の登録
910 + - Filesystem Access
911 + - ContentManager Access
912 +
913 + ```mermaid id="qpcxoz"
914 + flowchart TD
915 + Feature{"Themeに置いてよい?"}
916 +
917 + Feature -->|"CSS / Token / Visual"| Yes["Theme"]
918 + Feature -->|"Content処理"| Plugin["Plugin / Core"]
919 + Feature -->|"Browser Behavior"| Client["Plugin / Application"]
920 + Feature -->|"Route / Structure"| App["Application"]
921 + Feature -->|"Filesystem / Content"| Core["Core / Content System"]
922 + ```
923 +
924 + 見た目を実現するために JavaScript や DOM 操作が必要になった場合、その部分は Theme ではなく Plugin または Application の責務です。
925 +
926 + # Theme Package の例
927 +
928 + 公開 Theme は、たとえば次のように構成できます。
929 +
930 + ```text id="4hx4zz"
931 + packages/themes/example/
932 + ├─ index.ts
933 + ├─ package.json
934 + ├─ style.css
935 + ├─ README_ja.md
936 + └─ README.md
937 + ```
938 +
939 + Theme が Font などを持つ場合は必要に応じて追加します。
940 +
941 + ```text id="hs5y9a"
942 + packages/themes/example/
943 + ├─ index.ts
944 + ├─ package.json
945 + ├─ style.css
946 + ├─ styles/
947 + │ └─ fonts/
948 + ├─ README_ja.md
949 + └─ README.md
950 + ```
951 +
952 + # Repository 外で Theme を配布する
953 +
954 + 外部 Theme は Riebeckite monorepo 内部へ依存させません。
955 +
956 + 基本的には、
957 +
958 + ```text id="txlfzo"
959 + @riebeckite/core
960 + ```
961 +
962 + の Public API だけに依存し、`defineTheme()` を使います。
963 +
964 + Stylesheet は Theme package 自身の Export として公開します。
965 +
966 + たとえば、
967 +
968 + ```text id="uwoc8f"
969 + ./style.css
970 + ```
971 +
972 + を `package.json` の `exports` へ定義します。
973 +
974 + monorepo 内部の path や、
975 +
976 + ```text id="v9czqn"
977 + @riebeckite/core/src/**
978 + ```
979 +
980 + のような Internal API を参照しないでください。
981 +
982 + # Theme を交換できる理由
983 +
984 + Theme System の最も重要な目的は、**Application Logic を変更せずに Theme を交換できること**です。
985 +
986 + ```mermaid id="jfsn6x"
987 + flowchart LR
988 + App["Application"]
989 + Contract["Stable Hooks<br/>Semantic Tokens"]
990 +
991 + ThemeA["Minimal"]
992 + ThemeB["Gruvbox"]
993 + ThemeC["Tokyo Night"]
994 +
995 + App --> Contract
996 +
997 + ThemeA --> Contract
998 + ThemeB --> Contract
999 + ThemeC --> Contract
1000 + ```
1001 +
1002 + Application と Plugin は、
1003 +
1004 + ```text id="s1q47e"
1005 + Stable CSS Hooks
1006 + Semantic Design Tokens
1007 + ```
1008 +
1009 + という共通 Contract を提供します。
1010 +
1011 + Theme はその Contract に対して CSS を適用します。
1012 +
1013 + そのため Theme が変わっても、
1014 +
1015 + - Route
1016 + - Content
1017 + - Manifest
1018 + - Content Graph
1019 + - Plugin Behavior
1020 + - Client Behavior
1021 +
1022 + を変更する必要はありません。
1023 +
1024 + Theme 固有 Option も、その Theme を選択したときだけ意味を持ち、Core や他の Theme には漏れません。
1025 +
1026 + # Theme System の基本
1027 +
1028 + Theme System 全体は次のようになります。
1029 +
1030 + ```mermaid id="2hw1zc"
1031 + flowchart LR
1032 + App["Application"]
1033 + Plugins["Plugins"]
1034 +
1035 + App --> Hooks["Stable Hooks"]
1036 + Plugins --> Hooks
1037 +
1038 + Core["Core"] --> Tokens["Semantic Tokens"]
1039 +
1040 + Hooks --> Presentation["Presentation Contract"]
1041 + Tokens --> Presentation
1042 +
1043 + Theme["Theme"] --> Presentation
1044 +
1045 + Presentation --> Site["Final Site"]
1046 + ```
1047 +
1048 + Theme は Site の機能を所有するのではなく、Framework と Plugin が公開した **Presentation Contract** に対して見た目を与えます。
1049 +
1050 + 基本原則は、
1051 +
1052 + **機能は Plugin / Application、構造は Framework / Application、見た目は Theme**
1053 +
1054 + です。
1055 +
1056 + この境界を維持することで、Theme を自由に交換しながら、同じ Content、Plugin、Route、Application Logic をそのまま利用できます。
1057 +
1058 + ## 関連
1059 +
1060 + - [Architecture](../framework/architecture.md)
1061 + - [Plugin System](./plugin-api.md)
1062 + - [Configuration](./configuration.md)
1063 + - [Framework Reference](./README.md)
1064 +