Files
hyperframes/packages/studio/src/hooks/useBlockCatalog.ts
T
48fcf4ada5 feat(registry): add morph-text component and text-effects catalog section (#1247)
* feat(gsap): add innerText support to GSAP inspector for counter animations (#1244)

Adds 'innerText' as a supported GSAP property so number roll-up animations
(count-up from 0 to some value) are visible and editable in the GSAP inspector
panel.

- Add 'innerText' to SUPPORTED_PROPS in gsapConstants.ts
- Add label 'Counter Value', tooltip, and step constraint (1) in
  gsapAnimationConstants.ts

The snap modifier that controls integer rounding is already preserved
verbatim via the EXTRAS_KEYS round-trip, so rounding behavior survives
edits without any additional UI changes.

Closes #1179

* feat(registry): add text-effects catalog section and morph-text component

Introduces a new "Text Effects" catalog section (below Effects) for text-focused visual components.

- Add `text-effects` BlockCategory to core registry types with violet color
- Add `text-effect` tag resolver in resolveBlockCategory (checked before generic `effect` tag)
- Tag caption-blend-difference, texture-mask-text, and morph-text with `text-effect`
- Update studio catalog order and color map to include text-effects
- Add morph-text component: gooey SVG threshold morph cycling through editable statements
  using GSAP seekable proxy pattern for deterministic/seekable rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(registry): add morph-text preview video

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(registry): fix morph-text.html formatting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(catalog): add Text Effects section and morph-text page

Moves caption-blend-difference and texture-mask-text out of Effects into a new
"Text Effects" section below it. Adds morph-text component page with install
instructions and preview video.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(registry): add demo.html for morph-text catalog preview rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(registry): address PR review feedback on morph-text and text-effects

- Restore `effect` tag on caption-blend-difference and texture-mask-text
  alongside `text-effect` so existing tag-equality searches/analytics still match
- Fix morphPause script fallback from "0.25" to "1.5" to match data attribute default
- Add Math.max(0, ...) guard to blur values (intent clarity)
- Add prefers-reduced-motion: skip morph and show first word statically
- Remove CATEGORY_ORDER record from useBlockCatalog; derive order from
  BLOCK_CATEGORIES array (single source of truth, no drift)
- Add comment to demo.html documenting its purpose (catalog preview script only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:55:13 -07:00

78 lines
2.2 KiB
TypeScript

import { useState, useEffect, useMemo } from "react";
import type { RegistryItem } from "@hyperframes/core/registry";
import {
BLOCK_CATEGORIES,
type BlockCategory,
resolveBlockCategory,
} from "../utils/blockCategories";
export type CatalogItem = RegistryItem & {
category: BlockCategory;
};
export function useBlockCatalog() {
const [blocks, setBlocks] = useState<CatalogItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [category, setCategory] = useState<BlockCategory | null>(null);
// fallow-ignore-next-line complexity
useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await fetch("/api/registry/blocks");
if (!res.ok) throw new Error("Failed to load catalog");
const data = (await res.json()) as RegistryItem[];
if (cancelled) return;
const items = data
.map((b) => ({ ...b, category: resolveBlockCategory(b.tags) }))
.sort((a, b) => {
const ia = BLOCK_CATEGORIES.findIndex((c) => c.id === a.category);
const ib = BLOCK_CATEGORIES.findIndex((c) => c.id === b.category);
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
});
setBlocks(items);
} catch (err) {
if (cancelled) return;
setError(err instanceof Error ? err.message : "Failed to load catalog");
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
const filteredBlocks = useMemo(() => {
let result = blocks;
if (category) {
result = result.filter((b) => b.category === category);
}
if (search.trim()) {
const q = search.toLowerCase();
result = result.filter(
(b) =>
b.title.toLowerCase().includes(q) ||
b.description.toLowerCase().includes(q) ||
b.category.toLowerCase().includes(q) ||
b.tags?.some((t) => t.toLowerCase().includes(q)),
);
}
return result;
}, [blocks, category, search]);
return {
blocks,
loading,
error,
search,
setSearch,
category,
setCategory,
filteredBlocks,
};
}