Generated-code hygiene: listener cleanup, shared cn(), whitespace collapse, probe stripping, sub-pixel snapping, clone-origin metadata, dependency pinning

- DittoWire/Accordion/DropdownMenu runtime templates: AbortController-based
  listener cleanup, idempotent effects, orphan panel removal on unmount
- Emit a single src/lib/utils.ts cn() instead of a copy per component file
- Collapse captured whitespace runs under white-space:normal (pre* preserved)
- Tag source-injected font-metric probe nodes at capture; drop them from IR
- Snap sub-pixel arbitrary lengths (integer within 0.1px, else 1 decimal)
- Emit SITE_ORIGIN (src/lib/site.ts, env-overridable): sitemap/robots/
  canonical/og:url/JSON-LD resolve against the clone's own origin instead of
  the source domain
- Pin lottie-web in generated package.json whenever DittoLottie is emitted

144 tests pass (21 new), typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samraaj Bath
2026-07-03 22:51:50 -07:00
co-authored by Claude Fable 5
parent dfcd4a6563
commit 77be868ee3
13 changed files with 548 additions and 73 deletions
+26 -12
View File
@@ -94,7 +94,7 @@ export function dropdownMenuImportPath(depth: number): string {
/** The fixed DropdownMenu client component, written once per generated app. */
export const DROPDOWN_MENU_TSX = `"use client";
import { useEffect, useRef } from "react";
import { useEffect } from "react";
type RTMenu = { trigger: string; hoverOpen: boolean; gap: number; align: "left" | "right"; html: string };
const byCid = (cid: string): HTMLElement | null => document.querySelector('[data-cid="' + cid + '"]');
@@ -103,10 +103,10 @@ const byCid = (cid: string): HTMLElement | null => document.querySelector('[data
* user interaction does it inject the captured panel fragment under its trigger. The base
* render is therefore unchanged. */
export default function DropdownMenu({ menus }: { menus: RTMenu[] }) {
const wired = useRef(false);
useEffect(() => {
if (wired.current) return;
wired.current = true;
const ac = new AbortController();
const { signal } = ac;
const openPanels: HTMLElement[] = [];
for (const m of menus) {
const trig = byCid(m.trigger);
if (!trig) continue;
@@ -127,27 +127,41 @@ export default function DropdownMenu({ menus }: { menus: RTMenu[] }) {
panel = wrap.firstElementChild as HTMLElement | null;
if (!panel) return;
document.body.appendChild(panel);
openPanels.push(panel);
place();
trig.setAttribute("aria-expanded", "true");
};
const close = () => { if (panel) { panel.remove(); panel = null; } trig.setAttribute("aria-expanded", "false"); };
const close = () => {
if (panel) {
const i = openPanels.indexOf(panel);
if (i !== -1) openPanels.splice(i, 1);
panel.remove();
panel = null;
}
trig.setAttribute("aria-expanded", "false");
};
const toggle = () => (panel ? close() : open());
if (m.hoverOpen) {
const root = trig.parentElement ?? trig;
root.addEventListener("mouseenter", open);
root.addEventListener("mouseleave", close);
root.addEventListener("mouseenter", open, { signal });
root.addEventListener("mouseleave", close, { signal });
} else {
trig.addEventListener("click", (e) => { e.preventDefault(); toggle(); });
trig.addEventListener("click", (e) => { e.preventDefault(); toggle(); }, { signal });
}
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); }, { signal });
document.addEventListener("click", (e) => {
const t = e.target as Node;
if (panel && !trig.contains(t) && !panel.contains(t)) close();
});
window.addEventListener("resize", place);
window.addEventListener("scroll", place, { passive: true });
}, { signal });
window.addEventListener("resize", place, { signal });
window.addEventListener("scroll", place, { passive: true, signal });
}
(window as any).__dittoMenuReady = true; // wiring done — lets the gate drive deterministically
return () => {
ac.abort();
// Remove any still-open panels appended to document.body so unmount leaves no orphan nodes.
for (const p of openPanels.splice(0)) p.remove();
};
}, [menus]);
return null;
}