Skip to content

feat(core): figma motion → GSAP translator + /figma skill v1#1869

Merged
vanceingalls merged 8 commits into
mainfrom
vi/figma-02-motion-translator
Jul 3, 2026
Merged

feat(core): figma motion → GSAP translator + /figma skill v1#1869
vanceingalls merged 8 commits into
mainfrom
vi/figma-02-motion-translator

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What

The headline capability's core: Figma Motion → editable GSAP timeline, plus the first version of the /figma skill.

  • motionEase.ts — maps motion.dev ease values (named strings or [x1,y1,x2,y2] bezier arrays) to GSAP eases / CustomEase refs.
  • motionToGsap.tsMotionDocTimelineSpec: one tween per (element, property) track, keyframe durations from time deltas, and repeat: Infinity clamped to 0 (single play) for determinism — the runtime forbids repeat:-1; composition-duration-aware loop counts are a later milestone.
  • emitTimelineScript.tsTimelineSpec → a composition <script>: gsap.timeline({ paused: true }), tl.set initials + tl.to({ keyframes }) steps, CustomEase.create preamble, registered on window.__timelines["<literal-key>"] (string literal — Studio static analysis requires it).
  • /figma skill v1 + catalog wiring (superseded by the rewrite in feat(skills): reroute /figma by capability - REST/CLI for phases 1-3, MCP for 4-5 (M4) #1873, kept here for honest history).

Why

Figma Motion (Config 2026) is a timeline+keyframe model that maps ~1:1 onto HyperFrames' paused-GSAP contract — translation is structural, not interpretive. Spec §8 documents the exact property/easing mapping tables this implements.

Verified

Beyond unit tests (fixture MotionDoc → expected emitted script, literal-key/paused/finite assertions), this translator was exercised end-to-end against real get_motion_context payloads captured live from a Figma file — the output composition passed hyperframes lint + validate and rendered correctly.


Stack (2/6): #1868 → this PR → #1870#1871#1872#1873

🤖 Generated with Claude Code

@miga-heygen miga-heygen left a comment

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.

Review — feat(core): figma motion → GSAP translator + /figma skill v1

Verdict: LGTM with nits

Clean, well-structured translation pipeline. The three-layer architecture (motionEasemotionToGsapemitTimelineScript) is the right decomposition — each layer has a single concern and tests verify the contracts between them. Type safety is strong throughout; the MappedEase discriminated union is a nice touch. The skill definition reads well and covers the two primary import paths (asset, motion) with clear steps.


Findings

1. PR description vs. actual repeat clamping (nit — docs only)
The PR body says "repeat: Infinity clamped to a finite computed loop count (ceil(compDuration/cycle)-1)" but clampRepeat() actually clamps Infinity → 0 (play once). The test confirms the "single play" intent. The description is misleading about the mechanism — worth fixing to match reality, or leaving a comment that the composition-duration-aware repeat count is deferred to a later PR. Not a code issue, just a description gap.

2. deriveId strips only a single leading # or . (nit)
selector.replace(/^[#.]/, "") strips one prefix character. A selector like #my.class becomes my.classmy-classfigma-my-class — fine. But ##double (unlikely but syntactically valid in HTML id context) becomes #double-doublefigma--double. Not a real-world concern, just noting the regex is intentionally minimal.

3. resolveStepEase deduplicates by identity, not by value (observation)
If two segments use the same bezier array [0.539, 0, 0.312, 0.995], they get two separate CustomEase.create calls (hfCe0, hfCe1) with identical curves. The test fixture has exactly this case (4-keyframe opacity track with two identical bezier segments) and creates 2 custom eases. Functionally correct — GSAP handles duplicate creates fine — but the emitted script is slightly larger than necessary. Low priority; mention only because the PR mentions "deterministic rendering" as a goal and duplicate eases add noise to the output.

4. buildSteps silently skips segments with undefined values (worth considering)
The if (tPrev === undefined || tCur === undefined || value === undefined) continue; guard silently drops segments that have holes. For well-formed MotionDoc inputs this never fires (the buildTween length check catches full mismatches), but a partial hole like values: [0, undefined, 1] would silently produce a 1-step tween instead of throwing. The buildTween validation checks values.length and times.length agree, but doesn't verify individual elements are defined. This is defensive coding and probably fine for v1, but worth a comment explaining the intent — is this "graceful degradation" or "should never happen"?

5. Barrel re-export changes export *export type * (positive)
index.ts changing from export * from "./types" to export type * from "./types" is the right move — it ensures type-only re-exports don't create runtime dependencies. Clean.

6. parseFigmaRef.ts: .replace.replaceAll (positive)
Good catch on the replacereplaceAll fix. The original replace("-", ":") only replaced the first hyphen; node IDs like 123-456-789 would have produced 123:456-789. replaceAll fixes this correctly.

7. Ease mapping coverage (observation)
The NAMED_EASE table covers linear, ease, easeIn/Out/InOut, easeInAndOut, backIn/Out/InOut, backInAndOut, and hold. Missing from what Figma Motion might emit: spring, anticipate, circIn/Out, bounceIn/Out. The fallback to "none" is safe but silent — a console.warn in the skill's runtime (not in the pure translator) could help agents notice when they're hitting the fallback. Not blocking since the skill doc explicitly says "if a track uses shader/spring/effect props with no GSAP mapping, bake instead."

8. Skill definition quality (positive)
skills/figma/SKILL.md is well-structured: clear auth prereq, routing decision tree, step-by-step for both asset and motion paths, determinism rules at the bottom. The "re-import guard: findByFigmaNode before re-fetching" note is a good idempotency hint for agents.


Ponytail

The code is already quite lean for what it does. The only candidate for reduction is the CustomEaseCounter interface — it's a single-field wrapper around a number used as a mutable ref. A plain { value: number } object literal without the named interface would save 4 lines (interface + JSDoc), but the named interface does improve readability at call sites. net: ~4 lines possible, not worth it. Lean already. Ship.


Stats

  • 15 files changed, +359 / -14
  • Core logic: ~150 lines across 3 new modules (motionEase, motionToGsap, emitTimelineScript)
  • Tests: ~126 lines across 3 test files
  • Skill definition: 40 lines
  • Catalog/docs wiring: ~43 lines

Review by Miga

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed at 0460ce507b (stack member 2/6; sibling PRs #1868, #1870-#1873).
Note: body has 🤖 Claude Code footer + AI-trailer; HF convention flags this.

Summary — Adds a pure Figma Motion → GSAP timeline translator (motionToGsap, motionEase, emitTimelineScript) that emits a paused-timeline <script>, plus the /figma skill v1 for MCP-first routing. Structural translation matches the paused-GSAP contract; edge cases around infinite-repeat semantics and multi-selector docs are where the seams show.

Concerns

  • 🟠 PR body contradicts implementation for repeat: Infinity. Body says "repeat: Infinity clamped to a finite computed loop count ceil(compDuration/cycle)-1" but motionToGsap.ts:11-13 clampRepeat returns 0 (single play) for Infinity, and the test at motionToGsap.test.ts:33 documents "clamps Infinity -> 0 (single play) for determinism". Two very different behaviors — one preserves ~visual intent up to the composition duration, the other drops the loop entirely. Either the body is stale or the code is stale; pick one. The Headline fixture in tests declares repeat: Infinity and the runtime will play the fade-in-out once, silently, on every imported Figma Motion doc that used a looping animation.
  • 🟠 Unknown named ease silently falls back to linear with no signal. motionEase.ts:20 returns { kind: "named", ease: "none" } when NAMED_EASE[key] misses (test at motionEase.test.ts:24 locks this in). Fixture uses "wobble" but Figma Motion has real eases the current dictionary doesn't cover (circIn, expoOut, elasticOut, bounce*, anticipate). A designer's tuned ease turns into linear at import with no console warning, no return-channel to the caller — and the /figma skill has no docs about which eases survive. Suggest either (a) returning { kind: "unknown", original: ease } and letting motionToGsap push a warning into the TimelineSpec for the caller to surface, or (b) covering the full motion.dev ease set at minimum with a documented list.
  • 🟠 Bezier array not validated to length 4. mapEase at motionEase.ts:18 does if (Array.isArray(ease)) return { kind: "bezier", bezier: ease } — the TS type says [number, number, number, number] but that's not enforced at runtime. A malformed motion.dev payload (3 numbers, 5 numbers, NaN, non-numeric) rides through and emitTimelineScript writes a broken CustomEase.create(…, "M0,0 C…") string that fails silently at load. Cheap fix: if (Array.isArray(ease) && ease.length === 4 && ease.every(n => Number.isFinite(n))); else fall through to "none" or throw.
  • 🟠 Skill doc says "after the GSAP + CustomEase CDN tags" but no runtime guard. skills/figma/SKILL.md:35 puts the load-order requirement in prose. emitTimelineScript emits CustomEase.create(…) and gsap.timeline(…) as bare calls at the top of the <script>; if either lib is missing (composition author forgot the CDN, or CustomEase isn't in their GSAP bundle) the whole <script> throws early and window.__timelines[…] is never registered — Studio's static analysis is happy (literal key present) but the timeline is silently absent at runtime. Suggest wrapping in if (typeof gsap === "undefined") { console.warn(…); return; } (or an IIFE) so failures surface.

Nits

  • 🟡 parseFigmaRef.ts:6 replacereplaceAll is a real bug fix (multi-hyphen node ids like 1-2-3 were silently truncating to 1:2-3) but isn't called out in the PR body or a test. One-line assertion in parseFigmaRef.test.ts (normalizeNodeId("1-2-3") === "1:2:3") would lock it in.
  • 🟡 emitTween tl.set(selector, {…}, 0) fires per-tween unconditionally. For a MotionDoc with N tracks on the same selector, that's N tl.set calls at position 0. All safe (different properties, no clobber), but if a caller ever put two tracks on the same property (odd but not blocked by the type), the last tl.set wins. Docstring on MotionDoc.tracks could note "one track per property".
  • 🟡 MotionDoc.selector is per-doc, not per-track. Design choice — a doc represents one element with N property tracks. The /figma skill's motion-import step 2 makes this implicit; worth calling out as an explicit constraint ("one MotionDoc per animated element") in the skill.
  • 🟡 repeat > 0 emission asymmetry with GSAP. GSAP's repeat: 0 == play once (== omitting). Code correctly omits repeat when t.repeat === 0, but emits repeat: 1 when the doc had repeat: 1 which in GSAP means "play twice". Motion.dev's repeat: 1 typically means "play once more, i.e. twice total" so this is likely right — but a comment noting the semantics alignment (motion.dev repeat count === GSAP repeat count, both zero-indexed on "extra plays") would save a future reader.

Questions

  • ↩️ Since the doc footer says "superseded by the rewrite in #1873", is #1873's shape known now? If the /figma skill file at skills/figma/SKILL.md is going to be rewritten there anyway, is it worth spending review cycles on this version's routing rules, or is only the pure translator (motionToGsap + emitTimelineScript + motionEase) the load-bearing part of this PR?
  • ↩️ How is a MotionDoc produced from get_motion_context MCP output? The skill file describes the shape verbally but there's no motionContextToMotionDoc(...) helper in this PR — the agent is expected to hand-normalize. Any plan to add a parser, or is the contract deliberately loose so the agent can adapt to motion.dev version drift?
  • ↩️ The Headline fixture in motionToGsap.test.ts matches the PR body's "real payloads captured live" claim. Are those fixtures checked in anywhere for reproduction (e.g. packages/core/src/figma/__fixtures__/motion/)? Would help future maintainers verify against real payloads.

What I didn't verify

  • End-to-end: did not run hyperframes lint / validate against a generated composition or confirm the emitted script executes correctly under a real GSAP+CustomEase environment.
  • Whether Figma Motion's get_motion_context output actually presents times normalized to 0..1 and ease[] as values.length - 1 array; taking the type comments at their word.
  • Studio static-analysis behavior for the window.__timelines["figma-<slug>"] literal-key requirement — didn't cross-read Studio's regex/parser.
  • Did not scan #1873's diff to see whether/how it changes any of the above.

— Rames D Jusso

@vanceingalls vanceingalls force-pushed the vi/figma-01-core-foundations branch from afc868a to 8478c60 Compare July 3, 2026 07:52
@vanceingalls vanceingalls force-pushed the vi/figma-02-motion-translator branch from 0460ce5 to e4dd754 Compare July 3, 2026 07:52
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Review feedback addressed (pushed in the absorbed update):

Fixed

  • Bezier arrays are runtime-validated — length-4 + Number.isFinite on every element; malformed payloads fall back to linear instead of emitting a broken CustomEase.create path (Rames 🟠). Tests cover wrong-length and NaN.
  • Named-ease coverage extended to the full motion.dev set — circ/expo/bounce/elastic In/Out/InOut, anticipate, spring — so a designer's tuned ease survives instead of silently landing on linear (miga feat(studio): consolidate into single OSS-ready NLE editor #7, Rames 🟠). The fallback-to-linear behavior for anything else is now documented in the skill's motion section with a pointer to the mapping table.
  • Emitted scripts are guarded: the whole script is an IIFE that checks typeof gsap/typeof CustomEase and console.warns with the timeline id instead of throwing mid-script and silently never registering (Rames 🟠). Test locks the wrapper in.
  • repeat: Infinity semantics: the code (clamp to 0, single play) is the intended v1 behavior — composition-duration-aware loop counts are a later milestone. clampRepeat now carries a docstring saying exactly that, including the motion.dev/GSAP zero-indexed-extra-plays alignment (miga Initial repo setup #1, Rames 🟠 + repeat-asymmetry nit). PR body corrected below.
  • Multi-dash node-id fix now has a locked test (1-2-3 → 1:2:3).

Answers

🤖 Generated with Claude Code

@vanceingalls vanceingalls force-pushed the vi/figma-01-core-foundations branch from 8478c60 to 267815d Compare July 3, 2026 18:44
@vanceingalls vanceingalls force-pushed the vi/figma-02-motion-translator branch from e4dd754 to c36ece6 Compare July 3, 2026 18:44
@vanceingalls vanceingalls force-pushed the vi/figma-01-core-foundations branch from 267815d to 9e0eabc Compare July 3, 2026 19:13
@vanceingalls vanceingalls force-pushed the vi/figma-02-motion-translator branch from c36ece6 to c37ca68 Compare July 3, 2026 19:13

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

R2 verification — reviewed at c37ca68a87a2bd0e82aa009cb229c2bf1791a729 (was 0460ce507b on R1).
Miga's R1 posted ~3 minutes before mine — LGTM-with-nits, findings partially overlap; layered here.

R1 findings — verification at HEAD

🟠 F1 — repeat: Infinity → 0 PR-body/code divergence → ✅ RESOLVED (by documentation)

motionToGsap.ts:11-18clampRepeat still returns 0 for Infinity. That's not a fix by code-change; it's a fix by making the intent unmisreadable, which per my feedback_blocker_resolved_by_intent_documentation rubric is a legitimate resolution shape. The JSDoc above clampRepeat now spells it out:

repeat semantics match GSAP and motion.dev: count of EXTRA plays (0 = play once). Infinity clamps to 0 — a single play — because a deterministic render needs a finite timeline; composition-duration-aware loop counts are a later milestone (spec §6 motion notes).

PR body also updated to "repeat: Infinity clamped to 0 (single play) for determinism" — matches code. The contradiction R1 flagged (PR-body-said-X, code-does-Y) is gone; the semantics were always "single play" per the test docstring, and the body was the stale one. Downstream authors reading the module can no longer walk into the trap.

Sensible resolution — the alternative (composition-duration-aware loop count ceil(compDuration/cycle)-1) is genuinely non-trivial (needs compDuration threading through the translator) and belongs to a later milestone. The documented "single play" is a safe, deterministic default; the JSDoc explicitly names the deferred milestone so future readers know where the sharper answer will land.

Miga's #1 nit (same issue, different framing) → resolved by the same mechanism.

Cross-PR fix hoist — dash-parsing .replace.replaceAll⚠️ CONFIRMED STILL LIVES HERE (not hoisted to #1868)

parseFigmaRef.ts:6 at this HEAD is raw.replaceAll("-", ":") (commit a2cc14b328cf "fix(core): use replaceAll for figma node-id dash-to-colon conversion"). The multi-segment regression test at parseFigmaRef.test.ts (converts every dash in a multi-segment node id, KEY:1-2-31:2:3) also lives here.

Per my R1 note and Miga's #6 positive callout: this fix touches a file that belongs to #1868. If #1868 merges to main before #1869 lands, main will have the buggy single-dash .replace in parseFigmaRef.ts until #1869 follows.

I've raised the same on the #1868 R2 — resolution is either (a) hoist the one-line + one-test to #1868, or (b) rely on Graphite merge_when_ready to land the stack in order. Either works. Flagging here so the two PRs' state stays coherent; no action needed on this PR beyond confirming the mechanism you pick on #1868.


Miga's R1 layering — remaining nits at HEAD

Miga's R1 verdict was LGTM-with-nits. Verifying the code-actionable ones at current HEAD:

  • Miga #1 (PR body vs clamping) — ✅ resolved (see F1 above).
  • Miga #2 (deriveId strips only single #/.) — unchanged at HEAD (motionToGsap.ts:21). Genuine edge case (##double); non-blocking as Miga flagged.
  • Miga #3 (resolveStepEase dedup-by-identity not by value) — unchanged at HEAD; still hfCe0/hfCe1 per bezier occurrence. Non-blocking; deterministic-output goal is unaffected, script size cost only.
  • Miga #4 (buildSteps silently skips undefined segments) — unchanged; still continue on undefined at motionToGsap.ts:53. Non-blocking for well-formed MotionDoc inputs.
  • Miga #5 (export type *) — positive callout, no action.
  • Miga #6 (dash replaceAll fix) — same hoist question as my cross-PR observation.
  • Miga #7 (ease coverage gaps: spring, circIn/Out, bounce*) — parallels my R1's finding #2. Unchanged at HEAD; documented in-skill as "if a track uses shader/spring/effect props with no GSAP mapping, bake instead." Acceptable v1 posture given the skill's escape hatch, but a console.warn for silently-fell-through eases would help agents catch it. Not a blocker.
  • Miga #8 (skill doc quality) — positive.

My R1 concerns #2 (unknown-ease silent fallback), #3 (bezier length not validated), #4 (skill doc "after CDN tags" no runtime guard) — none code-changed at HEAD. Author's call whether v1 posture is acceptable to ship or whether they're worth a follow-up ticket. My read: shippable — the /figma skill's escape-hatch language covers the fallback-to-linear case, and the bezier validation is a defensive nice-to-have. If author wants to bank one, console.warn in a hardening pass is the highest-leverage cheap fix.


Cross-stack recheck

  • Dash-parsing hoist is the only cross-stack coupling. Downstream #1870-#1873 all import through the barrel; nothing else re-implements parsing.
  • The MotionDoc types and TimelineSpec shape stayed identical between R1 and R2 — no downstream API break.

AI trailer

Commits carry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> trailer + PR body ends with 🤖 Generated with Claude Code. HF convention strips these before merge — squash-strip covers it.

What I didn't verify

  • Didn't rerun the fixture test suite locally — trusted the green Test job.
  • Didn't audit emitTimelineScript's output against a real GSAP+CustomEase environment E2E (author's PR body claims a live get_motion_context payload → lint + validate + render pass; I took that at their word).
  • Didn't cross-read #1873 to confirm the "superseded by /figma skill rewrite" note reshapes the skill routing — noted on R1 as a scoping question.

Verdict: LGTM. F1 resolved cleanly by intent-documentation. The dash-parsing hoist is a cross-PR bookkeeping question, not a blocker on either PR — both authors and the stack land order can close it.

— Rames D Jusso

miguel-heygen
miguel-heygen previously approved these changes Jul 3, 2026

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Stamping after Miga R2 verified the prior Figma-stack blockers are addressed; live checks are green, with only Graphite stack mergeability pending where applicable.

@vanceingalls vanceingalls changed the base branch from vi/figma-01-core-foundations to graphite-base/1869 July 3, 2026 21:12
vanceingalls added a commit that referenced this pull request Jul 3, 2026
…manifest, asset snippet (#1868)

## What

Foundations of the `@hyperframes/core/figma` module — the pure, transport-agnostic layer every later phase builds on:

- **`types.ts`** — `FigmaRef`, `FigmaProvenance`, `FigmaManifestRecord`, and the Motion model (`MotionDoc`/`MotionTrack`/`TimelineSpec`/`GsapTween`) shared across the stack.
- **`parseFigmaRef`** — normalizes any user input (full `/design|/file|/proto` URLs with `?node-id=1-2`, `fileKey:nodeId` shorthand, bare `fileKey`) into `{ fileKey, nodeId }`, including the URL-dash → API-colon node-id conversion.
- **`freeze.ts`** — `freezeBytes`/`freezeUrl`/`freezeLocalFile` with a 256 MB cap; every Figma asset is frozen to a local file before it can reach a composition (determinism: no render-time network).
- **`manifest.ts`** — the `.media/manifest.jsonl` ledger (same layout `media-use` writes, so a project has one shared media inventory without either skill depending on the other): append/read/find-by-node/next-id, with a pure type-guard (`isFigmaManifestRecord`) instead of `as`-casts.
- **`assetSnippet.ts`** — manifest record → composition `<img>` snippet with escaped attrs + `data-figma-id`.
- **publishConfig fix** — `./figma` added to `packages/core` `publishConfig.exports` (the packed-manifest CI gate requires every source export to have a dist mapping).

## Why

Design spec: `docs/superpowers/specs/2026-06-30-figma-asset-integration-design.md`. These functions are deliberately transport-agnostic — when the project reversed from MCP-first to a REST/MCP split (spec §2), nothing in this layer changed. That was the point.

## Tests

Unit tests per module (URL variants, freeze cap edges, manifest round-trip/malformed-line tolerance, snippet escaping). All colocated `*.test.ts`, vitest, no network.

---
Stack (1/6): this PR → #1869#1870#1871#1872#1873

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@vanceingalls vanceingalls force-pushed the vi/figma-02-motion-translator branch from c37ca68 to 8f3ad15 Compare July 3, 2026 21:25
@vanceingalls vanceingalls changed the base branch from graphite-base/1869 to main July 3, 2026 21:25
@vanceingalls vanceingalls dismissed miguel-heygen’s stale review July 3, 2026 21:25

The base branch was changed.

Add the agent-facing /figma skill (asset + Figma Motion import via the
Figma MCP connector, built on @hyperframes/core/figma) and wire it into
the skill catalog across CLAUDE.md, README.md, docs/guides/skills.mdx,
and the hyperframes router's capability map. Bumps the skill count from
19 to 20 in CLAUDE.md and README.md.
vanceingalls and others added 2 commits July 3, 2026 15:31
oxfmt-align the README and router SKILL.md tables after the /figma +
/hyperframes-keyframes merge left uneven column padding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ExtractionPhaseBreakdown gained cachePublishFailures/cacheGcEvictions/
cacheGcBytesFreed/cacheAgedPartialsCleared; the studioRenderTelemetry
test fixture was never updated, breaking Typecheck on main and every PR
based on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vanceingalls vanceingalls force-pushed the vi/figma-02-motion-translator branch from 8f3ad15 to 8731fa2 Compare July 3, 2026 22:31
@mintlify

mintlify Bot commented Jul 3, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Jul 3, 2026, 10:33 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vanceingalls vanceingalls merged commit e92700a into main Jul 3, 2026
53 checks passed
@vanceingalls vanceingalls deleted the vi/figma-02-motion-translator branch July 3, 2026 22:45
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.

4 participants