pysx-framework 0.1.0__py3-none-any.whl

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 (148) hide show
  1. examples/README.md +346 -0
  2. examples/__init__.py +17 -0
  3. examples/attributes.py +89 -0
  4. examples/browser_recovery.py +67 -0
  5. examples/components/THEME.md +25 -0
  6. examples/components/__init__.py +51 -0
  7. examples/components/composition.py +195 -0
  8. examples/components/controls.py +109 -0
  9. examples/components/development.js +45 -0
  10. examples/components/development.py +24 -0
  11. examples/components/diagnostics.py +17 -0
  12. examples/components/events.py +153 -0
  13. examples/components/favicon.png +0 -0
  14. examples/components/forms.py +91 -0
  15. examples/components/hosting.py +37 -0
  16. examples/components/lists.py +48 -0
  17. examples/components/navigation.py +24 -0
  18. examples/components/page.py +28 -0
  19. examples/components/templates.py +21 -0
  20. examples/components/theme.py +75 -0
  21. examples/components/themes.py +25 -0
  22. examples/composition.py +27 -0
  23. examples/counter.py +41 -0
  24. examples/development.py +68 -0
  25. examples/diagnostics.py +95 -0
  26. examples/durable_state.py +161 -0
  27. examples/editor_features.py +213 -0
  28. examples/effects.py +113 -0
  29. examples/events.py +36 -0
  30. examples/forms.py +143 -0
  31. examples/hello.py +17 -0
  32. examples/keyed_rows.py +120 -0
  33. examples/navigation.py +160 -0
  34. examples/operators.py +242 -0
  35. examples/reactive_state.py +124 -0
  36. examples/reconnect.py +85 -0
  37. examples/recovery.py +96 -0
  38. examples/resources.py +108 -0
  39. examples/session_security.py +105 -0
  40. examples/templates.py +150 -0
  41. examples/themes.py +109 -0
  42. examples/todos.py +195 -0
  43. pysx/__init__.py +221 -0
  44. pysx/compiler/__init__.py +1 -0
  45. pysx/compiler/bindings.py +90 -0
  46. pysx/compiler/compiler.py +938 -0
  47. pysx/compiler/imports.py +168 -0
  48. pysx/compiler/loader.py +197 -0
  49. pysx/compiler/parser.py +1051 -0
  50. pysx/compiler/runtime.py +93 -0
  51. pysx/compiler/scope.py +340 -0
  52. pysx/compiler/source_map.py +268 -0
  53. pysx/compiler/template.py +152 -0
  54. pysx/components/__init__.py +1 -0
  55. pysx/components/boundaries.py +55 -0
  56. pysx/components/checkpoints.py +588 -0
  57. pysx/components/composition.py +97 -0
  58. pysx/components/elements.py +136 -0
  59. pysx/components/errors.py +87 -0
  60. pysx/components/lifecycle.py +623 -0
  61. pysx/components/native.py +1269 -0
  62. pysx/components/native_support.py +71 -0
  63. pysx/components/schema.py +699 -0
  64. pysx/development/__init__.py +1 -0
  65. pysx/development/control.py +689 -0
  66. pysx/development/development.py +973 -0
  67. pysx/development/diagnostics.py +79 -0
  68. pysx/development/tracing.py +145 -0
  69. pysx/hosting/__init__.py +1 -0
  70. pysx/hosting/_http_connection.py +33 -0
  71. pysx/hosting/_uvicorn.py +52 -0
  72. pysx/hosting/asgi.py +309 -0
  73. pysx/hosting/browser_recovery.py +230 -0
  74. pysx/hosting/durable_host.py +427 -0
  75. pysx/hosting/hosting.py +899 -0
  76. pysx/hosting/http.py +507 -0
  77. pysx/hosting/recovery_keys.py +285 -0
  78. pysx/hosting/request.py +71 -0
  79. pysx/hosting/security.py +123 -0
  80. pysx/hosting/server.py +234 -0
  81. pysx/hosting/telemetry.py +22 -0
  82. pysx/playgrounds/__init__.py +5 -0
  83. pysx/playgrounds/capstone.py +224 -0
  84. pysx/playgrounds/counter.py +33 -0
  85. pysx/playgrounds/development.py +43 -0
  86. pysx/playgrounds/durable.py +98 -0
  87. pysx/playgrounds/recovery.py +50 -0
  88. pysx/py.typed +0 -0
  89. pysx/reactive/__init__.py +1 -0
  90. pysx/reactive/operators.py +235 -0
  91. pysx/reactive/reactive.py +580 -0
  92. pysx/reactive/structured.py +75 -0
  93. pysx/rendering/__init__.py +1 -0
  94. pysx/rendering/identity.py +25 -0
  95. pysx/rendering/output.py +172 -0
  96. pysx/rendering/render.py +2248 -0
  97. pysx/rendering/tree.py +287 -0
  98. pysx/runtime/__init__.py +1 -0
  99. pysx/runtime/async_runtime.py +169 -0
  100. pysx/runtime/capsule.py +191 -0
  101. pysx/runtime/connection.py +974 -0
  102. pysx/runtime/dom.py +418 -0
  103. pysx/runtime/durable.py +164 -0
  104. pysx/runtime/events.py +123 -0
  105. pysx/runtime/forms.py +165 -0
  106. pysx/runtime/ownership.py +343 -0
  107. pysx/runtime/persistence.py +426 -0
  108. pysx/runtime/resources.py +144 -0
  109. pysx/runtime/routing.py +404 -0
  110. pysx/runtime/session.py +576 -0
  111. pysx/runtime/transport.py +133 -0
  112. pysx/runtime/wire.py +424 -0
  113. pysx/static/client.js +1273 -0
  114. pysx/static/development.js +53 -0
  115. pysx/static/index.html +13 -0
  116. pysx/styling/__init__.py +1 -0
  117. pysx/styling/scoped_css.py +183 -0
  118. pysx/styling/styled.py +285 -0
  119. pysx/styling/styled_native.py +1583 -0
  120. pysx/styling/styles.py +200 -0
  121. pysx/tooling/__init__.py +1 -0
  122. pysx/tooling/analysis.py +226 -0
  123. pysx/tooling/build.py +117 -0
  124. pysx/tooling/check.py +1048 -0
  125. pysx/tooling/diagnostic_rules.py +435 -0
  126. pysx/tooling/editor/__init__.py +1 -0
  127. pysx/tooling/editor/config.py +286 -0
  128. pysx/tooling/editor/model.py +353 -0
  129. pysx/tooling/editor/ownership.py +196 -0
  130. pysx/tooling/editor/types.py +84 -0
  131. pysx/tooling/examples.py +69 -0
  132. pysx/tooling/lint.py +242 -0
  133. pysx/tooling/lsp/__init__.py +1 -0
  134. pysx/tooling/lsp/backend.py +363 -0
  135. pysx/tooling/lsp/edits.py +268 -0
  136. pysx/tooling/lsp/features.py +1041 -0
  137. pysx/tooling/lsp/lsp.py +28 -0
  138. pysx/tooling/lsp/server.py +1537 -0
  139. pysx/tooling/lsp/workspace.py +408 -0
  140. pysx/tooling/playground.py +62 -0
  141. pysx/tooling/quality.py +280 -0
  142. pysx/tooling/sources.py +31 -0
  143. pysx/tooling/tools.py +111 -0
  144. pysx_framework-0.1.0.dist-info/METADATA +103 -0
  145. pysx_framework-0.1.0.dist-info/RECORD +148 -0
  146. pysx_framework-0.1.0.dist-info/WHEEL +4 -0
  147. pysx_framework-0.1.0.dist-info/entry_points.txt +6 -0
  148. pysx_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
examples/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # Running examples
2
+
3
+ New users can start with the [installed quickstart](../docs/QUICKSTART.md),
4
+ [interactive tutorial](../docs/TUTORIAL.md), [API index](../docs/API.md) and
5
+ [migration guide](../docs/MIGRATION.md). The full course is a separate application
6
+ in [tutorial](../tutorial/README.md), launched with `pysx-tutorial` in
7
+ its own environment. Run
8
+ `hello` for a minimal greeting, `attributes` for native/ARIA/keyed attributes and
9
+ `effects` for cleanup, automatic increments and cancelled/replaced work.
10
+
11
+ In `todos`, choose **Edit** on a row, change its text and **Save**. The same keyed
12
+ row remains; toggling or removing it acts on the edited record. Other tabs keep
13
+ their own lists. In `navigation`, the files root and missing-route links expose
14
+ wildcard/fallback matching; the last links demonstrate native downloads, new tabs
15
+ and external navigation.
16
+
17
+ Run commands from the repository root with [uv](https://docs.astral.sh/uv/).
18
+ Python 3.14+ is required for t-strings; uv selects the project's interpreter and
19
+ prepares the environment without activation.
20
+
21
+ ```sh
22
+ uv run --project . pysx-example run counter
23
+ uv run --project . pysx-example run todos
24
+ uv run --project . pysx-example run recovery
25
+ uv run --project . pysx-example run reconnect
26
+ uv run --project . pysx-example run browser_recovery
27
+ uv run --project . pysx-example run resources
28
+ uv run --project . pysx-example run reactive_state
29
+ uv run --project . pysx-example run operators
30
+ uv run --project . pysx-example run forms
31
+ uv run --project . pysx-example run events
32
+ uv run --project . pysx-example run templates
33
+ uv run --project . pysx-example run keyed_rows
34
+ uv run --project . pysx-example run durable_state
35
+ uv run --project . pysx-example run development
36
+ uv run --project . pysx-example run
37
+ ```
38
+
39
+ The last command lists available apps. Examples serve at http://127.0.0.1:8750.
40
+ The `development` entry is a simple reactive counter with its own shared state
41
+ panel. It needs no database or custom hot-reload wiring. Ordinary watch apps have
42
+ no such panel. The panel shows this tab's sample count plus workspace aggregates
43
+ and redacted reset reasons/source links. Discard starts this page fresh while other
44
+ live tabs keep their state. For live editing, use the watch launcher:
45
+
46
+ ```sh
47
+ uv run --project . pysx --app examples.development:example --watch
48
+ ```
49
+
50
+ Start at 0, increment to 1, edit the title in `examples/development.py` and save:
51
+ the count stays 1. Increment to 2, then refresh: it returns to 0. A new tab also
52
+ starts at 0. Separately, changing `count = signal(0)` to `signal(1)` resets that
53
+ slot to 1; restore `signal(0)` afterward. The page includes these steps.
54
+ Add `--inspect` for the separate inspector's redacted events, source links and
55
+ diagnostics. A broken edit leaves the previous worker usable; repair it to resume
56
+ replacement. Ctrl+C stops the supervisor and its workers. Explicit persistence
57
+ is demonstrated separately by `durable_state`. See
58
+ [development hosting](../docs/DEVELOPMENT.md) for configuration and limits.
59
+
60
+ Choose another port with `--port` or `PSX_PORT`:
61
+
62
+ ```sh
63
+ uv run --project . pysx-example run counter --port 9100
64
+ PSX_PORT=9100 uv run --project . pysx-example run counter
65
+ ```
66
+
67
+ Press Ctrl+C to stop the server. To use `example` directly, activate the environment:
68
+
69
+ ```sh
70
+ source .venv/bin/activate
71
+ pysx-example run counter
72
+ ```
73
+
74
+ The command is installed into the environment's bin directory. A new terminal
75
+ needs activation again, or you can keep using the `uv run --project .` prefix.
76
+
77
+ ## Browser-held draft recovery
78
+
79
+ Add two revisions, then use **Reverse revisions** to move the same keyed rows.
80
+ **Check draft** completes asynchronously while your name and notes stay editable.
81
+
82
+ Run `uv run --project . pysx-example run browser_recovery`. This ordinary callable uses
83
+ shared controls and ordinary local signals, with no state codec, database, recovery
84
+ decorator or custom form wiring.
85
+
86
+ 1. Enter a name and notes, then add a revision.
87
+ 2. Keep the tab open, stop the server with Ctrl+C, and start the same command again.
88
+ 3. The open page restores its latest delivered fields/revision and reports recovery.
89
+ 4. Reload the page or open another tab to start a fresh draft.
90
+
91
+ The same workflow works with these normal host commands:
92
+
93
+ ```sh
94
+ uv run --project . pysx --app examples.browser_recovery:example
95
+ uv run --project . pysx --app examples.browser_recovery:example --asgi
96
+ uv run --project . pysx --app examples.browser_recovery:example --watch
97
+ ```
98
+
99
+ The unchanged counter also recovers through `pysx --app examples.counter:app`, with
100
+ `--asgi` or `--watch` when desired. Keep the framework's private application-state
101
+ directory across restarts. Compatible deployments on other hosts need the same
102
+ [hosting key ring and namespace](../docs/HOSTING.md), together with current authentication.
103
+ Capsules last 24 hours and stay only in the open document's memory. Unsupported or
104
+ changed state can reset; undelivered edits and uncertain actions are not replayed.
105
+ Use normal business persistence for saved account data and cross-device access.
106
+
107
+ ## Durable state
108
+
109
+ Run `uv run --project . pysx-example run durable_state`. Edit the saved draft and increment
110
+ the counter, then keep the page open while stopping and restarting the server with
111
+ the same command. The browser restores committed count/draft state and reports that
112
+ recovery; the setup marker changes and a new resource loads. Another tab and a page
113
+ reload start independent sessions. Healthy socket reconnect retains the live graph.
114
+ Unsent edits are discarded on reconstruction, rather than replayed.
115
+
116
+ The same restart test works with `uv run --project . pysx --app
117
+ examples.durable_state:example --watch`: keep the tab open, press Ctrl+C, and restart
118
+ that command. The open page reconnects and recovers its count/draft without
119
+ navigating. Refreshing it still starts a new session.
120
+
121
+ The default SQLite file is `pysx-durable-example.sqlite3` in the OS temporary directory.
122
+ It survives process restart until removed by local cleanup. For an explicit local
123
+ file, set `PYSX_STATE_PATH` to an existing directory's file path before launching.
124
+ For concurrent workers on a local filesystem, opt into FULL-sync WAL with
125
+ `PYSX_STATE_WAL=1 uv run --project . pysx-example run durable_state`. This requires
126
+ SQLite 3.51.3+ or a [fixed backport](../docs/OWNERSHIP.md); the example closes its
127
+ retained connection during host shutdown. The default keeps scoped connections.
128
+ Only the typed count/draft root is persisted; Python handlers, effects and async
129
+ resources are rebuilt from code. This example uses a local demonstration principal,
130
+ not production authentication.
131
+
132
+ For owner routing across two workers, use the [Linux deployment runbook](../docs/DEPLOYMENT.md)
133
+ with `PYSX_APP=examples.durable_state:example`. The example reads `PYSX_STATE_PATH`,
134
+ `PYSX_WORKER` and `PYSX_ORIGIN` supplied by that topology. Supply disposable TLS
135
+ material for local testing; production applications must replace the demonstration
136
+ identity with their own authentication and authorization. The two workers share
137
+ declared snapshots and atomic leases, while each live graph has one owner.
138
+
139
+ ## Bounded reconnect
140
+
141
+ Run `uv run --project . pysx-example run reconnect`. Type a draft, increment the counter,
142
+ then interrupt the connection — switch the network off, suspend the machine, or close
143
+ the socket from the browser console. The page shows a disconnected status and refuses
144
+ clicks and edits instead of queuing them. It retries on its own with capped backoff and
145
+ offers an explicit retry once the attempts are exhausted. On reconnect the session
146
+ number, your draft, the click count and keyboard focus are unchanged, and the server
147
+ clock jumps straight to its current total rather than replaying the ticks you missed.
148
+ Leave the page away long enough for the session to expire and recovery visibly starts a
149
+ fresh session with a new session number. A second tab keeps its own session throughout.
150
+ No network, database or extra host setup is required. See
151
+ [the protocol](../docs/PROTOCOL.md) and [hosting](../docs/HOSTING.md) for the wire
152
+ version and the configurable transport limits.
153
+
154
+ ## Async resources
155
+
156
+ Run `uv run --project . pysx-example run resources`. The first HTTP response already
157
+ contains the styled page, draft and pending state. Wait without clicking: a message
158
+ loads, the delivered count increases through server push, and a timer posts a server
159
+ notice. **Reload message** starts another load; **Cancel load** stops pending work.
160
+ The setup counter stays at one when the browser attaches. Your draft survives
161
+ background updates; a second tab has its own draft and resource. Ctrl+C disposes
162
+ sessions and their work. The loader is local and needs no network/database or extra
163
+ host setup. Optional ASGI launch:
164
+ `uv run --project . pysx --app examples.resources:example --asgi` (Uvicorn is in the
165
+ checkout dev environment; installed users need `pysx-framework[asgi]`). See
166
+ [resources](../docs/RESOURCES.md) and [hosting](../docs/HOSTING.md).
167
+
168
+ ## Keyed rows
169
+
170
+ Run `uv run --project . pysx-example run keyed_rows`. Type a private draft, increase its
171
+ count, then reverse the rows or rename Alpha: both edits stay with that row.
172
+ **Focus draft and reverse** focuses its input using an owned ref before moving the
173
+ row; keyboard focus and caret stay in the field. **Add and reverse children** moves
174
+ the existing nested choices and adds one. Picking a child after renaming Alpha uses
175
+ the current label. **Remove Alpha** removes both its card and draft preview;
176
+ **Restore Alpha** starts fresh local state and refs. Each browser session owns its
177
+ drafts, counters, choices and rows. No extra host setup is needed.
178
+
179
+ ## Keyboard and focus
180
+
181
+ Run `uv run --project . pysx-example run events`. In the color chooser, Up/Down opens
182
+ the list and changes its active option; Enter selects it and Escape closes it.
183
+ Click an option to select it while keeping input focus. The event status shows
184
+ the key and modifier snapshot received by the server. Tab reaches the shortcut
185
+ row; Left/Right moves its single tab stop and browser focus. **Focus color chooser**
186
+ uses an owned asynchronous focus command. The chooser exposes expanded,
187
+ active-descendant and selected ARIA state. Each session has independent state.
188
+
189
+ Default cancellation happens immediately in the browser. Python callbacks receive
190
+ typed `BrowserEvent` snapshots through `on_event`; async callbacks await owned
191
+ `DomRef.handle()` commands. See [the protocol](../docs/PROTOCOL.md) for ownership,
192
+ imperative zones, supported operations and limits.
193
+
194
+ ## Templates
195
+
196
+ Run `uv run --project . pysx-example run diagnostics` for the static diagnostic workflow.
197
+ **Apply suggested fix** changes the frozen `count()` sample into an uncalled Signal
198
+ reference and clears its finding. **Show attribute warning** demonstrates an advisory
199
+ schema finding without a guessed fix; **Reset sample** restores the first case.
200
+ The live counter remains interactive, and each tab owns its state. Source samples
201
+ are displayed and analyzed as text, never executed. No additional setup is needed.
202
+ See [diagnostic APIs, codes and edits](../docs/DIAGNOSTICS.md) for the matching
203
+ buffer/CLI workflow; checking `examples/diagnostics.py` itself reports `[]`.
204
+
205
+ Run `uv run --project . pysx-example run templates` for live template controls.
206
+ **Update heading** changes a live heading followed by two styled break siblings on
207
+ one row. The action is a later inline sibling, and each session owns its heading.
208
+ Reverse/add/remove groups, pick a nested row and switch through three branches.
209
+ Headings use `each_indexed` and ordinary Python assignments; nested `each` callbacks
210
+ retain their captured group after reorder. **Add child to first group** updates an
211
+ independent readable child source. `when` lazily builds the selected branch alongside
212
+ optional markup conditions and value matching. Local styled tags and cross-module
213
+ snapshot fragments use ordinary imports and bare component names. The initial
214
+ comprehension, iterable `each`, and bounded while snapshots stay fixed. Each browser
215
+ session owns its state.
216
+
217
+ ## Reactive state
218
+
219
+ The `reactive_state` app demonstrates nested writable projections, multiplication
220
+ and batched computations. **Advance twice** shows a branch controlled by numeric
221
+ ordering and membership. **Increment first** updates the status and hides that
222
+ branch. **Edit a private snapshot** leaves the display unchanged, while **Rotate
223
+ list** changes the first positional projection. Collection length stays live.
224
+
225
+ The boolean helper card displays `all_of`, `any_of` and `not_`. Advance twice,
226
+ increment the first score, then **Reset count** to see each helper react to its inputs.
227
+ Each browser session owns its state.
228
+
229
+ ## Operators
230
+
231
+ The `operators` app groups supported operations by Python's return-value rules:
232
+ arithmetic, indexing and ordering use operator overloads; `all_of`, `any_of`,
233
+ `not_`, `eq`, `ne`, `contains`, `length` and `concat` use named functions.
234
+ Use `derived(lambda: ...)` for other computations, including Python's
235
+ short-circuiting `and`, `or` and `not`. Signals have no `&`, `|` or `~` overloads.
236
+
237
+ ## Live forms
238
+
239
+ ```sh
240
+ uv run --project . pysx-example run forms
241
+ ```
242
+
243
+ Open http://127.0.0.1:8750. Edit the required text, notes, checkbox, radio group,
244
+ single select and multiple select; their live values update below the controls.
245
+ Use Command/Ctrl to select multiple options. Each browser connection has its own state.
246
+
247
+ **Apply server values** updates every control. **Reset to initial values** restores
248
+ the browser defaults and live state. Empty the required text to see native
249
+ validation block submission; fill it and **Submit** to inspect enabled field pairs
250
+ and the submitter. Disabled fields are omitted.
251
+
252
+ The uppercase field demonstrates server normalization while preserving the caret.
253
+ The optional field can be hidden and restored. Text composition commits only its
254
+ final value. Press Ctrl+C in the terminal to stop the server.
255
+
256
+ The [native element API](../docs/ELEMENTS.md) documents typed construction and bindings.
257
+ The [verification guide](../tests/VERIFICATION.md) lists reproducible browser setup.
258
+
259
+ ## Themes and CSS variables
260
+
261
+ ```sh
262
+ uv run --project . pysx-example run themes
263
+ ```
264
+
265
+ Open http://127.0.0.1:8750. Light/Dark switch this connection's named theme; Clear theme
266
+ resets its overrides. The preview inherits layout and borders through three styled levels.
267
+ Compact/Roomy change its padding, Local accent changes its border and user class, and
268
+ Reset variables removes both overrides to reveal inherited fallbacks. Open a second tab
269
+ to verify it stays independent. Controls support keyboard activation and narrow screens.
270
+ Press Ctrl+C to stop the server. See [styling](../docs/STYLING.md) for the public API.
271
+
272
+ ## Callable components and lifetimes
273
+
274
+ ```sh
275
+ uv run --project . pysx-example run composition
276
+ ```
277
+
278
+ Open http://127.0.0.1:8750. The panel is an inherited styled callable that receives
279
+ caller-owned children. The tree recursively dispatches plain callable components.
280
+ Click a node or press Tab to focus the tree before using the keyboard.
281
+ Right opens a branch; Left closes it or moves to its parent. Up/Down moves through
282
+ visible nodes, Home/End reaches their endpoints, and Enter increments a node's local
283
+ activation count. Reverse roots preserves surviving state and DOM identity. Collapsing
284
+ a branch cleans its descendants; remounting starts their local activation counts fresh.
285
+ The counters distinguish server setup, acknowledged browser mount and cleanup.
286
+ Open a second tab to check session isolation. Press Ctrl+C to stop the server.
287
+ See [component ownership](../docs/GRAMMAR.md#component-ownership) for state and resource hooks.
288
+
289
+ ## Local recovery
290
+
291
+ Run `uv run --project . pysx-example run recovery`. Increment the panel, enter text in
292
+ the healthy sibling, and click "Fail this panel". Only that panel shows fallback;
293
+ the committed failed-attempt count increases. The sibling's text and counter remain
294
+ usable. "Retry panel" recreates the failed panel with its count at zero. Each
295
+ failure disposes one panel, and repeated retry does not duplicate healthy handlers.
296
+ Another tab starts with its own state. No extra host configuration is required.
297
+ See [recovery behavior and limits](../docs/RECOVERY.md) for ownership, safe errors,
298
+ development tracebacks and the background async host seam.
299
+
300
+ Todos explains empty filters and returns rows with their original todo keys when
301
+ you select a populated filter again. Retained rows keep their DOM identity; rows
302
+ removed by an empty filter receive fresh handlers when they return.
303
+
304
+ ## Routing and navigation
305
+
306
+ Run `uv run --project . pysx-example run navigation`. Open User 1 and increment its
307
+ counter, then switch to User 2: the route keeps its local count as the parameter
308
+ changes. Activity (`./activity`) descends into a nested route; Parent (`../`) returns
309
+ to its user, and User 3 (`../3`) selects a sibling. Plain child names also append to
310
+ the current pathname; `/` selects the root and `../../` climbs two levels. Files
311
+ demonstrates a wildcard.
312
+
313
+ Replace query changes the query without adding a history entry. Jump to details
314
+ scrolls and focuses the user section; browser Back and Forward restore known scroll
315
+ positions and focus. Cancel navigation leaves the current URL and view open. Another
316
+ tab starts its own session. The standalone example opens at `/`; HTTP deep-link
317
+ rendering is described by the [routing host contract](../docs/ROUTING.md).
318
+
319
+ Require sign-in changes a signal. An `effect()` observes that state and redirects to
320
+ `/login` with `router.navigate(..., replace=True)`, keeping history length unchanged.
321
+ Its path guard prevents repeated redirects. Continue clears the requirement and
322
+ navigates home; another tab keeps its own state. The effect is disposed with the app.
323
+
324
+ ## Session security
325
+
326
+ Run `uv run --project . pysx-example run session_security`. The Writer identity can
327
+ increment its counter. Choose Read-only and Attempt write: the page shows a denial
328
+ and the counter stays zero. Both identities can edit their private drafts. Open
329
+ another tab, including another Writer tab, to see independent drafts and counters.
330
+ Choosing an identity opens a fresh principal-bound session.
331
+
332
+ These are explicitly local fixtures selected by URL, with no credentials or
333
+ external setup. The default loopback policy confines the demonstration. Production
334
+ applications must supply real authentication and live authorization through the
335
+ [host integration](../docs/HOSTING.md) and [security contract](../docs/SECURITY.md).
336
+ The example uses shipped Application/HostConfig/SecurityPolicy/require_access APIs
337
+ and shared components.
338
+
339
+ ## Adding an example
340
+
341
+ Add `examples/<name>.py` with an `app` attribute; the registry discovers it
342
+ automatically, making `example run <name>` available.
343
+
344
+ Use shared layouts and controls from `examples/components/`, such as `Page`,
345
+ `Action` and `Title`. `page(t"""...""")` adds page-specific style overrides.
346
+ See [the shared theme](components/THEME.md) for palette, spacing and responsive layout.
examples/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """Example registry.
2
+
3
+ Values are complete server app specs, so a caller can pass one straight to
4
+ `python -m pysx.hosting.server --app`.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ _EXAMPLES_DIR = Path(__file__).resolve().parent
10
+
11
+ examples: dict[str, str] = {
12
+ path.stem: f"examples.{path.stem}:example"
13
+ for path in sorted(_EXAMPLES_DIR.glob("*.py"))
14
+ if path.name != "__init__.py"
15
+ }
16
+
17
+ __all__ = ["examples"]
examples/attributes.py ADDED
@@ -0,0 +1,89 @@
1
+ """Native booleans, advisory attribute schemas and independently reactive keyed rows."""
2
+
3
+ from pysx import Fragment, derived, each, native, pysx, signal
4
+
5
+ from .components import Action, Card, Description, Page, Title, example_app, example_title
6
+
7
+
8
+ def app() -> Fragment:
9
+ example_title("Attributes")
10
+ enabled = signal(False)
11
+ tone = signal("calm")
12
+ rows = signal([1, 2])
13
+ counts = {key: signal(0) for key in rows()}
14
+ picked = signal("None")
15
+ submits = signal(0)
16
+ custom = native.Div(
17
+ "Inspect the explicit custom attributes",
18
+ id="attribute-raw",
19
+ custom_attrs={"x-state": "ready", "x-tone": tone, "hx-get": "/api", "hx-swap": "outerHTML"},
20
+ )
21
+
22
+ def toggle(_: object) -> None:
23
+ enabled.set(not enabled())
24
+
25
+ def retone(_: object) -> None:
26
+ tone.set("bright" if tone() == "calm" else "calm")
27
+
28
+ def reverse(_: object) -> None:
29
+ rows.set(list(reversed(rows())))
30
+
31
+ def row(key: int) -> Fragment:
32
+ count = counts[key]
33
+ label = derived(lambda: f"Row {key}: {count()}")
34
+
35
+ def increment(_: object) -> None:
36
+ count.update(lambda value: value + 1)
37
+
38
+ def choose(_: object) -> None:
39
+ picked.set(label())
40
+
41
+ return pysx(t"""
42
+ div(data-row={key} data-n={count} data-tone={tone}
43
+ class={derived(lambda: "changed" if count() else "original")})
44
+ span(data-inner={count}): {label}
45
+ Action(data-increment={key} onClick={increment}): "Increment row"
46
+ Action(data-choose={key} disabled={enabled} onClick={choose}): "Choose row"
47
+ """)
48
+
49
+ def submit(_: object) -> None:
50
+ submits.update(lambda value: value + 1)
51
+
52
+ return pysx(t"""
53
+ Page(id="attributes-example")
54
+ Title: "Attributes you can inspect"
55
+ Description:
56
+ "Toggle native presence, inspect ARIA strings, then reorder independent rows."
57
+ Card:
58
+ Action(id="toggle-attrs" onClick={toggle} ariaExpanded={enabled}
59
+ ariaControls="attribute-details"): "Toggle attributes"
60
+ label(htmlFor="attribute-field"): "A typed native field"
61
+ input(id="attribute-field" type="text" placeholder="Type here" autoComplete="off"
62
+ maxLength={40} tabIndex={0} readOnly={enabled} required={enabled}
63
+ autoFocus={False})
64
+ input(id="attribute-autofocus" type="text" autoFocus={True}
65
+ ariaLabel="Initially focused field")
66
+ details(id="attribute-details" open={enabled})
67
+ summary: "Native details"
68
+ p: "Open follows the toggle."
69
+ form(id="attribute-form" noValidate={False} onSubmit={submit})
70
+ label(for="required-field"): "Required field"
71
+ input(id="required-field" required={enabled})
72
+ Action(id="attribute-submit" type="submit"): "Submit"
73
+ p(id="attribute-submits"): {submits}
74
+ video(id="attribute-video" controls={enabled} autoPlay={False} loop={enabled}
75
+ muted={enabled} playsInline={enabled})
76
+ input(id="attribute-check" type="checkbox" checked={enabled} disabled={enabled})
77
+ div(id="attribute-panel" hidden={enabled} ariaHidden={enabled}): "Visible panel"
78
+ my-widget(id="attribute-custom" dataTone={tone}
79
+ dataRevealDelay="200" ariaLabel="Custom attribute example")
80
+ {custom}
81
+ Card:
82
+ Action(id="retone" onClick={retone}): "Change independent tone"
83
+ Action(id="reverse-attrs" onClick={reverse}): "Reverse rows"
84
+ {each(rows, row, key=lambda key: key)}
85
+ p(id="attribute-picked"): {picked}
86
+ """)
87
+
88
+
89
+ example = example_app(app)
@@ -0,0 +1,67 @@
1
+ """An ordinary draft form that stays with its open browser document."""
2
+
3
+ import asyncio
4
+
5
+ from pysx import Fragment, each, pysx, signal
6
+
7
+ from .components import (
8
+ Action,
9
+ Card,
10
+ Description,
11
+ Eyebrow,
12
+ Field,
13
+ Meta,
14
+ Page,
15
+ Title,
16
+ example_app,
17
+ example_title,
18
+ )
19
+
20
+
21
+ def app() -> Fragment:
22
+ example_title("Browser recovery")
23
+ name = signal("")
24
+ notes = signal("")
25
+ revisions = signal(0)
26
+ history = signal(list[int]())
27
+ status = signal("Ready")
28
+ field_css = "\n".join(Field.declarations)
29
+
30
+ def revise(_event: object) -> None:
31
+ revisions.set(revisions() + 1)
32
+ history.set([*history(), revisions()])
33
+
34
+ def reverse(_event: object) -> None:
35
+ history.set(list(reversed(history())))
36
+
37
+ async def check(_event: object) -> None:
38
+ status.set("Checking")
39
+ await asyncio.sleep(0.1)
40
+ status.set("Checked")
41
+
42
+ def revision(value: int) -> Fragment:
43
+ return pysx(t"li(data-revision={value}): {value}")
44
+
45
+ return pysx(t"""
46
+ Page(id="browser-recovery"):
47
+ header:
48
+ Eyebrow: "pysx / examples"
49
+ Title: "A draft that stays with this tab"
50
+ Description: "Restart the server with this tab open. Reloading starts fresh."
51
+ Card:
52
+ label(htmlFor="recovery-name"): "Name"
53
+ Field(id="recovery-name" name="name" bindValue={name} autocomplete="off")
54
+ label(htmlFor="recovery-notes"): "Notes"
55
+ textarea(id="recovery-notes" bindValue={notes} rows="4" css={field_css})
56
+ Action(id="add-revision" type="button" onClick={revise}): "Add a revision"
57
+ Meta:
58
+ "Revision: "
59
+ span(id="recovery-revision"): {revisions}
60
+ Action(id="reverse-revisions" type="button" onClick={reverse}): "Reverse revisions"
61
+ ol(id="recovery-history"): {each(history, revision, key=lambda value: value)}
62
+ Action(id="check-draft" type="button" onClick={check}): "Check draft"
63
+ Meta(id="draft-status" ariaLive="polite"): {status}
64
+ """)
65
+
66
+
67
+ example = example_app(app)
@@ -0,0 +1,25 @@
1
+ # Example theme
2
+
3
+ The examples share a quiet light theme: a cool off-white canvas, white cards, slate
4
+ text, and a violet accent. `theme.py` owns the CSS tokens and document defaults;
5
+ `page.py` and `controls.py` own reusable layouts and controls.
6
+
7
+ - Center the main card in the viewport, with a maximum width of 680px. Longer pages
8
+ grow naturally and scroll. Keep 16px outside the card on narrow screens.
9
+ - Use `Page` for the main card and `Card` for related content inside it. Borders stay
10
+ subtle; shadows belong to the main card. Use `page(...)` for width/layout overrides.
11
+ - Group headings with `Eyebrow`, `Title`, and `Description`. Keep body copy muted,
12
+ short, and readable. Use `Metric` and `Value` for prominent numeric readouts, and
13
+ `Reading` with `Result` for dense reference rows where many values share a card.
14
+ - Use `CompactPage` for focused forms. `Form`, `Field`, `Submit`, `Filters`, and
15
+ `FilterButton` share control sizing and colors. `List`, `Item`, `Row`, `Checkbox`,
16
+ and `Text` provide checkable rows; `Remove` and `Clear` provide destructive actions.
17
+ - Group buttons in wrapping `Actions` rows. `Action` is the primary control; secondary
18
+ controls use light surfaces and borders. Keep interactive targets at least 44px tall.
19
+ - Use the shared `--ink`, `--muted`, `--border`, `--accent`, `--accent-soft`, and
20
+ `--danger` variables in example-specific styles. Avoid separate page palettes.
21
+ - Preserve visible keyboard focus, reduced-motion preferences, and mobile layouts.
22
+ Keep signals inside ordinary flow elements so flex/grid does not split inline text.
23
+
24
+ Verify changes with the example runner and the existing browser acceptance suite.
25
+ Check desktop and narrow viewports, live updates, and keyboard focus.
@@ -0,0 +1,51 @@
1
+ """Shared layout, controls and headings for runnable examples."""
2
+
3
+ from .controls import (
4
+ Action,
5
+ Actions,
6
+ Card,
7
+ Description,
8
+ Eyebrow,
9
+ Metric,
10
+ Reading,
11
+ Result,
12
+ Title,
13
+ Value,
14
+ )
15
+ from .forms import Clear, Field, FilterButton, Filters, Form, Meta, Remove, SecondaryAction, Submit
16
+ from .hosting import EXAMPLE_ASSETS, example_app, example_title
17
+ from .lists import Checkbox, Item, List, Row, Text
18
+ from .page import CompactPage, Page, page
19
+
20
+ __all__ = [
21
+ "EXAMPLE_ASSETS",
22
+ "Action",
23
+ "Actions",
24
+ "Card",
25
+ "Checkbox",
26
+ "Clear",
27
+ "CompactPage",
28
+ "Description",
29
+ "Eyebrow",
30
+ "Field",
31
+ "FilterButton",
32
+ "Filters",
33
+ "Form",
34
+ "Item",
35
+ "List",
36
+ "Meta",
37
+ "Metric",
38
+ "Page",
39
+ "Reading",
40
+ "Remove",
41
+ "Result",
42
+ "Row",
43
+ "SecondaryAction",
44
+ "Submit",
45
+ "Text",
46
+ "Title",
47
+ "Value",
48
+ "example_app",
49
+ "example_title",
50
+ "page",
51
+ ]