Full workflow for adding a new UI block to ui-flx: file structure, manifest, catalog + registry.json registration, MDX docs, validation, and screenshot capture. Triggers: "add block", "new block", "criar bloco", "novo bloco", "add a new section", or any request to create a new Flexnative block.
Installation
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Full workflow for adding a new UI block to ui-flx: file structure, manifest, catalog + registry.json registration, MDX docs, validation, and screenshot capture. Triggers: "add block", "new block", "criar bloco", "novo bloco", "add a new section", or any request to create a new Flexnative block.
Add a new block to ui-flx
The pipeline — do every step, in order
Do NOT stop early. Every step below is required; the two most-skipped are 6 (MDX)
and 8 (screenshots) — they are not optional.
Read the category's catalog.ts + registry.json (nothing else).
Create files — <slug>.tsx, <slug>-example.tsx, editor/fields.tsx, manifest.ts (+ any illustration/subcomponent).
Register in registry/blocks/<category>/catalog.ts.
Add entry to registry/blocks/<category>/registry.json.
Create MDX at src/app/content/blocks/<category>/<slug>.mdx.
Motion: enter = split + staggered per element; exit subtle. Only animate
transform/opacity/filter, never transition: all. Guard every animation with
useReducedMotion(). ease-out enter/exit, durations < 300ms, no perpetual loops.
Typography: headings use <Balancer>; dynamic numbers use tabular-nums.
Surfaces: images carry outline-black/10 dark:outline-white/10; nested radii are
concentric (outer = inner + padding); prefer shadow-sm over hard borders.
Interaction: active:scale-[0.96], ≥ 40×40px hit area.
Primitives over markup: reach for Card/Badge/Button/Avatar/Separator from
@/components/ui/* instead of re-inventing them with a styled div. List every one used
in registryDependencies.
Fades = Tailwind mask-*: any image/component dissolving into the background uses
composable mask-radial-* / mask-*-from/to on a wrapper (holding image + overlay) —
never inline style={{maskImage}}, [mask-image:…], or bg-gradient-* as the primary
fade. Read the docs. Reference: hero-01.tsx.
Never import from @/lib/block-defaults or @/lib/block-registry (deleted) or
import registry.json in app code — use @/lib/blocks/block-catalog.
Do NOT read root registry.json, src/lib/blocks/block-catalog.ts, or other categories.
New category? Also read registry/blocks/registry.json + src/lib/blocks/block-catalog.ts.
Step 2 — Create the files
<slug>.tsx — code ordering is the point
Server-safe, Readonly<Props>. Follow the reading order of
registry/blocks/hero/hero-03/hero-03.tsx exactly — this is about structure, not content:
Props interface — typed, exported. Include variant; add an animation union when it animates.
variantStyles map — const … as const keyed by variant, holding className strings. Replaces all sizing if/else.
Motion Variants — module-level consts, never inlined in JSX.
Body — resolve const vs = variantStyles[variant], build named element constants
(titleElement, mediaElement, …) guarded with &&, then return a clean composition.
No if/else / nested ternaries for layout — branch with the map, gate with cond && <El />.
For animated blocks: add the animation prop, declare Variants as module consts, and
branch the return per mode reusing the same element constants — like hero-03.tsx.
image.light + image.dark are both required (registry:validate fails if empty). Paths:
public/images/blocks/<category>/<slug>.png (light) and <slug>-dark.png (dark).
importtype { BlockManifest } from'@/lib/blocks/block-manifest-types'import { MyBlock } from'./my-block'import { MyBlockEditorFields } from'./editor/fields'import { MyBlockExample, values } from'./my-block-example'exportconstmanifest: BlockManifest = {
slug: 'my-block',
name: 'My Block',
description: 'Short description of what the block does.',
category: 'content', // must match an existing category slugimage: {
light: '/images/blocks/content/my-block.png',
dark: '/images/blocks/content/my-block-dark.png',
},
meta: { iframeHeight: 600 }, // + captureViewportOnly: true for scroll/interactive blockshasNew: true, // optional badgecomponent: MyBlock,
editorFields: MyBlockEditorFields,
example: MyBlockExample,
defaults: values, // must be the same `values` object from the example file
}
When the layout has visual space, prefer a purpose-built illustration over a stock photo.
Build it inside the block's files (not under registry/illustrations/) but to the
add-illustration bar: compose don't symbolize, layer for
depth, theme tokens (light + dark), motion optional (one-shot, reduced-motion-safe).
Hard-code its content — it simulates a real screen; do NOT wire it to props/editor fields.
Wire the file into registry.json — add it as a files[] entry with its own target
(step 4) and add any shadcn primitives it uses to registryDependencies.
Report the decision in step 8.
Palette to reach for (only when it fits): real Unsplash crops (?w=800&q=80 + image outline),
layered Card/Badge/Avatar fragments, small SVG sparklines, a big tabular-nums metric,
mask-* edge fades, a faint background grid/glow (opacity-[0.04]–10, pointer-events-none),
a card peeking behind another.
Step 3 — Register in catalog.ts
Two edits in registry/blocks/<category>/catalog.ts. Array position = display order.
src/lib/blocks/block-catalog.ts is a thin aggregator — never edit it.
Step 4 — Add entry to registry/blocks/<category>/registry.json
Per-category file (not root). files[].path is relative to the category dir (<slug>/<file>);
files[].target is the absolute install path. Add a files[] entry for every.tsx the
block ships (main, example, illustrations, subcomponents). Do NOT write title,
description, or meta.iframeHeight — registry:sync fills them from the manifest.
Add meta.containerClassName manually only for special previews (carousels: "max-w-full overflow-hidden px-0").
Step 5 — Create the docs MDX
src/app/content/blocks/<category>/<slug>.mdx — auto-discovered by path, no registration.
Copy the shape of src/app/content/blocks/hero/hero-01.mdx. metadata (title + description +
openGraph copies) matches the manifest. Add a <Step> + collapsible<CodeBlockFromFile>
for every extra file in files[].
export const metadata = {
title: 'My Block',
description: 'Same one-line description as the manifest.',
openGraph: { title: 'My Block', description: 'Same one-line description as the manifest.', type: 'article' },
}
<BlockView category="<category>" slug="<slug>" />
## Implementation
<CodeTabs>
<TabsList>
<TabsTrigger value="cli">Command</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
<CodeBlockCommand command="shadcn@latest add @flx/<slug>" />
## Usage
<CodeBlockFromFile filePath="registry/blocks/<category>/<slug>/<slug>-example.tsx" title="<slug>-example.tsx" />
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Install dependencies</Step>
<CodeBlockCommand command="react-wrap-balancer" isPackage />
<CodeBlockCommand command="motion" isPackage />
<Step>Add the illustration (optional)</Step>
One line describing the inline illustration and that it can be swapped/removed.
<CodeBlockFromFile filePath="registry/blocks/<category>/<slug>/<extra-file>.tsx" title="<extra-file>.tsx" collapsible />
<Step>Copy the component</Step>
<CodeBlockFromFile filePath="registry/blocks/<category>/<slug>/<slug>.tsx" title="<slug>.tsx" collapsible />
<Step>Use the component</Step>
<CodeBlockFromFile filePath="registry/blocks/<category>/<slug>/<slug>-example.tsx" />
</Steps>
</TabsContent>
</CodeTabs>
Step 6 — Validate, sync, build
Run in order. Pre-sync validate failing on the new block is expected (title/description not
synced yet) — continue.
pnpm run registry:sync# fills title/description/iframeHeight from manifest
pnpm run registry:validate # must PASS
pnpm run registry:build # regenerates public/r/*.json
registry:validate prints exactly which field is out of sync or missing.
Step 7 — Capture screenshots (REQUIRED)
The manifest points at image.light/image.dark PNGs that do not exist until you capture
them. Do not skip this — the block card renders broken without them.
pnpm run playwright:install # once per machine
pnpm run dev # in a separate terminal — capture needs it running
pnpm dlx tsx scripts/capture-block-screenshots.ts --slug=<slug>
Confirm both PNGs now exist under public/images/blocks/<category>/. Batch options:
pnpm run blocks:capture-screenshots (all) or ... --missing-only.
Step 8 — Report the illustration decision
State whether you built an inline illustration and why — e.g. "Inline dashboard illustration
in dashboard-demo.tsx (layered cards, static), following add-illustration rules" — or why you
chose a photo / none. Never silently drop it.
Checklist
<slug>.tsx — Readonly<Props>; hero-03 ordering (props → variantStyles → motion consts → named element constants → clean return); no if/else/ternaries for layout
Constitution held — staggered enter / subtle exit, prefers-reduced-motion, only transform/opacity/filter, <Balancer>, tabular-nums, image outlines, active:scale-[0.96], concentric radii, mask-* for fades, shadcn primitives
<slug>-example.tsx — exports values + named example
editor/fields.tsx — defaults from ../<slug>-example
manifest.ts — all fields incl. image.light + image.dark; defaults === example values
catalog.ts — import + array entry
registry.json — entry with files[] for every .tsx, registryDependencies, dependencies
<slug>.mdx — metadata matches manifest; Command + Manual tabs; a <Step> per extra file