HyperGen

テーマ設定

CSSカスタムプロパティでHyperGen UIにスタイルを適用。ライトテーマとダークテーマ、動的切り替え、テーマ対応HTMLを生成するエージェント向けのヒント。

テーマ設定

HyperGenは--hg-*プレフィックスのCSSカスタムプロパティ(変数)を使用して、ホストアプリケーションのデザインシステムをサンドボックス化されたiframeにブリッジします。ホストはpostMessage経由でテーマ変数をiframeに送信し、エージェント生成のHTMLはそれらの変数を参照して統一されたスタイリングを実現します。

仕組み

  1. ホストアプリケーション--hg-* CSS変数のセットを定義
  2. mountHyperGen()がそれらをhg:theme postMessageとしてiframeに送信
  3. iframeのブリッジスクリプトがそれらを:rootに適用
  4. エージェント生成のHTMLがCSSでvar(--hg-*)を使用
  5. ホストがテーマを更新すると、iframeも即座に更新

つまり、エージェントはホストがどの色やフォントを使っているかを知る必要がありません。エージェントはvar(--hg-accent)でHTMLを生成し、ホストが--hg-accentの実際の見た目を決定します。

CSS変数リファレンス

カラー

変数目的デフォルト
--hg-surfaceプライマリ背景サーフェス#ffffff
--hg-surface-elevated浮き出たサーフェス(カード、ポップオーバー)#f9fafb
--hg-textプライマリテキスト色#111827
--hg-text-mutedセカンダリ / ミュートテキスト#6b7280
--hg-accentアクセント / ブランドカラー(ボタン、リンク)#7c3aed
--hg-accent-fgアクセント背景上の前景色#ffffff
--hg-borderボーダーとセパレーター#e5e7eb

タイポグラフィ

変数目的デフォルト
--hg-font-familyベースフォントファミリーsystem-ui, -apple-system, sans-serif
--hg-font-mono等幅フォントファミリーui-monospace, monospace
--hg-font-sizeベースフォントサイズ16px
--hg-line-heightベース行の高さ1.5

スペーシング

変数目的デフォルト
--hg-space-1極小スペーシング4px
--hg-space-2小スペーシング8px
--hg-space-4中スペーシング16px
--hg-space-8大スペーシング32px

シェイプ

変数目的デフォルト
--hg-radiusデフォルトボーダー半径8px
--hg-radius-sm小ボーダー半径4px
--hg-radius-lg大ボーダー半径12px

慣例により拡張可能

これらは標準変数です。エージェントとホストは、必要に応じて追加の--hg-*変数(例: --hg-error--hg-success--hg-warning)を合意できます。プロトコルは閉じたセットを強制しません。

ライトテーマの例

controller.setTheme({
  "--hg-surface": "#ffffff",
  "--hg-surface-elevated": "#f9fafb",
  "--hg-text": "#111827",
  "--hg-text-muted": "#6b7280",
  "--hg-accent": "#7c3aed",
  "--hg-accent-fg": "#ffffff",
  "--hg-border": "#e5e7eb",
  "--hg-font-family": "system-ui, -apple-system, sans-serif",
  "--hg-font-mono": "ui-monospace, monospace",
  "--hg-font-size": "16px",
  "--hg-line-height": "1.5",
  "--hg-space-1": "4px",
  "--hg-space-2": "8px",
  "--hg-space-4": "16px",
  "--hg-space-8": "32px",
  "--hg-radius": "8px",
  "--hg-radius-sm": "4px",
  "--hg-radius-lg": "12px",
});

ダークテーマの例

controller.setTheme({
  "--hg-surface": "#111827",
  "--hg-surface-elevated": "#1f2937",
  "--hg-text": "#f9fafb",
  "--hg-text-muted": "#9ca3af",
  "--hg-accent": "#818cf8",
  "--hg-accent-fg": "#ffffff",
  "--hg-border": "#374151",
  "--hg-font-family": "system-ui, -apple-system, sans-serif",
  "--hg-font-mono": "ui-monospace, monospace",
  "--hg-font-size": "16px",
  "--hg-line-height": "1.5",
  "--hg-space-1": "4px",
  "--hg-space-2": "8px",
  "--hg-space-4": "16px",
  "--hg-space-8": "32px",
  "--hg-radius": "8px",
  "--hg-radius-sm": "4px",
  "--hg-radius-lg": "12px",
});

動的テーマ切り替え

ランタイムでライトテーマとダークテーマを切り替え:

const themes = {
  light: {
    "--hg-surface": "#ffffff",
    "--hg-surface-elevated": "#f9fafb",
    "--hg-text": "#111827",
    "--hg-text-muted": "#6b7280",
    "--hg-accent": "#7c3aed",
    "--hg-accent-fg": "#ffffff",
    "--hg-border": "#e5e7eb",
  },
  dark: {
    "--hg-surface": "#111827",
    "--hg-surface-elevated": "#1f2937",
    "--hg-text": "#f9fafb",
    "--hg-text-muted": "#9ca3af",
    "--hg-accent": "#818cf8",
    "--hg-accent-fg": "#ffffff",
    "--hg-border": "#374151",
  },
};

let isDark = false;

document.getElementById("theme-toggle")!.addEventListener("click", () => {
  isDark = !isDark;
  controller.setTheme(isDark ? themes.dark : themes.light);
});

切り替えは瞬時です — setTheme()がpostMessageを送信し、iframeのブリッジスクリプトが即座に:rootスタイルを更新します。スムーズな遷移が必要な場合は、iframeのextraHead CSSにtransitionルールを追加します:

bootstrapHtml({
  sseEndpoint: "/api/stream",
  extraHead: `<style>
    body { transition: background 0.3s, color 0.3s; }
    .card { transition: background 0.3s, border-color 0.3s; }
  </style>`,
});

サーバーサイドでの初期テーマ

bootstrapHtml()themeVarsを使用して、サーバーサイドで初期テーマを設定できます。これにより、クライアントサイドのsetTheme()呼び出し前のスタイル未適用のちらつきを回避できます:

const html = bootstrapHtml({
  sseEndpoint: "/api/stream",
  themeVars: {
    "--hg-surface": "#111827",
    "--hg-text": "#f9fafb",
    "--hg-accent": "#818cf8",
  },
});

これらの変数はブートストラップドキュメントの<style>ブロックにCSS宣言として注入されるため、JavaScriptが実行される前に利用可能です。

型安全のためのhypergen-theme.ts

オプションのhypergen-theme.tsファイルは、テーマ変数を扱うためのTypeScript型とユーティリティを提供します:

import { defaultTheme, serializeTheme, type HyperGenTheme } from "./hypergen-theme";

// defaultThemeはすべての標準変数とデフォルト値を持つ
console.log(defaultTheme["--hg-accent"]); // "#7c3aed"

// 型安全なテーマ構築
const myTheme: Partial<HyperGenTheme> = {
  "--hg-accent": "#10b981",
  "--hg-surface": "#0f172a",
};

// CSS文字列にシリアライズ(インラインスタイルに便利)
const css = serializeTheme(myTheme);
// "--hg-accent: #10b981; --hg-surface: #0f172a"

HyperGenThemeインターフェースは、すべてのキーが--hg-で始まることを強制し、標準変数の完全なセットをカバーします。サブセットのみをオーバーライドする場合はPartial<HyperGenTheme>を使用してください。

テーマ対応HTMLを生成するエージェント向けのヒント

エージェントが生成するHTMLを構築する際は、以下のガイドラインに従ってください:

視覚プロパティには必ずCSS変数を使用

<!-- 良い例: 任意のテーマに適応 -->
<div style="
  background: var(--hg-surface-elevated);
  color: var(--hg-text);
  border: 1px solid var(--hg-border);
  border-radius: var(--hg-radius);
  padding: var(--hg-space-4);
">
  Content here
</div>

<!-- 悪い例: ハードコードされた色はホストテーマと合わない -->
<div style="background: #f9fafb; color: #111827; border: 1px solid #e5e7eb;">
  Content here
</div>

フォールバック値を提供

CSSのvar()は第2引数としてフォールバックを受け取ります。ホストがテーマを送信しなくてもUIが動作するよう、妥当なデフォルトを含めてください:

<p style="color: var(--hg-text, #1a1a1a);">
  This text has a fallback color.
</p>

生の色ではなくセマンティック変数を使用

--hg-primary-600よりも--hg-accentを推奨します。セマンティック名はライトテーマとダークテーマの両方で調整なしに動作します。

エージェント生成のスタイルはインラインまたはフラグメント内の<style>ブロックで

各ストリーミングフラグメントは自己完結すべきです。インラインstyle属性を使用するか、フラグメント内に<style>ブロックを含めてください:

<style>
  .agent-card {
    background: var(--hg-surface-elevated);
    border: 1px solid var(--hg-border);
    border-radius: var(--hg-radius);
    padding: var(--hg-space-4);
  }
  .agent-card h3 { color: var(--hg-text); }
  .agent-card p { color: var(--hg-text-muted); }
</style>
<div class="agent-card">
  <h3>Result</h3>
  <p>Generated by the agent</p>
</div>

目次