chore: switch from biome to eslint + prettier; fix ton of lint issues

This commit is contained in:
Maze Winther
2026-04-29 00:37:56 +02:00
parent 6ec818fc11
commit d7692b4c79
159 changed files with 5630 additions and 5255 deletions
@@ -1,231 +0,0 @@
import { useEditor } from "@/editor/use-editor";
import { clamp } from "@/utils/math";
import { NumberField } from "@/components/ui/number-field";
import { OcCheckerboardIcon } from "@/components/icons";
import { Fragment, useRef } from "react";
import { useMenuPreview } from "@/editor/use-menu-preview";
import {
Section,
SectionContent,
SectionField,
SectionHeader,
SectionTitle,
} from "@/components/section";
import {
Select,
SelectContent,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { BlendMode } from "@/rendering";
import type { ElementType } from "@/timeline";
import type { ElementAnimations } from "@/animation/types";
import { HugeiconsIcon } from "@hugeicons/react";
import { RainDropIcon } from "@hugeicons/core-free-icons";
import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
import { resolveOpacityAtTime } from "@/animation/values";
import { DEFAULTS } from "@/timeline/defaults";
import { isPropertyAtDefault } from "./transform-tab";
import type { MediaTime } from "@/wasm";
type BlendingElement = {
id: string;
opacity: number;
type: ElementType;
blendMode?: BlendMode;
startTime: MediaTime;
duration: MediaTime;
animations?: ElementAnimations;
};
const BLEND_MODE_GROUPS: { value: BlendMode; label: string }[][] = [
[{ value: "normal", label: "Normal" }],
[
{ value: "darken", label: "Darken" },
{ value: "multiply", label: "Multiply" },
{ value: "color-burn", label: "Color Burn" },
],
[
{ value: "lighten", label: "Lighten" },
{ value: "screen", label: "Screen" },
{ value: "plus-lighter", label: "Plus Lighter" },
{ value: "color-dodge", label: "Color Dodge" },
],
[
{ value: "overlay", label: "Overlay" },
{ value: "soft-light", label: "Soft Light" },
{ value: "hard-light", label: "Hard Light" },
],
[
{ value: "difference", label: "Difference" },
{ value: "exclusion", label: "Exclusion" },
],
[
{ value: "hue", label: "Hue" },
{ value: "saturation", label: "Saturation" },
{ value: "color", label: "Color" },
{ value: "luminosity", label: "Luminosity" },
],
];
export function BlendingTab({
element,
trackId,
}: {
element: BlendingElement;
trackId: string;
}) {
const editor = useEditor();
const isPreviewActive = useEditor((e) => e.timeline.isPreviewActive());
const blendMode = element.blendMode ?? DEFAULTS.element.blendMode;
const committedBlendModeRef = useRef(blendMode);
if (!isPreviewActive) {
committedBlendModeRef.current = blendMode;
}
const {
onPointerLeave,
onOpenChange: handleBlendModeOpenChange,
markCommitted,
} = useMenuPreview();
const previewBlendMode = ({ value }: { value: BlendMode }) =>
editor.timeline.previewElements({
updates: [
{ trackId, elementId: element.id, updates: { blendMode: value } },
],
});
const commitBlendMode = (value: BlendMode) => {
if (editor.timeline.isPreviewActive()) {
editor.timeline.commitPreview();
} else {
editor.timeline.updateElements({
updates: [
{
trackId,
elementId: element.id,
patch: { blendMode: value },
},
],
});
}
markCommitted();
};
const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
startTime: element.startTime,
duration: element.duration,
});
const resolvedOpacity = resolveOpacityAtTime({
baseOpacity: element.opacity,
animations: element.animations,
localTime,
});
const opacity = useKeyframedNumberProperty({
trackId,
elementId: element.id,
animations: element.animations,
propertyPath: "opacity",
localTime,
isPlayheadWithinElementRange,
displayValue: Math.round(resolvedOpacity * 100).toString(),
parse: (input) => {
const parsed = parseFloat(input);
if (Number.isNaN(parsed)) return null;
return clamp({ value: parsed, min: 0, max: 100 }) / 100;
},
valueAtPlayhead: resolvedOpacity,
step: 0.01,
buildBaseUpdates: ({ value }) => ({ opacity: value }),
});
return (
<Section collapsible sectionKey={`${element.id}:blending`}>
<SectionHeader>
<SectionTitle>Blending</SectionTitle>
</SectionHeader>
<SectionContent>
<div className="flex items-start gap-2">
<SectionField
label="Opacity"
className="w-1/2"
beforeLabel={
<KeyframeToggle
isActive={opacity.isKeyframedAtTime}
isDisabled={!isPlayheadWithinElementRange}
title="Toggle opacity keyframe"
onToggle={opacity.toggleKeyframe}
/>
}
>
<NumberField
className="w-full"
icon={
<OcCheckerboardIcon className="size-3.5 text-muted-foreground" />
}
value={opacity.displayValue}
min={0}
max={100}
onFocus={opacity.onFocus}
onChange={opacity.onChange}
onBlur={opacity.onBlur}
onScrub={opacity.scrubTo}
onScrubEnd={opacity.commitScrub}
onReset={() =>
opacity.commitValue({ value: DEFAULTS.element.opacity })
}
isDefault={isPropertyAtDefault({
hasAnimatedKeyframes: opacity.hasAnimatedKeyframes,
isPlayheadWithinElementRange,
resolvedValue: resolvedOpacity,
staticValue: element.opacity,
defaultValue: DEFAULTS.element.opacity,
})}
dragSensitivity="slow"
/>
</SectionField>
<SectionField label="Blend mode" className="w-1/2">
<Select
value={committedBlendModeRef.current}
onOpenChange={handleBlendModeOpenChange}
onValueChange={commitBlendMode}
>
<SelectTrigger
icon={<HugeiconsIcon icon={RainDropIcon} />}
className="w-full"
>
<SelectValue placeholder="Select blend mode" />
</SelectTrigger>
<SelectContent className="w-36" onPointerLeave={onPointerLeave}>
{BLEND_MODE_GROUPS.map((group, groupIndex) => (
<Fragment key={group[0]?.value ?? `group-${groupIndex}`}>
{group.map((option) => (
<SelectItem
key={option.value}
value={option.value}
onPointerEnter={() =>
previewBlendMode({ value: option.value })
}
>
{option.label}
</SelectItem>
))}
{groupIndex < BLEND_MODE_GROUPS.length - 1 ? (
<SelectSeparator />
) : null}
</Fragment>
))}
</SelectContent>
</Select>
</SectionField>
</div>
</SectionContent>
</Section>
);
}
@@ -1,462 +0,0 @@
import { NumberField } from "@/components/ui/number-field";
import { useEditor } from "@/editor/use-editor";
import { clamp, isNearlyEqual } from "@/utils/math";
import type { VisualElement } from "@/timeline";
import {
Section,
SectionContent,
SectionField,
SectionFields,
SectionHeader,
SectionTitle,
} from "@/components/section";
import { Button } from "@/components/ui/button";
import { HugeiconsIcon } from "@hugeicons/react";
import {
ArrowExpandIcon,
Link05Icon,
RotateClockwiseIcon,
} from "@hugeicons/core-free-icons";
import {
getGroupKeyframesAtTime,
hasGroupKeyframeAtTime,
} from "@/animation";
import { resolveTransformAtTime } from "@/rendering/animation-values";
import { DEFAULTS } from "@/timeline/defaults";
import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead";
import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle";
import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property";
import { usePropertiesStore } from "@/components/editor/panels/properties/stores/properties-store";
export function parseNumericInput({ input }: { input: string }): number | null {
const parsed = parseFloat(input);
return Number.isNaN(parsed) ? null : parsed;
}
export function isPropertyAtDefault({
hasAnimatedKeyframes,
isPlayheadWithinElementRange,
resolvedValue,
staticValue,
defaultValue,
}: {
hasAnimatedKeyframes: boolean;
isPlayheadWithinElementRange: boolean;
resolvedValue: number;
staticValue: number;
defaultValue: number;
}): boolean {
if (hasAnimatedKeyframes && isPlayheadWithinElementRange) {
return isNearlyEqual({
leftValue: resolvedValue,
rightValue: defaultValue,
});
}
return staticValue === defaultValue;
}
export function TransformTab({
element,
trackId,
}: {
element: VisualElement;
trackId: string;
}) {
const editor = useEditor();
const isScaleLocked = usePropertiesStore((s) => s.isTransformScaleLocked);
const setTransformScaleLocked = usePropertiesStore(
(s) => s.setTransformScaleLocked,
);
const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
startTime: element.startTime,
duration: element.duration,
});
const resolvedTransform = resolveTransformAtTime({
baseTransform: element.transform,
animations: element.animations,
localTime,
});
const positionX = useKeyframedNumberProperty({
trackId,
elementId: element.id,
animations: element.animations,
propertyPath: "transform.positionX",
localTime,
isPlayheadWithinElementRange,
displayValue: Math.round(resolvedTransform.position.x).toString(),
parse: (input) => parseNumericInput({ input }),
valueAtPlayhead: resolvedTransform.position.x,
step: 1,
buildBaseUpdates: ({ value }) => ({
transform: {
...element.transform,
position: { ...element.transform.position, x: value },
},
}),
});
const positionY = useKeyframedNumberProperty({
trackId,
elementId: element.id,
animations: element.animations,
propertyPath: "transform.positionY",
localTime,
isPlayheadWithinElementRange,
displayValue: Math.round(resolvedTransform.position.y).toString(),
parse: (input) => parseNumericInput({ input }),
valueAtPlayhead: resolvedTransform.position.y,
step: 1,
buildBaseUpdates: ({ value }) => ({
transform: {
...element.transform,
position: { ...element.transform.position, y: value },
},
}),
});
const parseScale = (input: string) => {
const parsed = parseNumericInput({ input });
if (parsed === null) return null;
return parsed / 100;
};
const scaleX = useKeyframedNumberProperty({
trackId,
elementId: element.id,
animations: element.animations,
propertyPath: "transform.scaleX",
localTime,
isPlayheadWithinElementRange,
displayValue: Math.round(resolvedTransform.scaleX * 100).toString(),
parse: parseScale,
valueAtPlayhead: resolvedTransform.scaleX,
step: 0.01,
buildBaseUpdates: ({ value }) => ({
transform: {
...element.transform,
scaleX: value,
...(isScaleLocked ? { scaleY: value } : {}),
},
}),
buildAdditionalKeyframes: isScaleLocked
? ({ value }) => [{ propertyPath: "transform.scaleY", value }]
: undefined,
});
const scaleY = useKeyframedNumberProperty({
trackId,
elementId: element.id,
animations: element.animations,
propertyPath: "transform.scaleY",
localTime,
isPlayheadWithinElementRange,
displayValue: Math.round(resolvedTransform.scaleY * 100).toString(),
parse: parseScale,
valueAtPlayhead: resolvedTransform.scaleY,
step: 0.01,
buildBaseUpdates: ({ value }) => ({
transform: {
...element.transform,
scaleY: value,
...(isScaleLocked ? { scaleX: value } : {}),
},
}),
buildAdditionalKeyframes: isScaleLocked
? ({ value }) => [{ propertyPath: "transform.scaleX", value }]
: undefined,
});
const scaleFieldPropsX = {
value: scaleX.displayValue,
onFocus: scaleX.onFocus,
onChange: scaleX.onChange,
onBlur: scaleX.onBlur,
dragSensitivity: "slow" as const,
onScrub: scaleX.scrubTo,
onScrubEnd: scaleX.commitScrub,
onReset: () =>
scaleX.commitValue({ value: DEFAULTS.element.transform.scaleX }),
isDefault: isPropertyAtDefault({
hasAnimatedKeyframes: scaleX.hasAnimatedKeyframes,
isPlayheadWithinElementRange,
resolvedValue: resolvedTransform.scaleX,
staticValue: element.transform.scaleX,
defaultValue: DEFAULTS.element.transform.scaleX,
}),
};
const scaleFieldPropsY = {
value: scaleY.displayValue,
onFocus: scaleY.onFocus,
onChange: scaleY.onChange,
onBlur: scaleY.onBlur,
dragSensitivity: "slow" as const,
onScrub: scaleY.scrubTo,
onScrubEnd: scaleY.commitScrub,
onReset: () =>
scaleY.commitValue({ value: DEFAULTS.element.transform.scaleY }),
isDefault: isPropertyAtDefault({
hasAnimatedKeyframes: scaleY.hasAnimatedKeyframes,
isPlayheadWithinElementRange,
resolvedValue: resolvedTransform.scaleY,
staticValue: element.transform.scaleY,
defaultValue: DEFAULTS.element.transform.scaleY,
}),
};
const rotation = useKeyframedNumberProperty({
trackId,
elementId: element.id,
animations: element.animations,
propertyPath: "transform.rotate",
localTime,
isPlayheadWithinElementRange,
displayValue: Math.round(resolvedTransform.rotate).toString(),
parse: (input) => {
const parsed = parseNumericInput({ input });
if (parsed === null) return null;
return clamp({ value: parsed, min: -360, max: 360 });
},
valueAtPlayhead: resolvedTransform.rotate,
step: 1,
buildBaseUpdates: ({ value }) => ({
transform: {
...element.transform,
rotate: value,
},
}),
});
const hasScaleKeyframe = hasGroupKeyframeAtTime({
animations: element.animations,
group: "transform.scale",
time: localTime,
});
const toggleScaleKeyframe = () => {
if (!isPlayheadWithinElementRange) return;
const existing = getGroupKeyframesAtTime({
animations: element.animations,
group: "transform.scale",
time: localTime,
});
if (existing.length > 0) {
editor.timeline.removeKeyframes({
keyframes: existing.map((ref) => ({
trackId,
elementId: element.id,
...ref,
})),
});
return;
}
editor.timeline.upsertKeyframes({
keyframes: [
{
trackId,
elementId: element.id,
propertyPath: "transform.scaleX",
time: localTime,
value: resolvedTransform.scaleX,
},
{
trackId,
elementId: element.id,
propertyPath: "transform.scaleY",
time: localTime,
value: resolvedTransform.scaleY,
},
],
});
};
const scaleLockButton = (
<Button
type="button"
variant={isScaleLocked ? "secondary" : "ghost"}
size="icon"
aria-pressed={isScaleLocked}
onClick={() => setTransformScaleLocked(!isScaleLocked)}
>
<HugeiconsIcon icon={Link05Icon} />
</Button>
);
return (
<Section collapsible sectionKey={`${element.id}:transform`}>
<SectionHeader>
<SectionTitle>Transform</SectionTitle>
</SectionHeader>
<SectionContent>
<SectionFields>
<div className="flex items-end gap-2">
{isScaleLocked ? (
<>
<SectionField
label="Scale"
className="min-w-0 flex-1"
beforeLabel={
<KeyframeToggle
isActive={hasScaleKeyframe}
isDisabled={!isPlayheadWithinElementRange}
title="Toggle scale keyframe"
onToggle={toggleScaleKeyframe}
/>
}
>
<NumberField
icon={<HugeiconsIcon icon={ArrowExpandIcon} />}
{...scaleFieldPropsX}
/>
</SectionField>
{scaleLockButton}
</>
) : (
<>
<SectionField
label="Width"
className="min-w-0 flex-1"
beforeLabel={
<KeyframeToggle
isActive={scaleX.isKeyframedAtTime}
isDisabled={!isPlayheadWithinElementRange}
title="Toggle width scale keyframe"
onToggle={scaleX.toggleKeyframe}
/>
}
>
<NumberField icon="W" {...scaleFieldPropsX} />
</SectionField>
{scaleLockButton}
<SectionField
label="Height"
className="min-w-0 flex-1"
beforeLabel={
<KeyframeToggle
isActive={scaleY.isKeyframedAtTime}
isDisabled={!isPlayheadWithinElementRange}
title="Toggle height scale keyframe"
onToggle={scaleY.toggleKeyframe}
/>
}
>
<NumberField icon="H" {...scaleFieldPropsY} />
</SectionField>
</>
)}
</div>
<div className="flex items-end gap-2">
<SectionField
label="X"
className="min-w-0 flex-1"
beforeLabel={
<KeyframeToggle
isActive={positionX.isKeyframedAtTime}
isDisabled={!isPlayheadWithinElementRange}
title="Toggle X position keyframe"
onToggle={positionX.toggleKeyframe}
/>
}
>
<NumberField
icon="X"
value={positionX.displayValue}
onFocus={positionX.onFocus}
onChange={positionX.onChange}
onBlur={positionX.onBlur}
onScrub={positionX.scrubTo}
onScrubEnd={positionX.commitScrub}
onReset={() =>
positionX.commitValue({
value: DEFAULTS.element.transform.position.x,
})
}
isDefault={isPropertyAtDefault({
hasAnimatedKeyframes: positionX.hasAnimatedKeyframes,
isPlayheadWithinElementRange,
resolvedValue: resolvedTransform.position.x,
staticValue: element.transform.position.x,
defaultValue: DEFAULTS.element.transform.position.x,
})}
/>
</SectionField>
<SectionField
label="Y"
className="min-w-0 flex-1"
beforeLabel={
<KeyframeToggle
isActive={positionY.isKeyframedAtTime}
isDisabled={!isPlayheadWithinElementRange}
title="Toggle Y position keyframe"
onToggle={positionY.toggleKeyframe}
/>
}
>
<NumberField
icon="Y"
value={positionY.displayValue}
onFocus={positionY.onFocus}
onChange={positionY.onChange}
onBlur={positionY.onBlur}
onScrub={positionY.scrubTo}
onScrubEnd={positionY.commitScrub}
onReset={() =>
positionY.commitValue({
value: DEFAULTS.element.transform.position.y,
})
}
isDefault={isPropertyAtDefault({
hasAnimatedKeyframes: positionY.hasAnimatedKeyframes,
isPlayheadWithinElementRange,
resolvedValue: resolvedTransform.position.y,
staticValue: element.transform.position.y,
defaultValue: DEFAULTS.element.transform.position.y,
})}
/>
</SectionField>
</div>
<SectionField
label="Rotation"
beforeLabel={
<KeyframeToggle
isActive={rotation.isKeyframedAtTime}
isDisabled={!isPlayheadWithinElementRange}
title="Toggle rotation keyframe"
onToggle={rotation.toggleKeyframe}
/>
}
>
<div className="flex items-center gap-2">
<NumberField
icon={<HugeiconsIcon icon={RotateClockwiseIcon} />}
className="flex-none"
value={rotation.displayValue}
onFocus={rotation.onFocus}
onChange={rotation.onChange}
onBlur={rotation.onBlur}
dragSensitivity="slow"
onScrub={rotation.scrubTo}
onScrubEnd={rotation.commitScrub}
onReset={() =>
rotation.commitValue({
value: DEFAULTS.element.transform.rotate,
})
}
isDefault={isPropertyAtDefault({
hasAnimatedKeyframes: rotation.hasAnimatedKeyframes,
isPlayheadWithinElementRange,
resolvedValue: resolvedTransform.rotate,
staticValue: element.transform.rotate,
defaultValue: DEFAULTS.element.transform.rotate,
})}
/>
</div>
</SectionField>
</SectionFields>
</SectionContent>
</Section>
);
}