require-editor-reason
"Every fork on `inCanvas` must carry an `editor-reason:` comment naming what it protects."
Venue: eslint — Every fork on
inCanvasmust carry aneditor-reason:comment naming what it protects.
Every place your island forks its behavior on inCanvas must carry a comment directly
above it opening with editor-reason:, naming the published-only behavior the fork
protects.
// editor-reason: the lightbox is a fullscreen scroll-locking takeover that would cover// the page the author is editing.if (inCanvas) return;The comment sits on the line(s) immediately above — a blank line between breaks it. Both
// and {/* … */} count, so the marker works in JSX children as well as in a handler
or an attribute list. Where a block of comment lines sits above the fork, the first
line carries the marker.
Three reads are plumbing rather than a fork, and stay silent:
- seeding a ref with the flag —
useRef(inCanvas); - keeping that ref current —
inCanvasRef.current = inCanvas; - naming it as a hook dependency —
}, [inCanvas]).
A fork reached through such a ref (if (inCanvasRef.current) return; — what an
imperative event handler needs, since it reads outside render) is followed one level and
reports at the decision itself.
Reason
Section titled “Reason”The contract is that a live island runs its published behavior in the canvas. inCanvas
is the one sanctioned way to break it, and it is deliberately plain React — an ordinary
boolean, no wrapper, no declaration — so the rare case that needs it costs nothing to
write.
That cheapness is also the risk. With nothing asking why, inCanvas slides from “this one
takeover cannot run here” into a second, quietly divergent editor rendering of the island:
an editor mode. Authors then debug two behaviors instead of one, and the author sees
something in the canvas that no visitor will ever see.
Naming the protected behavior is the check. A fork you can justify in one line — a
fullscreen modal, a scroll lock, a takeover that hides the page — is the escape hatch
working. A fork you cannot is a sign you wanted editor = { live: false } for the whole
island, or wanted nothing at all.
The marker is greppable on purpose: editor-reason: is how every hatch in a workspace is
found at once, the same way css-reason: opens every hand-written stylesheet.
Write the reason, or remove the fork.
Before
Section titled “Before”"use client";
import { type IslandEditor, useEditor } from "@homepages/template-kit";import { useState } from "react";
export const editor = { live: true } satisfies IslandEditor;
export default function Slider() { const [galleryOpen, setGalleryOpen] = useState(false); const { inCanvas } = useEditor();
const openGallery = () => { if (inCanvas) return; setGalleryOpen(true); };
return ( <div> {!inCanvas && <button onClick={openGallery}>Expand</button>} {galleryOpen && <div role="dialog">…</div>} </div> );}"use client";
import { type IslandEditor, useEditor } from "@homepages/template-kit";import { useState } from "react";
export const editor = { live: true } satisfies IslandEditor;
export default function Slider() { const [galleryOpen, setGalleryOpen] = useState(false); const { inCanvas } = useEditor();
const openGallery = () => { // editor-reason: the gallery is a fullscreen scroll-locking takeover — it would // cover the whole page the author is editing. if (inCanvas) return; setGalleryOpen(true); };
return ( <div> {/* editor-reason: don't advertise a takeover openGallery refuses to perform. */} {!inCanvas && <button onClick={openGallery}>Expand</button>} {galleryOpen && <div role="dialog">…</div>} </div> );}See also
Section titled “See also”- Islands — when the hatch is the
right answer, and when
live: falseis. require-island-editor— thelivedeclaration itself.no-editor-globals— the other half:useEditor()is the only way to learn you are in the canvas.