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

@@ -4,7 +4,15 @@ Nice-to-have, non-blocking work — no client decision needed, just not worth do
## Cart payment modal → `app-dialog` migration ## Cart payment modal → `app-dialog` migration
`.bank-payment-modal` on the cart page is a custom overlay component with its own focus-trap (added during the WCAG audit) rather than the shared `app-dialog` primitive. Functionally and accessibly complete as-is — migrating it to the shared primitive is a composition cleanup, deliberately deferred across every polish pass so far because it touches multi-step payment state. `.payment-modal`/`.bank-payment-modal` on the cart page are custom overlays with their own focus-trap (added during the WCAG audit) rather than the shared `app-dialog` primitive. Functionally and accessibly complete as-is. Attempted 2026-08-06 and deliberately stopped, not just deferred — real conflicts found, not reluctance:
- Cart's modals have **no backdrop-click-to-close** today (intentional — an in-flight QR/bank payment shouldn't cancel on a stray click); `app-dialog` always closes on backdrop click, no opt-out existed.
- The QR/status modal and the bank-iframe modal can be **open simultaneously** (nested), with Escape closing only the top one and falling back to the QR view underneath, payment polling untouched. `app-dialog` has no stacking/priority concept — two instances would both react to one Escape.
- The bank iframe needs a full-bleed `min(960px,92vw)×min(760px,86vh)` panel with zero padding; `app-dialog`'s largest preset caps at 800px with fixed `1.5rem` padding.
`app-dialog` was extended with `closeOnEscape`/`closeOnBackdropClick` inputs (default `true`, backward-compatible with its other 13 call sites) to close the first two gaps, but the actual cart-template migration was reverted before landing — a same-session follow-up attempt deleted the hand-rolled focus-trap/Escape-key code from `cart.component.ts` without finishing the `cart.component.html` swap to `app-dialog`, which would have shipped live checkout code with no keyboard focus-trap at all. Caught before commit, fully reverted (`dialog.component.ts`, `cart.component.ts`, `cart.component.html` all back to original — the `closeOnEscape`/`closeOnBackdropClick` API extension did not ship either, since it had no consumer left).
Next attempt should land the `DialogComponent` API extension as its own small, independently-reviewed change first, then do the cart template migration as a second, separate, carefully-tested change — not both in one pass.
## Angular 22 upgrade ## Angular 22 upgrade

View File

@@ -33,7 +33,7 @@ Continues the sprint lettering from `docs/GLOBAL-SPRINT-PLAN.md` (Sprints AF,
- [x] Facade-level tests against existing mock gateways for the highest-risk domains first: `ProjectEditorFacade` (undo/redo, draft persistence, validation gating on publish), `AdminAnalyticsFacade` (the never-fabricate-a-number contract) - [x] Facade-level tests against existing mock gateways for the highest-risk domains first: `ProjectEditorFacade` (undo/redo, draft persistence, validation gating on publish), `AdminAnalyticsFacade` (the never-fabricate-a-number contract)
- [x] Unit tests for the pure validator primitives (`project-editor/schema/validators/primitives.ts`) — zero-dependency, highest value per line of test - [x] Unit tests for the pure validator primitives (`project-editor/schema/validators/primitives.ts`) — zero-dependency, highest value per line of test
- [x] Regression tests for the bugs fixed this cycle so they cannot silently return (carousel `layout.columns` sizing, hero `layout.columns` panel count, header profile login/logout gating) - [x] Regression tests for the bugs fixed this cycle so they cannot silently return (carousel `layout.columns` sizing, hero `layout.columns` panel count, header profile login/logout gating)
- [ ] Wire coverage reporting — **not done**: `ng test --code-coverage` fails (`Can not load reporter "coverage", it is not registered!`) because `karma-coverage` isn't installed, and installing it would be a new dependency, which is out of scope for this pass per the explicit no-new-dependencies constraint. Left for a follow-up that also gets sign-off on adding the package. - [x] Wire coverage reporting — **done**: installed `karma-coverage` as a devDependency, added it to `karma.conf.js` (`coverage` reporter + `coverageReporter` block emitting `text-summary`, `html`, and `lcovonly` into `coverage/`). `npx ng test --watch=false --code-coverage` runs clean (83/83 specs pass). Baseline: Statements 32.02% (1025/3201), Branches 18.53% (353/1904), Functions 21.73% (220/1012), Lines 32.76% (946/2887).
- [x] Decide whether to gate CI on it — **no, not yet**: 11 spec files is a foundation, not the coverage floor CI gating implies; gate once coverage reporting exists and a real floor number can be set, not before. - [x] Decide whether to gate CI on it — **no, not yet**: 11 spec files is a foundation, not the coverage floor CI gating implies; gate once coverage reporting exists and a real floor number can be set, not before.
**What shipped:** Harness confirmed working (`npm test` was already green, 57/57). Added 6 new spec files (test count 57 → 75): `ProjectEditorFacade` facade spec (undo/redo, draft-persistence round-trip via a second facade instance reading the same localStorage draft, publish blocked/allowed on `hasBlockingIssues()`) mocking `CONFIG_PROVIDER` as the gateway boundary; `AdminAnalyticsFacade` facade spec asserting `summary().conversionRate` stays `null` and `performance`/`backend-connectivity` health checks stay `'unknown'` rather than being guessed, mocking all 4 gateways + `AdminDashboardFacade`; `HeroWidgetComponent` and `ProductCarouselWidgetComponent` component specs regression-covering `layout.columns` (panel count / items-per-page); `HeaderComponent` component spec regression-covering the login/logout profile toggle (asserts on icon name, not translated aria-label text, since Russian is the default active language in tests). `primitives.ts` and the pure-util/service exemplar layers were already covered by pre-existing specs — verified, not re-done. Cart/checkout facade tests and a "manifest-filtered layout options" regression were scoped out to stay within this sprint's time budget — breadth across the 4 required layers (util/service/facade/component) was prioritized over a 5th facade. **What shipped:** Harness confirmed working (`npm test` was already green, 57/57). Added 6 new spec files (test count 57 → 75): `ProjectEditorFacade` facade spec (undo/redo, draft-persistence round-trip via a second facade instance reading the same localStorage draft, publish blocked/allowed on `hasBlockingIssues()`) mocking `CONFIG_PROVIDER` as the gateway boundary; `AdminAnalyticsFacade` facade spec asserting `summary().conversionRate` stays `null` and `performance`/`backend-connectivity` health checks stay `'unknown'` rather than being guessed, mocking all 4 gateways + `AdminDashboardFacade`; `HeroWidgetComponent` and `ProductCarouselWidgetComponent` component specs regression-covering `layout.columns` (panel count / items-per-page); `HeaderComponent` component spec regression-covering the login/logout profile toggle (asserts on icon name, not translated aria-label text, since Russian is the default active language in tests). `primitives.ts` and the pure-util/service exemplar layers were already covered by pre-existing specs — verified, not re-done. Cart/checkout facade tests and a "manifest-filtered layout options" regression were scoped out to stay within this sprint's time budget — breadth across the 4 required layers (util/service/facade/component) was prioritized over a 5th facade.

View File

@@ -12,6 +12,7 @@ module.exports = function (config) {
require('karma-jasmine'), require('karma-jasmine'),
require('karma-chrome-launcher'), require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'), require('karma-jasmine-html-reporter'),
require('karma-coverage'),
], ],
browsers: ['ChromeHeadlessNoSandbox'], browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: { customLaunchers: {
@@ -20,7 +21,12 @@ module.exports = function (config) {
flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
}, },
}, },
reporters: ['progress'], reporters: ['progress', 'coverage'],
coverageReporter: {
dir: require('path').join(__dirname, 'coverage'),
subdir: '.',
reporters: [{ type: 'text-summary' }, { type: 'html' }, { type: 'lcovonly' }],
},
restartOnFileChange: true, restartOnFileChange: true,
}); });
}; };

160
package-lock.json generated
View File

@@ -17,7 +17,6 @@
"@angular/platform-browser": "22.0.8", "@angular/platform-browser": "22.0.8",
"@angular/router": "22.0.8", "@angular/router": "22.0.8",
"@angular/service-worker": "22.0.8", "@angular/service-worker": "22.0.8",
"@lucide/angular": "^1.25.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.8.0", "tslib": "^2.8.0",
"zone.js": "~0.16.0" "zone.js": "~0.16.0"
@@ -32,6 +31,7 @@
"jasmine-core": "~5.5.0", "jasmine-core": "~5.5.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "^2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~6.0.3" "typescript": "~6.0.3"
@@ -1968,19 +1968,6 @@
"win32" "win32"
] ]
}, },
"node_modules/@lucide/angular": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/@lucide/angular/-/angular-1.25.0.tgz",
"integrity": "sha512-Hu1eHlGIeyamhSCFEO5YmiUlWn/h4y9BOXMDndY0HRu05CNeCt5BENkfBoeE/bzggMGGbYG+SOBkQUG939tFXw==",
"license": "ISC",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">=17.0.0",
"@angular/core": ">=17.0.0"
}
},
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.29.0", "version": "1.29.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
@@ -5459,6 +5446,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -5534,6 +5531,13 @@
"node": "20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
@@ -5934,6 +5938,60 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-lib-source-maps": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
"integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"debug": "^4.1.1",
"istanbul-lib-coverage": "^3.0.0",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-lib-source-maps/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jasmine-core": { "node_modules/jasmine-core": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz",
@@ -6097,6 +6155,51 @@
"which": "bin/which" "which": "bin/which"
} }
}, },
"node_modules/karma-coverage": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz",
"integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"istanbul-lib-coverage": "^3.2.0",
"istanbul-lib-instrument": "^5.1.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^4.0.1",
"istanbul-reports": "^3.0.5",
"minimatch": "^3.0.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/karma-coverage/node_modules/istanbul-lib-instrument": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
"integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.12.3",
"@babel/parser": "^7.14.7",
"@istanbuljs/schema": "^0.1.2",
"istanbul-lib-coverage": "^3.2.0",
"semver": "^6.3.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/karma-coverage/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/karma-jasmine": { "node_modules/karma-jasmine": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",
@@ -6610,6 +6713,22 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-fetch-happen": { "node_modules/make-fetch-happen": {
"version": "15.0.6", "version": "15.0.6",
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz",
@@ -8563,6 +8682,19 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tar": { "node_modules/tar": {
"version": "7.5.22", "version": "7.5.22",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",

View File

@@ -9,6 +9,7 @@
"build": "ng build", "build": "ng build",
"build:dexar": "ng build --configuration=production", "build:dexar": "ng build --configuration=production",
"test": "ng test --watch=false --browsers=ChromeHeadlessNoSandbox", "test": "ng test --watch=false --browsers=ChromeHeadlessNoSandbox",
"test:coverage": "ng test --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"arch:check:boundaries": "node tools/architecture/check-boundaries.mjs", "arch:check:boundaries": "node tools/architecture/check-boundaries.mjs",
"arch:check:cycles": "npx --yes madge --circular --extensions ts src/app --ts-config tsconfig.app.json", "arch:check:cycles": "npx --yes madge --circular --extensions ts src/app --ts-config tsconfig.app.json",
@@ -30,7 +31,6 @@
"@angular/platform-browser": "22.0.8", "@angular/platform-browser": "22.0.8",
"@angular/router": "22.0.8", "@angular/router": "22.0.8",
"@angular/service-worker": "22.0.8", "@angular/service-worker": "22.0.8",
"@lucide/angular": "^1.25.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.8.0", "tslib": "^2.8.0",
"zone.js": "~0.16.0" "zone.js": "~0.16.0"
@@ -45,8 +45,9 @@
"jasmine-core": "~5.5.0", "jasmine-core": "~5.5.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "^2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~6.0.3" "typescript": "~6.0.3"
} }
} }

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). * Canonical name -> raw Lucide icon node data map for the whole app
* One entry per concept so every screen renders the same icon for the same meaning * (storefront + builder + backoffice). One entry per concept so every screen
* (RC UI Sprint - Icon & Visual Language Audit). Extend this list rather than * renders the same icon for the same meaning (RC UI Sprint - Icon & Visual
* importing Lucide icons ad hoc in individual components. * 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 = { export const APP_ICONS = {
home: Home, home: [
store: Store, ["path", { "d": "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" }],
cart: ShoppingCart, ["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" }]
package: Package, ],
boxes: Boxes, store: [
folder: Folder, ["path", { "d": "M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5" }],
tag: Tag, ["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" }],
tags: Tags, ["path", { "d": "M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05" }]
users: Users, ],
user: User, cart: [
settings: Settings, ["circle", { "cx": "8", "cy": "21", "r": "1" }],
chartBar: BarChart3, ["circle", { "cx": "19", "cy": "21", "r": "1" }],
chartLine: LineChart, ["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" }]
search: Search, ],
filter: Filter, package: [
heart: Heart, ["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" }],
scale: Scale, ["path", { "d": "M12 22V12" }],
bell: Bell, ["polyline", { "points": "3.29 7 12 12 20.71 7" }],
image: Image, ["path", { "d": "m7.5 4.27 9 5.15" }]
images: Images, ],
palette: Palette, boxes: [
monitor: Monitor, ["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" }],
shield: Shield, ["path", { "d": "m7 16.5-4.74-2.85" }],
lock: Lock, ["path", { "d": "m7 16.5 5-3" }],
database: Database, ["path", { "d": "M7 16.5v5.17" }],
upload: Upload, ["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" }],
download: Download, ["path", { "d": "m17 16.5-5-3" }],
edit: Pencil, ["path", { "d": "m17 16.5 4.74-2.85" }],
trash: Trash2, ["path", { "d": "M17 16.5v5.17" }],
copy: Copy, ["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" }],
save: Save, ["path", { "d": "M12 8 7.26 5.15" }],
plus: Plus, ["path", { "d": "m12 8 4.74-2.85" }],
plusCircle: PlusCircle, ["path", { "d": "M12 13.5V8" }]
minus: Minus, ],
refresh: RefreshCw, folder: [
check: Check, ["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" }]
checkCircle: CheckCircle2, ],
x: X, tag: [
xCircle: XCircle, ["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" }],
arrowUp: ArrowUp, ["circle", { "cx": "7.5", "cy": "7.5", "r": ".5", "fill": "currentColor" }]
arrowDown: ArrowDown, ],
arrowLeft: ArrowLeft, tags: [
arrowRight: ArrowRight, ["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" }],
chevronUp: ChevronUp, ["path", { "d": "M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193" }],
chevronDown: ChevronDown, ["circle", { "cx": "10.5", "cy": "6.5", "r": ".5", "fill": "currentColor" }]
chevronLeft: ChevronLeft, ],
chevronRight: ChevronRight, users: [
info: Info, ["path", { "d": "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }],
help: HelpCircle, ["path", { "d": "M16 3.128a4 4 0 0 1 0 7.744" }],
warning: TriangleAlert, ["path", { "d": "M22 21v-2a4 4 0 0 0-3-3.87" }],
clock: Clock, ["circle", { "cx": "9", "cy": "7", "r": "4" }]
calendar: Calendar, ],
menu: Menu, user: [
alignLeft: AlignLeft, ["path", { "d": "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }],
book: Book, ["circle", { "cx": "12", "cy": "7", "r": "4" }]
building: Building2, ],
code: Code2, settings: [
compass: Compass, ["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" }],
creditCard: CreditCard, ["circle", { "cx": "12", "cy": "12", "r": "3" }]
flag: Flag, ],
globe: Globe, chartBar: [
grip: GripVertical, ["path", { "d": "M3 3v16a2 2 0 0 0 2 2h16" }],
history: History, ["path", { "d": "M18 17V9" }],
link: Link, ["path", { "d": "M13 17V5" }],
list: List, ["path", { "d": "M8 17v-3" }]
locate: Locate, ],
mapPin: MapPin, chartLine: [
megaphone: Megaphone, ["path", { "d": "M3 3v16a2 2 0 0 0 2 2h16" }],
logOut: LogOut, ["path", { "d": "m19 9-5 5-4-4-3 3" }]
network: Network, ],
slidersHorizontal: SlidersHorizontal, search: [
spinner: Loader2, ["path", { "d": "m21 21-4.34-4.34" }],
star: Star, ["circle", { "cx": "11", "cy": "11", "r": "8" }]
stop: Square, ],
table: Table, filter: [
layoutGrid: LayoutGrid, ["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" }]
thumbsUp: ThumbsUp, ],
thumbsDown: ThumbsDown, heart: [
verified: BadgeCheck, ["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" }]
video: Video, ],
circle: Circle, scale: [
zap: Zap, ["path", { "d": "M12 3v18" }],
trendingUp: TrendingUp ["path", { "d": "m19 8 3 8a5 5 0 0 1-6 0zV7" }],
} as const; ["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; export type AppIconName = keyof typeof APP_ICONS;

View File

@@ -1,12 +1,16 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { LucideDynamicIcon } from '@lucide/angular'; import { APP_ICONS, type AppIconName, type LucideIconNode } from './icon-registry';
import { APP_ICONS, type AppIconName } from './icon-registry';
/** /**
* Single entry point for every icon in the app. Renders a Lucide icon by * 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 * canonical name (see icon-registry.ts) so every screen uses one icon family
* at one consistent size/stroke-width by default. * 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 * Decorative by default (aria-hidden). Pass `ariaLabel` only when the icon
* itself carries meaning with no adjacent text (e.g. inside an icon-only * 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. * 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({ @Component({
selector: 'app-icon', selector: 'app-icon',
standalone: true, standalone: true,
imports: [LucideDynamicIcon],
template: ` template: `
<svg <svg
[lucideIcon]="iconData()" xmlns="http://www.w3.org/2000/svg"
[size]="size()" viewBox="0 0 24 24"
[color]="color()" fill="none"
[strokeWidth]="strokeWidth()" stroke-linecap="round"
[title]="ariaLabel()" stroke-linejoin="round"
class="lucide"
[attr.width]="size()"
[attr.height]="size()"
[attr.stroke]="color()"
[attr.stroke-width]="strokeWidth()"
[attr.role]="ariaLabel() ? 'img' : null" [attr.role]="ariaLabel() ? 'img' : null"
[attr.aria-label]="ariaLabel()" [attr.aria-label]="ariaLabel()"
[attr.aria-hidden]="ariaLabel() ? null : 'true'" [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, changeDetection: ChangeDetectionStrategy.OnPush,
host: { host: {
@@ -40,5 +85,10 @@ export class IconComponent {
readonly strokeWidth = input<number>(2); readonly strokeWidth = input<number>(2);
readonly ariaLabel = input<string | null>(null); 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[]
);
} }