{
  "schema_version": 1,
  "type": "skill",
  "slug": "hydrogen-webview-tab-icons",
  "title": "Hydrogen Webview Tab Icons",
  "brief": "How Hydrogen **webview** tab icons actually work in the Adom editor — specifically panels with `panelType: adom/a1b2c3d4-0031-4000-a000-000000000031`.",
  "version": "1.0.0",
  "tags": [],
  "license": "MIT",
  "source_path": "SKILL.md",
  "readme": "# Hydrogen Webview Tab Icons\n\nHow Hydrogen **webview** workspace tabs actually get their icons in\nthe Adom editor. **Read the \"Real mechanism\" section first** — the\nplaceholder vs. favicon distinction is the thing future Claude\nsessions usually get wrong, including an earlier version of this\nskill.\n\n## Scope\n\nThis skill is **only** about tabs whose `panelType` is\n`adom/a1b2c3d4-0031-4000-a000-000000000031` — the generic Hydrogen\nwebview that loads an HTML page from a URL. The favicon-override\nmechanism described below is specific to webview panels because\nonly they load an HTML page that can ship its own\n`<link rel=\"icon\">`.\n\nOther panel types — Schematic Editor, 3D Editor, KiCad panel,\nDesktop Demo, Movie Maker, Adom Viewer native panel, etc. —\nhave their icons assigned by Hydrogen's internal panel registry\nkeyed off `panelType`, not by anything the tab adder passes in.\nIf your tab isn't a webview and its icon is wrong, **this skill\nwill not help you** — open a Hydrogen issue instead.\n\n## Real mechanism (the thing everyone misses)\n\nHydrogen webview tabs have **two** sources for their displayed icon,\nand the loaded page wins:\n\n1. **`panelState.displayIcon`** — a placeholder you pass via\n   `--display-icon` / HTTP `displayIcon`. Shown **only until the page\n   loads**.\n2. **The loaded page's `<link rel=\"icon\">`** — Hydrogen's Web View\n   panel reads the favicon after `load` and writes it into\n   `panelState.displayIcon`, overriding whatever the placeholder was.\n\n**Practical consequence:** once any page with a favicon finishes\nloading, your `--display-icon \"mdi:chip\"` is gone. If you want the\nicon to be **stable and predictable**, you have to serve the favicon\nyou want **from the page you load**.\n\nEmpirical proof: create a tab with `--display-icon \"mdi:rocket-launch\"`,\nnavigate it to `https://example.com`, then inspect the tab's\n`panelState.displayIcon` via `adom-cli hydrogen workspace tabs` — it\nwill be a `data:image/svg+xml;base64,...` value matching example.com's\nfavicon, not `mdi:rocket-launch`.\n\n## Brand rule: icons must be monochrome `#e6edf3`\n\nPer `gallia/skills/brand/SKILL.md`, **all Adom icons must be monochrome\nwhite (`#e6edf3`)** — no colored icons, no multi-color, no brand-palette\ncolors in icons. This applies to tab icons too.\n\n- **First choice:** use an `mdi:<name>` placeholder for the `--display-icon`\n  flag. MDI icons are already monochrome and inherit the theme color\n  automatically, so you get brand compliance for free. Search\n  https://pictogrammers.com/library/mdi/ for a matching icon.\n- **If you need a custom icon** (no MDI match, or you want to ship your\n  own favicon for the page-load override): hand-roll an SVG with\n  `fill=\"#e6edf3\"` (or `fill=\"currentColor\"` so it inherits), a\n  `24x24` viewBox, and clean filled paths or 2px strokes. **No\n  gradients, no shadows, no color.** Details that break at 16 px lose.\n\nConcrete example of a brand-compliant custom icon (the adom-tsci\nchip) — 318 bytes, single path, monochrome `#e6edf3`:\n\n```svg\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"#e6edf3\">\n  <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\"/>\n</svg>\n```\n\nRendered in a dark-background Hydrogen tab bar, that's a white chip\nwith pins sticking out — visually identical to `mdi:chip` but served\nfrom your own app so it can't be overridden.\n\n## The right way: serve your own favicon from your app\n\nIf you're writing a Rust/Node/Python/Go app that opens a Hydrogen\nwebview tab, **put a `<link rel=\"icon\">` in the HTML you serve**.\nHydrogen will pick it up on load and that's what the user will see.\nUse a **brand-compliant `#e6edf3` monochrome SVG** (see section above).\n\n### Rust + tiny_http example (the `adom-tsci` pattern)\n\n```html\n<!-- src/assets/shell.html, embedded in the binary via include_str! -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<title>My App</title>\n<!-- Hydrogen reads this and uses it as the tab icon. -->\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"favicon.svg\">\n...\n```\n\n```rust\n// Serve favicon.svg from the tiny_http route handler.\nif url == \"/favicon.svg\" || url == \"/favicon.ico\" {\n    let body = include_str!(\"../assets/icon.svg\");\n    return Ok(request.respond(\n        Response::from_string(body)\n            .with_header(\n                Header::from_bytes(&b\"Content-Type\"[..], &b\"image/svg+xml\"[..]).unwrap()\n            ),\n    )?);\n}\n```\n\n```svg\n<!-- src/assets/icon.svg — MONOCHROME per the brand rule. fill must be\n     \"#e6edf3\" (or \"currentColor\"), NEVER a colored palette value. -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"#e6edf3\">\n  <rect x=\"5\" y=\"5\" width=\"14\" height=\"14\" rx=\"1\"/>\n  <rect x=\"3\"  y=\"7\"  width=\"2\" height=\"2\"/>\n  <rect x=\"3\"  y=\"11\" width=\"2\" height=\"2\"/>\n  <rect x=\"3\"  y=\"15\" width=\"2\" height=\"2\"/>\n  <rect x=\"19\" y=\"7\"  width=\"2\" height=\"2\"/>\n  <rect x=\"19\" y=\"11\" width=\"2\" height=\"2\"/>\n  <rect x=\"19\" y=\"15\" width=\"2\" height=\"2\"/>\n  <rect x=\"7\"  y=\"3\"  width=\"2\" height=\"2\"/>\n  <rect x=\"11\" y=\"3\"  width=\"2\" height=\"2\"/>\n  <rect x=\"15\" y=\"3\"  width=\"2\" height=\"2\"/>\n  <rect x=\"7\"  y=\"19\" width=\"2\" height=\"2\"/>\n  <rect x=\"11\" y=\"19\" width=\"2\" height=\"2\"/>\n  <rect x=\"15\" y=\"19\" width=\"2\" height=\"2\"/>\n</svg>\n```\n\n(Perfectly symmetric chip: body + 12 pins, 3 per side, even 4-unit\nspacing, rx=1 rounded corner, all in brand `#e6edf3`. This is the\nexact `adom-tsci` icon — taken from the repo's `src/assets/icon.svg`.)\n\nWhen the page loads from `https://<workspace>.adom.cloud/proxy/<port>/`,\nthe browser fetches `/proxy/<port>/favicon.svg`, your app returns the\nSVG, Hydrogen syncs it onto the tab, and the icon stays put.\n\n**Rules for the favicon file:**\n\n- **Square `viewBox`** (e.g. `0 0 64 64` or `0 0 24 24`).\n- **Self-contained SVG** — no external fonts, no `xlink:href` to\n  other files, no embedded images.\n- **Bold, filled shapes over thin strokes** — the tab bar is\n  ~16–20 px so details get lost.\n- **Transparent background** is fine, the tab bar shows through.\n- **Brand color in the SVG itself** if you want a colored icon. MDI\n  icons can't be tinted.\n\n## Placeholder-only case: `mdi:<name>` / `data:...` via add-tab\n\nIf the page you're loading **doesn't set its own favicon**, or if\nyou're creating a tab that navigates to about:blank or similar, the\n`--display-icon` placeholder is all you get — and it will stick.\n\nExamples where this is actually useful:\n\n- **A tab that loads an external site with no favicon.** Rare in\n  practice; most real sites have a favicon.\n- **A stopgap during the ~100ms the page takes to load** — the user\n  sees the placeholder, then it swaps to the page's favicon.\n- **When you don't own the page** — you can't inject a `<link>`, so\n  the placeholder is your only control point, knowing it's about to\n  get overridden.\n\n### CLI\n\n```bash\nadom-cli hydrogen workspace add-tab \\\n  --panel-id <leaf-id> \\\n  --panel-type \"adom/a1b2c3d4-0031-4000-a000-000000000031\" \\\n  --display-name \"My App\" \\\n  --display-icon \"mdi:chip\" \\\n  --initial-state '{\"url\":\"https://example.com/\"}'\n```\n\n### HTTP API\n\n```bash\ncurl -s -X POST \\\n  -H \"X-Api-Key: $API_KEY\" $TARGET_HEADER \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"panelId\": \"<leaf-id>\",\n    \"panelType\": \"adom/a1b2c3d4-0031-4000-a000-000000000031\",\n    \"displayName\": \"My App\",\n    \"displayIcon\": \"mdi:chip\",\n    \"initialState\": { \"url\": \"https://example.com/\" }\n  }' \\\n  \"https://hydrogen.adom.inc/api/workspaces/editor/$OWNER/$REPO/current/tabs\"\n```\n\n### Two placeholder formats\n\n| Format | When to use | Example |\n|---|---|---|\n| `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` |\n| `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...` |\n\n**Again: both are placeholders. The page's favicon overrides them\nboth as soon as it loads.**\n\n### Common MDI icons used in existing Adom tabs\n\n| Icon | Typical use |\n|---|---|\n| `mdi:chip` | chips, ICs, boards, PCB/tscircuit tools |\n| `mdi:eye` | viewers (AV) |\n| `mdi:bug` | bug-filing tools |\n| `mdi:chart-line` | visualizers |\n| `mdi:monitor-cellphone` | demo/desktop tools |\n| `mdi:github` | repo browsers |\n| `mdi:book-open-variant` | docs viewers |\n| `mdi:cart` | commerce tools |\n| `mdi:robot` | automation / AI |\n| `mdi:folder` | file explorers |\n| `mdi:code-tags` | code editors |\n| `mdi:palette` | design tools |\n| `mdi:camera` | screenshot tools |\n\nFull catalog: https://icon-sets.iconify.design/mdi/.\n\n## Building a custom SVG data URL by hand\n\nWhen you need a one-off custom icon and don't want to ship a file,\nyou can build a `data:` URL inline:\n\n```bash\nSVG='<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>'\nB64=$(echo -n \"$SVG\" | base64 -w0)\nDATA_URL=\"data:image/svg+xml;base64,$B64\"\n\nadom-cli hydrogen workspace add-tab \\\n  --panel-id <leaf-id> \\\n  --panel-type \"adom/a1b2c3d4-0031-4000-a000-000000000031\" \\\n  --display-name \"My App\" \\\n  --display-icon \"$DATA_URL\" \\\n  --initial-state '{\"url\":\"about:blank\"}'\n```\n\n`base64 -w0` (no line wrapping) is important — newlines inside the\nvalue break the JSON it gets embedded in.\n\nThis is still a **placeholder**. If the tab ever navigates to a page\nwith its own favicon, the placeholder is lost.\n\n## Troubleshooting\n\n### \"I pass `mdi:chip` but the tab shows a different icon\"\n\nYour loaded page has a favicon that overrode the placeholder.\nOptions:\n\n- **Best:** serve your own favicon from your app (see Real mechanism\n  above). Pick an SVG that matches the brand/icon you want. Hydrogen\n  will sync it onto the tab.\n- **Acceptable:** if you don't control the page, you can re-set the\n  `displayIcon` after load with another `add-tab`/PATCH call — but\n  the next navigation will override it again.\n\n### \"My placeholder flickers for a moment then reverts\"\n\nThat's the expected behavior — the placeholder shows until the page\nfinishes loading, then Hydrogen swaps in the page's favicon.\n\n### \"My tab has no icon at all\"\n\nThe loaded page has no `<link rel=\"icon\">` **and** no\n`--display-icon` was passed. Hydrogen falls back to the default Web\nView icon (a globe). Add a favicon to your served HTML.\n\n### \"I pass a `data:image/svg+xml;base64,...` and it's broken\"\n\nUsually a base64 encoding issue:\n\n- Use `base64 -w0` (no wrap) so the output is a single line.\n- Make sure the raw SVG has no unescaped newlines or tabs that break\n  the JSON wrapper.\n- The SVG must be self-contained: no `xlink:href` to other files, no\n  `@import` in `<style>`, no embedded `<image>` pointing at URLs.\n\n### \"I set the tab to `mdi:chip` but `workspace tabs` shows a data URL\"\n\nNormal. Hydrogen rewrites `panelState.displayIcon` after page load\nwith whatever the page's favicon was (usually a data URL because\nsome sites inline their favicons). This is the override in action.\n\n## How the `adom-tsci` app handles this\n\n1. `src/cli/start.rs` passes `--display-icon \"mdi:chip\"` to\n   `adom-cli hydrogen workspace add-tab` — this shows the MDI chip\n   icon for ~100ms while the page loads.\n2. `src/assets/shell.html` has\n   `<link rel=\"icon\" type=\"image/svg+xml\" href=\"favicon.svg\">`.\n3. `src/server/mod.rs` serves the favicon at `/favicon.svg` with the\n   content of `src/assets/icon.svg` — a teal-colored (`#00b8b0`)\n   custom SVG chip.\n4. When the shell loads, Hydrogen fetches `favicon.svg`, gets the\n   teal chip SVG, and writes that into `panelState.displayIcon`.\n5. The tab now **permanently** shows the teal chip — because every\n   future reload re-serves the same favicon.\n\nThe MDI `mdi:chip` placeholder and the served `favicon.svg` are\nvisually similar by design — the transition is invisible. If they\nwere different, you'd see a brief flash.\n\n## See also\n\n- `~/.claude/skills/adom/guides/adom-workspace-control.md` — the full\n  workspace API reference that documents `displayIcon` as a\n  `panelState` field (but buries the favicon-override behavior).\n- `~/.claude/skills/adom/webview/SKILL.md` — Web View panel actions\n  (navigate, refresh, setHeaderHidden, setProxyEnabled).\n- `~/.claude/skills/adom/guides/app-creator.md` — app conventions\n  including the `docs/icon.svg` brand-icon requirement.\n- [iconify MDI catalog](https://icon-sets.iconify.design/mdi/) — the\n  searchable list of every `mdi:<name>`.",
  "author": {
    "name": "Kyle Bergstedt",
    "email": "kyle@adom.inc"
  },
  "visibility": {
    "public": true
  },
  "hero": null,
  "sample_prompts": [],
  "discovery_triggers": [],
  "discovery_pitch": null,
  "metadata": {},
  "created_at": "2026-05-28T05:29:49.954Z",
  "updated_at": "2026-05-28T05:29:49.954Z",
  "sub_skills": [],
  "parent_app": null
}