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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .filesize-allowlist

This file was deleted.

7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Check file sizes (max 500 lines)
- name: Check file sizes (max 600 lines)
# Scoped to files THIS PR changed under packages/studio. Walking the
# whole tree blamed every unrelated PR for pre-existing offenders.
# Falls back to a full scan on push events (no base ref available)
Expand All @@ -494,10 +494,9 @@ jobs:
for f in "${files[@]}"; do
[ -z "$f" ] && continue
[ -f "$f" ] || continue # skip files deleted in this PR
if grep -qxF "$f" .filesize-allowlist 2>/dev/null; then continue; fi
lines=$(wc -l < "$f")
if [ "$lines" -gt 500 ]; then
echo "::error file=$f::$f has $lines lines (max 500)"
if [ "$lines" -gt 600 ]; then
echo "::error file=$f::$f has $lines lines (max 600)"
EXIT=1
fi
done
Expand Down
8 changes: 3 additions & 5 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,16 @@ pre-commit:
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
# Scoped to packages/studio — the 600 LOC limit is a studio architecture
# standard enforced as part of the App.tsx decomposition work. Player and
# other packages enforce size discipline via code review and convention.
# Files temporarily over the limit are listed in .filesize-allowlist.
glob: "packages/studio/**/*.{ts,tsx}"
exclude: "(\\.test\\.(ts|tsx)$|\\.generated\\.)"
run: |
for f in {staged_files}; do
if grep -qxF "$f" .filesize-allowlist 2>/dev/null; then continue; fi
lines=$(wc -l < "$f")
if [ "$lines" -gt 500 ]; then
echo "ERROR: $f has $lines lines (max 500) — add to .filesize-allowlist if temporarily needed"
if [ "$lines" -gt 600 ]; then
echo "ERROR: $f has $lines lines (max 600)"
exit 1
fi
done
Expand Down
397 changes: 38 additions & 359 deletions packages/studio/src/components/editor/manualEditsDom.ts

Large diffs are not rendered by default.

237 changes: 237 additions & 0 deletions packages/studio/src/components/editor/manualEditsDomPatches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import type { PatchOperation } from "../../utils/sourcePatcher";
import {
STUDIO_OFFSET_X_PROP,
STUDIO_OFFSET_Y_PROP,
STUDIO_WIDTH_PROP,
STUDIO_HEIGHT_PROP,
STUDIO_ROTATION_PROP,
STUDIO_PATH_OFFSET_ATTR,
STUDIO_BOX_SIZE_ATTR,
STUDIO_ROTATION_ATTR,
STUDIO_ROTATION_DRAFT_ATTR,
STUDIO_ORIGINAL_TRANSLATE_ATTR,
STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR,
STUDIO_ORIGINAL_WIDTH_ATTR,
STUDIO_ORIGINAL_HEIGHT_ATTR,
STUDIO_ORIGINAL_MIN_WIDTH_ATTR,
STUDIO_ORIGINAL_MIN_HEIGHT_ATTR,
STUDIO_ORIGINAL_MAX_WIDTH_ATTR,
STUDIO_ORIGINAL_MAX_HEIGHT_ATTR,
STUDIO_ORIGINAL_FLEX_BASIS_ATTR,
STUDIO_ORIGINAL_FLEX_GROW_ATTR,
STUDIO_ORIGINAL_FLEX_SHRINK_ATTR,
STUDIO_ORIGINAL_BOX_SIZING_ATTR,
STUDIO_ORIGINAL_SCALE_ATTR,
STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR,
STUDIO_ORIGINAL_DISPLAY_ATTR,
STUDIO_ORIGINAL_ROTATE_ATTR,
STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
} from "./manualEditsTypes";
import {
STUDIO_MOTION_ATTR,
STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
STUDIO_MOTION_ORIGINAL_OPACITY_ATTR,
STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
} from "./studioMotionTypes";

/* ── Shared helpers ──────────────────────────────────────────────── */

function collectInlineStyleOps(
element: HTMLElement,
properties: readonly string[],
ops: PatchOperation[],
): void {
for (const prop of properties) {
const val = element.style.getPropertyValue(prop);
if (val) ops.push({ type: "inline-style", property: prop, value: val });
}
}

function collectAttributeOps(
element: HTMLElement,
attrNames: readonly string[],
ops: PatchOperation[],
): void {
for (const attr of attrNames) {
const val = element.getAttribute(attr);
if (val !== null) ops.push({ type: "attribute", property: attr, value: val });
}
}

function appendTransformDisplayOps(element: HTMLElement, ops: PatchOperation[]): void {
const val = element.getAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR);
if (val !== null) {
ops.push({ type: "inline-style", property: "display", value: val || null });
ops.push({ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: null });
}
}

/* ── Path offset patches ─────────────────────────────────────────── */

export function buildPathOffsetPatches(element: HTMLElement): PatchOperation[] {
const ops: PatchOperation[] = [];
collectInlineStyleOps(element, [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP, "translate"], ops);
ops.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" });
collectAttributeOps(
element,
[STUDIO_ORIGINAL_TRANSLATE_ATTR, STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR],
ops,
);
collectInlineStyleOps(element, ["display"], ops);
collectAttributeOps(element, [STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR], ops);
return ops;
}

export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperation[] {
const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
const ops: PatchOperation[] = [
{ type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: null },
{ type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: null },
{ type: "inline-style", property: "translate", value: originalInlineTranslate || null },
{ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: null },
{ type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: null },
{ type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: null },
];
appendTransformDisplayOps(element, ops);
return ops;
}

/* ── Box size patches ────────────────────────────────────────────── */

const BOX_SIZE_STYLE_PROPS = [
"width",
"height",
"min-width",
"min-height",
"max-width",
"max-height",
"flex-basis",
"flex-grow",
"flex-shrink",
"box-sizing",
"scale",
"transform-origin",
"display",
] as const;

const BOX_SIZE_ORIG_ATTRS: ReadonlyArray<[string, string]> = [
[STUDIO_ORIGINAL_WIDTH_ATTR, "width"],
[STUDIO_ORIGINAL_HEIGHT_ATTR, "height"],
[STUDIO_ORIGINAL_MIN_WIDTH_ATTR, "min-width"],
[STUDIO_ORIGINAL_MIN_HEIGHT_ATTR, "min-height"],
[STUDIO_ORIGINAL_MAX_WIDTH_ATTR, "max-width"],
[STUDIO_ORIGINAL_MAX_HEIGHT_ATTR, "max-height"],
[STUDIO_ORIGINAL_FLEX_BASIS_ATTR, "flex-basis"],
[STUDIO_ORIGINAL_FLEX_GROW_ATTR, "flex-grow"],
[STUDIO_ORIGINAL_FLEX_SHRINK_ATTR, "flex-shrink"],
[STUDIO_ORIGINAL_BOX_SIZING_ATTR, "box-sizing"],
[STUDIO_ORIGINAL_SCALE_ATTR, "scale"],
[STUDIO_ORIGINAL_TRANSFORM_ORIGIN_ATTR, "transform-origin"],
[STUDIO_ORIGINAL_DISPLAY_ATTR, "display"],
[STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, ""],
];

export function buildBoxSizePatches(element: HTMLElement): PatchOperation[] {
const ops: PatchOperation[] = [];
collectInlineStyleOps(element, [STUDIO_WIDTH_PROP, STUDIO_HEIGHT_PROP], ops);
collectInlineStyleOps(element, BOX_SIZE_STYLE_PROPS, ops);
ops.push({ type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: "true" });
collectAttributeOps(
element,
BOX_SIZE_ORIG_ATTRS.map(([attr]) => attr),
ops,
);
return ops;
}

export function buildClearBoxSizePatches(element: HTMLElement): PatchOperation[] {
const ops: PatchOperation[] = [
{ type: "inline-style", property: STUDIO_WIDTH_PROP, value: null },
{ type: "inline-style", property: STUDIO_HEIGHT_PROP, value: null },
{ type: "attribute", property: STUDIO_BOX_SIZE_ATTR, value: null },
];
for (const [attrName, styleProp] of BOX_SIZE_ORIG_ATTRS) {
const origVal = element.getAttribute(attrName);
if (origVal !== null && styleProp) {
ops.push({ type: "inline-style", property: styleProp, value: origVal || null });
}
ops.push({ type: "attribute", property: attrName, value: null });
}
return ops;
}

/* ── Rotation patches ────────────────────────────────────────────── */

const ROTATION_STYLE_PROPS = [
STUDIO_ROTATION_PROP,
"rotate",
"transform-origin",
"display",
] as const;

const ROTATION_ORIG_ATTRS = [
STUDIO_ORIGINAL_ROTATE_ATTR,
STUDIO_ORIGINAL_INLINE_ROTATE_ATTR,
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR,
] as const;

export function buildRotationPatches(element: HTMLElement): PatchOperation[] {
const ops: PatchOperation[] = [];
collectInlineStyleOps(element, ROTATION_STYLE_PROPS, ops);
ops.push({ type: "attribute", property: STUDIO_ROTATION_ATTR, value: "true" });
collectAttributeOps(element, ROTATION_ORIG_ATTRS, ops);
return ops;
}

export function buildClearRotationPatches(element: HTMLElement): PatchOperation[] {
const origInlineRotate = element.getAttribute(STUDIO_ORIGINAL_INLINE_ROTATE_ATTR);
const origRotationTransformOrigin = element.getAttribute(
STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR,
);
const ops: PatchOperation[] = [
{ type: "inline-style", property: STUDIO_ROTATION_PROP, value: null },
{ type: "inline-style", property: "rotate", value: origInlineRotate || null },
{
type: "inline-style",
property: "transform-origin",
value: origRotationTransformOrigin !== null ? origRotationTransformOrigin || null : null,
},
{ type: "attribute", property: STUDIO_ROTATION_ATTR, value: null },
{ type: "attribute", property: STUDIO_ROTATION_DRAFT_ATTR, value: null },
{ type: "attribute", property: STUDIO_ORIGINAL_ROTATE_ATTR, value: null },
{ type: "attribute", property: STUDIO_ORIGINAL_INLINE_ROTATE_ATTR, value: null },
{ type: "attribute", property: STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR, value: null },
];
appendTransformDisplayOps(element, ops);
return ops;
}

/* ── Motion patches ──────────────────────────────────────────────── */

const MOTION_ORIG_ATTRS = [
STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
STUDIO_MOTION_ORIGINAL_OPACITY_ATTR,
STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
] as const;

export function buildMotionPatches(element: HTMLElement): PatchOperation[] {
const motionJson = element.getAttribute(STUDIO_MOTION_ATTR);
if (!motionJson) return [];
const ops: PatchOperation[] = [
{ type: "attribute", property: STUDIO_MOTION_ATTR, value: motionJson },
];
collectAttributeOps(element, MOTION_ORIG_ATTRS, ops);
return ops;
}

export function buildClearMotionPatches(_element: HTMLElement): PatchOperation[] {
return [
{ type: "attribute", property: STUDIO_MOTION_ATTR, value: null },
{ type: "attribute", property: STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR, value: null },
{ type: "attribute", property: STUDIO_MOTION_ORIGINAL_OPACITY_ATTR, value: null },
{ type: "attribute", property: STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR, value: null },
];
}
Loading
Loading