@liquefy/cascade.ui 1.0.0 → 2.0.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
@@ -1,6 +1,6 @@
1
1
  # cascade.ui
2
2
 
3
- Themed widgets, layout, overlays, portals, popovers and color schemes for
3
+ Themed widgets, layout, overlays, modals, popovers and color schemes for
4
4
  [Cascade](https://github.com/erobwen/liquefy#readme) - and its basic theme.
5
5
 
6
6
  ```console
@@ -61,13 +61,32 @@ import { button, currentColorScheme } from "@liquefy/cascade.ui";
61
61
  // button giving it another base color. Everything inside a
62
62
  // colorSchemeScope() of that theme follows (put one around your app).
63
63
  const scheme = currentColorScheme();
64
- return button({ key: "green" }, text({ key: "greenText", text: "Go green" }), () => { scheme.base = "#2e7d32"; });
64
+ return button(text("Go green"), () => { scheme.base = "#2e7d32"; });
65
65
  ```
66
66
 
67
67
  ## Layout and the rest
68
68
 
69
69
  - `row`, `column`, `filler`, `centerMiddle`, `zStack`, ... and their styles
70
70
  (`fillerStyle`, `fitContainerStyle`, ...).
71
- - `overlayFrame()` and `overlay()` - modal content over the app.
71
+ - `overlayFrame()` and `overlay()` - content over the app, shown from
72
+ wherever it's built: any content, custom or animated. One overlay per
73
+ frame: an overlay shown on a frame already showing another evicts it (the
74
+ other isn't shown again until it's shown anew). Overlays stack across
75
+ frames - one opened from inside another's content - not side by side on
76
+ one. By design, for now: this may change should a use case turn up that
77
+ needs several on one frame.
78
+ - `modalAssembly()` - what an `overlay()` usually shows: a backdrop (a click
79
+ on it closes) and the content in a centered window - full screen, without
80
+ the backdrop, in a frame narrower than `fullScreenBelow`, and a themed
81
+ `dialog()` in it follows, back arrow and all. Its parts on their own:
82
+ `modalBackdrop()` and `modal()`.
83
+
84
+ ```js
85
+ overlay({ showing: this.open },
86
+ modalAssembly({ close, width: 360, fullScreenBelow: 600 },
87
+ dialog({ title: "Settings", close }, ...)))
88
+ ```
72
89
  - `popover()` - beside an element, following it when it moves.
73
- - `portal()` and `portalContents()` - content placed somewhere else in the tree.
90
+ - `dropdown({ options, value, onSelect })` - one choice out of several: a button
91
+ showing it, opening a list of them all in a popover. Made of the theme's own
92
+ button, card and list items; an option's `style` previews it in the list.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liquefy/cascade.ui",
3
- "version": "1.0.0",
4
- "description": "Themed widgets, layout, overlays, portals, popovers and color schemes for Cascade - and its basic theme.",
3
+ "version": "2.0.0",
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",
7
7
  "ui",
@@ -32,9 +32,9 @@
32
32
  "url": "https://github.com/erobwen/liquefy/issues"
33
33
  },
34
34
  "homepage": "https://github.com/erobwen/liquefy/tree/main/cascade.ui#readme",
35
- "dependencies": {
36
- "@liquefy/cascade.component": "^1.0.0",
37
- "@liquefy/cascade.dom": "^1.0.0"
35
+ "peerDependencies": {
36
+ "@liquefy/cascade.component": "^2.0.0",
37
+ "@liquefy/cascade.dom": "^2.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "mocha": "^10.8.2",
package/src/BasicTheme.js CHANGED
@@ -55,8 +55,9 @@ const styleSheet = `
55
55
  width: 36px; height: 36px; padding: 0; margin: 0; border: none; border-radius: 6px;
56
56
  color: inherit; background: transparent; cursor: pointer; transition: background-color 0.15s;
57
57
  }
58
- .cb-icon-button:hover { background: rgba(127, 150, 175, 0.18); }
59
- .cb-icon-button:active { background: rgba(127, 150, 175, 0.32); }
58
+ .cb-icon-button:hover:not(:disabled) { background: rgba(127, 150, 175, 0.18); }
59
+ .cb-icon-button:active:not(:disabled) { background: rgba(127, 150, 175, 0.32); }
60
+ .cb-icon-button:disabled { opacity: 0.38; cursor: default; }
60
61
  .cb-list-item { padding: 8px 12px; border-radius: 6px; cursor: pointer; user-select: none; transition: background-color 0.15s; }
61
62
  .cb-list-item:hover { background: ${c.accentFaint}; }
62
63
  .cb-list-item.cb-active { background: ${c.accentSoft}; font-weight: bold; }
@@ -154,14 +155,17 @@ class BasicDialog extends Component {
154
155
  setProperties({ title, close, fullScreen, style, children }) {
155
156
  this.title = title || "";
156
157
  this.close = close || null;
157
- this.fullScreen = !!fullScreen;
158
+ // Given, or - unset - as the modal() it's shown in presents it (see
159
+ // cascade.ui's Modal.js).
160
+ this.fullScreen = typeof(fullScreen) === "boolean" ? fullScreen : null;
158
161
  this.style = frozen(style || null);
159
162
  this.dialogChildren = frozen(children || []);
160
163
  }
161
164
 
162
165
  build() {
166
+ const fullScreen = this.fullScreen !== null ? this.fullScreen : this.inherit("modalPresentation") === "fullScreen";
163
167
  const close = callback("close", () => this.close && this.close());
164
- const frame = this.fullScreen
168
+ const frame = fullScreen
165
169
  ? { width: "100%", height: "100%" }
166
170
  : { borderRadius: "10px", boxShadow: dialogShadow };
167
171
  return column(
@@ -173,16 +177,16 @@ class BasicDialog extends Component {
173
177
  {
174
178
  key: "titleBar",
175
179
  style: {
176
- padding: this.fullScreen ? "6px 16px 6px 6px" : "6px 6px 6px 16px",
177
- ...(this.fullScreen ? { minHeight: "48px" } : {}),
180
+ padding: fullScreen ? "6px 16px 6px 6px" : "6px 6px 6px 16px",
181
+ ...(fullScreen ? { minHeight: "48px" } : {}),
178
182
  background: c.chromeDark, color: c.onChrome, flex: "none", alignItems: "center", gap: "8px", fontWeight: "bold",
179
183
  },
180
184
  },
181
- iconButton({ key: "back", icon: "arrow_back", title: "Back", onClick: close }).show(this.fullScreen),
185
+ iconButton({ key: "back", icon: "arrow_back", title: "Back", onClick: close }).showIf(fullScreen),
182
186
  // text(): a lone string starting lowercase (a file name, say)
183
187
  // would be taken for an implicit key.
184
188
  filler({ key: "title" }, text({ key: "titleText", text: this.title })),
185
- iconButton({ key: "close", icon: "close", title: "Close", onClick: close }).show(!this.fullScreen),
189
+ iconButton({ key: "close", icon: "close", title: "Close", onClick: close }).showIf(!fullScreen),
186
190
  ),
187
191
  // Sized by its content, scrolling once the dialog has a height of its own.
188
192
  column({ key: "body", style: { flex: "1 1 auto", minHeight: 0, overflow: "auto" } }, this.dialogChildren),
@@ -208,7 +212,7 @@ class BasicTextField extends Component {
208
212
  ensureStyleSheet();
209
213
  return htmlLabel(
210
214
  { key: "field", style: { display: "flex", flexDirection: "column", gap: "4px", ...this.style } },
211
- span({ key: "label", style: { fontSize: "13px", opacity: 0.8 } }, text({ key: "labelText", text: this.label })).show(!!this.label),
215
+ span({ key: "label", style: { fontSize: "13px", opacity: 0.8 } }, text({ key: "labelText", text: this.label })).showIf(!!this.label),
212
216
  row(
213
217
  { key: "inputRow", style: { alignItems: "center", gap: "6px" } },
214
218
  htmlInput({
@@ -222,7 +226,7 @@ class BasicTextField extends Component {
222
226
  }),
223
227
  this.unit ? span({ key: "unit" }, text({ key: "unitText", text: this.unit })) : null,
224
228
  ),
225
- div({ key: "error", style: { fontSize: "12px", color: c.error } }, text({ key: "errorText", text: this.error || "" })).show(!!this.error),
229
+ div({ key: "error", style: { fontSize: "12px", color: c.error } }, text({ key: "errorText", text: this.error || "" })).showIf(!!this.error),
226
230
  );
227
231
  }
228
232
  }
@@ -0,0 +1,84 @@
1
+ import { Component, frozen, callback } from "@liquefy/cascade.component";
2
+ import { span, text } from "@liquefy/cascade.dom";
3
+ import { button, icon, card, listItem } from "./widgets.js";
4
+ import { popover } from "./Popover.js";
5
+
6
+ /**
7
+ * Dropdown - one choice out of several, compactly: a button showing the
8
+ * choice made, opening a list of them all below it (a popover - see
9
+ * Popover.js). Made of the theme's own widgets - button, card, list
10
+ * items - so it looks like the rest of whatever theme it's in.
11
+ *
12
+ * dropdown({
13
+ * options: [{ value: "Title", label: "Title", style: { fontWeight: 700 } }, ...],
14
+ * value: "Title",
15
+ * onSelect: (value) => ...,
16
+ * })
17
+ *
18
+ * - options: `{ value, label, style }` - `style`, if given, is the label's
19
+ * own in the list: a preview of what choosing it means (a font, say).
20
+ * - value: the option chosen - none shows `placeholder`.
21
+ * - onSelect(value): called with the option clicked; the list closes.
22
+ * - disabled, title, placeholder, style (the button's).
23
+ */
24
+ export function dropdown(...parameters) {
25
+ return new Dropdown(...parameters);
26
+ }
27
+
28
+ export class Dropdown extends Component {
29
+ setProperties({ options, value, onSelect, disabled, title, placeholder, style }) {
30
+ this.options = frozen(options || []);
31
+ this.value = value;
32
+ this.onSelect = onSelect || null;
33
+ this.disabled = !!disabled;
34
+ this.title = title || undefined;
35
+ this.placeholder = placeholder || "";
36
+ this.style = frozen(style || null);
37
+ }
38
+
39
+ initialState() {
40
+ return { open: false, anchor: null };
41
+ }
42
+
43
+ choose(value) {
44
+ this.open = false;
45
+ if (this.onSelect) this.onSelect(value);
46
+ }
47
+
48
+ build() {
49
+ // Disabled while open: closed, for good - not hidden only to pop open on
50
+ // its own again once it's enabled.
51
+ if (this.disabled && this.open) this.setState({ open: false });
52
+ const chosen = this.options.find((option) => option.value === this.value);
53
+ return [
54
+ button(
55
+ {
56
+ disabled: this.disabled,
57
+ title: this.title,
58
+ style: { justifyContent: "space-between", gap: "4px", paddingRight: "6px", ...this.style },
59
+ onClick: callback("toggle", (event) => {
60
+ // The button itself - the list follows it, should it move.
61
+ this.anchor = event.currentTarget;
62
+ this.open = !this.open;
63
+ }),
64
+ },
65
+ text(chosen ? chosen.label : this.placeholder),
66
+ icon({ name: "arrow_drop_down" }),
67
+ ),
68
+ popover(
69
+ { anchor: this.anchor, showing: this.open, close: callback("close", () => { this.open = false; }) },
70
+ card(
71
+ { style: { padding: "4px", display: "flex", flexDirection: "column", gap: "2px", minWidth: "160px", maxHeight: "60vh", overflowY: "auto", boxShadow: "0 4px 16px rgba(0, 0, 0, 0.25)" } },
72
+ this.options.map((option) => listItem(
73
+ {
74
+ key: "option" + option.value,
75
+ active: option.value === this.value,
76
+ onClick: callback("choose" + option.value, () => this.choose(option.value)),
77
+ },
78
+ span({ style: option.style || {} }, text(option.label)),
79
+ )),
80
+ ),
81
+ ),
82
+ ];
83
+ }
84
+ }
package/src/Layout.js CHANGED
@@ -1,120 +1,153 @@
1
- import { toPropertiesWithChildren } from "@liquefy/cascade.component";
2
- import { div } from "@liquefy/cascade.dom";
3
-
4
- /**
5
- * Layout - ported from flow.ui/basic/src/Layout.js: a small kit of style
6
- * constants plus the styled-div functions built from them, all following
7
- * the same unwritten flow convention every component's own doc in this
8
- * codebase now points back to (see cascade.DOM/src/applyStyle.js): every
9
- * one of these takes an optional `style` property, merged as
10
- * `{...ownDefault, ...style}` - the caller's own value always wins - and
11
- * applied straight onto the one div each of these builds, not onto some
12
- * further wrapper. That's the actual mechanism behind "extra divs are
13
- * usually unnecessary": a creator styles its child directly, including
14
- * fitting it into a flex row/column or making it fill its container,
15
- * instead of wrapping it in a div dedicated to just that.
16
- *
17
- * Two style families here, not one "fillStyle": fitContainerStyle sizes by
18
- * *absolute* dimensions (width/height: 100% of the parent's own box, no
19
- * flex participation implied), fillerStyle sizes by *flex* participation
20
- * (flexGrow/Shrink: 1, flexBasis: 0 - takes an equal share of whatever
21
- * room a flex row/column has left). Reach for fitContainerStyle when the
22
- * parent isn't a flex container (or you want 100% regardless of flex
23
- * distribution) and fillerStyle when it is and you want this child to
24
- * share/absorb the remaining space with its siblings.
25
- *
26
- * One deliberate correction from flow's own copy: flow.ui/basic/src/Layout.js's
27
- * own flexContainerStyle sets `justifyContent: "flexStart"` - not valid CSS
28
- * (camelCase is for the *property* name in a JS style object, not a
29
- * property's *value* - the real value is the kebab-case string
30
- * "flex-start"). Harmless there in practice (an invalid value is simply
31
- * ignored, and justify-content's own initial value, "normal", already packs
32
- * items at the start for ordinary content) but not worth reproducing
33
- * knowingly - fixed here to the real value.
34
- *
35
- * flow's own componentTypeName argument to styledDiv() (purely a debug-id
36
- * label, baked into flow.core's own aggregateToString()-style element id)
37
- * isn't ported - cascade.DOM's DOMElementComponent doesn't have an equivalent
38
- * hook yet (setProperties() has no special case for it, so passing one
39
- * through today would just land as a meaningless "componenttypename" DOM
40
- * attribute instead). Every element built here still gets a real, unique
41
- * debug id from DOMNodeRenderComponent's own aggregateToString() fallback
42
- * (this.constructor.name-based) - just not one that says "row" vs "column".
43
- */
44
-
45
- // --- Basic layout styles ---
46
-
47
- export const flexContainerStyle = {
48
- overflow: "hidden",
49
- boxSizing: "border-box",
50
- display: "flex",
51
- alignItems: "stretch",
52
- justifyContent: "flex-start",
53
- whiteSpace: "normal",
54
- userSelect: "none",
55
- };
56
-
57
- export const rowStyle = { ...flexContainerStyle, flexDirection: "row" };
58
- export const columnStyle = { ...flexContainerStyle, flexDirection: "column" };
59
-
60
- export const centerStyle = { ...rowStyle, justifyContent: "center", alignItems: "stretch" };
61
- export const middleStyle = { ...columnStyle, justifyContent: "center", alignItems: "stretch" };
62
- export const centerMiddleStyle = { ...rowStyle, justifyContent: "center", alignItems: "center" };
63
-
64
- // --- Basic element styles ---
65
-
66
- // For a bottom-up component inside a scroll container - sized by its own
67
- // content, not stretched or shrunk to fit.
68
- export const naturalSizeStyle = { overflow: "visible", flexGrow: "0", flexShrink: "0", flexBasis: "auto" };
69
-
70
- // Fill 100% of the parent's own box - absolute sizing, no flex participation
71
- // implied (works inside or outside a flex container).
72
- export const fitContainerStyle = { overflow: "hidden", boxSizing: "border-box", width: "100%", height: "100%" };
73
-
74
- // Grow/shrink to take an equal share of whatever room is left in a flex
75
- // row/column, ignoring its own content's natural size - scroll panels,
76
- // equal space distribution.
77
- export const fillerStyle = { overflow: "hidden", boxSizing: "border-box", flexGrow: "1", flexShrink: "1", flexBasis: "0" };
78
-
79
- // Same, but based on its own content's natural size rather than an equal
80
- // 0-basis share - grows/shrinks around whatever it would naturally be.
81
- export const autoFillerStyle = { overflow: "hidden", boxSizing: "border-box", flexGrow: "1", flexShrink: "1", flexBasis: "auto" };
82
-
83
- // Visualizes a component's own bounds during development - not meant to
84
- // ship.
85
- export const layoutBorderStyle = { borderStyle: "solid", borderColor: "light-gray", borderWidth: "1px", boxSizing: "border-box" };
86
-
87
- // Opts back out of fitContainerStyle/fillerStyle's own overflow: hidden -
88
- // for content that must be allowed to overflow its own box (an animation,
89
- // a popover, ...).
90
- export const overflowVisibleStyle = { overflow: "visible" };
91
-
92
- // --- styledDiv: the shared mechanism behind every function below ---
93
-
94
- // A div (asked for through the service locator, like any div() call) with
95
- // `defaultStyle` merged underneath whatever `style` property is given, so
96
- // the caller's own value always wins (see this file's own class doc).
97
- function styledDiv(defaultStyle, parameters) {
98
- const properties = toPropertiesWithChildren(parameters);
99
- properties.style = { ...defaultStyle, ...properties.style };
100
- return div(properties);
101
- }
102
-
103
- // --- Basic layout containers ---
104
-
105
- export const wrapper = (...parameters) => styledDiv({}, parameters);
106
- export const row = (...parameters) => styledDiv(rowStyle, parameters);
107
- export const column = (...parameters) => styledDiv(columnStyle, parameters);
108
- export const center = (...parameters) => styledDiv(centerStyle, parameters);
109
- export const middle = (...parameters) => styledDiv(middleStyle, parameters);
110
- export const centerMiddle = (...parameters) => styledDiv(centerMiddleStyle, parameters);
111
-
112
- // --- Basic layout fillers ---
113
-
114
- export const filler = (...parameters) => styledDiv(fillerStyle, parameters);
115
-
116
- // --- zStack: children stacked on top of each other, edge to edge ---
117
-
118
- export const zStackElementStyle = { ...fitContainerStyle, position: "absolute", top: 0, left: 0, width: "100%", height: "100%" };
119
-
120
- export const zStack = (...parameters) => styledDiv({ position: "relative" }, parameters);
1
+ import { toPropertiesWithChildren } from "@liquefy/cascade.component";
2
+ import { div } from "@liquefy/cascade.dom";
3
+
4
+ /**
5
+ * Layout - ported from flow.ui/basic/src/Layout.js: a small kit of style
6
+ * constants plus the styled-div functions built from them, all following
7
+ * the same unwritten flow convention every component's own doc in this
8
+ * codebase now points back to (see cascade.DOM/src/applyStyle.js): every
9
+ * one of these takes an optional `style` property, merged as
10
+ * `{...ownDefault, ...style}` - the caller's own value always wins - and
11
+ * applied straight onto the one div each of these builds, not onto some
12
+ * further wrapper. That's the actual mechanism behind "extra divs are
13
+ * usually unnecessary": a creator styles its child directly, including
14
+ * fitting it into a flex row/column or making it fill its container,
15
+ * instead of wrapping it in a div dedicated to just that.
16
+ *
17
+ * Two style families here, not one "fillStyle": fitContainerStyle sizes by
18
+ * *absolute* dimensions (width/height: 100% of the parent's own box, no
19
+ * flex participation implied), fillerStyle sizes by *flex* participation
20
+ * (flexGrow/Shrink: 1, flexBasis: 0 - takes an equal share of whatever
21
+ * room a flex row/column has left). Reach for fitContainerStyle when the
22
+ * parent isn't a flex container (or you want 100% regardless of flex
23
+ * distribution) and fillerStyle when it is and you want this child to
24
+ * share/absorb the remaining space with its siblings.
25
+ *
26
+ * One deliberate correction from flow's own copy: flow.ui/basic/src/Layout.js's
27
+ * own flexContainerStyle sets `justifyContent: "flexStart"` - not valid CSS
28
+ * (camelCase is for the *property* name in a JS style object, not a
29
+ * property's *value* - the real value is the kebab-case string
30
+ * "flex-start"). Harmless there in practice (an invalid value is simply
31
+ * ignored, and justify-content's own initial value, "normal", already packs
32
+ * items at the start for ordinary content) but not worth reproducing
33
+ * knowingly - fixed here to the real value.
34
+ *
35
+ * flow's own componentTypeName argument to styledDiv() (purely a debug-id
36
+ * label, baked into flow.core's own aggregateToString()-style element id)
37
+ * isn't ported - cascade.DOM's DOMElementComponent doesn't have an equivalent
38
+ * hook yet (setProperties() has no special case for it, so passing one
39
+ * through today would just land as a meaningless "componenttypename" DOM
40
+ * attribute instead). Every element built here still gets a real, unique
41
+ * debug id from DOMNodeComponent's own aggregateToString() fallback
42
+ * (this.constructor.name-based) - just not one that says "row" vs "column".
43
+ */
44
+
45
+ // --- Basic layout styles ---
46
+
47
+ // No overflow: hidden, unlike flow's: flow clipped every layout container,
48
+ // for a strict top-down layout that made bounds easier to calculate further
49
+ // down - measured bounds (cascade.dom's elementBoundsProvider()) do that job
50
+ // now. What it did for flex layout is kept, though: min-width/min-height 0
51
+ // let a container shrink below its content's size in the row or column
52
+ // it's in (flex items don't, by default - they'd push the row wider
53
+ // instead). Its content then overflows, visibly, where it doesn't fit;
54
+ // clip it where that's wanted (overflow: "hidden" in its style). And text
55
+ // in it can be selected, like text anywhere else.
56
+ const shrinkable = { minWidth: 0, minHeight: 0 };
57
+
58
+ export const flexContainerStyle = {
59
+ ...shrinkable,
60
+ boxSizing: "border-box",
61
+ display: "flex",
62
+ alignItems: "stretch",
63
+ justifyContent: "flex-start",
64
+ whiteSpace: "normal",
65
+ };
66
+
67
+ export const rowStyle = { ...flexContainerStyle, flexDirection: "row" };
68
+ export const columnStyle = { ...flexContainerStyle, flexDirection: "column" };
69
+
70
+ export const centerStyle = { ...rowStyle, justifyContent: "center", alignItems: "stretch" };
71
+ export const middleStyle = { ...columnStyle, justifyContent: "center", alignItems: "stretch" };
72
+ export const centerMiddleStyle = { ...rowStyle, justifyContent: "center", alignItems: "center" };
73
+
74
+ // --- Basic element styles ---
75
+
76
+ // For a bottom-up component inside a scroll container - sized by its own
77
+ // content, not stretched or shrunk to fit.
78
+ export const naturalSizeStyle = { overflow: "visible", flexGrow: "0", flexShrink: "0", flexBasis: "auto" };
79
+
80
+ // Fill 100% of the parent's own box - absolute sizing, no flex participation
81
+ // implied (works inside or outside a flex container).
82
+ export const fitContainerStyle = { ...shrinkable, boxSizing: "border-box", width: "100%", height: "100%" };
83
+
84
+ // Grow/shrink to take an equal share of whatever room is left in a flex
85
+ // row/column, ignoring its own content's natural size - scroll panels,
86
+ // equal space distribution.
87
+ export const fillerStyle = { ...shrinkable, boxSizing: "border-box", flexGrow: "1", flexShrink: "1", flexBasis: "0" };
88
+
89
+ // Same, but based on its own content's natural size rather than an equal
90
+ // 0-basis share - grows/shrinks around whatever it would naturally be.
91
+ export const autoFillerStyle = { ...shrinkable, boxSizing: "border-box", flexGrow: "1", flexShrink: "1", flexBasis: "auto" };
92
+
93
+ // Visualizes a component's own bounds during development - not meant to
94
+ // ship.
95
+ export const layoutBorderStyle = { borderStyle: "solid", borderColor: "lightgray", borderWidth: "1px", boxSizing: "border-box" };
96
+
97
+ // Content allowed to overflow its own box - what every layout container
98
+ // here does already (there's no clipping by default any more); kept for
99
+ // what's written against the old default.
100
+ export const overflowVisibleStyle = { overflow: "visible" };
101
+
102
+ // --- styledDiv: the shared mechanism behind every function below ---
103
+
104
+ // A div (asked for through the service locator, like any div() call) with
105
+ // `defaultStyle` merged underneath whatever `style` property is given, so
106
+ // the caller's own value always wins (see this file's own class doc).
107
+ function styledDiv(defaultStyle, parameters) {
108
+ const properties = toPropertiesWithChildren(parameters);
109
+ properties.style = { ...defaultStyle, ...properties.style };
110
+ return div(properties);
111
+ }
112
+
113
+ // --- Basic layout containers ---
114
+
115
+ export const wrapper = (...parameters) => styledDiv({}, parameters);
116
+ export const row = (...parameters) => styledDiv(rowStyle, parameters);
117
+ export const column = (...parameters) => styledDiv(columnStyle, parameters);
118
+ export const center = (...parameters) => styledDiv(centerStyle, parameters);
119
+ export const middle = (...parameters) => styledDiv(middleStyle, parameters);
120
+ export const centerMiddle = (...parameters) => styledDiv(centerMiddleStyle, parameters);
121
+
122
+ // --- Basic layout fillers ---
123
+
124
+ export const filler = (...parameters) => styledDiv(fillerStyle, parameters);
125
+
126
+ // --- zStack: children stacked on top of each other, edge to edge ---
127
+
128
+ // What each child of a zStack gets - by the zStack itself (a class, and a
129
+ // stylesheet rule for its direct children), so its children need no style
130
+ // of their own for it. A child's own style still wins: it can deviate.
131
+ export const zStackElementStyle = { ...fitContainerStyle, position: "absolute", top: 0, left: 0, width: "100%", height: "100%" };
132
+
133
+ const zStackClass = "cascade-z-stack";
134
+ const documentsStyled = new WeakSet();
135
+
136
+ function ensureZStackRule() {
137
+ if (typeof(document) === "undefined" || !document.head || documentsStyled.has(document)) return;
138
+ documentsStyled.add(document);
139
+ const element = document.createElement("style");
140
+ element.setAttribute("data-cascade-layout", "");
141
+ element.textContent = "." + zStackClass + " > * { position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box; min-width: 0; min-height: 0; }";
142
+ document.head.appendChild(element);
143
+ }
144
+
145
+ export const zStack = (...parameters) => {
146
+ ensureZStackRule();
147
+ const properties = toPropertiesWithChildren(parameters);
148
+ const given = properties.className || properties.class;
149
+ delete properties.className;
150
+ properties.class = given ? zStackClass + " " + given : zStackClass;
151
+ properties.style = { position: "relative", ...properties.style };
152
+ return div(properties);
153
+ };
package/src/Modal.js ADDED
@@ -0,0 +1,136 @@
1
+ import { Component, frozen, observable, accessInitialValues, withoutRecording } from "@liquefy/cascade.component";
2
+ import { div } from "@liquefy/cascade.dom";
3
+
4
+ /**
5
+ * Modals - ready-made pieces for what an overlay() shows, each usable on
6
+ * its own:
7
+ *
8
+ * - modalBackdrop({ close, style }): the dimmed layer behind a modal,
9
+ * covering the frame; a click on it calls `close`.
10
+ * - modal({ fullScreen, width, height, style }, content): places its
11
+ * content in the frame - a window of the given size, centered, or
12
+ * (`fullScreen`) filling the whole frame. The content fills it. What's
13
+ * inside can tell which: modal() provides `modalPresentation`
14
+ * ("window" or "fullScreen") - a themed dialog() goes full screen by
15
+ * itself, back arrow and all.
16
+ * - modalAssembly({ close, fullScreenBelow, width, height, backdrop }, content):
17
+ * both - a backdrop (unless `backdrop: false`) and a modal - and full
18
+ * screen, without a backdrop, while the frame is narrower than
19
+ * `fullScreenBelow` (a phone's screen): it reads the frame's size where
20
+ * it's shown (see OverlayFrame - a layer is a measured element).
21
+ *
22
+ * The ready-to-go way to show a dialog modally, then:
23
+ *
24
+ * overlay({ showing: this.open },
25
+ * modalAssembly({ close, fullScreenBelow: 600, width: 360 },
26
+ * dialog({ title: "Settings", close }, ...)))
27
+ *
28
+ * And for anything else - an animated entrance, a sheet sliding up from
29
+ * below - overlay() takes any content at all, a backdrop of its own
30
+ * included.
31
+ */
32
+ export function modalBackdrop(...parameters) {
33
+ return new ModalBackdrop(...parameters);
34
+ }
35
+
36
+ export function modal(...parameters) {
37
+ return new Modal(...parameters);
38
+ }
39
+
40
+ export function modalAssembly(...parameters) {
41
+ return new ModalAssembly(...parameters);
42
+ }
43
+
44
+ // The dimmed layer behind a modal - where a click closes it.
45
+ export const modalBackdropColor = "rgba(0, 0, 0, 0.4)";
46
+
47
+ const coverFrame = { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", boxSizing: "border-box" };
48
+
49
+ export class ModalBackdrop extends Component {
50
+ setProperties({ close, style }) {
51
+ this.close = close || null;
52
+ this.style = frozen(style || null);
53
+ }
54
+
55
+ build() {
56
+ return div({
57
+ onclick: this.close,
58
+ style: { ...coverFrame, pointerEvents: "auto", background: modalBackdropColor, ...this.style },
59
+ });
60
+ }
61
+ }
62
+
63
+ export class Modal extends Component {
64
+ setProperties({ fullScreen, width, height, style, children }) {
65
+ this.fullScreen = !!fullScreen;
66
+ this.width = typeof(width) === "number" ? width + "px" : (width || null);
67
+ this.height = typeof(height) === "number" ? height + "px" : (height || null);
68
+ this.style = frozen(style || null);
69
+ this.modalChildren = frozen(children || []);
70
+ }
71
+
72
+ // What's inside can tell how it's presented - not a size, a mode, the
73
+ // same for everything in it. An object of its own, rewritten at the
74
+ // baseline when it changes, as everything a context holds (see
75
+ // cascade.component's RenderContext.js).
76
+ provide() {
77
+ const u = this.unobservable;
78
+ if (!u.provided) u.provided = observable({ modalPresentation: this.presentation() });
79
+ return u.provided;
80
+ }
81
+
82
+ presentation() {
83
+ return this.fullScreen ? "fullScreen" : "window";
84
+ }
85
+
86
+ build() {
87
+ const presentation = this.presentation();
88
+ const provided = this.provide();
89
+ if (withoutRecording(() => provided.modalPresentation) !== presentation) {
90
+ accessInitialValues(() => { provided.modalPresentation = presentation; });
91
+ }
92
+ // The content fills whatever it's in: the frame, or the window.
93
+ const box = { display: "flex", flexDirection: "column", boxSizing: "border-box", pointerEvents: "auto" };
94
+ if (this.fullScreen) {
95
+ return div({ style: { ...coverFrame, ...box, ...this.style } }, this.modalChildren);
96
+ }
97
+ return div(
98
+ { style: { ...coverFrame, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none" } },
99
+ div(
100
+ { style: { ...box, width: this.width || "auto", height: this.height || "auto", maxWidth: "100%", maxHeight: "100%", ...this.style } },
101
+ this.modalChildren,
102
+ ),
103
+ );
104
+ }
105
+ }
106
+
107
+ export class ModalAssembly extends Component {
108
+ setProperties({ close, fullScreen, fullScreenBelow, width, height, backdrop, style, children }) {
109
+ this.close = close || null;
110
+ this.fullScreen = typeof(fullScreen) === "boolean" ? fullScreen : null;
111
+ this.fullScreenBelow = typeof(fullScreenBelow) === "number" ? fullScreenBelow : null;
112
+ this.width = width;
113
+ this.height = height;
114
+ this.backdrop = backdrop !== false;
115
+ this.style = frozen(style || null);
116
+ this.modalChildren = frozen(children || []);
117
+ }
118
+
119
+ // Full screen: as told - or below `fullScreenBelow`, by the width of the
120
+ // frame it's shown in (a measured layer - see OverlayFrame).
121
+ isFullScreen() {
122
+ if (this.fullScreen !== null) return this.fullScreen;
123
+ if (this.fullScreenBelow === null) return false;
124
+ const width = this.fromTarget("width");
125
+ return typeof(width) === "number" && width < this.fullScreenBelow;
126
+ }
127
+
128
+ build() {
129
+ const fullScreen = this.isFullScreen();
130
+ return [
131
+ // Keyed: left out while full screen, and kept.
132
+ modalBackdrop({ key: "backdrop", close: this.close }).showIf(this.backdrop && !fullScreen),
133
+ modal({ fullScreen, width: this.width, height: this.height, style: this.style }, this.modalChildren),
134
+ ];
135
+ }
136
+ }