@nuxtseo/design-system 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +181 -2
  2. package/app/app.config.ts +592 -0
  3. package/app/component-attributes.d.ts +8 -0
  4. package/app/components/README.md +38 -0
  5. package/app/components/brand/UiBrandMark.vue +63 -0
  6. package/app/components/brand/UiModuleIcon.vue +45 -0
  7. package/app/components/brand/UiProLettering.vue +26 -0
  8. package/app/components/brand/UiRangeTerrain.vue +311 -0
  9. package/app/components/brand/range-glyphs.ts +18 -0
  10. package/app/components/container/BrowserTab.vue +145 -0
  11. package/app/components/container/IntegrationRow.vue +120 -0
  12. package/app/components/container/KnowDoRails.vue +40 -0
  13. package/app/components/container/RailPanel.vue +93 -0
  14. package/app/components/container/UiAlert.vue +120 -0
  15. package/app/components/container/UiAppShell.vue +165 -0
  16. package/app/components/container/UiCard.vue +204 -0
  17. package/app/components/container/UiDisclosure.vue +43 -0
  18. package/app/components/container/UiEmptyState.vue +102 -0
  19. package/app/components/container/UiErrorState.vue +61 -0
  20. package/app/components/container/UiLoadingState.vue +20 -0
  21. package/app/components/container/UiPageHeader.vue +95 -0
  22. package/app/components/container/UiPopover.vue +102 -0
  23. package/app/components/container/UiSampleDataOverlay.vue +47 -0
  24. package/app/components/container/UiSectionHeader.vue +83 -0
  25. package/app/components/container/UiSummaryStrip.vue +40 -0
  26. package/app/components/container/UiToast.vue +230 -0
  27. package/app/components/container/UiToaster.vue +271 -0
  28. package/app/components/container/UiWidgetState.vue +143 -0
  29. package/app/components/container/ui-nav-drawer.ts +48 -0
  30. package/app/components/data/ActionRail.vue +152 -0
  31. package/app/components/data/FacetStrip.vue +93 -0
  32. package/app/components/data/UiBarCell.vue +37 -0
  33. package/app/components/data/UiBarChart.vue +281 -0
  34. package/app/components/data/UiChartAnnotationControls.vue +74 -0
  35. package/app/components/data/UiChartAnnotations.vue +163 -0
  36. package/app/components/data/UiChartBrushOverlay.vue +96 -0
  37. package/app/components/data/UiChartDataReveal.vue +81 -0
  38. package/app/components/data/UiChartFrame.vue +123 -0
  39. package/app/components/data/UiChartTooltip.vue +81 -0
  40. package/app/components/data/UiCoverageNote.vue +90 -0
  41. package/app/components/data/UiCwvSparkline.vue +188 -0
  42. package/app/components/data/UiDataList.vue +276 -0
  43. package/app/components/data/UiDataTableSection.vue +233 -0
  44. package/app/components/data/UiDistributionBars.vue +164 -0
  45. package/app/components/data/UiDonutChart.vue +132 -0
  46. package/app/components/data/UiDoorwayColumn.vue +108 -0
  47. package/app/components/data/UiDoorwayGrid.vue +58 -0
  48. package/app/components/data/UiDoorwayRow.vue +83 -0
  49. package/app/components/data/UiEntitySummary.vue +261 -0
  50. package/app/components/data/UiFactsGrid.vue +107 -0
  51. package/app/components/data/UiImpactMeter.vue +33 -0
  52. package/app/components/data/UiIssueRow.vue +149 -0
  53. package/app/components/data/UiLineChart.vue +431 -0
  54. package/app/components/data/UiMetricBarPopover.vue +121 -0
  55. package/app/components/data/UiMetricLabel.vue +30 -0
  56. package/app/components/data/UiMetricStat.vue +73 -0
  57. package/app/components/data/UiMetricToggle.vue +156 -0
  58. package/app/components/data/UiMetricsRow.vue +64 -0
  59. package/app/components/data/UiPositionMetric.vue +53 -0
  60. package/app/components/data/UiRelatedFixes.vue +113 -0
  61. package/app/components/data/UiRichText.vue +325 -0
  62. package/app/components/data/UiScatterPlot.vue +144 -0
  63. package/app/components/data/UiSeverityMarker.vue +30 -0
  64. package/app/components/data/UiSparkColumns.vue +160 -0
  65. package/app/components/data/UiSparkline.vue +681 -0
  66. package/app/components/data/UiStat.vue +445 -0
  67. package/app/components/data/UiStats.vue +76 -0
  68. package/app/components/data/UiTable.vue +517 -0
  69. package/app/components/data/UiTableExport.vue +40 -0
  70. package/app/components/data/UiTableFrame.vue +149 -0
  71. package/app/components/data/UiTablePagination.vue +37 -0
  72. package/app/components/data/UiTableShell.vue +48 -0
  73. package/app/components/data/UiTableTd.vue +39 -0
  74. package/app/components/data/UiTableTh.vue +61 -0
  75. package/app/components/data/UiTopEntityStackChart.vue +393 -0
  76. package/app/components/data/cells/UiTableDash.vue +3 -0
  77. package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
  78. package/app/components/data/cells/UiTableMetricCell.vue +72 -0
  79. package/app/components/data/cells/UiTablePathCell.vue +76 -0
  80. package/app/components/data/cells/UiTableScoreTile.vue +49 -0
  81. package/app/components/data/cells/UiTableTrendCell.vue +41 -0
  82. package/app/components/element/UiAtmosphere.vue +64 -0
  83. package/app/components/element/UiAuthHeading.vue +24 -0
  84. package/app/components/element/UiAuthProviders.vue +46 -0
  85. package/app/components/element/UiBrandIcon.vue +39 -0
  86. package/app/components/element/UiButton.vue +549 -0
  87. package/app/components/element/UiCelebration.vue +65 -0
  88. package/app/components/element/UiChannelChips.vue +62 -0
  89. package/app/components/element/UiChatQuickLink.vue +319 -0
  90. package/app/components/element/UiChip.vue +101 -0
  91. package/app/components/element/UiConfirmDialog.vue +84 -0
  92. package/app/components/element/UiCopyButton.vue +90 -0
  93. package/app/components/element/UiDitherAvatar.vue +177 -0
  94. package/app/components/element/UiDotLabel.vue +19 -0
  95. package/app/components/element/UiEjectMenu.vue +181 -0
  96. package/app/components/element/UiEjectOverlay.vue +29 -0
  97. package/app/components/element/UiFavicon.vue +235 -0
  98. package/app/components/element/UiFaviconStack.vue +84 -0
  99. package/app/components/element/UiFilterMenu.vue +324 -0
  100. package/app/components/element/UiFocusedSurface.vue +129 -0
  101. package/app/components/element/UiHealthDot.vue +32 -0
  102. package/app/components/element/UiHelpLabel.vue +28 -0
  103. package/app/components/element/UiIcon.vue +22 -0
  104. package/app/components/element/UiInput.vue +35 -0
  105. package/app/components/element/UiLogoPlausible.vue +12 -0
  106. package/app/components/element/UiLogoPro.vue +25 -0
  107. package/app/components/element/UiLogoUmami.vue +12 -0
  108. package/app/components/element/UiNavIcon.vue +107 -0
  109. package/app/components/element/UiNavList.vue +156 -0
  110. package/app/components/element/UiOnboardingAnswers.vue +127 -0
  111. package/app/components/element/UiOverflowCount.vue +31 -0
  112. package/app/components/element/UiProBadge.vue +19 -0
  113. package/app/components/element/UiProgressCircle.vue +79 -0
  114. package/app/components/element/UiProgressPercent.vue +35 -0
  115. package/app/components/element/UiRelativeTime.vue +58 -0
  116. package/app/components/element/UiScrollFade.vue +16 -0
  117. package/app/components/element/UiSelect.vue +68 -0
  118. package/app/components/element/UiSeverityDot.vue +27 -0
  119. package/app/components/element/UiSiteFavicon.vue +24 -0
  120. package/app/components/element/UiSkeleton.vue +148 -0
  121. package/app/components/element/UiSlotText.vue +65 -0
  122. package/app/components/element/UiSourceLogos.vue +113 -0
  123. package/app/components/element/UiSourceRail.vue +71 -0
  124. package/app/components/element/UiStaticTerminal.vue +107 -0
  125. package/app/components/element/UiStatusBadge.vue +62 -0
  126. package/app/components/element/UiSyncDot.vue +46 -0
  127. package/app/components/element/UiTabs.vue +245 -0
  128. package/app/components/element/UiTogglePill.vue +86 -0
  129. package/app/components/element/UiTooltip.vue +390 -0
  130. package/app/components/element/UiTooltipGrid.vue +457 -0
  131. package/app/components/element/UiTrend.vue +184 -0
  132. package/app/components/element/UiTrialBenefits.vue +71 -0
  133. package/app/components/element/UiUrlLabel.vue +75 -0
  134. package/app/components/element/UiValenceDot.vue +37 -0
  135. package/app/components/element/UiWizardModal.vue +284 -0
  136. package/app/components/element/UiWizardNav.vue +59 -0
  137. package/app/components/element/UiWizardProgress.vue +78 -0
  138. package/app/components/element/UiWizardShell.vue +43 -0
  139. package/app/components/element/eject-menu.ts +359 -0
  140. package/app/components/element/eject-tracker.ts +34 -0
  141. package/app/components/element/onboarding-answers.ts +93 -0
  142. package/app/components/element/onboarding-intents.ts +19 -0
  143. package/app/composables/dataVizColors.ts +350 -0
  144. package/app/composables/formatting.ts +252 -0
  145. package/app/composables/semanticColors.ts +80 -0
  146. package/app/composables/useActivityOverlay.ts +55 -0
  147. package/app/composables/useAppShellNav.ts +21 -0
  148. package/app/composables/useChartBrush.ts +221 -0
  149. package/app/composables/useChartHover.ts +153 -0
  150. package/app/composables/useChartTickPlan.ts +186 -0
  151. package/app/composables/useCloseOnNavigation.ts +7 -0
  152. package/app/composables/useConfirm.ts +38 -0
  153. package/app/composables/useEdgeFade.ts +37 -0
  154. package/app/composables/useEject.ts +69 -0
  155. package/app/composables/useEntranceMotion.ts +22 -0
  156. package/app/composables/useFaviconBacking.ts +164 -0
  157. package/app/composables/useGridCols.ts +39 -0
  158. package/app/composables/useIssueOverlay.ts +43 -0
  159. package/app/composables/useOnboardingService.ts +106 -0
  160. package/app/composables/useOpenedOnce.ts +19 -0
  161. package/app/composables/useScrollSpy.ts +178 -0
  162. package/app/composables/useSquircleFallback.ts +21 -0
  163. package/app/composables/useWizardRoute.ts +123 -0
  164. package/app/plugins/favicon.ts +15 -0
  165. package/app/plugins/lazy-motion.ts +22 -0
  166. package/app/plugins/selection-highlight.client.ts +176 -0
  167. package/app/shared/cli.ts +10 -0
  168. package/app/shared/error-state.ts +150 -0
  169. package/app/shared/icons.ts +1 -0
  170. package/app/shared/motion.ts +1 -0
  171. package/app/shared/nav.ts +38 -0
  172. package/app/shared/navigation.ts +5 -0
  173. package/app/shared/static-terminal.ts +14 -0
  174. package/app/shared/table-export.ts +36 -0
  175. package/app/shared/table.ts +105 -0
  176. package/app/shared/ui-icons.ts +1 -0
  177. package/app/shared/view-state.ts +85 -0
  178. package/app/shared/widget-failure.ts +126 -0
  179. package/app/utils/app-favicon.ts +64 -0
  180. package/app/utils/chartAnnotations.ts +326 -0
  181. package/app/utils/countries.ts +337 -0
  182. package/app/utils/export-download.ts +100 -0
  183. package/app/utils/favicon-fallback.ts +12 -0
  184. package/app/utils/icon-color.ts +105 -0
  185. package/app/utils/number.ts +74 -0
  186. package/app/utils/render-link.ts +14 -0
  187. package/app/utils/slot-text.ts +18 -0
  188. package/app/utils/squircle.ts +154 -0
  189. package/app/utils/topEntityStack.ts +197 -0
  190. package/app/utils/vue.ts +57 -0
  191. package/build/design-system.ts +69 -0
  192. package/build/external-links.ts +38 -0
  193. package/build/fonts.ts +39 -0
  194. package/build/icon-server-bundle.ts +276 -0
  195. package/build/runtime/icon-server-bundle.ts +29 -0
  196. package/css/global.css +1896 -0
  197. package/nuxt.config.ts +143 -0
  198. package/package.json +93 -3
  199. package/public/fonts/README.md +33 -0
  200. package/public/fonts/fira-code-OFL.txt +93 -0
  201. package/public/fonts/fira-code-var-latin.woff2 +0 -0
  202. package/public/fonts/hubot-sans-OFL.txt +93 -0
  203. package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
  204. package/server/api/favicon.get.ts +122 -0
  205. package/server/utils/favicon-source.ts +51 -0
  206. package/shared/contracts/feedback.ts +42 -0
  207. package/shared/contracts/index.ts +1 -0
  208. package/shared/delivery-channels.ts +53 -0
  209. package/shared/icons/index.ts +143 -0
  210. package/shared/icons/registry.ts +347 -0
  211. package/shared/icons/ui-slots.ts +58 -0
  212. package/shared/markdown.ts +44 -0
  213. package/shared/motion.ts +120 -0
  214. package/shared/rangi.ts +60 -0
  215. package/shared/rich-text.ts +34 -0
  216. package/shared/ui-icons.ts +6 -0
  217. package/shared/utils/urls.ts +64 -0
  218. package/types/index.ts +1 -0
  219. package/types/ui.ts +339 -0
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Both feedback endpoints are built into the PRO Worker
5
+ * (`layers/saas/server/api/feedback{,-thumbs}.post.ts`), because the row lands
6
+ * in the pro D1 `feedback` table, which is also the triage ledger. The site
7
+ * Worker has no handler and no binding to that database.
8
+ *
9
+ * The site's docs rail and free tools still POST these paths from the browser.
10
+ * That works in production only because `proRoutes()` in
11
+ * `deploy/cloudflare-env.ts` binds `nuxtseo.com/api/feedback*` to the pro
12
+ * Worker, so Cloudflare hands the request over at the edge.
13
+ *
14
+ * The paths are constants so that coupling is checkable: the site client, the
15
+ * dev proxy in `apps/site/nuxt.config.ts`, and the route-coverage guard in
16
+ * `tests/pro-worker-route-coverage.feature.test.ts` all read these. If you
17
+ * rename an endpoint, the guard fails instead of production 404ing silently.
18
+ */
19
+ export const FEEDBACK_THUMBS_PATH = '/api/feedback-thumbs'
20
+ export const FEEDBACK_COMMENT_PATH = '/api/feedback'
21
+
22
+ export interface ThumbsFeedbackResponse {
23
+ thumbs: 'up' | 'down'
24
+ stats: {
25
+ up: number
26
+ down: number
27
+ }
28
+ }
29
+
30
+ export const ThumbsFeedbackSchema = z.object({
31
+ thumbs: z.enum(['up', 'down']),
32
+ toolId: z.string().optional(),
33
+ context: z.record(z.string(), z.unknown()).optional(),
34
+ })
35
+
36
+ export const CommentFeedbackSchema = z.object({
37
+ comment: z.string().min(3, 'Must be at least 3 characters'),
38
+ toolId: z.string().optional(),
39
+ context: z.record(z.string(), z.unknown()).optional(),
40
+ })
41
+
42
+ export type CommentFeedbackSchemaOutput = z.output<typeof CommentFeedbackSchema>
@@ -0,0 +1 @@
1
+ export * from './feedback'
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The delivery-channel vocabulary: one place that names each channel and picks
3
+ * its glyph. Reports, alerts and notification settings all render the same four
4
+ * channels, and before this each surface re-derived its own label/icon pair, so
5
+ * "Email + Discord" strings and icon choices drifted per screen.
6
+ *
7
+ * Icons are semantic roles from the icon registry and stay MONOCHROME — the
8
+ * channels are told apart by glyph shape, never by colour (DESIGN.md).
9
+ */
10
+
11
+ export const DELIVERY_CHANNEL_KEYS = ['email', 'discord', 'slack', 'webhook'] as const
12
+
13
+ export type DeliveryChannelKey = typeof DELIVERY_CHANNEL_KEYS[number]
14
+
15
+ export interface DeliveryChannelMeta {
16
+ key: DeliveryChannelKey
17
+ label: string
18
+ /** Semantic icon role (see `shared/icons/registry.ts`). */
19
+ icon: string
20
+ }
21
+
22
+ export const DELIVERY_CHANNELS: Record<DeliveryChannelKey, DeliveryChannelMeta> = {
23
+ email: { key: 'email', label: 'Email', icon: 'mail' },
24
+ discord: { key: 'discord', label: 'Discord', icon: 'discord' },
25
+ slack: { key: 'slack', label: 'Slack', icon: 'slack' },
26
+ webhook: { key: 'webhook', label: 'Webhook', icon: 'webhook' },
27
+ }
28
+
29
+ export function deliveryChannelMeta(key: string): DeliveryChannelMeta | null {
30
+ return DELIVERY_CHANNELS[key as DeliveryChannelKey] ?? null
31
+ }
32
+
33
+ export function deliveryChannelLabel(key: string): string {
34
+ return deliveryChannelMeta(key)?.label ?? key
35
+ }
36
+
37
+ /**
38
+ * Enabled channels in canonical order, from any record shaped like the stored
39
+ * `channels` column (`{ email: true, discord: false, … }`).
40
+ */
41
+ export function enabledDeliveryChannels(
42
+ channels: Partial<Record<DeliveryChannelKey, unknown>> | null | undefined,
43
+ ): DeliveryChannelKey[] {
44
+ if (!channels)
45
+ return []
46
+ return DELIVERY_CHANNEL_KEYS.filter(key => !!channels[key])
47
+ }
48
+
49
+ /** "Email & Discord" / "No delivery channel" — the prose form of the same set. */
50
+ export function deliveryChannelsSummary(keys: readonly string[], empty = 'No delivery channel'): string {
51
+ const labels = keys.map(deliveryChannelLabel)
52
+ return labels.length ? labels.join(' & ') : empty
53
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Public icon API for the design system.
3
+ *
4
+ * - `DEFAULT_ICON_SET` — the set an app renders unless its Nuxt config picks
5
+ * another with `designSystem.iconSet`. The choice is read once at build time
6
+ * (`build/design-system.ts`), so UiIcon, UiButton, and Nuxt UI internals all
7
+ * follow it. Preview sets first in the brand-kit `/icons` playground.
8
+ * - `resolveUiIcon(role, set)` — role → iconify id for `set`, with fallback. Runtime
9
+ * code renders roles through `UiIcon` instead, which follows the app's choice.
10
+ * - Raw `i-*` ids pass straight through, so adoption can be incremental.
11
+ *
12
+ * Source scanning is disabled in the layer config. `iconBundleList()` is the
13
+ * explicit active-set client bundle, so candidate-set literals stay build-only.
14
+ */
15
+ import type { IconRole, IconSet, RoleMap } from './registry'
16
+ import { ICON_ALIASES, ICON_ROLES, ICON_SETS } from './registry'
17
+
18
+ export type { IconRole, IconSet, RoleMap } from './registry'
19
+ export { ICON_ALIASES, ICON_ROLES, ICON_SETS } from './registry'
20
+
21
+ /**
22
+ * The icon set an app renders when its Nuxt config sets no `designSystem.iconSet`.
23
+ * Nuxt SEO renders hugeicons. Compare sets first in brand-kit `/icons`.
24
+ */
25
+ export const DEFAULT_ICON_SET: IconSet = 'hugeicons'
26
+
27
+ /**
28
+ * Parse an untrusted icon set name, such as a Nuxt config value, into an
29
+ * `IconSet`. `undefined` selects `DEFAULT_ICON_SET`.
30
+ */
31
+ export function parseIconSet(value: unknown): IconSet {
32
+ if (value === undefined)
33
+ return DEFAULT_ICON_SET
34
+ if (typeof value === 'string' && (ICON_SETS as string[]).includes(value))
35
+ return value as IconSet
36
+ throw new TypeError(`designSystem.iconSet must be one of ${ICON_SETS.join(', ')}. Received ${JSON.stringify(value)}.`)
37
+ }
38
+
39
+ /** A curated semantic role or registered synonym. Prefer this for static registries. */
40
+ export type IconAlias = keyof typeof ICON_ALIASES
41
+ export type SemanticUiIcon = IconRole | IconAlias
42
+
43
+ /**
44
+ * Component-facing icon input. Runtime provider/flag data can be dynamic, while
45
+ * literal role names still retain autocomplete through `SemanticUiIcon`.
46
+ */
47
+ export type RawUiIcon = `i-${string}`
48
+ export type UiIcon = SemanticUiIcon | RawUiIcon | (string & {})
49
+
50
+ function idFor(role: RoleMap, set: IconSet): string | undefined {
51
+ if (role.fixed)
52
+ return role.fixed
53
+ // active set first, then fall back through the remaining sets in priority order
54
+ const order = [set, ...ICON_SETS.filter(s => s !== set)]
55
+ for (const s of order) {
56
+ const bare = role[s]
57
+ if (bare)
58
+ return `i-${s}-${bare}`
59
+ }
60
+ return undefined
61
+ }
62
+
63
+ /** Resolve a semantic role to its iconify id for `set`; pass raw `i-*` strings through. */
64
+ export function resolveUiIcon(icon: UiIcon | undefined, set: IconSet = DEFAULT_ICON_SET): string | undefined {
65
+ if (!icon)
66
+ return undefined
67
+ if (icon.startsWith('i-'))
68
+ return icon
69
+ // resolve aliases (synonyms) to their canonical role first
70
+ const name = (ICON_ALIASES as Record<string, IconRole>)[icon] ?? icon
71
+ const role = (ICON_ROLES as Record<string, RoleMap>)[name]
72
+ if (!role)
73
+ return icon // unknown name: pass through unchanged
74
+ return idFor(role, set)
75
+ }
76
+
77
+ /** Map of every role → resolved id for `set`. Used to generate app.config ui.icons. */
78
+ export function iconMapFor(set: IconSet = DEFAULT_ICON_SET): Record<IconRole, string> {
79
+ const out = {} as Record<IconRole, string>
80
+ for (const role of Object.keys(ICON_ROLES) as IconRole[])
81
+ out[role] = idFor(ICON_ROLES[role] as RoleMap, set)!
82
+ return out
83
+ }
84
+
85
+ // Collections that contain a hyphen, so `i-<collection>-<name>` can't be split naively.
86
+ const HYPHEN_COLLECTIONS = ['simple-icons', 'svg-spinners', 'circle-flags', 'vscode-icons']
87
+
88
+ // Brand marks used by UiSourceLogos. Keep these local in every app that extends
89
+ // the design system, so Source identity never depends on Iconify at runtime.
90
+ const SOURCE_LOGO_ICONS = [
91
+ 'logos:google-search-console',
92
+ 'logos:bing',
93
+ 'logos:nuxt-icon',
94
+ 'logos:chrome',
95
+ 'logos:lighthouse',
96
+ 'logos:cloudflare-icon',
97
+ 'logos:vercel-icon',
98
+ 'logos:netlify-icon',
99
+ 'logos:google-analytics',
100
+ 'logos:posthog-icon',
101
+ 'logos:reddit-icon',
102
+ 'logos:x',
103
+ 'logos:ycombinator',
104
+ 'logos:bluesky',
105
+ 'logos:youtube-icon',
106
+ 'logos:github-icon',
107
+ 'simple-icons:plausibleanalytics',
108
+ 'simple-icons:umami',
109
+ ] as const
110
+
111
+ /** `i-lucide-arrow-right` → `lucide:arrow-right` (handles multi-hyphen collections). */
112
+ function toPrefixName(id: string): string {
113
+ const body = id.replace(/^i-/, '')
114
+ const coll = HYPHEN_COLLECTIONS.find(c => body.startsWith(`${c}-`))
115
+ return coll ? `${coll}:${body.slice(coll.length + 1)}` : body.replace('-', ':')
116
+ }
117
+
118
+ /**
119
+ * The exact `prefix:name` ids for every role under `set` — feed to @nuxt/icon's
120
+ * `clientBundle.icons` so the build ships only the active set's resolved icons
121
+ * (plus the fixed brand/loading marks), never all four candidate sets.
122
+ */
123
+ export function iconBundleList(set: IconSet = DEFAULT_ICON_SET): string[] {
124
+ return [...new Set([
125
+ ...Object.values(iconMapFor(set)).map(toPrefixName),
126
+ ...SOURCE_LOGO_ICONS,
127
+ ])]
128
+ }
129
+
130
+ /**
131
+ * Role + synonym-alias name → `prefix:name` id, for @nuxt/icon's `aliases` config.
132
+ * MUST be `prefix:name` (not `i-...`): @nuxt/icon uses an alias's value as the resolved
133
+ * name verbatim and re-prefixes `i-`, so an `i-` value becomes a broken `i-i-...`.
134
+ */
135
+ export function iconAliasMap(set: IconSet = DEFAULT_ICON_SET): Record<string, string> {
136
+ const resolved = iconMapFor(set)
137
+ const out: Record<string, string> = {}
138
+ for (const [role, id] of Object.entries(resolved))
139
+ out[role] = toPrefixName(id)
140
+ for (const [alias, role] of Object.entries(ICON_ALIASES))
141
+ out[alias] = toPrefixName(resolved[role as IconRole])
142
+ return out
143
+ }
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Central, set-swappable icon registry.
3
+ *
4
+ * Every icon in the product is referenced by a *semantic role* (`next`, `delete`,
5
+ * `trending-up`) — never a raw iconify id. Each role maps to a real icon in every
6
+ * candidate set, so the entire app can be flipped between sets by changing
7
+ * `designSystem.iconSet` in the Nuxt config. Compare sets visually first in the
8
+ * brand-kit `/icons` playground.
9
+ *
10
+ * Values are the *bare* icon name (no `i-<set>-` prefix); the resolver adds the
11
+ * prefix for the active set. A role may instead pin a `fixed` full id (used for
12
+ * brand marks that only live in one set, e.g. `i-simple-icons-github`).
13
+ *
14
+ * Naming conventions per set: lucide/carbon use bare names; solar uses the
15
+ * `-linear` stroke style; hugeicons uses the `-01` variant.
16
+ */
17
+
18
+ export type IconSet = 'lucide' | 'carbon' | 'solar' | 'hugeicons'
19
+
20
+ /** The 4 candidate sets, in fallback priority order when a role lacks the active set. */
21
+ export const ICON_SETS: IconSet[] = ['lucide', 'carbon', 'solar', 'hugeicons']
22
+
23
+ export type RoleMap = Partial<Record<IconSet, string>> & { fixed?: string }
24
+
25
+ export const ICON_ROLES = {
26
+ // ── navigation ─────────────────────────────────────────────
27
+ 'next': { lucide: 'arrow-right', carbon: 'arrow-right', solar: 'arrow-right-linear', hugeicons: 'arrow-right-01' },
28
+ 'back': { lucide: 'arrow-left', carbon: 'arrow-left', solar: 'arrow-left-linear', hugeicons: 'arrow-left-01' },
29
+ 'up': { lucide: 'arrow-up', carbon: 'arrow-up', solar: 'arrow-up-linear', hugeicons: 'arrow-up-01' },
30
+ 'down': { lucide: 'arrow-down', carbon: 'arrow-down', solar: 'arrow-down-linear', hugeicons: 'arrow-down-01' },
31
+ 'external': { lucide: 'external-link', carbon: 'launch', solar: 'arrow-right-up-linear', hugeicons: 'link-square-02' },
32
+ 'expand': { lucide: 'chevron-down', carbon: 'chevron-down', solar: 'alt-arrow-down-linear', hugeicons: 'arrow-down-01' },
33
+ 'collapse': { lucide: 'chevron-up', carbon: 'chevron-up', solar: 'alt-arrow-up-linear', hugeicons: 'arrow-up-01' },
34
+ 'chevron-down': { lucide: 'chevron-down', carbon: 'chevron-down', solar: 'alt-arrow-down-linear', hugeicons: 'arrow-down-01' },
35
+ 'chevron-up': { lucide: 'chevron-up', carbon: 'chevron-up', solar: 'alt-arrow-up-linear', hugeicons: 'arrow-up-01' },
36
+ 'chevron-right': { lucide: 'chevron-right', carbon: 'chevron-right', solar: 'alt-arrow-right-linear', hugeicons: 'arrow-right-01' },
37
+ 'chevron-left': { lucide: 'chevron-left', carbon: 'chevron-left', solar: 'alt-arrow-left-linear', hugeicons: 'arrow-left-01' },
38
+ 'chevrons-up': { lucide: 'chevrons-up', carbon: 'chevron-up', solar: 'double-alt-arrow-up-linear', hugeicons: 'arrow-up-double' },
39
+ 'page-first': { lucide: 'chevrons-left', carbon: 'page-first', solar: 'double-alt-arrow-left-linear', hugeicons: 'arrow-left-double' },
40
+ 'page-last': { lucide: 'chevrons-right', carbon: 'page-last', solar: 'double-alt-arrow-right-linear', hugeicons: 'arrow-right-double' },
41
+ 'menu': { lucide: 'menu', carbon: 'menu', solar: 'hamburger-menu-linear', hugeicons: 'menu-01' },
42
+ 'hash': { lucide: 'hash', carbon: 'hashtag', solar: 'hashtag-linear', hugeicons: 'hashtag' },
43
+ 'home': { lucide: 'house', carbon: 'home', solar: 'home-linear', hugeicons: 'home-01' },
44
+ 'corner-down': { lucide: 'corner-down-right', carbon: 'arrow-down-right', solar: 'arrow-right-down-linear', hugeicons: 'arrow-turn-down' },
45
+
46
+ // ── crud / actions ─────────────────────────────────────────
47
+ 'add': { lucide: 'plus', carbon: 'add', solar: 'add-circle-linear', hugeicons: 'add-01' },
48
+ 'close': { lucide: 'x', carbon: 'close', solar: 'close-circle-linear', hugeicons: 'cancel-01' },
49
+ 'delete': { lucide: 'trash-2', carbon: 'trash-can', solar: 'trash-bin-trash-linear', hugeicons: 'delete-02' },
50
+ 'edit': { lucide: 'pencil', carbon: 'edit', solar: 'pen-linear', hugeicons: 'pencil-edit-02' },
51
+ 'copy': { lucide: 'copy', carbon: 'copy', solar: 'copy-linear', hugeicons: 'copy-01' },
52
+ 'check': { lucide: 'check', carbon: 'checkmark', solar: 'check-read-linear', hugeicons: 'tick-02' },
53
+ 'refresh': { lucide: 'rotate-cw', carbon: 'renew', solar: 'refresh-linear', hugeicons: 'reload' },
54
+ 'search': { lucide: 'search', carbon: 'search', solar: 'magnifer-linear', hugeicons: 'search-01' },
55
+ 'download': { lucide: 'download', carbon: 'download', solar: 'download-linear', hugeicons: 'download-01' },
56
+ 'upload': { lucide: 'upload', carbon: 'upload', solar: 'upload-linear', hugeicons: 'upload-01' },
57
+ 'paperclip': { lucide: 'paperclip', carbon: 'attachment', solar: 'paperclip-linear', hugeicons: 'attachment' },
58
+ 'send': { lucide: 'send', carbon: 'send', solar: 'plain-linear', hugeicons: 'sent-02' },
59
+ 'save': { lucide: 'save', carbon: 'save', solar: 'diskette-linear', hugeicons: 'floppy-disk' },
60
+ 'filter': { lucide: 'list-filter', carbon: 'filter', solar: 'filter-linear', hugeicons: 'filter' },
61
+ 'filter-remove': { lucide: 'filter-x', carbon: 'filter-remove', solar: 'filter-broken', hugeicons: 'filter-remove' },
62
+ 'settings': { lucide: 'settings', carbon: 'settings', solar: 'settings-linear', hugeicons: 'settings-01' },
63
+ 'sliders': { lucide: 'sliders-horizontal', carbon: 'settings-adjust', solar: 'tuning-2-linear', hugeicons: 'tools' },
64
+ 'more': { lucide: 'ellipsis-vertical', carbon: 'overflow-menu-vertical', solar: 'menu-dots-linear', hugeicons: 'more-vertical' },
65
+ 'more-horizontal': { lucide: 'ellipsis', carbon: 'overflow-menu-horizontal', solar: 'menu-dots-bold', hugeicons: 'more-horizontal' },
66
+ 'minus': { lucide: 'minus', carbon: 'subtract', solar: 'minus-circle-linear', hugeicons: 'remove-01' },
67
+ 'share': { lucide: 'share-2', carbon: 'share', solar: 'share-linear', hugeicons: 'share-08' },
68
+ 'undo': { lucide: 'undo-2', carbon: 'undo', solar: 'undo-left-linear', hugeicons: 'arrow-turn-backward' },
69
+ 'move': { lucide: 'move', carbon: 'move', hugeicons: 'move' },
70
+ 'grip': { lucide: 'grip-vertical', carbon: 'draggable', solar: 'menu-dots-linear', hugeicons: 'drag-drop-vertical' },
71
+ 'stop': { lucide: 'square', carbon: 'stop', solar: 'stop-linear', hugeicons: 'stop' },
72
+ 'play': { lucide: 'play', carbon: 'play', solar: 'play-linear', hugeicons: 'play' },
73
+ 'pause': { lucide: 'pause', carbon: 'pause', solar: 'pause-linear', hugeicons: 'pause' },
74
+ 'scan': { lucide: 'scan', carbon: 'scan', solar: 'scanner-linear', hugeicons: 'scan-image' },
75
+ 'gamepad': { lucide: 'gamepad-2', carbon: 'game-console', solar: 'gamepad-linear', hugeicons: 'game-controller-03' },
76
+
77
+ // ── status / meta ──────────────────────────────────────────
78
+ 'info': { lucide: 'info', carbon: 'information', solar: 'info-circle-linear', hugeicons: 'information-circle' },
79
+ 'note': { lucide: 'info', carbon: 'information', solar: 'info-square-linear', hugeicons: 'information-square' },
80
+ 'tip': { lucide: 'lightbulb', carbon: 'idea', solar: 'lightbulb-linear', hugeicons: 'idea-01' },
81
+ 'warning': { lucide: 'triangle-alert', carbon: 'warning', solar: 'danger-triangle-linear', hugeicons: 'alert-02' },
82
+ 'caution': { lucide: 'circle-alert', carbon: 'warning-alt', solar: 'danger-circle-linear', hugeicons: 'alert-circle' },
83
+ 'success': { lucide: 'circle-check', carbon: 'checkmark-filled', solar: 'check-circle-linear', hugeicons: 'checkmark-circle-02' },
84
+ 'error': { lucide: 'circle-x', carbon: 'close-filled', solar: 'close-circle-linear', hugeicons: 'cancel-circle' },
85
+ 'help': { lucide: 'circle-help', carbon: 'help', solar: 'question-circle-linear', hugeicons: 'help-circle' },
86
+ 'lock': { lucide: 'lock', carbon: 'locked', solar: 'lock-keyhole-linear', hugeicons: 'square-lock-02' },
87
+ 'view': { lucide: 'eye', carbon: 'view', solar: 'eye-linear', hugeicons: 'view' },
88
+ 'hide': { lucide: 'eye-off', carbon: 'view-off', solar: 'eye-closed-linear', hugeicons: 'view-off' },
89
+ 'ai': { lucide: 'sparkles', carbon: 'magic-wand', solar: 'magic-stick-3-linear', hugeicons: 'ai-magic' },
90
+ 'bot': { lucide: 'bot', carbon: 'bot', solar: 'chat-round-line-linear', hugeicons: 'ai-chat-02' },
91
+ 'star': { lucide: 'star', carbon: 'star', solar: 'star-linear', hugeicons: 'star' },
92
+ 'heart': { lucide: 'heart', carbon: 'favorite', solar: 'heart-linear', hugeicons: 'favourite' },
93
+ 'bell': { lucide: 'bell', carbon: 'notification', solar: 'bell-linear', hugeicons: 'notification-01' },
94
+ 'shield': { lucide: 'shield', carbon: 'security', solar: 'shield-linear', hugeicons: 'shield-01' },
95
+ 'shield-check': { lucide: 'shield-check', carbon: 'security', solar: 'shield-check-linear', hugeicons: 'shield-01' },
96
+ 'zap': { lucide: 'zap', carbon: 'lightning', solar: 'bolt-linear', hugeicons: 'flash' },
97
+ 'flag': { lucide: 'flag', carbon: 'flag', solar: 'flag-linear', hugeicons: 'flag-02' },
98
+ 'tag': { lucide: 'tag', carbon: 'tag', solar: 'tag-linear', hugeicons: 'tag-01' },
99
+ 'pin': { lucide: 'pin', carbon: 'pin', solar: 'pin-linear', hugeicons: 'pin' },
100
+ 'bookmark': { lucide: 'bookmark', carbon: 'bookmark', solar: 'bookmark-linear', hugeicons: 'bookmark-01' },
101
+ 'gift': { lucide: 'gift', carbon: 'gift', solar: 'gift-linear', hugeicons: 'gift' },
102
+ 'trophy': { lucide: 'trophy', carbon: 'trophy', solar: 'cup-star-linear', hugeicons: 'champion' },
103
+ 'target': { lucide: 'target', carbon: 'concept', solar: 'target-linear', hugeicons: 'target-02' },
104
+ // Rivalry / "you vs them" — competitor surfaces. lucide+hugeicons render swords;
105
+ // carbon/solar have no sword glyph, so they fall back to the rivalry-as-aim stand-in.
106
+ 'versus': { lucide: 'swords', carbon: 'concept', solar: 'target-linear', hugeicons: 'sword-03' },
107
+ 'bug': { lucide: 'bug', carbon: 'debug', solar: 'bug-linear', hugeicons: 'bug-01' },
108
+ 'circle-dash': { lucide: 'circle-dashed', carbon: 'circle-dash', solar: 'record-circle-linear', hugeicons: 'circle' },
109
+ 'loading': { fixed: 'i-svg-spinners-90-ring-with-bg' },
110
+
111
+ // ── theme ──────────────────────────────────────────────────
112
+ 'light': { lucide: 'sun', carbon: 'sun', solar: 'sun-linear', hugeicons: 'sun-02' },
113
+ 'dark': { lucide: 'moon', carbon: 'moon', solar: 'moon-linear', hugeicons: 'moon-02' },
114
+ 'system': { lucide: 'monitor', carbon: 'screen', solar: 'monitor-linear', hugeicons: 'computer' },
115
+
116
+ // ── objects / domain ───────────────────────────────────────
117
+ 'rocket': { lucide: 'rocket', carbon: 'rocket', solar: 'rocket-linear', hugeicons: 'rocket-01' },
118
+ 'link': { lucide: 'link', carbon: 'link', solar: 'link-linear', hugeicons: 'link-02' },
119
+ 'unlink': { lucide: 'unlink', carbon: 'unlink', solar: 'link-broken-linear', hugeicons: 'unlink-03' },
120
+ 'file': { lucide: 'file-text', carbon: 'document', solar: 'document-text-linear', hugeicons: 'file-01' },
121
+ 'file-search': { lucide: 'file-search', carbon: 'document-view', hugeicons: 'file-search' },
122
+ 'file-check': { lucide: 'file-check', carbon: 'document-tasks', hugeicons: 'file-validation' },
123
+ 'folder': { lucide: 'folder', carbon: 'folder', solar: 'folder-linear', hugeicons: 'folder-01' },
124
+ 'calendar': { lucide: 'calendar', carbon: 'calendar', solar: 'calendar-linear', hugeicons: 'calendar-03' },
125
+ 'clock': { lucide: 'clock', carbon: 'time', solar: 'clock-circle-linear', hugeicons: 'clock-01' },
126
+ 'timer': { lucide: 'timer', carbon: 'timer', solar: 'stopwatch-linear', hugeicons: 'timer-02' },
127
+ 'mail': { lucide: 'mail', carbon: 'email', solar: 'letter-linear', hugeicons: 'mail-01' },
128
+ 'globe': { lucide: 'globe', carbon: 'wikis', solar: 'global-linear', hugeicons: 'global' },
129
+ 'plug': { lucide: 'plug', carbon: 'plug', solar: 'plug-circle-linear', hugeicons: 'plug-01' },
130
+ 'database': { lucide: 'database', carbon: 'data-base', solar: 'database-linear', hugeicons: 'database' },
131
+ 'server': { lucide: 'server', carbon: 'bare-metal-server', solar: 'server-linear', hugeicons: 'database-02' },
132
+ 'cloud': { lucide: 'cloud', carbon: 'cloud', solar: 'cloud-linear', hugeicons: 'cloud' },
133
+ 'key': { lucide: 'key', carbon: 'password', solar: 'key-linear', hugeicons: 'key-01' },
134
+ 'user': { lucide: 'user', carbon: 'user', solar: 'user-linear', hugeicons: 'user' },
135
+ 'users': { lucide: 'users', carbon: 'group', solar: 'users-group-rounded-linear', hugeicons: 'user-group' },
136
+ 'user-plus': { lucide: 'user-plus', carbon: 'user-follow', solar: 'user-plus-linear', hugeicons: 'user-add-01' },
137
+ 'credit-card': { lucide: 'credit-card', carbon: 'purchase', solar: 'card-linear', hugeicons: 'credit-card' },
138
+ 'dollar': { lucide: 'dollar-sign', carbon: 'currency-dollar', solar: 'dollar-linear', hugeicons: 'dollar-01' },
139
+ 'book': { lucide: 'book-open', carbon: 'book', solar: 'book-linear', hugeicons: 'book-02' },
140
+ 'graduation': { lucide: 'graduation-cap', carbon: 'education', solar: 'square-academic-cap-linear', hugeicons: 'mortarboard-02' },
141
+ 'image': { lucide: 'image', carbon: 'image', solar: 'gallery-linear', hugeicons: 'image-01' },
142
+ 'code': { lucide: 'code', carbon: 'code', solar: 'code-linear', hugeicons: 'source-code' },
143
+ 'terminal': { lucide: 'terminal', carbon: 'terminal', solar: 'programming-linear', hugeicons: 'computer-terminal-01' },
144
+ 'command': { lucide: 'command', carbon: 'apps', solar: 'command-linear', hugeicons: 'command' },
145
+ 'layout': { lucide: 'layout-dashboard', carbon: 'dashboard', solar: 'widget-linear', hugeicons: 'dashboard-square-01' },
146
+ 'layers': { lucide: 'layers', carbon: 'layers', solar: 'layers-linear', hugeicons: 'layers-01' },
147
+ 'list': { lucide: 'list', carbon: 'list', solar: 'list-linear', hugeicons: 'task-01' },
148
+ 'table': { lucide: 'table', carbon: 'table', solar: 'widget-linear', hugeicons: 'grid-table' },
149
+ 'grid': { lucide: 'grid-3x3', carbon: 'grid', solar: 'widget-2-linear', hugeicons: 'grid-view' },
150
+ 'map': { lucide: 'map-pin', carbon: 'location', solar: 'map-point-linear', hugeicons: 'location-01' },
151
+ 'wifi': { lucide: 'wifi', carbon: 'wifi', solar: 'wi-fi-router-linear', hugeicons: 'wifi-01' },
152
+ 'monitor': { lucide: 'monitor', carbon: 'screen', solar: 'monitor-linear', hugeicons: 'computer' },
153
+ 'mouse-pointer': { lucide: 'mouse-pointer-click', carbon: 'cursor-1', solar: 'cursor-linear', hugeicons: 'cursor-01' },
154
+ 'gauge': { lucide: 'gauge', carbon: 'meter', solar: 'spedometer-max-linear', hugeicons: 'dashboard-speed-01' },
155
+ 'flask': { lucide: 'flask-conical', carbon: 'chemistry', solar: 'test-tube-linear', hugeicons: 'test-tube-01' },
156
+ 'wand': { lucide: 'wand-sparkles', carbon: 'magic-wand-filled', solar: 'magic-stick-3-linear', hugeicons: 'magic-wand-01' },
157
+ 'git': { lucide: 'git-branch', carbon: 'branch', solar: 'branching-paths-up-linear', hugeicons: 'git-branch' },
158
+ 'version': { lucide: 'git-branch', carbon: 'version', solar: 'branching-paths-up-linear', hugeicons: 'git-branch' },
159
+ 'message': { lucide: 'message-square', carbon: 'chat', solar: 'chat-round-linear', hugeicons: 'message-01' },
160
+ 'phone': { lucide: 'phone', carbon: 'phone', solar: 'phone-linear', hugeicons: 'call' },
161
+
162
+ // ── charts / data ──────────────────────────────────────────
163
+ 'chart': { lucide: 'chart-area', carbon: 'chart-area', solar: 'chart-square-linear', hugeicons: 'analytics-01' },
164
+ 'chart-line': { lucide: 'chart-line', carbon: 'chart-line', solar: 'graph-linear', hugeicons: 'analytics-up' },
165
+ 'chart-bar': { lucide: 'chart-column', carbon: 'chart-bar', solar: 'chart-2-linear', hugeicons: 'bar-chart' },
166
+ 'chart-pie': { lucide: 'chart-pie', carbon: 'chart-pie', solar: 'pie-chart-2-linear', hugeicons: 'pie-chart' },
167
+ 'activity': { lucide: 'activity', carbon: 'activity', solar: 'pulse-linear', hugeicons: 'activity-01' },
168
+ 'analytics': { lucide: 'chart-no-axes-combined', carbon: 'analytics', solar: 'chart-linear', hugeicons: 'analytics-01' },
169
+ 'trending-up': { lucide: 'trending-up', carbon: 'growth', solar: 'graph-up-linear', hugeicons: 'analytics-up' },
170
+ 'trending-down': { lucide: 'trending-down', carbon: 'misuse', solar: 'graph-down-linear', hugeicons: 'analytics-down' },
171
+ 'percent': { lucide: 'percent', carbon: 'percentage', solar: 'sale-linear', hugeicons: 'percent' },
172
+
173
+ 'sort': { lucide: 'chevrons-up-down', carbon: 'arrows-vertical', solar: 'sort-vertical-linear', hugeicons: 'arrow-up-down' },
174
+ 'arrow-up-right': { lucide: 'arrow-up-right', carbon: 'arrow-up-right', solar: 'arrow-right-up-linear', hugeicons: 'arrow-up-right-01' },
175
+ 'arrow-down-right': { lucide: 'arrow-down-right', carbon: 'arrow-down-right', solar: 'arrow-right-down-linear', hugeicons: 'arrow-down-right-01' },
176
+ 'compare': { lucide: 'git-compare', carbon: 'compare', solar: 'transfer-horizontal-linear', hugeicons: 'arrow-data-transfer-horizontal' },
177
+ 'thumbs-up': { lucide: 'thumbs-up', carbon: 'thumbs-up', solar: 'like-linear', hugeicons: 'thumbs-up' },
178
+ 'thumbs-down': { lucide: 'thumbs-down', carbon: 'thumbs-down', solar: 'dislike-linear', hugeicons: 'thumbs-down' },
179
+
180
+ // ── more objects / domain ──────────────────────────────────
181
+ 'smartphone': { lucide: 'smartphone', carbon: 'mobile', solar: 'smartphone-linear', hugeicons: 'smart-phone-01' },
182
+ 'unplug': { lucide: 'unplug', carbon: 'connection-signal-off', solar: 'plug-circle-broken', hugeicons: 'plug-socket' },
183
+ 'radar': { lucide: 'radar', carbon: 'radar', solar: 'radar-2-linear', hugeicons: 'radar-01' },
184
+ 'wrench': { lucide: 'wrench', carbon: 'tool-kit', hugeicons: 'wrench-01' },
185
+ 'history': { lucide: 'history', carbon: 'recently-viewed', solar: 'history-linear', hugeicons: 'time-quarter-pass' },
186
+ 'cpu': { lucide: 'cpu', carbon: 'chip', solar: 'cpu-linear', hugeicons: 'cpu' },
187
+ 'puzzle': { lucide: 'puzzle', carbon: 'plug-filled', solar: 'pip-linear', hugeicons: 'puzzle' },
188
+ 'inbox': { lucide: 'inbox', carbon: 'email-new', solar: 'inbox-linear', hugeicons: 'inbox' },
189
+ 'shopping-cart': { lucide: 'shopping-cart', carbon: 'shopping-cart', solar: 'cart-large-2-linear', hugeicons: 'shopping-cart-01' },
190
+ 'hourglass': { lucide: 'hourglass', carbon: 'hourglass', solar: 'hourglass-linear' },
191
+ 'cable': { lucide: 'cable', carbon: 'connect', solar: 'usb-linear', hugeicons: 'cable' },
192
+ 'crown': { lucide: 'crown', solar: 'crown-linear', hugeicons: 'crown' },
193
+ 'cloud-off': { lucide: 'cloud-off', carbon: 'cloud-offline', solar: 'cloud-cross-linear' },
194
+ 'braces': { lucide: 'braces', carbon: 'code', solar: 'code-square-linear', hugeicons: 'source-code-square' },
195
+ 'accessibility': { lucide: 'accessibility', carbon: 'accessibility', solar: 'accessibility-linear' },
196
+ 'video': { lucide: 'video', carbon: 'video', solar: 'videocamera-linear', hugeicons: 'video-01' },
197
+ 'log-out': { lucide: 'log-out', carbon: 'logout', solar: 'logout-2-linear', hugeicons: 'logout-01' },
198
+ 'log-in': { lucide: 'log-in', carbon: 'login', solar: 'login-2-linear', hugeicons: 'login-01' },
199
+ 'list-ordered': { lucide: 'list-ordered', carbon: 'list-numbered', solar: 'list-arrow-down-linear', hugeicons: 'left-to-right-list-number' },
200
+ 'list-checks': { lucide: 'list-checks', carbon: 'task', solar: 'checklist-minimalistic-linear', hugeicons: 'task-done-01' },
201
+ 'folder-plus': { lucide: 'folder-plus', carbon: 'folder-add', solar: 'folder-with-files-linear', hugeicons: 'folder-add' },
202
+ 'briefcase': { lucide: 'briefcase', carbon: 'portfolio', solar: 'case-linear', hugeicons: 'job-search' },
203
+ 'archive': { lucide: 'archive', carbon: 'archive', solar: 'archive-linear', hugeicons: 'archive-02' },
204
+ 'building': { lucide: 'building-2', carbon: 'building', solar: 'buildings-2-linear', hugeicons: 'building-03' },
205
+ 'newspaper': { lucide: 'newspaper', carbon: 'document', solar: 'document-text-linear', hugeicons: 'news' },
206
+ 'megaphone': { lucide: 'megaphone', carbon: 'bullhorn', hugeicons: 'megaphone-01' },
207
+ 'life-buoy': { lucide: 'life-buoy', carbon: 'help', hugeicons: 'help-circle' },
208
+ 'compass': { lucide: 'compass', carbon: 'compass', solar: 'compass-linear', hugeicons: 'navigation-03' },
209
+ 'sprout': { lucide: 'sprout', carbon: 'tree', solar: 'leaf-linear', hugeicons: 'plant-02' },
210
+ 'receipt': { lucide: 'receipt', carbon: 'receipt', solar: 'bill-list-linear', hugeicons: 'invoice-01' },
211
+ 'scale': { lucide: 'scale', carbon: 'scales', solar: 'scale-linear', hugeicons: 'justice-scale-01' },
212
+ 'webhook': { lucide: 'webhook', carbon: 'webhook', solar: 'link-circle-linear', hugeicons: 'api' },
213
+ 'badge': { lucide: 'badge-check', carbon: 'badge', solar: 'medal-ribbon-linear', hugeicons: 'checkmark-badge-01' },
214
+ 'at-sign': { lucide: 'at-sign', carbon: 'at', solar: 'mention-circle-linear', hugeicons: 'at' },
215
+ 'clipboard': { lucide: 'clipboard', carbon: 'document-tasks', solar: 'clipboard-linear', hugeicons: 'clipboard' },
216
+ 'search-x': { lucide: 'search-x', carbon: 'search-locate-mirror', solar: 'minimalistic-magnifer-zoom-out-linear', hugeicons: 'search-remove' },
217
+ 'file-x': { lucide: 'file-x', carbon: 'document-unknown', solar: 'file-corrupted-linear', hugeicons: 'file-not-found' },
218
+ 'calendar-clock': { lucide: 'calendar-clock', carbon: 'calendar-heat-map', solar: 'calendar-date-linear', hugeicons: 'calendar-02' },
219
+ 'video-off': { lucide: 'video-off', carbon: 'video-off', solar: 'videocamera-broken', hugeicons: 'video-off' },
220
+ 'wifi-off': { lucide: 'wifi-off', carbon: 'wifi-off', solar: 'wi-fi-router-minimalistic-linear', hugeicons: 'wifi-disconnected-01' },
221
+
222
+ // ── brand ─ stroke-weight (hugeicons) so they sit with the line icons rather
223
+ // than reading as heavy solid silhouettes. Pinned (fixed) for a consistent brand
224
+ // family across every active set.
225
+ // Google defaults to a monochrome G in muted product contexts. `google-color`
226
+ // is the official multicolor mark for source identity where color is the cue.
227
+ 'google': { fixed: 'i-simple-icons-google' },
228
+ 'google-color': { fixed: 'i-logos-google-icon' },
229
+ 'nuxt': { fixed: 'i-simple-icons-nuxtdotjs' },
230
+ 'github': { fixed: 'i-hugeicons-github' },
231
+ 'discord': { fixed: 'i-hugeicons-discord' },
232
+ 'linkedin': { fixed: 'i-hugeicons-linkedin-01' },
233
+ 'x-twitter': { fixed: 'i-hugeicons-new-twitter' },
234
+ 'facebook': { fixed: 'i-hugeicons-facebook-01' },
235
+ 'slack': { fixed: 'i-hugeicons-slack' },
236
+ } as const satisfies Record<string, RoleMap>
237
+
238
+ export type IconRole = keyof typeof ICON_ROLES
239
+
240
+ /**
241
+ * Alternate names that resolve to a canonical role — synonyms and common
242
+ * iconify-id variants encountered during migration (e.g. `alert-triangle`,
243
+ * `loader-2`, `check-circle` all collapse to one role). Keeps the role list
244
+ * small while letting the codemod map many raw ids onto few roles.
245
+ */
246
+ export const ICON_ALIASES = {
247
+ // status synonyms
248
+ 'alert-triangle': 'warning',
249
+ 'octagon-alert': 'warning',
250
+ 'warning-filled': 'warning',
251
+ 'file-warning': 'warning',
252
+ 'alert-circle': 'caution',
253
+ 'circle-alert': 'caution',
254
+ 'check-circle': 'success',
255
+ 'check-circle-2': 'success',
256
+ 'badge-check': 'success',
257
+ 'task-complete': 'success',
258
+ 'checkmark-outline': 'success',
259
+ 'checkmark-filled': 'success',
260
+ 'x-circle': 'error',
261
+ 'close-filled': 'error',
262
+ 'close-outline': 'error',
263
+ 'help-circle': 'help',
264
+ 'file-question': 'help',
265
+ 'message-circle-question': 'help',
266
+ 'shield-alert': 'shield',
267
+ 'shield-check': 'shield-check',
268
+ // loaders / refresh
269
+ 'loader': 'loading',
270
+ 'loader-2': 'loading',
271
+ 'loader-circle': 'loading',
272
+ 'spinner': 'loading',
273
+ 'renew': 'refresh',
274
+ 'reset': 'refresh',
275
+ 'rotate-cw': 'refresh',
276
+ 'restart': 'refresh',
277
+ // objects
278
+ 'arrow-right': 'next',
279
+ 'arrow-left': 'back',
280
+ 'external-link': 'external',
281
+ 'link-2': 'link',
282
+ 'bot-message-square': 'bot',
283
+ 'robot': 'bot',
284
+ 'message-circle': 'message',
285
+ 'chat': 'message',
286
+ 'tags': 'tag',
287
+ 'tag-group': 'tag',
288
+ 'globe-2': 'globe',
289
+ 'earth': 'globe',
290
+ 'wikis': 'globe',
291
+ 'cog': 'settings',
292
+ 'cog-6-tooth': 'settings',
293
+ 'magnifying-glass': 'search',
294
+ 'pen': 'edit',
295
+ 'pen-line': 'edit',
296
+ 'pencil': 'edit',
297
+ 'trash': 'delete',
298
+ 'trash-2': 'delete',
299
+ 'document-export': 'download',
300
+ 'file-text': 'file',
301
+ 'code-bracket': 'code',
302
+ 'code-reference': 'code',
303
+ 'xml': 'code',
304
+ // charts
305
+ 'line-chart': 'chart-line',
306
+ 'bar-chart-3': 'chart-bar',
307
+ 'pie-chart': 'chart-pie',
308
+ 'chart-no-axes-column': 'chart-bar',
309
+ 'trending-up-carbon': 'trending-up',
310
+ // misc
311
+ 'triangle-alert': 'warning',
312
+ 'clipboard-check': 'list-checks',
313
+ 'clipboard-document-check': 'list-checks',
314
+ 'beaker': 'flask',
315
+ 'swords': 'versus',
316
+ 'crossed-swords': 'versus',
317
+ 'tool-box': 'wrench',
318
+ 'tools': 'wrench',
319
+ 'tool': 'wrench',
320
+ // bare-name variants that collide with a role's canonical id
321
+ 'map': 'map',
322
+ 'file': 'file',
323
+ 'filter': 'filter',
324
+ 'circle-dot': 'circle-dash',
325
+ // more synonyms surfaced in migration
326
+ 'key-square': 'key',
327
+ 'key-round': 'key',
328
+ 'shield-off': 'shield',
329
+ 'pause-circle': 'pause',
330
+ 'bell-ring': 'bell',
331
+ 'search-check': 'search',
332
+ 'rocket-launch': 'rocket',
333
+ 'sparkle': 'ai',
334
+ 'sparkle-fill': 'ai',
335
+ 'ai-label': 'ai',
336
+ 'code-duotone': 'code',
337
+ 'books': 'book',
338
+ 'template': 'layout',
339
+ 'blocks': 'grid',
340
+ // brand logo aliases (carbon `logo-*`, ph duotone, etc.)
341
+ 'logo-x': 'x-twitter',
342
+ 'logo-github': 'github',
343
+ 'logo-discord': 'discord',
344
+ 'logo-linkedin': 'linkedin',
345
+ 'logo-facebook': 'facebook',
346
+ 'logo-slack': 'slack',
347
+ } as const satisfies Record<string, IconRole>