@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 +23 -4
- package/package.json +5 -5
- package/src/BasicTheme.js +14 -10
- package/src/Dropdown.js +84 -0
- package/src/Layout.js +153 -120
- package/src/Modal.js +136 -0
- package/src/Overlay.js +95 -85
- package/src/OverlayFrame.js +77 -39
- package/src/Popover.js +174 -158
- package/src/index.js +3 -2
- package/src/widgets.js +127 -109
- package/src/Portal.js +0 -132
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cascade.ui
|
|
2
2
|
|
|
3
|
-
Themed widgets, layout, overlays,
|
|
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(
|
|
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()` -
|
|
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
|
-
- `
|
|
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": "
|
|
4
|
-
"description": "Themed widgets, layout, overlays,
|
|
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
|
-
"
|
|
36
|
-
"@liquefy/cascade.component": "^
|
|
37
|
-
"@liquefy/cascade.dom": "^
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
177
|
-
...(
|
|
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 }).
|
|
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 }).
|
|
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 })).
|
|
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 || "" })).
|
|
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
|
}
|
package/src/Dropdown.js
ADDED
|
@@ -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
|
|
42
|
-
* (this.constructor.name-based) - just not one that says "row" vs "column".
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
// --- Basic layout styles ---
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
export const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
export const
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
export const
|
|
119
|
-
|
|
120
|
-
export const
|
|
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
|
+
}
|