diff --git a/public/index.html b/public/index.html index f373b0d..471e047 100644 --- a/public/index.html +++ b/public/index.html @@ -1221,6 +1221,32 @@ .chrome[data-mode="preview"] [data-bareforge-placement="background"] { pointer-events: none; } + /* --- editor-only overlay flattening (x-modal / x-drawer) ---------- */ + /* x-modal and x-drawer are `:host { display: contents }` fixed-overlay + components: their dialog/panel paints via `position: fixed` and the + host itself generates no box. On the canvas that means the host + can't be measured, the empty-container affordance has nothing to + size, and the drop hit-test (which reads the host's + getBoundingClientRect) can never classify a drop as :inside — so + content can't be dropped into them. Their fixed backdrop also + covers the canvas and swallows pointer events. We flatten them for + editing only (scoped to `.canvas-host`, never exported): give the + host a real box, hide the backdrop, and render the dialog/panel in + normal flow through the public ::part() API so it can be selected, + measured, and dropped into. The `!important` flags override the + components' high-specificity `:host([data-open=true])` animation + rules. */ + .canvas-host x-modal, + .canvas-host x-drawer { display: block; } + .canvas-host x-modal::part(backdrop), + .canvas-host x-drawer::part(backdrop) { display: none !important; } + .canvas-host x-modal::part(dialog), + .canvas-host x-drawer::part(panel) { + position: relative !important; + inset: auto !important; + transform: none !important; + max-height: none !important; + } diff --git a/src/bareforge/meta/events.cljs b/src/bareforge/meta/events.cljs index 2336bde..4182e7d 100644 --- a/src/bareforge/meta/events.cljs +++ b/src/bareforge/meta/events.cljs @@ -21,6 +21,9 @@ "x-menu-item" ["click"] "x-checkbox" ["change"] "x-popover" ["x-popover-toggle" "mouseenter" "mouseleave"] + "x-sidebar" ["toggle" "dismiss"] + "x-drawer" ["x-drawer-toggle" "x-drawer-dismiss"] + "x-modal" ["x-modal-toggle" "x-modal-dismiss"] "x-combobox" ["x-combobox-change" "x-combobox-input" "x-combobox-toggle"] "x-multi-combobox" ["x-multi-combobox-change-request" "x-multi-combobox-change" diff --git a/src/bareforge/meta/placement.cljs b/src/bareforge/meta/placement.cljs index ec86876..1fcc839 100644 --- a/src/bareforge/meta/placement.cljs +++ b/src/bareforge/meta/placement.cljs @@ -13,7 +13,6 @@ (def placement {"x-navbar" {:hint :top-full-width} - "x-sidebar" {:hint :top-full-width} "x-gaussian-blur" {:hint :background} "x-liquid-fill" {:hint :background} diff --git a/src/bareforge/meta/probes.cljs b/src/bareforge/meta/probes.cljs new file mode 100644 index 0000000..d891389 --- /dev/null +++ b/src/bareforge/meta/probes.cljs @@ -0,0 +1,31 @@ +(ns bareforge.meta.probes + "Per-tag shadow-DOM selectors for the handful of components whose + *painted* surface escapes their host element's layout box. + + The canvas selection overlay (`render/selection`) traces the host + element's bounding box. For 79 of BareDOM's 83 tags that box is + exactly the visible surface, so the host is the right thing to + measure. The four overlay components are the exception: + + - `x-sidebar` (docked) renders a full-width `display:block` host but + only paints an 18rem `.panel` on the leading edge. + - `x-drawer` / `x-modal` / `x-popover` paint a `position:fixed` / + `absolute` panel that leaves the host's box entirely, collapsing + the host to a thin strip. + + For these, the overlay measures the named shadow element instead so + the selection border hugs what the user actually sees, across every + variant. Selectors track BareDOM's shadow templates and are pinned + to the BareDOM version in `deps.edn` / `meta/versions.cljs`.") + +(def ^:private selection-selectors + {"x-sidebar" ".panel" + "x-drawer" "[part=panel]" + "x-modal" "[part=dialog]" + "x-popover" "[part=panel]"}) + +(defn selection-selector + "Shadow-DOM selector for the visible surface of `tag`, or nil when the + host's own box is the right thing to measure (the common case)." + [tag] + (get selection-selectors tag)) diff --git a/src/bareforge/meta/slots.cljs b/src/bareforge/meta/slots.cljs index 8f0087f..dd89774 100644 --- a/src/bareforge/meta/slots.cljs +++ b/src/bareforge/meta/slots.cljs @@ -40,6 +40,9 @@ {:name "actions" :label "Actions" :multiple? true} {:name "toggle" :label "Toggle" :multiple? false}] + "x-sidebar" + [{:name "default" :label "Content" :multiple? true}] + "x-typography" [{:name "default" :label "Text" :multiple? false}] diff --git a/src/bareforge/render/selection.cljs b/src/bareforge/render/selection.cljs index 1e5393a..fc9eed5 100644 --- a/src/bareforge/render/selection.cljs +++ b/src/bareforge/render/selection.cljs @@ -25,6 +25,7 @@ pointerup keeps the undo history clean." (:require [bareforge.doc.model :as m] [bareforge.doc.ops :as ops] + [bareforge.meta.probes :as probes] [bareforge.render.canvas :as canvas] [bareforge.state :as state])) @@ -79,10 +80,16 @@ and grows `:width / :height` instead of moving x/y." [node] (when node - (case (get-in node [:layout :placement]) - :background nil - :free :free - :flow))) + ;; Overlay components (sidebar/drawer/modal/popover) size their + ;; panel from CSS custom properties, not from flow width/height — + ;; flow-resize handles would write `:layout :width/:height` onto the + ;; host and change nothing visible. Suppress handles for them; they + ;; are exactly the tags carrying a selection probe. + (when-not (probes/selection-selector (:tag node)) + (case (get-in node [:layout :placement]) + :background nil + :free :free + :flow)))) (def ^:private flow-handles #{:e :s :se}) @@ -153,6 +160,32 @@ (defn- bcr->map [^js r] {:left (.-left r) :top (.-top r) :width (.-width r) :height (.-height r)}) +(defn- ^js nonzero-box + "Return `el` when its bounding box has a positive area, else nil. A + collapsed panel (e.g. an empty docked sidebar whose `min-block-size: + 100%` resolves against an auto-height host) reports a zero-area box." + [^js el] + (when el + (let [r (.getBoundingClientRect el)] + (when (and (pos? (.-width r)) (pos? (.-height r))) + el)))) + +(defn- ^js measure-target + "The element whose box the overlay should trace for the rendered host + `el` of `tag`. Overlay components paint a panel outside their host + box (a docked sidebar is narrower than its full-width host; drawer / + modal / popover panels are position:fixed and leave it entirely), so + for those we measure the shadow-DOM panel named by + `probes/selection-selector` — the border then hugs the visible + surface across every variant. Falls back to the host element when + there is no probe (the common case), the panel isn't in the shadow + tree yet, or the panel is collapsed to zero area (an empty docked + sidebar — the host then carries the empty-container affordance box)." + [^js el tag] + (or (when-let [sel (probes/selection-selector tag)] + (nonzero-box (some-> (.-shadowRoot el) (.querySelector sel)))) + el)) + (defn- show! [^js overlay ^js el ^js host] (let [er (.getBoundingClientRect el) @@ -220,8 +253,9 @@ (dotimes [i n] (let [{:keys [id el node]} (nth entries i) ^js o (aget p i) + ^js target (measure-target el (:tag node)) primary? (zero? i)] - (show! o el host) + (show! o target host) (if (and primary? single? (not= "root" id)) (let [mode (resize-mode-for-node node)] (if mode diff --git a/src/bareforge/ui/palette.cljs b/src/bareforge/ui/palette.cljs index f3d2cfe..081b918 100644 --- a/src/bareforge/ui/palette.cljs +++ b/src/bareforge/ui/palette.cljs @@ -107,6 +107,21 @@ ;; integer-string coercion exists as a safety net for legacy ;; docs, not as the authoring default. "x-grid" {: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 + ;; selectable the moment they land. Presence-attr convention follows + ;; the root node's `"fluid" ""` (see doc.model/empty-document). + ;; A docked sidebar has no intrinsic width in its parent's layout, so + ;; in plain `:flow` it collapses to a thin strip. Seed `width:100%` + ;; so it lands as a full-width, droppable block; the empty-container + ;; affordance supplies the height. (This width was previously a side + ;; effect of the `:top-full-width` placement snap — now that the + ;; sidebar is correctly `:flow`, it belongs here as an explicit seed.) + "x-sidebar" {:attrs {"open" ""} :layout {:width "100%"}} + "x-drawer" {:attrs {"open" ""}} + "x-modal" {:attrs {"open" ""}} + "x-popover" {:attrs {"open" ""}} {})) ;; --- effectful ------------------------------------------------------------ diff --git a/test/bareforge/meta/events_test.cljs b/test/bareforge/meta/events_test.cljs new file mode 100644 index 0000000..cf55dcd --- /dev/null +++ b/test/bareforge/meta/events_test.cljs @@ -0,0 +1,17 @@ +(ns bareforge.meta.events-test + (:require [cljs.test :refer [deftest is testing]] + [bareforge.meta.events :as events])) + +(deftest sidebar-exposes-toggle-and-dismiss + (testing "x-sidebar is interactive — its toggle/dismiss events surface + in the inspector and can be wired in exports" + (is (= ["toggle" "dismiss"] (events/events-for "x-sidebar"))))) + +(deftest overlay-dialogs-expose-toggle-and-dismiss + (testing "x-drawer and x-modal dispatch namespaced toggle/dismiss events" + (is (= ["x-drawer-toggle" "x-drawer-dismiss"] (events/events-for "x-drawer"))) + (is (= ["x-modal-toggle" "x-modal-dismiss"] (events/events-for "x-modal"))))) + +(deftest non-interactive-tags-return-nil + (is (nil? (events/events-for "x-card"))) + (is (nil? (events/events-for "x-totally-made-up")))) diff --git a/test/bareforge/meta/placement_test.cljs b/test/bareforge/meta/placement_test.cljs index 1e8acd5..d7ea0ac 100644 --- a/test/bareforge/meta/placement_test.cljs +++ b/test/bareforge/meta/placement_test.cljs @@ -10,8 +10,12 @@ ;; --- hint-for sanity ---------------------------------------------------- (deftest hint-for-structural-tags - (is (= :top-full-width (:hint (p/hint-for "x-navbar")))) - (is (= :top-full-width (:hint (p/hint-for "x-sidebar"))))) + (is (= :top-full-width (:hint (p/hint-for "x-navbar"))))) + +(deftest hint-for-sidebar-is-flow + ;; A sidebar is a flow container the user places like any other block — + ;; not a top-snapped bar. (x-navbar keeps :top-full-width.) + (is (= :flow (:hint (p/hint-for "x-sidebar"))))) (deftest hint-for-unknown-tag-defaults-to-flow (is (= :flow (:hint (p/hint-for "x-totally-made-up"))))) diff --git a/test/bareforge/meta/probes_test.cljs b/test/bareforge/meta/probes_test.cljs new file mode 100644 index 0000000..5be3d92 --- /dev/null +++ b/test/bareforge/meta/probes_test.cljs @@ -0,0 +1,16 @@ +(ns bareforge.meta.probes-test + (:require [cljs.test :refer [deftest is testing]] + [bareforge.meta.probes :as probes])) + +(deftest overlay-components-have-selection-selectors + (testing "the four overlay components map to their visible shadow surface" + (is (= ".panel" (probes/selection-selector "x-sidebar"))) + (is (= "[part=panel]" (probes/selection-selector "x-drawer"))) + (is (= "[part=dialog]" (probes/selection-selector "x-modal"))) + (is (= "[part=panel]" (probes/selection-selector "x-popover"))))) + +(deftest ordinary-tags-have-no-selector + (testing "host box is the right measure for non-overlay components" + (is (nil? (probes/selection-selector "x-button"))) + (is (nil? (probes/selection-selector "x-card"))) + (is (nil? (probes/selection-selector "x-some-unknown"))))) diff --git a/test/bareforge/meta/registry_test.cljs b/test/bareforge/meta/registry_test.cljs index 6bc80b7..f88afba 100644 --- a/test/bareforge/meta/registry_test.cljs +++ b/test/bareforge/meta/registry_test.cljs @@ -146,7 +146,7 @@ (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-modal" "x-drawer" "x-popover" "x-sidebar"]] (is (true? (r/container? tag)) (str tag " should be a container")))) diff --git a/test/bareforge/render/selection_test.cljs b/test/bareforge/render/selection_test.cljs index f9cdd92..68ef01f 100644 --- a/test/bareforge/render/selection_test.cljs +++ b/test/bareforge/render/selection_test.cljs @@ -121,6 +121,13 @@ (testing "older nodes without a :placement key still get flow handles" (is (= :flow (sel/resize-mode-for-node {:id "n" :layout {}}))))) +(deftest resize-mode-overlay-tags-get-no-handles + (testing "overlay components size via CSS vars, not flow w/h — no handles" + (doseq [tag ["x-sidebar" "x-drawer" "x-modal" "x-popover"]] + (is (nil? (sel/resize-mode-for-node + {:id "n" :tag tag :layout {:placement :flow}})) + (str tag " should expose no resize handles"))))) + ;; --- resize-mode-handle-allowed? ----------------------------------------- (deftest handle-allowed-free-accepts-everything diff --git a/test/bareforge/render/slot_strips_test.cljs b/test/bareforge/render/slot_strips_test.cljs index 427990f..ad1880e 100644 --- a/test/bareforge/render/slot_strips_test.cljs +++ b/test/bareforge/render/slot_strips_test.cljs @@ -10,15 +10,16 @@ ;; x-container are single-slot containers (they get one full-size ;; 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). ;; 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" + (is (= #{"x-card" "x-grid" "x-container" "x-sidebar" "x-navbar" "x-modal" "x-drawer" "x-popover" "x-split-pane"} qualifiers) - "eight container tags qualify; leaves (x-button / x-typography / x-icon) do not"))) + "nine container tags 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/palette_test.cljs b/test/bareforge/ui/palette_test.cljs index fbdcc0d..ad794e4 100644 --- a/test/bareforge/ui/palette_test.cljs +++ b/test/bareforge/ui/palette_test.cljs @@ -58,6 +58,12 @@ (testing "button seeds visible label" (is (= "Button" (:text (p/seed-for-tag "x-button")))) (is (= "Button" (get-in (p/seed-for-tag "x-button") [:attrs "label"])))) + (testing "overlay components seed open so they're visible on drop" + (doseq [tag ["x-sidebar" "x-drawer" "x-modal" "x-popover"]] + (is (= "" (get-in (p/seed-for-tag tag) [:attrs "open"])) + (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 "unknown tags get empty overrides" (is (= {} (p/seed-for-tag "x-some-unknown")))))