Skip to content

feat(mascot): componentized mascot — core service, solid/react wrappers, 16 effects, consumer migrations - #490

Merged
omridevk merged 85 commits into
mainfrom
feat/mascot-component
Aug 16, 2026
Merged

feat(mascot): componentized mascot — core service, solid/react wrappers, 16 effects, consumer migrations#490
omridevk merged 85 commits into
mainfrom
feat/mascot-component

Conversation

@omridevk

@omridevk omridevk commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Mascot componentization — full feature (phases 1 + 2)

Replaces the prototype FAB robot rig with a published, framework-free mascot component and mounts it in every consumer.

What ships

  • Core (@conciv/mascot): createMascot service — pose/follow/activity controllers over shared part elements, config diffing, connect() prop-getter surface, keyed mountEffect/unmountEffect, visibility parking, ResizeObserver-driven remeasure, skinnable art values (robotSkin default).
  • Wrappers: @conciv/mascot/solid + @conciv/mascot/react — compound <Mascot> / Mascot.Head|Eyes|Antenna|Binary|Effect; single-slot claim with synchronous throw on double mount; default parts stand in for unclaimed slots; default 44px stage via adopted :where() stylesheet; geometry-critical style blocklist on layers; React StrictMode-safe.
  • 16 effect subpaths (@conciv/mascot/effects/<name>): binary, matrix, steam, notes, pixel-bubbles (nozzle physics — particles snapshot the tip and fly world-fixed) + 11 attached ornaments. Module-graph tests prove per-effect chunk isolation.
  • Performance: zero per-frame DOM reads/writes on hot paths — gsap-cache-composed tip anchor, quickSetter transforms, pointer math on one-shot ticker, pooled canvas particles, baked gradients, off-screen parking.
  • Consumers migrated: widget FAB (apps/conciv, −17.6 kB gz JS / −19 kB gz CSS via de-duplicated layer art) and site landing FAB (apps/site, SSR-sized first paint). Legacy createFabRobotRig adapter deleted; "." now exports the core.
  • Gallery: storybook Playground (state/working/follow/16-effect/curve controls) + docs index over published subpaths only.

Verification

  • 113 vitest + 99 playwright in packages/mascot (browser + e2e, stepped-clock harness, packed-install fixtures for core-only/solid-only/react-only), 123 embed ITs incl. FAB state-table pins, site e2e 17, storybook suite, publint + attw, fallow 0 introduced across all categories; full forced landing gate green (107 tasks).
  • Replaces feat(mascot): eyes follow the cursor while the FAB robot is closed #486 (closed with pointer).

Known follow-ups (ledgered, non-blocking)

  • 17 package-local story files from the effect-wave era still import source paths (gallery supersedes them functionally; migrate-or-delete follow-up).
  • exports["./solid"].solid → dist/solid/index.jsx (JSX-preserved condition) has no downstream compile test; covered indirectly.
  • Storybook production build needs --max-old-space-size=8192 (pre-existing ceiling, now scripted).

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

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 004cbbb5-eba0-4441-aa01-30f274469543

📥 Commits

Reviewing files that changed from the base of the PR and between de2fdaf and 80723a7.

📒 Files selected for processing (28)
  • packages/mascot/src/binary.stories.tsx
  • packages/mascot/src/core/config.ts
  • packages/mascot/src/core/effects/binary.ts
  • packages/mascot/src/core/effects/effect-support.ts
  • packages/mascot/src/core/effects/matrix.ts
  • packages/mascot/src/core/effects/notes.ts
  • packages/mascot/src/core/effects/pixel-bubbles.ts
  • packages/mascot/src/core/effects/steam.ts
  • packages/mascot/src/core/index.ts
  • packages/mascot/src/core/mascot.ts
  • packages/mascot/src/core/parts/activity.ts
  • packages/mascot/src/core/parts/follow.ts
  • packages/mascot/src/mascot.stories.tsx
  • packages/mascot/src/rig.ts
  • packages/mascot/tests/e2e/activity-channels.it.test.ts
  • packages/mascot/tests/e2e/activity.it.test.ts
  • packages/mascot/tests/e2e/anchor-perf.it.test.ts
  • packages/mascot/tests/e2e/binary-nozzle.it.test.ts
  • packages/mascot/tests/e2e/curve.it.test.ts
  • packages/mascot/tests/e2e/effects.it.test.ts
  • packages/mascot/tests/e2e/emitter-scale.it.test.ts
  • packages/mascot/tests/e2e/gaze.it.test.ts
  • packages/mascot/tests/e2e/helpers/harness-page.ts
  • packages/mascot/tests/e2e/helpers/launches.ts
  • packages/mascot/tests/e2e/helpers/mascot-stage.ts
  • packages/mascot/tests/e2e/lifecycle.it.test.ts
  • packages/mascot/tests/e2e/stream-nozzle.it.test.ts
  • packages/mascot/tests/globals.d.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/mascot/tests/e2e/emitter-scale.it.test.ts
  • packages/mascot/src/core/config.ts
  • packages/mascot/src/core/index.ts
  • packages/mascot/tests/e2e/anchor-perf.it.test.ts
  • packages/mascot/src/rig.ts
  • packages/mascot/tests/e2e/lifecycle.it.test.ts
  • packages/mascot/tests/e2e/curve.it.test.ts
  • packages/mascot/src/core/effects/notes.ts
  • packages/mascot/src/core/mascot.ts
  • packages/mascot/src/core/effects/effect-support.ts
  • packages/mascot/tests/e2e/helpers/harness-page.ts
  • packages/mascot/tests/e2e/effects.it.test.ts
  • packages/mascot/src/core/parts/activity.ts

📝 Walkthrough

Walkthrough

The pull request adds a framework-free mascot service, composable effects, scale-relative binary emitters, browser validation, Storybook coverage, and shared live-session state for launcher busy indicators.

Changes

Mascot core and validation

Layer / File(s) Summary
Mascot controllers and service
packages/mascot/src/core/*
Adds pose, follow, activity, skin, anchoring, transition, registration, visibility, reduced-motion, and destruction APIs.
Effects and legacy integration
packages/mascot/src/core/effects/*, packages/mascot/src/rig.ts
Adds reusable effect lifecycle infrastructure and visual effects. The legacy rig delegates to createMascot.
Packaging, stories, and tests
packages/mascot/*, packages/mascot/tests/*, packages/embed/tests/e2e/fab-working.it.test.ts
Adds effect subpath exports, Playwright and Vitest wiring, Storybook stories, package-surface checks, geometry tests, and end-to-end lifecycle coverage.

Live-session FAB state

Layer / File(s) Summary
Live-session tracking
apps/conciv/src/app/live-sessions.ts, apps/conciv/src/app/context.ts, apps/conciv/src/routes/__root.tsx
Tracks registered pane accessors and exposes aggregate running state through application context.
Session activity integration
apps/conciv/src/pane/session-activity.ts, apps/conciv/src/pane/chat-pane.tsx
Coordinates activity start, settlement, cleanup, invalidation, announcements, marker refetching, and capture refreshes.
Launcher state and coverage
apps/conciv/src/shell/fab.tsx, apps/conciv/test/*
Includes live sessions in launcher working state, exposes aria-busy, and tests concurrent registration and cleanup behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Merge Risk: 🟡 Moderate · up to 80723

The PR adds pointer-follow and emitter behavior while preserving an adapter for existing consumers, but merge readiness is held back by a lint error that may fail CI, a teardown path that can leave a stale gaze offset, documentation that overstates site-FAB support, and a browser test cleanup issue.

Sequence Diagram(s)

sequenceDiagram
  participant MascotConsumer
  participant MascotService
  participant ActivityController
  participant EffectMount
  MascotConsumer->>MascotService: Register DOM parts and update configuration
  MascotService->>ActivityController: Start or stop activity
  ActivityController->>EffectMount: Mount, anchor, drain, or remove effects
  MascotConsumer->>MascotService: Destroy service
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the mascot componentization, core service, effects, wrappers, and consumer integration changes.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/mascot-component
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mascot-component

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

omridevk and others added 27 commits August 14, 2026 22:34
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s and legacy adapter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nect refs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds packages/mascot/harness (verify.mjs + page.html): a real-Chromium
Playwright harness that serves the built dist and asserts 78 behavior
facts across 15 checks — the legacy closed/open/work trio through
createFabRobotRig plus the createMascot lifecycle, connect() ref
stability, reduced motion, gaze falloff, channel discipline and
re-registration guarantees. Run with `pnpm --filter @conciv/mascot
verify:behavior`; it is a manual/agent gate, not wired into turbo test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An unknown --only value selected zero checks and reported ALL CHECKS
PASS with exit 0 — and the donor-parity claim rides on that flag. The
harness now derives its valid section list from the check table and
refuses an empty selection with a non-zero exit before it launches
Chromium.

Also widens the exit-drain sample window 350ms to 400ms (assertion
unchanged; clears the gsap ticker-lag flake margin), pins an explicit
viewport in openPage so the gaze pointer offsets never depend on the
Playwright default, and rescopes the changeset's parity wording to the
channels actually measured — the old work timeline's head bob is
intentionally dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anchor

Pose no longer kills the channels activity owns while working. killPosedTweens
narrows to head (all four), eyes scaleX and antenna rotation; eyes scaleY and
antenna scaleX/scaleY are handoff channels, owned by pose while idle and by
activity while working. Activity gains setEyeRest, which retargets the blink
return tween in place (vars + invalidate) instead of rebuilding the timeline,
so a mid-work state change keeps the original work timeline running and no
longer re-runs emitter.start (which fired a spurious returnToFull tween).

registerParts now requires the root. readyParts stopped falling back to
effectHost as the stage, so a bound effectHost could register with no root and
nulling the root never tore down. Root is the stage and coordinate origin;
effectHost is an optional separate mount target passed to the activity
controller as the emitter's parent.

The emitter re-anchors on a mid-work state change: trackTip runs a short gsap
tween that re-measures the antenna tip each frame while the pose settles.

Tip and lean-pivot measurement moved to tip-anchor.ts and became transform
aware: the antenna's untransformed layout box is walked up to the host and the
tip point is mapped through the element's own matrix, instead of reading
getBoundingClientRect (whose box inflates under rotation and would have thrown
the anchor ~15px off the tip on a rest to awake change). wrapForLean computes
the wrapper's transform-origin in pixels from the antenna's own box, so the
pivot lands on the antenna base regardless of the host's layer inset — the
site insets its layers 6px inside a 56px button, where the '50% 32.8%' string
resolved 2.06px above the antenna base. Widget behavior is unchanged (inset 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ontract

gsapAsset rejects a request whose normalized path escapes the gsap package
directory. The harness page's emitter/lean-wrapper predicates are structural
(digit count and child count) instead of keyed off inline style strings.

Check A now pins that a mid-work state change keeps the ORIGINAL work timeline
object running, keeps the same emitter node, fires no returnToFull (emitter
scale stays 1) and re-anchors the emitter shell to the leaned tip. New check M
binds all four required refs plus an effectHost and asserts that nulling any
one required ref tears down (wrappers 0, listeners 0, antenna restored), and
that an effectHost on its own never registers, never arms and never emits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The changeset claimed both consumers keep their existing states, verified
against the previous implementation. That was false: the rig that shipped on
main had no gaze and no emitter, and this merge turns pointer-follow on for
the closed state and the binary emitter on for the work state on both the
widget FAB and the site FAB. Parity was measured against PR #486's prototype
on the donor branch, not against main; the changeset now says so.

Plan amendments: decision 1 records eyes scaleY and antenna scale as handoff
channels rather than disjoint ones, with the kill-narrowing as the
enforcement; decision 4 records the identical-parts short-circuit as the
implemented, StrictMode-preferred behavior; Task 5 step 2 drops `contain` from
the listed layer styling, which would clip the emitter. The phase-2 plan gains
an M1/M2 acceptance inventory recording both consumers' real layer boxes and
the bare-<Mascot> default-sizing requirement for W2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
antennaTipAnchor mis-measured whenever an effectHost was bound. EFFECT_HOST_STYLE
is position:absolute inset:0 — a sibling overlay of the layers, never on the
antenna's offsetParent chain — so layoutOffsetWithin walked past it to null and
returned document-relative offsets, positioning the emitter by the stage's whole
page offset. It measures both the antenna and the host to the shared offset root
and subtracts, which is correct for an ancestor host (the base offsets cancel as
a prefix) and for a sibling host. Neither consumer binds an effect host today,
which is why phase 1 never surfaced it.

The tip measurement is normalized to rotation only. The antenna's throb drives
scaleX/scaleY continuously, so a tracker that ended on an arbitrary throb phase
settled the anchor with sub-pixel nondeterminism; rotation is the only antenna
channel that relocates the tip.

Harness: check M binds an effectHost together with all four required refs under
working: true and pins the emitter's parent and its offset inside that host;
check M2 pins the resting anchor against stage width x 0.5 and stage height x
0.15625, so the geometry module's absolute correctness no longer rests on a
one-off manual measurement. Both pins also pass against the donor dist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
startWorking measured the antenna tip once and never re-measured. On the
open -> work rising edge applyPose only STARTS the rest timeline, so
startEmitter reads the antenna while it still carries the awake -4deg
rotation; the pose then animates the rotation back to 0 and the emitter stays
at the leaned tip. That is the site's real path (hover -> awake, click ->
work), where it left the digit column ~1.4px off the tip on a 44px rig.
startWorking now calls trackTip over the same 0.45s window as the mid-work
path; it early-returns without an emitter, so reduced motion is unaffected.

Wiring that up exposed a second source of nondeterminism in the anchor:
gsap's CSSPlugin applies autoRound to px-valued CSS properties, so every
gsap.set on the emitter shell quantized the anchor to whole pixels — up to
0.5px of jitter as the pose settles, and inconsistent with the raw float the
shell is created with. Disabled with autoRound: false.

Harness: new core check N drives the adapter open -> work and pins that the
anchor enters at the leaned tip and settles exactly on the rest tip, against
the absolute stage-relative values the tip pins established. New anchorOf
helper reads the sub-pixel style value, because offsetLeft is integer-rounded
by the DOM and cannot resolve a 1.4px offset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The launcher's working state came only from sessions.list, and the one refresh
fired at send time raced the server's live-run registration: core awaits the run
row, content expansion and live-run settling before liveRuns.start, so the
refetch answered running:false and nothing asked again until the turn ended.

Track the streaming session ids locally in the app context and derive the
launcher state from that OR the server rows, and expose the state through
aria-busy so it is perceivable and assertable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two panes can stream the same session id (quick-route duplicate panes, PiP), so
the live-session ids are a multiset: start pushes an occurrence, stop removes
exactly one, and each pane pops only the occurrence it still holds.

Cover the paths the first commit left unasserted: the closed launcher after a
mid-run minimize, and the handoff from local truth to server truth when the
streaming pane unmounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bespoke harness (own static server, PASS/FAIL printer, --only/--dist
flags, verify:behavior script) is gone. All 17 checks and 101 assertions
move into a @playwright/test suite under tests/e2e grouped by concern,
following the embed IT conventions: playwright.config.ts mirrors embed's,
assets are served through page.route instead of a hand-rolled server, and
the page-side measurement helpers live in tests/e2e/helpers.

vitest keeps the unit tests under tests/unit; the package test script now
runs both, so turbo's test gate covers the browser checks and CI picks up
chromium provisioning from the @playwright/ devDependency prefix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Trim the mascot changeset to the user-facing effect: the harness paragraph
described a vehicle that no longer exists, and the donor/PR archaeology is
not release-note material.

Replace live-sessions' array bag with a per-session reference count so a
session that starts twice needs two stops, and the signal still hands back
a fresh map on every change.

Give the fab-working IT's immediacy claim in test 3 the same tight timeout
its siblings use (the busy-state handoff keeps the default: it waits on a
round-trip), and move the pane-harness live-sessions import into its group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The binary emitter read its geometry as absolute pixels approved against
the 44px widget FAB stage, so on a large stage the robot art scaled and
the digits stayed 9px specks. Every emitter distance is now multiplied by
min(stageWidth, stageHeight) / EMITTER_REFERENCE_STAGE_PX, measured on
the effect host when the emitter is created: font size, the two lane
offsets, the digit placement and the rise. Rise duration, stagger and
eases are timing, not geometry, and stay fixed; the tip anchor and the
tip enter/exit scale are already fraction-based. A stage that measures
zero falls back to the reference factor.

At the 44px product stage the factor is exactly 1, so the shipped FAB is
byte-for-byte the same geometry, pinned by a parity test at 44px next to
a scaling test at 132px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two controls join the state x working x follow matrix. stageSizePx is a
44-320px range whose lower bound is the widget FAB stage, and it demos
the stage-relative emitter directly: the digits grow with the stage.
poseApply picks how a state change lands, animated through update() or
instantly through the registration path.

Both ride one derived registration key on a keyed Show, so a size change
or a set-mode state change re-registers the parts and nothing else does:
no extra signal, no effect writing state. The emitter reads its scale
factor when it is created, so re-registering is what makes a size change
take effect.

No reduced-motion control: prefers-reduced-motion is a browser-level
media query page script cannot flip, and emulating it in the story would
demo the story instead of the core. The docs block points at the OS
setting and the DevTools Rendering panel instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
setRunning(id, false) for a session the map never saw still rebuilt the
map and handed the signal a fresh reference, waking every anyRunning
reader for a no-op. Return the same map when the id is absent and the
delta is negative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The factor came off the effect host, which is the same box as the
antenna on the widget FAB but not on the site FAB: that one insets its
layers inside a 56px button, so the emitter scaled by 56/44 and shipped
digits 27% too big while the changeset claimed the FAB was untouched.

Read the factor off the antenna layer instead. That is the frame the tip
math already works in, and it is the box the antenna art is drawn into,
so the digits now stay proportional to the art wherever the art lands.
The widget FAB measures 44px there and keeps factor 1 exactly; a stage
and an inset button that render the same 44px antenna now render the
same emitter.

Covered by a site-shaped case in the scale suite: a 44px antenna inset
in a 56px button must produce the approved geometry, which fails at
11.4545px font before this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
anyRunning read the counts signal directly, so every setRunning woke
every reader even when the boolean did not move: the second start and
the first stop of a multiset session both notified for nothing. A memo
collapses them, and the test's notification count drops from 5 to 3,
which is the assertion that proves it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The unknown-id guard in withDelta and the anyRunning memo solved the
same problem twice. The memo is the general one: it stops every no-op
transition, including the multiset middle start and stop the guard never
saw. With it in place no test could fail on the guard alone, because
counts is private and an unknown stop never flips the boolean, so the
guard was unfailable code. One good way; the memo stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk marked this pull request as ready for review August 15, 2026 08:17
omridevk and others added 11 commits August 16, 2026 05:47
The three hand-plumbed layer spans and the createFabRobotRig ref dance are
replaced by a single <Mascot> from @conciv/mascot/react inside the existing
button. The stage carries its 44px size as a class so the prerendered html
paints the robot before hydration installs the default stage stylesheet.
Hover and the working toggle feed the mascot through state/working/follow;
the unreachable onActivate and label props are gone, so the aria-label is
derived from the working state alone.

A focused site e2e covers the prerendered stage, the 44px box, the awake
pose on hover, the rest pose while hovering mid-work, the working toggle
with its binary emitter and label, and a clean unmount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…not bake stale emitter geometry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lished api

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The package entry is the framework-free core service now: createFabRobotRig
and its RigState/RigLayers/FabRobotRig types are gone, exports "." points at
the core index, and every story, harness and test reads the core and effect
subpaths directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The effect host itself claims its slot now, so a <Mascot.Effect> child stands
the default binary down exactly the way <Mascot.Binary> already did, and the
gallery gains a compound story over the published solid entry. The gallery
effect catalog is keyed by the sixteen effect names, so the lookup is total
instead of falling back to binary, and only travelling effects read the curve,
so switching curves no longer remounts an anchored effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ticle effects

matrix, notes, pixel-bubbles and steam each rebuilt the same emitter: measure
the antenna, anchor a tip shell, append the particles and hand the shell to the
nozzle emitter. That shape lives in effect-support now, and the gaze suite gets
its saturating setup as a helper instead of a third copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The widget FAB derives three mascot props from open/working, so the embed suite
now pins the table observably: closed and idle rests and tracks the pointer,
open with nothing running wakes, and opening the panel mid-run keeps the
emitter alive. The site prerender check reads the root tag before matching its
class, so attribute order cannot break it, and build-storybook gets the heap it
needs to finish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e the changeset

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… needs

The three fab tests that hold a run open each repeated the same five lines, so
the setup moves into the chat helpers. The mascot readme snippets read as
components instead of leading-semicolon expressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The exported MascotEffectProps no longer carries the internal fallback flag, so
a consumer cannot re-open the double-host bug by passing it; the root and the
binary shorthand render the wider EffectHost instead. The readme names React's
own conventions rather than claiming identical props, and warns that mount is a
dependency there — its example hands over a stable module-level mount. The embed
pin is named for what it asserts: the emitter keeps running while the panel is
open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ests read the skin

The react entry no longer exports ClaimToken or PartClaim: the claim tokens are
how the wrapper tracks slots, not API, and the solid entry exports neither. The
context memo drops the permanently-stable curve ref from its deps, the packed
install probe reads the one effect catalog the tests already share, and the
stage helpers derive the tip and origin fractions from robotSkin instead of
repeating its numbers. The readme and the changeset now describe the merge and
the react mount contract the way the code actually behaves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk requested a balanced review from Copilot August 16, 2026 04:58
@omridevk omridevk changed the title feat(mascot): componentized core (phase 1) feat(mascot): componentized mascot — core service, solid/react wrappers, 16 effects, consumer migrations Aug 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 140 out of 145 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (3)

packages/mascot/src/core/parts/follow.ts:229

  • dispose() kills the gaze return/quick-to tweens without restoring their current values. If registerParts() replaces one part while reusing the eyes element, or destroy() runs mid-return, the eyes retain a nonzero x/y; a subsequent registration with follow disabled never touches those properties, so the mascot stays permanently off-center. Reset the owned follow transforms during disposal.
    packages/mascot/src/react/mascot-root.tsx:39
  • React slot claims are only registered in layout effects, which do not run during server rendering. Thus SSR of <Mascot><Mascot.Eyes /></Mascot> serializes both the custom eyes child and this default eyes fallback (and custom effects similarly serialize alongside the default binary), violating the one-slot contract until hydration. The fallback decision needs an SSR-aware claim mechanism rather than relying solely on the post-commit claim store.
    packages/mascot/src/core/config.ts:31
  • The PR description says the work-state head bob is intentionally dropped until phase 2, but the new default enables bob, so every consumer that omits activity gets the bob immediately. Either disable this default for phase 1 or update the PR description and behavioral scope to reflect what is actually shipping.

Comment on lines +51 to +53
canvas.style.cssText =
`position:absolute;left:${tip.x - geometry.originX}px;top:${tip.y - geometry.originY}px;` +
`width:${geometry.width}px;height:${geometry.height}px;pointer-events:none;${WILL_CHANGE_STYLE}`
omridevk and others added 8 commits August 16, 2026 11:39
…in at the tip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ition .jsx dist

The solid-start e2e app excluded every workspace /packages/*/dist/ path from
vite-plugin-solid to keep solid-refresh's dev wrapper off precompiled minified
.js dists. @conciv/mascot now ships dist/solid/*.jsx as real JSX source behind
the "solid" export condition (the pattern @ark-ui/solid publishes), and the
embed bundle externalizes @conciv/mascot/solid, so the consumer host resolves
and compiles it. The over-broad exclude swallowed that JSX: vite fell back to
esbuild's classic runtime and served React.createElement calls, so the widget's
Solid render threw into an error boundary and the shadow root mounted empty
with no page error. Narrow the exclude to .js so precompiled dists stay
excluded and JSX source is compiled for Solid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rom viewport edges

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m a valid drop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk requested a balanced review from Copilot August 16, 2026 15:17
@omridevk
omridevk merged commit d796d1e into main Aug 16, 2026
27 checks passed
@omridevk
omridevk deleted the feat/mascot-component branch August 16, 2026 15:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 144 out of 150 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (9)

apps/storybook/src/mascot/mascot-gallery.stories.tsx:405

  • The repository review rules prohibit em dashes in TS/JS string literals. Use colons for these section-label separators.
**Compound** — the same robot from \`@conciv/mascot/solid\`: \`<Mascot>\` renders its own three layers, and the
\`<Mascot.Effect>\` child mounts an effect subpath in place of the default binary.

**Index** — the map of the package: the parts a host renders, all sixteen effect subpaths, and the three

apps/storybook/src/mascot/mascot-gallery.stories.tsx:408

  • The repository review rules prohibit em dashes in TS/JS string literals. Replace this separator with a colon.
**Reduced motion** — under \`prefers-reduced-motion: reduce\` poses are set instantly, follow never arms and no

packages/mascot/src/react/mascot-root.tsx:39

  • During server rendering, compound children cannot publish their claims because claimPart/claimEffect run only in layout effects. Therefore renderToString(<Mascot><Mascot.Eyes /></Mascot>) emits both the custom eyes and this default eyes (and custom effects likewise emit the default binary), then hydration removes the defaults. Derive claims before selecting fallbacks or add an SSR-safe claim mechanism, and cover server-rendered custom slots.
    .changeset/fab-drag-viewport-clamp.md:5
  • This release note describes a launcher drag/clamp implementation that is not part of this mascot PR, so the @conciv/embed release would advertise behavior this changeset does not ship. Replace it with a note for the widget mascot migration and live working-state changes.
Dragging the closed launcher no longer jitters outside the viewport and no longer snaps back from a valid drop. The live drag position is clamped so the button never renders past a viewport edge, the drag offset rides a single owner (an inline transform relative to the button's own resting anchor instead of competing `left`/`top` writes), and the release glides to the snap point through one Web Animations pass whose end state is the resting state — so the hand-off back to CSS moves nothing. A drop that is already at the resting spot skips the animation entirely.

apps/storybook/src/mascot/mascot-gallery.stories.tsx:400

  • The repository review rules prohibit em dashes in TS/JS string literals. Replace these separators with punctuation such as colons.

This issue also appears in the following locations of the same file:

  • line 402
  • line 408
**Playground** — the assembled robot over \`createMascot\`. \`state\` picks the resting expression, \`working\`
runs the activity overlay, \`follow\` arms pointer tracking per channel, \`bob\`/\`throb\`/\`blink\` isolate the
three overlay pieces, \`effect\` swaps which of the sixteen effects is mounted (switching drains the outgoing one
and starts the incoming one), \`curve\` picks the path travelling digits ride out of the antenna tip, and
\`stageSizePx\` resizes the stage — the emitter is scale-relative, so the whole effect grows with the robot.

packages/mascot/src/binary.stories.tsx:124

  • The repository review rules prohibit em dashes in TS/JS string literals. Replace this separator with a colon.
    packages/mascot/src/matrix.stories.tsx:102
  • The repository review rules prohibit em dashes in TS/JS string literals. Replace this separator with a colon.
    packages/mascot/src/pixel-bubbles.stories.tsx:102
  • The repository review rules prohibit em dashes in TS/JS string literals. Rephrase both separators without em dashes.
    packages/mascot/src/speech-bubble.stories.tsx:72
  • The repository review rules prohibit em dashes in TS/JS string literals. Replace both separators with a colon and comma.

import {useIsomorphicLayoutEffect} from './use-layout-effect.js'
import {useMascotHost} from './use-mascot.js'

export function MascotRoot(props: MascotProps): ReactElement {
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.

2 participants