diff --git a/packages/cli/src/registry/registryBlocks.test.ts b/packages/cli/src/registry/registryBlocks.test.ts index b21e79784c..4db240917d 100644 --- a/packages/cli/src/registry/registryBlocks.test.ts +++ b/packages/cli/src/registry/registryBlocks.test.ts @@ -8,7 +8,15 @@ import { describe, expect, it } from "vitest"; const blocksDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../registry/blocks"); interface RegistryManifest { - files: Array<{ path: string; type: string }>; + name: string; + tags?: string[]; + files: Array<{ path: string; target: string; type: string }>; +} + +const promotedTemplateTag = "ad-template"; + +function loadRegistryManifest(itemDir: string): RegistryManifest { + return JSON.parse(readFileSync(join(itemDir, "registry-item.json"), "utf8")) as RegistryManifest; } function findMissingLocalScripts(itemDir: string, manifest: RegistryManifest): string[] { @@ -32,6 +40,37 @@ function findMissingLocalScripts(itemDir: string, manifest: RegistryManifest): s } describe("registry blocks", () => { + it("ships an editing contract and declared variables for every promoted template", () => { + const promotedManifests = readdirSync(blocksDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => ({ + itemDir: join(blocksDir, entry.name), + manifest: loadRegistryManifest(join(blocksDir, entry.name)), + })) + .filter(({ manifest }) => manifest.tags?.includes(promotedTemplateTag)); + + expect(promotedManifests.length).toBeGreaterThan(0); + for (const { itemDir, manifest } of promotedManifests) { + const templateId = manifest.name; + const contractFiles = manifest.files.filter( + (file) => + file.path === "TEMPLATE.md" && + file.target === "TEMPLATE.md" && + file.type === "hyperframes:asset", + ); + const composition = manifest.files.find((file) => file.type === "hyperframes:composition"); + + expect(contractFiles, templateId).toHaveLength(1); + expect(composition, templateId).toBeDefined(); + const html = readFileSync(join(itemDir, composition?.path ?? ""), "utf8"); + const { document } = parseHTML(html); + const declarations = JSON.parse( + document.documentElement.getAttribute("data-composition-variables") ?? "[]", + ) as unknown[]; + expect(declarations.length, templateId).toBeGreaterThan(0); + } + }); + it("installs every local script referenced by a block composition", () => { const missing: string[] = []; @@ -39,9 +78,7 @@ describe("registry blocks", () => { if (!entry.isDirectory()) continue; const itemDir = join(blocksDir, entry.name); - const manifest = JSON.parse( - readFileSync(join(itemDir, "registry-item.json"), "utf8"), - ) as RegistryManifest; + const manifest = loadRegistryManifest(itemDir); for (const src of findMissingLocalScripts(itemDir, manifest)) { missing.push(`${entry.name}: ${src}`); diff --git a/registry/blocks/ai-chat-reveal/TEMPLATE.md b/registry/blocks/ai-chat-reveal/TEMPLATE.md new file mode 100644 index 0000000000..51a6ae6cd1 --- /dev/null +++ b/registry/blocks/ai-chat-reveal/TEMPLATE.md @@ -0,0 +1,19 @@ +# AI Chat Reveal editing contract + +## Surface ownership + +This is a generic assistant conversation followed by a brandable closing card. The website brand may appear in the declared conversation and closing-card slots; the chat shell and motion remain template-owned. + +## Editable slots + +Only defaults declared in `data-composition-variables` are editable: + +- `botName`, `userMessage`, `answer1` through `answer3` +- `bullet1` through `bullet3` +- `ecHeadline`, `ecSub`, `ecCta`, and `ecFooter` + +Typed and streamed copy is length-locked to within 20% of the original. + +## Protected + +Do not change chat chrome, keyboard, layout, logo asset, palette, fonts, scene order, duration, timing, easing, typing cadence, or reveal logic. This contract declares no image or color variables. diff --git a/registry/blocks/ai-chat-reveal/registry-item.json b/registry/blocks/ai-chat-reveal/registry-item.json index 3c7e458be8..dea186d267 100644 --- a/registry/blocks/ai-chat-reveal/registry-item.json +++ b/registry/blocks/ai-chat-reveal/registry-item.json @@ -119,6 +119,11 @@ "path": "assets/hyperframes-logo-white.svg", "target": "assets/hyperframes-logo-white.svg", "type": "hyperframes:asset" + }, + { + "path": "TEMPLATE.md", + "target": "TEMPLATE.md", + "type": "hyperframes:asset" } ] } diff --git a/registry/blocks/chatgpt-exchange/TEMPLATE.md b/registry/blocks/chatgpt-exchange/TEMPLATE.md new file mode 100644 index 0000000000..33e731704b --- /dev/null +++ b/registry/blocks/chatgpt-exchange/TEMPLATE.md @@ -0,0 +1,21 @@ +# ChatGPT Exchange editing contract + +## Surface ownership + +This template depicts the ChatGPT application. OpenAI owns the surrounding application identity. The supplied website is only the subject discussed inside the conversation. + +## Editable slots + +Only defaults declared in `data-composition-variables` are editable: + +- The user prompt and two response-introduction paragraphs +- The three comparison-table headings +- The use case, tool, explanation, and source-chip copy for each of four rows + +Typed and streamed copy is length-locked to within 20% of the original. + +## Protected + +Preserve the ChatGPT name, labels, suggestions, predictive keyboard text, header, composer, keyboard, icons, fonts, palette, layout, status UI, table geometry, scene structure, duration, timing, easing, typing cadence, and reveal behavior. + +Website colors and typography must never be applied to the ChatGPT shell. If a requested value has no declared variable, leave it unchanged. diff --git a/registry/blocks/chatgpt-exchange/chatgpt-exchange.html b/registry/blocks/chatgpt-exchange/chatgpt-exchange.html index 9282dc90ff..c3f4b8c032 100644 --- a/registry/blocks/chatgpt-exchange/chatgpt-exchange.html +++ b/registry/blocks/chatgpt-exchange/chatgpt-exchange.html @@ -1,5 +1,32 @@ - +
@@ -626,12 +653,16 @@