Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
</style>
</head>
<body>
Expand Down
3 changes: 3 additions & 0 deletions src/bareforge/meta/events.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion src/bareforge/meta/placement.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
31 changes: 31 additions & 0 deletions src/bareforge/meta/probes.cljs
Original file line number Diff line number Diff line change
@@ -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))
3 changes: 3 additions & 0 deletions src/bareforge/meta/slots.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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}]

Expand Down
44 changes: 39 additions & 5 deletions src/bareforge/render/selection.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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]))

Expand Down Expand Up @@ -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})

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/bareforge/ui/palette.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ------------------------------------------------------------
Expand Down
17 changes: 17 additions & 0 deletions test/bareforge/meta/events_test.cljs
Original file line number Diff line number Diff line change
@@ -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"))))
8 changes: 6 additions & 2 deletions test/bareforge/meta/placement_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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")))))
Expand Down
16 changes: 16 additions & 0 deletions test/bareforge/meta/probes_test.cljs
Original file line number Diff line number Diff line change
@@ -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")))))
2 changes: 1 addition & 1 deletion test/bareforge/meta/registry_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))))

Expand Down
7 changes: 7 additions & 0 deletions test/bareforge/render/selection_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions test/bareforge/render/slot_strips_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
6 changes: 6 additions & 0 deletions test/bareforge/ui/palette_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -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")))))

Expand Down
Loading