DevelopersSDKdefineBlock()

defineBlock()

defineBlock() registers a new block type. Each block has a stable type identifier, a human-readable name, a Zod schema for its props, default values, and a React component that renders it. The runtime validates the schema at publish time and at every PhiCo composition.

import { defineBlock } from '@phimajor-solutions/pwfabric-sdk'
import { z } from 'zod'
 
export const PricingCard = defineBlock({
  type: 'pricing-card',
  name: 'Pricing Card',
  category: 'content',
  icon: '💳',
  schema: z.object({
    title: z.string(),
    price: z.number(),
    currency: z.string().default('USD'),
    features: z.array(z.string()),
    highlighted: z.boolean().default(false),
  }),
  defaultProps: {
    title: 'Basic',
    price: 9.99,
    currency: 'USD',
    features: ['Feature 1', 'Feature 2'],
    highlighted: false,
  },
  component: ({ props }) => (
    <div className={props.highlighted ? 'border-2 border-primary' : 'border'}>
      <h3>{props.title}</h3>
      <p>{props.currency} {props.price}</p>
      <ul>{props.features.map((f, i) => <li key={i}>{f}</li>)}</ul>
    </div>
  ),
})

Signature

function defineBlock<TProps extends Record<string, unknown>>(
  options: CustomBlockOptions<TProps>
): CustomBlockDefinition<TProps>

Options

Core

Only type is hard-required. Per ADR-194 the runtime never throws on a malformed block — the sole exception is an omitted type. Leave any other field out and the runtime degrades gracefully with a console.warn: a missing name is coerced to the type; a missing schema, defaultProps, or component falls back to a safe default. Provide all five below for a complete, predictable block.

FieldTypeDescription
typestring (kebab-case)Stable identifier — pricing-card, customer-quote, cta-banner. Must be 2–50 characters, kebab-case, must not collide with built-in block types.
namestringHuman-readable label shown in PhiCo block picker.
schemaZodSchema<TProps>Zod schema validating every instance of the block’s props.
defaultPropsTPropsInitial values used when PhiCo inserts the block. Must satisfy schema.
component({ props, blockId, children, context }) => ReactNodeReact component that renders the block. blockId is the instance id; context carries theme / viewport / event hooks.

Optional

FieldTypeDefaultDescription
category'layout' | 'content' | 'media' | 'form' | 'navigation' | 'data' | 'control' | 'panel' | 'text''content'Sidebar grouping in PhiCo picker.
iconstring'◆'Glyph or emoji shown in the picker.
canHaveChildrenbooleanfalseIf true, the block accepts a children array of nested blocks.
allowedChildrenstring[]unrestrictedWhen canHaveChildren is true, restrict which child block types are allowed.
allowedParentsstring[]unrestrictedRestrict which parent block types may contain this block.
maxInstancesnumberunlimitedPer-Surface cap on instances of this block.

Schema and defaults

The schema and the defaultProps are validated against each other at defineBlock() call time. Defaults that fail the schema don’t abort the build — per ADR-194 the runtime degrades rather than throws: it logs a console.warn listing every Zod issue and keeps the block usable. The only condition that hard-fails is an omitted type.

defineBlock({
  type: 'testimonial',
  name: 'Customer testimonial',
  schema: z.object({
    quote: z.string().min(10).max(500),
    author: z.string(),
    role: z.string().optional(),
  }),
  defaultProps: {
    quote: 'A great product to work with.',
    author: 'Jane Doe',
    role: 'Product Manager',
  },
  component: ({ props }) => (
    <blockquote>
      <p>"{props.quote}"</p>
      <footer>{props.author}{props.role ? `, ${props.role}` : ''}</footer>
    </blockquote>
  ),
})

Nesting and composition

When canHaveChildren is true, your component receives the rendered children as the second prop:

defineBlock({
  type: 'feature-grid',
  name: 'Feature Grid',
  category: 'layout',
  canHaveChildren: true,
  allowedChildren: ['feature-card', 'text', 'heading'],
  schema: z.object({
    columns: z.number().int().min(1).max(6).default(3),
  }),
  defaultProps: { columns: 3 },
  component: ({ props, children }) => (
    <div className={`grid grid-cols-${props.columns} gap-6`}>
      {children}
    </div>
  ),
})

Publishing the block

Authored blocks ship as a pwpack. Run pwfabric pwpack:init, drop your defineBlock(...) modules in the pack source, then pwfabric package builds the bundle and pwfabric upload / pwfabric publish pushes it to the marketplace. Once published and installed into a World, PhiCo can summon your block by name just like a first-party block.

See also