From a86c3c1b70741c6f6e7149688e33267daa61b3ee Mon Sep 17 00:00:00 2001 From: James Date: Tue, 19 May 2026 01:24:30 +0000 Subject: [PATCH 1/3] ci: run fallow audit in lefthook pre-commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the same `fallow audit --base ... --fail-on-issues` check that runs in CI, but locally against HEAD so issues surface at commit time instead of after the push round-trip. Scoped to `packages/**` source files via the glob — non-code edits (README, docs, top-level configs) skip the hook entirely. Measured locally: ~5s in parallel with the existing lint/format/typecheck checks. Doesn't extend wall-clock time because typecheck (~11s) is the long pole, and lefthook runs commands in parallel. The default `--gate new-only` means inherited findings don't block the commit — same gate behavior as CI, so local pre-commit and PR audit agree. --- bun.lock | 19 +++++++++++++++++++ lefthook.yml | 8 ++++++++ package.json | 1 + 3 files changed, 28 insertions(+) diff --git a/bun.lock b/bun.lock index c94440c4b..84d73f6db 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,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", @@ -517,6 +518,22 @@ "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], + "@fallow-cli/darwin-arm64": ["@fallow-cli/darwin-arm64@2.75.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-izJTjYPNdiWPbf7QjVwje0JwJeVQJAUuncjPkuAO9hRM3t4oJ9fqAvyEXyyRq4dAJS4NO4DMEsPsN82QOF8fGw=="], + + "@fallow-cli/darwin-x64": ["@fallow-cli/darwin-x64@2.75.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZzaSidHYKFC82bjP/L+xKOp67ESgNjSUX3U2HzaBg/dKFVcyMFxub3O7r3SBFBU2zY5wsZK1qmfpw/oC01vsg=="], + + "@fallow-cli/linux-arm64-gnu": ["@fallow-cli/linux-arm64-gnu@2.75.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-K5ymLWqR6NJUV+wVDFK0+bKK4YSzYo9Lr2Xla5YptW6FnlpEAvRDdcr3lGdf3Ge1T14fKU3ys8dKHDHkJSeyeA=="], + + "@fallow-cli/linux-arm64-musl": ["@fallow-cli/linux-arm64-musl@2.75.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-FRBz19XQ6pPjnhsIry0w0cRqUEYGZH6rEDCau3SGa2DNgts/i4573HJOn7ZRAzgSNMtgoh3KKTyUNOEzNLTl0A=="], + + "@fallow-cli/linux-x64-gnu": ["@fallow-cli/linux-x64-gnu@2.75.0", "", { "os": "linux", "cpu": "x64" }, "sha512-81xmIf9G8hVTKbQGRGV6oTbu9W5XQPpKFpfXS3KFu16bbdgfwEaKzr5E7Y7dxECYsskhIiff5zdHcEPOFv29Wg=="], + + "@fallow-cli/linux-x64-musl": ["@fallow-cli/linux-x64-musl@2.75.0", "", { "os": "linux", "cpu": "x64" }, "sha512-lVPjmM+dGy3NG6nJ9PaZfkUClZC6OyfwyqVZdQFhq+6Xl/2c2jZI9vgmwIDBIGBhL9TU+V8NdSM7Xy85ifeOyw=="], + + "@fallow-cli/win32-arm64-msvc": ["@fallow-cli/win32-arm64-msvc@2.75.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-HbIXLbte8pzNX0XlbyRlRQA2+y8kfJ4O6XxB2VR2dE6q5LrNhG4fl0QRR/P9x8r3CNn9fw/25kk+WxULfbHu1w=="], + + "@fallow-cli/win32-x64-msvc": ["@fallow-cli/win32-x64-msvc@2.75.0", "", { "os": "win32", "cpu": "x64" }, "sha512-h8W+qEOPvyolBQO2Y0H4/pz2XXw/1yHA8/XRPo3/tYRovmN2zoNjDrvaGjjodzFt6fBVLus+T0uo+U4pMgOu/w=="], + "@fontsource/archivo-black": ["@fontsource/archivo-black@5.2.8", "", {}, "sha512-3zNj/o9LzWyDl/UEpY5IOHpAQyUtFr3hQaFS7NSKwCLLkXOfH/CMCt1L2b2Z+OF25OURtOYenCadgAebALz7/A=="], "@fontsource/eb-garamond": ["@fontsource/eb-garamond@5.2.7", "", {}, "sha512-V42tTlHDbnDo0+lENnXKWMx63Llq6Gfl2l7ozkoeRQN60S0jm6hEgCLlOT/5YyKAN9QZ0e2ofpy0rGKlz9jBrw=="], @@ -1243,6 +1260,8 @@ "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + "fallow": ["fallow@2.75.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@fallow-cli/darwin-arm64": "2.75.0", "@fallow-cli/darwin-x64": "2.75.0", "@fallow-cli/linux-arm64-gnu": "2.75.0", "@fallow-cli/linux-arm64-musl": "2.75.0", "@fallow-cli/linux-x64-gnu": "2.75.0", "@fallow-cli/linux-x64-musl": "2.75.0", "@fallow-cli/win32-arm64-msvc": "2.75.0", "@fallow-cli/win32-x64-msvc": "2.75.0" }, "bin": { "fallow": "bin/fallow", "fallow-lsp": "bin/fallow-lsp", "fallow-mcp": "bin/fallow-mcp" } }, "sha512-0/2cquNI/cDLP/LzcCbkwI4hMzkX4tE0VY3/69n3PBBeqFpbM2oai+2Cb0sB8dXB8MDUGPVoPJjDW5GiUo7a1A=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], diff --git a/lefthook.yml b/lefthook.yml index 237bd82ca..b2ea46992 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -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 diff --git a/package.json b/package.json index 11ac3ade0..4c8c81423 100644 --- a/package.json +++ b/package.json @@ -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", From e03d038647f5ed675b67cc6841c04506547f83e7 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 19 May 2026 01:37:32 +0000 Subject: [PATCH 2/3] refactor: delete orphan declarations flagged by fallow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After fallow's auto-fix de-exports unused symbols, oxlint surfaces them as no-unused-vars. This PR deletes those orphan declarations outright. Biggest cleanup: studio/src/icons/SystemIcons.tsx shrinks from 132 to 57 lines — 33 unused icon wrappers and their phosphor-icon imports deleted. Other deletions across 14 more files covering paired getter/setters, helper functions, dead env constants, internal components with no callers, and cascading unused imports. Cascade-causing files held back for follow-up PRs: renderOrchestrator barrel of captureCost re-exports, telemetry/portUtils/remote barrels, Button.tsx + ui/index.ts (would orphan whole file), studioMotion type re-exports. Test plan: typecheck clean across 8 packages, oxlint + oxfmt clean, fallow audit exit 0 (remaining findings inherited), cli + studio vitest suites pass. --- packages/aws-lambda/src/s3Transport.ts | 19 +--- packages/cli/src/browser/manager.ts | 10 -- packages/cli/src/capture/assetCataloger.ts | 73 -------------- packages/core/src/parsers/htmlParser.ts | 3 - packages/engine/src/services/hdrCapture.ts | 40 -------- packages/shader-transitions/src/webgl.ts | 10 -- .../components/editor/TimelineLayerPanel.tsx | 98 ------------------- .../components/editor/domEditingElement.ts | 8 +- .../editor/manualEditingAvailability.test.ts | 24 +---- .../editor/manualEditingAvailability.ts | 18 +--- .../components/editor/manualEditsSnapshot.ts | 34 ------- packages/studio/src/icons/SystemIcons.tsx | 75 -------------- .../components/useTimelineRangeSelection.ts | 16 --- .../player/hooks/useTimelineSyncCallbacks.ts | 6 -- packages/studio/src/utils/studioHelpers.ts | 12 +-- .../studio/src/utils/timelineDiscovery.ts | 16 --- 16 files changed, 9 insertions(+), 453 deletions(-) diff --git a/packages/aws-lambda/src/s3Transport.ts b/packages/aws-lambda/src/s3Transport.ts index 988ae2a62..0c74d378f 100644 --- a/packages/aws-lambda/src/s3Transport.ts +++ b/packages/aws-lambda/src/s3Transport.ts @@ -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"; @@ -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; -} diff --git a/packages/cli/src/browser/manager.ts b/packages/cli/src/browser/manager.ts index fc1c9999a..a29c943e4 100644 --- a/packages/cli/src/browser/manager.ts +++ b/packages/cli/src/browser/manager.ts @@ -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 { @@ -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" }; diff --git a/packages/cli/src/capture/assetCataloger.ts b/packages/cli/src/capture/assetCataloger.ts index be174548e..3bcd7e7da 100644 --- a/packages/cli/src/capture/assetCataloger.ts +++ b/packages/cli/src/capture/assetCataloger.ts @@ -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 = {}; - 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"; - } -} diff --git a/packages/core/src/parsers/htmlParser.ts b/packages/core/src/parsers/htmlParser.ts index 384b65f53..23fae3a2b 100644 --- a/packages/core/src/parsers/htmlParser.ts +++ b/packages/core/src/parsers/htmlParser.ts @@ -10,7 +10,6 @@ import type { StageZoomKeyframe, CompositionVariable, } from "../core.types"; -import { CANVAS_DIMENSIONS } from "../core.types"; import { parseGsapScript, validateCompositionGsap, @@ -902,5 +901,3 @@ function extractGsapScript(doc: Document): string | null { } return null; } - -export { CANVAS_DIMENSIONS }; diff --git a/packages/engine/src/services/hdrCapture.ts b/packages/engine/src/services/hdrCapture.ts index 91494244b..2d2975780 100644 --- a/packages/engine/src/services/hdrCapture.ts +++ b/packages/engine/src/services/hdrCapture.ts @@ -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 ─────────────────────────────────────────────────── /** diff --git a/packages/shader-transitions/src/webgl.ts b/packages/shader-transitions/src/webgl.ts index a36ffe51f..1a2c3584e 100644 --- a/packages/shader-transitions/src/webgl.ts +++ b/packages/shader-transitions/src/webgl.ts @@ -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, diff --git a/packages/studio/src/components/editor/TimelineLayerPanel.tsx b/packages/studio/src/components/editor/TimelineLayerPanel.tsx index a87e2ee07..3a0447a0f 100644 --- a/packages/studio/src/components/editor/TimelineLayerPanel.tsx +++ b/packages/studio/src/components/editor/TimelineLayerPanel.tsx @@ -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 { @@ -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 ( -
-
-
-
- Clip layers -
-
{clipLabel}
-
- -
-
- {getTimelineLayerPanelSummary(layers)} -
-
- {layers.map((layer) => { - const selected = layer.key === selectedLayerKey; - return ( - - ); - })} -
-
- ); -}); diff --git a/packages/studio/src/components/editor/domEditingElement.ts b/packages/studio/src/components/editor/domEditingElement.ts index 96d33ea12..4a339c859 100644 --- a/packages/studio/src/components/editor/domEditingElement.ts +++ b/packages/studio/src/components/editor/domEditingElement.ts @@ -12,9 +12,7 @@ import type { import { buildStableSelector, escapeCssString, - findClosestByAttribute, getElementDepth, - getPreferredClassSelector, getSelectorIndex, getSourceFileForElement, isHtmlElement, @@ -60,7 +58,7 @@ function isEmptyVisualContainer(el: HTMLElement): boolean { return true; } -export function hasRenderedBox(el: HTMLElement): boolean { +function hasRenderedBox(el: HTMLElement): boolean { const rect = el.getBoundingClientRect(); if (rect.width <= 1 || rect.height <= 1) return false; if (!isElementComputedVisible(el)) return false; @@ -324,7 +322,3 @@ export function getDirectLayerChildren( isHtmlElement(child) && getDomLayerPatchTarget(child, options.activeCompositionPath) !== null, ); } - -// ─── Composition source helpers ─────────────────────────────────────────────── - -export { findClosestByAttribute, getPreferredClassSelector, getSourceFileForElement }; diff --git a/packages/studio/src/components/editor/manualEditingAvailability.test.ts b/packages/studio/src/components/editor/manualEditingAvailability.test.ts index 7fde62f30..aa8968bb6 100644 --- a/packages/studio/src/components/editor/manualEditingAvailability.test.ts +++ b/packages/studio/src/components/editor/manualEditingAvailability.test.ts @@ -23,37 +23,15 @@ describe("manual editing availability", () => { expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(true); expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true); expect(availability.STUDIO_MOTION_PANEL_ENABLED).toBe(false); - expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(true); }); - it("keeps explicit truthy inspector env flags enabled", async () => { - const availability = await loadAvailabilityWithEnv({ - VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "1", - VITE_STUDIO_ENABLE_TIMELINE_LAYER_INSPECTOR: "true", - }); - - expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true); - expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(true); - }); - - it("allows explicit env flags to disable default-on inspector layers", async () => { - const availability = await loadAvailabilityWithEnv({ - VITE_STUDIO_ENABLE_TIMELINE_LAYER_INSPECTOR: "off", - }); - - expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(true); - expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(false); - }); - - it("keeps timeline layer inspection off when the parent inspector flag is off", async () => { + it("disables preview selection when the inspector panel flag is explicitly off", async () => { const availability = await loadAvailabilityWithEnv({ VITE_STUDIO_ENABLE_INSPECTOR_PANELS: "0", - VITE_STUDIO_ENABLE_TIMELINE_LAYER_INSPECTOR: "true", }); expect(availability.STUDIO_INSPECTOR_PANELS_ENABLED).toBe(false); expect(availability.STUDIO_PREVIEW_SELECTION_ENABLED).toBe(false); - expect(availability.STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED).toBe(false); }); it("enables feature flags with explicit truthy env values", () => { diff --git a/packages/studio/src/components/editor/manualEditingAvailability.ts b/packages/studio/src/components/editor/manualEditingAvailability.ts index 703211a13..bd3b59c5b 100644 --- a/packages/studio/src/components/editor/manualEditingAvailability.ts +++ b/packages/studio/src/components/editor/manualEditingAvailability.ts @@ -1,10 +1,8 @@ export type StudioFeatureFlagEnv = Record; -export const STUDIO_PREVIEW_MANUAL_DRAGGING_ENV = "VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING"; -export const STUDIO_INSPECTOR_PANELS_ENV = "VITE_STUDIO_ENABLE_INSPECTOR_PANELS"; -export const STUDIO_MOTION_PANEL_ENV = "VITE_STUDIO_ENABLE_MOTION_PANEL"; -export const STUDIO_TIMELINE_LAYER_INSPECTOR_ENV = "VITE_STUDIO_ENABLE_TIMELINE_LAYER_INSPECTOR"; - +const STUDIO_PREVIEW_MANUAL_DRAGGING_ENV = "VITE_STUDIO_ENABLE_PREVIEW_MANUAL_DRAGGING"; +const STUDIO_INSPECTOR_PANELS_ENV = "VITE_STUDIO_ENABLE_INSPECTOR_PANELS"; +const STUDIO_MOTION_PANEL_ENV = "VITE_STUDIO_ENABLE_MOTION_PANEL"; const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on", "enabled"]); const FALSY_ENV_VALUES = new Set(["0", "false", "no", "off", "disabled"]); @@ -52,14 +50,6 @@ export const STUDIO_MOTION_PANEL_ENABLED = resolveStudioBooleanEnvFlag( false, ); -export const STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED = - STUDIO_INSPECTOR_PANELS_ENABLED && - resolveStudioBooleanEnvFlag( - env, - [STUDIO_TIMELINE_LAYER_INSPECTOR_ENV, "VITE_STUDIO_TIMELINE_LAYER_INSPECTOR_ENABLED"], - true, - ); - export const STUDIO_BLOCKS_PANEL_ENABLED = resolveStudioBooleanEnvFlag( env, ["VITE_STUDIO_ENABLE_BLOCKS_PANEL", "VITE_STUDIO_BLOCKS_PANEL_ENABLED"], @@ -68,6 +58,4 @@ export const STUDIO_BLOCKS_PANEL_ENABLED = resolveStudioBooleanEnvFlag( export const STUDIO_PREVIEW_SELECTION_ENABLED = STUDIO_INSPECTOR_PANELS_ENABLED; -export const STUDIO_MANUAL_EDITING_ENABLED = STUDIO_PREVIEW_MANUAL_EDITING_ENABLED; - export const STUDIO_MANUAL_EDITING_DISABLED_TITLE = "Manual editing is temporarily disabled"; diff --git a/packages/studio/src/components/editor/manualEditsSnapshot.ts b/packages/studio/src/components/editor/manualEditsSnapshot.ts index ea14f255d..1cf840ff5 100644 --- a/packages/studio/src/components/editor/manualEditsSnapshot.ts +++ b/packages/studio/src/components/editor/manualEditsSnapshot.ts @@ -11,7 +11,6 @@ import { STUDIO_HEIGHT_PROP, STUDIO_ROTATION_PROP, STUDIO_PATH_OFFSET_ATTR, - STUDIO_MANUAL_EDIT_GESTURE_ATTR, STUDIO_BOX_SIZE_ATTR, STUDIO_ROTATION_ATTR, STUDIO_ORIGINAL_TRANSLATE_ATTR, @@ -33,7 +32,6 @@ import { STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR, STUDIO_ROTATION_DRAFT_ATTR, - STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, } from "./manualEditsTypes"; import type { StudioBoxSizeSnapshot, @@ -187,38 +185,6 @@ export function restoreStudioPathOffset( ); } -/* ── DOM element collection ───────────────────────────────────────── */ -export function collectStudioManualEditElements(doc: Document): HTMLElement[] { - const htmlElement = doc.defaultView?.HTMLElement; - if (!htmlElement) return []; - - const elements = [doc.documentElement, ...Array.from(doc.getElementsByTagName("*"))].filter( - (element): element is HTMLElement => element instanceof htmlElement, - ); - - return elements.filter( - (element) => - element.hasAttribute(STUDIO_PATH_OFFSET_ATTR) || - element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) || - element.hasAttribute(STUDIO_BOX_SIZE_ATTR) || - element.hasAttribute(STUDIO_ROTATION_ATTR) || - element.hasAttribute(STUDIO_ROTATION_DRAFT_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_MIN_WIDTH_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_FLEX_BASIS_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_ROTATE_ATTR) || - element.hasAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR) || - Boolean(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)) || - Boolean(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)) || - Boolean(element.style.getPropertyValue(STUDIO_WIDTH_PROP)) || - Boolean(element.style.getPropertyValue(STUDIO_HEIGHT_PROP)) || - Boolean(element.style.getPropertyValue(STUDIO_ROTATION_PROP)), - ); -} - /* ── Clear functions ──────────────────────────────────────────────── */ type BoxSizeProperty = | "width" diff --git a/packages/studio/src/icons/SystemIcons.tsx b/packages/studio/src/icons/SystemIcons.tsx index 5058fce5f..3a43f29fb 100644 --- a/packages/studio/src/icons/SystemIcons.tsx +++ b/packages/studio/src/icons/SystemIcons.tsx @@ -1,54 +1,18 @@ import { - WarningCircle, - Warning, - ArrowLeft as PhArrowLeft, Check as PhCheck, - CheckCircle as PhCheckCircle, - Circle as PhCircle, Clock as PhClock, - Code as PhCode, - DownloadSimple, - Pencil as PhPencil, - ArrowSquareOut, Eye as PhEye, - EyeClosed, - File as PhFile, - FileCode as PhFileCode, - FileText as PhFileText, FilmStrip, - Heart as PhHeart, - Image as PhImage, - Info as PhInfo, Stack, - SpinnerGap, - ArrowsOut, - CornersOut, - ChatCircle, ChatCenteredText, - Cursor, ArrowsOutCardinal, MusicNote, Palette as PhPalette, - Paperclip as PhPaperclip, - Pause as PhPause, - Play as PhPlay, Plus as PhPlus, - MagnifyingGlass, - PaperPlaneRight, - SkipBack as PhSkipBack, - SkipForward as PhSkipForward, Square as PhSquare, - Trash, TextT, - UploadSimple, - User as PhUser, - UsersThree, - VideoCamera, X as PhX, Lightning, - MagnifyingGlassPlus, - MagnifyingGlassMinus, - Terminal as PhTerminal, CaretDown, CaretRight, ClipboardText, @@ -69,60 +33,21 @@ const makeIcon = (Icon: PhosphorIcon) => { }; // Lucide name → Phosphor equivalent -export const AlertCircle = makeIcon(WarningCircle); -export const AlertTriangle = makeIcon(Warning); -export const ArrowLeft = makeIcon(PhArrowLeft); export const Check = makeIcon(PhCheck); -export const CheckCircle = makeIcon(PhCheckCircle); -/** CheckCircle2 in lucide is visually identical to CheckCircle */ -export const CheckCircle2 = makeIcon(PhCheckCircle); -export const Circle = makeIcon(PhCircle); export const Clock = makeIcon(PhClock); -export const Code = makeIcon(PhCode); -export const Download = makeIcon(DownloadSimple); -export const Edit2 = makeIcon(PhPencil); -export const ExternalLink = makeIcon(ArrowSquareOut); export const Eye = makeIcon(PhEye); -export const EyeOff = makeIcon(EyeClosed); -export const File = makeIcon(PhFile); -export const FileCode = makeIcon(PhFileCode); -export const FileText = makeIcon(PhFileText); export const Film = makeIcon(FilmStrip); -export const Heart = makeIcon(PhHeart); -export const Image = makeIcon(PhImage); -export const Info = makeIcon(PhInfo); export const Layers = makeIcon(Stack); -export const Loader2 = makeIcon(SpinnerGap); -export const Maximize = makeIcon(ArrowsOut); -export const Maximize2 = makeIcon(CornersOut); -export const MessageCircle = makeIcon(ChatCircle); export const MessageSquare = makeIcon(ChatCenteredText); -export const MousePointer = makeIcon(Cursor); export const Move = makeIcon(ArrowsOutCardinal); export const Music = makeIcon(MusicNote); export const Palette = makeIcon(PhPalette); -export const Paperclip = makeIcon(PhPaperclip); -export const Pause = makeIcon(PhPause); -export const Pencil = makeIcon(PhPencil); -export const Play = makeIcon(PhPlay); export const Plus = makeIcon(PhPlus); -export const Search = makeIcon(MagnifyingGlass); -export const Send = makeIcon(PaperPlaneRight); -export const SkipBack = makeIcon(PhSkipBack); -export const SkipForward = makeIcon(PhSkipForward); export const Square = makeIcon(PhSquare); -export const Trash2 = makeIcon(Trash); export const Type = makeIcon(TextT); -export const Upload = makeIcon(UploadSimple); -export const User = makeIcon(PhUser); -export const Users = makeIcon(UsersThree); -export const Video = makeIcon(VideoCamera); export const X = makeIcon(PhX); export const Zap = makeIcon(Lightning); -export const ZoomIn = makeIcon(MagnifyingGlassPlus); -export const ZoomOut = makeIcon(MagnifyingGlassMinus); // Extra icons used in this project (not in lucide's default mapping above) -export const Terminal = makeIcon(PhTerminal); export const ChevronDown = makeIcon(CaretDown); export const ChevronRight = makeIcon(CaretRight); export const ClipboardList = makeIcon(ClipboardText); diff --git a/packages/studio/src/player/components/useTimelineRangeSelection.ts b/packages/studio/src/player/components/useTimelineRangeSelection.ts index 6bfc2a555..6183057e7 100644 --- a/packages/studio/src/player/components/useTimelineRangeSelection.ts +++ b/packages/studio/src/player/components/useTimelineRangeSelection.ts @@ -144,19 +144,3 @@ export function useTimelineRangeSelection({ handlePointerUp, }; } - -/* ── Seek + scroll utilities (used in Timeline only) ──────────────── */ -export function seekTimeFromScrollX( - scrollEl: HTMLDivElement, - clientX: number, - effectiveDuration: number, - pps: number, - onSeek?: (time: number) => void, -): void { - const rect = scrollEl.getBoundingClientRect(); - const x = clientX - rect.left + scrollEl.scrollLeft - GUTTER; - if (x < 0) return; - const time = Math.max(0, Math.min(effectiveDuration, x / pps)); - liveTime.notify(time); - onSeek?.(time); -} diff --git a/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts b/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts index 54c28732f..06dd91221 100644 --- a/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts +++ b/packages/studio/src/player/hooks/useTimelineSyncCallbacks.ts @@ -18,7 +18,6 @@ import { findTimelineDomNodeForClip, createImplicitTimelineLayersFromDOM, buildStandaloneRootTimelineElement, - mergeTimelineElementsPreservingDowngrades, getTimelineElementSelector, } from "../lib/timelineDOM"; import { @@ -26,7 +25,6 @@ import { autoHealMissingCompositionIds, buildMissingCompositionElements, } from "../lib/timelineIframeHelpers"; -import { getTimelineElementIdentity } from "../lib/timelineElementHelpers"; interface UseTimelineSyncCallbacksParams { iframeRef: React.RefObject; @@ -288,7 +286,3 @@ export function useTimelineSyncCallbacks({ onIframeLoad, }; } - -// Re-export the merge helper so the hook can use it via this module (avoids -// adding another import line to the already-large useTimelinePlayer.ts). -export { mergeTimelineElementsPreservingDowngrades, getTimelineElementIdentity }; diff --git a/packages/studio/src/utils/studioHelpers.ts b/packages/studio/src/utils/studioHelpers.ts index 3790dee6f..b193364c4 100644 --- a/packages/studio/src/utils/studioHelpers.ts +++ b/packages/studio/src/utils/studioHelpers.ts @@ -23,13 +23,7 @@ export function getTimelineElementLabel(element: TimelineElement): string { return element.label || element.id || element.tag; } -export function confirmElementDelete(label: string, kind: "timeline clip" | "element"): boolean { - return window.confirm( - `Delete ${kind} "${label}"?\n\nThis removes it from the project source. You can use Undo to restore it.`, - ); -} - -export function normalizeProjectAssetPath(value: string): string { +function normalizeProjectAssetPath(value: string): string { const trimmed = value.trim(); const maybeUrl = /^[a-z]+:\/\//i.test(trimmed) ? new URL(trimmed).pathname : trimmed; return decodeURIComponent(maybeUrl) @@ -51,7 +45,7 @@ export function toRelativeProjectAssetPath(sourceFile: string, assetPath: string return [...fromParts.map(() => ".."), ...targetParts].join("/") || assetPath; } -export function isAbsoluteFilePath(value: string): boolean { +function isAbsoluteFilePath(value: string): boolean { return /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(value); } @@ -181,7 +175,7 @@ export function collectHtmlIds(source: string): string[] { return Array.from(source.matchAll(/\bid="([^"]+)"/g), (match) => match[1] ?? ""); } -export const DEFAULT_TIMELINE_ASSET_DURATION: Record = { +const DEFAULT_TIMELINE_ASSET_DURATION: Record = { image: 3, video: 5, audio: 5, diff --git a/packages/studio/src/utils/timelineDiscovery.ts b/packages/studio/src/utils/timelineDiscovery.ts index b449a4f1e..f4027953e 100644 --- a/packages/studio/src/utils/timelineDiscovery.ts +++ b/packages/studio/src/utils/timelineDiscovery.ts @@ -1,6 +1,4 @@ export const TIMELINE_TOGGLE_SHORTCUT_LABEL = "Shift+T"; -const TIMELINE_EDITOR_HINT_STORAGE_KEY = "hf-studio-timeline-editor-hint-dismissed"; - type TimelineToggleHotkeyEvent = Pick< KeyboardEvent, "key" | "shiftKey" | "metaKey" | "ctrlKey" | "altKey" | "target" @@ -41,17 +39,3 @@ export function shouldHandleTimelineToggleHotkey(event: TimelineToggleHotkeyEven export function getTimelineToggleTitle(timelineVisible: boolean): string { return `${timelineVisible ? "Hide" : "Show"} timeline editor (${TIMELINE_TOGGLE_SHORTCUT_LABEL})`; } - -export function getTimelineEditorHintDismissed(): boolean { - if (typeof window === "undefined") return false; - return window.localStorage.getItem(TIMELINE_EDITOR_HINT_STORAGE_KEY) === "1"; -} - -export function setTimelineEditorHintDismissed(dismissed: boolean): void { - if (typeof window === "undefined") return; - if (dismissed) { - window.localStorage.setItem(TIMELINE_EDITOR_HINT_STORAGE_KEY, "1"); - return; - } - window.localStorage.removeItem(TIMELINE_EDITOR_HINT_STORAGE_KEY); -} From a67436b445738701c0786c78f25fb209658cd509 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 19 May 2026 01:40:31 +0000 Subject: [PATCH 3/3] ci: post sticky PR comment with fallow audit findings Reviewers shouldn't have to dig through CI logs to see what fallow flagged. With this change, on every PR the fallow job posts (or updates) a sticky comment containing the full audit report formatted as a collapsible markdown table. The comment uses fallow's built-in `pr-comment-github` format, which already emits a `` sentinel. `marocchino/sticky-pull-request-comment@v2.9.1` matches that header so each run replaces the previous comment instead of stacking new ones. The job now runs in three steps: 1. Run `fallow audit ... --format pr-comment-github` with `continue-on-error: true` so the comment posts even when the audit fails. Exit code is captured. 2. Post (or update) the sticky comment with the captured output. 3. Re-emit the audit exit code so the job still fails-the-build on new findings. Bumps the workflow's `pull-requests` permission from read to write, needed for the sticky-comment poster to call the issues API. --- .github/workflows/ci.yml | 56 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b96dc1703..f5791bf8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,24 +86,74 @@ jobs: # the changed files. The default `--gate new-only` means existing legacy # findings don't fail the build — only NEW issues introduced by the PR do. # This stops bleeding while letting incremental cleanup land separately. + # + # On findings, the job posts (or updates) a sticky comment on the PR so + # reviewers see the full list inline instead of digging through CI logs. fallow: name: Fallow audit needs: changes if: needs.changes.outputs.code == 'true' && github.event_name == 'pull_request' runs-on: ubuntu-latest timeout-minutes: 5 + # Scope write access to this single job — the rest of `ci.yml` keeps the + # workflow-level `pull-requests: read` default so build / lint / test + # tokens can't post or modify PR comments. Job-level permissions override + # the workflow block. + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: # Full history so `--base origin/main` can diff against the merge # base on stacked PRs, not just the shallow tip. fetch-depth: 0 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - # Pinned version — bumps land via a deliberate PR. Keep in sync with - # the version that produced the current `.fallowrc.jsonc` config. - - run: npx -y fallow@2.75.0 audit --base origin/main --fail-on-issues + - run: bun install --frozen-lockfile + - name: Run fallow audit + id: audit + # `bun install` above made `bunx fallow` resolve from node_modules, so + # we don't re-download fallow each run. The script disables `errexit` + # so the audit's non-zero exit (on findings) doesn't abort before we + # write the exit code to the step output. The size check guards + # against fallow crashing before producing markdown (e.g. transient + # parse failure) — without it we'd post a blank sticky comment. + run: | + set +e + bunx fallow audit --base origin/main --fail-on-issues \ + --format pr-comment-github \ + > /tmp/fallow-comment.md + echo "exit_code=$?" >> "$GITHUB_OUTPUT" + if [ ! -s /tmp/fallow-comment.md ]; then + echo "fallow produced no output — see the job logs above." > /tmp/fallow-comment.md + fi + - name: Post sticky comment (findings) + if: steps.audit.outputs.exit_code != '0' + # Fork PRs run with a read-only GITHUB_TOKEN regardless of the + # workflow's `permissions:` block, so the comment post will fail on + # forks. Don't fail the whole job — the audit gate below still fires. + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 + with: + # `header` matches fallow's built-in `` + # sentinel so subsequent runs update the same comment. + header: fallow-results + path: /tmp/fallow-comment.md + - name: Remove stale sticky comment (clean run) + if: steps.audit.outputs.exit_code == '0' + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 + with: + header: fallow-results + delete: true + - name: Fail if audit found issues + if: steps.audit.outputs.exit_code != '0' + run: | + echo "::error::Fallow audit found new issues — see the PR comment above for details." + exit 1 format: name: Format