feat(ui): elegant, composable motion system — vocabulary, <Presence>, CI gate (0199) - #171
Merged
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…retire ease-bounce The canonical motion system gains a documented vocabulary header (the two laws + the allowed durations/easings/primitives), a <Presence> enter/exit keyframe layer (.motion-presence[data-state][data-motion]), a .stagger list-entrance utility, and a pop-in primitive. --ease-bounce is retired (no consumers; its negative anticipation is the opposite of minimal) and dropped from the Tailwind config. Refs docs/explorations/0198. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
<Presence> extends the Base UI data-ending-style enter/exit trick to any React child (keep mounted through exit, unmount on animationend) — no JS animation library. useViewTransition() wraps the native View Transitions API with reduced-motion + feature-detect fallbacks for discrete surface/ list swaps. Both exported from @xnetjs/ui; 11 unit tests green. Refs docs/explorations/0198. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t transition-all Adds docs/MOTION.md (the one-page vocabulary that doubles as the AI prompt) and scripts/check-motion-vocab.mjs, wired into the CI lint job. The gate bans transition-all, raw duration-<ms> literals, ease-bounce, and arbitrary animate-[…] across packages/ui/src + apps/web/src (the token-bearing scope). Codemods the 18 transition-all + 3 raw-duration sites in scope to explicit, compositor-only forms (transition-[opacity,transform], transition-[width], duration tokens) so the gate is green. 370 files scanned, 0 violations. Refs docs/explorations/0198. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…umb; de-drift editor - UndoToast: enter+exit via <Presence motion=slide-up> (previously popped in with no exit); message latched so it survives the exit; centered with auto-margins so the translateY keyframe doesn't fight a static transform. - StorageWarningBanner: <Presence motion=slide-down> enter/exit. - TabBreadcrumb: space re-scope cross-fades via useViewTransition(). - editor: menu-disappear now uses ease-in (the exit law) instead of ease-out, with a comment aligning the local set to docs/MOTION.md. Refs docs/explorations/0198. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… on main) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on-normal) The Linear-grade tasks UI merged from main predated the motion gate; two raw duration-150 literals are exact no-op renames to the duration-normal token. Keeps check:motion-vocab green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
🖼️ UI changes in this PRScreensInteractionsAuto-captured by CI · run. Informational — not a blocking check. |
Contributor
|
Preview removed for PR #171. |
| ] | ||
|
|
||
| /** Recursively collect .ts/.tsx files under a directory. */ | ||
| function collect(dir, out) { |
crs48
added a commit
that referenced
this pull request
Jun 18, 2026
## What Backfills the changelog so it reflects what's live on `main`. The per-PR fragment system (exploration 0197) was current through PR #163 (PR #178 backfilled #151/#153/#156/#157/#160/#163), but the recent **0198/0199 batch** and a couple of stragglers shipped without entries. This adds 7 fragments, with prose lifted from each PR's own `## Changelog` block where present. | PR | Entry | Tags | |---|---|---| | #143 | AI chat that knows your workspace | `ai` | | #167 | Linear-grade Tasks | `tasks` `app` | | #169 | Cleaner page editor typography | `editor` | | #170 | Bring your Slack data into xNet | `plugins` `platform` | | #171 | Smoother, more consistent motion | `app` | | #173 | Airtable-grade database grids | `app` | | #174 | A redesigned chat & channels experience | `app` | ## Deliberately excluded - **In-editor AI (#154 / #158)** — shipped the transform engine + diff/approval logic, but both PRs state the `/ai` UI wiring into the running app is **deferred**, so it isn't user-visible yet. An entry would overclaim. - **Slack (#170)** — entry scoped to the migration connector + compatibility layer (genuinely live); avoids claiming the webhook/slash-command hub endpoints work, since they're generic-over-an-injected-sink and not mounted in a running server yet. - Pure infra/ops/docs PRs (cloud M1/M2, CI, runbooks) — not user-facing. ## Verification - `validate-changelog.ts` → **26 fragments valid** (19 existing + 7 new) - `prettier --check` on all new files → clean - Newest-first ordering confirmed — the 7 entries slot in at the top ## Changelog <!-- This PR only adds changelog fragments for already-merged work; it ships no user-facing change of its own. --> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.







Summary
Implements exploration 0199 — Elegant, Composable Motion System. The repo already had a strong CSS-first motion foundation; this hardens it into a single, enforced, AI-legible motion vocabulary and extends the elegant Base UI enter/exit pattern to plain React mount/unmount — 0 KB added on the default path, no animation library.
The same restraint that makes the UI feel designed makes motion easy for an agent to author consistently. So the deliverable is a small frozen vocabulary + a checker that turns "wrong animation" into a build error the author self-corrects.
What's in it
packages/ui/src/theme/motion.css): documented header (the two laws — enter slower+ease-out, exit faster+ease-in; the allowed durations/easings/primitives). Retired--ease-bounce(no consumers; dropped from the Tailwind config too) and added apop-inprimitive.<Presence>(packages/ui/src/motion/Presence.tsx): extends the Base UIdata-ending-styletrick to any React child — keeps the node mounted through its exit keyframe, unmounts onanimationend. No JS animation library.useViewTransition(): wraps the native View Transitions API with reduced-motion + feature-detect fallbacks for discrete surface/list swaps..staggerutility for list entrances (single--idelay var).docs/MOTION.md: the one-page style guide that doubles as the AI system prompt.scripts/check-motion-vocab.mjswired into the CIlintjob: banstransition-all, rawduration-<ms>literals,ease-bounce, and arbitraryanimate-[…]acrosspackages/ui/src+apps/web/src(the token-bearing scope). Follows the repo's existingcheck-*.mjspattern rather than a custom ESLint plugin.transition-all+ 5 raw-duration sites in scope → explicit, compositor-only forms (transition-[opacity,transform],transition-[width],transition-transform, duration tokens). Gate is green across 379 files.UndoToast(slide-up, with message-latching so text survives the exit) andStorageWarningBanner(slide-down) now animate in and out;TabBreadcrumbspace re-scope cross-fades viauseViewTransition(); the editor'smenu-disappearcorrected toease-in(the exit law).Deviations (documented in the exploration)
check-*.mjsCI pattern; a text scanner is also more reliable for Tailwind class strings. Same outcome.packages/ui+apps/web— only these build with the token-bearing config. The editor's--editor-*theme is intentionally out of scope (whereduration-200is a correct Tailwind default).EditorArea/TabBarroute-level transitions deferred — both are router-authoritative and exercised by the flake-sensitiveeditor-uxe2e; the reusable helper ships and is proven on the contained breadcrumb re-scope.Testing
<Presence>+useViewTransition(green).@xnetjs/uiandxnet-webtypecheck clean against built deps.pnpm check:motion-vocab: 0 violations / 379 files; verified it exits non-zero on each planted footgun.<Presence>-wrappedStorageWarningBannerlive, no console errors,ease-bouncegone without breakage.vitest --changed=origin/main: 724/726; the 2 failures are pre-existing perf-budget flakes (markdown-import timing, 10k-row grid cursor) that pass in isolation and get higher budgets on CI — unrelated to motion.Changelog
Motion: a consistent, minimal animation vocabulary — toasts and banners now glide in and out, list re-scopes cross-fade, and a CI gate keeps motion clean and on-style across the app.
🤖 Generated with Claude Code