refactor(web): one icon system — vendor the settings glyphs, drop lucide-solid #267

Merged
dominik.polakovics merged 1 commit from afk/199 into main 2026-08-03 02:24:54 +02:00

Closes #199

Consolidates the SPA back to a single icon mechanism: the vendored Icon component (ADR-0019). The settings rework (#198) had introduced lucide-solid as a package dependency for its section/category icons, so the app was carrying two delivery mechanisms for the same Lucide designs.

What changed

  • Icon.tsx — twelve glyphs vendored into the IconName union and GLYPHS map: bell, bot, calendar-clock, container, folder-input, lock-keyhole, plane-landing, plug, settings-2, triangle-alert, chevron-left, chevron-right. Every child is a path/rect/circle — all in the file's known-tag set, so no <line>-style workaround was needed. The component's API and <svg> attributes are untouched.
  • SettingsCategory.icon — now an IconName string instead of a component reference, so the shared layout renders it. The LucideProps-typed SettingsIcon alias is gone and the two registries no longer import anything to name an icon.
  • SettingsLayout.tsx — the row icon and both chevrons render through Icon at the same sizes and classes (.settings-index-icon, .settings-index-chevron stay load-bearing for settings-ia.css). The <svg> stays aria-hidden; the accessible name stays on the enclosing control.
  • SettingsLayout.test.tsx — the stub-icon factory is dropped (with real glyphs rendering there is nothing to stub) and the fixtures use real icon names. The row-icon assertion now pins size 20 and aria-hidden rather than a stub's data-icon attribute; every other assertion is unchanged and still passes.
  • package.json / package-lock.jsonlucide-solid removed.
  • ADR-0019 — a short note recording that the temporary package usage was folded back in. The decision itself is upheld, not superseded.

No icon choice changed: every category keeps the glyph it had.

Verification

Run locally against a Node 24 toolchain (the worktree shipped without one, so it was bootstrapped):

gate result
npm ci clean — confirms package.json/lockfile stay in sync without the dependency
npm run lint pass
npm run format:check pass
npm test 982 tests / 67 files, all passing
npm run build (tsc --noEmit && vite build) pass

Acceptance criteria checked directly rather than by eye:

  • grep -rn lucide web/src web/e2e → only the ISC attribution comment in Icon.tsx.
  • npm ls lucide-solid → empty; grep -c lucide dist/assets/*.js0. The new glyph paths are inlined in the built bundle.
  • Glyph fidelity: the lucide-solid@1.28.0 tarball was fetched and each vendored glyph diffed against the iconNode array the package actually rendered. All twelve are byte-identical, and Icon's <svg> attributes match the package's defaultAttributes exactly (viewBox, fill="none", stroke="currentColor", stroke-width 2, round caps/joins) — so currentColor inheritance and light/dark behavior are unchanged by construction.

One knowing difference, flagged

git-branch (the Branches category) was already vendored — from an earlier Lucide revision than the one lucide-solid@1.28.0 shipped. The old design draws the stem and the arc as two elements; the current upstream design merges them into one path with slightly different arc endpoints. Both are Lucide's own git-branch glyph.

The issue's brief calls git-branch out as already vendored, so this keeps it as-is. That is also the better outcome: the Branches icon now matches the git-branch the chat header already draws (ChatHeader.tsx), which is exactly the one-icon-system consistency this change exists for. Changing the vendored glyph instead would have altered an icon outside the settings UI, beyond this issue's scope.

Not run locally

The Go half of CI (go build -tags ui, go test -tags ui, golangci-lint) — no Go toolchain in this environment. The diff is web + docs only and touches nothing the Go side reads beyond web/dist, which builds cleanly above. CI covers it.

Closes #199 Consolidates the SPA back to a single icon mechanism: the vendored `Icon` component (ADR-0019). The settings rework (#198) had introduced `lucide-solid` as a package dependency for its section/category icons, so the app was carrying two delivery mechanisms for the same Lucide designs. ## What changed - **`Icon.tsx`** — twelve glyphs vendored into the `IconName` union and `GLYPHS` map: `bell`, `bot`, `calendar-clock`, `container`, `folder-input`, `lock-keyhole`, `plane-landing`, `plug`, `settings-2`, `triangle-alert`, `chevron-left`, `chevron-right`. Every child is a `path`/`rect`/`circle` — all in the file's known-tag set, so no `<line>`-style workaround was needed. The component's API and `<svg>` attributes are untouched. - **`SettingsCategory.icon`** — now an `IconName` string instead of a component reference, so the shared layout renders it. The `LucideProps`-typed `SettingsIcon` alias is gone and the two registries no longer import anything to name an icon. - **`SettingsLayout.tsx`** — the row icon and both chevrons render through `Icon` at the same sizes and classes (`.settings-index-icon`, `.settings-index-chevron` stay load-bearing for `settings-ia.css`). The `<svg>` stays `aria-hidden`; the accessible name stays on the enclosing control. - **`SettingsLayout.test.tsx`** — the stub-icon factory is dropped (with real glyphs rendering there is nothing to stub) and the fixtures use real icon names. The row-icon assertion now pins size 20 *and* `aria-hidden` rather than a stub's `data-icon` attribute; every other assertion is unchanged and still passes. - **`package.json` / `package-lock.json`** — `lucide-solid` removed. - **ADR-0019** — a short note recording that the temporary package usage was folded back in. The decision itself is upheld, not superseded. No icon *choice* changed: every category keeps the glyph it had. ## Verification Run locally against a Node 24 toolchain (the worktree shipped without one, so it was bootstrapped): | gate | result | | --- | --- | | `npm ci` | clean — confirms package.json/lockfile stay in sync without the dependency | | `npm run lint` | pass | | `npm run format:check` | pass | | `npm test` | **982 tests / 67 files, all passing** | | `npm run build` (`tsc --noEmit && vite build`) | pass | Acceptance criteria checked directly rather than by eye: - `grep -rn lucide web/src web/e2e` → only the ISC attribution comment in `Icon.tsx`. - `npm ls lucide-solid` → empty; `grep -c lucide dist/assets/*.js` → `0`. The new glyph paths are inlined in the built bundle. - **Glyph fidelity**: the `lucide-solid@1.28.0` tarball was fetched and each vendored glyph diffed against the `iconNode` array the package actually rendered. All twelve are byte-identical, and `Icon`'s `<svg>` attributes match the package's `defaultAttributes` exactly (viewBox, `fill="none"`, `stroke="currentColor"`, stroke-width 2, round caps/joins) — so `currentColor` inheritance and light/dark behavior are unchanged by construction. ## One knowing difference, flagged `git-branch` (the Branches category) was **already** vendored — from an earlier Lucide revision than the one `lucide-solid@1.28.0` shipped. The old design draws the stem and the arc as two elements; the current upstream design merges them into one path with slightly different arc endpoints. Both are Lucide's own git-branch glyph. The issue's brief calls `git-branch` out as already vendored, so this keeps it as-is. That is also the better outcome: the Branches icon now matches the `git-branch` the chat header already draws (`ChatHeader.tsx`), which is exactly the one-icon-system consistency this change exists for. Changing the vendored glyph instead would have altered an icon outside the settings UI, beyond this issue's scope. ## Not run locally The Go half of CI (`go build -tags ui`, `go test -tags ui`, `golangci-lint`) — no Go toolchain in this environment. The diff is web + docs only and touches nothing the Go side reads beyond `web/dist`, which builds cleanly above. CI covers it.
refactor(web): one icon system — vendor the settings glyphs, drop lucide-solid
All checks were successful
ci / native (pull_request) Successful in 8m27s
798359793d
The settings rework (#198) introduced `lucide-solid` as a package dependency
for its section/category icons, leaving the SPA with two delivery mechanisms
for the same Lucide designs: the vendored `Icon` component that ADR-0019 pins,
and per-icon deep imports from the package. This folds the second one back
into the first.

Twelve glyphs move into `Icon`'s map and `IconName` union — bell, bot,
calendar-clock, container, folder-input, lock-keyhole, plane-landing, plug,
settings-2, triangle-alert, chevron-left, chevron-right — copied verbatim from
upstream Lucide (ISC, attribution already in the component header). Each child
is a path/rect/circle, all in the file's known-tag set, so no `<line>`-style
workaround was needed.

`SettingsCategory.icon` becomes an `IconName` string rather than a component
reference, so the shared layout renders it: the registries no longer import
anything to name an icon, and the `LucideProps`-typed `SettingsIcon` alias is
gone. The layout's own chevrons become `Icon` usages at the same sizes and
classes, the `<svg>` stays aria-hidden, and the accessible name stays on the
enclosing control. The layout test drops its stub-icon factory — with real
glyphs rendering there is nothing left to stub — and pins the row icon's size
and aria-hidden instead of a stub's data attribute.

The web app's dependency set is back to solid-js + @solidjs/router.

Verified against the removed package rather than by eye: all twelve vendored
glyphs are byte-identical to the `iconNode` arrays lucide-solid@1.28.0 shipped,
and `Icon`'s `<svg>` attributes match its `defaultAttributes` exactly. The one
knowing difference is Branches' `git-branch`, which was already vendored from
an earlier Lucide revision — it now matches the git-branch the chat header
already draws, which is the consistency this change is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

Validated against ci / native (pull_request) — green in 8m27s, and that job is unconditional (no path gate), running the full SPA gate (npm ci, lint, format:check, test, build) plus go build/test -tags ui and golangci-lint against the embedded dist. Diff matches issue #199's brief exactly: all twelve glyphs vendored into IconName/GLYPHS, SettingsCategory.icon retyped to IconName, SettingsLayout renders row icon + both chevrons through Icon at the same sizes and load-bearing classes, lucide-solid dropped from package.json and the lockfile, ADR-0019 noted rather than superseded. grep confirms no lucide reference survives outside the ISC attribution comment in Icon.tsx, and no SettingsIcon or data-icon reference is left dangling. No drive-by changes.

[autoland] verdict: pass Validated against ci / native (pull_request) — green in 8m27s, and that job is unconditional (no path gate), running the full SPA gate (npm ci, lint, format:check, test, build) plus go build/test -tags ui and golangci-lint against the embedded dist. Diff matches issue #199's brief exactly: all twelve glyphs vendored into IconName/GLYPHS, SettingsCategory.icon retyped to IconName, SettingsLayout renders row icon + both chevrons through Icon at the same sizes and load-bearing classes, lucide-solid dropped from package.json and the lockfile, ADR-0019 noted rather than superseded. grep confirms no lucide reference survives outside the ISC attribution comment in Icon.tsx, and no SettingsIcon or data-icon reference is left dangling. No drive-by changes.
Sign in to join this conversation.
No reviewers
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
Cloonar/coding-lab!267
No description provided.