@open-mercato/ui 0.6.7-develop.6785.1.1dd7cfac55 → 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/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/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
package/.turbo/turbo-build.log
CHANGED
package/build.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { buildPackage } from '../../scripts/build-package.mjs'
|
|
6
|
+
import { buildLucideRegistrySource } from './scripts/lucideRegistrySource.cjs'
|
|
6
7
|
|
|
7
8
|
const packageDir = dirname(fileURLToPath(import.meta.url))
|
|
8
9
|
|
|
@@ -88,61 +89,8 @@ async function generateLucideRegistry() {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
resolved.sort((a, b) => a.kebab.localeCompare(b.kebab))
|
|
92
|
-
const uniqueExports = [...new Set(resolved.map((r) => r.exportName))].sort((a, b) => a.localeCompare(b))
|
|
93
|
-
|
|
94
|
-
const importSection = uniqueExports.length
|
|
95
|
-
? `import {\n${uniqueExports.map((n) => ` ${n},`).join('\n')}\n} from 'lucide-react'\n`
|
|
96
|
-
: ``
|
|
97
|
-
|
|
98
|
-
const registryEntries = resolved.map((r) => ` '${r.kebab}': ${r.exportName},`).join('\n')
|
|
99
|
-
|
|
100
92
|
const outPath = join(packageDir, 'src/backend/icons/lucideRegistry.generated.tsx')
|
|
101
|
-
|
|
102
|
-
// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
|
|
103
|
-
// Generated from discovered icon string usages across the repo.
|
|
104
|
-
|
|
105
|
-
import type * as React from 'react'
|
|
106
|
-
import type { LucideIcon } from 'lucide-react'
|
|
107
|
-
${importSection}
|
|
108
|
-
export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
|
|
109
|
-
${registryEntries ? `${registryEntries}\n` : ''}}
|
|
110
|
-
|
|
111
|
-
function normalizeKebabIconName(input: string): string {
|
|
112
|
-
const trimmed = input.trim()
|
|
113
|
-
if (!trimmed) return ''
|
|
114
|
-
const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed
|
|
115
|
-
if (!withoutPrefix) return ''
|
|
116
|
-
if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {
|
|
117
|
-
return withoutPrefix
|
|
118
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
119
|
-
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
120
|
-
.toLowerCase()
|
|
121
|
-
}
|
|
122
|
-
return withoutPrefix
|
|
123
|
-
.replace(/[_\\s]+/g, '-')
|
|
124
|
-
.replace(/-+/g, '-')
|
|
125
|
-
.toLowerCase()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {
|
|
129
|
-
if (!name) return null
|
|
130
|
-
const normalized = normalizeKebabIconName(name)
|
|
131
|
-
if (!normalized) return null
|
|
132
|
-
return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function resolveRegisteredLucideIconNode(
|
|
136
|
-
name: string | undefined,
|
|
137
|
-
className: string
|
|
138
|
-
): React.ReactNode | null {
|
|
139
|
-
const Icon = resolveRegisteredLucideIcon(name)
|
|
140
|
-
if (!Icon) return null
|
|
141
|
-
return <Icon className={className} />
|
|
142
|
-
}
|
|
143
|
-
`
|
|
144
|
-
|
|
145
|
-
writeFileSync(outPath, file)
|
|
93
|
+
writeFileSync(outPath, buildLucideRegistrySource(resolved))
|
|
146
94
|
console.log(`Generated lucide registry with ${resolved.length} icons -> ${outPath}`)
|
|
147
95
|
}
|
|
148
96
|
|
|
@@ -82,6 +82,18 @@ function ConfirmDialog({
|
|
|
82
82
|
dialog.addEventListener("cancel", handleCancel2);
|
|
83
83
|
return () => dialog.removeEventListener("cancel", handleCancel2);
|
|
84
84
|
}, [loading, requestClose]);
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
if (!open) return;
|
|
87
|
+
const handleEscape = (event) => {
|
|
88
|
+
if (event.key !== "Escape") return;
|
|
89
|
+
if (!(event.target instanceof Node) || !dialogRef.current?.contains(event.target)) return;
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
if (!loading) requestClose();
|
|
93
|
+
};
|
|
94
|
+
window.addEventListener("keydown", handleEscape, true);
|
|
95
|
+
return () => window.removeEventListener("keydown", handleEscape, true);
|
|
96
|
+
}, [loading, open, requestClose]);
|
|
85
97
|
const handleBackdropClick = (e) => {
|
|
86
98
|
if (e.target === dialogRef.current && !loading) {
|
|
87
99
|
requestClose();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/confirm-dialog/ConfirmDialog.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { IconButton } from \"../../primitives/icon-button\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { Loader2, X } from \"lucide-react\";\n\nexport type ConfirmDialogProps = {\n /** Whether the dialog is open (controlled mode \u2014 used by useConfirmDialog) */\n open?: boolean;\n /** Callback when open state changes (controlled mode) */\n onOpenChange?: (open: boolean) => void;\n /** Callback when user confirms */\n onConfirm: () => void | Promise<void>;\n /** Callback when user cancels (optional, defaults to closing) */\n onCancel?: () => void;\n /** Dialog title. Defaults to i18n key \"ui.dialogs.confirm.defaultTitle\" (\"Are you sure?\") */\n title?: string;\n /** Dialog body text / description */\n text?: string;\n /** Confirm button label. Defaults to i18n \"ui.dialogs.confirm.confirmText\" (\"Confirm\").\n * Pass `false` to hide the confirm button entirely. */\n confirmText?: string | false;\n /** Cancel button label. Defaults to i18n \"ui.dialogs.confirm.cancelText\" (\"Cancel\").\n * Pass `false` to hide the cancel button entirely. */\n cancelText?: string | false;\n /** Visual variant \u2014 \"destructive\" renders the confirm button in red */\n variant?: \"default\" | \"destructive\";\n /** Whether the confirm button shows a loading spinner.\n * Useful for async onConfirm handlers (e.g., waiting for API response before closing). */\n loading?: boolean;\n /** Trigger element \u2014 when provided, component manages its own open state (declarative mode).\n * Clicking the trigger opens the dialog. */\n trigger?: React.ReactNode;\n};\n\nexport function ConfirmDialog({\n open: controlledOpen,\n onOpenChange,\n onConfirm,\n onCancel,\n title,\n text,\n confirmText,\n cancelText,\n variant = \"default\",\n loading = false,\n trigger,\n}: ConfirmDialogProps) {\n const t = useT();\n const dialogRef = React.useRef<HTMLDialogElement>(null);\n const [internalOpen, setInternalOpen] = React.useState(false);\n const cancelButtonRef = React.useRef<HTMLButtonElement>(null);\n const confirmButtonRef = React.useRef<HTMLButtonElement>(null);\n const [portalTarget, setPortalTarget] = React.useState<HTMLElement | null>(null);\n // Unique IDs so multiple ConfirmDialog instances on the same page don't\n // collide and make `aria-labelledby` resolve to the wrong dialog's title.\n const reactId = React.useId();\n const titleId = `confirm-dialog-title-${reactId}`;\n const descriptionId = `confirm-dialog-description-${reactId}`;\n\n // Determine if we're in controlled mode (open prop provided) or declarative mode (trigger provided)\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n const setOpen = isControlled\n ? onOpenChange || (() => {})\n : setInternalOpen;\n const handleCancelCallback = React.useCallback(() => {\n if (!isControlled) {\n onCancel?.();\n }\n }, [isControlled, onCancel]);\n\n // Default text values from i18n\n const resolvedTitle =\n title ?? t(\"ui.dialogs.confirm.defaultTitle\", \"Are you sure?\");\n const resolvedConfirmText =\n confirmText === false\n ? false\n : confirmText ?? t(\"ui.dialogs.confirm.confirmText\", \"Confirm\");\n const resolvedCancelText =\n cancelText === false\n ? false\n : cancelText ?? t(\"ui.dialogs.confirm.cancelText\", \"Cancel\");\n const closeAriaLabel = t(\"ui.dialog.close.ariaLabel\", \"Close\");\n const requestClose = React.useCallback(() => {\n setOpen(false);\n handleCancelCallback();\n }, [setOpen, handleCancelCallback]);\n\n React.useLayoutEffect(() => {\n setPortalTarget(document.body);\n }, []);\n\n // Handle dialog open/close with native showModal/close\n React.useLayoutEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (open) {\n if (!dialog.open) {\n dialog.showModal();\n // Focus cancel button (safe default) or confirm if no cancel\n setTimeout(() => {\n if (resolvedCancelText !== false && cancelButtonRef.current) {\n cancelButtonRef.current.focus();\n } else if (confirmButtonRef.current) {\n confirmButtonRef.current.focus();\n }\n }, 0);\n }\n } else {\n if (dialog.open) {\n dialog.close();\n }\n }\n }, [open, resolvedCancelText, portalTarget]);\n\n // Handle native cancel event (Escape key)\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const handleCancel = (e: Event) => {\n // Prevent close if loading\n if (loading) {\n e.preventDefault();\n return;\n }\n requestClose();\n };\n\n dialog.addEventListener(\"cancel\", handleCancel);\n return () => dialog.removeEventListener(\"cancel\", handleCancel);\n }, [loading, requestClose]);\n\n // Handle backdrop click\n const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {\n // Only close if clicking directly on the dialog (backdrop), not its children\n if (e.target === dialogRef.current && !loading) {\n requestClose();\n }\n };\n\n // Handle keyboard shortcuts\n React.useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl+Enter confirms\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\" && !loading) {\n e.preventDefault();\n handleConfirm();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [open, loading]);\n\n const handleConfirm = async () => {\n await onConfirm();\n // Don't auto-close if loading \u2014 let the parent control when to close\n if (!loading) {\n setOpen(false);\n }\n };\n\n const handleCancel = () => {\n requestClose();\n };\n\n const handleTriggerClick = () => {\n if (trigger) {\n setOpen(true);\n }\n };\n\n const dialogElement = (\n <dialog\n ref={dialogRef}\n role=\"alertdialog\"\n aria-labelledby={titleId}\n aria-describedby={text ? descriptionId : undefined}\n onClick={handleBackdropClick}\n className={cn(\n // Reset dialog defaults\n \"m-0 p-0 max-w-none bg-transparent border-none pointer-events-auto\",\n // Backdrop styling\n \"backdrop:bg-black/50 backdrop:backdrop-blur-sm backdrop:transition-opacity\",\n // Mobile: bottom sheet\n \"fixed inset-x-0 bottom-0 top-auto w-full\",\n // Desktop: centered\n \"sm:inset-auto sm:mx-auto sm:my-auto sm:max-w-md sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2\",\n // Animation with reduced motion support\n \"motion-safe:open:animate-in motion-safe:open:fade-in-0 motion-safe:open:slide-in-from-bottom-4\",\n \"sm:motion-safe:open:slide-in-from-bottom-0 sm:motion-safe:open:zoom-in-95\",\n // Duration\n \"motion-safe:open:duration-300\"\n )}\n >\n <div\n role=\"document\"\n className={cn(\n // Panel container\n \"flex flex-col gap-4 rounded-t-2xl border-t bg-card p-6 text-foreground shadow-lg\",\n \"sm:rounded-xl sm:border\",\n // Relative positioning for close button\n \"relative\"\n )}\n >\n {/* Close button */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={loading}\n aria-label={closeAriaLabel}\n className=\"absolute right-4 top-4 opacity-70 hover:opacity-100\"\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n\n {/* Title */}\n <h2\n id={titleId}\n className={cn(\n \"text-sm font-medium leading-snug tracking-tight pr-6\",\n // Mobile: centered, Desktop: left-aligned\n \"text-center sm:text-left\"\n )}\n >\n {resolvedTitle}\n </h2>\n\n {/* Description (optional) */}\n {text && (\n <p\n id={descriptionId}\n className=\"text-sm font-medium leading-snug text-muted-foreground\"\n >\n {text}\n </p>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n {resolvedCancelText !== false && (\n <Button\n ref={cancelButtonRef}\n variant=\"outline\"\n onClick={handleCancel}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {resolvedCancelText}\n </Button>\n )}\n {resolvedConfirmText !== false && (\n <Button\n ref={confirmButtonRef}\n variant={variant === \"destructive\" ? \"destructive\" : \"default\"}\n onClick={handleConfirm}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {resolvedConfirmText}\n </Button>\n )}\n </div>\n </div>\n </dialog>\n );\n\n return (\n <>\n {trigger && (\n <div onClick={handleTriggerClick} className=\"inline-block\" role=\"presentation\">\n {trigger}\n </div>\n )}\n\n {portalTarget ? createPortal(dialogElement, portalTarget) : null}\n </>\n );\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { IconButton } from \"../../primitives/icon-button\";\nimport { cn } from \"@open-mercato/shared/lib/utils\";\nimport { Loader2, X } from \"lucide-react\";\n\nexport type ConfirmDialogProps = {\n /** Whether the dialog is open (controlled mode \u2014 used by useConfirmDialog) */\n open?: boolean;\n /** Callback when open state changes (controlled mode) */\n onOpenChange?: (open: boolean) => void;\n /** Callback when user confirms */\n onConfirm: () => void | Promise<void>;\n /** Callback when user cancels (optional, defaults to closing) */\n onCancel?: () => void;\n /** Dialog title. Defaults to i18n key \"ui.dialogs.confirm.defaultTitle\" (\"Are you sure?\") */\n title?: string;\n /** Dialog body text / description */\n text?: string;\n /** Confirm button label. Defaults to i18n \"ui.dialogs.confirm.confirmText\" (\"Confirm\").\n * Pass `false` to hide the confirm button entirely. */\n confirmText?: string | false;\n /** Cancel button label. Defaults to i18n \"ui.dialogs.confirm.cancelText\" (\"Cancel\").\n * Pass `false` to hide the cancel button entirely. */\n cancelText?: string | false;\n /** Visual variant \u2014 \"destructive\" renders the confirm button in red */\n variant?: \"default\" | \"destructive\";\n /** Whether the confirm button shows a loading spinner.\n * Useful for async onConfirm handlers (e.g., waiting for API response before closing). */\n loading?: boolean;\n /** Trigger element \u2014 when provided, component manages its own open state (declarative mode).\n * Clicking the trigger opens the dialog. */\n trigger?: React.ReactNode;\n};\n\nexport function ConfirmDialog({\n open: controlledOpen,\n onOpenChange,\n onConfirm,\n onCancel,\n title,\n text,\n confirmText,\n cancelText,\n variant = \"default\",\n loading = false,\n trigger,\n}: ConfirmDialogProps) {\n const t = useT();\n const dialogRef = React.useRef<HTMLDialogElement>(null);\n const [internalOpen, setInternalOpen] = React.useState(false);\n const cancelButtonRef = React.useRef<HTMLButtonElement>(null);\n const confirmButtonRef = React.useRef<HTMLButtonElement>(null);\n const [portalTarget, setPortalTarget] = React.useState<HTMLElement | null>(null);\n // Unique IDs so multiple ConfirmDialog instances on the same page don't\n // collide and make `aria-labelledby` resolve to the wrong dialog's title.\n const reactId = React.useId();\n const titleId = `confirm-dialog-title-${reactId}`;\n const descriptionId = `confirm-dialog-description-${reactId}`;\n\n // Determine if we're in controlled mode (open prop provided) or declarative mode (trigger provided)\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n const setOpen = isControlled\n ? onOpenChange || (() => {})\n : setInternalOpen;\n const handleCancelCallback = React.useCallback(() => {\n if (!isControlled) {\n onCancel?.();\n }\n }, [isControlled, onCancel]);\n\n // Default text values from i18n\n const resolvedTitle =\n title ?? t(\"ui.dialogs.confirm.defaultTitle\", \"Are you sure?\");\n const resolvedConfirmText =\n confirmText === false\n ? false\n : confirmText ?? t(\"ui.dialogs.confirm.confirmText\", \"Confirm\");\n const resolvedCancelText =\n cancelText === false\n ? false\n : cancelText ?? t(\"ui.dialogs.confirm.cancelText\", \"Cancel\");\n const closeAriaLabel = t(\"ui.dialog.close.ariaLabel\", \"Close\");\n const requestClose = React.useCallback(() => {\n setOpen(false);\n handleCancelCallback();\n }, [setOpen, handleCancelCallback]);\n\n React.useLayoutEffect(() => {\n setPortalTarget(document.body);\n }, []);\n\n // Handle dialog open/close with native showModal/close\n React.useLayoutEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (open) {\n if (!dialog.open) {\n dialog.showModal();\n // Focus cancel button (safe default) or confirm if no cancel\n setTimeout(() => {\n if (resolvedCancelText !== false && cancelButtonRef.current) {\n cancelButtonRef.current.focus();\n } else if (confirmButtonRef.current) {\n confirmButtonRef.current.focus();\n }\n }, 0);\n }\n } else {\n if (dialog.open) {\n dialog.close();\n }\n }\n }, [open, resolvedCancelText, portalTarget]);\n\n // Handle native cancel event (Escape key)\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const handleCancel = (e: Event) => {\n // Prevent close if loading\n if (loading) {\n e.preventDefault();\n return;\n }\n requestClose();\n };\n\n dialog.addEventListener(\"cancel\", handleCancel);\n return () => dialog.removeEventListener(\"cancel\", handleCancel);\n }, [loading, requestClose]);\n\n React.useEffect(() => {\n if (!open) return;\n\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key !== \"Escape\") return;\n if (!(event.target instanceof Node) || !dialogRef.current?.contains(event.target)) return;\n event.preventDefault();\n event.stopPropagation();\n if (!loading) requestClose();\n };\n\n window.addEventListener(\"keydown\", handleEscape, true);\n return () => window.removeEventListener(\"keydown\", handleEscape, true);\n }, [loading, open, requestClose]);\n\n // Handle backdrop click\n const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {\n // Only close if clicking directly on the dialog (backdrop), not its children\n if (e.target === dialogRef.current && !loading) {\n requestClose();\n }\n };\n\n // Handle keyboard shortcuts\n React.useEffect(() => {\n if (!open) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl+Enter confirms\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\" && !loading) {\n e.preventDefault();\n handleConfirm();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [open, loading]);\n\n const handleConfirm = async () => {\n await onConfirm();\n // Don't auto-close if loading \u2014 let the parent control when to close\n if (!loading) {\n setOpen(false);\n }\n };\n\n const handleCancel = () => {\n requestClose();\n };\n\n const handleTriggerClick = () => {\n if (trigger) {\n setOpen(true);\n }\n };\n\n const dialogElement = (\n <dialog\n ref={dialogRef}\n role=\"alertdialog\"\n aria-labelledby={titleId}\n aria-describedby={text ? descriptionId : undefined}\n onClick={handleBackdropClick}\n className={cn(\n // Reset dialog defaults\n \"m-0 p-0 max-w-none bg-transparent border-none pointer-events-auto\",\n // Backdrop styling\n \"backdrop:bg-black/50 backdrop:backdrop-blur-sm backdrop:transition-opacity\",\n // Mobile: bottom sheet\n \"fixed inset-x-0 bottom-0 top-auto w-full\",\n // Desktop: centered\n \"sm:inset-auto sm:mx-auto sm:my-auto sm:max-w-md sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2\",\n // Animation with reduced motion support\n \"motion-safe:open:animate-in motion-safe:open:fade-in-0 motion-safe:open:slide-in-from-bottom-4\",\n \"sm:motion-safe:open:slide-in-from-bottom-0 sm:motion-safe:open:zoom-in-95\",\n // Duration\n \"motion-safe:open:duration-300\"\n )}\n >\n <div\n role=\"document\"\n className={cn(\n // Panel container\n \"flex flex-col gap-4 rounded-t-2xl border-t bg-card p-6 text-foreground shadow-lg\",\n \"sm:rounded-xl sm:border\",\n // Relative positioning for close button\n \"relative\"\n )}\n >\n {/* Close button */}\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleCancel}\n disabled={loading}\n aria-label={closeAriaLabel}\n className=\"absolute right-4 top-4 opacity-70 hover:opacity-100\"\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n\n {/* Title */}\n <h2\n id={titleId}\n className={cn(\n \"text-sm font-medium leading-snug tracking-tight pr-6\",\n // Mobile: centered, Desktop: left-aligned\n \"text-center sm:text-left\"\n )}\n >\n {resolvedTitle}\n </h2>\n\n {/* Description (optional) */}\n {text && (\n <p\n id={descriptionId}\n className=\"text-sm font-medium leading-snug text-muted-foreground\"\n >\n {text}\n </p>\n )}\n\n {/* Actions */}\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n {resolvedCancelText !== false && (\n <Button\n ref={cancelButtonRef}\n variant=\"outline\"\n onClick={handleCancel}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {resolvedCancelText}\n </Button>\n )}\n {resolvedConfirmText !== false && (\n <Button\n ref={confirmButtonRef}\n variant={variant === \"destructive\" ? \"destructive\" : \"default\"}\n onClick={handleConfirm}\n disabled={loading}\n className=\"w-full sm:w-auto\"\n >\n {loading && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {resolvedConfirmText}\n </Button>\n )}\n </div>\n </div>\n </dialog>\n );\n\n return (\n <>\n {trigger && (\n <div onClick={handleTriggerClick} className=\"inline-block\" role=\"presentation\">\n {trigger}\n </div>\n )}\n\n {portalTarget ? createPortal(dialogElement, portalTarget) : null}\n </>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["handleCancel"]
|
|
7
7
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
1
|
import {
|
|
3
2
|
Activity,
|
|
4
3
|
AlertCircle,
|
|
@@ -293,30 +292,7 @@ const LUCIDE_ICON_REGISTRY = {
|
|
|
293
292
|
"x": X,
|
|
294
293
|
"x-circle": XCircle
|
|
295
294
|
};
|
|
296
|
-
function normalizeKebabIconName(input) {
|
|
297
|
-
const trimmed = input.trim();
|
|
298
|
-
if (!trimmed) return "";
|
|
299
|
-
const withoutPrefix = trimmed.startsWith("lucide:") ? trimmed.slice("lucide:".length) : trimmed;
|
|
300
|
-
if (!withoutPrefix) return "";
|
|
301
|
-
if (!withoutPrefix.includes("-") && !withoutPrefix.includes("_") && !withoutPrefix.includes(" ") && /[A-Z]/.test(withoutPrefix)) {
|
|
302
|
-
return withoutPrefix.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
303
|
-
}
|
|
304
|
-
return withoutPrefix.replace(/[_\s]+/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
305
|
-
}
|
|
306
|
-
function resolveRegisteredLucideIcon(name) {
|
|
307
|
-
if (!name) return null;
|
|
308
|
-
const normalized = normalizeKebabIconName(name);
|
|
309
|
-
if (!normalized) return null;
|
|
310
|
-
return LUCIDE_ICON_REGISTRY[normalized] ?? null;
|
|
311
|
-
}
|
|
312
|
-
function resolveRegisteredLucideIconNode(name, className) {
|
|
313
|
-
const Icon = resolveRegisteredLucideIcon(name);
|
|
314
|
-
if (!Icon) return null;
|
|
315
|
-
return /* @__PURE__ */ jsx(Icon, { className });
|
|
316
|
-
}
|
|
317
295
|
export {
|
|
318
|
-
LUCIDE_ICON_REGISTRY
|
|
319
|
-
resolveRegisteredLucideIcon,
|
|
320
|
-
resolveRegisteredLucideIconNode
|
|
296
|
+
LUCIDE_ICON_REGISTRY
|
|
321
297
|
};
|
|
322
298
|
//# sourceMappingURL=lucideRegistry.generated.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/icons/lucideRegistry.generated.tsx"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type { LucideIcon } from 'lucide-react';\nimport {\n Activity,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Archive,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Book,\n Bookmark,\n Box,\n Boxes,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CreditCard,\n Crown,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n Eye,\n FileMinus,\n FilePenLine,\n FileSpreadsheet,\n FileText,\n FilterX,\n Flag,\n Flame,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hand,\n Handshake,\n Headphones,\n Heart,\n HelpCircle,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Lightbulb,\n LineChart,\n Link,\n List,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n MapPin,\n MapPinned,\n MessageCircle,\n MinusCircle,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageSearch,\n PackageX,\n PauseCircle,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n RefreshCw,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Share2,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Snowflake,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Timer,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n UserX,\n Wallet,\n Warehouse,\n Webhook,\n Workflow,\n Wrench,\n X,\n XCircle,\n} from 'lucide-react';\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'archive': Archive,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'book': Book,\n 'bookmark': Bookmark,\n 'box': Box,\n 'boxes': Boxes,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'credit-card': CreditCard,\n 'crown': Crown,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'eye': Eye,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-spreadsheet': FileSpreadsheet,\n 'file-text': FileText,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'flame': Flame,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hand': Hand,\n 'handshake': Handshake,\n 'headphones': Headphones,\n 'heart': Heart,\n 'help-circle': HelpCircle,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map-pin': MapPin,\n 'map-pinned': MapPinned,\n 'message-circle': MessageCircle,\n 'minus-circle': MinusCircle,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-search': PackageSearch,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'refresh-cw': RefreshCw,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'share-2': Share2,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'snowflake': Snowflake,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'timer': Timer,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'user-x': UserX,\n 'users': Users,\n 'wallet': Wallet,\n 'warehouse': Warehouse,\n 'webhook': Webhook,\n 'workflow': Workflow,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n};\n"],
|
|
5
|
+
"mappings": "AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveRegisteredLucideIcon,
|
|
3
|
-
resolveRegisteredLucideIconNode
|
|
4
|
-
|
|
5
|
-
} from "./lucideRegistry.generated.js";
|
|
3
|
+
resolveRegisteredLucideIconNode
|
|
4
|
+
} from "./lucideRegistryRuntime.js";
|
|
5
|
+
import { LUCIDE_ICON_REGISTRY } from "./lucideRegistry.generated.js";
|
|
6
6
|
import { LUCIDE_ICON_REGISTRY as LUCIDE_ICON_REGISTRY2 } from "./lucideRegistry.generated.js";
|
|
7
7
|
function registerAdditionalIcons(icons) {
|
|
8
8
|
for (const [name, component] of Object.entries(icons)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/icons/lucideRegistry.ts"],
|
|
4
|
-
"sourcesContent": ["export {\n resolveRegisteredLucideIcon,\n resolveRegisteredLucideIconNode,\n
|
|
5
|
-
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["export {\n resolveRegisteredLucideIcon,\n resolveRegisteredLucideIconNode,\n} from './lucideRegistryRuntime'\nexport { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'\n\nimport type { LucideIcon } from 'lucide-react'\nimport { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'\n\nexport function registerAdditionalIcons(icons: Record<string, LucideIcon>): void {\n for (const [name, component] of Object.entries(icons)) {\n LUCIDE_ICON_REGISTRY[name] = component\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AAGrC,SAAS,wBAAAA,6BAA4B;AAE9B,SAAS,wBAAwB,OAAyC;AAC/E,aAAW,CAAC,MAAM,SAAS,KAAK,OAAO,QAAQ,KAAK,GAAG;AACrD,IAAAA,sBAAqB,IAAI,IAAI;AAAA,EAC/B;AACF;",
|
|
6
6
|
"names": ["LUCIDE_ICON_REGISTRY"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { LUCIDE_ICON_REGISTRY } from "./lucideRegistry.generated.js";
|
|
3
|
+
function normalizeKebabIconName(input) {
|
|
4
|
+
const trimmed = input.trim();
|
|
5
|
+
if (!trimmed) return "";
|
|
6
|
+
const withoutPrefix = trimmed.startsWith("lucide:") ? trimmed.slice("lucide:".length) : trimmed;
|
|
7
|
+
if (!withoutPrefix) return "";
|
|
8
|
+
if (!withoutPrefix.includes("-") && !withoutPrefix.includes("_") && !withoutPrefix.includes(" ") && /[A-Z]/.test(withoutPrefix)) {
|
|
9
|
+
return withoutPrefix.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
return withoutPrefix.replace(/[_\s]+/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
function resolveRegisteredLucideIcon(name) {
|
|
14
|
+
if (!name) return null;
|
|
15
|
+
const normalized = normalizeKebabIconName(name);
|
|
16
|
+
if (!normalized) return null;
|
|
17
|
+
return LUCIDE_ICON_REGISTRY[normalized] ?? null;
|
|
18
|
+
}
|
|
19
|
+
function resolveRegisteredLucideIconNode(name, className) {
|
|
20
|
+
const Icon = resolveRegisteredLucideIcon(name);
|
|
21
|
+
if (!Icon) return null;
|
|
22
|
+
return /* @__PURE__ */ jsx(Icon, { className });
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
resolveRegisteredLucideIcon,
|
|
26
|
+
resolveRegisteredLucideIconNode
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=lucideRegistryRuntime.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/icons/lucideRegistryRuntime.tsx"],
|
|
4
|
+
"sourcesContent": ["import type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport { LUCIDE_ICON_REGISTRY } from './lucideRegistry.generated'\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed\n if (!withoutPrefix) return ''\n if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {\n return withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n }\n return withoutPrefix\n .replace(/[_\\s]+/g, '-')\n .replace(/-+/g, '-')\n .toLowerCase()\n}\n\nexport function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {\n if (!name) return null\n const normalized = normalizeKebabIconName(name)\n if (!normalized) return null\n return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null\n}\n\nexport function resolveRegisteredLucideIconNode(\n name: string | undefined,\n className: string\n): React.ReactNode | null {\n const Icon = resolveRegisteredLucideIcon(name)\n if (!Icon) return null\n return <Icon className={className} />\n}\n"],
|
|
5
|
+
"mappings": "AAkCS;AAhCT,SAAS,4BAA4B;AAErC,SAAS,uBAAuB,OAAuB;AACrD,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,gBAAgB,QAAQ,WAAW,SAAS,IAAI,QAAQ,MAAM,UAAU,MAAM,IAAI;AACxF,MAAI,CAAC,cAAe,QAAO;AAC3B,MAAI,CAAC,cAAc,SAAS,GAAG,KAAK,CAAC,cAAc,SAAS,GAAG,KAAK,CAAC,cAAc,SAAS,GAAG,KAAK,QAAQ,KAAK,aAAa,GAAG;AAC/H,WAAO,cACJ,QAAQ,sBAAsB,OAAO,EACrC,QAAQ,wBAAwB,OAAO,EACvC,YAAY;AAAA,EACjB;AACA,SAAO,cACJ,QAAQ,WAAW,GAAG,EACtB,QAAQ,OAAO,GAAG,EAClB,YAAY;AACjB;AAEO,SAAS,4BAA4B,MAA6C;AACvF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,aAAa,uBAAuB,IAAI;AAC9C,MAAI,CAAC,WAAY,QAAO;AACxB,SAAQ,qBAAgE,UAAU,KAAK;AACzF;AAEO,SAAS,gCACd,MACA,WACwB;AACxB,QAAM,OAAO,4BAA4B,IAAI;AAC7C,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,oBAAC,QAAK,WAAsB;AACrC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/jest.config.cjs
CHANGED
|
@@ -16,7 +16,8 @@ module.exports = {
|
|
|
16
16
|
'^remark-gfm$': '<rootDir>/jest.markdown-mock.tsx',
|
|
17
17
|
},
|
|
18
18
|
transform: {
|
|
19
|
-
|
|
19
|
+
// `.cjs` is included so the build-time source emitters under scripts/ are testable.
|
|
20
|
+
'^.+\\.(cjs|(t|j)sx?)$': [
|
|
20
21
|
'<rootDir>/../../scripts/jest-mikroorm-transformer.cjs',
|
|
21
22
|
{
|
|
22
23
|
tsconfig: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6788.1.c2a1520a30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,13 +155,13 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
158
|
+
"@open-mercato/shared": "0.6.7-develop.6788.1.c2a1520a30",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
164
|
+
"@open-mercato/shared": "0.6.7-develop.6788.1.c2a1520a30",
|
|
165
165
|
"@testing-library/dom": "^10.4.1",
|
|
166
166
|
"@testing-library/jest-dom": "^6.9.1",
|
|
167
167
|
"@testing-library/react": "^16.3.1",
|
|
@@ -174,6 +174,7 @@
|
|
|
174
174
|
"react-dom": "19.2.7",
|
|
175
175
|
"react-is": "^19.2.7",
|
|
176
176
|
"ts-jest": "^29.4.11",
|
|
177
|
+
"ts-morph": "^28.0.0",
|
|
177
178
|
"typescript": "7.0.2"
|
|
178
179
|
},
|
|
179
180
|
"publishConfig": {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Pure source emitter for src/backend/icons/lucideRegistry.generated.tsx.
|
|
2
|
+
// Kept side-effect free, and CommonJS so both build.mjs and the Jest suite can load it.
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
IndentationText,
|
|
6
|
+
Project,
|
|
7
|
+
QuoteKind,
|
|
8
|
+
ScriptKind,
|
|
9
|
+
StructureKind,
|
|
10
|
+
VariableDeclarationKind,
|
|
11
|
+
} = require('ts-morph')
|
|
12
|
+
|
|
13
|
+
const GENERATED_FILE_NAME = 'lucideRegistry.generated.tsx'
|
|
14
|
+
const LUCIDE_MODULE_SPECIFIER = 'lucide-react'
|
|
15
|
+
const BANNER = `/* eslint-disable */
|
|
16
|
+
// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.
|
|
17
|
+
// Generated from discovered icon string usages across the repo.
|
|
18
|
+
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
let sharedProject = null
|
|
22
|
+
|
|
23
|
+
function getProject() {
|
|
24
|
+
if (!sharedProject) {
|
|
25
|
+
sharedProject = new Project({
|
|
26
|
+
useInMemoryFileSystem: true,
|
|
27
|
+
manipulationSettings: {
|
|
28
|
+
indentationText: IndentationText.TwoSpaces,
|
|
29
|
+
quoteKind: QuoteKind.Single,
|
|
30
|
+
useTrailingCommas: true,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return sharedProject
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sortResolvedIcons(resolvedIcons) {
|
|
39
|
+
return [...resolvedIcons].sort((a, b) => a.kebab.localeCompare(b.kebab))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectUniqueExports(resolvedIcons) {
|
|
43
|
+
return [...new Set(resolvedIcons.map((icon) => icon.exportName))].sort((a, b) => a.localeCompare(b))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeNamedImports(exportNames) {
|
|
47
|
+
return (writer) => {
|
|
48
|
+
writer.newLine()
|
|
49
|
+
for (const name of exportNames) {
|
|
50
|
+
writer.write(`${name},`)
|
|
51
|
+
writer.newLine()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeRegistryLiteral(entries) {
|
|
57
|
+
return (writer) => {
|
|
58
|
+
if (entries.length === 0) {
|
|
59
|
+
writer.write('{}')
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
writer.write('{')
|
|
64
|
+
writer.newLine()
|
|
65
|
+
for (const icon of entries) {
|
|
66
|
+
writer.write(`'${icon.kebab}': ${icon.exportName},`)
|
|
67
|
+
writer.newLine()
|
|
68
|
+
}
|
|
69
|
+
writer.write('}')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertNoSyntacticDiagnostics(project, sourceFile) {
|
|
74
|
+
const diagnostics = project.getProgram().getSyntacticDiagnostics(sourceFile)
|
|
75
|
+
if (diagnostics.length === 0) return
|
|
76
|
+
|
|
77
|
+
const details = diagnostics
|
|
78
|
+
.map((diagnostic) => ` line ${diagnostic.getLineNumber() ?? 0}: ${diagnostic.getMessageText()}`)
|
|
79
|
+
.join('\n')
|
|
80
|
+
throw new Error(`[internal] Generated ${GENERATED_FILE_NAME} is not syntactically valid:\n${details}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeSourceText(text) {
|
|
84
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
85
|
+
return normalized.endsWith('\n') ? normalized : `${normalized}\n`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildLucideRegistrySource(resolvedIcons) {
|
|
89
|
+
const entries = sortResolvedIcons(resolvedIcons)
|
|
90
|
+
const uniqueExports = collectUniqueExports(entries)
|
|
91
|
+
|
|
92
|
+
const project = getProject()
|
|
93
|
+
const sourceFile = project.createSourceFile(GENERATED_FILE_NAME, '', {
|
|
94
|
+
overwrite: true,
|
|
95
|
+
scriptKind: ScriptKind.TSX,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
sourceFile.addImportDeclaration({
|
|
99
|
+
kind: StructureKind.ImportDeclaration,
|
|
100
|
+
isTypeOnly: true,
|
|
101
|
+
namedImports: [{ name: 'LucideIcon' }],
|
|
102
|
+
moduleSpecifier: LUCIDE_MODULE_SPECIFIER,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (uniqueExports.length > 0) {
|
|
106
|
+
sourceFile.addImportDeclaration({
|
|
107
|
+
kind: StructureKind.ImportDeclaration,
|
|
108
|
+
namedImports: writeNamedImports(uniqueExports),
|
|
109
|
+
moduleSpecifier: LUCIDE_MODULE_SPECIFIER,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sourceFile.addVariableStatement({
|
|
114
|
+
kind: StructureKind.VariableStatement,
|
|
115
|
+
isExported: true,
|
|
116
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
117
|
+
declarations: [
|
|
118
|
+
{
|
|
119
|
+
name: 'LUCIDE_ICON_REGISTRY',
|
|
120
|
+
type: 'Record<string, LucideIcon>',
|
|
121
|
+
initializer: writeRegistryLiteral(entries),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
sourceFile.formatText({ indentSize: 2, convertTabsToSpaces: true })
|
|
127
|
+
sourceFile.insertText(0, BANNER)
|
|
128
|
+
assertNoSyntacticDiagnostics(project, sourceFile)
|
|
129
|
+
|
|
130
|
+
return normalizeSourceText(sourceFile.getFullText())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { buildLucideRegistrySource }
|
|
@@ -136,6 +136,21 @@ export function ConfirmDialog({
|
|
|
136
136
|
return () => dialog.removeEventListener("cancel", handleCancel);
|
|
137
137
|
}, [loading, requestClose]);
|
|
138
138
|
|
|
139
|
+
React.useEffect(() => {
|
|
140
|
+
if (!open) return;
|
|
141
|
+
|
|
142
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
143
|
+
if (event.key !== "Escape") return;
|
|
144
|
+
if (!(event.target instanceof Node) || !dialogRef.current?.contains(event.target)) return;
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
if (!loading) requestClose();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
window.addEventListener("keydown", handleEscape, true);
|
|
151
|
+
return () => window.removeEventListener("keydown", handleEscape, true);
|
|
152
|
+
}, [loading, open, requestClose]);
|
|
153
|
+
|
|
139
154
|
// Handle backdrop click
|
|
140
155
|
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
|
141
156
|
// Only close if clicking directly on the dialog (backdrop), not its children
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
import * as React from 'react'
|
|
5
|
-
import { screen } from '@testing-library/react'
|
|
5
|
+
import { fireEvent, screen, within } from '@testing-library/react'
|
|
6
6
|
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
7
7
|
import { ConfirmDialog } from '../ConfirmDialog'
|
|
8
8
|
|
|
@@ -60,4 +60,36 @@ describe('ConfirmDialog', () => {
|
|
|
60
60
|
expect(container.contains(dialog)).toBe(false)
|
|
61
61
|
expect(dialog.parentElement).toBe(document.body)
|
|
62
62
|
})
|
|
63
|
+
|
|
64
|
+
it('closes only the confirmation that owns an Escape event', () => {
|
|
65
|
+
const firstOpenChange = jest.fn()
|
|
66
|
+
const secondOpenChange = jest.fn()
|
|
67
|
+
|
|
68
|
+
renderWithProviders(
|
|
69
|
+
<>
|
|
70
|
+
<ConfirmDialog
|
|
71
|
+
open
|
|
72
|
+
onOpenChange={firstOpenChange}
|
|
73
|
+
onConfirm={() => undefined}
|
|
74
|
+
title="First confirmation"
|
|
75
|
+
confirmText="Confirm first"
|
|
76
|
+
cancelText="Cancel first"
|
|
77
|
+
/>
|
|
78
|
+
<ConfirmDialog
|
|
79
|
+
open
|
|
80
|
+
onOpenChange={secondOpenChange}
|
|
81
|
+
onConfirm={() => undefined}
|
|
82
|
+
title="Second confirmation"
|
|
83
|
+
confirmText="Confirm second"
|
|
84
|
+
cancelText="Cancel second"
|
|
85
|
+
/>
|
|
86
|
+
</>,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const secondDialog = screen.getByRole('alertdialog', { name: 'Second confirmation' })
|
|
90
|
+
fireEvent.keyDown(within(secondDialog).getByRole('button', { name: 'Cancel second' }), { key: 'Escape' })
|
|
91
|
+
|
|
92
|
+
expect(firstOpenChange).not.toHaveBeenCalled()
|
|
93
|
+
expect(secondOpenChange).toHaveBeenCalledWith(false)
|
|
94
|
+
})
|
|
63
95
|
})
|
|
@@ -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
|
+
}
|