Skip to content

shakedown round 2: docs + chainable Draggable.onMouseDown#82

Merged
re-marked merged 6 commits into
mainfrom
docs/shakedown-round-2
May 9, 2026
Merged

shakedown round 2: docs + chainable Draggable.onMouseDown#82
re-marked merged 6 commits into
mainfrom
docs/shakedown-round-2

Conversation

@re-marked

Copy link
Copy Markdown
Owner

Summary

Bundled follow-up to the 6 codex shakedown PRs (#76-#81). 5 commits, mostly docs, one small code change.

# Commit Closes Files
1 `feat(Draggable): expose chainable onMouseDown` C11 #64 Draggable.tsx + .test + draggable.md
2 `docs(layout): document chrome flexShrink:0 pitfall` B6 #59 concepts/layout.md + components/box.md
3 `docs(text): document ink-virtual-text reconciler rewrite` D14 #67 concepts/text.md + components/text.md
4 `docs(patterns): document window pattern` D15 #68 new patterns/window.md + components/draggable.md
5 `docs(install): document link: workspace react dedup` E16 #69 getting-started/install.md

Highlights

  • Draggable.onMouseDown is back — consumer handler fires BEFORE Draggable's internal drag wiring, can preempt the drag via `event.captureGesture(...)` (first-call-wins per A21). Press-time z-bump still fires regardless of preemption — "press is press."
  • Layout pitfalls section — explicit guidance that chrome elements (titlebars / footers / status rows) need `flexShrink={0}` because Box defaults to 1 (CSS-flex spec).
  • Nested vs sibling Text — explains the `ink-virtual-text` reconciler rewrite, why nested Text isn't a flex child, when to reach for sibling Texts in a Box instead. Forward-link to known-issue Nested <Text> inside flex-row parent silently truncates trailing content even with abundant slack #51.
  • Window pattern — documents the scratchpad demo's shape as the canonical "build a window" recipe until the proper `` primitive (A4: No window primitive — Draggable + Resizable don't compose #56) ships.
  • `link:` dedup guide — the React peer-dep singleton trap that broke termos until we pinned react/react-reconciler from yokai's node_modules. Saves the next consumer the hour of debugging.

Test plan

  • `pnpm typecheck` clean
  • `pnpm test` — 703 passing (was 702; +1 type-contract test for new `onMouseDown` prop)
  • `pnpm lint` clean
  • `pnpm build` clean

Notes

  • Commit 1 only: type-contract change. `DraggableProps` no longer omits `onMouseDown`. Additive at type level (consumers can now pass it; previously they couldn't), no runtime regression.
  • Commit 1 z-bump semantics: confirmed in the new prop's docstring. Press always raises; consumers who want fully-suppressed press behavior set `disabled`.
  • All commits include co-authorship per CLAUDE.md.

re-marked and others added 5 commits May 9, 2026 13:56
…ling

Closes shakedown C11 (#64).

Stop excluding `onMouseDown` from `DraggableProps`. The component now
calls the consumer's onMouseDown FIRST (synchronously, before its
internal handleDragPress) so app-level concerns like "raise window on
press" / "focus this widget on click" / press-time analytics can
observe every press.

Consumer can call `event.captureGesture(...)` themselves to preempt
the drag — captureGesture is first-call-wins (PR #81 / A21), so a
consumer that captures here owns the subsequent motion + release and
Draggable's internal capture becomes a no-op.

Side-effect contract: the press-time z-bump (raise on press) still
fires regardless of whether the consumer preempts. "Press is press"
— the z-bump is a UI signal independent of "is this a drag." To
suppress all press behavior including the z-bump, set `disabled`
instead.

Implementation matches the existing onDragStart/onDrag/onDragEnd ref
pattern: stash the consumer handler in a ref, read fresh closure on
each press, keep the useCallback identity stable across prop changes.

Type contract: `DraggableProps` now omits only `position | top | left`
(was previously also omitting `onMouseDown`). The `onMouseDown` prop's
JSDoc spells out the chain order + the captureGesture preemption
escape hatch + the z-bump exception.

Tests: extended `DraggableProps` describe with a satisfies-check that
the type accepts a consumer onMouseDown. The chain ordering itself is
thin (one ref read + one call) — exercised by the existing
`pnpm demo:drag` / `pnpm demo:scratchpad` smoke tests; the underlying
captureGesture first-call-wins semantic that makes preemption work
is already covered by mouse-event.test.ts (PR #81).

Docs: `docs/components/draggable.md` props table gets the new row +
the "owned internally" sentence is updated to reflect the new
forward-chain behavior.

Co-Authored-By: Mark <psyhik17@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Closes shakedown B6 (#59).

Promote the existing one-line hint into a full Layout pitfalls
section: cause (Box default flexShrink: 1, CSS-flex spec compliant),
symptom (titlebars / footers / status rows silently disappear when a
sibling has large natural size), fix (flexShrink={0} on chrome that
should always render), heuristic ("if collapsing the element would
make the UI broken or unrecognizable, it should be flexShrink={0}").

Also notes the same pitfall affects wrapping <Text> in fixed-height
containers (links to A1 territory until that's properly fixed).

Cross-link from docs/components/box.md's flexShrink prop row to the
new section so consumers reading the props table get to the pitfall
without first having to know the concepts page exists.

Co-Authored-By: Mark <psyhik17@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Closes shakedown D14 (#67).

When a consumer writes <Text> nested inside <Text>, the reconciler
rewrites the inner one to ink-virtual-text (reconciler.ts:289-310).
Virtual-text has no Yoga node — the whole nested-Text tree becomes a
single flex leaf, sized by squashTextNodesToSegments which flattens
descendants into one measurement string.

This is invisible to consumers today: they reach for nested Text
expecting Box-like flex semantics (multiple flex children), get
single-leaf semantics (one flex child), and get bitten when a
sibling pushes width pressure that collapses or wraps the whole
nested tree.

New "Nested vs sibling Text" section in docs/concepts/text.md
explains:
- the reconciler rewrite (with file:line ref)
- what virtual-text means for layout (one Yoga leaf, not many)
- when nested makes sense (style spans inside flowing text)
- when sibling Texts in a Box make sense (independent layout, gap,
  individual flexShrink, hit-testing)
- the known-issue forward-link to #51 (silent truncation under
  certain flex configurations) until that's properly fixed

Cross-link from docs/components/text.md's "Nested styles" example so
the props-table reader sees the gotcha without first knowing the
concepts page exists.

Co-Authored-By: Mark <psyhik17@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Closes shakedown D15 (#68).

The scratchpad demo's `<Draggable width=W height=H borderStyle bg
flexDirection="column">` shape with content directly inside, plus the
`onMouseDown` + `stopImmediatePropagation` + `captureGesture({onUp})`
pattern for in-window buttons, IS the canonical "build a window-
shaped element on yokai" recipe today. It was a demo, not labeled as
the pattern; consumers reverse-engineered it (termos did this).

New docs/patterns/window.md mirrors the structure of existing pattern
docs (modal.md, confirmation-dialog.md): full code, "How it works"
walkthrough, "Variations" (no close button, multiple windows,
constrained drag, modal-window, resizable corners gap), "Known
caveats" (interactive-children pattern, fixed dimensions required,
implicit titlebar drag), "Related" (links to Draggable, Modal,
scratchpad source, A4 issue #56, A22 issue #72).

Marked as INTERIM — explicit forward-link to the proper <Window>
primitive (issue #56) so consumers know there's a long-term answer
coming.

Cross-link from docs/components/draggable.md Related section so
draggable readers see the window recipe.

Co-Authored-By: Mark <psyhik17@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Closes shakedown E16 (#69).

When a consumer uses link: to depend on a local yokai checkout (not
a monorepo workspace, not an npm install), their react and yokai's
react resolve to DIFFERENT module instances. React's per-module
dispatcher singleton check fails, useContext returns null for every
yokai-provided context (FocusContext, TerminalSizeContext, etc.),
and core hooks silently no-op:
  - useFocus returns no-op ref + isFocused: false
  - useTerminalViewport returns no dimensions
  - Cursor declarations don't fire
  - FocusGroup doesn't cycle on Tab

The fix is to force the consumer's react and react-reconciler to
link from yokai's node_modules, so both sides share one module
instance.

New "Local-checkout development (link:)" section in
docs/getting-started/install.md covers:
- the package.json shape (4 link: entries)
- the why (peer-dep dedup model + React's per-module singleton)
- the symptoms (so consumers recognize the bug from observed
  behavior)
- a verification step (compare symlink targets via ls)
- reference to termos's package.json as the working example

Section placed AFTER "Monorepo workspace" so the canonical-install
flows (npm tarball, GitHub tag, monorepo workspace) appear in
discoverability order; link: development is the niche case at the
bottom.

Co-Authored-By: Mark <psyhik17@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@re-marked re-marked added the source:shakedown Imported from the local Yokai shakedown issue list label May 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

| Package | Version |
|---------|---------|
| `react` | `^19.2.5` |
| `node` | `>=22` |

P2 Badge Include the reconciler peer in install steps

The renderer package declares react-reconciler as a peer dependency and imports it at runtime (packages/renderer/src/reconciler.ts), but this peer table and the canonical install command only tell consumers to install react. In pnpm/npm configurations that do not auto-install peers, following these docs leaves @yokai-tui/renderer unable to resolve react-reconciler; the peer table and install command should include the reconciler peer as well.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/patterns/window.md Outdated
- **Different titlebar content**: replace the `<Text dim>` with breadcrumbs, tab bar, status indicators, etc. As long as one element captures the close-or-equivalent action via `onMouseDown` + `e.captureGesture`, the rest can stay clickable.
- **Multiple windows**: render N `<Draggable>` siblings. Each owns its own z-index (Draggable bumps `persistedZ` on press for raise-on-press). The most-recently-pressed window stays in front of the others.
- **Constrained drag**: pass `bounds={{ width, height }}` so the window can't be dragged past viewport edges. Useful when the WM should keep windows reachable.
- **Modal mode**: combine with the [Modal](./modal.md) pattern's backdrop — render a full-viewport `<Box position="absolute" backgroundColor="black" zIndex={100}>` behind the window's `<Draggable zIndex={101}>`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid recommending ignored Draggable zIndex

This modal recipe relies on <Draggable zIndex={101}>, but Draggable always passes its own zIndex to the underlying Box after spreading user props, so the consumer-supplied value is ignored. With the suggested backdrop at zIndex={100}, a fresh Draggable starts around z=10 and will render behind the backdrop, making the modal window unusable; the docs need a pattern that actually raises the window above the backdrop or the component needs to expose a base z-index.

Useful? React with 👍 / 👎.

Two P2s flagged by codex:

1. **Install instructions missed react-reconciler peer.** The renderer
   declares `react-reconciler` as a peer dep and imports it at
   runtime (reconciler.ts), but the peer table and canonical install
   command only listed react. pnpm doesn't auto-install peers; npm
   only does on clean installs. Following the docs would leave
   consumers with an unresolved react-reconciler at runtime.

   Add react-reconciler to the peer-deps table, the npm install
   command, and the GitHub-tag jsonc example. Add a note explaining
   why both peers must be passed explicitly.

2. **Window pattern recommended `<Draggable zIndex={101}>` which is
   ignored.** Draggable always passes its own internal zIndex
   (persistedZ, starting around 10) to the underlying Box AFTER
   spreading user props, so consumer-supplied zIndex is silently
   dropped. With the previously-suggested backdrop at zIndex=100,
   a fresh Draggable at z=10 would render BEHIND the backdrop —
   modal window invisible.

   Fix the pattern: give the BACKDROP a low absolute zIndex (e.g. 1)
   and let Draggable's natural press-bumped z float above it. Pressing
   the window once bumps it well above any low backdrop value. Note
   that an explicit `baseZIndex` prop on Draggable would be a cleaner
   API (forward-link to a future enhancement).

Co-Authored-By: Mark <psyhik17@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

source:shakedown Imported from the local Yokai shakedown issue list

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant