Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ pre-commit:
typecheck:
glob: "*.{ts,tsx}"
run: cd packages/core && bunx tsc --noEmit && cd ../studio && bunx tsc --noEmit
# Mirrors the CI gate (same `--base origin/main` so the local hook can't
# pass on a branch CI would fail). Audits the working tree, which means
# unstaged WIP in `packages/**` is part of the diff — stash before
# committing if that surprises you. `--gate new-only` (the default) only
# fails on issues introduced by the branch, not inherited findings.
fallow:
glob: "packages/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
run: bunx fallow audit --base origin/main --fail-on-issues
filesize:
# Scoped to packages/studio — the 500 LOC limit is a studio architecture
# standard enforced as part of the App.tsx decomposition work. Player and
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@hyperframes/player": "workspace:*",
"@types/node": "^25.0.10",
"concurrently": "^8.2.0",
"fallow": "^2.75.0",
"happy-dom": "^20.9.0",
"knip": "^6.0.3",
"lefthook": "^2.1.4",
Expand Down
19 changes: 1 addition & 18 deletions packages/aws-lambda/src/s3Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ import {
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
rmSync,
statSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { dirname } from "node:path";
import { pipeline } from "node:stream/promises";
import { GetObjectCommand, PutObjectCommand, type S3Client } from "@aws-sdk/client-s3";
import * as tar from "tar";
Expand Down Expand Up @@ -138,19 +137,3 @@ export async function untarDirectory(tarballPath: string, destDir: string): Prom
mkdirSync(destDir, { recursive: true });
await tar.extract({ file: tarballPath, cwd: destDir });
}

/** List all regular files under a directory, sorted, returned as absolute paths. */
export function listFilesInDirectory(dir: string): string[] {
const out: string[] = [];
function walk(d: string): void {
for (const entry of readdirSync(d, { withFileTypes: true }).sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
)) {
const full = join(d, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.isFile()) out.push(full);
}
}
walk(dir);
return out;
}
10 changes: 0 additions & 10 deletions packages/cli/src/browser/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "chrome");
// too or it silently picks system Chrome over a perfectly good headless-shell.
const PUPPETEER_CACHE_DIR = join(homedir(), ".cache", "puppeteer", "chrome-headless-shell");

/** Override browser path via --browser-path flag. Takes priority over env var. */
let _browserPathOverride: string | undefined;
export function setBrowserPath(path: string): void {
_browserPathOverride = path;
}

export type BrowserSource = "env" | "cache" | "system" | "download";

export interface BrowserResult {
Expand Down Expand Up @@ -61,10 +55,6 @@ function whichBinary(name: string): string | undefined {
}

function findFromEnv(): BrowserResult | undefined {
// --browser-path flag takes priority
if (_browserPathOverride && existsSync(_browserPathOverride)) {
return { executablePath: _browserPathOverride, source: "env" };
}
const envPath = process.env["HYPERFRAMES_BROWSER_PATH"];
if (envPath && existsSync(envPath)) {
return { executablePath: envPath, source: "env" };
Expand Down
73 changes: 0 additions & 73 deletions packages/cli/src/capture/assetCataloger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,76 +288,3 @@ function getWidthParam(url: string): number {
return 0;
}
}

/**
* Format cataloged assets as markdown for the DESIGN.md Assets section.
* Matches Aura.build's format: grouped by type, named from file paths.
*/
export function formatAssetCatalog(assets: CatalogedAsset[]): string {
if (assets.length === 0) return "No assets detected.\n";

// Group by type
const groups: Record<string, CatalogedAsset[]> = {};
for (const a of assets) {
const group = a.type;
if (!groups[group]) groups[group] = [];
groups[group]!.push(a);
}

const lines: string[] = [];

// Output in order: Fonts, Images, Videos, Icons, Background, Other
const order: CatalogedAsset["type"][] = ["Font", "Image", "Video", "Icon", "Background", "Other"];
for (const type of order) {
const group = groups[type];
if (!group || group.length === 0) continue;

const sectionName =
type === "Font"
? "Fonts"
: type === "Image"
? "Images"
: type === "Video"
? "Videos"
: type === "Icon"
? "Icons"
: type === "Background"
? "Backgrounds"
: "Other";
lines.push(`### ${sectionName}`);

for (const a of group) {
const name = a.notes || deriveAssetName(a.url);
const contexts = a.contexts.join(", ");
lines.push(`- **${name}**: ${a.url} — contexts: ${contexts}`);
}
lines.push("");
}

return lines.join("\n");
}

/**
* Derive a human-readable name from a URL's file path.
* E.g., "ConnectBentoBackground.jpg" → "Connect Bento Background"
*/
function deriveAssetName(url: string): string {
try {
const u = new URL(url);
const path = u.pathname;
// Get filename without extension
const filename = path.split("/").pop() || "";
const nameWithoutExt = filename.replace(/\.[^.]+$/, "");
// Remove hash suffixes (e.g., "Sohne.cb178166" → "Sohne")
const cleaned = nameWithoutExt.replace(/\.[a-f0-9]{6,}$/, "");
// Convert camelCase/PascalCase to spaces
const spaced = cleaned
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/[-_]/g, " ")
.replace(/\s+/g, " ")
.trim();
return spaced || filename;
} catch {
return "Asset";
}
}
3 changes: 0 additions & 3 deletions packages/core/src/parsers/htmlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
StageZoomKeyframe,
CompositionVariable,
} from "../core.types";
import { CANVAS_DIMENSIONS } from "../core.types";
import {
parseGsapScript,
validateCompositionGsap,
Expand Down Expand Up @@ -902,5 +901,3 @@ function extractGsapScript(doc: Document): string | null {
}
return null;
}

export { CANVAS_DIMENSIONS };
40 changes: 0 additions & 40 deletions packages/engine/src/services/hdrCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,46 +158,6 @@ export async function initHdrReadback(page: Page, width: number, height: number)

// ── HDR frame conversion ──────────────────────────────────────────────────────

/**
* Convert raw rgba64le pixels (from FFmpeg) to a base64 string for FFmpeg encoding.
*
* For HLG sources: the pixel values are already HLG-encoded. We pass them through
* as-is (normalized to 16-bit) and tag the output as HLG. No OETF conversion needed —
* the HLG signal values ARE the correct encoding. Converting to linear and back to
* PQ produces worse results because every viewer's PQ→display tone-mapping differs
* from its HLG→display tone-mapping.
*
* The WebGPU round-trip is skipped for pass-through — the pixels go directly from
* FFmpeg extraction to FFmpeg encoding. WebGPU is only needed when transforms
* (scale, rotate, opacity from GSAP) must be applied to the HDR pixels.
*/
export function convertHdrFrameToRgb48le(
rawRgba64le: Buffer,
width: number,
height: number,
): Buffer {
const input = new Uint16Array(
rawRgba64le.buffer,
rawRgba64le.byteOffset,
rawRgba64le.byteLength / 2,
);

// Convert RGBA → RGB (drop alpha) for rgb48le output
const output = Buffer.alloc(width * height * 6);

for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const srcIdx = (y * width + x) * 4;
const dstIdx = (y * width + x) * 6;
output.writeUInt16LE(input[srcIdx] ?? 0, dstIdx);
output.writeUInt16LE(input[srcIdx + 1] ?? 0, dstIdx + 2);
output.writeUInt16LE(input[srcIdx + 2] ?? 0, dstIdx + 4);
}
}

return output;
}

// ── Frame upload + readback ───────────────────────────────────────────────────

/**
Expand Down
10 changes: 0 additions & 10 deletions packages/shader-transitions/src/webgl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,6 @@ export function createTexture(gl: WebGLRenderingContext): WebGLTexture {
return tex;
}

export function uploadTexture(
gl: WebGLRenderingContext,
tex: WebGLTexture,
canvas: HTMLCanvasElement,
): void {
uploadTextureSource(gl, tex, canvas);
canvas.width = 0;
canvas.height = 0;
}

export function uploadTextureSource(
gl: WebGLRenderingContext,
tex: WebGLTexture,
Expand Down
98 changes: 0 additions & 98 deletions packages/studio/src/components/editor/TimelineLayerPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { memo } from "react";
import type { DomEditLayerItem } from "./domEditing";

interface TimelineLayerPanelProps {
clipLabel: string;
layers: DomEditLayerItem[];
selectedLayerKey: string | null;
onSelectLayer: (layer: DomEditLayerItem) => void;
onClose: () => void;
}

const MEDIA_LAYER_TAGS = new Set(["audio", "canvas", "img", "picture", "svg", "video"]);

export function getTimelineLayerPanelSummary(layers: readonly DomEditLayerItem[]): string {
Expand All @@ -22,92 +13,3 @@ export function getTimelineLayerPanelSummary(layers: readonly DomEditLayerItem[]
? "Single selectable media layer"
: "Single selectable layer";
}

export const TimelineLayerPanel = memo(function TimelineLayerPanel({
clipLabel,
layers,
selectedLayerKey,
onSelectLayer,
onClose,
}: TimelineLayerPanelProps) {
return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-neutral-950">
<div className="flex items-start justify-between gap-3 border-b border-white/10 px-3 py-3">
<div className="min-w-0">
<div className="text-[9px] font-semibold uppercase tracking-[0.18em] text-neutral-500">
Clip layers
</div>
<div className="mt-1 truncate text-sm font-semibold text-neutral-100">{clipLabel}</div>
</div>
<button
type="button"
onPointerDown={(event) => {
event.stopPropagation();
}}
onClick={onClose}
className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border border-white/10 bg-black/20 text-neutral-500 transition-colors hover:border-white/20 hover:text-neutral-200"
aria-label="Close clip layers"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div className="border-b border-white/10 px-3 py-2 text-[11px] text-neutral-500">
{getTimelineLayerPanelSummary(layers)}
</div>
<div className="min-h-0 flex-1 overflow-y-auto py-1">
{layers.map((layer) => {
const selected = layer.key === selectedLayerKey;
return (
<button
key={layer.key}
type="button"
data-timeline-layer-row={layer.key}
onPointerDown={(event) => {
event.stopPropagation();
onSelectLayer(layer);
}}
onClick={(event) => {
event.stopPropagation();
onSelectLayer(layer);
}}
className={`group flex w-full items-center gap-2 px-2.5 py-1.5 text-left transition-colors ${
selected
? "bg-studio-accent/14 text-studio-accent"
: "text-neutral-300 hover:bg-white/[0.04] hover:text-neutral-100"
}`}
style={{ paddingLeft: 10 + layer.depth * 14 }}
>
<span
className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md border text-[9px] font-bold uppercase ${
selected
? "border-studio-accent/50 bg-studio-accent/18"
: "border-white/10 bg-black/20 text-neutral-500 group-hover:text-neutral-300"
}`}
>
{layer.tagName.slice(0, 2)}
</span>
<span className="min-w-0 flex-1 truncate text-xs font-medium">{layer.label}</span>
{layer.childCount > 0 && (
<span className="rounded-full border border-white/10 bg-black/25 px-1.5 py-0.5 text-[9px] font-semibold tabular-nums text-neutral-500">
{layer.childCount}
</span>
)}
</button>
);
})}
</div>
</div>
);
});
Loading
Loading