From 4d127a038bf768eded68aae3cfa5c1b12241bdac Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 27 Mar 2026 00:55:15 +0100 Subject: chore: normalize line endings --- docs/actions.md | 170 ++++++++++---------- docs/countries-search.md | 102 ++++++------ docs/keyframes.md | 408 +++++++++++++++++++++++------------------------ 3 files changed, 340 insertions(+), 340 deletions(-) (limited to 'docs') diff --git a/docs/actions.md b/docs/actions.md index 44c2153f..3d001e86 100644 --- a/docs/actions.md +++ b/docs/actions.md @@ -1,85 +1,85 @@ -# Actions System - -Actions are the trigger layer for user-initiated operations. They connect keyboard shortcuts, UI buttons, and context menus to editor functionality. - -## Adding a New Action - -### 1. Define the action — `src/lib/actions/definitions.ts` - -Add an entry to the `ACTIONS` object: - -```typescript -"my-action": { - description: "What it does", - category: "editing", // playback | navigation | editing | selection | history | timeline | controls - defaultShortcuts: ["ctrl+m"], // optional - args: { someValue: "number" }, // optional, only if it takes args -}, -``` - -**If your shortcut uses a special key** (not a letter/digit), check `getPressedKey` in `src/stores/keybindings-store.ts` and add a case if it's missing: - -```typescript -if (key === "escape") return "escape"; -``` - -**If your action has a `defaultShortcuts`**, also add a keybindings migration so existing users get it (keybindings are persisted in localStorage — new defaults only apply to fresh installs): - -1. Create `src/stores/keybindings/migrations/vN-to-vN+1.ts`: - -```typescript -export function vNToVN1({ state }: { state: unknown }): unknown { - const s = state as { keybindings: Record; isCustomized: boolean }; - const keybindings = { ...s.keybindings }; - if (!keybindings["my-key"]) { - keybindings["my-key"] = "my-action"; - } - return { ...s, keybindings }; -} -``` - -2. Register it in `src/stores/keybindings/migrations/index.ts` and bump `CURRENT_VERSION`. - -### 2. Register the handler — `src/hooks/actions/use-editor-actions.ts` - -```typescript -useActionHandler( - "my-action", - () => { - editor.timeline.doSomething(); - }, - undefined, // isActive: MutableRefObject | boolean | undefined -); -``` - -### 3. Register arg types (if needed) — `src/lib/actions/types.ts` - -Only required if your action accepts arguments: - -```typescript -export type TActionArgsMap = { - // ...existing actions... - "my-action": { someValue: number } | undefined; // | undefined = optional args -}; -``` - -## Invoking Actions - -Use `invokeAction` for any user-triggered operation (buttons, context menus, etc.): - -```typescript -import { invokeAction } from "@/lib/actions"; - -invokeAction("my-action"); -invokeAction("seek-forward", { seconds: 5 }); -``` - -Avoid calling `editor.xxx()` directly from UI components — that bypasses the action layer (toasts, validation feedback, keybinding support). - -## The `isActive` parameter - -The third argument to `useActionHandler` controls when the handler is active: - -- `undefined` — always active -- `true` / `false` — statically enabled/disabled -- `MutableRefObject` — reactive, toggled at runtime (e.g. only active when a panel is focused) +# Actions System + +Actions are the trigger layer for user-initiated operations. They connect keyboard shortcuts, UI buttons, and context menus to editor functionality. + +## Adding a New Action + +### 1. Define the action — `src/lib/actions/definitions.ts` + +Add an entry to the `ACTIONS` object: + +```typescript +"my-action": { + description: "What it does", + category: "editing", // playback | navigation | editing | selection | history | timeline | controls + defaultShortcuts: ["ctrl+m"], // optional + args: { someValue: "number" }, // optional, only if it takes args +}, +``` + +**If your shortcut uses a special key** (not a letter/digit), check `getPressedKey` in `src/stores/keybindings-store.ts` and add a case if it's missing: + +```typescript +if (key === "escape") return "escape"; +``` + +**If your action has a `defaultShortcuts`**, also add a keybindings migration so existing users get it (keybindings are persisted in localStorage — new defaults only apply to fresh installs): + +1. Create `src/stores/keybindings/migrations/vN-to-vN+1.ts`: + +```typescript +export function vNToVN1({ state }: { state: unknown }): unknown { + const s = state as { keybindings: Record; isCustomized: boolean }; + const keybindings = { ...s.keybindings }; + if (!keybindings["my-key"]) { + keybindings["my-key"] = "my-action"; + } + return { ...s, keybindings }; +} +``` + +2. Register it in `src/stores/keybindings/migrations/index.ts` and bump `CURRENT_VERSION`. + +### 2. Register the handler — `src/hooks/actions/use-editor-actions.ts` + +```typescript +useActionHandler( + "my-action", + () => { + editor.timeline.doSomething(); + }, + undefined, // isActive: MutableRefObject | boolean | undefined +); +``` + +### 3. Register arg types (if needed) — `src/lib/actions/types.ts` + +Only required if your action accepts arguments: + +```typescript +export type TActionArgsMap = { + // ...existing actions... + "my-action": { someValue: number } | undefined; // | undefined = optional args +}; +``` + +## Invoking Actions + +Use `invokeAction` for any user-triggered operation (buttons, context menus, etc.): + +```typescript +import { invokeAction } from "@/lib/actions"; + +invokeAction("my-action"); +invokeAction("seek-forward", { seconds: 5 }); +``` + +Avoid calling `editor.xxx()` directly from UI components — that bypasses the action layer (toasts, validation feedback, keybinding support). + +## The `isActive` parameter + +The third argument to `useActionHandler` controls when the handler is active: + +- `undefined` — always active +- `true` / `false` — statically enabled/disabled +- `MutableRefObject` — reactive, toggled at runtime (e.g. only active when a panel is focused) diff --git a/docs/countries-search.md b/docs/countries-search.md index 62ed3913..8bce73b4 100644 --- a/docs/countries-search.md +++ b/docs/countries-search.md @@ -1,51 +1,51 @@ -# Countries search — Data Guide - -## Structure - -Each country entry in `apps/web/public/countries.json` follows this shape: - -```json -{ - "name": "Denmark", - "code": "DK", - "languages": ["danish"], - "flag_colors": ["red", "white"], - "region": "Northern Europe" -} -``` - -### name -Use the commonly recognized country or territory name. - -### code -Use the canonical country/territory code. - -### flag_colors -Canonical color names only: `red`, `white`, `blue`, `green`, `yellow`, `black`, `orange`, `purple`. - -### languages -The official or widely spoken languages. - -### region -Consistent values — don't mix "Europe" and "Western Europe" for the same type of country. - -| Region | Examples | -|---|---| -| Northern Europe | Denmark, Sweden, Norway, Finland, Iceland | -| Western Europe | France, Germany, Switzerland, UK, Netherlands | -| Southern Europe | Italy, Spain, Greece, Portugal | -| Eastern Europe | Poland, Russia, Czech Republic, Hungary | -| Middle East | Saudi Arabia, UAE, Israel, Turkey | -| East Asia | Japan, China, South Korea | -| Southeast Asia | Thailand, Vietnam, Indonesia, Singapore | -| South Asia | India, Pakistan, Bangladesh | -| Central Asia | Kazakhstan, Uzbekistan | -| North Africa | Egypt, Morocco, Tunisia | -| Sub-Saharan Africa | Nigeria, Kenya, South Africa, Ethiopia | -| North America | USA, Canada, Mexico | -| Central America | Panama, Costa Rica, Guatemala | -| Caribbean | Cuba, Bahamas, Jamaica | -| South America | Brazil, Argentina, Colombia | -| Oceania | Australia, New Zealand | -| Atlantic Ocean | Ascension Island, Saint Helena | -| North Atlantic | Bermuda | +# Countries search — Data Guide + +## Structure + +Each country entry in `apps/web/public/countries.json` follows this shape: + +```json +{ + "name": "Denmark", + "code": "DK", + "languages": ["danish"], + "flag_colors": ["red", "white"], + "region": "Northern Europe" +} +``` + +### name +Use the commonly recognized country or territory name. + +### code +Use the canonical country/territory code. + +### flag_colors +Canonical color names only: `red`, `white`, `blue`, `green`, `yellow`, `black`, `orange`, `purple`. + +### languages +The official or widely spoken languages. + +### region +Consistent values — don't mix "Europe" and "Western Europe" for the same type of country. + +| Region | Examples | +|---|---| +| Northern Europe | Denmark, Sweden, Norway, Finland, Iceland | +| Western Europe | France, Germany, Switzerland, UK, Netherlands | +| Southern Europe | Italy, Spain, Greece, Portugal | +| Eastern Europe | Poland, Russia, Czech Republic, Hungary | +| Middle East | Saudi Arabia, UAE, Israel, Turkey | +| East Asia | Japan, China, South Korea | +| Southeast Asia | Thailand, Vietnam, Indonesia, Singapore | +| South Asia | India, Pakistan, Bangladesh | +| Central Asia | Kazakhstan, Uzbekistan | +| North Africa | Egypt, Morocco, Tunisia | +| Sub-Saharan Africa | Nigeria, Kenya, South Africa, Ethiopia | +| North America | USA, Canada, Mexico | +| Central America | Panama, Costa Rica, Guatemala | +| Caribbean | Cuba, Bahamas, Jamaica | +| South America | Brazil, Argentina, Colombia | +| Oceania | Australia, New Zealand | +| Atlantic Ocean | Ascension Island, Saint Helena | +| North Atlantic | Bermuda | diff --git a/docs/keyframes.md b/docs/keyframes.md index 97619626..afa85846 100644 --- a/docs/keyframes.md +++ b/docs/keyframes.md @@ -1,204 +1,204 @@ -# Keyframe System - -Keyframes allow element properties to change over time. The system is split into three layers: the **data model** (how keyframes are stored), the **registry** (which properties support keyframes and how to read/write them), and the **UI** (hooks and components that wire it all together). - -## How It Works - -### Data model - -Every `BaseTimelineElement` has an optional `animations?: ElementAnimations` field: - -```typescript -interface ElementAnimations { - channels: Record; -} -``` - -A channel is a typed bucket of keyframes keyed by property path (e.g. `"opacity"`, `"background.color"`). Three channel types exist: `NumberAnimationChannel`, `ColorAnimationChannel`, and `DiscreteAnimationChannel`. - -### Registry - -`src/lib/animation/property-registry.ts` defines which property paths are animatable and how to read/write their values on an element. `src/types/animation.ts` holds the canonical list of valid paths in `ANIMATION_PROPERTY_PATHS`. - -### Resolver - -`src/lib/animation/resolve.ts` provides functions that return the effective value of a property at a given local time — falling back to the element's static value when no keyframes exist. - -### Renderer - -Nodes in `src/services/renderer/` call the resolve functions before drawing so that animated properties interpolate correctly during export and preview. - -### UI - -Two hooks in `src/components/editor/panels/properties/hooks/` handle the keyframe-aware field logic: - -- `useKeyframedNumberProperty` — for numeric fields (opacity, position, scale, etc.) -- `useKeyframedColorProperty` — for color pickers - -Both hooks handle the toggle/add/remove keyframe flow and automatically switch between writing to the static property and writing to the animation channel depending on whether keyframes are active. - ---- - -## Adding a New Animatable Property - -Using `"background.paddingX"` as an example. - -### 1. Register the path — `src/types/animation.ts` - -```typescript -export const ANIMATION_PROPERTY_PATHS = [ - // ...existing paths - "background.paddingX", -] as const; -``` - -### 2. Add a registry entry — `src/lib/animation/property-registry.ts` - -```typescript -"background.paddingX": { - valueKind: "number", // "number" | "color" | "discrete" - defaultInterpolation: "linear", - numericRange: { min: 0 }, // optional, only for number properties - supportsElement: ({ element }) => element.type === "text", - getValue: ({ element }) => - element.type === "text" - ? (element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX) - : null, - setValue: ({ element, value }) => - element.type === "text" - ? { ...element, background: { ...element.background, paddingX: value as number } } - : element, -}, -``` - -**Notes:** -- `getValue` must return the effective value including any defaults — this is what gets recorded when a keyframe is added. -- `setValue` receives `AnimationValue` (`number | string | boolean`). Cast to the correct type since `coerceAnimationValueForProperty` already validated it upstream. -- For color properties, use `valueKind: "color"` and cast `value as string`. - -### 3. Add a resolve function — `src/lib/animation/resolve.ts` - -For **numbers**, use the existing generic `resolveNumberAtTime`: - -```typescript -import { resolveNumberAtTime } from "@/lib/animation"; - -const resolvedPaddingX = resolveNumberAtTime({ - baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, - animations: element.animations, - propertyPath: "background.paddingX", - localTime, -}); -``` - -For **colors**, use `resolveColorAtTime`: - -```typescript -const resolvedColor = resolveColorAtTime({ - baseColor: element.color, - animations: element.animations, - propertyPath: "color", - localTime, -}); -``` - -If neither fits (new value kind), add a dedicated resolve function following the same pattern as `resolveOpacityAtTime` and export it from `src/lib/animation/index.ts`. - -### 4. Wire the renderer - -In the relevant node (`src/services/renderer/nodes/`), call the resolve function before drawing: - -```typescript -const resolvedPaddingX = resolveNumberAtTime({ - baseValue: this.params.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, - animations: this.params.animations, - propertyPath: "background.paddingX", - localTime, -}); -``` - -Use the resolved value (not `this.params.*`) anywhere that value affects rendering. - -### 5. Wire the UI - -In the properties panel, replace `usePropertyDraft` with the appropriate keyframe hook and add a `KeyframeToggle` to the field. - -**For number fields:** - -```typescript -const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({ - startTime: element.startTime, - duration: element.duration, -}); - -const resolvedPaddingX = resolveNumberAtTime({ - baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, - animations: element.animations, - propertyPath: "background.paddingX", - localTime, -}); - -const paddingX = useKeyframedNumberProperty({ - trackId, - elementId: element.id, - animations: element.animations, - propertyPath: "background.paddingX", - localTime, - isPlayheadWithinElementRange, - displayValue: Math.round(resolvedPaddingX).toString(), - parse: (input) => { - const parsed = parseFloat(input); - return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed)); - }, - valueAtPlayhead: resolvedPaddingX, - buildBaseUpdates: ({ value }) => ({ - background: { ...element.background, paddingX: value }, - }), -}); -``` - -In JSX: - -```tsx - - } -> - paddingX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingX })} - isDefault={isPropertyAtDefault({ - hasAnimatedKeyframes: paddingX.hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue: resolvedPaddingX, - staticValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, - defaultValue: DEFAULT_TEXT_BACKGROUND.paddingX, - })} - /> - -``` - -**For color fields**, use `useKeyframedColorProperty` instead. It returns `{ onChange, onChangeEnd, toggleKeyframe, isKeyframedAtTime }` — wire `onChange({ color })` and `onChangeEnd` directly to the `ColorPicker`. - ---- - -## Checklist - -- [ ] Path added to `ANIMATION_PROPERTY_PATHS` -- [ ] Registry entry added with correct `valueKind`, `supportsElement`, `getValue`, `setValue` -- [ ] Resolve call added in the renderer node -- [ ] UI field uses `useKeyframedNumberProperty` or `useKeyframedColorProperty` (not `usePropertyDraft`) -- [ ] `KeyframeToggle` added to the `SectionField` -- [ ] `onReset` calls `commitValue` (not `editor.timeline.updateElements` directly) +# Keyframe System + +Keyframes allow element properties to change over time. The system is split into three layers: the **data model** (how keyframes are stored), the **registry** (which properties support keyframes and how to read/write them), and the **UI** (hooks and components that wire it all together). + +## How It Works + +### Data model + +Every `BaseTimelineElement` has an optional `animations?: ElementAnimations` field: + +```typescript +interface ElementAnimations { + channels: Record; +} +``` + +A channel is a typed bucket of keyframes keyed by property path (e.g. `"opacity"`, `"background.color"`). Three channel types exist: `NumberAnimationChannel`, `ColorAnimationChannel`, and `DiscreteAnimationChannel`. + +### Registry + +`src/lib/animation/property-registry.ts` defines which property paths are animatable and how to read/write their values on an element. `src/types/animation.ts` holds the canonical list of valid paths in `ANIMATION_PROPERTY_PATHS`. + +### Resolver + +`src/lib/animation/resolve.ts` provides functions that return the effective value of a property at a given local time — falling back to the element's static value when no keyframes exist. + +### Renderer + +Nodes in `src/services/renderer/` call the resolve functions before drawing so that animated properties interpolate correctly during export and preview. + +### UI + +Two hooks in `src/components/editor/panels/properties/hooks/` handle the keyframe-aware field logic: + +- `useKeyframedNumberProperty` — for numeric fields (opacity, position, scale, etc.) +- `useKeyframedColorProperty` — for color pickers + +Both hooks handle the toggle/add/remove keyframe flow and automatically switch between writing to the static property and writing to the animation channel depending on whether keyframes are active. + +--- + +## Adding a New Animatable Property + +Using `"background.paddingX"` as an example. + +### 1. Register the path — `src/types/animation.ts` + +```typescript +export const ANIMATION_PROPERTY_PATHS = [ + // ...existing paths + "background.paddingX", +] as const; +``` + +### 2. Add a registry entry — `src/lib/animation/property-registry.ts` + +```typescript +"background.paddingX": { + valueKind: "number", // "number" | "color" | "discrete" + defaultInterpolation: "linear", + numericRange: { min: 0 }, // optional, only for number properties + supportsElement: ({ element }) => element.type === "text", + getValue: ({ element }) => + element.type === "text" + ? (element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX) + : null, + setValue: ({ element, value }) => + element.type === "text" + ? { ...element, background: { ...element.background, paddingX: value as number } } + : element, +}, +``` + +**Notes:** +- `getValue` must return the effective value including any defaults — this is what gets recorded when a keyframe is added. +- `setValue` receives `AnimationValue` (`number | string | boolean`). Cast to the correct type since `coerceAnimationValueForProperty` already validated it upstream. +- For color properties, use `valueKind: "color"` and cast `value as string`. + +### 3. Add a resolve function — `src/lib/animation/resolve.ts` + +For **numbers**, use the existing generic `resolveNumberAtTime`: + +```typescript +import { resolveNumberAtTime } from "@/lib/animation"; + +const resolvedPaddingX = resolveNumberAtTime({ + baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, + animations: element.animations, + propertyPath: "background.paddingX", + localTime, +}); +``` + +For **colors**, use `resolveColorAtTime`: + +```typescript +const resolvedColor = resolveColorAtTime({ + baseColor: element.color, + animations: element.animations, + propertyPath: "color", + localTime, +}); +``` + +If neither fits (new value kind), add a dedicated resolve function following the same pattern as `resolveOpacityAtTime` and export it from `src/lib/animation/index.ts`. + +### 4. Wire the renderer + +In the relevant node (`src/services/renderer/nodes/`), call the resolve function before drawing: + +```typescript +const resolvedPaddingX = resolveNumberAtTime({ + baseValue: this.params.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, + animations: this.params.animations, + propertyPath: "background.paddingX", + localTime, +}); +``` + +Use the resolved value (not `this.params.*`) anywhere that value affects rendering. + +### 5. Wire the UI + +In the properties panel, replace `usePropertyDraft` with the appropriate keyframe hook and add a `KeyframeToggle` to the field. + +**For number fields:** + +```typescript +const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({ + startTime: element.startTime, + duration: element.duration, +}); + +const resolvedPaddingX = resolveNumberAtTime({ + baseValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, + animations: element.animations, + propertyPath: "background.paddingX", + localTime, +}); + +const paddingX = useKeyframedNumberProperty({ + trackId, + elementId: element.id, + animations: element.animations, + propertyPath: "background.paddingX", + localTime, + isPlayheadWithinElementRange, + displayValue: Math.round(resolvedPaddingX).toString(), + parse: (input) => { + const parsed = parseFloat(input); + return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed)); + }, + valueAtPlayhead: resolvedPaddingX, + buildBaseUpdates: ({ value }) => ({ + background: { ...element.background, paddingX: value }, + }), +}); +``` + +In JSX: + +```tsx + + } +> + paddingX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingX })} + isDefault={isPropertyAtDefault({ + hasAnimatedKeyframes: paddingX.hasAnimatedKeyframes, + isPlayheadWithinElementRange, + resolvedValue: resolvedPaddingX, + staticValue: element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX, + defaultValue: DEFAULT_TEXT_BACKGROUND.paddingX, + })} + /> + +``` + +**For color fields**, use `useKeyframedColorProperty` instead. It returns `{ onChange, onChangeEnd, toggleKeyframe, isKeyframedAtTime }` — wire `onChange({ color })` and `onChangeEnd` directly to the `ColorPicker`. + +--- + +## Checklist + +- [ ] Path added to `ANIMATION_PROPERTY_PATHS` +- [ ] Registry entry added with correct `valueKind`, `supportsElement`, `getValue`, `setValue` +- [ ] Resolve call added in the renderer node +- [ ] UI field uses `useKeyframedNumberProperty` or `useKeyframedColorProperty` (not `usePropertyDraft`) +- [ ] `KeyframeToggle` added to the `SectionField` +- [ ] `onReset` calls `commitValue` (not `editor.timeline.updateElements` directly) -- cgit v1.3.1