refactor: delete orphan declarations flagged by fallow (#949)

* ci: run fallow audit in lefthook pre-commit

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.

* refactor: delete orphan declarations flagged by fallow

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.
This commit is contained in:
James Russo
2026-05-18 21:11:03 -07:00
committed by GitHub
parent b6b7bcb51a
commit 2729ee5087
16 changed files with 9 additions and 453 deletions
+1 -18
View File
@@ -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;
}
-10
View File
@@ -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" };
@@ -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
View File
@@ -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 };
@@ -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 ───────────────────────────────────────────────────
/**
-10
View File
@@ -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,
@@ -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 (
<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>
);
});
@@ -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 };
@@ -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", () => {
@@ -1,10 +1,8 @@
export type StudioFeatureFlagEnv = Record<string, boolean | string | undefined>;
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";
@@ -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"
-75
View File
@@ -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);
@@ -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);
}
@@ -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<HTMLIFrameElement | null>;
@@ -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 };
+3 -9
View File
@@ -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<TimelineAssetKind, number> = {
const DEFAULT_TIMELINE_ASSET_DURATION: Record<TimelineAssetKind, number> = {
image: 3,
video: 5,
audio: 5,
@@ -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);
}