Summary
Add provider-neutral UI component metadata contracts to @ankhorage/contracts so core ZORA and standalone ZORA extension packages can expose component metadata through the same stable contract.
Today, @ankhorage/zora owns ZoraComponentMeta, ZoraComponentMetaRegistry, prop schemas, slot metadata, event metadata, and related types internally. That works for core ZORA, but extension packages such as @ankhorage/zora-chessboard need to export metadata with the same shape without depending on ZORA-specific type ownership forever.
Motivation
Standalone ZORA extension packages should be discoverable by tools in the same way as core ZORA components.
Examples:
@ankhorage/zora
@ankhorage/zora-chessboard
@ankhorage/zora-charts
@ankhorage/zora-calendar
@ankhorage/zora-map
Each package should be able to export a metadata registry that satisfies a shared contract.
Current state
@ankhorage/contracts currently contains component event DTO contracts, such as:
ComponentEventDto
ComponentEventDtoKind
KnownComponentEventDto
But it does not currently define the broader UI component metadata contract used by ZORA.
@ankhorage/zora currently owns:
ZoraComponentMeta
ZoraComponentMetaRegistry
ZoraComponentPropSchema
ZoraComponentEventMeta
ZoraComponentSlotMeta
Proposed direction
Move or duplicate-neutralize the generic metadata shape into @ankhorage/contracts with provider-neutral names.
Suggested names:
UiComponentCategory
UiComponentPropType
UiComponentPropValue
UiComponentPropSchema
UiComponentPropArrayItemSchema
UiComponentBlueprint
UiComponentI18nMeta
UiComponentEventPayloadKind
UiComponentEventPayloadFieldType
UiComponentEventPayloadFieldMeta
UiComponentEventMeta
UiComponentSlotMeta
UiComponentMeta
UiComponentMetaRegistry
UiComponentPackageManifest
Exact names can be refined during implementation.
Suggested contracts
The initial shape should closely match current ZORA metadata, but without ZORA-specific naming.
export type UiComponentCategory = 'foundation' | 'component' | 'pattern' | 'layout';
export type UiComponentPropType =
| 'string'
| 'number'
| 'boolean'
| 'enum'
| 'color'
| 'spacing'
| 'radius'
| 'shadow'
| 'typographySize'
| 'typographyWeight'
| 'action'
| 'imageAsset'
| 'array';
export type UiComponentPropValue =
| string
| number
| boolean
| null
| readonly UiComponentPropValue[]
| { readonly [key: string]: UiComponentPropValue };
export interface UiComponentPropArrayItemSchema {
key: string;
schema: UiComponentPropSchema;
}
export interface UiComponentPropSchema {
type: UiComponentPropType;
category: string;
label?: string;
enum?: readonly (string | number)[];
default?: UiComponentPropValue;
itemSchema?: readonly UiComponentPropArrayItemSchema[];
}
export interface UiComponentBlueprint {
label: string;
icon?: {
name: string;
provider?: string;
};
defaultProps?: Readonly<Record<string, UiComponentPropValue>>;
}
export interface UiComponentI18nMeta {
fields: readonly {
keyProp: string;
defaultTextProp: string;
}[];
}
export type UiComponentEventPayloadKind = ComponentEventDtoKind | (string & {});
export type UiComponentEventPayloadFieldType =
| 'boolean'
| 'number'
| 'object'
| 'record'
| 'string'
| 'unknown';
export interface UiComponentEventPayloadFieldMeta {
readonly path: string;
readonly type: UiComponentEventPayloadFieldType;
readonly label?: string;
readonly description?: string;
}
export interface UiComponentEventMeta {
readonly label: string;
readonly eventType: UiComponentEventPayloadKind;
readonly description?: string;
readonly payloadFields?: readonly UiComponentEventPayloadFieldMeta[];
}
export interface UiComponentSlotMeta {
label?: string;
allowedChildren?: readonly string[];
}
export interface UiComponentMeta {
name: string;
category: UiComponentCategory;
description?: string;
directManifestNode: boolean;
allowedChildren: readonly string[];
blueprint?: UiComponentBlueprint;
events?: Readonly<Record<string, UiComponentEventMeta>>;
i18n?: UiComponentI18nMeta;
slots?: Readonly<Record<string, UiComponentSlotMeta>>;
note?: string;
props: Readonly<Record<string, UiComponentPropSchema>>;
}
export type UiComponentMetaRegistry = Readonly<Record<string, UiComponentMeta>>;
Consider whether an extension/package-level manifest is useful:
export interface UiComponentPackageManifest {
packageName: string;
displayName?: string;
components: UiComponentMetaRegistry;
}
Migration strategy
Recommended staged approach:
- Add the neutral types to
@ankhorage/contracts without changing ZORA yet.
- Update
@ankhorage/zora to re-export or alias its current ZoraComponent* types from the new contract types.
- Keep existing ZORA public type names for compatibility.
- Update extension packages such as
@ankhorage/zora-chessboard to type metadata against @ankhorage/contracts once available.
Example compatibility aliases in ZORA later:
export type ZoraComponentMeta = UiComponentMeta;
export type ZoraComponentMetaRegistry = UiComponentMetaRegistry;
Acceptance criteria
@ankhorage/contracts exports provider-neutral UI component metadata types.
- Types are independent from React, React Native, ZORA, Surface, and runtime implementations.
- Types reuse existing
ComponentEventDtoKind where appropriate.
- Existing
@ankhorage/contracts build/lint/test checks pass.
- Tests cover at least one valid
UiComponentMeta object with events, props, and slots.
- A changeset is included.
Out of scope
- Updating
@ankhorage/zora in the same PR.
- Updating
@ankhorage/zora-chessboard in the same PR.
- Runtime registry changes.
- Manifest rendering behavior changes.
- Studio UI changes.
Verification
Run before merging:
bun install
bun run knip
bun run lint
bun run build
bun run test
Implementation constraints
- Keep TypeScript strict.
- Do not use
any, as any, broad casts, @ts-ignore, @ts-expect-error, or eslint disables.
- Do not weaken tsconfig strictness.
- Keep contracts standalone and independent from UI/runtime packages.
Summary
Add provider-neutral UI component metadata contracts to
@ankhorage/contractsso core ZORA and standalone ZORA extension packages can expose component metadata through the same stable contract.Today,
@ankhorage/zoraownsZoraComponentMeta,ZoraComponentMetaRegistry, prop schemas, slot metadata, event metadata, and related types internally. That works for core ZORA, but extension packages such as@ankhorage/zora-chessboardneed to export metadata with the same shape without depending on ZORA-specific type ownership forever.Motivation
Standalone ZORA extension packages should be discoverable by tools in the same way as core ZORA components.
Examples:
Each package should be able to export a metadata registry that satisfies a shared contract.
Current state
@ankhorage/contractscurrently contains component event DTO contracts, such as:But it does not currently define the broader UI component metadata contract used by ZORA.
@ankhorage/zoracurrently owns:Proposed direction
Move or duplicate-neutralize the generic metadata shape into
@ankhorage/contractswith provider-neutral names.Suggested names:
Exact names can be refined during implementation.
Suggested contracts
The initial shape should closely match current ZORA metadata, but without ZORA-specific naming.
Consider whether an extension/package-level manifest is useful:
Migration strategy
Recommended staged approach:
@ankhorage/contractswithout changing ZORA yet.@ankhorage/zorato re-export or alias its currentZoraComponent*types from the new contract types.@ankhorage/zora-chessboardto type metadata against@ankhorage/contractsonce available.Example compatibility aliases in ZORA later:
Acceptance criteria
@ankhorage/contractsexports provider-neutral UI component metadata types.ComponentEventDtoKindwhere appropriate.@ankhorage/contractsbuild/lint/test checks pass.UiComponentMetaobject with events, props, and slots.Out of scope
@ankhorage/zorain the same PR.@ankhorage/zora-chessboardin the same PR.Verification
Run before merging:
bun install bun run knip bun run lint bun run build bun run testImplementation constraints
any,as any, broad casts,@ts-ignore,@ts-expect-error, or eslint disables.