Repository navigation
chore(frontend): replace mixed Unicode glyphs with one SVG icon set - #1386
Conversation
The sidebar draws Feather-style inline SVGs, but row actions, toasts, sort arrows, column-filter triggers, tooltips and expanders used Unicode glyphs (▶ ⏸ ⏵ ✎ ✕ ↻ ⚠ ↕ ▲ ▼ ⛛ ⓘ ⚙︎ ↓ ×). Those come from whatever font the platform picks, so their size and weight drift between browsers, and ⏸ / ⏵ / ⚠ can render as color emoji on macOS. Add src/icons.ts with one set of Feather (MIT) shapes, plus Lucide's chevrons-up-down, all on the sidebar's 24px grid with a 2px round stroke and currentColor. It exposes iconSvg() for the template-literal renderers and createIcon() for the DOM-built ones that avoid innerHTML. Use it for the planned-purchase row actions, toast kind and close icons, the Opportunities sort indicators and expand chevrons, every column-filter trigger, the History retry/ops-hint badges, the user-row expander, the federation download icon, modal close buttons, the copy tick, the info tooltips and the Exchange settings button. The costly "Run now" action becomes a labelled button (icon + text) so it no longer looks like the other small icon-only buttons. A jest guard fails if the replaced glyphs come back in UI source. Refs LeanerCloud#751 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review feedback on the icon set: the API keys table's Revoke and Delete
were chunky text buttons that stacked vertically in the Actions column,
and the group editor's permission Remove button stretched to the height
of label + select beside the Action/Resource selects. Both now use the
shared icon module: compact square icon buttons (slash for Revoke, trash
for Delete and Remove) that keep the .btn-warning/.btn-danger colours,
with aria-label and title ("Revoke API key", "Delete API key", "Remove
permission"). Revoke/Delete sit side by side in a non-wrapping
.row-actions group; Remove is pinned to the row bottom at the selects'
height. Handlers, confirm dialogs and data attributes are unchanged.
New icons copy, slash and trash-2 (Feather). The group-edit injection
guards now ignore svg.icon, which is the Remove button's own icon.
New jest and Playwright tests check the icons, accessible names,
handlers and the side-by-side / aligned geometry.
Refs LeanerCloud#751
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same admin page, same pattern: the users table's Edit/Delete and the group cards' Edit/Duplicate/Delete were text buttons next to the new icon-only API key actions. They now use pencil, copy and trash icons from the shared set in a .row-actions group, keeping the danger variant on Delete. Users keep their per-row aria-labels; group buttons gain "Edit/Duplicate/Delete group <name>" aria-labels and short titles. groups.test's XSS case asserted on serialised innerHTML, which leaves < and > unescaped inside attribute values, so the group name in the new aria-label tripped it although no element is created. It now checks the DOM: no script/img element, text round-trips, and the aria-label holds the raw name without extra attributes. Refs LeanerCloud#751 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/groups/groupList.ts: combined. Main (LeanerCloud#1392) hides Edit and Delete on system-managed groups and shows a "System group" badge instead; this branch turned Edit / Duplicate / Delete into icon buttons in a .row-actions group. Result: the badge replaces the pencil Edit button for locked groups, Delete (trash) is omitted for them, and Duplicate (copy) always shows, all inside .row-actions. Imports keep both iconSvg and main's systemGroups helpers. - frontend/src/groups/groupModals.ts: import block. Kept iconSvg (used by the trash Remove-permission button) and main's groupFormValidation and permissionOptions imports; dropped ALL_ACTIONS/ALL_RESOURCES/Action/ Resource, which main moved into permissionOptions.ts with the option builders. - frontend/src/history.ts: imports keep both iconSvg and main's history-queue-link helpers. Retry button and retry lineage badge keep this branch's alert-triangle / rotate-cw icons with main's Number(p.retry_attempt_n) || 0 coercion (LeanerCloud#1318 escaping hardening). - frontend/src/lib/history-filter-popover.ts: filter button keeps main's data-filter-scope attribute (LeanerCloud#1342) with this branch's filter icon in place of the ⛛ glyph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/history.ts: imports kept both (this branch's iconSvg and main's history-paging helpers from LeanerCloud#1358). Main (LeanerCloud#1394) moved the retry lineage links into renderLineageLinks() and appends them to the ops-hint badge; took main's structure and applied this branch's icons inside it: the ops hint uses iconSvg('alert-triangle') and both lineage links use iconSvg('rotate-cw') instead of the restored glyphs. - frontend/src/ladder.ts: imports kept both (iconSvg and main's openModal/closeModal from LeanerCloud#1372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 1rem literal duplicates --cudly-fs-md (same value), and LeanerCloud#741's typography guard rejects literals that duplicate a token, so landing both would turn main's jest red. Same computed size. Refs LeanerCloud#751 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Applied the review's non-blocking suggestion in |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Local verification at
|
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
Conflicts: - frontend/src/ladder.ts: main's LeanerCloud#1386 (LeanerCloud#751 icon set) imported iconSvg and swapped the ladder modal's close × for iconSvg('x') in the inline modal markup, which this branch moved to ladder-config-modal.ts. Kept this branch's side (confirmDialog import, renderLadderConfigModal() call) and reapplied main's change in ladder-config-modal.ts: it now imports iconSvg and renders the close button as ${iconSvg('x')}. ladder.ts no longer needs the iconSvg import. frontend/src/toast.ts auto-merged: main's SVG kind/close icons (createIcon) sit alongside this branch's layoutToastStack placement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidebar draws Feather-style SVG icons, but row actions, toasts, sort arrows, column filters, tooltips and expanders used Unicode glyphs (
▶ ⏸ ⏵ ✎ ✕ ↻ ⚠ ↕ ▲ ▼ ⛛ ⓘ ⚙︎). Their size and weight come from whatever font the platform has,⏸/⏵/⚠can render as color emoji on macOS, and the money-triggering "Run now" action was only a small blue▶. This PR adds one SVG icon module, uses it for those glyphs and for the admin page's row actions, and makes Run now a labeled button.Refs #751
Partial: the issue's suggested overflow menu (
⋯) for row actions is not added (Run now became a labeled button instead; the menu is left as a design decision), and glyphs inside plain text strings,<option>labels and CSS::beforecontent are unchanged because SVG cannot go there.What changed
frontend/src/icons.ts: one typed name-to-shape table (Feather shapes, plus Lucide'schevrons-up-down) on the sidebar's 24px grid, 2px round stroke,currentColor. It exportsiconSvg()for template-literal renderers andcreateIcon()for DOM-built ones that avoidinnerHTML; both mark the iconaria-hiddenandfocusable="false". Newfrontend/src/styles/icons.csssizes icons inem.plans.ts: row actions and column filter; Run now is a labeled button (play icon + "Run now").toast.ts: kind icons and close.recommendations.ts: sort indicators, column filter, expand chevrons, lookback info tooltip.riexchange.ts(column filters, remove-target x),lib/history-filter-popover.ts(filter),history.ts(Retry, override, ops hint, lineage),users/userList.ts(expand chevron),federation.ts(download),confirmDialog.tsandladder.ts(close x),settings.ts(copy tick).index.html: 10 info tooltips, the Exchange settings gear, a modal close.apikeys.ts): Revoke (slash,.btn-warning) and Delete (trash,.btn-danger) sit side by side in a non-wrapping.row-actionsgroup instead of stacking.groups/groupModals.ts): the permission Remove is a trash icon button pinned to the row bottom at the selects' height, instead of stretching to label + select.users/userList.ts) Edit / Delete and group cards (groups/groupList.ts) Edit / Duplicate / Delete use pencil / copy / trash icon buttons, keeping the danger variant on Delete.aria-label("Revoke API key", "Delete API key", "Remove permission",Edit user <email>,Edit group <name>, ...) and atitletooltip. Handlers, confirm dialogs, classes and data attributes are unchanged.copy,slash,trash-2(Feather). New CSS:button.icon-btn(1.75rem square, no padding),.row-actions, and.permission-item .form-row > .remove-permission-btn.src/__tests__/icons.test.ts,src/__tests__/row-action-icons.test.tsandtests-e2e/admin-row-actions.spec.ts;plans-permissions.test.ts,groups.test.tsandgroup-edit-unrepresentable-permissions.test.tsupdated.Why this approach
A typed name-to-shape table with two renderers keeps one source of truth, matches the inline SVG the sidebar already uses, and adds no dependency (no icon font or npm package). Icons inherit
currentColor, so the existing color, hover and active rules keep working. A labeled Run now button fixes the weak affordance without adding a new menu widget and its keyboard handling. Using the same icon buttons for all row actions on the admin page keeps the API key, user and group actions consistent with each other.Screenshots
Before is main (
a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API. They were taken on Linux, where the glyphs do not turn into emoji, so the pairs show the style mismatch and its fix rather than emoji rendering.▶ ⏸ ⏵ ✎ ✕) of mismatched sizes, a bare▶for Run now and⛛column filters; this branch uses SVG play / pause / pencil / x icons, a labeled Run now and funnel filter icons.✓and×glyphs become check-circle and x icons in the same colors.↕/▼sort glyphs and⛛filters become chevron sort indicators (the active column shows a blue chevron-down) and funnel icons.↻ Retry,⚠ Retried 5×, the⚠ops hint and↻ Retry #2use rotate-cw and alert-triangle icons with the same text, and the filters use funnels.Verification
Run on this branch's head
8fe39de1on top of maina14dc8f2:npm run lint: 0 errors (142 pre-existing warnings). Typecheck and build pass.icons.test.ts: every icon renders a decorativecurrentColorSVG,createIconmatchesiconSvg, toasts use SVG kind and close icons, and a source guard fails if any of the replaced glyphs return in UI code.plans-permissions.test.tsasserts every row action has an SVG icon and that Run now has a text label.row-action-icons.test.ts(5 tests): finds each button bygetByRolename and checks the SVG icon, empty visible text,title, color variant and data attributes, and that Revoke / Delete still confirm and then call the API and Remove still drops its row.groups.test.ts: the XSS case now checks the DOM (noscript/imgelement, text round-trips, thearia-labelholds the raw name with no extra attributes), because serializedinnerHTMLleaves<and>unescaped inside the newaria-label. Thegroup-edit-unrepresentable-permissionsinjection guards ignoresvg.icon, the Remove button's own icon.e2e-local.sh: 159 passed. Newadmin-row-actions.spec.ts: Revoke and Delete share one row (same top, under 8px apart), and Remove matches the selects' top and height to within 1px.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Notes for review
⋯), as the issue suggested, is the alternative..sort-indicator; the size is a judgment call.⚠inside text strings (the approval-details "Account deleted" cell, agroupModals<option>), the "Columns ▾" button text, Archera's "← Back", CSS::beforeglyphs (util-green/yellow/red▲■▼,✓incomponents.css,▸inmodals.css), the auth password checklist○/✓, permission-matrix✓cells, and→arrows in prose.icons.tsheader; the repo may want a third-party notice file.aria-labelcoverage on the remaining icon-only actions is tracked separately in fix(frontend): icon-only row actions (Plans, RI Exchange) lack labels, tooltips and aria-names #184.Overlaps
history.tslineage / Retry lines and keep↻/⚠. They conflict with this branch, and theicons.test.tssource guard will flag↻if their version wins.lib/history-filter-popover.ts(addsdata-filter-scope), a textual conflict.ladder.tsintoladder-config-modal.ts, so this branch's close-icon change inladder.tsconflicts and has to be reapplied in the new file.users/userList.tsandgroups/groupList.ts; a merge-tree check shows no textual conflict.plans.ts,recommendations.ts,riexchange.tsandindex.htmlelsewhere.Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code