From d59ed0fd56e863e628841a4819411ba7e1b76d78 Mon Sep 17 00:00:00 2001 From: vanelsas <58037137+avanelsas@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:35:47 +0200 Subject: [PATCH 1/3] fix(canvas): empty table cells stretch to the row height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic empty-container affordance (`.canvas-host [data-bareforge-container]:empty`) gives childless containers a visible, droppable footprint via `min-height`, `min-width: 120px` and `margin: 4px 0`. On an empty `x-table-cell` that margin breaks the table layout: the cell is a subgrid grid item, and a stretched grid item fills `track − margins`, so an empty cell beside populated siblings renders ~8px shorter and vertically centred instead of filling the row. The `min-width: 120px` also fights the table's equal-width column tracks. Override both for empty table cells (`margin: 0; min-width: 0`) so the cell stretches to the row height and follows its column track. The affordance's `min-height` floor and dashed `(empty)` label survive, so an all-empty row keeps a droppable footprint while subgrid stretch wins the moment a sibling makes the row taller. Editor-only (`.canvas-host`); the export is unaffected. Extends the adjacent editor-only table-fallback CSS on this branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- public/index.html | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/public/index.html b/public/index.html index 471e047..a31b551 100644 --- a/public/index.html +++ b/public/index.html @@ -1190,6 +1190,57 @@ .canvas-host [data-bareforge-container][data-bareforge-hint]:empty::before { content: attr(data-bareforge-hint); } + /* --- editor-only table fallback (no `columns` configured) -------- */ + /* x-table is `display:grid` and x-table-row is + `grid-template-columns: subgrid`, both driven by the table's + `columns` attribute. With no `columns`: + - the table falls back to *implicit* tracks that size to + max-content, so its cells overflow the page box and ignore the + container's padding; and + - the row's subgrid collapses to one column, stacking cells + vertically. + Until `columns` is configured (it's inspector-editable), give the + table one shrinkable full-width track so it fits the container, and + flow each row's cells into equal auto columns so they read + horizontally. The moment the user sets `columns`, neither rule + matches and the real subgrid takes over — on the canvas and in the + export. Editor-only (.canvas-host). */ + .canvas-host x-table:not([columns]), + .canvas-host x-table[columns=""] { + grid-template-columns: minmax(0, 1fr) !important; + } + .canvas-host x-table:not([columns]) x-table-row, + .canvas-host x-table[columns=""] x-table-row { + grid-template-columns: none !important; + grid-auto-flow: column !important; + grid-auto-columns: minmax(0, 1fr) !important; + } + /* The table/row/cell are nested grid items, and a grid item's default + `min-width: auto` (its content's min-content) blocks shrinking — so + populated cells refuse to fit and the row overflows the page box. + Let all three levels shrink to their track. Scoped to `:not(:empty)` + so EMPTY table elements keep the empty-container affordance's + `min-width: 120px` + `min-height: 80px` — otherwise an empty table + collapses in a flex/grid parent and has no droppable footprint to + drop the first row into. */ + .canvas-host x-table:not(:empty), + .canvas-host x-table-row:not(:empty), + .canvas-host x-table-cell:not(:empty) { min-width: 0 !important; } + /* An EMPTY cell beside populated siblings is still a subgrid (or + auto-flow) grid item, not a standalone drop zone. The generic + empty-container affordance adds `margin: 4px 0`, and a stretched grid + item fills `track − margins` — so the empty cell renders ~8px shorter + than its populated siblings and sits vertically centred (4px gap top + and bottom) instead of filling the row. Its `min-width: 120px` also + fights the table's equal-width column tracks. Zero both for empty + table cells: the cell then stretches to the row height and follows its + column track, while the affordance's `min-height` floor + dashed + (empty) label survive so an all-empty row keeps a droppable footprint + (stretch wins whenever a sibling makes the row taller). */ + .canvas-host x-table-cell[data-bareforge-container]:empty { + margin: 0; + min-width: 0; + } /* Containers that host an absolute :background child get this class from the reconciler so they form a positioned ancestor without touching their own inline style. `isolation: isolate` From 845b486b2bc60a77f2b33d0c27048b918c55d31d Mon Sep 17 00:00:00 2001 From: vanelsas <58037137+avanelsas@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:41:24 +0200 Subject: [PATCH 2/3] fix(canvas): make table & structural containers droppable in the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several BareDOM container components collapsed to ~0 height when empty on the canvas, so they had no footprint to drop children into and couldn't be authored. Register slot metadata and seed sensible defaults so they behave like first-class containers in edit mode. - meta/slots.cljs: register slots for x-table / x-table-row / x-table-cell and a batch of other structural containers (x-bento-grid, x-fieldset, x-form, x-collapse, x-tabs, x-carousel, x-timeline(+item), x-scroll*, …) that previously fell through to the leaf default and got no container affordance. - meta/augment.cljs + ui/palette.cljs: x-table's `columns` attribute is a grid-template-columns track list (its subgrid rows inherit it). Drive the inspector field through the :grid-columns transform (edit a count, store a real track list) and seed a dropped table with `repeat(3, 1fr)` — same shape/convention as x-grid — so a fresh table has a real, editable column config instead of relying on the canvas auto-flow fallback. - render/canvas_view.cljs: arm canvas-pan from the composed-path target, not `.-target`. Inspector fields are custom elements (x-search-field, x-text-area) whose real input lives in shadow DOM; a keydown retargeted to the host made the editable-widget check miss, swallowing spaces typed into inspector fields. Extracted editable-target? as a pure, tested fn. Tests added for slot metadata, the columns transform/default, and the space-pan editable-target guard. Pairs with the empty-table-cell stretch CSS in the previous commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/bareforge/meta/augment.cljs | 6 +- src/bareforge/meta/slots.cljs | 72 +++++++++++++++++++++ src/bareforge/render/canvas_view.cljs | 29 +++++++-- src/bareforge/ui/palette.cljs | 7 ++ test/bareforge/meta/registry_test.cljs | 10 ++- test/bareforge/render/canvas_view_test.cljs | 25 +++++++ test/bareforge/render/slot_strips_test.cljs | 14 +++- test/bareforge/ui/inspector_test.cljs | 9 +++ test/bareforge/ui/palette_test.cljs | 2 + 9 files changed, 164 insertions(+), 10 deletions(-) diff --git a/src/bareforge/meta/augment.cljs b/src/bareforge/meta/augment.cljs index 979e728..0ed0ab5 100644 --- a/src/bareforge/meta/augment.cljs +++ b/src/bareforge/meta/augment.cljs @@ -653,7 +653,11 @@ {:category :data :label "Table" :properties - [{:name "columns" :kind :number} + ;; `columns` is a grid-template-columns track list; the :grid-columns + ;; transform lets the inspector show/edit a simple count (3) while the + ;; stored attribute stays a real track layout ("repeat(3, 1fr)"). + ;; Matches x-grid's columns field. + [{:name "columns" :kind :number :transform :grid-columns} {:name "row-count" :kind :number} {:name "caption" :kind :string-short} {:name "selectable" :kind :enum :choices ["none" "single" "multi"] diff --git a/src/bareforge/meta/slots.cljs b/src/bareforge/meta/slots.cljs index dd89774..22afe91 100644 --- a/src/bareforge/meta/slots.cljs +++ b/src/bareforge/meta/slots.cljs @@ -28,6 +28,78 @@ "x-grid" [{:name "default" :label "Items" :multiple? true}] + "x-table" + [{:name "default" :label "Rows" :multiple? true}] + + "x-table-row" + [{:name "default" :label "Cells" :multiple? true}] + + "x-table-cell" + [{:name "default" :label "Content" :multiple? true} + {:name "sort-icon" :label "Sort icon" :multiple? false}] + + ;; --- structural containers that collapse to ~0 height when empty --- + ;; Without an entry these fall through to the leaf default, so they get + ;; no container affordance and can't be dropped into. They all hold + ;; child components, so register a multiple-child slot. + "x-bento-grid" + [{:name "default" :label "Items" :multiple? true}] + + "x-bento-item" + [{:name "default" :label "Content" :multiple? true}] + + "x-fieldset" + [{:name "default" :label "Fields" :multiple? true}] + + "x-form" + [{:name "default" :label "Fields" :multiple? true}] + + "x-collapse" + [{:name "default" :label "Content" :multiple? true}] + + "x-tabs" + [{:name "default" :label "Tabs" :multiple? true}] + + "x-carousel" + [{:name "default" :label "Slides" :multiple? true}] + + "x-breadcrumbs" + [{:name "default" :label "Items" :multiple? true}] + + "x-timeline" + [{:name "default" :label "Items" :multiple? true}] + + "x-timeline-item" + [{:name "label" :label "Label" :multiple? false} + {:name "icon" :label "Icon" :multiple? false} + {:name "default" :label "Content" :multiple? true} + {:name "actions" :label "Actions" :multiple? true}] + + "x-spotlight-card" + [{:name "default" :label "Content" :multiple? true}] + + "x-morph-stack" + [{:name "state" :label "States" :multiple? true}] + + "x-proximity-list" + [{:name "default" :label "Items" :multiple? true}] + + "x-scroll" + [{:name "default" :label "Content" :multiple? true}] + + "x-scroll-stack" + [{:name "default" :label "Content" :multiple? true}] + + "x-scroll-story" + [{:name "media" :label "Media" :multiple? false} + {:name "default" :label "Steps" :multiple? true}] + + "x-scroll-timeline" + [{:name "default" :label "Content" :multiple? true}] + + "x-scroll-parallax" + [{:name "default" :label "Content" :multiple? true}] + "x-split-pane" [{:name "start" :label "Start panel" :multiple? true} {:name "end" :label "End panel" :multiple? true}] diff --git a/src/bareforge/render/canvas_view.cljs b/src/bareforge/render/canvas_view.cljs index 15874d2..0f3becf 100644 --- a/src/bareforge/render/canvas_view.cljs +++ b/src/bareforge/render/canvas_view.cljs @@ -211,21 +211,38 @@ :pan-y (+ (unchecked-get pan-state "start-pan-y") dy))] (state/set-canvas-view! view')))) +(defn editable-target? + "Pure: true when `el` is a text-editable widget (native input/ + textarea/select or a contenteditable host). Used to decide whether + a Space keystroke is the user typing vs. arming the canvas pan." + [^js el] + (boolean + (and el + (or (.-isContentEditable el) + (contains? #{"input" "textarea" "select"} + (some-> el .-tagName .toLowerCase)))))) + (defn- on-keydown! "Track Space so a subsequent left-button drag pans. Ignored when the keystroke targets an editable widget — typing a space into an inspector field must not arm pan. Preview mode is also ignored so - spaces typed into the rendered page stay spaces." + spaces typed into the rendered page stay spaces. + + Reads the deepest composed-path node, not `.-target`: inspector + fields are custom elements (x-search-field, x-text-area) whose real + input lives in shadow DOM. A keydown bubbling to window is retargeted + to the host, so `.-target` would be the custom-element tag and the + editable check would miss it — swallowing the space." [^js e] (when (and (= " " (.-key e)) (not (.-repeat e)) (not (space-down?)) (edit-mode?)) - (let [^js t (.-target e) - tag (some-> t .-tagName .toLowerCase) - ce? (and t (.-isContentEditable t))] - (when-not (or ce? - (contains? #{"input" "textarea" "select"} tag)) + (let [^js path (.composedPath e) + ^js t (if (and path (pos? (.-length path))) + (aget path 0) + (.-target e))] + (when-not (editable-target? t) (.preventDefault e) (unchecked-set pan-state "space-down?" true) (update-host-cursor!))))) diff --git a/src/bareforge/ui/palette.cljs b/src/bareforge/ui/palette.cljs index 081b918..83c7aae 100644 --- a/src/bareforge/ui/palette.cljs +++ b/src/bareforge/ui/palette.cljs @@ -107,6 +107,13 @@ ;; integer-string coercion exists as a safety net for legacy ;; docs, not as the authoring default. "x-grid" {:attrs {"columns" "repeat(3, 1fr)"}} + ;; x-table is a CSS grid; its `columns` attr is a grid-template-columns + ;; track list that its subgrid rows inherit. Seed a default 3-column + ;; layout (same shape/convention as x-grid) so a dropped table has a + ;; real, inspector-editable column config — its `columns` field shows + ;; "3" via the :grid-columns transform — instead of an empty field and + ;; cells that depend on the canvas auto-flow fallback. + "x-table" {:attrs {"columns" "repeat(3, 1fr)"}} ;; Overlay components default to `open=false`, so a bare drop renders ;; them closed — zero visible footprint on the canvas (collapsed ;; height, full-width host). Seed them open so they're visible and diff --git a/test/bareforge/meta/registry_test.cljs b/test/bareforge/meta/registry_test.cljs index f88afba..88d0b21 100644 --- a/test/bareforge/meta/registry_test.cljs +++ b/test/bareforge/meta/registry_test.cljs @@ -146,7 +146,15 @@ (deftest container-true-for-registered-multi-slot-tags (doseq [tag ["x-container" "x-grid" "x-card" "x-navbar" - "x-modal" "x-drawer" "x-popover" "x-sidebar"]] + "x-modal" "x-drawer" "x-popover" "x-sidebar" + "x-table" "x-table-row" "x-table-cell" + ;; structural containers registered so they're droppable + "x-bento-grid" "x-bento-item" "x-fieldset" "x-form" + "x-collapse" "x-tabs" "x-carousel" "x-breadcrumbs" + "x-timeline" "x-timeline-item" "x-spotlight-card" + "x-morph-stack" "x-proximity-list" "x-scroll" + "x-scroll-stack" "x-scroll-story" "x-scroll-timeline" + "x-scroll-parallax"]] (is (true? (r/container? tag)) (str tag " should be a container")))) diff --git a/test/bareforge/render/canvas_view_test.cljs b/test/bareforge/render/canvas_view_test.cljs index 9f42a37..cfd20ee 100644 --- a/test/bareforge/render/canvas_view_test.cljs +++ b/test/bareforge/render/canvas_view_test.cljs @@ -97,3 +97,28 @@ (is (= "83%" (cv/format-zoom-percent 0.834))) (is (= "25%" (cv/format-zoom-percent cv/min-zoom))) (is (= "400%" (cv/format-zoom-percent cv/max-zoom)))) + +;; --- editable-target? ---------------------------------------------------- + +(deftest editable-target-recognises-native-inputs + (testing "Native text-editable tags arm typing, not pan" + (is (true? (cv/editable-target? #js {:tagName "INPUT"}))) + (is (true? (cv/editable-target? #js {:tagName "TEXTAREA"}))) + (is (true? (cv/editable-target? #js {:tagName "SELECT"}))) + (is (true? (cv/editable-target? #js {:tagName "input"})) + "tag match is case-insensitive") + (is (true? (cv/editable-target? #js {:isContentEditable true + :tagName "DIV"})) + "contenteditable host counts as editable"))) + +(deftest editable-target-rejects-non-editables + (testing "Non-editable targets fall through to the pan gesture" + (is (false? (cv/editable-target? #js {:tagName "DIV"}))) + (is (false? (cv/editable-target? nil))) + ;; The crux of the bug: a keydown bubbling out of an inspector + ;; field's shadow DOM is retargeted to the custom-element host. + ;; That host must still be treated as non-editable here — the real + ;; editable check happens against composedPath()[0] (the inner + ;; native input), not this retargeted host. + (is (false? (cv/editable-target? #js {:tagName "X-SEARCH-FIELD"}))) + (is (false? (cv/editable-target? #js {:tagName "X-TEXT-AREA"}))))) diff --git a/test/bareforge/render/slot_strips_test.cljs b/test/bareforge/render/slot_strips_test.cljs index ad1880e..3f45f1e 100644 --- a/test/bareforge/render/slot_strips_test.cljs +++ b/test/bareforge/render/slot_strips_test.cljs @@ -11,15 +11,25 @@ ;; strip with a label). x-navbar / x-modal / x-drawer / x-popover / ;; x-split-pane are multi-slot containers (they get N subdivisions). ;; x-sidebar is a single-slot container (one full-size strip). + ;; x-table / x-table-row / x-table-cell and the structural containers + ;; (bento, fieldset, form, collapse, tabs, carousel, breadcrumbs, + ;; timeline, scroll-*, …) each carry a :multiple? slot and qualify. ;; Leaves like x-button / x-typography / x-icon have no :multiple? ;; slot, so `classify-position` never returns :inside for them and ;; the strips would be dead zones — `render-strips?` excludes them. (let [all-tags (keys slots/slots) qualifiers (set (filter ss/render-strips? all-tags))] (is (= #{"x-card" "x-grid" "x-container" "x-sidebar" - "x-navbar" "x-modal" "x-drawer" "x-popover" "x-split-pane"} + "x-navbar" "x-modal" "x-drawer" "x-popover" "x-split-pane" + "x-table" "x-table-row" "x-table-cell" + "x-bento-grid" "x-bento-item" "x-fieldset" "x-form" + "x-collapse" "x-tabs" "x-carousel" "x-breadcrumbs" + "x-timeline" "x-timeline-item" "x-spotlight-card" + "x-morph-stack" "x-proximity-list" "x-scroll" + "x-scroll-stack" "x-scroll-story" "x-scroll-timeline" + "x-scroll-parallax"} qualifiers) - "nine container tags qualify; leaves (x-button / x-typography / x-icon) do not"))) + "all registered multi-child containers qualify; leaves (x-button / x-typography / x-icon) do not"))) (testing "unknown tags fall through to single-slot default and return false" (is (false? (ss/render-strips? "x-made-up-tag"))) (is (false? (ss/render-strips? nil))) diff --git a/test/bareforge/ui/inspector_test.cljs b/test/bareforge/ui/inspector_test.cljs index 423ed91..fc516cf 100644 --- a/test/bareforge/ui/inspector_test.cljs +++ b/test/bareforge/ui/inspector_test.cljs @@ -61,6 +61,15 @@ (let [node {:id "n" :tag "x-button" :attrs {} :props {}}] (is (nil? (model/current-value node {:name "variant" :kind :enum}))))) +(deftest grid-columns-transform-round-trips-table-columns + (testing "a seeded track layout shows as a plain count in the number field" + (let [node {:id "n" :tag "x-table" :attrs {"columns" "repeat(3, 1fr)"} :props {}}] + (is (= "3" (model/current-value + node {:name "columns" :kind :number :transform :grid-columns}))))) + (testing "a typed count commits as a track layout, custom lists pass through" + (is (= "repeat(5, 1fr)" (model/transform-for-commit :grid-columns "5"))) + (is (= "2fr 1fr 1fr" (model/transform-for-commit :grid-columns "2fr 1fr 1fr"))))) + ;; --- inspector-model ------------------------------------------------------ (deftest inspector-model-nil-without-selection diff --git a/test/bareforge/ui/palette_test.cljs b/test/bareforge/ui/palette_test.cljs index ad794e4..da25def 100644 --- a/test/bareforge/ui/palette_test.cljs +++ b/test/bareforge/ui/palette_test.cljs @@ -64,6 +64,8 @@ (str tag " should seed open=\"\"")))) (testing "sidebar seeds full width so a flow drop is a droppable block" (is (= "100%" (get-in (p/seed-for-tag "x-sidebar") [:layout :width])))) + (testing "table seeds a default 3-column track layout" + (is (= "repeat(3, 1fr)" (get-in (p/seed-for-tag "x-table") [:attrs "columns"])))) (testing "unknown tags get empty overrides" (is (= {} (p/seed-for-tag "x-some-unknown"))))) From 10f2d73860ad7c6fc0ccb79b3ea0496055de3030 Mon Sep 17 00:00:00 2001 From: vanelsas <58037137+avanelsas@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:44:47 +0200 Subject: [PATCH 3/3] fix(export): stop Closure parsing @vanelsas as a JSDoc tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `manifest-url`'s docstring wrapped its example CDN URL right before `@vanelsas/baredom`, so the `@` opened a line and Closure Advanced parsed it as an unknown JSDoc annotation, emitting a parse note on every release build. Rewrap so `@` follows the `/` on the same line — note gone, release build is clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/bareforge/export/integrity.cljs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bareforge/export/integrity.cljs b/src/bareforge/export/integrity.cljs index 9043e34..9d9204d 100644 --- a/src/bareforge/export/integrity.cljs +++ b/src/bareforge/export/integrity.cljs @@ -43,9 +43,9 @@ ;; --- pure: URL composition ---------------------------------------------- (defn manifest-url - "Compose the URL of BareDOM's integrity manifest at version `v` on - the CDN rooted at `cdn-base` (e.g. `https://cdn.jsdelivr.net/npm/ - @vanelsas/baredom`)." + "Compose the URL of BareDOM's integrity manifest at version `v` on the + CDN rooted at `cdn-base` + (e.g. `https://cdn.jsdelivr.net/npm/@vanelsas/baredom`)." [cdn-base v] (str cdn-base "@" v "/dist/integrity.json"))