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.
@@ -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
+ [![quality](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/quality.yml/badge.svg?branch=main)](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/quality.yml)
42
+ [![security](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/security.yml)
43
+ [![PyPI](https://img.shields.io/pypi/v/reflex-plasma-ui.svg)](https://pypi.org/project/reflex-plasma-ui/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/reflex-plasma-ui.svg)](https://pypi.org/project/reflex-plasma-ui/)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ [![quality](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/quality.yml/badge.svg?branch=main)](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/quality.yml)
4
+ [![security](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/ecrespo/reflex-plasma-ui/actions/workflows/security.yml)
5
+ [![PyPI](https://img.shields.io/pypi/v/reflex-plasma-ui.svg)](https://pypi.org/project/reflex-plasma-ui/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/reflex-plasma-ui.svg)](https://pypi.org/project/reflex-plasma-ui/)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ """