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.
- examples/README.md +346 -0
- examples/__init__.py +17 -0
- examples/attributes.py +89 -0
- examples/browser_recovery.py +67 -0
- examples/components/THEME.md +25 -0
- examples/components/__init__.py +51 -0
- examples/components/composition.py +195 -0
- examples/components/controls.py +109 -0
- examples/components/development.js +45 -0
- examples/components/development.py +24 -0
- examples/components/diagnostics.py +17 -0
- examples/components/events.py +153 -0
- examples/components/favicon.png +0 -0
- examples/components/forms.py +91 -0
- examples/components/hosting.py +37 -0
- examples/components/lists.py +48 -0
- examples/components/navigation.py +24 -0
- examples/components/page.py +28 -0
- examples/components/templates.py +21 -0
- examples/components/theme.py +75 -0
- examples/components/themes.py +25 -0
- examples/composition.py +27 -0
- examples/counter.py +41 -0
- examples/development.py +68 -0
- examples/diagnostics.py +95 -0
- examples/durable_state.py +161 -0
- examples/editor_features.py +213 -0
- examples/effects.py +113 -0
- examples/events.py +36 -0
- examples/forms.py +143 -0
- examples/hello.py +17 -0
- examples/keyed_rows.py +120 -0
- examples/navigation.py +160 -0
- examples/operators.py +242 -0
- examples/reactive_state.py +124 -0
- examples/reconnect.py +85 -0
- examples/recovery.py +96 -0
- examples/resources.py +108 -0
- examples/session_security.py +105 -0
- examples/templates.py +150 -0
- examples/themes.py +109 -0
- examples/todos.py +195 -0
- pysx/__init__.py +221 -0
- pysx/compiler/__init__.py +1 -0
- pysx/compiler/bindings.py +90 -0
- pysx/compiler/compiler.py +938 -0
- pysx/compiler/imports.py +168 -0
- pysx/compiler/loader.py +197 -0
- pysx/compiler/parser.py +1051 -0
- pysx/compiler/runtime.py +93 -0
- pysx/compiler/scope.py +340 -0
- pysx/compiler/source_map.py +268 -0
- pysx/compiler/template.py +152 -0
- pysx/components/__init__.py +1 -0
- pysx/components/boundaries.py +55 -0
- pysx/components/checkpoints.py +588 -0
- pysx/components/composition.py +97 -0
- pysx/components/elements.py +136 -0
- pysx/components/errors.py +87 -0
- pysx/components/lifecycle.py +623 -0
- pysx/components/native.py +1269 -0
- pysx/components/native_support.py +71 -0
- pysx/components/schema.py +699 -0
- pysx/development/__init__.py +1 -0
- pysx/development/control.py +689 -0
- pysx/development/development.py +973 -0
- pysx/development/diagnostics.py +79 -0
- pysx/development/tracing.py +145 -0
- pysx/hosting/__init__.py +1 -0
- pysx/hosting/_http_connection.py +33 -0
- pysx/hosting/_uvicorn.py +52 -0
- pysx/hosting/asgi.py +309 -0
- pysx/hosting/browser_recovery.py +230 -0
- pysx/hosting/durable_host.py +427 -0
- pysx/hosting/hosting.py +899 -0
- pysx/hosting/http.py +507 -0
- pysx/hosting/recovery_keys.py +285 -0
- pysx/hosting/request.py +71 -0
- pysx/hosting/security.py +123 -0
- pysx/hosting/server.py +234 -0
- pysx/hosting/telemetry.py +22 -0
- pysx/playgrounds/__init__.py +5 -0
- pysx/playgrounds/capstone.py +224 -0
- pysx/playgrounds/counter.py +33 -0
- pysx/playgrounds/development.py +43 -0
- pysx/playgrounds/durable.py +98 -0
- pysx/playgrounds/recovery.py +50 -0
- pysx/py.typed +0 -0
- pysx/reactive/__init__.py +1 -0
- pysx/reactive/operators.py +235 -0
- pysx/reactive/reactive.py +580 -0
- pysx/reactive/structured.py +75 -0
- pysx/rendering/__init__.py +1 -0
- pysx/rendering/identity.py +25 -0
- pysx/rendering/output.py +172 -0
- pysx/rendering/render.py +2248 -0
- pysx/rendering/tree.py +287 -0
- pysx/runtime/__init__.py +1 -0
- pysx/runtime/async_runtime.py +169 -0
- pysx/runtime/capsule.py +191 -0
- pysx/runtime/connection.py +974 -0
- pysx/runtime/dom.py +418 -0
- pysx/runtime/durable.py +164 -0
- pysx/runtime/events.py +123 -0
- pysx/runtime/forms.py +165 -0
- pysx/runtime/ownership.py +343 -0
- pysx/runtime/persistence.py +426 -0
- pysx/runtime/resources.py +144 -0
- pysx/runtime/routing.py +404 -0
- pysx/runtime/session.py +576 -0
- pysx/runtime/transport.py +133 -0
- pysx/runtime/wire.py +424 -0
- pysx/static/client.js +1273 -0
- pysx/static/development.js +53 -0
- pysx/static/index.html +13 -0
- pysx/styling/__init__.py +1 -0
- pysx/styling/scoped_css.py +183 -0
- pysx/styling/styled.py +285 -0
- pysx/styling/styled_native.py +1583 -0
- pysx/styling/styles.py +200 -0
- pysx/tooling/__init__.py +1 -0
- pysx/tooling/analysis.py +226 -0
- pysx/tooling/build.py +117 -0
- pysx/tooling/check.py +1048 -0
- pysx/tooling/diagnostic_rules.py +435 -0
- pysx/tooling/editor/__init__.py +1 -0
- pysx/tooling/editor/config.py +286 -0
- pysx/tooling/editor/model.py +353 -0
- pysx/tooling/editor/ownership.py +196 -0
- pysx/tooling/editor/types.py +84 -0
- pysx/tooling/examples.py +69 -0
- pysx/tooling/lint.py +242 -0
- pysx/tooling/lsp/__init__.py +1 -0
- pysx/tooling/lsp/backend.py +363 -0
- pysx/tooling/lsp/edits.py +268 -0
- pysx/tooling/lsp/features.py +1041 -0
- pysx/tooling/lsp/lsp.py +28 -0
- pysx/tooling/lsp/server.py +1537 -0
- pysx/tooling/lsp/workspace.py +408 -0
- pysx/tooling/playground.py +62 -0
- pysx/tooling/quality.py +280 -0
- pysx/tooling/sources.py +31 -0
- pysx/tooling/tools.py +111 -0
- pysx_framework-0.1.0.dist-info/METADATA +103 -0
- pysx_framework-0.1.0.dist-info/RECORD +148 -0
- pysx_framework-0.1.0.dist-info/WHEEL +4 -0
- pysx_framework-0.1.0.dist-info/entry_points.txt +6 -0
- 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
|
+
]
|