Skip to content

Replace Unicode glyphs with vendored Bootstrap Icons - #128

Merged
samkeen merged 2 commits into
mainfrom
claude/ui-icons-exploration-y01pfa
Aug 1, 2026
Merged

Replace Unicode glyphs with vendored Bootstrap Icons#128
samkeen merged 2 commits into
mainfrom
claude/ui-icons-exploration-y01pfa

Conversation

@samkeen

@samkeen samkeen commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces hand-rolled inline SVG and Unicode text glyphs throughout the UI with a curated subset of Bootstrap Icons (v1.13.1), vendored at build time. This unifies the icon family, makes meanings explicit (e.g., "what does a PDF look like?"), and ensures consistency across the app.

Key Changes

  • Icon registry (ui/src/icons.ts): New module that maps semantic names to icon meanings — resourceIcon(class), foldChevron(open), folderIcon(open), directionIcon(direction) — so the answer to "what does a PDF look like in B2" is one place, not scattered across call sites.

  • Icon vendoring (ui/scripts/gen-icons.ts): Generator that lifts path data from the bootstrap-icons npm package into ui/src/icons.gen.ts. Runs at build time (not runtime) because the webview CSP forbids a CDN, and npm test runs off the source through node where Vite's ?raw imports don't resolve. The --check mode gates CI: a stale generated file fails the test suite.

  • Icon test suite (ui/src/icons.test.ts): Pure-logic tests (no DOM, no dependencies) that verify every semantic name resolves to a real icon, that markup stays the shape the panes assume, and that the vendored data carries no scripts or handlers.

  • Render layer (ui/src/render.ts): Replaces Unicode carets (/) and hand-rolled inline SVG with calls to icon() and sceneIcon(). Tree rows now have two explicit slots — fold chevron and thing icon — so files line up under their folder's icon rather than under the chevron.

  • Tree navigation (ui/src/treenav.ts): TreeFile.glyph (a string) becomes TreeFile.icon (an IconName), keeping the module DOM-free so node can run its test off the source.

  • Styling (ui/style.css): New .icon rule (flex, inline-block, vertical-align); .tree-caret and .tree-icon now flex containers that center their SVG children; removed font-size and text-align rules that applied to Unicode glyphs.

  • Shell and controls (ui/src/main.ts): Navigation buttons, the anomaly badge, and other chrome now use icon() instead of pasted SVG.

Notable Details

  • Semantic names, not icon names: Callers ask for resourceIcon("pdf") or foldChevron(true), not "file-earmark-pdf" or "chevron-down". A typo is a type error; adding an icon is a name in the generator's manifest plus just icons.

  • No color of its own: Every icon uses fill="currentColor", so hover, is-active, and graph node states tint the glyph by setting color on the parent — no per-icon color rules.

  • Accessibility: Every icon is aria-hidden="true" with no opt-out. Icons are always beside their accessible name (a label or visible text), never instead of it.

  • Scene icons: sceneIcon() places a nested <svg> in graph coordinates, deliberately setting no fill so the stylesheet's .gglyph rule is the only thing that colors it.

  • Generator gate: just test-ui runs gen-icons.ts --check first, so a stale generated file fails the test suite before shipping. Bumping the bootstrap-icons devDependency requires re-running just icons and committing the result.

https://claude.ai/code/session_01PVBNfpZv9FSj2YDtUFStDK

Summary by CodeRabbit

  • New Features

    • Added a centralized icon system using Bootstrap Icons across navigation, trees, notes, graphs, banners, controls, and resource indicators.
    • Added semantic icons for folders, notes, file types, connection directions, and expandable sections.
    • Icons now inherit surrounding colors and scale consistently throughout the interface.
  • Accessibility

    • Improved icon markup with decorative-image handling so screen readers can focus on meaningful content.
  • Documentation

    • Documented icon generation, supported subsets, validation, and UI architecture.

The chrome carried two kinds of icon and neither could be reasoned about.
Fold state and resource markers were **Unicode text** — `▶ ▼`, and
`▣ ▶ ▤ ◇ ≡ ◆` for a resource's class — so a row's marker said "not a note"
and stopped there: the shape carried nothing about *what* the file was, at
whatever weight the system font felt like. Everything else was **inline SVG
pasted per call site**, each with its own stroke width and its own idea of
15px vs 16px, so a second copy of a chevron could (and did) drift from the
first.

Both are now one thing: a name in `ui/src/icons.ts`, resolving to path data
vendored from Bootstrap Icons (twbs/icons, MIT).

**The split.** `icons.gen.ts` holds shapes; `icons.ts` holds meanings. Call
sites ask for `resourceIcon(r.class)` or `foldChevron(open)`, never for a
literal icon name — so "what does a PDF look like in B2" has one answer, and
changing it is a line here rather than a grep across three panes and a graph.

**Why generated rather than imported.** Three constraints, one shape that
satisfies all of them: the webview CSP is `default-src 'self'` (no CDN);
`npm test` runs off the source through node, where the Vite-native
`?raw` import wouldn't resolve, taking every test that transitively imports
render.ts with it; and 2078 icons ship where 24 are used. So the package is a
devDependency — provenance and the upgrade path — and `scripts/gen-icons.ts`
lifts the named subset into a checked-in `.ts` file. Its `--check` mode runs
first in `npm test`, exiting non-zero on a stale file, because a check that
reports a problem and exits 0 is a hole in the gate.

**The tree** spends two slots per row now: the chevron says *this folds*, the
icon says *what this is*. A file leaves the chevron slot empty, which is what
lines its icon up under the folder's icon rather than under its chevron.

Everything else follows the same registry: the frontmatter drawer and the
discovery pane's section and card folds (one `foldCaretHtml`, so three pasted
`▶`/`▼` pairs can't drift), the graph's resource and dangling nodes (placed by
`sceneIcon`, fill left to CSS), and the nine hand-drawn chrome icons.

Two accessibility notes. `icon()` is `aria-hidden` with no opt-out: an icon
here is always beside an accessible name, never instead of one — so the source
toggle, which used to name itself with the literal characters it printed, gains
an `aria-label`, and the broken-link and unreadable-frontmatter markers gain
`role="img"` over labels a span alone would have had ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVBNfpZv9FSj2YDtUFStDK
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@samkeen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 52f28925-e95d-4b70-bb54-88929e6072a6

📥 Commits

Reviewing files that changed from the base of the PR and between 70b806b and 18c8cac.

📒 Files selected for processing (5)
  • ui/scripts/gen-icons.ts
  • ui/src/icons.gen.ts
  • ui/src/icons.test.ts
  • ui/src/icons.ts
  • ui/src/main.ts
📝 Walkthrough

Walkthrough

The UI now uses a centralized, typed Bootstrap Icons registry. A generator vendors a curated icon subset, tests validate the registry, and UI rendering and CSS use shared icon helpers.

Changes

Centralized Bootstrap icon system

Layer / File(s) Summary
Icon generation and validation
ui/scripts/gen-icons.ts, ui/src/icons.gen.ts, ui/package.json, justfile
A pinned Bootstrap Icons subset is generated into TypeScript. The --check mode detects missing or stale output, and test commands run this validation.
Icon registry and tests
ui/src/icons.ts, ui/src/icons.test.ts
Typed icon names, semantic resource and state mappings, HTML and scene renderers, accessibility attributes, and registry tests are added.
Tree and UI rendering integration
ui/src/treenav.ts, ui/src/render.ts, ui/src/main.ts
Tree rows, controls, warnings, graph elements, navigation buttons, and find-bar controls use generated icons instead of glyphs or inline SVGs.
Icon styling and architecture documentation
ui/style.css, CLAUDE.md, crates/b2-desktop/CLAUDE.md
Shared icon sizing, alignment, color, tree-slot, badge, toggle, and scene styles are added. Documentation describes the registry and generation workflow.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BootstrapIcons
  participant gen-icons.ts
  participant icons.gen.ts
  participant icons.ts
  participant UI
  BootstrapIcons->>gen-icons.ts: provide upstream SVG files
  gen-icons.ts->>icons.gen.ts: write normalized icon bodies and version
  icons.gen.ts->>icons.ts: expose ICON_BODIES
  icons.ts->>UI: render semantic HTML or scene icons
Loading

Possibly related PRs

Suggested reviewers: claude

Poem

A rabbit stamps icons, neat and bright,
Bootstrap stars in the generated night.
Chevrons fold and folders glow,
Typed paths tell the views where to go.
Fresh checks guard each little sprite.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main UI change: replacing Unicode glyphs with vendored Bootstrap Icons.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ui-icons-exploration-y01pfa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ui/scripts/gen-icons.ts`:
- Around line 107-122: Update the generated header emitted by the icon generator
around ICON_BODIES to include the complete Bootstrap Icons MIT permission
notice, or reference a committed third-party notices file shipped with the
application; ensure the generated src/icons.gen.ts contains this attribution
without relying solely on ui/node_modules.

In `@ui/src/icons.ts`:
- Around line 51-54: Escape class values with escapeHtml before interpolating
them into SVG markup in ui/src/icons.ts lines 51-54 and 67-71, covering both
opts.class and cls while preserving existing class composition. Add a
quote-containing class test in ui/src/icons.test.ts lines 143-151 and verify the
quote remains escaped inside the class attribute.

In `@ui/src/main.ts`:
- Around line 392-395: Update the badge.innerHTML construction near icon() to
escape the anomaly count before interpolation, using escapeHtml(String(n)) for
the span content while preserving the existing icon markup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 95bdf150-b2a2-4e9b-9311-290de71be8d3

📥 Commits

Reviewing files that changed from the base of the PR and between c2d3402 and 70b806b.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • CLAUDE.md
  • crates/b2-desktop/CLAUDE.md
  • justfile
  • ui/package.json
  • ui/scripts/gen-icons.ts
  • ui/src/icons.gen.ts
  • ui/src/icons.test.ts
  • ui/src/icons.ts
  • ui/src/main.ts
  • ui/src/render.ts
  • ui/src/treenav.ts
  • ui/style.css

Comment thread ui/scripts/gen-icons.ts
Comment thread ui/src/icons.ts Outdated
Comment thread ui/src/main.ts Outdated
Three findings from review on #128. Two were right; the third was right about
the rule and wrong about the fix.

**The license was the real defect.** `icons.gen.ts` carried the copyright
notice and pointed at `ui/node_modules/bootstrap-icons/LICENSE` for the rest —
a file that is neither committed nor shipped, so anyone receiving the generated
data received it without the terms it is used under. MIT requires the copyright
notice *and* the permission notice to travel with any copy.

The notice is now emitted from one source in the generator into two forms,
because source and bundle are different distributions and a comment only covers
the first: a `/*!` legal comment, which is the marker esbuild keeps when it
strips every other comment (verified — it reaches ui/dist), and an exported
string, which is what the suite can assert against. The string is tree-shaken
out of the bundle, so the notice ships once. `--check` alone could not have
guarded this: it compares the file to what the generator emits, so a generator
edited to drop the notice would pass. The obligation now has a check that fails.

**Class values are escaped at the emitter.** Every caller passes a literal, and
a CSS class is chrome vocabulary that has no business carrying vault data — but
that is the argument for escaping in `icon()`/`sceneIcon()` rather than against
it. This is a markup emitter, and the seam is where this codebase makes rules
structural instead of remembered, the same reason `sanitize.ts` hangs off
`marked`'s postprocess hook rather than each caller (E5).

**The anomaly badge drops the interpolation instead of escaping it.** The
review asked for `escapeHtml(String(n))`; escaping a number is a ritual that
teaches the reader to stop asking which values need it. `innerHTML` now takes a
hoisted constant and the count is `append`ed as a text node, which no parser
ever sees — stronger than escaping, and it stays correct if `n` ever stops
being a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVBNfpZv9FSj2YDtUFStDK
@samkeen
samkeen merged commit 6b2b7c2 into main Aug 1, 2026
2 checks passed
@samkeen
samkeen deleted the claude/ui-icons-exploration-y01pfa branch August 1, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants