feat(core): add fitTextFontSize utility for pixel-accurate text measurement (#152)

Add @chenglou/pretext dependency and fitTextFontSize() utility that uses
canvas measureText to compute the largest font size that fits text within
a given width. Replaces character-count heuristics with actual font-aware
measurement.

- New fitTextFontSize() in @hyperframes/core/text, exposed on window.__hyperframes
- Generalized for all text elements (captions, titles, etc.), not just captions
- Unit tests (mocked pretext) + browser integration test (real Chromium canvas)
- Updated captions skill docs with usage, exit guarantee, and self-lint patterns

Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-30 20:14:13 -07:00
committed by GitHub
co-authored by James Claude Opus 4.6
parent dac304ed9f
commit ef6225da1d
12 changed files with 781 additions and 32 deletions
+11 -5
View File
@@ -19,7 +19,7 @@
},
"packages/cli": {
"name": "@hyperframes/cli",
"version": "0.1.10",
"version": "0.1.13",
"bin": {
"hyperframes": "./dist/cli.js",
},
@@ -53,11 +53,15 @@
"tsup": "^8.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0",
"vitest": "^3.2.4",
},
},
"packages/core": {
"name": "@hyperframes/core",
"version": "0.1.10",
"version": "0.1.13",
"dependencies": {
"@chenglou/pretext": "^0.0.3",
},
"devDependencies": {
"@types/jsdom": "^28.0.0",
"@types/node": "^24.10.13",
@@ -80,7 +84,7 @@
},
"packages/engine": {
"name": "@hyperframes/engine",
"version": "0.1.6",
"version": "0.1.13",
"dependencies": {
"@hono/node-server": "^1.13.0",
"@hyperframes/core": "workspace:^",
@@ -97,7 +101,7 @@
},
"packages/producer": {
"name": "@hyperframes/producer",
"version": "0.1.6",
"version": "0.1.13",
"dependencies": {
"@fontsource/archivo-black": "^5.2.8",
"@fontsource/eb-garamond": "^5.2.7",
@@ -127,7 +131,7 @@
},
"packages/studio": {
"name": "@hyperframes/studio",
"version": "0.1.10",
"version": "0.1.13",
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
@@ -219,6 +223,8 @@
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "3.2.1" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
"@chenglou/pretext": ["@chenglou/pretext@0.0.3", "", {}, "sha512-RQmqMqUAPRCyv4R3LlRi/ao6KbNWYclqLA+V1HS7sWgyUUbjn3JmmlfXZSY/BjM4rbmIaMSyIVisYocYGYftiQ=="],
"@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="],
"@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="],
+11
View File
@@ -32,6 +32,10 @@
"./studio-api": {
"import": "./src/studio-api/index.ts",
"types": "./src/studio-api/index.ts"
},
"./text": {
"import": "./src/text/index.ts",
"types": "./src/text/index.ts"
}
},
"publishConfig": {
@@ -53,6 +57,10 @@
"./studio-api": {
"import": "./dist/studio-api/index.js",
"types": "./dist/studio-api/index.d.ts"
},
"./text": {
"import": "./dist/text/index.js",
"types": "./dist/text/index.d.ts"
}
},
"main": "./dist/index.js",
@@ -80,6 +88,9 @@
"debug:timeline": "tsx scripts/debug-timeline.ts",
"prepublishOnly": "echo skip"
},
"dependencies": {
"@chenglou/pretext": "^0.0.3"
},
"devDependencies": {
"@types/jsdom": "^28.0.0",
"@types/node": "^24.10.13",
@@ -18,6 +18,8 @@ const requiredSnippets = [
"hf-preview",
"hf-parent",
"renderSeek",
"__hyperframes",
"fitTextFontSize",
];
for (const snippet of requiredSnippets) {
+4
View File
@@ -154,3 +154,7 @@ export type {
export type { FrameAdapter, FrameAdapterContext } from "./adapters/types";
export type { GSAPTimelineLike, CreateGSAPFrameAdapterOptions } from "./adapters/gsap";
export { createGSAPFrameAdapter } from "./adapters/gsap";
// Text measurement
export { fitTextFontSize } from "./text/index.js";
export type { FitTextOptions, FitTextResult } from "./text/index.js";
+10
View File
@@ -1,13 +1,23 @@
import { initSandboxRuntimeModular } from "./init";
import { fitTextFontSize } from "../text/fitTextFontSize";
type HyperframeWindow = Window & {
__hyperframeRuntimeBootstrapped?: boolean;
__hyperframes?: {
fitTextFontSize: typeof fitTextFontSize;
};
};
// Inline composition scripts can run before DOMContentLoaded.
// Ensure timeline registry exists at script evaluation time.
(window as HyperframeWindow).__timelines = (window as HyperframeWindow).__timelines || {};
// Expose text utilities immediately so composition scripts can use them
// before DOMContentLoaded (font sizing runs during script evaluation).
(window as HyperframeWindow).__hyperframes = {
fitTextFontSize,
};
function bootstrapHyperframeRuntime(): void {
const win = window as HyperframeWindow;
if (win.__hyperframeRuntimeBootstrapped) {
@@ -0,0 +1,73 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock @chenglou/pretext since jsdom lacks real canvas measureText accuracy.
vi.mock("@chenglou/pretext", () => ({
prepare: vi.fn((_text: string, _font: string) => ({ __mock: true, font: _font })),
layout: vi.fn(),
}));
import { fitTextFontSize } from "./fitTextFontSize.js";
import { prepare, layout } from "@chenglou/pretext";
const mockLayout = vi.mocked(layout);
const mockPrepare = vi.mocked(prepare);
describe("fitTextFontSize", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns base font size when text fits at base size", () => {
mockLayout.mockReturnValue({ height: 90, lineCount: 1 });
const result = fitTextFontSize("short text");
expect(result).toEqual({ fontSize: 78, fits: true });
expect(mockPrepare).toHaveBeenCalledTimes(1);
expect(mockLayout).toHaveBeenCalledTimes(1);
});
it("shrinks font size when text wraps at base size", () => {
mockLayout
.mockReturnValueOnce({ height: 180, lineCount: 2 })
.mockReturnValueOnce({ height: 180, lineCount: 2 })
.mockReturnValueOnce({ height: 90, lineCount: 1 });
const result = fitTextFontSize("this is a much wider piece of text");
expect(result).toEqual({ fontSize: 74, fits: true });
expect(mockPrepare).toHaveBeenCalledTimes(3);
});
it("returns minFontSize with fits: false when text never fits", () => {
mockLayout.mockReturnValue({ height: 180, lineCount: 2 });
const result = fitTextFontSize("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW");
expect(result).toEqual({ fontSize: 42, fits: false });
expect(mockPrepare).toHaveBeenCalledTimes(19); // (78 - 42) / 2 + 1
});
it("respects custom options", () => {
mockLayout.mockReturnValue({ height: 60, lineCount: 1 });
const result = fitTextFontSize("hello", {
baseFontSize: 60,
minFontSize: 30,
fontWeight: 700,
fontFamily: "Inter",
maxWidth: 800,
step: 4,
});
expect(result).toEqual({ fontSize: 60, fits: true });
expect(mockPrepare).toHaveBeenCalledWith("hello", "700 60px Inter");
expect(mockLayout).toHaveBeenCalledWith(expect.anything(), 800, 72);
});
it("passes correct font string to prepare for each size step", () => {
mockLayout
.mockReturnValueOnce({ height: 180, lineCount: 2 })
.mockReturnValueOnce({ height: 90, lineCount: 1 });
fitTextFontSize("test", {
baseFontSize: 80,
step: 10,
fontWeight: 900,
fontFamily: "Outfit",
});
expect(mockPrepare).toHaveBeenNthCalledWith(1, "test", "900 80px Outfit");
expect(mockPrepare).toHaveBeenNthCalledWith(2, "test", "900 70px Outfit");
});
});
+48
View File
@@ -0,0 +1,48 @@
import { prepare, layout } from "@chenglou/pretext";
export type FitTextOptions = {
/** Container width in px */
maxWidth: number;
/** Starting font size in px */
baseFontSize: number;
/** Floor font size in px */
minFontSize: number;
/** CSS font-weight */
fontWeight: number;
/** CSS font-family */
fontFamily: string;
/** Decrement step in px */
step: number;
};
export type FitTextResult = {
/** The computed font size that fits */
fontSize: number;
/** True if text fits at >= minFontSize */
fits: boolean;
};
const DEFAULTS: FitTextOptions = {
maxWidth: 1600,
baseFontSize: 78,
minFontSize: 42,
fontWeight: 900,
fontFamily: "Outfit",
step: 2,
};
export function fitTextFontSize(text: string, options?: Partial<FitTextOptions>): FitTextResult {
const opts = { ...DEFAULTS, ...options };
const lineHeightRatio = 1.2;
for (let size = opts.baseFontSize; size >= opts.minFontSize; size -= opts.step) {
const font = `${opts.fontWeight} ${size}px ${opts.fontFamily}`;
const prepared = prepare(text, font);
const { lineCount } = layout(prepared, opts.maxWidth, size * lineHeightRatio);
if (lineCount <= 1) {
return { fontSize: size, fits: true };
}
}
return { fontSize: opts.minFontSize, fits: false };
}
+2
View File
@@ -0,0 +1,2 @@
export { fitTextFontSize } from "./fitTextFontSize.js";
export type { FitTextOptions, FitTextResult } from "./fitTextFontSize.js";
@@ -0,0 +1,131 @@
/**
* Browser integration test for fitTextFontSize.
*
* Launches headless Chrome, loads the runtime IIFE into a page,
* and verifies that window.__hyperframes.fitTextFontSize produces
* correct results with real canvas measureText.
*
* Requires: puppeteer (dep of @hyperframes/engine)
* Run: cd packages/engine && npx tsx scripts/test-fitTextFontSize-browser.ts
*/
import { buildHyperframesRuntimeScript } from "../../core/src/inline-scripts/hyperframesRuntime.engine";
function assert(condition: unknown, message: string): void {
if (!condition) {
throw new Error(`FAIL: ${message}`);
}
}
async function main() {
// Dynamic import — puppeteer is a monorepo dep, not a core dep
let puppeteer;
try {
puppeteer = (await import("puppeteer")).default;
} catch {
console.log(
JSON.stringify({
event: "fitTextFontSize_browser_test_skipped",
reason: "puppeteer not available",
}),
);
return;
}
const runtimeSource = buildHyperframesRuntimeScript({ minify: false });
const html = `<!DOCTYPE html>
<html><head>
<style>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@900&display=block');
</style>
</head><body>
<script>${runtimeSource}</script>
<script>
window.__testResults = {};
// Test 1: Short text should fit at base size
var r1 = window.__hyperframes.fitTextFontSize("HI");
window.__testResults.shortText = r1;
// Test 2: Wide text should shrink below base size but still fit at 1600px
var r2 = window.__hyperframes.fitTextFontSize(
"CONGRATULATIONS TO EVERYBODY IN THE WORLD",
{ fontFamily: "sans-serif", fontWeight: 900, maxWidth: 1600 }
);
window.__testResults.wideText = r2;
// Test 3: Extremely wide text that can't fit should return minFontSize
var r3 = window.__hyperframes.fitTextFontSize(
"WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW",
{ fontFamily: "sans-serif", fontWeight: 900, maxWidth: 400 }
);
window.__testResults.extremeText = r3;
// Test 4: Function exists and is callable
window.__testResults.exists = typeof window.__hyperframes.fitTextFontSize === "function";
</script>
</body></html>`;
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0", timeout: 10000 });
// Wait for font to potentially load (best effort — sans-serif fallback is fine for testing)
await new Promise((r) => setTimeout(r, 1000));
const results = await page.evaluate(() => (window as any).__testResults);
// Test 1: Short text fits at base size (78px default)
assert(results.exists === true, "fitTextFontSize should exist on window.__hyperframes");
assert(
results.shortText.fits === true,
`Short text should fit, got fits=${results.shortText.fits}`,
);
assert(
results.shortText.fontSize === 78,
`Short text should use base size 78, got ${results.shortText.fontSize}`,
);
// Test 2: Wide text should shrink below base size
assert(
results.wideText.fits === true,
`Wide text should still fit, got fits=${results.wideText.fits}`,
);
assert(
results.wideText.fontSize < 78,
`Wide text should shrink below 78, got ${results.wideText.fontSize}`,
);
// Test 3: Extreme text should hit floor
assert(
results.extremeText.fontSize === 42,
`Extreme text should hit minFontSize 42, got ${results.extremeText.fontSize}`,
);
assert(
results.extremeText.fits === false,
`Extreme text should not fit, got fits=${results.extremeText.fits}`,
);
console.log(
JSON.stringify({
event: "fitTextFontSize_browser_test_passed",
shortText: results.shortText,
wideText: results.wideText,
extremeText: results.extremeText,
}),
);
} finally {
await browser.close();
}
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
+422 -17
View File
@@ -40,7 +40,41 @@ importers:
version: 5.9.3
packages/cli:
dependencies:
'@hono/node-server':
specifier: ^1.8.0
version: 1.19.11(hono@4.12.9)
'@puppeteer/browsers':
specifier: ^2.13.0
version: 2.13.0
adm-zip:
specifier: ^0.5.16
version: 0.5.16
cheerio:
specifier: ^1.2.0
version: 1.2.0
citty:
specifier: ^0.2.1
version: 0.2.1
esbuild:
specifier: ^0.25.0
version: 0.25.12
hono:
specifier: ^4.0.0
version: 4.12.9
mime-types:
specifier: ^3.0.2
version: 3.0.2
open:
specifier: ^10.0.0
version: 10.2.0
puppeteer-core:
specifier: ^24.39.1
version: 24.40.0
devDependencies:
'@clack/prompts':
specifier: ^1.1.0
version: 1.1.0
'@hyperframes/core':
specifier: workspace:*
version: link:../core
@@ -50,14 +84,39 @@ importers:
'@hyperframes/producer':
specifier: workspace:*
version: link:../producer
'@types/adm-zip':
specifier: ^0.5.7
version: 0.5.8
'@types/mime-types':
specifier: ^3.0.1
version: 3.0.1
'@types/node':
specifier: ^22.0.0
version: 25.5.0
version: 22.19.15
linkedom:
specifier: ^0.18.12
version: 0.18.12
picocolors:
specifier: ^1.1.1
version: 1.1.1
tsup:
specifier: ^8.0.0
version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
tsx:
specifier: ^4.0.0
version: 4.21.0
typescript:
specifier: ^5.0.0
version: 5.9.3
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/node@25.5.0)(jsdom@29.0.1)
version: 3.2.4(@types/node@22.19.15)(jsdom@29.0.1)
packages/core:
dependencies:
'@chenglou/pretext':
specifier: ^0.0.3
version: 0.0.3
devDependencies:
'@types/jsdom':
specifier: ^28.0.0
@@ -230,15 +289,15 @@ importers:
codemirror:
specifier: ^6.0.1
version: 6.0.2
motion:
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
specifier: ^18.0.0 || ^19.0.0
version: 19.2.4
react-dom:
specifier: ^18.0.0 || ^19.0.0
version: 19.2.4(react@19.2.4)
zustand:
specifier: ^4.0.0 || ^5.0.0
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)
devDependencies:
'@types/react':
specifier: ^19.0.0
@@ -255,6 +314,9 @@ importers:
postcss:
specifier: ^8.4.0
version: 8.5.8
puppeteer-core:
specifier: ^24.40.0
version: 24.40.0
tailwindcss:
specifier: ^3.4.0
version: 3.4.19(tsx@4.21.0)(yaml@2.8.3)
@@ -264,6 +326,12 @@ importers:
vite:
specifier: ^5.0.0
version: 5.4.21(@types/node@25.5.0)
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/node@25.5.0)(jsdom@29.0.1)
zustand:
specifier: ^5.0.0
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)
packages:
@@ -381,6 +449,15 @@ packages:
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
hasBin: true
'@chenglou/pretext@0.0.3':
resolution: {integrity: sha512-RQmqMqUAPRCyv4R3LlRi/ao6KbNWYclqLA+V1HS7sWgyUUbjn3JmmlfXZSY/BjM4rbmIaMSyIVisYocYGYftiQ==}
'@clack/core@1.1.0':
resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==}
'@clack/prompts@1.1.0':
resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==}
'@codemirror/autocomplete@6.20.1':
resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==}
@@ -1706,6 +1783,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/adm-zip@0.5.8':
resolution: {integrity: sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1730,6 +1810,9 @@ packages:
'@types/jsdom@28.0.1':
resolution: {integrity: sha512-GJq2QE4TAZ5ajSoCasn5DOFm8u1mI3tIFvM5tIq3W5U/RTB6gsHwc6Yhpl91X9VSDOUVblgXmG+2+sSvFQrdlw==}
'@types/mime-types@3.0.1':
resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==}
'@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
@@ -1797,6 +1880,15 @@ packages:
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
adm-zip@0.5.16:
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
engines: {node: '>=12.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@@ -1948,6 +2040,16 @@ packages:
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
@@ -1986,11 +2088,18 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chromium-bidi@14.0.0:
resolution: {integrity: sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==}
peerDependencies:
devtools-protocol: '*'
citty@0.2.1:
resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -2017,6 +2126,13 @@ packages:
engines: {node: ^14.13.0 || >=16.0.0}
hasBin: true
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
conventional-changelog-angular@8.3.0:
resolution: {integrity: sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==}
engines: {node: '>=18'}
@@ -2107,6 +2223,18 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
default-browser-id@5.0.1:
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
engines: {node: '>=18'}
default-browser@5.5.0:
resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
engines: {node: '>=18'}
define-lazy-prop@3.0.0:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
degenerator@5.0.1:
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
engines: {node: '>= 14'}
@@ -2264,6 +2392,9 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -2276,6 +2407,20 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
framer-motion@12.38.0:
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2388,6 +2533,11 @@ packages:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -2400,6 +2550,11 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
hasBin: true
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@@ -2415,6 +2570,10 @@ packages:
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
is-wsl@3.1.1:
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
engines: {node: '>=16'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -2445,6 +2604,10 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
@@ -2558,6 +2721,10 @@ packages:
canvas:
optional: true
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@@ -2621,6 +2788,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
@@ -2639,6 +2814,29 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
motion-utils@12.36.0:
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
motion@12.38.0:
resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2675,6 +2873,10 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
open@10.2.0:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
oxc-parser@0.120.0:
resolution: {integrity: sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2768,6 +2970,9 @@ packages:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
@@ -2865,6 +3070,10 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -2898,6 +3107,10 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-applescript@7.1.0:
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
engines: {node: '>=18'}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -2942,6 +3155,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -2966,6 +3182,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
spawn-command@0.0.2:
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
@@ -3111,6 +3331,25 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsup@8.5.1:
resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
'@microsoft/api-extractor': ^7.36.0
'@swc/core': ^1
postcss: ^8.4.12
typescript: '>=4.5.0'
peerDependenciesMeta:
'@microsoft/api-extractor':
optional: true
'@swc/core':
optional: true
postcss:
optional: true
typescript:
optional: true
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
@@ -3124,6 +3363,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
uhyphen@0.2.0:
resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==}
@@ -3288,6 +3530,10 @@ packages:
utf-8-validate:
optional: true
wsl-utils@0.1.0:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
@@ -3489,6 +3735,17 @@ snapshots:
dependencies:
css-tree: 3.2.1
'@chenglou/pretext@0.0.3': {}
'@clack/core@1.1.0':
dependencies:
sisteransi: 1.0.5
'@clack/prompts@1.1.0':
dependencies:
'@clack/core': 1.1.0
sisteransi: 1.0.5
'@codemirror/autocomplete@6.20.1':
dependencies:
'@codemirror/language': 6.12.3
@@ -4419,6 +4676,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/adm-zip@0.5.8':
dependencies:
'@types/node': 22.19.15
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.2
@@ -4456,6 +4717,8 @@ snapshots:
parse5: 7.3.0
undici-types: 7.24.6
'@types/mime-types@3.0.1': {}
'@types/node@22.19.15':
dependencies:
undici-types: 6.21.0
@@ -4572,6 +4835,10 @@ snapshots:
loupe: 3.2.1
tinyrainbow: 2.0.0
acorn@8.16.0: {}
adm-zip@0.5.16: {}
agent-base@7.1.4: {}
ajv@8.18.0:
@@ -4697,6 +4964,15 @@ snapshots:
buffer-crc32@0.2.13: {}
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
bundle-require@5.1.0(esbuild@0.27.4):
dependencies:
esbuild: 0.27.4
load-tsconfig: 0.2.5
cac@6.7.14: {}
callsites@3.1.0: {}
@@ -4728,7 +5004,6 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
optional: true
cheerio@1.2.0:
dependencies:
@@ -4743,7 +5018,6 @@ snapshots:
parse5-parser-stream: 7.1.2
undici: 7.24.6
whatwg-mimetype: 4.0.0
optional: true
chokidar@3.6.0:
dependencies:
@@ -4757,12 +5031,18 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
chromium-bidi@14.0.0(devtools-protocol@0.0.1581282):
dependencies:
devtools-protocol: 0.0.1581282
mitt: 3.0.1
zod: 3.25.76
citty@0.2.1: {}
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@@ -4804,6 +5084,10 @@ snapshots:
tree-kill: 1.2.2
yargs: 17.7.2
confbox@0.1.8: {}
consola@3.4.2: {}
conventional-changelog-angular@8.3.0:
dependencies:
compare-func: 2.0.0
@@ -4885,6 +5169,15 @@ snapshots:
deep-eql@5.0.2: {}
default-browser-id@5.0.1: {}
default-browser@5.5.0:
dependencies:
bundle-name: 4.1.0
default-browser-id: 5.0.1
define-lazy-prop@3.0.0: {}
degenerator@5.0.1:
dependencies:
ast-types: 0.13.4
@@ -4931,7 +5224,6 @@ snapshots:
dependencies:
iconv-lite: 0.6.3
whatwg-encoding: 3.1.1
optional: true
end-of-stream@1.4.5:
dependencies:
@@ -5005,7 +5297,6 @@ snapshots:
'@esbuild/win32-arm64': 0.25.12
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
optional: true
esbuild@0.27.4:
optionalDependencies:
@@ -5108,6 +5399,12 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
fix-dts-default-cjs-exports@1.0.1:
dependencies:
magic-string: 0.30.21
mlly: 1.8.2
rollup: 4.60.0
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
@@ -5119,6 +5416,15 @@ snapshots:
fraction.js@5.3.4: {}
framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
motion-dom: 12.38.0
motion-utils: 12.36.0
tslib: 2.8.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
fsevents@2.3.3:
optional: true
@@ -5215,7 +5521,6 @@ snapshots:
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
optional: true
import-fresh@3.3.1:
dependencies:
@@ -5238,6 +5543,8 @@ snapshots:
dependencies:
hasown: 2.0.2
is-docker@3.0.0: {}
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
@@ -5246,6 +5553,10 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
is-number@7.0.0: {}
is-obj@2.0.0: {}
@@ -5254,6 +5565,10 @@ snapshots:
is-potential-custom-element-name@1.0.1: {}
is-wsl@3.1.1:
dependencies:
is-inside-container: 1.0.0
isexe@2.0.0: {}
istanbul-lib-coverage@3.2.2: {}
@@ -5287,6 +5602,8 @@ snapshots:
jiti@2.6.1: {}
joycon@3.1.1: {}
js-tokens@10.0.0: {}
js-tokens@4.0.0: {}
@@ -5404,6 +5721,8 @@ snapshots:
htmlparser2: 10.1.0
uhyphen: 0.2.0
load-tsconfig@0.2.5: {}
lodash.camelcase@4.3.0: {}
lodash.kebabcase@4.1.1: {}
@@ -5455,6 +5774,12 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.2
mime-db@1.54.0: {}
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
minimatch@10.2.4:
dependencies:
brace-expansion: 5.0.5
@@ -5469,6 +5794,27 @@ snapshots:
mitt@3.0.1: {}
mlly@1.8.2:
dependencies:
acorn: 8.16.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.3
motion-dom@12.38.0:
dependencies:
motion-utils: 12.36.0
motion-utils@12.36.0: {}
motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tslib: 2.8.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
ms@2.1.3: {}
mz@2.7.0:
@@ -5497,6 +5843,13 @@ snapshots:
dependencies:
wrappy: 1.0.2
open@10.2.0:
dependencies:
default-browser: 5.5.0
define-lazy-prop: 3.0.0
is-inside-container: 1.0.0
wsl-utils: 0.1.0
oxc-parser@0.120.0:
dependencies:
'@oxc-project/types': 0.120.0
@@ -5626,12 +5979,10 @@ snapshots:
dependencies:
domhandler: 5.0.3
parse5: 7.3.0
optional: true
parse5-parser-stream@7.1.2:
dependencies:
parse5: 7.3.0
optional: true
parse5@7.3.0:
dependencies:
@@ -5666,6 +6017,12 @@ snapshots:
pirates@4.0.7: {}
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
mlly: 1.8.2
pathe: 2.0.3
postcss-import@15.1.0(postcss@8.5.8):
dependencies:
postcss: 8.5.8
@@ -5687,6 +6044,15 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.3
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 2.6.1
postcss: 8.5.8
tsx: 4.21.0
yaml: 2.8.3
postcss-nested@6.2.0(postcss@8.5.8):
dependencies:
postcss: 8.5.8
@@ -5782,6 +6148,8 @@ snapshots:
dependencies:
picomatch: 2.3.2
readdirp@4.1.2: {}
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
@@ -5831,6 +6199,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.60.0
fsevents: 2.3.3
run-applescript@7.1.0: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -5839,8 +6209,7 @@ snapshots:
dependencies:
tslib: 2.8.1
safer-buffer@2.1.2:
optional: true
safer-buffer@2.1.2: {}
saxes@6.0.0:
dependencies:
@@ -5864,6 +6233,8 @@ snapshots:
signal-exit@4.1.0: {}
sisteransi@1.0.5: {}
smart-buffer@4.2.0: {}
smol-toml@1.6.1: {}
@@ -5886,6 +6257,8 @@ snapshots:
source-map@0.6.1:
optional: true
source-map@0.7.6: {}
spawn-command@0.0.2: {}
stackback@0.0.2: {}
@@ -6072,6 +6445,34 @@ snapshots:
tslib@2.8.1: {}
tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.4)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3
esbuild: 0.27.4
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3)
resolve-from: 5.0.0
rollup: 4.60.0
source-map: 0.7.6
sucrase: 3.35.1
tinyexec: 0.3.2
tinyglobby: 0.2.15
tree-kill: 1.2.2
optionalDependencies:
postcss: 8.5.8
typescript: 5.9.3
transitivePeerDependencies:
- jiti
- supports-color
- tsx
- yaml
tsx@4.21.0:
dependencies:
esbuild: 0.27.4
@@ -6083,6 +6484,8 @@ snapshots:
typescript@5.9.3: {}
ufo@1.6.3: {}
uhyphen@0.2.0: {}
unbash@2.2.0: {}
@@ -6318,10 +6721,8 @@ snapshots:
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
optional: true
whatwg-mimetype@4.0.0:
optional: true
whatwg-mimetype@4.0.0: {}
whatwg-mimetype@5.0.0: {}
@@ -6358,6 +6759,10 @@ snapshots:
ws@8.20.0: {}
wsl-utils@0.1.0:
dependencies:
is-wsl: 3.1.1
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
+66 -10
View File
@@ -205,17 +205,47 @@ Break groups on sentence boundaries (`.` `?` `!`), pauses (>150ms gap), or max w
## Text Overflow Prevention
Captions must never clip off-screen. Apply these rules:
Use `window.__hyperframes.fitTextFontSize()` to measure actual rendered text width and compute the correct font size. This replaces character-count heuristics with pixel-accurate measurement powered by [pretext](https://github.com/chenglou/pretext).
- Set `max-width: 1600px` (landscape) or `max-width: 900px` (portrait) on caption container
- Add `overflow: hidden` as a safety net
- **Auto-scale font size** based on character count:
- ≤18 chars → full size (e.g., 78px)
- 1925 chars → reduce ~15% (e.g., 68px)
- 26+ chars → reduce ~25% (e.g., 58px)
- Reduce `letter-spacing` for long text (switch from `-0.02em` to `-0.04em`)
- Give the caption container an explicit `height` (e.g., `200px`) — don't rely on content sizing with absolute children
- Use `position: absolute` on all caption elements — `position: relative` causes overflow
**Usage in composition scripts:**
```js
GROUPS.forEach(function (group, gi) {
// Measure with text-transform applied (captions typically uppercase)
var result = window.__hyperframes.fitTextFontSize(group.text.toUpperCase(), {
fontFamily: "Outfit",
fontWeight: 900,
maxWidth: 1600,
});
// Apply computed font size to all word spans
wordEls.forEach(function (el) {
el.style.fontSize = result.fontSize + "px";
});
// If result.fits is false, text exceeds minFontSize — overflow: hidden catches it
});
```
**Options:**
| Option | Default | Description |
| -------------- | ---------- | ---------------------------------------------------- |
| `maxWidth` | `1600` | Container width in px (1600 landscape, 900 portrait) |
| `baseFontSize` | `78` | Starting font size — used when text fits |
| `minFontSize` | `42` | Floor — never shrink below this |
| `fontWeight` | `900` | Must match the CSS font-weight |
| `fontFamily` | `"Outfit"` | Must match the CSS font-family |
| `step` | `2` | Decrement step in px per iteration |
**Important:** The `fontWeight` and `fontFamily` options must match the CSS applied to the text elements exactly, or measurements will be inaccurate.
**Safety nets (still required in CSS):**
- `max-width: 1600px` (landscape) or `max-width: 900px` (portrait) on caption container
- `overflow: hidden` as a fallback for `fits: false` edge cases
- `position: absolute` on all caption elements
- Explicit `height` on caption container (e.g., `200px`)
## Caption Exit Guarantee
@@ -239,6 +269,32 @@ tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end);
The `tl.set` at `group.end` is a deterministic kill — it fires at an exact time, doesn't animate, and can't be overridden by other tweens at different times.
**Self-lint rule:** After building the timeline, verify every caption group has a hard kill. Run this check before registering the timeline:
```js
// Caption lint: verify every group has a hard kill
GROUPS.forEach(function (group, gi) {
var el = document.getElementById("cg-" + gi);
if (!el) return;
tl.seek(group.end + 0.01);
var computed = window.getComputedStyle(el);
if (computed.opacity !== "0" && computed.visibility !== "hidden") {
console.warn(
"[caption-lint] group " +
gi +
" ('" +
group.text +
"') still visible at t=" +
(group.end + 0.01).toFixed(2) +
"s",
);
}
});
tl.seek(0); // reset after lint
```
Place this **before** `window.__timelines[id] = tl` so it runs at composition init. Warnings appear in the browser console during `hyperframes dev`.
## Constraints
- **Deterministic.** No `Math.random()`, no `Date.now()`.
+1
View File
@@ -133,6 +133,7 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
- Use `font-display: block` for local fonts — renderer needs fonts loaded before capturing
- Add `crossorigin="anonymous"` to media loaded from external URLs
- Minimum readable text: 20px landscape, 18px portrait
- For dynamic text that may overflow its container, use `window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })` to compute the largest font size that fits on one line. Returns `{ fontSize, fits }`. Options: `baseFontSize` (default 78), `minFontSize` (default 42), `step` (default 2). `fontFamily` and `fontWeight` must match the CSS applied to the element.
- All files (video, audio, fonts, images) live at the project root alongside `index.html`
- From sub-compositions, use `../` to reference root files