@liquefy/cascade.ui 1.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, Robert Renbris
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # cascade.ui
2
+
3
+ Themed widgets, layout, overlays, portals, popovers and color schemes for
4
+ [Cascade](https://github.com/erobwen/liquefy#readme) - and its basic theme.
5
+
6
+ ```console
7
+ npm install @liquefy/cascade.ui @liquefy/cascade.dom @liquefy/cascade.component @liquefy/cascade.reactive
8
+ ```
9
+
10
+ ## Themed widgets
11
+
12
+ Nothing in an app names a theme when it builds a widget: `button(...)` asks
13
+ the render context's service locator for one, and whichever theme is in it
14
+ provides it. Switching the theme replaces whole components, not just their
15
+ style - with all app state kept where it was. A part of an app can have a
16
+ theme of its own (`serviceProvider()`).
17
+
18
+ `button`, `icon`, `iconButton`, `card`, `controlPanel`, `alert`, `listItem`,
19
+ `dialog`, `textField`, `checkbox`, `colorField`, `tabBar` - each with a
20
+ property contract every theme implements (see `src/widgets.js`).
21
+
22
+ Two themes to choose from: `basicTheme`, from this package - plain HTML, in
23
+ tones of one base color, softly rounded - and `materialTheme`, from
24
+ [@liquefy/cascade.ui.material](https://github.com/erobwen/liquefy/tree/main/cascade.ui.material#readme).
25
+
26
+ ```js
27
+ import { CompoundServiceLocator } from "@liquefy/cascade.component";
28
+ import { DOMServiceLocator } from "@liquefy/cascade.dom";
29
+ import { basicTheme } from "@liquefy/cascade.ui";
30
+
31
+ const services = new CompoundServiceLocator(new DOMServiceLocator(), basicTheme);
32
+ ```
33
+
34
+ To switch themes while the app runs, use an `ObservableCompoundServiceLocator`
35
+ and replace the theme in it: everything built with a themed widget rebuilds
36
+ with the new one.
37
+
38
+ The icons are Google's icon fonts - link them in your page:
39
+
40
+ ```html
41
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
42
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
43
+ ```
44
+
45
+ (Material Symbols for the basic theme, Material Icons for the Material theme.)
46
+
47
+ ## Color schemes
48
+
49
+ A theme is its colors too: a base and an accent, and every other color made
50
+ from them - tones of their hue, at preset brightness levels. They're CSS
51
+ variables (`themeColor.page` is `var(--cascade-page, ...)`): put a theme's
52
+ variables on an element with `colorSchemeScope()`, and everything in it takes
53
+ its colors - change the base color, and only that element's variables are
54
+ rewritten.
55
+
56
+ ```js
57
+ import { text } from "@liquefy/cascade.dom";
58
+ import { button, currentColorScheme } from "@liquefy/cascade.ui";
59
+
60
+ // In a build(): the scheme of the theme in the render context - and a
61
+ // button giving it another base color. Everything inside a
62
+ // colorSchemeScope() of that theme follows (put one around your app).
63
+ const scheme = currentColorScheme();
64
+ return button({ key: "green" }, text({ key: "greenText", text: "Go green" }), () => { scheme.base = "#2e7d32"; });
65
+ ```
66
+
67
+ ## Layout and the rest
68
+
69
+ - `row`, `column`, `filler`, `centerMiddle`, `zStack`, ... and their styles
70
+ (`fillerStyle`, `fitContainerStyle`, ...).
71
+ - `overlayFrame()` and `overlay()` - modal content over the app.
72
+ - `popover()` - beside an element, following it when it moves.
73
+ - `portal()` and `portalContents()` - content placed somewhere else in the tree.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
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.",
5
+ "keywords": [
6
+ "reactive",
7
+ "ui",
8
+ "widgets",
9
+ "themes",
10
+ "cascade"
11
+ ],
12
+ "author": "Robert Renbris",
13
+ "license": "ISC",
14
+ "type": "module",
15
+ "main": "./src/index.js",
16
+ "module": "./src/index.js",
17
+ "files": [
18
+ "src",
19
+ "!src/test",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "test": "mocha ./src/test"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/erobwen/liquefy.git",
29
+ "directory": "cascade.ui"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/erobwen/liquefy/issues"
33
+ },
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"
38
+ },
39
+ "devDependencies": {
40
+ "mocha": "^10.8.2",
41
+ "jsdom": "^24.1.1"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
@@ -0,0 +1,390 @@
1
+ import { Component, frozen, callback } from "@liquefy/cascade.component";
2
+ import { button as htmlButton, input as htmlInput, label as htmlLabel, span, div, text } from "@liquefy/cascade.dom";
3
+ import { row, column, filler, wrapper } from "./Layout.js";
4
+ import { icon, iconButton, alertSeverities } from "./widgets.js";
5
+ import { themeColor, ColorScheme, defaultColors } from "./colorScheme.js";
6
+
7
+ /**
8
+ * The basic theme - plain HTML, lightly styled (ported from
9
+ * flow.ui/basic's own buttons.js, card.js, alert.js, listItem.js and
10
+ * dialogue.js). A service locator (see cascade.component's
11
+ * ServiceLocator.js) answering `{ type: "widget", name }` queries for the
12
+ * widgets in widgets.js (see there for each one's property contract),
13
+ * built out of ordinary HTML elements - which are themselves looked up the
14
+ * same way, so a DOM-level locator further along still decides what an
15
+ * HTML `button` actually is. Widgets used inside other widgets (the icon
16
+ * in an alert, ...) are asked for the same way too, so they come from
17
+ * whatever theme is in the context.
18
+ *
19
+ * The look: tones of one base color - slate blue, unless changed (see
20
+ * colorScheme.js: every color here is one of its CSS variables) - soft
21
+ * corners - square, just rounded a little (6px on controls, 8px on
22
+ * surfaces; the Material theme's are pill shaped) - and one discreet
23
+ * shadow for whatever lies on the page. Buttons are white chips on it.
24
+ *
25
+ * What changes on hover, focus or press can't be written as inline style,
26
+ * so those widgets get their look from a small stylesheet of the theme's
27
+ * own, added to the document the first time one is made (see
28
+ * ensureStyleSheet()) - and inline style is left for what the caller gives.
29
+ */
30
+
31
+ // The theme's colors - its scheme's CSS variables - and its shadows.
32
+ const c = { ...themeColor, error: "#c0392b" };
33
+ export const basicShadow = "0 1px 2px rgba(30, 40, 55, 0.10), 0 2px 6px rgba(30, 40, 55, 0.08)";
34
+ const dialogShadow = "0 12px 32px rgba(30, 40, 55, 0.35)";
35
+
36
+ const styleSheet = `
37
+ .cb-button {
38
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
39
+ box-sizing: border-box; min-height: 32px; padding: 0 14px; margin: 0;
40
+ font: inherit; font-size: 14px; font-weight: 500; line-height: 1.2; white-space: nowrap;
41
+ color: ${c.text}; background: ${c.surface}; border: 1px solid ${c.border}; border-radius: 6px;
42
+ box-shadow: ${basicShadow}; cursor: pointer; user-select: none;
43
+ transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
44
+ }
45
+ .cb-button:hover:not(:disabled) { background: ${c.surfaceHover}; border-color: ${c.borderStrong}; }
46
+ .cb-button:active:not(:disabled) { background: ${c.filled}; box-shadow: none; }
47
+ .cb-button.cb-filled { color: ${c.onChrome}; background: ${c.chrome}; border-color: ${c.chrome}; }
48
+ .cb-button.cb-filled:hover:not(:disabled) { background: ${c.chromeLight}; border-color: ${c.chromeLight}; }
49
+ .cb-button.cb-filled:active:not(:disabled) { background: ${c.chromeDark}; }
50
+ .cb-button:disabled { opacity: 0.5; cursor: default; box-shadow: none; }
51
+ .cb-button .cb-icon { font-size: 20px; }
52
+ .cb-icon { font-size: 24px; line-height: 1; user-select: none; }
53
+ .cb-icon-button {
54
+ display: inline-flex; align-items: center; justify-content: center; flex: none;
55
+ width: 36px; height: 36px; padding: 0; margin: 0; border: none; border-radius: 6px;
56
+ color: inherit; background: transparent; cursor: pointer; transition: background-color 0.15s;
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); }
60
+ .cb-list-item { padding: 8px 12px; border-radius: 6px; cursor: pointer; user-select: none; transition: background-color 0.15s; }
61
+ .cb-list-item:hover { background: ${c.accentFaint}; }
62
+ .cb-list-item.cb-active { background: ${c.accentSoft}; font-weight: bold; }
63
+ .cb-input {
64
+ box-sizing: border-box; height: 32px; padding: 0 10px; min-width: 0; font: inherit;
65
+ color: ${c.text}; background: ${c.surface}; border: 1px solid ${c.border}; border-radius: 6px;
66
+ transition: border-color 0.15s, box-shadow 0.15s;
67
+ }
68
+ .cb-input:focus { outline: none; border-color: ${c.accent}; box-shadow: 0 0 0 3px ${c.accentRing}; }
69
+ .cb-input.cb-invalid { border-color: ${c.error}; background: #fdf1f0; }
70
+ .cb-color {
71
+ box-sizing: border-box; width: 40px; height: 32px; padding: 3px; margin: 0; flex: none;
72
+ background: ${c.surface}; border: 1px solid ${c.border}; border-radius: 6px; cursor: pointer;
73
+ }
74
+ .cb-color:hover { border-color: ${c.borderStrong}; }
75
+ .cb-color::-webkit-color-swatch-wrapper { padding: 0; }
76
+ .cb-color::-webkit-color-swatch { border: none; border-radius: 4px; }
77
+ .cb-color::-moz-color-swatch { border: none; border-radius: 4px; }
78
+ .cb-tab-bar { display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid ${c.border}; }
79
+ .cb-tab {
80
+ padding: 8px 16px; margin: 0 0 -1px 0; font: inherit; font-weight: 500; color: ${c.textSoft};
81
+ background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 6px 6px 0 0;
82
+ cursor: pointer; transition: background-color 0.15s, color 0.15s;
83
+ }
84
+ .cb-tab:hover { color: ${c.text}; background: ${c.accentFaint}; }
85
+ .cb-tab.cb-selected { color: ${c.accent}; border-bottom-color: ${c.accent}; }
86
+ .cb-button:focus-visible, .cb-icon-button:focus-visible, .cb-list-item:focus-visible, .cb-tab:focus-visible, .cb-color:focus-visible {
87
+ outline: 2px solid ${c.accent}; outline-offset: 2px;
88
+ }
89
+ `;
90
+
91
+ // Per document - each gets its own copy.
92
+ const documentsStyled = new WeakSet();
93
+
94
+ function ensureStyleSheet() {
95
+ if (typeof(document) === "undefined" || !document.head || documentsStyled.has(document)) return;
96
+ documentsStyled.add(document);
97
+ const element = document.createElement("style");
98
+ element.setAttribute("data-cascade-theme", "basic");
99
+ element.textContent = styleSheet;
100
+ document.head.appendChild(element);
101
+ }
102
+
103
+ const cardStyles = {
104
+ elevated: { background: c.surface, boxShadow: basicShadow },
105
+ filled: { background: c.filled },
106
+ outlined: { background: c.surface, border: "1px solid " + c.border },
107
+ };
108
+
109
+ const onClickOf = (onClick) => (onClick ? { onclick: onClick } : {});
110
+
111
+ // A square button (rounded a little) with just an icon in it. Its color is
112
+ // inherited, so it fits on a dark bar as well as a light page.
113
+ class BasicIconButton extends Component {
114
+ setProperties({ icon, onClick, style, children, ...rest }) {
115
+ this.icon = icon;
116
+ this.onClick = onClick || null;
117
+ this.style = frozen(style || null);
118
+ this.rest = frozen(rest);
119
+ }
120
+
121
+ build() {
122
+ ensureStyleSheet();
123
+ return htmlButton(
124
+ { ...this.rest, key: "button", class: "cb-icon-button", ...onClickOf(this.onClick), style: this.style || {} },
125
+ icon({ key: "icon", name: this.icon }),
126
+ );
127
+ }
128
+ }
129
+
130
+ class BasicAlert extends Component {
131
+ setProperties({ severity, style, children }) {
132
+ this.severity = alertSeverities[severity] ? severity : "info";
133
+ this.style = frozen(style || null);
134
+ this.alertChildren = frozen(children || []);
135
+ }
136
+
137
+ build() {
138
+ const severity = alertSeverities[this.severity];
139
+ return row(
140
+ {
141
+ key: "alert",
142
+ style: {
143
+ gap: "12px", padding: "12px 16px", borderRadius: "8px", alignItems: "center", boxSizing: "border-box",
144
+ background: severity.background, color: severity.color, ...this.style,
145
+ },
146
+ },
147
+ icon({ key: "icon", name: severity.icon, style: { fontSize: "32px", color: severity.iconColor, flex: "none" } }),
148
+ wrapper({ key: "message", style: { flex: "1 1 auto", minWidth: 0 } }, this.alertChildren),
149
+ );
150
+ }
151
+ }
152
+
153
+ class BasicDialog extends Component {
154
+ setProperties({ title, close, fullScreen, style, children }) {
155
+ this.title = title || "";
156
+ this.close = close || null;
157
+ this.fullScreen = !!fullScreen;
158
+ this.style = frozen(style || null);
159
+ this.dialogChildren = frozen(children || []);
160
+ }
161
+
162
+ build() {
163
+ const close = callback("close", () => this.close && this.close());
164
+ const frame = this.fullScreen
165
+ ? { width: "100%", height: "100%" }
166
+ : { borderRadius: "10px", boxShadow: dialogShadow };
167
+ return column(
168
+ {
169
+ key: "dialog",
170
+ style: { background: c.surface, color: c.text, boxSizing: "border-box", overflow: "hidden", pointerEvents: "auto", ...frame, ...this.style },
171
+ },
172
+ row(
173
+ {
174
+ key: "titleBar",
175
+ style: {
176
+ padding: this.fullScreen ? "6px 16px 6px 6px" : "6px 6px 6px 16px",
177
+ ...(this.fullScreen ? { minHeight: "48px" } : {}),
178
+ background: c.chromeDark, color: c.onChrome, flex: "none", alignItems: "center", gap: "8px", fontWeight: "bold",
179
+ },
180
+ },
181
+ iconButton({ key: "back", icon: "arrow_back", title: "Back", onClick: close }).show(this.fullScreen),
182
+ // text(): a lone string starting lowercase (a file name, say)
183
+ // would be taken for an implicit key.
184
+ filler({ key: "title" }, text({ key: "titleText", text: this.title })),
185
+ iconButton({ key: "close", icon: "close", title: "Close", onClick: close }).show(!this.fullScreen),
186
+ ),
187
+ // Sized by its content, scrolling once the dialog has a height of its own.
188
+ column({ key: "body", style: { flex: "1 1 auto", minHeight: 0, overflow: "auto" } }, this.dialogChildren),
189
+ );
190
+ }
191
+ }
192
+
193
+ // A label above an input (and its unit, if any), and the error under it,
194
+ // if there is one.
195
+ class BasicTextField extends Component {
196
+ setProperties({ label, placeholder, value, onInput, type, unit, error, style }) {
197
+ this.label = label || "";
198
+ this.placeholder = placeholder || "";
199
+ this.value = value === undefined || value === null ? "" : value;
200
+ this.onInput = onInput || null;
201
+ this.type = type || "text";
202
+ this.unit = unit || null;
203
+ this.error = error || null;
204
+ this.style = frozen(style || null);
205
+ }
206
+
207
+ build() {
208
+ ensureStyleSheet();
209
+ return htmlLabel(
210
+ { 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),
212
+ row(
213
+ { key: "inputRow", style: { alignItems: "center", gap: "6px" } },
214
+ htmlInput({
215
+ key: "input",
216
+ class: this.error ? "cb-input cb-invalid" : "cb-input",
217
+ type: this.type,
218
+ placeholder: this.placeholder,
219
+ value: this.value,
220
+ oninput: callback("input", (event) => this.onInput && this.onInput(event.target.value)),
221
+ style: this.type === "number" ? { width: "88px" } : { flex: "1 1 auto" },
222
+ }),
223
+ this.unit ? span({ key: "unit" }, text({ key: "unitText", text: this.unit })) : null,
224
+ ),
225
+ div({ key: "error", style: { fontSize: "12px", color: c.error } }, text({ key: "errorText", text: this.error || "" })).show(!!this.error),
226
+ );
227
+ }
228
+ }
229
+
230
+ class BasicCheckbox extends Component {
231
+ setProperties({ label, checked, onChange, style }) {
232
+ this.label = label || "";
233
+ this.checked = !!checked;
234
+ this.onChange = onChange || null;
235
+ this.style = frozen(style || null);
236
+ }
237
+
238
+ build() {
239
+ return htmlLabel(
240
+ { key: "field", style: { display: "flex", alignItems: "center", gap: "8px", cursor: "pointer", userSelect: "none", ...this.style } },
241
+ htmlInput({
242
+ key: "input",
243
+ type: "checkbox",
244
+ checked: this.checked,
245
+ onchange: callback("change", (event) => this.onChange && this.onChange(event.target.checked)),
246
+ style: { width: "18px", height: "18px", margin: 0, accentColor: c.chrome },
247
+ }),
248
+ text({ key: "labelText", text: this.label }),
249
+ );
250
+ }
251
+ }
252
+
253
+ // A color, its label beside it - the browser's own picker opens on a click.
254
+ class BasicColorField extends Component {
255
+ setProperties({ label, value, onInput, style }) {
256
+ this.label = label || "";
257
+ this.value = value || "#000000";
258
+ this.onInput = onInput || null;
259
+ this.style = frozen(style || null);
260
+ }
261
+
262
+ build() {
263
+ ensureStyleSheet();
264
+ return htmlLabel(
265
+ { key: "field", style: { display: "flex", alignItems: "center", gap: "8px", cursor: "pointer", ...this.style } },
266
+ htmlInput({
267
+ key: "input",
268
+ class: "cb-color",
269
+ type: "color",
270
+ value: this.value,
271
+ oninput: callback("input", (event) => this.onInput && this.onInput(event.target.value)),
272
+ }),
273
+ text({ key: "labelText", text: this.label }),
274
+ );
275
+ }
276
+ }
277
+
278
+ // Tabs, underlined in the accent color when selected.
279
+ class BasicTabBar extends Component {
280
+ setProperties({ tabs, selected, onSelect, style }) {
281
+ this.tabs = frozen(tabs || []);
282
+ this.selected = selected;
283
+ this.onSelect = onSelect || null;
284
+ this.style = frozen(style || null);
285
+ }
286
+
287
+ build() {
288
+ ensureStyleSheet();
289
+ return div(
290
+ { key: "tabs", class: "cb-tab-bar", role: "tablist", style: this.style || {} },
291
+ this.tabs.map((tab) => htmlButton(
292
+ {
293
+ key: tab.key,
294
+ class: tab.key === this.selected ? "cb-tab cb-selected" : "cb-tab",
295
+ role: "tab",
296
+ onclick: callback("select" + tab.key, () => this.onSelect && this.onSelect(tab.key)),
297
+ },
298
+ text({ key: tab.key + "Title", text: tab.title }),
299
+ )),
300
+ );
301
+ }
302
+ }
303
+
304
+ const widgets = {
305
+ // A chip: white, on the page - or, `variant: "filled"`, slate, for the
306
+ // one thing to do.
307
+ button({ onClick, variant, style, children, ...rest }) {
308
+ ensureStyleSheet();
309
+ return htmlButton({
310
+ ...rest,
311
+ class: variant === "filled" ? "cb-button cb-filled" : "cb-button",
312
+ ...onClickOf(onClick),
313
+ style: style || {},
314
+ children,
315
+ });
316
+ },
317
+
318
+ // Google's Material Symbols font: the icon's name, drawn as a ligature.
319
+ // A function, not a component - so the key of its text is derived from
320
+ // its own: a fixed one would collide between icons built by one build.
321
+ icon({ key, name, style, ...rest }) {
322
+ ensureStyleSheet();
323
+ return span(
324
+ { ...rest, key, class: "material-symbols-outlined cb-icon", style: style || {} },
325
+ text({ key: key ? key + "Name" : undefined, text: name || "" }),
326
+ );
327
+ },
328
+
329
+ iconButton: (properties) => new BasicIconButton(properties),
330
+
331
+ card({ variant, style, children, ...rest }) {
332
+ return wrapper({
333
+ ...rest,
334
+ style: { padding: "16px", borderRadius: "8px", boxSizing: "border-box", ...(cardStyles[variant] || cardStyles.elevated), ...style },
335
+ children,
336
+ });
337
+ },
338
+
339
+ // Controls in a row, on a surface of their own - wrapping when there
340
+ // isn't room for them all.
341
+ controlPanel({ style, children, ...rest }) {
342
+ return wrapper({
343
+ ...rest,
344
+ style: {
345
+ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "8px 12px", flex: "none",
346
+ padding: "10px 16px", borderRadius: "8px", boxSizing: "border-box", overflow: "visible",
347
+ background: c.surface, boxShadow: basicShadow, ...style,
348
+ },
349
+ children,
350
+ });
351
+ },
352
+
353
+ alert: (properties) => new BasicAlert(properties),
354
+
355
+ listItem({ active, onClick, style, children, ...rest }) {
356
+ ensureStyleSheet();
357
+ return wrapper({
358
+ ...rest,
359
+ class: active ? "cb-list-item cb-active" : "cb-list-item",
360
+ ...onClickOf(onClick),
361
+ style: style || {},
362
+ children,
363
+ });
364
+ },
365
+
366
+ dialog: (properties) => new BasicDialog(properties),
367
+
368
+ textField: (properties) => new BasicTextField(properties),
369
+
370
+ checkbox: (properties) => new BasicCheckbox(properties),
371
+
372
+ colorField: (properties) => new BasicColorField(properties),
373
+
374
+ tabBar: (properties) => new BasicTabBar(properties),
375
+ };
376
+
377
+ export class BasicThemeServiceLocator {
378
+ constructor() {
379
+ this.colorScheme = new ColorScheme(defaultColors);
380
+ }
381
+
382
+ locate(query) {
383
+ if (query.type === "colorScheme") return this.colorScheme;
384
+ if (query.type !== "widget") return undefined;
385
+ const build = widgets[query.name];
386
+ return build ? build(query.properties) : undefined;
387
+ }
388
+ }
389
+
390
+ export const basicTheme = new BasicThemeServiceLocator();
package/src/Layout.js ADDED
@@ -0,0 +1,120 @@
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);