Authoring shortcuts: multi-select, drag-to-scrub, Cmd-K, cheat sheet, patterns, hints#11
Merged
Authoring shortcuts: multi-select, drag-to-scrub, Cmd-K, cheat sheet, patterns, hints#11
Conversation
:selection moves from a single {:id ...} map to a vector of node ids.
Pure helpers (selected-ids, selected?, single-selected-id) plus
effectful select-one!, select-clear!, select-toggle! cover the call
sites; single-node consumers (resize handles, nudge, inspector,
inline edit) route through single-selected-id and degrade gracefully
under multi-select.
The selection overlay becomes a pool of N borders, one per selected
DOM id; the primary overlay carries the resize handles and only
exposes them when exactly one node is selected. Shift-click on the
canvas or in the layers panel toggles a node in the selection;
pointerdown on empty canvas (or padding) starts a marquee that paints
a rectangle and adds every overlapped node id on release, with Shift
extending the existing selection. Esc cancels mid-drag.
ops/remove-many is the batch counterpart to ops/remove; the :delete
shortcut now drops the entire selection in one commit, idempotent
against cascaded removals and root.
In edit mode the canvas-host gets user-select: none so Shift-click
and marquee-drag don't paint a native text-selection band over the
rendered preview; .bareforge-inline-edit re-enables user-select: text
so the inline text editor works as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ops/duplicate deep-clones a node and its subtree with fresh ids, inserting the clone as the next sibling. ops/duplicate-many maps that over a selection in input order, skipping missing/root ids silently so an unfiltered selection vector is safe to feed. ops/wrap-many wraps a set of sibling ids in a fresh container at the position of the lowest-indexed sibling, preserving original document order inside the wrapper's default slot. Refuses to wrap when ids don't share a parent+slot or include root. Cmd-D in shortcuts/dispatch maps to :duplicate; the perform! handler runs duplicate-many over the canonicalised selection and selects the new clones — natural follow-up gesture is to drag the duplicates. Cmd-G maps to [:wrap-in "x-container"]; Cmd-Shift-G maps to :wrap-in-prompt, which uses window.prompt restricted to a small container whitelist (x-container, x-grid, x-card, x-flex). The wrapper becomes the new selection so the user can immediately keep editing it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inspector field labels for :number kind editors and the free-coord
:layout fields (x/y/w/h) become horizontal drag handles. Pointerdown
captures the start value; pointermove computes new = start + dx,
optionally x10 with Shift; pointerup releases. The first move of a
drag commits normally so the pre-drag state lands in :past, and
every subsequent move uses commit-coalesced! so the entire scrub
collapses into a single undo entry — the same pattern arrow-key
nudge already uses.
The mechanism is opt-in: each scrubbable widget builder calls
attach-scrub-meta! to stash a {:read-fn :commit-fn! :step} spec on
the widget element; the field-row wrapper detects the spec and
wires pointer-scrub! on the row's label, adding the is-scrubbable
class so CSS can show ew-resize on hover and suppress text
selection. Non-numeric rows are unaffected.
CSS-length-shaped attributes (width/height/padding/margin) and
percentage / rem / em values stay non-scrubbable for now —
preserving the suffix while scrubbing needs its own design and is
deferred to a follow-up. Free-coord layout fields are clean numbers
in the document, so they scrub directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original M2.1 read-fns returned nil when a numeric attribute was empty, which short-circuited the pointerdown handler's `(when (number? v) ...)` guard — the gesture engaged the cursor hint but nothing else, so dragging an unset min/max/step or a non-:free x/y/w/h field did nothing visible. Both scrub-eligible builders now fall back to 0 when the underlying value is missing or unparseable, so the drag picks up at zero and populates the field as the user drags. Existing numeric values behave exactly as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cmd-Opt-C captures the single-selected node's :attrs + :props into an in-memory clipboard slot under :ui :clipboard :attrs (no history, no DOM clipboard). Cmd-Opt-V applies that snapshot onto every canonicalised id in the current selection, filtered by each target tag's supported attributes — pasting an x-button's `variant` onto an x-card silently drops it instead of stamping an unknown attr. The gesture deliberately lives on Cmd-Opt-C/V instead of plain Cmd-C/V so the browser's native copy/paste keeps working in inspector text fields and the layers panel; the editable-target guard already covers most surfaces, but the alt requirement keeps the canvas gesture out of muscle-memory range of native copy/paste entirely. ops/set-attrs and ops/set-props are the batch counterparts to set- attr / set-prop — nil values dispatch to the unset path so blanket- pasting a sparse clipboard correctly clears keys absent on the source. They're also the foundation for M2.3 multi-select edit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On macOS US layout Option+C produces 'ç' and Option+V produces '√' —
the literal `.key` value never equalled "c" / "v", so dispatch fell
through and the gesture was silently swallowed. event-descriptor now
also captures `.code`, and dispatch matches either ("c" + "KeyC" /
"v" + "KeyV") so the same gesture works across macOS / Linux /
Windows. Added a regression test for the Option-modified key path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
inspector-model under multi-select now surfaces every selected node and its tag set instead of a bare count, and the inspector's multi-mode rebuilds as a "Shared attributes" section: every property that exists on every selected tag (matched by :name and :kind) becomes an editable row. Mixed values render with a "Mixed" placeholder and an .is-mixed class (dashed border, slight opacity) so it's obvious which rows are uniform versus divergent. Editing a row dispatches a single ops/set-attrs-many (or set-props- many for booleans) commit, applying the user's value to every selected node. The doc-changed watcher branch short-circuits in multi-mode so per-keystroke commits don't tear down the input the user is typing into; visual "Mixed" markers may lag until the user re-enters multi-select, an acceptable v1 trade-off. Widget coverage in v1: enum, string-short, string-long, number, boolean. CSS-var, layout, and seed-record editors stay single- select-only — they need their own multi-edit story (different sources of truth) and the UX for partial overlap there is not obviously a win. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-multi-enum subscribed to :x-select-input, but x-select fires :select-change (mirroring build-enum's single-select wiring). The selected option also needs the `selected=""` attribute on the matching <option>, not a separate value= write on the host. With the wrong event the dropdown captured input but never reached the commit handler — picking a variant from the multi-select list was silent. build-multi-boolean was reading read-event-value off an x-switch event, but switches expose state via .detail.checked / .target.checked through read-event-checked. Toggle now propagates to every selected node. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bareforge.meta.design-tokens mirrors the 50 tk-* tokens defined in
baredom.components.x-theme.model as a {:name :category} vector,
categorised colour / length / font / shadow / motion / z / opacity.
Tests assert prefix, category membership, and uniqueness so a
BareDOM bump that adds tokens shows up as a missing-coverage
failure rather than silent autocomplete drift.
inspector/install-token-datalists! mounts two `<datalist>` elements
on document.body — one per surfaced category (colour and length).
Idempotent so a hot-reload doesn't duplicate them. main/init wires
it up alongside baredom/register! so the lists are present before
the inspector first paints.
Colour-shaped attribute fields (:kind :color) and the layout fields
(width / height / padding / margin) reference the matching datalist
via the native `list=` attribute. Typing `var(` into either now
surfaces every theme token as an autocomplete suggestion. Multi-
select shared-attr search fields pick up the same wiring when their
property is colour-shaped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native HTML <datalist> autocomplete activates only when list= is on the actual <input>. Setting it on the custom-element host did nothing — x-search-field doesn't observe `list` and doesn't forward it to its shadow inner <input part="input">. attach-datalist-to-shadow-input! reaches into the open shadow root on the next animation frame (after connectedCallback has populated it) and writes list= directly on the inner <input>. A retry loop handles components whose shadow root is built one frame later. Wired through build-search-field (colour kind), build-layout-field (length tokens), and build-multi-search-field for multi-select parity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Body-level <datalist> can't be referenced by list= on an <input> that lives inside a shadow root — the HTML spec scopes the lookup to the input's own tree, not the host document. The previous fix attached list= to the shadow input but the global datalist was unreachable, so autocomplete never fired. attach-datalist-to-shadow-input! now also clones the global datalist into the field's shadow root before wiring list=. The clone is idempotent: if a datalist with the target id already exists in the shadow root we skip the second injection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shortcut-info is the single source of display data — every entry carries a :keys string, a :label, and a :category. dispatch grew a '?' arm returning :show-shortcuts. perform! invokes a callback the modal registers on init via shortcuts/set-show-shortcuts!, which keeps the shortcuts ↔ cheat-sheet wiring acyclic (the modal requires shortcuts for the static data; the action flows the other way through an atom). bareforge.ui.cheat-sheet owns the modal element, mounts it on document.body, groups rows by category-labels, and intercepts keydown in capture phase while open so Esc closes the modal without also clearing the canvas selection. Click on the backdrop, the close button, or '?' a second time also dismisses. A coverage test asserts every shortcut-info entry uses one of the declared categories — drift between the data and category-labels shows up at compile time. Tests for the new dispatch arms (with / without modifiers, in / out of editable targets) round out the gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hand-rolled overlay/panel/header markup is replaced with a single x-modal element. Backdrop click, focus trap, ARIA roles, size variants, and the open/close transition come from BareDOM — no parallel implementations to keep in sync, and theming switches (default/ocean/forest/…) automatically restyle the modal. Every text node inside the modal renders through x-typography: title is `h3`, group labels use `overline`, shortcut keys use the `kbd` variant, and shortcut descriptions use `body2`. Type scale, font family, and palette inherit from the live theme. The cheat-sheet still owns Esc handling: a capture-phase keydown intercepts Escape before x-modal's internal dismiss handler and the document-level shortcut layer's :deselect handler fire — so closing via Esc neither double-handles dismiss nor clears the canvas selection. Backdrop clicks still flow through x-modal- dismiss, which our handler routes to hide!. CSS shrinks to the row+group grid inside the modal body; the overlay, panel chrome, and header styling are deleted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
document.body sits outside the chrome's <x-theme preset="…"> element (which wraps #app in index.html), so a modal appended to body never saw the ocean / forest / sunset / … presets and rendered with the default token palette. build-modal! now resolves the theme host as `#app`'s parent node, which is the chrome's outer x-theme. The modal mounts there as a sibling of #app and inherits every CSS custom property the preset sets — typography variants, surface colours, focus rings, all follow the live theme. document.body remains the fallback if the index.html shape ever changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A searchable modal that runs every chrome action — File menu, panel toggles, selection ops, the four wrap-in targets, and `Insert <Tag>` for every BareDOM tag — from one keyboard surface. Built on x-modal + x-typography so it inherits the active theme like the cheat sheet. The empty-query view is a curated short list (file, view, selection, wrap). Once the user types, the full pool joins in including the 80+ component inserts, ranked by score: prefix matches outrank in-word substring hits, ties break alphabetically. The match logic is pure and unit-tested in `command_palette_test`. ArrowUp / ArrowDown move the active row, Enter runs it, Esc closes, clicking a row also runs. The active command runs on a microtask so modal mutations finish before the action mutates state — important for actions that move focus or open another modal. shortcuts.cljs grew a Cmd-K dispatch arm and a set-show-command-palette! callback (mirroring set-show-shortcuts) that main/init wires to command-palette/toggle!. Shared chrome thunks (theme / templates / welcome-tour) flow through command-palette/install! so the palette and the toolbar can't drift on what those buttons do. Action helpers in shortcuts.cljs (duplicate!, wrap-in!, copy-attrs!, paste-attrs!) lose their defn- and become public so the palette reuses them instead of duplicating the selection-commit-reselect flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calling .focus() on the x-search-field host did nothing because the component doesn't delegateFocus. x-modal's own focus trap also landed on the host, so opening Cmd-K left the user one Tab away from typing. show! now walks into the field's shadow root on rAF and calls focus() on the real <input part="input">. A retry loop handles the case where the shadow root isn't populated yet — same pattern as the datalist clone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hand-rolled modal lost focus to x-modal's focus trap and to
the x-search-field's host-vs-shadow-input split. Switching to the
x-command-palette BareDOM component lets the component own the
modal chrome, scrim, focus, fuzzy filter, ARIA roles, and keyboard
navigation — all the parts we were re-implementing — and inherits
the active theme through its own shadow CSS.
This namespace is now just the command catalogue and a thin
dispatch shim. all-commands maps each command into the JS shape
the component consumes (id, label, group, keywords); a synthetic
id ('cmd-N') indexes a Clojure id→run! map kept on modal-state so
the select event resolves back to the right thunk. Curated File /
View / Selection commands sit alongside the four wrap-in choices
and one entry per registered BareDOM tag, so 'btn' or 'card'
finds the right insert without typing the full x- name.
show! refreshes the items property and calls .open() on the host;
hide! calls .close(); toggle! flips between the two. The component
emits x-command-palette-select with the chosen item; we look up
its id and run the thunk on a microtask so the palette's own close
work finishes first.
The hand-rolled filter / row-list / focus retry / index state and
the matching command-palette CSS are gone. The pure
filter-commands unit tests are dropped along with them — the
component's filter is BareDOM's responsibility now, covered by its
own tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The layers list becomes a focus stop (tabindex=0). With it focused, ArrowUp/Down walk the depth-first row order, ArrowLeft selects the parent, ArrowRight enters the first child. Alt+Up / Alt+Down reorder the selection within its parent slot via ops/move. The keydown handler stopPropagation's so the document-level shortcut layer doesn't also fire — an arrow on a free-placed selection no longer simultaneously navigates and nudges. The walk and reorder logic land as pure helpers nav-target / reorder-target so a node test covers the boundaries (top of list, root, leaf, edge of slot). Existing flatten-tree feeds them. shortcut-info gains two rows under :navigation describing the gesture so the cheat sheet picks them up automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first cut traversed the depth-first row list, so pressing Down on a parent stepped into its first child instead of moving to the next sibling. Re-reading the M3.2 plan calls for sibling-only navigation: Up/Down stay inside the parent slot, ArrowRight is the explicit "step into children" gesture, and ArrowLeft jumps to the parent. That matches the standard tree-nav idiom (Finder, VS Code explorer) and keeps each arrow direction's role distinct. nav-target's :prev / :next branches now read parent-of and the parent's slot vector instead of indexing into the flattened rows list. Edge cases (already at slot boundary, root, leaf) return nil so the keydown handler is a no-op there. Tests rewritten to assert sibling boundaries and that Down on a parent does NOT enter its children. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bareforge.meta.patterns is a per-tag map of named pre-styled
configurations (`{:id :label :overrides}`). Each :overrides map
matches the shape palette/seed-for-tag returns, so a pattern lands
in the document by passing it straight to ops/insert-new — no new
plumbing in ops or the reconciler.
Coverage starts with the most-reached-for tags: x-button gets
primary/secondary/ghost/danger/loading; x-typography spans
h1/h2/h3/body/caption/code; x-alert and x-badge cover their
type/variant axes; x-card carries elevation + interactive variants;
x-grid offers 2/3/4-col and sidebar layouts; x-divider, x-switch,
x-checkbox, x-chip round out the seed set. A coverage warning
prints uncovered tags in test output without failing — gaps stay
visible without blocking BareDOM bumps.
Palette tiles for tags with patterns grow a `▾` caret that toggles
an inline flyout of pattern chips. Clicking a chip inserts the
pattern's overrides via the new 2-arity insert-at-selection!.
Pointerdown on caret/chip stopPropagation's so the surrounding
tile's drag-arm doesn't compete with the click.
The 1-arity insert-at-selection! (used by the dnd palette tap path
and the Cmd-K command palette) is unchanged, so no caller migration
is needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bareforge.meta.hints carries a curated string for each container
that's specific enough to suggest what belongs inside ("Drop nav
links / actions" inside an empty x-navbar, "Drop tiles into the
grid" inside x-grid, "Drop x-tab here" inside x-tabs, etc.). Tags
without a curated hint fall back to the existing generic
`<tag> (empty)` placeholder.
The canvas reconciler stamps each container's chosen hint onto a
`data-bareforge-hint` attribute on creation. A new CSS rule
`[data-bareforge-container][data-bareforge-hint]:empty::before`
overrides the generic placeholder when the attribute is present —
specificity wins, no rule duplication. Drag-time slot strips and
preview-mode invisibility both fall out of the existing rules.
While here, drop x-flex from the wrap whitelist (shortcuts +
command palette) — BareDOM 2.4 doesn't ship that tag, so the
Cmd-Shift-G prompt and palette would silently insert a
non-rendering element. x-navbar takes its place as the fourth
wrap target.
Coverage test asserts every hinted tag is registered and that
hint strings stay under the 28-char footprint cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an "Authoring shortcuts" section between Features and Fields & bindings, grouping the new editor surfaces by user concern (multi-select & bulk ops / inspector ergonomics / discoverability) rather than by milestone. Each bullet leads with the gesture or keystroke so a reader scanning for "how do I…" finds it fast. Updates the Status line's test count from 486 to 571 to reflect the gates after this branch's work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a detailed Added / Changed log under [Unreleased]. Grouped by user-visible feature so a release reader can match each entry to a README bullet without diffing the source. Calls out the three Changed items that ship as side-effects of new features (selection shape, public action helpers, wrap whitelist) and notes that none of them touches the document model, project file format, or any export plugin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rebase resolution at 51c6cbc kept the older 2-branch render! shape (implicit-truthy `[(empty-view)]` as the final cond clause), but a later replayed commit (M2.3) added a third branch. With the implicit form, cond ended up with an odd number of forms — caught immediately by clj-kondo. Restoring the explicit `:else` keeps the 3-branch shape that landed mid-feature originally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's cljfmt check caught five files with formatting drift in the rebased commits — mostly inconsistent destructure spacing and column-aligned key/value pairs that cljfmt 0.16's defaults re-align. Pure whitespace, no semantic change. Verified locally: - cljfmt check — All source files formatted correctly - npx shadow-cljs compile test — 571 tests, 0 failures - npx shadow-cljs release app — 0 warnings - clj-kondo — 0 warnings, 0 errors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
avanelsas
added a commit
that referenced
this pull request
Apr 29, 2026
Promotes the [Unreleased] section in CHANGELOG.md to a tagged [0.2.0] — 2026-04-29 entry, adds the standard ### Verified block, and refreshes the comparison links at the bottom. package.json's version bumps 0.1.1 → 0.2.0 to match. This is a minor (not patch) release because the work in PR #11 adds substantial editor surfaces (multi-select, inspector multi- edit, drag-to-scrub, copy/paste attributes, var() autocomplete, ? cheat sheet, Cmd-K command palette, layers keyboard nav, palette pattern flyout, per-tag empty-slot hints). No breaking changes — saved project files load identically and every export plugin stays at full feature parity. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
24 commits of editor authoring quality-of-life. No document-model,
project-file, or export changes — saved projects load identically and
every export plugin stays at full feature parity. Test count moves from
486 → 571; release build remains warning-free under Closure Advanced.
The README's new Authoring shortcuts section and the CHANGELOG's
[Unreleased]entry have the user-facing detail. Highlights:Shift extend). Inspector renders the shared-attribute set; one edit
fans out to every selected node in a single undo step.
x-container/Cmd-Shift-G wrap with prompt / Cmd-Opt-C / Cmd-Opt-V copy
and paste attributes (filtered to the target tag's supported attrs).
var(--x-…)autocomplete on colour and length fields via shadow-DOM datalist
injection.
?cheat sheet (x-modal+x-typography) and Cmd-Kcommand palette (built on BareDOM's
x-command-palette) — everyshortcut and chrome action one keystroke away, theme-inheriting.
Alt+↑/↓ reorder).
x-button/x-typography/x-alert/
x-card/x-grid/ … grow a▾caret with pre-styled chips.Drop nav links / actions,Drop tiles into the grid, etc.The only change-not-feature worth calling out:
:selectioninapp-statebecomes a vector of node ids (was{:id …}map).Internal-only refactor; saved project files are unchanged. The
wrap-in whitelist drops
x-flex(not in BareDOM 2.4) and gainsx-navbar.Test plan
npx shadow-cljs compile test— 571 tests, 0 failures.npx shadow-cljs release app— 0 warnings under Closure Advanced.clj-kondo --lint src test scripts— 0 warnings, 0 errors.🤖 Generated with Claude Code