Skip to content

feat: composed-view ribbon and site-editor slug deep links - #696

Merged
ViewFromTheBox merged 2 commits into
feature/templates-previewfrom
feature/623-composed-ribbon-and-site-editor-deep-link
Aug 4, 2026
Merged

ViewFromTheBox merged 2 commits into
feature/templates-previewfrom
feature/623-composed-ribbon-and-site-editor-deep-link

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Aug 4, 2026 •

Copy link
Copy Markdown
Member

Description

Adds the composed view's locked-canvas ribbon (#623) and the site-editor
deep-link surface its CTA targets (#625). Shipped together because the
ribbon's Edit template ↗ button is the deep link's first consumer —
splitting them would land a button pointing at a route that doesn't
resolve yet.

Closes: #623, #625

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #623, #625 (both under parent #618, milestone v1.6)

Motivation and Context

The composed view renders a template's header and footer as inert blocks
around the author's content. Until now nothing on that surface explained
what the template was or offered a way to go edit it — an author reading
a header they can't click had no next step.

The ribbon is that step, and it is deliberately the only one: per-part
chrome would put four or five competing controls on a surface whose whole
point is to read like the finished page.

The CTA needed somewhere to land. Path routes address entities by id, and
the applied-template endpoint returns only a slug, so the site editor
gained a slug-keyed query-string entry point rather than the router
learning a second addressing mode.

Changes Made

#623 — composed-view ribbon

  • New editor/composed-view/composed-view-ribbon.tsx + .css — sticky bar rendered inside the canvas iframe so it scrolls with the preview and reads as part of the page, not another editor toolbar
  • editor/canvas-styles.ts — ribbon CSS injected ?inline into the iframe (the blocks/*/*.css glob doesn't reach composed-view/)
  • editor/editor-canvas.tsx — CanvasChrome gained templateName / templateSlug; the ribbon mounts only when chrome is present, so bare-content mode never renders it
  • editor/editor-app.tsx — forwards the name/slug it had already resolved
  • Fallback template (Composed view: missing/invalid template fallback + toast #624) shows its name with no CTA — that tree is a client-side constant with no site-editor record to link to

#625 — site-editor deep link

  • New site-editor/deep-link.ts — parseDeepLink() / buildTemplateDeepLink(), both pure. The ribbon builds its href with the builder so the two sides of the contract move together
  • New site-editor/use-deep-link.ts — resolves slug → id via the templates list endpoint on mount, then navigates. Unknown slug or failed lookup lands on the Templates index with a toast
  • site-editor/site-editor-app.tsx — wrapped in ToastProvider (mirrors the post editor) and calls the hook
  • docs/site-editor.md — new "Deep links by slug" section documenting the parameter table and guarantees

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (Darwin 27.0.0)
  • Browser (if applicable): verified in-browser against the ArtisanPack UI dev app
  • PHP Version: 8.4.3
  • Project Version: v1.6 milestone (branched off feature/templates-preview)

Tests Performed:

  1. Composed view on a post with a template — ribbon renders, sticks to the top of the canvas while scrolling, names the template
  2. Toggled back to content mode — ribbon disappears entirely
  3. Clicked Edit template ↗ — new tab, site editor lands directly on that template's editor view, address bar rewrites to the canonical /visual-editor/site/templates/{id}
  4. Post with no template / unknown slug — ribbon reads "Editing content inside Default template" with no CTA
  5. Hand-typed ?entity=template&slug=does-not-exist — Templates index plus the not-found toast
  6. Plain /visual-editor/site — unchanged from before

Accessibility Tests Run

  • Keyboard navigation tested
  • Screen reader tested
  • Color contrast verified
  • ARIA labels checked

Details: The ribbon is a role="region" landmark labelled
"Composed-view controls", so screen-reader users can reach it by landmark
rather than having to scroll the canvas. The CTA is a real <a> — keyboard
focusable in document order, with a visible :focus-visible outline drawn
from the theme's focus-width token. Colours resolve from the ArtisanPack UI
--color-base-* tokens (with hex fallbacks for non-DaisyUI hosts) so the
ribbon inherits whatever contrast the active theme guarantees. Not yet
verified with an actual screen reader.

Tests Added

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing

Test details:

  • composed-view-ribbon.test.tsx (6) — name rendering, landmark labelling, deep-link href, slug encoding, host route-base override, CTA hidden on the fallback
  • editor-canvas.test.tsx (+3) — ribbon absent in bare-content mode, mounted inside the BlockCanvas stub rather than beside it, CTA dropped on the fallback
  • deep-link.test.ts (16) — parse/build round-trip plus every rejected shape (unknown entity, blank slug, no entity, …)
  • use-deep-link.test.tsx (10) — resolve-and-navigate, explicit slug matching when the host ignores the filter, unknown-slug and lookup-failure toasts, entity_id short-circuit, no-op cases, once-per-mount
  • canvas-styles.test.ts — cascade-order assertion updated for the appended ribbon sheet

npm test: 326 files, 2996 tests, all passing.

This branch also fixes a test-harness break it inherited. All 18 tests in
site-editor-app.test.tsx were failing on window.localStorage.clear(),
on this branch and on feature/templates-preview alike. Root cause was
neither jsdom nor this repo: vitest's getWindowKeys() only copies a
jsdom window property onto the test global if the key is absent from
Node's globalThis or present in vitest's own allow-list. Node 22+ ships
an experimental Web Storage global that returns undefined without
--localstorage-file, and localStorage is not on vitest's list — so
Node's inert stub shadowed jsdom's working implementation
('localStorage' in window true, window.localStorage undefined). It
surfaced on a Node upgrade rather than a code change.

test-setup.ts now installs an in-memory Storage for localStorage
and sessionStorage, but only when the global is genuinely unusable, so
a working jsdom implementation is never replaced. That restores real
coverage of SiteEditorApp — including this PR's ToastProvider wrapper
and the deep-link hook — rather than leaving it silently unexercised.

Also verified with a real npm run build that the ?inline ribbon
stylesheet lands in the editor chunk.

Documentation

  • Inline code documentation added
  • README updated
  • Wiki updated
  • API documentation updated

Documentation details: docs/site-editor.md gained a "Deep links by
slug" section under URL routing — the parameter table, the additive /
unknown-entity-no-op / access-gate-unchanged guarantees, and a pointer at
buildTemplateDeepLink() as the supported way to construct links.

It also records a known limitation surfaced during review: the ribbon's
CTA uses the default /visual-editor/site base because the post editor
has no way to discover a data-route-base a host may have overridden on
the site-editor mount element. buildTemplateDeepLink() and the ribbon
both already accept the base as a parameter, so threading the host value
through is the follow-up — left out of scope here rather than guessed at.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted
  • Accessibility tests completed
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Adds the locked-canvas ribbon to the post editor's composed view and the
query-string deep-link surface it targets in the site editor.

The ribbon renders inside the canvas iframe (not the editor chrome) as a
sticky bar naming the resolved template, with a single Edit template
button that opens the site editor in a new tab. It is the only editing
affordance for template chrome in v1 — individual template parts get no
per-part badge. Composing against the built-in fallback template shows
the name but hides the CTA, since that tree is a client-side constant
with no site-editor record behind it.

The site editor now parses `?entity=template&slug=…` on mount, resolves
the slug to an entity id through the templates list endpoint, and
navigates to that template's editor view. Path routing addresses
entities by id and the applied-template endpoint returns only a slug, so
the lookup keeps the router's single id-shaped contract instead of
teaching it a second addressing mode. Handling is additive: no query
string behaves exactly as before, and an unknown entity is a no-op
rather than a throw. An unresolvable slug or a failed lookup lands on
the Templates index with a toast naming the slug.

Closes #623
Closes #625

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

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ViewFromTheBox, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c2423a31-b0f3-4f49-9119-8ce4d4d3747e

📥 Commits

Reviewing files that changed from the base of the PR and between 70b234f and 6afd9d2.

📒 Files selected for processing (1)
  • resources/js/visual-editor/test-setup.ts
📝 Walkthrough

Walkthrough

The PR adds a composed-view ribbon with a template edit link. It adds site-editor deep-link parsing, slug resolution, navigation, error toasts, URL canonicalization, iframe styling, tests, and documentation.

Changes

Template deep-link flow

Layer / File(s) Summary
Deep-link contract and resolution
resources/js/visual-editor/site-editor/deep-link.ts, resources/js/visual-editor/site-editor/use-deep-link.ts, resources/js/visual-editor/site-editor/site-editor-app.tsx, resources/js/visual-editor/site-editor/__tests__/*, docs/site-editor.md
The site editor accepts template query parameters, resolves slugs or entity_id values, navigates to matching templates, canonicalizes URLs, and shows warning toasts for failed lookups.
Composed-view ribbon component
resources/js/visual-editor/editor/composed-view/*
The ribbon displays the template name and conditionally renders an encoded, accessible edit link that opens in a new tab.
Canvas integration and iframe styles
resources/js/visual-editor/editor/editor-app.tsx, resources/js/visual-editor/editor/editor-canvas.tsx, resources/js/visual-editor/editor/canvas-styles.ts, resources/js/visual-editor/editor/__tests__/*
The editor passes template metadata to composed mode, renders the ribbon before template content, injects its stylesheet into the iframe, and tests placement and fallback behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EditorCanvas
  participant ComposedViewRibbon
  participant SiteEditorApp
  participant useSiteEditorDeepLink
  participant TemplatesAPI
  EditorCanvas->>ComposedViewRibbon: Pass template name and slug
  ComposedViewRibbon->>SiteEditorApp: Open template deep-link in new tab
  SiteEditorApp->>useSiteEditorDeepLink: Parse query parameters
  useSiteEditorDeepLink->>TemplatesAPI: Resolve template slug
  TemplatesAPI-->>useSiteEditorDeepLink: Return template entities
  useSiteEditorDeepLink->>SiteEditorApp: Navigate to template editor or index
Loading

Possibly related issues

  • #623: The PR implements the composed-view ribbon and template edit deep link described by this issue.
  • #626: The PR adds the composed-view and site-editor deep-link changes associated with this issue.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #623, including the sticky ribbon, CTA deep link, accessibility, iframe styling, fallback behavior, content-mode hiding, and tests.
Out of Scope Changes check ✅ Passed The documentation, deep-link implementation, ribbon code, styling, integration, and tests directly support the stated objectives and issue #623.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies both primary changes: the composed-view ribbon and site-editor slug deep links.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/623-composed-ribbon-and-site-editor-deep-link

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

`site-editor-app.test.tsx` failed all 18 of its tests on `window.localStorage.clear()`
with "Cannot read properties of undefined". jsdom implements Web Storage
correctly; the global was being shadowed before the suite ever ran.

vitest's `getWindowKeys()` filters jsdom's window properties with
`if (k in global) return KEYS.includes(k)` — a key already present on
Node's `globalThis` is only copied when it appears in vitest's own
allow-list, and `localStorage` does not. Node 22+ ships an experimental
Web Storage global whose accessor returns `undefined` unless the process
was started with `--localstorage-file`, so on those versions the key is
present, vitest skips it, and Node's inert stub shadows jsdom's working
implementation: `'localStorage' in window` is true while
`window.localStorage` is undefined.

That is why this surfaced without a code change — on older Node the key
is absent, vitest copies jsdom's version, and the suite passes.

Install an in-memory `Storage` for `localStorage` and `sessionStorage` in
the shared test setup, but only when the global is genuinely unusable, so
a working jsdom implementation is never replaced. Defined via
`defineProperty` rather than assignment because the property Node leaves
behind is an accessor whose setter would swallow a plain write.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Aug 4, 2026
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review August 4, 2026 20:32
@ViewFromTheBox
ViewFromTheBox merged commit 42a32b2 into feature/templates-preview Aug 4, 2026
1 check was pending
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Aug 4, 2026
@ViewFromTheBox
ViewFromTheBox deleted the feature/623-composed-ribbon-and-site-editor-deep-link branch August 4, 2026 20:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/site-editor.md`:
- Around line 158-171: Align the site editor documentation’s route identifier
model by making the deep-link example and canonical path use the same identifier
type. Update the `/visual-editor/site?entity=template&slug=single` example and
its resolution description, or the `/visual-editor/site/templates/12` route, so
both consistently describe either database IDs or slugs.
- Around line 198-204: Thread the configured route base through the editor mount
configuration: expose routeBase in EditorApp and EditorAppShell, pass it from
EditorCanvas to ComposerViewRibbon as siteEditorRouteBase, and ensure
ComposedViewRibbon/buildTemplateDeepLink uses that value instead of the package
default when provided. Preserve the default route base when no override is
configured.

In `@resources/js/visual-editor/site-editor/deep-link.ts`:
- Around line 95-98: Update the route-base normalization in the deep-link helper
so trimming trailing slashes does not turn the root route "/" into an empty
string; preserve "/" when routeBase is root, while retaining current
normalization for other bases. Add a test covering the root route and asserting
the generated URL starts with "/?entity=template".

In `@resources/js/visual-editor/site-editor/use-deep-link.ts`:
- Around line 69-76: Update useSiteEditorDeepLink to capture navigate, toast,
apiConfig, and search in one mount-time useRef snapshot instead of assigning to
separate refs during render. Ensure the cleanup-aware effect and pending
resolver read this stable committed snapshot, eliminating render-time ref
mutations.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5908ef6c-cde8-4319-991a-611ad8a7dd75

📥 Commits

Reviewing files that changed from the base of the PR and between 73f0a30 and 70b234f.

📒 Files selected for processing (15)
  • docs/site-editor.md
  • resources/js/visual-editor/editor/__tests__/canvas-styles.test.ts
  • resources/js/visual-editor/editor/__tests__/editor-canvas.test.tsx
  • resources/js/visual-editor/editor/canvas-styles.ts
  • resources/js/visual-editor/editor/composed-view/__tests__/composed-view-ribbon.test.tsx
  • resources/js/visual-editor/editor/composed-view/composed-view-ribbon.css
  • resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx
  • resources/js/visual-editor/editor/composed-view/index.ts
  • resources/js/visual-editor/editor/editor-app.tsx
  • resources/js/visual-editor/editor/editor-canvas.tsx
  • resources/js/visual-editor/site-editor/__tests__/deep-link.test.ts
  • resources/js/visual-editor/site-editor/__tests__/use-deep-link.test.tsx
  • resources/js/visual-editor/site-editor/deep-link.ts
  • resources/js/visual-editor/site-editor/site-editor-app.tsx
  • resources/js/visual-editor/site-editor/use-deep-link.ts

Comment thread docs/site-editor.md
Comment on lines +158 to +171
Path routes address entities by database id. Callers outside the SPA
usually do not have one — the post editor's composed view, for instance,
knows only the slug of the template it composed against. For those, the
site editor accepts a query-string deep link on the mount URL:

```text
/visual-editor/site?entity=template&slug=single
```

On mount the SPA parses the query string, resolves the slug through the
templates list endpoint, and navigates to that template's editor view.
The `pushState` that follows rewrites the address bar to the canonical
path route (`/visual-editor/site/templates/12`), so the query-string form
is an entry point rather than a URL the user is left holding.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use one identifier model for path routes.

Lines 158-171 define path routes as database-ID routes and show /visual-editor/site/templates/12. Lines 146-150 show slug-like values such as single. Update the examples or the contract so both describe the same route identifier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/site-editor.md` around lines 158 - 171, Align the site editor
documentation’s route identifier model by making the deep-link example and
canonical path use the same identifier type. Update the
`/visual-editor/site?entity=template&slug=single` example and its resolution
description, or the `/visual-editor/site/templates/12` route, so both
consistently describe either database IDs or slugs.

Comment thread docs/site-editor.md
Comment on lines +198 to +204
`buildTemplateDeepLink()` defaults to the package's own mount path
(`/visual-editor/site`) and takes the route base as an optional second
argument. The composed-view ribbon currently relies on that default: the
post editor has no way to discover a `data-route-base` the host may have
overridden on the site-editor mount element. A host that mounts the site
editor at a non-default path will get a ribbon CTA pointing at the
default path until that value is threaded through to the post editor.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline resources/js/visual-editor/editor/editor-app.tsx --view expanded
ast-grep outline resources/js/visual-editor/editor/editor-canvas.tsx --view expanded
ast-grep outline resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx --view expanded

rg -n -C 5 --glob '*.{ts,tsx}' \
  'siteEditorRouteBase|routeBase|buildTemplateDeepLink|ComposedViewRibbon' \
  resources/js/visual-editor

Repository: ArtisanPack-UI/visual-editor

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== editor-app outline =="
ast-grep outline resources/js/visual-editor/editor/editor-app.tsx --view expanded

echo "== editor-canvas relevant lines =="
sed -n '1,220p' resources/js/visual-editor/editor/editor-canvas.tsx

echo "== composed-view-ribbon relevant lines =="
sed -n '1,130p' resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx

echo "== focused call sites for ComposedViewRibbon and route/data propagation =="
rg -n -C 4 --glob '*.{ts,tsx}' 'EditorApp|<EditorApp|EditorAppShell|<EditorAppShell|<ComposedViewRibbon|siteEditorRouteBase|data-route-base|routeBase' resources/js/visual-editor/editor resources/js/visual-editor/site-editor || true

echo "== visual-editor mount / post-editor related files =="
fd -a '.*post.*|.*visual.*|editor-app' resources/js/visual-editor | sed 's#^\./##'

Repository: ArtisanPack-UI/visual-editor

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== editor main config/readers =="
sed -n '80,180p' resources/js/visual-editor/editor/main.tsx
sed -n '300,460p' resources/js/visual-editor/editor/main.tsx

echo "== EditorCanvas / EditorApp rendering =="
sed -n '365,460p' resources/js/visual-editor/editor/editor-app.tsx

echo "== CanvasChrome / EditorCanvas call sites =="
rg -n -C 3 --glob '*.{ts,tsx}' 'type CanvasChrome|interface CanvasChrome|<EditorCanvas|EditorCanvas\(' resources/js/visual-editor/editor resources/js/visual-editor/site-editor

echo "== routeBase usages in editor/main/editor-app with file names =="
for f in $(rg -l --glob '*.{ts,tsx}' 'routeBase|data-route' resources/js/visual-editor/editor resources/js/visual-editor/site-editor); do
  echo "--- $f"
  rg -n -C 2 'routeBase|data-route' "$f"
done

echo "== docs limitation text =="
sed -n '190,210p' docs/site-editor.md

Repository: ArtisanPack-UI/visual-editor

Length of output: 34656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

paths = [
    Path('resources/js/visual-editor/editor/editor-app.tsx'),
    Path('resources/js/visual-editor/editor/editor-canvas.tsx'),
    Path('resources/js/visual-editor/editor/composed-view/composed-view- ribbon.tsx'),
    Path('resources/js/visual-editor/editor/main.tsx'),
]
for p in paths:
    print('path exists', p, p.exists())

files = {
    'editor-app.tsx': Path('resources/js/visual-editor/editor/editor-app.tsx').read_text(),
    'editor-canvas.tsx': Path('resources/js/visual-editor/editor/editor-canvas.tsx').read_text(),
    'composed-view-ribbon.tsx': Path('resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx').read_text(),
    'main.tsx': Path('resources/js/visual-editor/editor/main.tsx').read_text(),
}

props = re.findall(r'^export interface EditorAppProps \{(?P<body>.*?)^\}', files['editor-app.tsx'], re.M|re.S)
prop_names = re.findall(r'^\s*(?:export )?([A-Za-z_$][\w$]*)\?', props[0], re.M) if props else []
print('EditorAppProps has apiBase and routeBase?', 'apiBase' in prop_names, 'routeBase' in prop_names)
print('EditorAppProps routeBase mentions data-route-base/config?', any('routeBase' in prop_names and ('data-route' in props[0] or 'route-base' in props[0])) for _ in [1])
print('EditorCanvasProps has routeBase?', 'routeBase' in files['editor-canvas.tsx'])
print('EditorAppShell routeBase parsed/used?', bool(re.search(r'routeBase', files['editor-app.tsx'], re.I)))
print('EditorAppShell EditorCanvas chrome props routeBase?', bool(re.search(r'<EditorCanvas[\s\S]{0,2500}routeBase', files['editor-app.tsx'])))
print('MountConfig has routeBase?', 'routeBase:' in re.search(r'export interface MountConfig \{(?P<body>.*?)\n\}', files['main.tsx'], re.S).group(0))
PY

Repository: ArtisanPack-UI/visual-editor

Length of output: 496


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import re
from pathlib import Path

files = {
    'editor-app': Path('resources/js/visual-editor/editor/editor-app.tsx').read_text(),
    'editor-canvas': Path('resources/js/visual-editor/editor/editor-canvas.tsx').read_text(),
    'composed-view-ribbon': Path('resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx').read_text(),
    'main': Path('resources/js/visual-editor/editor/main.tsx').read_text(),
}

app_props = re.search(r'export interface EditorAppProps \{(?P<body>.*?)\n\}', files['editor-app'], re.S).group('body')
app_props_names = re.findall(r'^\s*([A-Za-z_$][\w$]*)\?', app_props, re.M)

print('EditorAppProps contains routeBase:', 'routeBase' in app_props_names)
print('EditorAppProps contains apiBase:', 'apiBase' in app_props_names)
print('EditorCanvasProps contains routeBase:', bool(re.search(r'\brouteBase\b', files['editor-canvas'])))
print('EditorAppShell EditorCanvas props contain routeBase:', bool(re.search(r'<EditorCanvas(?:[\\s\\S]*?)routeBase', files['editor-app'])))

editor_app_route_mentions = re.findall(r'\brouteBase\b', files['editor-app'], re.I)
print('EditorApp routeBase mentions:', len(editor_app_route_mentions))

mount_config_lines = re.search(r'export interface MountConfig \{(?P<body>.*?)\n\}', files['main'], re.S).group('body')
print('Editor mount MountConfig routeBase:', 'routeBase' in mount_config_lines)
print('Editor mount MountConfig data-route/base text:', 'data-route' in mount_config_lines or 'route-base' in mount_config_lines)
PY

echo "== EditorApp Shell / EditorCanvas call from editor-app =="
sed -n '1420,1475p' resources/js/visual-editor/editor/editor-app.tsx

Repository: ArtisanPack-UI/visual-editor

Length of output: 2795


Thread the configured route base through the post editor.

ComposerViewRibbon accepts siteEditorRouteBase, but EditorCanvas never passes it, and EditorApp/EditorAppShell do not expose routeBase at all. Hosts that mount the site editor at /data-ap-site-editor/directive/mount?data-route-base=%2Fcustom%2Fsite-editor still get the ribbon Edit template ↗ CTA built to the package default unless the configured value is added to the mount config and forwarded through to ComposedViewRibbon.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/site-editor.md` around lines 198 - 204, Thread the configured route base
through the editor mount configuration: expose routeBase in EditorApp and
EditorAppShell, pass it from EditorCanvas to ComposerViewRibbon as
siteEditorRouteBase, and ensure ComposedViewRibbon/buildTemplateDeepLink uses
that value instead of the package default when provided. Preserve the default
route base when no override is configured.

Comment on lines +95 to +98
const normalizedBase = routeBase.replace(/\/+$/, '');
const query = new URLSearchParams({ entity: 'template', slug });

return `${normalizedBase}?${query.toString()}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep / as a route base.

When routeBase is /, Line 95 produces an empty base. The helper emits ?entity=template... instead of /?entity=template.... Preserve / and add a root-route test.

Proposed fix
-    const normalizedBase = routeBase.replace(/\/+$/, '');
+    const normalizedBase =
+        routeBase === '/' ? '/' : routeBase.replace(/\/+$/, '');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const normalizedBase = routeBase.replace(/\/+$/, '');
const query = new URLSearchParams({ entity: 'template', slug });
return `${normalizedBase}?${query.toString()}`;
const normalizedBase =
routeBase === '/' ? '/' : routeBase.replace(/\/+$/, '');
const query = new URLSearchParams({ entity: 'template', slug });
return `${normalizedBase}?${query.toString()}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/site-editor/deep-link.ts` around lines 95 - 98,
Update the route-base normalization in the deep-link helper so trimming trailing
slashes does not turn the root route "/" into an empty string; preserve "/" when
routeBase is root, while retaining current normalization for other bases. Add a
test covering the root route and asserting the generated URL starts with
"/?entity=template".

Comment on lines +69 to +76
const navigateRef = useRef(navigate);
navigateRef.current = navigate;
const toastRef = useRef(toast);
toastRef.current = toast;
const apiConfigRef = useRef(apiConfig);
apiConfigRef.current = apiConfig;
const searchRef = useRef(search);
searchRef.current = search;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline resources/js/visual-editor/site-editor/use-deep-link.ts \
  --match useSiteEditorDeepLink --view expanded

# Expect no render-time assignments after the fix.
rg -n -C 3 '[A-Za-z]+Ref\.current\s*=' \
  resources/js/visual-editor/site-editor/use-deep-link.ts

# Locate the deferred-lookup regression coverage.
rg -n -C 4 'useSiteEditorDeepLink|rerender|mockResolvedValue' \
  resources/js/visual-editor/site-editor/__tests__/use-deep-link.test.tsx

Repository: ArtisanPack-UI/visual-editor

Length of output: 3358


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,170p' resources/js/visual-editor/site-editor/use-deep-link.ts
sed -n '120,165p' resources/js/visual-editor/site-editor/__tests__/use-deep-link.test.tsx

Repository: ArtisanPack-UI/visual-editor

Length of output: 7008


🌐 Web query:

React useRef mutating during render concurrent rendering discouraged

💡 Result:

In React, reading or writing to ref.current during the render phase is explicitly discouraged and considered unsafe [1][2][3]. This practice is strongly cautioned against, particularly in the context of concurrent rendering, because it makes component behavior unpredictable and unreliable [1][2][4][5]. The primary reasons and rules regarding this are: 1. Unpredictability in Concurrent Mode: During concurrent rendering, React may invoke your component function multiple times before committing the result to the screen [4][5]. If you perform side effects—such as writing to a ref—during these render passes, your component may behave differently than expected, or ref values may become inconsistent with the rendered output [4][5]. 2. Purity and Side Effects: React components should ideally be pure functions of their props and state [4]. Writing to a ref during render is a side effect. Since React does not guarantee how many times a component will render, any side effect occurring during this phase can lead to bugs that are difficult to reproduce and debug [1][2][4]. 3. The Exception for Initialization: The only widely accepted exception is using a ref for lazy initialization, such as the pattern if (!ref.current) ref.current = new Thing() [1][2][6][3]. This is considered safe because the result is deterministic and occurs only once, essentially treating the ref as a stable object container rather than a dynamic source of state during rendering [2][6][3]. 4. Recommended Alternatives: If you need to store or read information that affects rendering, you should use state (useState) instead [1][2]. State is managed by React, follows the "snapshot" model of rendering, and ensures that your UI remains consistent across re-renders [1][2][3]. If you must perform a side effect based on a value, use useEffect or useLayoutEffect, which are designed to run only after the commit phase [2][4][7]. In summary, treating ref.current as a source of truth to be read or written during the render phase violates the component lifecycle model and can lead to issues in any React application, especially those leveraging concurrent features [1][2][4][5]. Top Results: [1][2][4][5]

Citations:


Capture the mount-time context without render-time ref writes.

useSiteEditorDeepLink reads these refs in a cleanup-aware effect, so navigate/toast mutations during a discarded render can leave the pending resolver calling a callback from uncommitted work. Store the mount-time snapshot instead, such as a single useRef({ navigate, toast, apiConfig, search }) created in the hook’s render phase.

🧰 Tools
🪛 React Doctor (0.9.3)

[error] 70-70: This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits.

Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.

(no-ref-current-in-render)


[error] 72-72: This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits.

Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.

(no-ref-current-in-render)


[error] 74-74: This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits.

Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.

(no-ref-current-in-render)


[error] 76-76: This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits.

Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.

(no-ref-current-in-render)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/visual-editor/site-editor/use-deep-link.ts` around lines 69 -
76, Update useSiteEditorDeepLink to capture navigate, toast, apiConfig, and
search in one mount-time useRef snapshot instead of assigning to separate refs
during render. Ensure the cleanup-aware effect and pending resolver read this
stable committed snapshot, eliminating render-time ref mutations.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant