mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user