feat(ui): close the documented Motion escape hatch (0427) - #681
Conversation
…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>
📝 WalkthroughWalkthroughThis change adds a lazy ChangesMotion feature
Repository maintenance
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
|
Preview removed for PR #681. |
🖼️ UI changes in this PRNo visual differences detected in the changed UI. |
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>
Two things changed after the first CI run1. Renumbered 0422 → 0427. 2. Fixed a pre-existing main breakage, in its own commit (f42e1dd).
The refresh is purely additive — 58 insertions, 0 deletions:
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: |
…681) Signed-off-by: xNet Test <test@xnet.dev>
…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>
…ollision with #681) Signed-off-by: xNet Test <test@xnet.dev>
…v-escape-hatch # Conflicts: # docs/explorations/STALE.md # scripts/check-motion-vocab.mjs
…tch' into claude/0422-motion-dev-escape-hatch
There was a problem hiding this comment.
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 winDetect multiline static imports.
scanText()splits the source into physical lines before it applies thestatic motion/react importrule. 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 winAdd a unit test for the card-wrapping logic.
No test accompanies this change. Cover, at minimum, that
overlaycards render without them.div/layoutIdwrapper (line 166) while normal cards do, so a future refactor can't accidentally give the drag overlay alayoutIdthat 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 winAdd a unit test for the new tab-reorder wiring.
No test accompanies this change. Cover, at minimum, that
TabItemrenders the nativerole="tab"element with its drag handlers intact when wrapped by the newm.div, so a future refactor of the Motion wrapper can't silently droponDragStart/onDrophandlers.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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (20)
.claude/launch.jsonapps/web/src/coachmarks/tips.test.tsdocs/ECONOMICS.mddocs/MOTION.mddocs/explorations/0427_[-]_MOTION_DEV_ESCAPE_HATCH.mddocs/explorations/0429_[x]_THE_RUST_TEST_ASTERISK_15_AND_THE_PRICE_OF_A_REFUSAL.mddocs/explorations/STALE.mdpackages/ui/package.jsonpackages/ui/src/index.tspackages/ui/src/motion/MotionStage.test.tsxpackages/ui/src/motion/MotionStage.tsxpackages/views/package.jsonpackages/views/src/database-views/BoardView.tsxpackages/workbench/package.jsonpackages/workbench/src/TabBar.tsxscripts/check-motion-vocab.mjssite/src/data/changelog/2026-08-01-choose-how-the-ai-assistant-works-with-y.jsonsite/src/data/changelog/2026-08-01-kanban-cards-settle-into-place-instead-o.jsonsite/src/data/changelog/2026-08-01-the-assistant-now-tells-you-when-its-sea.jsonsite/src/data/changelog/2026-08-01-the-charter-now-tests-whether-its-own-re.json
| | 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 | — | | ||
|
|
There was a problem hiding this comment.
🎯 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)
PYRepository: 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 || trueRepository: 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}")
PYRepository: 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"
doneRepository: 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))
PYRepository: 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.
| "libphonenumber-js": "^1.12.0", | ||
| "linkifyjs": "^4.3.2", | ||
| "lucide-react": "^0.400.0", | ||
| "motion": "^12.43.0", |
There was a problem hiding this comment.
🎯 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.jsonRepository: 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:
- 1: https://www.npmjs.com/package/motion
- 2: https://motion.dev/docs/react-upgrade-guide
- 3: https://motion.dev/docs/react-reduce-bundle-size
- 4: https://www.mintlify.com/motiondivision/motion/api/lazy-motion
- 5: https://github.com/0xkynz/codekit/blob/main/templates/skills/framer-motion/references/bundle-dom-animation.md
- 6: [BUG] LazyMotion is not working in motion@^12.4.3 motiondivision/motion#3091
- 7: [BUG] Latest version incompatible with NextJS 15 build? motiondivision/motion#3211
- 8: https://heckerspace.com/article/boosting-framer-motion-performance-and-accessibility
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-L43packages/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.
| 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> | ||
| ) | ||
| } | ||
| }) |
There was a problem hiding this comment.
🩺 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
| /** | ||
| * 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
📐 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 -200Repository: 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.
| /** 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'] |
There was a problem hiding this comment.
🚀 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.
| 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 |
There was a problem hiding this comment.
📐 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.
| 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
Implements exploration 0422.
The problem
docs/MOTION.mdhas told every author — human and AI — to "reach formotion/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 runningpnpm add motioninto whichever package it happened to be editing.Two further problems found while confirming that:
domMax(+25KB). Real cost of a layout animation is ~30KB.check:*guards measures bundle size;manualChunksinvite.config.tsis a workbox 6MB ceiling, not a budget.What this does
Guard first, dependency second — so there was never a window where
motionwas installed and unguarded.scripts/check-motion-vocab.mjsgains aglobalrule scope that scans all ofpackages/andapps/(3588 files), separate from the design-token rules which stay inpackages/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 ~34KBmotion/reactbarrel andframer-motion; deliberately allowsmotion/react-mandmotion/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+domMaxload via dynamicimport(). 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.reducedMotiondefaults to'user', because motion.css's global collapse cannot reach Motion's inline transforms.MOTION_TRANSITIONSkeeps the duration/easing tokens restated for Motion in exactly one place.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 writetransformon the same node.Verification
motionin its own chunkreact-BkFNx9Ox.js, 29.9 kB gzipimport(...)modulepreloadpnpm typecheckpnpm testpnpm lintpnpm build+check:packagingThe 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, andEditorAreaalso returnsnullon 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.mdrequires 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
ui,viewsandworkbenchare allprivate: true. The exploration assumed@xnetjs/uiwas publishable.check:api-reportdoes not cover this export — it tracks['react','core','data','sync']only. Noted so its green tick isn't misread.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores