Deliver icons as CSS, not as React - #48
Open
ariofrio wants to merge 18 commits into
Open
Conversation
heading-icons.mjs read a package.json from every directory beside the plugins and crashed on the first one that had none. Tooling leaves such directories there — an agent's gitignored `.claude/`, for one — which took down both `check:heading-icons` and `npm test` in a working tree that was otherwise clean. Carries over the test plugin-layout.mjs already makes for the same question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An icon was drawn by rendering it: one React portal per place, and a plugin that wanted one had to fetch it and draw its own. Both cost per row, and both put React-owned nodes into DOM React does not own — which is the thing bb's foreign-DOM guard refuses, and which it refused 68 times in one measured run. The glyph now travels as a custom property instead. The content script publishes one stylesheet holding every icon anyone has picked, keyed by an attribute a consumer puts on a box it draws itself, and the cascade delivers it. That costs nothing per row — no scan, no observer, no node — so a list of any length is the same work as one row. This plugin's own placements read it the same way any other plugin's would. Where bb has already given a row a job, the placeholder is a plain span the stylesheet fills in; only where the icon has to answer a click and open the picker is anything still rendered into it. Measured over 300 rows and 10 list changes: 0 ms of plugin work per change against 34.2 ms of React portals, and 600 nodes against 1,800. The contract is documented in the README as an extension point. Nothing chains one owner kind to another there — each carries its own properties, so the `var()` chain a consumer writes is the whole of the precedence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar fetched project icons from the Icons plugin over an RPC that bb relayed, mirrored that plugin's response shape in its own schemas, listened on a broadcast channel for edits, held two maps of state, and rendered an SVG per row. All of it is gone. A row names its project on its own box and the glyph arrives through the cascade, so a list of any length costs what one row costs. One stylesheet carries it, inserted once. It is plain CSS rather than classes on the spans because the rule that collapses a row's icon has to name `:root`, which is outside the scope root bb compiles a plugin's own stylesheet into. Nothing looks different. A row still shows its project's icon, the filter still falls back to a folder, and a sidebar without the Icons plugin still lays out as it did before there were icons — the row boxes collapse rather than leaving an empty gutter down the list. Verified against a running bb rather than only in jsdom, which applies no stylesheet: rows and filter rows resolve a mask, a size, and a picked colour, and every row box collapses the moment the plugin's marker is taken off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-css # Conflicts: # plugins/bb-plugin-thread-stages/src/app.tsx # plugins/bb-plugin-thread-stages/src/components/ThreadFilter.tsx
The same three figures had accreted across the README, the changeset, and six comments: a list costing what one row costs, a thing being the whole of some other thing, and a closing clause restating the sentence before it. Each one is kept where it explains something and dropped everywhere else. One of them was also wrong. The ready marker's comment said an early marker would offer a picker that could not open; what it actually guards is a consumer drawing empty boxes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-css Only the two Icons screenshots conflicted; both sides had recaptured them, and neither side's bytes are right for the merge. Takes main's and leaves the correct ones to CI, which owns them.
…-css #58 added a third consumer of the icon maps this branch deletes: the section rows in a thread's own menu. It reads the contract like the other two now — the row names its section, and the map, the prop threaded down to it, and the fetched glyph all go. That menu is portaled out of the plugin's tree by Radix, so it is the first surface to test the claim that a global rule reaches anywhere. Measured in a running bb: 16x16, mask resolved, and the section's own colour rather than its project's.
…ariofrio/ribbon-icons-css
…-css #54 and this branch both added a test where the last one ended; both are kept.
…-css Only screenshots conflicted; #36 recaptured them for the suite icon it added to the README, and this branch had its own recapture. Takes main's and leaves the correct ones to CI, which owns them.
The filter, the thread menu, and the menu's test each restated the mechanism icon-styles.ts already documents. Each keeps a line saying why its span is empty, and points there.
…-css #49 moved the sidebar out of Thread stages and into Ribbon sidebar, taking the fetch-based icon path with it: a copy of the deleted icons.ts, the same mirrored schemas, the same maps, and a fourth consumer the old sidebar never had in the group headings. So the Thread stages half of this branch is dropped rather than resolved — every file it edited is gone or no longer holds that code — and rebuilt on Ribbon sidebar. The Icons half is untouched by the move.
The sidebar's module docstring restated the contract its README already documents, three call-site comments described the same mechanism at three different lengths, and two comments carried a clause restating the sentence before them. Also corrects the Icons README: the sidebar draws these icons on group headings, in the scope filter and in a thread's menu, not only on rows.
A reader stopped to ask what the word meant, which is enough to lose it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Icons publishes every chosen icon as one stylesheet, keyed by an attribute a consumer puts on a box it draws itself. Ribbon sidebar draws its icons that way instead of over RPC, and Icons draws its own read-only placements that way too. React is left only where the icon opens the picker.
Nothing looks different.
Purpose
An icon was drawn by rendering it: one React portal per place, and any other plugin had to fetch it and draw its own SVG per row. That costs per row, and it puts React-owned nodes into DOM React does not own — which bb's foreign-DOM guard refuses, 68 times in one measured run.
Over 300 rows and 10 list changes:
Changes
The contract, in the Icons README as an additive-only extension point:
data-ribbon-icons-project="…"or-section="…"--ribbon-icons-{kind}-{glyph,color}on that elementdocumentElementcarriesdata-ribbon-icons-readyonce the sheet is inBox, size and fallback belong to the consumer, and each kind carries its own properties — so precedence is whatever
var()chain the consumer writes, and a later chain (a thread's icon before its project's) needs no version bump.Icons publishes the sheet from its content script, independent of any placement, and gives its own read-only placements a marked span instead of a portal.
Ribbon sidebar consumes the contract, dropping the cross-plugin RPC, a mirror of the Icons response schema, a second copy of its colour palette, and a per-row SVG.
Unrelated, but it blocked
npm test:heading-icons.mjscrashed on any directory beside the plugins without apackage.json, such as an agent's gitignored.claude/.Evidence
jsdom applies no stylesheet, so a harness boots the throwaway bb the screenshots use, seeds the same fixture, and reads
getComputedStyle:Both plugins and the root pass their checks; CI recaptures the screenshots.
Caveats
-webkit-maskis emitted besidemaskfor the web client; bb's own Chromium needs neither.🤖 Generated with Claude Code