feat(core): support nested component parts

This commit is contained in:
Rahim
2026-06-24 13:07:47 -07:00
parent ddccbd89d4
commit 69d5afecdd
10 changed files with 132 additions and 20 deletions
+17 -2
View File
@@ -9,7 +9,11 @@ export interface ComponentPart<Props extends object = EmptyProps> {
readonly [__PROPS_BRAND__]?: Props;
}
export type ComponentPartRecord = Record<string, ComponentPart<any>>;
export interface ComponentPartGroup<Parts extends ComponentPartRecord = ComponentPartRecord> {
parts: Parts;
}
export type ComponentPartRecord = Record<string, ComponentPart<any> | ComponentPartGroup>;
export interface ComponentManifest<Props extends object = EmptyProps> {
name: string;
@@ -36,7 +40,12 @@ interface DefineComponentFactory<Props extends object> {
export type InferProps<T> =
T extends ComponentManifest<infer Props> ? Props : T extends ComponentPart<infer Props> ? Props : never;
export type InferParts<T> = T extends ComponentGroupManifest<infer Parts> ? keyof Parts : never;
export type InferParts<T> =
T extends ComponentGroupManifest<infer Parts>
? keyof Parts
: T extends ComponentPartGroup<infer Parts>
? keyof Parts
: never;
export type InferPartProps<T, K extends string> =
T extends ComponentGroupManifest<infer Parts> ? (K extends keyof Parts ? InferProps<Parts[K]> : never) : never;
@@ -45,6 +54,12 @@ export function defineComponentPart<Props extends object = EmptyProps>(): Compon
return {} as ComponentPart<Props>;
}
export function defineComponentPartGroup<const Parts extends ComponentPartRecord>(
parts: Parts
): ComponentPartGroup<Parts> {
return { parts };
}
/** Define a component manifest. */
export function defineComponent<Props extends object = EmptyProps>(): DefineComponentFactory<Props> {
return ((manifest: ComponentDefinition<Props> | ComponentGroupDefinition<ComponentPartRecord>) =>
@@ -1,4 +1,4 @@
import { defineComponent, defineComponentPart } from '../manifest';
import { defineComponent, defineComponentPart, defineComponentPartGroup } from '../manifest';
import type { SliderProps, SliderValueProps } from './props';
import { SliderDataAttrs } from './slider-data-attrs';
@@ -10,7 +10,10 @@ export default defineComponent()({
Fill: defineComponentPart(),
Buffer: defineComponentPart(),
Thumb: defineComponentPart(),
Thumbnail: defineComponentPart(),
Thumbnail: defineComponentPartGroup({
Root: defineComponentPart(),
Image: defineComponentPart(),
}),
Preview: defineComponentPart(),
Value: defineComponentPart<SliderValueProps>(),
},
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { createComponent } from '../../../jsx-runtime';
import { defineComponent, defineComponentPart, defineComponentPartGroup } from '../manifest';
describe('createComponent', () => {
it('creates nested component parts with dotted part paths', () => {
const Slider = createComponent(
defineComponent()({
name: 'Slider',
parts: {
Root: defineComponentPart(),
Thumbnail: defineComponentPartGroup({
Root: defineComponentPart(),
Image: defineComponentPart(),
}),
},
})
);
expect(Slider.Root.$$component).toEqual({ name: 'Slider', part: 'Root' });
expect(Slider.Thumbnail.Root.$$component).toEqual({ name: 'Slider', part: 'Thumbnail.Root' });
expect(Slider.Thumbnail.Image.$$component).toEqual({ name: 'Slider', part: 'Thumbnail.Image' });
});
});
+25 -14
View File
@@ -1,8 +1,9 @@
import type {
AnyComponentManifest,
ComponentGroupManifest,
InferPartProps,
InferParts,
ComponentPart,
ComponentPartGroup,
ComponentPartRecord,
InferProps,
} from './core/ui/manifest';
@@ -40,18 +41,20 @@ export interface Component<Props extends object> {
readonly $$component: { name: string; part: string | null };
}
type PartComponentProps<M, K extends string> = K extends 'Root'
? NonNullable<InferPartProps<M, K>>
: [NonNullable<InferPartProps<M, K>>] extends [never]
? EmptyProps
: NonNullable<InferPartProps<M, K>>;
type InferPartNodeProps<Node> = Node extends ComponentPart<infer Props> ? Props : never;
type CompoundComponent<M> = {
[K in InferParts<M> & string]: Component<PartComponentProps<M, K>>;
type PartComponentProps<Node> = [NonNullable<InferPartNodeProps<Node>>] extends [never]
? EmptyProps
: NonNullable<InferPartNodeProps<Node>>;
type CompoundComponent<Parts extends ComponentPartRecord> = {
[K in keyof Parts & string]: Parts[K] extends ComponentPartGroup<infer ChildParts>
? CompoundComponent<ChildParts>
: Component<PartComponentProps<Parts[K]>>;
};
export type CreateComponentResult<M> = M extends ComponentGroupManifest
? CompoundComponent<M>
? CompoundComponent<M['parts']>
: Component<InferProps<M>>;
function createRuntimeComponentPart<Props extends object>(name: string, part: string | null): Component<Props> {
@@ -71,13 +74,21 @@ export function createComponent<M extends AnyComponentManifest>(manifest: M): Cr
return createRuntimeComponentPart(manifest.name, null) as CreateComponentResult<M>;
}
const compound: Record<string, Component<never>> = {};
return createComponentParts(manifest.name, manifest.parts) as CreateComponentResult<M>;
}
for (const part of Object.keys(manifest.parts)) {
compound[part] = createRuntimeComponentPart(manifest.name, part);
function createComponentParts(name: string, parts: ComponentPartRecord, prefix = ''): Record<string, unknown> {
const compound: Record<string, unknown> = {};
for (const part of Object.keys(parts)) {
const path = prefix ? `${prefix}.${part}` : part;
const value = parts[part]!;
compound[part] =
'parts' in value ? createComponentParts(name, value.parts, path) : createRuntimeComponentPart(name, path);
}
return compound as CreateComponentResult<M>;
return compound;
}
function createNode(type: ComponentType, props: Record<string, unknown>, key?: string | number | null): ComponentNode {