feat(dialog): add overlay-based dialog pattern (dialog-overlay + dialog-panel)
For reactive frameworks (Eucalypt, etc.) where native <dialog> with .showModal() isn't practical. dialog-overlay provides the fixed backdrop, dialog-panel provides the content box with padding/gap/animation. Shared inner styles (dialog-header, dialog-body, dialog-footer) work with both native <dialog> and overlay-based patterns.
This commit is contained in:
@@ -11,11 +11,13 @@
|
|||||||
[opts]
|
[opts]
|
||||||
(str/join " " (dialog-class-list opts)))
|
(str/join " " (dialog-class-list opts)))
|
||||||
|
|
||||||
(defn dialog
|
;; ── Native <dialog> ─────────────────────────────────────────────────
|
||||||
"Render a dialog element.
|
|
||||||
|
|
||||||
Clicking the backdrop (the area outside the dialog box) will close the
|
(defn dialog
|
||||||
dialog automatically when opened via .showModal().
|
"Render a native <dialog> element.
|
||||||
|
|
||||||
|
Use with .showModal() for built-in backdrop and focus trapping.
|
||||||
|
Clicking the backdrop closes the dialog automatically.
|
||||||
|
|
||||||
Props:
|
Props:
|
||||||
:open - boolean, whether the dialog is open
|
:open - boolean, whether the dialog is open
|
||||||
@@ -57,6 +59,84 @@
|
|||||||
attrs)]
|
attrs)]
|
||||||
(into [:dialog base-attrs] children))))
|
(into [:dialog base-attrs] children))))
|
||||||
|
|
||||||
|
;; ── Overlay-based dialog ────────────────────────────────────────────
|
||||||
|
;; For reactive frameworks (Eucalypt, etc.) where native <dialog> isn't
|
||||||
|
;; practical. Renders a backdrop + panel as plain divs, controlled by
|
||||||
|
;; show/hide in app state.
|
||||||
|
|
||||||
|
(defn dialog-overlay
|
||||||
|
"Render a dialog backdrop overlay.
|
||||||
|
|
||||||
|
Covers the viewport and centers its children. Clicking the backdrop
|
||||||
|
fires :on-close. Wrap a dialog-panel inside this.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
:on-close - callback when backdrop is clicked
|
||||||
|
:class - additional CSS classes
|
||||||
|
:attrs - additional HTML attributes"
|
||||||
|
[{:keys [on-close class attrs] :as _props} & children]
|
||||||
|
#?(:squint
|
||||||
|
(let [classes (cond-> "dialog-overlay"
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (merge {:class classes
|
||||||
|
:on-click (when on-close
|
||||||
|
(fn [e]
|
||||||
|
(when (identical? (.-target e) (.-currentTarget e))
|
||||||
|
(on-close))))}
|
||||||
|
attrs)]
|
||||||
|
(into [:div base-attrs] children))
|
||||||
|
|
||||||
|
:cljs
|
||||||
|
(let [classes (cond-> ["dialog-overlay"]
|
||||||
|
class (conj class))
|
||||||
|
base-attrs (merge {:class classes
|
||||||
|
:on (when on-close
|
||||||
|
{:click (fn [e]
|
||||||
|
(when (identical? (.-target e) (.-currentTarget e))
|
||||||
|
(on-close)))})}
|
||||||
|
attrs)]
|
||||||
|
(into [:div base-attrs] children))
|
||||||
|
|
||||||
|
:clj
|
||||||
|
(let [classes (cond-> "dialog-overlay"
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
(into [:div base-attrs] children))))
|
||||||
|
|
||||||
|
(defn dialog-panel
|
||||||
|
"Render a dialog panel (the visible box inside an overlay).
|
||||||
|
|
||||||
|
Use inside dialog-overlay. Stops click propagation so clicks
|
||||||
|
inside the panel don't close the overlay.
|
||||||
|
|
||||||
|
Props:
|
||||||
|
:class - additional CSS classes
|
||||||
|
:attrs - additional HTML attributes"
|
||||||
|
[{:keys [class attrs] :as _props} & children]
|
||||||
|
#?(:squint
|
||||||
|
(let [classes (cond-> "dialog-panel"
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (merge {:class classes
|
||||||
|
:on-click (fn [e] (.stopPropagation e))}
|
||||||
|
attrs)]
|
||||||
|
(into [:div base-attrs] children))
|
||||||
|
|
||||||
|
:cljs
|
||||||
|
(let [classes (cond-> ["dialog-panel"]
|
||||||
|
class (conj class))
|
||||||
|
base-attrs (merge {:class classes
|
||||||
|
:on {:click (fn [e] (.stopPropagation e))}}
|
||||||
|
attrs)]
|
||||||
|
(into [:div base-attrs] children))
|
||||||
|
|
||||||
|
:clj
|
||||||
|
(let [classes (cond-> "dialog-panel"
|
||||||
|
class (str " " class))
|
||||||
|
base-attrs (merge {:class classes} attrs)]
|
||||||
|
(into [:div base-attrs] children))))
|
||||||
|
|
||||||
|
;; ── Shared sections ─────────────────────────────────────────────────
|
||||||
|
|
||||||
(defn dialog-header
|
(defn dialog-header
|
||||||
"Render a dialog header section."
|
"Render a dialog header section."
|
||||||
[{:keys [class attrs] :as _props} & children]
|
[{:keys [class attrs] :as _props} & children]
|
||||||
|
|||||||
@@ -1,3 +1,46 @@
|
|||||||
|
/* ── Shared dialog inner styles ───────────────────────────────────── */
|
||||||
|
/* Used by both native <dialog> and overlay-based .dialog-panel */
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--size-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header h1,
|
||||||
|
.dialog-header h2,
|
||||||
|
.dialog-header h3,
|
||||||
|
.dialog-header h4,
|
||||||
|
.dialog-header h5,
|
||||||
|
.dialog-header h6 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header p {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--fg-2);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-3);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body > *:first-child { margin-top: 0; }
|
||||||
|
.dialog-body > *:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Native <dialog> ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -26,36 +69,65 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header {
|
/* ── Overlay-based dialog ────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--size-4);
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
animation: dialog-overlay-in 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-overlay-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-panel {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--size-1);
|
gap: var(--size-3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem;
|
||||||
|
max-height: 85vh;
|
||||||
|
padding: var(--size-6);
|
||||||
|
background: var(--bg-1);
|
||||||
|
color: var(--fg-0);
|
||||||
|
border: var(--border-0);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-3);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: dialog-panel-in 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header h1,
|
@keyframes dialog-panel-in {
|
||||||
.dialog-header h2,
|
from { opacity: 0; transform: scale(0.96) translateY(var(--size-2)); }
|
||||||
.dialog-header h3,
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
.dialog-header h4,
|
|
||||||
.dialog-header h5,
|
|
||||||
.dialog-header h6 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header p {
|
/* ── Dark mode ───────────────────────────────────────────────────── */
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--fg-2);
|
[data-theme="dark"] .dialog-overlay {
|
||||||
margin: 0;
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-body {
|
@media (prefers-color-scheme: dark) {
|
||||||
overflow-y: auto;
|
:root:not([data-theme="light"]) .dialog-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-body > *:first-child { margin-top: 0; }
|
/* ── Mobile ──────────────────────────────────────────────────────── */
|
||||||
.dialog-body > *:last-child { margin-bottom: 0; }
|
|
||||||
|
|
||||||
.dialog-footer {
|
@media (max-width: 768px) {
|
||||||
display: flex;
|
.dialog-panel {
|
||||||
justify-content: flex-end;
|
max-width: none;
|
||||||
gap: var(--size-2);
|
padding: var(--size-4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user