# Hydrogen Webview Tab Icons

How Hydrogen **webview** workspace tabs actually get their icons in
the Adom editor. **Read the "Real mechanism" section first** — the
placeholder vs. favicon distinction is the thing future Claude
sessions usually get wrong, including an earlier version of this
skill.

## Scope

This skill is **only** about tabs whose `panelType` is
`adom/a1b2c3d4-0031-4000-a000-000000000031` — the generic Hydrogen
webview that loads an HTML page from a URL. The favicon-override
mechanism described below is specific to webview panels because
only they load an HTML page that can ship its own
`<link rel="icon">`.

Other panel types — Schematic Editor, 3D Editor, KiCad panel,
Desktop Demo, Movie Maker, Adom Viewer native panel, etc. —
have their icons assigned by Hydrogen's internal panel registry
keyed off `panelType`, not by anything the tab adder passes in.
If your tab isn't a webview and its icon is wrong, **this skill
will not help you** — open a Hydrogen issue instead.

## Real mechanism (the thing everyone misses)

Hydrogen webview tabs have **two** sources for their displayed icon,
and the loaded page wins:

1. **`panelState.displayIcon`** — a placeholder you pass via
   `--display-icon` / HTTP `displayIcon`. Shown **only until the page
   loads**.
2. **The loaded page's `<link rel="icon">`** — Hydrogen's Web View
   panel reads the favicon after `load` and writes it into
   `panelState.displayIcon`, overriding whatever the placeholder was.

**Practical consequence:** once any page with a favicon finishes
loading, your `--display-icon "mdi:chip"` is gone. If you want the
icon to be **stable and predictable**, you have to serve the favicon
you want **from the page you load**.

Empirical proof: create a tab with `--display-icon "mdi:rocket-launch"`,
navigate it to `https://example.com`, then inspect the tab's
`panelState.displayIcon` via `adom-cli hydrogen workspace tabs` — it
will be a `data:image/svg+xml;base64,...` value matching example.com's
favicon, not `mdi:rocket-launch`.

## Brand rule: icons must be monochrome `#e6edf3`

Per `gallia/skills/brand/SKILL.md`, **all Adom icons must be monochrome
white (`#e6edf3`)** — no colored icons, no multi-color, no brand-palette
colors in icons. This applies to tab icons too.

- **First choice:** use an `mdi:<name>` placeholder for the `--display-icon`
  flag. MDI icons are already monochrome and inherit the theme color
  automatically, so you get brand compliance for free. Search
  https://pictogrammers.com/library/mdi/ for a matching icon.
- **If you need a custom icon** (no MDI match, or you want to ship your
  own favicon for the page-load override): hand-roll an SVG with
  `fill="#e6edf3"` (or `fill="currentColor"` so it inherits), a
  `24x24` viewBox, and clean filled paths or 2px strokes. **No
  gradients, no shadows, no color.** Details that break at 16 px lose.

Concrete example of a brand-compliant custom icon (the adom-tsci
chip) — 318 bytes, single path, monochrome `#e6edf3`:

```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e6edf3">
  <path d="M6 2a2 2 0 0 0-2 2v2H2v2h2v3H2v2h2v3H2v2h2v2a2 2 0 0 0 2 2h2v2h2v-2h3v2h2v-2h3v2h2v-2a2 2 0 0 0 2-2v-2h2v-2h-2v-3h2v-2h-2V8h2V6h-2V4a2 2 0 0 0-2-2h-2V0h-2v2h-3V0h-2v2H9V0H7v2H6zm0 2h12v16H6V4zm2 3v10h8V7H8zm2 2h4v6h-4V9z"/>
</svg>
```

Rendered in a dark-background Hydrogen tab bar, that's a white chip
with pins sticking out — visually identical to `mdi:chip` but served
from your own app so it can't be overridden.

## The right way: serve your own favicon from your app

If you're writing a Rust/Node/Python/Go app that opens a Hydrogen
webview tab, **put a `<link rel="icon">` in the HTML you serve**.
Hydrogen will pick it up on load and that's what the user will see.
Use a **brand-compliant `#e6edf3` monochrome SVG** (see section above).

### Rust + tiny_http example (the `adom-tsci` pattern)

```html
<!-- src/assets/shell.html, embedded in the binary via include_str! -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My App</title>
<!-- Hydrogen reads this and uses it as the tab icon. -->
<link rel="icon" type="image/svg+xml" href="favicon.svg">
...
```

```rust
// Serve favicon.svg from the tiny_http route handler.
if url == "/favicon.svg" || url == "/favicon.ico" {
    let body = include_str!("../assets/icon.svg");
    return Ok(request.respond(
        Response::from_string(body)
            .with_header(
                Header::from_bytes(&b"Content-Type"[..], &b"image/svg+xml"[..]).unwrap()
            ),
    )?);
}
```

```svg
<!-- src/assets/icon.svg — MONOCHROME per the brand rule. fill must be
     "#e6edf3" (or "currentColor"), NEVER a colored palette value. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e6edf3">
  <rect x="5" y="5" width="14" height="14" rx="1"/>
  <rect x="3"  y="7"  width="2" height="2"/>
  <rect x="3"  y="11" width="2" height="2"/>
  <rect x="3"  y="15" width="2" height="2"/>
  <rect x="19" y="7"  width="2" height="2"/>
  <rect x="19" y="11" width="2" height="2"/>
  <rect x="19" y="15" width="2" height="2"/>
  <rect x="7"  y="3"  width="2" height="2"/>
  <rect x="11" y="3"  width="2" height="2"/>
  <rect x="15" y="3"  width="2" height="2"/>
  <rect x="7"  y="19" width="2" height="2"/>
  <rect x="11" y="19" width="2" height="2"/>
  <rect x="15" y="19" width="2" height="2"/>
</svg>
```

(Perfectly symmetric chip: body + 12 pins, 3 per side, even 4-unit
spacing, rx=1 rounded corner, all in brand `#e6edf3`. This is the
exact `adom-tsci` icon — taken from the repo's `src/assets/icon.svg`.)

When the page loads from `https://<workspace>.adom.cloud/proxy/<port>/`,
the browser fetches `/proxy/<port>/favicon.svg`, your app returns the
SVG, Hydrogen syncs it onto the tab, and the icon stays put.

**Rules for the favicon file:**

- **Square `viewBox`** (e.g. `0 0 64 64` or `0 0 24 24`).
- **Self-contained SVG** — no external fonts, no `xlink:href` to
  other files, no embedded images.
- **Bold, filled shapes over thin strokes** — the tab bar is
  ~16–20 px so details get lost.
- **Transparent background** is fine, the tab bar shows through.
- **Brand color in the SVG itself** if you want a colored icon. MDI
  icons can't be tinted.

## Placeholder-only case: `mdi:<name>` / `data:...` via add-tab

If the page you're loading **doesn't set its own favicon**, or if
you're creating a tab that navigates to about:blank or similar, the
`--display-icon` placeholder is all you get — and it will stick.

Examples where this is actually useful:

- **A tab that loads an external site with no favicon.** Rare in
  practice; most real sites have a favicon.
- **A stopgap during the ~100ms the page takes to load** — the user
  sees the placeholder, then it swaps to the page's favicon.
- **When you don't own the page** — you can't inject a `<link>`, so
  the placeholder is your only control point, knowing it's about to
  get overridden.

### CLI

```bash
adom-cli hydrogen workspace add-tab \
  --panel-id <leaf-id> \
  --panel-type "adom/a1b2c3d4-0031-4000-a000-000000000031" \
  --display-name "My App" \
  --display-icon "mdi:chip" \
  --initial-state '{"url":"https://example.com/"}'
```

### HTTP API

```bash
curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{
    "panelId": "<leaf-id>",
    "panelType": "adom/a1b2c3d4-0031-4000-a000-000000000031",
    "displayName": "My App",
    "displayIcon": "mdi:chip",
    "initialState": { "url": "https://example.com/" }
  }' \
  "https://hydrogen.adom.inc/api/workspaces/editor/$OWNER/$REPO/current/tabs"
```

### Two placeholder formats

| Format | When to use | Example |
|---|---|---|
| `mdi:<icon-name>` | Monochrome icon from [Material Design Icons](https://icon-sets.iconify.design/mdi/). Inherits the editor theme's foreground color. Can't be tinted. | `mdi:chip`, `mdi:bug`, `mdi:eye` |
| `data:image/svg+xml;base64,<b64>` | Custom SVG, full control. **Must still be monochrome `#e6edf3`** per the brand rule — don't color your own placeholder. | `data:image/svg+xml;base64,PHN2...` |

**Again: both are placeholders. The page's favicon overrides them
both as soon as it loads.**

### Common MDI icons used in existing Adom tabs

| Icon | Typical use |
|---|---|
| `mdi:chip` | chips, ICs, boards, PCB/tscircuit tools |
| `mdi:eye` | viewers (AV) |
| `mdi:bug` | bug-filing tools |
| `mdi:chart-line` | visualizers |
| `mdi:monitor-cellphone` | demo/desktop tools |
| `mdi:github` | repo browsers |
| `mdi:book-open-variant` | docs viewers |
| `mdi:cart` | commerce tools |
| `mdi:robot` | automation / AI |
| `mdi:folder` | file explorers |
| `mdi:code-tags` | code editors |
| `mdi:palette` | design tools |
| `mdi:camera` | screenshot tools |

Full catalog: https://icon-sets.iconify.design/mdi/.

## Building a custom SVG data URL by hand

When you need a one-off custom icon and don't want to ship a file,
you can build a `data:` URL inline:

```bash
SVG='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#00b8b0" d="M13.13 22.19L11.5 18.36..."/></svg>'
B64=$(echo -n "$SVG" | base64 -w0)
DATA_URL="data:image/svg+xml;base64,$B64"

adom-cli hydrogen workspace add-tab \
  --panel-id <leaf-id> \
  --panel-type "adom/a1b2c3d4-0031-4000-a000-000000000031" \
  --display-name "My App" \
  --display-icon "$DATA_URL" \
  --initial-state '{"url":"about:blank"}'
```

`base64 -w0` (no line wrapping) is important — newlines inside the
value break the JSON it gets embedded in.

This is still a **placeholder**. If the tab ever navigates to a page
with its own favicon, the placeholder is lost.

## Troubleshooting

### "I pass `mdi:chip` but the tab shows a different icon"

Your loaded page has a favicon that overrode the placeholder.
Options:

- **Best:** serve your own favicon from your app (see Real mechanism
  above). Pick an SVG that matches the brand/icon you want. Hydrogen
  will sync it onto the tab.
- **Acceptable:** if you don't control the page, you can re-set the
  `displayIcon` after load with another `add-tab`/PATCH call — but
  the next navigation will override it again.

### "My placeholder flickers for a moment then reverts"

That's the expected behavior — the placeholder shows until the page
finishes loading, then Hydrogen swaps in the page's favicon.

### "My tab has no icon at all"

The loaded page has no `<link rel="icon">` **and** no
`--display-icon` was passed. Hydrogen falls back to the default Web
View icon (a globe). Add a favicon to your served HTML.

### "I pass a `data:image/svg+xml;base64,...` and it's broken"

Usually a base64 encoding issue:

- Use `base64 -w0` (no wrap) so the output is a single line.
- Make sure the raw SVG has no unescaped newlines or tabs that break
  the JSON wrapper.
- The SVG must be self-contained: no `xlink:href` to other files, no
  `@import` in `<style>`, no embedded `<image>` pointing at URLs.

### "I set the tab to `mdi:chip` but `workspace tabs` shows a data URL"

Normal. Hydrogen rewrites `panelState.displayIcon` after page load
with whatever the page's favicon was (usually a data URL because
some sites inline their favicons). This is the override in action.

## How the `adom-tsci` app handles this

1. `src/cli/start.rs` passes `--display-icon "mdi:chip"` to
   `adom-cli hydrogen workspace add-tab` — this shows the MDI chip
   icon for ~100ms while the page loads.
2. `src/assets/shell.html` has
   `<link rel="icon" type="image/svg+xml" href="favicon.svg">`.
3. `src/server/mod.rs` serves the favicon at `/favicon.svg` with the
   content of `src/assets/icon.svg` — a teal-colored (`#00b8b0`)
   custom SVG chip.
4. When the shell loads, Hydrogen fetches `favicon.svg`, gets the
   teal chip SVG, and writes that into `panelState.displayIcon`.
5. The tab now **permanently** shows the teal chip — because every
   future reload re-serves the same favicon.

The MDI `mdi:chip` placeholder and the served `favicon.svg` are
visually similar by design — the transition is invisible. If they
were different, you'd see a brief flash.

## See also

- `~/.claude/skills/adom/guides/adom-workspace-control.md` — the full
  workspace API reference that documents `displayIcon` as a
  `panelState` field (but buries the favicon-override behavior).
- `~/.claude/skills/adom/webview/SKILL.md` — Web View panel actions
  (navigate, refresh, setHeaderHidden, setProxyEnabled).
- `~/.claude/skills/adom/guides/app-creator.md` — app conventions
  including the `docs/icon.svg` brand-icon requirement.
- [iconify MDI catalog](https://icon-sets.iconify.design/mdi/) — the
  searchable list of every `mdi:<name>`.
