hyperclass 0.0.1__tar.gz → 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hyperclass-0.0.1 → hyperclass-0.0.2}/PKG-INFO +98 -14
- {hyperclass-0.0.1 → hyperclass-0.0.2}/README.md +97 -13
- hyperclass-0.0.2/hyperclass/__init__.py +132 -0
- hyperclass-0.0.2/hyperclass/css.py +158 -0
- hyperclass-0.0.2/hyperclass/html.py +380 -0
- hyperclass-0.0.2/hyperclass/htmx.py +110 -0
- hyperclass-0.0.2/hyperclass/wsgi.py +419 -0
- {hyperclass-0.0.1 → hyperclass-0.0.2}/hyperclass.egg-info/PKG-INFO +98 -14
- {hyperclass-0.0.1 → hyperclass-0.0.2}/hyperclass.egg-info/SOURCES.txt +9 -1
- {hyperclass-0.0.1 → hyperclass-0.0.2}/pyproject.toml +4 -1
- hyperclass-0.0.2/tests/test_bookmarks.py +98 -0
- hyperclass-0.0.2/tests/test_html.py +109 -0
- hyperclass-0.0.2/tests/test_htmx.py +26 -0
- hyperclass-0.0.2/tests/test_package.py +5 -0
- hyperclass-0.0.2/tests/test_wsgi.py +202 -0
- hyperclass-0.0.1/hyperclass/__init__.py +0 -5
- hyperclass-0.0.1/tests/test_hyperclass.py +0 -5
- {hyperclass-0.0.1 → hyperclass-0.0.2}/LICENSE +0 -0
- {hyperclass-0.0.1 → hyperclass-0.0.2}/hyperclass.egg-info/dependency_links.txt +0 -0
- {hyperclass-0.0.1 → hyperclass-0.0.2}/hyperclass.egg-info/requires.txt +0 -0
- {hyperclass-0.0.1 → hyperclass-0.0.2}/hyperclass.egg-info/top_level.txt +0 -0
- {hyperclass-0.0.1 → hyperclass-0.0.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperclass
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Build interactive web applications as Python class hierarchies.
|
|
5
5
|
Author: Grant Jenks
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -85,6 +85,10 @@ Hyperclass treats classes and instances differently:
|
|
|
85
85
|
- Multiple inheritance composes multiple CSS classes.
|
|
86
86
|
- Classes are also usable as selectors and htmx targets.
|
|
87
87
|
- Instances contain attributes, state, and child content.
|
|
88
|
+
- Typed route parameters turn URL segments into handler arguments.
|
|
89
|
+
- htmx 4 partials can update several object-selected regions from one response.
|
|
90
|
+
- `id.some_name` creates an interned Python reference for `some-name`.
|
|
91
|
+
- Decorated handlers are reversible route references; URLs need not be repeated.
|
|
88
92
|
|
|
89
93
|
~~~python
|
|
90
94
|
class compact:
|
|
@@ -149,24 +153,23 @@ The class `counter` simultaneously represents:
|
|
|
149
153
|
|
|
150
154
|
## WSGI and htmx
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
Application subclasses collect decorated method routes:
|
|
153
157
|
|
|
154
158
|
~~~python
|
|
155
|
-
from hyperclass import App
|
|
159
|
+
from hyperclass import App, get, post
|
|
156
160
|
|
|
157
|
-
app = App()
|
|
158
161
|
|
|
162
|
+
class counter_app(App):
|
|
163
|
+
@get("/")
|
|
164
|
+
def index(self, request):
|
|
165
|
+
return counter(0)
|
|
159
166
|
|
|
160
|
-
@
|
|
161
|
-
def
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@app.post("/counter")
|
|
166
|
-
def increment(request):
|
|
167
|
-
return counter(request.form.int("value") + 1)
|
|
167
|
+
@post("/counter")
|
|
168
|
+
def increment(self, request):
|
|
169
|
+
return counter(request.form.int("value") + 1)
|
|
168
170
|
|
|
169
171
|
|
|
172
|
+
app = counter_app()
|
|
170
173
|
if __name__ == "__main__":
|
|
171
174
|
app.run()
|
|
172
175
|
~~~
|
|
@@ -174,10 +177,88 @@ if __name__ == "__main__":
|
|
|
174
177
|
The application is a normal WSGI callable. The development server can use
|
|
175
178
|
Python's standard library; production deployment can use any WSGI server.
|
|
176
179
|
|
|
180
|
+
Decorated handlers retain their routing metadata, so application code can refer
|
|
181
|
+
to Python rather than repeat URL strings:
|
|
182
|
+
|
|
183
|
+
~~~python
|
|
184
|
+
hx.patch(
|
|
185
|
+
increment,
|
|
186
|
+
target=closest(counter),
|
|
187
|
+
swap=outer_morph,
|
|
188
|
+
)
|
|
189
|
+
~~~
|
|
190
|
+
|
|
191
|
+
Typed path parameters are supplied alongside htmx options. Class attributes
|
|
192
|
+
make the handler available to components without a route registry:
|
|
193
|
+
|
|
194
|
+
~~~python
|
|
195
|
+
hx.patch(
|
|
196
|
+
bookmarks.toggle,
|
|
197
|
+
bookmark_id=bookmark.id,
|
|
198
|
+
target=closest(bookmark_card),
|
|
199
|
+
)
|
|
200
|
+
~~~
|
|
201
|
+
|
|
202
|
+
The same endpoint exposes `.url(...)` for ordinary links and form actions. IDs
|
|
203
|
+
work similarly: `id.unread_count` renders as `unread-count` in an HTML
|
|
204
|
+
attribute and as `#unread-count` when used as a selector.
|
|
205
|
+
|
|
177
206
|
htmx supplies browser-to-server interaction without introducing a client-side
|
|
178
207
|
component runtime. Hyperclass should favor native HTML and CSS for local
|
|
179
208
|
behavior and use htmx when the server needs to participate.
|
|
180
209
|
|
|
210
|
+
## Pages
|
|
211
|
+
|
|
212
|
+
For ordinary browser requests, returning an element wraps it in a complete page.
|
|
213
|
+
For htmx requests, the same route returns only the fragment to swap. Use
|
|
214
|
+
`Page` when you want to control the document explicitly:
|
|
215
|
+
|
|
216
|
+
~~~python
|
|
217
|
+
from hyperclass import Page
|
|
218
|
+
|
|
219
|
+
return Page(counter(0), title="Counter")
|
|
220
|
+
~~~
|
|
221
|
+
|
|
222
|
+
Pages collect the styles used by their element tree and include pinned htmx
|
|
223
|
+
4.0.0 from its CDN.
|
|
224
|
+
|
|
225
|
+
## Responsive CSS and states
|
|
226
|
+
|
|
227
|
+
Pseudo-states and media rules are ordinary class attributes:
|
|
228
|
+
|
|
229
|
+
~~~python
|
|
230
|
+
from hyperclass import css, media, rem
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class primary_button(button):
|
|
234
|
+
style = css(background="#6d28d9", color="white")
|
|
235
|
+
hover = css(background="#5b21b6")
|
|
236
|
+
focus_visible = css(outline="3px solid #c4b5fd")
|
|
237
|
+
narrow = media(max_width=40 * rem, width="100%")
|
|
238
|
+
~~~
|
|
239
|
+
|
|
240
|
+
State names translate underscores to CSS hyphens, and named media rules can use
|
|
241
|
+
Python values for width, orientation, and color-scheme conditions. They follow
|
|
242
|
+
the same inheritance and collection rules as base styles.
|
|
243
|
+
|
|
244
|
+
## Try the examples
|
|
245
|
+
|
|
246
|
+
The repository includes the counter and a complete SQLite bookmark inbox:
|
|
247
|
+
|
|
248
|
+
~~~console
|
|
249
|
+
git clone https://github.com/grantjenks/python-hyperclass
|
|
250
|
+
cd python-hyperclass
|
|
251
|
+
python -m examples.bookmarks
|
|
252
|
+
~~~
|
|
253
|
+
|
|
254
|
+
Then open <http://127.0.0.1:8000>. The bookmark app supports adding, filtering,
|
|
255
|
+
marking read or unread, and deleting bookmarks. Its implementation is still only
|
|
256
|
+
Python and the standard library: semantic subclasses style read and unread
|
|
257
|
+
cards, routes such as `/bookmarks/<int:bookmark_id>` receive typed arguments,
|
|
258
|
+
and htmx 4 `<hx-partial>` responses update a card and the unread count together.
|
|
259
|
+
|
|
260
|
+
Use `python -m examples.counter` for the smaller introduction.
|
|
261
|
+
|
|
181
262
|
## Principles
|
|
182
263
|
|
|
183
264
|
- **Python is the authoring language.** Control flow, composition, inheritance,
|
|
@@ -195,5 +276,8 @@ behavior and use htmx when the server needs to participate.
|
|
|
195
276
|
|
|
196
277
|
## Status
|
|
197
278
|
|
|
198
|
-
|
|
199
|
-
|
|
279
|
+
Version 0.0.2 is the first working vertical slice: HTML elements, semantic
|
|
280
|
+
subclasses, inherited and responsive CSS, object and ID selectors, htmx
|
|
281
|
+
attributes and partials, pages, request parsing, reversible typed WSGI routing,
|
|
282
|
+
class-based applications, and a persistent example application. The API remains
|
|
283
|
+
deliberately pre-alpha.
|
|
@@ -55,6 +55,10 @@ Hyperclass treats classes and instances differently:
|
|
|
55
55
|
- Multiple inheritance composes multiple CSS classes.
|
|
56
56
|
- Classes are also usable as selectors and htmx targets.
|
|
57
57
|
- Instances contain attributes, state, and child content.
|
|
58
|
+
- Typed route parameters turn URL segments into handler arguments.
|
|
59
|
+
- htmx 4 partials can update several object-selected regions from one response.
|
|
60
|
+
- `id.some_name` creates an interned Python reference for `some-name`.
|
|
61
|
+
- Decorated handlers are reversible route references; URLs need not be repeated.
|
|
58
62
|
|
|
59
63
|
~~~python
|
|
60
64
|
class compact:
|
|
@@ -119,24 +123,23 @@ The class `counter` simultaneously represents:
|
|
|
119
123
|
|
|
120
124
|
## WSGI and htmx
|
|
121
125
|
|
|
122
|
-
|
|
126
|
+
Application subclasses collect decorated method routes:
|
|
123
127
|
|
|
124
128
|
~~~python
|
|
125
|
-
from hyperclass import App
|
|
129
|
+
from hyperclass import App, get, post
|
|
126
130
|
|
|
127
|
-
app = App()
|
|
128
131
|
|
|
132
|
+
class counter_app(App):
|
|
133
|
+
@get("/")
|
|
134
|
+
def index(self, request):
|
|
135
|
+
return counter(0)
|
|
129
136
|
|
|
130
|
-
@
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@app.post("/counter")
|
|
136
|
-
def increment(request):
|
|
137
|
-
return counter(request.form.int("value") + 1)
|
|
137
|
+
@post("/counter")
|
|
138
|
+
def increment(self, request):
|
|
139
|
+
return counter(request.form.int("value") + 1)
|
|
138
140
|
|
|
139
141
|
|
|
142
|
+
app = counter_app()
|
|
140
143
|
if __name__ == "__main__":
|
|
141
144
|
app.run()
|
|
142
145
|
~~~
|
|
@@ -144,10 +147,88 @@ if __name__ == "__main__":
|
|
|
144
147
|
The application is a normal WSGI callable. The development server can use
|
|
145
148
|
Python's standard library; production deployment can use any WSGI server.
|
|
146
149
|
|
|
150
|
+
Decorated handlers retain their routing metadata, so application code can refer
|
|
151
|
+
to Python rather than repeat URL strings:
|
|
152
|
+
|
|
153
|
+
~~~python
|
|
154
|
+
hx.patch(
|
|
155
|
+
increment,
|
|
156
|
+
target=closest(counter),
|
|
157
|
+
swap=outer_morph,
|
|
158
|
+
)
|
|
159
|
+
~~~
|
|
160
|
+
|
|
161
|
+
Typed path parameters are supplied alongside htmx options. Class attributes
|
|
162
|
+
make the handler available to components without a route registry:
|
|
163
|
+
|
|
164
|
+
~~~python
|
|
165
|
+
hx.patch(
|
|
166
|
+
bookmarks.toggle,
|
|
167
|
+
bookmark_id=bookmark.id,
|
|
168
|
+
target=closest(bookmark_card),
|
|
169
|
+
)
|
|
170
|
+
~~~
|
|
171
|
+
|
|
172
|
+
The same endpoint exposes `.url(...)` for ordinary links and form actions. IDs
|
|
173
|
+
work similarly: `id.unread_count` renders as `unread-count` in an HTML
|
|
174
|
+
attribute and as `#unread-count` when used as a selector.
|
|
175
|
+
|
|
147
176
|
htmx supplies browser-to-server interaction without introducing a client-side
|
|
148
177
|
component runtime. Hyperclass should favor native HTML and CSS for local
|
|
149
178
|
behavior and use htmx when the server needs to participate.
|
|
150
179
|
|
|
180
|
+
## Pages
|
|
181
|
+
|
|
182
|
+
For ordinary browser requests, returning an element wraps it in a complete page.
|
|
183
|
+
For htmx requests, the same route returns only the fragment to swap. Use
|
|
184
|
+
`Page` when you want to control the document explicitly:
|
|
185
|
+
|
|
186
|
+
~~~python
|
|
187
|
+
from hyperclass import Page
|
|
188
|
+
|
|
189
|
+
return Page(counter(0), title="Counter")
|
|
190
|
+
~~~
|
|
191
|
+
|
|
192
|
+
Pages collect the styles used by their element tree and include pinned htmx
|
|
193
|
+
4.0.0 from its CDN.
|
|
194
|
+
|
|
195
|
+
## Responsive CSS and states
|
|
196
|
+
|
|
197
|
+
Pseudo-states and media rules are ordinary class attributes:
|
|
198
|
+
|
|
199
|
+
~~~python
|
|
200
|
+
from hyperclass import css, media, rem
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class primary_button(button):
|
|
204
|
+
style = css(background="#6d28d9", color="white")
|
|
205
|
+
hover = css(background="#5b21b6")
|
|
206
|
+
focus_visible = css(outline="3px solid #c4b5fd")
|
|
207
|
+
narrow = media(max_width=40 * rem, width="100%")
|
|
208
|
+
~~~
|
|
209
|
+
|
|
210
|
+
State names translate underscores to CSS hyphens, and named media rules can use
|
|
211
|
+
Python values for width, orientation, and color-scheme conditions. They follow
|
|
212
|
+
the same inheritance and collection rules as base styles.
|
|
213
|
+
|
|
214
|
+
## Try the examples
|
|
215
|
+
|
|
216
|
+
The repository includes the counter and a complete SQLite bookmark inbox:
|
|
217
|
+
|
|
218
|
+
~~~console
|
|
219
|
+
git clone https://github.com/grantjenks/python-hyperclass
|
|
220
|
+
cd python-hyperclass
|
|
221
|
+
python -m examples.bookmarks
|
|
222
|
+
~~~
|
|
223
|
+
|
|
224
|
+
Then open <http://127.0.0.1:8000>. The bookmark app supports adding, filtering,
|
|
225
|
+
marking read or unread, and deleting bookmarks. Its implementation is still only
|
|
226
|
+
Python and the standard library: semantic subclasses style read and unread
|
|
227
|
+
cards, routes such as `/bookmarks/<int:bookmark_id>` receive typed arguments,
|
|
228
|
+
and htmx 4 `<hx-partial>` responses update a card and the unread count together.
|
|
229
|
+
|
|
230
|
+
Use `python -m examples.counter` for the smaller introduction.
|
|
231
|
+
|
|
151
232
|
## Principles
|
|
152
233
|
|
|
153
234
|
- **Python is the authoring language.** Control flow, composition, inheritance,
|
|
@@ -165,5 +246,8 @@ behavior and use htmx when the server needs to participate.
|
|
|
165
246
|
|
|
166
247
|
## Status
|
|
167
248
|
|
|
168
|
-
|
|
169
|
-
|
|
249
|
+
Version 0.0.2 is the first working vertical slice: HTML elements, semantic
|
|
250
|
+
subclasses, inherited and responsive CSS, object and ID selectors, htmx
|
|
251
|
+
attributes and partials, pages, request parsing, reversible typed WSGI routing,
|
|
252
|
+
class-based applications, and a persistent example application. The API remains
|
|
253
|
+
deliberately pre-alpha.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Build interactive web applications as Python class hierarchies."""
|
|
2
|
+
|
|
3
|
+
from . import html as _html
|
|
4
|
+
from .css import (
|
|
5
|
+
AlphaColor,
|
|
6
|
+
Color,
|
|
7
|
+
Length,
|
|
8
|
+
Media,
|
|
9
|
+
Style,
|
|
10
|
+
Unit,
|
|
11
|
+
block,
|
|
12
|
+
css,
|
|
13
|
+
em,
|
|
14
|
+
flex,
|
|
15
|
+
grid,
|
|
16
|
+
inline,
|
|
17
|
+
media,
|
|
18
|
+
none,
|
|
19
|
+
orange,
|
|
20
|
+
percent,
|
|
21
|
+
pointer,
|
|
22
|
+
px,
|
|
23
|
+
rem,
|
|
24
|
+
vh,
|
|
25
|
+
vw,
|
|
26
|
+
)
|
|
27
|
+
from .htmx import (
|
|
28
|
+
Attributes,
|
|
29
|
+
Htmx,
|
|
30
|
+
Target,
|
|
31
|
+
after,
|
|
32
|
+
append,
|
|
33
|
+
before,
|
|
34
|
+
closest,
|
|
35
|
+
find,
|
|
36
|
+
hx,
|
|
37
|
+
inner_html,
|
|
38
|
+
inner_morph,
|
|
39
|
+
next,
|
|
40
|
+
outer_html,
|
|
41
|
+
outer_morph,
|
|
42
|
+
outer_sync,
|
|
43
|
+
prepend,
|
|
44
|
+
previous,
|
|
45
|
+
)
|
|
46
|
+
from .htmx import (
|
|
47
|
+
delete as delete_swap,
|
|
48
|
+
)
|
|
49
|
+
from .wsgi import (
|
|
50
|
+
App,
|
|
51
|
+
BoundEndpoint,
|
|
52
|
+
Endpoint,
|
|
53
|
+
Request,
|
|
54
|
+
Response,
|
|
55
|
+
Route,
|
|
56
|
+
Values,
|
|
57
|
+
get,
|
|
58
|
+
patch,
|
|
59
|
+
post,
|
|
60
|
+
put,
|
|
61
|
+
route,
|
|
62
|
+
)
|
|
63
|
+
from .wsgi import delete as delete_route
|
|
64
|
+
|
|
65
|
+
__version__ = "0.0.2"
|
|
66
|
+
|
|
67
|
+
hidden = "hidden"
|
|
68
|
+
submit = "submit"
|
|
69
|
+
|
|
70
|
+
for _name in _html.__all__:
|
|
71
|
+
globals()[_name] = getattr(_html, _name)
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"__version__",
|
|
75
|
+
"AlphaColor",
|
|
76
|
+
"App",
|
|
77
|
+
"Attributes",
|
|
78
|
+
"BoundEndpoint",
|
|
79
|
+
"Color",
|
|
80
|
+
"Endpoint",
|
|
81
|
+
"Htmx",
|
|
82
|
+
"Length",
|
|
83
|
+
"Media",
|
|
84
|
+
"Request",
|
|
85
|
+
"Response",
|
|
86
|
+
"Route",
|
|
87
|
+
"Style",
|
|
88
|
+
"Target",
|
|
89
|
+
"Unit",
|
|
90
|
+
"Values",
|
|
91
|
+
"after",
|
|
92
|
+
"append",
|
|
93
|
+
"before",
|
|
94
|
+
"block",
|
|
95
|
+
"closest",
|
|
96
|
+
"css",
|
|
97
|
+
"delete_swap",
|
|
98
|
+
"delete_route",
|
|
99
|
+
"em",
|
|
100
|
+
"find",
|
|
101
|
+
"flex",
|
|
102
|
+
"grid",
|
|
103
|
+
"get",
|
|
104
|
+
"hidden",
|
|
105
|
+
"hx",
|
|
106
|
+
"inline",
|
|
107
|
+
"inner_html",
|
|
108
|
+
"inner_morph",
|
|
109
|
+
"media",
|
|
110
|
+
"next",
|
|
111
|
+
"none",
|
|
112
|
+
"orange",
|
|
113
|
+
"outer_html",
|
|
114
|
+
"outer_morph",
|
|
115
|
+
"outer_sync",
|
|
116
|
+
"percent",
|
|
117
|
+
"pointer",
|
|
118
|
+
"patch",
|
|
119
|
+
"prepend",
|
|
120
|
+
"previous",
|
|
121
|
+
"post",
|
|
122
|
+
"put",
|
|
123
|
+
"px",
|
|
124
|
+
"rem",
|
|
125
|
+
"route",
|
|
126
|
+
"submit",
|
|
127
|
+
"vh",
|
|
128
|
+
"vw",
|
|
129
|
+
*_html.__all__,
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
del _html, _name
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Small Python values for authoring CSS."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _number(value: int | float) -> str:
|
|
10
|
+
if isinstance(value, float) and value.is_integer():
|
|
11
|
+
return str(int(value))
|
|
12
|
+
return str(value)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Unit:
|
|
17
|
+
"""A CSS unit which can be multiplied by a number."""
|
|
18
|
+
|
|
19
|
+
suffix: str
|
|
20
|
+
|
|
21
|
+
def __rmul__(self, value: int | float) -> Length:
|
|
22
|
+
return Length(value, self)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Length:
|
|
27
|
+
value: int | float
|
|
28
|
+
unit: Unit
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
return f"{_number(self.value)}{self.unit.suffix}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Color:
|
|
36
|
+
name: str
|
|
37
|
+
rgb: tuple[int, int, int] | None = None
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
return self.name
|
|
41
|
+
|
|
42
|
+
def fade(self, alpha: float) -> AlphaColor:
|
|
43
|
+
if not 0 <= alpha <= 1:
|
|
44
|
+
raise ValueError("alpha must be between 0 and 1")
|
|
45
|
+
if self.rgb is None:
|
|
46
|
+
return AlphaColor(self.name, alpha)
|
|
47
|
+
red, green, blue = self.rgb
|
|
48
|
+
return AlphaColor(f"{red} {green} {blue}", alpha, rgb=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class AlphaColor:
|
|
53
|
+
color: str
|
|
54
|
+
alpha: float
|
|
55
|
+
rgb: bool = False
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
if self.rgb:
|
|
59
|
+
return f"rgb({self.color} / {_number(self.alpha)})"
|
|
60
|
+
return f"color-mix(in srgb, {self.color} {self.alpha * 100:g}%, transparent)"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def css_value(value: Any) -> str:
|
|
64
|
+
if isinstance(value, bool):
|
|
65
|
+
return "true" if value else "false"
|
|
66
|
+
return str(value)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Style:
|
|
70
|
+
"""An ordered collection of CSS declarations."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, **declarations: Any):
|
|
73
|
+
self.declarations = tuple(
|
|
74
|
+
(name.replace("_", "-"), value)
|
|
75
|
+
for name, value in declarations.items()
|
|
76
|
+
if value is not None
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def render(self) -> str:
|
|
80
|
+
return ";".join(
|
|
81
|
+
f"{name}:{css_value(value)}" for name, value in self.declarations
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
return self.render()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def css(**declarations: Any) -> Style:
|
|
89
|
+
return Style(**declarations)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class Media:
|
|
94
|
+
"""A stylesheet rule guarded by a CSS media query."""
|
|
95
|
+
|
|
96
|
+
conditions: tuple[tuple[str, Any], ...]
|
|
97
|
+
style: Style
|
|
98
|
+
|
|
99
|
+
def query(self) -> str:
|
|
100
|
+
return " and ".join(
|
|
101
|
+
f"({name.replace('_', '-')}:{css_value(value)})"
|
|
102
|
+
for name, value in self.conditions
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def media(
|
|
107
|
+
*,
|
|
108
|
+
min_width: Any = None,
|
|
109
|
+
max_width: Any = None,
|
|
110
|
+
orientation: str | None = None,
|
|
111
|
+
prefers_color_scheme: str | None = None,
|
|
112
|
+
**declarations: Any,
|
|
113
|
+
) -> Media:
|
|
114
|
+
"""Create a media rule with Python-named conditions and declarations."""
|
|
115
|
+
|
|
116
|
+
conditions = tuple(
|
|
117
|
+
(name, value)
|
|
118
|
+
for name, value in (
|
|
119
|
+
("min_width", min_width),
|
|
120
|
+
("max_width", max_width),
|
|
121
|
+
("orientation", orientation),
|
|
122
|
+
("prefers_color_scheme", prefers_color_scheme),
|
|
123
|
+
)
|
|
124
|
+
if value is not None
|
|
125
|
+
)
|
|
126
|
+
if not conditions:
|
|
127
|
+
raise ValueError("media requires at least one condition")
|
|
128
|
+
return Media(conditions, css(**declarations))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
PSEUDO_STATES = {
|
|
132
|
+
"active",
|
|
133
|
+
"checked",
|
|
134
|
+
"disabled",
|
|
135
|
+
"focus",
|
|
136
|
+
"focus_visible",
|
|
137
|
+
"focus_within",
|
|
138
|
+
"hover",
|
|
139
|
+
"invalid",
|
|
140
|
+
"visited",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
px = Unit("px")
|
|
145
|
+
rem = Unit("rem")
|
|
146
|
+
em = Unit("em")
|
|
147
|
+
percent = Unit("%")
|
|
148
|
+
vh = Unit("vh")
|
|
149
|
+
vw = Unit("vw")
|
|
150
|
+
|
|
151
|
+
orange = Color("orange", (255, 165, 0))
|
|
152
|
+
|
|
153
|
+
grid = "grid"
|
|
154
|
+
flex = "flex"
|
|
155
|
+
block = "block"
|
|
156
|
+
inline = "inline"
|
|
157
|
+
none = "none"
|
|
158
|
+
pointer = "pointer"
|