@open-mercato/ui 0.6.7-develop.6784.1.f80b9afce5 → 0.6.7-develop.6788.1.c2a1520a30
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.
- package/.turbo/turbo-build.log +1 -1
- package/build.mjs +2 -54
- package/dist/ai/AiChat.js +21 -2
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +12 -0
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +1 -25
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.js +3 -3
- package/dist/backend/icons/lucideRegistry.js.map +2 -2
- package/dist/backend/icons/lucideRegistryRuntime.js +28 -0
- package/dist/backend/icons/lucideRegistryRuntime.js.map +7 -0
- package/jest.config.cjs +2 -1
- package/package.json +4 -3
- package/scripts/lucideRegistrySource.cjs +133 -0
- package/src/ai/AiChat.tsx +30 -1
- package/src/ai/__tests__/AiChat.test.tsx +31 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +15 -0
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +33 -1
- package/src/backend/icons/__tests__/lucideRegistryGenerator.test.ts +177 -0
- package/src/backend/icons/lucideRegistry.generated.tsx +3 -37
- package/src/backend/icons/lucideRegistry.ts +2 -2
- package/src/backend/icons/lucideRegistryRuntime.tsx +36 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { Project, ScriptKind, SyntaxKind, type ObjectLiteralExpression, type SourceFile } from 'ts-morph'
|
|
5
|
+
import { buildLucideRegistrySource } from '../../../../scripts/lucideRegistrySource.cjs'
|
|
6
|
+
|
|
7
|
+
const packageDir = join(__dirname, '..', '..', '..', '..')
|
|
8
|
+
const iconsDir = join(packageDir, 'src', 'backend', 'icons')
|
|
9
|
+
|
|
10
|
+
type ResolvedIcon = { kebab: string; exportName: string }
|
|
11
|
+
|
|
12
|
+
const populatedFixture: ResolvedIcon[] = [
|
|
13
|
+
{ kebab: 'bell', exportName: 'Bell' },
|
|
14
|
+
{ kebab: 'alert-circle', exportName: 'AlertCircle' },
|
|
15
|
+
{ kebab: 'bar-chart-2', exportName: 'BarChart2' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const duplicateExportFixture: ResolvedIcon[] = [
|
|
19
|
+
{ kebab: 'trash', exportName: 'Trash' },
|
|
20
|
+
{ kebab: 'trash-can', exportName: 'Trash' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function parse(source: string): SourceFile {
|
|
24
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
25
|
+
return project.createSourceFile('lucideRegistry.generated.tsx', source, {
|
|
26
|
+
overwrite: true,
|
|
27
|
+
scriptKind: ScriptKind.TSX,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function syntacticDiagnostics(source: string): string[] {
|
|
32
|
+
const sourceFile = parse(source)
|
|
33
|
+
return sourceFile
|
|
34
|
+
.getProject()
|
|
35
|
+
.getProgram()
|
|
36
|
+
.getSyntacticDiagnostics(sourceFile)
|
|
37
|
+
.map((diagnostic) => `${diagnostic.getLineNumber() ?? 0}: ${diagnostic.getMessageText()}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registryObject(source: string): ObjectLiteralExpression {
|
|
41
|
+
const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
|
|
42
|
+
return declaration.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function registryEntries(source: string): Record<string, string> {
|
|
46
|
+
const entries: Record<string, string> = {}
|
|
47
|
+
for (const property of registryObject(source).getProperties()) {
|
|
48
|
+
const assignment = property.asKindOrThrow(SyntaxKind.PropertyAssignment)
|
|
49
|
+
entries[assignment.getSymbolOrThrow().getName()] = assignment.getInitializerOrThrow().getText()
|
|
50
|
+
}
|
|
51
|
+
return entries
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function valueImportNames(source: string, moduleSpecifier: string): string[] {
|
|
55
|
+
return parse(source)
|
|
56
|
+
.getImportDeclarations()
|
|
57
|
+
.filter((declaration) => declaration.getModuleSpecifierValue() === moduleSpecifier)
|
|
58
|
+
.filter((declaration) => !declaration.isTypeOnly())
|
|
59
|
+
.flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('buildLucideRegistrySource', () => {
|
|
63
|
+
it('maps every discovered kebab name to its lucide export', () => {
|
|
64
|
+
expect(registryEntries(buildLucideRegistrySource(populatedFixture))).toEqual({
|
|
65
|
+
'alert-circle': 'AlertCircle',
|
|
66
|
+
'bar-chart-2': 'BarChart2',
|
|
67
|
+
bell: 'Bell',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('emits the registry keys sorted by kebab name', () => {
|
|
72
|
+
const keys = registryObject(buildLucideRegistrySource(populatedFixture))
|
|
73
|
+
.getProperties()
|
|
74
|
+
.map((property) => property.asKindOrThrow(SyntaxKind.PropertyAssignment).getSymbolOrThrow().getName())
|
|
75
|
+
expect(keys).toEqual(['alert-circle', 'bar-chart-2', 'bell'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('imports each lucide export exactly once', () => {
|
|
79
|
+
expect(valueImportNames(buildLucideRegistrySource(populatedFixture), 'lucide-react')).toEqual([
|
|
80
|
+
'AlertCircle',
|
|
81
|
+
'BarChart2',
|
|
82
|
+
'Bell',
|
|
83
|
+
])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('quotes keys that are not valid identifiers', () => {
|
|
87
|
+
const property = registryObject(buildLucideRegistrySource(populatedFixture))
|
|
88
|
+
.getPropertyOrThrow("'bar-chart-2'")
|
|
89
|
+
.asKindOrThrow(SyntaxKind.PropertyAssignment)
|
|
90
|
+
expect(property.getNameNode().getKind()).toBe(SyntaxKind.StringLiteral)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('de-duplicates the value import when two names share one export', () => {
|
|
94
|
+
const source = buildLucideRegistrySource(duplicateExportFixture)
|
|
95
|
+
expect(valueImportNames(source, 'lucide-react')).toEqual(['Trash'])
|
|
96
|
+
expect(registryEntries(source)).toEqual({ trash: 'Trash', 'trash-can': 'Trash' })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('emits a valid module with an empty registry when no icons are discovered', () => {
|
|
100
|
+
const source = buildLucideRegistrySource([])
|
|
101
|
+
expect(registryObject(source).getProperties()).toHaveLength(0)
|
|
102
|
+
expect(valueImportNames(source, 'lucide-react')).toEqual([])
|
|
103
|
+
expect(source).not.toContain("import {} from 'lucide-react'")
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('keeps the registry a plain mutable object literal', () => {
|
|
107
|
+
const source = buildLucideRegistrySource(populatedFixture)
|
|
108
|
+
const declaration = parse(source).getVariableDeclarationOrThrow('LUCIDE_ICON_REGISTRY')
|
|
109
|
+
expect(declaration.getTypeNodeOrThrow().getText()).toBe('Record<string, LucideIcon>')
|
|
110
|
+
expect(declaration.getVariableStatementOrThrow().isExported()).toBe(true)
|
|
111
|
+
expect(source).not.toContain('as const')
|
|
112
|
+
expect(source).not.toContain('Object.freeze')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('keeps the LucideIcon import type-only', () => {
|
|
116
|
+
const typeOnly = parse(buildLucideRegistrySource(populatedFixture))
|
|
117
|
+
.getImportDeclarations()
|
|
118
|
+
.filter((declaration) => declaration.isTypeOnly())
|
|
119
|
+
.flatMap((declaration) => declaration.getNamedImports().map((named) => named.getName()))
|
|
120
|
+
expect(typeOnly).toEqual(['LucideIcon'])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it.each([
|
|
124
|
+
['populated', populatedFixture],
|
|
125
|
+
['empty', [] as ResolvedIcon[]],
|
|
126
|
+
['duplicate exports', duplicateExportFixture],
|
|
127
|
+
])('emits %s output with zero syntactic diagnostics', (_label, fixture) => {
|
|
128
|
+
expect(syntacticDiagnostics(buildLucideRegistrySource(fixture))).toEqual([])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('fails loudly when an export name would produce an unparseable module', () => {
|
|
132
|
+
expect(() => buildLucideRegistrySource([{ kebab: 'broken', exportName: 'not a valid export' }])).toThrow(
|
|
133
|
+
/not syntactically valid/
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('lucideRegistry barrel', () => {
|
|
139
|
+
it('keeps its public export list unchanged', () => {
|
|
140
|
+
const project = new Project({ useInMemoryFileSystem: true })
|
|
141
|
+
const barrel = project.createSourceFile(
|
|
142
|
+
'lucideRegistry.ts',
|
|
143
|
+
readFileSync(join(iconsDir, 'lucideRegistry.ts'), 'utf-8'),
|
|
144
|
+
{ overwrite: true }
|
|
145
|
+
)
|
|
146
|
+
expect([...barrel.getExportedDeclarations().keys()].sort()).toEqual([
|
|
147
|
+
'LUCIDE_ICON_REGISTRY',
|
|
148
|
+
'registerAdditionalIcons',
|
|
149
|
+
'resolveRegisteredLucideIcon',
|
|
150
|
+
'resolveRegisteredLucideIconNode',
|
|
151
|
+
])
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('lucideRegistry.generated importers', () => {
|
|
156
|
+
it('is imported only from within src/backend/icons', () => {
|
|
157
|
+
const repoRoot = join(packageDir, '..', '..')
|
|
158
|
+
let matches: string[] = []
|
|
159
|
+
try {
|
|
160
|
+
matches = execFileSync(
|
|
161
|
+
'git',
|
|
162
|
+
['grep', '-l', 'lucideRegistry.generated', '--', '*.ts', '*.tsx'],
|
|
163
|
+
{ cwd: repoRoot, encoding: 'utf-8' }
|
|
164
|
+
)
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const status = (error as { status?: number }).status
|
|
169
|
+
if (status !== 1) throw error
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const outsideIconsFolder = matches.filter(
|
|
173
|
+
(path) => !path.startsWith('packages/ui/src/backend/icons/')
|
|
174
|
+
)
|
|
175
|
+
expect(outsideIconsFolder).toEqual([])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
|
|
3
3
|
// Generated from discovered icon string usages across the repo.
|
|
4
4
|
|
|
5
|
-
import type
|
|
6
|
-
import type { LucideIcon } from 'lucide-react'
|
|
5
|
+
import type { LucideIcon } from 'lucide-react';
|
|
7
6
|
import {
|
|
8
7
|
Activity,
|
|
9
8
|
AlertCircle,
|
|
@@ -150,7 +149,7 @@ import {
|
|
|
150
149
|
Wrench,
|
|
151
150
|
X,
|
|
152
151
|
XCircle,
|
|
153
|
-
} from 'lucide-react'
|
|
152
|
+
} from 'lucide-react';
|
|
154
153
|
|
|
155
154
|
export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
156
155
|
'activity': Activity,
|
|
@@ -298,37 +297,4 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
|
298
297
|
'wrench': Wrench,
|
|
299
298
|
'x': X,
|
|
300
299
|
'x-circle': XCircle,
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function normalizeKebabIconName(input: string): string {
|
|
304
|
-
const trimmed = input.trim()
|
|
305
|
-
if (!trimmed) return ''
|
|
306
|
-
const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
|
|
307
|
-
if (!withoutPrefix) return ''
|
|
308
|
-
if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
|
|
309
|
-
return withoutPrefix
|
|
310
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
311
|
-
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
312
|
-
.toLowerCase()
|
|
313
|
-
}
|
|
314
|
-
return withoutPrefix
|
|
315
|
-
.replace(/[_\s]+/g, '-')
|
|
316
|
-
.replace(/-+/g, '-')
|
|
317
|
-
.toLowerCase()
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
|
|
321
|
-
if (!name) return null
|
|
322
|
-
const normalized = normalizeKebabIconName(name)
|
|
323
|
-
if (!normalized) return null
|
|
324
|
-
return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function resolveRegisteredLucideIconNode(
|
|
328
|
-
name: string | undefined,
|
|
329
|
-
className: string
|
|
330
|
-
): React.ReactNode | null {
|
|
331
|
-
const Icon = resolveRegisteredLucideIcon(name)
|
|
332
|
-
if (!Icon) return null
|
|
333
|
-
return <Icon className={className} />
|
|
334
|
-
}
|
|
300
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
2
|
resolveRegisteredLucideIcon,
|
|
3
3
|
resolveRegisteredLucideIconNode,
|
|
4
|
-
|
|
5
|
-
} from './lucideRegistry.generated'
|
|
4
|
+
} from './lucideRegistryRuntime'
|
|
5
|
+
export { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
6
6
|
|
|
7
7
|
import type { LucideIcon } from 'lucide-react'
|
|
8
8
|
import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import type { LucideIcon } from 'lucide-react'
|
|
3
|
+
import { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'
|
|
4
|
+
|
|
5
|
+
function normalizeKebabIconName(input: string): string {
|
|
6
|
+
const trimmed = input.trim()
|
|
7
|
+
if (!trimmed) return ''
|
|
8
|
+
const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
|
|
9
|
+
if (!withoutPrefix) return ''
|
|
10
|
+
if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
|
|
11
|
+
return withoutPrefix
|
|
12
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
13
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
}
|
|
16
|
+
return withoutPrefix
|
|
17
|
+
.replace(/[_\s]+/g, '-')
|
|
18
|
+
.replace(/-+/g, '-')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
|
|
23
|
+
if (!name) return null
|
|
24
|
+
const normalized = normalizeKebabIconName(name)
|
|
25
|
+
if (!normalized) return null
|
|
26
|
+
return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveRegisteredLucideIconNode(
|
|
30
|
+
name: string | undefined,
|
|
31
|
+
className: string
|
|
32
|
+
): React.ReactNode | null {
|
|
33
|
+
const Icon = resolveRegisteredLucideIcon(name)
|
|
34
|
+
if (!Icon) return null
|
|
35
|
+
return <Icon className={className} />
|
|
36
|
+
}
|