fix(showcase): render object props, bridge the mega menu gap, make scrollspy testable
Quality / quality (ubuntu-latest) (push) Failing after 12m38s
Quality / quality (windows-latest) (push) Canceled after 0s

Object props never worked in generated demos, and my two earlier attempts each
traded one failure for another:

  - a bare {...} attribute is read by the compiler as an interpolation, so it
    parsed JSON as JavaScript and the page 500ed
  - parenthesising it compiled, but prop coercion runs JSON.parse on the raw
    attribute, so ({...}) threw and every demo rendered empty and silent
  - entity-escaping the braces did not help either: the compiler hands the
    attribute over without decoding, so JSON.parse still failed

They are now hoisted into page state and bound, which is what the playground
has always done. The state initialiser uses JSON.parse rather than an object
literal because the parser reads a leading brace as the start of a block.

Navbar gains a real profile: a brand, links, a two-column dropdown panel and
calls to action, instead of the generic scaffold samples that made every demo
look identical and showed no dropdown at all.

MegaMenu closed while the pointer travelled to it. The panel sits below the
trigger and that offset belongs to neither element, so crossing it fired
mouseleave on the root. A descendant now covers the gap.

Scrollspy could not be exercised at all: its links pointed at ids that did not
exist on the page. The demo now ships real sections, in page flow because the
runtime observes against the viewport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 23:12:37 +05:30
co-authored by Claude Opus 5
parent 6a9c48a207
commit 68c7b96a9f
13 changed files with 603 additions and 2152 deletions
@@ -424,16 +424,42 @@ function sampleValue(prop, component, variation) {
return undefined;
}
/*
* Object props are hoisted into page state rather than written inline.
*
* A bare {...} attribute is read by the compiler as an interpolation, and
* escaping the braces does not help either: prop coercion runs JSON.parse on
* the raw attribute, which never sees the entities decoded, so the mount threw
* and the demo silently rendered nothing. Binding to state is what the
* playground already does, and it is the only form that survives both.
*
* Arrays start with [ and are unambiguous, so they stay inline.
*/
const hoistedObjects = [];
function resetHoistedObjects() {
hoistedObjects.length = 0;
}
function hoistObjectProp(value) {
const name = `demo_obj_${hoistedObjects.length}`;
// JSON.parse, not a bare object literal: the parser reads a leading brace
// as the start of a block and rejects the rest of the page.
const json = JSON.stringify(JSON.stringify(value));
hoistedObjects.push(` state ${name} = JSON.parse(${json})`);
return name;
}
function hoistedObjectStates() {
if (!hoistedObjects.length) return "";
// playgroundStates has no trailing newline, so lead with one.
return "\n" + hoistedObjects.join("\n");
}
function wrnAttribute(name, value) {
if (typeof value === "object" && value !== null) {
const expression = JSON.stringify(value).replaceAll("'", "\\u0027");
/*
* An object literal has to be parenthesised. A bare {...} at the start of
* an attribute is read as an interpolation, so the compiler tried to parse
* the JSON as JavaScript and stopped at the first colon. Arrays start with
* [ and were never affected, which is why only object props broke.
*/
return `${name}='${Array.isArray(value) ? expression : `(${expression})`}'`;
if (!Array.isArray(value)) return `${name}='{${hoistObjectProp(value)}}'`;
return `${name}='${JSON.stringify(value).replaceAll("'", "\\u0027")}'`;
}
return `${name}="${escapeAttribute(String(value))}"`;
}
@@ -1475,6 +1501,8 @@ function eventDocumentation(component) {
}
function detailPage(component, categoryComponents) {
// Names are per page; without this they accumulate across the whole run.
resetHoistedObjects();
const index = categoryComponents.findIndex((item) => item.name === component.name);
const previous = categoryComponents[index - 1];
const next = categoryComponents[index + 1];
@@ -1552,7 +1580,7 @@ function detailPage(component, categoryComponents) {
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
page ${component.name.replace(/[^A-Za-z0-9_]/g, "")}Detail {
layout = "showcase"
${playgroundStates(component)}
${playgroundStates(component)}${hoistedObjectStates()}
seo {
title = "${escapeAttribute(displayName(component.name))}"
description = "${escapeAttribute(component.purpose)}"