テーマとレイアウト
ユーザーはトップバーでライト / ダークモードを切り替えられるほか、「外観設定」パネルでアクセントカラー、ナビゲーションモード、サイドバーのスタイル、コンテンツ幅、タブバーを有効にするかどうかを選べます。選択内容はすぐに反映され、現在のブラウザに保存されます。
このページでは、これらのオプションの実装方法と、それらがページのコードに求める要件を説明します。
ライト / ダーク
<html class="dark">で切り替えます。shadcn / Tailwind の慣例に従っています。- ユーザーが選んだモードは
localStorageのthemeキーに保存されます。まだ選んでいない間はシステムの設定に従い、システムの設定が変わると追従します。 - 切り替えの瞬間は 1 フレームだけすべての CSS トランジションを無効にし(
<html>のtheme-switchingクラス)、hover や色のトランジションが新旧の色の間でアニメーションせず、ページが一度に切り替わるようにしています。
実装は apps/web/src/context/ThemeContext.tsx にあります。ページでセマンティックカラークラスを使っていれば(フロントエンド を参照)、ダークモードは自動で正しく表示されます。
アクセントカラー
外観設定には 6 種類のアクセントカラーがあり、デフォルトはオーシャンです。
| ID | 名前 |
|---|---|
ocean | オーシャン(デフォルト) |
violet | バイオレット |
emerald | エメラルド |
rose | ローズ |
amber | アンバー |
slate | スレート |
トークンの派生方法
アクセントカラーは <html data-accent="<id>"> の形で適用されます。apps/web/src/index.css では、各プリセットが 6 つの変数を、ライトとダークでそれぞれ 1 組ずつ設定しています。
[data-accent='ocean'] { --brand-from: #2563eb; --brand-via: #0284c7; --brand-to: #22d3ee; --brand-primary: #2563eb; --brand-strong-from: #2563eb; --brand-strong-to: #077bba; }
.dark[data-accent='ocean'], .dark [data-accent='ocean'] { --brand-from: #3b82f6; --brand-via: #0ea5e9; --brand-to: #22d3ee; --brand-primary: #3d85f9; --brand-strong-from: #2970e3; --brand-strong-to: #017cb2; }--brand-from/--brand-via/--brand-to:装飾用の 3 つのグラデーションのカラーストップ。--brand-primary:文字を載せる色(リンク、フォーカスリング、--primary-foregroundの下の塗り)。すべての背景とbrand-softの上で 4.6:1 以上のコントラストになるように選ばれています。--brand-strong-from/--brand-strong-to:白い文字の下に敷くグラデーション。白に対して 4.6:1 以上です。
アクセントカラーに追従するそのほかのトークンは、すべてこれらから派生します。
| トークン | 派生元 |
|---|---|
--primary、--ring、--sidebar-primary、--sidebar-ring | --brand-primary |
--brand-gradient | 3 つのカラーストップによる線形グラデーション |
--brand-gradient-strong | --brand-strong-from / --brand-strong-to による線形グラデーション |
--brand-soft、--brand-glow、--brand-shadow | color-mix() でカラーストップを透明色と混ぜる |
したがって、ページで primary、brand-* などのセマンティッククラスを使っていれば、アクセントカラーを切り替えたときに自動で追従します。ページ内で特定のアクセントカラーの色値を直接書かないでください。
グラフの系列色 --chart-1 … --chart-5 は、アクセントカラーに追従しない固定のカテゴリ用パレット(ライトとダークで別の値)です。ECharts のグラフは @/lib/chart-theme の useChartColors() で現在の CSS 変数の実際の値を読み取り、テーマやアクセントカラーが切り替わると再計算します。chartBase() はカテゴリ用パレットを適用し、単一系列のグラフではアクセントカラーのカラーストップから作る brandLine() / brandArea() を使います。
アクセントカラーを追加する
apps/web/src/lib/appearance.tsのACCENTSに{ id, label }を追加します。labelは中国語の原文で、翻訳キーも兼ねます。apps/web/src/index.cssに、対応する[data-accent='<id>']のライトとダークの 2 つのルールを追加します。どちらも 6 つの変数をすべて設定し、上記のコントラストの基準を満たすようにします。色は 16 進表記のままにしてください。chart-theme.tsが--brand-fromを rgba に変換します。labelの英語と日本語の訳文を追加します。
ナビゲーションモード
| ID | 名前 | 説明 |
|---|---|---|
sidebar | サイドバー(デフォルト) | 左側にメニューツリー全体を表示 |
top | トップ | メニューをトップバーに表示し、サイドバーは表示しない |
mixed | ミックス | トップバーに第 1 階層のセクションを表示し、左側に現在のセクション配下のメニューを表示 |
サイドバーのスタイル
shadcn の <Sidebar variant> と 1 対 1 で対応しています。
| ID | 名前 |
|---|---|
sidebar | 標準(デフォルト) |
floating | フローティング |
inset | インセット |
ナビゲーションモードが「トップ」の場合はサイドバーが表示されないため、このオプションは使えません。
コンテンツ幅
| ID | 名前 | 説明 |
|---|---|---|
boxed | 固定幅 | コンテンツ領域を中央に配置し、最大幅は 1600px |
fluid | フル幅(デフォルト) | コンテンツ領域を利用可能な幅いっぱいに広げる |
モバイル
ビューポートの幅が 768px 未満の場合:
- どのナビゲーションモードでも、メニュー全体を表示するドロワー式のサイドバーになります
- タブバーは表示されず、ページの状態保持も行いません
タブバーとページの状態保持
タブバーはデフォルトで有効で、外観設定で無効にできます。有効にすると、開いたページがトップバーの下にタブとして表示されます。
- トップレベルのページ(ホームなど)は先頭に固定され、閉じることはできません
- タブでは「閉じる」「他を閉じる」「右側を閉じる」「すべて閉じる」「再読み込み」が使えます
- タブの一覧は
sessionStorageのtags-viewキーに保存され、現在のブラウザタブでのみ有効です - タブに戻ると、前回のクエリパラメーターとスクロール位置が復元されます
状態管理は apps/web/src/context/TagsViewContext.tsx、ページ領域の描画は apps/web/src/components/app/AppLayout.tsx にあります。
状態保持の仕組み
タブバーが有効な場合、開いているタブのページはそれぞれ React の <Activity> で包まれ、マウントされたまま保持されます。
| 状態 | 切り替えて離れたとき(非表示) | 戻ったとき(表示) |
|---|---|---|
| コンポーネントの state(フィルター条件、ページング、フォームの入力) | 保持 | そのまま復元 |
useEffect の副作用 | クリーンアップ関数を実行 | 再実行 |
つまり、ページに戻ると useEffect 内のリクエストがもう一度データを取得し、非表示の間はタイマー、ポーリング、WebSocket がクリーンアップ関数によって自動で停止します。
タブを閉じると対応するページはアンマウントされます。タブを再読み込みするとページが再マウントされ、先頭から表示されます。
ページのコードに求められること
副作用は effect の中に書き、正しくクリーンアップすること
- タイマー、ポーリング、購読、WebSocket 接続はすべて
useEffectの中で開始し、クリーンアップ関数で停止してください。 - モジュールのトップレベルやレンダリング中にタイマーを開始しないでください。ページが非表示になっても動き続けてしまいます。
useEffect(() => {
const timer = setInterval(refresh, 5000)
return () => clearInterval(timer)
}, [refresh])設定の保存先
| ストレージ | キー | 内容 |
|---|---|---|
localStorage | theme | light / dark |
localStorage | appearance | { accent, navMode, sidebarVariant, contentWidth, tagsView } |
localStorage | lang | UI の言語。多言語対応 を参照 |
sessionStorage | tags-view | 開いているタブ |
appearance 内の未知のキーや不正な値は無視され、デフォルト値に戻ります。外観設定パネルには「デフォルトに戻す」ボタンがあります。オプションの唯一の定義は apps/web/src/lib/appearance.ts にあります。
