reflex-reactbits 0.1.1__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.
- reflex_reactbits/__init__.py +12 -0
- reflex_reactbits/animations.py +2128 -0
- reflex_reactbits/animations.pyi +2043 -0
- reflex_reactbits/backgrounds.py +3503 -0
- reflex_reactbits/backgrounds.pyi +3149 -0
- reflex_reactbits/base.py +97 -0
- reflex_reactbits/base.pyi +58 -0
- reflex_reactbits/catalog.json +26580 -0
- reflex_reactbits/catalog.py +23 -0
- reflex_reactbits/cli.py +125 -0
- reflex_reactbits/components.py +2383 -0
- reflex_reactbits/components.pyi +2430 -0
- reflex_reactbits/micro.py +2336 -0
- reflex_reactbits/micro.pyi +1858 -0
- reflex_reactbits/py.typed +0 -0
- reflex_reactbits/source.py +236 -0
- reflex_reactbits/text_animations.py +1629 -0
- reflex_reactbits/text_animations.pyi +1671 -0
- reflex_reactbits-0.1.1.dist-info/METADATA +223 -0
- reflex_reactbits-0.1.1.dist-info/RECORD +24 -0
- reflex_reactbits-0.1.1.dist-info/WHEEL +5 -0
- reflex_reactbits-0.1.1.dist-info/entry_points.txt +2 -0
- reflex_reactbits-0.1.1.dist-info/licenses/LICENSE +21 -0
- reflex_reactbits-0.1.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Fetch React Bits component sources into the Reflex app that uses them.
|
|
2
|
+
|
|
3
|
+
React Bits (https://reactbits.dev) is distributed as copy-paste source code,
|
|
4
|
+
not as an npm package, and its license (MIT + Commons Clause) does not allow
|
|
5
|
+
redistributing the components themselves in a bundle. This package therefore
|
|
6
|
+
ships only the Python wrappers: the first time a component is used, its
|
|
7
|
+
original JSX/CSS source is copied into the app's ``assets/reactbits/``
|
|
8
|
+
directory, exactly as the official ``shadcn``/``jsrepo`` CLIs would copy it
|
|
9
|
+
into a React project. The files are then owned by the app and can be edited
|
|
10
|
+
freely; existing files are never overwritten.
|
|
11
|
+
|
|
12
|
+
Sources are resolved in this order:
|
|
13
|
+
|
|
14
|
+
1. ``REACTBITS_SOURCE_DIR`` - path to a local clone of the React Bits repository.
|
|
15
|
+
2. GitHub raw files of ``DavidHDev/react-bits`` at ``REACTBITS_REF`` (defaults to
|
|
16
|
+
the commit these wrappers were generated and tested against).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import urllib.error
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections.abc import Iterable
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("reflex_reactbits")
|
|
30
|
+
|
|
31
|
+
#: Commit of DavidHDev/react-bits the wrappers were generated from.
|
|
32
|
+
PINNED_REF = "5fc9addb5b2362043332ad6d403bb436f2596318"
|
|
33
|
+
|
|
34
|
+
#: Directory (inside the app's ``assets/``) that receives the component sources.
|
|
35
|
+
ASSETS_SUBDIR = "reactbits"
|
|
36
|
+
|
|
37
|
+
RAW_URL = "https://raw.githubusercontent.com/DavidHDev/react-bits/{ref}/{path}"
|
|
38
|
+
|
|
39
|
+
# Files a component loads from the React Bits site's public/ folder at runtime.
|
|
40
|
+
# They are copied next to the component and imported with Vite's ``?url``.
|
|
41
|
+
EXTRA_FILES: dict[tuple[str, str], dict[str, str]] = {
|
|
42
|
+
("Components", "FluidGlass"): {
|
|
43
|
+
"lens.glb": "public/assets/3d/lens.glb",
|
|
44
|
+
"cube.glb": "public/assets/3d/cube.glb",
|
|
45
|
+
"bar.glb": "public/assets/3d/bar.glb",
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_TEXT_SUFFIXES = {".js", ".jsx", ".css", ".ts", ".tsx", ".json"}
|
|
50
|
+
|
|
51
|
+
# Small, surgical patches so the plain JS-CSS variant runs inside a Reflex app
|
|
52
|
+
# (which has no react-router, Chakra UI or Tailwind by default).
|
|
53
|
+
PATCHES: dict[tuple[str, str], list[tuple[str, str]]] = {
|
|
54
|
+
# Ballpit renders its own <canvas> through a React ref, so React keeps and
|
|
55
|
+
# reuses that DOM node across remounts. forceContextLoss() loses the context
|
|
56
|
+
# of that canvas permanently: on the next mount getContext() hands back the
|
|
57
|
+
# same lost context, gl.getShaderPrecisionFormat() returns null and three.js
|
|
58
|
+
# throws "Cannot read properties of null (reading 'precision')" while
|
|
59
|
+
# building WebGLCapabilities. dispose() alone already frees the GPU
|
|
60
|
+
# resources, and reusing the live context avoids leaking one per remount.
|
|
61
|
+
("Backgrounds", "Ballpit.jsx"): [
|
|
62
|
+
(
|
|
63
|
+
" this.renderer.dispose();\n this.renderer.forceContextLoss();\n",
|
|
64
|
+
" this.renderer.dispose();\n",
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
# Hyperspeed loads its SMAA assets asynchronously and then calls init().
|
|
68
|
+
# If the component unmounts during that window (React StrictMode remounts
|
|
69
|
+
# every effect in dev), the cleanup already ran dispose(), which calls
|
|
70
|
+
# renderer.forceContextLoss(). The pending promise then initialises a dead
|
|
71
|
+
# App: postprocessing reads renderer.getContext().getContextAttributes(),
|
|
72
|
+
# which is null on a lost context, and throws "can't access property alpha".
|
|
73
|
+
("Backgrounds", "Hyperspeed.jsx"): [
|
|
74
|
+
(
|
|
75
|
+
" init() {\n this.initPasses();",
|
|
76
|
+
" init() {\n if (this.disposed) return;\n this.initPasses();",
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
("TextAnimations", "BlurText.jsx"): [
|
|
80
|
+
(
|
|
81
|
+
'className="inline-block will-change-[transform,filter,opacity]"',
|
|
82
|
+
"style={{ display: 'inline-block', willChange: 'transform, filter, opacity' }}",
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
("Components", "PillNav.jsx"): [
|
|
86
|
+
("import { Link } from 'react-router-dom';\n", ""),
|
|
87
|
+
("<Link", "<a"),
|
|
88
|
+
("</Link>", "</a>"),
|
|
89
|
+
(" to={", " href={"),
|
|
90
|
+
],
|
|
91
|
+
("Components", "ElasticSlider.jsx"): [
|
|
92
|
+
("import { Icon } from '@chakra-ui/react';\n", ""),
|
|
93
|
+
("<Icon as={RiVolumeDownFill} />", "<RiVolumeDownFill />"),
|
|
94
|
+
("<Icon as={RiVolumeUpFill} />", "<RiVolumeUpFill />"),
|
|
95
|
+
],
|
|
96
|
+
("Components", "Lanyard.jsx"): [
|
|
97
|
+
("from './card.glb';", "from './card.glb?url';"),
|
|
98
|
+
],
|
|
99
|
+
("Components", "FluidGlass.jsx"): [
|
|
100
|
+
(
|
|
101
|
+
"import * as THREE from 'three';\n",
|
|
102
|
+
"import * as THREE from 'three';\n"
|
|
103
|
+
"import lensGlb from './lens.glb?url';\n"
|
|
104
|
+
"import cubeGlb from './cube.glb?url';\n"
|
|
105
|
+
"import barGlb from './bar.glb?url';\n",
|
|
106
|
+
),
|
|
107
|
+
('glb="/assets/3d/lens.glb"', "glb={lensGlb}"),
|
|
108
|
+
('glb="/assets/3d/cube.glb"', "glb={cubeGlb}"),
|
|
109
|
+
('glb="/assets/3d/bar.glb"', "glb={barGlb}"),
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def app_assets_dir() -> Path:
|
|
115
|
+
"""Return the ``assets/`` directory of the Reflex app being compiled."""
|
|
116
|
+
return Path.cwd() / "assets"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def component_dir(category: str, name: str, assets_dir: Path | None = None) -> Path:
|
|
120
|
+
"""Return where a component's source lives inside the app."""
|
|
121
|
+
return (assets_dir or app_assets_dir()) / ASSETS_SUBDIR / category / name
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _apply_patches(category: str, file: str, data: bytes) -> bytes:
|
|
125
|
+
patches = PATCHES.get((category, file))
|
|
126
|
+
if not patches:
|
|
127
|
+
return data
|
|
128
|
+
text = data.decode("utf-8")
|
|
129
|
+
for old, new in patches:
|
|
130
|
+
if old not in text:
|
|
131
|
+
# The patch anchors on upstream code at PINNED_REF. A miss means the
|
|
132
|
+
# vendored file keeps an import or a call the wrappers do not expect
|
|
133
|
+
# (for instance a dependency listed in DROPPED_DEPS, which is then
|
|
134
|
+
# never installed), so say so here rather than fail later in Vite.
|
|
135
|
+
logger.warning(
|
|
136
|
+
"React Bits patch for %s/%s no longer matches upstream and was skipped; "
|
|
137
|
+
"the vendored source may not work with this wrapper.", category, file,
|
|
138
|
+
)
|
|
139
|
+
continue
|
|
140
|
+
text = text.replace(old, new)
|
|
141
|
+
return text.encode("utf-8")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _repo_path(category: str, name: str, file: str) -> str:
|
|
145
|
+
extra = EXTRA_FILES.get((category, name), {})
|
|
146
|
+
return extra.get(file) or f"src/content/{category}/{name}/{file}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _read_local(repo_path: str) -> bytes | None:
|
|
150
|
+
root = os.environ.get("REACTBITS_SOURCE_DIR")
|
|
151
|
+
if not root:
|
|
152
|
+
return None
|
|
153
|
+
path = Path(root).expanduser() / repo_path
|
|
154
|
+
return path.read_bytes() if path.exists() else None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _read_remote(repo_path: str, ref: str) -> bytes:
|
|
158
|
+
url = RAW_URL.format(ref=ref, path=repo_path)
|
|
159
|
+
# RAW_URL is a constant https template, so the scheme can never be attacker-chosen.
|
|
160
|
+
request = urllib.request.Request(url, headers={"User-Agent": "reflex-reactbits"}) # noqa: S310
|
|
161
|
+
try:
|
|
162
|
+
with urllib.request.urlopen(request, timeout=30) as response: # noqa: S310 # nosec B310
|
|
163
|
+
return response.read()
|
|
164
|
+
except urllib.error.URLError as err:
|
|
165
|
+
msg = (
|
|
166
|
+
f"Could not download React Bits source {url}: {err}. Run "
|
|
167
|
+
"`reactbits add <Name>` with network access, or set REACTBITS_SOURCE_DIR "
|
|
168
|
+
"to a local clone of https://github.com/DavidHDev/react-bits."
|
|
169
|
+
)
|
|
170
|
+
local = os.environ.get("REACTBITS_SOURCE_DIR")
|
|
171
|
+
if local:
|
|
172
|
+
msg = (
|
|
173
|
+
f"REACTBITS_SOURCE_DIR is set to {local}, but it has no {repo_path}, "
|
|
174
|
+
f"so the download from {url} was attempted and failed: {err}. Check that "
|
|
175
|
+
"the clone is complete and matches the expected layout."
|
|
176
|
+
)
|
|
177
|
+
raise RuntimeError(msg) from err
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def backend_only() -> bool:
|
|
181
|
+
"""Whether this process runs without a frontend to copy component sources for."""
|
|
182
|
+
return os.environ.get("REFLEX_BACKEND_ONLY", "").lower() in {"1", "true", "yes"}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def ensure_component_source(
|
|
186
|
+
category: str,
|
|
187
|
+
name: str,
|
|
188
|
+
files: Iterable[str],
|
|
189
|
+
*,
|
|
190
|
+
assets_dir: Path | None = None,
|
|
191
|
+
ref: str | None = None,
|
|
192
|
+
force: bool = False,
|
|
193
|
+
) -> list[Path]:
|
|
194
|
+
"""Make sure the component's source files exist in the app's assets.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
category: React Bits category folder (e.g. ``TextAnimations``).
|
|
198
|
+
name: Component folder name (e.g. ``BlurText``).
|
|
199
|
+
files: File names that make up the component.
|
|
200
|
+
assets_dir: Override the app assets directory.
|
|
201
|
+
ref: Git ref of the React Bits repository to download from.
|
|
202
|
+
force: Re-download and overwrite existing files.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The list of files written during this call.
|
|
206
|
+
"""
|
|
207
|
+
target = component_dir(category, name, assets_dir)
|
|
208
|
+
ref = ref or os.environ.get("REACTBITS_REF") or PINNED_REF
|
|
209
|
+
written: list[Path] = []
|
|
210
|
+
for file in [*files, *EXTRA_FILES.get((category, name), {})]:
|
|
211
|
+
dest = target / file
|
|
212
|
+
if dest.exists() and not force:
|
|
213
|
+
continue
|
|
214
|
+
repo_path = _repo_path(category, name, file)
|
|
215
|
+
data = _read_local(repo_path)
|
|
216
|
+
if data is None:
|
|
217
|
+
data = _read_remote(repo_path, ref)
|
|
218
|
+
if Path(file).suffix in _TEXT_SUFFIXES:
|
|
219
|
+
data = _apply_patches(category, file, data)
|
|
220
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
|
222
|
+
tmp.write_bytes(data)
|
|
223
|
+
tmp.replace(dest)
|
|
224
|
+
written.append(dest)
|
|
225
|
+
if written:
|
|
226
|
+
logger.info("reflex-reactbits: added %s/%s to %s", category, name, target)
|
|
227
|
+
return written
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def remove_component_source(category: str, name: str, assets_dir: Path | None = None) -> bool:
|
|
231
|
+
"""Delete a component's copied source from the app assets."""
|
|
232
|
+
target = component_dir(category, name, assets_dir)
|
|
233
|
+
if target.exists():
|
|
234
|
+
shutil.rmtree(target)
|
|
235
|
+
return True
|
|
236
|
+
return False
|