Files
marketplaces/docs/UI-ICON-AUDIT.md
sdarbinyan a362dd4668
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
docs(icons): add UI icon & visual language audit
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 03:03:30 +04:00

111 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RC UI Sprint — Icon & Visual Language Audit
Full-application audit and consolidation onto **one icon library** (Lucide, via
`@lucide/angular`) across storefront, Marketplace Builder, and backoffice. Before this
pass the app mixed three icon systems in the same screens: **PrimeIcons** (`pi pi-*`
CSS classes), **dozens of hand-rolled inline `<svg>` blocks** (often the same icon
redrawn from scratch at different sizes/colors), and **bare text glyphs** (`♥`, `⇄`,
`«`, `»`) standing in for icons entirely outside any icon system.
## Foundation
- Added `@lucide/angular` (the maintained package; the unscoped `lucide-angular` is
deprecated upstream — installed the correct one directly).
- One shared entry point for every icon: `app-icon` (`src/app/shared/ui/icon/`),
backed by a canonical `name → Lucide icon` registry (`icon-registry.ts`). Default
size 20px, default stroke-width 2 — the same visual weight everywhere unless a
call site explicitly overrides it for a specific context (e.g. a 48px empty-state
illustration vs. a 14px inline chevron).
- `app-icon` is decorative (`aria-hidden`) by default. An `ariaLabel` input exists for
the rare case an icon carries meaning with no adjacent text; in practice, the label
almost always belongs on the surrounding interactive element instead.
## Old icon → New icon → Reason
| Area | Old | New | Reason |
|---|---|---|---|
| Admin dashboard | `pi-file-edit`, `pi-heart`, `pi-clock`, `pi-question-circle`, `pi-check-circle`, `pi-save`, `pi-upload`, `pi-book`, `pi-bolt`, `pi-flag`, `pi-megaphone`, `pi-circle` (data-driven via `icon: 'pi-*'` fields) | `edit`, `heart`, `clock`, `help`, `checkCircle`, `save`, `upload`, `book`, `zap`, `flag`, `megaphone`, `circle` | PrimeIcons → Lucide; icon fields retyped `AppIconName` so an unmapped name is a compile error, not a silently blank icon |
| Backoffice shell (sidebar nav + topbar) | `pi-home`, `pi-box`, `pi-tags`, `pi-shopping-cart`, `pi-user`, `pi-credit-card`, `pi-star`, `pi-chart-bar`, `pi-sitemap`, `pi-users`, `pi-cog`, `pi-desktop`, `pi-chart-line`, `pi-sign-out`, `pi-bars`, `pi-search`, `pi-upload`, `pi-building`, `pi-bell` | `home`, `package`, `tags`, `cart`, `user`, `creditCard`, `star`, `chartBar`, `network`, `users`, `settings`, `monitor`, `chartLine`, `logOut`, `menu`, `search`, `upload`, `building`, `bell` | PrimeIcons → Lucide |
| Marketplace Builder (overview, nav, sections, HTML editor toolbar) | `pi-arrow-left/right-circle/up/down`, `pi-check-circle`, `pi-circle`, `pi-circle-fill`, `pi-question-circle`, `pi-desktop`, `pi-shop`, `pi-image`, `pi-th-large`, `pi-box`, `pi-images`, `pi-history`, `pi-megaphone`, `pi-verified`, `pi-code`, `pi-globe`, `pi-sliders-h`, `pi-compass`, `pi-copy`, `pi-trash`, `pi-list`, `pi-link`, `pi-table`, `pi-minus`, `pi-video`, **`pi-bars` reused for drag handles** | `arrowLeft/Right/Up/Down`, `checkCircle`, `circle`, `help`, `monitor`, `store`, `image`, `layoutGrid`, `package`, `images`, `history`, `megaphone`, `verified`, `code`, `globe`, `slidersHorizontal`, `compass`, `copy`, `trash`, `list`, `link`, `table`, `minus`, `video`, **new dedicated `grip` icon** | PrimeIcons → Lucide. `pi-bars` had been reused for both the hamburger menu **and** every drag-to-reorder handle — two unrelated meanings sharing one icon, exactly the "duplicated icon, different meaning" pattern the audit brief calls out. Added `GripVertical` as a distinct drag-handle icon. |
| Header (storefront) | Two hand-drawn magnifying-glass SVGs (`#576463` desktop / `#1e3c38` mobile — same icon, two different hardcoded colors), `♥`/`⇄` **text glyphs** for wishlist/compare, hand-drawn cart SVG, hand-drawn home/catalog mobile-menu icons, 3× duplicated inline chevron SVG | `search` (color via `currentColor`), `heart`, `scale`, `cart`, `home`, `layoutGrid`, `chevronRight` | Consolidates one icon drawn twice with two different colors into one; replaces bare text characters (`♥`/`⇄`) that weren't part of any icon system at all |
| Search page | Same magnifying-glass path hand-duplicated **4 times** (input icon, empty-query, no-results, no-query states) at 3 sizes/3 colors | `search` ×4, color preserved per state via a CSS `color` property on the wrapper | Textbook duplication — one icon, four copy-pasted SVGs |
| Cart | Trash/X/plus/minus inline SVGs, standalone `EmptyCartIconComponent` (an 80px duplicate of the same cart glyph, only used once), chat-bubble "login gate" icon, duplicated close-X icon (2 modals), duplicated refresh icon (QR expired/error) | `trash`, `x`, `plus`, `minus`, `cart` (inline, component deleted), `lock`, `x`, `refresh` | Removed a whole component that existed only to duplicate an icon already available; unified the "login required" icon with telegram-login's identical icon (same concept, was drawn twice) |
| Language/region selectors | 3× duplicated chevron SVG (language dropdown, currency dropdown, region dropdown — identical path, copy-pasted), map-pin SVG, crosshair "locate" SVG, globe SVG | `chevronDown` ×3, `mapPin`, `locate`, `globe` | Same chevron redrawn three times in two components |
| Items carousel, category/subcategories, item-detail | Hand-drawn star (hardcoded `#497671` fill), hand-drawn cart icon (×3 separate redraws across 3 files), no-image/package/grid empty-state illustrations, check/X status icons, thumbs-up/down vote icons, dynamic fill/stroke rating stars | `star` (with new `color` input + `.dx-star--filled` CSS class for the solid/outline toggle), `cart`, `image`/`package`/`layoutGrid`, `check`/`x`, `thumbsUp`/`thumbsDown` | The cart icon alone had been hand-drawn from scratch in 5 different files across this audit (header, items-carousel, subcategories, item-detail, cart) — now one icon, one registry entry |
| Shared `app-select` | Browser-native `<select>` dropdown indicator (renders differently per browser, no relation to the icon system) | `chevronDown` (native indicator hidden via `appearance: none`) | "Selects: replace browser default indicators" — this component is used by nearly every admin form, so the fix applies everywhere at once |
| Shared `app-pagination` | `&laquo;` / `&raquo;` HTML entities | `chevronLeft` / `chevronRight` | "Pagination: use proper chevrons" |
| Every native `<details>`/`<summary>` expander (7 call sites: admin product form ×4, page editor, builder widget panel) | Browser-default disclosure triangle (differs Chrome/Firefox/Safari) | Lucide `ChevronDown` path drawn via one global CSS rule (`details > summary::after`), rotated 180° on `[open]` | Covers all 7 expanders with a single CSS change rather than touching each template — no shared class existed to hang a component-based fix on |
## Consistency improvements
- **One icon family everywhere.** Zero PrimeIcons (`pi-*`) class usages remain
anywhere in the app (verified by full-repo sweep after each batch). PrimeIcons
(`primeicons` package) is still installed as a PrimeNG peer dependency but no
longer used directly for any icon in app code.
- **One default size/stroke-width.** `app-icon` defaults to 20px / stroke-width 2;
every call site that deviates does so for a legible, deliberate reason (48px empty-
state illustration, 14px inline chevron), not arbitrary per-instance sizing.
- **Color via `currentColor`, not hardcoded hex.** Every hand-rolled SVG that baked a
specific hex into its `fill`/`stroke` attribute now inherits color from its
surrounding CSS `color`, so hover/active/disabled states that already change text
color also correctly recolor the icon — previously several icons ignored those
states entirely because their color was hardcoded in the SVG markup.
- **Duplicated icons resolved to one instance:**
- Magnifying glass: was hand-drawn independently in header (×2), search page (×4).
Now one `search` icon everywhere.
- Shopping cart: was hand-drawn independently in header, items-carousel,
subcategories, item-detail, and cart (via the now-deleted
`EmptyCartIconComponent`). Now one `cart` icon everywhere.
- Chevron/dropdown arrow: was hand-drawn independently in language-selector (×2),
region-selector, and the builder mobile nav (×3). Now one `chevronDown`/
`chevronRight` everywhere.
- "Login required" icon (chat-bubble shape): was drawn identically in both
telegram-login and cart's login gate. Now one `lock` icon in both, matching the
actual semantic ("authentication required") better than a chat bubble did.
- **Bare text glyphs replaced with real icons.** `♥` and `⇄` in the header were plain
Unicode characters, not part of any icon system, inconsistent stroke weight and
optical size versus every other icon on the same toolbar. Now `heart` and `scale`.
- **Corrected a genuine meaning collision**, not just a style one: PrimeIcons'
`pi-bars` (hamburger lines) was reused in the Marketplace Builder for both the
mobile menu toggle *and* every drag-to-reorder handle. A user scanning the builder
UI would see the same glyph mean "open navigation" in the header and "drag this
row" in a list — added a dedicated `grip` icon (`GripVertical`) so the two concepts
are now visually distinct.
- **Icon-only buttons audited for accessible names.** Swept every button whose only
content is an icon; found three relying on `title` alone (not reliably announced by
screen readers) or nothing at all (region-selector detect-location, carousel
add-to-cart, subcategories add-to-cart) and added `aria-label` to all three.
- **Decorative icons intentionally kept as custom SVG (not migrated):**
- The **Telegram brand logo** (cart, telegram-login) — a brand mark, not a generic
icon; replacing it with a generic Lucide icon would misrepresent the brand.
- **`layout-switcher`'s grid-pattern preview icons** — these show the actual layout
being selected (2-column, compact grid, list, etc.) as a literal visual preview,
not a stand-in for a word. Lucide has no equivalent "this exact grid pattern"
icon set; redrawing them as generic layout icons would lose the preview function.
## Remaining issues / backlog
- **No sortable table columns exist anywhere in the app** to add sort-direction
icons to (every admin list table has static, non-interactive `<th>` labels). Adding
actual column-sort interactivity would be new functionality, out of scope for an
icon/visual-language pass — flagged here rather than invented.
- **Bundle size**: `app-icon` renders each Lucide icon as its own standalone Angular
component (the current `@lucide/angular` API — no tree-shakeable "icon font" or
sprite sheet). The initial bundle grew from the prior build's already-over-budget
~585 KB over the 700 KB target to ~760 KB over, mostly from icon components now
bundled eagerly in the admin shell/dashboard/builder (loaded on every admin route).
Worth a follow-up pass to lazy-load icon-heavy admin sections if bundle size becomes
a concrete problem.
- **No `docs/DESIGN.md` exists** in this repo (confirmed across all three RC sprints
this session). The `impeccable` design-quality hook flagged pre-existing font-size/
radius/color values against a document that doesn't exist — none of those findings
were introduced by this pass; they're pre-existing values in files this pass
touched for unrelated reasons (colors, radii, spacing untouched).
- **PrimeIcons (`primeicons` npm package) is still installed** — it's a transitive
dependency PrimeNG components may rely on internally (calendar, carousel nav
arrows rendered by `p-carousel`, etc.), so it wasn't removed from `package.json`.
No app code imports `pi-*` classes directly anymore, but a future pass could audit
whether PrimeNG's own internal icon usage (e.g. `p-carousel`'s built-in prev/next
arrows) should also be re-skinned to match, or left as PrimeNG's own visual
language since those are framework-owned, not hand-authored.