Skip to content

feat: plan + scaffold Lexical-backed VariableInput v2#674

Open
0xdeafcafe wants to merge 3 commits into
masterfrom
feat/lexical-variable-input-v2
Open

feat: plan + scaffold Lexical-backed VariableInput v2#674
0xdeafcafe wants to merge 3 commits into
masterfrom
feat/lexical-variable-input-v2

Conversation

@0xdeafcafe

@0xdeafcafe 0xdeafcafe commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Plans and scaffolds the replacement of the bespoke contenteditable
VariableInput with a Lexical-backed implementation, per ADR-0002. Lands
the architecture decision + Gherkin behaviour spec, and the V2 scaffold
living side-by-side with legacy. The V2 implementation is NOT routed to
any production call site yet — only the in-app playground (Cmd+K →
"Variable input lab") mounts it, stacked against the legacy input for
direct comparison.

The cutover — deleting the legacy folder, renaming v2 → canonical path,
wiring every call site, and landing the test gate — is a follow-up PR.
ADR-0002 §5 has the ordered checklist.

Changes

  • docs(adr)docs/adr/0002-lexical-variable-input.md with 5
    sub-decisions (library choice, chip DOM reuse, ValueSections boundary,
    trigger flow, hard-cutover policy) and docs/features/variable-input.feature
    with 32 scenarios covering caret, trigger, state modal, clipboard,
    masking, missing variables, disabled/readOnly, single-line constraint,
    parity contract, and debounced sync. Both READMEs updated.
  • feat(variable-input) — new packages/ui/src/features/variable-input-v2/:
    VariableInputV2, VariableChipNode (a Lexical DecoratorNode reusing
    the same .bvs-blob DOM contract as legacy), plus plugins for the {
    trigger, single-line constraint, ValueSections bridge, history, and
    chip-click → state modal. Adds lexical + @lexical/react to
    @beak/ui. One backwards-compatible addition to VariableSelector (an
    optional anchorRect prop) so V2 can position the popover by caret
    rect rather than the legacy partIndex DOM lookup. Legacy code is
    untouched otherwise.
  • Review feedback addressed in the third commit: VariableSelector's
    position effect now refires on partIndex/offset changes so the
    popover tracks the caret as the user types {query}; VariableChipNode
    emits <span> rather than <div> to remain valid inside Lexical's
    <p>; SingleLinePlugin's doc comment now describes the real Cmd+Enter
    flow (host-handled, not global); feature-flag dispatches its change
    event unconditionally so private-mode/quota failures don't silence the
    toggle.

What's intentionally NOT in this PR

  • The cutover: legacy delete + rename + call-site routing + vitest /
    Playwright suites. See ADR-0002 §5 for the ordered checklist.
  • A COPY_COMMAND / PASTE_COMMAND plugin for V2. PlainTextPlugin's
    paste handler reads only text/plain, so the chip-aware clipboard
    round-trip (spec scenarios variable-input.feature:164 and :176)
    currently fails — chips are dropped on paste. Known gap; lands with
    the cutover.
  • A LineBreakNode transform. Multi-line paste keeps the data
    newline-stripped (via the TextNode transform in SingleLinePlugin)
    but leaves invisible <br> remnants in the editor tree. CSS hides
    them; lands with the cutover.

Test plan

Playground (Cmd+K → "Variable input lab"):

  • Empty scenario — placeholder renders, hides on first keystroke.
  • Text + chip + text — looks visually identical between Legacy and V2.
  • ArrowRight over a chip hops it as a single character.
  • Backspace after a chip deletes the whole chip in one keystroke.
  • Type { + query + Enter inserts a chip and removes the trigger text.
  • Escape closes the variable selector without inserting.
  • Click an editable chip → state modal opens; change a field, save;
    chip's data-payload reflects the new value.
  • Cmd+Z undoes a chip insertion as one step; Shift+Cmd+Z redoes it.
  • Masked scenario renders dots via CSS text-security.
  • Missing-variable chip wears the alert tint.
  • Adjacent chips — clicking in the gap (or arrow-keying between)
    places the caret correctly.

Linked

Copilot AI review requested due to automatic review settings June 8, 2026 12:32
@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apps-web-marketing Error Error Jun 8, 2026 1:31pm

Copilot AI 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.

Pull request overview

This PR documents (ADR-0002 + Gherkin feature spec) and scaffolds a Lexical-backed VariableInputV2 implementation that lives alongside the legacy contenteditable input, with side-by-side rendering in the /lab/variable-input playground for parity verification.

Changes:

  • Adds Lexical dependencies to @beak/ui and introduces the variable-input-v2/ scaffold (editor component, chip node, plugins, ValueSections conversion, and a localStorage-backed preference hook).
  • Extends the shared legacy VariableSelector with an optional anchorRect to support caret-rect popover anchoring for Lexical.
  • Adds ADR-0002 and a comprehensive variable-input.feature parity/behaviour contract, plus docs index updates.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pnpm-lock.yaml Locks new Lexical (+ transitive) dependencies.
packages/ui/package.json Adds lexical and @lexical/react to @beak/ui.
packages/ui/src/features/variable-input/playground/VariableInputPlayground.tsx Renders legacy + V2 side-by-side; adds persisted “prefer V2” toggle.
packages/ui/src/features/variable-input/components/molecules/VariableSelector.tsx Adds anchorRect prop and positioning branch for caret-rect anchoring.
packages/ui/src/features/variable-input-v2/index.ts Exports VariableInputV2 + feature-flag helpers.
packages/ui/src/features/variable-input-v2/components/VariableInputV2.tsx Lexical-backed VariableInput implementation + plugins + VariableEditor integration.
packages/ui/src/features/variable-input-v2/nodes/VariableChipNode.tsx Lexical DecoratorNode implementing the .bvs-blob DOM contract and import/export.
packages/ui/src/features/variable-input-v2/plugins/VariableTriggerPlugin.tsx Implements { trigger detection + VariableSelector integration and chip insertion.
packages/ui/src/features/variable-input-v2/plugins/SingleLinePlugin.tsx Enforces single-line behaviour (Enter/newline/paste transforms).
packages/ui/src/features/variable-input-v2/plugins/InitialValuePlugin.tsx One-shot initial seed from ValueSections on mount.
packages/ui/src/features/variable-input-v2/plugins/ExternalValueSyncPlugin.tsx Debounced external prop sync with legacy-like 100ms guard.
packages/ui/src/features/variable-input-v2/plugins/ChipDataIndexPlugin.tsx Stamps legacy data-index to reuse existing VariableEditor wiring during scaffold phase.
packages/ui/src/features/variable-input-v2/utils/value-sections-conversion.ts ValueSections ↔ Lexical tree conversion helpers.
packages/ui/src/features/variable-input-v2/utils/feature-flag.ts localStorage-backed preference hook for opting into V2 at call sites.
docs/features/variable-input.feature Behaviour contract (caret, trigger, modal, clipboard, masking, sync, parity, etc.).
docs/features/README.md Adds feature doc index entry for variable input.
docs/adr/README.md Adds ADR-0002 to ADR index.
docs/adr/0002-lexical-variable-input.md ADR describing Lexical decision, architecture, and cutover checklist.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/ui/src/features/variable-input/components/molecules/VariableSelector.tsx Outdated
Comment thread packages/ui/src/features/variable-input-v2/nodes/VariableChipNode.tsx Outdated
Comment thread packages/ui/src/features/variable-input-v2/plugins/SingleLinePlugin.tsx Outdated
Comment thread packages/ui/src/features/variable-input-v2/utils/feature-flag.ts
Plans the replacement of the bespoke contenteditable VariableInput with
a Lexical-backed editor: library choice, chip DOM contract reuse,
ValueSections bridge, hard-cutover policy, and the vitest + Playwright
test gate. Companion .feature covers caret, trigger, state-modal,
clipboard, masking, missing variables, disabled/readOnly, single-line
constraint, debounced sync, and the parity contract for every legacy
call site.
Adds `packages/ui/src/features/variable-input-v2/` — a Lexical-backed
VariableInput implementation that lives next to the legacy contenteditable
one for the side-by-side phase described in ADR-0002. The legacy
implementation is untouched except for one backwards-compatible addition
to VariableSelector (an optional `anchorRect` prop the V2 trigger plugin
uses for popover positioning) and the playground, which now renders both
implementations stacked for direct comparison.

The chip rendering reuses the existing `.bvs-blob` DOM contract and CSS,
so chips look identical between the two. `ValueSections` is unchanged —
two boundary helpers translate it to/from Lexical's editor state. The
trigger flow (`{` opens the selector), the state-modal popover, and
masking all work; clipboard round-trip and adjacent-chip caret behaviour
still need work (called out in the spec).

A localStorage feature flag (`beak.featureFlags.variableInputV2`) is the
toggle for the playground — production call sites remain on the legacy
implementation, per the hard-cutover policy. The cutover PR is the
follow-up that retires the legacy folder and routes every call site at
once.

See docs/adr/0002-lexical-variable-input.md (committed earlier in 21ef603d)
and docs/features/variable-input.feature.
- VariableSelector: refire position effect on partIndex/offset
  changes so the popover tracks the caret as the user types `{query}`.
  Previously `Boolean(sel)` was effectively a one-shot dep.
- VariableChipNode: createDOM returns `<span>` instead of `<div>` so
  the inline decorator is valid inside Lexical's host `<p>` element
  and the browser doesn't auto-close the paragraph + re-parent the
  chip. CSS already uses `display: inline-block`, so visuals are
  identical.
- SingleLinePlugin: doc comment now describes how `Cmd/Ctrl + Enter`
  is actually wired — VariableInputV2's own keydown listener on the
  root element handles it and stops propagation, the global shortcut
  layer never sees the event.
- feature-flag: dispatch the change event even when `localStorage`
  throws (private mode, quota), so in-memory subscribers update and
  UI toggles reflect the click. Persistence is best-effort.
@0xdeafcafe 0xdeafcafe force-pushed the feat/lexical-variable-input-v2 branch from b0e4faa to 2c44650 Compare June 8, 2026 14:15
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.

2 participants