diff --git a/packages/cli/src/commands/add.test.ts b/packages/cli/src/commands/add.test.ts index 87b82f525..6836b20d0 100644 --- a/packages/cli/src/commands/add.test.ts +++ b/packages/cli/src/commands/add.test.ts @@ -181,9 +181,9 @@ describe("runAdd (integration, mocked registry)", () => { expect(result.type).toBe("hyperframes:block"); expect(result.written).toHaveLength(1); expect(existsSync(join(dir, "compositions/my-block.html"))).toBe(true); - expect(readFileSync(join(dir, "compositions/my-block.html"), "utf-8")).toContain( - "my-block.html", - ); + const installed = readFileSync(join(dir, "compositions/my-block.html"), "utf-8"); + expect(installed).toContain(""); + expect(installed).toContain("my-block.html"); expect(result.snippet).toContain("compositions/my-block.html"); } finally { rmSync(dir, { recursive: true, force: true }); diff --git a/packages/cli/src/registry/installer.ts b/packages/cli/src/registry/installer.ts index 5ba6d2cde..4ce4314e1 100644 --- a/packages/cli/src/registry/installer.ts +++ b/packages/cli/src/registry/installer.ts @@ -6,6 +6,7 @@ * runtime to reject traversal even if the registry JSON schema was bypassed. */ +import { readFileSync, writeFileSync } from "node:fs"; import { resolve, relative, isAbsolute } from "node:path"; import type { FileTarget, RegistryItem } from "@hyperframes/core"; import { fetchItemFile, DEFAULT_REGISTRY_URL } from "./remote.js"; @@ -45,6 +46,22 @@ export function assertSafeTarget(destDir: string, target: string): void { } } +function isInstalledRegistryBlockComposition(item: RegistryItem, file: FileTarget): boolean { + return ( + item.type === "hyperframes:block" && + file.type === "hyperframes:composition" && + file.target.toLowerCase().endsWith(".html") + ); +} + +function addRegistryItemMarker(source: string, item: RegistryItem): string { + if (/^\s*/i.test(source.slice(0, 512))) { + return source; + } + + return `\n${source}`; +} + /** * Install a resolved `RegistryItem` into `destDir` by fetching each file in * parallel and writing it to its validated target path. @@ -65,6 +82,10 @@ export async function installItem( item.files.map(async (file: FileTarget) => { const destPath = resolve(destDir, file.target); await fetchItemFile(item, file, destPath, baseUrl); + if (isInstalledRegistryBlockComposition(item, file)) { + const source = readFileSync(destPath, "utf-8"); + writeFileSync(destPath, addRegistryItemMarker(source, item), "utf-8"); + } return destPath; }), ); diff --git a/packages/core/src/lint/rules/composition.test.ts b/packages/core/src/lint/rules/composition.test.ts index 484eb9273..72fad8465 100644 --- a/packages/core/src/lint/rules/composition.test.ts +++ b/packages/core/src/lint/rules/composition.test.ts @@ -35,19 +35,45 @@ describe("composition rules", () => { expect(finding).toBeUndefined(); }); - it("warns on large HTML files regardless of path", () => { + it("does not warn for large registry source block files", () => { const html = Array.from({ length: 301 }, (_, i) => i === 0 ? "" : ``, ).join("\n"); const result = lintHyperframeHtml(html, { - filePath: "/project/registry/blocks/data-chart.html", + filePath: "/project/registry/blocks/data-chart/data-chart.html", + }); + const finding = result.findings.find((f) => f.code === "composition_file_too_large"); + expect(finding).toBeUndefined(); + }); + + it("warns for large installed block composition files", () => { + const html = Array.from({ length: 301 }, (_, i) => + i === 0 ? "" : ``, + ).join("\n"); + + const result = lintHyperframeHtml(html, { + filePath: "/project/compositions/data-chart.html", }); const finding = result.findings.find((f) => f.code === "composition_file_too_large"); expect(finding).toBeDefined(); expect(finding?.severity).toBe("warning"); }); + it("does not warn for large registry-installed block composition files", () => { + const html = + "\n" + + Array.from({ length: 300 }, (_, i) => + i === 0 ? "" : ``, + ).join("\n"); + + const result = lintHyperframeHtml(html, { + filePath: "/project/compositions/data-chart.html", + }); + const finding = result.findings.find((f) => f.code === "composition_file_too_large"); + expect(finding).toBeUndefined(); + }); + it("uses nested split copy for large sub-composition files", () => { const html = Array.from({ length: 301 }, (_, i) => i === 0 ? "" : ``, diff --git a/packages/core/src/lint/rules/composition.ts b/packages/core/src/lint/rules/composition.ts index 65e738898..27d6f078a 100644 --- a/packages/core/src/lint/rules/composition.ts +++ b/packages/core/src/lint/rules/composition.ts @@ -15,6 +15,17 @@ function countPhysicalLines(source: string): number { return withoutFinalNewline.split("\n").length; } +function isRegistrySourceFile(filePath?: string): boolean { + if (!filePath) return false; + + const normalized = filePath.replace(/\\/g, "/"); + return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized); +} + +function isRegistryInstalledFile(rawSource: string): boolean { + return /^\s*/i.test(rawSource.slice(0, 512)); +} + function isCompositionRootOrMount(rawTag: string): boolean { return Boolean( readAttr(rawTag, "data-composition-id") || readAttr(rawTag, "data-composition-src"), @@ -24,6 +35,8 @@ function isCompositionRootOrMount(rawTag: string): boolean { export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [ // composition_file_too_large ({ rawSource, options }) => { + if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return []; + const lineCount = countPhysicalLines(rawSource); if (lineCount <= MAX_COMPOSITION_LINES) return []; diff --git a/registry/blocks/app-showcase/app-showcase.html b/registry/blocks/app-showcase/app-showcase.html index deb3887d4..52b7c2d10 100644 --- a/registry/blocks/app-showcase/app-showcase.html +++ b/registry/blocks/app-showcase/app-showcase.html @@ -25,6 +25,7 @@ data-composition-id="app-showcase" data-width="1920" data-height="1080" + data-start="0" data-duration="5.5" > diff --git a/registry/blocks/chromatic-radial-split/chromatic-radial-split.html b/registry/blocks/chromatic-radial-split/chromatic-radial-split.html index 1785f5b83..cdbd1c5d6 100644 --- a/registry/blocks/chromatic-radial-split/chromatic-radial-split.html +++ b/registry/blocks/chromatic-radial-split/chromatic-radial-split.html @@ -190,6 +190,7 @@
diff --git a/registry/blocks/domain-warp-dissolve/domain-warp-dissolve.html b/registry/blocks/domain-warp-dissolve/domain-warp-dissolve.html index 734ef17be..8a24dc98d 100644 --- a/registry/blocks/domain-warp-dissolve/domain-warp-dissolve.html +++ b/registry/blocks/domain-warp-dissolve/domain-warp-dissolve.html @@ -190,6 +190,7 @@
@@ -309,32 +310,32 @@ (function () { const tl = gsap.timeline({ paused: true }); const S = '[data-composition-id="flowchart"]'; - const nodePython = document.querySelector(`${S} #node-python`); - const pythonText = document.querySelector(`${S} #python-text`); + const nodePython = document.querySelector(S + " #node-python"); + const pythonText = document.querySelector(S + " #python-text"); // 1. Root node scales in - tl.to(`${S} #node-root`, { scale: 1, duration: 0.4, ease: "power2.out" }); + tl.to(S + " #node-root", { scale: 1, duration: 0.4, ease: "power2.out" }); // 2. Hold tl.addLabel("hold1", "+=0.6"); // 3. Connectors draw tl.to( - `${S} .connector#path-1-L, ${S} .connector#path-1-R`, + S + " .connector#path-1-L, " + S + " .connector#path-1-R", { strokeDashoffset: 0, duration: 0.5, ease: "none" }, "hold1", ); tl.to( - `${S} #label-yes, ${S} #label-not-sure`, + S + " #label-yes, " + S + " #label-not-sure", { opacity: 1, duration: 0.2 }, "hold1+=0.25", ); // 4. Level 2 nodes - tl.to(`${S} #node-yes`, { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4"); + tl.to(S + " #node-yes", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4"); tl.to( - `${S} #node-not-sure`, + S + " #node-not-sure", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.6", ); @@ -344,17 +345,24 @@ // 6. Level 2→3 connectors tl.to( - `${S} .connector#path-2-LL, ${S} .connector#path-2-LR, ${S} .connector#path-2-RL, ${S} .connector#path-2-RR`, + S + + " .connector#path-2-LL, " + + S + + " .connector#path-2-LR, " + + S + + " .connector#path-2-RL, " + + S + + " .connector#path-2-RR", { strokeDashoffset: 0, duration: 0.4, ease: "none" }, "hold2", ); // 7. Leaf nodes const leafNodes = [ - `${S} #node-python`, - `${S} #node-nocode`, - `${S} #node-website`, - `${S} #node-course`, + S + " #node-python", + S + " #node-nocode", + S + " #node-website", + S + " #node-course", ]; leafNodes.forEach((id, i) => { tl.to( @@ -364,14 +372,14 @@ ); }); - tl.to(`${S} .squiggle-container`, { opacity: 1, duration: 0.1 }, "hold2+=0.3"); + tl.to(S + " .squiggle-container", { opacity: 1, duration: 0.1 }, "hold2+=0.3"); // 8. Hold tl.addLabel("hold3", "+=0.8"); // 9. Cursor drifts in tl.to( - `${S} #cursor`, + S + " #cursor", { left: 450, top: 540, duration: 1, ease: "power1.inOut" }, "hold3", ); @@ -384,11 +392,11 @@ border.className = "selection-border"; nodePython.appendChild(border); } - gsap.set(`${S} .selection-border`, { opacity: 1 }); + gsap.set(S + " .selection-border", { opacity: 1 }); }, "hold3+=1"); tl.to( - `${S} #cursor`, + S + " #cursor", { scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" }, "hold3+=1", ); @@ -398,7 +406,7 @@ // 12. Double-click to highlight "Pythom" tl.to( - `${S} #cursor`, + S + " #cursor", { scale: 0.8, duration: 0.05, yoyo: true, repeat: 3, overwrite: "auto" }, "hold4", ); @@ -426,24 +434,24 @@ const typingEnd = typingStart + words.length * 0.05; tl.add(() => { if (pythonText) pythonText.innerHTML = "Start with Python"; - gsap.set(`${S} .squiggle-container`, { opacity: 0 }); + gsap.set(S + " .squiggle-container", { opacity: 0 }); }, typingEnd); // 15. Hold tl.addLabel("hold6", typingEnd + 0.5); // 16. Cursor clicks away to deselect - tl.to(`${S} #cursor`, { left: 500, top: 580, duration: 0.3, overwrite: "auto" }, "hold6"); + tl.to(S + " #cursor", { left: 500, top: 580, duration: 0.3, overwrite: "auto" }, "hold6"); tl.to( - `${S} #cursor`, + S + " #cursor", { scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" }, "hold6+=0.3", ); - tl.to(`${S} .selection-border`, { opacity: 0, duration: 0.1 }, "hold6+=0.3"); + tl.to(S + " .selection-border", { opacity: 0, duration: 0.1 }, "hold6+=0.3"); // 17. Emoji pop tl.to( - `${S} #emoji-thumb`, + S + " #emoji-thumb", { scale: 1, duration: 0.15, ease: "back.out(2)" }, "hold6+=0.4", ); @@ -452,7 +460,7 @@ tl.addLabel("hold7", "+=2"); // 19. Fade out - tl.to(`${S} #fade-overlay`, { opacity: 1, duration: 0.5 }, "hold7"); + tl.to(S + " #fade-overlay", { opacity: 1, duration: 0.5 }, "hold7"); window.__timelines = window.__timelines || {}; window.__timelines["flowchart"] = tl; diff --git a/registry/blocks/glitch/glitch.html b/registry/blocks/glitch/glitch.html index 73a6f8038..79fc5206b 100644 --- a/registry/blocks/glitch/glitch.html +++ b/registry/blocks/glitch/glitch.html @@ -190,6 +190,7 @@
diff --git a/registry/blocks/ridged-burn/ridged-burn.html b/registry/blocks/ridged-burn/ridged-burn.html index af0eb1e7a..94629341c 100644 --- a/registry/blocks/ridged-burn/ridged-burn.html +++ b/registry/blocks/ridged-burn/ridged-burn.html @@ -190,6 +190,7 @@
-
+
@@ -2897,6 +2903,7 @@ O + 4.8, ); + window.__timelines = window.__timelines || {}; window.__timelines["ui-3d-reveal"] = tl; })(); diff --git a/registry/blocks/whip-pan/whip-pan.html b/registry/blocks/whip-pan/whip-pan.html index 057a69dd6..d1ea5aa1c 100644 --- a/registry/blocks/whip-pan/whip-pan.html +++ b/registry/blocks/whip-pan/whip-pan.html @@ -190,6 +190,7 @@