perf: drop @lucide/angular, hand-roll used icons - initial bundle 13.68MB -> 2.64MB
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

@lucide/angular shipped its entire ~1500-icon set in the initial bundle
despite the app only using 85 named-imported icons - confirmed upstream
tree-shaking failure (sideEffects:false, clean named imports, single
non-splittable fesm file). Replaced icon-registry.ts/icon.component.ts
with hand-rolled inline SVG rendering of just the 85 used icons,
transcribed from lucide's own node data for pixel-identical output.
Zero call-site changes - AppIconName and app-icon's public API unchanged.

Also: karma-coverage wired (npm run test:coverage), baseline captured
in docs/SPRINT-PLAN-NEXT.md (32% statements / 18.5% branches).

Cart-modal -> app-dialog migration was attempted and reverted - real
conflicts (backdrop-close, nested-modal escape priority, iframe sizing),
documented in docs/FUTURE_FEATURES.md for a properly scoped follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-06 10:55:00 +04:00
parent a95ca37a4b
commit 6d075fc5b9
7 changed files with 635 additions and 203 deletions

View File

@@ -1,179 +1,414 @@
import {
LucideAlignLeft as AlignLeft,
LucideArrowDown as ArrowDown,
LucideArrowLeft as ArrowLeft,
LucideArrowRight as ArrowRight,
LucideArrowUp as ArrowUp,
LucideBadgeCheck as BadgeCheck,
LucideBarChart3 as BarChart3,
LucideBell as Bell,
LucideBook as Book,
LucideBoxes as Boxes,
LucideBuilding2 as Building2,
LucideCalendar as Calendar,
LucideCheck as Check,
LucideCheckCircle2 as CheckCircle2,
LucideChevronDown as ChevronDown,
LucideChevronLeft as ChevronLeft,
LucideChevronRight as ChevronRight,
LucideChevronUp as ChevronUp,
LucideCircle as Circle,
LucideClock as Clock,
LucideCode2 as Code2,
LucideCompass as Compass,
LucideCopy as Copy,
LucideCreditCard as CreditCard,
LucideDatabase as Database,
LucideDownload as Download,
LucideFilter as Filter,
LucideFlag as Flag,
LucideFolder as Folder,
LucideGlobe as Globe,
LucideGripVertical as GripVertical,
LucideHeart as Heart,
LucideHelpCircle as HelpCircle,
LucideHistory as History,
LucideHome as Home,
LucideImage as Image,
LucideImages as Images,
LucideInfo as Info,
LucideLayoutGrid as LayoutGrid,
LucideLineChart as LineChart,
LucideLink as Link,
LucideList as List,
LucideLoader2 as Loader2,
LucideLocate as Locate,
LucideLock as Lock,
LucideLogOut as LogOut,
LucideMapPin as MapPin,
LucideMegaphone as Megaphone,
LucideMenu as Menu,
LucideMinus as Minus,
LucideMonitor as Monitor,
LucideNetwork as Network,
LucidePackage as Package,
LucidePalette as Palette,
LucidePencil as Pencil,
LucidePlus as Plus,
LucidePlusCircle as PlusCircle,
LucideRefreshCw as RefreshCw,
LucideSave as Save,
LucideScale as Scale,
LucideSearch as Search,
LucideSettings as Settings,
LucideShield as Shield,
LucideShoppingCart as ShoppingCart,
LucideSlidersHorizontal as SlidersHorizontal,
LucideSquare as Square,
LucideStar as Star,
LucideStore as Store,
LucideTable as Table,
LucideTag as Tag,
LucideTags as Tags,
LucideThumbsDown as ThumbsDown,
LucideThumbsUp as ThumbsUp,
LucideTrash2 as Trash2,
LucideTrendingUp as TrendingUp,
LucideTriangleAlert as TriangleAlert,
LucideUpload as Upload,
LucideUser as User,
LucideUsers as Users,
LucideVideo as Video,
LucideX as X,
LucideXCircle as XCircle,
LucideZap as Zap
} from '@lucide/angular';
/**
* Canonical name -> Lucide icon map for the whole app (storefront + builder + backoffice).
* One entry per concept so every screen renders the same icon for the same meaning
* (RC UI Sprint - Icon & Visual Language Audit). Extend this list rather than
* importing Lucide icons ad hoc in individual components.
* Canonical name -> raw Lucide icon node data map for the whole app
* (storefront + builder + backoffice). One entry per concept so every screen
* renders the same icon for the same meaning (RC UI Sprint - Icon & Visual
* Language Audit). Extend this list rather than importing icon data ad hoc in
* individual components.
*
* Node data is transcribed directly from @lucide/angular's icon set
* (fesm2022/lucide-angular.mjs) rather than importing the package, because
* the package ships as a single non-tree-shakeable bundle that would pull
* ~1500 unused icon classes into the initial JS bundle. See IconComponent
* for the renderer that consumes this data.
*/
export type LucideIconNode = readonly [string, Readonly<Record<string, string>>];
export const APP_ICONS = {
home: Home,
store: Store,
cart: ShoppingCart,
package: Package,
boxes: Boxes,
folder: Folder,
tag: Tag,
tags: Tags,
users: Users,
user: User,
settings: Settings,
chartBar: BarChart3,
chartLine: LineChart,
search: Search,
filter: Filter,
heart: Heart,
scale: Scale,
bell: Bell,
image: Image,
images: Images,
palette: Palette,
monitor: Monitor,
shield: Shield,
lock: Lock,
database: Database,
upload: Upload,
download: Download,
edit: Pencil,
trash: Trash2,
copy: Copy,
save: Save,
plus: Plus,
plusCircle: PlusCircle,
minus: Minus,
refresh: RefreshCw,
check: Check,
checkCircle: CheckCircle2,
x: X,
xCircle: XCircle,
arrowUp: ArrowUp,
arrowDown: ArrowDown,
arrowLeft: ArrowLeft,
arrowRight: ArrowRight,
chevronUp: ChevronUp,
chevronDown: ChevronDown,
chevronLeft: ChevronLeft,
chevronRight: ChevronRight,
info: Info,
help: HelpCircle,
warning: TriangleAlert,
clock: Clock,
calendar: Calendar,
menu: Menu,
alignLeft: AlignLeft,
book: Book,
building: Building2,
code: Code2,
compass: Compass,
creditCard: CreditCard,
flag: Flag,
globe: Globe,
grip: GripVertical,
history: History,
link: Link,
list: List,
locate: Locate,
mapPin: MapPin,
megaphone: Megaphone,
logOut: LogOut,
network: Network,
slidersHorizontal: SlidersHorizontal,
spinner: Loader2,
star: Star,
stop: Square,
table: Table,
layoutGrid: LayoutGrid,
thumbsUp: ThumbsUp,
thumbsDown: ThumbsDown,
verified: BadgeCheck,
video: Video,
circle: Circle,
zap: Zap,
trendingUp: TrendingUp
} as const;
home: [
["path", { "d": "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" }],
["path", { "d": "M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }]
],
store: [
["path", { "d": "M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5" }],
["path", { "d": "M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244" }],
["path", { "d": "M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05" }]
],
cart: [
["circle", { "cx": "8", "cy": "21", "r": "1" }],
["circle", { "cx": "19", "cy": "21", "r": "1" }],
["path", { "d": "M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12" }]
],
package: [
["path", { "d": "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" }],
["path", { "d": "M12 22V12" }],
["polyline", { "points": "3.29 7 12 12 20.71 7" }],
["path", { "d": "m7.5 4.27 9 5.15" }]
],
boxes: [
["path", { "d": "M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" }],
["path", { "d": "m7 16.5-4.74-2.85" }],
["path", { "d": "m7 16.5 5-3" }],
["path", { "d": "M7 16.5v5.17" }],
["path", { "d": "M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" }],
["path", { "d": "m17 16.5-5-3" }],
["path", { "d": "m17 16.5 4.74-2.85" }],
["path", { "d": "M17 16.5v5.17" }],
["path", { "d": "M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" }],
["path", { "d": "M12 8 7.26 5.15" }],
["path", { "d": "m12 8 4.74-2.85" }],
["path", { "d": "M12 13.5V8" }]
],
folder: [
["path", { "d": "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" }]
],
tag: [
["path", { "d": "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" }],
["circle", { "cx": "7.5", "cy": "7.5", "r": ".5", "fill": "currentColor" }]
],
tags: [
["path", { "d": "M13.172 2a2 2 0 0 1 1.414.586l6.71 6.71a2.4 2.4 0 0 1 0 3.408l-4.592 4.592a2.4 2.4 0 0 1-3.408 0l-6.71-6.71A2 2 0 0 1 6 9.172V3a1 1 0 0 1 1-1z" }],
["path", { "d": "M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193" }],
["circle", { "cx": "10.5", "cy": "6.5", "r": ".5", "fill": "currentColor" }]
],
users: [
["path", { "d": "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }],
["path", { "d": "M16 3.128a4 4 0 0 1 0 7.744" }],
["path", { "d": "M22 21v-2a4 4 0 0 0-3-3.87" }],
["circle", { "cx": "9", "cy": "7", "r": "4" }]
],
user: [
["path", { "d": "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }],
["circle", { "cx": "12", "cy": "7", "r": "4" }]
],
settings: [
["path", { "d": "M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" }],
["circle", { "cx": "12", "cy": "12", "r": "3" }]
],
chartBar: [
["path", { "d": "M3 3v16a2 2 0 0 0 2 2h16" }],
["path", { "d": "M18 17V9" }],
["path", { "d": "M13 17V5" }],
["path", { "d": "M8 17v-3" }]
],
chartLine: [
["path", { "d": "M3 3v16a2 2 0 0 0 2 2h16" }],
["path", { "d": "m19 9-5 5-4-4-3 3" }]
],
search: [
["path", { "d": "m21 21-4.34-4.34" }],
["circle", { "cx": "11", "cy": "11", "r": "8" }]
],
filter: [
["path", { "d": "M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z" }]
],
heart: [
["path", { "d": "M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" }]
],
scale: [
["path", { "d": "M12 3v18" }],
["path", { "d": "m19 8 3 8a5 5 0 0 1-6 0zV7" }],
["path", { "d": "M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1" }],
["path", { "d": "m5 8 3 8a5 5 0 0 1-6 0zV7" }],
["path", { "d": "M7 21h10" }]
],
bell: [
["path", { "d": "M10.268 21a2 2 0 0 0 3.464 0" }],
["path", { "d": "M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" }]
],
image: [
["rect", { "width": "18", "height": "18", "x": "3", "y": "3", "rx": "2", "ry": "2" }],
["circle", { "cx": "9", "cy": "9", "r": "2" }],
["path", { "d": "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" }]
],
images: [
["path", { "d": "m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16" }],
["path", { "d": "M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2" }],
["circle", { "cx": "13", "cy": "7", "r": "1", "fill": "currentColor" }],
["rect", { "x": "8", "y": "2", "width": "14", "height": "14", "rx": "2" }]
],
palette: [
["path", { "d": "M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" }],
["circle", { "cx": "13.5", "cy": "6.5", "r": ".5", "fill": "currentColor" }],
["circle", { "cx": "17.5", "cy": "10.5", "r": ".5", "fill": "currentColor" }],
["circle", { "cx": "6.5", "cy": "12.5", "r": ".5", "fill": "currentColor" }],
["circle", { "cx": "8.5", "cy": "7.5", "r": ".5", "fill": "currentColor" }]
],
monitor: [
["rect", { "width": "20", "height": "14", "x": "2", "y": "3", "rx": "2" }],
["line", { "x1": "8", "x2": "16", "y1": "21", "y2": "21" }],
["line", { "x1": "12", "x2": "12", "y1": "17", "y2": "21" }]
],
shield: [
["path", { "d": "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" }]
],
lock: [
["rect", { "width": "18", "height": "11", "x": "3", "y": "11", "rx": "2", "ry": "2" }],
["path", { "d": "M7 11V7a5 5 0 0 1 10 0v4" }]
],
database: [
["ellipse", { "cx": "12", "cy": "5", "rx": "9", "ry": "3" }],
["path", { "d": "M3 5V19A9 3 0 0 0 21 19V5" }],
["path", { "d": "M3 12A9 3 0 0 0 21 12" }]
],
upload: [
["path", { "d": "M12 3v12" }],
["path", { "d": "m17 8-5-5-5 5" }],
["path", { "d": "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }]
],
download: [
["path", { "d": "M12 15V3" }],
["path", { "d": "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
["path", { "d": "m7 10 5 5 5-5" }]
],
edit: [
["path", { "d": "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" }],
["path", { "d": "m15 5 4 4" }]
],
trash: [
["path", { "d": "M10 11v6" }],
["path", { "d": "M14 11v6" }],
["path", { "d": "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }],
["path", { "d": "M3 6h18" }],
["path", { "d": "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }]
],
copy: [
["rect", { "width": "14", "height": "14", "x": "8", "y": "8", "rx": "2", "ry": "2" }],
["path", { "d": "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }]
],
save: [
["path", { "d": "M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" }],
["path", { "d": "M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" }],
["path", { "d": "M7 3v4a1 1 0 0 0 1 1h7" }]
],
plus: [
["path", { "d": "M5 12h14" }],
["path", { "d": "M12 5v14" }]
],
plusCircle: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "M8 12h8" }],
["path", { "d": "M12 8v8" }]
],
minus: [
["path", { "d": "M5 12h14" }]
],
refresh: [
["path", { "d": "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }],
["path", { "d": "M21 3v5h-5" }],
["path", { "d": "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }],
["path", { "d": "M8 16H3v5" }]
],
check: [
["path", { "d": "M20 6 9 17l-5-5" }]
],
checkCircle: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "m9 12 2 2 4-4" }]
],
x: [
["path", { "d": "M18 6 6 18" }],
["path", { "d": "m6 6 12 12" }]
],
xCircle: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "m15 9-6 6" }],
["path", { "d": "m9 9 6 6" }]
],
arrowUp: [
["path", { "d": "m5 12 7-7 7 7" }],
["path", { "d": "M12 19V5" }]
],
arrowDown: [
["path", { "d": "M12 5v14" }],
["path", { "d": "m19 12-7 7-7-7" }]
],
arrowLeft: [
["path", { "d": "m12 19-7-7 7-7" }],
["path", { "d": "M19 12H5" }]
],
arrowRight: [
["path", { "d": "M5 12h14" }],
["path", { "d": "m12 5 7 7-7 7" }]
],
chevronUp: [
["path", { "d": "m18 15-6-6-6 6" }]
],
chevronDown: [
["path", { "d": "m6 9 6 6 6-6" }]
],
chevronLeft: [
["path", { "d": "m15 18-6-6 6-6" }]
],
chevronRight: [
["path", { "d": "m9 18 6-6-6-6" }]
],
info: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "M12 16v-4" }],
["path", { "d": "M12 8h.01" }]
],
help: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }],
["path", { "d": "M12 17h.01" }]
],
warning: [
["path", { "d": "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }],
["path", { "d": "M12 9v4" }],
["path", { "d": "M12 17h.01" }]
],
clock: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "M12 6v6l4 2" }]
],
calendar: [
["path", { "d": "M8 2v4" }],
["path", { "d": "M16 2v4" }],
["rect", { "width": "18", "height": "18", "x": "3", "y": "4", "rx": "2" }],
["path", { "d": "M3 10h18" }]
],
menu: [
["path", { "d": "M4 5h16" }],
["path", { "d": "M4 12h16" }],
["path", { "d": "M4 19h16" }]
],
alignLeft: [
["path", { "d": "M21 5H3" }],
["path", { "d": "M15 12H3" }],
["path", { "d": "M17 19H3" }]
],
book: [
["path", { "d": "M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20" }]
],
building: [
["path", { "d": "M10 12h4" }],
["path", { "d": "M10 8h4" }],
["path", { "d": "M14 21v-3a2 2 0 0 0-4 0v3" }],
["path", { "d": "M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2" }],
["path", { "d": "M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16" }]
],
code: [
["path", { "d": "m18 16 4-4-4-4" }],
["path", { "d": "m6 8-4 4 4 4" }],
["path", { "d": "m14.5 4-5 16" }]
],
compass: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" }]
],
creditCard: [
["rect", { "width": "20", "height": "14", "x": "2", "y": "5", "rx": "2" }],
["line", { "x1": "2", "x2": "22", "y1": "10", "y2": "10" }]
],
flag: [
["path", { "d": "M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528" }]
],
globe: [
["circle", { "cx": "12", "cy": "12", "r": "10" }],
["path", { "d": "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }],
["path", { "d": "M2 12h20" }]
],
grip: [
["circle", { "cx": "9", "cy": "12", "r": "1" }],
["circle", { "cx": "9", "cy": "5", "r": "1" }],
["circle", { "cx": "9", "cy": "19", "r": "1" }],
["circle", { "cx": "15", "cy": "12", "r": "1" }],
["circle", { "cx": "15", "cy": "5", "r": "1" }],
["circle", { "cx": "15", "cy": "19", "r": "1" }]
],
history: [
["path", { "d": "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }],
["path", { "d": "M3 3v5h5" }],
["path", { "d": "M12 7v5l4 2" }]
],
link: [
["path", { "d": "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }],
["path", { "d": "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }]
],
list: [
["path", { "d": "M3 5h.01" }],
["path", { "d": "M3 12h.01" }],
["path", { "d": "M3 19h.01" }],
["path", { "d": "M8 5h13" }],
["path", { "d": "M8 12h13" }],
["path", { "d": "M8 19h13" }]
],
locate: [
["line", { "x1": "2", "x2": "5", "y1": "12", "y2": "12" }],
["line", { "x1": "19", "x2": "22", "y1": "12", "y2": "12" }],
["line", { "x1": "12", "x2": "12", "y1": "2", "y2": "5" }],
["line", { "x1": "12", "x2": "12", "y1": "19", "y2": "22" }],
["circle", { "cx": "12", "cy": "12", "r": "7" }]
],
mapPin: [
["path", { "d": "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" }],
["circle", { "cx": "12", "cy": "10", "r": "3" }]
],
megaphone: [
["path", { "d": "M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z" }],
["path", { "d": "M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14" }],
["path", { "d": "M8 6v8" }]
],
logOut: [
["path", { "d": "m16 17 5-5-5-5" }],
["path", { "d": "M21 12H9" }],
["path", { "d": "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }]
],
network: [
["rect", { "x": "16", "y": "16", "width": "6", "height": "6", "rx": "1" }],
["rect", { "x": "2", "y": "16", "width": "6", "height": "6", "rx": "1" }],
["rect", { "x": "9", "y": "2", "width": "6", "height": "6", "rx": "1" }],
["path", { "d": "M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3" }],
["path", { "d": "M12 12V8" }]
],
slidersHorizontal: [
["path", { "d": "M10 5H3" }],
["path", { "d": "M12 19H3" }],
["path", { "d": "M14 3v4" }],
["path", { "d": "M16 17v4" }],
["path", { "d": "M21 12h-9" }],
["path", { "d": "M21 19h-5" }],
["path", { "d": "M21 5h-7" }],
["path", { "d": "M8 10v4" }],
["path", { "d": "M8 12H3" }]
],
spinner: [
["path", { "d": "M21 12a9 9 0 1 1-6.219-8.56" }]
],
star: [
["path", { "d": "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" }]
],
stop: [
["rect", { "width": "18", "height": "18", "x": "3", "y": "3", "rx": "2" }]
],
table: [
["path", { "d": "M12 3v18" }],
["rect", { "width": "18", "height": "18", "x": "3", "y": "3", "rx": "2" }],
["path", { "d": "M3 9h18" }],
["path", { "d": "M3 15h18" }]
],
layoutGrid: [
["rect", { "width": "7", "height": "7", "x": "3", "y": "3", "rx": "1" }],
["rect", { "width": "7", "height": "7", "x": "14", "y": "3", "rx": "1" }],
["rect", { "width": "7", "height": "7", "x": "14", "y": "14", "rx": "1" }],
["rect", { "width": "7", "height": "7", "x": "3", "y": "14", "rx": "1" }]
],
thumbsUp: [
["path", { "d": "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" }],
["path", { "d": "M7 10v12" }]
],
thumbsDown: [
["path", { "d": "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" }],
["path", { "d": "M17 14V2" }]
],
verified: [
["path", { "d": "M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" }],
["path", { "d": "m9 12 2 2 4-4" }]
],
video: [
["path", { "d": "m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" }],
["rect", { "x": "2", "y": "6", "width": "14", "height": "12", "rx": "2" }]
],
circle: [
["circle", { "cx": "12", "cy": "12", "r": "10" }]
],
zap: [
["path", { "d": "M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" }]
],
trendingUp: [
["path", { "d": "M16 7h6v6" }],
["path", { "d": "m22 7-8.5 8.5-5-5L2 17" }]
],
} as const satisfies Record<string, readonly LucideIconNode[]>;
export type AppIconName = keyof typeof APP_ICONS;

View File

@@ -1,12 +1,16 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { LucideDynamicIcon } from '@lucide/angular';
import { APP_ICONS, type AppIconName } from './icon-registry';
import { APP_ICONS, type AppIconName, type LucideIconNode } from './icon-registry';
/**
* Single entry point for every icon in the app. Renders a Lucide icon by
* canonical name (see icon-registry.ts) so every screen uses one icon family
* at one consistent size/stroke-width by default.
*
* Renders directly from transcribed Lucide node data (see icon-registry.ts)
* instead of using @lucide/angular's runtime, matching the shape switch and
* host attributes of Lucide's own LucideIconBase/LucideDynamicIcon so output
* is pixel-identical.
*
* Decorative by default (aria-hidden). Pass `ariaLabel` only when the icon
* itself carries meaning with no adjacent text (e.g. inside an icon-only
* button) - the label belongs on the interactive element when there is one.
@@ -14,18 +18,59 @@ import { APP_ICONS, type AppIconName } from './icon-registry';
@Component({
selector: 'app-icon',
standalone: true,
imports: [LucideDynamicIcon],
template: `
<svg
[lucideIcon]="iconData()"
[size]="size()"
[color]="color()"
[strokeWidth]="strokeWidth()"
[title]="ariaLabel()"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide"
[attr.width]="size()"
[attr.height]="size()"
[attr.stroke]="color()"
[attr.stroke-width]="strokeWidth()"
[attr.role]="ariaLabel() ? 'img' : null"
[attr.aria-label]="ariaLabel()"
[attr.aria-hidden]="ariaLabel() ? null : 'true'"
></svg>
>
@if (ariaLabel(); as label) {
<title>{{ label }}</title>
}
@for (node of nodes(); track $index) {
@let attrs = node[1];
@switch (node[0]) {
@case ('path') {
<path [attr.d]="attrs['d']" [attr.fill]="attrs['fill']" />
}
@case ('line') {
<line [attr.x1]="attrs['x1']" [attr.x2]="attrs['x2']" [attr.y1]="attrs['y1']" [attr.y2]="attrs['y2']" />
}
@case ('polygon') {
<polygon [attr.points]="attrs['points']" />
}
@case ('polyline') {
<polyline [attr.points]="attrs['points']" />
}
@case ('circle') {
<circle [attr.cx]="attrs['cx']" [attr.cy]="attrs['cy']" [attr.r]="attrs['r']" [attr.fill]="attrs['fill']" />
}
@case ('ellipse') {
<ellipse [attr.cx]="attrs['cx']" [attr.cy]="attrs['cy']" [attr.rx]="attrs['rx']" [attr.ry]="attrs['ry']" />
}
@case ('rect') {
<rect
[attr.x]="attrs['x']"
[attr.y]="attrs['y']"
[attr.width]="attrs['width']"
[attr.height]="attrs['height']"
[attr.rx]="attrs['rx']"
[attr.ry]="attrs['ry']"
/>
}
}
}
</svg>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
@@ -40,5 +85,10 @@ export class IconComponent {
readonly strokeWidth = input<number>(2);
readonly ariaLabel = input<string | null>(null);
protected readonly iconData = computed(() => APP_ICONS[this.name()]);
// `APP_ICONS` is declared `as const satisfies ...` so per-icon tuple types stay
// literal (keeps `AppIconName` a precise key union for callers); widen here at
// the single consumption point instead of loosening the registry's own typing.
protected readonly nodes = computed(
() => APP_ICONS[this.name()] as readonly LucideIconNode[]
);
}