Shared SVG icon mechanism + embed the provided icon set across Watch/Star/Fork/Clone/bell/theme controls #465

Closed
opened 2026-09-13 15:27:24 +00:00 by crueber · 15 comments
Owner

What's requested

Add a shared, embedded SVG icon mechanism for the repo header controls and the site chrome, and wire the 10 provided icons (delivered as files, embedded verbatim — see "Icon assets") into the six controls: Watch toggle, Star toggle, Fork pill, Clone trigger, notification bell, and theme toggle. Every control gets a real icon with an on/off state mapping where one exists; all emoji/unicode glyphs go away.

Icon files are provided at /tmp/svg-icons/*.svg (all sized 1em×1em, all paint via fill="currentColor" / stroke="currentColor" — they inherit text color and scale with font size, which is what makes one shared mechanism possible):

  • watch-on.svg / watch-off.svg (eye / closed eye, viewBox 0 0 16 16)
  • star-on.svg / star-off.svg (filled / outline star, viewBox 0 0 24 24)
  • fork.svg (single icon, viewBox 0 0 1200 1200)
  • clone.svg (cloud-download, viewBox 0 0 1024 1024)
  • notify-on.svg / notify-off.svg (bell, viewBox 0 0 1024 1024)
  • light-mode.svg / dark-mode.svg (sun / moon, viewBox 0 0 24 24)

Evidence (current tree, SHA 06c9743)

Six controls, five of them text/emoji glyphs today:

Control File:line Current rendering
Star toggle web/src/pages/Repo.jsx:369 unicode star glyph (★), no on/off distinction
Watch toggle web/src/pages/Repo.jsx:214 eye emoji glyph, off-style visually (color emoji)
Fork pill web/src/pages/Repo.jsx:636 {n} Fork — no icon at all
Clone trigger web/src/pages/Repo.jsx:99 Clone — label-only <summary>
Notification bell web/src/components/NotificationTray.jsx:88 <span></span> — emoji; unread badge is a separate absolute-positioned circle
Theme toggle web/src/App.jsx:113-121 sun/moon unicode glyphs (☀ / ☾) via <Show>

The header action strip already speaks one idiom (issue #447: btn px-2 py-1 text-sm, count LEFT of label). The icons must compose into that idiom, not redefine it.

Architecture notes

Shared icon mechanism (do this once, consume everywhere). Create web/src/lib/icons.jsx exporting one component, e.g.:

import Icon from "../lib/icons.jsx";
<Icon name="watch-on" />  // renders the raw <svg> inline, class from caller
  • The 10 SVG files are embedded as inline JSX (either a Record<string, JSX> map of raw svg bodies or a vite raw-import ?raw + innerHTML); keep each file's viewBox as-is so mixed viewport sizes (16/24/512/1024/1200 units) all scale through the existing width="1em" height="1em" attributes.
  • Keep fill/stroke="currentColor" — no color literals in the icon layer; state coloring comes from the control's own classes (primary on toggles is the existing on-state signal).
  • Size/spacing (align-baseline, margin-right, etc.) belongs to the consumer's classes or a shared .btn-icon-style rule in web/css/wal.css, not per-icon CSS. Recommend one utility (e.g. icon class: display:inline-block; vertical-align:-0.125em; plus a flex gap on the buttons) so glyphs and labels align identically across all six controls.
  • Server-side count payloads are untouched — icons ride data already in hand.

Placement + state mapping per control:

Control Icon(s) State mapping
Watch toggle (WatchToggle) watch-on / watch-off watching → watch-on; not watching → watch-off. Keep classList={{ primary: watching }} + aria-pressed as the other half of the state signal.
Star toggle (StarToggle) star-on / star-off starred → star-on; not → star-off. Same pairing with primary/aria-pressed.
Fork pill fork static (it's a link, not a toggle); count stays left of the label, icon left of the count.
Clone trigger (CloneMenu <summary>) clone static; icon left of the "Clone" label inside the existing <summary>.
Notification bell (NotificationTray trigger) notify-on / notify-off unread count > 0 → notify-on; 0 → notify-off. The unread badge circle stays as-is, positioned over the icon (no idiom change — #446's badge direction was already settled as absorbed).
Theme toggle (App.jsx) light-mode / dark-mode the existing <Show> flips the glyph: theme() === "dark" → show dark-mode (moon, action = go light is the current semantics — preserve the existing semantics exactly; planner's call if a swap reads better, note it). Replace ☀/☾ spans; keep aria-hidden on the icon.

Non-BMP note for the implementer: the Watch eye and bell emoji are 4-byte characters — removing them also removes a latent Forgejo-MySQL charset footgun from any future server-side echo of these strings.

Acceptance criteria

  • web/src/lib/icons.jsx (or equivalent) exists; all six controls import the one shared mechanism — no inline <svg> duplicated per page, no emoji/unicode glyph left in any of the six controls (grep for the five old glyphs in web/src comes back clean)
  • The 10 provided SVGs are embedded verbatim (paths/viewBoxes unmodified); no icon fetches at runtime
  • State mappings land as specified: Watch on/off, Star on/off, bell on/off, theme sun/moon; Fork and Clone static
  • Toggle state remains visually doubled (icon swap + primary class) so the on/off state is not color-only
  • Header action strip metrics from #447 unchanged: same btn px-2 py-1 text-sm pills, counts still LEFT of labels, one consistent gap mechanism (see #463 — coordinate, don't re-style)
  • Bell unread badge still overlays the icon correctly at both glyph states; aria-live and aria-label behavior unchanged
  • aria-hidden="true" on decorative icons; aria-pressed/aria-labels on toggles unchanged in text (they are the accessible state signal)
  • Icons align with labels at both text-sm (header pills) and default (chrome buttons) sizes; no vertical misalignment between 1em icons of differing viewBoxes
## What's requested Add a shared, embedded SVG icon mechanism for the repo header controls and the site chrome, and wire the 10 provided icons (delivered as files, embedded verbatim — see "Icon assets") into the six controls: Watch toggle, Star toggle, Fork pill, Clone trigger, notification bell, and theme toggle. Every control gets a real icon with an on/off state mapping where one exists; all emoji/unicode glyphs go away. Icon files are provided at `/tmp/svg-icons/*.svg` (all sized `1em`×`1em`, all paint via `fill="currentColor"` / `stroke="currentColor"` — they inherit text color and scale with font size, which is what makes one shared mechanism possible): - `watch-on.svg` / `watch-off.svg` (eye / closed eye, viewBox 0 0 16 16) - `star-on.svg` / `star-off.svg` (filled / outline star, viewBox 0 0 24 24) - `fork.svg` (single icon, viewBox 0 0 1200 1200) - `clone.svg` (cloud-download, viewBox 0 0 1024 1024) - `notify-on.svg` / `notify-off.svg` (bell, viewBox 0 0 1024 1024) - `light-mode.svg` / `dark-mode.svg` (sun / moon, viewBox 0 0 24 24) ## Evidence (current tree, SHA 06c9743) Six controls, five of them text/emoji glyphs today: | Control | File:line | Current rendering | |---|---|---| | Star toggle | `web/src/pages/Repo.jsx:369` | unicode star glyph (`★`), no on/off distinction | | Watch toggle | `web/src/pages/Repo.jsx:214` | eye emoji glyph, off-style visually (color emoji) | | Fork pill | `web/src/pages/Repo.jsx:636` | `{n} Fork` — no icon at all | | Clone trigger | `web/src/pages/Repo.jsx:99` | `Clone` — label-only `<summary>` | | Notification bell | `web/src/components/NotificationTray.jsx:88` | `<span></span>` — emoji; unread badge is a separate absolute-positioned circle | | Theme toggle | `web/src/App.jsx:113-121` | sun/moon unicode glyphs (`☀` / `☾`) via `<Show>` | The header action strip already speaks one idiom (issue #447: `btn px-2 py-1 text-sm`, count LEFT of label). The icons must compose into that idiom, not redefine it. ## Architecture notes **Shared icon mechanism (do this once, consume everywhere).** Create `web/src/lib/icons.jsx` exporting one component, e.g.: ```jsx import Icon from "../lib/icons.jsx"; <Icon name="watch-on" /> // renders the raw <svg> inline, class from caller ``` - The 10 SVG files are embedded as inline JSX (either a `Record<string, JSX>` map of raw svg bodies or a vite raw-import `?raw` + `innerHTML`); keep each file's `viewBox` as-is so mixed viewport sizes (16/24/512/1024/1200 units) all scale through the existing `width="1em" height="1em"` attributes. - Keep `fill`/`stroke="currentColor"` — no color literals in the icon layer; state coloring comes from the control's own classes (`primary` on toggles is the existing on-state signal). - Size/spacing (`align-baseline`, `margin-right`, etc.) belongs to the consumer's classes or a shared `.btn-icon`-style rule in `web/css/wal.css`, not per-icon CSS. Recommend one utility (e.g. `icon` class: `display:inline-block; vertical-align:-0.125em;` plus a flex gap on the buttons) so glyphs and labels align identically across all six controls. - Server-side count payloads are untouched — icons ride data already in hand. **Placement + state mapping per control:** | Control | Icon(s) | State mapping | |---|---|---| | Watch toggle (`WatchToggle`) | `watch-on` / `watch-off` | watching → `watch-on`; not watching → `watch-off`. Keep `classList={{ primary: watching }}` + `aria-pressed` as the other half of the state signal. | | Star toggle (`StarToggle`) | `star-on` / `star-off` | starred → `star-on`; not → `star-off`. Same pairing with `primary`/`aria-pressed`. | | Fork pill | `fork` | static (it's a link, not a toggle); count stays left of the label, icon left of the count. | | Clone trigger (`CloneMenu` `<summary>`) | `clone` | static; icon left of the "Clone" label inside the existing `<summary>`. | | Notification bell (`NotificationTray` trigger) | `notify-on` / `notify-off` | unread count > 0 → `notify-on`; 0 → `notify-off`. The unread badge circle stays as-is, positioned over the icon (no idiom change — #446's badge direction was already settled as absorbed). | | Theme toggle (`App.jsx`) | `light-mode` / `dark-mode` | the existing `<Show>` flips the glyph: `theme() === "dark"` → show `dark-mode` (moon, action = go light is the current semantics — preserve the existing semantics exactly; planner's call if a swap reads better, note it). Replace `☀`/`☾` spans; keep `aria-hidden` on the icon. | **Non-BMP note for the implementer:** the Watch eye and bell emoji are 4-byte characters — removing them also removes a latent Forgejo-MySQL charset footgun from any future server-side echo of these strings. ## Acceptance criteria - [ ] `web/src/lib/icons.jsx` (or equivalent) exists; all six controls import the one shared mechanism — no inline `<svg>` duplicated per page, no emoji/unicode glyph left in any of the six controls (grep for the five old glyphs in `web/src` comes back clean) - [ ] The 10 provided SVGs are embedded verbatim (paths/viewBoxes unmodified); no icon fetches at runtime - [ ] State mappings land as specified: Watch on/off, Star on/off, bell on/off, theme sun/moon; Fork and Clone static - [ ] Toggle state remains visually doubled (icon swap + `primary` class) so the on/off state is not color-only - [ ] Header action strip metrics from #447 unchanged: same `btn px-2 py-1 text-sm` pills, counts still LEFT of labels, one consistent gap mechanism (see #463 — coordinate, don't re-style) - [ ] Bell unread badge still overlays the icon correctly at both glyph states; `aria-live` and `aria-label` behavior unchanged - [ ] `aria-hidden="true"` on decorative icons; `aria-pressed`/`aria-label`s on toggles unchanged in text (they are the accessible state signal) - [ ] Icons align with labels at both `text-sm` (header pills) and default (chrome buttons) sizes; no vertical misalignment between `1em` icons of differing viewBoxes
crueber added this to the v1 milestone 2026-09-13 15:27:40 +00:00
Author
Owner

Blocked: the 10 provided SVG files are not present — /tmp/svg-icons/ does not exist on this host, no *.svg anywhere in the repo or /tmp, and no attachments on this issue. Cannot embed verbatim per the spec without the files. Unblock by placing watch-on/off, star-on/off, fork, clone, notify-on/off, light-mode, dark-mode (+ the plus icon for #466) at /tmp/svg-icons/ or naming another path.

Blocked: the 10 provided SVG files are not present — /tmp/svg-icons/ does not exist on this host, no *.svg anywhere in the repo or /tmp, and no attachments on this issue. Cannot embed verbatim per the spec without the files. Unblock by placing watch-on/off, star-on/off, fork, clone, notify-on/off, light-mode, dark-mode (+ the plus icon for #466) at /tmp/svg-icons/ or naming another path.
Author
Owner

Icon file watch-on.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
	<path fill="currentColor" d="M8 3c3.218 0 5.788 3.235 6.67 4.501a.865.865 0 0 1 0 .998C13.789 9.765 11.219 13 8 13S2.21 9.765 1.329 8.499a.865.865 0 0 1 0-.998c.882-1.266 3.453-4.5 6.67-4.501m0 2a3 3 0 1 0 0 6a3 3 0 0 0 0-6" />
</svg>
Icon file `watch-on.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"> <path fill="currentColor" d="M8 3c3.218 0 5.788 3.235 6.67 4.501a.865.865 0 0 1 0 .998C13.789 9.765 11.219 13 8 13S2.21 9.765 1.329 8.499a.865.865 0 0 1 0-.998c.882-1.266 3.453-4.5 6.67-4.501m0 2a3 3 0 1 0 0 6a3 3 0 0 0 0-6" /> </svg> ```
Author
Owner

Icon file watch-off.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
	<path fill="currentColor" d="M13.21 6.386a1 1 0 0 1 1.687 1.054l-.05.09v.003l-.003.004l-.01.013l-.027.043l-.097.145c-.084.123-.207.294-.363.497c-.205.265-.473.586-.794.931a1 1 0 0 1 .279.28l1 1.5a1 1 0 1 1-1.664 1.109l-1-1.5a1 1 0 0 1-.05-.083c-.85.643-1.905 1.22-3.118 1.436V14a1 1 0 0 1-2 0v-2.092c-1.213-.215-2.269-.793-3.12-1.436q-.021.042-.048.083l-1 1.5a1 1 0 0 1-1.664-1.11l1-1.5a1 1 0 0 1 .278-.279a13 13 0 0 1-.793-.93a11 11 0 0 1-.46-.643l-.028-.043l-.009-.013l-.003-.004v-.002A1 1 0 1 1 2.847 6.47l.002.004l.016.024l.072.108a11 11 0 0 0 1.435 1.666C5.366 9.206 6.629 10 8 10s2.634-.794 3.627-1.729a11 11 0 0 0 1.434-1.666q.05-.072.073-.108l.016-.024l.002-.004z" />
</svg>
Icon file `watch-off.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"> <path fill="currentColor" d="M13.21 6.386a1 1 0 0 1 1.687 1.054l-.05.09v.003l-.003.004l-.01.013l-.027.043l-.097.145c-.084.123-.207.294-.363.497c-.205.265-.473.586-.794.931a1 1 0 0 1 .279.28l1 1.5a1 1 0 1 1-1.664 1.109l-1-1.5a1 1 0 0 1-.05-.083c-.85.643-1.905 1.22-3.118 1.436V14a1 1 0 0 1-2 0v-2.092c-1.213-.215-2.269-.793-3.12-1.436q-.021.042-.048.083l-1 1.5a1 1 0 0 1-1.664-1.11l1-1.5a1 1 0 0 1 .278-.279a13 13 0 0 1-.793-.93a11 11 0 0 1-.46-.643l-.028-.043l-.009-.013l-.003-.004v-.002A1 1 0 1 1 2.847 6.47l.002.004l.016.024l.072.108a11 11 0 0 0 1.435 1.666C5.366 9.206 6.629 10 8 10s2.634-.794 3.627-1.729a11 11 0 0 0 1.434-1.666q.05-.072.073-.108l.016-.024l.002-.004z" /> </svg> ```
Author
Owner

Icon file star-on.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
	<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
		<path d="M9 8c-1.667.667-5.4 2.7-7 5.5m9.5-2.5C9.167 12.333 4 16.4 2 22m10.5-7.5c-1.167 1.167-3.8 4.1-5 6.5" />
		<path fill="currentColor" d="m14.674 6.45l.673-3.285l2.225 2.51l3.027-.294l-1.768 3.062l1.743 2.639l-3.286-.673l-2.51 2.225l.19-3.156l-3.062-1.768z" />
	</g>
</svg>
Icon file `star-on.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> <path d="M9 8c-1.667.667-5.4 2.7-7 5.5m9.5-2.5C9.167 12.333 4 16.4 2 22m10.5-7.5c-1.167 1.167-3.8 4.1-5 6.5" /> <path fill="currentColor" d="m14.674 6.45l.673-3.285l2.225 2.51l3.027-.294l-1.768 3.062l1.743 2.639l-3.286-.673l-2.51 2.225l.19-3.156l-3.062-1.768z" /> </g> </svg> ```
Author
Owner

Icon file star-off.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
	<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8c-1.667.667-5.4 2.7-7 5.5m9.5-2.5C9.167 12.333 4 16.4 2 22m10.5-7.5c-1.167 1.167-3.8 4.1-5 6.5m7.174-14.55l.673-3.285l2.225 2.51l3.027-.294l-1.768 3.062l1.743 2.639l-3.286-.673l-2.51 2.225l.19-3.156l-3.062-1.768z" />
</svg>
Icon file `star-off.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8c-1.667.667-5.4 2.7-7 5.5m9.5-2.5C9.167 12.333 4 16.4 2 22m10.5-7.5c-1.167 1.167-3.8 4.1-5 6.5m7.174-14.55l.673-3.285l2.225 2.51l3.027-.294l-1.768 3.062l1.743 2.639l-3.286-.673l-2.51 2.225l.19-3.156l-3.062-1.768z" /> </svg> ```
Author
Owner

Icon file fork.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1200 1200">
	<path fill="currentColor" d="M42.203 935.926h186.061V763.958c0-54.408 26.559-114.484 77.32-164.729c50.762-50.242 126.065-104.842 249.904-191.527c124.394-87.076 199.565-135.567 233.346-165.807c33.78-30.24 30.882-25.376 30.882-69.388V0h147.863v172.507c0 66.078-27.619 132.54-80.093 179.516s-125.164 91.312-247.208 176.741c-122.601 85.82-195.159 140.381-230.651 175.512c-35.491 35.129-33.5 36.641-33.5 59.685v171.967h194.147L306.276 1200zm587.524 0h189.988V763.958c0-23.043 1.914-24.554-33.577-59.684c-23.477-23.237-65.093-56.146-124.76-99.809c7.49-5.281 13.418-9.555 21.333-15.095c43.674-30.571 75.183-51.648 107.816-73.777c41.578 31.395 73.875 58.12 99.652 83.637c50.763 50.242 77.397 110.319 77.397 164.729v171.968h190.22L893.801 1200z" />
</svg>
Icon file `fork.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1200 1200"> <path fill="currentColor" d="M42.203 935.926h186.061V763.958c0-54.408 26.559-114.484 77.32-164.729c50.762-50.242 126.065-104.842 249.904-191.527c124.394-87.076 199.565-135.567 233.346-165.807c33.78-30.24 30.882-25.376 30.882-69.388V0h147.863v172.507c0 66.078-27.619 132.54-80.093 179.516s-125.164 91.312-247.208 176.741c-122.601 85.82-195.159 140.381-230.651 175.512c-35.491 35.129-33.5 36.641-33.5 59.685v171.967h194.147L306.276 1200zm587.524 0h189.988V763.958c0-23.043 1.914-24.554-33.577-59.684c-23.477-23.237-65.093-56.146-124.76-99.809c7.49-5.281 13.418-9.555 21.333-15.095c43.674-30.571 75.183-51.648 107.816-73.777c41.578 31.395 73.875 58.12 99.652 83.637c50.763 50.242 77.397 110.319 77.397 164.729v171.968h190.22L893.801 1200z" /> </svg> ```
Author
Owner

Icon file clone.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024">
	<path fill="currentColor" d="M624 706.3h-74.1V464c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v242.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.7a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9" />
	<path fill="currentColor" d="M811.4 366.7C765.6 245.9 648.9 160 512.2 160S258.8 245.8 213 366.6C127.3 389.1 64 467.2 64 560c0 110.5 89.5 200 199.9 200H304c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8h-40.1c-33.7 0-65.4-13.4-89-37.7c-23.5-24.2-36-56.8-34.9-90.6c.9-26.4 9.9-51.2 26.2-72.1c16.7-21.3 40.1-36.8 66.1-43.7l37.9-9.9l13.9-36.6c8.6-22.8 20.6-44.1 35.7-63.4a245.6 245.6 0 0 1 52.4-49.9c41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.2c19.9 14 37.5 30.8 52.4 49.9c15.1 19.3 27.1 40.7 35.7 63.4l13.8 36.5l37.8 10C846.1 454.5 884 503.8 884 560c0 33.1-12.9 64.3-36.3 87.7a123.07 123.07 0 0 1-87.6 36.3H720c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h40.1C870.5 760 960 670.5 960 560c0-92.7-63.1-170.7-148.6-193.3" />
</svg>
Icon file `clone.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024"> <path fill="currentColor" d="M624 706.3h-74.1V464c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v242.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.7a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9" /> <path fill="currentColor" d="M811.4 366.7C765.6 245.9 648.9 160 512.2 160S258.8 245.8 213 366.6C127.3 389.1 64 467.2 64 560c0 110.5 89.5 200 199.9 200H304c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8h-40.1c-33.7 0-65.4-13.4-89-37.7c-23.5-24.2-36-56.8-34.9-90.6c.9-26.4 9.9-51.2 26.2-72.1c16.7-21.3 40.1-36.8 66.1-43.7l37.9-9.9l13.9-36.6c8.6-22.8 20.6-44.1 35.7-63.4a245.6 245.6 0 0 1 52.4-49.9c41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.2c19.9 14 37.5 30.8 52.4 49.9c15.1 19.3 27.1 40.7 35.7 63.4l13.8 36.5l37.8 10C846.1 454.5 884 503.8 884 560c0 33.1-12.9 64.3-36.3 87.7a123.07 123.07 0 0 1-87.6 36.3H720c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h40.1C870.5 760 960 670.5 960 560c0-92.7-63.1-170.7-148.6-193.3" /> </svg> ```
Author
Owner

Icon file notify-on.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024">
	<path fill="currentColor" d="M880 112c-3.8 0-7.7.7-11.6 2.3L292 345.9H128c-8.8 0-16 7.4-16 16.6v299c0 9.2 7.2 16.6 16 16.6h101.6c-3.7 11.6-5.6 23.9-5.6 36.4c0 65.9 53.8 119.5 120 119.5c55.4 0 102.1-37.6 115.9-88.4l408.6 164.2c3.9 1.5 7.8 2.3 11.6 2.3c16.9 0 32-14.2 32-33.2V145.2C912 126.2 897 112 880 112M344 762.3c-26.5 0-48-21.4-48-47.8c0-11.2 3.9-21.9 11-30.4l84.9 34.1c-2 24.6-22.7 44.1-47.9 44.1" />
</svg>
Icon file `notify-on.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024"> <path fill="currentColor" d="M880 112c-3.8 0-7.7.7-11.6 2.3L292 345.9H128c-8.8 0-16 7.4-16 16.6v299c0 9.2 7.2 16.6 16 16.6h101.6c-3.7 11.6-5.6 23.9-5.6 36.4c0 65.9 53.8 119.5 120 119.5c55.4 0 102.1-37.6 115.9-88.4l408.6 164.2c3.9 1.5 7.8 2.3 11.6 2.3c16.9 0 32-14.2 32-33.2V145.2C912 126.2 897 112 880 112M344 762.3c-26.5 0-48-21.4-48-47.8c0-11.2 3.9-21.9 11-30.4l84.9 34.1c-2 24.6-22.7 44.1-47.9 44.1" /> </svg> ```
Author
Owner

Icon file notify-off.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024">
	<path fill="currentColor" d="M880 112c-3.8 0-7.7.7-11.6 2.3L292 345.9H128c-8.8 0-16 7.4-16 16.6v299c0 9.2 7.2 16.6 16 16.6h101.7c-3.7 11.6-5.7 23.9-5.7 36.4c0 65.9 53.8 119.5 120 119.5c55.4 0 102.1-37.6 115.9-88.4l408.6 164.2c3.9 1.5 7.8 2.3 11.6 2.3c16.9 0 32-14.2 32-33.2V145.2C912 126.2 897 112 880 112M344 762.3c-26.5 0-48-21.4-48-47.8c0-11.2 3.9-21.9 11-30.4l84.9 34.1c-2 24.6-22.7 44.1-47.9 44.1m496 58.4L318.8 611.3l-12.9-5.2H184V417.9h121.9l12.9-5.2L840 203.3z" />
</svg>
Icon file `notify-off.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024"> <path fill="currentColor" d="M880 112c-3.8 0-7.7.7-11.6 2.3L292 345.9H128c-8.8 0-16 7.4-16 16.6v299c0 9.2 7.2 16.6 16 16.6h101.7c-3.7 11.6-5.7 23.9-5.7 36.4c0 65.9 53.8 119.5 120 119.5c55.4 0 102.1-37.6 115.9-88.4l408.6 164.2c3.9 1.5 7.8 2.3 11.6 2.3c16.9 0 32-14.2 32-33.2V145.2C912 126.2 897 112 880 112M344 762.3c-26.5 0-48-21.4-48-47.8c0-11.2 3.9-21.9 11-30.4l84.9 34.1c-2 24.6-22.7 44.1-47.9 44.1m496 58.4L318.8 611.3l-12.9-5.2H184V417.9h121.9l12.9-5.2L840 203.3z" /> </svg> ```
Author
Owner

Icon file light-mode.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024">
	<path fill="currentColor" fill-rule="evenodd" d="M548 818v126c0 8.837-7.163 16-16 16h-40c-8.837 0-16-7.163-16-16V818q23.768 2.464 36 2.464T548 818m205.251-115.66l89.096 89.095c6.248 6.248 6.248 16.38 0 22.627l-28.285 28.285c-6.248 6.248-16.379 6.248-22.627 0L702.34 753.25q18.548-15.064 27.198-23.713q8.649-8.65 23.713-27.198m-482.502 0q15.064 18.548 23.713 27.198q8.65 8.649 27.198 23.713l-89.095 89.096c-6.248 6.248-16.38 6.248-22.627 0l-28.285-28.285c-6.248-6.248-6.248-16.379 0-22.627zM512 278c129.235 0 234 104.765 234 234S641.235 746 512 746S278 641.235 278 512s104.765-234 234-234M206 476q-2.464 23.768-2.464 36T206 548H80c-8.837 0-16-7.163-16-16v-40c0-8.837 7.163-16 16-16zm738 0c8.837 0 16 7.163 16 16v40c0 8.837 7.163 16 16 16H818q2.464-23.768 2.464-36T818 476ZM814.062 180.653l28.285 28.285c6.248 6.248 6.248 16.379 0 22.627L753.25 320.66q-15.064-18.548-23.713-27.198q-8.65-8.649-27.198-23.713l89.095-89.096c6.248-6.248 16.38-6.248 22.627 0m-581.497 0l89.095 89.096q-18.548 15.064-27.198 23.713q-8.649 8.65-23.713 27.198l-89.096-89.095c-6.248-6.248-6.248-16.38 0-22.627l28.285-28.285c6.248-6.248 16.379-6.248 22.627 0M532 64c8.837 0 16 7.163 16 16v126q-23.768-2.464-36-2.464T476 206V80c0-8.837 7.163-16 16-16z" />
</svg>
Icon file `light-mode.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024"> <path fill="currentColor" fill-rule="evenodd" d="M548 818v126c0 8.837-7.163 16-16 16h-40c-8.837 0-16-7.163-16-16V818q23.768 2.464 36 2.464T548 818m205.251-115.66l89.096 89.095c6.248 6.248 6.248 16.38 0 22.627l-28.285 28.285c-6.248 6.248-16.379 6.248-22.627 0L702.34 753.25q18.548-15.064 27.198-23.713q8.649-8.65 23.713-27.198m-482.502 0q15.064 18.548 23.713 27.198q8.65 8.649 27.198 23.713l-89.095 89.096c-6.248 6.248-16.38 6.248-22.627 0l-28.285-28.285c-6.248-6.248-6.248-16.379 0-22.627zM512 278c129.235 0 234 104.765 234 234S641.235 746 512 746S278 641.235 278 512s104.765-234 234-234M206 476q-2.464 23.768-2.464 36T206 548H80c-8.837 0-16-7.163-16-16v-40c0-8.837 7.163-16 16-16zm738 0c8.837 0 16 7.163 16 16v40c0 8.837 7.163 16 16 16H818q2.464-23.768 2.464-36T818 476ZM814.062 180.653l28.285 28.285c6.248 6.248 6.248 16.379 0 22.627L753.25 320.66q-15.064-18.548-23.713-27.198q-8.65-8.649-27.198-23.713l89.095-89.096c6.248-6.248 16.38-6.248 22.627 0m-581.497 0l89.095 89.096q-18.548 15.064-27.198 23.713q-8.649 8.65-23.713 27.198l-89.096-89.095c-6.248-6.248-6.248-16.38 0-22.627l28.285-28.285c6.248-6.248 16.379-6.248 22.627 0M532 64c8.837 0 16 7.163 16 16v126q-23.768-2.464-36-2.464T476 206V80c0-8.837 7.163-16 16-16z" /> </svg> ```
Author
Owner

Icon file dark-mode.svg (verbatim from the user; 1em, currentColor):

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
	<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2">
		<path d="M12 3V2m0 20v-1m9-9h1M2 12h1m15.5-6.5L20 4M4 20l1.5-1.5M4 4l1.5 1.5m13 13L20 20" />
		<circle cx="12" cy="12" r="4" />
	</g>
</svg>
Icon file `dark-mode.svg` (verbatim from the user; `1em`, `currentColor`): ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"> <path d="M12 3V2m0 20v-1m9-9h1M2 12h1m15.5-6.5L20 4M4 20l1.5-1.5M4 4l1.5 1.5m13 13L20 20" /> <circle cx="12" cy="12" r="4" /> </g> </svg> ```
Author
Owner

Note: all 10 icon source files are posted as individual comments below (watch-on/off, star-on/off, fork, clone, notify-on/off, light-mode, dark-mode), each in an ```svg block — copy them verbatim into web/src/lib/icons.jsx. They are not all inline in the body; the comments are the source of truth for the exact paths.

Note: all 10 icon source files are posted as individual comments below (watch-on/off, star-on/off, fork, clone, notify-on/off, light-mode, dark-mode), each in an ```svg block — copy them verbatim into web/src/lib/icons.jsx. They are not all inline in the body; the comments are the source of truth for the exact paths.
Author
Owner

Fix PR: #475 (branch fix/issue-465).

Asset-source note (resolves the earlier blocker): the 10 icon files were taken from the issue comments 4783-4792, mirrored to /tmp/svg-icons/ (read-only source, unmodified), and transcribed verbatim into web/src/lib/icons.jsx — each keeps its shipped viewBox (16/24/1024/1200) and currentColor paint (mechanically diffed token-for-token against /tmp/svg-icons/).

What landed: one shared Icon component consumed by all six controls with the issue's state mapping (watch/star/bell pairs, theme Show kept, fork/clone static); all old glyphs gone from web/src; #447 metrics + #463 spacing intact; no new deps; law-12 decision in docs/go/12_web_ui.md. Tests: node --test 998/996/2 (+10 net new, the 2 failures pre-existing live-server smoke, identical on main); vite + esbuild green, icons verified in the bundle. Browser proof open (shared-daemon loopback guard — no private daemon per workspace rules). Do NOT merge from my side.

Fix PR: https://git.packden.us/crueber/walhub/pulls/475 (branch fix/issue-465). Asset-source note (resolves the earlier blocker): the 10 icon files were taken from the issue comments 4783-4792, mirrored to /tmp/svg-icons/ (read-only source, unmodified), and transcribed verbatim into web/src/lib/icons.jsx — each keeps its shipped viewBox (16/24/1024/1200) and currentColor paint (mechanically diffed token-for-token against /tmp/svg-icons/). What landed: one shared Icon component consumed by all six controls with the issue's state mapping (watch/star/bell pairs, theme Show kept, fork/clone static); all old glyphs gone from web/src; #447 metrics + #463 spacing intact; no new deps; law-12 decision in docs/go/12_web_ui.md. Tests: node --test 998/996/2 (+10 net new, the 2 failures pre-existing live-server smoke, identical on main); vite + esbuild green, icons verified in the bundle. Browser proof open (shared-daemon loopback guard — no private daemon per workspace rules). Do NOT merge from my side.
Author
Owner

Review of PR #475 (fix/issue-465, commit fff3d37) against #465 — verified in scratch worktree /tmp/pr475 (removed afterward); main worktree untouched (still clean on main). No browser (per instructions — node tests + bundle reasoning only; browser proof remains open as the PR itself notes).

(1) VERBATIM TRANSCRIPTION — PASS. Programmatically diffed all 12 d="..." path strings + all 10 viewBoxes in web/src/lib/icons.jsx against /tmp/svg-icons/*.svg: every path byte-identical, every viewBox preserved (16/16, 24/24 x3 incl. dark-mode, 1024 x5 incl. light-mode, 1200 fork). All paint via currentColor (12 occurrences); zero hex/rgb literals in code (the two scanner hits — "#465", "innerHTML" — are both in // comments). Clone's two paths wrapped in a JSX fragment — semantically identical. NOTE (non-blocking): the issue prose says light/dark-mode are both "viewBox 0 0 24 24", but shipped light-mode.svg is actually 0 0 1024 1024 (sun with fill-rule evenodd, kept correctly). Asset wins over prose — transcription is faithful to the source of truth. Also noted: both theme assets render sun-like as shipped; PR maps strictly by name (dark to dark-mode, light to light-mode) and documents this — correct call.

(2) SIX CONTROLS WIRED PER TABLE — PASS. Watch (Repo.jsx:215 Icon swaps on w().watching), Star (Repo.jsx:370 on s().viewer star state), Fork static icon left of count (Repo.jsx:662, icon before s().forks), Clone static left of label (Repo.jsx:100), bell on when unreadCount greater than 0 (NotificationTray.jsx:87), theme keeps existing Show when theme()==="dark" with dark-mode in branch / light-mode fallback (App.jsx:116-118). All import the one shared mechanism.

(3) OLD GLYPHS GONE — PASS. Fixed-string grep for the five old glyphs (eye-emoji, black-star, bell-emoji, sun, moon) plus the retired fork mark over web/src in the branch: zero hits.

(4) STATE DOUBLED — PASS. Watch/Star keep classList primary + aria-pressed alongside the icon swap; bell keeps badge + tray aria-label. Never color-only.

(5) #447 METRICS + #463 SPACING — PASS. btn px-2 py-1 text-sm classes byte-identical (icons added inside, no class edits); counts still LEFT of labels; .icon carries no margin (row gap-2 untouched). 390px re-based arithmetic (about 386px) re-verified in header-pills-447/header-gap-463/fork-pill-split-464 pins.

(6) BELL BADGE — PASS. Diff touches exactly one line (glyph to Icon); absolute overlay, aria-live polite, tray aria-label, outside-click/Esc all untouched.

(7) A11Y — PASS. aria-hidden="true" lives on the shared svg (covers all six, incl. theme where old spans carried it individually); toggle aria-pressed/aria-label/title texts unchanged. (Watch/Star glyphs were previously bare text with no aria-hidden — new state is strictly better.)

(8) .icon UTILITY — PASS (ui.css:82). inline-block 1em box, shrink-0 (flex safety), vertical-align -0.125em for non-flex contexts; .btn is inline-flex+gap so pills align via gap. No per-icon CSS; all six Icon usages are name-only (no class= overrides).

(9) NO RUNTIME FETCHES — PASS. No ?raw/innerHTML/dangerouslySetInnerHTML/fetch in icons.jsx (comment mention only); the one fetch( in Repo.jsx is the pre-existing setup.json call. Icons verified present in the vite bundle (all 12 path strings in dist/assets/*.js) with zero runtime requests.

(10) NO NEW DEPS; DOCS ACCURATE — PASS. package.json untouched (solid-js+router+marked+dompurify only, law 1 holds). Law-12 decision appended to docs/go/12_web_ui.md in the same commit; the entry's claims (counts, semantics, sun-like-asset note) all check out.

VERIFY RESULTS (scratch worktree, node_modules symlinked from main): node --test web/test/unit/*.test.js gives 998 total / 996 pass / 2 fail; the 2 failures are the pre-existing live-server smoke subtests, byte-identical on pristine main (same assertion pair, environment has no live Go server). vite build exit 0, .icon rule present in dist CSS, all icon data in dist JS bundle; esbuild exit 0.

Amusing footnote: my first attempt to post this comment with the literal old glyphs pasted in was rejected by the Forgejo MySQL backend (Error 1366 incorrect string value for 4-byte chars) — a live demonstration of the non-BMP footgun this PR removes.

MERGE RECOMMENDATION: ready to merge (browser proof still open per workspace loopback-guard constraints — same standing as #447/#450/#464 — but headless + bundle evidence is complete; no findings, nothing fixed, no push made).

Review of PR #475 (fix/issue-465, commit fff3d37) against #465 — verified in scratch worktree /tmp/pr475 (removed afterward); main worktree untouched (still clean on main). No browser (per instructions — node tests + bundle reasoning only; browser proof remains open as the PR itself notes). (1) VERBATIM TRANSCRIPTION — PASS. Programmatically diffed all 12 d="..." path strings + all 10 viewBoxes in web/src/lib/icons.jsx against /tmp/svg-icons/*.svg: every path byte-identical, every viewBox preserved (16/16, 24/24 x3 incl. dark-mode, 1024 x5 incl. light-mode, 1200 fork). All paint via currentColor (12 occurrences); zero hex/rgb literals in code (the two scanner hits — "#465", "innerHTML" — are both in // comments). Clone's two paths wrapped in a JSX fragment — semantically identical. NOTE (non-blocking): the issue prose says light/dark-mode are both "viewBox 0 0 24 24", but shipped light-mode.svg is actually 0 0 1024 1024 (sun with fill-rule evenodd, kept correctly). Asset wins over prose — transcription is faithful to the source of truth. Also noted: both theme assets render sun-like as shipped; PR maps strictly by name (dark to dark-mode, light to light-mode) and documents this — correct call. (2) SIX CONTROLS WIRED PER TABLE — PASS. Watch (Repo.jsx:215 Icon swaps on w().watching), Star (Repo.jsx:370 on s().viewer star state), Fork static icon left of count (Repo.jsx:662, icon before s().forks), Clone static left of label (Repo.jsx:100), bell on when unreadCount greater than 0 (NotificationTray.jsx:87), theme keeps existing Show when theme()==="dark" with dark-mode in branch / light-mode fallback (App.jsx:116-118). All import the one shared mechanism. (3) OLD GLYPHS GONE — PASS. Fixed-string grep for the five old glyphs (eye-emoji, black-star, bell-emoji, sun, moon) plus the retired fork mark over web/src in the branch: zero hits. (4) STATE DOUBLED — PASS. Watch/Star keep classList primary + aria-pressed alongside the icon swap; bell keeps badge + tray aria-label. Never color-only. (5) #447 METRICS + #463 SPACING — PASS. btn px-2 py-1 text-sm classes byte-identical (icons added inside, no class edits); counts still LEFT of labels; .icon carries no margin (row gap-2 untouched). 390px re-based arithmetic (about 386px) re-verified in header-pills-447/header-gap-463/fork-pill-split-464 pins. (6) BELL BADGE — PASS. Diff touches exactly one line (glyph to Icon); absolute overlay, aria-live polite, tray aria-label, outside-click/Esc all untouched. (7) A11Y — PASS. aria-hidden="true" lives on the shared svg (covers all six, incl. theme where old spans carried it individually); toggle aria-pressed/aria-label/title texts unchanged. (Watch/Star glyphs were previously bare text with no aria-hidden — new state is strictly better.) (8) .icon UTILITY — PASS (ui.css:82). inline-block 1em box, shrink-0 (flex safety), vertical-align -0.125em for non-flex contexts; .btn is inline-flex+gap so pills align via gap. No per-icon CSS; all six Icon usages are name-only (no class= overrides). (9) NO RUNTIME FETCHES — PASS. No ?raw/innerHTML/dangerouslySetInnerHTML/fetch in icons.jsx (comment mention only); the one fetch( in Repo.jsx is the pre-existing setup.json call. Icons verified present in the vite bundle (all 12 path strings in dist/assets/*.js) with zero runtime requests. (10) NO NEW DEPS; DOCS ACCURATE — PASS. package.json untouched (solid-js+router+marked+dompurify only, law 1 holds). Law-12 decision appended to docs/go/12_web_ui.md in the same commit; the entry's claims (counts, semantics, sun-like-asset note) all check out. VERIFY RESULTS (scratch worktree, node_modules symlinked from main): node --test web/test/unit/*.test.js gives 998 total / 996 pass / 2 fail; the 2 failures are the pre-existing live-server smoke subtests, byte-identical on pristine main (same assertion pair, environment has no live Go server). vite build exit 0, .icon rule present in dist CSS, all icon data in dist JS bundle; esbuild exit 0. Amusing footnote: my first attempt to post this comment with the literal old glyphs pasted in was rejected by the Forgejo MySQL backend (Error 1366 incorrect string value for 4-byte chars) — a live demonstration of the non-BMP footgun this PR removes. MERGE RECOMMENDATION: ready to merge (browser proof still open per workspace loopback-guard constraints — same standing as #447/#450/#464 — but headless + bundle evidence is complete; no findings, nothing fixed, no push made).
Author
Owner

Fixed by PR #475 (review clean — all 10 icons byte-identical to source, all 6 controls per table, #447/#463 intact), merged. Closing.

Fixed by PR #475 (review clean — all 10 icons byte-identical to source, all 6 controls per table, #447/#463 intact), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#465
No description provided.