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`
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"))
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")))))