Editing

Bound text

When the page shows a value from your data — not a hardcoded string in the JSX — you can still edit it in the browser. nextcanvas finds the matching entry in your source data and rewrites that string.

How it feels

Bound text looks like normal text on the page. You double-click and type the same way. Under the hood, the commit updates a property in an array or object (often in another file), not the JSX expression itself.

Targeting is by value.If a list is filtered or reordered on screen, nextcanvas still edits the data entry whose current string matches what you changed — not "whatever is at index 3."

Shapes that work

Object fields in a map

{SPEAKERS.map((s) => (
  <h3>{s.name}</h3>
))}

Double-click a name on the page → that speaker's name string in the SPEAKERS array (or imported data module) updates.

Fallback expressions

<h3>{s.name ?? s.role}</h3>
<p>{result.number ?? "—"}</p>

Edit whatever the page is currently showing. If the name is present, you're editing name; if the UI fell through to the role, you're editing role. Same idea for ||.

Literal fallbacks work too — when the page shows the "—", editing it rewrites that string in the JSX. When it shows the bound side, you're editing the data property.

Optional chaining

<h3>{job?.service}</h3>

Treated like a normal member path (job.service). If job comes from a .map, the matching array entry updates.

Button labels and other string ternaries

<button>{loading ? "Signing in..." : "Sign in"}</button>
<span>{status === "a" ? "Alpha" : status === "b" ? "Beta" : "Other"}</span>

When every arm is a string literal (nested ternaries ok), double-click the label you see. nextcanvas rewrites that arm in source — the other arms stay put. Great for loading CTAs and status chrome.

String arrays

{truths.map((t) => (
  <p>{t}</p>
))}

Each rendered line maps to a string literal in the array. Change one on the page; that array entry updates.

Config / copy objects

<Reveal as="p">{COUNCIL_COPY.eyebrow}</Reveal>
<h1>{cfg.title}</h1>

Nested paths like cfg.meta.title work too. If the object is imported from another file, that file is what gets saved.

Props drilled through a child component

function SessionCard({ session }) {
  return <h3>{session.title}</h3>;
}

{visible.map((session) => (
  <SessionCard session={session} />
))}

You still edit the title on the card. nextcanvas follows session back through the prop to the mapped data (and, when the list is a filtered view, to the underlying source array).

FAQ-style props work the same way:

function Row({ q, a }) {
  return (
    <>
      <span>{q}</span>
      <p>{a}</p>
    </>
  );
}

{faqs.map((f) => (
  <Row q={f.q} a={f.a} />
))}

Bound text beside siblings

<div>
  {msg.role === "assistant" && <p>Relay</p>}
  {msg.text}
</div>

A bound expression that isn't the only child still becomes editable — nextcanvas wraps it in a stamped span at compile time. You double-click the text as usual; write-back targets the data the same way.

Inert chrome like {isStreaming && <span />} next to a bound value is ignored, so {content} can still be the editable child.

When an edit is refused

  • Duplicate values— if two entries share the exact same string, nextcanvas can't tell which you meant. Make the copy unique, or edit the data file directly.
  • Stale text — if the source no longer contains the old value (someone else edited the file), reload and try again.
  • Non-string leaves— numbers or expressions in the data object can't be edited as text.

What stays uneditable

  • Mixed literal + expression: Day {session.day}, {SITE.dates} · {SITE.location}
  • Computed access or calls: {items[i].x}, {fn().y}
  • Ternaries whose arms aren't plain string literals (components, fragments, calls)
  • Runtime-only strings (API payloads, live clocks, validation errors) — there's no source literal to rewrite
  • Bare identifiers that are React reserved props (children, className, …) or arbitrary locals with no resolvable data binding