@liquefy/cascade.ui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,38 @@ return button(text("Go green"), () => { scheme.base = "#2e7d32"; });
86
86
  modalAssembly({ close, width: 360, fullScreenBelow: 600 },
87
87
  dialog({ title: "Settings", close }, ...)))
88
88
  ```
89
+ - `drawer()` - a panel sliding in from an edge (`side`: left, right, top or
90
+ bottom) of its nearest positioned ancestor, over what's there. `modal`
91
+ adds a backdrop that fades in and closes it when clicked; `header` (the
92
+ default) a title and a chevron that closes it - or, `header: false`, the
93
+ content is the whole panel and calls `close` itself. `collapsed` is
94
+ what's left of it while it's closed: `"nothing"` (the default), a
95
+ `"handle"` on its edge that opens it (`onOpen`), or a `"bar"` - the panel
96
+ collapsed to a thin strip along the edge, with a chevron that opens it
97
+ and, after that, `bar`: shortcuts of the app's own. Opening it, the bar
98
+ grows into the panel: a component the app puts in `bar` while closed and
99
+ in the content while open (the same one, keyed) flies to its new place,
100
+ and the rest fades. Leave `drawerBarSize` of room for it. The panel is always
101
+ there - just past the edge, and inert, when closed - and a
102
+ FlipAnimationContainer animates it moving between the two. Moved to
103
+ another edge, it closes on one and opens on the other.
104
+
105
+ ```js
106
+ drawer({ open: this.open, close, side: "right", modal: true, title: "Filters" }, ...)
107
+
108
+ drawer({ open: this.open, close, onOpen, title: "Filters", collapsed: "bar",
109
+ bar: [iconButton({ icon: "movie", title: "Films", onClick: chooseFilms })] }, ...)
110
+ ```
111
+
112
+ Several at once, one per edge, each opened and closed on its own - with
113
+ `drawers()` and a `drawerPanel()` each (one shared backdrop when modal,
114
+ closing every open one):
115
+
116
+ ```js
117
+ drawers({ modal: false },
118
+ drawerPanel({ side: "left", open: this.navigationOpen, close: closeNavigation, title: "Pages" }, ...),
119
+ drawerPanel({ side: "right", open: this.inspectorOpen, close: closeInspector, title: "Properties" }, ...))
120
+ ```
89
121
  - `popover()` - beside an element, following it when it moves.
90
122
  - `dropdown({ options, value, onSelect })` - one choice out of several: a button
91
123
  showing it, opening a list of them all in a popover. Made of the theme's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquefy/cascade.ui",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Themed widgets, layout, overlays, modals, popovers, a dropdown and color schemes for Cascade - and its basic theme.",
5
5
  "keywords": [
6
6
  "reactive",
@@ -34,7 +34,7 @@
34
34
  "homepage": "https://github.com/erobwen/liquefy/tree/main/cascade.ui#readme",
35
35
  "peerDependencies": {
36
36
  "@liquefy/cascade.component": "^2.0.0",
37
- "@liquefy/cascade.dom": "^2.0.0"
37
+ "@liquefy/cascade.dom": "^2.2.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "mocha": "^10.8.2",
package/src/Drawer.js ADDED
@@ -0,0 +1,463 @@
1
+ import { Component, callback, frozen } from "@liquefy/cascade.component";
2
+ import { div, text, flipAnimationContainer } from "@liquefy/cascade.dom";
3
+ import { iconButton } from "./widgets.js";
4
+ import { modalBackdropColor } from "./Modal.js";
5
+ import { themeColor } from "./colorScheme.js";
6
+
7
+ /**
8
+ * Drawers - panels that slide in from the edges of whatever they're placed
9
+ * in, over what's there, and out again. One:
10
+ *
11
+ * drawer({ open: this.filtersOpen, close, side: "right", title: "Filters" }, ...content)
12
+ *
13
+ * or several at once, one per edge - a navigation on the left and an
14
+ * inspector on the right, say, each opened and closed on its own:
15
+ *
16
+ * drawers({ modal: false },
17
+ * drawerPanel({ side: "left", open: this.navigationOpen, close: closeNavigation, title: "Pages" }, ...),
18
+ * drawerPanel({ side: "right", open: this.inspectorOpen, close: closeInspector, title: "Properties" }, ...))
19
+ *
20
+ * They cover their nearest positioned ancestor (`position: absolute`, the
21
+ * whole of it), so that's what they slide over - a page's work area, a
22
+ * card, the whole app. Wherever they're placed, they're best placed last,
23
+ * so they're drawn above what comes before them.
24
+ *
25
+ * drawers({ modal, animate, speed }, ...panels):
26
+ *
27
+ * - modal: a dimmed backdrop behind them while any is open, which closes
28
+ * every open one when clicked - for panels the user deals with before
29
+ * going on. Off by default: what's beside them stays usable, and each is
30
+ * closed by its button.
31
+ * - animate: whether they slide (default true). Off, they're just there,
32
+ * or not. `speed`: how fast, as FlipAnimationContainer's (see
33
+ * cascade.dom).
34
+ *
35
+ * drawerPanel({ open, close, onOpen, side, size, header, title, collapsed, bar, style }, ...content)
36
+ * - one of them. Whether it's open is its owner's (`open`), changed by
37
+ * whoever opens it - and by `close`, which it calls when the user closes
38
+ * it: its chevron button, the backdrop, Escape.
39
+ *
40
+ * - open: whether it's open.
41
+ * - close(): called when the user closes it.
42
+ * - onOpen(): called when the user opens it - from its handle or its bar
43
+ * (below).
44
+ * - side: "left" (the default), "right", "top" or "bottom" - the edge it
45
+ * slides in from. One panel per edge: a second on the same one is left
46
+ * out.
47
+ * - size: its width (from the left or right) or height (from the top or
48
+ * bottom) - pixels, or any CSS length ("80%"). 300 by default. Never
49
+ * more than the room it has.
50
+ * - header: a header with `title` and a chevron button that closes it (the
51
+ * default). With `header: false`, the content is the whole panel - a
52
+ * panel of its own, which closes it itself (calling `close`).
53
+ * - title: the header's title.
54
+ * - collapsed: what's left of it while it's closed.
55
+ * - "nothing" (the default): nothing at all.
56
+ * - "handle": a tab on its inner edge - a chevron that opens it
57
+ * (`onOpen`), and closes it again.
58
+ * - "bar": a thin bar along the edge (drawerBarSize wide) - the panel
59
+ * collapsed. At its start, a chevron that opens it; after that,
60
+ * `bar`. Opening it, the bar grows into the panel: what's in the bar
61
+ * and not in the panel fades out, what's in the panel and not in the
62
+ * bar fades in - and what's in both moves (see below).
63
+ * The bar covers a strip along the edge of what the drawers are over:
64
+ * leave room for it there (drawerBarSize).
65
+ * - bar: what's in the bar, after its chevron - shortcuts to what's in
66
+ * the panel, say: icon buttons, one under the other (one after the
67
+ * other, at the top or bottom). Shown only while it's closed, as the
68
+ * content is only while it's open.
69
+ *
70
+ * Shared: a component the app puts in the bar while it's closed, and in
71
+ * the content while it's open - the same one, keyed - moves from one
72
+ * place to the other: a category's icon, in the bar, flies to its place
73
+ * beside the category's heading in the panel, and back. (It's in one or
74
+ * the other, never both: built where `open` says.) So does the chevron,
75
+ * from the bar to the header. To do that, what's in a panel collapsing
76
+ * to a bar isn't placed as one piece (see below): the container sees -
77
+ * and animates - every element in it, and a change in its structure
78
+ * places the whole of what it covers again.
79
+ * - style: the panel's own (its background, shadow, ...).
80
+ *
81
+ * drawer({ modal, animate, speed, ...panel }, ...content) is drawers() with
82
+ * that one panel.
83
+ *
84
+ * How they move: a FlipAnimationContainer (see cascade.dom), around a
85
+ * sheet per edge - all four always there, shown at their edge when open,
86
+ * just past it when closed. Opening and closing is a sheet moving from one
87
+ * place to the other, which the container animates - a spring, so closing
88
+ * one halfway through opening turns it round smoothly. The backdrop comes
89
+ * and goes, and fades as it does.
90
+ *
91
+ * A panel moving to another edge isn't carried across: the sheet it leaves
92
+ * closes - looking as it did, but for its content, which fades out as it
93
+ * goes - and the one it comes to opens with it, if it's open. The content
94
+ * is the same - the components the app gave it, their state kept - shown
95
+ * in another sheet.
96
+ *
97
+ * Closed, a panel is still there - built, its state kept (a scroll
98
+ * position, a half-filled field), but inert: nothing in it can be clicked
99
+ * or focused, and it's hidden from assistive technology.
100
+ *
101
+ * What's in a panel is animated by nothing: it's placed as one piece (an
102
+ * island - see cascade.dom's DOMPlacingContainer), rendered as it would be
103
+ * anywhere else - so changes in there are its own business, as efficient
104
+ * as anywhere else. It fills the panel's body, and scrolls when it's
105
+ * larger. Except in a panel collapsing to a bar: that doesn't slide, it
106
+ * grows out of its bar, and what's in it is placed element by element, so
107
+ * what it shares with the bar can move (see `collapsed`). A panel like
108
+ * that moved to another edge while open has what's in it fly across.
109
+ */
110
+ export function drawers(...parameters) {
111
+ return new Drawers(...parameters);
112
+ }
113
+
114
+ export function drawerPanel(...parameters) {
115
+ return new DrawerPanel(...parameters);
116
+ }
117
+
118
+ export function drawer(...parameters) {
119
+ return new Drawer(...parameters);
120
+ }
121
+
122
+ // Per side: which of top/right/bottom/left it's placed by, whether its
123
+ // size is a width, and its chevrons - "close" pointing at its edge, "open"
124
+ // away from it. In the order their sheets are drawn: a later one over an
125
+ // earlier one, where they overlap.
126
+ const sides = {
127
+ left: { edge: "left", horizontal: true, close: "chevron_left", open: "chevron_right" },
128
+ right: { edge: "right", horizontal: true, close: "chevron_right", open: "chevron_left" },
129
+ top: { edge: "top", horizontal: false, close: "expand_less", open: "expand_more" },
130
+ bottom: { edge: "bottom", horizontal: false, close: "expand_more", open: "expand_less" },
131
+ };
132
+ const sideNames = Object.keys(sides);
133
+
134
+ const coverAll = { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", boxSizing: "border-box" };
135
+
136
+ // The panel's shadow - only while it's open: closed, it lies just past the
137
+ // edge, and its shadow would show along it.
138
+ const drawerShadow = "0 0 16px rgba(0, 0, 0, 0.25)";
139
+
140
+ // The handle's size: how far it sticks out, and how long it is.
141
+ const HANDLE_DEPTH = 28;
142
+ const HANDLE_LENGTH = 48;
143
+
144
+ // How thick a bar is (see `collapsed`) - the room to leave for it.
145
+ export const drawerBarSize = 48;
146
+
147
+ const collapsedModes = ["nothing", "handle", "bar"];
148
+
149
+ /**
150
+ * One panel of a drawers() - what it shows, and how. Built by the app,
151
+ * shown by the drawers() it's given to: it's never rendered itself.
152
+ */
153
+ export class DrawerPanel extends Component {
154
+ setProperties({ open, close, onOpen, side, size, header, title, collapsed, bar, style, children }) {
155
+ this.open = !!open;
156
+ this.close = close || null;
157
+ this.onOpen = onOpen || null;
158
+ this.side = sides[side] ? side : "left";
159
+ this.size = typeof(size) === "number" ? size + "px" : (size || "300px");
160
+ this.header = header !== false;
161
+ this.title = title || "";
162
+ this.collapsed = collapsedModes.includes(collapsed) ? collapsed : "nothing";
163
+ this.bar = bar || null;
164
+ this.style = frozen(style || null);
165
+ this.panelChildren = frozen(children || []);
166
+ }
167
+
168
+ build() {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ export class Drawers extends Component {
174
+ setProperties({ modal, animate, speed, children }) {
175
+ this.modal = !!modal;
176
+ this.animate = animate !== false;
177
+ this.speed = typeof(speed) === "number" ? speed : null;
178
+ this.panels = frozen(children || []);
179
+ }
180
+
181
+ // How each edge's panel looked last - its sheet still does, once the
182
+ // panel has gone elsewhere, while it closes (see sheetLook()).
183
+ initialUnobservables() {
184
+ return { looks: {} };
185
+ }
186
+
187
+ // The panel at each edge, now - the first one there.
188
+ panelsBySide() {
189
+ const result = {};
190
+ for (const panel of this.panels) {
191
+ if (!(panel instanceof DrawerPanel)) {
192
+ throw new Error("drawers() takes drawerPanel()s only.");
193
+ }
194
+ if (result[panel.side]) {
195
+ console.warn("drawers(): two panels on the " + panel.side + " edge - the second is left out.");
196
+ continue;
197
+ }
198
+ result[panel.side] = panel;
199
+ }
200
+ return result;
201
+ }
202
+
203
+ build() {
204
+ const panels = this.panelsBySide();
205
+ const anyOpen = sideNames.some((side) => panels[side] && panels[side].open);
206
+ // Each edge's: its sheet's handle and its bar both use it - named once.
207
+ const toggles = {};
208
+ for (const side of sideNames) toggles[side] = this.toggleCallback(side);
209
+ // Every open one - as they are when it's clicked.
210
+ const closeAll = callback("closeAll", () => {
211
+ const now = this.panelsBySide();
212
+ for (const side of sideNames) {
213
+ const panel = now[side];
214
+ if (panel && panel.open && panel.close) panel.close();
215
+ }
216
+ });
217
+
218
+ return flipAnimationContainer(
219
+ {
220
+ enabled: this.animate,
221
+ speed: this.speed,
222
+ // The backdrop fades in where it lies, below the panels - never
223
+ // lifted above them.
224
+ confine: true,
225
+ // What's in a panel is the app's, placed as one piece - an island
226
+ // - but for one collapsing to a bar (see buildSheet()).
227
+ isUnit: isDrawerContent,
228
+ style: { ...coverAll, overflow: "hidden", pointerEvents: "none" },
229
+ },
230
+ // Below the sheets: a bar is usable over it.
231
+ div({
232
+ key: "backdrop",
233
+ onclick: closeAll,
234
+ style: { ...coverAll, pointerEvents: "auto", background: modalBackdropColor },
235
+ }).showIf(this.modal && anyOpen),
236
+ sideNames.map((side) => this.buildSheet(side, panels[side] || null, toggles[side])),
237
+ );
238
+ }
239
+
240
+ // Opens the panel at `side` if it's closed, closes it if it's open.
241
+ toggleCallback(side) {
242
+ return callback(side + "Toggle", () => {
243
+ const now = this.panelsBySide()[side];
244
+ if (now) this.call(side, now.open ? "close" : "onOpen");
245
+ });
246
+ }
247
+
248
+ // How an edge's sheet looks: as its panel says - or, with none there
249
+ // now, as the last one did (it's closing, or closed), so a panel moving
250
+ // elsewhere leaves a sheet that closes as it was.
251
+ sheetLook(side, panel) {
252
+ const u = this.unobservable;
253
+ if (panel) {
254
+ u.looks[side] = { size: panel.size, header: panel.header, title: panel.title, style: panel.style };
255
+ }
256
+ return u.looks[side] || { size: "300px", header: false, title: "", style: null };
257
+ }
258
+
259
+ // Calls the panel at `side` - as it is when called - has, if it has it.
260
+ call(side, name) {
261
+ const panel = this.panelsBySide()[side];
262
+ if (panel && panel[name]) panel[name]();
263
+ }
264
+
265
+ // An edge's sheet: what moves - and in it, the panel and its handle.
266
+ // Keyed by its edge, as everything in it: each is the same element,
267
+ // whatever else comes and goes.
268
+ //
269
+ // Most panels slide: at the edge when open, just past it when closed.
270
+ // One collapsing to a bar doesn't: it's always at the edge, and grows
271
+ // from the bar into the panel - showing, closed, the bar (a chevron, and
272
+ // the app's `bar`), and open, the panel (its header - with that same
273
+ // chevron - and its content). Nothing in it is an island, so the
274
+ // container sees every element in it: what the app moves from the bar
275
+ // into the content (the same component - keyed) flies to its new place,
276
+ // and what's only in one fades in or out.
277
+ buildSheet(side, panel, toggle) {
278
+ const geometry = sides[side];
279
+ const look = this.sheetLook(side, panel);
280
+ const open = !!(panel && panel.open);
281
+ const collapsed = panel ? panel.collapsed : "nothing";
282
+ const handle = collapsed === "handle";
283
+ const morphing = collapsed === "bar";
284
+ const showingBar = morphing && !open;
285
+ // Usable: open, or showing its bar.
286
+ const usable = open || showingBar;
287
+
288
+ // Along its whole edge, as deep as its size - or its bar - and, sliding,
289
+ // just past the edge when closed: by its handle's depth more, when it
290
+ // has none - then nothing at all shows of it.
291
+ const depth = showingBar ? drawerBarSize + "px" : look.size;
292
+ const across = geometry.horizontal
293
+ ? { top: 0, height: "100%", width: depth, maxWidth: "100%" }
294
+ : { left: 0, width: "100%", height: depth, maxHeight: "100%" };
295
+ const past = handle ? "0px" : HANDLE_DEPTH + "px";
296
+ const placed = {
297
+ position: "absolute", boxSizing: "border-box", pointerEvents: "none", ...across,
298
+ [geometry.edge]: open || morphing ? "0px" : `calc(0px - ${look.size} - ${past})`,
299
+ // A stacking context of its own: its handle goes under its panel.
300
+ zIndex: 0,
301
+ };
302
+
303
+ // Opens and closes it: in the header when open, at the start of the
304
+ // bar when collapsed to it - the same button, moving between them. (A
305
+ // sliding panel's is only ever seen open.)
306
+ const chevronOpen = open || !morphing;
307
+ const chevron = iconButton({
308
+ key: side + "Chevron",
309
+ icon: chevronOpen ? geometry.close : geometry.open,
310
+ title: chevronOpen ? "Close" : "Open",
311
+ onClick: toggle,
312
+ style: { flex: "none" },
313
+ });
314
+
315
+ const inner = { left: "Right", right: "Left", top: "Bottom", bottom: "Top" }[side];
316
+ const sheetPanel = div(
317
+ {
318
+ inert: !usable,
319
+ "aria-hidden": usable ? undefined : "true",
320
+ onkeydown: callback(side + "Keydown", (event) => {
321
+ if (event.key !== "Escape" || !open) return;
322
+ event.stopPropagation();
323
+ this.call(side, "close");
324
+ }),
325
+ style: {
326
+ ...coverAll, display: "flex", overflow: "hidden",
327
+ background: themeColor.surface, color: themeColor.text,
328
+ boxShadow: open ? drawerShadow : "none", transition: "box-shadow 0.3s",
329
+ pointerEvents: usable ? "auto" : "none",
330
+ ...(showingBar
331
+ // The bar: the chevron and the app's shortcuts, one under the
332
+ // other - or, along the top or the bottom, one after the other.
333
+ ? {
334
+ flexDirection: geometry.horizontal ? "column" : "row", alignItems: "center", gap: "4px",
335
+ padding: geometry.horizontal ? "6px 0" : "0 6px", ["border" + inner]: "1px solid " + themeColor.border,
336
+ }
337
+ : { flexDirection: "column" }),
338
+ ...look.style,
339
+ },
340
+ },
341
+ showingBar ? chevron : null,
342
+ showingBar && panel.bar ? this.buildBarItems(side, panel) : null,
343
+ showingBar ? null : this.buildHeader(side, look, chevron).showIf(look.header),
344
+ showingBar ? null : this.buildBody(side, panel, morphing),
345
+ );
346
+
347
+ return div(
348
+ { key: side + "Sheet", style: placed },
349
+ sheetPanel,
350
+ this.buildHandle(side, open, toggle).showIf(handle),
351
+ );
352
+ }
353
+
354
+ // A title, and the chevron at the end nearest the edge (the start, for
355
+ // one on the right), pointing there.
356
+ buildHeader(side, look, chevron) {
357
+ const title = div({ style: { flex: "1 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, text(look.title));
358
+ return div(
359
+ {
360
+ key: side + "Header",
361
+ style: {
362
+ display: "flex", alignItems: "center", gap: "8px", flex: "none", minHeight: "48px", boxSizing: "border-box",
363
+ padding: side === "right" ? "6px 16px 6px 6px" : "6px 6px 6px 16px",
364
+ background: themeColor.chromeDark, color: themeColor.onChrome, fontWeight: "bold",
365
+ },
366
+ },
367
+ ...(side === "right" ? [chevron, title] : [title, chevron]),
368
+ );
369
+ }
370
+
371
+ // The panel's content. A grid of one cell: what's in it fills the body -
372
+ // its own height, 100%, is the body's - and the body scrolls when it's
373
+ // larger.
374
+ //
375
+ // Placed as one piece - an island, this edge's own: a panel coming here
376
+ // from another edge is shown in a new one, appearing in this sheet, not
377
+ // carried across. But morphing from and into a bar, it's placed element
378
+ // by element, so what it shares with the bar moves between them.
379
+ buildBody(side, panel, morphing) {
380
+ const children = panel ? panel.panelChildren : [];
381
+ return div(
382
+ { key: side + "Body", style: { flex: "1 1 auto", minHeight: 0, overflow: "auto", display: "grid", gridTemplateColumns: "minmax(0, 1fr)", gridTemplateRows: "1fr" } },
383
+ morphing
384
+ ? div({ key: side + "Morphing" }, children)
385
+ : new DrawerContent({ key: side + "Content", children }).showIf(!!panel),
386
+ );
387
+ }
388
+
389
+ // The app's shortcuts in the bar, after a line under the chevron.
390
+ buildBarItems(side, panel) {
391
+ const horizontal = sides[side].horizontal;
392
+ return [
393
+ div({
394
+ key: side + "BarLine",
395
+ style: { flex: "none", background: themeColor.border, ...(horizontal ? { width: "60%", height: "1px", margin: "2px 0" } : { height: "60%", width: "1px", margin: "0 2px" }) },
396
+ }),
397
+ div(
398
+ { key: side + "BarItems", style: { display: "flex", flexDirection: horizontal ? "column" : "row", alignItems: "center", gap: "4px" } },
399
+ [].concat(panel.bar),
400
+ ),
401
+ ];
402
+ }
403
+
404
+ // A tab sticking out of the panel's inner edge, at its middle - not
405
+ // transformed into place: the container animating it owns its transform.
406
+ buildHandle(side, open, toggle) {
407
+ const geometry = sides[side];
408
+ const middle = `calc(50% - ${HANDLE_LENGTH / 2}px)`;
409
+ const corners = { left: "0 8px 8px 0", right: "8px 0 0 8px", top: "0 0 8px 8px", bottom: "8px 8px 0 0" }[side];
410
+ const placement = geometry.horizontal
411
+ ? { top: middle, width: HANDLE_DEPTH + "px", height: HANDLE_LENGTH + "px", [side]: "100%" }
412
+ : { left: middle, height: HANDLE_DEPTH + "px", width: HANDLE_LENGTH + "px", [side]: "100%" };
413
+ return div(
414
+ {
415
+ key: side + "Handle",
416
+ style: {
417
+ position: "absolute", ...placement, boxSizing: "border-box", display: "flex", alignItems: "center", justifyContent: "center",
418
+ borderRadius: corners, background: themeColor.surface, color: themeColor.text, boxShadow: drawerShadow, pointerEvents: "auto",
419
+ // Its shadow under the panel's: it's part of it.
420
+ zIndex: -1,
421
+ },
422
+ },
423
+ iconButton({
424
+ icon: open ? geometry.close : geometry.open,
425
+ title: open ? "Close" : "Open",
426
+ onClick: toggle,
427
+ style: { flex: "none", width: "100%", height: "100%", padding: 0, minWidth: 0 },
428
+ }),
429
+ );
430
+ }
431
+ }
432
+
433
+ // drawers() with one panel - see the module doc.
434
+ export class Drawer extends Component {
435
+ setProperties({ modal, animate, speed, children, ...panel }) {
436
+ this.modal = !!modal;
437
+ this.animate = animate !== false;
438
+ this.speed = typeof(speed) === "number" ? speed : null;
439
+ this.panel = frozen(panel);
440
+ this.drawerChildren = frozen(children || []);
441
+ }
442
+
443
+ build() {
444
+ return drawers(
445
+ { modal: this.modal, animate: this.animate, speed: this.speed },
446
+ drawerPanel({ ...this.panel, children: this.drawerChildren }),
447
+ );
448
+ }
449
+ }
450
+
451
+ // The same function every build - a new one would count as a change.
452
+ const isDrawerContent = (component) => component instanceof DrawerContent;
453
+
454
+ // What's in a drawer's panel - placed as one piece (see Drawers.build()).
455
+ class DrawerContent extends Component {
456
+ setProperties({ children }) {
457
+ this.contentChildren = frozen(children || []);
458
+ }
459
+
460
+ build() {
461
+ return this.contentChildren;
462
+ }
463
+ }
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { OverlayFrame, overlayFrame } from "./OverlayFrame.js";
2
2
  export { Overlay, overlay } from "./Overlay.js";
3
3
  export { ModalBackdrop, Modal, ModalAssembly, modalBackdrop, modal, modalAssembly, modalBackdropColor } from "./Modal.js";
4
+ export { Drawer, Drawers, DrawerPanel, drawer, drawers, drawerPanel, drawerBarSize } from "./Drawer.js";
4
5
  export {
5
6
  flexContainerStyle, rowStyle, columnStyle,
6
7
  centerStyle, middleStyle, centerMiddleStyle,