@open-mercato/ui 0.4.11-develop.2631.481e9df5b0 → 0.5.0

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 (41) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/AGENTS.md +28 -4
  3. package/agentic/standalone-guide.md +97 -0
  4. package/build.mjs +10 -6
  5. package/dist/backend/AppShell.js +15 -2
  6. package/dist/backend/AppShell.js.map +2 -2
  7. package/dist/backend/DataTable.js +22 -1
  8. package/dist/backend/DataTable.js.map +2 -2
  9. package/dist/backend/detail/CustomDataSection.js +1 -5
  10. package/dist/backend/detail/CustomDataSection.js.map +2 -2
  11. package/dist/backend/detail/InlineEditors.js +2 -5
  12. package/dist/backend/detail/InlineEditors.js.map +2 -2
  13. package/dist/backend/detail/NotesSection.js +2 -6
  14. package/dist/backend/detail/NotesSection.js.map +2 -2
  15. package/dist/backend/icons/lucideRegistry.generated.js +93 -3
  16. package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
  17. package/dist/backend/markdown/MarkdownContent.js +47 -4
  18. package/dist/backend/markdown/MarkdownContent.js.map +2 -2
  19. package/dist/portal/PortalShell.js +41 -11
  20. package/dist/portal/PortalShell.js.map +2 -2
  21. package/dist/portal/hooks/usePortalDashboardWidgets.js +40 -1
  22. package/dist/portal/hooks/usePortalDashboardWidgets.js.map +2 -2
  23. package/dist/portal/utils/nav.js +84 -0
  24. package/dist/portal/utils/nav.js.map +7 -0
  25. package/package.json +13 -9
  26. package/src/backend/AppShell.tsx +22 -2
  27. package/src/backend/DataTable.tsx +28 -5
  28. package/src/backend/__tests__/AppShell.test.tsx +67 -0
  29. package/src/backend/__tests__/FormHeader.test.tsx +0 -1
  30. package/src/backend/detail/CustomDataSection.tsx +1 -10
  31. package/src/backend/detail/InlineEditors.tsx +3 -15
  32. package/src/backend/detail/NotesSection.tsx +5 -14
  33. package/src/backend/icons/lucideRegistry.generated.tsx +93 -3
  34. package/src/backend/injection/__tests__/resolveInjectedIcon.test.tsx +7 -0
  35. package/src/backend/markdown/MarkdownContent.tsx +76 -6
  36. package/src/backend/section-page/types.ts +1 -0
  37. package/src/portal/PortalShell.tsx +43 -11
  38. package/src/portal/hooks/__tests__/usePortalDashboardWidgets.test.tsx +117 -0
  39. package/src/portal/hooks/usePortalDashboardWidgets.ts +55 -1
  40. package/src/portal/utils/__tests__/nav.test.ts +199 -0
  41. package/src/portal/utils/nav.ts +150 -0
@@ -54,6 +54,7 @@ export type AppShellProps = {
54
54
  title: string
55
55
  defaultTitle?: string
56
56
  icon?: React.ReactNode
57
+ iconName?: string
57
58
  iconMarkup?: string
58
59
  enabled?: boolean
59
60
  hidden?: boolean
@@ -64,6 +65,7 @@ export type AppShellProps = {
64
65
  title: string
65
66
  defaultTitle?: string
66
67
  icon?: React.ReactNode
68
+ iconName?: string
67
69
  iconMarkup?: string
68
70
  enabled?: boolean
69
71
  hidden?: boolean
@@ -109,6 +111,7 @@ function convertInjectedMenuItemToSidebarItem(item: InjectionMenuItem, title: st
109
111
  title,
110
112
  defaultTitle: title,
111
113
  icon: resolveInjectedIcon(item.icon) ?? undefined,
114
+ iconName: item.icon,
112
115
  enabled: true,
113
116
  hidden: false,
114
117
  pageContext: 'main',
@@ -302,8 +305,17 @@ function SerializedIcon({ markup }: { markup: string }) {
302
305
  return <span aria-hidden="true" dangerouslySetInnerHTML={{ __html: markup }} />
303
306
  }
304
307
 
305
- function renderIcon(icon: React.ReactNode | undefined, iconMarkup: string | undefined, fallback: React.ReactNode) {
308
+ function renderIcon(
309
+ icon: React.ReactNode | undefined,
310
+ iconName: string | undefined,
311
+ iconMarkup: string | undefined,
312
+ fallback: React.ReactNode,
313
+ ) {
306
314
  if (icon) return icon
315
+ if (iconName) {
316
+ const resolved = resolveInjectedIcon(iconName)
317
+ if (resolved) return resolved
318
+ }
307
319
  if (iconMarkup) return <SerializedIcon markup={iconMarkup} />
308
320
  return fallback
309
321
  }
@@ -862,6 +874,7 @@ function AppShellBody({ productName, email, groups, rightHeaderSlot, children, s
862
874
  <span className={`flex items-center justify-center shrink-0 ${compact ? '' : 'text-muted-foreground'}`}>
863
875
  {renderIcon(
864
876
  item.icon,
877
+ item.iconName,
865
878
  item.iconMarkup,
866
879
  item.href.includes('/backend/entities/user/') && item.href.endsWith('/records') ? DataTableIcon : DefaultIcon,
867
880
  )}
@@ -1243,7 +1256,12 @@ function AppShellBody({ productName, email, groups, rightHeaderSlot, children, s
1243
1256
  <span className="absolute left-0 top-1 bottom-1 w-0.5 rounded bg-foreground" />
1244
1257
  ) : null}
1245
1258
  <span className={`flex items-center justify-center shrink-0 ${compact ? '' : 'text-muted-foreground'}`}>
1246
- {renderIcon(i.icon, i.iconMarkup, DefaultIcon)}
1259
+ {renderIcon(
1260
+ i.icon,
1261
+ i.iconName,
1262
+ i.iconMarkup,
1263
+ DefaultIcon,
1264
+ )}
1247
1265
  </span>
1248
1266
  {!compact && <span>{i.title}</span>}
1249
1267
  </Link>
@@ -1270,6 +1288,7 @@ function AppShellBody({ productName, email, groups, rightHeaderSlot, children, s
1270
1288
  <span className={`flex items-center justify-center shrink-0 ${compact ? '' : 'text-muted-foreground'}`}>
1271
1289
  {renderIcon(
1272
1290
  c.icon,
1291
+ c.iconName,
1273
1292
  c.iconMarkup,
1274
1293
  c.href.includes('/backend/entities/user/') && c.href.endsWith('/records') ? DataTableIcon : DefaultIcon,
1275
1294
  )}
@@ -1564,6 +1583,7 @@ AppShell.cloneGroups = function cloneGroups(groups: AppShellProps['groups']): Ap
1564
1583
  title: item.title,
1565
1584
  defaultTitle: item.defaultTitle,
1566
1585
  icon: item.icon,
1586
+ iconName: item.iconName,
1567
1587
  iconMarkup: item.iconMarkup,
1568
1588
  enabled: item.enabled,
1569
1589
  hidden: item.hidden,
@@ -677,11 +677,34 @@ function ExportMenu({ config, sections }: { config: DataTableExportConfig; secti
677
677
  }
678
678
 
679
679
  function sanitizeDndContextId(value: string): string {
680
- const normalized = value
681
- .trim()
682
- .toLowerCase()
683
- .replace(/[^a-z0-9_-]+/g, '-')
684
- .replace(/^-+|-+$/g, '')
680
+ const trimmed = value.trim().toLowerCase()
681
+ let normalized = ''
682
+ let previousWasDash = false
683
+
684
+ for (const character of trimmed) {
685
+ const isLowercaseLetter = character >= 'a' && character <= 'z'
686
+ const isDigit = character >= '0' && character <= '9'
687
+
688
+ if (isLowercaseLetter || isDigit || character === '_') {
689
+ normalized += character
690
+ previousWasDash = false
691
+ continue
692
+ }
693
+
694
+ if (!previousWasDash) {
695
+ normalized += '-'
696
+ previousWasDash = true
697
+ }
698
+ }
699
+
700
+ while (normalized.startsWith('-')) {
701
+ normalized = normalized.slice(1)
702
+ }
703
+
704
+ while (normalized.endsWith('-')) {
705
+ normalized = normalized.slice(0, -1)
706
+ }
707
+
685
708
  return normalized.length > 0 ? normalized : 'data-table'
686
709
  }
687
710
 
@@ -383,4 +383,71 @@ describe('AppShell', () => {
383
383
  ;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = previousOriginalFetch
384
384
  }
385
385
  })
386
+
387
+ it('renders nav icons from iconName when iconMarkup is missing', async () => {
388
+ const previousFetch = global.fetch
389
+ const previousWindowFetch = window.fetch
390
+ const previousOriginalFetch = (window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch
391
+ const fetchMock = jest.fn(async (input: RequestInfo | URL) => {
392
+ const url = typeof input === 'string'
393
+ ? input
394
+ : input instanceof Request
395
+ ? input.url
396
+ : input.toString()
397
+ if (url.includes('/api/auth/admin/nav-icon-fallback')) {
398
+ return new Response(JSON.stringify({
399
+ groups: [
400
+ {
401
+ id: 'checkout',
402
+ name: 'Checkout',
403
+ defaultName: 'Checkout',
404
+ items: [
405
+ {
406
+ href: '/backend/checkout/pay-links',
407
+ title: 'Pay Links',
408
+ defaultTitle: 'Pay Links',
409
+ enabled: true,
410
+ iconName: 'ticket',
411
+ },
412
+ ],
413
+ },
414
+ ],
415
+ settingsSections: [],
416
+ settingsPathPrefixes: [],
417
+ profileSections: [],
418
+ profilePathPrefixes: ['/backend/profile/'],
419
+ grantedFeatures: ['checkout.view'],
420
+ roles: ['admin'],
421
+ }), { status: 200, headers: { 'content-type': 'application/json' } })
422
+ }
423
+ return new Response(JSON.stringify([]), { status: 200, headers: { 'content-type': 'application/json' } })
424
+ }) as unknown as typeof fetch
425
+ global.fetch = fetchMock
426
+ window.fetch = fetchMock
427
+ ;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = fetchMock
428
+
429
+ try {
430
+ renderWithProviders(
431
+ <AppShell
432
+ email="demo@example.com"
433
+ groups={[]}
434
+ adminNavApi="/api/auth/admin/nav-icon-fallback"
435
+ >
436
+ <div>Hydrated content</div>
437
+ </AppShell>,
438
+ { dict },
439
+ )
440
+
441
+ await waitFor(() => {
442
+ expect(screen.getByText('Pay Links')).toBeInTheDocument()
443
+ })
444
+
445
+ const link = screen.getByRole('link', { name: 'Pay Links' })
446
+ expect(link.querySelector('svg.lucide-ticket')).toBeTruthy()
447
+ } finally {
448
+ global.fetch = previousFetch
449
+ window.fetch = previousWindowFetch
450
+ ;(window as Window & { __omOriginalFetch?: typeof fetch }).__omOriginalFetch = previousOriginalFetch
451
+ }
452
+ })
386
453
  })
@@ -78,4 +78,3 @@ describe('FormHeader', () => {
78
78
  )
79
79
  })
80
80
  })
81
-
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import dynamic from 'next/dynamic'
6
5
  import type { PluggableList } from 'unified'
7
6
  import { Pencil, X } from 'lucide-react'
8
7
  import { useQuery, useQueryClient } from '@tanstack/react-query'
@@ -30,19 +29,11 @@ import {
30
29
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
31
30
  import { cn } from '@open-mercato/shared/lib/utils'
32
31
  import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
32
+ import { MarkdownPreview } from '../markdown'
33
33
  import { useRegisteredComponent } from '../injection/useRegisteredComponent'
34
34
 
35
- type MarkdownPreviewProps = { children: string; className?: string; remarkPlugins?: PluggableList }
36
-
37
35
  const isTestEnv = typeof process !== 'undefined' && process.env.NODE_ENV === 'test'
38
36
 
39
- const MarkdownPreview: React.ComponentType<MarkdownPreviewProps> = isTestEnv
40
- ? ({ children, className }) => <div className={className}>{children}</div>
41
- : (dynamic(() => import('react-markdown').then((mod) => mod.default as React.ComponentType<MarkdownPreviewProps>), {
42
- ssr: false,
43
- loading: () => null,
44
- }) as unknown as React.ComponentType<MarkdownPreviewProps>)
45
-
46
37
  let markdownPluginsPromise: Promise<PluggableList> | null = null
47
38
 
48
39
  async function loadMarkdownPlugins(): Promise<PluggableList> {
@@ -11,6 +11,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
11
  import { cn } from '@open-mercato/shared/lib/utils'
12
12
  import { LoadingMessage } from './LoadingMessage'
13
13
  import { mapCrudServerErrorToFormErrors } from '../utils/serverErrors'
14
+ import { MarkdownPreview } from '../markdown'
14
15
 
15
16
  function resolveInlineErrorMessage(err: unknown, fallbackMessage: string): string {
16
17
  const { message, fieldErrors } = mapCrudServerErrorToFormErrors(err)
@@ -390,12 +391,6 @@ type UiMarkdownEditorProps = {
390
391
  previewOptions?: { remarkPlugins?: unknown[] }
391
392
  }
392
393
 
393
- type MarkdownPreviewProps = {
394
- children: string
395
- className?: string
396
- remarkPlugins?: PluggableList
397
- }
398
-
399
394
  const isTestEnv = typeof process !== 'undefined' && process.env.NODE_ENV === 'test'
400
395
 
401
396
  function MarkdownEditorFallback() {
@@ -421,13 +416,6 @@ const MarkdownEditorComponent: React.ComponentType<UiMarkdownEditorProps> = isTe
421
416
  loading: () => <MarkdownEditorFallback />,
422
417
  }) as unknown as React.ComponentType<UiMarkdownEditorProps>)
423
418
 
424
- const MarkdownPreviewComponent: React.ComponentType<MarkdownPreviewProps> = isTestEnv
425
- ? ({ children, className }) => <div className={className}>{children}</div>
426
- : (dynamic(() => import('react-markdown').then((mod) => mod.default as React.ComponentType<MarkdownPreviewProps>), {
427
- ssr: false,
428
- loading: () => null,
429
- }) as unknown as React.ComponentType<MarkdownPreviewProps>)
430
-
431
419
  let markdownPluginsPromise: Promise<PluggableList> | null = null
432
420
 
433
421
  async function loadMarkdownPlugins(): Promise<PluggableList> {
@@ -708,12 +696,12 @@ export function InlineMultilineEditor({
708
696
  {renderDisplay ? (
709
697
  renderDisplay({ value, emptyLabel })
710
698
  ) : value && value.length ? (
711
- <MarkdownPreviewComponent
699
+ <MarkdownPreview
712
700
  remarkPlugins={markdownPlugins}
713
701
  className="prose prose-sm max-w-none text-foreground [&>*]:my-2 [&>*:last-child]:mb-0 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5"
714
702
  >
715
703
  {value}
716
- </MarkdownPreviewComponent>
704
+ </MarkdownPreview>
717
705
  ) : (
718
706
  <span className="text-muted-foreground">{emptyLabel}</span>
719
707
  )}
@@ -1,7 +1,6 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import dynamic from 'next/dynamic'
5
4
  import type { PluggableList } from 'unified'
6
5
  import type { AppearanceSelectorLabels } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'
7
6
  import { AppearanceDialog } from '@open-mercato/core/modules/customers/components/detail/AppearanceDialog'
@@ -17,9 +16,12 @@ import { TabEmptyState } from './TabEmptyState'
17
16
  import { useConfirmDialog } from '../confirm-dialog'
18
17
  import { formatDateTime } from '@open-mercato/shared/lib/time'
19
18
  import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
19
+ import { MarkdownPreview } from '../markdown'
20
20
  import { useRegisteredComponent } from '../injection/useRegisteredComponent'
21
21
  type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
22
22
 
23
+ const isTestEnv = typeof process !== 'undefined' && process.env.NODE_ENV === 'test'
24
+
23
25
  export type SectionAction = {
24
26
  label: React.ReactNode
25
27
  onClick: () => void
@@ -70,17 +72,6 @@ export type NotesDataAdapter<C = unknown> = {
70
72
  type RenderIconFn = (icon: string, className?: string) => React.ReactNode
71
73
  type RenderColorFn = (color: string, className?: string) => React.ReactNode
72
74
 
73
- type MarkdownPreviewProps = { children: string; className?: string; remarkPlugins?: PluggableList }
74
-
75
- const isTestEnv = typeof process !== 'undefined' && process.env.NODE_ENV === 'test'
76
-
77
- const MarkdownPreviewComponent: React.ComponentType<MarkdownPreviewProps> = isTestEnv
78
- ? ({ children, className }) => <div className={className}>{children}</div>
79
- : (dynamic(() => import('react-markdown').then((mod) => mod.default as React.ComponentType<MarkdownPreviewProps>), {
80
- ssr: false,
81
- loading: () => null,
82
- }) as unknown as React.ComponentType<MarkdownPreviewProps>)
83
-
84
75
  let markdownPluginsPromise: Promise<PluggableList> | null = null
85
76
 
86
77
  async function loadMarkdownPlugins(): Promise<PluggableList> {
@@ -1193,12 +1184,12 @@ function NotesSectionImpl<C = unknown>({
1193
1184
  onClick={() => setContentEditor({ id: note.id, value: note.body })}
1194
1185
  onKeyDown={(event) => handleContentKeyDown(event, note)}
1195
1186
  >
1196
- <MarkdownPreviewComponent
1187
+ <MarkdownPreview
1197
1188
  remarkPlugins={markdownPlugins}
1198
1189
  className="break-words text-foreground [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs"
1199
1190
  >
1200
1191
  {note.body}
1201
- </MarkdownPreviewComponent>
1192
+ </MarkdownPreview>
1202
1193
  </div>
1203
1194
  )}
1204
1195
  </div>
@@ -5,12 +5,20 @@
5
5
  import type * as React from 'react'
6
6
  import type { LucideIcon } from 'lucide-react'
7
7
  import {
8
+ Activity,
8
9
  AlertCircle,
10
+ AlertOctagon,
9
11
  AlertTriangle,
12
+ Archive,
13
+ Award,
10
14
  BadgeCheck,
15
+ Ban,
16
+ Banknote,
11
17
  BarChart2,
18
+ BarChart3,
12
19
  Bell,
13
20
  Bolt,
21
+ Bookmark,
14
22
  Box,
15
23
  Briefcase,
16
24
  BriefcaseBusiness,
@@ -19,11 +27,16 @@ import {
19
27
  Calendar,
20
28
  CalendarCheck,
21
29
  CalendarClock,
30
+ CalendarCog,
31
+ CalendarMinus,
22
32
  CalendarOff,
23
33
  CalendarX,
24
34
  Check,
25
35
  CheckCircle,
36
+ CheckCircle2,
26
37
  CheckSquare,
38
+ Circle,
39
+ ClipboardCheck,
27
40
  ClipboardList,
28
41
  Clock,
29
42
  Clock3,
@@ -35,68 +48,107 @@ import {
35
48
  Download,
36
49
  ExternalLink,
37
50
  FileMinus,
51
+ FilePenLine,
38
52
  FileText,
39
53
  FilterX,
54
+ Flag,
40
55
  FolderTree,
56
+ Gauge,
41
57
  GitBranch,
42
58
  GitCompareArrows,
43
59
  GitPullRequestArrow,
60
+ Globe,
61
+ GraduationCap,
62
+ Hand,
44
63
  Handshake,
45
64
  Heart,
65
+ Hourglass,
46
66
  Inbox,
47
67
  Key,
48
68
  KeyRound,
49
69
  Layers,
70
+ Lightbulb,
50
71
  LineChart,
72
+ Link,
51
73
  List,
74
+ Loader,
75
+ Loader2,
52
76
  Lock,
53
77
  Mail,
54
78
  MailOpen,
55
79
  MapPin,
80
+ Megaphone,
81
+ Notebook,
56
82
  Package,
83
+ PackageCheck,
57
84
  PackagePlus,
58
85
  PackageX,
86
+ PauseCircle,
59
87
  Percent,
88
+ Phone,
89
+ PhoneCall,
60
90
  PieChart,
61
91
  PlusSquare,
62
92
  Receipt,
63
93
  ReceiptText,
94
+ RefreshCcw,
64
95
  Reply,
96
+ RotateCcw,
65
97
  Ruler,
98
+ Send,
66
99
  Settings,
67
100
  Shapes,
68
101
  Shield,
69
102
  ShieldAlert,
70
103
  ShieldCheck,
104
+ ShoppingBag,
71
105
  ShoppingCart,
106
+ Shuffle,
72
107
  Sliders,
73
108
  Smartphone,
109
+ Sparkles,
110
+ Star,
74
111
  StickyNote,
75
112
  Store,
76
113
  Tag,
114
+ Target,
115
+ ThumbsUp,
77
116
  Ticket,
78
117
  Trash2,
79
118
  TrendingUp,
119
+ TriangleAlert,
80
120
  Trophy,
81
121
  Truck,
122
+ Undo2,
82
123
  Unlock,
83
124
  User,
125
+ UserCheck,
84
126
  UserMinus,
85
127
  UserPlus,
86
128
  UserRound,
87
129
  Users,
130
+ Wallet,
88
131
  Webhook,
132
+ Wrench,
89
133
  X,
90
134
  XCircle,
91
135
  } from 'lucide-react'
92
136
 
93
137
  export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
138
+ 'activity': Activity,
94
139
  'alert-circle': AlertCircle,
140
+ 'alert-octagon': AlertOctagon,
95
141
  'alert-triangle': AlertTriangle,
142
+ 'archive': Archive,
143
+ 'award': Award,
96
144
  'badge-check': BadgeCheck,
145
+ 'ban': Ban,
146
+ 'banknote': Banknote,
97
147
  'bar-chart-2': BarChart2,
148
+ 'bar-chart-3': BarChart3,
98
149
  'bell': Bell,
99
150
  'bolt': Bolt,
151
+ 'bookmark': Bookmark,
100
152
  'box': Box,
101
153
  'briefcase': Briefcase,
102
154
  'briefcase-business': BriefcaseBusiness,
@@ -105,11 +157,16 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
105
157
  'calendar': Calendar,
106
158
  'calendar-check': CalendarCheck,
107
159
  'calendar-clock': CalendarClock,
160
+ 'calendar-cog': CalendarCog,
161
+ 'calendar-minus': CalendarMinus,
108
162
  'calendar-off': CalendarOff,
109
163
  'calendar-x': CalendarX,
110
164
  'check': Check,
111
165
  'check-circle': CheckCircle,
166
+ 'check-circle-2': CheckCircle2,
112
167
  'check-square': CheckSquare,
168
+ 'circle': Circle,
169
+ 'clipboard-check': ClipboardCheck,
113
170
  'clipboard-list': ClipboardList,
114
171
  'clock': Clock,
115
172
  'clock-3': Clock3,
@@ -121,57 +178,88 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
121
178
  'download': Download,
122
179
  'external-link': ExternalLink,
123
180
  'file-minus': FileMinus,
181
+ 'file-pen-line': FilePenLine,
124
182
  'file-text': FileText,
125
183
  'filter-x': FilterX,
184
+ 'flag': Flag,
126
185
  'folder-tree': FolderTree,
186
+ 'gauge': Gauge,
127
187
  'git-branch': GitBranch,
128
188
  'git-compare-arrows': GitCompareArrows,
129
189
  'git-pull-request-arrow': GitPullRequestArrow,
190
+ 'globe': Globe,
191
+ 'graduation-cap': GraduationCap,
192
+ 'hand': Hand,
130
193
  'handshake': Handshake,
131
194
  'heart': Heart,
195
+ 'hourglass': Hourglass,
132
196
  'inbox': Inbox,
133
197
  'key': Key,
134
198
  'key-round': KeyRound,
135
199
  'layers': Layers,
200
+ 'lightbulb': Lightbulb,
136
201
  'line-chart': LineChart,
202
+ 'link': Link,
137
203
  'list': List,
204
+ 'loader': Loader,
205
+ 'loader-2': Loader2,
138
206
  'lock': Lock,
139
207
  'mail': Mail,
140
208
  'mail-open': MailOpen,
141
209
  'map-pin': MapPin,
210
+ 'megaphone': Megaphone,
211
+ 'notebook': Notebook,
142
212
  'package': Package,
213
+ 'package-check': PackageCheck,
143
214
  'package-plus': PackagePlus,
144
215
  'package-x': PackageX,
216
+ 'pause-circle': PauseCircle,
145
217
  'percent': Percent,
218
+ 'phone': Phone,
219
+ 'phone-call': PhoneCall,
146
220
  'pie-chart': PieChart,
147
221
  'plus-square': PlusSquare,
148
222
  'receipt': Receipt,
149
223
  'receipt-text': ReceiptText,
224
+ 'refresh-ccw': RefreshCcw,
150
225
  'reply': Reply,
226
+ 'rotate-ccw': RotateCcw,
151
227
  'ruler': Ruler,
228
+ 'send': Send,
152
229
  'settings': Settings,
153
230
  'shapes': Shapes,
154
231
  'shield': Shield,
155
232
  'shield-alert': ShieldAlert,
156
233
  'shield-check': ShieldCheck,
234
+ 'shopping-bag': ShoppingBag,
157
235
  'shopping-cart': ShoppingCart,
236
+ 'shuffle': Shuffle,
158
237
  'sliders': Sliders,
159
238
  'smartphone': Smartphone,
239
+ 'sparkles': Sparkles,
240
+ 'star': Star,
160
241
  'sticky-note': StickyNote,
161
242
  'store': Store,
162
243
  'tag': Tag,
244
+ 'target': Target,
245
+ 'thumbs-up': ThumbsUp,
163
246
  'ticket': Ticket,
164
247
  'trash-2': Trash2,
165
248
  'trending-up': TrendingUp,
249
+ 'triangle-alert': TriangleAlert,
166
250
  'trophy': Trophy,
167
251
  'truck': Truck,
252
+ 'undo-2': Undo2,
168
253
  'unlock': Unlock,
169
254
  'user': User,
255
+ 'user-check': UserCheck,
170
256
  'user-minus': UserMinus,
171
257
  'user-plus': UserPlus,
172
258
  'user-round': UserRound,
173
259
  'users': Users,
260
+ 'wallet': Wallet,
174
261
  'webhook': Webhook,
262
+ 'wrench': Wrench,
175
263
  'x': X,
176
264
  'x-circle': XCircle,
177
265
  }
@@ -179,13 +267,15 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
179
267
  function normalizeKebabIconName(input: string): string {
180
268
  const trimmed = input.trim()
181
269
  if (!trimmed) return ''
182
- if (!trimmed.includes('-') && !trimmed.includes('_') && !trimmed.includes(' ') && /[A-Z]/.test(trimmed)) {
183
- return trimmed
270
+ const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
271
+ if (!withoutPrefix) return ''
272
+ if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
273
+ return withoutPrefix
184
274
  .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
185
275
  .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
186
276
  .toLowerCase()
187
277
  }
188
- return trimmed
278
+ return withoutPrefix
189
279
  .replace(/[_\s]+/g, '-')
190
280
  .replace(/-+/g, '-')
191
281
  .toLowerCase()
@@ -36,4 +36,11 @@ describe('resolveInjectedIcon', () => {
36
36
  it('returns null for an empty string', () => {
37
37
  expect(resolveInjectedIcon('')).toBeNull()
38
38
  })
39
+
40
+ it('resolves lucide-prefixed icon names', () => {
41
+ const node = resolveInjectedIcon('lucide:bell')
42
+ expect(node).not.toBeNull()
43
+ const { container } = render(<>{node}</>)
44
+ expect(container.querySelector('svg')).toBeTruthy()
45
+ })
39
46
  })