Skip to content

feat: rectangle and ellipse shape annotations (#275)#499

Draft
Enriquefft wants to merge 13 commits intosiddharthvaddem:mainfrom
Enriquefft:feat/shape-annotations-275
Draft

feat: rectangle and ellipse shape annotations (#275)#499
Enriquefft wants to merge 13 commits intosiddharthvaddem:mainfrom
Enriquefft:feat/shape-annotations-275

Conversation

@Enriquefft
Copy link
Copy Markdown
Contributor

@Enriquefft Enriquefft commented Apr 27, 2026

Closes #275.

Adds rectangle and ellipse shape annotations alongside the existing arrow. Discriminator field kind on FigureData; render dispatch in overlay and export. Optional with default "arrow" on load — legacy projects unchanged.

What changed

  • FigureData.kind?: "arrow" | "rectangle" | "ellipse" discriminator (optional; defaults to "arrow" via load-path normalization in projectPersistence.ts).
  • Overlay renderer (AnnotationOverlay.tsx) and exporter (annotationRenderer.ts) dispatch on kind ?? "arrow"; exhaustive : never defaults.
  • Two new toolbar buttons (rectangle, ellipse) in the timeline annotation toolbar.
  • Inspector Fill section for closed shapes: toggle + 0–100 integer opacity slider driving the alpha byte of figureData.fill.
  • New figureFill.ts utility — single source of truth for #RRGGBB[AA] parsing/formatting; throws on malformed input. All previously hardcoded ${color}33 / "#34B27B33" literals routed through it.
  • Color/fill RGB stay coherent: changing border color preserves fill alpha; changing fill color preserves opacity slider value.

Files

  • src/components/video-editor/types.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/AnnotationOverlay.tsx
  • src/components/video-editor/AnnotationSettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/figureFill.ts (new)
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/lib/exporter/annotationRenderer.ts
  • src/i18n/locales/{en,zh-CN,zh-TW,es,fr,tr,ko-KR}/{timeline,settings}.json (4 keys × 7 locales)

~280 LOC source + 28 i18n entries.

Conflict-free against open PRs

#350, #489, #370, #240 — see commit 1 description for the conflict-avoidance map.

CI

  • npm run lint ✓ (touched files clean; pre-existing warnings on useScreenRecorder.ts and untracked .mcp.json predate this PR)
  • npx tsc --noEmit
  • npx vite build
  • npm run test:browser ✓ (6/6 pass)
  • npm run i18n:check — added keys present in all 7 locales; pre-existing parity violations on blur*, zoom.speed.*, audio.* are out of scope.

Notes

  • i18n keys use the existing timeline and settings namespaces (buttons.addRectangle, buttons.addEllipse, annotation.fill.label, annotation.fill.toggle, annotation.fill.opacity) for consistency with surrounding code, rather than a new editor namespace. Spec §5.5 sketched an editor namespace placement; the chosen namespaces match where these keys actually fire from (TimelineEditor, AnnotationSettingsPanel).
  • zh-TW covered alongside the 6 locales the spec listed, matching the rest of the repo's locale roster.

Test plan

  • Add a rectangle annotation from the toolbar → renders a stroked rectangle at the playhead, exports correctly to MP4 and GIF.
  • Add an ellipse annotation → same.
  • Toggle Fill on for a rectangle → 20% translucent fill of the border color appears.
  • Drag the opacity slider → only the fill alpha changes; fill RGB stays put even after diverging fill color from border.
  • Change the border color → fill RGB updates to match while preserving the user's slider value.
  • Open a legacy project saved before this PR → existing arrow annotations still render unchanged (kind populated as "arrow" by load-path normalization).
  • Verify all 7 locales show localized labels for the new toolbar tooltips and inspector Fill / Opacity strings.

Summary by CodeRabbit

  • New Features
    • Added rectangle and ellipse shape annotations alongside arrows.
    • Added fill color and opacity controls for shapes (including a toggle and slider).
    • Added toolbar buttons in the timeline for quick rectangle/ellipse insertion.
    • Added translations for the new shape and fill UI across multiple locales.

Introduce a discriminated-union field on FigureData to distinguish
the upcoming rectangle and ellipse annotation kinds from the existing
arrow. Adds an optional fill color used by closed shapes.

Legacy projects load with kind defaulted to "arrow" at the single
normalization site in projectPersistence.normalizeProjectEditor; no
schema bump required.

Pure data-model groundwork — no rendering, toolbar, or inspector
changes in this commit.
Render the rectangle annotation kind in the in-editor SVG overlay and
the export canvas pipeline. Both renderers now switch on
FigureData.kind with an exhaustive default arm; the existing arrow
path is preserved unchanged.

The ellipse arm is a no-op stub here; it will be filled in by the next
commit.

Rectangles use FigureData.color for stroke and FigureData.fill (when
set) for fill, matching the export pipeline pixel-for-pixel.
Fill in the ellipse arm of the renderFigure switch in both the SVG
overlay and the export canvas pipeline. Mirrors the structure used
for rectangle: an inscribed ellipse with non-scaling stroke,
FigureData.color for stroke and FigureData.fill (when set) for fill.

The default _exhaustiveCheck arm now passes for all three kinds; no
placeholder branches remain.
Add two new buttons to the timeline annotation toolbar that create
shape annotations: rectangle (Square icon) and ellipse (Circle icon).
Each new annotation is born with kind set to its shape, the existing
default color, and a translucent fill matching the border color
(alpha 0x33).

The annotation-added handler is widened to accept an optional
FigureData. When provided, the region is constructed directly as
type "figure" with that figureData; the legacy text-annotation path
is preserved unchanged for the existing arrow button.

Tooltip and aria-label use placeholder i18n keys that the next
commit (i18n) will translate across all six locales.
Add a Fill section to the annotation inspector that lets users
toggle and edit the fill color of rectangle and ellipse
annotations. The section is hidden when the selected annotation is
an arrow.

Toggling fill on derives a translucent color from the current
border color (alpha 0x33), matching the default produced by the
toolbar buttons. Toggling off sets fill to undefined so the shape
renders as outline-only.

Purely additive below the existing color block; no refactor of the
existing inspector controls. Labels use placeholder i18n keys that
the next commit will translate across all six locales.
Adds the 4 new translation keys used by Commits 4 (toolbar) and 5
(inspector fill section) to every locale already shipped in the repo,
keeping locale parity intact:

- timeline.buttons.addRectangle
- timeline.buttons.addEllipse
- settings.annotation.fill.label
- settings.annotation.fill.toggle

Locales: en, zh-CN, es, fr, tr, ko-KR (the 6 listed in the spec) plus
zh-TW (which exists in src/i18n/locales/ and is enforced by
i18n:check). Spec §5.5 originally listed 9 keys; only 4 ended up
referenced by the implementation, so only those are added — no dead
strings.
Spec §3 declares kind as optional. The previous commits implemented it
as required, which was a type-contract regression. All runtime consumers
now read figureData.kind ?? "arrow" so a fresh FigureData without an
explicit kind is treated as the legacy arrow shape. Load-path
normalization in projectPersistence.ts continues to fill in the default
for stored projects, so persisted state is unchanged.

Exhaustive switches in AnnotationOverlay and annotationRenderer are
preserved by switching on the narrowed local rather than figureData.kind
directly; the never-typed default arm still proves exhaustiveness.
Introduce figureFill.ts as the single source of truth for hex-color
handling on figure annotations: typed parser/formatter for #RRGGBB and
#RRGGBBAA, alpha extraction, and percent<->alpha conversions. Malformed
input throws; no silent fallbacks.

The inspector's Fill section now exposes an integer opacity slider
(0-100) per spec §5.3. The slider drives the alpha byte of
figureData.fill while figureData.color remains the RGB source. When the
user changes the border color, the fill adopts the new RGB while
preserving its current alpha; when they pick a new fill color, the same
invariant holds. The previously hardcoded 0x33 alpha string is gone
from both the inspector and the timeline toolbar buttons; both call
sites now compose fills via withAlpha(color, percentToAlpha(20)).
Adds annotation.fill.opacity to every settings namespace so the new
inspector slider label localizes correctly. Translations chosen for
visual parity with surrounding annotation.fill keys.
The slider's onChange wrote withAlpha(figureData.color, …) which silently
overwrote the fill's RGB with the border color whenever a user had
diverged the two via the fill color picker. Read RGB from the current
fillValue instead so the slider only changes the alpha byte.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds rectangle and ellipse figure kinds with optional fill/alpha; updates rendering (overlay + exporter), settings UI, types, persistence, timeline tools, and i18n; preserves arrow behavior and validates/normalizes figure data.

Changes

Cohort / File(s) Summary
Type System & Utilities
src/components/video-editor/types.ts, src/components/video-editor/figureFill.ts, src/components/video-editor/projectPersistence.ts
Introduces kind (`"arrow"
Overlay & Settings UI
src/components/video-editor/AnnotationOverlay.tsx, src/components/video-editor/AnnotationSettingsPanel.tsx
Refactors arrow-only drawing to renderFigure dispatcher (defaults to "arrow"), adds inline SVG rendering for rectangle/ellipse with stroke/fill; settings panel adds fill controls (toggle, color picker, opacity) and preserves alpha when stroke color changes.
Annotation Creation & Timeline Tools
src/components/video-editor/VideoEditor.tsx, src/components/video-editor/timeline/TimelineEditor.tsx
handleAnnotationAdded accepts optional figureData to create "figure" annotations; timeline editor adds callbacks and toolbar buttons to insert rectangle/ellipse figures with precomputed green stroke + semi-transparent fill; prop type updated.
Export Rendering
src/lib/exporter/annotationRenderer.ts
renderFigure dispatcher added for canvas export: arrow (existing), rectangle and ellipse (stroke + optional fill); warns and skips unsupported kinds.
Internationalization
src/i18n/locales/*/settings.json, src/i18n/locales/*/timeline.json (en, es, fr, ko-KR, tr, zh-CN, zh-TW)
Adds annotation.fill translation block (label, toggle, opacity) and timeline keys buttons.addRectangle / buttons.addEllipse across locales.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested reviewers

  • siddharthvaddem

Poem

arrows made room for shapes tonight,
rectangles stand square, ellipses spin light,
alpha tucked in hex like a secret hue,
reviewers, take a peek—lowkey risky but true 😉
✨🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main change: adding rectangle and ellipse shape annotations. It's specific, avoids vague terms, and directly reflects the primary objective.
Description check ✅ Passed Description is well-structured, covers all template sections, includes motivation, implementation details, file list, testing plan, and CI results. Exceeds minimum requirements with detailed technical depth.
Linked Issues check ✅ Passed PR fully addresses #275 by implementing shape annotations (rectangle, ellipse) alongside arrows with UI controls, rendering, export, and i18n support as requested.
Out of Scope Changes check ✅ Passed All changes stay in scope: shape rendering dispatch, fill controls, toolbar buttons, i18n keys, and utility functions directly support the shape annotations feature. No unrelated refactoring or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/video-editor/projectPersistence.ts (1)

374-380: ⚠️ Potential issue | 🟠 Major

unknown kind values aren't normalized — escapes to renderers and trips the exhaustive check

region.figureData.kind ?? "arrow" only handles null/undefined. a persisted project with kind: "triangle" (corrupt file, future-version-downgrade, hand-edited json, etc.) flows straight through and hits _exhaustiveCheck: never in AnnotationOverlay.renderFigure and the same dispatch in the exporter. lowkey risky for backward/forward compatibility.

similar concern for fill: it's not validated here, and figureFill parsers throw on malformed input — a bad string in a loaded project would crash the inspector/renderer at first interaction rather than degrading gracefully.

proposed fix
+const VALID_FIGURE_KINDS = new Set(["arrow", "rectangle", "ellipse"] as const);
+const HEX_FILL_RE = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
+
...
 						figureData: region.figureData
 							? {
 									...DEFAULT_FIGURE_DATA,
 									...region.figureData,
-									kind: region.figureData.kind ?? "arrow",
+									kind:
+										region.figureData.kind &&
+										VALID_FIGURE_KINDS.has(region.figureData.kind)
+											? region.figureData.kind
+											: "arrow",
+									fill:
+										typeof region.figureData.fill === "string" &&
+										HEX_FILL_RE.test(region.figureData.fill)
+											? region.figureData.fill
+											: undefined,
 								}
 							: undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/projectPersistence.ts` around lines 374 - 380,
Persisted figure kinds and fills aren't being validated: region.figureData.kind
uses nullish coalescing so unknown strings (e.g., "triangle") pass through and
later hit the exhaustive failure in AnnotationOverlay.renderFigure, and
malformed fills propagate to figureFill parsers; update the normalization in the
place building figureData (where region.figureData is merged with
DEFAULT_FIGURE_DATA) to validate and map kind against the known enum/union (fall
back to "arrow" for any unrecognized value) and sanitize/validate fill values
before assigning (use the same validator/parsers used by figureFill or a safe
mapper), ensuring DEFAULT_FIGURE_DATA, region.figureData, and the exported
payload only ever contain normalized kinds/fills to prevent runtime
_exhaustiveCheck errors and parser throws.
🧹 Nitpick comments (9)
src/i18n/locales/zh-TW/settings.json (1)

143-147: LGTM (with a tiny zh-TW nit)

structure matches en. minor regional flavor: zh-TW often prefers "填滿" over "填充" (which is more zh-CN flavored), but "填充" is widely understood. totally optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/zh-TW/settings.json` around lines 143 - 147, Change the
zh-TW wording for the "fill" entries to the more regionally preferred term:
update the JSON object with key "fill" so that "label": "填充" -> "填滿" and
"toggle": "啟用填充" -> "啟用填滿" (keep "opacity": "不透明度" unchanged); target the "fill"
object in src/i18n/locales/zh-TW/settings.json (keys: "fill", "label", "toggle")
and apply the replacements.
src/components/video-editor/figureFill.ts (2)

84-96: NaN slips through the clamp

Math.max(0, NaN) returns NaN, and Math.round(NaN) is NaN. so a non-finite input here produces a NaN alpha that will later blow up in formatHexColor's Number.isInteger check with a confusing error far from the source. probably never happens in practice (slider values are finite), but a defensive Number.isFinite guard would make the failure mode obvious.

proposed tweak
 export function alphaToPercent(alpha: number): number {
+	if (!Number.isFinite(alpha)) {
+		throw new Error(`Invalid alpha (expected finite number): ${alpha}`);
+	}
 	const clamped = Math.min(255, Math.max(0, alpha));
 	return Math.round((clamped / 255) * 100);
 }

 export function percentToAlpha(percent: number): number {
+	if (!Number.isFinite(percent)) {
+		throw new Error(`Invalid percent (expected finite number): ${percent}`);
+	}
 	const clamped = Math.min(100, Math.max(0, percent));
 	return Math.round((clamped / 100) * 255);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/figureFill.ts` around lines 84 - 96,
alphaToPercent and percentToAlpha currently let NaN/non-finite values pass
through the clamp (Math.max/min) and produce NaN results; add an explicit
Number.isFinite check at the start of each function (alphaToPercent and
percentToAlpha) and if the input is not finite, throw a clear TypeError (or
range error) that includes the function name and the invalid value so the
failure is immediate and obvious rather than propagating NaN into
formatHexColor.

9-11: doc says "throws on malformed input" but the conversion fns silently clamp

alphaToPercent / percentToAlpha clamp out-of-range values rather than throwing, which contradicts the module-level "All functions throw on malformed input" claim. either tighten the doc to scope the throw promise to the parse/format/withAlpha helpers, or make the conversions throw on out-of-range too. nit-tier, but future-you will thank you.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/figureFill.ts` around lines 9 - 11, The
module-level doc promises that "All functions throw on malformed input" but
alphaToPercent and percentToAlpha currently clamp out-of-range values; change
them to validate inputs and throw on invalid ranges instead of clamping to
satisfy the doc. Specifically, in alphaToPercent and percentToAlpha add explicit
range checks (e.g., reject alpha <0 or >1 and percent <0 or >100) and throw a
clear Error mentioning the function name and the invalid value; update any tests
or callers that assumed clamping accordingly. If you prefer to keep clamping,
instead update the module comment to limit the "throws" guarantee to
parse/format/withAlpha helpers and document that the conversion helpers clamp —
but pick one approach and apply it consistently across alphaToPercent,
percentToAlpha, and any related utilities.
src/i18n/locales/es/settings.json (1)

152-152: nit: toggle wording reads as a state, not a toggle label

"toggle": "Relleno activado" translates to "Fill activated/enabled" (a status), while other locales use imperative forms (zh-CN 启用填充 = "Enable fill", ko-KR 채우기 사용 = "Use fill"). assuming the en source is something like "Enable fill", consider:

proposed wording
-			"toggle": "Relleno activado",
+			"toggle": "Activar relleno",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/es/settings.json` at line 152, The Spanish locale string for
the settings key "toggle" is phrased as a state ("Relleno activado") but should
use an imperative/label form to match other locales; update the "toggle" value
in settings.json (the "toggle" key) to an imperative like "Activar relleno" or
"Habilitar relleno" so it reads as a toggle label rather than a status.
src/components/video-editor/types.ts (1)

64-70: recommended: lean into a proper discriminated union

shape works fine as-is and the kind ?? "arrow" fallback keeps legacy data happy, but arrowDirection being required on rectangle/ellipse is kinda cursed — those shapes don't have a direction. same for fill only being meaningful on closed shapes. consumers currently switch on kind and just ignore the irrelevant fields.

if you want better compile-time guarantees later, a discriminated union narrows the renderer/inspector branches naturally:

sketch (optional, not blocking)
interface ArrowFigureData {
  kind?: "arrow"; // optional for legacy load-path
  arrowDirection: ArrowDirection;
  color: string;
  strokeWidth: number;
}

interface ClosedShapeFigureData {
  kind: "rectangle" | "ellipse";
  color: string;
  strokeWidth: number;
  fill?: string;
}

export type FigureData = ArrowFigureData | ClosedShapeFigureData;

then renderFigure's switch narrows fields per branch and you can drop the ?? "none" / ?? "arrow" defensive bits inside arrow branches.

happy to defer this — sticking with the flat shape is fine for v1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/types.ts` around lines 64 - 70, FigureData
currently forces arrowDirection on all shapes and mixes fields; change it to a
discriminated union so each kind only has relevant properties: create
ArrowFigureData (kind?: "arrow" for legacy, arrowDirection: ArrowDirection,
color, strokeWidth) and ClosedShapeFigureData (kind: "rectangle" | "ellipse",
color, strokeWidth, fill?: string), then export type FigureData =
ArrowFigureData | ClosedShapeFigureData and update consumers like renderFigure
to rely on narrowing instead of optional fallbacks.
src/i18n/locales/tr/timeline.json (1)

7-8: nit: capitalization is a bit off vs. siblings

other Turkish button labels in this file use Title Case ("Yakınlaştırma Ekle", "Kırpma Ekle", "Açıklama Ekle"), but these two go sentence case ("Dikdörtgen ekle", "Elips ekle"). kinda sticks out. align with the pattern for consistency:

proposed tweak
-		"addRectangle": "Dikdörtgen ekle",
-		"addEllipse": "Elips ekle",
+		"addRectangle": "Dikdörtgen Ekle",
+		"addEllipse": "Elips Ekle",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/i18n/locales/tr/timeline.json` around lines 7 - 8, The translation
entries for addRectangle and addEllipse use sentence case; update their values
to Title Case to match sibling labels (change "Dikdörtgen ekle" → "Dikdörtgen
Ekle" and "Elips ekle" → "Elips Ekle") so the keys "addRectangle" and
"addEllipse" are consistent with other Turkish button labels in this locale
file.
src/components/video-editor/AnnotationOverlay.tsx (1)

188-244: rendering looks right; tiny nit on the exhaustive default.

shapes look good — vectorEffect="non-scaling-stroke" + overflow:visible keeps stroke width in screen pixels and consistent with the canvas exporter's strokeRect/stroke so preview ↔ export shouldn't drift.

one nit: the default branch returns the never-typed _exhaustiveCheck. compile-time it's an exhaustiveness assertion, but at runtime if some legacy/forward-compat data ever sneaks past normalization, you'd render the literal string ("circle", "polygon", whatever) into the dom as text. cheap to harden:

♻️ safer default
 			default: {
 				const _exhaustiveCheck: never = kind;
-				return _exhaustiveCheck;
+				void _exhaustiveCheck;
+				return null;
 			}

also worth a sanity check: arrow renders inside p-2 (so it shrinks within the box) while rect/ellipse fill the full container. probably intentional, but means same Rnd dimensions = visually different shape sizes. confirm that's the design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/AnnotationOverlay.tsx` around lines 188 - 244,
renderFigure's default currently returns the never-typed _exhaustiveCheck which
is fine for compile-time exhaustiveness but will render the unexpected string at
runtime; update the default branch in renderFigure (the _exhaustiveCheck
handling for kind) to instead log or warn about the unexpected kind (include the
value) and return a safe fallback such as null (or a visually-neutral
placeholder) so no unexpected text is injected into the DOM at runtime.
src/components/video-editor/timeline/TimelineEditor.tsx (1)

1224-1270: dedupe these two handlers — they're 95% the same function.

both handlers do identical timing/clamping work and only differ by kind. easy win to factor out:

♻️ proposed factory
-	const handleAddRectangleAnnotation = useCallback(() => {
-		if (!videoDuration || videoDuration === 0 || totalMs === 0 || !onAnnotationAdded) {
-			return;
-		}
-
-		const defaultDuration = Math.min(defaultRegionDurationMs, totalMs);
-		if (defaultDuration <= 0) {
-			return;
-		}
-
-		const startPos = Math.max(0, Math.min(currentTimeMs, totalMs));
-		const endPos = Math.min(startPos + defaultDuration, totalMs);
-
-		const figureData: FigureData = {
-			kind: "rectangle",
-			arrowDirection: "right",
-			color: "#34B27B",
-			strokeWidth: 4,
-			fill: withAlpha("#34B27B", percentToAlpha(20)),
-		};
-
-		onAnnotationAdded({ start: startPos, end: endPos }, figureData);
-	}, [videoDuration, totalMs, currentTimeMs, onAnnotationAdded, defaultRegionDurationMs]);
-
-	const handleAddEllipseAnnotation = useCallback(() => {
-		// ...same body, kind: "ellipse"
-	}, [...]);
+	const addShapeAnnotation = useCallback(
+		(kind: "rectangle" | "ellipse") => {
+			if (!videoDuration || videoDuration === 0 || totalMs === 0 || !onAnnotationAdded) return;
+			const defaultDuration = Math.min(defaultRegionDurationMs, totalMs);
+			if (defaultDuration <= 0) return;
+			const startPos = Math.max(0, Math.min(currentTimeMs, totalMs));
+			const endPos = Math.min(startPos + defaultDuration, totalMs);
+			const figureData: FigureData = {
+				kind,
+				arrowDirection: "right",
+				color: "#34B27B",
+				strokeWidth: 4,
+				fill: withAlpha("#34B27B", percentToAlpha(20)),
+			};
+			onAnnotationAdded({ start: startPos, end: endPos }, figureData);
+		},
+		[videoDuration, totalMs, currentTimeMs, onAnnotationAdded, defaultRegionDurationMs],
+	);
+	const handleAddRectangleAnnotation = useCallback(() => addShapeAnnotation("rectangle"), [addShapeAnnotation]);
+	const handleAddEllipseAnnotation = useCallback(() => addShapeAnnotation("ellipse"), [addShapeAnnotation]);

side note: the arrowDirection: "right" placeholder is a low-key smell — FigureData.arrowDirection is required even for closed shapes, so every rect/ellipse drags a meaningless field around. probably worth a follow-up to make it optional or move to a discriminated union, but out of scope here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/timeline/TimelineEditor.tsx` around lines 1224 -
1270, The two handlers handleAddRectangleAnnotation and
handleAddEllipseAnnotation duplicate the same timing/clamping logic and only
differ by FigureData.kind; refactor by extracting a shared factory/helper (e.g.,
createAddAnnotationHandler or addAnnotationWithKind) that computes
defaultDuration, startPos and endPos, builds the common FigureData fields and
accepts the differing kind value, then return/use two callbacks produced from
that factory (or a single parameterized handler) wired into the UI; ensure you
keep the same dependencies (videoDuration, totalMs, currentTimeMs,
onAnnotationAdded, defaultRegionDurationMs) in the resulting useCallback(s) and
remove the duplicate code blocks for rectangle/ellipse.
src/components/video-editor/AnnotationSettingsPanel.tsx (1)

615-714: nit: extract the fill block into its own component.

the inline IIFE returning ~100 lines of jsx is kinda cursed to read inside an already-deep TabsContent. pulling this out into a <FigureFillControls figureData={...} onChange={onFigureDataChange} /> would shrink this file, make the closed-shape gating explicit, and let you unit-test the percent↔alpha plumbing in isolation. purely optional, but your future self will thank you.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/AnnotationSettingsPanel.tsx` around lines 615 -
714, The large inline IIFE rendering the fill UI should be extracted into a new
presentational component (e.g. FigureFillControls) to improve readability and
testability: create a FigureFillControls React component that accepts props {
figureData, onFigureDataChange, colorPalette, t } (or whatever context you
already use), move the JSX that references figureData, fillEnabled logic,
defaultFillFromColor, fillValue, getAlpha/alphaToPercent/percentToAlpha,
withAlpha, Popover/PopoverTrigger/PopoverContent, Block, Slider and Switch into
that component, keep the closed-shape gating (the isClosedShape check) in the
parent or make the new component return null when figureData is missing/not a
closed shape, and replace the IIFE with <FigureFillControls
figureData={annotation.figureData} onFigureDataChange={onFigureDataChange} /> so
the percent↔alpha plumbing (getAlpha, percentToAlpha, alphaToPercent, withAlpha)
is isolated and easier to unit test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/AnnotationSettingsPanel.tsx`:
- Around line 626-647: The Switch (component instance using
checked={fillEnabled} and onCheckedChange) lacks an accessible name; add an
accessible label by giving the visible label or the span a unique id (e.g.,
"annotation-fill-label" or "annotation-fill-toggle") and add
aria-labelledby="<that-id>" to the Switch (or add aria-label with
t("annotation.fill.label") directly on the Switch) so screen readers announce
the purpose of the control; update the JSX around the label/span and the Switch
(referencing the Switch, fillEnabled, and onCheckedChange) to include this
association.

In `@src/i18n/locales/es/timeline.json`:
- Around line 7-8: The keys addRectangle and addEllipse use "Añadir" but other
toolbar keys (e.g., addZoom, addTrim, addAnnotation, addSpeed, addBlur) use
"Agregar"; update the translations for addRectangle and addEllipse to use
"Agregar" (i.e., change their values to "Agregar rectángulo" and "Agregar
elipse") to keep verb usage consistent across the timeline toolbar.

---

Outside diff comments:
In `@src/components/video-editor/projectPersistence.ts`:
- Around line 374-380: Persisted figure kinds and fills aren't being validated:
region.figureData.kind uses nullish coalescing so unknown strings (e.g.,
"triangle") pass through and later hit the exhaustive failure in
AnnotationOverlay.renderFigure, and malformed fills propagate to figureFill
parsers; update the normalization in the place building figureData (where
region.figureData is merged with DEFAULT_FIGURE_DATA) to validate and map kind
against the known enum/union (fall back to "arrow" for any unrecognized value)
and sanitize/validate fill values before assigning (use the same
validator/parsers used by figureFill or a safe mapper), ensuring
DEFAULT_FIGURE_DATA, region.figureData, and the exported payload only ever
contain normalized kinds/fills to prevent runtime _exhaustiveCheck errors and
parser throws.

---

Nitpick comments:
In `@src/components/video-editor/AnnotationOverlay.tsx`:
- Around line 188-244: renderFigure's default currently returns the never-typed
_exhaustiveCheck which is fine for compile-time exhaustiveness but will render
the unexpected string at runtime; update the default branch in renderFigure (the
_exhaustiveCheck handling for kind) to instead log or warn about the unexpected
kind (include the value) and return a safe fallback such as null (or a
visually-neutral placeholder) so no unexpected text is injected into the DOM at
runtime.

In `@src/components/video-editor/AnnotationSettingsPanel.tsx`:
- Around line 615-714: The large inline IIFE rendering the fill UI should be
extracted into a new presentational component (e.g. FigureFillControls) to
improve readability and testability: create a FigureFillControls React component
that accepts props { figureData, onFigureDataChange, colorPalette, t } (or
whatever context you already use), move the JSX that references figureData,
fillEnabled logic, defaultFillFromColor, fillValue,
getAlpha/alphaToPercent/percentToAlpha, withAlpha,
Popover/PopoverTrigger/PopoverContent, Block, Slider and Switch into that
component, keep the closed-shape gating (the isClosedShape check) in the parent
or make the new component return null when figureData is missing/not a closed
shape, and replace the IIFE with <FigureFillControls
figureData={annotation.figureData} onFigureDataChange={onFigureDataChange} /> so
the percent↔alpha plumbing (getAlpha, percentToAlpha, alphaToPercent, withAlpha)
is isolated and easier to unit test.

In `@src/components/video-editor/figureFill.ts`:
- Around line 84-96: alphaToPercent and percentToAlpha currently let
NaN/non-finite values pass through the clamp (Math.max/min) and produce NaN
results; add an explicit Number.isFinite check at the start of each function
(alphaToPercent and percentToAlpha) and if the input is not finite, throw a
clear TypeError (or range error) that includes the function name and the invalid
value so the failure is immediate and obvious rather than propagating NaN into
formatHexColor.
- Around line 9-11: The module-level doc promises that "All functions throw on
malformed input" but alphaToPercent and percentToAlpha currently clamp
out-of-range values; change them to validate inputs and throw on invalid ranges
instead of clamping to satisfy the doc. Specifically, in alphaToPercent and
percentToAlpha add explicit range checks (e.g., reject alpha <0 or >1 and
percent <0 or >100) and throw a clear Error mentioning the function name and the
invalid value; update any tests or callers that assumed clamping accordingly. If
you prefer to keep clamping, instead update the module comment to limit the
"throws" guarantee to parse/format/withAlpha helpers and document that the
conversion helpers clamp — but pick one approach and apply it consistently
across alphaToPercent, percentToAlpha, and any related utilities.

In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 1224-1270: The two handlers handleAddRectangleAnnotation and
handleAddEllipseAnnotation duplicate the same timing/clamping logic and only
differ by FigureData.kind; refactor by extracting a shared factory/helper (e.g.,
createAddAnnotationHandler or addAnnotationWithKind) that computes
defaultDuration, startPos and endPos, builds the common FigureData fields and
accepts the differing kind value, then return/use two callbacks produced from
that factory (or a single parameterized handler) wired into the UI; ensure you
keep the same dependencies (videoDuration, totalMs, currentTimeMs,
onAnnotationAdded, defaultRegionDurationMs) in the resulting useCallback(s) and
remove the duplicate code blocks for rectangle/ellipse.

In `@src/components/video-editor/types.ts`:
- Around line 64-70: FigureData currently forces arrowDirection on all shapes
and mixes fields; change it to a discriminated union so each kind only has
relevant properties: create ArrowFigureData (kind?: "arrow" for legacy,
arrowDirection: ArrowDirection, color, strokeWidth) and ClosedShapeFigureData
(kind: "rectangle" | "ellipse", color, strokeWidth, fill?: string), then export
type FigureData = ArrowFigureData | ClosedShapeFigureData and update consumers
like renderFigure to rely on narrowing instead of optional fallbacks.

In `@src/i18n/locales/es/settings.json`:
- Line 152: The Spanish locale string for the settings key "toggle" is phrased
as a state ("Relleno activado") but should use an imperative/label form to match
other locales; update the "toggle" value in settings.json (the "toggle" key) to
an imperative like "Activar relleno" or "Habilitar relleno" so it reads as a
toggle label rather than a status.

In `@src/i18n/locales/tr/timeline.json`:
- Around line 7-8: The translation entries for addRectangle and addEllipse use
sentence case; update their values to Title Case to match sibling labels (change
"Dikdörtgen ekle" → "Dikdörtgen Ekle" and "Elips ekle" → "Elips Ekle") so the
keys "addRectangle" and "addEllipse" are consistent with other Turkish button
labels in this locale file.

In `@src/i18n/locales/zh-TW/settings.json`:
- Around line 143-147: Change the zh-TW wording for the "fill" entries to the
more regionally preferred term: update the JSON object with key "fill" so that
"label": "填充" -> "填滿" and "toggle": "啟用填充" -> "啟用填滿" (keep "opacity": "不透明度"
unchanged); target the "fill" object in src/i18n/locales/zh-TW/settings.json
(keys: "fill", "label", "toggle") and apply the replacements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb46bf6f-1f25-4891-ab5f-9b4138281e00

📥 Commits

Reviewing files that changed from the base of the PR and between 1fefde8 and 9bd4261.

📒 Files selected for processing (22)
  • src/components/video-editor/AnnotationOverlay.tsx
  • src/components/video-editor/AnnotationSettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/figureFill.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/components/video-editor/types.ts
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/en/timeline.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/es/timeline.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/fr/timeline.json
  • src/i18n/locales/ko-KR/settings.json
  • src/i18n/locales/ko-KR/timeline.json
  • src/i18n/locales/tr/settings.json
  • src/i18n/locales/tr/timeline.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-CN/timeline.json
  • src/i18n/locales/zh-TW/settings.json
  • src/i18n/locales/zh-TW/timeline.json
  • src/lib/exporter/annotationRenderer.ts

Comment thread src/components/video-editor/AnnotationSettingsPanel.tsx
Comment thread src/i18n/locales/es/timeline.json Outdated
@Enriquefft Enriquefft marked this pull request as draft April 27, 2026 18:59
- es: addRectangle/addEllipse "Añadir" → "Agregar" (sibling consistency)
- tr: addRectangle/addEllipse sentence → Title Case
- zh-TW: fill.label/toggle 填充 → 填滿 (regional preference)
- figureFill: alphaToPercent/percentToAlpha throw on non-finite or
  out-of-range input, matching the module's documented contract; add
  isValidHexColor guard for untrusted input.
- types: derive FigureKind from a single FIGURE_KINDS const so the
  union and runtime validator share one source of truth.
- projectPersistence: normalize unknown kind values from loaded JSON
  back to "arrow" and drop malformed fill strings before they reach
  the renderer or hex parsers.
- AnnotationOverlay/annotationRenderer: exhaustive-default branches
  warn and return null/void instead of returning the never-typed
  kind string (which would have been injected into the DOM if a bad
  payload ever slipped past validation).
The Switch in the figure tab had no accessible name; pair its
label with an id and reference it via aria-labelledby so screen
readers announce the control's purpose.
@Enriquefft Enriquefft marked this pull request as ready for review April 28, 2026 01:26
@Enriquefft Enriquefft marked this pull request as draft April 28, 2026 01:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/video-editor/types.ts (1)

64-72: Make kind required in the runtime model.

Now that legacy data is normalized on load and DEFAULT_FIGURE_DATA always sets a kind, keeping FigureData.kind optional here just leaks persistence concerns into app state. every consumer still needs ?? "arrow", and the new switches aren't really exhaustive. lowkey cleaner to keep kind? only in the raw/persisted shape and make runtime FigureData.kind required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/types.ts` around lines 64 - 72, FigureData
currently has kind optional which forces callers to guard with defaults; update
the runtime model by making kind required (change FigureData.kind from optional
to required using the existing FigureKind type), keep legacy/raw persisted
shapes as the only place where kind remains optional, and ensure
DEFAULT_FIGURE_DATA continues to provide a kind so switches over FigureData are
exhaustive; look for symbols FIGURE_KINDS, FigureKind, FigureData, and
DEFAULT_FIGURE_DATA to apply this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/AnnotationSettingsPanel.tsx`:
- Around line 615-619: The UI shows arrow-specific controls for closed shapes;
use the existing figureData.kind check (the isClosedShape boolean computed from
annotation.figureData in AnnotationSettingsPanel) to conditionally hide the
"Arrow Direction" grid and the "Arrow Color" label when kind !== "arrow", and
change the shared color control label to a generic term like "Stroke" or
"Border" so it is correct for both arrows and closed shapes; update the
conditional rendering around the Arrow Direction/Arrow Color UI blocks to rely
on isClosedShape (or its negation) and rename the color label text used by the
shared color control.

---

Nitpick comments:
In `@src/components/video-editor/types.ts`:
- Around line 64-72: FigureData currently has kind optional which forces callers
to guard with defaults; update the runtime model by making kind required (change
FigureData.kind from optional to required using the existing FigureKind type),
keep legacy/raw persisted shapes as the only place where kind remains optional,
and ensure DEFAULT_FIGURE_DATA continues to provide a kind so switches over
FigureData are exhaustive; look for symbols FIGURE_KINDS, FigureKind,
FigureData, and DEFAULT_FIGURE_DATA to apply this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94b3681d-155a-4a5b-8ef3-037935280bc9

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd4261 and e02807e.

📒 Files selected for processing (9)
  • src/components/video-editor/AnnotationOverlay.tsx
  • src/components/video-editor/AnnotationSettingsPanel.tsx
  • src/components/video-editor/figureFill.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/types.ts
  • src/i18n/locales/es/timeline.json
  • src/i18n/locales/tr/timeline.json
  • src/i18n/locales/zh-TW/settings.json
  • src/lib/exporter/annotationRenderer.ts
✅ Files skipped from review due to trivial changes (3)
  • src/i18n/locales/tr/timeline.json
  • src/i18n/locales/zh-TW/settings.json
  • src/i18n/locales/es/timeline.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/exporter/annotationRenderer.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/figureFill.ts

Comment on lines +615 to +619
{(() => {
const figureData = annotation.figureData;
if (!figureData) return null;
const isClosedShape = (figureData.kind ?? "arrow") !== "arrow";
if (!isClosedShape) return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Closed shapes still inherit arrow-only controls.

This branch already knows when kind !== "arrow", but rectangles/ellipses still show the Arrow Direction grid and Arrow Color label above it. that’s lowkey confusing: direction becomes a no-op, and the color label is wrong. Reuse the same kind check to hide arrow-only controls and rename the shared color control to something generic like Stroke/Border.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/AnnotationSettingsPanel.tsx` around lines 615 -
619, The UI shows arrow-specific controls for closed shapes; use the existing
figureData.kind check (the isClosedShape boolean computed from
annotation.figureData in AnnotationSettingsPanel) to conditionally hide the
"Arrow Direction" grid and the "Arrow Color" label when kind !== "arrow", and
change the shared color control label to a generic term like "Stroke" or
"Border" so it is correct for both arrows and closed shapes; update the
conditional rendering around the Arrow Direction/Arrow Color UI blocks to rely
on isClosedShape (or its negation) and rename the color label text used by the
shared color control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: allow to add annotation with Shape

1 participant