Skip to content

[fix] roadmap #15 second drop: properties UX, toasts, stars, PWA, selection/outline (K) - #84

Merged
AlexZ005 merged 30 commits into
release/nextfrom
fix/roadmap15-lmno
Aug 5, 2026
Merged

[fix] roadmap #15 second drop: properties UX, toasts, stars, PWA, selection/outline (K)#84
AlexZ005 merged 30 commits into
release/nextfrom
fix/roadmap15-lmno

Conversation

@AlexZ005

@AlexZ005 AlexZ005 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Roadmap #15 second drop — now the full set: batches K / L / M / N / O + the toast-system rework and two review fixes (plan: cloud repo plans-core/roadmap-15…#second-drop-2026-08-02).

Stacks on #82 (branch cut from fix/roadmap15-b-ui) — merge #82 first and this reduces to its 5 commits.

K — viewport selection & outline (Fable)

Three reported symptoms, one cause: the outline read selectedObject, and selection state disagreed between paths.

  • Outline follows the selection SET — deselect clears it (selectedObject deliberately keeps the last object for the inspector, so it could never signal "no outline").
  • Every set member is traversed: groups outline all child meshes (a Group alone outlined nothing), multi-selections outline every member, peer-locked groups too.
  • Creation populates the set (/create, lights, groups, imports) — it attached the gizmo but never told the set.
  • Ctrl+D on an empty selection toasts instead of duplicating the sticky last object (safe now that the set is authoritative; viewing a peer-locked object still duplicates an editable copy — the one deliberate fall-through).

Toast system (review follow-ups)

  • The two z-tiers were pinned to the same coordinates — approval cards physically covered info toasts. One .toasts-stack wrapper now stacks them (auto-margins on purpose: a transform would trap the z-index that keeps approvals above modals).
  • Connection requests wear the shared card (amber), fold at 3 into "+N more requests…" → drawer.
  • "Receiving objects" wears the card — and its lifecycle is fixed: the old machinery re-armed on every objectsGroup poke once loading emptied, so the finished card re-showed forever. Now: visible during transfer → 2.5s grace at N/N → gone.
  • "Watching: X" is a mode banner pinned at the stack head (red frame, live dot, big Exit) — toasts can never shift it.
  • share-or-stash is STICKY — it used to auto-share after 14s, which could silently publish a scene the user meant to stash. The joiner's handshake reply now waits; ✕ takes the safe default (share).
  • Sticky prompts are never evicted by toast bursts; the fold count excludes them.

O — properties panel UX

Pin toggle (pinned = follows selection, deselect falls back to Scene) · plain click only selects (it used to open the panel on every click); opens on double-click / new context-menu Properties / object list · material slider readouts fixed (a $derived returning the same in-place-mutated material never propagates — === equality; snapshot per poke) · Configure Scene → row highlight (no left bar, per review).

L / M / N

Info-kind sticky toasts in the drawer · redundant hex textboxes removed · "+N more" opens the drawer on Toasts · GitHub stars on Welcome + cloud profile (cloud ec87928; 12h cache, hidden on failure) · PWA manifest + icons + no-cache SW (version.json stays the update path; dev/self-signed HTTPS can't show the install prompt — verify on the deployed site).

Docs

CLAUDE.md + e2e-verify/peer-feature skills updated with the #15 learnings ($derived === snapshot rule, picker v4, selection-set truth, toast API, port-shadow proof, baseline 419/62 — C dropped 14, K dropped 2 more; release gate ratcheted).

Verification

  • New suites: ui-fixes-15lmno (45 checks), viewport-selection (14 checks); themed-select extended.
  • Green: multi-select + undo (two-peer), inspector, sessions, share-stash, whats-new, color-picker-15c, ui-fixes-15b, connect-states, delete-object, vr-mesh-undo.
  • npm run build green; npx svelte-check 419/62.
  • Fixed a stale assertion in roadmap-13-notifications-notes (expected "Connection request from peer" — text the UI never had); that suite is down to its one pre-existing unread-badge failure.

🤖 Generated with Claude Code

AlexZ005 and others added 7 commits August 1, 2026 15:26
…w resize

Roadmap #15 batch B (plan: cloud repo plans-core/roadmap-15, specs inline):

- B1 ThemedSelect: the portaled popup copied the TRIGGER's width, and the
  trigger is sized to the SELECTED label - picking a short option ("Box")
  ellipsised every longer name on reopen. The trigger width is now a MINIMUM;
  the popup sizes to its content (capped at 28rem / the viewport) and is
  clamped back on screen when that overflows the right edge. Fixes all ~22
  call sites at once.
- B2 duplicate: a multi-select member wears an emissive highlight, and the
  clone's fresh material baked it in - the FIRST clone stayed selection-blue
  forever (its tinted value got recorded as its "original"). Clones now
  restore the source's recorded emissive (stripSelectionTint), and
  duplicateSelection no longer selects each clone mid-loop (which collapsed
  the set and restored sources one at a time); it selects the whole clone set
  once at the end.
- B3 packs: double-click / Enter / "Place in scene" now show the same
  dismissible loading toast the viewport DROP path shows (shared
  holdLoadingToast helper) - a slow CDN fetch used to look like nothing
  happened.
- B4 Connect: the chevron badges the toast count the CLOSED drawer is holding
  when toasts are routed drawer-only (mirrors the drawer tab badge; amber when
  approvals are pending).
- B5 Sessions: naming a session (save + rename) is an inline textbox instead
  of window.prompt - Enter commits, Esc cancels, focus is taken automatically.
- B6 modals: app modals are non-modal <dialog>s, so the page behind them is
  NOT inert and window key handlers still fired (WASD flew the camera behind
  Settings). New derived `anyModalOpen` gates shortcuts.js, editorNavigation
  (keydown AND the per-frame fly, which also clears held keys) and
  inputRuntime; only Settings was guarded before.
- B7 windows: dragWindow gains an opt-in `resizable` corner grabber that
  persists {w,h} in the same win:<key> record and re-clamps on viewport
  resize; What's New opts in and goes full-screen below the Connect bar at
  <=640px (the .tp-modal-frame treatment).
- B8 context menu: Edit mesh / Sculpt hide for a multi-selection - they are
  single-object modes and silently acted on the last-picked object only.

Verification: new tests/e2e/ui-fixes-15b.test.cjs (22 checks covering B2/B4/
B5/B6/B7/B8), themed-select extended for B1; sessions + whats-new + the new
suite green; build green; svelte-check baseline 435/62 held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap #15 batch C. svelte-awesome-color-picker 3.x -> 4.1.3 (deps
housekeeping) is a Svelte-5 runes rewrite with NO component events, so every
`on:input` handler in the Inspector silently stopped firing - the swatch still
tracked the drag (bind:hex) while nothing was applied. That is the reported
"changing color in mesh properties does nothing".

- C1 mechanism: all five pickers (material, light, hemisphere ground, scene
  background, fog) move to the v4 `onInput` PROP and read `c.hex` (v4 passes
  {color,hsv,rgb,hex} directly, not event.detail).
- C1 binding: the pickers now take `hex` ONE-WAY. v4's ColorPicker writes its
  own snapshot back through a binding, which CLOBBERS external writes - a
  plain `backgroundColor.set(...)` was reverted while the picker was mounted.
  onInput is the input channel, so a binding buys nothing and costs
  correctness. (Also drops 14 svelte-check errors: 435 -> 421, gate updated.)
- C1 sky: background/fog edits wrote scene.background / scene.fog directly and
  the next applyEnvironment() restored the preset values - so even with the
  handler alive the color snapped back. New `editEnvSky()` in environment.js
  detaches a live custom preset (the editRigComponent precedent), so sky edits
  stick, persist and replicate as {type:'environment'} instead of the old
  point-fix {type:'color', uuid:'background'} messages.
- C1 material: the typed hex box routes through the shared
  materialsHandler.setObjectColor (undo + replication); the picker drag keeps
  its 600ms gesture debounce so a drag is ONE undo entry, not one per frame.
- C2: the picker's hex/rgb/hsv text inputs are enabled everywhere
  (isTextInput + textInputModes), with the built-in mode cycle button - in
  both mesh properties and configure scene.

Verification: new tests/e2e/color-picker-15c.test.cjs (12 checks - modes
render, the picker applies + replicates + records one debounced undo entry,
undo steps back, the scene background reaches three.js); inspector,
environment, environment-v2 and ui-fixes-15b green; build green; svelte-check
421/62 (down from 435, release.yml gate ratcheted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Roadmap #15 second drop, Opus batches L / M / N / O.

O1 material readouts: Roughness/Metalness/Opacity showed the PRE-drag value.
  `material` was $derived from $selectedObject, but setMaterialParam mutates
  the material in place and pokes objectsGroup - and $derived compares with
  ===, so returning the same THREE material never propagated. It now returns a
  fresh SNAPSHOT per poke, which fixes every material row at once.
O2 pin: the properties panel gains a pin toggle (persisted). Pinned it stays
  open and follows you - object selected shows its properties, nothing
  selected falls back to the Scene's (deselect no longer closes it).
O3 opening it: a plain viewport click now only SELECTS (it used to force the
  panel open on every click - raycastSelect passed openProperties=true). The
  panel opens on DOUBLE-click, from the new context-menu "Properties" entry,
  or from the object list - and always when pinned.
O4 menu indicator: the "● " prefix on Configure Scene shifted the label as it
  appeared and duplicated what the open panel already shows; the row carries
  an `active` highlight instead.

L1/L2 info toasts: "Restore previous session?" and the first-run open-source
  notice were hand-rolled flowbite <Toast> blocks - which is exactly why they
  looked nothing like the other cards AND never appeared in the Connect
  drawer's Toasts tab. They are now sticky INFO entries in the normal toast
  store (teal accent vs the blue default and amber approvals), mirrored from
  their source stores so they still appear/disappear with them and keep their
  dismiss side effects. autoDismiss skips sticky entries.
L3 the standalone hex textboxes under the colour pickers are gone - 15-C2 gave
  every picker its own hex/rgb/hsv field, so they were duplicate UI.
L4 the "+N more" overflow line is a button that opens the drawer on Toasts.

M GitHub stars: the Welcome overlay's GitHub button shows the star count, and
  the cloud profile dropdown gains "★ Star on GitHub" ABOVE Support (cloud
  repo, separate commit). Cached 12h in localStorage (unauthenticated GitHub
  allows 60 req/h/IP) and HIDDEN on any failure - never a 0.
N PWA: manifest + icon set (rasterized from logo.svg) + iOS meta, and a
  service worker that is a pure network passthrough. That satisfies Chrome's
  installability criteria without ever serving a stale build - version.json
  polling stays the update path.

Verification: new tests/e2e/ui-fixes-15lmno.test.cjs (26 checks); inspector,
whats-new, themed-select, color-picker-15c, ui-fixes-15b, connect-states all
green; build green; svelte-check 421/62 held. panels + roadmap-13-notifications
failures verified PRE-EXISTING on clean HEAD (git stash).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…banner

Follow-up to the 15-L toast work (user review 2026-08-02).

- The two z-tiers were BOTH `absolute; top:65px; left:50%`, i.e. pinned to the
  same coordinates - approval cards physically covered the info toasts. They
  now live in ONE wrapper and stack. The wrapper centres with auto margins on
  purpose: a transform would create a stacking context and trap the children's
  z-index, which is what keeps approvals above modals and ordinary toasts
  below them.
- Connection requests wear the shared .tp-toast card (amber = needs a
  decision) instead of their own .cxreq chrome, keeping the role-coloured
  buttons. A rush of joiners folds like any other burst: 3 cards + a
  "+N more requests…" line into the drawer's Toasts tab.
- "Receiving objects" wears the card too (green, progress bar inside).
- STICKY prompts can no longer be evicted: only transient toasts are capped,
  the "+N more" count reflects just those, and sticky cards render last so
  they hold a stable spot. (A burst used to fold the restore prompt away.)
- Spectating is a MODE, not a notification: "Watching X" is now a pinned
  banner at the head of the stack - red framing, live dot, prominent Exit -
  so incoming toasts can never shove it (the reported jumping) and it stays
  centred. It is not `fixed` at the top because that collides with the
  Connect pill.
- Configure Scene: dropped the left accent bar from the active row (tint only).

Verification: ui-fixes-15lmno extended to 37 checks (stacking, request folding,
sticky survival, banner stability) - all green; svelte-check 421/62 held (the
dead .cxreq rules were removed rather than left as unused-selector warnings);
build green. Also fixed a STALE assertion in roadmap-13-notifications-notes
that expected "Connection request from peer" - text the UI never had; that
suite is down to its one pre-existing unread-badge failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- "Receiving objects: N/N" never disappeared: the old visibility machinery
  (showToast + toastStatus + a re-arming trigger()) fired its "done" branch on
  EVERY effect run once $loading emptied - and $objectsGroup pokes on every
  scene mutation, so the finished card kept re-showing forever. Replaced with
  one state and one rule: visible while a transfer runs, a 2.5s grace so the
  user sees "N/N", then gone until the next transfer starts.
- The share-or-stash prompt ("Share your N objects with X, or stash...") was a
  15s action toast with a 14s auto-share fallback - it could expire underneath
  the user and silently publish a scene they meant to stash. It is now a
  STICKY prompt: the joiner's handshake reply waits until it is answered;
  closing it with the X takes the safe old default (share).

e2e: ui-fixes-15lmno grows to 45 checks (progress shows/lingers/hides and is
NOT resurrected by scene mutations; the gate prompt is sticky, offers both
choices, resolves on answer); share-stash + sessions suites green (the
Share/Stash buttons match the shared card's action buttons). The spectator
banner-stability check now samples a STABLE baseline first - its own intro fly
can outlive a fixed wait under machine saturation. svelte-check 421/62 held.

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

Roadmap #15 batch K (viewport selection & outline) - the three reported
viewport symptoms shared one cause: the outline read `selectedObject`, and
selection state disagreed between paths.

- K1: the outline is driven by `selectedObjects` (the SET). `selectedObject`
  deliberately KEEPS the last object after deselect (the open inspector binds
  to it) so it could never signal "no outline" - deselecting now clears the
  outline. NOTE for future work: never read selectedObject for
  presence-of-selection; that is the documented sticky-primary trap.
- K2: every uuid in the set is TRAVERSED and each child mesh outlined -
  postprocessing's OutlineEffect only renders meshes, so adding a Group
  outlined nothing (the "selecting a group shows nothing" symptom) and only
  the primary of a multi-selection was ever added. Peer-locked uuids get the
  same traversal (a locked GROUP had the same blind spot).
- K3: createGeometry/createLight/createGroup and the import/animated-import
  paths now populate the selection SET alongside the sticky primary (they
  attach the gizmo, so the object IS selected - the set just never said so).
  Direct store writes, sceneStore only - no new import edges, no lock
  broadcast (replication unchanged).
- K4: Ctrl+D with an empty SET toasts "Nothing selected to duplicate" instead
  of resurrecting the sticky last object. Safe only now that K3 makes the set
  authoritative; the one legitimate empty-set-with-primary state - VIEWING a
  peer-locked object - still falls through and duplicates an editable copy.

Verification: new tests/e2e/viewport-selection.test.cjs (14 checks: create
outlines + populates the set, deselect clears, trio outlines 3, a GROUP
outlines its 3 child meshes, locked-group traversal, duplicate-after-create,
empty-duplicate toast, locked-view duplicate). Green: multi-select, undo
(two-peer), inspector, delete-object, ui-fixes-15b, ui-fixes-15lmno,
vr-mesh-undo. Build green; svelte-check dropped 421 -> 419 (the rework
removed two legacy errors; release.yml gate ratcheted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- $derived compares with === : a derived returning the same in-place-mutated
  THREE object never propagates - return a fresh SNAPSHOT (the O1 material
  readout bug; adding the store as a dependency alone does not fix it)
- color-picker 4.x: onInput PROP (no component events), one-way `hex` (the
  binding writes back and clobbers external writes), sky edits via editEnvSky
  (applyEnvironment reverts direct scene.background/fog writes)
- selection truth is the SET since #15-K (creation populates it, the outline
  traverses set members' meshes, empty-set Ctrl+D toasts; locked-VIEW is the
  one deliberate empty-set-with-primary state); #15-O click semantics (plain
  click selects, dbl-click/menu/pin opens properties)
- toast system: showToast vs sticky showInfoToast/dismissToastById, the
  .toasts-stack wrapper (auto-margin, transform would trap z-index), request
  cards + fold, the spectator MODE banner; anyModalOpen gate
- PWA: no-cache SW rule (never cache without wiring the version poll);
  dragWindow `resizable`
- e2e skill: baseline 419/62; port-shadow trap grew the "curl a source file,
  grep your symbol" proof + lane-ui moved to 5186; PEER_CONFIG reminder for
  two-peer on localhost (the Approve-button timeout); sticky-toast +
  __outlineDebug test notes
- peer-feature skill: sticky-prompt guidance (safe-default on dismiss)
- status entry for the #15 PRs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AlexZ005 AlexZ005 changed the title [fix] roadmap #15 second drop: properties UX, info toasts, stars, PWA [fix] roadmap #15 second drop: properties UX, toasts, stars, PWA, selection/outline (K) Aug 2, 2026
AlexZ005 and others added 2 commits August 3, 2026 05:26
Review call (15-P2 follow-up): the prompt exists because auto-deciding was
wrong, and a ✕ that silently picks Share is the same trap smaller. Sticky
info toasts gain an optional `noClose` flag - no ✕ on the card, no Dismiss
row in the drawer - and share-or-stash uses it. The restore prompt and the
first-run notice keep their ✕ (dismissing those has a sensible meaning).

e2e: ui-fixes-15lmno asserts the fork renders no ✕ (46 checks).

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

Roadmap #15-Q (user-approved layout + type-to-filter). Functionality is
unchanged - every action, guard and submenu survives; only structure,
metadata and styling change. All three menus (object / viewport / Selected)
now read as one family.

- HEADER strip: what the menu acts on - object name + type badge, or
  "N objects selected"; a peer lock shows "locked by X" here instead of
  scattered tooltips.
- SECTIONS: quiet uppercase labels (Edit / Physics & effects / Share) over
  thin rules; Delete sits last after a plain divider, red, with a Del hint.
- ICONS: lucide kebab names in the item data, resolved via ui/Icon.svelte
  (registry extended); a level reserves the icon column only when a sibling
  has one, so small data-driven menus (packs, node cards) are untouched.
- HINTS: shortcut hints (F, Ctrl+D, Del, Ctrl+Z/Y) render as a dimmed
  monospace right column instead of hiding in tooltips.
- HOVER-INTENT: submenus open after 120ms and close after 150ms - a diagonal
  pass no longer flickers them; entering the submenu cancels the close. The
  container deliberately keeps NO role attribute (the menu suites locate
  submenus by "fixed div without a role").
- TYPE-TO-FILTER (dense menus only, >=8 leaf actions): an autofocused row -
  typing flattens every leaf command-palette style with its submenu path
  ("Snapping > Snap to surface"), Enter runs the top hit, Esc clears then
  closes. Focus is deferred one frame: the parent's use:portal moves the menu
  into <body> AFTER the child action runs, and moving a focused element blurs
  it.
- Show/Hide REMOVED from the object menu (user call): hiding from the menu
  was a one-way door - you cannot right-click an invisible object to bring it
  back; the object list's eye toggle owns visibility.
- 'Selected >' label loses its baked-in arrow (the renderer draws the
  chevron).

Verification: new tests/e2e/context-menu-redesign.test.cjs (16 checks);
context-menu-v2, context-menu-overflow, multiselect-menu (two-peer),
add-menu(+cursor), object-search, packs-explorer, mobile-hud all green
(multiselect-menu updated: header/section rows carry no label). Build green;
svelte-check 419/62 held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AlexZ005 and others added 19 commits August 4, 2026 11:13
Roadmap #16 P6.

- LOCK LEAK: `applySelectionSet` broadcasts a `lock` message, but a deselect
  broadcast nothing - and `lock` only ever REPLACES the sender's set
  (lockGeometry ignores an empty list), so clicking empty space left the object
  highlighted and "locked by X" on every other peer until the deselecting peer
  happened to select something else. `broadcastSelectionRelease(uuids)` now
  sends one `unlock` per released uuid from all three release paths
  (deselectObject, applySelectionSet([]), and switching to a locked-VIEW).
  No new message type - `unlock`/`applyUnlock` already existed, and a peer that
  had asked us for control now gets it, which is right.
- "Selected" SUBMENU: the viewport menu gated it on `selectedObject`, which is
  STICKY by design (it keeps the last object so the open inspector has something
  to bind to), so the submenu lingered after a deselect. It now gates on the
  live selection SET, keeping the one legitimate empty-set state - VIEWING a
  peer-locked object - which retains its view-only actions.
- Configure Scene's active nav row drops the `inset 2px` accent bar (it read as
  a stray border); the tint + accent text stay.

Verification: new tests/e2e/deselect-unlock.test.cjs (two-peer, 9 checks:
lock replace, release on deselect / on applySelectionSet([]) / on locked-view,
sticky primary preserved) + viewport-selection extended with the menu gating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P1. The 15-Q filter row was always visible on dense menus and the
menu had no keyboard model beyond Enter; both are fixed without changing any
action.

- HIDDEN UNTIL TYPING: the filter input stays MOUNTED and FOCUSED (that is what
  swallows app shortcuts while a menu is open - the input-focus guard) but
  collapses to height 0 / opacity 0 until there is a query. Not display:none or
  hidden: the input must remain focusable to own the keyboard. Revealed, it is a
  normal menu ROW - same padding and font-size as an item (verified 26px vs 26px
  in the suite) with a tint + bottom rule instead of the old inset pill.
- EVERY menu now carries it, so the >=8-leaf threshold is gone: small menus show
  no filter row (it is collapsed) but still get arrow navigation.
- KEYBOARD: Up/Down walk the current level skipping disabled rows (wrapping),
  Enter runs the highlighted row or OPENS its submenu and moves the cursor
  inside, Esc unwinds one step at a time (query -> open submenu -> close the
  menu). Left/Right stay with the textbox caret as asked; with an EMPTY box (no
  caret to move) they double as open/close submenu.
- ONE highlight for mouse and keyboard: hovering a row moves the cursor, so
  Enter always acts on the row you are looking at. The open submenu keeps its
  150ms close-intent delay while the cursor moves - that delay is what lets you
  reach a submenu diagonally, so pointer hover deliberately does NOT close it
  immediately (hence openPath for rendering vs navPath for the cursor).
- Clicking a row no longer steals focus from the filter (mousedown default
  prevented) - opening a submenu by click used to hand the keyboard back to the
  app's global shortcuts.
- Submenus now anchor to their own row element instead of a captured mouse rect,
  so a keyboard-opened submenu places itself exactly like a hovered one (and the
  stale-rect risk is gone).
- New `$lib/menuFilter.js` holds the flatten + ranking (label prefix > path
  prefix > substring > fuzzy subsequence, 40-row cap) so there is ONE matcher -
  P2 retires the node editor's private search box onto it.

Verification: context-menu-redesign extended (hidden-then-revealed row, row
height parity, Up/Down/Enter/Esc, highlight lands INSIDE the submenu);
context-menu-v2, context-menu-overflow, add-menu-cursor, object-search,
mobile-hud green. add-menu fails at its documented pre-existing flake
(right-tap does not open the menu, line 105) - identical with these changes
stashed. Build green, svelte-check 419/62 held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P2. The flow pane menu carried its OWN search popup
(#node-search-box, a separate fixed box that replaced the menu at the same
coordinates, with its own ranking, its own arrow-key handling and its own
drag-to-move) and, since 15-Q, the shared filter row on top of it - visibly
duplicated. There is now ONE implementation.

- Deleted the popup and everything only it needed: openSearch, searchEntries,
  searchResults, its private subsequence ranker, pickResult, onSearchKeydown,
  scrollHighlightIntoView, savedMenu, the window keydown that jumped into
  search, and the rightDragMove/inputContextMenu imports (the box's right-drag
  move goes with it - the menu opens at the click).
- "Search nodes..." stays as the first row but is now a `revealFilter` item: it
  reveals the shared filter row WITHOUT closing the menu, so the grouped list
  stays as the browse view it always was. Typing anywhere in the menu does the
  same (the filter input is always focused), which is how it already behaved.
- Matches render as "Group > Node" - the same information the old box showed as
  "group . label" - ranked by the shared matcher, with arrow-key navigation and
  shortcut interception for free.
- Esc unwinds one step at a time: query -> revealed filter -> submenu -> close.

FIX found by the suite: a menu opened near the bottom edge (flipped up) grew
straight off the screen when filtering changed its height, because `place()` only
ran on open and on window resize. It now re-places on any size change via a
ResizeObserver, guarded against a maxHeight write loop.

Also repaired two STALE assertions in node-search (a known-failing suite): they
asserted context menus never scroll and are never height-capped, which the later
cap-and-scroll change deliberately reversed - the suite is fully green now.

Verification: node-search rewritten against the shared filter (20 checks, ALL
PASS - previously on the known-failure list); context-menu-redesign,
context-menu-v2, context-menu-overflow, multiselect-menu, object-search,
packs-explorer green. Build green, svelte-check 419/62 held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P3.

CONFIGURE SCENE gains two sections (LOCAL per-device prefs, like the clip planes
and the render mode that already live there - a grid is a view aid, not scene
content, so nothing here replicates):
- GRID: show, cell size, "match snapping step" (the drawn cell tracks the
  translate snap step, so the grid you see is the grid objects land on), major
  line every N cells, cell + major colours, fade auto/fixed with radius and edge
  softness, infinite vs a finite extent, follow-camera, and origin axes. Reset
  restores the defaults. New `$lib/gridSettings.js` holds it (cameraClip idiom,
  unknown keys fall back to defaults so old payloads keep working);
  extensions/Grid.svelte reads it and keeps the I4 no-lerp auto-fade math.
  The axes helper renders in the world rig, never under objectsGroup - anything
  there would enter GLTF sync and duplicate for peers.
- SNAPPING: enable, position/rotation/scale steps as preset chips PLUS a free
  number each (custom steps were menu-only before), and surface snap.

CONTEXT MENU: new `checked` item flag renders the active choice of a group as
BOLD WHITE on a brand-tinted pill, replacing the '● ' label prefix that shifted
labels sideways as it appeared. The text itself is deliberately not accent-tinted:
the app's accent is a salmon (#fe795d), which would sit too close to the red
`danger` rows in the same menu.

SNAPPING SUBMENU is sectioned (Position / Rotation / Scale / Surface) instead of
one flat run of ten steps, the parent row's hint column shows the live setup
("0.5 · 15° · 0.1", or "off"), and a last row jumps to Configure Scene for custom
values. The View submenu gets the same treatment (Show grid as a checked toggle +
a grid-settings jump).

Verification: new tests/e2e/grid-snapping.test.cjs (15 checks: sections, checked
marks, no bullets, live hint, click-applies-and-mark-follows, panel toggles reach
the store, AxesHelper outside objectsGroup, match-snap-step, persistence across a
reload, custom step); context-menu-redesign, context-menu-overflow, vr-snap-menu
green. Build green, svelte-check 419/62 held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P4.

BOOKMARKS were five anonymous slots where a sixth save silently pushed the
oldest out, labelled by save TIME, storing only position + target.
- Unlimited NAMED entries (default "View N"), each storing the LENS it was
  saved with (FOV + near/far); recall restores the whole look, near/far through
  cameraClip so the orbit zoom clamp stays paired with the far plane.
- Management in Configure Scene: rename inline, re-shoot from the current view
  (keeps the name), reorder (order IS the Shift+N mapping), delete, plus Save
  current view. Shift+1..5 still recall the first five.
- Legacy payloads normalize on read: no name -> "View N", no lens -> recall
  leaves your lens alone. The viewport menu lists them by name with a ⇧N hint
  and a "Manage saved views…" jump.

CAMERA SETTINGS were a "Camera lens" sub-label buried inside the View section,
and the orbit feel was hardcoded in Scene.svelte's <OrbitControls>. Now one
Camera section: lens presets + FOV, near/far clip, rotate/zoom/pan speed,
damped orbiting, invert vertical orbit (the sign rides rotateSpeed - three has
no flag), Frame scene (fit everything along the current view direction), Reset
view (the mount defaults), Reset feel, and the saved-view manager. Orbit prefs
are LOCAL and re-apply whenever the controls remount (VR exit, spectator mode).

Verification: new tests/e2e/camera-bookmarks.test.cjs (14 checks: legacy
normalization, lens capture + restore, no cap, panel rename/overwrite/reorder/
delete, Shift+2, orbit prefs reach the live controls + reset); camera-clipping
and roadmap-13-viewport-camera green. Build green, svelte-check 419/62 held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 P5. Perspective and orthographic CAMERA objects you can place, aim,
look through and shoot from.

DATA: a camera is a normal replicated marker MESH (`/create Camera` /
`/create CameraOrtho`, body from customGeometries - a box with a lens cone down
-Z, three's camera convention) whose settings live on `userData.camera`. That is
the userData.physics trick, so replication, undo, sessions, autosave, prefabs
and GLTF extras all work with ZERO new plumbing, and peers agree because the
same /create stamps the same deterministic defaults. Markers are never physics
bodies and never cast shadows. Add > Camera > Perspective / Orthographic.

WRITES go through `setCameraFor(uuid, patch)` - one props history entry, one
`objectParameters` message, one poke that rebuilds viz + preview (the
setPhysicsFor precedent); the 'props' history kind and the objectParameters
applier both learned `camera`.

FRUSTUM VIZ (`cameraHelpers.js`, the colliderHelpers pattern): a wireframe
frustum per camera, built from the spec, living at the SCENE ROOT so it stays
out of GLTF sync, following its marker per frame, hidden in wireframe view mode
and for the camera you are currently inside. On by default with a local
`showCameraFrustums` toggle.

PREVIEW is a TRUE camera swap: `CameraPreview.svelte` mounts a real
Perspective/OrthographicCamera as threlte's default, so an ortho camera renders
genuinely orthographic and fov/near/far are exact. Two things this needed:
- Outline.svelte re-points its EffectComposer (setMainCamera + N8AO's own camera
  reference) whenever the active camera changes - every pass baked
  `camera.current` at construction, so without this you keep seeing the editor
  view. It tracks `$camera` (the STORE): threlte's CurrentWritable exposes
  `.current` as a plain property, so reading it inside an $effect registers no
  dependency and the effect runs exactly once.
- the previewed marker hides itself locally (you stand inside its body mesh
  otherwise), the way spectator mode hides the avatar you watch from.

CONTROL (banner button): hands the camera to the normal viewport navigation -
WASD + mouse, because the preview camera owns the OrbitControls and Scene's
existing per-frame nav call drives whatever is active. Every controlled frame
writes the pose back onto the marker (throttled ~10Hz `move` broadcasts), and
ending control leaves exactly ONE undo entry for the whole ride (the possess
"one undo per ride" precedent).

Also: framing guide (letterbox bars for 16:9 / 4:3 / 1:1 / 2.39:1 while
previewing), Capture (renders one offscreen frame at the framing aspect and
downloads it), Set from current view / Align view to camera, an Inspector Camera
section, the two headline actions in the object context menu, and REPLICATED
preview presence (`campreview`) - the Users popover shows "previewing <camera>"
per peer with a click to join, cleared on disconnect and shared on handshake.

Verification: new tests/e2e/camera-objects.test.cjs (27 checks, two-peer:
defaults, replication, write+undo replication, scene-root frustums that follow,
ortho preview really orthographic, banner, own-frustum suppression, peers seeing
the preview, flying writes the marker + replicates + one undo entry, exit
restores your camera, Capture at 640x360). collider-viz, inspector,
object-properties, viewport-selection, roadmap-13-viewport-camera green;
view-mode fails a pre-existing shadow-catcher check (identical with these
changes stashed). Build green, svelte-check 419/62 held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs in 16-P5's Control mode, both reported.

1. Pressing Control SNAPPED the view. A fresh OrbitControls targets the world
   origin and its update() ends with camera.lookAt(target), so mounting it
   rotated the preview camera the moment Control was pressed. The controls are
   now seated behind the camera first - the target goes on the camera's own
   forward axis, making that lookAt a no-op - and because OrbitControls already
   ran one update() during construction (before any effect could run), the camera
   is re-synced from the MARKER first, so the mount rotation cannot get baked into
   the pose. Measured drift is now 0.0000.

2. After exiting a preview, dragging the transform gizmo also ORBITED the camera.
   Both sets of controls were bound to the same `orbitControls` store, and
   threlte clears a bound ref on unmount - so the preview's teardown could land
   after the editor's controls remounted and leave the store EMPTY. Every
   suppression site (`$orbitControls.enabled = false` on gizmo/marquee drags) then
   wrote to nothing, for the rest of the session. The preview camera now publishes
   its own `previewOrbit` store, a derived `activeOrbit` says which controls are
   actually steering, and Scene routes its six suppression sites plus the
   per-frame navigation call through it.

Verification: new tests/e2e/camera-preview-control.test.cjs (9 checks: no drift
on Control, target seated ahead, ownership handover, editor controls restored
live and attached to the active camera, suppression reaches them, two cycles stay
clean). svelte-check 419/62 held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 second drop, batch Q1 — three reported annoyances in the menu.

- SUBMENU CURSOR is remembered per level: stepping out of a submenu lands back on
  the row you came from instead of jumping to the top. Pointer hovers record the
  same memory, so mouse and keyboard agree about where you were.
- SEARCH IS STICKY. Deleting the query used to snap you back to the grouped menu
  mid-thought; now the box stays and the list simply widens to EVERY action
  (capped at 200 rows, scrolling) - the browse list the retired node-search box
  used to be. Only Esc leaves search, so Esc now unwinds query -> search ->
  submenu -> menu, one step per press.
- THE MENU KEEPS ITS ANCHOR. It picked the roomier side of the click on EVERY
  re-place, so a long match list re-decided and teleported the menu to the top of
  the screen. The side is chosen once, the anchored edge stays on the click, and
  anything that doesn't fit is capped and scrolls (verified: top 140 -> 140 while
  the list grows from 2 rows to 59).
- "Search nodes…" no longer appears among its own search results (`revealFilter`
  rows are excluded from the leaf collection).

Verification: context-menu-redesign +9 checks (sticky search, anchor stability,
internal scroll, three-step Esc, cursor memory into and out of a mid-list
submenu); node-search updated for the new Esc semantics +1 check for the
exclusion; context-menu-v2 and context-menu-overflow green. svelte-check 419/62.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ckboxes

Roadmap #16 second drop, batch Q2 — five reported items.

- DEEP LINKS: "More snapping settings…", "Grid & axes settings…" and "Manage
  saved views…" now open Configure Scene, EXPAND the named section (even one you
  had collapsed) and scroll to it. They went through `showSidebar('scene')`,
  which TOGGLES - so clicking one while the panel was already open closed it. New
  `openSceneSection(label)` only ever opens, and Section.svelte watches an
  `inspectorScrollTo` store, expands, scrolls and clears it.
- GRID FOLLOW is a three-way choice: Off / Look-at / Camera. "Follow the camera"
  tracked your POSITION, which is the wrong thing while you are looking somewhere
  else; Look-at centres the grid under the orbit target. Both modes stay
  HORIZONTAL (y = 0 - it is the ground plane, not a flying sheet) and snap the
  centre to whole cells so the lines keep agreeing with world coordinates instead
  of sliding under objects. The old boolean migrates to Look-at.
- SNAPPING STEPS: Scale gained the missing 0.25 (and Rotation 90°), and a value
  typed in Configure Scene now joins the presets in the menu, sorted and marked
  as active - custom steps used to be invisible and unreachable there.
- PHYSICS CHECKBOXES: the Sensor, freeze-axis and show-collider boxes were raw
  <input type="checkbox"> rendering as native controls; they use the themed
  component now, like everything around them.
- ADD opens the new object's properties, even when another panel was showing.

Verification: new tests/e2e/panel-deeplinks.test.cjs (16 checks: open from
closed, expand a collapsed section, no-close on a second link, look-at follow
snapped to cells with y pinned, Off recentres, 0.25 + custom step present and
marked, themed checkbox, Add switches the panel); grid-snapping,
add-menu-cursor, physics-colliders, collider-viz, inspector green. Build green,
svelte-check 419/62.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 second drop, batch Q3.

The transform rows had a drag-to-scrub control; every other number was a plain
<input type="number"> that only committed on Enter or blur - so its arrows looked
like they did nothing, and the two behaved differently. DragRow is now THE numeric
field and the sliders' boxes, the snapping steps, the clip planes, render order
and the particle offsets all use it:

  drag horizontally   scrub (Shift = fine, Ctrl = snap)
  click / type        applies LIVE, no Enter needed
  ArrowUp / Down      one MINOR unit (0.01 at 2 decimals), Ctrl x10, Shift x100;
                      integer fields step by 1 / 10 / 100
  Esc                 back to the value you focused it with

It is a real <input> the whole time instead of swapping a button for a box: the
caret is always there, ids keep working, and touch gets the numeric keypad
(inputmode). type="text" on purpose - a native number spinner would fight our own
arrow steps.

TWO DELEGATION TRAPS, both found by the suites: Svelte's `onkeydown`/`onpointerdown`
attributes are DELEGATED, running only once the event reaches the app root, and
the panels this field lives in swallow both on the way up (the drawer's drag/resize
wiring eats pointerdown, the flowbite dialog eats Escape). Esc-to-revert did
nothing, and a drag whose pointerdown never arrived jumped the value by the
pointer's absolute X (+22 instead of +2). Both are direct element listeners now.
The pointer trio rides the WRAPPER so a scrub can start on the axis label too,
and a click there focuses the input for typing.

Verification: new tests/e2e/number-fields.test.cjs (13 checks: live typing, the
three arrow steps, integer stepping, Esc revert, drag scrub, the same rules in a
slider box); inspector updated for the always-input field (its drag check now
lands on 2.0 instead of 22.9); camera-clipping, grid-snapping, camera-bookmarks,
object-properties, panel-deeplinks, geometry-params, particles(+impact),
environment(+v2) green. Build green, svelte-check 419/62.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Roadmap #16 second drop, batch Q4.

Selecting a camera object now shows a small LIVE preview bottom-right. It is not
a second WebGL context (that duplicates every texture and geometry on the GPU):
Outline.svelte draws one extra SCISSORED viewport of the same renderer after the
composer pass, into the rect the window publishes - so the DOM part is pure chrome
and the cost is one viewport draw, only while a camera is selected. gl clears
respect the scissor box, so the inset clears just itself.

- Right-drag moves it (left stays free for the viewport), touch-HOLD then drag;
  it clamps fully on screen and parks bottom-right, LEFT of an open side panel so
  the two never overlap.
- Inside the frame the camera's own marker and the frustum lines are hidden for
  that draw - you would otherwise be looking at the inside of its body.
- A ⤢ button jumps to the full-screen preview; ✕ turns the window off for that
  camera. On by default, with "Preview window while selected" in Camera
  properties.
- CAPTURE moved onto its own row below Preview / Set from view / Align view, with
  a camera icon and a note that it saves a PNG at the framing aspect.

Two familiar traps, both caught by the suite: `pipTarget` reads the selection SET
(the sticky `selectedObject` kept the window open after a deselect) and takes
`objectsGroup` as a dependency (the `pip` flag lives on userData, and THREE trees
are not reactive - the post-write poke is the only signal a derived store gets).

Verification: new tests/e2e/camera-pip.test.cjs (18 checks: targeting, published
rect matches the frame, framing aspect, parking clear of the panel, right-drag +
clamping, hidden during a full preview and after deselect, the per-camera switch,
and the DOM->gl y-flip); camera-objects (banner check hardened against its fly
transition), camera-preview-control, viewport-selection, collider-viz green.
view-mode still fails its pre-existing shadow-catcher check. Build green,
svelte-check 419/62.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… PiP polish

Roadmap #16 second drop, batch Q5.

THE BUG, properly this time. Nothing in the app ever disabled orbiting during a
gizmo drag: threlte's TransformControls disables whatever controls sit in ITS OWN
context slot, and a camera preview unmounts + remounts the editor's OrbitControls,
after which that slot points at an instance which no longer drives the view. So
dragging an object also spun the camera. The `dragging-changed` hook now does the
suppression itself, first thing and for every gizmo target, writing through
`activeOrbit` - instance-proof. The preview's own controls are disposed when
Control ends (three keeps DOM listeners on a merely-dropped instance).
Reproduced end to end first: the SAME real drag on the REAL gizmo arrow rotated
the view 0.21 after a preview cycle and 0.00 before it; now both read 0.00000 and
the object still moves the identical 1.04 units.

MENU placement, rewritten to the rules you gave: open AT the cursor preferring
downward; when the content does not fit below, shift the WHOLE menu up just far
enough that its bottom stays inside (no flipping to the other side of the
pointer); a scrollbar appears only when the content is taller than the window.
SEARCHING no longer moves anything - the menu keeps the top it opened with, the
list gets a sensible height instead of unfolding to the floor, and a corner grip
resizes it while searching.

GRID follow: 'camera' now uses threlte's own followCamera, which keeps the shader
drawing lines at WORLD positions - smooth while you pan. Only 'look-at' snaps the
centre to whole cells, which is what you want when it is locked to an object.

DEEP LINKS scroll the container with the sticky header's height as an offset, so
the section label lands BELOW "Filter properties" instead of under it, and
"Manage saved views…" targets the SAVED VIEWS sub-heading (`data-anchor`) rather
than the top of the Camera section.

SNAP STEPS are clean numbers: the shared numeric field quantizes a drag to its own
precision (a scrub used to produce 0.7999999999999999), the position/scale snap
fields are 2-decimal, and every menu label and hint prints through one formatter.

CAMERA PiP: the title bar drags with the LEFT button too (the body keeps
right-drag so a stray left-click cannot move it), the transform gizmo is hidden
for the inset draw - it sat on the lens as a giant coloured blob, and hiding the
controls was not enough because three keeps the gizmo's visuals in a separate
helper object - and the parked window keeps clear of the round HUD buttons.

Verification: new tests/e2e/gizmo-orbit-leak.test.cjs (9 checks, A/B real-mouse
gizmo drag before and after a preview cycle, so normal left-drag orbiting can
never be mistaken for the bug); context-menu-redesign +5 (cursor-anchored open,
shift-up with no scrollbar, sticky top while searching, bounded height, grip);
panel-deeplinks +4 (three headers clear the sticky header, clean snap hint);
camera-pip +4 (left-drag on the bar, body left-drag inert, gizmo restored after
each draw). camera-objects, camera-preview-control, node-search, grid-snapping,
number-fields, inspector all green. Build green, svelte-check 419/62.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ght, PiP layer

Roadmap #16 second drop, batch Q6 — the six follow-ups.

- LOOK-AT POINT no longer jumps to the world origin when you exit a preview. The
  editor's OrbitControls unmount while the preview owns the view, and the pair that
  mounts on exit starts from the default target (0, 1.5, 0). The target is now
  snapshotted when the preview takes over and copied back onto the FRESH instance
  once it appears (the store still holds the disposed one for a beat). Verified:
  (6, 2, -3) in, (6, 2, -3) out.
- SNAPPING ROWS align: label | chips | field in one grid, so the three numeric
  boxes share a right edge no matter how many preset chips a row has (4 / 4 / 3),
  and the chips are tightened enough to stay on ONE line in the 320px panel.
- NO CARET during a scrub: pointerdown's default is blocked so a drag can never
  place or drag the caret through the digits (you could watch it skate about),
  selection is suppressed while scrubbing, and focus is handed over on RELEASE
  instead — which is also what makes click-to-type work.
- THE SEARCH LIST REMEMBERS the height you drag it to, per menu kind: the viewport
  menu, the node editor and the object menu each keep their own (`sizeKey` prop →
  `ctx:searchHeight:<kind>` locally).
- DEEP LINKS are deterministic: measure → scroll → re-measure → correct, with the
  scroller found by real SCROLLABILITY rather than class names, an instant scroll
  (a `smooth` one was being cancelled by the reflow of the section it had just
  expanded) and retries while the just-expanded content lays out. All three land
  8-20px under the sticky filter header even when the panel starts scrolled to the
  bottom. NOTE: the previous check passed vacuously — it only asked for "somewhere
  below the header", which is true without any scrolling at all.
- CAMERA PiP frame drops to z-index 2: it is a viewport overlay, not chrome, so
  panels and HUD now cover it (verified pip z 2 vs panel z 10).

Verification: camera-preview-control +2 (look-at round trip), panel-deeplinks +3
(tight landing band from a bottom-scrolled panel), context-menu-redesign +3
(height persisted, restored on reopen, per-kind keys), number-fields +4 (scrub
reports itself, no focus, no selection, click still focuses), camera-pip +2
(layer below the panel). camera-objects, gizmo-orbit-leak, grid-snapping,
node-search, inspector, object-properties green — 11 suites in one run. Build
green, svelte-check 419/62.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e2e-verify gains an ASSERTION DISCIPLINE section, because the expensive failures in
this roadmap were not broken code but checks that could not fail:
- position/layout asserts need a tight band AND a start state that forces the
  behaviour (the deep-link check passed while nothing scrolled, because most
  sections were collapsed and the label was trivially "below the header");
- isolate a regression with an A/B of the same gesture, and assert the gesture did
  its job, so a no-op cannot read as a pass;
- match the metric to the gesture (OrbitControls: LEFT rotates, RIGHT pans - a
  quaternion check on a right-drag reads 0.0000 forever);
- when a check says pass and the user says fail, re-read the check first.

Also in e2e-verify: HMR churn makes runs LIE (a page loaded while vite
re-transforms edited modules is half-mounted - three runs "proved" a working
feature dead); component-side debug hooks as the cure (__outlineDebug,
__cameraPreviewDebug, cameraHelpersDebug, pipDebug); the real-mouse GIZMO drag
recipe (find the picker via getHelper().traverse + project, never guess pixels, and
remember three keeps the gizmo visuals in that helper); panels scroll so
scrollIntoViewIfNeeded before boundingBox; section headers are buttons with a -/+
glyph so match with startsWith; the new numeric-field (.dn-wrap/.dn-input) and
context-menu (.ctx-filter-input/.ctx-match/.ctx-grip/[data-ctx-active]) selectors;
the new __stores keys; and node-search is OFF the known-failing list (two of its
assertions were asserting the old no-scroll contract).

peer-feature gains "the cheapest replicated feature: put it on userData" - the
full recipe behind userData.camera/physics/particles (deterministic defaults at
creation, ONE write path with props history + objectParameters + poke, the applier
case that a two-peer suite must cover, scene-root visuals), plus PRESENCE-style
state (campreview: one message on change, per-peer map, cleanup in BOTH teardown
paths, late joiners piggyback an existing handshake reply) and the lock-RELEASE
trap (a `lock` message only replaces the sender's set, so letting go needs explicit
`unlock` per uuid).

CLAUDE.md: the Configure-Scene deep-link seam (openSceneSection/inspectorScrollTo/
data-anchor and why smooth scrolling and class-name scroller lookups fail), the
remembered per-kind menu search height (sizeKey), revealFilter rows, and the PiP
frame's deliberate z-index 2 (a viewport overlay, so chrome covers it).

svelte-check 419/62 unchanged (docs only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[feat] roadmap #16: menus, grid & scene cameras (P1-P6)
[feat] context menu redesign: header, sections, icons, hints, type-to-filter
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.

1 participant