Skip to content

Commit 0c48bbe

Browse files
avanelsasclaude
andauthored
fix(canvas): make table & structural containers droppable; empty table cells fill the row (#66)
* fix(canvas): empty table cells stretch to the row height 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) <noreply@anthropic.com> * fix(canvas): make table & structural containers droppable in the editor 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) <noreply@anthropic.com> * fix(export): stop Closure parsing @vanelsas as a JSDoc tag `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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2852e66 commit 0c48bbe

11 files changed

Lines changed: 218 additions & 13 deletions

File tree

public/index.html

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,57 @@
11901190
.canvas-host [data-bareforge-container][data-bareforge-hint]:empty::before {
11911191
content: attr(data-bareforge-hint);
11921192
}
1193+
/* --- editor-only table fallback (no `columns` configured) -------- */
1194+
/* x-table is `display:grid` and x-table-row is
1195+
`grid-template-columns: subgrid`, both driven by the table's
1196+
`columns` attribute. With no `columns`:
1197+
- the table falls back to *implicit* tracks that size to
1198+
max-content, so its cells overflow the page box and ignore the
1199+
container's padding; and
1200+
- the row's subgrid collapses to one column, stacking cells
1201+
vertically.
1202+
Until `columns` is configured (it's inspector-editable), give the
1203+
table one shrinkable full-width track so it fits the container, and
1204+
flow each row's cells into equal auto columns so they read
1205+
horizontally. The moment the user sets `columns`, neither rule
1206+
matches and the real subgrid takes over — on the canvas and in the
1207+
export. Editor-only (.canvas-host). */
1208+
.canvas-host x-table:not([columns]),
1209+
.canvas-host x-table[columns=""] {
1210+
grid-template-columns: minmax(0, 1fr) !important;
1211+
}
1212+
.canvas-host x-table:not([columns]) x-table-row,
1213+
.canvas-host x-table[columns=""] x-table-row {
1214+
grid-template-columns: none !important;
1215+
grid-auto-flow: column !important;
1216+
grid-auto-columns: minmax(0, 1fr) !important;
1217+
}
1218+
/* The table/row/cell are nested grid items, and a grid item's default
1219+
`min-width: auto` (its content's min-content) blocks shrinking — so
1220+
populated cells refuse to fit and the row overflows the page box.
1221+
Let all three levels shrink to their track. Scoped to `:not(:empty)`
1222+
so EMPTY table elements keep the empty-container affordance's
1223+
`min-width: 120px` + `min-height: 80px` — otherwise an empty table
1224+
collapses in a flex/grid parent and has no droppable footprint to
1225+
drop the first row into. */
1226+
.canvas-host x-table:not(:empty),
1227+
.canvas-host x-table-row:not(:empty),
1228+
.canvas-host x-table-cell:not(:empty) { min-width: 0 !important; }
1229+
/* An EMPTY cell beside populated siblings is still a subgrid (or
1230+
auto-flow) grid item, not a standalone drop zone. The generic
1231+
empty-container affordance adds `margin: 4px 0`, and a stretched grid
1232+
item fills `track − margins` — so the empty cell renders ~8px shorter
1233+
than its populated siblings and sits vertically centred (4px gap top
1234+
and bottom) instead of filling the row. Its `min-width: 120px` also
1235+
fights the table's equal-width column tracks. Zero both for empty
1236+
table cells: the cell then stretches to the row height and follows its
1237+
column track, while the affordance's `min-height` floor + dashed
1238+
(empty) label survive so an all-empty row keeps a droppable footprint
1239+
(stretch wins whenever a sibling makes the row taller). */
1240+
.canvas-host x-table-cell[data-bareforge-container]:empty {
1241+
margin: 0;
1242+
min-width: 0;
1243+
}
11931244
/* Containers that host an absolute :background child get this
11941245
class from the reconciler so they form a positioned ancestor
11951246
without touching their own inline style. `isolation: isolate`

src/bareforge/export/integrity.cljs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@
4343
;; --- pure: URL composition ----------------------------------------------
4444

4545
(defn manifest-url
46-
"Compose the URL of BareDOM's integrity manifest at version `v` on
47-
the CDN rooted at `cdn-base` (e.g. `https://cdn.jsdelivr.net/npm/
48-
@vanelsas/baredom`)."
46+
"Compose the URL of BareDOM's integrity manifest at version `v` on the
47+
CDN rooted at `cdn-base`
48+
(e.g. `https://cdn.jsdelivr.net/npm/@vanelsas/baredom`)."
4949
[cdn-base v]
5050
(str cdn-base "@" v "/dist/integrity.json"))
5151

src/bareforge/meta/augment.cljs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,11 @@
653653
{:category :data
654654
:label "Table"
655655
:properties
656-
[{:name "columns" :kind :number}
656+
;; `columns` is a grid-template-columns track list; the :grid-columns
657+
;; transform lets the inspector show/edit a simple count (3) while the
658+
;; stored attribute stays a real track layout ("repeat(3, 1fr)").
659+
;; Matches x-grid's columns field.
660+
[{:name "columns" :kind :number :transform :grid-columns}
657661
{:name "row-count" :kind :number}
658662
{:name "caption" :kind :string-short}
659663
{:name "selectable" :kind :enum :choices ["none" "single" "multi"]

src/bareforge/meta/slots.cljs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,78 @@
2828
"x-grid"
2929
[{:name "default" :label "Items" :multiple? true}]
3030

31+
"x-table"
32+
[{:name "default" :label "Rows" :multiple? true}]
33+
34+
"x-table-row"
35+
[{:name "default" :label "Cells" :multiple? true}]
36+
37+
"x-table-cell"
38+
[{:name "default" :label "Content" :multiple? true}
39+
{:name "sort-icon" :label "Sort icon" :multiple? false}]
40+
41+
;; --- structural containers that collapse to ~0 height when empty ---
42+
;; Without an entry these fall through to the leaf default, so they get
43+
;; no container affordance and can't be dropped into. They all hold
44+
;; child components, so register a multiple-child slot.
45+
"x-bento-grid"
46+
[{:name "default" :label "Items" :multiple? true}]
47+
48+
"x-bento-item"
49+
[{:name "default" :label "Content" :multiple? true}]
50+
51+
"x-fieldset"
52+
[{:name "default" :label "Fields" :multiple? true}]
53+
54+
"x-form"
55+
[{:name "default" :label "Fields" :multiple? true}]
56+
57+
"x-collapse"
58+
[{:name "default" :label "Content" :multiple? true}]
59+
60+
"x-tabs"
61+
[{:name "default" :label "Tabs" :multiple? true}]
62+
63+
"x-carousel"
64+
[{:name "default" :label "Slides" :multiple? true}]
65+
66+
"x-breadcrumbs"
67+
[{:name "default" :label "Items" :multiple? true}]
68+
69+
"x-timeline"
70+
[{:name "default" :label "Items" :multiple? true}]
71+
72+
"x-timeline-item"
73+
[{:name "label" :label "Label" :multiple? false}
74+
{:name "icon" :label "Icon" :multiple? false}
75+
{:name "default" :label "Content" :multiple? true}
76+
{:name "actions" :label "Actions" :multiple? true}]
77+
78+
"x-spotlight-card"
79+
[{:name "default" :label "Content" :multiple? true}]
80+
81+
"x-morph-stack"
82+
[{:name "state" :label "States" :multiple? true}]
83+
84+
"x-proximity-list"
85+
[{:name "default" :label "Items" :multiple? true}]
86+
87+
"x-scroll"
88+
[{:name "default" :label "Content" :multiple? true}]
89+
90+
"x-scroll-stack"
91+
[{:name "default" :label "Content" :multiple? true}]
92+
93+
"x-scroll-story"
94+
[{:name "media" :label "Media" :multiple? false}
95+
{:name "default" :label "Steps" :multiple? true}]
96+
97+
"x-scroll-timeline"
98+
[{:name "default" :label "Content" :multiple? true}]
99+
100+
"x-scroll-parallax"
101+
[{:name "default" :label "Content" :multiple? true}]
102+
31103
"x-split-pane"
32104
[{:name "start" :label "Start panel" :multiple? true}
33105
{:name "end" :label "End panel" :multiple? true}]

src/bareforge/render/canvas_view.cljs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,21 +211,38 @@
211211
:pan-y (+ (unchecked-get pan-state "start-pan-y") dy))]
212212
(state/set-canvas-view! view'))))
213213

214+
(defn editable-target?
215+
"Pure: true when `el` is a text-editable widget (native input/
216+
textarea/select or a contenteditable host). Used to decide whether
217+
a Space keystroke is the user typing vs. arming the canvas pan."
218+
[^js el]
219+
(boolean
220+
(and el
221+
(or (.-isContentEditable el)
222+
(contains? #{"input" "textarea" "select"}
223+
(some-> el .-tagName .toLowerCase))))))
224+
214225
(defn- on-keydown!
215226
"Track Space so a subsequent left-button drag pans. Ignored when the
216227
keystroke targets an editable widget — typing a space into an
217228
inspector field must not arm pan. Preview mode is also ignored so
218-
spaces typed into the rendered page stay spaces."
229+
spaces typed into the rendered page stay spaces.
230+
231+
Reads the deepest composed-path node, not `.-target`: inspector
232+
fields are custom elements (x-search-field, x-text-area) whose real
233+
input lives in shadow DOM. A keydown bubbling to window is retargeted
234+
to the host, so `.-target` would be the custom-element tag and the
235+
editable check would miss it — swallowing the space."
219236
[^js e]
220237
(when (and (= " " (.-key e))
221238
(not (.-repeat e))
222239
(not (space-down?))
223240
(edit-mode?))
224-
(let [^js t (.-target e)
225-
tag (some-> t .-tagName .toLowerCase)
226-
ce? (and t (.-isContentEditable t))]
227-
(when-not (or ce?
228-
(contains? #{"input" "textarea" "select"} tag))
241+
(let [^js path (.composedPath e)
242+
^js t (if (and path (pos? (.-length path)))
243+
(aget path 0)
244+
(.-target e))]
245+
(when-not (editable-target? t)
229246
(.preventDefault e)
230247
(unchecked-set pan-state "space-down?" true)
231248
(update-host-cursor!)))))

src/bareforge/ui/palette.cljs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@
107107
;; integer-string coercion exists as a safety net for legacy
108108
;; docs, not as the authoring default.
109109
"x-grid" {:attrs {"columns" "repeat(3, 1fr)"}}
110+
;; x-table is a CSS grid; its `columns` attr is a grid-template-columns
111+
;; track list that its subgrid rows inherit. Seed a default 3-column
112+
;; layout (same shape/convention as x-grid) so a dropped table has a
113+
;; real, inspector-editable column config — its `columns` field shows
114+
;; "3" via the :grid-columns transform — instead of an empty field and
115+
;; cells that depend on the canvas auto-flow fallback.
116+
"x-table" {:attrs {"columns" "repeat(3, 1fr)"}}
110117
;; Overlay components default to `open=false`, so a bare drop renders
111118
;; them closed — zero visible footprint on the canvas (collapsed
112119
;; height, full-width host). Seed them open so they're visible and

test/bareforge/meta/registry_test.cljs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,15 @@
146146

147147
(deftest container-true-for-registered-multi-slot-tags
148148
(doseq [tag ["x-container" "x-grid" "x-card" "x-navbar"
149-
"x-modal" "x-drawer" "x-popover" "x-sidebar"]]
149+
"x-modal" "x-drawer" "x-popover" "x-sidebar"
150+
"x-table" "x-table-row" "x-table-cell"
151+
;; structural containers registered so they're droppable
152+
"x-bento-grid" "x-bento-item" "x-fieldset" "x-form"
153+
"x-collapse" "x-tabs" "x-carousel" "x-breadcrumbs"
154+
"x-timeline" "x-timeline-item" "x-spotlight-card"
155+
"x-morph-stack" "x-proximity-list" "x-scroll"
156+
"x-scroll-stack" "x-scroll-story" "x-scroll-timeline"
157+
"x-scroll-parallax"]]
150158
(is (true? (r/container? tag))
151159
(str tag " should be a container"))))
152160

test/bareforge/render/canvas_view_test.cljs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,28 @@
9797
(is (= "83%" (cv/format-zoom-percent 0.834)))
9898
(is (= "25%" (cv/format-zoom-percent cv/min-zoom)))
9999
(is (= "400%" (cv/format-zoom-percent cv/max-zoom))))
100+
101+
;; --- editable-target? ----------------------------------------------------
102+
103+
(deftest editable-target-recognises-native-inputs
104+
(testing "Native text-editable tags arm typing, not pan"
105+
(is (true? (cv/editable-target? #js {:tagName "INPUT"})))
106+
(is (true? (cv/editable-target? #js {:tagName "TEXTAREA"})))
107+
(is (true? (cv/editable-target? #js {:tagName "SELECT"})))
108+
(is (true? (cv/editable-target? #js {:tagName "input"}))
109+
"tag match is case-insensitive")
110+
(is (true? (cv/editable-target? #js {:isContentEditable true
111+
:tagName "DIV"}))
112+
"contenteditable host counts as editable")))
113+
114+
(deftest editable-target-rejects-non-editables
115+
(testing "Non-editable targets fall through to the pan gesture"
116+
(is (false? (cv/editable-target? #js {:tagName "DIV"})))
117+
(is (false? (cv/editable-target? nil)))
118+
;; The crux of the bug: a keydown bubbling out of an inspector
119+
;; field's shadow DOM is retargeted to the custom-element host.
120+
;; That host must still be treated as non-editable here — the real
121+
;; editable check happens against composedPath()[0] (the inner
122+
;; native input), not this retargeted host.
123+
(is (false? (cv/editable-target? #js {:tagName "X-SEARCH-FIELD"})))
124+
(is (false? (cv/editable-target? #js {:tagName "X-TEXT-AREA"})))))

test/bareforge/render/slot_strips_test.cljs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,25 @@
1111
;; strip with a label). x-navbar / x-modal / x-drawer / x-popover /
1212
;; x-split-pane are multi-slot containers (they get N subdivisions).
1313
;; x-sidebar is a single-slot container (one full-size strip).
14+
;; x-table / x-table-row / x-table-cell and the structural containers
15+
;; (bento, fieldset, form, collapse, tabs, carousel, breadcrumbs,
16+
;; timeline, scroll-*, …) each carry a :multiple? slot and qualify.
1417
;; Leaves like x-button / x-typography / x-icon have no :multiple?
1518
;; slot, so `classify-position` never returns :inside for them and
1619
;; the strips would be dead zones — `render-strips?` excludes them.
1720
(let [all-tags (keys slots/slots)
1821
qualifiers (set (filter ss/render-strips? all-tags))]
1922
(is (= #{"x-card" "x-grid" "x-container" "x-sidebar"
20-
"x-navbar" "x-modal" "x-drawer" "x-popover" "x-split-pane"}
23+
"x-navbar" "x-modal" "x-drawer" "x-popover" "x-split-pane"
24+
"x-table" "x-table-row" "x-table-cell"
25+
"x-bento-grid" "x-bento-item" "x-fieldset" "x-form"
26+
"x-collapse" "x-tabs" "x-carousel" "x-breadcrumbs"
27+
"x-timeline" "x-timeline-item" "x-spotlight-card"
28+
"x-morph-stack" "x-proximity-list" "x-scroll"
29+
"x-scroll-stack" "x-scroll-story" "x-scroll-timeline"
30+
"x-scroll-parallax"}
2131
qualifiers)
22-
"nine container tags qualify; leaves (x-button / x-typography / x-icon) do not")))
32+
"all registered multi-child containers qualify; leaves (x-button / x-typography / x-icon) do not")))
2333
(testing "unknown tags fall through to single-slot default and return false"
2434
(is (false? (ss/render-strips? "x-made-up-tag")))
2535
(is (false? (ss/render-strips? nil)))

test/bareforge/ui/inspector_test.cljs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@
6161
(let [node {:id "n" :tag "x-button" :attrs {} :props {}}]
6262
(is (nil? (model/current-value node {:name "variant" :kind :enum})))))
6363

64+
(deftest grid-columns-transform-round-trips-table-columns
65+
(testing "a seeded track layout shows as a plain count in the number field"
66+
(let [node {:id "n" :tag "x-table" :attrs {"columns" "repeat(3, 1fr)"} :props {}}]
67+
(is (= "3" (model/current-value
68+
node {:name "columns" :kind :number :transform :grid-columns})))))
69+
(testing "a typed count commits as a track layout, custom lists pass through"
70+
(is (= "repeat(5, 1fr)" (model/transform-for-commit :grid-columns "5")))
71+
(is (= "2fr 1fr 1fr" (model/transform-for-commit :grid-columns "2fr 1fr 1fr")))))
72+
6473
;; --- inspector-model ------------------------------------------------------
6574

6675
(deftest inspector-model-nil-without-selection

0 commit comments

Comments
 (0)