fix(site-builder): add Hero/CTA components to Layers virtual-rows registry

A grep across every component under src/components/ (not just the 12
files the original registry hardcoded) turns up three more leaf
components with the identical array-prop-as-content pattern: Hero
(HeroSimple.tsx), Call to Action (CallToAction.tsx) and CTA Section
(CTASection.tsx) all render a shared `ctas?: CtaButton[]` prop whose
items are `{ text, href, variant?, target? }`. Without this, a Hero's
CTA buttons still wouldn't appear in the Layers tree.

Verified displayName, prop name and label field against each
component's craft.props defaults and the shared CtaButton type in
sections/_cta-helpers.tsx -- all three matched exactly, no corrections
needed. A follow-up sweep for any further array-prop leaf components
found none: the only other array fields in the tree are nested one
level inside already-covered items (ContactFormField.options,
PricingPlan.features), not top-level component props.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 07:04:54 -07:00
co-authored by Claude Opus 5
parent eb4290a45e
commit f30fc6efec
2 changed files with 44 additions and 1 deletions
@@ -45,6 +45,35 @@ describe('deriveVirtualRows', () => {
]);
});
test('Hero, Call to Action and CTA Section derive rows from their shared ctas prop', () => {
const ctas = [
{ text: 'Get Started', href: '#', variant: 'primary' },
{ text: 'Learn More', href: '#learn', variant: 'outline' },
];
expect(deriveVirtualRows('Hero', { ctas })).toEqual([
{ index: 0, label: 'Get Started' },
{ index: 1, label: 'Learn More' },
]);
expect(deriveVirtualRows('Call to Action', { ctas })).toEqual([
{ index: 0, label: 'Get Started' },
{ index: 1, label: 'Learn More' },
]);
expect(deriveVirtualRows('CTA Section', { ctas })).toEqual([
{ index: 0, label: 'Get Started' },
{ index: 1, label: 'Learn More' },
]);
});
test('a CTA with no text falls back to "Button N"', () => {
const rows = deriveVirtualRows('Hero', {
ctas: [{ href: '#' }, { text: '', href: '#empty' }],
});
expect(rows).toEqual([
{ index: 0, label: 'Button 1' },
{ index: 1, label: 'Button 2' },
]);
});
test('every registry entry has a non-empty prop, label and fallback', () => {
for (const [name, spec] of Object.entries(VIRTUAL_CHILD_PROPS)) {
expect(spec.prop, `${name}.prop`).toBeTruthy();
+15 -1
View File
@@ -38,7 +38,18 @@ export interface VirtualChildSpec {
* - ContentSlider items are `{ heading, text, ... }`, not `{ title }` --
* registry corrected from `title` to `heading`.
* Every other entry (prop name and label field) matched the component as
* originally drafted. */
* originally drafted.
*
* Hero, Call to Action and CTA Section were added after a follow-up sweep
* of every component under src/components/ for top-level array props (not
* just the 12 files initially listed) turned up three more leaf components
* with the identical pattern: all three render a `ctas?: CtaButton[]` prop
* (see sections/_cta-helpers.tsx) whose items are `{ text, href, variant?,
* target? }`, so `text` is the label field. Verified against each file's
* `craft.displayName` and `craft.props.ctas` defaults. That sweep found no
* further candidates -- the only other array fields in the tree are nested
* one level down inside already-covered items (ContactFormField.options,
* PricingPlan.features), not top-level component props. */
export const VIRTUAL_CHILD_PROPS: Record<string, VirtualChildSpec> = {
'Features Grid': { prop: 'features', label: 'title', fallback: 'Feature' },
Tabs: { prop: 'tabs', label: 'label', fallback: 'Tab' },
@@ -52,6 +63,9 @@ export const VIRTUAL_CHILD_PROPS: Record<string, VirtualChildSpec> = {
'Social Links': { prop: 'links', label: 'platform', fallback: 'Link' },
Navbar: { prop: 'links', label: 'text', fallback: 'Link' },
'Contact Form': { prop: 'fields', label: 'label', fallback: 'Field' },
Hero: { prop: 'ctas', label: 'text', fallback: 'Button' },
'Call to Action': { prop: 'ctas', label: 'text', fallback: 'Button' },
'CTA Section': { prop: 'ctas', label: 'text', fallback: 'Button' },
};
const MAX_LABEL = 40;