Skip to content

feat(ui): close the documented Motion escape hatch (0427) - #681

Merged
crs48 merged 17 commits into
mainfrom
claude/0422-motion-dev-escape-hatch
Aug 2, 2026
Merged

feat(ui): close the documented Motion escape hatch (0427)#681
crs48 merged 17 commits into
mainfrom
claude/0422-motion-dev-escape-hatch

Conversation

@crs48

@crs48 crs48 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Implements exploration 0422.

The problem

docs/MOTION.md has told every author — human and AI — to "reach for motion/react" for drag-coupled and FLIP motion since exploration 0199. The dependency was installed nowhere and imported nowhere. The guide wrote a cheque the repo could not cash, and nothing in CI would have caught an agent responding by running pnpm add motion into whichever package it happened to be editing.

Two further problems found while confirming that:

  • The documented size was wrong by ~6×. MOTION.md cited a "~4.6KB shell", but FLIP lives in domMax (+25KB). Real cost of a layout animation is ~30KB.
  • "Lazy only" was enforced by nothing. None of the 16 check:* guards measures bundle size; manualChunks in vite.config.ts is a workbox 6MB ceiling, not a budget.

What this does

Guard first, dependency second — so there was never a window where motion was installed and unguarded.

  • scripts/check-motion-vocab.mjs gains a global rule scope that scans all of packages/ and apps/ (3588 files), separate from the design-token rules which stay in packages/ui + apps/web. The two call sites that legitimately need Motion live outside the old scope — a guard that could not see them would not be a guard. Bans the ~34KB motion/react barrel and framer-motion; deliberately allows motion/react-m and motion/react-mini, since those shells are the reason the LazyMotion split exists.
  • <MotionStage> (packages/ui/src/motion/MotionStage.tsx) is the one sanctioned entry point. LazyMotion + domMax load via dynamic import(). Children render unanimated rather than blank while the chunk resolves — the degraded state is exactly the instant snap that shipped before, and the alternative is empty space where a tab bar should be. reducedMotion defaults to 'user', because motion.css's global collapse cannot reach Motion's inline transforms.
  • MOTION_TRANSITIONS keeps the duration/easing tokens restated for Motion in exactly one place.
  • Kanban drop settle (BoardView) and tab reorder FLIP (TabBar). In both, Motion animates a wrapper while dnd-kit / native HTML5 drag keeps the inner element — the two libraries never write transform on the same node.

Verification

Check Result
Guard fails on a real violation ✅ proven red on a probe file, then green
motion in its own chunk react-BkFNx9Ox.js, 29.9 kB gzip
Reached only by import(...) ✅ no static import anywhere, not in modulepreload
No motion module at boot 0 of 250 requests, app driven on :5221
pnpm typecheck ✅ 101/101
pnpm test ✅ 11891 passed
pnpm lint ✅ 0 errors
pnpm build + check:packaging

⚠️ Not verified

The animations have not been watched in a browser. The workbench tab strip never rendered in apps/web: tabs are off by default since 0353 made nav tabless, and EditorArea also returns null on an empty group. ⌘K → "Turn on tabs", switching Calm ↔ Workbench, and opening pages from both the empty state and Recent all left [role="tab"] at 0. The kanban check needs seeded board data on top of that. AGENTS.md requires driving the real app for UI, so those four checks are left unchecked rather than waved through — see the callout in the exploration for how to finish them.

The doc is marked [-] partial: implementation 12/12, validation 6/13.

Corrections to the exploration, recorded in the doc

  • No changeset neededui, views and workbench are all private: true. The exploration assumed @xnetjs/ui was publishable.
  • check:api-report does not cover this export — it tracks ['react','core','data','sync'] only. Noted so its green tick isn't misread.
  • Site 2 reaches fewer users than assumed — tabs are opt-in, so tab reorder is the weaker half. If the chunk ever needs trimming, drop it first.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added smoother animated reordering for Kanban cards and tabs.
    • Animations load on demand to minimize initial loading impact.
    • Added a shared motion component with reduced-motion support.
    • Added a changelog entry describing animated card placement and tab reordering.
  • Documentation

    • Expanded guidance for supported drag and layout animations.
    • Updated motion architecture and exploration documentation.
  • Chores

    • Improved motion usage checks and formatting across documentation and metadata.

xNet Test added 7 commits August 1, 2026 14:01
…ape hatch

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
FLIP lives in domMax (+25KB), so the real cost of a layout animation is
~30KB — not the 4.6KB shell figure the guide quoted. Point authors and
agents at the single sanctioned entry point instead of a bare package
name, and record that only the full motion/react barrel is banned:
motion/react-m is the tree-shakeable shell the split exists to provide.

Signed-off-by: xNet Test <test@xnet.dev>
Splits the motion guard into two scopes. The design-token rules stay in
packages/ui + apps/web, where the token-bearing Tailwind config lives.
The new bundle-weight rules scan all of packages/ and apps/, because the
two call sites that legitimately need Motion live in packages/views and
packages/workbench — a guard that could not see them would not be a
guard.

Bans the full ~34KB motion/react barrel and the superseded framer-motion
name, while deliberately allowing motion/react-m (~4.6KB) and
motion/react-mini (2.3KB): those shells are the reason the LazyMotion
split exists. Dynamic import() expressions are unaffected, which is how
MotionStage reaches the feature bundle.

Signed-off-by: xNet Test <test@xnet.dev>
The only sanctioned entry point to Motion in xNet. LazyMotion + the
domMax feature bundle load through a dynamic import so the ~30KB cost of
a layout animation lands in its own chunk instead of on the default path
of every surface importing @xnetjs/ui.

Children render unanimated rather than blank while the chunk resolves —
the degraded state is exactly the instant snap that shipped before, and
the alternative is empty space where a tab bar should be. reducedMotion
defaults to 'user' because motion.css's global collapse cannot reach
Motion's inline transforms.

No changeset: ui, views and workbench are all private, so none of them
publish.

Signed-off-by: xNet Test <test@xnet.dev>
Tabs previously jumped to their new index instantly — the only
transition on a tab was transition-colors. Wraps the strip in
<MotionStage> and gives each tab layout="position".

"position" rather than plain layout: a reorder translates tabs without
resizing them, so there is no scale component and therefore no
distortion of the rounded corners. layout sits on a wrapper because
m.div replaces React's onDragStart with Motion's pan-gesture signature,
and the tab needs the native HTML5 drag handler intact.

Adds MOTION_TRANSITIONS to @xnetjs/ui so the duration/easing tokens are
restated for Motion in exactly one place.

Signed-off-by: xNet Test <test@xnet.dev>
A card moving between columns unmounted from one column and mounted in
another, arriving instantly. Wraps the board in <MotionStage> and gives
each card a layoutId so Motion matches the two mounts and animates
between them.

Motion animates a wrapper, not the card: dnd-kit already writes
transform on the card during a drag, and both libraries on one element
would fight over it. The wrapper's layout box only moves once the drop
has reordered the DOM, which is exactly the settle that was missing.

The DragOverlay copy deliberately gets no layoutId — it is a floating
clone, and a duplicate id would animate between two live elements.

Signed-off-by: xNet Test <test@xnet.dev>
Implementation 12/12; validation 6/13. The four interaction checks are
deliberately left unchecked: the workbench tab strip never rendered in
apps/web (tabless by default since 0353, plus EditorArea's empty-group
guard), and the kanban check needs seeded board data on top of that.
AGENTS.md requires driving the real app for UI, so they stay open rather
than being waved through.

Also records two findings that correct the exploration: no changeset is
needed (ui/views/workbench are all private), and site 2 reaches fewer
users than assumed because tabs are opt-in.

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-681 August 1, 2026 21:51 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds a lazy MotionStage boundary, animated tab and Kanban interactions, import-policy checks, and related documentation. It also includes formatting, generated-content updates, configuration reordering, and changelog maintenance.

Changes

Motion feature

Layer / File(s) Summary
MotionStage boundary and public API
packages/ui/src/motion/MotionStage.tsx, packages/ui/src/motion/MotionStage.test.tsx, packages/ui/src/index.ts, packages/*/package.json
Adds lazy motion/react loading with domMax, reduced-motion handling, shared transitions, public exports, dependencies, and tests.
Tab and Kanban animation wiring
packages/workbench/src/TabBar.tsx, packages/views/src/database-views/BoardView.tsx, site/src/data/changelog/2026-08-01-kanban-cards-settle-into-place-instead-o.json
Adds position-based tab animations and cross-column Kanban card animations while preserving drag handlers and overlays.
Motion policy and validation
docs/MOTION.md, docs/explorations/0427_[-]_MOTION_DEV_ESCAPE_HATCH.md, scripts/check-motion-vocab.mjs
Documents the MotionStage escape hatch and enforces scoped rules for Motion imports, framer-motion, and vocabulary tokens.

Repository maintenance

Layer / File(s) Summary
Formatting and generated content
.claude/launch.json, apps/web/src/coachmarks/tips.test.ts, docs/ECONOMICS.md, docs/explorations/0429_[x]_THE_RUST_TEST_ASTERISK_15_AND_THE_PRICE_OF_A_REFUSAL.md, docs/explorations/STALE.md, site/src/data/changelog/*
Reorders launch and import entries, reformats Markdown and JSON, adjusts whitespace, and regenerates stale exploration statistics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TabBar
  participant BoardView
  participant MotionStage
  participant MotionChunk
  User->>TabBar: Reorder tabs
  User->>BoardView: Drag card
  TabBar->>MotionStage: Render animated tab items
  BoardView->>MotionStage: Render animated cards
  MotionStage->>MotionChunk: Load motion/react features
  MotionChunk-->>MotionStage: Resolve domMax features
  MotionStage-->>TabBar: Apply position transitions
  MotionStage-->>BoardView: Apply layout transitions
Loading

Possibly related PRs

  • crs48/xNet#682: Directly related formatting cleanup in an exploration document.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a guarded Motion integration to close the documented escape hatch.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/0422-motion-dev-escape-hatch

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.

github-actions Bot added a commit that referenced this pull request Aug 1, 2026
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #681.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No visual differences detected in the changed UI.

CI run

github-actions Bot added a commit that referenced this pull request Aug 1, 2026
xNet Test added 3 commits August 1, 2026 15:24
0422 collided with the relationship-primitives exploration that landed on
main while this branch was open. That one is merged, checked off, and
referenced from CHARTER.md and VIBE.md, so this one moves.

Signed-off-by: xNet Test <test@xnet.dev>
…age)

Not part of the Motion work. main has been red on the 'API report drift
(0370)' step for its last three pushes — #679 (relationship primitives)
and #680 (hub address) added public API without regenerating the
reports, and every PR inherits the failure.

Purely additive: 58 insertions, 0 deletions. proposePromotion(s),
PromotionProposal, DEFAULT_PROMOTION_THRESHOLD and PortableHubAddress in
data; HubAddressConfig in react. No removals, so no breaking change — but
these are someone else's exports, and CODEOWNERS should still eyeball
them rather than treat this commit as sign-off.

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-681 August 1, 2026 22:35 — with GitHub Actions Inactive
@crs48 crs48 changed the title feat(ui): close the documented Motion escape hatch (0422) feat(ui): close the documented Motion escape hatch (0427) Aug 1, 2026
@crs48

crs48 commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Two things changed after the first CI run

1. Renumbered 0422 → 0427. 0422_[x]_RELATIONSHIP_PRIMITIVES_UNBUNDLING_THE_SOCIAL_GRAPH.md landed on main (#679) while this branch was open. Mine was committed 7 minutes earlier (13:31 vs 13:38), so by the earliest-commit-wins rule it had the claim — but theirs is merged, checked off, and referenced from CHARTER.md and VIBE.md, so moving mine is the non-disruptive call. Same resolution the surrender exploration took (0422 → 0426). Doc and all in-code references now say 0427.

2. Fixed a pre-existing main breakage, in its own commit (f42e1dd).

typecheck failed on the first run, but not because of this PR — main has been red on the API report drift (0370) step for its last three pushes (runs 30719852351, 30720243899, 30721127183). #679 and #680 added public API without regenerating the reports, so every PR inherits it.

The refresh is purely additive — 58 insertions, 0 deletions:

Package Added
data proposePromotion, proposePromotions, PromotionProposal, ProposePromotionOptions, DEFAULT_PROMOTION_THRESHOLD, PortableHubAddress, hubAddress on bundle options
react HubAddressConfig, hubAddress on XNetConfig

No removals, so no breaking change. These are not my exports — I kept them in a separate commit so CODEOWNERS can review them on their merits rather than treating this PR as sign-off.

Branch is now merged up to main and green locally: typecheck 101/101, check:api-report ✓, check:motion-vocab ✓, check:exploration-links ✓.

github-actions Bot added a commit that referenced this pull request Aug 1, 2026
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
@crs48
crs48 temporarily deployed to pr-681 August 1, 2026 22:45 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
@crs48
crs48 temporarily deployed to pr-681 August 1, 2026 22:55 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
xNet Test added 2 commits August 1, 2026 16:04
…v-escape-hatch

# Conflicts:
#	docs/explorations/STALE.md
The undecided count is generated; the merge conflict was 277 vs 279 with
neither reflecting the merged tree. Regenerated: 281 undecided, 41 stale
(baseline 41, unchanged).

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-681 August 1, 2026 23:08 — with GitHub Actions Inactive
crs48 pushed a commit that referenced this pull request Aug 1, 2026
…ollision with #681)

Signed-off-by: xNet Test <test@xnet.dev>
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
@crs48
crs48 temporarily deployed to pr-681 August 1, 2026 23:17 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
xNet Test added 2 commits August 1, 2026 18:28
…v-escape-hatch

# Conflicts:
#	docs/explorations/STALE.md
#	scripts/check-motion-vocab.mjs
…tch' into claude/0422-motion-dev-escape-hatch
@crs48
crs48 temporarily deployed to pr-681 August 2, 2026 01:30 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Aug 2, 2026
github-actions Bot added a commit that referenced this pull request Aug 2, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
scripts/check-motion-vocab.mjs (1)

137-145: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Detect multiline static imports.

scanText() splits the source into physical lines before it applies the static motion/react import rule. This valid import passes the guard:

import {
  motion
} from 'motion/react'

Parse or tokenize import declarations before matching, while preserving the declaration start line in diagnostics. Add multiline import and re-export cases to the self-test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-motion-vocab.mjs` around lines 137 - 145, Update scanText() to
parse or tokenize import declarations before applying the static motion/react
rule, so multiline imports and re-exports are detected while violations retain
the declaration’s starting line. Preserve existing rule scanning behavior, and
extend the script’s self-test with multiline import and re-export cases.
🧹 Nitpick comments (2)
packages/views/src/database-views/BoardView.tsx (1)

162-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a unit test for the card-wrapping logic.

No test accompanies this change. Cover, at minimum, that overlay cards render without the m.div/layoutId wrapper (line 166) while normal cards do, so a future refactor can't accidentally give the drag overlay a layoutId that collides with the live card.

As per path instructions, "Unit tests are required for core packages" for packages/**/src/**/*.{ts,tsx}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/views/src/database-views/BoardView.tsx` around lines 162 - 186, Add
unit tests for the card-wrapping logic around the component or helper containing
the overlay check: verify overlay cards render directly without the m.div
wrapper or layoutId, and normal cards are wrapped with m.div using row.id as
layoutId. Follow the package’s existing test conventions and preserve the
current rendering behavior.

Source: Path instructions

packages/workbench/src/TabBar.tsx (1)

244-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a unit test for the new tab-reorder wiring.

No test accompanies this change. Cover, at minimum, that TabItem renders the native role="tab" element with its drag handlers intact when wrapped by the new m.div, so a future refactor of the Motion wrapper can't silently drop onDragStart/onDrop handlers.

As per path instructions, "Unit tests are required for core packages" for packages/**/src/**/*.{ts,tsx}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/workbench/src/TabBar.tsx` around lines 244 - 295, Add a unit test
for the TabItem component covering the native role="tab" element inside the
m.div wrapper. Verify the element retains its draggable behavior and invokes the
existing onDragStart and onDrop wiring, using the component’s current tab setup
and handlers without changing production behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/explorations/STALE.md`:
- Around line 43-86: Update the generated STALE.md output from
scripts/check-exploration-fallow.mjs to record the as-of generation date
explicitly, using the intended checkout date rather than leaving the
Date.now()-based overdue values undocumented. Regenerate the table with that
date and preserve the existing exploration entries and overdue calculations.

In `@packages/ui/package.json`:
- Line 50: Keep the sole motion declaration in packages/ui/package.json at line
50, where MotionStage is owned; remove the duplicate declaration from
packages/views/package.json lines 31-43 and packages/workbench/package.json line
42, then either peer-declare motion for those consumers or make them depend on
`@xnetjs/ui` for the boundary used by their direct motion/react-m imports.

In `@packages/ui/src/motion/MotionStage.tsx`:
- Around line 85-106: Correct the lazy-loading behavior in MotionStage:
MotionFeatures currently renders unconditionally, causing its chunk to load on
mount rather than first interaction. Either update the surrounding fallback
documentation to accurately describe mount-time loading, or add an
interaction-driven render gate that preserves the intended
fallback-until-first-use behavior.
- Around line 49-72: Update MotionFeatures to handle rejected
import('motion/react') explicitly: report the failure through the project’s
logging or telemetry mechanism, then render the documented unanimated fallback
without representing the failed load as a successful module. Keep pending and
failed states distinguishable, and preserve the existing reducedMotion behavior
for successfully loaded motion features.

In `@scripts/check-motion-vocab.mjs`:
- Around line 293-295: Remove the unused parameter from the expect callback in
the “token rules do not fire on a global-only file” self-test, while preserving
its existing scanText assertion.
- Around line 52-58: Update the EXT set in the motion vocabulary checker to
include .js, .jsx, .mjs, and .cjs so collect() scans JavaScript module files in
the global scope. Add an integration fixture exercising runScan() with one such
file containing a static motion/react import, and assert that the scan discovers
and rejects it.

---

Outside diff comments:
In `@scripts/check-motion-vocab.mjs`:
- Around line 137-145: Update scanText() to parse or tokenize import
declarations before applying the static motion/react rule, so multiline imports
and re-exports are detected while violations retain the declaration’s starting
line. Preserve existing rule scanning behavior, and extend the script’s
self-test with multiline import and re-export cases.

---

Nitpick comments:
In `@packages/views/src/database-views/BoardView.tsx`:
- Around line 162-186: Add unit tests for the card-wrapping logic around the
component or helper containing the overlay check: verify overlay cards render
directly without the m.div wrapper or layoutId, and normal cards are wrapped
with m.div using row.id as layoutId. Follow the package’s existing test
conventions and preserve the current rendering behavior.

In `@packages/workbench/src/TabBar.tsx`:
- Around line 244-295: Add a unit test for the TabItem component covering the
native role="tab" element inside the m.div wrapper. Verify the element retains
its draggable behavior and invokes the existing onDragStart and onDrop wiring,
using the component’s current tab setup and handlers without changing production
behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b59179af-9937-431b-a3be-c94c6448a991

📥 Commits

Reviewing files that changed from the base of the PR and between a106b4d and c765f96.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • .claude/launch.json
  • apps/web/src/coachmarks/tips.test.ts
  • docs/ECONOMICS.md
  • docs/MOTION.md
  • docs/explorations/0427_[-]_MOTION_DEV_ESCAPE_HATCH.md
  • docs/explorations/0429_[x]_THE_RUST_TEST_ASTERISK_15_AND_THE_PRICE_OF_A_REFUSAL.md
  • docs/explorations/STALE.md
  • packages/ui/package.json
  • packages/ui/src/index.ts
  • packages/ui/src/motion/MotionStage.test.tsx
  • packages/ui/src/motion/MotionStage.tsx
  • packages/views/package.json
  • packages/views/src/database-views/BoardView.tsx
  • packages/workbench/package.json
  • packages/workbench/src/TabBar.tsx
  • scripts/check-motion-vocab.mjs
  • site/src/data/changelog/2026-08-01-choose-how-the-ai-assistant-works-with-y.json
  • site/src/data/changelog/2026-08-01-kanban-cards-settle-into-place-instead-o.json
  • site/src/data/changelog/2026-08-01-the-assistant-now-tells-you-when-its-sea.json
  • site/src/data/changelog/2026-08-01-the-charter-now-tests-whether-its-own-re.json

Comment on lines +43 to 86
| Exploration | Due | Overdue | Decider |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- | ------- |
| [0079*[*]\_AUTH_SCHEMA_DSL_VARIATIONS.md](0079_%5B_%5D_AUTH_SCHEMA_DSL_VARIATIONS.md) | 2026-05-09 _(default)_ | 84d | — |
| [0080*[*]\_UCAN_HYBRID_AUTHORIZATION_INTEGRATION.md](0080_%5B_%5D_UCAN_HYBRID_AUTHORIZATION_INTEGRATION.md) | 2026-05-10 _(default)_ | 83d | — |
| [0081*[*]\_NODE_PERMISSIONS_UCAN_EVALUATION.md](0081_%5B_%5D_NODE_PERMISSIONS_UCAN_EVALUATION.md) | 2026-05-10 _(default)_ | 83d | — |
| [0082*[*]\_GLOBAL_NAMESPACE_AUTHORIZATION.md](0082_%5B_%5D_GLOBAL_NAMESPACE_AUTHORIZATION.md) | 2026-05-10 _(default)_ | 83d | — |
| [0083*[*]\_UNIFIED_AUTHORIZATION_ARCHITECTURE.md](0083_%5B_%5D_UNIFIED_AUTHORIZATION_ARCHITECTURE.md) | 2026-05-10 _(default)_ | 83d | — |
| [0084*[*]\_GROUPS_AS_RELATIONS.md](0084_%5B_%5D_GROUPS_AS_RELATIONS.md) | 2026-05-10 _(default)_ | 83d | — |
| [0086*[*]\_NATIVE_REWRITE_ZIG_RUST.md](0086_%5B_%5D_NATIVE_REWRITE_ZIG_RUST.md) | 2026-05-12 _(default)_ | 81d | — |
| [0088*[*]\_DATABASE_UI_COMPETITIVE_ARCHITECTURE.md](0088_%5B_%5D_DATABASE_UI_COMPETITIVE_ARCHITECTURE.md) | 2026-05-13 _(default)_ | 80d | — |
| [0089*[*]\_REST_GRAPHQL_INTEROPERABILITY_BOUNDARY.md](0089_%5B_%5D_REST_GRAPHQL_INTEROPERABILITY_BOUNDARY.md) | 2026-05-18 _(default)_ | 75d | — |
| [0090*[*]\_ELECTRON_P2P_REMOTE_SHARE_OPTIONS.md](0090_%5B_%5D_ELECTRON_P2P_REMOTE_SHARE_OPTIONS.md) | 2026-05-21 _(default)_ | 72d | — |
| [0091*[*]\_GLOBAL_SCHEMA_FEDERATION_MODEL.md](0091_%5B_%5D_GLOBAL_SCHEMA_FEDERATION_MODEL.md) | 2026-05-21 _(default)_ | 72d | — |
| [0093*[*]\_NODE_NATIVE_GLOBAL_SCHEMA_FEDERATION_MODEL.md](0093_%5B_%5D_NODE_NATIVE_GLOBAL_SCHEMA_FEDERATION_MODEL.md) | 2026-05-21 _(default)_ | 72d | — |
| [0095*[*]\_PACKAGE_PORTFOLIO_CLEANUP_AND_API_SIMPLIFICATION.md](0095_%5B_%5D_PACKAGE_PORTFOLIO_CLEANUP_AND_API_SIMPLIFICATION.md) | 2026-05-30 _(default)_ | 63d | — |
| [0096*[*]\_PLAN03_ERP_REALITY_CHECK_AND_EXECUTION_RESET.md](0096_%5B_%5D_PLAN03_ERP_REALITY_CHECK_AND_EXECUTION_RESET.md) | 2026-05-30 _(default)_ | 63d | — |
| [0098*[*]\_OPENCLAW_INTEGRATION.md](0098_%5B_%5D_OPENCLAW_INTEGRATION.md) | 2026-06-01 _(default)_ | 62d | — |
| [0099*[*]\_DATABASE_EDITING_UX_AND_UNDO_REDO_REMEDIATION_PLAN.md](0099_%5B_%5D_DATABASE_EDITING_UX_AND_UNDO_REDO_REMEDIATION_PLAN.md) | 2026-06-01 _(default)_ | 61d | — |
| [0100*[*]\_NPM_PUBLISH_WORKFLOW_FOR_XNETJS.md](0100_%5B_%5D_NPM_PUBLISH_WORKFLOW_FOR_XNETJS.md) | 2026-06-02 _(default)_ | 60d | — |
| [0101*[*]\_END_TO_END_NPM_TRUSTED_PUBLISHING_PLAYBOOK.md](0101_%5B_%5D_END_TO_END_NPM_TRUSTED_PUBLISHING_PLAYBOOK.md) | 2026-06-03 _(default)_ | 59d | — |
| [0102*[*]\_AFFINE_BLOCKSUITE_INTEGRATION_FEASIBILITY.md](0102_%5B_%5D_AFFINE_BLOCKSUITE_INTEGRATION_FEASIBILITY.md) | 2026-06-03 _(default)_ | 59d | — |
| [0103\_[-]\_TASKS_EMBEDDED_IN_PAGES_BACKED_BY_NODES_MENTIONS_DUE_DATES_NESTED_SUBTASKS_DATABASES_CANVASES_AND_CROSS_SURFACE_TASK_MODEL.md](0103_%5B-%5D_TASKS_EMBEDDED_IN_PAGES_BACKED_BY_NODES_MENTIONS_DUE_DATES_NESTED_SUBTASKS_DATABASES_CANVASES_AND_CROSS_SURFACE_TASK_MODEL.md) | 2026-06-04 _(default)_ | 59d | — |
| [0104\_[-]\_EXPLORE_DRAMATICALLY_SIMPLIFYING_THE_UX_AROUND_A_CANVAS_FIRST_PRIMARY_APP_INSPIRED_BY_AFFINE_MINIMIZING_BUTTONS_AND_CHROME_WITH_ZOOM_IN_DOCUMENTS_AND_DATABASES.md](0104_%5B-%5D_EXPLORE_DRAMATICALLY_SIMPLIFYING_THE_UX_AROUND_A_CANVAS_FIRST_PRIMARY_APP_INSPIRED_BY_AFFINE_MINIMIZING_BUTTONS_AND_CHROME_WITH_ZOOM_IN_DOCUMENTS_AND_DATABASES.md) | 2026-06-04 _(default)_ | 58d | — |
| [0105*[*]\_WHAT_TO_WORK_ON_NEXT_AFTER_OPEN_SOURCE_LAUNCH.md](0105_%5B_%5D_WHAT_TO_WORK_ON_NEXT_AFTER_OPEN_SOURCE_LAUNCH.md) | 2026-06-05 _(default)_ | 57d | — |
| [0106*[*]\_CI_PERF_TESTING_OPTIONS.md](0106_%5B_%5D_CI_PERF_TESTING_OPTIONS.md) | 2026-06-05 _(default)_ | 57d | — |
| [0106*[*]\_JOIN_QUERIES_MULTI_TYPE_AGGREGATES_QUERY_PLANNING_API.md](0106_%5B_%5D_JOIN_QUERIES_MULTI_TYPE_AGGREGATES_QUERY_PLANNING_API.md) | 2026-06-05 _(default)_ | 57d | — |
| [0107*[*]\_STORYBOOK_PERFORMANCE_PANEL_AND_ELECTRON_IDE_WORKSHOP.md](0107_%5B_%5D_STORYBOOK_PERFORMANCE_PANEL_AND_ELECTRON_IDE_WORKSHOP.md) | 2026-06-06 _(default)_ | 56d | — |
| [0108*[*]\_CANVAS_V1_PAGES_DATABASES_AND_INFINITE_CANVAS_DEEP_DIVE.md](0108_%5B_%5D_CANVAS_V1_PAGES_DATABASES_AND_INFINITE_CANVAS_DEEP_DIVE.md) | 2026-06-07 _(default)_ | 55d | — |
| [0108*[*]\_EXPO_APP_PARITY_WITH_ELECTRON_AND_WEB.md](0108_%5B_%5D_EXPO_APP_PARITY_WITH_ELECTRON_AND_WEB.md) | 2026-06-07 _(default)_ | 55d | — |
| [0108*[*]\_TIMING_FOR_INTEGRATING_CHAT_AND_VIDEO_INTO_XNET_NOW_VS_LATER.md](0108_%5B_%5D_TIMING_FOR_INTEGRATING_CHAT_AND_VIDEO_INTO_XNET_NOW_VS_LATER.md) | 2026-06-07 _(default)_ | 55d | — |
| [0108*[*]\_USEQUERY_UPGRADE_TIMING_AND_INTEGRATION_SEQUENCING.md](0108_%5B_%5D_USEQUERY_UPGRADE_TIMING_AND_INTEGRATION_SEQUENCING.md) | 2026-06-07 _(default)_ | 55d | — |
| [0109*[*]\_REPOSITORY_AND_PROJECT_SUMMARY_FOR_NON_TECHNICAL_USERS.md](0109_%5B_%5D_REPOSITORY_AND_PROJECT_SUMMARY_FOR_NON_TECHNICAL_USERS.md) | 2026-06-08 _(default)_ | 54d | — |
| [0110*[*]\_XNET_AS_A_VIABLE_WIKIPEDIA_ALTERNATIVE.md](0110_%5B_%5D_XNET_AS_A_VIABLE_WIKIPEDIA_ALTERNATIVE.md) | 2026-07-04 _(default)_ | 28d | — |
| [0111*[*]\_UNIFIED_WORKBENCH_ARCHITECTURE_FOR_XNET.md](0111_%5B_%5D_UNIFIED_WORKBENCH_ARCHITECTURE_FOR_XNET.md) | 2026-07-04 _(default)_ | 28d | — |
| [0112*[*]\_UNIVERSAL_CLIPPER_AND_AI_KNOWLEDGE_GRAPH_INGESTION.md](0112_%5B_%5D_UNIVERSAL_CLIPPER_AND_AI_KNOWLEDGE_GRAPH_INGESTION.md) | 2026-07-04 _(default)_ | 28d | — |
| [0113*[*]\_OTHER_INTERNET_INFRASTRUCTURE_ROLES_FOR_XNET.md](0113_%5B_%5D_OTHER_INTERNET_INFRASTRUCTURE_ROLES_FOR_XNET.md) | 2026-07-04 _(default)_ | 28d | — |
| [0114*[*]\_DECENTRALIZED_ALTERNATIVES_FOR_NON_XNET_INTERNET_LAYERS.md](0114_%5B_%5D_DECENTRALIZED_ALTERNATIVES_FOR_NON_XNET_INTERNET_LAYERS.md) | 2026-07-04 _(default)_ | 28d | — |
| [0115*[*]\_ARCHITECTING_FULLY_DECENTRALIZED_GLOBAL_WEB_SEARCH.md](0115_%5B_%5D_ARCHITECTING_FULLY_DECENTRALIZED_GLOBAL_WEB_SEARCH.md) | 2026-07-06 _(default)_ | 26d | — |
| [0116*[*]\_ARCHITECTING_DECENTRALIZED_TWITTER_X_ON_XNET.md](0116_%5B_%5D_ARCHITECTING_DECENTRALIZED_TWITTER_X_ON_XNET.md) | 2026-07-06 _(default)_ | 26d | — |
| [0117*[*]\_ARCHITECTING_DECENTRALIZED_AI_ON_XNET.md](0117_%5B_%5D_ARCHITECTING_DECENTRALIZED_AI_ON_XNET.md) | 2026-07-06 _(default)_ | 26d | — |
| [0118*[*]\_ARCHITECTING_A_DECENTRALIZED_OSS_FORGE_ON_XNET.md](0118_%5B_%5D_ARCHITECTING_A_DECENTRALIZED_OSS_FORGE_ON_XNET.md) | 2026-07-06 _(default)_ | 26d | — |
| [0119*[*]\_XNET_AS_A_COMPELLING_WEB_AND_MOBILE_DEVELOPER_TOOL.md](0119_%5B_%5D_XNET_AS_A_COMPELLING_WEB_AND_MOBILE_DEVELOPER_TOOL.md) | 2026-07-06 _(default)_ | 26d | — |
| [0120*[*]\_XNET_PACKAGE_SECURITY_AND_RELIABILITY_EXPLORATION.md](0120_%5B_%5D_XNET_PACKAGE_SECURITY_AND_RELIABILITY_EXPLORATION.md) | 2026-07-06 _(default)_ | 26d | — |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
python3 - <<'PY'
from datetime import date

as_of = date(2026, 8, 2)
for due in ("2026-05-09", "2026-07-04"):
    print(due, (as_of - date.fromisoformat(due)).days)
PY

Repository: crs48/xNet

Length of output: 176


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== docs/explorations/STALE.md header and first 86 lines =="
sed -n '1,90p' docs/explorations/STALE.md

echo
echo "== references to snapshot/as-of/source generation =="
rg -n "STALE|overdue|snapshot|as of|as-of|August|generated|generation|Regenerate|update|graphify" docs/explorations README.md .github package.json -S || true

Repository: crs48/xNet

Length of output: 50367


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== docs/explorations/STALE.md first 120 lines =="
sed -n '1,120p' docs/explorations/STALE.md

echo
echo "== STALE.md metadata or date-like header candidates =="
python3 - <<'PY'
from pathlib import Path
p = Path("docs/explorations/STALE.md")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if i <= 140 and any(s in line.lower() for s in ["as of", "snapshot", "generated", "overwrite", "regenerate", "run", "script", "overdue"]) or i <= 20 and line.startswith(("#","|","generated","Generated")):
        print(f"{i}: {line}")
PY

Repository: crs48/xNet

Length of output: 22046


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== locate generator file =="
fd -a 'check-exploration-fallow\.mjs$' .

echo
echo "== first 240 lines of generator if present =="
for f in $(fd 'check-exploration-fallow\.mjs$' .); do
  echo "--- $f"
  sed -n '1,240p' "$f"
done

Repository: crs48/xNet

Length of output: 10167


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== generator overdue/now/date computation =="
python3 - <<'PY'
from pathlib import Path
src = Path("scripts/check-exploration-fallow.mjs").read_text()
for mark in ["nowMs", "DAY_MS", "overdue", "today", "Date.now", "Date.from"]:
    print("--", mark)
    print("\n".join(f"line {i+1}: {line}" for i,line in enumerate(src.splitlines()) if mark in line))
PY

echo
echo "== generator lines around stale row construction =="
rg -n -C 20 "overdue|STALE|markdown|rows|writeFileSync" scripts/check-exploration-fallow.mjs

echo
echo "== stale doc count with exact due dates from current table =="
python3 - <<'PY'
from pathlib import Path
import re
text=Path("docs/explorations/STALE.md").read_text()
rows=[]
state="none"
for line in text.splitlines():
    if not line or state!="stale":
        if line.strip()=="| Exploration | Due | Overdue | Decider |":
            state="stale"
        continue
    m=re.search(r"\|\s*\[([^\]]+)\]\([^)]+\)\s*\|\s*(\d{4}-\d{2}-\d{2})\s+\(.*\)\s*\|\s*(\d+)d\s*\|\s*—\s*\|", line)
    if m:
        rows.append((m.group(2), int(m.group(3))))
    if line.startswith("| Undated"):
        break
for due, present in rows[:8]+rows[-8:]:
    print(due, present, rows.count(present))
PY

Repository: crs48/xNet

Length of output: 7025


Record the as-of date used as-of-generation.

The overdue column is generated by scripts/check-exploration-fallow.mjs from Date.now(), but docs/explorations/STALE.md does not record that date. Add an explicit generation date, or regenerate the file after running the generator with the intended checkout date.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/explorations/STALE.md` around lines 43 - 86, Update the generated
STALE.md output from scripts/check-exploration-fallow.mjs to record the as-of
generation date explicitly, using the intended checkout date rather than leaving
the Date.now()-based overdue values undocumented. Regenerate the table with that
date and preserve the existing exploration entries and overdue calculations.

Comment thread packages/ui/package.json
"libphonenumber-js": "^1.12.0",
"linkifyjs": "^4.3.2",
"lucide-react": "^0.400.0",
"motion": "^12.43.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether `motion` resolves to a single version/instance across the workspace.
fd -HI 'pnpm-lock.yaml|package-lock.json|yarn.lock' -d 2 --exec grep -n "motion@" {} \;
rg -n '"motion"' packages/ui/package.json packages/views/package.json packages/workbench/package.json

Repository: crs48/xNet

Length of output: 521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pnpm workspaces =="
sed -n '1,140p' pnpm-workspace.yaml 2>/dev/null || true

echo
echo "== package.json package fields =="
for f in package.json packages/*/package.json; do
  [ -f "$f" ] || continue
  python3 - <<'PY' "$f"
import json,sys
p=sys.argv[1]
data=json.load(open(p))
print(p)
for section in ("private","packageManager","type","name","exports","dependencies","peerDependencies","devDependencies"):
    v=data.get(section)
    if v:
        print(f"  {section}: {v}")
PY
done

echo
echo "== lockfile motion entries =="
sed -n '9550,9575p' pnpm-lock.yaml
echo "---"
sed -n '11515,11540p' pnpm-lock.yaml
echo "---"
sed -n '28495,28520p' pnpm-lock.yaml

echo
echo "== motion imports/exports in source =="
rg -n "from ['\"]motion/react|from ['\"]motion/react-m|import \(['\"]motion/react|import \(['\"]motion/react-m|LazyMotion|framerMotion|domMax|MotionStage" packages -g '*.ts' -g '*.tsx'

Repository: crs48/xNet

Length of output: 39164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== `@xnetjs/views` package lockfile sections =="
python3 - <<'PY'
from pathlib import Path
text = Path('pnpm-lock.yaml').read_text()
lines = text.splitlines()
for i,l in enumerate(lines):
    if l.startswith('  packages/views@1.0.0('):
        start=i
        break
else:
    raise SystemExit('packages/views@ not found')
for j in range(start, min(start+35, len(lines))):
    print(f'{j+1}:{lines[j]}')

echo
echo "== root package dependencies =="
sed -n '9400,9620p' pnpm-lock.yaml

echo
echo "== workspace imports of `@xnetjs/ui` with MotionStage =="
rg -n "(`@xnetjs/ui`|MotionStage|m from 'motion/react-m'|m from \"motion/react-m\")" packages/workbench packages/views packages/ui --glob '*.tsx' --glob '*.ts'

Repository: crs48/xNet

Length of output: 389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant package lockfile sections =="
sed -n '25790,25815p' pnpm-lock.yaml
echo "---"
sed -n '28498,28535p' pnpm-lock.yaml
echo "---"
sed -n '9555,9570p' pnpm-lock.yaml
echo "---"
sed -n '11524,11536p' pnpm-lock.yaml

echo
echo "== imports of MotionStage / motion/react-m =="
rg -n "(`@xnetjs/ui`|MotionStage|from 'motion/react-m'|from \"motion/react-m\")" packages/workbench packages/views packages/ui --glob '*.tsx' --glob '*.ts'

Repository: crs48/xNet

Length of output: 11482


🌐 Web query:

motion package v12 npm react react-m LazyMotion domMax multiple instances different versions

💡 Result:

In version 12, the package formerly known as "framer-motion" is now simply "motion" [1]. When using the motion package with React, you should update your imports to reference motion/react [1][2]. Regarding your specific configuration questions: LazyMotion and domMax The LazyMotion component is used for code-splitting by deferring the loading of animation features [3][4]. The domMax bundle is a comprehensive feature set that includes all basic animations plus advanced features like layout animations, drag, and pan gestures [3][4][5]. To use them, you must pair them with the m component (rather than the standard motion component) [4]. While one LazyMotion provider at the root of your application is the standard best practice [4], you can provide features synchronously or asynchronously [3][4]. Multiple Instances and Versions Running multiple instances of different versions of the motion package is generally discouraged and can lead to complex dependency conflicts, mismatching types, and bundle bloat [6][7]. Key points for your setup: 1. Version Consistency: Ensure all parts of your application use the same version of the motion package to avoid symbol reference mismatches or incompatible type definitions [6][7]. 2. Import Path: Always use the motion/react import path for all React-related components (e.g., import { LazyMotion, domMax } from 'motion/react') [3][1]. 3. Architecture: Avoid using export * in your own shared modules if they are part of a client-side boundary, as this can cause build-time issues with frameworks like Next.js [7]. 4. Strict Mode: You can pass the strict prop to LazyMotion to ensure that only the m component is used within its scope, which helps prevent accidental usage of the full motion component [3][4][8]. If you are experiencing issues with LazyMotion after upgrading to v12, ensure that you are importing m components correctly and that your build environment (e.g., Next.js) is not misinterpreting the exports [6][7].

Citations:


Remove duplicate motion declarations from consumer packages. packages/ui owns MotionStage, while packages/views and packages/workbench import m directly from motion/react-m. Declare motion only in packages/ui and either peer-declare it for views/workbench or let them depend on @xnetjs/ui for this boundary; otherwise yarn/npm can install a separate copy outside the lockfile and break FLIP feature sharing.

📍 Affects 3 files
  • packages/ui/package.json#L50-L50 (this comment)
  • packages/views/package.json#L31-L43
  • packages/workbench/package.json#L42-L42
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/package.json` at line 50, Keep the sole motion declaration in
packages/ui/package.json at line 50, where MotionStage is owned; remove the
duplicate declaration from packages/views/package.json lines 31-43 and
packages/workbench/package.json line 42, then either peer-declare motion for
those consumers or make them depend on `@xnetjs/ui` for the boundary used by their
direct motion/react-m imports.

Comment on lines +49 to +72
const MotionFeatures = React.lazy(async () => {
// domMax is what carries `layout` / `layoutId` (FLIP). Anything less and the
// two call sites this exists for silently stop animating.
const { LazyMotion, MotionConfig, domMax } = await import('motion/react')
return {
default: ({
children,
reducedMotion
}: {
children: React.ReactNode
reducedMotion: 'user' | 'never'
}) => (
// reducedMotion is a MotionConfig concern, not a LazyMotion one.
// `strict` throws if a full `motion.*` component is rendered inside,
// which is the runtime half of the CI guard: it catches a bypass that
// reached the tree some other way.
<MotionConfig reducedMotion={reducedMotion}>
<LazyMotion features={domMax} strict>
{children}
</LazyMotion>
</MotionConfig>
)
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle a rejected import('motion/react') instead of letting it propagate uncaught.

The lazy factory does not catch a failed dynamic import (network failure, CDN error, etc.). If the chunk request fails, React.lazy rethrows the rejection during render, which crashes to the nearest error boundary — or the whole page if there is none — rather than falling back to the "unanimated" state this component was designed to guarantee (lines 88-95 describe rendering unanimated as the deliberate degraded path, but that only covers the pending state, not the failed state).

Catch the import failure explicitly and report it distinctly (e.g., log or telemetry) rather than silently reusing the same fallback path for both "still loading" and "failed to load" — collapsing those into one indistinguishable outcome would itself violate the project rule that failure must stay distinguishable from success.

🛠️ Proposed fix sketch
 const MotionFeatures = React.lazy(async () => {
-  const { LazyMotion, MotionConfig, domMax } = await import('motion/react')
-  return {
-    default: ({
-      children,
-      reducedMotion
-    }: {
-      children: React.ReactNode
-      reducedMotion: 'user' | 'never'
-    }) => (
-      <MotionConfig reducedMotion={reducedMotion}>
-        <LazyMotion features={domMax} strict>
-          {children}
-        </LazyMotion>
-      </MotionConfig>
-    )
-  }
+  try {
+    const { LazyMotion, MotionConfig, domMax } = await import('motion/react')
+    return {
+      default: ({
+        children,
+        reducedMotion
+      }: {
+        children: React.ReactNode
+        reducedMotion: 'user' | 'never'
+      }) => (
+        <MotionConfig reducedMotion={reducedMotion}>
+          <LazyMotion features={domMax} strict>
+            {children}
+          </LazyMotion>
+        </MotionConfig>
+      )
+    }
+  } catch (cause) {
+    reportMotionChunkLoadFailure(new Error('Failed to load Motion feature chunk', { cause }))
+    // Still render unanimated — but the failure was surfaced loudly first.
+    return { default: ({ children }: { children: React.ReactNode }) => <>{children}</> }
+  }
 })

As per coding guidelines, "Errors must remain distinguishable from success: do not use catches, defaults, or coercions that turn failure into a plausible successful value."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/motion/MotionStage.tsx` around lines 49 - 72, Update
MotionFeatures to handle rejected import('motion/react') explicitly: report the
failure through the project’s logging or telemetry mechanism, then render the
documented unanimated fallback without representing the failed load as a
successful module. Keep pending and failed states distinguishable, and preserve
the existing reducedMotion behavior for successfully loaded motion features.

Source: Coding guidelines

Comment on lines +85 to +106
/**
* Wraps a subtree that needs drag-coupled or FLIP motion.
*
* Until the feature chunk resolves, children render **unanimated rather than
* blank** — hence `fallback={children}` rather than a spinner. This is a
* deliberate exception to the repo rule against indistinguishable fallbacks
* (AGENTS.md): the degraded state here is precisely the behaviour that shipped
* before this component existed — an instant snap — and the alternative is
* flashing empty space where a tab bar should be. The chunk is requested on
* first interaction and stays warm for the session, so only the very first
* drag can land early enough to see it.
*/
export function MotionStage({
children,
reducedMotion = 'user'
}: MotionStageProps): React.ReactElement {
return (
<React.Suspense fallback={<>{children}</>}>
<MotionFeatures reducedMotion={reducedMotion}>{children}</MotionFeatures>
</React.Suspense>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate MotionStage.tsx and related files:"
fd -a 'MotionStage\.tsx|MotionFeatures|TabBar|BoardView' . | sed 's#^\./##' | head -200

echo
echo "Inspect MotionStage.tsx:"
MOON_FILE="$(fd 'MotionStage\.tsx' . | head -1 || true)"
if [ -n "$MOON_FILE" ]; then
  wc -l "$MOON_FILE"
  cat -n "$MOON_FILE"
fi

echo
echo "Search MotionFeatures definition/usages:"
rg -n "MotionFeatures|lazy\\(|Suspense|MotionStage" packages/ui packages -S --glob '*.{ts,tsx,js,jsx}' | head -200

Repository: crs48/xNet

Length of output: 11039


Correct the lazy import trigger.

MotionFeatures is rendered unconditionally inside <Suspense>, so React.lazy starts the feature chunk on MotionStage mount, not on first interaction. Update the fallback comment to describe the mount-time request, or gate <MotionFeatures> behind an interaction flag if the fallback-until-first-use behavior is intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/motion/MotionStage.tsx` around lines 85 - 106, Correct the
lazy-loading behavior in MotionStage: MotionFeatures currently renders
unconditionally, causing its chunk to load on mount rather than first
interaction. Either update the surrounding fallback documentation to accurately
describe mount-time loading, or add an interaction-driven render gate that
preserves the intended fallback-until-first-use behavior.

Comment on lines +52 to +58
/** Where the bundle-weight rules apply: every library and every surface. */
const WIDE_DIRS = [join(root, 'packages'), join(root, 'apps')]
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', 'coverage'])
const EXT = new Set(['.ts', '.tsx'])

/** The banned patterns. Each entry: a name, a regex, and the fix to suggest. */
/** Both rule scopes — the default for scanText and the self-test. */
const ALL_SCOPES = ['vocab', 'global']

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Scan JavaScript module files in the global scope.

WIDE_DIRS covers all packages and apps, but collect() still admits only .ts and .tsx files through EXT. A static motion/react import in .js, .jsx, .mjs, or .cjs passes this CI gate.

Add the JavaScript module extensions to EXT. Add an integration fixture that verifies runScan() discovers and rejects one of these files.

Proposed fix
-const EXT = new Set(['.ts', '.tsx'])
+const EXT = new Set([
+  '.ts', '.tsx', '.mts', '.cts',
+  '.js', '.jsx', '.mjs', '.cjs'
+])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-motion-vocab.mjs` around lines 52 - 58, Update the EXT set in
the motion vocabulary checker to include .js, .jsx, .mjs, and .cjs so collect()
scans JavaScript module files in the global scope. Add an integration fixture
exercising runScan() with one such file containing a static motion/react import,
and assert that the scan discovers and rejects it.

Comment on lines +293 to +295
label: 'token rules do not fire on a global-only file',
text: '<div className="transition-all" />',
expect: (v) => scanText('<div className="transition-all" />', ['global']).length === 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unused self-test callback parameter.

v is unused. ESLint requires unused parameters to start with _, and currently reports this as an error.

Proposed fix
-      expect: (v) => scanText('<div className="transition-all" />', ['global']).length === 0
+      expect: () => scanText('<div className="transition-all" />', ['global']).length === 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
label: 'token rules do not fire on a global-only file',
text: '<div className="transition-all" />',
expect: (v) => scanText('<div className="transition-all" />', ['global']).length === 0
label: 'token rules do not fire on a global-only file',
text: '<div className="transition-all" />',
expect: () => scanText('<div className="transition-all" />', ['global']).length === 0
🧰 Tools
🪛 ESLint

[error] 295-295: 'v' is defined but never used. Allowed unused args must match /^_/u.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-motion-vocab.mjs` around lines 293 - 295, Remove the unused
parameter from the expect callback in the “token rules do not fire on a
global-only file” self-test, while preserving its existing scanText assertion.

Source: Linters/SAST tools

@crs48
crs48 merged commit dbde317 into main Aug 2, 2026
23 checks passed
@crs48
crs48 deleted the claude/0422-motion-dev-escape-hatch branch August 2, 2026 01:42
github-actions Bot added a commit that referenced this pull request Aug 2, 2026
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.

1 participant