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