Add unified icon system - #12955
Conversation
One curated set on a 24x24 / 2px / currentColor grid, replacing the ad-hoc
mix of PNG sprites, file SVGs, and inline-SVG-in-JS.
Source of truth: static/icons/src/{lucide,custom}/. scripts/build_icon_sprite.mjs
(make icons) emits three outputs from it:
- static/build/icons/sprite.svg - <symbol> sheet for the document/light DOM
- static/icons/manifest.json - committed icon-name list (gallery + lint)
- openlibrary/components/lit/icons.generated.js - committed Lit svg fragments
for shadow-DOM components (which can't reach the document sprite via <use>)
Delivery:
- $:icon(name, size, label) Templetor macro for server templates
- <ol-icon> Lit component (light DOM) for JS/light-DOM contexts
Both use a same-document <use href="#name">; the sprite is inlined once per page
via the icon_sprite() helper (footer), since external <use href="file.svg#id">
is unreliable on the Safari/iOS versions in our browserslist.
Shadow-DOM components inline glyphs imported from icons.generated.js instead;
ol-dialog's close icon is migrated as the first example.
Also: icon-size tokens (sm/md/lg), shared ol-icon.css, an icon gallery with
copy-to-clipboard at /developers/design#icons, and a pre-commit/npm check that
the generated sprite/manifest/module stay in sync with the sources.
Replace inline SVG glyphs with imports from the generated icon module: - OLMarkdownEditor: all 19 toolbar glyphs (wrapped in the editor's <svg> shell so toolbar CSS still governs size/stroke-width) - Close (X) icon deduped across OLChip, OlBanner, OlToast, and SearchModal to the canonical `x` glyph (removes four hand-written copies) Geometry is unchanged (the icon set was harvested from these components), so the rendered output is identical. Verified in-browser: editor toolbar, dialog/chip/ banner/toast/search-modal close icons all render as real SVG elements.
Size the ol-icon host element from its size attribute in render-blocking CSS (a plain `ol-icon` selector, so it applies before and after the component upgrades). The box is held from first paint, so no surrounding content reflows when the Lit bundle runs. The glyph still appears on upgrade — server-rendered templates that need instant, JS-free icons should use the $:icon() macro.
…gn page Add a 'which technique to use' comparison table, usage examples for all three paths, and a 'what you control' section spelling out sizing (sm/md/lg tokens + numeric escape hatch) and color (currentColor — set color on any ancestor).
A const declaration sat between the JSDoc block and the class, so the analyzer attached the @prop descriptions to the const instead of OlIcon — leaving the design-page API table empty. Move it above the doc comment so the table renders name/size/label with their descriptions.
…ns anywhere The icons-generated pre-commit hook ran the generator, which imported svgo — a devDependency absent in CI/containers/fresh checkouts, so the hook failed with ERR_MODULE_NOT_FOUND. The source SVGs are authored clean, so svgo was only merging paths; the generator already keeps just the attributes it needs. Drop svgo from the generator (pure Node built-ins now) and remove the unused sprite SVGO config. Glyph geometry is unchanged (paths are simply no longer merged).
…n-system # Conflicts: # static/css/page-design.css
| placeholder="Filter $len(icons) icons…" | ||
| aria-label="Filter icons" | ||
| > | ||
| <ul class="icon-gallery" id="icon-gallery"> |
There was a problem hiding this comment.
WCAG 1.3.1: <ul> contains direct text content. Wrap in <li>.
<ul> and <ol> must only contain <li>, <script>, or <template> as direct children.
Details
Screen readers announce list structure ('list with 5 items') based on proper markup. Placing non-<li> elements directly inside <ul> or <ol> breaks this structure. Wrap content in <li> elements, or if you need wrapper divs for styling, restructure your CSS to style the <li> elements directly.
…ent foundations A broad snapshot of in-flight design-system work on this branch. - Icons: unified <ol-icon> / $:icon() sprite system over static/icons/src, with a build-time sprite plus generated fragment module for shadow DOM. - Design docs: /developers/design rebuilt into Foundations / Components / Icons / Playground, with tokens and component APIs derived rather than hand-maintained. Icon size presets now document their pixel values (sm 16px, md 20px, lg 24px) and that the 2px grid stroke scales with them. - Web components: focus/tabbing utilities, FocusableHostMixin, FormAssociatedMixin, keyboard-nav helpers, new OlMenuPopover and OlSegmentedControl, and a11y tests across the Lit layer. - Tokens/CSS: token contrast tooling and wide stylesheet updates. - Also carries subject-page macros, results-toolbar sort work, and assorted plugin/template changes. Claude-Session: https://claude.ai/code/session_01XfFpmRvFnGA4zTwUpokNzW
…n-system # Conflicts: # openlibrary/components/lit/OlDialog.js # openlibrary/components/lit/OlToast.js # openlibrary/components/lit/custom-elements.json # openlibrary/plugins/openlibrary/design.py # openlibrary/plugins/openlibrary/js/design-system/index.js # openlibrary/templates/design.html # static/css/page-design.css
djLint H021 flagged the Color demo's inline `style="color: ..."`, which was the last lint failure on the icons page. Swap it for a .ds-demo-danger class alongside the other ds-demo-* helpers, and show the reader the CSS rule in the blurb so the snippet teaches the real technique rather than an inline style nobody should copy. Claude-Session: https://claude.ai/code/session_01XfFpmRvFnGA4zTwUpokNzW
…ite early
Review follow-ups on the icon system:
- Docs and the gallery's copy button taught `$:icon("search")`, which is not a
global — Templetor exposes disk macros only as `$:macros.icon(...)`, so every
documented snippet raised NameError when pasted into a template.
- Sprite <symbol> ids were unprefixed, putting document-global ids like #search
and #code on every page. Any colliding id earlier in the document won <use>
resolution and the icon silently rendered blank. Namespaced to icon-<name>;
the names callers pass and manifest.json are unchanged.
- The sprite was inlined just before </body>, so icons above the fold had
nothing to resolve against until the response finished streaming. It is now
the first thing in the body (~1.5KB gzipped).
- <ol-icon size="32"> styled the inner <svg> but not the host, painting a 32px
glyph inside a 20px box. Raw pixel sizes are gone; a one-off is a CSS job,
which also gets the box right before the component upgrades.
- The macro emitted 18 leading newlines, one per comment line above its output;
its docs now sit below it.
- The drift-check hook matched a path that does not exist and did not guard the
generated outputs it exists to protect.
- The build script now rejects icon names that are not kebab-case or that
collide as JS identifiers, naming the offending source file rather than
failing later inside a generated one.
Claude-Session: https://claude.ai/code/session_01XnwG8xofUcTkTSyUaz7fu7
Jinja templates are the preferred target for new code but had no way to
draw an icon: Infogami exposes disk macros only through Templetor's
`macros` namespace, so the design page had to hand `icon=macros.icon` to
render_jinja_template as a kwarg, and any other template would have had
to do the same.
Registers `icon` in the Jinja env instead, wrapping the same macro and
returning Markup so it survives autoescape. Templates now write
`{{ icon("search") }}` against the same sprite the Templetor macro uses.
The design page drops its per-call-site shim and documents both forms.
Claude-Session: https://claude.ai/code/session_01XnwG8xofUcTkTSyUaz7fu7
The sprite shipped on every page but nothing outside /developers/design referenced it — $:macros.icon() had no production callers, and the glyphs patrons actually saw were hand-copied paths, <img> icons that could not inherit color, and a second icon vocabulary. Deduped 20 hand-copied glyphs. Six Lit components now import from icons.generated.js rather than carrying their own path data; the chevron-down path alone existed verbatim in four files. Converted 22 <img> icons to sprite references: all 12 right-chevron sites, the password-visibility eye (password-toggle.js now repoints <use href> instead of swapping img.src), signup's success check, the language globe, search-inside, the barcode scanner, the edition nav arrows and the hamburger. As images these could not inherit currentColor — two of them hardcoded internetarchive#666/#8a7f71 and ignored the theme outright. Retired the second vocabulary: PreviewSearchInside drew three Material Symbols glyphs (viewBox="0 -960 960 960", filled, one with a hardcoded fill) next to Lucide stroke icons in the same row on the book page. Supporting changes: - The macro emits a fixed class list, which blocked nearly every call site — they carry load-bearing hooks (.down-arrow's rotation, .sas-avatar__icon's sizing, .search-icon). Added an `extra_class` passthrough, mirrored in the Jinja global, so adopting the system does not mean rewriting CSS at each site. - icon_link() takes an icon name instead of an image src. Migrating only its share icon would have left a stroke glyph between two 700pt filled illustrations, so Review and Notes moved with it. - Four new glyphs, taken from lucide-static rather than redrawn: share-2, scan-barcode, message-square-text, sticky-note. - Dropped three shape-describing alt strings from the catalog ("right chevron", "search inside icon", "Search Inside Icon"). layout_options.html deliberately keeps its inline glyphs, with a comment saying why: ol-segmented-control harvests each segment's innerHTML and re-renders it inside its shadow root, where a same-document <use> resolves to nothing. Glyphs drawn through the macro take the sprite's baked stroke-width of 2, so the browse chevron goes from 2.5 to 2. The Lit components keep their own wrapper stroke-width and are unchanged. Twelve image assets are now unreferenced and left in place for a separate cleanup pass alongside the 30 that were already dead. Claude-Session: https://claude.ai/code/session_01XfFpmRvFnGA4zTwUpokNzW
Rolldown (Vite 8) only honors /*#__PURE__*/ before a call or new expression - before a tagged template it warns INVALID_ANNOTATION and keeps unused glyphs in the bundle. Wrap each glyph in an IIFE so the annotation precedes a real call; terser understands the same form, so the webpack build keeps its tree-shaking hint too. Claude-Session: https://claude.ai/code/session_01V312UXw8oNocbu5SkC1A42
The sprite was inlined into every page body; now it's one external
content-hashed file cached sitewide. icon_sprite() becomes
icon_sprite_url(), the macro and <ol-icon> reference
<use href="{sprite}#icon-name">, and client JS picks the URL up from a
<meta name="ol-icon-sprite"> tag in site/head.
Also: delete the legacy one-off SVGs now replaced by the sprite, drop
the unused chevron-up glyph, thicken the scan-barcode icon and match its
header color to the search magnifier, and trim the language-dropdown
labels to the native name only.
Claude-Session: https://claude.ai/code/session_01RjCpiDb519vwQx4yDAKpra
These 28 files were unreferenced before the icon-system branch — no template, CSS, JS, or Python mentions them, nothing constructs their paths dynamically, and the service worker caches by URL pattern rather than a file list. Claude-Session: https://claude.ai/code/session_01V312UXw8oNocbu5SkC1A42
Replace the last PNG sprite consumers with the icon system: sort state in the editions table header now renders arrow-up-down / arrow-up-narrow-wide / arrow-down-wide-narrow glyphs driven purely by DataTables' sorting classes, and the full_numbers pager is dropped from the `dom` string in favor of an <ol-pagination> synced to the table's paging state. The nine legacy sort/paginate PNGs and their CSS are deleted, along with the hand-rolled arrow-toggling jQuery. Along the way: - Add per-size stroke tokens (--icon-stroke-sm/md/lg); the sprite build now strips stroke-width from each <symbol> so the weight inherits from the referencing <svg>, letting `lg` pull back to 1.75px instead of a full 2px. - Auto-discover source groups under static/icons/src/ instead of hardcoding lucide/custom, and rename LICENSE-lucide.md to ATTRIBUTION.md to match the library-agnostic framing. - Swap the ✏️ emoji on the subject-edit button for the pencil glyph. - Document stroke behavior and glyph provenance on /developers/design. Claude-Session: https://claude.ai/code/session_01RjCpiDb519vwQx4yDAKpra
Pull the how-to out of the provenance blurb into its own numbered section: author in the canonical form, drop the kebab-case file in the folder matching its source, run `make icons`, commit the two generated files alongside it. Claude-Session: https://claude.ai/code/session_01RjCpiDb519vwQx4yDAKpra
Several comment blocks on this branch had grown to explain more than the code needs — the sprite build's module header ran 27 lines, <ol-icon>'s class JSDoc 22. Trim each to the fact you can't recover by reading the code (why stroke-width is stripped from the symbol, why the sprite meta isn't a rel=preload, why the segmented control's glyphs stay inline, why the IIFE wraps the PURE annotation) and drop the restatement around it. Regenerates icons.generated.js, whose header the build script emits. The /developers/design prose is untouched — that's the documentation itself, not commentary on code. Claude-Session: https://claude.ai/code/session_01AzJjBQ55TN5JbSkaCiLFUt
Clicking a glyph used to copy the Templetor macro with no hint that it had. Now it opens a shared top-layer panel (native Popover API) showing all four usage forms — Templetor, Jinja, Lit import, <ol-icon> — each in a recessed well that copies on click anywhere in it, with a copy glyph that flips to a check. Browsers without the Popover API keep the old copy-on-click. Adds Lucide's `copy` icon to the set for the wells. Claude-Session: https://claude.ai/code/session_01V312UXw8oNocbu5SkC1A42
The svg chevron was still carrying two PNG-era pixel nudges (.icon-link__image's top: 3px plus a 6px top margin), landing it ~9px low. Center it with flex on .dropdown instead; the scoped selector is needed because mybooks.css loads later and would re-apply the nudge. Claude-Session: https://claude.ai/code/session_01V312UXw8oNocbu5SkC1A42
The sprite was the only one of the build script's three outputs left uncommitted, so a checkout that hadn't run `make icons` rendered blank icons everywhere. That is what happens on testing.openlibrary.org: patch deploys apply a diff and restart, they never run a build, so the sprite 404s and every glyph draws empty. Committing it removes the state where the code is present but the asset isn't — testing, fresh clones and local dev all work with no build step. The manifest and the Lit glyph module were already committed, so this makes the three outputs consistent rather than introducing a new idea. It moves to static/icons/ because static/build/ is gitignored wholesale and `make clean` does `rm -rf $(BUILD)`, which would delete a committed file. All four icon artifacts now live in one directory. Knock-on simplifications: icon_sprite_url() loses the fallback that existed only for the missing-sprite case, the Makefile target loses the atomic-swap dance that kept a half-written sprite from being served, and --out loses its only caller. Symbols are now one per line so the committed file diffs readably and conflicts can be resolved by rebuild. check:icons and the pre-commit hook both gained the sprite path — with the file committed, drift is now possible and those guards are what catch it. Claude-Session: https://claude.ai/code/session_01Lv9GuFfV3JRG7hmKGmN1t4
These 50 files are the only binary content in the PR, and GitHub's .diff omits binary data — so `git apply` aborts the entire patch unless the deploy passes --exclude=static/images/*. That one flag was the last thing standing between this branch and an ordinary patch deploy to testing. The deletions don't need to ship here. 29 of these have been dead for years; the other 21 stop being referenced as soon as this PR's call sites move to the macro. Unreferenced files on disk hurt nothing, so they move to a follow-up that never has to reach testing. Also drops the PR from 170 files to 120. Claude-Session: https://claude.ai/code/session_01Lv9GuFfV3JRG7hmKGmN1t4
cdrini
left a comment
There was a problem hiding this comment.
Niiice! This is a great cleanup 😊
A few thoughts:
(1) I wonder if we can reduce from 3 ways to 1...
Not sure if this is feasible, but was thinking that we might be able to switch to using <ol-icon name="foo"> everywhere, by:
- For pre-js, we can generate a sprite sheet as a .css file, which sets the background/width/height/etc of the ol-icon[name="foo"]:not(:defined) { background: 'data:...'}
- For post-js, we can have lit components with the SVG in them, which should give us full control over the display.
This would mean we would have exactly two copies of the sprite sheet sent over once as CSS once as JS, but might be worth it if we can have only one way to do icons that works everywhere.
(2) There are a lot of generated files being commited into the repo. Can we remove some of these?
For icons.generated.js, can we potentially have vite plugin to handle importing the svg icons? Something like this according to claude might work:
function svgNamedExport() {
return {
name: 'svg-named-export',
enforce: 'pre',
transform(code, id) {
if (!id.endsWith('.svg')) return null;
const escaped = JSON.stringify(code);
return {
code: `export const svg = ${escaped}; export default svg;`,
map: null,
};
},
};
}
// ....
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [svgNamedExport()],
});Which will allow us to do:
import { fooIcon } from 'static/icons/src/foo.svg';But not sure.
For manifest, likewise I think we might be able to replace it with a glob in the code.
And the sprite sheet should no longer be needed once I update the testing file to run the icons step!
| <ul class="locale-options $classes"> | ||
| $for lang in get_supported_languages().values(): | ||
| <li><a href="#" lang="$lang['code']" data-lang-id="$lang['code']" title="$lang['localized']">$lang['native'] ($lang['code'])</a></li> $# detect-missing-i18n-skip-line | ||
| <li><a href="#" lang="$lang['code']" data-lang-id="$lang['code']" title="$lang['localized']">$lang['native']</a></li> $# detect-missing-i18n-skip-line |
There was a problem hiding this comment.
Intentional, but it is a drive-by product change.
Context: the language dropdown's trigger in site/alert.html was English (en) next to language-icon.svg. Swapping that PNG-era icon for $:macros.icon("globe") was in scope, and while I was there I dropped the (en) from the trigger. The code felt redundant next to the name and the lang attribute plus the title (localized name) are still there for anything that needs them.
Say the word, and I'm happy to put the codes back.
There was a problem hiding this comment.
Oh just checking since I thought maybe AI did of its own accord 😁 Up to you ; one thing I will note, is that it would be nice to have a way to also display the language names in the current language. E.g as an English user, I might want to see "Français / French" or something, so I can understand what I'm seeing. The language codes were added to provide a bit of that context, but longer term it might be nice to have both labels somehow!
The Lit components each imported a named glyph from icons.generated.js and hand-wrote the <svg> wrapper around it — a third icon path alongside the macro and the element, with the wrapper's attributes duplicated at every call site and free to drift. They now use <ol-icon name="..."> instead. OlIcon renders the glyph inside its own shadow root, so it works where <use> can't reach the sprite, and each host sizes it with plain width/height rules. Stroke weight, where a component wanted a non-default, moves to --ol-icon-stroke-width. Drops the lit-import snippet from the design page's icon gallery: with the glyph module no longer imported by hand, only three usage forms remain.
Committing the sprite, the manifest, and the Lit glyph module bought a checkout that renders icons with no build, and cost a freshness check in both CI and pre-commit, plus a merge conflict on every concurrent icon PR. The build is a dependency-free Node script that runs in well under a second, so paying it is cheaper than policing the outputs. `make icons` now writes only the sprite and the glyph module, both gitignored. `make lit-components` depends on it, so the bundle can't be built from a stale module, and a pretest:js hook covers the JS tests that import it. The freshness check and its CI step and pre-commit hook are gone. The sprite is written to a temp file and renamed, since a rebuild under a running server would otherwise serve a half-written one. The manifest existed only to list icon names for the design page's gallery, which now globs static/icons/src/ — one less generated file, and a list that can't drift from the sources. icon_sprite_url() falls back to the unhashed path when the sprite is missing, so a checkout that never ran `make icons` draws blank icons rather than 500ing every page with one.
Every helper now documents its params and return shape, and parseAttrs and collectIcons get real descriptions — collectIcons in particular explains why it enforces both name and identifier uniqueness (the sprite keys on the id, the Lit module on the export name). Claude-Session: https://claude.ai/code/session_01MKQA4KK4i8E3osyV98359T
|
Live on testing: https://testing.openlibrary.org/developers/design/icons Worked through your comments. I reduced the ways of rendering icons down from three, to two, and most importantly was able to simplify the usage in our Lit web components. No more fragment importing and constructing svgs, now simply: import './OlIcon.js';
html`<ol-icon name="search"></ol-icon>` |
Only the a11y attributes differed between the labeled and decorative branches; the conditional now sits inside the tag instead of duplicating the whole element. Claude-Session: https://claude.ai/code/session_01LhQSynuMDhyeBxHv96LyCS
The layout switcher and the component's docs hand-wrote their glyphs because the control re-renders each segment inside its shadow root, where a sprite <use> can't resolve. <ol-icon> inlines its glyph, so it works there — which leaves no inline SVG in the segment call sites. Adds lucide/layout-grid for the grid segment, and sizes .segment ol-icon alongside .segment svg (an outer-tree rule beats <ol-icon>'s :host). Claude-Session: https://claude.ai/code/session_01LhQSynuMDhyeBxHv96LyCS
<ol-books-display> renders a titled set of books for a Solr query from /books-display.json, switchable between a covers carousel (on <ol-carousel>) and a list view, with offset paging and a per-user shelf/rating overlay. It renders into the light DOM so the carousel gets real children and sitewide handlers keep working; styles live in ol-books-display.css. <ol-book-actions> is the per-book popover: reading-log shelves, a star rating, and an add-to-list pane (filter, membership, inline create) that slides in from the right. Optimistic updates with toast rollback; fires ol-book-state-change so cards stay in sync. Lucide glyphs are inlined for now (swap for <ol-icon> once internetarchive#12955 lands). Claude-Session: https://claude.ai/code/session_015u26V9Ppiy5NwqnrhtYShC
Resolve conflicts with the shadow-DOM ol-button (internetarchive#13350): dialog/toast close controls keep <ol-button shape="icon"> but draw <ol-icon name="x">, and the icon macro gains a `slot` arg so PreviewSearchInside can slot into icon-start. Claude-Session: https://claude.ai/code/session_01Vmhke1AR5VjcNKP7KnciCJ
…n-system # Conflicts: # package.json
…n-system # Conflicts: # static/css/components/header-bar.css # static/css/legacy-datatables.css # static/css/tokens/colors.css
cdrini
left a comment
There was a problem hiding this comment.
Niiiiice this looks fantastic!! 😊 All of this looks great to me ; all my comments are non-blockers. One blocking question which it would be good to answer is do we want to store all the icons directly in the repo? There are some great aggregator sites like https://icon-sets.iconify.design/ with I think npm packages that we might be able to use to fetch them during the build instead of committing them to the repo.
|
|
||
| // Toolbar glyphs from the shared icon set, keyed by the editor's local names. | ||
| // .toolbar-btn ol-icon governs size and stroke width. | ||
| const glyph = (name) => html`<ol-icon name="${name}"></ol-icon>`; |
There was a problem hiding this comment.
I would just inline this method ; introducing a new noun here glyph I think adds a layer of indirection which isn't particularly useful.
There was a problem hiding this comment.
I think switching to a css-based sprite sheet might be worth exploring, since then we'll go down to only one way to use an icon! And we can remove this duplicate mapping of the web component attributes to python templetor parameters -- as well as the jinja wrapper.
| static _backIcon = html`<ol-icon name="arrow-left"></ol-icon>`; | ||
|
|
||
| static _personIcon = html`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`; | ||
| static _personIcon = html`<ol-icon name="user"></ol-icon>`; |
There was a problem hiding this comment.
Soooo much nicer!!! 🥳 I would also likely inline these ; not sure we're getting much out of having them stored as static constants. Before it made sense since it was a decent junk of SVG code that would make the place it was used difficult to read, but now it's a hyper readable short string!
There was a problem hiding this comment.
This could in theory potentially be replaced by a css-based sprite sheet.
| <ul class="locale-options $classes"> | ||
| $for lang in get_supported_languages().values(): | ||
| <li><a href="#" lang="$lang['code']" data-lang-id="$lang['code']" title="$lang['localized']">$lang['native'] ($lang['code'])</a></li> $# detect-missing-i18n-skip-line | ||
| <li><a href="#" lang="$lang['code']" data-lang-id="$lang['code']" title="$lang['localized']">$lang['native']</a></li> $# detect-missing-i18n-skip-line |
There was a problem hiding this comment.
Oh just checking since I thought maybe AI did of its own accord 😁 Up to you ; one thing I will note, is that it would be nice to have a way to also display the language names in the current language. E.g as an English user, I might want to see "Français / French" or something, so I can understand what I'm seeing. The language codes were added to provide a bit of that context, but longer term it might be nice to have both labels somehow!
Co-authored-by: Drini Cami <[email protected]>
book-icons.js inlined Lucide paths as a stopgap until the shared icon system landed (internetarchive#12955). It has, so the books-display components now render <ol-icon> like every other Lit component. The eight glyphs the sprite lacked are added as Lucide sources, plus two non-Lucide ones: covers-row (custom) and star-filled, since a symbol's fill="none" outranks inherited CSS so the rating stars need a filled variant. Per-use stroke widths move to --ol-icon-stroke-width. Claude-Session: https://claude.ai/code/session_016bwMwkcQzpKTpEqrtCoeaw
21 of these were replaced by sprite icons in internetarchive#12955; the other 29 have had no references in the codebase for years and turned up while auditing. Verified none of the 50 is referenced from any template, JS, CSS or Python on the icon-system branch. Split out of internetarchive#12955 so that PR carries no binary content: GitHub's .diff omits binary data, so a patch deploy of a PR containing binary deletions aborts unless it passes --exclude=static/images/*. This one never needs to reach testing, so the flag stops mattering. Merge after internetarchive#12955. Claude-Session: https://claude.ai/code/session_01Lv9GuFfV3JRG7hmKGmN1t4
* Delete 50 legacy image files replaced by the icon system 21 of these were replaced by sprite icons in #12955; the other 29 have had no references in the codebase for years and turned up while auditing. Verified none of the 50 is referenced from any template, JS, CSS or Python on the icon-system branch. Split out of #12955 so that PR carries no binary content: GitHub's .diff omits binary data, so a patch deploy of a PR containing binary deletions aborts unless it passes --exclude=static/images/*. This one never needs to reach testing, so the flag stops mattering. Merge after #12955. Claude-Session: https://claude.ai/code/session_01Lv9GuFfV3JRG7hmKGmN1t4 * Replace the list delete trashcan link with an ol-button The delete/remove control on list pages was an anchor with a sprite-image trashcan; it now uses ol-button with the trash icon, which lets the last of the legacy image files go. On a list's own page the button moves into the CTA column under the lending button and dropper, so the dropper's ad-hoc margin and its 50px reservation for the absolutely positioned control give way to a single flex gap owning the rhythm. Claude-Session: https://claude.ai/code/session_01NmxEn3FtZunApjLZAW78wo * Move the unreferenced phase-2 icon set out to its own PR The 17 lucide additions had ridden along in the ol-button commit with nothing consuming them; they now land via #13424 instead. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Point the service-worker fixture at a surviving image The matcher test used the now-deleted barcode_scanner.svg as its sample path. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Scope the empty check-in-container rule to list pages The grid variant applied to every .list-books--grid (search results, author pages, reading log), not just list seeds. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Move the ol-button trashcan swap to its own PR Restores the list templates, JS, and CSS to master and keeps back_list-trash.png, whose last reference lives until #13425 lands. This PR is back to being a pure deletion of unreferenced files. Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD * Keep read aloud.svg: buttonCta.css still references it URL-encoded The audit grep missed the %20-encoded url() in buttonCta.css:39, so deleting the file would blank the open-access audiobook button icon. It goes once that button is on the sprite (icons/chore/phase2-icon-set). Claude-Session: https://claude.ai/code/session_01RtTEd3rAHJYz3zKi3zUWwD
Part of #13240 — Epic: UI Modernization (Foundation workstream).
Open Library's icons are currently a patchwork: some are PNG images from the 2010s, some are one-off SVG files, some are SVG markup pasted inline into JavaScript. They come from different families, sit at different weights, and each one is its own file to download.
This PR replaces that with a single icon system: 43 icons, one visual family, one small file, and one obvious way to use them. [feature]
Docs, a searchable gallery, and the API reference live at
/developers/design/icons.What patrons get
rem, so icons grow when someone bumps their browser font size instead of staying stuck at 16px next to enlarged text. Every glyph inherits the surrounding text color, so it stays legible wherever it sits.aria-hidden) instead of announcing a filename; meaningful ones take a real translated label. The old<img alt="...">pattern got this wrong in both directions.What developers get
$:macros.icon("search"). That's the whole API in a template. Previously you'd hunt for a file path, guess a size, and hand-write<img>with the right classes./developers/design/iconslists every available icon with a click-to-copy call. No more greppingstatic/images/to find out whether a chevron already exists.{{ icon("search") }}— same macro, no imports, no passing it through as a render kwarg.make iconsrebuilds everything from the source SVGs. It's a single dependency-free Node script, already part ofnpm run build-assets. No icon font, no npm package, no bundler plugin, no runtime library.How it works
One thing you edit, one command, two outputs:
Neither output is committed — both are gitignored.
npm run build-assetsrunsmake icons, andmake lit-componentsdepends on it, so the Lit bundle can't be built from a stale glyph module.The sprite URL includes a content hash (
sprite.svg?v=abc123) so browsers can cache it forever and still pick up changes when it's rebuilt. The macro emits a plain<svg><use href="…sprite.svg#icon-search">pointing at that URL, and a<meta name="ol-icon-sprite">tag in the page head exposes the same URL to client-side JS.Two ways to draw an icon
Both read the same source files and look identical. You pick based on where the icon lives:
1. The macro — server-rendered templates. This is the default and covers most cases.
$:macros.icon("search") $# Templetor {{ icon("globe", size="lg", label=_("Language")) }} {# Jinja #}2. The
<ol-icon>element — anything drawn by client-side JS, including inside a web component's shadow DOM.It's registered site-wide by the Lit bundle, so plain markup and runtime-built markup need no import. Inside a Lit component,
import './OlIcon.js'and use it in a template like any other element.What you control: size (
sm/md/lg= 16/20/24px), color, and — where a component wants a non-default — stroke weight via--ol-icon-stroke-width.Why two and not one
This is the one place the system isn't as simple as I'd like, so it's worth stating plainly: a
<use>reference cannot cross a shadow-DOM boundary. That's a browser rule, not a design choice. Without shadow-DOM components, this PR would be just the sprite and the macro.So
<ol-icon>inlines the glyph into its own shadow root rather than pointing at the sprite, which is why the build emits a glyph module alongside it. Both come from the same sources — there's no second set of icons to keep in sync. And the macro stays because an element has to wait for JavaScript, which a server-rendered icon shouldn't.Earlier revisions of this branch had a third path: components imported a named glyph and hand-wrote the
<svg>wrapper around it. That's gone. The wrapper's attributes were duplicated at every call site and free to drift, and<ol-icon>does the same job in one tag.Also rejected
<use>story, but it repeats the payload on every response. A hashed external file caches once for the whole session.<ol-icon>for server-rendered icons too. One path instead of two, but every icon on the page would then wait on JavaScript and reserve a box that fills in a frame later. The macro covers those without the wait.build-assets, so paying it is cheaper than policing the outputs.icon_sprite_url()falls back to the unhashed path when the sprite is missing, so a checkout that never ranmake iconsdraws blank icons rather than 500ing every page that has one.Scope
43 icons, adopted across 17 templates and 12 Lit components — the header, search, book and edition pages, My Books, the markdown editor, dialogs, and the signup/login forms. The editions table also moves off its PNG-era DataTables chrome onto
<ol-pagination>and sprite sort arrows.Remaining surfaces still use their old images and are unaffected; they can be converted incrementally. Nothing in this PR requires a follow-up to be correct.
The 50 now-dead image files — the 21 replaced here, plus 29 that had already been dead for years and were found while auditing — are deleted in a follow-up rather than here. They're the only binary content the branch had, and GitHub's
.diffomits binary data, which forced every patch deploy to pass--exclude=static/images/*or abort outright. Unreferenced files on disk cost nothing in the meantime.How to review this
119 files is misleading — 43 of them are one-line SVG sources, with nothing to read:
design.py+ its JS)The 17 call-site templates are one-line swaps of an
<img>or an inline<svg>for a macro call, so they're fine to skim. Two things are worth real attention:static/icons/src/lucide/, runmake icons, call$:macros.icon("name"). If that flow is awkward it stays awkward for everyone after us, so it's the part most worth pushing back on.Nothing generated is checked in, so there's nothing that can silently drift from its sources:
make iconsis the only way to produce the sprite and the glyph module, and both are gitignored. The gallery on the design page reads the source SVG file names directly for the same reason.One thing here isn't icon-specific, and is the reason the docs shouldn't rot:
/developers/designnow has three sections whose content is derived rather than hand-written — design tokens parsed out of the token CSS, Lit API tables from the Custom Elements Manifest, and the icon gallery from the icon sources. Icons are the third consumer of that idea, not a new mechanism.Testing
Have a browse around testing and make sure everything still looks right — icons show up in a lot of places, so general clicking around is more useful here than a checklist. Then take a look at the Icons section of the design docs at
/developers/design/icons, which is where the gallery and the usage guidance live.Pulling the branch locally: the sprite and the glyph module are generated, so run
npm run build-assets(or justmake iconsif that's all you need) once after checkout. If icons come up blank anywhere, that's the build not having run.Stakeholders
@cdrini
cc: @mekarpeles @jimchamp