---
name: webui-design
description: |
  moorestech Web UI（moorestech_web/webui）のデザイン哲学。見た目・構造のホワイトリスト。
  Use when: 1.moorestech_web/webui配下のコードを読む・書く・レビューする時 2.新しいパネル・モーダル・HUD・コンポーネントを追加する時
  3.CSS・色・レイアウト・装飾を変更する時 4.Web UIのデザイン判断に迷った時
---

# moorestech Web UI デザイン哲学

このドキュメントは moorestech Web UI の見た目・構造の**ホワイトリスト**である。

**大原則: ここに書かれていない表現・コンポーネント・パターンは使わない、やらない。**
新しい表現が必要になったら、実装する前にこのドキュメントを更新して裁定を取る。
「とりあえず作って後で様式化」は禁止。様式が先、実装が後。

**大原則: フェード・余白などの視覚寸法は固定長トークンが既定。パネル寸法に比例する%指定は破綻源。**
%指定は基準サイズでは正しく見え、寸法違いのパネル（大型化・別画面流用）で初めて破綻する。
比例させたい明確な理由がある場合のみ%を使い、その理由をコメントで明記する。

正本（リファレンス実装）はインベントリ画面（`InventoryPanel` + `RecipeViewer` + `ItemListPanel`）。
迷ったらインベントリ画面がどうしているかを見て、それに従う。

---

## 1. 画面構成

- **全画面UIは作らない。** すべてフローティングパネルまたはモーダル形式。
  - Web UIは3D世界の上に載る透明オーバーレイ（CEF）であり、世界が透けて見えることが前提。
  - 画面全体を不透明な面で塗り潰すレイアウトは、いかなる画面でも禁止。
- **背景ディムは App の screen backdrop 1枚だけが担う。** 各パネルが独自に画面を暗くしない。
- **重なり順は `index.css` の `--z-*` トークンのみで制御する。** 数値のz-index直書き禁止。
- 常時表示HUD（ホットバー・クロスヘア・キーヒント等）は例外的にパネル外だが、これも「浮いている」表現であること。面で塗らない。

## 2. パネル — GamePanel を使い回す

- **パネル背景はすべて `shared/ui/GamePanel`。** 新しいパネル背景を発明しない。
  - `variant="default"`: 縁を持たず世界背景へ溶ける半透明ネイビー面（インベントリパネルの背景）。側面・一覧系パネルの標準。
  - `variant="craft"`: 1px枠+内周線を持つ中央詳細用の細めバリアント。
- **面の左右フェード幅は固定長トークン `--panel-edge-fade` のみ。** %指定はパネル幅でフェード幅が伸びて内容がフェード帯に載るため禁止。内容はGamePanelのpadding内に置く限り不透明領域内に収まることを保証する（はみ出し防止の唯一の機構）。
- **ただし共通GamePanelのpaddingは全辺でこの保証を満たしていない**（左28pxのみフェード幅12px超。右10px・上8pxはフェード幅未満）。正本合わせの持ち物パネルでは意図的な非対称なので共通paddingは変更せず、**内容量でサイズが決まるパネル（チェスト等）は不足する辺を安全帯トークンで補う**（前例: `--block-panel-right-safe-area` / `--block-panel-bottom-safe-area`）。内容の縁とフェード開始位置が近い辺は「面が内容の直後で途切れて見える」ため、余白は「フェード幅+視認できる余白」を確保する。
- **上部2本線+タイトル（`title` 指定）は「一覧の置き場」に限る。**
  - 使う: インベントリ、クラフトレシピ一覧など、アイテムが並ぶ主要パネル。
  - 使わない: 詳細表示、小型フロート、モーダル、HUD。`title` を渡さなければ罫線は出ない。
- 新しい見た目が必要なら GamePanel に variant を追加し、本ドキュメントに追記してから使う。GamePanel の外で独自CSSのパネル面を作るのは禁止。
- **注: インベントリ画面の「整理」ボタンとpingボタンは仮実装であり、様式に含めない。** これらを前例として引用しない。

## 2.5 ブロックUIパネル

- **ブロックインベントリの外枠は `GamePanel variant="default"` + `title`=ブロック名。** スロットが並ぶ主要パネルを「一覧の置き場」として扱い、タイトル上下の2本罫線を許可する。
- App の stage グリッドにある `viewer` 領域へ置き、持ち物パネルの右隣で上端を揃える。機能側の固定配置・独自z-index・パネル面・下端フェードは禁止し、配置は stage、面表現は GamePanel が一元供給する。
- GamePanel の下向き三角と内容が重ならないよう、ブロックパネルだけ `--block-panel-bottom-safe-area` の下部安全帯を確保する。共通 GamePanel の余白は変更しない。
- 内容量で幅が決まる小型ブロックパネル（チェスト等）は、GamePanel共通の右余白10pxがフェード帯に食われて面が途切れて見えるため、`--block-panel-right-safe-area`（左インデント28pxと対称）の右余白を追加する。大型機械パネルは固定幅・中央揃えのため対象外。
- 閉じる操作はパネル右上の `shared/ui/PanelCloseButton` を使う。面を持たない浮遊の×とし、Mantine CloseButton は使わない。
- **レシピ選択を持つ機械ブロックのみ大型レイアウト**: 研究パネル同様 `viewer-start / items-end` の2列を占有し、上端は持ち物パネルと揃え、下端はホットバー手前で止める。中身は `ModeSwitch` を横向きタブバーとした「インベントリ / レシピ選択」の2タブ切替（§8.7）。レシピ0件のブロックは従来の小型パネルのまま。

## 3. モーダル

- **モーダルの面もインベントリパネル系（GamePanelのトーン）を使う。** Mantine標準テーマ剥き出しの白/グレー面を出さない。
- モーダルは中央配置+backdropディム。backdropはモーダル専用の1枚のみ（screen backdropと二重にしない）。
- 確認・入力等の定型モーダルは `ModalHost`（`ui.modal` トピック駆動）を通す。機能側が勝手に独自モーダルをマウントしない。

## 4. スロットとグリッド

- **アイテム・ブロック・液体を1マスで表すものは `shared/ui` のコンポーネントのみ。**
  - `ItemSlot` / `BlockSlot` / `FluidSlot` / `FluidSlotRow` / 素枠は `SlotFrame`。
  - 並べるのは `SlotGrid`（既定9列）。独自の grid CSS でスロットを並べない。
- スロット寸法は `--slot-size`、間隔は `--slot-grid-gap` の局所上書きで調整する。コンポーネント内にpx直書きしない。
- スロットの状態表現は data属性（`data-selected` / `data-filled` / `data-catalog` / `data-insufficient`）に統一。新しい状態が要るなら data属性を追加する。
- マウス操作の契約は `useSlotMouse`（左押下・右押下・ドラッグ進入・ダブルクリック）。スロットに生の onClick を生やさない。
- ホバープレビュー（ホバー中スロットの詳細を別領域へ出す）は SlotFrame/ItemSlot の `onHoverChange` を使う。機能側で生の onMouseEnter/Leave をスロットに生やさない。
- **用途の異なるスロット群を同一パネル内に並置する時は、ラベル（`--text-muted`）または `FadeRule` の区切りで必ず区別する。** 無札の並置は入出力と誤読されるため禁止（例: アップグレードスロット）。
- **左右のスロット数が非対称になり得る行の中央要素（進捗矢印等）は `1fr auto 1fr` グリッドで中央に固定する。** 行全体のflex中央寄せは個数差で中央要素がずれるため使わない。

## 5. 色・トーン

- パレットはuGUI由来の**半透明ネイビー（#0a0e1b / #070912 系, α0.8）+ 寒色グレー**。
- 色は `index.css` の CSS変数（`--color-*` / `--text-*` / `--bevel-*` 等）から取る。機能側CSSへの新色ハードコード禁止。新色が必要ならトークン化してから使う。
- アクセントの青グラデ（`--recipe-action-background`）は**主要アクションボタン限定**。装飾や面には使わない。
- 面は必ず半透明。不透明100%の面は作らない（世界が透けるのが前提のため）。
- `index.css` の `--text-muted` は従属テキスト、`--text-insufficient` は不足/警告、`--gauge-track` はゲージの溝、`--gauge-fill` はゲージの充填に使う。
- 機能側への色ハードコードは引き続き禁止し、これらの色も必ずトークン経由で参照する。

## 6. 装飾

- **UI装飾の画像アセット化は禁止。** 枠・罫線・文字・グリップ等はCSS/DOM/インラインSVGで再現する。（例外はテスト用モックの世界背景のみ）
- 装飾語彙は既存の3つに限る:
  1. 両端フェードする水平罫線（タイトル上下の2本線）
  2. 下向き三角の底面テクスチャ（default パネル下部）
  3. 右下三角グリップ（craft パネル）
- 新しい装飾モチーフ（光彩、パーティクル、角丸カード、ドロップシャドウの多用等）を増やさない。
- 装飾アニメーションは基本入れない。トランジションを入れる場合もe2eが同期検証できること（モーダルは duration 0）。

## 7. 文字

- フォントは `--font-ui` のみ。個別 font-family 指定禁止。
- 実フォントは単一ウェイトのため**合成bold/italicは禁止**（`font-synthesis: none` を崩さない）。
- **表示文字列は必ず `t()` を通す。** JSXへの生リテラルは lint（no-jsx-visible-literal）で落ちる。
- キー操作ヒントは `<kbd>` + `t()` の既存様式（InventoryScreenChrome の keyHints）に従う。

## 8. 通知・情報表示

- 一時通知は `ToastHost`（クライアントローカルの汎用トースト）または `NotificationHost`（`features/notification`。サーバー発のゲーム通知＝achievement/operationDenied、topic `notification.events`、左端縦中央・5秒・`ItemIcon`付き可）のどちらかを使う。カーソル追従の説明は `CursorTooltip`。機能側でこの2ホスト以外の独自トースト・独自ツールチップを作らない。
- **NotificationHostの見た目は研究ノードカード同族の枠付き浮遊行**: 面=`--notification-face`（半透明ネイビー）+ 枠=`--notification-border` 1px（直角・角丸/影なし）。最大幅は`--notification-max-width`（画面幅20%・ユーザー裁定の画面比例値）で超過分は折返す。文字色はトークンのみ: achievement=`--text-high-contrast`、operationDenied=`--text-insufficient`。カテゴリはdata属性（`data-category`）で表す。Mantine `Notification` コンポーネントは使わない。
- 接続前のプレースホルダは `ConnectingPlaceholder`。
- 進捗矢印は `ProgressArrow`。

## 8.5 グラフビュー（研究ツリー等のノードグラフ）

- グラフの置き場は `GamePanel variant="default"` + タイトル罫線。body内で `shared/treeView` のパン・ズームを使う。
- **研究ノードカード**: 「名前1行(ellipsis) + `ItemSlot`アイコン」の縦積みのみ。説明・消費・報酬・ボタンはカードに載せない。
  面は `--research-node-face`、枠は `--research-node-border`（index.cssのトークン）。
  状態はdata属性（`data-completed` / `data-researchable` / `data-locked` / `data-selected`）。
  lockedはopacity減衰、selectedは `--text-high-contrast` のoutlineで表す。新しい色相・光彩は使わない。
- **グラフ内詳細ペイン**: ノード選択で開く `GamePanel variant="craft"` のフロート。グラフパネル内の固定位置
  （パン・ズーム非追従）。内容は名前・説明・消費(`ItemSlot`+insufficient)・報酬/解放(`ItemSlot`)・
  主要アクションボタン（青グラデ）・閉じるボタン。オンオフ可能（同ノード再クリック/閉じるで消える）。

## 8.6 shared/ui の汎用表示部品

- **GaugeBar**: 読み取り専用の水平ゲージ。溝は `--gauge-track`（半透明ネイビー）と `--bevel-c1` の薄い内周輪郭、充填は `--gauge-fill`（寒色グレー）を使い、青グラデは禁止。`value`（0..1）を描くだけでドメイン語彙を持たない。
- **ModeSwitch**: `option.value` / `option.label` / `onChange` の汎用I/Fを持つ択一モード切替。選択中は `data-selected`（`--text-high-contrast` + 寒色面）、非選択は `--text-muted` とし、各選択肢は間隔を空けて独立したボタンとして示す。青グラデは禁止。
  - **縦利用（`orientation="vertical"`）はサイドバーナビとして使ってよい。** カテゴリ切替のような縦積み択一に、新規コンポーネントを作らずこれを転用する。
  - **`disabled?: boolean`**: root に `data-disabled` を付与し全ボタンを `disabled` にする汎用減衰。選択肢は `--text-muted` 系へさらに減衰しクリック不可（`pointer-events: none`）。判断（いつdisabledにするか）は利用側が持ち、ModeSwitch自体はドメイン語彙を持たない。
- **PanelCloseButton**: パネル右上の面を持たない×。インラインSVGまたはCSSで描画する。
- **FadeRule**: 両端フェードする水平罫線（装飾語彙1）の単体部品。パネル内のセクション区切りに使う。GamePanel のタイトル罫線と同族の青灰グラデで、新しい色相は持たない。

## 8.7 機械レシピ選択タブ

- **MachineSection のタブとして置く。** 対象レシピが1件以上ある機械は `ModeSwitch`（横向き）で「インベントリ / レシピ選択」を切り替える。デフォルトはインベントリタブ。0件ならタブ自体を出さず従来表示のまま。
- **機械UIの中身は基本的に中央揃え。** 両タブとも詳細・グリッド・テキストを水平中央に揃える。電力率テキストはタブの外の共通フッタとして両タブで常時表示する。
- インベントリタブは従来の機械表示（入出力/モジュールスロット・進捗矢印・流体行・分間生産数）に加え、レシピ選択中はその生産物（代表出力アイテム）を `ItemSlot` 1個（個数バッジ無し）で表示する。
  - **加工行は進捗矢印をパネル中央に固定**し、左右を等幅（1fr auto 1fr）にして入力は矢印へ右寄せ、出力は矢印から左寄せで対称に置く。
  - **モジュールスロットは加工行から1段下げ、`--text-muted` の「アップグレードスロット」ラベルを直上に付けて**用途を明示する。入出力と紛れる無札の並置は禁止。
- レシピ選択タブは上から「詳細プレビュー → `FadeRule` の区切り罫線 → レシピグリッド」の縦構成。
  - **詳細プレビュー**: ホバー中レシピを優先し、無ければ選択中レシピを表示。どちらも無ければ `--text-muted` の案内テキスト。内容は「材料 `ItemSlot` 列 → 矢印テキスト（直下に所要時間） → 出力 `ItemSlot` 列」で、RecipeViewer の `MachineRecipeView`（矢印テキスト＋ItemSlot列）様式に準拠。高さを固定しホバーで段落が跳ねないようにする。
  - **レシピグリッド**: 解放済みレシピの代表出力アイテムを `shared/ui` の `ItemSlot` で `SlotGrid`（9列折返し）に列挙し、独自gridは作らない。
- 選択中は ItemSlot の `selected`（SlotFrame の `data-selected`）で示し、新しい色相・光彩は足さない。
- 左クリックで選択し、**選択と同時にインベントリタブへ切り替える**。右クリックは選択中の場合だけ解除する。マウス契約は ItemSlot の `onLeftDown` / `onRightDown`、ホバーは `onHoverChange` に従う。

## 8.8 ワールドピンHUD（チュートリアルの位置誘導）

- **座標の正はUnity。** Unityがワールド座標を正規化ビューポート座標（0..1、左上原点）と画面中心からの方向ベクトルへ毎フレーム射影し、`tutorial.world_pins` トピックで配信する。Web側は受信値を描くだけで、3D射影・カメラ知識を一切持たない。
- 表示は常時表示HUD族（§1の例外）。パネル面を持たず「浮いている」表現とし、`pointer-events: none` で入力を素通しする。
- **画面内ピン**: 指定座標にインラインSVGの下向きマーカー + 直上のテキストラベル。ラベル面は `--world-pin-face`（半透明ネイビー族）、文字は `--text-high-contrast`。マーカー先端が指定座標に一致するよう配置する。
- **画面外矢印**: 方向ベクトルを画面端（マージン `--world-pin-edge-margin` の固定長）へクランプした位置に、方向へ回転したインラインSVGシェブロンを置く。テキストラベルは付けない（uGUI版HudArrowと同じ責務分担）。
- 色相・光彩・アニメーションは追加しない。z層は `--z-world-pin` トークンのみで制御する。

## 8.9 検索入力

- Mantine `TextInput` は使わない。素の `<input>` に `--gauge-track` 同族の半透明面（GaugeBar の溝と同トーン）を背景として与える。
- プレースホルダは `--text-muted`。フォーカス表現は ModeSwitch の `:focus-visible`（`--text-high-contrast` の outline）を踏襲し、新しいフォーカス様式を増やさない。
- 幅・高さは固定長トークンで指定する（パネル幅比例の%指定は禁止・大原則参照）。

## 8.10 カスタムスクロールバー

- Mantine `ScrollArea` の `:global(.mantine-ScrollArea-*)` セレクタで上書きする（前例: `ItemListPanel.module.css:10-30`）。ScrollArea自体は使ってよいが、既定の白ノブ/透明トラックのまま出さない。
- トラックは `var(--gauge-track)`、ノブは `var(--bevel-c2)` を基調にしたネイビートーンへ統一する（ItemListPanelの白ノブは持ち物一覧固有の正本合わせであり、他パネルではこのネイビートーンに従う）。
- ノブ寸法はコンテンツ量から自然算出させ、固定pxで決め打ちしない。

## 8.11 建設メニュー

- **大型2列レイアウト**: `viewer-start / items-end` の2列を占有する `.panelLarge` 前例（§2.5・§8.7の機械大型パネルと同一の stage 配置）に従う。独自の固定配置・独自z-indexは禁止。
- **縦ModeSwitchサイドバー**: カテゴリ切替は §8.6 の縦向き ModeSwitch を左サイドバーとして使う。幅は `--build-menu-sidebar-width`（固定長）。
- **検索**: §8.9 の検索入力をサイドバー上部またはグリッド上部に置く。
- **固定高プレビュー**: 選択中ブロックのプレビューは §8.7 の詳細プレビュー同様、高さを固定して内容でホバーが跳ねないようにする（`--build-menu-preview-height`）。
- **サブカテゴリ見出し**: グリッド内のサブカテゴリ区切りは `--text-muted` のラベル + `FadeRule`（§8.6と同一部品）。無札の並置は禁止（§4のスロット群区別ルールに従う）。
- グリッド本体は `SlotGrid` を使い独自gridを作らない。端の安全余白は `--build-menu-edge-safe-area`。

## 9. やらないことリスト（再掲・明示）

- 全画面UI・不透明な面での塗り潰し
- Mantine標準テーマ剥き出しの見た目
- UI装飾のための画像アセット追加
- GamePanel 以外のパネル背景 / shared/ui 以外のスロット表現
- 機能側CSSへの色・z-index・スロット寸法の直書き
- 新しい装飾モチーフ・装飾アニメーションの無断追加
- 面フェード・余白の%指定（固定長トークンを使う。理由なき%は破綻源）
- 用途の異なるスロット群の無札並置（ラベルか区切りで区別する）
- **このドキュメントに書かれていないパターンの使用**（必要なら先にここを更新する）

## 10. 実装後の目視QA（必須）

パネルの新設・寸法変更・レイアウト変更をしたら、コードレビューだけで終えず**mockホストのスクリーンショットで実画面を確認する**
（`e2e/capture-eval.ts` の様式でmock-hostを起動し、`/__block` `/__uistate` で対象画面を再現して撮影する）。

チェック項目:
1. **端**: 内容（タブバー・ボタン・グリッド）がパネル面のフェード帯に載って「はみ出て」見えないか。逆に、内容の直後で面が途切れて「切れて」見えないか（内容の縁〜フェード開始の余白が左右で対称か）。拡大クロップで**4辺すべて**確認する。内容量でサイズが決まるパネルは特に右端・下端が危ない（共通paddingがフェード幅未満の辺）
2. **中央と対称**: 中央揃え指定の要素が実際にパネル中心線上にあるか。左右の要素数が非対称なケースで確認する
3. **区別**: 無札のスロット群・用途が読めない要素が並んでいないか
4. **重なり**: 対象画面のuiStateを正しく設定したか（別パネルの透け重なりを問題と誤認しない・実際の重なりを見逃さない）

%指定や幅依存の値を触った場合は、基準幅（持ち物378px）と大型幅（機械/研究759px）の両方で確認する。
