tempestweb 0.5.2__tar.gz → 0.6.0__tar.gz
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.
- {tempestweb-0.5.2 → tempestweb-0.6.0}/CHANGELOG.md +44 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/PKG-INFO +7 -1
- {tempestweb-0.5.2 → tempestweb-0.6.0}/README.md +6 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/constants.js +3 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/dom.js +74 -6
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/style.js +25 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/tempestweb.js +6 -0
- tempestweb-0.6.0/client/theme.js +190 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/pyproject.toml +1 -1
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/build.py +1 -0
- tempestweb-0.6.0/tempestweb/components/__init__.py +60 -0
- tempestweb-0.6.0/tempestweb/components/buttons.py +183 -0
- tempestweb-0.6.0/tempestweb/components/fields.py +262 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/dom.test.js +25 -6
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/style.test.js +12 -0
- tempestweb-0.6.0/tests/client/theme.test.js +81 -0
- tempestweb-0.5.2/tempestweb/components/__init__.py +0 -44
- tempestweb-0.5.2/tempestweb/components/fields.py +0 -124
- {tempestweb-0.5.2 → tempestweb-0.6.0}/.gitignore +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/events.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/livereload.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/audio.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/camera.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/clipboard.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/file.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/geolocation.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/http.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/index.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/install.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/notifications.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/onnx.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/share.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/storage.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/offline/store.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/offline/sync.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/push/web-push-client.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/pwa/install-prompt.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/pwa/manifest.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/router.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/sw/register.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/sw/sw.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport-sse.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport-wasm.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport-ws.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/client/virtualize.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/_core/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/dev.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/new.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/run.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/sync.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/config.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/loader.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/main.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/scaffold.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/components/forms.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/core/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/core/constants.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/http.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/reload.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/watcher.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/audio.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/bridges.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/camera.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/clipboard.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/dispatch.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/file.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/geolocation.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/http.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/install.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/notifications.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/onnx.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/share.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/storage.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/auth.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/error_boundary.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/feature_flags.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/logger.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/telemetry.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/icons.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/manifest.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/pyodide_vendor.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/events.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/serialize.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/session.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/wasm.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/wasm_main.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/server/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/server/app.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/server/webpush.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/__init__.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/base.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/sse.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/wasm.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/websocket.py +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/events.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/mount.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/native-onnx-file.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/native.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/offline-store.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/offline-sync.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/pwa-install-prompt.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/pwa-manifest.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/router.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/setup.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/smoke.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/sw-register.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/sw-strategies.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/transport-sse.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/transport-wasm.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/transport-ws.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/virtualize.test.js +0 -0
- {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/web-push-client.test.js +0 -0
|
@@ -4,6 +4,50 @@ All notable changes to **tempestweb** are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project adheres to semantic
|
|
5
5
|
versioning.
|
|
6
6
|
|
|
7
|
+
## [0.6.0] — 2026-06-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Always-on Material 3 base stylesheet.** The web client now ships a small
|
|
12
|
+
always-on MD3 base theme (`client/theme.js`), keyed off `data-tw-type`, so
|
|
13
|
+
apps get sensible typography, spacing and accented controls out of the box —
|
|
14
|
+
no per-widget styling required. Inline widget `Style` still overrides it.
|
|
15
|
+
- **`Style.shadow` renders as `box-shadow` on the web.** Elevation set on a
|
|
16
|
+
widget's `Style` now emits a real CSS `box-shadow`, matching the native
|
|
17
|
+
renderers.
|
|
18
|
+
- **MD3 field and button variants.** The pre-built components (`fields`,
|
|
19
|
+
`buttons`) gained light Material 3 variants (filled/outlined/text buttons,
|
|
20
|
+
themed text fields) so forms look finished without hand-styling.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Checkbox MD3 theming targets the nested input.** Following the Checkbox
|
|
25
|
+
`<label><input>` structure (0.5.3), the base theme sizes/accents the nested
|
|
26
|
+
`[data-tw-type="Checkbox"] > input` rather than the keyed `<label>` wrapper,
|
|
27
|
+
so the box is styled without shrinking the whole caption row.
|
|
28
|
+
|
|
29
|
+
## [0.5.3] — 2026-06-13
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`Checkbox` now renders its label as visible text on the web.** The DOM
|
|
34
|
+
renderer mapped `Checkbox` to a bare `<input type=checkbox>` and put its
|
|
35
|
+
`label` on `aria-label` only, so labelled checkboxes (todo items, settings
|
|
36
|
+
toggles) showed as empty boxes. A `Checkbox` now renders as a `<label>`
|
|
37
|
+
wrapping the real `<input>` plus a caption text node: the box and its caption
|
|
38
|
+
lay out as one tidy row (the wrapper also gives the input its accessible name
|
|
39
|
+
natively). The `<label>` is the keyed, path-addressed element; the nested
|
|
40
|
+
input carries `checked` and fires `change`, which bubbles to the label for
|
|
41
|
+
event delegation. An explicit widget `Style` still wins.
|
|
42
|
+
|
|
43
|
+
### Examples
|
|
44
|
+
|
|
45
|
+
- **Fixed three examples that passed `children=` to `Container`.** `Container`
|
|
46
|
+
holds a single `child`, not a `children` list (that is `Column`/`Row`/`Stack`).
|
|
47
|
+
Pydantic silently dropped the unknown kwarg, so the container rendered empty:
|
|
48
|
+
`list_demo` lost its row text (1000-item list showed blank rows), `gesture_demo`
|
|
49
|
+
lost its "swipe or tap me" hint, and `anim_demo` carried a no-op `children=[]`.
|
|
50
|
+
|
|
7
51
|
## [0.5.2] — 2026-06-13
|
|
8
52
|
|
|
9
53
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tempestweb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Build web apps in typed Python — one tree, a DOM renderer, two execution modes (WASM + server).
|
|
5
5
|
Author-email: Mauricio Benjamin <mauricio.benjamin@reloverelations.com>
|
|
6
6
|
License: MIT
|
|
@@ -60,6 +60,12 @@ demos (stopwatch, forms, data table, kanban, chat, theming, i18n, canvas, native
|
|
|
60
60
|
capabilities, observability, PWA/WebPush, and a server-mode walkthrough), each
|
|
61
61
|
running unchanged in both modes.
|
|
62
62
|
|
|
63
|
+
Building something real? Read the **[App architecture & best practices](https://mauriciobenjamin700.github.io/tempestweb/best-practices/)**
|
|
64
|
+
guide ([EN](https://mauriciobenjamin700.github.io/tempestweb/en/best-practices/)) —
|
|
65
|
+
the ideal layered structure (routes · pages · components · styles · controllers ·
|
|
66
|
+
services · storages · schemas · utils · core), mirroring `tempest-fastapi-sdk`, so
|
|
67
|
+
your app doesn't rot into garbage code.
|
|
68
|
+
|
|
63
69
|
## How it works
|
|
64
70
|
|
|
65
71
|
```text
|
|
@@ -32,6 +32,12 @@ demos (stopwatch, forms, data table, kanban, chat, theming, i18n, canvas, native
|
|
|
32
32
|
capabilities, observability, PWA/WebPush, and a server-mode walkthrough), each
|
|
33
33
|
running unchanged in both modes.
|
|
34
34
|
|
|
35
|
+
Building something real? Read the **[App architecture & best practices](https://mauriciobenjamin700.github.io/tempestweb/best-practices/)**
|
|
36
|
+
guide ([EN](https://mauriciobenjamin700.github.io/tempestweb/en/best-practices/)) —
|
|
37
|
+
the ideal layered structure (routes · pages · components · styles · controllers ·
|
|
38
|
+
services · storages · schemas · utils · core), mirroring `tempest-fastapi-sdk`, so
|
|
39
|
+
your app doesn't rot into garbage code.
|
|
40
|
+
|
|
35
41
|
## How it works
|
|
36
42
|
|
|
37
43
|
```text
|
|
@@ -15,3 +15,6 @@ export const GESTURE_TYPE = "GestureDetector";
|
|
|
15
15
|
|
|
16
16
|
/** Id of the injected stylesheet that carries virtualized-list spacer heights. */
|
|
17
17
|
export const VIRT_STYLE_ID = "tw-virt-styles";
|
|
18
|
+
|
|
19
|
+
/** Id of the injected stylesheet that carries the always-on MD3 base theme. */
|
|
20
|
+
export const BASE_THEME_STYLE_ID = "tw-base-theme";
|
|
@@ -32,7 +32,12 @@ const TAG_BY_TYPE = Object.freeze({
|
|
|
32
32
|
Text: "span",
|
|
33
33
|
Button: "button",
|
|
34
34
|
Input: "input",
|
|
35
|
-
Checkbox
|
|
35
|
+
// A Checkbox renders as a <label> wrapping a real <input type=checkbox> plus
|
|
36
|
+
// its caption text, so the box and its label show side by side and the input
|
|
37
|
+
// gets its accessible name natively. The <label> is the keyed, path-addressed
|
|
38
|
+
// element; the input it wraps is renderer-internal (Checkbox is an IR leaf, so
|
|
39
|
+
// no patch path ever descends into it).
|
|
40
|
+
Checkbox: "label",
|
|
36
41
|
Image: "img",
|
|
37
42
|
});
|
|
38
43
|
|
|
@@ -89,14 +94,15 @@ function applyProps(el, props) {
|
|
|
89
94
|
el.removeAttribute("style");
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
|
-
// Text-bearing props. A Checkbox is
|
|
93
|
-
//
|
|
97
|
+
// Text-bearing props. A Checkbox is a <label> wrapping an <input>, so its
|
|
98
|
+
// caption is set as a trailing text node (see setCheckboxLabel) rather than
|
|
99
|
+
// overwriting textContent, which would drop the nested input.
|
|
94
100
|
if ("content" in props) {
|
|
95
101
|
el.textContent = props.content == null ? "" : String(props.content);
|
|
96
102
|
}
|
|
97
103
|
if ("label" in props) {
|
|
98
104
|
if (type === "Checkbox") {
|
|
99
|
-
el
|
|
105
|
+
setCheckboxLabel(el, props.label == null ? "" : String(props.label));
|
|
100
106
|
} else {
|
|
101
107
|
el.textContent = props.label == null ? "" : String(props.label);
|
|
102
108
|
}
|
|
@@ -188,6 +194,46 @@ function applyLazyProps(el, type, props) {
|
|
|
188
194
|
el.setAttribute("data-tw-window-start", String(start ?? 0));
|
|
189
195
|
}
|
|
190
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Get (or lazily create) the real ``<input type=checkbox>`` nested inside a
|
|
199
|
+
* Checkbox's ``<label>`` wrapper. The label is the keyed, path-addressed
|
|
200
|
+
* element; this nested input carries the actual ``checked`` state and fires the
|
|
201
|
+
* native ``change`` event (which bubbles up to the keyed label for delegation).
|
|
202
|
+
*
|
|
203
|
+
* @param {HTMLElement} el The Checkbox ``<label>`` element.
|
|
204
|
+
* @returns {HTMLInputElement} The nested checkbox input.
|
|
205
|
+
*/
|
|
206
|
+
function ensureCheckboxInput(el) {
|
|
207
|
+
let input = /** @type {HTMLInputElement|null} */ (el.querySelector("input"));
|
|
208
|
+
if (input == null) {
|
|
209
|
+
input = /** @type {HTMLInputElement} */ (document.createElement("input"));
|
|
210
|
+
input.setAttribute("type", "checkbox");
|
|
211
|
+
el.insertBefore(input, el.firstChild);
|
|
212
|
+
}
|
|
213
|
+
return input;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Set a Checkbox's visible caption, kept as a single text node after the nested
|
|
218
|
+
* input so the box and its label render side by side. Wrapping the input in the
|
|
219
|
+
* ``<label>`` also gives it its accessible name natively (no ``aria-label``).
|
|
220
|
+
*
|
|
221
|
+
* @param {HTMLElement} el The Checkbox ``<label>`` element.
|
|
222
|
+
* @param {string} text The caption text (``""`` clears it).
|
|
223
|
+
* @returns {void}
|
|
224
|
+
*/
|
|
225
|
+
function setCheckboxLabel(el, text) {
|
|
226
|
+
const input = ensureCheckboxInput(el);
|
|
227
|
+
for (const node of Array.from(el.childNodes)) {
|
|
228
|
+
if (node !== input) {
|
|
229
|
+
el.removeChild(node);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (text) {
|
|
233
|
+
el.appendChild(document.createTextNode(text));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
191
237
|
function applyControlProps(el, type, props) {
|
|
192
238
|
if (type === "Input") {
|
|
193
239
|
el.setAttribute("type", props.secure ? "password" : "text");
|
|
@@ -201,9 +247,29 @@ function applyControlProps(el, type, props) {
|
|
|
201
247
|
el.setAttribute("maxlength", String(props.max_length));
|
|
202
248
|
}
|
|
203
249
|
} else if (type === "Checkbox") {
|
|
204
|
-
el
|
|
250
|
+
const input = ensureCheckboxInput(el);
|
|
205
251
|
if ("checked" in props) {
|
|
206
|
-
|
|
252
|
+
input.checked = Boolean(props.checked);
|
|
253
|
+
}
|
|
254
|
+
// Lay the box and caption out on one line, as a block-level row so stacked
|
|
255
|
+
// checkboxes each get their own line (an inline default would let adjacent
|
|
256
|
+
// labels flow together in a non-flex parent). Defaults only — an explicit
|
|
257
|
+
// Style on the widget (applied just before this) wins. Re-applied here on
|
|
258
|
+
// every update because a `style` patch resets the element's inline cssText.
|
|
259
|
+
if (!el.style.display) {
|
|
260
|
+
el.style.display = "flex";
|
|
261
|
+
}
|
|
262
|
+
if (!el.style.alignItems) {
|
|
263
|
+
el.style.alignItems = "center";
|
|
264
|
+
}
|
|
265
|
+
if (!el.style.gap) {
|
|
266
|
+
el.style.gap = "0.4em";
|
|
267
|
+
}
|
|
268
|
+
// Size to the box + caption. Without this the <label>, as a flex item of a
|
|
269
|
+
// column, collapses to the input's width and the caption overflows onto the
|
|
270
|
+
// next row; fit-content keeps it a tidy one-line row instead of stretching.
|
|
271
|
+
if (!el.style.width) {
|
|
272
|
+
el.style.width = "fit-content";
|
|
207
273
|
}
|
|
208
274
|
} else if (type === "Image") {
|
|
209
275
|
if ("src" in props && props.src != null) {
|
|
@@ -263,6 +329,8 @@ function applyUpdate(root, patch) {
|
|
|
263
329
|
for (const key of patch.unset_props ?? []) {
|
|
264
330
|
if (key === "style") {
|
|
265
331
|
el.removeAttribute("style");
|
|
332
|
+
} else if (key === "label" && el.getAttribute(TYPE_ATTR) === "Checkbox") {
|
|
333
|
+
setCheckboxLabel(el, "");
|
|
266
334
|
} else if (key === "content" || key === "label") {
|
|
267
335
|
el.textContent = "";
|
|
268
336
|
}
|
|
@@ -48,6 +48,14 @@
|
|
|
48
48
|
* @property {number} bottom_left Bottom-left radius, px.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} ShadowDump
|
|
53
|
+
* @property {?ColorDump} color The shadow color, or null for a default tint.
|
|
54
|
+
* @property {number} blur The blur radius, px.
|
|
55
|
+
* @property {number} offset_x The horizontal offset, px.
|
|
56
|
+
* @property {number} offset_y The vertical offset, px.
|
|
57
|
+
*/
|
|
58
|
+
|
|
51
59
|
/**
|
|
52
60
|
* @typedef {Object} GradientStopDump
|
|
53
61
|
* @property {ColorDump} color The stop color.
|
|
@@ -230,6 +238,20 @@ const CURVE_CSS = Object.freeze({
|
|
|
230
238
|
elastic: "cubic-bezier(0.68, -0.6, 0.32, 1.6)",
|
|
231
239
|
});
|
|
232
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Render a serialized Shadow as a CSS `box-shadow` value.
|
|
243
|
+
*
|
|
244
|
+
* `offset_x offset_y blur color`. A null shadow color falls back to a neutral
|
|
245
|
+
* translucent black so an elevation set without an explicit tint still reads.
|
|
246
|
+
*
|
|
247
|
+
* @param {ShadowDump} shadow The shadow dump.
|
|
248
|
+
* @returns {string} The CSS `box-shadow` value.
|
|
249
|
+
*/
|
|
250
|
+
function shadowToCss(shadow) {
|
|
251
|
+
const color = shadow.color ? colorToRgba(shadow.color) : "rgba(0, 0, 0, 0.3)";
|
|
252
|
+
return `${shadow.offset_x}px ${shadow.offset_y}px ${shadow.blur}px ${color}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
/**
|
|
234
256
|
* Translate a serialized Transition into a CSS `transition` shorthand.
|
|
235
257
|
* @param {{duration_ms:number, curve:string, delay_ms:number}} transition
|
|
@@ -317,6 +339,9 @@ export function styleToCss(style, type) {
|
|
|
317
339
|
if (style.opacity != null) {
|
|
318
340
|
rules.push(`opacity: ${style.opacity}`);
|
|
319
341
|
}
|
|
342
|
+
if (style.shadow != null) {
|
|
343
|
+
rules.push(`box-shadow: ${shadowToCss(style.shadow)}`);
|
|
344
|
+
}
|
|
320
345
|
|
|
321
346
|
// Typography.
|
|
322
347
|
if (style.font_family != null) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { applyPatches, buildElement } from "./dom.js";
|
|
10
10
|
import { bindEvents } from "./events.js";
|
|
11
11
|
import { installRouter } from "./router.js";
|
|
12
|
+
import { installBaseTheme } from "./theme.js";
|
|
12
13
|
import { installVirtualization } from "./virtualize.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -48,6 +49,11 @@ import { installVirtualization } from "./virtualize.js";
|
|
|
48
49
|
* @returns {MountHandle} A handle to unmount the app.
|
|
49
50
|
*/
|
|
50
51
|
export function mount(root, transport, initialNode = null) {
|
|
52
|
+
// Modern look for every app: inject the MD3 base stylesheet once. It styles
|
|
53
|
+
// bare widgets (Button/Input/Checkbox) and their interaction states; an app's
|
|
54
|
+
// inline Style still overrides it (see theme.js).
|
|
55
|
+
installBaseTheme();
|
|
56
|
+
|
|
51
57
|
/** @type {HTMLElement | null} */
|
|
52
58
|
let tree = null;
|
|
53
59
|
if (initialNode != null) {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// theme.js — the always-on Material 3 base stylesheet. UI MODERNIZATION.
|
|
2
|
+
//
|
|
3
|
+
// Core widgets render with no intrinsic visual style (a Button is a bare
|
|
4
|
+
// <button>), so unstyled apps fall back to the browser's ugly chrome defaults.
|
|
5
|
+
// This module injects ONE base stylesheet — once, at mount — that gives every
|
|
6
|
+
// widget a modern Material 3 look: filled buttons with state layers + elevation,
|
|
7
|
+
// outlined text fields, themed checkboxes, smooth focus rings.
|
|
8
|
+
//
|
|
9
|
+
// Why a stylesheet and not inline Style: inline CSS cannot express :hover,
|
|
10
|
+
// :focus-visible, :active or :disabled — the very states that make a control feel
|
|
11
|
+
// modern. Those live here, keyed off the `data-tw-type` attribute dom.js stamps on
|
|
12
|
+
// every element.
|
|
13
|
+
//
|
|
14
|
+
// Override order: an app's inline Style (emitted by style.js onto the element's
|
|
15
|
+
// `style` attribute) ALWAYS wins over this sheet — inline declarations beat a
|
|
16
|
+
// stylesheet rule of equal-or-lower specificity, and nothing here uses
|
|
17
|
+
// `!important`. So the base is a floor, not a cage: set `background`/`radius`/etc.
|
|
18
|
+
// on a widget's Style and your value takes over while the interaction states stay.
|
|
19
|
+
//
|
|
20
|
+
// Tokens are CSS custom properties on :root, so an app can rebrand the whole UI by
|
|
21
|
+
// overriding e.g. `--tw-primary` from its own <style> without touching this file.
|
|
22
|
+
|
|
23
|
+
import { BASE_THEME_STYLE_ID as STYLE_ID } from "./constants.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The Material 3 base theme CSS, exported so tests can assert its content
|
|
27
|
+
* without a live DOM. Tokens (`--tw-*`) are overridable by the app.
|
|
28
|
+
* @type {string}
|
|
29
|
+
*/
|
|
30
|
+
export const BASE_THEME_CSS = `
|
|
31
|
+
:root {
|
|
32
|
+
/* Material 3 baseline palette (light scheme). Override any of these from the
|
|
33
|
+
app to rebrand: e.g. \`:root { --tw-primary: #0b57d0; }\`. */
|
|
34
|
+
--tw-primary: #6750a4;
|
|
35
|
+
--tw-on-primary: #ffffff;
|
|
36
|
+
--tw-primary-container: #eaddff;
|
|
37
|
+
--tw-on-primary-container: #21005d;
|
|
38
|
+
--tw-secondary-container: #e8def8;
|
|
39
|
+
--tw-on-secondary-container: #1d192b;
|
|
40
|
+
--tw-surface: #fef7ff;
|
|
41
|
+
--tw-on-surface: #1d1b20;
|
|
42
|
+
--tw-on-surface-variant: #49454f;
|
|
43
|
+
--tw-outline: #79747e;
|
|
44
|
+
--tw-error: #b3261e;
|
|
45
|
+
|
|
46
|
+
/* MD3 elevation levels (umbra + penumbra). */
|
|
47
|
+
--tw-elevation-1: 0 1px 2px rgba(0,0,0,0.30), 0 1px 3px 1px rgba(0,0,0,0.15);
|
|
48
|
+
--tw-elevation-2: 0 1px 2px rgba(0,0,0,0.30), 0 2px 6px 2px rgba(0,0,0,0.15);
|
|
49
|
+
|
|
50
|
+
--tw-radius-full: 9999px;
|
|
51
|
+
--tw-font: "Roboto", "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
52
|
+
--tw-motion: 180ms cubic-bezier(0.2, 0, 0, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Sensible page baseline so apps don't sit on Times New Roman. */
|
|
56
|
+
[data-tw-type] { box-sizing: border-box; }
|
|
57
|
+
|
|
58
|
+
/* ── Button: MD3 filled button ─────────────────────────────────────────────
|
|
59
|
+
A pill-shaped filled button with an interaction state layer (::before) and
|
|
60
|
+
elevation on hover. The state layer is the MD3 way to show hover/focus/press:
|
|
61
|
+
a translucent overlay of the on-color tinting the surface. */
|
|
62
|
+
[data-tw-type="Button"] {
|
|
63
|
+
position: relative;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
gap: 8px;
|
|
69
|
+
min-height: 40px;
|
|
70
|
+
padding: 10px 24px;
|
|
71
|
+
border: none;
|
|
72
|
+
border-radius: var(--tw-radius-full);
|
|
73
|
+
background: var(--tw-primary);
|
|
74
|
+
color: var(--tw-on-primary);
|
|
75
|
+
font-family: var(--tw-font);
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
letter-spacing: 0.1px;
|
|
79
|
+
line-height: 20px;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
user-select: none;
|
|
82
|
+
-webkit-tap-highlight-color: transparent;
|
|
83
|
+
transition: box-shadow var(--tw-motion), background var(--tw-motion);
|
|
84
|
+
}
|
|
85
|
+
/* The state layer: an overlay tinted with the foreground color, invisible at
|
|
86
|
+
rest and fading in for hover (8%) / focus & press (12%) per MD3 specs. */
|
|
87
|
+
[data-tw-type="Button"]::before {
|
|
88
|
+
content: "";
|
|
89
|
+
position: absolute;
|
|
90
|
+
inset: 0;
|
|
91
|
+
background: currentColor;
|
|
92
|
+
opacity: 0;
|
|
93
|
+
transition: opacity var(--tw-motion);
|
|
94
|
+
pointer-events: none;
|
|
95
|
+
}
|
|
96
|
+
/* State-layer overlay — universal, correct for every button variant. */
|
|
97
|
+
[data-tw-type="Button"]:hover::before { opacity: 0.08; }
|
|
98
|
+
[data-tw-type="Button"]:focus-visible { outline: none; }
|
|
99
|
+
[data-tw-type="Button"]:focus-visible::before { opacity: 0.12; }
|
|
100
|
+
[data-tw-type="Button"]:active::before { opacity: 0.12; }
|
|
101
|
+
/* Elevation — only the default filled button (no app-set inline background).
|
|
102
|
+
A variant that sets its own \`background\` (tonal/outlined/text/elevated) opts
|
|
103
|
+
out of this and controls its own shadow via inline Style. */
|
|
104
|
+
[data-tw-type="Button"]:not([style*="background"]):hover { box-shadow: var(--tw-elevation-1); }
|
|
105
|
+
[data-tw-type="Button"]:not([style*="background"]):active { box-shadow: var(--tw-elevation-2); }
|
|
106
|
+
[data-tw-type="Button"]:not([style*="background"]):focus-visible { box-shadow: var(--tw-elevation-1); }
|
|
107
|
+
[data-tw-type="Button"]:disabled,
|
|
108
|
+
[data-tw-type="Button"][aria-disabled="true"] {
|
|
109
|
+
background: rgba(29,27,32,0.12);
|
|
110
|
+
color: rgba(29,27,32,0.38);
|
|
111
|
+
box-shadow: none;
|
|
112
|
+
cursor: default;
|
|
113
|
+
}
|
|
114
|
+
[data-tw-type="Button"]:disabled::before { opacity: 0; }
|
|
115
|
+
|
|
116
|
+
/* ── Input: MD3 outlined text field ────────────────────────────────────────
|
|
117
|
+
A rounded, outlined field whose border thickens and recolors to the primary
|
|
118
|
+
tone on focus. */
|
|
119
|
+
[data-tw-type="Input"] {
|
|
120
|
+
min-height: 40px;
|
|
121
|
+
padding: 9px 16px;
|
|
122
|
+
border: 1px solid var(--tw-outline);
|
|
123
|
+
border-radius: 8px;
|
|
124
|
+
background: var(--tw-surface);
|
|
125
|
+
color: var(--tw-on-surface);
|
|
126
|
+
font-family: var(--tw-font);
|
|
127
|
+
font-size: 16px;
|
|
128
|
+
line-height: 22px;
|
|
129
|
+
transition: border-color var(--tw-motion), box-shadow var(--tw-motion);
|
|
130
|
+
}
|
|
131
|
+
[data-tw-type="Input"]::placeholder { color: var(--tw-on-surface-variant); }
|
|
132
|
+
[data-tw-type="Input"]:hover { border-color: var(--tw-on-surface); }
|
|
133
|
+
[data-tw-type="Input"]:focus,
|
|
134
|
+
[data-tw-type="Input"]:focus-visible {
|
|
135
|
+
outline: none;
|
|
136
|
+
border-color: var(--tw-primary);
|
|
137
|
+
box-shadow: inset 0 0 0 1px var(--tw-primary);
|
|
138
|
+
}
|
|
139
|
+
[data-tw-type="Input"]:disabled {
|
|
140
|
+
border-color: rgba(29,27,32,0.12);
|
|
141
|
+
color: rgba(29,27,32,0.38);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ── Checkbox: a <label> wrapping the real <input type=checkbox> ─────────────
|
|
145
|
+
dom.js renders a Checkbox as a keyed <label> with the caption text and lays
|
|
146
|
+
the row out inline (display/gap/align/width), so the base only sets the
|
|
147
|
+
caption type ramp + cursor on the label and themes the nested box itself. */
|
|
148
|
+
[data-tw-type="Checkbox"] {
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
font-family: var(--tw-font);
|
|
151
|
+
font-size: 14px;
|
|
152
|
+
color: var(--tw-on-surface);
|
|
153
|
+
}
|
|
154
|
+
[data-tw-type="Checkbox"] > input {
|
|
155
|
+
width: 18px;
|
|
156
|
+
height: 18px;
|
|
157
|
+
margin: 0;
|
|
158
|
+
accent-color: var(--tw-primary);
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ── Text: inherit the modern font instead of the UA serif default ─────────── */
|
|
163
|
+
[data-tw-type="Text"] { font-family: var(--tw-font); }
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Inject the Material 3 base stylesheet into the document head, once.
|
|
168
|
+
*
|
|
169
|
+
* Idempotent: if a sheet with {@link BASE_THEME_STYLE_ID} already exists (a
|
|
170
|
+
* previous mount, or the page provided its own) it is left untouched. A no-op
|
|
171
|
+
* when there is no `document` (e.g. a non-DOM test harness).
|
|
172
|
+
*
|
|
173
|
+
* @returns {?HTMLStyleElement} The injected (or pre-existing) style element, or
|
|
174
|
+
* `null` when no document is available.
|
|
175
|
+
*/
|
|
176
|
+
export function installBaseTheme() {
|
|
177
|
+
if (typeof document === "undefined") {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const existing = document.getElementById(STYLE_ID);
|
|
181
|
+
if (existing != null) {
|
|
182
|
+
return /** @type {HTMLStyleElement} */ (existing);
|
|
183
|
+
}
|
|
184
|
+
const el = document.createElement("style");
|
|
185
|
+
el.id = STYLE_ID;
|
|
186
|
+
el.textContent = BASE_THEME_CSS;
|
|
187
|
+
// Prepend so app- and inline-styles declared later still win the cascade.
|
|
188
|
+
document.head.insertBefore(el, document.head.firstChild);
|
|
189
|
+
return el;
|
|
190
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""tempestweb.components — ready-to-use fields, forms and Material 3 buttons.
|
|
2
|
+
|
|
3
|
+
Pre-built, validated building blocks so an app declares a field, a button or a
|
|
4
|
+
whole form in one line instead of wiring inputs, labels, errors and validators
|
|
5
|
+
by hand:
|
|
6
|
+
|
|
7
|
+
from tempestweb.components import EmailField, PasswordField, LoginForm
|
|
8
|
+
from tempestweb.components import filled_button, text_button
|
|
9
|
+
|
|
10
|
+
``EmailField``/``PasswordField`` and ``TextField`` are tempestweb-native, styled
|
|
11
|
+
for the Material 3 light surface the base stylesheet renders; the BR fields
|
|
12
|
+
(:class:`PhoneField`/:class:`CPFField`/:class:`CNPJField`/:class:`AddressField`)
|
|
13
|
+
wrap the core's masked inputs. The ``*_button`` helpers build the MD3 button
|
|
14
|
+
variants. Everything renders identically in Mode A (WASM) and Mode B (server).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from tempestweb.components.buttons import (
|
|
20
|
+
elevated_button,
|
|
21
|
+
filled_button,
|
|
22
|
+
outlined_button,
|
|
23
|
+
text_button,
|
|
24
|
+
tonal_button,
|
|
25
|
+
)
|
|
26
|
+
from tempestweb.components.fields import (
|
|
27
|
+
AddressField,
|
|
28
|
+
CNPJField,
|
|
29
|
+
CPFField,
|
|
30
|
+
EmailField,
|
|
31
|
+
PasswordField,
|
|
32
|
+
PhoneField,
|
|
33
|
+
TextField,
|
|
34
|
+
validate_cnpj,
|
|
35
|
+
validate_cpf,
|
|
36
|
+
validate_email,
|
|
37
|
+
validate_phone,
|
|
38
|
+
)
|
|
39
|
+
from tempestweb.components.forms import LoginForm, SignupForm
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"AddressField",
|
|
43
|
+
"CNPJField",
|
|
44
|
+
"CPFField",
|
|
45
|
+
"EmailField",
|
|
46
|
+
"LoginForm",
|
|
47
|
+
"PasswordField",
|
|
48
|
+
"SignupForm",
|
|
49
|
+
"TextField",
|
|
50
|
+
"PhoneField",
|
|
51
|
+
"elevated_button",
|
|
52
|
+
"filled_button",
|
|
53
|
+
"outlined_button",
|
|
54
|
+
"text_button",
|
|
55
|
+
"tonal_button",
|
|
56
|
+
"validate_cnpj",
|
|
57
|
+
"validate_cpf",
|
|
58
|
+
"validate_email",
|
|
59
|
+
"validate_phone",
|
|
60
|
+
]
|