Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/5631-ui-icon-icon-key-migration.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
---
'@object-ui/types': minor
'@object-ui/components': minor
---

**BREAKING (authoring): `ui:icon` names its glyph with `icon`, not `name`**

`{ "type": "icon", "name": "check" }` no longer renders an icon. Write
`{ "type": "icon", "icon": "check" }`. Stored metadata authored before this
release needs converting — see the migration below.

Marked `minor` per AGENTS.md §版本号策略 (this repo never publishes `major`
outside an `@objectstack` major sync); the break is real and is stated here.

**Why**

`name` is the SDUI identity key every authored node carries, alongside `id` —
it is not `ui:icon`'s private prop. So an ordinary node like
`{ type: 'icon', id: 'save_icon', name: 'save_icon' }` asked lucide for a glyph
called `SaveIcon`, missed, and rendered **nothing at all**: silent to a human,
and clean to a DOM gate, because a renderer that renders nothing spreads no
attributes to find. `action:*` already reads `icon`, so this is the vocabulary's
existing answer, and it leaves no node type on which the identity key is
unusable.

**What changed**

- `IconSchema` (types + its zod mirror) declares `icon: string` **required**,
exactly as `name` was required before it — a key rename at constant
strictness. `name` reverts to the optional identity inherited from
`BaseSchema`. The mirror previously *required* `name`, which is why the
renderer could not be migrated on its own: the published contract refused the
correct shape.
- `ui:icon` resolves its glyph from `schema.icon`. There is deliberately **no**
`icon ?? name` fallback: a key meaning "identity" or "glyph" depending on
whether a lucide lookup happened to hit is the ambiguity being removed.
- The registry's `inputs` entry and `content/docs/components/basic/icon.mdx`
moved in the same change as the resolver.
- All 98 authored icon nodes in this repo are converted.

**The break is loud in three places, never silent**

1. `IconSchema` **refuses** a legacy node, with a message that names the rename
and points at the converter — not zod's default `expected string, received
undefined`.
2. A legacy node that reaches the renderer unvalidated draws the visible
placeholder shipped in the previous release, and its `console.warn` now
carries the exact rename (`icon: "save_icon"`) plus the converter's name.
Its accessible name says so too, and it gains a
`data-objectui-icon-legacy-name-key` marker so a gate can tell
"unmigrated node" from "glyph that does not resolve".
3. **Migration for stored metadata** — `migrateIconNodeKeys` from
`@object-ui/types`:

```ts
import { migrateIconNodeKeys } from '@object-ui/types';

const { document, converted, warnings } = migrateIconNodeKeys(storedPage);
if (warnings.length) console.warn(warnings.map((w) => w.message).join('\n'));
if (document !== storedPage) await save(document);
```

It walks the whole document and lifts `name` to `icon` on every icon node.
It is a one-shot conversion a deployer runs over stored documents — **not** a
read-path fallback; nothing calls it during rendering or parsing. It
**reports rather than guesses** for the two cases it will not touch: a node
already declaring both keys (`icon` wins, `name` stays the identity it is),
and a node naming no glyph at all.
50 changes: 25 additions & 25 deletions apps/site/app/playground/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -271,7 +271,7 @@ const EXAMPLE_SCHEMAS = {
},
{
type: "icon",
name: "DollarSign",
icon: "DollarSign",
className: "h-4 w-4 text-muted-foreground"
}
]
Expand DownExpand Up@@ -314,7 +314,7 @@ const EXAMPLE_SCHEMAS = {
},
{
type: "icon",
name: "Users",
icon: "Users",
className: "h-4 w-4 text-muted-foreground"
}
]
Expand DownExpand Up@@ -357,7 +357,7 @@ const EXAMPLE_SCHEMAS = {
},
{
type: "icon",
name: "CreditCard",
icon: "CreditCard",
className: "h-4 w-4 text-muted-foreground"
}
]
Expand DownExpand Up@@ -400,7 +400,7 @@ const EXAMPLE_SCHEMAS = {
},
{
type: "icon",
name: "Activity",
icon: "Activity",
className: "h-4 w-4 text-muted-foreground"
}
]
Expand DownExpand Up@@ -583,7 +583,7 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Smartphone",
icon: "Smartphone",
className: "h-5 w-5 text-blue-600 dark:text-blue-400"
}
]
Expand DownExpand Up@@ -626,7 +626,7 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Laptop",
icon: "Laptop",
className: "h-5 w-5 text-purple-600 dark:text-purple-400"
}
]
Expand DownExpand Up@@ -669,7 +669,7 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Headphones",
icon: "Headphones",
className: "h-5 w-5 text-green-600 dark:text-green-400"
}
]
Expand DownExpand Up@@ -878,7 +878,7 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Smartphone",
icon: "Smartphone",
className: "h-20 w-20 text-blue-600 dark:text-blue-400"
}
]
Expand DownExpand Up@@ -935,27 +935,27 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 text-gray-300"
},
{
Expand DownExpand Up@@ -985,7 +985,7 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Laptop",
icon: "Laptop",
className: "h-20 w-20 text-purple-600 dark:text-purple-400"
}
]
Expand DownExpand Up@@ -1037,27 +1037,27 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
Expand DownExpand Up@@ -1087,7 +1087,7 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Headphones",
icon: "Headphones",
className: "h-20 w-20 text-green-600 dark:text-green-400"
}
]
Expand DownExpand Up@@ -1139,27 +1139,27 @@ const EXAMPLE_SCHEMAS = {
children: [
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
type: "icon",
name: "Star",
icon: "Star",
className: "h-4 w-4 fill-yellow-400 text-yellow-400"
},
{
Expand Down
2 changes: 1 addition & 1 deletion content/docs/blocks/block-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -280,7 +280,7 @@ const cardBlock: BlockSchema = {
children: [
{
type: 'icon',
name: '${icon}',
icon: '${icon}',
size: 48,
className: 'text-primary'
}
Expand Down
46 changes: 45 additions & 1 deletion content/docs/components/basic/icon.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,9 +16,53 @@ description: "Display icons from the Lucide icon library"
```plaintext
interface IconSchema {
type: 'icon';
name: string; // Lucide icon name (kebab-case)
icon: string; // Lucide glyph name (kebab-case), REQUIRED
size?: number | 'sm' | 'md' | 'lg' | 'xl';
color?: string; // Tailwind color class
className?: string;
}
```

The glyph is named by `icon`:

```json
{ "type": "icon", "icon": "check" }
```

## `name` is identity, not the glyph

`name` is the SDUI identity key every node carries, alongside `id`. It is **not**
read as a glyph name on `ui:icon`, and writing one there renders no icon:

```json
{ "type": "icon", "id": "save_icon", "name": "save_icon" }
```

That node is **refused** by `IconSchema` — with a message naming this rename —
and, if it reaches the renderer unvalidated, draws a dashed-square placeholder
with an accessible name saying which icon failed, rather than rendering nothing.
There is deliberately no fallback from `icon` to `name`: a key that means
"identity" sometimes and "glyph" other times, depending on whether a lucide
lookup happened to hit, is the ambiguity this contract removes.

### Migrating stored metadata

Metadata authored before this change names its glyph with `name`. Convert it in
bulk — once, at the source — with the converter shipped for it:

```ts
import { migrateIconNodeKeys } from '@object-ui/types';

declare const storedPage: unknown;
declare function save(document: unknown): Promise<void>;

const { document, converted, warnings } = migrateIconNodeKeys(storedPage);
console.log(`converted ${converted} icon node(s)`);
if (warnings.length) console.warn(warnings.map((w) => w.message).join('\n'));
if (document !== storedPage) await save(document);
```

It walks the whole document, lifts `name` to `icon` on every icon node, and
**reports rather than guesses** for the two cases it will not touch: a node that
already declares both keys (`icon` wins; `name` stays the identity it is), and a
node that names no glyph at all.
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@
"children": [
{
"type": "icon",
"name": "box",
"icon": "box",
"className": "h-6 w-6 text-primary"
},
{
Expand Down
12 changes: 6 additions & 6 deletions examples/schema-catalog/src/schemas/app/sidebar-navigation.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@
"children": [
{
"type": "icon",
"name": "layout-dashboard",
"icon": "layout-dashboard",
"className": "h-4 w-4"
},
{
Expand All@@ -32,7 +32,7 @@
"children": [
{
"type": "icon",
"name": "users",
"icon": "users",
"className": "h-4 w-4 text-muted-foreground"
},
{
Expand All@@ -56,7 +56,7 @@
"children": [
{
"type": "icon",
"name": "target",
"icon": "target",
"className": "h-4 w-4 text-muted-foreground"
},
{
Expand All@@ -83,7 +83,7 @@
"children": [
{
"type": "icon",
"name": "megaphone",
"icon": "megaphone",
"className": "h-4 w-4 text-muted-foreground"
},
{
Expand All@@ -101,7 +101,7 @@
"children": [
{
"type": "icon",
"name": "mail",
"icon": "mail",
"className": "h-4 w-4 text-muted-foreground"
},
{
Expand All@@ -123,7 +123,7 @@
"children": [
{
"type": "icon",
"name": "settings",
"icon": "settings",
"className": "h-4 w-4 text-muted-foreground"
},
{
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@
"children": [
{
"type": "icon",
"name": "layout-template",
"icon": "layout-template",
"className": "h-12 w-12 text-primary/60"
}
]
Expand Down
Loading
Loading