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.
Files changed (121) hide show
  1. {tempestweb-0.5.2 → tempestweb-0.6.0}/CHANGELOG.md +44 -0
  2. {tempestweb-0.5.2 → tempestweb-0.6.0}/PKG-INFO +7 -1
  3. {tempestweb-0.5.2 → tempestweb-0.6.0}/README.md +6 -0
  4. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/constants.js +3 -0
  5. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/dom.js +74 -6
  6. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/style.js +25 -0
  7. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/tempestweb.js +6 -0
  8. tempestweb-0.6.0/client/theme.js +190 -0
  9. {tempestweb-0.5.2 → tempestweb-0.6.0}/pyproject.toml +1 -1
  10. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/build.py +1 -0
  11. tempestweb-0.6.0/tempestweb/components/__init__.py +60 -0
  12. tempestweb-0.6.0/tempestweb/components/buttons.py +183 -0
  13. tempestweb-0.6.0/tempestweb/components/fields.py +262 -0
  14. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/dom.test.js +25 -6
  15. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/style.test.js +12 -0
  16. tempestweb-0.6.0/tests/client/theme.test.js +81 -0
  17. tempestweb-0.5.2/tempestweb/components/__init__.py +0 -44
  18. tempestweb-0.5.2/tempestweb/components/fields.py +0 -124
  19. {tempestweb-0.5.2 → tempestweb-0.6.0}/.gitignore +0 -0
  20. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/events.js +0 -0
  21. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/livereload.js +0 -0
  22. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/audio.js +0 -0
  23. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/camera.js +0 -0
  24. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/clipboard.js +0 -0
  25. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/file.js +0 -0
  26. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/geolocation.js +0 -0
  27. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/http.js +0 -0
  28. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/index.js +0 -0
  29. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/install.js +0 -0
  30. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/notifications.js +0 -0
  31. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/onnx.js +0 -0
  32. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/share.js +0 -0
  33. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/native/storage.js +0 -0
  34. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/offline/store.js +0 -0
  35. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/offline/sync.js +0 -0
  36. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/push/web-push-client.js +0 -0
  37. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/pwa/install-prompt.js +0 -0
  38. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/pwa/manifest.js +0 -0
  39. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/router.js +0 -0
  40. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/sw/register.js +0 -0
  41. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/sw/sw.js +0 -0
  42. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport-sse.js +0 -0
  43. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport-wasm.js +0 -0
  44. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport-ws.js +0 -0
  45. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/transport.js +0 -0
  46. {tempestweb-0.5.2 → tempestweb-0.6.0}/client/virtualize.js +0 -0
  47. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/__init__.py +0 -0
  48. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/_core/__init__.py +0 -0
  49. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/__init__.py +0 -0
  50. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/__init__.py +0 -0
  51. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/dev.py +0 -0
  52. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/new.py +0 -0
  53. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/run.py +0 -0
  54. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/commands/sync.py +0 -0
  55. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/config.py +0 -0
  56. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/loader.py +0 -0
  57. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/main.py +0 -0
  58. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/cli/scaffold.py +0 -0
  59. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/components/forms.py +0 -0
  60. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/core/__init__.py +0 -0
  61. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/core/constants.py +0 -0
  62. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/__init__.py +0 -0
  63. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/http.py +0 -0
  64. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/reload.py +0 -0
  65. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/devserver/watcher.py +0 -0
  66. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/__init__.py +0 -0
  67. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/audio.py +0 -0
  68. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/bridges.py +0 -0
  69. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/camera.py +0 -0
  70. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/clipboard.py +0 -0
  71. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/dispatch.py +0 -0
  72. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/file.py +0 -0
  73. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/geolocation.py +0 -0
  74. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/http.py +0 -0
  75. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/install.py +0 -0
  76. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/notifications.py +0 -0
  77. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/onnx.py +0 -0
  78. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/share.py +0 -0
  79. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/native/storage.py +0 -0
  80. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/__init__.py +0 -0
  81. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/auth.py +0 -0
  82. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/error_boundary.py +0 -0
  83. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/feature_flags.py +0 -0
  84. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/logger.py +0 -0
  85. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/observability/telemetry.py +0 -0
  86. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/__init__.py +0 -0
  87. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/icons.py +0 -0
  88. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/manifest.py +0 -0
  89. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/pwa/pyodide_vendor.py +0 -0
  90. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/__init__.py +0 -0
  91. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/events.py +0 -0
  92. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/serialize.py +0 -0
  93. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/session.py +0 -0
  94. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/wasm.py +0 -0
  95. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/runtime/wasm_main.py +0 -0
  96. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/server/__init__.py +0 -0
  97. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/server/app.py +0 -0
  98. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/server/webpush.py +0 -0
  99. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/__init__.py +0 -0
  100. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/base.py +0 -0
  101. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/sse.py +0 -0
  102. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/wasm.py +0 -0
  103. {tempestweb-0.5.2 → tempestweb-0.6.0}/tempestweb/transports/websocket.py +0 -0
  104. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/events.test.js +0 -0
  105. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/mount.test.js +0 -0
  106. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/native-onnx-file.test.js +0 -0
  107. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/native.test.js +0 -0
  108. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/offline-store.test.js +0 -0
  109. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/offline-sync.test.js +0 -0
  110. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/pwa-install-prompt.test.js +0 -0
  111. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/pwa-manifest.test.js +0 -0
  112. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/router.test.js +0 -0
  113. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/setup.js +0 -0
  114. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/smoke.test.js +0 -0
  115. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/sw-register.test.js +0 -0
  116. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/sw-strategies.test.js +0 -0
  117. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/transport-sse.test.js +0 -0
  118. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/transport-wasm.test.js +0 -0
  119. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/transport-ws.test.js +0 -0
  120. {tempestweb-0.5.2 → tempestweb-0.6.0}/tests/client/virtualize.test.js +0 -0
  121. {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.5.2
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: "input",
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 an <input> and cannot hold text, so its
93
- // label rides as an accessible name instead of textContent.
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.setAttribute("aria-label", props.label == null ? "" : String(props.label));
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.setAttribute("type", "checkbox");
250
+ const input = ensureCheckboxInput(el);
205
251
  if ("checked" in props) {
206
- el.checked = Boolean(props.checked);
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
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tempestweb"
3
- version = "0.5.2"
3
+ version = "0.6.0"
4
4
  description = "Build web apps in typed Python — one tree, a DOM renderer, two execution modes (WASM + server)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -58,6 +58,7 @@ _CLIENT_ASSETS: tuple[str, ...] = (
58
58
  "tempestweb.js",
59
59
  "dom.js",
60
60
  "style.js",
61
+ "theme.js",
61
62
  "events.js",
62
63
  "transport.js",
63
64
  "virtualize.js",
@@ -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
+ ]