Skip to content

misc updates - #306

Merged
williamstein merged 41 commits into
mainfrom
misc
Aug 27, 2026
Merged

misc updates#306
williamstein merged 41 commits into
mainfrom
misc

Conversation

@williamstein

@williamstein williamstein commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

haraldschilly and others added 30 commits August 25, 2026 14:46
Forward port of the snapping half of cocalc#8816 (plus the follow-up
28bf57adf4 icon tweak) from sagemathinc/cocalc onto cocalc-ai.

## Snap engine

- `snap.ts`: computes snap targets from the page/slide border, the edges
  and center axes of the other elements on the page, and the background
  grid; returns the adjusted offset plus the guide lines to render.
- `snap-guides.tsx`: renders those guide lines on the canvas while a drag
  is in progress.
- Integrated for both focused (edit-mode) and unfocused (select-mode)
  dragging; guide state lives in `canvas.tsx` and is cleared on unmount.
- Grid snapping uses the major grid (100px) at any zoom and the minor grid
  (20px) from 200% zoom on. The grid constants now live in `elements/grid.tsx`
  as the single source of truth.
- Toolbar toggle (`snapToAlignment` in the frame desc, default on) with a
  shift-key override to bypass snapping for a single drag.

## Page geometry

- The CSS background grid is anchored to the data-space origin (0,0), so
  grid lines sit at stable positions that actually match the snap targets
  instead of drifting with the canvas extent.
- `fitToScreen` uses the screen-pixel viewport directly instead of
  `getViewportData()`, which divides by the current scale. The old code
  also multiplied by `canvasScale` on top of that, so repeated calls
  oscillated instead of converging.
- The zoom-reset button now fits content to the screen rather than jumping
  to a fixed 100%.

The Jupyter cell height and ctrl+wheel zoom parts of #8816 are handled
separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Forward port of the Jupyter-cell half of cocalc#8816, which also folds in
the height work from cocalc#8780 that never reached this fork.

## The bug

Cell height was derived from `divRef.getBoundingClientRect().height /
canvasScale + EXTRA_HEIGHT`, i.e. a rendered pixel height divided by the
canvas scale plus a hardcoded 30px fudge factor. That is wrong at every
zoom level other than 100%, and the fudge factor was covering for a real
measurement error: the InputPrompt's `margin-top` collapses through the
unstyled inner div, so `divRef.scrollHeight` silently omits it.

## The fix

- Measure from `outerRef.scrollHeight` plus the outer border. `outerRef`
  has padding, which blocks margin collapse, so the prompt's margin is
  included. No scale division and no fudge constant.
- `measureHeightInner` (inner div + padding + border) is the fallback used
  to detect shrink while focused, where `outerRef` has `height: 100%` and
  therefore cannot report a height below `element.h`.
- The three separate measurement effects (focused resize observer, resize
  trigger, unfocused re-measure on blur) collapse into one effect with a
  single ResizeObserver watching both the outer and the inner div, so
  focused and unfocused cells go through the same code path.
- Grow immediately, shrink debounced by 250ms, and only shrink when
  unfocused -- shrinking while focused oscillates.
- `element.h` is read through a ref so the observer callback never acts on
  a stale height.
- A `requestAnimationFrame` re-measure catches children that lay out after
  the first frame (CodeMirror, output rendering); the observer detaches
  after 5s for unfocused cells unless a computation is still streaming.
- Unfocused cells render with `height: auto` so the box can actually track
  content; focused cells keep `height: 100%` with `overflowY: hidden`.
- The control bar is anchored with `bottom: calc(100% + 10px)` instead of
  a hardcoded `top: -34px`, so it cannot overlap the cell when its own
  height changes.

## box-sizing

`getStyle` sets `boxSizing: "border-box"` (the one line #8780 added to
`style.ts`). The measurement above depends on it. As content-box, the
outer div's `height: 100%` resolves to `element.h` for the content box
with the 5px padding and border outside it, so `outer.scrollHeight`
reports at least `element.h + 10`; adding the border makes every
measurement exceed the current height, the grow branch fires, the
ResizeObserver sees the taller box and measures larger again. The cell
then expands without bound as soon as it is inserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deliberate deviation from upstream cocalc#8816, which grows on any
increase (`h > element.h`) while requiring a >2px delta to shrink.

That asymmetry is what let the box-sizing bug run away instead of
settling: a constant per-cycle overshoot in the measurement always
satisfied the grow condition, the ResizeObserver saw the taller box, and
the cell expanded without bound. The overshoot itself is fixed, but the
missing threshold is what turned a static offset into an infinite loop.

Making the grow side symmetric with the shrink side means any future
measurement error costs at most a couple of pixels instead of an
unbounded loop. Growth stays immediate for real content changes, which
are far larger than the threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Forward port of the ctrl+wheel-zoom half of cocalc#8816 together with its
follow-up cocalc#8823 (the PR actually carrying the PR-TODO-cocalc2 label).

## The bug

Two handlers were fighting over the same events. Chrome translates
ctrl+wheel into pinch gesture events, so `usePinch` saw them at the same
time as `useWheel` did, and the resulting zoom was erratic. The `useWheel`
path was also disabled outright on macOS to work around that conflict, and
it scaled linearly, so a scroll tick moved zoom by a fixed number of font
size units -- a huge relative jump when zoomed out, a barely visible one
when zoomed in.

## The fix

- Replace `useWheel` with a native `wheel` listener registered in capture
  phase, which calls `stopImmediatePropagation` so the event never reaches
  `@use-gesture`. `usePinch` is now touch-pinch only, and the macOS
  exclusion is gone.
- Multiplicative (log2) scaling, so equal scroll ticks produce equal
  percentage changes at any zoom level, with the per-event exponent capped
  so a fast scroll cannot jump across the range.
- Font size is tracked in a ref during a gesture, so throttled store
  updates cannot feed a stale value back into the next event.
- `getFontSize` and the save callback are read through refs, removing the
  stale-closure hazard in the pinch `from()` callback.
- New `wheelSpeed` option; the whiteboard canvas passes 2, which restores
  the faster zoom that felt right there.
- The PDF viewer always supplies `getFontSize` now, falling back to
  `font_size` when no external `zoom` is provided. Previously it only
  supplied it in the `zoom != null` case, so the wheel handler had no
  starting value and ctrl+wheel did nothing in the PDF frame.

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

Addresses two review findings on the snap work.

## Snap tolerance was in data coordinates

`SNAP_THRESHOLD = 8` was compared against data-space distances, while
`canvasScale` was only consulted to choose the grid step. The tolerance
was therefore 2 screen pixels at 25% zoom and 32 at 400% -- snapping was
almost unreachable zoomed out and unpleasantly sticky zoomed in.

The constant is now documented as screen pixels and divided by
`canvasScale` at use, via a `snapThreshold()` helper that also guards
against a missing or non-positive scale.

## Snap toggle was not exposed to assistive tech

The button contained only an `aria-hidden` icon, so it had no accessible
name, and its on/off state was conveyed by background colour alone. Added
`aria-label` and `aria-pressed`, per `src/.agents/accessibility.md` which
requires icon-only controls to carry a real name rather than lean on a
tooltip. The inline `#fff` is replaced by a named `SELECTED_FG` beside
`SELECTED` in `tools/common.tsx`.

`ToolButton` above still has its own inline `#fff`; that is pre-existing
and left alone rather than widening this diff.

## Tests

`snap.test.ts` covers threshold scaling across zoom levels, exact
alignment and guide emission, page-border snapping, and the major/minor
grid transition at 200%. `tools/panel.test.tsx` queries the toggle by
accessible role and name and asserts `aria-pressed` in both states plus
focus and activation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses a review finding: unfocused cells could grow but never shrink.

`position.tsx` renders every element into a parent with a hard
`height: ${element.h}px`, and the unfocused code element carries
`minHeight: 100%`. `outer.scrollHeight` is therefore floored at
`element.h` and `measureHeight()` can never report a smaller box, so the
shrink comparison was dead code -- a cell that lost output stayed at its
old height forever.

Shrink is now measured from the inner div, which is `height: auto` and so
tracks content. That is what the pre-#8816 code did, with a comment saying
exactly why; #8816 moved unfocused measurement to the outer div and lost
it. The reason it moved was margin collapse: the InputPrompt's margin-top
escaped the unstyled inner div, making `inner.scrollHeight` underestimate.
That is fixed at the source instead -- the inner div now has
`display: flow-root`, establishing a block formatting context so the
margin is contained and counted. Outer padding already blocked the margin
from escaping the outer box, so this changes measurement only, not layout.

Grow still measures the outer div, where the floor is harmless.

`height.test.tsx` covers shrink, the commit flag for collaborators, the
2px deadband in both directions, immediate growth, and the MIN_HEIGHT
clamp. Three of its six cases fail against the previous implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses a review finding: the native ctrl+wheel handler treated
`deltaY` as pixels unconditionally.

Browsers may report wheel deltas in lines (`deltaMode` 1) or pages
(`deltaMode` 2) rather than pixels. Firefox and several mouse drivers
report roughly +-3 lines per notch, so the exponent was computed from 3
instead of ~100 and zoom moved by about 0.3% per notch -- effectively
inert, while the same mouse in Chrome worked normally.

Line and page deltas are now scaled to approximate pixels before the
exponent is computed, so a notch feels the same regardless of how the
browser reports it. `normalizeWheelDeltaY` is exported and unit tested,
including that a Chrome pixel notch and a Firefox line notch land within
the same order of magnitude, and that sign is preserved in every mode so
zoom direction never inverts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two review findings on the guide lifecycle.

## Guides vanished at exact alignment

Guide emission was gated on `dx !== 0` / `dy !== 0`, but dx is zero exactly
when the element is already perfectly aligned -- so the guide disappeared
at the moment it was most useful, and reappeared as soon as you drifted
off again. Gate on whether a target actually matched instead. When nothing
is within tolerance the matched lists are empty, so out-of-range drags
still emit nothing.

## Stale guides after a null drag

`not-focused.tsx` cleared the guides inside `if (data.x || data.y)`.
Dragging an element away and back to its exact starting position takes the
else branch, which treats the gesture as a click, so the guides were left
on screen. Clear them unconditionally at the top of `onStop`.

The same component was also missing the unmount cleanup that `focused.tsx`
already has, so guides could survive a component teardown mid-drag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review finding: `saveThrottled` and `save` were built by `useMemo` keyed
only on the frame `id`, while closing over `disabled`, `onZoom`, `actions`
and `throttleMs`.

A hook that first mounted with `disabled: true` therefore kept a no-op
save forever. Once it was enabled again the wheel listener was installed
and fired, but every zoom went into the discarded closure -- zoom appeared
dead with no error. A changed `onZoom` was ignored for the same reason,
and a changed `throttleMs` never took effect.

Those values are now read through refs, so the throttled function stays
stable without going stale, and the memo is keyed on `[id, throttleMs]`.
The `disabled` check moved inside the callback. A cleanup effect cancels
the throttle when it is replaced or the hook unmounts, so a pending
trailing call cannot land after teardown.

`pinch-to-zoom-hook.test.tsx` drives real ctrl+wheel events through the
hook and covers enabled, disabled, disabled → enabled, a swapped onZoom,
and enabled → disabled. The disabled → enabled case fails against the
previous implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review finding: moving the `#fff` literal from `panel.tsx` into a
`SELECTED_FG` constant relocated it rather than removing it. The repo rule
is to take colors from `COLORS`.

There was no white token, so this adds `WHITE` to `MAIN_COLORS` and points
`SELECTED_FG` at it. Note this touches `util/theme.ts`, which widens the
CI matrix for the PR.

`ToolButton` in `panel.tsx` still carries its own inline `#fff`; that is
pre-existing and left for a separate cleanup rather than widening this
change set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to 763fec9, which moved the guide clear to the top of
`onStop` so the null-drag branch would reach it. That was the wrong end:
`computeSnapForDrag` runs later in the same handler and calls
`setSnapLines(result.lines)` for the drop position, so React's batched
update ended with the guides set. Every snapped, non-zero drop left them
on screen -- worse than the null-drag case it was fixing.

The clear now runs last, after the computation and the move, and outside
the if/else so both branches reach it.

`not-focused.test.tsx` drives Draggable's callbacks directly (jsdom cannot
produce a real drag) and asserts the final `setSnapLines` call is the
clear, for a normal drop and for a drag returning to its exact starting
position, plus the unmount cleanup. The normal-drop case fails against the
previous ordering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses three Codex review findings on the snap engine.

## Guides could point at a line nothing snapped to

Tie collection compared only absolute distances while the applied offset
kept its sign, so two targets equidistant on opposite sides both produced
a guide although only one was used. Candidates now carry their signed
adjustment, and guides are filtered to those matching the adjustment
actually applied.

## Duplicate targets made dragging quadratic

Every element sharing an edge or centre coordinate contributed its own
matched entry, and each entry then called an extent helper that scans all
other elements -- so a board with many aligned elements did that scan
repeatedly per drag event and rendered a stack of identical guide divs.
Matched targets are now deduplicated by coordinate, so each guide line is
computed and drawn once.

## Multi-selection snapped against inflated bounds

For a multi-selection the moving element is the synthetic "selection" rect
from canvas.tsx, whose w/h are `xMax - xMin + 1` to give the selection
border somewhere to draw. Snapping against that left right/bottom
alignment a unit short and centre alignment half a unit off. The padding
is removed again before computing the snap.

## Grid targets only where a grid is drawn

`<Grid>` renders only for `mainFrameType == "whiteboard"`, but grid
targets were added whenever a scale was supplied, so slides snapped to
invisible 100/20-unit lines competing with the page border, page centre
and element targets that alignment on a slide is actually about.
`computeSnap` takes `gridEnabled`, derived in canvas.tsx from the same
condition that renders the grid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The arrowhead sat past the end of the line: the line was drawn as a full
`border` box spanning the whole distance, with an `Icon` caret overlaid at
a negative offset, so the line ran on underneath the head and the arrow
read as too long.

Ports the #8780 rendering: the head is an SVG triangle sized from
`arrowSize`, and the line carries `marginRight: tipLength` so it stops
where the head begins. Selection uses `outline` rather than `border` so
the indicator cannot shift the rotation pivot, and the click target is an
explicit invisible strip instead of padding on the rotated container --
padding was distorting the geometry it was attached to.

Endpoint selection moves to `getEdgeEndpoints` (also from #8780), which
picks sides by center-to-center ray analysis weighted by each rect's
aspect ratio: vertically stacked elements connect bottom-to-top, and
horizontal ones right-to-left, instead of the previous nearest-midpoint
heuristic that could pick a side across a corner.

`edge-endpoints.test.ts` covers the four cardinal arrangements, the
aspect-ratio weighting, coincident centers, and zero-size rects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clicking a text box grew it and left an empty strip above the text.

`modeSwitchStyle` passed `top: -82px; left: -18px` but no `position`, and
the wrapper in `editors/markdown-input/mode-switch.tsx` is
`position: relative`. Relative offsets move an element visually without
releasing its place in normal flow, so the control appeared outside the
box -- which is why it looked correctly placed -- while still holding a
strip of empty space where it used to sit. The hardcoded `+ 15` in the
height calculation was compensating for that reserved space.

Both go: the mode switch becomes `position: absolute` (as in cocalc#8780,
which is what took it out of flow upstream), and the `+ 15` term is
dropped. Upstream kept `+ 15` and added a blur handler that restores the
pre-edit height instead; dropping it outright is a deliberate divergence,
since with the control out of flow there is nothing left for it to
reserve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rendered text container is `width: 100%` with `padding: 5px` and was
content-box, so it laid out 2*PADDING wider than the element it lives in:
measured live, an 847px text element rendered a 857px inner div, pushing
the paragraph 5px past the element's right edge. The editor's container
inset the text by 5px instead, so switching between the two modes shifted
the text horizontally by 10px.

Both containers are now border-box, so PADDING sits inside the element
width in either mode. Measured against the running Lite instance
afterwards: left and right gaps are 5px in both modes, where before the
rendered view overflowed by 5px on the right.

Upstream has the same bug; this is not a port regression.

A vertical mismatch remains and is deliberately not addressed here: the
rendered paragraph carries markdown's 1em top margin (29px total from the
element top) while the editor's paragraph has none (4.9px). Fixing that
properly means reconciling the slate editor's block margins with the
static markdown CSS, which is shared well beyond the whiteboard.

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

Three geometry defects in the arrow renderer, all inherited from the
cocalc#8780 code this was ported from.

## Thick stems hung below the endpoint axis

The endpoints lie on y=0 of the rotated frame, but the stem was a bare
`borderTop`, which hangs entirely downward from its element's top edge.
At thickness 1 the half-pixel offset is invisible; at the supported
maximum of 30 the line sits 15px off-axis -- and the head, which was
centred on y=0, no longer met the stem. That is the "arrow head against
the stem" mismatch.

The stem is now an absolutely positioned child offset by half its
thickness, so line and head straddle the same axis at any thickness.

## Head was drawn at opacity squared

The shared parent carried `opacity` and the polygon carried it again, so
`opacity: 0.5` rendered a 0.25 head against a 0.5 line. The polygon's own
opacity is removed; the parent applies it once to both.

## Head overshot short edges

`tipLength` was a fixed fraction of `arrowSize`, so for an edge shorter
than the head the stem's `marginRight` exceeded the available width, the
line collapsed to nothing, and the head extended past the element it
points at -- e.g. a 13px head on a 5px gap. Head length and width now
scale down together once the edge is shorter than the head, preserving
its proportions, and it is omitted entirely at zero length.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses the Codex accessibility finding on the replaced edge hit target.

The selection surface was a plain `div` carrying only `onClick`, so an edge
could be selected by pointer but not reached by Tab or activated with
Enter/Space. AGENTS.md requires focused accessibility coverage for changed
interactive UI, and this hit target was changed by the arrow rewrite.

It is now a real `<button type="button">` with an accessible name
(`ariaLabel`, defaulting to "Edge"), so keyboard activation comes from the
platform rather than hand-rolled key handling. It stays visually
transparent, and the browser's focus ring is deliberately not suppressed
so the focused edge is visible.

`onClick` moved off the outer container onto the button. The container
renders at zero height -- its children are absolutely positioned -- so the
20px strip was already the real hit area, and having the handler in one
place means a click cannot fire twice.

Tests query by role and accessible name per `src/.agents/accessibility.md`,
covering the default and caller-supplied name, focusability, activation,
that the focus ring is not suppressed, and that no control is rendered for
a non-clickable edge.

Note this makes edges the only keyboard-reachable whiteboard elements;
text, code and notes remain pointer-only. A coherent canvas-wide selection
and traversal model is still needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…give it a chisel nib

The live preview of a pen stroke was drawn incrementally: on every mousemove
the last two points were stroked onto the preview canvas. With a translucent
pen (the highlighter, opacity 0.4) the overlapping round ends of consecutive
segments each composited separately, so the stroke built up to fully opaque
while drawing and then snapped back to translucent the moment the mouse was
released and the real element rendered.

The preview now redraws the whole path in one stroke/fill, coalesced through
`requestAnimationFrame`, and `drawCurve` always paints the curve in a single
canvas operation. Points closer than one pixel to the previous one are
dropped, so a high frequency pointing device does not blow up the path.

Also fixed along the way:

- `Pen` cleared its canvas *after* translating by `pad`, so a `pad` wide strip
  along the top and left was never cleared. That strip holds the top half of a
  wide stroke, so every re-render of a translucent stroke composited it onto
  itself and the stroke got darker (0.4 -> 0.64 -> 0.78 ...) as the board was
  edited. Clearing now happens with the identity transform.
- The preview canvas had no explicit CSS size, so it was laid out at its
  bitmap size; the old code compensated by drawing at 1/devicePixelRatio.
  The canvas is now sized in CSS pixels and drawn at full device resolution,
  which also makes the preview line up with the finished element on HiDPI
  displays.
- `drawCurve` sets `globalAlpha` unconditionally instead of only when an
  opacity is given, so a translucent stroke can no longer leak its alpha into
  whatever is drawn next on the same context.

The highlighter preset now uses `nib: "chisel"`: an upright rectangle 2*radius
tall and a fraction of that wide, swept along the path, which is how a real
highlighter behaves -- a wide band when swept horizontally, a thin line when
moved vertically. The stroke is the union of the nib's positions, emitted as
one path with consistently oriented subpaths and filled once. Elements without
`nib` keep the old round pen rendering, so existing drawings are unchanged.
Fail recipient creation closed until the Zendesk webhook and opt-out secret is
configured. This prevents a draft from retaining a deterministic fallback
token and later becoming deliverable after configuration changes.
Translate app-server exits reported as status 137 or SIGKILL into an actionable user-facing error that identifies project RAM exhaustion as the usual cause and suggests increasing RAM or reducing memory use. Preserve existing diagnostics for all other exit statuses and cover both Node process exit representations.
Wait for an older locked project outbox row instead of allowing newer rows to overtake it. The collaborator projector can briefly hold project creation events; skipping those rows allowed a later stale creation payload to overwrite an already projected host and state.

Add unit and PGlite coverage for the locking contract and the observed causal-order regression.
Await course folder creation before dismissing the assignment selector or modal. Keep the controls open and show a local error when the project operation fails, while disabling repeat submissions until it settles.

Propagate assignment mkdir failures to the caller and cover the selector's success and failure behavior with accessible interaction tests.
Add seed-global immutable PDF storage for purchase orders, including digest verification, optimistic concurrency, idempotent upload, retained voiding, and audited downloads. Expose the complete workflow through fresh-authenticated Conat RPCs, preview-first CLI commands, the AR detail UI, and admin documentation.

Keep document bytes out of ordinary order payloads, preserve late-arriving procurement evidence after completion, and let a reviewed PO reference fill an otherwise empty order PO number. Correct nested quote downloads to use --output-file so they do not collide with the root CLI output option.

Document a separate Stripe-native Quotes rollout that retains CoCalc commercial authority and local document history while accounting for Stripe Invoicing Plus requirements.
Load the current membership tier catalog when the account storage warning is opened and compare each purchasable tier with the account's effective storage caps. Show total and per-project storage, headroom at current measured usage, annual pricing, and calculated annual savings while excluding tiers that would not improve both existing limits.

Allow users to enter the existing membership purchase flow directly from a suitable tier, with annual billing selected, and add accessible rendering and comparison coverage.
…ordering

Harden project projection and support-facing workflows
Carry the selected storage upgrade tier's available billing interval into the membership purchase modal. Prefer annual billing when the tier offers it, but use monthly billing for monthly-only tiers instead of requesting an unavailable annual quote.

Cover both annual and monthly-only option selection.
Persist an authoritative guidance delivery timestamp after the live Codex turn accepts a steer request, including requests routed to another ACP worker. Preserve that state while removing the queued projection, and use the delivery time to interleave guidance with agent activity instead of its earlier composition time.

Include delivery time in virtualized chat row revisions so mounted activity logs update when the authoritative timestamp arrives.
…ordering

Handle monthly-only storage upgrade tiers
Override the archived image-size package used by Metro with the audited
image-size-next 1.2.2 compatibility release, fixing CVE-2025-71329 and
CVE-2025-71330. Keep the override scoped to vulnerable versions so a future
upstream resolution remains unaffected.
Use the OIDC-published Patchflow 0.8.1 release in the sync and backend
consumers. The release updates immutable to its patched version and removes the
obsolete save-dev placeholder from Patchflow's dependency graph.
@williamstein

Copy link
Copy Markdown
Contributor Author

lgtm

Make alignment offsets visible during dragging before committing the same
snapped position on drop, and exclude invisible frame contents from both snap
targets and guide extents.

Keep completed, unfocused Jupyter cells observed so delayed and collaborative
content changes continue to correct their saved height. Prevent visible arrow
geometry from intercepting the accessible selection button.

Add focused regressions for each behavior, including snap preview position and
resize notifications after the previous five-second cutoff.
Render active whiteboard code-cell output from the Jupyter run overlay instead of waiting for the final durable notebook update. Keep the output component mounted while a cell is running so early stream messages have a render target, while persisting only the completed result to the collaborative board document.

Add focused coverage for the running-without-output state and overlay forwarding.
Replace rotated HTML edge lines with side-aware SVG cubic Bezier paths and target markers. Render completed edges below both endpoint elements, use the curve itself for selection and keyboard focus, and retain a wide transparent hit path for pointer selection without changing persisted board data.

Extend endpoint geometry with attachment sides and cover geometry, z-ordering, previews, marker sizing, and accessible interaction.
Move delete and insert controls into a dedicated row beneath each page thumbnail so the page-strip scrollbar cannot cover them. Constrain page items with border-box sizing, reclaim the former button gutter for the preview, and prevent insert clicks from bubbling into page selection.

Give both controls explicit accessible names and cover the layout and click isolation with focused tests.
Place the tooltip outside the page-delete Popconfirm so the confirmation trigger reaches its button. The previous nesting swallowed Popconfirm's injected trigger props, leaving the trash control inert.

Cover opening the confirmation and invoking deletion only after explicit confirmation.
@williamstein
williamstein merged commit 04cb6b0 into main Aug 27, 2026
7 checks passed
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