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.
Files changed (70) hide show
  1. millwork/__init__.py +45 -0
  2. millwork/_bundle/dist/.vite/manifest.json +92 -0
  3. millwork/_bundle/dist/charts-inner-Cq0vWbjt.js +39 -0
  4. millwork/_bundle/dist/favicon.svg +15 -0
  5. millwork/_bundle/dist/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  6. millwork/_bundle/dist/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  7. millwork/_bundle/dist/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  8. millwork/_bundle/dist/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  9. millwork/_bundle/dist/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  10. millwork/_bundle/dist/icons.svg +24 -0
  11. millwork/_bundle/dist/index.global-cOFus1M1.js +952 -0
  12. millwork/_bundle/dist/index.modern-CITEfQrV.js +135 -0
  13. millwork/_bundle/dist/lib-3DI92Zxk.js +1 -0
  14. millwork/_bundle/dist/main-BoZpFGOH.js +64 -0
  15. millwork/_bundle/dist/main-F2tEXmC5.css +2 -0
  16. millwork/_bundle/dist/react-markdown-DLcbNoI3.js +14 -0
  17. millwork/_bundle/dist/remark-gfm-DoHjwpPg.js +16 -0
  18. millwork/app.py +726 -0
  19. millwork/auth.py +877 -0
  20. millwork/chat.py +397 -0
  21. millwork/cli.py +316 -0
  22. millwork/context.py +42 -0
  23. millwork/files.py +450 -0
  24. millwork/forms.py +349 -0
  25. millwork/manual/__init__.py +449 -0
  26. millwork/manual/components/Hello.jsx +24 -0
  27. millwork/manual/components/Voice.jsx +46 -0
  28. millwork/manual/pages/app/auth.md +188 -0
  29. millwork/manual/pages/app/pages.md +200 -0
  30. millwork/manual/pages/app/running.md +242 -0
  31. millwork/manual/pages/app/state.md +151 -0
  32. millwork/manual/pages/app/testing.md +147 -0
  33. millwork/manual/pages/app/theming.md +122 -0
  34. millwork/manual/pages/components/charts.md +263 -0
  35. millwork/manual/pages/components/chat.md +280 -0
  36. millwork/manual/pages/components/custom.md +148 -0
  37. millwork/manual/pages/components/data.md +112 -0
  38. millwork/manual/pages/components/feedback.md +212 -0
  39. millwork/manual/pages/components/files.md +194 -0
  40. millwork/manual/pages/components/form.md +135 -0
  41. millwork/manual/pages/components/inputs.md +360 -0
  42. millwork/manual/pages/components/layout.md +154 -0
  43. millwork/manual/pages/components/tasks.md +140 -0
  44. millwork/manual/pages/components/text.md +145 -0
  45. millwork/manual/pages/index.md +67 -0
  46. millwork/manual/public/fonts/InterVariable.woff2 +0 -0
  47. millwork/manual/public/fonts/LICENSE.txt +2 -0
  48. millwork/manual/public/mark.svg +15 -0
  49. millwork/manual/public/theme.json +97 -0
  50. millwork/nodes.py +300 -0
  51. millwork/protocol.py +238 -0
  52. millwork/proxy.py +122 -0
  53. millwork/py.typed +0 -0
  54. millwork/session.py +1422 -0
  55. millwork/shell.py +226 -0
  56. millwork/tasks.py +473 -0
  57. millwork/testing.py +336 -0
  58. millwork/theme.py +338 -0
  59. millwork/ui/__init__.py +725 -0
  60. millwork/ui/charts.py +214 -0
  61. millwork/ui/chat.py +97 -0
  62. millwork/ui/files.py +101 -0
  63. millwork/ui/form.py +102 -0
  64. millwork/ui/table.py +145 -0
  65. millwork/ws.py +220 -0
  66. millwork-0.1.0.dist-info/METADATA +131 -0
  67. millwork-0.1.0.dist-info/RECORD +70 -0
  68. millwork-0.1.0.dist-info/WHEEL +4 -0
  69. millwork-0.1.0.dist-info/entry_points.txt +2 -0
  70. millwork-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,212 @@
1
+ ---
2
+ title: Feedback
3
+ order: 3
4
+ ---
5
+
6
+ # Feedback
7
+
8
+ Telling the user what is going on: alerts, toasts, dialogs, progress and the
9
+ empty and loading states.
10
+
11
+ ## Alert
12
+
13
+ `ui.Alert` is a banner with a `title`, a `description`, a `tone`, an `icon`,
14
+ `actions` and, with `dismissable`, a close button that fires `on_dismiss`.
15
+
16
+ ```python live
17
+ ui.Stack(
18
+ ui.Alert(title="Heads up", description="Something to know, with the default tone."),
19
+ ui.Alert(title="Saved", description="The project was saved.", tone="success"),
20
+ ui.Alert(title="Check the dates", description="The end is before the start.", tone="warning"),
21
+ ui.Alert(
22
+ title="Upload failed",
23
+ description="The file was larger than 50 MB.",
24
+ tone="destructive",
25
+ actions=[ui.Button("Try again", size="sm", on_click=lambda s, _: s.toast("Retrying"))],
26
+ ),
27
+ ui.Alert(title="Dismiss me", dismissable=True, on_dismiss=lambda s, _: s.toast("Dismissed")),
28
+ gap=3,
29
+ )
30
+ ```
31
+
32
+ ## Toast
33
+
34
+ A toast is not a component but a command: `s.toast(message, tone=, title=)`
35
+ from any handler. `action=(label, handler)` puts a button on it that runs
36
+ the handler once.
37
+
38
+ ```python live
39
+ ui.Row(
40
+ ui.Button("Plain", variant="outline", on_click=lambda s, _: s.toast("A plain toast.")),
41
+ ui.Button(
42
+ "Success", variant="outline", on_click=lambda s, _: s.toast("Saved.", tone="success")
43
+ ),
44
+ ui.Button(
45
+ "With title",
46
+ variant="outline",
47
+ on_click=lambda s, _: s.toast("Three rows changed.", title="Imported"),
48
+ ),
49
+ ui.Button(
50
+ "With action",
51
+ variant="outline",
52
+ on_click=lambda s, _: s.toast(
53
+ "Deleted.", tone="destructive", action=("Undo", lambda s, _: s.toast("Restored."))
54
+ ),
55
+ ),
56
+ gap=2,
57
+ wrap=True,
58
+ )
59
+ ```
60
+
61
+ ## Dialog
62
+
63
+ `ui.Dialog` is open when state says so. It has a `title`, a `description`, a
64
+ `footer`, a `size` from `sm` to `full`, and `on_close` fires when the user
65
+ dismisses it, so the handler sets state and it closes.
66
+
67
+ ```python live
68
+ from pydantic import BaseModel
69
+
70
+
71
+ class Modal(BaseModel):
72
+ open: bool = False
73
+
74
+
75
+ def example(s):
76
+ st = state(Modal)
77
+
78
+ def close(s, _):
79
+ st.open = False
80
+
81
+ return ui.Row(
82
+ ui.Button("Open a dialog", on_click=lambda s, _: setattr(st, "open", True)),
83
+ ui.Dialog(
84
+ ui.Text("Dialogs hold any children. This one has a footer with a button."),
85
+ open=st.open,
86
+ title="A dialog",
87
+ description="Close it with the button, the cross, Escape or a click outside.",
88
+ footer=ui.Button("Done", on_click=close),
89
+ on_close=close,
90
+ ),
91
+ )
92
+ ```
93
+
94
+ ## Confirm and ask
95
+
96
+ Two session calls wait for the user from inside a handler. `await s.confirm(
97
+ title, description=, destructive=)` shows a dialog and returns True or
98
+ False. `await s.ask(component)` shows any component and returns what it
99
+ submits: a `Form` answers with its validated model, a closed dialog answers
100
+ None.
101
+
102
+ ```python live
103
+ from pydantic import BaseModel
104
+
105
+
106
+ class Asked(BaseModel):
107
+ answer: str = "nothing yet"
108
+
109
+
110
+ async def confirm(s, _):
111
+ st = state(Asked)
112
+ ok = await s.confirm("Archive the project?", "You can restore it later.", destructive=True)
113
+ st.answer = "archived" if ok else "kept"
114
+
115
+
116
+ def example(s):
117
+ st = state(Asked)
118
+ return ui.Row(
119
+ ui.Button("Archive", variant="outline", on_click=confirm),
120
+ ui.Text(f"Result: {st.answer}", variant="muted"),
121
+ gap=3,
122
+ )
123
+ ```
124
+
125
+ ## Sheet
126
+
127
+ `ui.Sheet` slides a panel in from a `side`. Like a dialog it is open when
128
+ state says so and `on_close` lets the handler close it.
129
+
130
+ ```python live
131
+ from pydantic import BaseModel
132
+
133
+
134
+ class Panel(BaseModel):
135
+ open: bool = False
136
+
137
+
138
+ def example(s):
139
+ st = state(Panel)
140
+ return ui.Row(
141
+ ui.Button(
142
+ "Open settings", variant="outline", on_click=lambda s, _: setattr(st, "open", True)
143
+ ),
144
+ ui.Sheet(
145
+ ui.Switch(label="Email summaries", default_checked=True),
146
+ ui.Select(options=["Fast", "Thorough"], default="Fast"),
147
+ open=st.open,
148
+ side="right",
149
+ title="Settings",
150
+ description="A sheet holding a few controls.",
151
+ on_close=lambda s, _: setattr(st, "open", False),
152
+ ),
153
+ )
154
+ ```
155
+
156
+ ## Spinner and Skeleton
157
+
158
+ `ui.Spinner` shows work in progress with an optional `label`. `ui.Skeleton`
159
+ stands in for content still loading, shaped like `text`, a `card` or a
160
+ `table`.
161
+
162
+ ```python live
163
+ ui.Stack(
164
+ ui.Row(
165
+ ui.Spinner(size="sm"),
166
+ ui.Spinner(),
167
+ ui.Spinner(size="lg", label="Loading the project"),
168
+ gap=6,
169
+ ),
170
+ ui.Grid(
171
+ ui.Skeleton(rows=3),
172
+ ui.Skeleton(shape="card"),
173
+ ui.Skeleton(shape="table", rows=4),
174
+ cols={"sm": 1, "md": 3},
175
+ ),
176
+ gap=6,
177
+ )
178
+ ```
179
+
180
+ ## Empty
181
+
182
+ `ui.Empty` is the state before there is anything: a `title`, a
183
+ `description`, an `icon` and an `action`.
184
+
185
+ ```python live
186
+ ui.Empty(
187
+ title="No projects yet",
188
+ description="Create one to start, or import from a file.",
189
+ icon="inbox",
190
+ action=ui.Button("New project", on_click=lambda s, _: s.toast("Creating")),
191
+ )
192
+ ```
193
+
194
+ ## Progress and Steps
195
+
196
+ `ui.Progress` is a bar for a `value` from 0 to 1 with a `label` and a
197
+ `description`, or for a running task (see the Tasks chapter). `ui.Steps`
198
+ draws a list of `mw.Step` items with their status.
199
+
200
+ ```python live
201
+ ui.Stack(
202
+ ui.Progress(value=0.65, label="Importing", description="130 of 200 rows"),
203
+ ui.Steps(
204
+ items=[
205
+ mw.Step(id="read", label="Read the document", status="done", note="12 pages"),
206
+ mw.Step(id="find", label="Find the headings", status="running"),
207
+ mw.Step(id="route", label="Add routing", status="pending"),
208
+ ]
209
+ ),
210
+ gap=6,
211
+ )
212
+ ```
@@ -0,0 +1,194 @@
1
+ ---
2
+ title: Files
3
+ order: 7
4
+ ---
5
+
6
+ # Files
7
+
8
+ Files belong to the session. An upload becomes a `mw.FileRef` with an id, a
9
+ name, a size and a type; the bytes live on the server for as long as the
10
+ session does and travel to the browser over the session's own socket, so a
11
+ file has no URL and needs no signing.
12
+
13
+ ## FileUpload
14
+
15
+ `ui.FileUpload` is a drop zone (`dropzone=False` makes it a button). `accept`
16
+ lists extensions, `multiple` and `max_files` allow more than one,
17
+ `max_size_mb` caps each. `on_upload` receives the `FileRef`s; `on_reject`
18
+ receives `{name, reason}` for a file that was refused.
19
+
20
+ ```python live
21
+ from pydantic import BaseModel
22
+
23
+
24
+ class Uploads(BaseModel):
25
+ files: list = []
26
+
27
+
28
+ async def uploaded(s, refs):
29
+ state(Uploads).files.extend(refs)
30
+
31
+
32
+ def example(s):
33
+ st = state(Uploads)
34
+ return ui.Stack(
35
+ ui.FileUpload(
36
+ accept=[".txt", ".md", ".png", ".pdf"],
37
+ multiple=True,
38
+ max_files=3,
39
+ max_size_mb=5,
40
+ on_upload=uploaded,
41
+ on_reject=lambda s, r: s.toast(f"{r['name']}: {r['reason']}", tone="warning"),
42
+ ),
43
+ *(ui.FileCard(file=f, key=f.id) for f in st.files),
44
+ gap=3,
45
+ )
46
+ ```
47
+
48
+ ## FileCard
49
+
50
+ `ui.FileCard` shows a file with its size and kind, an optional `meta` line,
51
+ a `status` and `actions`. Given a `FileRef`, Open shows the bytes in a new
52
+ tab; given a `url`, it links there.
53
+
54
+ ```python live
55
+ from pydantic import BaseModel
56
+
57
+
58
+ class Held(BaseModel):
59
+ ref: object = None
60
+
61
+
62
+ def example(s):
63
+ st = state(Held)
64
+ if st.ref is None:
65
+ st.ref = s.files.add("Notes for the pilot.\n", name="notes.txt")
66
+ return ui.Stack(
67
+ ui.FileCard(
68
+ file=st.ref,
69
+ meta="added just now",
70
+ actions=[ui.MenuItem(id="remove", label="Remove", icon="trash-2", destructive=True)],
71
+ on_action=lambda s, a: s.toast(f"{a} requested"),
72
+ ),
73
+ ui.FileCard(
74
+ name="report.pdf", size=482_000, kind="pdf", status="loading", meta="converting"
75
+ ),
76
+ ui.FileCard(
77
+ name="broken.csv", size=12, kind="text", status="error", meta="could not parse"
78
+ ),
79
+ gap=2,
80
+ )
81
+ ```
82
+
83
+ ## Image and Pdf
84
+
85
+ `ui.Image` and `ui.Pdf` take a `FileRef` or an outside URL. An image has a
86
+ `size` and a `fit`; a PDF a `page` and a `height`. The browser's own viewer
87
+ shows the PDF, so a form or a link inside it looks like part of the app:
88
+ show files the app made or the person uploaded themselves, not documents
89
+ from strangers.
90
+
91
+ ```python live
92
+ import struct, zlib
93
+
94
+ from pydantic import BaseModel
95
+
96
+
97
+ def png(w=48, h=48, rgb=(10, 126, 164)) -> bytes:
98
+ raw = b"".join(b"\x00" + bytes(rgb) * w for _ in range(h))
99
+
100
+ def chunk(tag, data):
101
+ return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data))
102
+
103
+ return (
104
+ b"\x89PNG\r\n\x1a\n"
105
+ + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
106
+ + chunk(b"IDAT", zlib.compress(raw))
107
+ + chunk(b"IEND", b"")
108
+ )
109
+
110
+
111
+ class Pic(BaseModel):
112
+ ref: object = None
113
+
114
+
115
+ def example(s):
116
+ st = state(Pic)
117
+ if st.ref is None:
118
+ st.ref = s.files.add(png(), name="square.png")
119
+ return ui.Row(
120
+ ui.Image(src=st.ref, alt="A teal square", size="sm"),
121
+ ui.Image(
122
+ src="https://picsum.photos/seed/millwork/160/96",
123
+ alt="An outside image",
124
+ size="sm",
125
+ fit="cover",
126
+ ),
127
+ gap=4,
128
+ )
129
+ ```
130
+
131
+ A PDF the session holds shows the same way, from its bytes:
132
+
133
+ ```python live
134
+ from pydantic import BaseModel
135
+
136
+
137
+ def tiny_pdf(text: str) -> bytes:
138
+ stream = f"BT /F1 24 Tf 40 120 Td ({text}) Tj ET".encode()
139
+ objs = [
140
+ b"<< /Type /Catalog /Pages 2 0 R >>",
141
+ b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
142
+ b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 320 180] /Contents 4 0 R "
143
+ b"/Resources << /Font << /F1 5 0 R >> >> >>",
144
+ b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream",
145
+ b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
146
+ ]
147
+ out = b"%PDF-1.4\n"
148
+ offsets = []
149
+ for n, body in enumerate(objs, 1):
150
+ offsets.append(len(out))
151
+ out += f"{n} 0 obj\n".encode() + body + b"\nendobj\n"
152
+ xref = len(out)
153
+ out += f"xref\n0 {len(objs) + 1}\n0000000000 65535 f \n".encode()
154
+ out += b"".join(f"{o:010d} 00000 n \n".encode() for o in offsets)
155
+ out += f"trailer\n<< /Size {len(objs) + 1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
156
+ return out
157
+
158
+ class Doc(BaseModel):
159
+ ref: object = None
160
+
161
+ def example(s):
162
+ st = state(Doc)
163
+ if st.ref is None:
164
+ st.ref = s.files.add(tiny_pdf("Hello from millwork"), name="hello.pdf")
165
+ return ui.Pdf(src=st.ref, height="14rem")
166
+ ```
167
+
168
+ ## Download
169
+
170
+ `ui.Download` is a button that saves a file. `content` is bytes, text, a
171
+ path, a callable that makes any of those on click, or a `FileRef`, which
172
+ saves straight away without a round trip. `s.download(filename, content)`
173
+ does the same from a handler, without a button.
174
+
175
+ ```python live
176
+ def report() -> str:
177
+ return "name,score\nAda,0.92\nGrace,0.88\n"
178
+
179
+
180
+ ui.Row(
181
+ ui.Download("Text", filename="hello.txt", content="Hello from millwork.\n"),
182
+ ui.Download(
183
+ "Made on click", filename="report.csv", content=report, mime="text/csv", variant="primary"
184
+ ),
185
+ ui.Button(
186
+ "From a handler",
187
+ variant="outline",
188
+ icon="download",
189
+ on_click=lambda s, _: s.download("pushed.txt", "Sent by a handler.\n"),
190
+ ),
191
+ gap=2,
192
+ wrap=True,
193
+ )
194
+ ```
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: Form
3
+ order: 6
4
+ ---
5
+
6
+ # Form
7
+
8
+ `ui.Form` renders a Pydantic model as fields and hands `on_submit` a
9
+ validated instance. Types choose the controls: `str` an input, `int` and
10
+ `float` a number, `bool` a switch, `Literal` and `Enum` a select, `date` a
11
+ date picker, a nested model a fieldset, a list of models a repeatable list.
12
+ Validation errors land on the field they concern.
13
+
14
+ ## From a model
15
+
16
+ `Field(title=, description=, examples=)` give a field its label, its help
17
+ text and its placeholder. Constraints such as `min_length`, `ge` and `le`
18
+ become the control's limits and the validation.
19
+
20
+ ```python live
21
+ import datetime as dt
22
+ from typing import Literal
23
+
24
+ from pydantic import BaseModel, Field
25
+
26
+
27
+ class Project(BaseModel):
28
+ title: str = Field(title="Title", examples=["Q1 report"], min_length=3)
29
+ language: Literal["en", "nl", "de"] = Field("en", title="Language")
30
+ starts: dt.date | None = Field(None, title="Starts")
31
+ count: int = Field(3, ge=1, le=10, title="Copies", description="Between one and ten.")
32
+ enabled: bool = Field(True, title="Active")
33
+ notes: str | None = Field(None, title="Notes", max_length=500)
34
+
35
+
36
+ class Saved(BaseModel):
37
+ last: str = "nothing yet"
38
+
39
+
40
+ async def save(s, project: Project):
41
+ state(Saved).last = f"{project.title} ({project.language}, {project.count})"
42
+
43
+
44
+ def example(s):
45
+ return ui.Stack(
46
+ ui.Form(Project, on_submit=save, layout=2),
47
+ ui.Text(f"Saved: {state(Saved).last}", variant="muted"),
48
+ gap=4,
49
+ )
50
+ ```
51
+
52
+ ## Labels and overrides
53
+
54
+ `labels` maps a field's pointer, or its name, to a `label`, `help`,
55
+ `placeholder` and `choices`, so the model stays free of interface copy.
56
+ `overrides` maps a pointer to any spec key: a `widget`, `min`, `max`,
57
+ `step`, `rows`. `fields` picks a subset in order.
58
+
59
+ ```python live
60
+ from pydantic import BaseModel
61
+
62
+
63
+ class Settings(BaseModel):
64
+ name: str = ""
65
+ level: int = 1
66
+ mood: str = "ok"
67
+ tags: list[Literal["a", "b", "c"]] = []
68
+
69
+
70
+ ui.Form(
71
+ Settings,
72
+ fields=["name", "mood", "level", "tags"],
73
+ labels={"name": {"label": "Display name", "help": "Shown to other users."}},
74
+ overrides={
75
+ "/mood": {"choices": [("ok", "Fine"), ("bad", "Not fine")]},
76
+ "/level": {"widget": "slider", "min": 0, "max": 5},
77
+ "/tags": {"widget": "multiselect"},
78
+ },
79
+ on_submit=lambda s, v: s.toast(f"{v.name or 'someone'} is {v.mood}"),
80
+ )
81
+ ```
82
+
83
+ ## Rejecting a valid draft
84
+
85
+ A handler that finds something wrong raises `ui.FormIssues` with a list of
86
+ `(pointer, message)` pairs; the form shows each at its field and keeps the
87
+ draft.
88
+
89
+ ```python live
90
+ from pydantic import BaseModel, Field
91
+
92
+
93
+ class Signup(BaseModel):
94
+ email: str = Field(title="Email")
95
+ handle: str = Field(title="Handle", min_length=2)
96
+
97
+
98
+ TAKEN = {"ada", "grace"}
99
+
100
+
101
+ async def register(s, v: Signup):
102
+ if v.handle.lower() in TAKEN:
103
+ raise ui.FormIssues([("/handle", "That handle is taken.")])
104
+ s.toast(f"Welcome, {v.handle}.", tone="success")
105
+
106
+
107
+ ui.Form(Signup, on_submit=register, submit="Register", cancel="Clear")
108
+ ```
109
+
110
+ ## Submitting from outside
111
+
112
+ `submit=None` hides the form's own button; a `ui.Button(form=key)` anywhere
113
+ on the page submits the form with that key.
114
+
115
+ ```python live
116
+ from pydantic import BaseModel, Field
117
+
118
+
119
+ class Quick(BaseModel):
120
+ query: str = Field("", title="Query")
121
+
122
+
123
+ ui.Row(
124
+ ui.Form(
125
+ Quick,
126
+ key="quick",
127
+ submit=None,
128
+ on_submit=lambda s, v: s.toast(f"Searching {v.query!r}"),
129
+ cls="grow",
130
+ ),
131
+ ui.Button("Go", form="quick", icon="search"),
132
+ align="end",
133
+ gap=3,
134
+ )
135
+ ```