Align JSON UI registry with canonical components

This commit is contained in:
2026-01-18 01:53:48 +00:00
parent 7cd15ca7ba
commit 8864436425
3 changed files with 203 additions and 72 deletions

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const registryPath = path.join(process.cwd(), 'json-components-registry.json')
const schemaPath = path.join(process.cwd(), 'src', 'schemas', 'registry-validation.json')
if (!fs.existsSync(registryPath)) {
console.error('❌ Could not find json-components-registry.json')
process.exit(1)
}
if (!fs.existsSync(schemaPath)) {
console.error('❌ Could not find src/schemas/registry-validation.json')
process.exit(1)
}
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'))
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
const primitiveTypes = new Set([
'div',
'span',
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'section',
'article',
'header',
'footer',
'main',
'aside',
'nav',
])
const registryTypes = new Set()
for (const entry of registry.components || []) {
if (entry.source === 'atoms' || entry.source === 'molecules') {
const name = entry.export || entry.name || entry.type
if (name) {
registryTypes.add(name)
}
}
}
const schemaTypes = new Set()
const collectTypes = (components) => {
if (!components) return
if (Array.isArray(components)) {
components.forEach(collectTypes)
return
}
if (components.type) {
schemaTypes.add(components.type)
}
if (components.children) {
collectTypes(components.children)
}
}
collectTypes(schema.components || [])
const missing = []
for (const type of schemaTypes) {
if (!primitiveTypes.has(type) && !registryTypes.has(type)) {
missing.push(type)
}
}
if (missing.length) {
console.error(`❌ Missing registry entries for: ${missing.join(', ')}`)
process.exit(1)
}
console.log('✅ JSON UI registry validation passed for primitives and atom/molecule components.')

View File

@@ -17,40 +17,9 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Skeleton as ShadcnSkeleton } from '@/components/ui/skeleton'
import { Progress } from '@/components/ui/progress'
import { Avatar as ShadcnAvatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Heading } from '@/components/atoms/Heading'
import { Text } from '@/components/atoms/Text'
import { TextArea } from '@/components/atoms/TextArea'
import { List as ListComponent } from '@/components/atoms/List'
import { Grid } from '@/components/atoms/Grid'
import { Stack } from '@/components/atoms/Stack'
import { Flex } from '@/components/atoms/Flex'
import { Container } from '@/components/atoms/Container'
import { Link } from '@/components/atoms/Link'
import { Image } from '@/components/atoms/Image'
import { Avatar as AtomAvatar } from '@/components/atoms/Avatar'
import { Code } from '@/components/atoms/Code'
import { Tag } from '@/components/atoms/Tag'
import { Spinner } from '@/components/atoms/Spinner'
import { Skeleton as AtomSkeleton } from '@/components/atoms/Skeleton'
import { Slider } from '@/components/atoms/Slider'
import { NumberInput } from '@/components/atoms/NumberInput'
import { Radio } from '@/components/atoms/Radio'
import { Alert as AtomAlert } from '@/components/atoms/Alert'
import { InfoBox } from '@/components/atoms/InfoBox'
import { EmptyState } from '@/components/atoms/EmptyState'
import { Table as AtomTable } from '@/components/atoms/Table'
import { KeyValue } from '@/components/atoms/KeyValue'
import { StatCard } from '@/components/atoms/StatCard'
import { StatusBadge } from '@/components/atoms/StatusBadge'
import { DataCard } from '@/components/molecules/DataCard'
import { SearchInput } from '@/components/molecules/SearchInput'
import { ActionBar } from '@/components/molecules/ActionBar'
import { AppBranding } from '@/components/molecules/AppBranding'
import { LabelWithBadge } from '@/components/molecules/LabelWithBadge'
import { EmptyEditorState } from '@/components/molecules/EmptyEditorState'
import { LoadingFallback } from '@/components/molecules/LoadingFallback'
import { LoadingState } from '@/components/molecules/LoadingState'
import { NavigationGroupHeader } from '@/components/molecules/NavigationGroupHeader'
import * as AtomComponents from '@/components/atoms'
import * as MoleculeComponents from '@/components/molecules'
import jsonComponentsRegistry from '../../../json-components-registry.json'
import {
ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass,
Funnel, Download, Upload, PencilSimple, Trash, Eye, EyeClosed,
@@ -64,6 +33,42 @@ export interface UIComponentRegistry {
[key: string]: ComponentType<any>
}
interface JsonRegistryEntry {
name?: string
type?: string
export?: string
source?: string
}
interface JsonComponentRegistry {
components?: JsonRegistryEntry[]
}
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
const buildRegistryFromNames = (
names: string[],
components: Record<string, ComponentType<any>>
): UIComponentRegistry => {
return names.reduce<UIComponentRegistry>((registry, name) => {
const component = components[name]
if (component) {
registry[name] = component
}
return registry
}, {})
}
const jsonRegistryEntries = jsonRegistry.components ?? []
const atomRegistryNames = jsonRegistryEntries
.filter((entry) => entry.source === 'atoms')
.map((entry) => entry.export ?? entry.name ?? entry.type)
.filter((name): name is string => Boolean(name))
const moleculeRegistryNames = jsonRegistryEntries
.filter((entry) => entry.source === 'molecules')
.map((entry) => entry.export ?? entry.name ?? entry.type)
.filter((name): name is string => Boolean(name))
export const primitiveComponents: UIComponentRegistry = {
div: 'div' as any,
span: 'span' as any,
@@ -131,45 +136,15 @@ export const shadcnComponents: UIComponentRegistry = {
AvatarImage,
}
export const atomComponents: UIComponentRegistry = {
Heading,
Text,
TextArea,
List: ListComponent,
Grid,
Stack,
Flex,
Container,
Link,
Image,
Avatar: AtomAvatar,
Code,
Tag,
Spinner,
Skeleton: AtomSkeleton,
Slider,
NumberInput,
Radio,
Alert: AtomAlert,
InfoBox,
EmptyState,
Table: AtomTable,
KeyValue,
StatCard,
StatusBadge,
}
export const atomComponents: UIComponentRegistry = buildRegistryFromNames(
atomRegistryNames,
AtomComponents as Record<string, ComponentType<any>>
)
export const moleculeComponents: UIComponentRegistry = {
DataCard,
SearchInput,
ActionBar,
AppBranding,
LabelWithBadge,
EmptyEditorState,
LoadingFallback,
LoadingState,
NavigationGroupHeader,
}
export const moleculeComponents: UIComponentRegistry = buildRegistryFromNames(
moleculeRegistryNames,
MoleculeComponents as Record<string, ComponentType<any>>
)
export const iconComponents: UIComponentRegistry = {
ArrowLeft,

View File

@@ -0,0 +1,74 @@
{
"id": "registry-validation",
"name": "Registry Validation",
"layout": {
"type": "single"
},
"dataSources": [],
"components": [
{
"id": "root",
"type": "div",
"props": {
"className": "p-6 space-y-6"
},
"children": [
{
"id": "branding",
"type": "AppBranding",
"props": {
"title": "Registry Validation",
"subtitle": "Atoms, molecules, and primitives"
}
},
{
"id": "stack",
"type": "Stack",
"props": {
"direction": "horizontal",
"spacing": "lg",
"className": "items-stretch"
},
"children": [
{
"id": "stat-card",
"type": "StatCard",
"props": {
"title": "Active Users",
"value": "128",
"description": "Past 24 hours"
}
},
{
"id": "flex",
"type": "Flex",
"props": {
"direction": "col",
"gap": "sm",
"className": "rounded-md border p-4"
},
"children": [
{
"id": "flex-title",
"type": "Heading",
"props": {
"level": 4,
"children": "Flex Container"
}
},
{
"id": "flex-text",
"type": "Text",
"props": {
"children": "Ensures primitives and atom components resolve."
}
}
]
}
]
}
]
}
],
"globalActions": []
}