millwork 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.
- millwork/__init__.py +45 -0
- millwork/_bundle/dist/.vite/manifest.json +92 -0
- millwork/_bundle/dist/charts-inner-Cq0vWbjt.js +39 -0
- millwork/_bundle/dist/favicon.svg +15 -0
- millwork/_bundle/dist/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- millwork/_bundle/dist/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- millwork/_bundle/dist/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- millwork/_bundle/dist/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- millwork/_bundle/dist/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- millwork/_bundle/dist/icons.svg +24 -0
- millwork/_bundle/dist/index.global-cOFus1M1.js +952 -0
- millwork/_bundle/dist/index.modern-CITEfQrV.js +135 -0
- millwork/_bundle/dist/lib-3DI92Zxk.js +1 -0
- millwork/_bundle/dist/main-BoZpFGOH.js +64 -0
- millwork/_bundle/dist/main-F2tEXmC5.css +2 -0
- millwork/_bundle/dist/react-markdown-DLcbNoI3.js +14 -0
- millwork/_bundle/dist/remark-gfm-DoHjwpPg.js +16 -0
- millwork/app.py +726 -0
- millwork/auth.py +877 -0
- millwork/chat.py +397 -0
- millwork/cli.py +316 -0
- millwork/context.py +42 -0
- millwork/files.py +450 -0
- millwork/forms.py +349 -0
- millwork/manual/__init__.py +449 -0
- millwork/manual/components/Hello.jsx +24 -0
- millwork/manual/components/Voice.jsx +46 -0
- millwork/manual/pages/app/auth.md +188 -0
- millwork/manual/pages/app/pages.md +200 -0
- millwork/manual/pages/app/running.md +242 -0
- millwork/manual/pages/app/state.md +151 -0
- millwork/manual/pages/app/testing.md +147 -0
- millwork/manual/pages/app/theming.md +122 -0
- millwork/manual/pages/components/charts.md +263 -0
- millwork/manual/pages/components/chat.md +280 -0
- millwork/manual/pages/components/custom.md +148 -0
- millwork/manual/pages/components/data.md +112 -0
- millwork/manual/pages/components/feedback.md +212 -0
- millwork/manual/pages/components/files.md +194 -0
- millwork/manual/pages/components/form.md +135 -0
- millwork/manual/pages/components/inputs.md +360 -0
- millwork/manual/pages/components/layout.md +154 -0
- millwork/manual/pages/components/tasks.md +140 -0
- millwork/manual/pages/components/text.md +145 -0
- millwork/manual/pages/index.md +67 -0
- millwork/manual/public/fonts/InterVariable.woff2 +0 -0
- millwork/manual/public/fonts/LICENSE.txt +2 -0
- millwork/manual/public/mark.svg +15 -0
- millwork/manual/public/theme.json +97 -0
- millwork/nodes.py +300 -0
- millwork/protocol.py +238 -0
- millwork/proxy.py +122 -0
- millwork/py.typed +0 -0
- millwork/session.py +1422 -0
- millwork/shell.py +226 -0
- millwork/tasks.py +473 -0
- millwork/testing.py +336 -0
- millwork/theme.py +338 -0
- millwork/ui/__init__.py +725 -0
- millwork/ui/charts.py +214 -0
- millwork/ui/chat.py +97 -0
- millwork/ui/files.py +101 -0
- millwork/ui/form.py +102 -0
- millwork/ui/table.py +145 -0
- millwork/ws.py +220 -0
- millwork-0.1.0.dist-info/METADATA +131 -0
- millwork-0.1.0.dist-info/RECORD +70 -0
- millwork-0.1.0.dist-info/WHEEL +4 -0
- millwork-0.1.0.dist-info/entry_points.txt +2 -0
- millwork-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inputs
|
|
3
|
+
order: 2
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Inputs
|
|
7
|
+
|
|
8
|
+
Controls the user types into or picks from. Each one tells Python what
|
|
9
|
+
happened through an `on_change` handler, and most can be owned by either side.
|
|
10
|
+
|
|
11
|
+
## Who owns the value
|
|
12
|
+
|
|
13
|
+
A control with `default=` keeps its own value in the browser and reports
|
|
14
|
+
changes; a control with `value=` shows what Python says and only changes when
|
|
15
|
+
Python changes it. Give one or the other, never both. Pass `value` when the
|
|
16
|
+
page needs to read or reset the control, `default` when it does not.
|
|
17
|
+
|
|
18
|
+
```python live
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Owner(BaseModel):
|
|
23
|
+
text: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def example(s):
|
|
27
|
+
st = state(Owner)
|
|
28
|
+
return ui.Stack(
|
|
29
|
+
ui.Input(
|
|
30
|
+
value=st.text,
|
|
31
|
+
placeholder="Python owns this one",
|
|
32
|
+
on_change=lambda s, v: setattr(st, "text", v),
|
|
33
|
+
),
|
|
34
|
+
ui.Row(
|
|
35
|
+
ui.Text(f"Python sees: {st.text!r}", variant="muted"),
|
|
36
|
+
ui.Button(
|
|
37
|
+
"Clear from Python",
|
|
38
|
+
size="sm",
|
|
39
|
+
variant="outline",
|
|
40
|
+
on_click=lambda s, _: setattr(st, "text", ""),
|
|
41
|
+
),
|
|
42
|
+
gap=3,
|
|
43
|
+
),
|
|
44
|
+
ui.Input(
|
|
45
|
+
default="The browser owns this one", on_change=lambda s, v: s.toast(f"Changed to {v!r}")
|
|
46
|
+
),
|
|
47
|
+
gap=3,
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Button
|
|
52
|
+
|
|
53
|
+
`ui.Button` takes a label, a `variant`, a `size`, an `icon` (before the label,
|
|
54
|
+
or after with `icon_position="end"`), `loading`, `disabled` and a `tooltip`.
|
|
55
|
+
`confirm="..."` asks before the handler runs. `form="key"` submits the Form
|
|
56
|
+
with that key from anywhere on the page.
|
|
57
|
+
|
|
58
|
+
```python live
|
|
59
|
+
ui.Stack(
|
|
60
|
+
ui.Row(
|
|
61
|
+
ui.Button("Primary"),
|
|
62
|
+
ui.Button("Secondary", variant="secondary"),
|
|
63
|
+
ui.Button("Outline", variant="outline"),
|
|
64
|
+
ui.Button("Ghost", variant="ghost"),
|
|
65
|
+
ui.Button("Link", variant="link"),
|
|
66
|
+
ui.Button("Destructive", variant="destructive"),
|
|
67
|
+
gap=2,
|
|
68
|
+
wrap=True,
|
|
69
|
+
),
|
|
70
|
+
ui.Row(
|
|
71
|
+
ui.Button("Small", size="sm", variant="outline"),
|
|
72
|
+
ui.Button("Medium", variant="outline"),
|
|
73
|
+
ui.Button("Large", size="lg", variant="outline"),
|
|
74
|
+
ui.Button("With icon", icon="download", variant="outline"),
|
|
75
|
+
ui.Button("Icon after", icon="arrow-right", icon_position="end", variant="outline"),
|
|
76
|
+
gap=2,
|
|
77
|
+
wrap=True,
|
|
78
|
+
),
|
|
79
|
+
ui.Row(
|
|
80
|
+
ui.Button("Loading", loading=True),
|
|
81
|
+
ui.Button("Disabled", disabled=True),
|
|
82
|
+
ui.Button("With tooltip", tooltip="Explains itself on hover", variant="outline"),
|
|
83
|
+
ui.Button(
|
|
84
|
+
"Delete",
|
|
85
|
+
variant="destructive",
|
|
86
|
+
confirm="Delete this for good?",
|
|
87
|
+
on_click=lambda s, _: s.toast("Deleted.", tone="destructive"),
|
|
88
|
+
),
|
|
89
|
+
gap=2,
|
|
90
|
+
wrap=True,
|
|
91
|
+
),
|
|
92
|
+
gap=3,
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## ButtonGroup
|
|
97
|
+
|
|
98
|
+
`ui.ButtonGroup` joins buttons into one control.
|
|
99
|
+
|
|
100
|
+
```python live
|
|
101
|
+
ui.ButtonGroup(
|
|
102
|
+
ui.Button("Day", variant="outline", on_click=lambda s, _: s.toast("Day")),
|
|
103
|
+
ui.Button("Week", variant="outline", on_click=lambda s, _: s.toast("Week")),
|
|
104
|
+
ui.Button("Month", variant="outline", on_click=lambda s, _: s.toast("Month")),
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Input
|
|
109
|
+
|
|
110
|
+
`ui.Input` is a single line of text. `kind` chooses the keyboard and
|
|
111
|
+
validation (`text`, `password`, `email`, `number`, `search`, `url`),
|
|
112
|
+
`prefix_icon` and `suffix` decorate it, `clearable` adds a clear button.
|
|
113
|
+
`on_change` fires after a pause in typing (`debounce_ms`, 250 by default);
|
|
114
|
+
`on_submit` fires on Enter.
|
|
115
|
+
|
|
116
|
+
```python live
|
|
117
|
+
ui.Stack(
|
|
118
|
+
ui.Input(
|
|
119
|
+
placeholder="Search",
|
|
120
|
+
prefix_icon="search",
|
|
121
|
+
kind="search",
|
|
122
|
+
clearable=True,
|
|
123
|
+
on_submit=lambda s, v: s.toast(f"Searching for {v!r}"),
|
|
124
|
+
),
|
|
125
|
+
ui.Input(placeholder="Distance", suffix="km", kind="number"),
|
|
126
|
+
ui.Input(placeholder="Password", kind="password"),
|
|
127
|
+
ui.Input(value="Read only", readonly=True),
|
|
128
|
+
ui.Input(value="Disabled", disabled=True),
|
|
129
|
+
gap=3,
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Textarea
|
|
134
|
+
|
|
135
|
+
`ui.Textarea` is several lines. `rows` sets the starting height, `autosize`
|
|
136
|
+
grows it with the text, `max_length` caps it. Ctrl+Enter submits.
|
|
137
|
+
|
|
138
|
+
```python live
|
|
139
|
+
ui.Textarea(
|
|
140
|
+
placeholder="Notes. Ctrl+Enter submits.",
|
|
141
|
+
rows=3,
|
|
142
|
+
autosize=True,
|
|
143
|
+
max_length=200,
|
|
144
|
+
on_submit=lambda s, v: s.toast(f"{len(v)} characters"),
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## NumberInput
|
|
149
|
+
|
|
150
|
+
`ui.NumberInput` takes a number within `min` and `max`, stepping by `step`,
|
|
151
|
+
with an optional `suffix`. It reports on blur and on Enter.
|
|
152
|
+
|
|
153
|
+
```python live
|
|
154
|
+
ui.Row(
|
|
155
|
+
ui.NumberInput(default=3, min=1, max=10, step=1, suffix="items"),
|
|
156
|
+
ui.NumberInput(default=0.5, min=0, max=1, step=0.1, placeholder="0 to 1"),
|
|
157
|
+
gap=4,
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Slider
|
|
162
|
+
|
|
163
|
+
`ui.Slider` picks a number by dragging. `marks` labels points under the
|
|
164
|
+
track; `show_value` prints the value beside it. It reports on release.
|
|
165
|
+
|
|
166
|
+
```python live
|
|
167
|
+
ui.Stack(
|
|
168
|
+
ui.Slider(default=40, min=0, max=100, step=10),
|
|
169
|
+
ui.Slider(default=0.3, min=0, max=1, step=0.1, marks={0: "cold", 0.5: "warm", 1: "hot"}),
|
|
170
|
+
gap=6,
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Switch and Checkbox
|
|
175
|
+
|
|
176
|
+
Both are a yes or no with a `label` and an optional `description`. Use
|
|
177
|
+
`checked` or `default_checked`.
|
|
178
|
+
|
|
179
|
+
```python live
|
|
180
|
+
ui.Stack(
|
|
181
|
+
ui.Switch(
|
|
182
|
+
label="Email me a summary", description="Once a day, at eight.", default_checked=True
|
|
183
|
+
),
|
|
184
|
+
ui.Checkbox(
|
|
185
|
+
label="I have read the terms", on_change=lambda s, v: s.toast("Read" if v else "Unread")
|
|
186
|
+
),
|
|
187
|
+
ui.Checkbox(label="Disabled", disabled=True),
|
|
188
|
+
gap=3,
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Select
|
|
193
|
+
|
|
194
|
+
`ui.Select` picks one from `options`. Options are strings, `(value, label)`
|
|
195
|
+
pairs, or `ui.Option` objects with a `description`, an `icon`, `disabled` and a
|
|
196
|
+
`group`. `clearable` allows no choice.
|
|
197
|
+
|
|
198
|
+
```python live
|
|
199
|
+
ui.Row(
|
|
200
|
+
ui.Select(options=["Fast", "Balanced", "Thorough"], default="Balanced"),
|
|
201
|
+
ui.Select(
|
|
202
|
+
options=[
|
|
203
|
+
ui.Option(value="nl", label="Dutch", group="Europe"),
|
|
204
|
+
ui.Option(value="de", label="German", group="Europe"),
|
|
205
|
+
ui.Option(value="ja", label="Japanese", group="Asia", disabled=True),
|
|
206
|
+
],
|
|
207
|
+
placeholder="Language",
|
|
208
|
+
clearable=True,
|
|
209
|
+
),
|
|
210
|
+
gap=4,
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Combobox
|
|
215
|
+
|
|
216
|
+
`ui.Combobox` is a Select with a search box. With `options` it filters in the
|
|
217
|
+
browser; with `on_search` Python answers each search over the session's
|
|
218
|
+
socket, without a render. `allow_custom=True` lets typed text that matches
|
|
219
|
+
nothing be the value.
|
|
220
|
+
|
|
221
|
+
```python live
|
|
222
|
+
PEOPLE = [("ada", "Ada Lovelace"), ("grace", "Grace Hopper"), ("edsger", "Edsger Dijkstra")]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def search(s, query: str):
|
|
226
|
+
return [p for p in PEOPLE if query.lower() in p[1].lower()]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def example(s):
|
|
230
|
+
return ui.Row(
|
|
231
|
+
ui.Combobox(options=PEOPLE, placeholder="Filters in the browser"),
|
|
232
|
+
ui.Combobox(on_search=search, placeholder="Asks Python as you type", clearable=True),
|
|
233
|
+
ui.Combobox(options=["red", "green"], allow_custom=True, placeholder="Or type your own"),
|
|
234
|
+
gap=4,
|
|
235
|
+
wrap=True,
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## MultiSelect
|
|
240
|
+
|
|
241
|
+
`ui.MultiSelect` picks several, shown as chips. `max` stops further picks.
|
|
242
|
+
|
|
243
|
+
```python live
|
|
244
|
+
ui.MultiSelect(
|
|
245
|
+
options=["Alpha", "Beta", "Gamma", "Delta"],
|
|
246
|
+
default=["Alpha"],
|
|
247
|
+
max=3,
|
|
248
|
+
placeholder="Pick up to three",
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The chips keep the order they were picked in, and that is the order Python
|
|
253
|
+
receives. `sortable=True` lets the order be changed afterwards: drag a chip
|
|
254
|
+
to its place, or focus it and press Alt with an arrow key. In a form, the
|
|
255
|
+
same hint on the field (`json_schema_extra={"sortable": True}`) does it.
|
|
256
|
+
|
|
257
|
+
```python live
|
|
258
|
+
from pydantic import BaseModel
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Pipeline(BaseModel):
|
|
262
|
+
steps: list[str] = ["Collect", "Clean", "Model"]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def reorder(s, steps):
|
|
266
|
+
state(Pipeline).steps = steps
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def example(s):
|
|
270
|
+
st = state(Pipeline)
|
|
271
|
+
return ui.Stack(
|
|
272
|
+
ui.MultiSelect(
|
|
273
|
+
options=["Collect", "Clean", "Model", "Report", "Share"],
|
|
274
|
+
value=st.steps,
|
|
275
|
+
sortable=True,
|
|
276
|
+
on_change=reorder,
|
|
277
|
+
),
|
|
278
|
+
ui.Text(" then ".join(st.steps) or "Nothing yet.", variant="muted"),
|
|
279
|
+
gap=2,
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## RadioGroup, CheckboxGroup and ToggleGroup
|
|
284
|
+
|
|
285
|
+
Three ways to choose from a short list: one with radios, several with
|
|
286
|
+
checkboxes, or either with pressed buttons (`multiple=True` for several).
|
|
287
|
+
|
|
288
|
+
```python live
|
|
289
|
+
ui.Stack(
|
|
290
|
+
ui.RadioGroup(options=["Small", "Medium", "Large"], default="Medium", orientation="horizontal"),
|
|
291
|
+
ui.CheckboxGroup(
|
|
292
|
+
options=[
|
|
293
|
+
ui.Option(value="a", label="Alerts", description="Only the important ones"),
|
|
294
|
+
ui.Option(value="d", label="Digest"),
|
|
295
|
+
],
|
|
296
|
+
default=["a"],
|
|
297
|
+
),
|
|
298
|
+
ui.ToggleGroup(
|
|
299
|
+
options=[("left", "Left"), ("center", "Center"), ("right", "Right")], default="left"
|
|
300
|
+
),
|
|
301
|
+
ui.ToggleGroup(
|
|
302
|
+
options=["Bold", "Italic", "Underline"], multiple=True, default=["Bold"], size="sm"
|
|
303
|
+
),
|
|
304
|
+
gap=4,
|
|
305
|
+
)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## DatePicker
|
|
309
|
+
|
|
310
|
+
`ui.DatePicker` picks a date from a calendar; `range=True` picks a span.
|
|
311
|
+
Values are ISO strings on the wire, `date` objects are accepted. `min` and
|
|
312
|
+
`max` bound the pickable days.
|
|
313
|
+
|
|
314
|
+
```python live
|
|
315
|
+
ui.Row(
|
|
316
|
+
ui.DatePicker(
|
|
317
|
+
placeholder="A day", clearable=True, on_change=lambda s, v: s.toast(f"Picked {v}")
|
|
318
|
+
),
|
|
319
|
+
ui.DatePicker(range=True, placeholder="A span", min="2026-01-01"),
|
|
320
|
+
gap=4,
|
|
321
|
+
wrap=True,
|
|
322
|
+
)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## DropdownMenu
|
|
326
|
+
|
|
327
|
+
`ui.DropdownMenu` opens a menu behind a `trigger` button. Items carry an
|
|
328
|
+
`icon`, `disabled`, `destructive` and `separator_after`; `on_select` receives
|
|
329
|
+
the chosen id.
|
|
330
|
+
|
|
331
|
+
```python live
|
|
332
|
+
ui.DropdownMenu(
|
|
333
|
+
trigger=ui.Button("Actions", icon="more-horizontal", variant="outline"),
|
|
334
|
+
items=[
|
|
335
|
+
ui.MenuItem(id="rename", label="Rename", icon="pencil"),
|
|
336
|
+
ui.MenuItem(id="copy", label="Duplicate", icon="copy", separator_after=True),
|
|
337
|
+
ui.MenuItem(id="delete", label="Delete", icon="trash-2", destructive=True),
|
|
338
|
+
],
|
|
339
|
+
on_select=lambda s, item: s.toast(f"Chose {item}"),
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Field
|
|
344
|
+
|
|
345
|
+
`ui.Field` puts a `label`, `help` and an `error` around one control, for a
|
|
346
|
+
form built by hand rather than from a model.
|
|
347
|
+
|
|
348
|
+
```python live
|
|
349
|
+
from pydantic import Field
|
|
350
|
+
|
|
351
|
+
ui.Grid(
|
|
352
|
+
ui.Field(ui.Input(placeholder="Ada"), label="Name", required=True, help="As on your badge."),
|
|
353
|
+
ui.Field(
|
|
354
|
+
ui.Input(placeholder="ada@example.com", kind="email"),
|
|
355
|
+
label="Email",
|
|
356
|
+
error="This address bounced.",
|
|
357
|
+
),
|
|
358
|
+
cols={"sm": 1, "md": 2},
|
|
359
|
+
)
|
|
360
|
+
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Layout
|
|
3
|
+
order: 0
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Layout
|
|
7
|
+
|
|
8
|
+
A page is a tree. Layout components arrange their children; they carry no
|
|
9
|
+
state and emit no events. Children are positional, so a page reads like the
|
|
10
|
+
screen it makes.
|
|
11
|
+
|
|
12
|
+
## Page
|
|
13
|
+
|
|
14
|
+
`ui.Page` is the root of every page. It is as high as the viewport, scrolls its
|
|
15
|
+
body under a fixed header, and lays itself out around a `ui.Sidebar` child when
|
|
16
|
+
there is one. Give it a `title` for the browser tab and a `header` for the app
|
|
17
|
+
bar; `padding=False` lets a child such as `ui.Chat` fill it edge to edge.
|
|
18
|
+
|
|
19
|
+
This manual is a `Page` with a `Sidebar`, so it is not shown inline.
|
|
20
|
+
|
|
21
|
+
## Stack and Row
|
|
22
|
+
|
|
23
|
+
`ui.Stack` lays children out vertically, `ui.Row` horizontally. Both take
|
|
24
|
+
`gap`, `align` and `justify`; a `Row` can `wrap`.
|
|
25
|
+
|
|
26
|
+
```python live
|
|
27
|
+
ui.Stack(
|
|
28
|
+
ui.Row(ui.Badge("one"), ui.Badge("two"), ui.Badge("three"), gap=2),
|
|
29
|
+
ui.Row(ui.Button("Left", variant="outline"), ui.Button("Right"), justify="between"),
|
|
30
|
+
ui.Row(ui.Text("Centred", variant="muted"), justify="center"),
|
|
31
|
+
gap=4,
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`divided=True` draws a rule between the children of a `Stack`.
|
|
36
|
+
|
|
37
|
+
```python live
|
|
38
|
+
ui.Stack(ui.Text("First"), ui.Text("Second"), ui.Text("Third"), divided=True, gap=2)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Grid
|
|
42
|
+
|
|
43
|
+
`ui.Grid` places children in columns. `cols` is a number, or a mapping with
|
|
44
|
+
`sm`, `md` and `lg` for the breakpoints at which the count changes.
|
|
45
|
+
|
|
46
|
+
```python live
|
|
47
|
+
ui.Grid(
|
|
48
|
+
ui.Stat(label="Sessions", value="1,204", hint="today"),
|
|
49
|
+
ui.Stat(label="Replies", value="3.8 s", hint="median", tone="success"),
|
|
50
|
+
ui.Stat(label="Errors", value="2", tone="destructive"),
|
|
51
|
+
cols={"sm": 1, "md": 3},
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Card
|
|
56
|
+
|
|
57
|
+
`ui.Card` frames content with an optional `title`, `description` and `footer`.
|
|
58
|
+
`tone` colours its border; `on_click` makes the whole card a target.
|
|
59
|
+
|
|
60
|
+
```python live
|
|
61
|
+
ui.Grid(
|
|
62
|
+
ui.Card(
|
|
63
|
+
ui.Text("The body of the card goes here, as children."),
|
|
64
|
+
title="A card",
|
|
65
|
+
description="With a description under the title.",
|
|
66
|
+
footer=ui.Button("Action", size="sm"),
|
|
67
|
+
),
|
|
68
|
+
ui.Card(
|
|
69
|
+
ui.Text("Clicking me toasts."),
|
|
70
|
+
title="Clickable",
|
|
71
|
+
tone="primary",
|
|
72
|
+
on_click=lambda s, _: s.toast("The card was clicked."),
|
|
73
|
+
),
|
|
74
|
+
cols={"sm": 1, "md": 2},
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Separator
|
|
79
|
+
|
|
80
|
+
A rule between things, with an optional `label`.
|
|
81
|
+
|
|
82
|
+
```python live
|
|
83
|
+
ui.Stack(
|
|
84
|
+
ui.Text("Above"),
|
|
85
|
+
ui.Separator(),
|
|
86
|
+
ui.Text("Between, with a label below"),
|
|
87
|
+
ui.Separator(label="or"),
|
|
88
|
+
ui.Text("Below"),
|
|
89
|
+
gap=3,
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Split
|
|
94
|
+
|
|
95
|
+
`ui.Split` puts its children in resizable panes. `sizes` are the starting
|
|
96
|
+
percentages, `min_sizes` the smallest each may be dragged to, and
|
|
97
|
+
`collapsible` lets a pane fold away under its minimum. Give it a height with
|
|
98
|
+
`cls`, since it fills what it is given.
|
|
99
|
+
|
|
100
|
+
```python live
|
|
101
|
+
ui.Split(
|
|
102
|
+
ui.Card(ui.Text("Left pane. Drag the handle."), title="Left"),
|
|
103
|
+
ui.Card(ui.Text("Right pane."), title="Right"),
|
|
104
|
+
sizes=[40, 60],
|
|
105
|
+
min_sizes=[20, 20],
|
|
106
|
+
cls="h-48",
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Tabs
|
|
111
|
+
|
|
112
|
+
`ui.Tabs` holds `ui.Tab` children, each with an `id` and a `label`, and an
|
|
113
|
+
optional `icon` or `badge`. The chosen tab is the tab's business unless the
|
|
114
|
+
page passes `value`; `default` names the first one shown. Panels stay mounted
|
|
115
|
+
when hidden, so what the user typed in one survives a switch.
|
|
116
|
+
|
|
117
|
+
```python live
|
|
118
|
+
ui.Tabs(
|
|
119
|
+
ui.Tab(ui.Text("Overview text."), id="overview", label="Overview"),
|
|
120
|
+
ui.Tab(ui.Input(placeholder="Type, switch away and back"), id="edit", label="Edit", badge="1"),
|
|
121
|
+
ui.Tab(ui.Text("Settings text."), id="settings", label="Settings", icon="settings"),
|
|
122
|
+
default="overview",
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Accordion and Collapsible
|
|
127
|
+
|
|
128
|
+
`ui.Accordion` opens one `ui.AccordionItem` at a time, or several with
|
|
129
|
+
`multiple=True`. `ui.Collapsible` is a single section with a `title`.
|
|
130
|
+
|
|
131
|
+
```python live
|
|
132
|
+
ui.Stack(
|
|
133
|
+
ui.Accordion(
|
|
134
|
+
ui.AccordionItem(
|
|
135
|
+
ui.Text("What a session is, and what it holds."), id="s", title="Sessions"
|
|
136
|
+
),
|
|
137
|
+
ui.AccordionItem(ui.Text("How a handler changes state."), id="h", title="Handlers"),
|
|
138
|
+
default=["s"],
|
|
139
|
+
),
|
|
140
|
+
ui.Collapsible(ui.Text("Details that are usually folded away."), title="Advanced"),
|
|
141
|
+
gap=4,
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## ScrollArea
|
|
146
|
+
|
|
147
|
+
`ui.ScrollArea` scrolls its children inside a fixed `height`.
|
|
148
|
+
|
|
149
|
+
```python live
|
|
150
|
+
ui.ScrollArea(
|
|
151
|
+
ui.Stack(*(ui.Text(f"Line {i + 1}") for i in range(30)), gap=1),
|
|
152
|
+
height="8rem",
|
|
153
|
+
)
|
|
154
|
+
```
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tasks
|
|
3
|
+
order: 8
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tasks
|
|
7
|
+
|
|
8
|
+
Work that takes longer than a click goes in a task. The session owns it, the
|
|
9
|
+
page shows its progress, and the user can cancel it. A handler that awaits a
|
|
10
|
+
task releases the handler lock, so the page stays live meanwhile.
|
|
11
|
+
|
|
12
|
+
## Starting one
|
|
13
|
+
|
|
14
|
+
`s.task(work, *args, label=)` runs a coroutine or a plain function (a
|
|
15
|
+
function runs in a thread). It returns a `mw.Task` whose `done`, `status`,
|
|
16
|
+
`progress`, `steps` and `result` the page can read. `ui.Progress(task=)`
|
|
17
|
+
and `ui.Steps(task=)` draw it, and `task.cancel()` stops it.
|
|
18
|
+
|
|
19
|
+
```python live
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def crunch(n: int) -> str:
|
|
26
|
+
p = mw.progress()
|
|
27
|
+
p.plan(["Read", "Analyse", "Write"])
|
|
28
|
+
with p.step("Read"):
|
|
29
|
+
time.sleep(0.4)
|
|
30
|
+
with p.step("Analyse") as step:
|
|
31
|
+
for i in range(n):
|
|
32
|
+
p.check()
|
|
33
|
+
p.set((i + 1) / n, f"row {i + 1} of {n}")
|
|
34
|
+
time.sleep(0.15)
|
|
35
|
+
step.note(f"{n} rows")
|
|
36
|
+
with p.step("Write"):
|
|
37
|
+
time.sleep(0.3)
|
|
38
|
+
return f"{n} rows analysed"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Job(BaseModel):
|
|
42
|
+
run: object = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def example(s):
|
|
46
|
+
st = state(Job)
|
|
47
|
+
running = st.run is not None and not st.run.done
|
|
48
|
+
return ui.Stack(
|
|
49
|
+
ui.Row(
|
|
50
|
+
ui.Button(
|
|
51
|
+
"Start",
|
|
52
|
+
disabled=running,
|
|
53
|
+
on_click=lambda s, _: setattr(st, "run", s.task(crunch, 12, label="Crunching")),
|
|
54
|
+
),
|
|
55
|
+
ui.Button(
|
|
56
|
+
"Cancel",
|
|
57
|
+
variant="outline",
|
|
58
|
+
disabled=not running,
|
|
59
|
+
on_click=lambda s, _: st.run.cancel(),
|
|
60
|
+
),
|
|
61
|
+
gap=2,
|
|
62
|
+
),
|
|
63
|
+
ui.Progress(task=st.run) if st.run else ui.Text("No job yet.", variant="muted"),
|
|
64
|
+
ui.Steps(task=st.run) if st.run else None,
|
|
65
|
+
ui.Text(f"Result: {st.run.result}" if st.run and st.run.ok else "", variant="muted"),
|
|
66
|
+
gap=4,
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Reporting from the work
|
|
71
|
+
|
|
72
|
+
Inside the work, `mw.progress()` is the reporter. `set(value, message)`
|
|
73
|
+
moves the bar, `plan([...])` declares steps up front, `step(label)` opens
|
|
74
|
+
one as a context manager, `note(text)` leaves a short result on it,
|
|
75
|
+
`log(line)` appends output, and `check()` raises `mw.Cancelled` when the
|
|
76
|
+
user cancelled, so a loop stops at its next check.
|
|
77
|
+
|
|
78
|
+
## Waiting for a result
|
|
79
|
+
|
|
80
|
+
A handler can `await` the task to use its result; the lock is released while
|
|
81
|
+
it waits. `on_done=` names a handler to run when the task ends, with the
|
|
82
|
+
task as its argument. `timeout=` cancels a task that runs too long.
|
|
83
|
+
|
|
84
|
+
```python live
|
|
85
|
+
import asyncio
|
|
86
|
+
|
|
87
|
+
from pydantic import BaseModel
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def slow_sum(a: int, b: int) -> int:
|
|
91
|
+
await asyncio.sleep(0.8)
|
|
92
|
+
return a + b
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Sum(BaseModel):
|
|
96
|
+
text: str = "not computed"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def compute(s, _):
|
|
100
|
+
st = state(Sum)
|
|
101
|
+
st.text = "computing"
|
|
102
|
+
result = await s.task(slow_sum, 2, 3, label="Adding")
|
|
103
|
+
st.text = f"2 + 3 = {result}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def example(s):
|
|
107
|
+
return ui.Row(
|
|
108
|
+
ui.Button("Compute", on_click=compute),
|
|
109
|
+
ui.Text(state(Sum).text, variant="muted"),
|
|
110
|
+
gap=3,
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Later and threads
|
|
115
|
+
|
|
116
|
+
`s.later(seconds, work)` runs work after a delay; `s.thread(fn, *args)`
|
|
117
|
+
runs a blocking function in the thread pool and is what `s.task` does for a
|
|
118
|
+
plain function.
|
|
119
|
+
|
|
120
|
+
```python live
|
|
121
|
+
from pydantic import BaseModel
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Tick(BaseModel):
|
|
125
|
+
text: str = "waiting"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def example(s):
|
|
129
|
+
st = state(Tick)
|
|
130
|
+
|
|
131
|
+
def arm(s, _):
|
|
132
|
+
st.text = "armed"
|
|
133
|
+
s.later(1.5, lambda: setattr(st, "text", "fired"))
|
|
134
|
+
|
|
135
|
+
return ui.Row(
|
|
136
|
+
ui.Button("Arm a timer", variant="outline", on_click=arm),
|
|
137
|
+
ui.Text(st.text, variant="muted"),
|
|
138
|
+
gap=3,
|
|
139
|
+
)
|
|
140
|
+
```
|