diff --git a/docs/catalog/components/texture-mask-text.mdx b/docs/catalog/components/texture-mask-text.mdx new file mode 100644 index 000000000..7f0309097 --- /dev/null +++ b/docs/catalog/components/texture-mask-text.mdx @@ -0,0 +1,487 @@ +--- +title: "Texture Mask Text" +--- + +`text` `texture` `mask` `effect` + +
+
+
Brick
+
BRICK
+
+
+
Rock
+
ROCK
+
+
+
Ground 103
+
GROUND
+
+
+
Wood
+
WOOD
+
+
+
Metal
+
METAL
+
+
+
Lava
+
LAVA
+
+
+ +## Install + + + +```bash Terminal +npx hyperframes add texture-mask-text +``` + + + +## Details + +| Property | Value | +| --- | --- | +| Type | Component | + +## Agent Usage + +Use this wording when asking an agent to apply a texture: + +```text +Use the Texture Mask Text catalog component. + +1. From the project root, run: + npx hyperframes add texture-mask-text +2. That command creates this installed snippet: + compositions/components/texture-mask-text/texture-mask-text.html +3. Open that file and paste the real + +`; + const project = makeProject(html); + + const { totalErrors, results } = lintProject(project); + const finding = results[0]?.result.findings.find( + (item) => item.code === "texture_mask_asset_not_found", + ); + + expect(totalErrors).toBeGreaterThan(0); + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("error"); + expect(finding?.message).toContain("masks/lava.png"); + }); + + it("does not error when the referenced texture mask exists", () => { + const html = ` +
+
TEXT
+
+ + +`; + const project = makeProject(html); + mkdirSync(join(project.dir, "masks"), { recursive: true }); + writeFileSync(join(project.dir, "masks", "lava.png"), "fake"); + + const { results } = lintProject(project); + const finding = results[0]?.result.findings.find( + (item) => item.code === "texture_mask_asset_not_found", + ); + + expect(finding).toBeUndefined(); + }); + + it("resolves mask-image URLs inside linked sub-composition stylesheets", () => { + const project = makeProject(validHtml(), { + "scene.html": ` +
+
TEXT
+
+ +`, + }); + writeFileSync( + join(project.dir, "compositions", "scene.css"), + '.hf-texture-lava { mask-image: url("masks/lava.png"); }', + ); + mkdirSync(join(project.dir, "compositions", "masks"), { recursive: true }); + writeFileSync(join(project.dir, "compositions", "masks", "lava.png"), "fake"); + + const { results } = lintProject(project); + const finding = results[0]?.result.findings.find( + (item) => item.code === "texture_mask_asset_not_found", + ); + + expect(finding).toBeUndefined(); + }); + + it("resolves root-absolute mask-image URLs from the project root", () => { + const html = ` +
+
TEXT
+
+ + +`; + const project = makeProject(html); + mkdirSync(join(project.dir, "assets", "texture-mask-text", "masks"), { + recursive: true, + }); + writeFileSync(join(project.dir, "assets", "texture-mask-text", "masks", "lava.png"), "fake"); + + const { results } = lintProject(project); + const finding = results[0]?.result.findings.find( + (item) => item.code === "texture_mask_asset_not_found", + ); + + expect(finding).toBeUndefined(); + }); +}); + describe("multiple_root_compositions", () => { it("fires when two HTML files have data-composition-id", () => { const project = makeProject(validHtml()); diff --git a/packages/cli/src/utils/lintProject.ts b/packages/cli/src/utils/lintProject.ts index 45eef143c..9e3ff2199 100644 --- a/packages/cli/src/utils/lintProject.ts +++ b/packages/cli/src/utils/lintProject.ts @@ -18,6 +18,12 @@ interface HtmlSource { compSrcPath?: string; } +interface CssSource { + content: string; + /** Root-relative path to the CSS file. Undefined means inline HTML CSS. */ + rootRelativePath?: string; +} + export interface ProjectLintResult { results: Array<{ file: string; result: HyperframeLintResult }>; totalErrors: number; @@ -26,6 +32,16 @@ export interface ProjectLintResult { } const AUDIO_EXTENSIONS = new Set([".mp3", ".wav", ".aac", ".ogg", ".m4a", ".flac", ".opus"]); +const STYLE_BLOCK_RE = /]*>([\s\S]*?)<\/style>/gi; +const OPEN_TAG_RE = /<([a-z][\w:-]*)(\s[^<>]*?)?>/gi; +const MASK_IMAGE_URL_RE = + /\b(?:-webkit-)?mask-image\s*:\s*[^;{}]*url\(\s*(?:"([^"]+)"|'([^']+)'|([^"')\s]+))\s*\)/gi; + +function readHtmlAttr(tag: string, name: string): string | null { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = tag.match(new RegExp(`\\b${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i")); + return match?.[1] ?? match?.[2] ?? null; +} function isLocalStylesheetHref(href: string): boolean { return !!href && !/^(https?:|data:|blob:|\/\/)/i.test(href); @@ -53,6 +69,62 @@ function collectExternalStyles( return styles; } +function collectCssSources(projectDir: string, html: string, compSrcPath?: string): CssSource[] { + const sources: CssSource[] = []; + + let styleMatch: RegExpExecArray | null; + const stylePattern = new RegExp(STYLE_BLOCK_RE.source, STYLE_BLOCK_RE.flags); + while ((styleMatch = stylePattern.exec(html)) !== null) { + sources.push({ content: styleMatch[1] ?? "" }); + } + + const linkRe = /]*>/gi; + let linkMatch: RegExpExecArray | null; + while ((linkMatch = linkRe.exec(html)) !== null) { + const tag = linkMatch[0]; + const rel = readHtmlAttr(tag, "rel") ?? ""; + if (!rel.split(/\s+/).some((part) => part.toLowerCase() === "stylesheet")) continue; + const href = readHtmlAttr(tag, "href") ?? ""; + if (!isLocalStylesheetHref(href)) continue; + + const rootRelativePath = compSrcPath ? join(dirname(compSrcPath), href) : href; + const resolved = resolve(projectDir, rootRelativePath); + if (!existsSync(resolved)) continue; + sources.push({ content: readFileSync(resolved, "utf-8"), rootRelativePath }); + } + + let tagMatch: RegExpExecArray | null; + const tagPattern = new RegExp(OPEN_TAG_RE.source, OPEN_TAG_RE.flags); + while ((tagMatch = tagPattern.exec(html)) !== null) { + const tag = tagMatch[0]; + const style = readHtmlAttr(tag, "style"); + if (!style) continue; + sources.push({ content: style }); + } + + return sources; +} + +function isRemoteOrInlineUrl(url: string): boolean { + return /^(https?:|data:|blob:|\/\/|#)/i.test(url); +} + +function cleanAssetUrl(url: string): string { + return url.trim().split(/[?#]/, 1)[0] ?? ""; +} + +function resolveCssAssetPath( + projectDir: string, + url: string, + htmlCompSrcPath?: string, + cssRootRelativePath?: string, +): string { + if (url.startsWith("/")) return resolve(projectDir, url.slice(1)); + if (cssRootRelativePath) return resolve(projectDir, join(dirname(cssRootRelativePath), url)); + if (htmlCompSrcPath) return resolve(projectDir, rewriteAssetPath(htmlCompSrcPath, url)); + return resolve(projectDir, url); +} + /** * Lint the root index.html and all sub-compositions in the compositions/ directory. * Returns aggregated results across all files. @@ -101,6 +173,7 @@ export function lintProject(project: ProjectDir): ProjectLintResult { const projectFindings = [ ...lintProjectAudioFiles(project.dir, allHtmlSources), ...lintAudioSrcNotFound(project.dir, allHtmlSources), + ...lintTextureMaskAssetNotFound(project.dir, allHtmlSources), ...lintMultipleRootCompositions(project.dir), ...lintDuplicateAudioTracks(allHtmlSources), ]; @@ -215,6 +288,49 @@ function lintAudioSrcNotFound( return findings; } +function lintTextureMaskAssetNotFound( + projectDir: string, + htmlSources: HtmlSource[], +): HyperframeLintFinding[] { + const missing = new Map(); + + for (const { html, compSrcPath } of htmlSources) { + for (const cssSource of collectCssSources(projectDir, html, compSrcPath)) { + let match: RegExpExecArray | null; + const pattern = new RegExp(MASK_IMAGE_URL_RE.source, MASK_IMAGE_URL_RE.flags); + while ((match = pattern.exec(cssSource.content)) !== null) { + const rawUrl = match[1] ?? match[2] ?? match[3] ?? ""; + const url = cleanAssetUrl(rawUrl); + if (!url || isRemoteOrInlineUrl(url)) continue; + if (/^__[A-Z_]+__$/.test(url)) continue; + + const resolved = resolveCssAssetPath( + projectDir, + url, + compSrcPath, + cssSource.rootRelativePath, + ); + if (existsSync(resolved)) continue; + missing.set(url, resolved); + } + } + } + + if (missing.size === 0) return []; + const urls = [...missing.keys()]; + return [ + { + code: "texture_mask_asset_not_found", + severity: "error", + message: `CSS mask-image references file(s) not found in the project: ${urls.join(", ")}.`, + fixHint: + urls.length === 1 + ? `Add "${urls[0]}" to the project, or update the mask-image URL to point to an existing texture mask.` + : "Add the missing texture mask files to the project, or update the mask-image URLs to point to existing files.", + }, + ]; +} + /** * Error if multiple root-level HTML files with data-composition-id exist. * Scans the project directory filesystem (not just what lintProject chose to read) diff --git a/packages/core/src/lint/hyperframeLinter.ts b/packages/core/src/lint/hyperframeLinter.ts index 513d76cfe..e50518b69 100644 --- a/packages/core/src/lint/hyperframeLinter.ts +++ b/packages/core/src/lint/hyperframeLinter.ts @@ -7,6 +7,7 @@ import { gsapRules } from "./rules/gsap"; import { captionRules } from "./rules/captions"; import { compositionRules } from "./rules/composition"; import { adapterRules } from "./rules/adapters"; +import { textureRules } from "./rules/textures"; const ALL_RULES = [ ...coreRules, @@ -15,6 +16,7 @@ const ALL_RULES = [ ...captionRules, ...compositionRules, ...adapterRules, + ...textureRules, ]; export function lintHyperframeHtml( diff --git a/packages/core/src/lint/rules/composition.test.ts b/packages/core/src/lint/rules/composition.test.ts index f94de67fd..ce1e0087f 100644 --- a/packages/core/src/lint/rules/composition.test.ts +++ b/packages/core/src/lint/rules/composition.test.ts @@ -35,6 +35,21 @@ describe("composition rules", () => { expect(finding).toBeUndefined(); }); + it("does not count inline style block internals as structural lines", () => { + const style = ``; + const html = ` + + ${style} + +
TEXT
+ +`; + + const result = lintHyperframeHtml(html, { filePath: "/project/index.html" }); + const finding = result.findings.find((f) => f.code === "composition_file_too_large"); + expect(finding).toBeUndefined(); + }); + it("does not warn for large registry source block 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 7107702e3..e9392ad4b 100644 --- a/packages/core/src/lint/rules/composition.ts +++ b/packages/core/src/lint/rules/composition.ts @@ -16,6 +16,10 @@ function countPhysicalLines(source: string): number { return withoutFinalNewline.split("\n").length; } +function countStructuralLines(source: string): number { + return countPhysicalLines(source.replace(/]*>[\s\S]*?<\/style>/gi, "")); +} + function isRegistrySourceFile(filePath?: string): boolean { if (!filePath) return false; @@ -38,7 +42,7 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding ({ rawSource, options }) => { if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return []; - const lineCount = countPhysicalLines(rawSource); + const lineCount = countStructuralLines(rawSource); if (lineCount <= MAX_COMPOSITION_LINES) return []; const splitTarget = options.isSubComposition diff --git a/packages/core/src/lint/rules/textures.test.ts b/packages/core/src/lint/rules/textures.test.ts new file mode 100644 index 000000000..b2b1b6b1c --- /dev/null +++ b/packages/core/src/lint/rules/textures.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from "vitest"; +import { lintHyperframeHtml } from "../hyperframeLinter.js"; + +function baseHtml(body: string, style = ""): string { + return ` +
+ ${body} +
+ + +`; +} + +const textureCss = ` +.hf-texture-text { + color: #fff; + -webkit-mask-size: var(--mask-size, cover); + mask-size: var(--mask-size, cover); +} +.hf-texture-lava { + -webkit-mask-image: url("masks/lava.png"); + mask-image: url("masks/lava.png"); +} +`; + +describe("texture rules", () => { + it("does not warn for a valid texture mask text usage", () => { + const html = baseHtml( + '
TEXT
', + `${textureCss}.shadow { filter: drop-shadow(1px 2px 1px rgba(0,0,0,.48)); }`, + ); + + const result = lintHyperframeHtml(html); + + expect(result.findings.filter((finding) => finding.code.startsWith("texture_"))).toEqual([]); + }); + + it("warns when a material class is used without hf-texture-text", () => { + const html = baseHtml('
TEXT
', textureCss); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_class_missing_base"); + + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("warning"); + expect(finding?.fixHint).toContain("hf-texture-text"); + }); + + it("warns when hf-texture-text has no material class or custom mask image", () => { + const html = baseHtml('
TEXT
', textureCss); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_text_missing_mask"); + + expect(finding).toBeDefined(); + expect(finding?.severity).toBe("warning"); + }); + + it("allows hf-texture-text with an inline custom mask image", () => { + const html = baseHtml( + '
TEXT
', + textureCss, + ); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_text_missing_mask"); + + expect(finding).toBeUndefined(); + }); + + it("warns when a texture material class is not defined by local CSS", () => { + const html = baseHtml('
TEXT
', textureCss); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_class_unknown"); + + expect(finding).toBeDefined(); + expect(finding?.message).toContain("hf-texture-marbel"); + }); + + it("warns when drop-shadow is applied inline to the textured text element", () => { + const html = baseHtml( + '
TEXT
', + textureCss, + ); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text"); + + expect(finding).toBeDefined(); + expect(finding?.fixHint).toContain("wrapper"); + }); + + it("warns when drop-shadow is applied by CSS directly to hf-texture-text", () => { + const html = baseHtml( + '
TEXT
', + `${textureCss}.hf-texture-text { filter: drop-shadow(1px 2px 1px black); }`, + ); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text"); + + expect(finding).toBeDefined(); + expect(finding?.selector).toBe(".hf-texture-text"); + }); + + it("warns when drop-shadow targets a material class before the mask rule is declared", () => { + const html = baseHtml( + '
TEXT
', + `.hf-texture-lava { filter: drop-shadow(1px 2px 1px black); } + ${textureCss}`, + ); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text"); + + expect(finding).toBeDefined(); + expect(finding?.selector).toBe(".hf-texture-lava"); + }); + + it("warns when drop-shadow targets another class on the textured text element", () => { + const html = baseHtml( + '
TEXT
', + `${textureCss}.headline { filter: drop-shadow(1px 2px 1px black); }`, + ); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text"); + + expect(finding).toBeDefined(); + expect(finding?.selector).toBe(".headline"); + }); + + it("does not warn when another-class drop-shadow selector needs an unmatched ancestor", () => { + const html = baseHtml( + '
TEXT
', + `${textureCss}.card .headline { filter: drop-shadow(1px 2px 1px black); }`, + ); + + const result = lintHyperframeHtml(html); + const finding = result.findings.find((item) => item.code === "texture_drop_shadow_on_text"); + + expect(finding).toBeUndefined(); + }); +}); diff --git a/packages/core/src/lint/rules/textures.ts b/packages/core/src/lint/rules/textures.ts new file mode 100644 index 000000000..4c1c8d4a2 --- /dev/null +++ b/packages/core/src/lint/rules/textures.ts @@ -0,0 +1,223 @@ +import postcss from "postcss"; +import type { LintContext, HyperframeLintFinding, OpenTag } from "../context"; +import { readAttr, truncateSnippet } from "../utils"; + +const TEXTURE_BASE_CLASS = "hf-texture-text"; +const TEXTURE_CLASS_PREFIX = "hf-texture-"; + +type DropShadowRule = { + selector: string; + directlyTargetsTexture: boolean; +}; + +function classNames(tag: OpenTag): string[] { + return (readAttr(tag.raw, "class") ?? "").split(/\s+/).filter(Boolean); +} + +function isTextureMaterialClass(className: string): boolean { + return className.startsWith(TEXTURE_CLASS_PREFIX) && className !== TEXTURE_BASE_CLASS; +} + +function hasInlineMaskImage(tag: OpenTag): boolean { + const style = readAttr(tag.raw, "style") ?? ""; + return /\b(?:-webkit-)?mask-image\s*:/i.test(style); +} + +function hasInlineDropShadow(tag: OpenTag): boolean { + const style = readAttr(tag.raw, "style") ?? ""; + return /\bfilter\s*:\s*[^;]*\bdrop-shadow\s*\(/i.test(style); +} + +function classNamesInSelector(selector: string): string[] { + const classes = new Set(); + const pattern = /\.([A-Za-z_][\w-]*)/g; + let match: RegExpExecArray | null; + while ((match = pattern.exec(selector)) !== null) { + const className = match[1]; + if (!className) continue; + classes.add(className); + } + return [...classes]; +} + +function textureClassesInSelector(selector: string): string[] { + return classNamesInSelector(selector).filter(isTextureMaterialClass); +} + +function simpleSelectorMatchesTag(selector: string, tag: OpenTag, tagClasses: string[]): boolean { + const trimmed = selector.trim(); + const simpleSelectorPattern = /^(?:[A-Za-z][\w-]*)?(?:\.[A-Za-z_][\w-]*)+$/; + if (!simpleSelectorPattern.test(trimmed)) return false; + + const typeMatch = /^([A-Za-z][\w-]*)/.exec(trimmed); + if (typeMatch && typeMatch[1]!.toLowerCase() !== tag.name) return false; + + const selectorClasses = classNamesInSelector(trimmed); + return ( + selectorClasses.length > 0 && + selectorClasses.every((className) => tagClasses.includes(className)) + ); +} + +function collectTextureCss(styles: LintContext["styles"]): { + definedTextureClasses: Set; + dropShadowRules: DropShadowRule[]; +} { + const definedTextureClasses = new Set(); + const dropShadowRules: DropShadowRule[] = []; + const roots: postcss.Root[] = []; + + for (const style of styles) { + let root: postcss.Root; + try { + root = postcss.parse(style.content); + } catch { + continue; + } + roots.push(root); + + root.walkRules((rule) => { + const selectors = rule.selectors ?? []; + let hasMaskImage = false; + + for (const node of rule.nodes ?? []) { + if (node.type !== "decl") continue; + const prop = node.prop.toLowerCase(); + if (prop === "mask-image" || prop === "-webkit-mask-image") hasMaskImage = true; + } + + if (hasMaskImage) { + for (const selector of selectors) { + for (const className of textureClassesInSelector(selector)) { + definedTextureClasses.add(className); + } + } + } + }); + } + + for (const root of roots) { + root.walkRules((rule) => { + const selectors = rule.selectors ?? []; + let hasDropShadow = false; + + for (const node of rule.nodes ?? []) { + if (node.type !== "decl") continue; + if (node.prop.toLowerCase() === "filter" && /\bdrop-shadow\s*\(/i.test(node.value)) { + hasDropShadow = true; + } + } + + if (hasDropShadow) { + for (const selector of selectors) { + const targetsBaseClass = /\.hf-texture-text\b/.test(selector); + const targetsDefinedTextureClass = textureClassesInSelector(selector).some((className) => + definedTextureClasses.has(className), + ); + dropShadowRules.push({ + selector, + directlyTargetsTexture: targetsBaseClass || targetsDefinedTextureClass, + }); + } + } + }); + } + + return { definedTextureClasses, dropShadowRules }; +} + +export const textureRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [ + ({ tags, styles }) => { + const findings: HyperframeLintFinding[] = []; + const { definedTextureClasses, dropShadowRules } = collectTextureCss(styles); + + for (const { selector, directlyTargetsTexture } of dropShadowRules) { + if (!directlyTargetsTexture) continue; + findings.push({ + code: "texture_drop_shadow_on_text", + severity: "warning", + message: "Drop shadow is applied directly to textured text.", + selector, + fixHint: + "Wrap the textured text and apply `filter: drop-shadow(...)` to the wrapper, not the `hf-texture-text` element.", + }); + } + + for (const tag of tags) { + if (tag.name === "style" || tag.name === "script") continue; + + const classes = classNames(tag); + if (classes.length === 0) continue; + + const hasBaseClass = classes.includes(TEXTURE_BASE_CLASS); + const textureClasses = classes.filter(isTextureMaterialClass); + + if (textureClasses.length > 0 && !hasBaseClass) { + findings.push({ + code: "texture_class_missing_base", + severity: "warning", + message: `Texture material class \`${textureClasses[0]}\` is used without \`${TEXTURE_BASE_CLASS}\`.`, + elementId: readAttr(tag.raw, "id") || undefined, + fixHint: `Add \`${TEXTURE_BASE_CLASS}\` alongside the material class, for example \`class="${TEXTURE_BASE_CLASS} ${textureClasses[0]}"\`.`, + snippet: truncateSnippet(tag.raw), + }); + } + + if (hasBaseClass && textureClasses.length === 0 && !hasInlineMaskImage(tag)) { + findings.push({ + code: "texture_text_missing_mask", + severity: "warning", + message: `\`${TEXTURE_BASE_CLASS}\` is used without a texture material class or custom mask image.`, + elementId: readAttr(tag.raw, "id") || undefined, + fixHint: + "Add a material class such as `hf-texture-lava`, or set `mask-image` and `-webkit-mask-image` on the element.", + snippet: truncateSnippet(tag.raw), + }); + } + + for (const textureClass of textureClasses) { + if (definedTextureClasses.has(textureClass)) continue; + findings.push({ + code: "texture_class_unknown", + severity: "warning", + message: `Texture material class \`${textureClass}\` is not defined by local CSS.`, + elementId: readAttr(tag.raw, "id") || undefined, + fixHint: + "Paste the Texture Mask Text component `` block into the composition, or fix the texture class typo.", + snippet: truncateSnippet(tag.raw), + }); + } + + if (hasBaseClass) { + for (const rule of dropShadowRules) { + if (rule.directlyTargetsTexture) continue; + if (!simpleSelectorMatchesTag(rule.selector, tag, classes)) continue; + findings.push({ + code: "texture_drop_shadow_on_text", + severity: "warning", + message: "Drop shadow is applied directly to textured text.", + selector: rule.selector, + elementId: readAttr(tag.raw, "id") || undefined, + fixHint: + "Wrap the textured text and apply `filter: drop-shadow(...)` to the wrapper, not the `hf-texture-text` element.", + snippet: truncateSnippet(tag.raw), + }); + } + } + + if (hasBaseClass && hasInlineDropShadow(tag)) { + findings.push({ + code: "texture_drop_shadow_on_text", + severity: "warning", + message: "Drop shadow is applied directly to textured text.", + elementId: readAttr(tag.raw, "id") || undefined, + fixHint: + "Wrap the textured text and apply `filter: drop-shadow(...)` to the wrapper, not the `hf-texture-text` element.", + snippet: truncateSnippet(tag.raw), + }); + } + } + + return findings; + }, +]; diff --git a/packages/core/src/registry/catalogGeneratorInstructions.test.ts b/packages/core/src/registry/catalogGeneratorInstructions.test.ts new file mode 100644 index 000000000..5871339b4 --- /dev/null +++ b/packages/core/src/registry/catalogGeneratorInstructions.test.ts @@ -0,0 +1,18 @@ +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const here = dirname(fileURLToPath(import.meta.url)); +const generatorSource = readFileSync( + resolve(here, "../../../../scripts/generate-catalog-pages.ts"), + "utf-8", +); + +describe("catalog generator texture instructions", () => { + it("pins the unambiguous texture style-block copy instruction", () => { + expect(generatorSource).toContain("paste the real + + +
+
+
+
Texture Mask Text
+
66 ambientCG luminance masks
+
+
+
+ + + +
+ + diff --git a/registry/components/texture-mask-text/masks/asphalt-031.png b/registry/components/texture-mask-text/masks/asphalt-031.png new file mode 100644 index 000000000..e8ef7b22c Binary files /dev/null and b/registry/components/texture-mask-text/masks/asphalt-031.png differ diff --git a/registry/components/texture-mask-text/masks/bark-014.png b/registry/components/texture-mask-text/masks/bark-014.png new file mode 100644 index 000000000..b66705951 Binary files /dev/null and b/registry/components/texture-mask-text/masks/bark-014.png differ diff --git a/registry/components/texture-mask-text/masks/brick.png b/registry/components/texture-mask-text/masks/brick.png new file mode 100644 index 000000000..c6ef2e8df Binary files /dev/null and b/registry/components/texture-mask-text/masks/brick.png differ diff --git a/registry/components/texture-mask-text/masks/bricks-075-a.png b/registry/components/texture-mask-text/masks/bricks-075-a.png new file mode 100644 index 000000000..e5dbd85c8 Binary files /dev/null and b/registry/components/texture-mask-text/masks/bricks-075-a.png differ diff --git a/registry/components/texture-mask-text/masks/bricks-101.png b/registry/components/texture-mask-text/masks/bricks-101.png new file mode 100644 index 000000000..d93562bcc Binary files /dev/null and b/registry/components/texture-mask-text/masks/bricks-101.png differ diff --git a/registry/components/texture-mask-text/masks/bricks-102.png b/registry/components/texture-mask-text/masks/bricks-102.png new file mode 100644 index 000000000..34972a755 Binary files /dev/null and b/registry/components/texture-mask-text/masks/bricks-102.png differ diff --git a/registry/components/texture-mask-text/masks/bricks-104.png b/registry/components/texture-mask-text/masks/bricks-104.png new file mode 100644 index 000000000..eef6bb632 Binary files /dev/null and b/registry/components/texture-mask-text/masks/bricks-104.png differ diff --git a/registry/components/texture-mask-text/masks/carpet.png b/registry/components/texture-mask-text/masks/carpet.png new file mode 100644 index 000000000..63fd6d53f Binary files /dev/null and b/registry/components/texture-mask-text/masks/carpet.png differ diff --git a/registry/components/texture-mask-text/masks/concrete-034.png b/registry/components/texture-mask-text/masks/concrete-034.png new file mode 100644 index 000000000..4e021bcad Binary files /dev/null and b/registry/components/texture-mask-text/masks/concrete-034.png differ diff --git a/registry/components/texture-mask-text/masks/concrete-042-a.png b/registry/components/texture-mask-text/masks/concrete-042-a.png new file mode 100644 index 000000000..1099dac00 Binary files /dev/null and b/registry/components/texture-mask-text/masks/concrete-042-a.png differ diff --git a/registry/components/texture-mask-text/masks/concrete-046.png b/registry/components/texture-mask-text/masks/concrete-046.png new file mode 100644 index 000000000..170028247 Binary files /dev/null and b/registry/components/texture-mask-text/masks/concrete-046.png differ diff --git a/registry/components/texture-mask-text/masks/concrete-047-a.png b/registry/components/texture-mask-text/masks/concrete-047-a.png new file mode 100644 index 000000000..f50ea3b53 Binary files /dev/null and b/registry/components/texture-mask-text/masks/concrete-047-a.png differ diff --git a/registry/components/texture-mask-text/masks/concrete.png b/registry/components/texture-mask-text/masks/concrete.png new file mode 100644 index 000000000..f0e97a598 Binary files /dev/null and b/registry/components/texture-mask-text/masks/concrete.png differ diff --git a/registry/components/texture-mask-text/masks/diamond-plate-009.png b/registry/components/texture-mask-text/masks/diamond-plate-009.png new file mode 100644 index 000000000..81ee39130 Binary files /dev/null and b/registry/components/texture-mask-text/masks/diamond-plate-009.png differ diff --git a/registry/components/texture-mask-text/masks/fabric-080.png b/registry/components/texture-mask-text/masks/fabric-080.png new file mode 100644 index 000000000..801e3395e Binary files /dev/null and b/registry/components/texture-mask-text/masks/fabric-080.png differ diff --git a/registry/components/texture-mask-text/masks/fabric-083.png b/registry/components/texture-mask-text/masks/fabric-083.png new file mode 100644 index 000000000..e24cf6ae8 Binary files /dev/null and b/registry/components/texture-mask-text/masks/fabric-083.png differ diff --git a/registry/components/texture-mask-text/masks/grass-001.png b/registry/components/texture-mask-text/masks/grass-001.png new file mode 100644 index 000000000..f1096a690 Binary files /dev/null and b/registry/components/texture-mask-text/masks/grass-001.png differ diff --git a/registry/components/texture-mask-text/masks/grass-004.png b/registry/components/texture-mask-text/masks/grass-004.png new file mode 100644 index 000000000..b89067723 Binary files /dev/null and b/registry/components/texture-mask-text/masks/grass-004.png differ diff --git a/registry/components/texture-mask-text/masks/grass-005.png b/registry/components/texture-mask-text/masks/grass-005.png new file mode 100644 index 000000000..f33f1eaf1 Binary files /dev/null and b/registry/components/texture-mask-text/masks/grass-005.png differ diff --git a/registry/components/texture-mask-text/masks/ground-037.png b/registry/components/texture-mask-text/masks/ground-037.png new file mode 100644 index 000000000..e85e5d952 Binary files /dev/null and b/registry/components/texture-mask-text/masks/ground-037.png differ diff --git a/registry/components/texture-mask-text/masks/ground-054.png b/registry/components/texture-mask-text/masks/ground-054.png new file mode 100644 index 000000000..e6cdb8f46 Binary files /dev/null and b/registry/components/texture-mask-text/masks/ground-054.png differ diff --git a/registry/components/texture-mask-text/masks/ground-068.png b/registry/components/texture-mask-text/masks/ground-068.png new file mode 100644 index 000000000..a8d017a21 Binary files /dev/null and b/registry/components/texture-mask-text/masks/ground-068.png differ diff --git a/registry/components/texture-mask-text/masks/ground-080.png b/registry/components/texture-mask-text/masks/ground-080.png new file mode 100644 index 000000000..21a584b4d Binary files /dev/null and b/registry/components/texture-mask-text/masks/ground-080.png differ diff --git a/registry/components/texture-mask-text/masks/ground-103.png b/registry/components/texture-mask-text/masks/ground-103.png new file mode 100644 index 000000000..e04342dc7 Binary files /dev/null and b/registry/components/texture-mask-text/masks/ground-103.png differ diff --git a/registry/components/texture-mask-text/masks/ground-104.png b/registry/components/texture-mask-text/masks/ground-104.png new file mode 100644 index 000000000..6fff79261 Binary files /dev/null and b/registry/components/texture-mask-text/masks/ground-104.png differ diff --git a/registry/components/texture-mask-text/masks/lava.png b/registry/components/texture-mask-text/masks/lava.png new file mode 100644 index 000000000..3de2ac6c2 Binary files /dev/null and b/registry/components/texture-mask-text/masks/lava.png differ diff --git a/registry/components/texture-mask-text/masks/leather-037.png b/registry/components/texture-mask-text/masks/leather-037.png new file mode 100644 index 000000000..497a868f2 Binary files /dev/null and b/registry/components/texture-mask-text/masks/leather-037.png differ diff --git a/registry/components/texture-mask-text/masks/marble-012.png b/registry/components/texture-mask-text/masks/marble-012.png new file mode 100644 index 000000000..d578034f8 Binary files /dev/null and b/registry/components/texture-mask-text/masks/marble-012.png differ diff --git a/registry/components/texture-mask-text/masks/marble-016.png b/registry/components/texture-mask-text/masks/marble-016.png new file mode 100644 index 000000000..3a1a7c6d9 Binary files /dev/null and b/registry/components/texture-mask-text/masks/marble-016.png differ diff --git a/registry/components/texture-mask-text/masks/metal-032.png b/registry/components/texture-mask-text/masks/metal-032.png new file mode 100644 index 000000000..dadbf8775 Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-032.png differ diff --git a/registry/components/texture-mask-text/masks/metal-038.png b/registry/components/texture-mask-text/masks/metal-038.png new file mode 100644 index 000000000..4488d924b Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-038.png differ diff --git a/registry/components/texture-mask-text/masks/metal-041-a.png b/registry/components/texture-mask-text/masks/metal-041-a.png new file mode 100644 index 000000000..4df7f9998 Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-041-a.png differ diff --git a/registry/components/texture-mask-text/masks/metal-046-b.png b/registry/components/texture-mask-text/masks/metal-046-b.png new file mode 100644 index 000000000..22190c06d Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-046-b.png differ diff --git a/registry/components/texture-mask-text/masks/metal-048-a.png b/registry/components/texture-mask-text/masks/metal-048-a.png new file mode 100644 index 000000000..4f12da06f Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-048-a.png differ diff --git a/registry/components/texture-mask-text/masks/metal-049-a.png b/registry/components/texture-mask-text/masks/metal-049-a.png new file mode 100644 index 000000000..6372c3304 Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-049-a.png differ diff --git a/registry/components/texture-mask-text/masks/metal-055-a.png b/registry/components/texture-mask-text/masks/metal-055-a.png new file mode 100644 index 000000000..acb356fd5 Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-055-a.png differ diff --git a/registry/components/texture-mask-text/masks/metal-061-b.png b/registry/components/texture-mask-text/masks/metal-061-b.png new file mode 100644 index 000000000..67459b965 Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal-061-b.png differ diff --git a/registry/components/texture-mask-text/masks/metal.png b/registry/components/texture-mask-text/masks/metal.png new file mode 100644 index 000000000..e6da05e0e Binary files /dev/null and b/registry/components/texture-mask-text/masks/metal.png differ diff --git a/registry/components/texture-mask-text/masks/onyx.png b/registry/components/texture-mask-text/masks/onyx.png new file mode 100644 index 000000000..9489f474c Binary files /dev/null and b/registry/components/texture-mask-text/masks/onyx.png differ diff --git a/registry/components/texture-mask-text/masks/painted-plaster-017.png b/registry/components/texture-mask-text/masks/painted-plaster-017.png new file mode 100644 index 000000000..572323d18 Binary files /dev/null and b/registry/components/texture-mask-text/masks/painted-plaster-017.png differ diff --git a/registry/components/texture-mask-text/masks/paving-stones-138.png b/registry/components/texture-mask-text/masks/paving-stones-138.png new file mode 100644 index 000000000..152d0a07c Binary files /dev/null and b/registry/components/texture-mask-text/masks/paving-stones-138.png differ diff --git a/registry/components/texture-mask-text/masks/paving-stones-150.png b/registry/components/texture-mask-text/masks/paving-stones-150.png new file mode 100644 index 000000000..ec7ff8057 Binary files /dev/null and b/registry/components/texture-mask-text/masks/paving-stones-150.png differ diff --git a/registry/components/texture-mask-text/masks/plaster-001.png b/registry/components/texture-mask-text/masks/plaster-001.png new file mode 100644 index 000000000..53a56bca3 Binary files /dev/null and b/registry/components/texture-mask-text/masks/plaster-001.png differ diff --git a/registry/components/texture-mask-text/masks/road-007.png b/registry/components/texture-mask-text/masks/road-007.png new file mode 100644 index 000000000..75274de20 Binary files /dev/null and b/registry/components/texture-mask-text/masks/road-007.png differ diff --git a/registry/components/texture-mask-text/masks/road-008-a.png b/registry/components/texture-mask-text/masks/road-008-a.png new file mode 100644 index 000000000..4b6c651f6 Binary files /dev/null and b/registry/components/texture-mask-text/masks/road-008-a.png differ diff --git a/registry/components/texture-mask-text/masks/road-009-c.png b/registry/components/texture-mask-text/masks/road-009-c.png new file mode 100644 index 000000000..50c4b4486 Binary files /dev/null and b/registry/components/texture-mask-text/masks/road-009-c.png differ diff --git a/registry/components/texture-mask-text/masks/road-012-a.png b/registry/components/texture-mask-text/masks/road-012-a.png new file mode 100644 index 000000000..f561c52db Binary files /dev/null and b/registry/components/texture-mask-text/masks/road-012-a.png differ diff --git a/registry/components/texture-mask-text/masks/road-012-b.png b/registry/components/texture-mask-text/masks/road-012-b.png new file mode 100644 index 000000000..f7a343cf4 Binary files /dev/null and b/registry/components/texture-mask-text/masks/road-012-b.png differ diff --git a/registry/components/texture-mask-text/masks/road-013-a.png b/registry/components/texture-mask-text/masks/road-013-a.png new file mode 100644 index 000000000..bc75b0d64 Binary files /dev/null and b/registry/components/texture-mask-text/masks/road-013-a.png differ diff --git a/registry/components/texture-mask-text/masks/rock-058.png b/registry/components/texture-mask-text/masks/rock-058.png new file mode 100644 index 000000000..80637b21f Binary files /dev/null and b/registry/components/texture-mask-text/masks/rock-058.png differ diff --git a/registry/components/texture-mask-text/masks/rock-063.png b/registry/components/texture-mask-text/masks/rock-063.png new file mode 100644 index 000000000..952556b01 Binary files /dev/null and b/registry/components/texture-mask-text/masks/rock-063.png differ diff --git a/registry/components/texture-mask-text/masks/rock.png b/registry/components/texture-mask-text/masks/rock.png new file mode 100644 index 000000000..a7f570063 Binary files /dev/null and b/registry/components/texture-mask-text/masks/rock.png differ diff --git a/registry/components/texture-mask-text/masks/snow-015.png b/registry/components/texture-mask-text/masks/snow-015.png new file mode 100644 index 000000000..33c5ea9f1 Binary files /dev/null and b/registry/components/texture-mask-text/masks/snow-015.png differ diff --git a/registry/components/texture-mask-text/masks/snow.png b/registry/components/texture-mask-text/masks/snow.png new file mode 100644 index 000000000..f7b860cdb Binary files /dev/null and b/registry/components/texture-mask-text/masks/snow.png differ diff --git a/registry/components/texture-mask-text/masks/tiles-138.png b/registry/components/texture-mask-text/masks/tiles-138.png new file mode 100644 index 000000000..d61cc6a19 Binary files /dev/null and b/registry/components/texture-mask-text/masks/tiles-138.png differ diff --git a/registry/components/texture-mask-text/masks/travertine-009.png b/registry/components/texture-mask-text/masks/travertine-009.png new file mode 100644 index 000000000..03feeeaa5 Binary files /dev/null and b/registry/components/texture-mask-text/masks/travertine-009.png differ diff --git a/registry/components/texture-mask-text/masks/wood-049.png b/registry/components/texture-mask-text/masks/wood-049.png new file mode 100644 index 000000000..bb3460b2f Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-049.png differ diff --git a/registry/components/texture-mask-text/masks/wood-051.png b/registry/components/texture-mask-text/masks/wood-051.png new file mode 100644 index 000000000..9cf0bfa38 Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-051.png differ diff --git a/registry/components/texture-mask-text/masks/wood-058.png b/registry/components/texture-mask-text/masks/wood-058.png new file mode 100644 index 000000000..0cd708224 Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-058.png differ diff --git a/registry/components/texture-mask-text/masks/wood-066.png b/registry/components/texture-mask-text/masks/wood-066.png new file mode 100644 index 000000000..cbc9d247f Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-066.png differ diff --git a/registry/components/texture-mask-text/masks/wood-092.png b/registry/components/texture-mask-text/masks/wood-092.png new file mode 100644 index 000000000..b044d573e Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-092.png differ diff --git a/registry/components/texture-mask-text/masks/wood-094.png b/registry/components/texture-mask-text/masks/wood-094.png new file mode 100644 index 000000000..09ddadfb9 Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-094.png differ diff --git a/registry/components/texture-mask-text/masks/wood-floor-051.png b/registry/components/texture-mask-text/masks/wood-floor-051.png new file mode 100644 index 000000000..435485ed3 Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-floor-051.png differ diff --git a/registry/components/texture-mask-text/masks/wood-floor-064.png b/registry/components/texture-mask-text/masks/wood-floor-064.png new file mode 100644 index 000000000..aed6c0a23 Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-floor-064.png differ diff --git a/registry/components/texture-mask-text/masks/wood-floor-070.png b/registry/components/texture-mask-text/masks/wood-floor-070.png new file mode 100644 index 000000000..f31c6059f Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood-floor-070.png differ diff --git a/registry/components/texture-mask-text/masks/wood.png b/registry/components/texture-mask-text/masks/wood.png new file mode 100644 index 000000000..b61caa385 Binary files /dev/null and b/registry/components/texture-mask-text/masks/wood.png differ diff --git a/registry/components/texture-mask-text/registry-item.json b/registry/components/texture-mask-text/registry-item.json new file mode 100644 index 000000000..cad115792 --- /dev/null +++ b/registry/components/texture-mask-text/registry-item.json @@ -0,0 +1,443 @@ +{ + "$schema": "https://hyperframes.heygen.com/schema/registry-item.json", + "name": "texture-mask-text", + "type": "hyperframes:component", + "title": "Texture Mask Text", + "description": "CSS luminance masks that cut holes through letterforms - 66 pre-built texture masks from ambientCG PBR color maps", + "tags": ["text", "texture", "mask", "effect"], + "textureGroups": [ + { + "title": "Masonry", + "items": [ + "brick", + "bricks-104", + "bricks-102", + "bricks-101", + "bricks-075-a", + "concrete", + "concrete-034", + "concrete-047-a", + "concrete-046", + "concrete-042-a", + "plaster-001", + "painted-plaster-017" + ] + }, + { + "title": "Stone", + "items": [ + "rock", + "rock-063", + "rock-058", + "onyx", + "marble-012", + "marble-016", + "travertine-009", + "paving-stones-150", + "paving-stones-138", + "tiles-138" + ] + }, + { + "title": "Ground / Road", + "items": [ + "ground-103", + "ground-037", + "ground-054", + "ground-104", + "ground-068", + "ground-080", + "road-012-a", + "road-008-a", + "road-007", + "road-013-a", + "road-012-b", + "road-009-c", + "asphalt-031" + ] + }, + { + "title": "Wood", + "items": [ + "wood", + "wood-094", + "wood-092", + "wood-051", + "wood-066", + "wood-049", + "wood-058", + "wood-floor-051", + "wood-floor-064", + "wood-floor-070", + "bark-014" + ] + }, + { + "title": "Metal", + "items": [ + "metal", + "metal-049-a", + "metal-055-a", + "metal-046-b", + "metal-061-b", + "metal-048-a", + "metal-032", + "metal-041-a", + "metal-038", + "diamond-plate-009" + ] + }, + { + "title": "Organic / Soft", + "items": [ + "lava", + "grass-005", + "grass-001", + "grass-004", + "carpet", + "fabric-083", + "snow", + "snow-015", + "leather-037", + "fabric-080" + ] + } + ], + "files": [ + { + "path": "texture-mask-text.html", + "target": "compositions/components/texture-mask-text/texture-mask-text.html", + "type": "hyperframes:snippet" + }, + { + "path": "masks/lava.png", + "target": "assets/texture-mask-text/masks/lava.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/rock.png", + "target": "assets/texture-mask-text/masks/rock.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/concrete.png", + "target": "assets/texture-mask-text/masks/concrete.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/brick.png", + "target": "assets/texture-mask-text/masks/brick.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood.png", + "target": "assets/texture-mask-text/masks/wood.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/onyx.png", + "target": "assets/texture-mask-text/masks/onyx.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/carpet.png", + "target": "assets/texture-mask-text/masks/carpet.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal.png", + "target": "assets/texture-mask-text/masks/metal.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/snow.png", + "target": "assets/texture-mask-text/masks/snow.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/bricks-104.png", + "target": "assets/texture-mask-text/masks/bricks-104.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/ground-103.png", + "target": "assets/texture-mask-text/masks/ground-103.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/grass-005.png", + "target": "assets/texture-mask-text/masks/grass-005.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/rock-063.png", + "target": "assets/texture-mask-text/masks/rock-063.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-094.png", + "target": "assets/texture-mask-text/masks/wood-094.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/road-012-a.png", + "target": "assets/texture-mask-text/masks/road-012-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-092.png", + "target": "assets/texture-mask-text/masks/wood-092.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-049-a.png", + "target": "assets/texture-mask-text/masks/metal-049-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-floor-051.png", + "target": "assets/texture-mask-text/masks/wood-floor-051.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/paving-stones-150.png", + "target": "assets/texture-mask-text/masks/paving-stones-150.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/road-008-a.png", + "target": "assets/texture-mask-text/masks/road-008-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/road-007.png", + "target": "assets/texture-mask-text/masks/road-007.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-055-a.png", + "target": "assets/texture-mask-text/masks/metal-055-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/concrete-034.png", + "target": "assets/texture-mask-text/masks/concrete-034.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/grass-001.png", + "target": "assets/texture-mask-text/masks/grass-001.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-051.png", + "target": "assets/texture-mask-text/masks/wood-051.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/asphalt-031.png", + "target": "assets/texture-mask-text/masks/asphalt-031.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-floor-064.png", + "target": "assets/texture-mask-text/masks/wood-floor-064.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/concrete-047-a.png", + "target": "assets/texture-mask-text/masks/concrete-047-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-046-b.png", + "target": "assets/texture-mask-text/masks/metal-046-b.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/grass-004.png", + "target": "assets/texture-mask-text/masks/grass-004.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/marble-012.png", + "target": "assets/texture-mask-text/masks/marble-012.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/ground-037.png", + "target": "assets/texture-mask-text/masks/ground-037.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/tiles-138.png", + "target": "assets/texture-mask-text/masks/tiles-138.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-066.png", + "target": "assets/texture-mask-text/masks/wood-066.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/concrete-046.png", + "target": "assets/texture-mask-text/masks/concrete-046.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-049.png", + "target": "assets/texture-mask-text/masks/wood-049.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/road-013-a.png", + "target": "assets/texture-mask-text/masks/road-013-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/plaster-001.png", + "target": "assets/texture-mask-text/masks/plaster-001.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/snow-015.png", + "target": "assets/texture-mask-text/masks/snow-015.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/road-012-b.png", + "target": "assets/texture-mask-text/masks/road-012-b.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/concrete-042-a.png", + "target": "assets/texture-mask-text/masks/concrete-042-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/bricks-102.png", + "target": "assets/texture-mask-text/masks/bricks-102.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/rock-058.png", + "target": "assets/texture-mask-text/masks/rock-058.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-floor-070.png", + "target": "assets/texture-mask-text/masks/wood-floor-070.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/bricks-101.png", + "target": "assets/texture-mask-text/masks/bricks-101.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/travertine-009.png", + "target": "assets/texture-mask-text/masks/travertine-009.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/ground-054.png", + "target": "assets/texture-mask-text/masks/ground-054.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/wood-058.png", + "target": "assets/texture-mask-text/masks/wood-058.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/ground-104.png", + "target": "assets/texture-mask-text/masks/ground-104.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/painted-plaster-017.png", + "target": "assets/texture-mask-text/masks/painted-plaster-017.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/paving-stones-138.png", + "target": "assets/texture-mask-text/masks/paving-stones-138.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/ground-068.png", + "target": "assets/texture-mask-text/masks/ground-068.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/bricks-075-a.png", + "target": "assets/texture-mask-text/masks/bricks-075-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/ground-080.png", + "target": "assets/texture-mask-text/masks/ground-080.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/road-009-c.png", + "target": "assets/texture-mask-text/masks/road-009-c.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/bark-014.png", + "target": "assets/texture-mask-text/masks/bark-014.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/marble-016.png", + "target": "assets/texture-mask-text/masks/marble-016.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/fabric-083.png", + "target": "assets/texture-mask-text/masks/fabric-083.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-061-b.png", + "target": "assets/texture-mask-text/masks/metal-061-b.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-048-a.png", + "target": "assets/texture-mask-text/masks/metal-048-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-032.png", + "target": "assets/texture-mask-text/masks/metal-032.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-041-a.png", + "target": "assets/texture-mask-text/masks/metal-041-a.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/metal-038.png", + "target": "assets/texture-mask-text/masks/metal-038.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/diamond-plate-009.png", + "target": "assets/texture-mask-text/masks/diamond-plate-009.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/leather-037.png", + "target": "assets/texture-mask-text/masks/leather-037.png", + "type": "hyperframes:asset" + }, + { + "path": "masks/fabric-080.png", + "target": "assets/texture-mask-text/masks/fabric-080.png", + "type": "hyperframes:asset" + } + ] +} diff --git a/registry/components/texture-mask-text/texture-mask-text.html b/registry/components/texture-mask-text/texture-mask-text.html new file mode 100644 index 000000000..1e5d5c975 --- /dev/null +++ b/registry/components/texture-mask-text/texture-mask-text.html @@ -0,0 +1,379 @@ + + + diff --git a/registry/registry.json b/registry/registry.json index 596c4494b..9becb0bbd 100644 --- a/registry/registry.json +++ b/registry/registry.json @@ -59,6 +59,10 @@ "name": "grid-pixelate-wipe", "type": "hyperframes:component" }, + { + "name": "texture-mask-text", + "type": "hyperframes:component" + }, { "name": "instagram-follow", "type": "hyperframes:block" diff --git a/scripts/generate-catalog-pages.ts b/scripts/generate-catalog-pages.ts index d95acc25b..bc235a014 100644 --- a/scripts/generate-catalog-pages.ts +++ b/scripts/generate-catalog-pages.ts @@ -38,6 +38,11 @@ interface SourceMetadata { sourcePrompt?: string; } +interface TextureGroup { + title: string; + items: string[]; +} + interface CatalogEntry { name: string; type: ItemKind; @@ -97,23 +102,233 @@ function typeDir(kind: ItemKind): string { return ITEM_TYPE_DIRS[kind === "block" ? "hyperframes:block" : "hyperframes:component"]; } -function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string { - const tags = manifest.tags ?? []; - const tagBadges = tags.map((t) => `\`${t}\``).join(" "); - const installCmd = `npx hyperframes add ${manifest.name}`; - const source = manifest as RegistryItem & SourceMetadata; +function textureGroupsFor(manifest: RegistryItem): TextureGroup[] { + if (!("textureGroups" in manifest)) return []; + const value = manifest.textureGroups; + if (!Array.isArray(value)) return []; + + return value.filter((group): group is TextureGroup => { + if (!group || typeof group !== "object") return false; + if (!("title" in group) || typeof group.title !== "string") return false; + if (!("items" in group) || !Array.isArray(group.items)) return false; + return group.items.every((item) => typeof item === "string"); + }); +} + +function textureLabel(slug: string): string { + return slug + .split("-") + .map((part) => + part.length === 1 ? part.toUpperCase() : part[0]!.toUpperCase() + part.slice(1), + ) + .join(" "); +} +function textureSampleWord(slug: string): string { + if (slug.includes("brick")) return "BRICK"; + if (slug.includes("concrete")) return "CONCRETE"; + if (slug.includes("plaster")) return "PLASTER"; + if (slug.includes("rock")) return "ROCK"; + if (slug.includes("onyx")) return "ONYX"; + if (slug.includes("marble")) return "MARBLE"; + if (slug.includes("travertine")) return "STONE"; + if (slug.includes("paving")) return "STONE"; + if (slug.includes("tiles")) return "TILE"; + if (slug.includes("ground")) return "GROUND"; + if (slug.includes("road")) return "ROAD"; + if (slug.includes("asphalt")) return "ASPHALT"; + if (slug.includes("wood-floor")) return "FLOOR"; + if (slug.includes("wood")) return "WOOD"; + if (slug.includes("bark")) return "BARK"; + if (slug.includes("diamond")) return "PLATE"; + if (slug.includes("metal")) return "METAL"; + if (slug.includes("lava")) return "LAVA"; + if (slug.includes("grass")) return "GRASS"; + if (slug.includes("carpet")) return "WOVEN"; + if (slug.includes("fabric")) return "FABRIC"; + if (slug.includes("snow")) return "SNOW"; + if (slug.includes("leather")) return "LEATHER"; + return slug.toUpperCase(); +} + +function textureMaskUrlFor(manifest: RegistryItem, texture: string): string { + return `${catalogImageBase}/components/${manifest.name}/masks/${texture}.png`; +} + +function generateTextureExamples(manifest: RegistryItem, textureGroups: TextureGroup[]): string[] { const lines: string[] = [ - "---", - `title: "${manifest.title.replace(/"/g, '\\"')}"`, - `description: "${manifest.description.replace(/"/g, '\\"')}"`, - "---", + "## Texture Examples", "", - `# ${manifest.title}`, + '
', + ]; + + for (const group of textureGroups) { + lines.push( + "
", + `

${group.title}

`, + '
', + ); + for (const item of group.items) { + const maskPath = textureMaskUrlFor(manifest, item); + const textureClass = `hf-texture-${item}`; + lines.push( + `
`, + `
${textureLabel(item)}
${textureClass}
`, + `
${textureSampleWord(item)}
`, + `
Use hf-texture-text ${textureClass}
`, + "
", + ); + } + lines.push("
", "
"); + } + + lines.push("
", ""); + return lines; +} + +function generateTextureAgentUsage( + manifest: RegistryItem, + textureGroups: TextureGroup[], +): string[] { + const firstTexture = textureGroups[0]?.items[0] ?? "brick"; + const firstClass = `hf-texture-${firstTexture}`; + const installedSnippet = `compositions/components/${manifest.name}/${manifest.name}.html`; + + return [ + "## Agent Usage", + "", + "Use this wording when asking an agent to apply a texture:", "", - manifest.description, + "```text", + `Use the ${manifest.title} catalog component.`, + "", + "1. From the project root, run:", + ` npx hyperframes add ${manifest.name}`, + "2. That command creates this installed snippet:", + ` ${installedSnippet}`, + "3. Open that file and paste the real