tempestweb 0.1.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 (108) hide show
  1. tempestweb-0.1.0/.gitignore +29 -0
  2. tempestweb-0.1.0/CHANGELOG.md +50 -0
  3. tempestweb-0.1.0/PKG-INFO +110 -0
  4. tempestweb-0.1.0/README.md +83 -0
  5. tempestweb-0.1.0/client/constants.js +17 -0
  6. tempestweb-0.1.0/client/dom.js +373 -0
  7. tempestweb-0.1.0/client/events.js +205 -0
  8. tempestweb-0.1.0/client/livereload.js +29 -0
  9. tempestweb-0.1.0/client/native/audio.js +58 -0
  10. tempestweb-0.1.0/client/native/camera.js +73 -0
  11. tempestweb-0.1.0/client/native/clipboard.js +46 -0
  12. tempestweb-0.1.0/client/native/geolocation.js +44 -0
  13. tempestweb-0.1.0/client/native/http.js +126 -0
  14. tempestweb-0.1.0/client/native/index.js +158 -0
  15. tempestweb-0.1.0/client/native/notifications.js +109 -0
  16. tempestweb-0.1.0/client/native/share.js +48 -0
  17. tempestweb-0.1.0/client/native/storage.js +103 -0
  18. tempestweb-0.1.0/client/offline/store.js +316 -0
  19. tempestweb-0.1.0/client/offline/sync.js +225 -0
  20. tempestweb-0.1.0/client/push/web-push-client.js +190 -0
  21. tempestweb-0.1.0/client/pwa/install-prompt.js +153 -0
  22. tempestweb-0.1.0/client/pwa/manifest.js +253 -0
  23. tempestweb-0.1.0/client/router.js +67 -0
  24. tempestweb-0.1.0/client/style.js +359 -0
  25. tempestweb-0.1.0/client/sw/register.js +159 -0
  26. tempestweb-0.1.0/client/sw/sw.js +401 -0
  27. tempestweb-0.1.0/client/tempestweb.js +151 -0
  28. tempestweb-0.1.0/client/transport-sse.js +158 -0
  29. tempestweb-0.1.0/client/transport-wasm.js +94 -0
  30. tempestweb-0.1.0/client/transport-ws.js +154 -0
  31. tempestweb-0.1.0/client/transport.js +49 -0
  32. tempestweb-0.1.0/client/virtualize.js +160 -0
  33. tempestweb-0.1.0/pyproject.toml +104 -0
  34. tempestweb-0.1.0/tempestweb/__init__.py +7 -0
  35. tempestweb-0.1.0/tempestweb/_core/__init__.py +48 -0
  36. tempestweb-0.1.0/tempestweb/cli/__init__.py +83 -0
  37. tempestweb-0.1.0/tempestweb/cli/commands/__init__.py +43 -0
  38. tempestweb-0.1.0/tempestweb/cli/commands/build.py +737 -0
  39. tempestweb-0.1.0/tempestweb/cli/commands/dev.py +200 -0
  40. tempestweb-0.1.0/tempestweb/cli/commands/new.py +60 -0
  41. tempestweb-0.1.0/tempestweb/cli/commands/run.py +137 -0
  42. tempestweb-0.1.0/tempestweb/cli/config.py +95 -0
  43. tempestweb-0.1.0/tempestweb/cli/loader.py +133 -0
  44. tempestweb-0.1.0/tempestweb/cli/main.py +261 -0
  45. tempestweb-0.1.0/tempestweb/cli/scaffold.py +245 -0
  46. tempestweb-0.1.0/tempestweb/components/__init__.py +44 -0
  47. tempestweb-0.1.0/tempestweb/components/fields.py +123 -0
  48. tempestweb-0.1.0/tempestweb/components/forms.py +199 -0
  49. tempestweb-0.1.0/tempestweb/core/__init__.py +29 -0
  50. tempestweb-0.1.0/tempestweb/core/constants.py +44 -0
  51. tempestweb-0.1.0/tempestweb/devserver/__init__.py +80 -0
  52. tempestweb-0.1.0/tempestweb/devserver/http.py +184 -0
  53. tempestweb-0.1.0/tempestweb/devserver/reload.py +158 -0
  54. tempestweb-0.1.0/tempestweb/devserver/watcher.py +164 -0
  55. tempestweb-0.1.0/tempestweb/native/__init__.py +154 -0
  56. tempestweb-0.1.0/tempestweb/native/audio.py +74 -0
  57. tempestweb-0.1.0/tempestweb/native/bridges.py +171 -0
  58. tempestweb-0.1.0/tempestweb/native/camera.py +77 -0
  59. tempestweb-0.1.0/tempestweb/native/clipboard.py +54 -0
  60. tempestweb-0.1.0/tempestweb/native/dispatch.py +265 -0
  61. tempestweb-0.1.0/tempestweb/native/geolocation.py +62 -0
  62. tempestweb-0.1.0/tempestweb/native/http.py +307 -0
  63. tempestweb-0.1.0/tempestweb/native/notifications.py +120 -0
  64. tempestweb-0.1.0/tempestweb/native/share.py +91 -0
  65. tempestweb-0.1.0/tempestweb/native/storage.py +108 -0
  66. tempestweb-0.1.0/tempestweb/observability/__init__.py +111 -0
  67. tempestweb-0.1.0/tempestweb/observability/auth.py +440 -0
  68. tempestweb-0.1.0/tempestweb/observability/error_boundary.py +235 -0
  69. tempestweb-0.1.0/tempestweb/observability/feature_flags.py +360 -0
  70. tempestweb-0.1.0/tempestweb/observability/logger.py +238 -0
  71. tempestweb-0.1.0/tempestweb/observability/telemetry.py +303 -0
  72. tempestweb-0.1.0/tempestweb/pwa/__init__.py +54 -0
  73. tempestweb-0.1.0/tempestweb/pwa/icons.py +151 -0
  74. tempestweb-0.1.0/tempestweb/pwa/manifest.py +296 -0
  75. tempestweb-0.1.0/tempestweb/pwa/pyodide_vendor.py +159 -0
  76. tempestweb-0.1.0/tempestweb/runtime/__init__.py +46 -0
  77. tempestweb-0.1.0/tempestweb/runtime/events.py +124 -0
  78. tempestweb-0.1.0/tempestweb/runtime/serialize.py +257 -0
  79. tempestweb-0.1.0/tempestweb/runtime/session.py +335 -0
  80. tempestweb-0.1.0/tempestweb/runtime/wasm.py +395 -0
  81. tempestweb-0.1.0/tempestweb/runtime/wasm_main.py +166 -0
  82. tempestweb-0.1.0/tempestweb/server/__init__.py +29 -0
  83. tempestweb-0.1.0/tempestweb/server/app.py +224 -0
  84. tempestweb-0.1.0/tempestweb/server/webpush.py +345 -0
  85. tempestweb-0.1.0/tempestweb/transports/__init__.py +89 -0
  86. tempestweb-0.1.0/tempestweb/transports/base.py +231 -0
  87. tempestweb-0.1.0/tempestweb/transports/sse.py +234 -0
  88. tempestweb-0.1.0/tempestweb/transports/wasm.py +191 -0
  89. tempestweb-0.1.0/tempestweb/transports/websocket.py +184 -0
  90. tempestweb-0.1.0/tests/client/dom.test.js +211 -0
  91. tempestweb-0.1.0/tests/client/events.test.js +180 -0
  92. tempestweb-0.1.0/tests/client/mount.test.js +180 -0
  93. tempestweb-0.1.0/tests/client/native.test.js +431 -0
  94. tempestweb-0.1.0/tests/client/offline-store.test.js +140 -0
  95. tempestweb-0.1.0/tests/client/offline-sync.test.js +151 -0
  96. tempestweb-0.1.0/tests/client/pwa-install-prompt.test.js +135 -0
  97. tempestweb-0.1.0/tests/client/pwa-manifest.test.js +127 -0
  98. tempestweb-0.1.0/tests/client/router.test.js +96 -0
  99. tempestweb-0.1.0/tests/client/setup.js +17 -0
  100. tempestweb-0.1.0/tests/client/smoke.test.js +11 -0
  101. tempestweb-0.1.0/tests/client/style.test.js +203 -0
  102. tempestweb-0.1.0/tests/client/sw-register.test.js +163 -0
  103. tempestweb-0.1.0/tests/client/sw-strategies.test.js +194 -0
  104. tempestweb-0.1.0/tests/client/transport-sse.test.js +152 -0
  105. tempestweb-0.1.0/tests/client/transport-wasm.test.js +100 -0
  106. tempestweb-0.1.0/tests/client/transport-ws.test.js +181 -0
  107. tempestweb-0.1.0/tests/client/virtualize.test.js +95 -0
  108. tempestweb-0.1.0/tests/client/web-push-client.test.js +186 -0
@@ -0,0 +1,29 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ *.egg-info/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ dist/
10
+ build/
11
+
12
+ # Pyodide / WASM build artifacts
13
+ /site/
14
+ /public/pyodide/
15
+
16
+ # Node (only if a test runner needs it; the client itself ships no node_modules)
17
+ node_modules/
18
+
19
+ # Editor / OS
20
+ .vscode/
21
+ .idea/
22
+ .DS_Store
23
+
24
+ # Worktrees live outside the tree; ignore any stray local ones
25
+ /worktrees/
26
+
27
+ # Local session permissions (not shared)
28
+ .claude/settings.local.json
29
+ .playwright-mcp/
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to **tempestweb** are documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/); this project adheres to semantic
5
+ versioning.
6
+
7
+ ## [0.1.0] — 2026-06-11
8
+
9
+ First public release. Build web apps in typed Python — one declarative tree, a
10
+ pure-JS DOM renderer, two execution modes (WASM in the browser via Pyodide, or a
11
+ FastAPI server over WebSocket/SSE).
12
+
13
+ ### Added
14
+
15
+ - **Two execution modes, one `view()`.** Mode A (WASM/Pyodide) runs Python in the
16
+ browser; Mode B (server) runs it on FastAPI over WebSocket + SSE. The app never
17
+ names a transport.
18
+ - **`tempestweb` CLI** — `new` (scaffold), `dev` (watch + reload), `build`
19
+ (`--mode wasm|server`), `run`. The wasm build emits a static bundle (Pyodide +
20
+ the `tempest_core`/`tempestweb` payload + `app.py`); the server build emits a
21
+ FastAPI host.
22
+ - **Pure-JS client** (no TypeScript, no framework, no build step): DOM patcher,
23
+ `Style`→CSS, delegated events, the three transports (wasm/ws/sse).
24
+ - **Trilho E parity** (live in Mode A): URL routing (deep links + back/forward +
25
+ pushState), virtualized lists with a proportional scrollbar, overlays
26
+ (dialogs/sheets), CSS transitions, pointer gestures (tap/swipe/long-press),
27
+ real form controls (Input/Checkbox/Image), and a11y (semantics→ARIA) / i18n /
28
+ theme.
29
+ - **Native capabilities** wired in both modes (geolocation, clipboard, http,
30
+ share, camera, audio, storage, notifications) — in-process FFI in Mode A,
31
+ proxied over the transport in Mode B.
32
+ - **PWA layer**: installable manifest + icons + a service worker with an injected
33
+ app-shell precache (offline second load).
34
+ - **Observability**: telemetry, logger, error boundary, feature flags, auth —
35
+ adapter pattern.
36
+ - **`tempestweb.components`**: ready-to-use validated fields (EmailField,
37
+ PasswordField, PhoneField, CPFField, CNPJField, AddressField, TextField) and
38
+ forms (LoginForm, SignupForm).
39
+ - **Bilingual docs** (PT-BR + EN) built with MkDocs Material.
40
+
41
+ ### Depends on
42
+
43
+ - [`tempest-core`](https://pypi.org/project/tempest-core/) `>=0.1.0` — the
44
+ renderer-agnostic UI engine (IR/reconciler/state/style/widgets).
45
+
46
+ ### Known follow-ups
47
+
48
+ - Mode B `view→URL` (pushState) needs a server→client nav envelope (Mode A is
49
+ bidirectional today).
50
+ - WebPush tab-closed delivery and real camera/geo need on-device verification.
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: tempestweb
3
+ Version: 0.1.0
4
+ Summary: Build web apps in typed Python — one tree, a DOM renderer, two execution modes (WASM + server).
5
+ Author-email: Mauricio Benjamin <mauricio.benjamin@reloverelations.com>
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: tempest-core>=0.1.0
9
+ Provides-Extra: cli
10
+ Requires-Dist: watchfiles>=0.21; extra == 'cli'
11
+ Provides-Extra: dev
12
+ Requires-Dist: httpx>=0.27; extra == 'dev'
13
+ Requires-Dist: mypy>=1.10; extra == 'dev'
14
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
15
+ Requires-Dist: pytest>=8; extra == 'dev'
16
+ Requires-Dist: ruff>=0.6; extra == 'dev'
17
+ Provides-Extra: docs
18
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
19
+ Requires-Dist: mkdocs-static-i18n>=1.2; extra == 'docs'
20
+ Provides-Extra: server
21
+ Requires-Dist: fastapi>=0.110; extra == 'server'
22
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'server'
23
+ Requires-Dist: websockets>=12; extra == 'server'
24
+ Provides-Extra: webpush
25
+ Requires-Dist: pywebpush>=1.14; extra == 'webpush'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # tempestweb
29
+
30
+ 📚 **Documentation:** [Português (Brasil)](https://mauriciobenjamin700.github.io/tempestweb/)
31
+ · [English (US)](https://mauriciobenjamin700.github.io/tempestweb/en/) — bilingual
32
+ docs site (PT-BR default + EN-US), deployed to GitHub Pages.
33
+
34
+ > Build web apps in **typed Python**. One declarative widget tree, a **DOM**
35
+ > renderer, and **two execution modes** that share 100% of the application code:
36
+ > **Mode A (WASM)** runs your Python in the browser via Pyodide; **Mode B
37
+ > (server)** runs it on the server (FastAPI) and talks to a thin JS client over
38
+ > **WebSocket or SSE**. Installable **PWA**, **offline-first** (service worker +
39
+ > IndexedDB), and **WebPush** are first-class — parity with `tempest-react-sdk`.
40
+
41
+ Sister project to [tempestroid](../tempestroid) — same "one tree, multiple
42
+ renderers" architecture. The renderer-agnostic engine (IR, reconciler, state,
43
+ style, widgets) is shared; tempestweb adds a **DOM** leaf renderer (pure
44
+ JavaScript, no framework, no build step, no TypeScript) and two patch transports.
45
+
46
+ ## Status
47
+
48
+ 🚧 Early construction. See the design docs:
49
+
50
+ - [`docs/plan.md`](docs/plan.md) — full design and phase plan.
51
+ - [`docs/roadmap.md`](docs/roadmap.md) — phase checklist.
52
+ - [`docs/arquitetura.md`](docs/arquitetura.md) — architecture.
53
+ - [`docs/contract.md`](docs/contract.md) — the Python↔client wire format.
54
+ - [`docs/agents/MANIFEST.md`](docs/agents/MANIFEST.md) — parallel agent task plan.
55
+
56
+ Want runnable apps? Browse the **[Example Gallery](https://mauriciobenjamin700.github.io/tempestweb/en/examples/)**
57
+ ([PT-BR](https://mauriciobenjamin700.github.io/tempestweb/examples/)) — 35 single-concept
58
+ demos (stopwatch, forms, data table, kanban, chat, theming, i18n, canvas, native
59
+ capabilities, observability, PWA/WebPush, and a server-mode walkthrough), each
60
+ running unchanged in both modes.
61
+
62
+ ## How it works
63
+
64
+ ```text
65
+ view(app) ──build──▶ Node tree (IR) ← shared core (vendored from tempestroid)
66
+
67
+ diff
68
+
69
+ [ Patch ] insert / remove / update / reorder / replace
70
+ ╱ ╲
71
+ Mode A transport Mode B transport
72
+ (pyodide.ffi) (WebSocket | SSE)
73
+ ╲ ╱
74
+ client/ (pure JS): apply patches to the DOM
75
+ + Style→CSS + event capture ← same code in both modes
76
+ ```
77
+
78
+ The application's `view()` never names a transport — the same `examples/counter/app.py`
79
+ runs under `--mode wasm` and `--mode server` unchanged. Capabilities (`native/`:
80
+ http, audio, share, geolocation, clipboard, storage, camera) are typed awaitables
81
+ with the same Python API in both modes — Mode A calls the Web API in-process, Mode
82
+ B proxies it over a round-trip (see [`docs/contract.md`](docs/contract.md)).
83
+
84
+ ## Develop
85
+
86
+ ```bash
87
+ uv venv && uv pip install -e ".[dev,server,cli]"
88
+ make check # ruff + mypy + pytest + JS (jsdom) tests
89
+ ```
90
+
91
+ ## Layout
92
+
93
+ | Path | What |
94
+ |---|---|
95
+ | `tempest-core` (dependency) | Renderer-agnostic engine — IR/reconciler/state/style/widgets (`import tempest_core`), extracted from tempestroid. |
96
+ | `tempestweb/components/` | Ready-to-use fields + forms (EmailField, PasswordField, PhoneField, LoginForm, …). |
97
+ | `tempestweb/transports/` | The one seam between modes (`base.py` Protocol, `wasm.py`, `websocket.py`, `sse.py`). |
98
+ | `tempestweb/server/` | FastAPI + WebSocket/SSE host (Mode B). |
99
+ | `tempestweb/native/` | Web API capability adapters — http, audio, share, geo, clipboard, storage, camera (Track N). |
100
+ | `tempestweb/observability/` | Telemetry, logger, error boundary, feature flags, auth — adapter pattern (Track O). |
101
+ | `tempestweb/pwa/` | Web App Manifest + icon emitter (Track P). |
102
+ | `tempestweb/cli/` | `tempestweb new/dev/build/run`. |
103
+ | `client/` | Pure-JS DOM renderer, Style→CSS, event capture; `pwa/` `sw/` `offline/` `push/` `native/` subdirs. |
104
+ | `tests/fixtures/` | Golden wire-format fixtures derived from the core. |
105
+
106
+ ## Conventions
107
+
108
+ Python: double quotes, full typing (mypy `--strict`), Google docstrings in English,
109
+ async-first. Client: **plain JavaScript only** — no TypeScript, no framework, no
110
+ build step. See [`CLAUDE.md`](CLAUDE.md).
@@ -0,0 +1,83 @@
1
+ # tempestweb
2
+
3
+ 📚 **Documentation:** [Português (Brasil)](https://mauriciobenjamin700.github.io/tempestweb/)
4
+ · [English (US)](https://mauriciobenjamin700.github.io/tempestweb/en/) — bilingual
5
+ docs site (PT-BR default + EN-US), deployed to GitHub Pages.
6
+
7
+ > Build web apps in **typed Python**. One declarative widget tree, a **DOM**
8
+ > renderer, and **two execution modes** that share 100% of the application code:
9
+ > **Mode A (WASM)** runs your Python in the browser via Pyodide; **Mode B
10
+ > (server)** runs it on the server (FastAPI) and talks to a thin JS client over
11
+ > **WebSocket or SSE**. Installable **PWA**, **offline-first** (service worker +
12
+ > IndexedDB), and **WebPush** are first-class — parity with `tempest-react-sdk`.
13
+
14
+ Sister project to [tempestroid](../tempestroid) — same "one tree, multiple
15
+ renderers" architecture. The renderer-agnostic engine (IR, reconciler, state,
16
+ style, widgets) is shared; tempestweb adds a **DOM** leaf renderer (pure
17
+ JavaScript, no framework, no build step, no TypeScript) and two patch transports.
18
+
19
+ ## Status
20
+
21
+ 🚧 Early construction. See the design docs:
22
+
23
+ - [`docs/plan.md`](docs/plan.md) — full design and phase plan.
24
+ - [`docs/roadmap.md`](docs/roadmap.md) — phase checklist.
25
+ - [`docs/arquitetura.md`](docs/arquitetura.md) — architecture.
26
+ - [`docs/contract.md`](docs/contract.md) — the Python↔client wire format.
27
+ - [`docs/agents/MANIFEST.md`](docs/agents/MANIFEST.md) — parallel agent task plan.
28
+
29
+ Want runnable apps? Browse the **[Example Gallery](https://mauriciobenjamin700.github.io/tempestweb/en/examples/)**
30
+ ([PT-BR](https://mauriciobenjamin700.github.io/tempestweb/examples/)) — 35 single-concept
31
+ demos (stopwatch, forms, data table, kanban, chat, theming, i18n, canvas, native
32
+ capabilities, observability, PWA/WebPush, and a server-mode walkthrough), each
33
+ running unchanged in both modes.
34
+
35
+ ## How it works
36
+
37
+ ```text
38
+ view(app) ──build──▶ Node tree (IR) ← shared core (vendored from tempestroid)
39
+
40
+ diff
41
+
42
+ [ Patch ] insert / remove / update / reorder / replace
43
+ ╱ ╲
44
+ Mode A transport Mode B transport
45
+ (pyodide.ffi) (WebSocket | SSE)
46
+ ╲ ╱
47
+ client/ (pure JS): apply patches to the DOM
48
+ + Style→CSS + event capture ← same code in both modes
49
+ ```
50
+
51
+ The application's `view()` never names a transport — the same `examples/counter/app.py`
52
+ runs under `--mode wasm` and `--mode server` unchanged. Capabilities (`native/`:
53
+ http, audio, share, geolocation, clipboard, storage, camera) are typed awaitables
54
+ with the same Python API in both modes — Mode A calls the Web API in-process, Mode
55
+ B proxies it over a round-trip (see [`docs/contract.md`](docs/contract.md)).
56
+
57
+ ## Develop
58
+
59
+ ```bash
60
+ uv venv && uv pip install -e ".[dev,server,cli]"
61
+ make check # ruff + mypy + pytest + JS (jsdom) tests
62
+ ```
63
+
64
+ ## Layout
65
+
66
+ | Path | What |
67
+ |---|---|
68
+ | `tempest-core` (dependency) | Renderer-agnostic engine — IR/reconciler/state/style/widgets (`import tempest_core`), extracted from tempestroid. |
69
+ | `tempestweb/components/` | Ready-to-use fields + forms (EmailField, PasswordField, PhoneField, LoginForm, …). |
70
+ | `tempestweb/transports/` | The one seam between modes (`base.py` Protocol, `wasm.py`, `websocket.py`, `sse.py`). |
71
+ | `tempestweb/server/` | FastAPI + WebSocket/SSE host (Mode B). |
72
+ | `tempestweb/native/` | Web API capability adapters — http, audio, share, geo, clipboard, storage, camera (Track N). |
73
+ | `tempestweb/observability/` | Telemetry, logger, error boundary, feature flags, auth — adapter pattern (Track O). |
74
+ | `tempestweb/pwa/` | Web App Manifest + icon emitter (Track P). |
75
+ | `tempestweb/cli/` | `tempestweb new/dev/build/run`. |
76
+ | `client/` | Pure-JS DOM renderer, Style→CSS, event capture; `pwa/` `sw/` `offline/` `push/` `native/` subdirs. |
77
+ | `tests/fixtures/` | Golden wire-format fixtures derived from the core. |
78
+
79
+ ## Conventions
80
+
81
+ Python: double quotes, full typing (mypy `--strict`), Google docstrings in English,
82
+ async-first. Client: **plain JavaScript only** — no TypeScript, no framework, no
83
+ build step. See [`CLAUDE.md`](CLAUDE.md).
@@ -0,0 +1,17 @@
1
+ // constants.js — shared client-side constants (tunables used across modules).
2
+ //
3
+ // Module-private values stay in their module; this file holds the few constants
4
+ // that are shared or are worth naming/tuning in one place: gesture-recognition
5
+ // thresholds and the virtualization stylesheet id.
6
+
7
+ /** Minimum pointer travel (px) for a drag to count as a swipe. */
8
+ export const SWIPE_MIN_PX = 30;
9
+
10
+ /** Hold time (ms, with little travel) for a press to count as a long press. */
11
+ export const LONG_PRESS_MS = 500;
12
+
13
+ /** Widget type tag that opts into gesture events (tap/swipe/long_press). */
14
+ export const GESTURE_TYPE = "GestureDetector";
15
+
16
+ /** Id of the injected stylesheet that carries virtualized-list spacer heights. */
17
+ export const VIRT_STYLE_ID = "tw-virt-styles";
@@ -0,0 +1,373 @@
1
+ // dom.js — build a DOM tree from the Node IR and apply patch batches to it. W1.
2
+ //
3
+ // buildElement(node) turns one serialized Node into a live DOM element (recursing
4
+ // into children); applyPatches(root, patches) mutates a tree in place. Given the
5
+ // DOM built from node_initial.json, applying patches_all_kinds.json yields the
6
+ // expected DOM. Patch kinds are distinguished by key presence (see transport.js):
7
+ // - set_props present -> Update
8
+ // - node + index present -> Insert
9
+ // - index only -> Remove
10
+ // - order present -> Reorder
11
+ // - node without index -> Replace
12
+ //
13
+ // Every element carries `data-tw-key` when its Node has a key, so events.js can
14
+ // read the originating widget key via event delegation. Verify against
15
+ // ../tests/fixtures/ in tests/client/ (jsdom). No framework.
16
+
17
+ import { styleToCss } from "./style.js";
18
+
19
+ /** Attribute holding a widget's stable reconciliation key. */
20
+ export const KEY_ATTR = "data-tw-key";
21
+ /** Attribute holding a widget's IR type (so patches can re-key/inspect it). */
22
+ export const TYPE_ATTR = "data-tw-type";
23
+
24
+ // Each widget type maps to one HTML tag. Container-like widgets are <div>; Text is
25
+ // an inline <span>; Button is a real <button>. Unknown types fall back to <div> so
26
+ // a new core widget renders (as a generic box) rather than throwing.
27
+ const TAG_BY_TYPE = Object.freeze({
28
+ Column: "div",
29
+ Row: "div",
30
+ Container: "div",
31
+ Stack: "div",
32
+ Text: "span",
33
+ Button: "button",
34
+ Input: "input",
35
+ Checkbox: "input",
36
+ Image: "img",
37
+ });
38
+
39
+ /**
40
+ * Resolve the HTML tag name for an IR widget type.
41
+ * @param {string} type The widget type ("Column", "Text", "Button", ...).
42
+ * @returns {string} The HTML tag name (defaults to "div").
43
+ */
44
+ function tagForType(type) {
45
+ return TAG_BY_TYPE[type] ?? "div";
46
+ }
47
+
48
+ /**
49
+ * Apply a node's props to an element: style, key/type attributes and text.
50
+ *
51
+ * `content` (Text) and `label` (Button) become the element's text. The `style`
52
+ * prop is translated by {@link styleToCss} into the inline `style` attribute. The
53
+ * widget `key` and `type` are mirrored onto data attributes for event delegation.
54
+ *
55
+ * @param {HTMLElement} el The target element.
56
+ * @param {string} type The widget type.
57
+ * @param {?string} key The widget key, or null.
58
+ * @param {Object} props The widget props (may include `style`).
59
+ * @returns {void}
60
+ */
61
+ function applyNodeShape(el, type, key, props) {
62
+ el.setAttribute(TYPE_ATTR, type);
63
+ if (key != null) {
64
+ el.setAttribute(KEY_ATTR, key);
65
+ } else {
66
+ el.removeAttribute(KEY_ATTR);
67
+ }
68
+ applyProps(el, props ?? {});
69
+ }
70
+
71
+ /**
72
+ * Apply a bag of props onto an element (style + text-bearing props).
73
+ *
74
+ * Used both when first building an element and by Update patches. `style` is
75
+ * (re)translated to CSS; `content`/`label` set the text. Other keys are widget
76
+ * metadata the DOM does not render and are ignored.
77
+ *
78
+ * @param {HTMLElement} el The target element.
79
+ * @param {Object} props The props to apply.
80
+ * @returns {void}
81
+ */
82
+ function applyProps(el, props) {
83
+ if ("style" in props) {
84
+ const css = styleToCss(props.style);
85
+ if (css) {
86
+ el.style.cssText = css;
87
+ } else {
88
+ el.removeAttribute("style");
89
+ }
90
+ }
91
+ const type = el.getAttribute(TYPE_ATTR);
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.
94
+ if ("content" in props) {
95
+ el.textContent = props.content == null ? "" : String(props.content);
96
+ }
97
+ if ("label" in props) {
98
+ if (type === "Checkbox") {
99
+ el.setAttribute("aria-label", props.label == null ? "" : String(props.label));
100
+ } else {
101
+ el.textContent = props.label == null ? "" : String(props.label);
102
+ }
103
+ }
104
+ applyControlProps(el, type, props);
105
+ applyA11yProps(el, props);
106
+ applyLazyProps(el, type, props);
107
+ }
108
+
109
+ /**
110
+ * Apply accessibility props (semantics + focus) onto an element.
111
+ *
112
+ * Maps the core's renderer-agnostic a11y model to ARIA/DOM: ``semantics.label``
113
+ * → ``aria-label``, ``semantics.role`` → ``role``, ``semantics.hint`` →
114
+ * ``aria-description``; ``focus_order`` sets an explicit ``tabindex`` and
115
+ * ``focusable`` toggles a default one (``0`` to include, ``-1`` to exclude).
116
+ *
117
+ * @param {HTMLElement} el The target element.
118
+ * @param {Object} props The props to apply.
119
+ * @returns {void}
120
+ */
121
+ function applyA11yProps(el, props) {
122
+ const sem = props.semantics;
123
+ if (sem != null && typeof sem === "object") {
124
+ if (sem.label != null) {
125
+ el.setAttribute("aria-label", String(sem.label));
126
+ }
127
+ if (sem.role != null) {
128
+ el.setAttribute("role", String(sem.role));
129
+ }
130
+ if (sem.hint != null) {
131
+ el.setAttribute("aria-description", String(sem.hint));
132
+ }
133
+ }
134
+ if (props.focus_order != null) {
135
+ el.setAttribute("tabindex", String(props.focus_order));
136
+ } else if (props.focusable === true) {
137
+ el.setAttribute("tabindex", "0");
138
+ } else if (props.focusable === false) {
139
+ el.setAttribute("tabindex", "-1");
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Apply form-control / media props (Input, Checkbox, Image) onto an element.
145
+ *
146
+ * Maps the widget's typed props onto the right DOM property/attribute so the
147
+ * control is actually interactive (a real <input> holding `value`, a checkbox
148
+ * reflecting `checked`, an <img> pointing at `src`). No-ops for other types.
149
+ *
150
+ * @param {HTMLElement} el The target element.
151
+ * @param {?string} type The widget type (from the data-tw-type attribute).
152
+ * @param {Object} props The props to apply.
153
+ * @returns {void}
154
+ */
155
+ // Virtualized list widgets: rendered as scroll viewports whose visible window
156
+ // the runtime slides in response to scroll events (see client/virtualize.js).
157
+ const LAZY_TYPES = Object.freeze(["LazyColumn", "LazyRow", "LazyGrid"]);
158
+
159
+ /**
160
+ * Mark a virtualized list element and mirror its windowing metadata to data
161
+ * attributes so the scroll controller can compute the visible window.
162
+ *
163
+ * @param {HTMLElement} el The target element.
164
+ * @param {?string} type The widget type.
165
+ * @param {Object} props The props to apply.
166
+ * @returns {void}
167
+ */
168
+ function applyLazyProps(el, type, props) {
169
+ if (type == null || !LAZY_TYPES.includes(type)) {
170
+ return;
171
+ }
172
+ const horizontal = type === "LazyRow";
173
+ // A bounded, scrollable viewport: the app's Style sets the extent (e.g.
174
+ // height); overflow scrolls the materialized window, and scrolling past the
175
+ // edge slides the window (see client/virtualize.js). min-height:0 stops a flex
176
+ // parent from growing the viewport to fit its content instead of scrolling.
177
+ el.style.overflowY = horizontal ? "hidden" : "auto";
178
+ el.style.overflowX = horizontal ? "auto" : "hidden";
179
+ el.style.minHeight = "0";
180
+ if ("item_count" in props && props.item_count != null) {
181
+ el.setAttribute("data-tw-item-count", String(props.item_count));
182
+ }
183
+ if ("window_size" in props && props.window_size != null) {
184
+ el.setAttribute("data-tw-window-size", String(props.window_size));
185
+ }
186
+ // window is [start, end) when slid, or null (start at 0).
187
+ const start = Array.isArray(props.window) ? props.window[0] : 0;
188
+ el.setAttribute("data-tw-window-start", String(start ?? 0));
189
+ }
190
+
191
+ function applyControlProps(el, type, props) {
192
+ if (type === "Input") {
193
+ el.setAttribute("type", props.secure ? "password" : "text");
194
+ if ("value" in props) {
195
+ el.value = props.value == null ? "" : String(props.value);
196
+ }
197
+ if ("placeholder" in props && props.placeholder != null) {
198
+ el.setAttribute("placeholder", String(props.placeholder));
199
+ }
200
+ if (props.max_length != null) {
201
+ el.setAttribute("maxlength", String(props.max_length));
202
+ }
203
+ } else if (type === "Checkbox") {
204
+ el.setAttribute("type", "checkbox");
205
+ if ("checked" in props) {
206
+ el.checked = Boolean(props.checked);
207
+ }
208
+ } else if (type === "Image") {
209
+ if ("src" in props && props.src != null) {
210
+ el.setAttribute("src", String(props.src));
211
+ }
212
+ if ("alt" in props && props.alt != null) {
213
+ el.setAttribute("alt", String(props.alt));
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Build a DOM element from an IR node (recursing into its children).
220
+ * @param {import("./transport.js").Node} node The serialized node.
221
+ * @returns {HTMLElement} The constructed element subtree.
222
+ */
223
+ export function buildElement(node) {
224
+ const el = document.createElement(tagForType(node.type));
225
+ applyNodeShape(el, node.type, node.key ?? null, node.props ?? {});
226
+ for (const child of node.children ?? []) {
227
+ el.appendChild(buildElement(child));
228
+ }
229
+ return el;
230
+ }
231
+
232
+ /**
233
+ * Walk a path of child indices from `root` down to the target element.
234
+ * @param {HTMLElement} root The root element.
235
+ * @param {number[]} path Child indices from the root ([] = root).
236
+ * @returns {HTMLElement} The element at `path`.
237
+ * @throws {RangeError} If an index does not resolve to an element.
238
+ */
239
+ function resolvePath(root, path) {
240
+ /** @type {HTMLElement} */
241
+ let el = root;
242
+ for (const index of path) {
243
+ const next = el.children[index];
244
+ if (next == null) {
245
+ throw new RangeError(`tempestweb: patch path out of range at index ${index}`);
246
+ }
247
+ el = /** @type {HTMLElement} */ (next);
248
+ }
249
+ return el;
250
+ }
251
+
252
+ /**
253
+ * Apply a single Update patch: set/unset props on the node at `path`.
254
+ * @param {HTMLElement} root The root element.
255
+ * @param {{path:number[], set_props?:Object, unset_props?:string[]}} patch The patch.
256
+ * @returns {void}
257
+ */
258
+ function applyUpdate(root, patch) {
259
+ const el = resolvePath(root, patch.path);
260
+ if (patch.set_props) {
261
+ applyProps(el, patch.set_props);
262
+ }
263
+ for (const key of patch.unset_props ?? []) {
264
+ if (key === "style") {
265
+ el.removeAttribute("style");
266
+ } else if (key === "content" || key === "label") {
267
+ el.textContent = "";
268
+ }
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Apply a single Insert patch: insert a new child at `index` under `path`.
274
+ * @param {HTMLElement} root The root element.
275
+ * @param {{path:number[], index:number, node:import("./transport.js").Node}} patch
276
+ * @returns {void}
277
+ */
278
+ function applyInsert(root, patch) {
279
+ const parent = resolvePath(root, patch.path);
280
+ const child = buildElement(patch.node);
281
+ const ref = parent.children[patch.index] ?? null;
282
+ parent.insertBefore(child, ref);
283
+ }
284
+
285
+ /**
286
+ * Apply a single Remove patch: remove the child at `index` under `path`.
287
+ * @param {HTMLElement} root The root element.
288
+ * @param {{path:number[], index:number}} patch The patch.
289
+ * @returns {void}
290
+ */
291
+ function applyRemove(root, patch) {
292
+ const parent = resolvePath(root, patch.path);
293
+ const child = parent.children[patch.index];
294
+ if (child != null) {
295
+ parent.removeChild(child);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Apply a single Reorder patch: new child `i` = old child `order[i]`.
301
+ *
302
+ * Snapshots the current children first so indices in `order` refer to the
303
+ * pre-reorder positions, then re-appends them in the requested order.
304
+ *
305
+ * @param {HTMLElement} root The root element.
306
+ * @param {{path:number[], order:number[]}} patch The patch.
307
+ * @returns {void}
308
+ */
309
+ function applyReorder(root, patch) {
310
+ const parent = resolvePath(root, patch.path);
311
+ const before = Array.from(parent.children);
312
+ for (const index of patch.order) {
313
+ const child = before[index];
314
+ if (child != null) {
315
+ parent.appendChild(child);
316
+ }
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Apply a single Replace patch: swap the element at `path` for a fresh subtree.
322
+ * @param {HTMLElement} root The root element.
323
+ * @param {{path:number[], node:import("./transport.js").Node}} patch The patch.
324
+ * @returns {void}
325
+ */
326
+ function applyReplace(root, patch) {
327
+ const old = resolvePath(root, patch.path);
328
+ const fresh = buildElement(patch.node);
329
+ if (old.parentNode) {
330
+ old.parentNode.replaceChild(fresh, old);
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Classify a patch by key presence and dispatch it to the right applier.
336
+ * @param {HTMLElement} root The root element.
337
+ * @param {import("./transport.js").Patch} patch The patch to apply.
338
+ * @returns {void}
339
+ * @throws {TypeError} If the patch shape is unrecognized.
340
+ */
341
+ function applyPatch(root, patch) {
342
+ if ("set_props" in patch || "unset_props" in patch) {
343
+ applyUpdate(root, /** @type {any} */ (patch));
344
+ } else if ("order" in patch) {
345
+ applyReorder(root, /** @type {any} */ (patch));
346
+ } else if ("node" in patch && "index" in patch) {
347
+ applyInsert(root, /** @type {any} */ (patch));
348
+ } else if ("node" in patch) {
349
+ applyReplace(root, /** @type {any} */ (patch));
350
+ } else if ("index" in patch) {
351
+ applyRemove(root, /** @type {any} */ (patch));
352
+ } else {
353
+ throw new TypeError(`tempestweb: unrecognized patch shape ${JSON.stringify(patch)}`);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Apply a coalesced batch of patches to the DOM tree rooted at `root`.
359
+ *
360
+ * The reconciler coalesces a tick's mutations into one ordered list; the whole
361
+ * list is applied before the next frame. Patches are applied in array order — the
362
+ * order the core emitted them — so index-relative ops (insert/remove/reorder)
363
+ * stay consistent.
364
+ *
365
+ * @param {HTMLElement} root The mounted root element.
366
+ * @param {import("./transport.js").Patch[]} patches The tick's patch batch.
367
+ * @returns {void}
368
+ */
369
+ export function applyPatches(root, patches) {
370
+ for (const patch of patches) {
371
+ applyPatch(root, patch);
372
+ }
373
+ }