patera-desktop-ui 0.2.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 (32) hide show
  1. patera_desktop_ui-0.2.0/LICENSE +21 -0
  2. patera_desktop_ui-0.2.0/PKG-INFO +457 -0
  3. patera_desktop_ui-0.2.0/README.md +444 -0
  4. patera_desktop_ui-0.2.0/docs/runtime.md +241 -0
  5. patera_desktop_ui-0.2.0/pyproject.toml +20 -0
  6. patera_desktop_ui-0.2.0/src/patera/desktop/ui/__init__.py +74 -0
  7. patera_desktop_ui-0.2.0/src/patera/desktop/ui/application.py +640 -0
  8. patera_desktop_ui-0.2.0/src/patera/desktop/ui/assets/adapter.js +180 -0
  9. patera_desktop_ui-0.2.0/src/patera/desktop/ui/assets/bootstrap.py +21 -0
  10. patera_desktop_ui-0.2.0/src/patera/desktop/ui/browser.py +941 -0
  11. patera_desktop_ui-0.2.0/src/patera/desktop/ui/build.py +291 -0
  12. patera_desktop_ui-0.2.0/src/patera/desktop/ui/cache.py +97 -0
  13. patera_desktop_ui-0.2.0/src/patera/desktop/ui/components.py +410 -0
  14. patera_desktop_ui-0.2.0/src/patera/desktop/ui/controller.py +72 -0
  15. patera_desktop_ui-0.2.0/src/patera/desktop/ui/discovery.py +45 -0
  16. patera_desktop_ui-0.2.0/src/patera/desktop/ui/exceptions.py +46 -0
  17. patera_desktop_ui-0.2.0/src/patera/desktop/ui/lifecycle.py +130 -0
  18. patera_desktop_ui-0.2.0/src/patera/desktop/ui/lists.py +214 -0
  19. patera_desktop_ui-0.2.0/src/patera/desktop/ui/metadata.py +55 -0
  20. patera_desktop_ui-0.2.0/src/patera/desktop/ui/properties.py +84 -0
  21. patera_desktop_ui-0.2.0/src/patera/desktop/ui/py.typed +0 -0
  22. patera_desktop_ui-0.2.0/src/patera/desktop/ui/routing.py +166 -0
  23. patera_desktop_ui-0.2.0/src/patera/desktop/ui/server.py +424 -0
  24. patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/LICENSE-MPL-2.0 +373 -0
  25. patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/NOTICE.md +25 -0
  26. patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/README.md +54 -0
  27. patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/__init__.py +5 -0
  28. patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/__init__.pyi +18879 -0
  29. patera_desktop_ui-0.2.0/src/patera/desktop/ui/types/_ecma.pyi +214 -0
  30. patera_desktop_ui-0.2.0/src/patera/desktop/ui/updates.py +106 -0
  31. patera_desktop_ui-0.2.0/src/patera/desktop/ui/view.py +335 -0
  32. patera_desktop_ui-0.2.0/tools/generate_browser_types.py +130 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marko Šterk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,457 @@
1
+ Metadata-Version: 2.4
2
+ Name: patera-desktop-ui
3
+ Version: 0.2.0
4
+ Summary: Python browser UI for Patera desktop applications
5
+ License-Expression: MIT AND MPL-2.0
6
+ License-File: LICENSE
7
+ License-File: src/patera/desktop/ui/types/LICENSE-MPL-2.0
8
+ Requires-Dist: patera-desktop>=0.11.2
9
+ Requires-Dist: jinja2>=3.1.6,<4
10
+ Requires-Dist: markupsafe>=3.0,<4
11
+ Requires-Python: >=3.13
12
+ Description-Content-Type: text/markdown
13
+
14
+ # patera-desktop-ui
15
+
16
+ Experimental Python UI for `patera-desktop`, using a locally bundled PyScript /
17
+ Pyodide interpreter. Install with `pip install patera-desktop-ui`.
18
+ Requires `patera-desktop>=0.11.2` for namespace and packaging integration.
19
+
20
+ The backend stays native Python. Browser controllers, components and models run
21
+ in a separate interpreter and communicate with Patera HTTP endpoints. Native
22
+ database sessions, files and services do not cross that boundary.
23
+
24
+ ## Application structure
25
+
26
+ Keep browser sources outside the backend package, so backend discovery and
27
+ PyInstaller do not import browser modules:
28
+
29
+ ```text
30
+ project/
31
+ app/__init__.py
32
+ app/configs.py
33
+ app/api/notes_api.py
34
+ frontend/
35
+ frontend/__init__.py
36
+ templates/note.html
37
+ ui-runtime/ complete local runtime, including runtime.json
38
+ build/ui/ generated, do not commit
39
+ ```
40
+
41
+ ```python
42
+ from patera import Patera, app
43
+ from patera.desktop import Desktop
44
+ from patera.desktop.ui import DesktopUI
45
+ from .configs import Config
46
+
47
+ @app(__name__, configs=Config)
48
+ class App(Patera[Config]):
49
+ app_extensions = [Desktop, DesktopUI]
50
+ ```
51
+
52
+ ```python
53
+ from patera import BaseConfig
54
+ from patera.desktop import DesktopConfig
55
+ from patera.desktop.ui import DesktopUIConfig
56
+
57
+ class Config(BaseConfig):
58
+ DESKTOP: DesktopConfig = DesktopConfig(INDEX_PAGE="/app/")
59
+ DESKTOP_UI: DesktopUIConfig = DesktopUIConfig(TITLE="Notes")
60
+ ```
61
+
62
+ Paths in `DesktopUIConfig` are relative to the backend application's root:
63
+
64
+ | Setting | Default | Meaning |
65
+ | --- | --- | --- |
66
+ | `SOURCE` | `../frontend` | Dedicated browser source root |
67
+ | `ENTRYPOINT` | `frontend:app` | An `Application` instance; never imported during build |
68
+ | `RUNTIME` | `../ui-runtime` | Complete offline runtime and hash manifest |
69
+ | `OUTPUT` | `../build/ui` | Build artifacts, outside source/runtime folders |
70
+ | `MOUNT` | `/app` | Shell and page URLs |
71
+ | `ASSETS` | `/__ui__` | Immutable asset URLs |
72
+ | `TITLE` | `Patera` | Initial window document title |
73
+ | `PUBLIC` | `{}` | Explicitly public browser configuration |
74
+ | `API_PATHS` | `['/api']` | API prefixes requiring the UI session token |
75
+
76
+ `URL_PREFIX` applies to shell, assets and API prefixes. Mounts must be disjoint,
77
+ non-root paths. Ordinary Patera endpoints outside these prefixes are unchanged.
78
+
79
+ ## Pages
80
+
81
+ `frontend/frontend/__init__.py`:
82
+
83
+ ```python
84
+ from patera.desktop.ui import Application, Controller, PageContext, View, path, page
85
+
86
+ @path("/notes")
87
+ class Notes(Controller):
88
+ @page("/<int:note_id>")
89
+ async def detail(self, ctx: PageContext, note_id: int) -> View:
90
+ note = await ctx.api.get_json(f"/api/notes/{note_id}")
91
+ return View("templates/note.html", note=note, title=note["title"])
92
+
93
+ app = Application(controllers=[Notes])
94
+ ```
95
+
96
+ Templates use Jinja2 with autoescaping and strict undefined values. Inheritance,
97
+ includes, macros, expressions, loops, filters and globals are supported.
98
+ Use `View(markup=html("<h1>{{ title }}</h1>", title=value))` for inline markup.
99
+ Use `trusted_html()` only for application-authored markup. Interpolation belongs
100
+ in HTML text or quoted ordinary attributes, never scripts, styles, event-handler
101
+ attributes or unvalidated URLs. Jinja2 and MarkupSafe's portable Python sources
102
+ and licenses are bundled automatically; no browser-side installation is needed.
103
+
104
+ Component pages, persistent layouts, reactive fragments, lifecycle hooks,
105
+ authorization, lazy dependencies and server-rendered components are described
106
+ in the [runtime guide](docs/runtime.md). Controller pages remain supported and
107
+ can also use layouts.
108
+
109
+ Routes support `string` (default), nonnegative `int`, and `path` converters.
110
+ `app.url_for("Notes.detail", note_id=3, query={"tab": "details"})` includes the
111
+ mount after startup. Multiple `@page` decorators may alias one method; the first
112
+ registered route is used for URL generation. Distinct handlers cannot share a
113
+ route or endpoint. Trailing slashes are ignored by default.
114
+
115
+ `ctx.query` maps keys to lists. `ctx.spawn(coroutine)` tracks a task for the page;
116
+ navigation cancels it. `ctx.on_cleanup(callback)` registers resource cleanup.
117
+ Navigation cancels superseded work and checks a generation number before
118
+ committing a result. Controllers live for the application lifetime; page-local
119
+ state belongs in the context or components.
120
+
121
+ `ExceptionHandler` with `@handles(ValueError)` methods `(self, ctx, error)` handles
122
+ page and event errors; the most specific exception type wins. Return a `View`
123
+ for an error page, or `None` from an event handler to retain the current page.
124
+ Unhandled failures appear in an alert and the browser console.
125
+
126
+ ## Components
127
+
128
+ ```python
129
+ from __future__ import annotations
130
+ from typing import TYPE_CHECKING
131
+
132
+ from patera.desktop.ui import Component, State, event, html, tag
133
+
134
+ if TYPE_CHECKING:
135
+ from patera.desktop.ui.types import MouseEvent
136
+
137
+ @tag("note-counter")
138
+ class Counter(Component):
139
+
140
+ def __init__(self, app, element):
141
+ super().__init__(app, element)
142
+ self.state = State(count=0)
143
+
144
+ def render(self):
145
+ return html('<button data-ui-on-click="increment">Count: '
146
+ '<span data-ui-text="count"></span></button>')
147
+
148
+ @event("click")
149
+ def increment(self, event: MouseEvent) -> None:
150
+ self.state["count"] += 1
151
+ ```
152
+
153
+ Register with `Application(..., components=[Counter])`; use
154
+ `<note-counter></note-counter>` in a view. `mounted()` and `unmounted()` may be
155
+ async. Event names are explicit; `data-ui-on-input="change"` requires
156
+ `@event("input")`. `data-ui-value` updates a control's value; use an input handler
157
+ to write back to state. Bindings update nodes without replacing the component,
158
+ preserving input focus. Assign to state to notify subscribers; in-place mutation
159
+ of nested values does not notify. `State.subscribe()` returns an unsubscribe
160
+ function. The runtime owns and releases DOM listener proxies.
161
+
162
+ ## Offline runtime and builds
163
+
164
+ Provision a **complete** pinned PyScript/Pyodide offline distribution in
165
+ `ui-runtime`. Follow the official [offline guide](https://docs.pyscript.net/2026.7.3/user-guide/offline/).
166
+ Include PyScript's supporting modules, the interpreter's `.mjs`, `.js`, `.wasm`,
167
+ `python_stdlib.zip`, `pyodide-lock.json`, and packages required by that runtime.
168
+ Preserve its directory layout and licenses. Nothing downloads at application
169
+ startup or as a side effect of extension registration.
170
+
171
+ Record the actual entry paths and versions (these arguments describe your
172
+ chosen runtime; do not substitute a different version's files):
173
+
174
+ ```sh
175
+ patera cli ui:lock-runtime pyscript/core.js pyscript/pyodide/pyodide.mjs 2026.2.1 0.28.0.dev0 3.13
176
+ patera cli ui:build
177
+ patera cli desktop:run
178
+ ```
179
+
180
+ The tested PyScript **2026.2.1 offline archive** uses entry paths
181
+ `pyscript/core.js` and `pyscript/pyodide/pyodide.mjs`, with Pyodide
182
+ `0.28.0.dev0` and Python `3.13`. For that archive, extract its `offline/`
183
+ contents into `ui-runtime/` and use the command above.
184
+
185
+ Other releases can require a newer host Python for syntax validation. The
186
+ runtime is an explicit build input rather than a floating CDN dependency.
187
+
188
+ Commit `runtime.json` or retain it with your build inputs. Every build checks
189
+ the recorded SHA-256 hashes. The builder validates Python syntax against the
190
+ recorded browser Python version; build with a host Python at least that new.
191
+ This does not validate third-party package compatibility or ensure an incomplete
192
+ runtime has all of its transitive assets: test a fresh browser profile offline.
193
+
194
+ The browser archive contains minimal `patera`/`patera.desktop` parent packages,
195
+ portable UI modules, and only allowlisted frontend files. It never installs the
196
+ native Patera wheel in Pyodide. Thus public UI imports stay the same without
197
+ changing Patera core imports. Frontend packages need `__init__.py`; their parent
198
+ initializers must be browser-safe. Keep shared models in a separate lightweight
199
+ package rather than importing through the backend application's initializer.
200
+
201
+ The builder excludes hidden files, caches, logs, databases, bytecode and unknown
202
+ file extensions. Frontend Python, HTML, JS, CSS, fonts and images are public.
203
+ Do not put secrets in this source root. `.html` files become named templates;
204
+ other non-Python assets are served below the build's `public/` URL.
205
+
206
+ Builds are content-addressed and switch `current.json` atomically. Re-run
207
+ `ui:build` after edits, or run `patera cli ui:watch` in another terminal.
208
+ For a single-command desktop workflow, use `patera cli ui:run`: it builds first,
209
+ watches in a background thread, and runs the desktop window on the main thread.
210
+ Closing the window or Ctrl+C stops the watcher; any active build finishes before
211
+ exit. The desktop extension handles backend shutdown. Initial build failures
212
+ prevent launch; subsequent failures retain the working build. `ui:run prod`
213
+ builds and launches without watching. Both modes require the Desktop extension.
214
+ Development windows (`DEBUG=True`) poll the build version and reload after a
215
+ successful build. Failed builds preserve the working version. Previous builds
216
+ remain available to already-open windows. Frozen applications never poll or
217
+ rebuild. Third-party dependency installation is not included in this version.
218
+
219
+ Use `app.config` for `PUBLIC` settings and `app.asset_url("styles.css")` for
220
+ bundled non-template resources after startup. Build with the deployment's
221
+ `URL_PREFIX`; the generated interpreter configuration contains that prefix.
222
+
223
+ ## Desktop packaging
224
+
225
+ New `desktop:spec` files include extension asset preparation. Both
226
+ `desktop:package` and direct `pyinstaller desktop.spec` builds prepare the UI.
227
+ Existing specs need the `DESKTOP_ASSETS` setting and `prepare_desktop_assets`
228
+ call from a newly generated spec. The package command rejects old specs when
229
+ the UI extension is registered. Custom specs must retain this call.
230
+
231
+ Artifacts are installed under `patera_ui` in PyInstaller's resource directory.
232
+ Frozen applications read those artifacts, never compile sources or write to
233
+ the installation directory. Runtime configuration is separate from the bundle;
234
+ the API session token is generated per backend process and never persisted.
235
+
236
+ ## Local API boundary
237
+
238
+ Protected API prefixes require `X-Patera-UI-Token` on every HTTP method. The
239
+ built-in API client adds it, checks status codes (`APIError`), and aborts fetches
240
+ when cancelled. Shell/API requests require a loopback Host and reject foreign
241
+ Origin and cross-site fetch metadata. This works with both the development
242
+ subprocess and packaged server thread because the shell obtains the token from
243
+ the running server. Do not enable permissive CORS for these routes.
244
+
245
+ This is a browser cross-origin boundary, not authentication against other local
246
+ processes. Backend authorization is still required where applicable. WebSocket
247
+ routes and native bridges are not provided or protected by this HTTP middleware;
248
+ secure them independently. Keep heavy computation on the backend. The initial
249
+ browser adapter targets Pyodide on the main thread, not MicroPython or workers.
250
+
251
+ ## Verification
252
+
253
+ `pytest tests/extensions/test_desktop_ui.py` covers portable routing, escaping,
254
+ cancellation, deterministic builds, runtime hashes, native-import isolation,
255
+ HTTP serving, API protection, frozen paths, and standalone spec execution.
256
+
257
+ The optional `tests/browser/desktop_ui.cjs` Playwright test exercises the notes
258
+ example in a fresh browser context with all non-local requests blocked. It
259
+ checks saving, escaped text, navigation, Back/Forward and deep-link refresh.
260
+ Install Playwright separately; set `NODE_PATH` to its node_modules folder and
261
+ `UI_TEST_URL` to the running example's `/app/` URL. `UI_TEST_BROWSER=msedge`
262
+ uses installed Edge instead of Playwright's Chromium. Run with
263
+ `node tests/browser/desktop_ui.cjs`. No browser automation dependency is added
264
+ to the framework's runtime dependencies.
265
+
266
+ ### Folder discovery and type hints
267
+
268
+ Use `app = Application(__name__)` in your frontend package's `__init__.py`.
269
+ The application recursively imports public modules under `controllers/`,
270
+ `components/`, `pages/`, `layouts/`, and `exceptions/` (or `exception_handlers/`). Give each folder
271
+ an `__init__.py`; nested packages work too, including inside the browser ZIP.
272
+ Concrete subclasses defined in those modules are registered automatically.
273
+ Imported/re-exported classes are ignored, abstract classes are skipped, and
274
+ import errors are surfaced. Modules are visited in deterministic name order.
275
+ Only these frontend folders are scanned; backend discovery is unchanged.
276
+
277
+ ```text
278
+ frontend/
279
+ __init__.py # app = Application(__name__)
280
+ controllers/
281
+ __init__.py
282
+ pages.py # @path class Pages(Controller), @page methods
283
+ components/
284
+ __init__.py
285
+ counter.py # @tag("note-counter") class Counter(Component)
286
+ exceptions/
287
+ __init__.py
288
+ errors.py # class Errors(ExceptionHandler), @handles methods
289
+ ```
290
+
291
+ Explicit `controllers=`, `components=`, `pages=`, `layouts=`, and `exception_handlers=` lists may
292
+ supplement discovery; the same class is registered once. Omit the package name
293
+ to use only explicit registration. Use `@tag("note-counter")` on each component.
294
+
295
+ Import `PageContext` to annotate page handlers as
296
+ `async def index(self, ctx: PageContext) -> View`. Error handlers may receive
297
+ `PageContext | None` when no page is active. For event handlers, import `Event`,
298
+ `MouseEvent`, `KeyboardEvent`, `InputEvent`, or `SubmitEvent` from
299
+ `patera.desktop.ui.types` under `TYPE_CHECKING`, with postponed annotations.
300
+ These stubs describe the original browser event proxy; they do not wrap or
301
+ convert events. Narrow or cast event targets to access element-specific fields.
302
+
303
+ ## Reactive update scheduling and diagnostics
304
+
305
+ Existing applications keep synchronous binding updates. Opt into frame batching
306
+ when constructing the frontend application:
307
+
308
+ ```python
309
+ app = Application(__name__, batch_updates=True, diagnostics=True)
310
+
311
+ # Inside an async component handler:
312
+ self.count += 1 # Python reads see the new value immediately
313
+ self.count += 1
314
+ await self.flush() # or await app.flush()
315
+ ```
316
+
317
+ With batching, repeated invalidations of a binding are combined. A selected
318
+ parent fragment also covers its selected descendants. Normal commits run on the
319
+ next animation frame; explicit `flush()` commits without waiting for a frame,
320
+ including when the window is hidden. Updates caused during rendering are handled
321
+ in subsequent batches, with a 100-batch limit to surface update loops. Disposing a
322
+ component removes its queued work. Full `rerender()` remains an explicit async
323
+ operation; batching applies to reactive binding/text/value updates.
324
+
325
+ `flush()` is a DOM commit barrier for queued bindings, not a paint, network,
326
+ initialization, or event-task barrier. After inserting components through a
327
+ binding, use `await self.wait_children()` if their initialization must
328
+ finish before continuing. `insert_html()` and `hydrate()` already wait for
329
+ non-deferred children. Initial rendering resolves `init_complete` after those
330
+ children and `BEFORE_INIT_RESOLVE`; `AFTER_INIT` and `mounted()` run afterward.
331
+ Explicit `rerender()` waits for children before `AFTER_RERENDER`. Fragment updates
332
+ do not invoke full-render lifecycle hooks. A newer render or disposal prevents
333
+ obsolete asynchronous results from committing.
334
+
335
+ Fragment commits retain listeners outside the replaced subtree, hydrate only
336
+ inserted nodes, and skip identical output. Binding identifiers remain stable
337
+ when an unchanged parent recreates its nested bindings. Form submission and
338
+ explicit event modifiers still cancel events during synchronous DOM dispatch.
339
+ Ordinary list bindings still replace their rendered range. Use the opt-in list
340
+ blocks below for keyed reconciliation or virtualization.
341
+
342
+ Diagnostics are disabled by default. With `diagnostics=True`, inspect
343
+ `app.diagnostics_snapshot()` for aggregate counts and timings in milliseconds:
344
+ template evaluation, DOM replacement, hydration, listener addition/removal,
345
+ initialization, and cleanup. Initialization/cleanup timings include awaited work;
346
+ nested measurements overlap and must not be summed as a total. The snapshot also
347
+ counts registered components, component/browser tasks, listeners/proxies,
348
+ subscriptions, fragments, and queued components. These are framework resource
349
+ counts, not total Python/WebAssembly memory or arbitrary user-created tasks.
350
+ `app.diagnostics.reset()` resets measurements without changing live resources.
351
+ Snapshots contain copied numeric data, not retained component/event references.
352
+
353
+ See `tests/browser/framework_updates.cjs` for isolated framework workloads and
354
+ `tests/browser/BENCHMARKS.md` for running the browser checks and comparisons.
355
+
356
+ ## Keyed lists
357
+
358
+ Use a Jinja `keyed` block where a `data-bind` loop would otherwise replace every
359
+ row. The block name is the invalidation name, just like a normal binding:
360
+
361
+ ```html
362
+ <ul>
363
+ {% keyed "items" %}
364
+ {% for item in component.items %}
365
+ <li data-ui-key="{{ item.id }}">
366
+ <input value="{{ item.title }}">
367
+ <button @click="remove" :item="{{ item.id }}">Remove</button>
368
+ </li>
369
+ {% endfor %}
370
+ {% endkeyed %}
371
+ </ul>
372
+ ```
373
+
374
+ For example, an `items = react_factory(list)` field invalidates this block on
375
+ assignment. Every direct row needs a stable, unique, non-empty `data-ui-key`.
376
+ Keys describe item identity, so avoid array positions when items can move.
377
+ Missing/duplicate keys and malformed row HTML fail before DOM reconciliation.
378
+ Use explicitly balanced HTML; for table rows put the block inside a `<tbody>`.
379
+
380
+ Matching keys and element types retain their row nodes. The framework patches
381
+ changed text and declared attributes, creates new rows, removes obsolete rows,
382
+ and moves existing rows without remounting their Patera child components. Input
383
+ edits remain intact when their declared value did not change; an explicitly
384
+ changed value updates the field. Focus and selection survive moves of retained
385
+ elements. Event handlers are rebound only where their declarations changed.
386
+
387
+ A mounted child component owns its interior. Its host and local Python state
388
+ survive a move; changed `:argument`/`data-ui-arg-*` values update `args` and schedule
389
+ its normal `rerender()` lifecycle. After `await self.flush()`, use
390
+ `await self.wait_children()` to await new child initialization and pending list
391
+ argument updates. Removed children are disposed. Changing a row's key or element
392
+ type deliberately replaces it. Deferred children receive new arguments without
393
+ being initialized automatically. These guarantees cover framework-controlled moves
394
+ within the list, not arbitrary DOM moves across component/application owners.
395
+
396
+ Nested ordinary `bind`/`data-bind` regions inside rows are supported. Place a
397
+ nested keyed or virtual list in a child Component so it has its own renderer and
398
+ lifetime. Keyed rendering still evaluates the entire loop; it reduces DOM and
399
+ lifecycle churn, rather than the amount of row-template evaluation.
400
+
401
+ ## Fixed-height virtual lists
402
+
403
+ For a large sequence, a `virtual` block evaluates and mounts only a visible
404
+ window plus overscan. Its body receives `item` and zero-based `index`:
405
+
406
+ ```html
407
+ {% virtual "items", component.items, key="id", row_height=40, height=400, overscan=3 %}
408
+ <span>{{ item.title }}</span>
409
+ <button @click="open" :item="{{ item.id }}">Open</button>
410
+ {% endvirtual %}
411
+ ```
412
+
413
+ `key` is a mapping key, object attribute name, or callable such as
414
+ `key=component.item_key`. Key values must be non-empty strings or integers and
415
+ must be unique across the sequence (integer `1` and string `"1"` collide).
416
+ `row_height` and viewport `height` are positive pixel sizes; `overscan` is a
417
+ non-negative integer. Defaults are 40, 400, and 3 respectively. The framework
418
+ provides a scroll viewport, spacer elements, fixed-height row wrappers, and
419
+ list/listitem roles with position and total-size attributes. Keep row content
420
+ within the configured height; variable-height rows and automatic measurement
421
+ are not supported. Use a normal block container, not a table or select element.
422
+
423
+ Scroll updates are frame-coalesced in either application update mode, and a
424
+ scroll that stays within the same rendered window does not reevaluate templates.
425
+ The viewport and overlapping keyed rows retain identity. Filtering clamps the
426
+ scroll offset when the sequence shrinks; empty sequences remove all rows.
427
+ Off-screen rows are disposed, so keep persistent edits/selection in application
428
+ state rather than solely in a row component. Keyboard focus on a row removed
429
+ from the window is not pinned or restored automatically.
430
+
431
+ The items expression and keys are reevaluated on invalidation; key validation
432
+ still scans the sequence. Only row-template evaluation, DOM size, and mounted
433
+ row components are bounded by the viewport. These APIs add no frontend runtime
434
+ dependencies and keep Jinja2 as the template engine.
435
+
436
+ ## Browser type library
437
+
438
+ The package includes a broad browser API stub library under
439
+ `patera.desktop.ui.types`, covering DOM elements, events, networking, storage,
440
+ canvas, media and common JavaScript built-ins. No webtypy dependency is needed.
441
+
442
+ ```python
443
+ from __future__ import annotations
444
+ from typing import TYPE_CHECKING
445
+
446
+ if TYPE_CHECKING:
447
+ from patera.desktop.ui.types import MouseEvent, HTMLInputElement
448
+
449
+ def clicked(event: MouseEvent) -> None:
450
+ event.preventDefault()
451
+ button: int = event.button
452
+ ```
453
+
454
+ These declarations are for static analysis; actual browser objects still come
455
+ from `js` or `pyscript`. See the [type library guide](src/patera/desktop/ui/types/README.md)
456
+ for coverage and limitations and [provenance](src/patera/desktop/ui/types/NOTICE.md)
457
+ for the generated definitions' MPL-2.0 license.