mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
refactor: migrate templates/ → registry/examples/ (#253)
## What PR 2/17 of the catalog system rollout. **Physical directory rename.** Stacks on #252. - `git mv templates/ registry/examples/` — all 8 example directories (`decision-tree`, `kinetic-type`, `nyt-graph`, `play-mode`, `product-promo`, `swiss-grid`, `vignelli`, `warm-grain`) plus `templates.json` - `packages/cli/src/templates/remote.ts` — `TEMPLATES_DIR` constant from `"templates"` → `"registry/examples"`, exported for regression testing - `scripts/generate-template-previews.ts` — `remoteTemplatesDir` resolved to the new path - Comment updates in `packages/cli/src/templates/generators.ts` and `packages/cli/src/commands/init.ts` - New regression test `packages/cli/src/templates/remote.test.ts` pinning the path constants so future reverts fail a test instead of silently breaking installed CLIs Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a). ## Why The current `templates/` directory is a flat "things that scaffold projects" bucket. The catalog model splits content into three tiers: **examples** (full projects — what today's templates are), **blocks** (sub-compositions), and **components** (effect snippets). `registry/examples/` is the canonical home for what was previously at `templates/`, and this PR makes room for `registry/blocks/` and `registry/components/` in future PRs without top-level clutter. ## How - `git mv` preserves file history — GitHub renders these as renames, not deletions + additions. - Remote template fetch via giget reads `TEMPLATES_DIR`, so updating that one constant is sufficient for the CLI's remote code path. - The CLI's **internal** `packages/cli/src/templates/` directory (which holds the `blank` and `_shared` bundled assets plus `generators.ts`/`remote.ts`) is a separate concept and is **not** touched here. Renaming that module belongs to PR 3 where the abstraction changes to a registry resolver. - `templates.json` keeps its existing shape and location (now at `registry/examples/templates.json`). **PR 3 will transform it** to the new `registry.json` shape introduced in PR 1 and generate a per-item `registry-item.json` for each example. Leaving the shape change to PR 3 keeps this PR a pure physical move. ## ⚠️ Breaking change for previously-installed CLIs (`hyperframes@0.1.0` – `0.3.0`) **What happens:** every published CLI version has `TEMPLATES_DIR = "templates"` baked in. After this PR lands on `main`, those CLIs will 404 on: - `raw.githubusercontent.com/heygen-com/hyperframes/main/templates/templates.json` (manifest list) — caught silently in `listRemoteTemplates`, so the template picker falls back to showing only `blank` - `github:heygen-com/hyperframes/templates/<id>#main` (giget download) — raises "Template downloaded but missing index.html" **Decision: accept the break.** Hyperframes is pre-1.0 OSS with a small installed base; complex mitigations (dual-path fetch, redirect stubs, manifest-at-old-path with empty array) add permanent maintenance cost for a one-time rename. **Rollout plan:** 1. Merge #252 (PR 1 — types & schemas) first 2. Merge this PR (#253) 3. Ship a patched CLI release (`hyperframes@0.3.1`) in the same work-day. Already-pinned old CLIs break on remote examples, but upgrading restores full functionality 4. Note the break in release notes + `CHANGELOG.md` under the `0.3.1` entry Users still on an older CLI will see the failure only if they invoke `hyperframes init` with `--template <non-blank>`; `--template blank` (bundled) continues to work offline on every version. ## Test plan - [x] `bun run test` in `packages/cli`: **57 passed** (was 55 on main, +2 regression tests for the path constants). Same 4 pre-existing failures (SRT/VTT whisper normalizer + `lintProject` clean-project test) — unchanged from main. No regressions - [x] **Manual smoke test**: `hyperframes init /tmp/x --template blank` works (bundled code path, unchanged) - [x] `bunx oxfmt --check` + `bunx oxlint`: clean - [x] `bun run typecheck` (core + studio, pre-commit hook): clean - [ ] **Manual smoke test for remote fetch (`--template warm-grain`)** — not verifiable locally before merge. Remote fetch resolves `github:heygen-com/hyperframes/registry/examples/<id>#main`, which doesn't exist until this PR lands. Will work on `main` immediately after merge. ## Breaking / migration - Internal repo path changes only. `--template` CLI flag continues to accept the same template names. - See "Breaking change for previously-installed CLIs" above — decision is to ship a simultaneous CLI release rather than add a compat shim. ## Commits 1. `d691bd1` — initial rename + CLI path constant update 2. `fc0c642` — review feedback: docstring fix, regression tests, clarifying comment in `init.ts`, export constants for testing ## Stacks on #252 — base branch. When #252 merges, this rebases onto `main`. ## Next in stack PR 3 — `feat(cli): registry resolver + installer`. Transforms `templates.json` to the new `registry.json` shape (from PR 1's schema), generates `registry-item.json` for every existing example, introduces `packages/cli/src/registry/{resolver,installer,remote}.ts`, renames the `packages/cli/src/templates/` CLI module, and refactors `init` to call through the new abstraction. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
<template id="nyt-chart-template">
|
||||
<div data-composition-id="nyt-chart" data-width="1920" data-height="1080" data-duration="15">
|
||||
<!-- Google Fonts Import -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&family=Libre+Franklin:wght@300;400;600&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<div class="chart-container">
|
||||
<!-- Header Section -->
|
||||
<div class="header">
|
||||
<h1 class="headline">Monthly Revenue vs. Conversion Rate</h1>
|
||||
<p class="subtitle">Jan–Jun 2024, in thousands</p>
|
||||
<div class="key">
|
||||
<div class="key-item">
|
||||
<div class="key-box revenue-box"></div>
|
||||
<span class="key-text">Revenue</span>
|
||||
</div>
|
||||
<div class="key-item">
|
||||
<div class="key-line conversion-line"></div>
|
||||
<span class="key-text">Conversion Rate</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SVG Chart Area -->
|
||||
<svg class="chart-svg" viewBox="0 0 1600 700" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- Gridlines Group -->
|
||||
<g class="gridlines"></g>
|
||||
|
||||
<!-- X-Axis Labels -->
|
||||
<g class="x-axis-labels"></g>
|
||||
|
||||
<!-- Bars Group (Revenue) -->
|
||||
<g class="bars-group"></g>
|
||||
|
||||
<!-- Bar Labels Group -->
|
||||
<g class="bar-labels-group"></g>
|
||||
|
||||
<!-- Line Group (Conversion Rate) -->
|
||||
<g class="line-group">
|
||||
<path class="conversion-path" fill="none" stroke="#326FA8" stroke-width="1.5" />
|
||||
<circle class="line-dot" r="5" fill="#326FA8" style="opacity: 0" />
|
||||
</g>
|
||||
|
||||
<!-- Line Labels Group -->
|
||||
<g class="line-labels-group"></g>
|
||||
|
||||
<!-- Baseline -->
|
||||
<line x1="100" y1="600" x2="1500" y2="600" stroke="black" stroke-width="1" />
|
||||
</svg>
|
||||
|
||||
<!-- Source Line -->
|
||||
<div class="source">Source: Internal analytics</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[data-composition-id="nyt-chart"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background-color: #faf9f6;
|
||||
font-family: "Libre Franklin", sans-serif;
|
||||
color: #333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .chart-container {
|
||||
width: 1600px;
|
||||
height: 900px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
transform: scale(1.15);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .header {
|
||||
margin-bottom: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .headline {
|
||||
font-family: "Libre Baskerville", serif;
|
||||
font-size: 42px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .subtitle {
|
||||
font-size: 16px;
|
||||
color: #d4d4d4;
|
||||
margin: 8px 0 16px 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .key {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .key-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .key-box {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #5c5c5c;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .key-line {
|
||||
width: 16px;
|
||||
height: 1.5px;
|
||||
background-color: #326fa8;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .key-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .chart-svg {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .source {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .grid-line {
|
||||
stroke: #e8e8e8;
|
||||
stroke-width: 0.5px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .axis-label {
|
||||
font-size: 14px;
|
||||
fill: #666;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .bar {
|
||||
fill: #5c5c5c;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .bar-label {
|
||||
font-size: 14px;
|
||||
fill: #333;
|
||||
text-anchor: middle;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="nyt-chart"] .line-label {
|
||||
font-size: 14px;
|
||||
fill: #326fa8;
|
||||
font-weight: 600;
|
||||
text-anchor: middle;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function initChart() {
|
||||
// Wait for DOM — the runtime may not have cloned the template content yet
|
||||
const svg = document.querySelector(`[data-composition-id="nyt-chart"] .chart-svg`);
|
||||
if (!svg) {
|
||||
setTimeout(initChart, 50);
|
||||
return;
|
||||
}
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Data
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
|
||||
const revenueData = [8, 12, 15, 11, 18, 22]; // in thousands
|
||||
const conversionData = [2.1, 2.8, 3.2, 2.9, 3.8, 4.2]; // in percentage
|
||||
|
||||
// Chart Dimensions
|
||||
const chartWidth = 1400;
|
||||
const chartHeight = 500;
|
||||
const startX = 100;
|
||||
const startY = 600;
|
||||
const barWidth = 80;
|
||||
const spacing = (chartWidth - barWidth * months.length) / (months.length + 1);
|
||||
|
||||
// Scales
|
||||
const maxRevenue = 25;
|
||||
const maxConversion = 5;
|
||||
|
||||
const getBarY = (val) => startY - (val / maxRevenue) * chartHeight;
|
||||
const getLineY = (val) => startY - (val / maxConversion) * chartHeight;
|
||||
const getX = (i) => startX + spacing + i * (barWidth + spacing) + barWidth / 2;
|
||||
|
||||
// 1. Setup Elements (svg already found above)
|
||||
const gridGroup = svg.querySelector(".gridlines");
|
||||
const xAxisGroup = svg.querySelector(".x-axis-labels");
|
||||
const barsGroup = svg.querySelector(".bars-group");
|
||||
const barLabelsGroup = svg.querySelector(".bar-labels-group");
|
||||
const lineLabelsGroup = svg.querySelector(".line-labels-group");
|
||||
const conversionPath = svg.querySelector(".conversion-path");
|
||||
const lineDot = svg.querySelector(".line-dot");
|
||||
|
||||
// Create Gridlines (5 lines)
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const y = startY - i * (chartHeight / 5);
|
||||
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
||||
line.setAttribute("x1", startX);
|
||||
line.setAttribute("y1", y);
|
||||
line.setAttribute("x2", startX + chartWidth);
|
||||
line.setAttribute("y2", y);
|
||||
line.setAttribute("class", "grid-line");
|
||||
gridGroup.appendChild(line);
|
||||
}
|
||||
|
||||
// Create X-Axis Labels & Bars & Bar Labels
|
||||
const linePoints = [];
|
||||
months.forEach((month, i) => {
|
||||
const x = getX(i);
|
||||
|
||||
// X-Axis Label
|
||||
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
label.setAttribute("x", x);
|
||||
label.setAttribute("y", startY + 30);
|
||||
label.setAttribute("class", "axis-label");
|
||||
label.textContent = month;
|
||||
xAxisGroup.appendChild(label);
|
||||
|
||||
// Bar
|
||||
const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
|
||||
const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
bar.setAttribute("x", x - barWidth / 2);
|
||||
bar.setAttribute("y", startY);
|
||||
bar.setAttribute("width", barWidth);
|
||||
bar.setAttribute("height", 0);
|
||||
bar.setAttribute("class", "bar");
|
||||
barsGroup.appendChild(bar);
|
||||
|
||||
// Bar Label
|
||||
const barLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
barLabel.setAttribute("x", x);
|
||||
barLabel.setAttribute("y", getBarY(revenueData[i]) - 10);
|
||||
barLabel.setAttribute("class", "bar-label");
|
||||
barLabel.textContent = `$${revenueData[i]}K`;
|
||||
barLabelsGroup.appendChild(barLabel);
|
||||
|
||||
// Line Point
|
||||
linePoints.push(`${x},${getLineY(conversionData[i])}`);
|
||||
|
||||
// Line Label
|
||||
const lineLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
lineLabel.setAttribute("x", x);
|
||||
lineLabel.setAttribute("y", getLineY(conversionData[i]) - 15);
|
||||
lineLabel.setAttribute("class", "line-label");
|
||||
lineLabel.textContent = `${conversionData[i]}%`;
|
||||
lineLabelsGroup.appendChild(lineLabel);
|
||||
});
|
||||
|
||||
// Set Path Data
|
||||
conversionPath.setAttribute("d", `M ${linePoints.join(" L ")}`);
|
||||
const pathLength = conversionPath.getTotalLength();
|
||||
conversionPath.style.strokeDasharray = pathLength;
|
||||
conversionPath.style.strokeDashoffset = pathLength;
|
||||
|
||||
// --- ANIMATION TIMELINE ---
|
||||
|
||||
// 2. Headline & Subtitle (Draw on left-to-right)
|
||||
tl.to(
|
||||
`[data-composition-id="nyt-chart"] .headline`,
|
||||
{
|
||||
clipPath: "inset(0 0% 0 0)",
|
||||
duration: 1.2,
|
||||
ease: "power1.inOut",
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
`[data-composition-id="nyt-chart"] .subtitle`,
|
||||
{
|
||||
clipPath: "inset(0 0% 0 0)",
|
||||
duration: 1.2,
|
||||
ease: "power1.inOut",
|
||||
},
|
||||
0.2,
|
||||
);
|
||||
|
||||
// 3. Inline Key
|
||||
tl.to(
|
||||
`[data-composition-id="nyt-chart"] .key`,
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.4,
|
||||
ease: "power2.out",
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
|
||||
// 4. Gridlines (Stagger bottom to top)
|
||||
tl.to(
|
||||
`[data-composition-id="nyt-chart"] .grid-line`,
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
stagger: 0.25,
|
||||
ease: "power2.out",
|
||||
},
|
||||
0.5,
|
||||
);
|
||||
|
||||
// 5. Bars (Revenue)
|
||||
const bars = barsGroup.querySelectorAll(".bar");
|
||||
const barLabels = barLabelsGroup.querySelectorAll(".bar-label");
|
||||
|
||||
bars.forEach((bar, i) => {
|
||||
const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
|
||||
const startTime = 1.5 + i * 0.5; // 0.8s duration, 0.3s overlap (0.8 - 0.3 = 0.5 step)
|
||||
|
||||
tl.to(
|
||||
bar,
|
||||
{
|
||||
attr: {
|
||||
height: barHeight,
|
||||
y: startY - barHeight,
|
||||
},
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
},
|
||||
startTime,
|
||||
);
|
||||
|
||||
// 6. Bar Labels
|
||||
tl.to(
|
||||
barLabels[i],
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.3,
|
||||
ease: "power2.out",
|
||||
},
|
||||
startTime + 0.8,
|
||||
);
|
||||
});
|
||||
|
||||
// 7. Line (Conversion Rate)
|
||||
// Starts 0.5s after the last bar begins
|
||||
const lastBarStartTime = 1.5 + (months.length - 1) * 0.5;
|
||||
const lineStartTime = lastBarStartTime + 0.5;
|
||||
|
||||
tl.to(
|
||||
conversionPath,
|
||||
{
|
||||
strokeDashoffset: 0,
|
||||
duration: 3,
|
||||
ease: "none",
|
||||
},
|
||||
lineStartTime,
|
||||
);
|
||||
|
||||
// 8. Line Dot & 9. Line Labels
|
||||
// We need to sync the dot and labels with the path drawing
|
||||
tl.set(
|
||||
lineDot,
|
||||
{ opacity: 1, attr: { cx: getX(0), cy: getLineY(conversionData[0]) } },
|
||||
lineStartTime,
|
||||
);
|
||||
|
||||
const lineLabels = lineLabelsGroup.querySelectorAll(".line-label");
|
||||
months.forEach((_, i) => {
|
||||
if (i > 0) {
|
||||
const x = getX(i);
|
||||
const y = getLineY(conversionData[i]);
|
||||
tl.to(
|
||||
lineDot,
|
||||
{
|
||||
attr: {
|
||||
cx: x,
|
||||
cy: y,
|
||||
},
|
||||
duration: 3 / (months.length - 1),
|
||||
ease: "none",
|
||||
},
|
||||
lineStartTime + ((i - 1) / (months.length - 1)) * 3,
|
||||
);
|
||||
}
|
||||
|
||||
const labelTime = lineStartTime + (i / (months.length - 1)) * 3 + 0.2;
|
||||
|
||||
// Set initial text to 0.0%
|
||||
lineLabels[i].textContent = "0.0%";
|
||||
|
||||
// Fade in
|
||||
tl.to(
|
||||
lineLabels[i],
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.1,
|
||||
ease: "power2.out",
|
||||
},
|
||||
labelTime,
|
||||
);
|
||||
|
||||
// Animate value
|
||||
const dummy = { val: 0 };
|
||||
tl.to(
|
||||
dummy,
|
||||
{
|
||||
val: conversionData[i],
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
onUpdate: () => {
|
||||
lineLabels[i].textContent = dummy.val.toFixed(1) + "%";
|
||||
},
|
||||
},
|
||||
labelTime,
|
||||
);
|
||||
});
|
||||
|
||||
// 11. Source Line
|
||||
tl.to(
|
||||
`[data-composition-id="nyt-chart"] .source`,
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
ease: "power2.out",
|
||||
},
|
||||
">+0.5",
|
||||
);
|
||||
|
||||
// Register timeline
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["nyt-chart"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user