reflex-plasma-ui 0.1.1__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.
- reflex_plasma_ui-0.1.1/LICENSE +21 -0
- reflex_plasma_ui-0.1.1/PKG-INFO +244 -0
- reflex_plasma_ui-0.1.1/README.md +206 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui/__init__.py +61 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui/_js.py +119 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui/plasma_ui.py +239 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui/plasma_ui.pyi +458 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui/presets.py +127 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui/py.typed +0 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui/runtime.py +98 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui.egg-info/PKG-INFO +244 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui.egg-info/SOURCES.txt +20 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui.egg-info/dependency_links.txt +1 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui.egg-info/requires.txt +15 -0
- reflex_plasma_ui-0.1.1/custom_components/reflex_plasma_ui.egg-info/top_level.txt +1 -0
- reflex_plasma_ui-0.1.1/pyproject.toml +146 -0
- reflex_plasma_ui-0.1.1/setup.cfg +4 -0
- reflex_plasma_ui-0.1.1/tests/test_components.py +144 -0
- reflex_plasma_ui-0.1.1/tests/test_docs.py +59 -0
- reflex_plasma_ui-0.1.1/tests/test_package.py +74 -0
- reflex_plasma_ui-0.1.1/tests/test_presets.py +99 -0
- reflex_plasma_ui-0.1.1/tests/test_runtime.py +82 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ernesto Crespo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reflex-plasma-ui
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Plasma UI for Reflex: liquid WebGL panels that fuse on contact, refract what is behind them and snap to a grid.
|
|
5
|
+
Author-email: Ernesto Crespo <ecrespo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ecrespo/reflex-plasma-ui
|
|
8
|
+
Project-URL: Source, https://github.com/ecrespo/reflex-plasma-ui
|
|
9
|
+
Project-URL: Issues, https://github.com/ecrespo/reflex-plasma-ui/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/ecrespo/reflex-plasma-ui/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Plasma UI, https://github.com/CruxGarden/plasma-ui
|
|
12
|
+
Keywords: reflex,reflex-custom-components,plasma-ui,webgl,liquid-glass,panels
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: reflex>=0.9.12
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
28
|
+
Requires-Dist: twine>=5.1; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.14; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
33
|
+
Requires-Dist: bandit[toml]>=1.7; extra == "dev"
|
|
34
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
35
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "dev"
|
|
36
|
+
Requires-Dist: trove-classifiers>=2024.1.1; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# reflex-plasma-ui
|
|
40
|
+
|
|
41
|
+
[](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/quality.yml)
|
|
42
|
+
[](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/security.yml)
|
|
43
|
+
[](https://pypi.org/project/reflex-plasma-ui/)
|
|
44
|
+
[](https://pypi.org/project/reflex-plasma-ui/)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
|
|
47
|
+
[Plasma UI](https://github.com/CruxGarden/plasma-ui) for [Reflex](https://reflex.dev): liquid panels rendered in WebGL on canvas. Every panel on the page is **one shared plasma** — surfaces fuse on contact, refract whatever is behind them, stretch when thrown and snap to a grid. The DOM stays ordinary, accessible HTML; the canvas only draws.
|
|
48
|
+
|
|
49
|
+
Wraps `@cruxgarden/plasma-ui@0.7.0` (pinned, installed automatically by Reflex). Works on Chrome, Edge, Firefox and Safari 16.4+ (WebGL2). Without WebGL2, every surface falls back to a frosted CSS panel with drag and snap intact. The effect is GPU-heavy, so it belongs on desktop browsers.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install reflex-plasma-ui
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import reflex as rx
|
|
59
|
+
from reflex_plasma_ui import plasma, plasma_provider, pulse
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Layout(rx.State):
|
|
63
|
+
offset: dict[str, float] = {"x": 24, "y": 24}
|
|
64
|
+
|
|
65
|
+
def save(self, offset: dict[str, float]):
|
|
66
|
+
self.offset = offset
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def index():
|
|
70
|
+
return plasma_provider(
|
|
71
|
+
plasma(rx.el.strong("My app"), as_="header", lean=False, fuse=False, padding=16),
|
|
72
|
+
rx.el.div(
|
|
73
|
+
plasma(
|
|
74
|
+
rx.el.h3("Inbox"),
|
|
75
|
+
rx.el.button("Pulse", on_click=pulse()),
|
|
76
|
+
draggable=True,
|
|
77
|
+
padding=20,
|
|
78
|
+
bounds_selector="#stage",
|
|
79
|
+
offset=Layout.offset,
|
|
80
|
+
on_drag_end=Layout.save,
|
|
81
|
+
width="240px",
|
|
82
|
+
),
|
|
83
|
+
id="stage",
|
|
84
|
+
style={"position": "relative", "height": "70vh"},
|
|
85
|
+
),
|
|
86
|
+
mood="tidal",
|
|
87
|
+
theme="dark",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
app = rx.App()
|
|
92
|
+
app.add_page(index)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> The canvas sits at `z-index: -1`, behind the page. If your app uses the Radix theme with its page background (`RadixThemesPlugin`, `rx.theme`), that opaque background hides the canvas: set `has_background=False` or make `.radix-themes` transparent.
|
|
96
|
+
|
|
97
|
+
## Components
|
|
98
|
+
|
|
99
|
+
### `plasma_provider(*children, **props)`
|
|
100
|
+
|
|
101
|
+
Owns the renderer and every shared setting. All `<PlasmaProvider>` props are available in snake_case. Any prop left unset keeps the library default, and every prop can be bound to state, so changes go live.
|
|
102
|
+
|
|
103
|
+
| Group | Props |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| Look | `mood` (`"tidal"`/`"aurora"`/`"ember"` or a custom `{colors, blend, spring}` dict), `theme` (`auto`/`light`/`dark`), `background` (CSS color or image URL), `radius`, `blend`, `tint`, `opacity`, `frost`, `elevation`, `smoothness`, `refraction`, `dispersion` |
|
|
106
|
+
| Rim & sheen | `rim`, `rim_color` (`"iridescent"`, `"tint"` or hex), `rim_width`, `highlight`, `edge_line`, `shimmer`, `shimmer_speed`, `glow`, `wash`, `grain`, `background_blur` |
|
|
107
|
+
| Material | `material` (`plasma`, `crystal`, `metal`, `mercury`, `wood`, `stone`, `cloud`), `light_dir`, `roughness`, `anisotropy`, `edge`, `edge_scale`, `edge_sharpness`, `thickness`, `tension` |
|
|
108
|
+
| Motion | `viscosity`, `stretch`, `flow`, `form_in`, `form_speed`, `form_out` |
|
|
109
|
+
| Pointer & layout | `pointer_drop`, `pointer_pull`, `ambient_drops`, `grid`, `magnet` |
|
|
110
|
+
| Rendering | `ground` (`"field"`/`"clear"`), `preserve_drawing_buffer`, `quality`, `freeze_on_scroll`, `max_surfaces`, `z_index`, `canvas` |
|
|
111
|
+
| Reflex extras | `name` (registry name for `pulse`/`bump`, default `"default"`), `background_selector` (use an `img`/`canvas`/`video` element as the background), `on_ready` → `{"supported": bool, "reduced_motion": bool}` |
|
|
112
|
+
|
|
113
|
+
To follow Reflex's color mode, use `theme=rx.color_mode_cond("light", "dark")`.
|
|
114
|
+
|
|
115
|
+
### `plasma(*children, **props)`
|
|
116
|
+
|
|
117
|
+
A plasma surface. Anything else you pass (`class_name`, `id`, `aria_*`, `on_click`, CSS style props, …) goes to the rendered element.
|
|
118
|
+
|
|
119
|
+
| Prop | Description |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `as_` | Element to render: `"div"` (default), `"header"`, `"nav"`, `"section"`, `"a"`… |
|
|
122
|
+
| `radius`, `lean` | Corner radius (px); lean toward the pointer while standalone (px, `False` disables) |
|
|
123
|
+
| `tint`, `opacity`, `frost`, `elevation` | Per-surface look; joined surfaces blend these into each other |
|
|
124
|
+
| `padding` | Inner padding (px), halved on joined edges so gutters equal the free-edge inset |
|
|
125
|
+
| `fuse` | `False` for bars, docks and fixed chrome that must never blend |
|
|
126
|
+
| `draggable`, `snap`, `group` | Drag (arrow keys step one grid cell), snap on release, snap groups |
|
|
127
|
+
| `bounds_selector` | CSS selector of the drag area and grid origin (default: the viewport) |
|
|
128
|
+
| `offset`, `default_offset` | Controlled / initial `{"x", "y"}`; changes spring into place |
|
|
129
|
+
| `form_in`, `form_out` | Per-surface override of the provider's form-in/out |
|
|
130
|
+
| `on_drag_start`, `on_drag_end(offset)`, `on_join_change(joined)`, `on_forming`, `on_formed` | Backend events |
|
|
131
|
+
|
|
132
|
+
On `plasma`, `opacity` and `padding` are the plasma props (tint strength, join-aware padding in px), not the CSS properties. Don't set `transform`, `translate` or `scale` on a surface; the library animates those.
|
|
133
|
+
|
|
134
|
+
### `plasma_canvas(class_name=..., z_index=..., canvas_style=...)`
|
|
135
|
+
|
|
136
|
+
With `plasma_provider(canvas=False)`, place the canvas yourself. `canvas_style` is the real inline style (camelCase keys).
|
|
137
|
+
|
|
138
|
+
## Runtime helpers
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from reflex_plasma_ui import pulse, bump, pulse_on_press, NO_DRAG
|
|
142
|
+
|
|
143
|
+
rx.el.button("Pulse", on_click=pulse()) # at the last pointer-down
|
|
144
|
+
rx.el.button("Here", on_click=pulse(120, 80, strength=2))
|
|
145
|
+
rx.el.button("Bump", on_click=bump(0.8))
|
|
146
|
+
|
|
147
|
+
class S(rx.State):
|
|
148
|
+
def play(self):
|
|
149
|
+
return bump(0.6) # from a backend handler
|
|
150
|
+
|
|
151
|
+
rx.el.div("empty stage", custom_attrs=pulse_on_press()) # pulses on its own empty space
|
|
152
|
+
rx.el.ul(..., custom_attrs=NO_DRAG) # stays clickable in a draggable surface
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`pulse` and `bump` run entirely in the browser. They take `provider="name"` when there is more than one provider. `forming_style()` / `FORMING_CSS` fade a surface's contents in after its material has formed.
|
|
156
|
+
|
|
157
|
+
## Presets
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from reflex_plasma_ui import presets
|
|
161
|
+
|
|
162
|
+
plasma_provider(..., **presets.LOOKS["aqua"]) # lumen, studio, slate, aqua, neon, entropy
|
|
163
|
+
plasma_provider(..., **presets.MOTIONS["honey"]) # water, gel, honey, solid
|
|
164
|
+
plasma_provider(..., **presets.MATERIAL_PRESETS["mercury"]) # material + its physics and outline
|
|
165
|
+
plasma_provider(..., **presets.CLEAR_AS_WATER)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## A dialog above a scrim
|
|
169
|
+
|
|
170
|
+
The material is one canvas behind everything, so a dialog above a scrim needs a second provider. It uses a clear ground and refracts the first canvas:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
plasma_provider(
|
|
174
|
+
plasma_canvas(class_name="ground"),
|
|
175
|
+
...page...,
|
|
176
|
+
rx.cond(State.open, rx.el.div(
|
|
177
|
+
rx.el.div(class_name="scrim"),
|
|
178
|
+
plasma_provider(
|
|
179
|
+
plasma_canvas(canvas_style={"position": "absolute", "zIndex": 1}),
|
|
180
|
+
plasma(..., elevation=0.7, on_formed=State.reveal),
|
|
181
|
+
name="overlay",
|
|
182
|
+
background_selector="canvas.ground",
|
|
183
|
+
**presets.OVERLAY, # ground="clear", canvas=False, small budget
|
|
184
|
+
),
|
|
185
|
+
class_name="dialog-root", # position: fixed; inset: 0
|
|
186
|
+
)),
|
|
187
|
+
preserve_drawing_buffer=True,
|
|
188
|
+
canvas=False,
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## How it works
|
|
193
|
+
|
|
194
|
+
The package has no `library` import of its own. Three small wrapper components (`ReflexPlasmaProvider`, `ReflexPlasma`, `ReflexPlasmaCanvas`) are compiled into the page as custom code, and Reflex installs the pinned npm package from their imports. The wrappers:
|
|
195
|
+
|
|
196
|
+
- turn serialisable props into what React expects: selectors become ref objects and live elements;
|
|
197
|
+
- publish each provider's `usePlasmaRuntime()` on `window.__reflexPlasma[name]`, which is how `pulse`/`bump` reach it;
|
|
198
|
+
- track the last pointer-down, so `pulse()` lands where the user clicked.
|
|
199
|
+
|
|
200
|
+
## Demo app
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
cd plasma_ui_demo
|
|
204
|
+
uv run reflex run
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
| Page | What it shows |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
| `/` Playground | The upstream docs playground: six looks, motion presets, every provider prop bound to state, draggable panels with join tracking, arrange/scatter/add/remove, the generated Python snippet |
|
|
210
|
+
| `/workspace` | The upstream real-world example: bar, dock and four panels, focus elevation, layout persisted with `rx.LocalStorage`, `bump` from a backend event |
|
|
211
|
+
| `/materials` | All seven materials with their physics, grounds, light direction, roughness, anisotropy and outline controls |
|
|
212
|
+
| `/layers` | Backgrounds (field / color / image), form-in/out lifecycle events, pulses, and the two-provider dialog above a scrim |
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
uv venv && uv pip install -e ".[dev]"
|
|
218
|
+
cd plasma_ui_demo && uv run reflex run
|
|
219
|
+
PYTHONPATH=. uv run reflex component build # .pyi stubs + sdist/wheel in dist/
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The checks CI runs, which you can run locally in the same order:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
ruff check custom_components tests plasma_ui_demo/plasma_ui_demo
|
|
226
|
+
mypy
|
|
227
|
+
pytest
|
|
228
|
+
bandit -c pyproject.toml -r custom_components plasma_ui_demo/plasma_ui_demo
|
|
229
|
+
pip-audit --skip-editable
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) has the details, [RELEASING.md](RELEASING.md)
|
|
233
|
+
describes cutting a release, [SECURITY.md](SECURITY.md) the reporting process
|
|
234
|
+
and what this package's attack surface actually is, and
|
|
235
|
+
[CHANGELOG.md](CHANGELOG.md) what changed.
|
|
236
|
+
|
|
237
|
+
## Notes
|
|
238
|
+
|
|
239
|
+
- In `reflex run` (dev mode) React StrictMode mounts effects twice, so a surface added after the first render can fire `on_forming`/`on_formed` twice. Production builds fire them once.
|
|
240
|
+
- `PYTHONPATH=.` lets the stub generator import `custom_components.reflex_plasma_ui`.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT. Plasma UI is © Crux Garden, MIT.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# reflex-plasma-ui
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/quality.yml)
|
|
4
|
+
[](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/security.yml)
|
|
5
|
+
[](https://pypi.org/project/reflex-plasma-ui/)
|
|
6
|
+
[](https://pypi.org/project/reflex-plasma-ui/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
[Plasma UI](https://github.com/CruxGarden/plasma-ui) for [Reflex](https://reflex.dev): liquid panels rendered in WebGL on canvas. Every panel on the page is **one shared plasma** — surfaces fuse on contact, refract whatever is behind them, stretch when thrown and snap to a grid. The DOM stays ordinary, accessible HTML; the canvas only draws.
|
|
10
|
+
|
|
11
|
+
Wraps `@cruxgarden/plasma-ui@0.7.0` (pinned, installed automatically by Reflex). Works on Chrome, Edge, Firefox and Safari 16.4+ (WebGL2). Without WebGL2, every surface falls back to a frosted CSS panel with drag and snap intact. The effect is GPU-heavy, so it belongs on desktop browsers.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install reflex-plasma-ui
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import reflex as rx
|
|
21
|
+
from reflex_plasma_ui import plasma, plasma_provider, pulse
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Layout(rx.State):
|
|
25
|
+
offset: dict[str, float] = {"x": 24, "y": 24}
|
|
26
|
+
|
|
27
|
+
def save(self, offset: dict[str, float]):
|
|
28
|
+
self.offset = offset
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def index():
|
|
32
|
+
return plasma_provider(
|
|
33
|
+
plasma(rx.el.strong("My app"), as_="header", lean=False, fuse=False, padding=16),
|
|
34
|
+
rx.el.div(
|
|
35
|
+
plasma(
|
|
36
|
+
rx.el.h3("Inbox"),
|
|
37
|
+
rx.el.button("Pulse", on_click=pulse()),
|
|
38
|
+
draggable=True,
|
|
39
|
+
padding=20,
|
|
40
|
+
bounds_selector="#stage",
|
|
41
|
+
offset=Layout.offset,
|
|
42
|
+
on_drag_end=Layout.save,
|
|
43
|
+
width="240px",
|
|
44
|
+
),
|
|
45
|
+
id="stage",
|
|
46
|
+
style={"position": "relative", "height": "70vh"},
|
|
47
|
+
),
|
|
48
|
+
mood="tidal",
|
|
49
|
+
theme="dark",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
app = rx.App()
|
|
54
|
+
app.add_page(index)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> The canvas sits at `z-index: -1`, behind the page. If your app uses the Radix theme with its page background (`RadixThemesPlugin`, `rx.theme`), that opaque background hides the canvas: set `has_background=False` or make `.radix-themes` transparent.
|
|
58
|
+
|
|
59
|
+
## Components
|
|
60
|
+
|
|
61
|
+
### `plasma_provider(*children, **props)`
|
|
62
|
+
|
|
63
|
+
Owns the renderer and every shared setting. All `<PlasmaProvider>` props are available in snake_case. Any prop left unset keeps the library default, and every prop can be bound to state, so changes go live.
|
|
64
|
+
|
|
65
|
+
| Group | Props |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| Look | `mood` (`"tidal"`/`"aurora"`/`"ember"` or a custom `{colors, blend, spring}` dict), `theme` (`auto`/`light`/`dark`), `background` (CSS color or image URL), `radius`, `blend`, `tint`, `opacity`, `frost`, `elevation`, `smoothness`, `refraction`, `dispersion` |
|
|
68
|
+
| Rim & sheen | `rim`, `rim_color` (`"iridescent"`, `"tint"` or hex), `rim_width`, `highlight`, `edge_line`, `shimmer`, `shimmer_speed`, `glow`, `wash`, `grain`, `background_blur` |
|
|
69
|
+
| Material | `material` (`plasma`, `crystal`, `metal`, `mercury`, `wood`, `stone`, `cloud`), `light_dir`, `roughness`, `anisotropy`, `edge`, `edge_scale`, `edge_sharpness`, `thickness`, `tension` |
|
|
70
|
+
| Motion | `viscosity`, `stretch`, `flow`, `form_in`, `form_speed`, `form_out` |
|
|
71
|
+
| Pointer & layout | `pointer_drop`, `pointer_pull`, `ambient_drops`, `grid`, `magnet` |
|
|
72
|
+
| Rendering | `ground` (`"field"`/`"clear"`), `preserve_drawing_buffer`, `quality`, `freeze_on_scroll`, `max_surfaces`, `z_index`, `canvas` |
|
|
73
|
+
| Reflex extras | `name` (registry name for `pulse`/`bump`, default `"default"`), `background_selector` (use an `img`/`canvas`/`video` element as the background), `on_ready` → `{"supported": bool, "reduced_motion": bool}` |
|
|
74
|
+
|
|
75
|
+
To follow Reflex's color mode, use `theme=rx.color_mode_cond("light", "dark")`.
|
|
76
|
+
|
|
77
|
+
### `plasma(*children, **props)`
|
|
78
|
+
|
|
79
|
+
A plasma surface. Anything else you pass (`class_name`, `id`, `aria_*`, `on_click`, CSS style props, …) goes to the rendered element.
|
|
80
|
+
|
|
81
|
+
| Prop | Description |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `as_` | Element to render: `"div"` (default), `"header"`, `"nav"`, `"section"`, `"a"`… |
|
|
84
|
+
| `radius`, `lean` | Corner radius (px); lean toward the pointer while standalone (px, `False` disables) |
|
|
85
|
+
| `tint`, `opacity`, `frost`, `elevation` | Per-surface look; joined surfaces blend these into each other |
|
|
86
|
+
| `padding` | Inner padding (px), halved on joined edges so gutters equal the free-edge inset |
|
|
87
|
+
| `fuse` | `False` for bars, docks and fixed chrome that must never blend |
|
|
88
|
+
| `draggable`, `snap`, `group` | Drag (arrow keys step one grid cell), snap on release, snap groups |
|
|
89
|
+
| `bounds_selector` | CSS selector of the drag area and grid origin (default: the viewport) |
|
|
90
|
+
| `offset`, `default_offset` | Controlled / initial `{"x", "y"}`; changes spring into place |
|
|
91
|
+
| `form_in`, `form_out` | Per-surface override of the provider's form-in/out |
|
|
92
|
+
| `on_drag_start`, `on_drag_end(offset)`, `on_join_change(joined)`, `on_forming`, `on_formed` | Backend events |
|
|
93
|
+
|
|
94
|
+
On `plasma`, `opacity` and `padding` are the plasma props (tint strength, join-aware padding in px), not the CSS properties. Don't set `transform`, `translate` or `scale` on a surface; the library animates those.
|
|
95
|
+
|
|
96
|
+
### `plasma_canvas(class_name=..., z_index=..., canvas_style=...)`
|
|
97
|
+
|
|
98
|
+
With `plasma_provider(canvas=False)`, place the canvas yourself. `canvas_style` is the real inline style (camelCase keys).
|
|
99
|
+
|
|
100
|
+
## Runtime helpers
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from reflex_plasma_ui import pulse, bump, pulse_on_press, NO_DRAG
|
|
104
|
+
|
|
105
|
+
rx.el.button("Pulse", on_click=pulse()) # at the last pointer-down
|
|
106
|
+
rx.el.button("Here", on_click=pulse(120, 80, strength=2))
|
|
107
|
+
rx.el.button("Bump", on_click=bump(0.8))
|
|
108
|
+
|
|
109
|
+
class S(rx.State):
|
|
110
|
+
def play(self):
|
|
111
|
+
return bump(0.6) # from a backend handler
|
|
112
|
+
|
|
113
|
+
rx.el.div("empty stage", custom_attrs=pulse_on_press()) # pulses on its own empty space
|
|
114
|
+
rx.el.ul(..., custom_attrs=NO_DRAG) # stays clickable in a draggable surface
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`pulse` and `bump` run entirely in the browser. They take `provider="name"` when there is more than one provider. `forming_style()` / `FORMING_CSS` fade a surface's contents in after its material has formed.
|
|
118
|
+
|
|
119
|
+
## Presets
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from reflex_plasma_ui import presets
|
|
123
|
+
|
|
124
|
+
plasma_provider(..., **presets.LOOKS["aqua"]) # lumen, studio, slate, aqua, neon, entropy
|
|
125
|
+
plasma_provider(..., **presets.MOTIONS["honey"]) # water, gel, honey, solid
|
|
126
|
+
plasma_provider(..., **presets.MATERIAL_PRESETS["mercury"]) # material + its physics and outline
|
|
127
|
+
plasma_provider(..., **presets.CLEAR_AS_WATER)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## A dialog above a scrim
|
|
131
|
+
|
|
132
|
+
The material is one canvas behind everything, so a dialog above a scrim needs a second provider. It uses a clear ground and refracts the first canvas:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
plasma_provider(
|
|
136
|
+
plasma_canvas(class_name="ground"),
|
|
137
|
+
...page...,
|
|
138
|
+
rx.cond(State.open, rx.el.div(
|
|
139
|
+
rx.el.div(class_name="scrim"),
|
|
140
|
+
plasma_provider(
|
|
141
|
+
plasma_canvas(canvas_style={"position": "absolute", "zIndex": 1}),
|
|
142
|
+
plasma(..., elevation=0.7, on_formed=State.reveal),
|
|
143
|
+
name="overlay",
|
|
144
|
+
background_selector="canvas.ground",
|
|
145
|
+
**presets.OVERLAY, # ground="clear", canvas=False, small budget
|
|
146
|
+
),
|
|
147
|
+
class_name="dialog-root", # position: fixed; inset: 0
|
|
148
|
+
)),
|
|
149
|
+
preserve_drawing_buffer=True,
|
|
150
|
+
canvas=False,
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## How it works
|
|
155
|
+
|
|
156
|
+
The package has no `library` import of its own. Three small wrapper components (`ReflexPlasmaProvider`, `ReflexPlasma`, `ReflexPlasmaCanvas`) are compiled into the page as custom code, and Reflex installs the pinned npm package from their imports. The wrappers:
|
|
157
|
+
|
|
158
|
+
- turn serialisable props into what React expects: selectors become ref objects and live elements;
|
|
159
|
+
- publish each provider's `usePlasmaRuntime()` on `window.__reflexPlasma[name]`, which is how `pulse`/`bump` reach it;
|
|
160
|
+
- track the last pointer-down, so `pulse()` lands where the user clicked.
|
|
161
|
+
|
|
162
|
+
## Demo app
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cd plasma_ui_demo
|
|
166
|
+
uv run reflex run
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| Page | What it shows |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `/` Playground | The upstream docs playground: six looks, motion presets, every provider prop bound to state, draggable panels with join tracking, arrange/scatter/add/remove, the generated Python snippet |
|
|
172
|
+
| `/workspace` | The upstream real-world example: bar, dock and four panels, focus elevation, layout persisted with `rx.LocalStorage`, `bump` from a backend event |
|
|
173
|
+
| `/materials` | All seven materials with their physics, grounds, light direction, roughness, anisotropy and outline controls |
|
|
174
|
+
| `/layers` | Backgrounds (field / color / image), form-in/out lifecycle events, pulses, and the two-provider dialog above a scrim |
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
uv venv && uv pip install -e ".[dev]"
|
|
180
|
+
cd plasma_ui_demo && uv run reflex run
|
|
181
|
+
PYTHONPATH=. uv run reflex component build # .pyi stubs + sdist/wheel in dist/
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The checks CI runs, which you can run locally in the same order:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
ruff check custom_components tests plasma_ui_demo/plasma_ui_demo
|
|
188
|
+
mypy
|
|
189
|
+
pytest
|
|
190
|
+
bandit -c pyproject.toml -r custom_components plasma_ui_demo/plasma_ui_demo
|
|
191
|
+
pip-audit --skip-editable
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) has the details, [RELEASING.md](RELEASING.md)
|
|
195
|
+
describes cutting a release, [SECURITY.md](SECURITY.md) the reporting process
|
|
196
|
+
and what this package's attack surface actually is, and
|
|
197
|
+
[CHANGELOG.md](CHANGELOG.md) what changed.
|
|
198
|
+
|
|
199
|
+
## Notes
|
|
200
|
+
|
|
201
|
+
- In `reflex run` (dev mode) React StrictMode mounts effects twice, so a surface added after the first render can fire `on_forming`/`on_formed` twice. Production builds fire them once.
|
|
202
|
+
- `PYTHONPATH=.` lets the stub generator import `custom_components.reflex_plasma_ui`.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT. Plasma UI is © Crux Garden, MIT.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""reflex-plasma-ui: liquid WebGL panels (Plasma UI) for Reflex."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _version
|
|
5
|
+
|
|
6
|
+
from . import presets
|
|
7
|
+
from .plasma_ui import (
|
|
8
|
+
GroundName,
|
|
9
|
+
MaterialName,
|
|
10
|
+
MoodName,
|
|
11
|
+
Plasma,
|
|
12
|
+
PlasmaCanvas,
|
|
13
|
+
PlasmaProvider,
|
|
14
|
+
ThemeName,
|
|
15
|
+
plasma,
|
|
16
|
+
plasma_canvas,
|
|
17
|
+
plasma_provider,
|
|
18
|
+
)
|
|
19
|
+
from .runtime import (
|
|
20
|
+
FORMED_EVENT,
|
|
21
|
+
FORMING_ATTR,
|
|
22
|
+
FORMING_CSS,
|
|
23
|
+
FORMING_EVENT,
|
|
24
|
+
NO_DRAG,
|
|
25
|
+
NODRAG_ATTR,
|
|
26
|
+
bump,
|
|
27
|
+
forming_style,
|
|
28
|
+
pulse,
|
|
29
|
+
pulse_on_press,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"FORMED_EVENT",
|
|
34
|
+
"FORMING_ATTR",
|
|
35
|
+
"FORMING_CSS",
|
|
36
|
+
"FORMING_EVENT",
|
|
37
|
+
"NODRAG_ATTR",
|
|
38
|
+
"NO_DRAG",
|
|
39
|
+
"GroundName",
|
|
40
|
+
"MaterialName",
|
|
41
|
+
"MoodName",
|
|
42
|
+
"Plasma",
|
|
43
|
+
"PlasmaCanvas",
|
|
44
|
+
"PlasmaProvider",
|
|
45
|
+
"ThemeName",
|
|
46
|
+
"__version__",
|
|
47
|
+
"bump",
|
|
48
|
+
"forming_style",
|
|
49
|
+
"plasma",
|
|
50
|
+
"plasma_canvas",
|
|
51
|
+
"plasma_provider",
|
|
52
|
+
"presets",
|
|
53
|
+
"pulse",
|
|
54
|
+
"pulse_on_press",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
__version__ = _version("reflex-plasma-ui")
|
|
59
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
60
|
+
__version__ = "0.0.0+unknown"
|
|
61
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""JavaScript glue compiled into every page that uses a plasma component.
|
|
2
|
+
|
|
3
|
+
Plasma UI is React-first: it hands out its runtime (``pulse``, ``bump``)
|
|
4
|
+
through hooks and takes DOM elements / ref objects for ``bounds`` and
|
|
5
|
+
``background``. Reflex props are serialisable values, so three small wrapper
|
|
6
|
+
components bridge the gap:
|
|
7
|
+
|
|
8
|
+
* ``ReflexPlasmaProvider`` resolves ``backgroundSelector`` to a live element
|
|
9
|
+
and mounts ``ReflexPlasmaBridge``, which publishes the provider runtime on
|
|
10
|
+
``window.__reflexPlasma`` under the provider ``name`` and fires ``onReady``.
|
|
11
|
+
* ``ReflexPlasma`` turns ``boundsSelector`` into a ref-like object.
|
|
12
|
+
* ``ReflexPlasmaCanvas`` forwards ``canvasStyle`` as the real inline ``style``
|
|
13
|
+
(Reflex turns ``style=`` into an emotion class, which cannot beat the
|
|
14
|
+
canvas's own inline ``position: fixed``).
|
|
15
|
+
|
|
16
|
+
The registry also tracks the last pointer-down position, so ``pulse()`` with
|
|
17
|
+
no coordinates lands where the user just clicked, and it pulses automatically
|
|
18
|
+
on elements marked with ``data-plasma-pulse``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
NPM_PACKAGE = "@cruxgarden/plasma-ui"
|
|
22
|
+
NPM_VERSION = "0.7.0"
|
|
23
|
+
NPM_SPEC = f"{NPM_PACKAGE}@{NPM_VERSION}"
|
|
24
|
+
|
|
25
|
+
REGISTRY_CODE = r"""
|
|
26
|
+
const __rxPlasma = (() => {
|
|
27
|
+
if (typeof window === "undefined") return null;
|
|
28
|
+
if (window.__reflexPlasma) return window.__reflexPlasma;
|
|
29
|
+
const reg = {
|
|
30
|
+
runtimes: {},
|
|
31
|
+
last: { x: window.innerWidth / 2, y: window.innerHeight / 2 },
|
|
32
|
+
get(name) {
|
|
33
|
+
if (name && reg.runtimes[name]) return reg.runtimes[name];
|
|
34
|
+
return reg.runtimes.default || Object.values(reg.runtimes)[0] || null;
|
|
35
|
+
},
|
|
36
|
+
pulse(x, y, strength, name) {
|
|
37
|
+
const rt = reg.get(name);
|
|
38
|
+
if (!rt) return;
|
|
39
|
+
rt.pulse(x ?? reg.last.x, y ?? reg.last.y, strength ?? 1);
|
|
40
|
+
},
|
|
41
|
+
bump(energy, name) {
|
|
42
|
+
const rt = reg.get(name);
|
|
43
|
+
if (rt) rt.bump(energy ?? 0.6);
|
|
44
|
+
},
|
|
45
|
+
info(name) {
|
|
46
|
+
const rt = reg.get(name);
|
|
47
|
+
return rt ? { supported: rt.supported, reduced_motion: rt.reducedMotion } : null;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
window.addEventListener("pointerdown", (e) => {
|
|
51
|
+
reg.last = { x: e.clientX, y: e.clientY };
|
|
52
|
+
const t = e.target;
|
|
53
|
+
if (t && t.hasAttribute && t.hasAttribute("data-plasma-pulse")) {
|
|
54
|
+
const s = Number(t.getAttribute("data-plasma-pulse-strength") ?? 1);
|
|
55
|
+
reg.pulse(e.clientX, e.clientY, Number.isFinite(s) ? s : 1, t.getAttribute("data-plasma-pulse") || undefined);
|
|
56
|
+
}
|
|
57
|
+
}, true);
|
|
58
|
+
window.__reflexPlasma = reg;
|
|
59
|
+
return reg;
|
|
60
|
+
})();
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
BRIDGE_CODE = r"""
|
|
64
|
+
function ReflexPlasmaBridge({ name, onReady }) {
|
|
65
|
+
const rt = usePlasmaRuntime();
|
|
66
|
+
const readyRef = useRef(onReady);
|
|
67
|
+
useEffect(() => { readyRef.current = onReady; });
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!__rxPlasma) return;
|
|
70
|
+
__rxPlasma.runtimes[name] = rt;
|
|
71
|
+
return () => { if (__rxPlasma.runtimes[name] === rt) delete __rxPlasma.runtimes[name]; };
|
|
72
|
+
}, [rt, name]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (rt.renderer || !rt.supported) {
|
|
75
|
+
readyRef.current?.({ supported: rt.supported, reduced_motion: rt.reducedMotion });
|
|
76
|
+
}
|
|
77
|
+
}, [rt.renderer, rt.supported, rt.reducedMotion]);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
PROVIDER_CODE = r"""
|
|
83
|
+
function ReflexPlasmaProvider({ name = "default", backgroundSelector, background, onReady, children, ...props }) {
|
|
84
|
+
const [bgEl, setBgEl] = useState(null);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!backgroundSelector) { setBgEl(null); return; }
|
|
87
|
+
let raf = 0;
|
|
88
|
+
const find = () => {
|
|
89
|
+
const el = document.querySelector(backgroundSelector);
|
|
90
|
+
if (el) setBgEl(el); else raf = requestAnimationFrame(find);
|
|
91
|
+
};
|
|
92
|
+
find();
|
|
93
|
+
return () => cancelAnimationFrame(raf);
|
|
94
|
+
}, [backgroundSelector]);
|
|
95
|
+
const bg = backgroundSelector ? (bgEl ?? undefined) : (background || undefined);
|
|
96
|
+
return createElement(
|
|
97
|
+
PlasmaProvider,
|
|
98
|
+
{ ...props, background: bg },
|
|
99
|
+
createElement(ReflexPlasmaBridge, { name, onReady }),
|
|
100
|
+
children,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
SURFACE_CODE = r"""
|
|
106
|
+
const ReflexPlasma = forwardRef(function ReflexPlasma({ boundsSelector, ...props }, ref) {
|
|
107
|
+
const bounds = useMemo(
|
|
108
|
+
() => boundsSelector ? { get current() { return document.querySelector(boundsSelector); } } : undefined,
|
|
109
|
+
[boundsSelector],
|
|
110
|
+
);
|
|
111
|
+
return createElement(Plasma, { ...props, bounds, ref });
|
|
112
|
+
});
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
CANVAS_CODE = r"""
|
|
116
|
+
function ReflexPlasmaCanvas({ canvasStyle, className, zIndex }) {
|
|
117
|
+
return createElement(PlasmaCanvas, { className, zIndex, style: canvasStyle });
|
|
118
|
+
}
|
|
119
|
+
"""
|