Preview Inspect Mode (draft for internal review) - #188
Draft
erikfrerejean wants to merge 61 commits into
Draft
Conversation
The holder template lives at templates/WeDevelop/Grid/Model/GridElement_holder.ss, not under an Includes/ subdirectory. Passing 'type' => 'Includes' triggers a MissingTemplateException, so drop it and let SSViewer resolve the class-style path directly.
…drop unused import
…ants, strict resolvePath
`InspectContext.test.tsx` and `InspectBridgeHost.test.tsx` each redefined the same Map-backed `createMockLocalStorage` / `installMockLocalStorage` pair. Drift between the two copies would be a silent correctness bug (tests pass locally, disagree on jsdom behaviour). Pull the helpers into `client/src/testing/mockLocalStorage.ts` so both test files import from the same source.
Preview Inspect Mode highlights elements by attaching the .grid-inspect-target class (outline + background) to the [data-grid-element-id] wrapper and by reading its getBoundingClientRect(). A display:contents element has no principal box, so both CSS painting and the rect read produce nothing -- container elements (Section/Row/Column) were visible because their holders attach the attribute to their own real <section>/<div>, but content elements, which fall through to GridElement_holder.ss, were invisible to the inspector. Removing style="display:contents" turns the wrapper into a plain block-level <div>. Column holders already wrap child output in a <div>, so the extra box does not change flex/grid participation. Adds three integration tests to GridHolderMarkupTest: - testContentElementForTemplateEmitsDataGridElementId confirms forTemplate() resolves to GridElement_holder for ContentElement. - testContentElementHolderIsNotDisplayContents is the regression guard for the fix. - testColumnLoopRendersContentElementWithWrapper exercises the realistic Column -> $Elements -> $Me -> forTemplate() flow.
- useCallback on hover handlers was cargo-culted; they're spread onto
native DOM elements where React uses the latest reference directly.
- ElementCard's {...hoverBinding} now precedes onClick so an explicit
event handler on the anchor always wins over the spread default.
(Today hoverBinding only carries optional mouse handlers, but the
ordering convention matches the other three block components and
avoids a silent override trap for future readers.)
When the editor re-mounts after a page reload with `enabled=true` hydrated from localStorage, the preview iframe may have already booted and emitted its startup `ready` before the parent's subscribe listener attached. That lost `ready` left `readyRef=false`, the queued activate never flushed, and inspect stayed inactive until the user manually toggled off/on. The existing echo-on-inbound fix only helps when the parent sends something outbound to trigger an echo — but the post-reload path sent nothing when queuing. Now we also send a `deactivate` probe in the queuing branch. The preview echoes `ready`, `readyRef` flips true, and the queued activate flushes. A no-op deactivate against an already- inactive inspector has no side effect.
…ransitions React's synthetic onMouseEnter on a wrapper does not refire when the cursor re-enters the wrapper from a descendant, so after hovering a column's ElementCard child and moving back up to the column's own header the column never re-entered hover state — the preview stayed highlighted on the child. Replace the per-element onMouseEnter/Leave with a single document-level mousemove listener that resolves the innermost [data-node-id] ancestor via closest() and drives setEditorHover from that — matching how the preview-side inspector already hit-tests. - InspectHoverDelegate: new component, one delegated listener, drag-suppression via ref, mouseleave on documentElement to clear. - useInspectHoverBinding: stripped to just data-node-id. - GridEditor: mount <InspectHoverDelegate /> inside DragContext in both editable and readonly branches. - E2E: new Journey B step exercises the child→parent regression. - Unit: 10-case InspectHoverDelegate spec covers hit-test depth, drag suppression, mouseleave clear, dedupe, and disabled short-circuit.
…apsed container When a preview-side hover resolves to an element whose editor-tree ancestors are collapsed, the halo previously just landed on the nearest visible ancestor (usually the Section header) with no indication of the full path — the admin couldn't tell where in the tree the element actually lived without hunting through the collapse state. This adds two coordinated signals: 1. Editor-side floating breadcrumb showing the complete Section › Row › Column › Element trail, rendered near the halo (mirroring the preview-side breadcrumb for symmetry). Segments whose containers are currently collapsed are dimmed + italicized + carry a subtle corner glyph, so the admin can see at a glance which ancestors they'd need to expand to reach the element. 2. Indirect halo variant (dashed, softer colour) when the halo lands on a proxy ancestor rather than the real target — distinguishes 'this is the element' from 'the element is hidden inside this'. Implementation: - ancestry.ts: new resolveAncestorPath returning full PathSegment[] (id + title + type) so consumers don't look nodes back up in the tree on every render. - InspectContext: preview hover state carries the path. - InspectBridgeHost: resolves and passes path alongside ancestorIds. - InspectOverlay: rewritten to render halo + breadcrumb together; introduces rect-based visibility check (findVisibleById) so CSS-collapsed descendants count as hidden, not just absent ones. - inspect.scss: breadcrumb surface + --indirect halo variant. Tests: - Unit: 3 new ancestry tests for resolveAncestorPath; 4 new InspectOverlay breadcrumb tests (full trail render, dimmed hidden segments, target marker, empty-path defensive null). - E2E: Journey A gains a step that collapses Hero section, hovers the paragraph in the preview, and asserts the halo becomes --indirect while the breadcrumb flags Row/Column/Element as data-segment-hidden.
| const { result } = renderHook(() => useInspect(), { wrapper }); | ||
| localStorage.setItem(STORAGE_KEY, 'true'); | ||
| act(() => { | ||
| window.dispatchEvent(new StorageEvent('storage', { key: STORAGE_KEY })); |
| const { result } = renderHook(() => useInspect(), { wrapper }); | ||
| localStorage.setItem(STORAGE_KEY, 'true'); | ||
| act(() => { | ||
| window.dispatchEvent(new StorageEvent('storage', { key: 'something-else' })); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in Inspect Mode that bridges the CMS grid editor and the rendered preview iframe, in both directions:
Section › Row › Column › Elementpath with collapsed segments dimmed — so admins never lose orientation on large grids.The toggle lives in the editor header next to the viewport switcher, persists across reloads via
localStorage(grid:inspect-mode), and is a user preference (not per-page).Status: draft — opened so the work is saved while we internally debate whether to ship.
Architecture at a glance
Three loosely-coupled pieces talking over
postMessage:data-grid-element-id="\$ID"+data-grid-element-title="\$Title.ATT". For content elements that don't have their own holder template,GridElement_holder.sssupplies a plain<div>wrapper carrying the attributes.client/src/preview/): vanilla-TS inspector loaded only on CMS-preview renders via a newPreviewInspectorExtension. Single delegated mousemove handler, hand-rolled message validators, no Zod (< 4 KB gz).client/src/inspect/): React context +InspectBridgeHost(handshake, debounce, message wiring),InspectHoverDelegate(document-level delegated mousemove so nested bindings compose correctly),InspectOverlay(halo + breadcrumb rendering),InspectToggle(the button).Full spec & plan live in
docs/superpowers/(untracked per repo convention).Notable engineering decisions
onMouseEnterdoesn't refire when the cursor re-enters a wrapper from one of its descendants — breaking column hover when moving from a child content element back to the column header. Swapped for a singledocument-levelmousemovelistener that resolves the innermost[data-node-id]viaclosest()on every move, matching the preview side's hit-test model.grid-inspect:readyon every inbound control message, so the editor's firstactivate/deactivatealways elicits a fresh ready on its now-live listener — closes the window where preview's boot-time ready fires before the editor'suseEffectsubscribe attaches.SectionBlockkeeps descendants mounted when collapsed (CSS-only hide), soquerySelectoralone falsely claims they're visible.findVisibleByIdusesgetBoundingClientRect()as the source of truth, correctly distinguishing collapsed vs expanded.display: contentsfor layout invisibility but the inspector's paint depends on a non-zero rect, so content elements became invisible to the halo. Dropped.InspectBridgeHostinstalls aMutationObserverondocument.bodyto re-wire theloadlistener whenever the preview iframe appears/disappears (the CMS mounts it lazily in some flows).Test coverage
PreviewInspectorExtensionon both preview and public requests, template markup contract.Known open discussion
This PR is intentionally draft — the team is internally debating whether to ship. Questions still on the table:
--indirecthalo also offer an "Expand to reveal" affordance?Test plan
preview.js+preview.cssdo NOT load on public (non-CMS) page renders.