dash-startup-loading-plugin 0.1.0__tar.gz → 0.2.0__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.
- {dash_startup_loading_plugin-0.1.0/src/dash_startup_loading_plugin.egg-info → dash_startup_loading_plugin-0.2.0}/PKG-INFO +173 -17
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/README.md +172 -16
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/pyproject.toml +1 -1
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin/__init__.py +1 -1
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin/plugin.py +55 -20
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin/resources/startup-loading.css +6 -8
- dash_startup_loading_plugin-0.2.0/src/dash_startup_loading_plugin/resources/startup-theme.js +102 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0/src/dash_startup_loading_plugin.egg-info}/PKG-INFO +173 -17
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin.egg-info/SOURCES.txt +1 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/tests/test_plugin.py +57 -8
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/LICENSE +0 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/setup.cfg +0 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin/resources/startup-loading.js +0 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin.egg-info/dependency_links.txt +0 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin.egg-info/entry_points.txt +0 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin.egg-info/requires.txt +0 -0
- {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dash-startup-loading-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin.
|
|
5
5
|
Author-email: Ethan Zhang <ethan.zhang2016@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,10 +20,10 @@ Dynamic: license-file
|
|
|
20
20
|
that displays a full-screen loading overlay while a Dash application performs
|
|
21
21
|
its initial browser-side startup.
|
|
22
22
|
|
|
23
|
-
The overlay
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
The overlay, CSS, and JavaScript are injected into the final HTML document with
|
|
24
|
+
`hooks.index`, so they are available before Dash and React mount the application
|
|
25
|
+
layout without additional startup resource requests. An app does not need to
|
|
26
|
+
copy assets or replace Dash's `index_string`.
|
|
27
27
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
@@ -32,8 +32,10 @@ does not need to copy assets or replace Dash's `index_string`.
|
|
|
32
32
|
- Waits for real rendered content and optional app-specific readiness selectors.
|
|
33
33
|
- Can wait for lazy-loading placeholders to disappear.
|
|
34
34
|
- Includes a timeout fallback, minimum display time, and fade-out transition.
|
|
35
|
-
-
|
|
36
|
-
|
|
35
|
+
- Inlines its small startup CSS and JavaScript, avoiding two additional requests.
|
|
36
|
+
- Resolves light and dark themes before first paint from Dash component
|
|
37
|
+
persistence, Tailwind root classes, Mantine color-scheme state, or the
|
|
38
|
+
operating-system preference.
|
|
37
39
|
- Exposes a small browser API and emits a completion event.
|
|
38
40
|
- Requires no changes to the app layout or callbacks.
|
|
39
41
|
|
|
@@ -49,7 +51,7 @@ discovery, resource hooks, and the index hook described in the official
|
|
|
49
51
|
## Installation
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
|
-
pip install dash-startup-loading-plugin
|
|
54
|
+
pip install "dash-startup-loading-plugin>=0.2.0"
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
The package declares the following entry point:
|
|
@@ -64,6 +66,17 @@ package therefore enables the default startup overlay for Dash applications in
|
|
|
64
66
|
that Python environment; an explicit import is only needed when changing its
|
|
65
67
|
configuration or using its Python API.
|
|
66
68
|
|
|
69
|
+
## What's new in 0.2.0
|
|
70
|
+
|
|
71
|
+
- Resolves the loading background before first paint from a persisted
|
|
72
|
+
`dash_antd_components.ConfigProvider.theme`.
|
|
73
|
+
- Supports Tailwind's `html.dark` and `html.light` classes.
|
|
74
|
+
- Supports Dash Mantine Components' `data-mantine-color-scheme` attribute and
|
|
75
|
+
`mantine-color-scheme-value` localStorage entry.
|
|
76
|
+
- Adds `theme_mode` and `dash_theme_component_id` configuration.
|
|
77
|
+
- Keeps the startup CSS and JavaScript inline, so theme detection does not add
|
|
78
|
+
network requests before Dash mounts.
|
|
79
|
+
|
|
67
80
|
## Quick start
|
|
68
81
|
|
|
69
82
|
The defaults work without any plugin-specific code:
|
|
@@ -152,14 +165,57 @@ immutable `StartupLoadingConfig` instance.
|
|
|
152
165
|
| `dark_background` | `"#0f0f0f"` | Dark-theme background color. |
|
|
153
166
|
| `color` | `"#1677ff"` | Light-theme spinner/current color. |
|
|
154
167
|
| `dark_color` | `"#4096ff"` | Dark-theme spinner/current color. |
|
|
168
|
+
| `theme_mode` | `"auto"` | Startup theme policy: `"auto"`, `"light"`, or `"dark"`. Auto follows persisted or active app state, then the operating-system preference. |
|
|
169
|
+
| `dash_theme_component_id` | `None` | Preferred Dash component ID when reading a persisted `theme` prop. When omitted, the first compatible persisted theme is used. |
|
|
155
170
|
| `spinner_size_px` | `28` | Default spinner width and height in pixels. |
|
|
156
171
|
| `spinner_stroke_px` | `3` | Default spinner stroke width in pixels. |
|
|
157
172
|
| `hide_default_loading` | `True` | Hide Dash's built-in initial `._dash-loading` indicator while the overlay is present. |
|
|
158
173
|
| `custom_loader_html` | `None` | Trusted HTML that replaces the default spinner. |
|
|
159
174
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
With `theme_mode="auto"`, the inline head bootstrap resolves the startup theme
|
|
176
|
+
in this order:
|
|
177
|
+
|
|
178
|
+
1. The persisted Dash `theme` for `dash_theme_component_id`, when configured.
|
|
179
|
+
2. An existing `html.dark` or `html.light` class, including Tailwind's manual
|
|
180
|
+
dark-mode convention.
|
|
181
|
+
3. `data-theme`, `data-color-scheme`, or `data-mantine-color-scheme` on
|
|
182
|
+
`<html>`.
|
|
183
|
+
4. Other Dash local persistence entries for a component's `theme` prop. Set
|
|
184
|
+
`dash_theme_component_id` when an app contains more than one persisted
|
|
185
|
+
theme-capable component.
|
|
186
|
+
5. Dash Mantine Components' `mantine-color-scheme-value` localStorage entry,
|
|
187
|
+
followed by the common `theme`, `color-theme`, and `color-scheme` keys.
|
|
188
|
+
6. `prefers-color-scheme`.
|
|
189
|
+
|
|
190
|
+
The result is written to `data-dash-startup-loading-theme` before the body is
|
|
191
|
+
parsed. This isolates the overlay styling without changing classes or data
|
|
192
|
+
attributes owned by Tailwind, Dash Mantine Components, or the application.
|
|
193
|
+
The stylesheet also adjusts its animation for `prefers-reduced-motion`.
|
|
194
|
+
|
|
195
|
+
### Dash Ant Design persistence
|
|
196
|
+
|
|
197
|
+
`dash_antd_components.ConfigProvider` can persist its `theme` prop. The startup
|
|
198
|
+
plugin reads that same Dash persistence record on the next page load:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import dash_antd_components as dac
|
|
202
|
+
from dash_startup_loading_plugin import configure
|
|
203
|
+
|
|
204
|
+
configure(dash_theme_component_id="theme-provider")
|
|
205
|
+
|
|
206
|
+
provider = dac.ConfigProvider(
|
|
207
|
+
app_content,
|
|
208
|
+
id="theme-provider",
|
|
209
|
+
persistence=True,
|
|
210
|
+
persisted_props=["theme"],
|
|
211
|
+
persistence_type="local",
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Update `theme-provider.theme` from a regular or clientside callback. Dash stores
|
|
216
|
+
the UI edit, `ConfigProvider` restores it after mount, and the startup plugin
|
|
217
|
+
uses it for the loading background before mount. See
|
|
218
|
+
[`examples/basic.py`](examples/basic.py) for the complete switcher.
|
|
163
219
|
|
|
164
220
|
### Custom loader markup
|
|
165
221
|
|
|
@@ -241,11 +297,18 @@ configure(
|
|
|
241
297
|
)
|
|
242
298
|
```
|
|
243
299
|
|
|
244
|
-
The plugin preserves Dash's normal index template, injects
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
300
|
+
The plugin preserves Dash's normal index template, injects its stylesheet before
|
|
301
|
+
`</head>`, and inserts the overlay followed by its startup script directly after
|
|
302
|
+
the opening `<body>` tag. The source CSS and JavaScript remain package resources
|
|
303
|
+
for maintainability, but the browser receives them inline and makes no separate
|
|
304
|
+
request for either file. The index hook uses priority `100`, so it runs before
|
|
305
|
+
lower-priority index hooks. If multiple hooks share the same priority, Dash does
|
|
306
|
+
not guarantee their relative order.
|
|
307
|
+
|
|
308
|
+
Because the resources are inline, applications with a strict Content Security
|
|
309
|
+
Policy must permit the injected style and script, or add nonce support in a
|
|
310
|
+
customized deployment. The previous external-resource form is more suitable
|
|
311
|
+
when a policy forbids all inline code.
|
|
249
312
|
|
|
250
313
|
## Scope and process model
|
|
251
314
|
|
|
@@ -294,15 +357,108 @@ Run the tests and example:
|
|
|
294
357
|
|
|
295
358
|
```bash
|
|
296
359
|
uv run pytest
|
|
297
|
-
uv run python examples/basic.py
|
|
360
|
+
uv run --with dash-ant-design python examples/basic.py
|
|
298
361
|
```
|
|
299
362
|
|
|
363
|
+
The example uses `dash_antd_components.ConfigProvider` to demonstrate native
|
|
364
|
+
theme persistence, so `dash-ant-design` is an example-only dependency rather
|
|
365
|
+
than a runtime dependency of the plugin.
|
|
366
|
+
|
|
300
367
|
Build the source distribution and wheel:
|
|
301
368
|
|
|
302
369
|
```bash
|
|
303
370
|
uv build
|
|
304
371
|
```
|
|
305
372
|
|
|
373
|
+
## Publishing to PyPI
|
|
374
|
+
|
|
375
|
+
PyPI does not allow replacing an existing distribution file. Confirm that the
|
|
376
|
+
version in `pyproject.toml` is new before building, then create the artifacts
|
|
377
|
+
from a clean `dist/` directory:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
rm -rf build dist src/*.egg-info
|
|
381
|
+
uv lock
|
|
382
|
+
uv run --extra test pytest
|
|
383
|
+
uvx ruff check src tests examples
|
|
384
|
+
uv build
|
|
385
|
+
uvx twine check dist/*
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Inspect the resulting files before upload:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
ls -lh dist/
|
|
392
|
+
unzip -l dist/dash_startup_loading_plugin-0.2.0-py3-none-any.whl
|
|
393
|
+
tar -tzf dist/dash_startup_loading_plugin-0.2.0.tar.gz
|
|
394
|
+
uv publish --dry-run dist/*
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### TestPyPI
|
|
398
|
+
|
|
399
|
+
Create a TestPyPI API token, then upload to its legacy upload endpoint:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
export UV_PUBLISH_TOKEN="pypi-your-test-token"
|
|
403
|
+
uv publish \
|
|
404
|
+
--publish-url https://test.pypi.org/legacy/ \
|
|
405
|
+
--check-url https://test.pypi.org/simple/ \
|
|
406
|
+
dist/*
|
|
407
|
+
unset UV_PUBLISH_TOKEN
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Test installation in an isolated environment:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
uv run --isolated \
|
|
414
|
+
--index-url https://test.pypi.org/simple/ \
|
|
415
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
416
|
+
--with dash-startup-loading-plugin==0.2.0 \
|
|
417
|
+
python -c "import dash_startup_loading_plugin; print('import ok')"
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The extra PyPI index is needed because TestPyPI may not contain the plugin's
|
|
421
|
+
runtime dependencies.
|
|
422
|
+
|
|
423
|
+
### PyPI with an API token
|
|
424
|
+
|
|
425
|
+
Create a project-scoped PyPI token and publish the checked artifacts:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
export UV_PUBLISH_TOKEN="pypi-your-project-token"
|
|
429
|
+
uv publish dist/*
|
|
430
|
+
unset UV_PUBLISH_TOKEN
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Do not place tokens in `pyproject.toml`, shell history, source control, or CI
|
|
434
|
+
logs.
|
|
435
|
+
|
|
436
|
+
### PyPI Trusted Publishing
|
|
437
|
+
|
|
438
|
+
For CI releases, configure the repository and workflow as a Trusted Publisher
|
|
439
|
+
in the PyPI project settings. The publishing job needs the
|
|
440
|
+
`id-token: write` permission and can upload without a long-lived token:
|
|
441
|
+
|
|
442
|
+
```yaml
|
|
443
|
+
permissions:
|
|
444
|
+
contents: read
|
|
445
|
+
id-token: write
|
|
446
|
+
|
|
447
|
+
steps:
|
|
448
|
+
- uses: actions/checkout@v4
|
|
449
|
+
- uses: astral-sh/setup-uv@v8
|
|
450
|
+
- run: uv build
|
|
451
|
+
- run: uv publish --trusted-publishing always
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
After publishing, verify the release:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
uv run --isolated \
|
|
458
|
+
--with dash-startup-loading-plugin==0.2.0 \
|
|
459
|
+
python -c "import dash_startup_loading_plugin; print('import ok')"
|
|
460
|
+
```
|
|
461
|
+
|
|
306
462
|
## License
|
|
307
463
|
|
|
308
464
|
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
that displays a full-screen loading overlay while a Dash application performs
|
|
5
5
|
its initial browser-side startup.
|
|
6
6
|
|
|
7
|
-
The overlay
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
The overlay, CSS, and JavaScript are injected into the final HTML document with
|
|
8
|
+
`hooks.index`, so they are available before Dash and React mount the application
|
|
9
|
+
layout without additional startup resource requests. An app does not need to
|
|
10
|
+
copy assets or replace Dash's `index_string`.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
@@ -16,8 +16,10 @@ does not need to copy assets or replace Dash's `index_string`.
|
|
|
16
16
|
- Waits for real rendered content and optional app-specific readiness selectors.
|
|
17
17
|
- Can wait for lazy-loading placeholders to disappear.
|
|
18
18
|
- Includes a timeout fallback, minimum display time, and fade-out transition.
|
|
19
|
-
-
|
|
20
|
-
|
|
19
|
+
- Inlines its small startup CSS and JavaScript, avoiding two additional requests.
|
|
20
|
+
- Resolves light and dark themes before first paint from Dash component
|
|
21
|
+
persistence, Tailwind root classes, Mantine color-scheme state, or the
|
|
22
|
+
operating-system preference.
|
|
21
23
|
- Exposes a small browser API and emits a completion event.
|
|
22
24
|
- Requires no changes to the app layout or callbacks.
|
|
23
25
|
|
|
@@ -33,7 +35,7 @@ discovery, resource hooks, and the index hook described in the official
|
|
|
33
35
|
## Installation
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
|
-
pip install dash-startup-loading-plugin
|
|
38
|
+
pip install "dash-startup-loading-plugin>=0.2.0"
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
The package declares the following entry point:
|
|
@@ -48,6 +50,17 @@ package therefore enables the default startup overlay for Dash applications in
|
|
|
48
50
|
that Python environment; an explicit import is only needed when changing its
|
|
49
51
|
configuration or using its Python API.
|
|
50
52
|
|
|
53
|
+
## What's new in 0.2.0
|
|
54
|
+
|
|
55
|
+
- Resolves the loading background before first paint from a persisted
|
|
56
|
+
`dash_antd_components.ConfigProvider.theme`.
|
|
57
|
+
- Supports Tailwind's `html.dark` and `html.light` classes.
|
|
58
|
+
- Supports Dash Mantine Components' `data-mantine-color-scheme` attribute and
|
|
59
|
+
`mantine-color-scheme-value` localStorage entry.
|
|
60
|
+
- Adds `theme_mode` and `dash_theme_component_id` configuration.
|
|
61
|
+
- Keeps the startup CSS and JavaScript inline, so theme detection does not add
|
|
62
|
+
network requests before Dash mounts.
|
|
63
|
+
|
|
51
64
|
## Quick start
|
|
52
65
|
|
|
53
66
|
The defaults work without any plugin-specific code:
|
|
@@ -136,14 +149,57 @@ immutable `StartupLoadingConfig` instance.
|
|
|
136
149
|
| `dark_background` | `"#0f0f0f"` | Dark-theme background color. |
|
|
137
150
|
| `color` | `"#1677ff"` | Light-theme spinner/current color. |
|
|
138
151
|
| `dark_color` | `"#4096ff"` | Dark-theme spinner/current color. |
|
|
152
|
+
| `theme_mode` | `"auto"` | Startup theme policy: `"auto"`, `"light"`, or `"dark"`. Auto follows persisted or active app state, then the operating-system preference. |
|
|
153
|
+
| `dash_theme_component_id` | `None` | Preferred Dash component ID when reading a persisted `theme` prop. When omitted, the first compatible persisted theme is used. |
|
|
139
154
|
| `spinner_size_px` | `28` | Default spinner width and height in pixels. |
|
|
140
155
|
| `spinner_stroke_px` | `3` | Default spinner stroke width in pixels. |
|
|
141
156
|
| `hide_default_loading` | `True` | Hide Dash's built-in initial `._dash-loading` indicator while the overlay is present. |
|
|
142
157
|
| `custom_loader_html` | `None` | Trusted HTML that replaces the default spinner. |
|
|
143
158
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
159
|
+
With `theme_mode="auto"`, the inline head bootstrap resolves the startup theme
|
|
160
|
+
in this order:
|
|
161
|
+
|
|
162
|
+
1. The persisted Dash `theme` for `dash_theme_component_id`, when configured.
|
|
163
|
+
2. An existing `html.dark` or `html.light` class, including Tailwind's manual
|
|
164
|
+
dark-mode convention.
|
|
165
|
+
3. `data-theme`, `data-color-scheme`, or `data-mantine-color-scheme` on
|
|
166
|
+
`<html>`.
|
|
167
|
+
4. Other Dash local persistence entries for a component's `theme` prop. Set
|
|
168
|
+
`dash_theme_component_id` when an app contains more than one persisted
|
|
169
|
+
theme-capable component.
|
|
170
|
+
5. Dash Mantine Components' `mantine-color-scheme-value` localStorage entry,
|
|
171
|
+
followed by the common `theme`, `color-theme`, and `color-scheme` keys.
|
|
172
|
+
6. `prefers-color-scheme`.
|
|
173
|
+
|
|
174
|
+
The result is written to `data-dash-startup-loading-theme` before the body is
|
|
175
|
+
parsed. This isolates the overlay styling without changing classes or data
|
|
176
|
+
attributes owned by Tailwind, Dash Mantine Components, or the application.
|
|
177
|
+
The stylesheet also adjusts its animation for `prefers-reduced-motion`.
|
|
178
|
+
|
|
179
|
+
### Dash Ant Design persistence
|
|
180
|
+
|
|
181
|
+
`dash_antd_components.ConfigProvider` can persist its `theme` prop. The startup
|
|
182
|
+
plugin reads that same Dash persistence record on the next page load:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import dash_antd_components as dac
|
|
186
|
+
from dash_startup_loading_plugin import configure
|
|
187
|
+
|
|
188
|
+
configure(dash_theme_component_id="theme-provider")
|
|
189
|
+
|
|
190
|
+
provider = dac.ConfigProvider(
|
|
191
|
+
app_content,
|
|
192
|
+
id="theme-provider",
|
|
193
|
+
persistence=True,
|
|
194
|
+
persisted_props=["theme"],
|
|
195
|
+
persistence_type="local",
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Update `theme-provider.theme` from a regular or clientside callback. Dash stores
|
|
200
|
+
the UI edit, `ConfigProvider` restores it after mount, and the startup plugin
|
|
201
|
+
uses it for the loading background before mount. See
|
|
202
|
+
[`examples/basic.py`](examples/basic.py) for the complete switcher.
|
|
147
203
|
|
|
148
204
|
### Custom loader markup
|
|
149
205
|
|
|
@@ -225,11 +281,18 @@ configure(
|
|
|
225
281
|
)
|
|
226
282
|
```
|
|
227
283
|
|
|
228
|
-
The plugin preserves Dash's normal index template, injects
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
284
|
+
The plugin preserves Dash's normal index template, injects its stylesheet before
|
|
285
|
+
`</head>`, and inserts the overlay followed by its startup script directly after
|
|
286
|
+
the opening `<body>` tag. The source CSS and JavaScript remain package resources
|
|
287
|
+
for maintainability, but the browser receives them inline and makes no separate
|
|
288
|
+
request for either file. The index hook uses priority `100`, so it runs before
|
|
289
|
+
lower-priority index hooks. If multiple hooks share the same priority, Dash does
|
|
290
|
+
not guarantee their relative order.
|
|
291
|
+
|
|
292
|
+
Because the resources are inline, applications with a strict Content Security
|
|
293
|
+
Policy must permit the injected style and script, or add nonce support in a
|
|
294
|
+
customized deployment. The previous external-resource form is more suitable
|
|
295
|
+
when a policy forbids all inline code.
|
|
233
296
|
|
|
234
297
|
## Scope and process model
|
|
235
298
|
|
|
@@ -278,15 +341,108 @@ Run the tests and example:
|
|
|
278
341
|
|
|
279
342
|
```bash
|
|
280
343
|
uv run pytest
|
|
281
|
-
uv run python examples/basic.py
|
|
344
|
+
uv run --with dash-ant-design python examples/basic.py
|
|
282
345
|
```
|
|
283
346
|
|
|
347
|
+
The example uses `dash_antd_components.ConfigProvider` to demonstrate native
|
|
348
|
+
theme persistence, so `dash-ant-design` is an example-only dependency rather
|
|
349
|
+
than a runtime dependency of the plugin.
|
|
350
|
+
|
|
284
351
|
Build the source distribution and wheel:
|
|
285
352
|
|
|
286
353
|
```bash
|
|
287
354
|
uv build
|
|
288
355
|
```
|
|
289
356
|
|
|
357
|
+
## Publishing to PyPI
|
|
358
|
+
|
|
359
|
+
PyPI does not allow replacing an existing distribution file. Confirm that the
|
|
360
|
+
version in `pyproject.toml` is new before building, then create the artifacts
|
|
361
|
+
from a clean `dist/` directory:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
rm -rf build dist src/*.egg-info
|
|
365
|
+
uv lock
|
|
366
|
+
uv run --extra test pytest
|
|
367
|
+
uvx ruff check src tests examples
|
|
368
|
+
uv build
|
|
369
|
+
uvx twine check dist/*
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Inspect the resulting files before upload:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
ls -lh dist/
|
|
376
|
+
unzip -l dist/dash_startup_loading_plugin-0.2.0-py3-none-any.whl
|
|
377
|
+
tar -tzf dist/dash_startup_loading_plugin-0.2.0.tar.gz
|
|
378
|
+
uv publish --dry-run dist/*
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### TestPyPI
|
|
382
|
+
|
|
383
|
+
Create a TestPyPI API token, then upload to its legacy upload endpoint:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
export UV_PUBLISH_TOKEN="pypi-your-test-token"
|
|
387
|
+
uv publish \
|
|
388
|
+
--publish-url https://test.pypi.org/legacy/ \
|
|
389
|
+
--check-url https://test.pypi.org/simple/ \
|
|
390
|
+
dist/*
|
|
391
|
+
unset UV_PUBLISH_TOKEN
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Test installation in an isolated environment:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
uv run --isolated \
|
|
398
|
+
--index-url https://test.pypi.org/simple/ \
|
|
399
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
400
|
+
--with dash-startup-loading-plugin==0.2.0 \
|
|
401
|
+
python -c "import dash_startup_loading_plugin; print('import ok')"
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The extra PyPI index is needed because TestPyPI may not contain the plugin's
|
|
405
|
+
runtime dependencies.
|
|
406
|
+
|
|
407
|
+
### PyPI with an API token
|
|
408
|
+
|
|
409
|
+
Create a project-scoped PyPI token and publish the checked artifacts:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
export UV_PUBLISH_TOKEN="pypi-your-project-token"
|
|
413
|
+
uv publish dist/*
|
|
414
|
+
unset UV_PUBLISH_TOKEN
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Do not place tokens in `pyproject.toml`, shell history, source control, or CI
|
|
418
|
+
logs.
|
|
419
|
+
|
|
420
|
+
### PyPI Trusted Publishing
|
|
421
|
+
|
|
422
|
+
For CI releases, configure the repository and workflow as a Trusted Publisher
|
|
423
|
+
in the PyPI project settings. The publishing job needs the
|
|
424
|
+
`id-token: write` permission and can upload without a long-lived token:
|
|
425
|
+
|
|
426
|
+
```yaml
|
|
427
|
+
permissions:
|
|
428
|
+
contents: read
|
|
429
|
+
id-token: write
|
|
430
|
+
|
|
431
|
+
steps:
|
|
432
|
+
- uses: actions/checkout@v4
|
|
433
|
+
- uses: astral-sh/setup-uv@v8
|
|
434
|
+
- run: uv build
|
|
435
|
+
- run: uv publish --trusted-publishing always
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
After publishing, verify the release:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
uv run --isolated \
|
|
442
|
+
--with dash-startup-loading-plugin==0.2.0 \
|
|
443
|
+
python -c "import dash_startup_loading_plugin; print('import ok')"
|
|
444
|
+
```
|
|
445
|
+
|
|
290
446
|
## License
|
|
291
447
|
|
|
292
448
|
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dash-startup-loading-plugin"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -5,7 +5,7 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
5
5
|
try:
|
|
6
6
|
__version__ = version("dash-startup-loading-plugin")
|
|
7
7
|
except PackageNotFoundError: # pragma: no cover - source tree fallback
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
9
|
|
|
10
10
|
from .plugin import ( # noqa: E402
|
|
11
11
|
StartupLoadingConfig,
|
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
7
|
from dataclasses import asdict, dataclass, fields, replace
|
|
8
|
+
from functools import lru_cache
|
|
8
9
|
from html import escape
|
|
10
|
+
from importlib.resources import files
|
|
9
11
|
from threading import RLock
|
|
10
12
|
from typing import Any, Iterable
|
|
11
13
|
|
|
@@ -13,6 +15,7 @@ from dash import hooks
|
|
|
13
15
|
|
|
14
16
|
_OVERLAY_MARKER = "data-dash-startup-loading"
|
|
15
17
|
_BODY_PATTERN = re.compile(r"<body(?:\s[^>]*)?>", flags=re.IGNORECASE)
|
|
18
|
+
_HEAD_END_PATTERN = re.compile(r"</head\s*>", flags=re.IGNORECASE)
|
|
16
19
|
_CONFIG_LOCK = RLock()
|
|
17
20
|
|
|
18
21
|
|
|
@@ -38,6 +41,8 @@ class StartupLoadingConfig:
|
|
|
38
41
|
dark_background: str = "#0f0f0f"
|
|
39
42
|
color: str = "#1677ff"
|
|
40
43
|
dark_color: str = "#4096ff"
|
|
44
|
+
theme_mode: str = "auto"
|
|
45
|
+
dash_theme_component_id: str | None = None
|
|
41
46
|
spinner_size_px: int = 28
|
|
42
47
|
spinner_stroke_px: int = 3
|
|
43
48
|
hide_default_loading: bool = True
|
|
@@ -70,6 +75,13 @@ def _validate(config: StartupLoadingConfig) -> StartupLoadingConfig:
|
|
|
70
75
|
raise ValueError("pending_selector must be None or a non-empty CSS selector")
|
|
71
76
|
if config.timeout_ms is not None and config.timeout_ms < 0:
|
|
72
77
|
raise ValueError("timeout_ms must be None or greater than or equal to zero")
|
|
78
|
+
if config.theme_mode not in {"auto", "light", "dark"}:
|
|
79
|
+
raise ValueError("theme_mode must be 'auto', 'light', or 'dark'")
|
|
80
|
+
if config.dash_theme_component_id is not None and (
|
|
81
|
+
not isinstance(config.dash_theme_component_id, str)
|
|
82
|
+
or not config.dash_theme_component_id.strip()
|
|
83
|
+
):
|
|
84
|
+
raise ValueError("dash_theme_component_id must be None or a non-empty string")
|
|
73
85
|
for name in ("minimum_display_ms", "fade_duration_ms", "spinner_size_px", "spinner_stroke_px"):
|
|
74
86
|
if getattr(config, name) < 0:
|
|
75
87
|
raise ValueError(f"{name} must be greater than or equal to zero")
|
|
@@ -125,6 +137,13 @@ def _client_config(config: StartupLoadingConfig) -> dict[str, Any]:
|
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
|
|
140
|
+
def _theme_config(config: StartupLoadingConfig) -> dict[str, Any]:
|
|
141
|
+
return {
|
|
142
|
+
"themeMode": config.theme_mode,
|
|
143
|
+
"dashThemeComponentId": config.dash_theme_component_id,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
128
147
|
def _overlay_html(config: StartupLoadingConfig) -> str:
|
|
129
148
|
client_config = escape(
|
|
130
149
|
json.dumps(_client_config(config), ensure_ascii=False, separators=(",", ":")),
|
|
@@ -160,6 +179,33 @@ def _overlay_html(config: StartupLoadingConfig) -> str:
|
|
|
160
179
|
)
|
|
161
180
|
|
|
162
181
|
|
|
182
|
+
@lru_cache(maxsize=2)
|
|
183
|
+
def _resource_text(name: str) -> str:
|
|
184
|
+
return (
|
|
185
|
+
files("dash_startup_loading_plugin")
|
|
186
|
+
.joinpath("resources", name)
|
|
187
|
+
.read_text(encoding="utf-8")
|
|
188
|
+
.strip()
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _inline_head_resources(app_index: str, config: StartupLoadingConfig) -> str:
|
|
193
|
+
theme_config = json.dumps(_theme_config(config), ensure_ascii=False, separators=(",", ":"))
|
|
194
|
+
resources = (
|
|
195
|
+
'<style data-dash-startup-loading-resource="style">'
|
|
196
|
+
f"{_resource_text('startup-loading.css')}"
|
|
197
|
+
"</style>"
|
|
198
|
+
'<script data-dash-startup-loading-resource="theme">'
|
|
199
|
+
f"window.__dashStartupLoadingThemeConfig={theme_config};"
|
|
200
|
+
f"{_resource_text('startup-theme.js')}"
|
|
201
|
+
"</script>"
|
|
202
|
+
)
|
|
203
|
+
head_end = _HEAD_END_PATTERN.search(app_index)
|
|
204
|
+
if head_end is not None:
|
|
205
|
+
return app_index[: head_end.start()] + resources + app_index[head_end.start() :]
|
|
206
|
+
return resources + app_index
|
|
207
|
+
|
|
208
|
+
|
|
163
209
|
def _inject_overlay(app_index: str) -> str:
|
|
164
210
|
config = get_config()
|
|
165
211
|
if not config.enabled or _OVERLAY_MARKER in app_index:
|
|
@@ -168,27 +214,16 @@ def _inject_overlay(app_index: str) -> str:
|
|
|
168
214
|
body_match = _BODY_PATTERN.search(app_index)
|
|
169
215
|
if body_match is None:
|
|
170
216
|
return app_index
|
|
217
|
+
app_index = _inline_head_resources(app_index, config)
|
|
218
|
+
body_match = _BODY_PATTERN.search(app_index)
|
|
219
|
+
assert body_match is not None
|
|
171
220
|
position = body_match.end()
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"relative_package_path": "resources/startup-loading.css",
|
|
179
|
-
"namespace": "dash_startup_loading_plugin",
|
|
180
|
-
}
|
|
181
|
-
]
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
hooks.script(
|
|
185
|
-
[
|
|
186
|
-
{
|
|
187
|
-
"relative_package_path": "resources/startup-loading.js",
|
|
188
|
-
"namespace": "dash_startup_loading_plugin",
|
|
189
|
-
}
|
|
190
|
-
]
|
|
191
|
-
)
|
|
221
|
+
script = (
|
|
222
|
+
'<script data-dash-startup-loading-resource="script">'
|
|
223
|
+
f"{_resource_text('startup-loading.js')}"
|
|
224
|
+
"</script>"
|
|
225
|
+
)
|
|
226
|
+
return app_index[:position] + _overlay_html(config) + script + app_index[position:]
|
|
192
227
|
|
|
193
228
|
|
|
194
229
|
@hooks.index(priority=100)
|
|
@@ -37,16 +37,14 @@
|
|
|
37
37
|
display: none;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
html
|
|
41
|
-
color: var(--dash-startup-loading-
|
|
42
|
-
background: var(--dash-startup-loading-
|
|
40
|
+
html[data-dash-startup-loading-theme="light"] .dash-startup-loading {
|
|
41
|
+
color: var(--dash-startup-loading-color, #1677ff);
|
|
42
|
+
background: var(--dash-startup-loading-background, #fff);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
background: var(--dash-startup-loading-dark-background, #0f0f0f);
|
|
49
|
-
}
|
|
45
|
+
html[data-dash-startup-loading-theme="dark"] .dash-startup-loading {
|
|
46
|
+
color: var(--dash-startup-loading-dark-color, #4096ff);
|
|
47
|
+
background: var(--dash-startup-loading-dark-background, #0f0f0f);
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var root = document.documentElement;
|
|
5
|
+
var config = window.__dashStartupLoadingThemeConfig || {};
|
|
6
|
+
var persistencePrefix = "_dash_persistence.";
|
|
7
|
+
|
|
8
|
+
function parseStored(value) {
|
|
9
|
+
if (value === null || value === undefined) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(value);
|
|
14
|
+
} catch (_) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalize(value) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
value = value[0];
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
var name = value.toLowerCase();
|
|
25
|
+
if (name === "dark") return "dark";
|
|
26
|
+
if (name === "light" || name === "default") return "light";
|
|
27
|
+
if (name === "system" || name === "auto") return "system";
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (!value || typeof value !== "object") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var explicit = value.mode || value.colorScheme || value.color_scheme;
|
|
35
|
+
var normalized = normalize(explicit);
|
|
36
|
+
if (normalized) return normalized;
|
|
37
|
+
|
|
38
|
+
var algorithms = Array.isArray(value.algorithm) ? value.algorithm : [value.algorithm];
|
|
39
|
+
if (algorithms.some(function (algorithm) { return algorithm === "dark"; })) return "dark";
|
|
40
|
+
if (algorithms.some(function (algorithm) { return algorithm === "default"; })) return "light";
|
|
41
|
+
|
|
42
|
+
return Object.keys(value).length ? "light" : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rootTheme() {
|
|
46
|
+
if (root.classList.contains("dark")) return "dark";
|
|
47
|
+
if (root.classList.contains("light")) return "light";
|
|
48
|
+
return normalize(
|
|
49
|
+
root.getAttribute("data-theme")
|
|
50
|
+
|| root.getAttribute("data-color-scheme")
|
|
51
|
+
|| root.getAttribute("data-mantine-color-scheme")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dashPersistenceTheme() {
|
|
56
|
+
var id = config.dashThemeComponentId;
|
|
57
|
+
var exactPrefix = id ? persistencePrefix + id + ".theme." : null;
|
|
58
|
+
var fallback = null;
|
|
59
|
+
try {
|
|
60
|
+
for (var index = 0; index < localStorage.length; index += 1) {
|
|
61
|
+
var key = localStorage.key(index);
|
|
62
|
+
if (!key || key.indexOf(persistencePrefix) !== 0 || key.indexOf(".theme.") < 0) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
var theme = normalize(parseStored(localStorage.getItem(key)));
|
|
66
|
+
if (!theme) continue;
|
|
67
|
+
if (exactPrefix && key.indexOf(exactPrefix) === 0) return theme;
|
|
68
|
+
if (!fallback) fallback = theme;
|
|
69
|
+
}
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function conventionalStoredTheme() {
|
|
77
|
+
var keys = ["mantine-color-scheme-value", "theme", "color-theme", "color-scheme"];
|
|
78
|
+
try {
|
|
79
|
+
for (var index = 0; index < keys.length; index += 1) {
|
|
80
|
+
var theme = normalize(parseStored(localStorage.getItem(keys[index])));
|
|
81
|
+
if (theme) return theme;
|
|
82
|
+
}
|
|
83
|
+
} catch (_) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var configuredTheme = normalize(config.themeMode);
|
|
90
|
+
var theme = configuredTheme === "light" || configuredTheme === "dark" ? configuredTheme : null;
|
|
91
|
+
if (!theme && config.dashThemeComponentId) {
|
|
92
|
+
theme = dashPersistenceTheme();
|
|
93
|
+
}
|
|
94
|
+
if (!theme) {
|
|
95
|
+
theme = rootTheme() || dashPersistenceTheme() || conventionalStoredTheme() || "system";
|
|
96
|
+
}
|
|
97
|
+
if (theme === "system") {
|
|
98
|
+
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
root.setAttribute("data-dash-startup-loading-theme", theme);
|
|
102
|
+
}());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dash-startup-loading-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin.
|
|
5
5
|
Author-email: Ethan Zhang <ethan.zhang2016@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,10 +20,10 @@ Dynamic: license-file
|
|
|
20
20
|
that displays a full-screen loading overlay while a Dash application performs
|
|
21
21
|
its initial browser-side startup.
|
|
22
22
|
|
|
23
|
-
The overlay
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
The overlay, CSS, and JavaScript are injected into the final HTML document with
|
|
24
|
+
`hooks.index`, so they are available before Dash and React mount the application
|
|
25
|
+
layout without additional startup resource requests. An app does not need to
|
|
26
|
+
copy assets or replace Dash's `index_string`.
|
|
27
27
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
@@ -32,8 +32,10 @@ does not need to copy assets or replace Dash's `index_string`.
|
|
|
32
32
|
- Waits for real rendered content and optional app-specific readiness selectors.
|
|
33
33
|
- Can wait for lazy-loading placeholders to disappear.
|
|
34
34
|
- Includes a timeout fallback, minimum display time, and fade-out transition.
|
|
35
|
-
-
|
|
36
|
-
|
|
35
|
+
- Inlines its small startup CSS and JavaScript, avoiding two additional requests.
|
|
36
|
+
- Resolves light and dark themes before first paint from Dash component
|
|
37
|
+
persistence, Tailwind root classes, Mantine color-scheme state, or the
|
|
38
|
+
operating-system preference.
|
|
37
39
|
- Exposes a small browser API and emits a completion event.
|
|
38
40
|
- Requires no changes to the app layout or callbacks.
|
|
39
41
|
|
|
@@ -49,7 +51,7 @@ discovery, resource hooks, and the index hook described in the official
|
|
|
49
51
|
## Installation
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
|
-
pip install dash-startup-loading-plugin
|
|
54
|
+
pip install "dash-startup-loading-plugin>=0.2.0"
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
The package declares the following entry point:
|
|
@@ -64,6 +66,17 @@ package therefore enables the default startup overlay for Dash applications in
|
|
|
64
66
|
that Python environment; an explicit import is only needed when changing its
|
|
65
67
|
configuration or using its Python API.
|
|
66
68
|
|
|
69
|
+
## What's new in 0.2.0
|
|
70
|
+
|
|
71
|
+
- Resolves the loading background before first paint from a persisted
|
|
72
|
+
`dash_antd_components.ConfigProvider.theme`.
|
|
73
|
+
- Supports Tailwind's `html.dark` and `html.light` classes.
|
|
74
|
+
- Supports Dash Mantine Components' `data-mantine-color-scheme` attribute and
|
|
75
|
+
`mantine-color-scheme-value` localStorage entry.
|
|
76
|
+
- Adds `theme_mode` and `dash_theme_component_id` configuration.
|
|
77
|
+
- Keeps the startup CSS and JavaScript inline, so theme detection does not add
|
|
78
|
+
network requests before Dash mounts.
|
|
79
|
+
|
|
67
80
|
## Quick start
|
|
68
81
|
|
|
69
82
|
The defaults work without any plugin-specific code:
|
|
@@ -152,14 +165,57 @@ immutable `StartupLoadingConfig` instance.
|
|
|
152
165
|
| `dark_background` | `"#0f0f0f"` | Dark-theme background color. |
|
|
153
166
|
| `color` | `"#1677ff"` | Light-theme spinner/current color. |
|
|
154
167
|
| `dark_color` | `"#4096ff"` | Dark-theme spinner/current color. |
|
|
168
|
+
| `theme_mode` | `"auto"` | Startup theme policy: `"auto"`, `"light"`, or `"dark"`. Auto follows persisted or active app state, then the operating-system preference. |
|
|
169
|
+
| `dash_theme_component_id` | `None` | Preferred Dash component ID when reading a persisted `theme` prop. When omitted, the first compatible persisted theme is used. |
|
|
155
170
|
| `spinner_size_px` | `28` | Default spinner width and height in pixels. |
|
|
156
171
|
| `spinner_stroke_px` | `3` | Default spinner stroke width in pixels. |
|
|
157
172
|
| `hide_default_loading` | `True` | Hide Dash's built-in initial `._dash-loading` indicator while the overlay is present. |
|
|
158
173
|
| `custom_loader_html` | `None` | Trusted HTML that replaces the default spinner. |
|
|
159
174
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
With `theme_mode="auto"`, the inline head bootstrap resolves the startup theme
|
|
176
|
+
in this order:
|
|
177
|
+
|
|
178
|
+
1. The persisted Dash `theme` for `dash_theme_component_id`, when configured.
|
|
179
|
+
2. An existing `html.dark` or `html.light` class, including Tailwind's manual
|
|
180
|
+
dark-mode convention.
|
|
181
|
+
3. `data-theme`, `data-color-scheme`, or `data-mantine-color-scheme` on
|
|
182
|
+
`<html>`.
|
|
183
|
+
4. Other Dash local persistence entries for a component's `theme` prop. Set
|
|
184
|
+
`dash_theme_component_id` when an app contains more than one persisted
|
|
185
|
+
theme-capable component.
|
|
186
|
+
5. Dash Mantine Components' `mantine-color-scheme-value` localStorage entry,
|
|
187
|
+
followed by the common `theme`, `color-theme`, and `color-scheme` keys.
|
|
188
|
+
6. `prefers-color-scheme`.
|
|
189
|
+
|
|
190
|
+
The result is written to `data-dash-startup-loading-theme` before the body is
|
|
191
|
+
parsed. This isolates the overlay styling without changing classes or data
|
|
192
|
+
attributes owned by Tailwind, Dash Mantine Components, or the application.
|
|
193
|
+
The stylesheet also adjusts its animation for `prefers-reduced-motion`.
|
|
194
|
+
|
|
195
|
+
### Dash Ant Design persistence
|
|
196
|
+
|
|
197
|
+
`dash_antd_components.ConfigProvider` can persist its `theme` prop. The startup
|
|
198
|
+
plugin reads that same Dash persistence record on the next page load:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import dash_antd_components as dac
|
|
202
|
+
from dash_startup_loading_plugin import configure
|
|
203
|
+
|
|
204
|
+
configure(dash_theme_component_id="theme-provider")
|
|
205
|
+
|
|
206
|
+
provider = dac.ConfigProvider(
|
|
207
|
+
app_content,
|
|
208
|
+
id="theme-provider",
|
|
209
|
+
persistence=True,
|
|
210
|
+
persisted_props=["theme"],
|
|
211
|
+
persistence_type="local",
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Update `theme-provider.theme` from a regular or clientside callback. Dash stores
|
|
216
|
+
the UI edit, `ConfigProvider` restores it after mount, and the startup plugin
|
|
217
|
+
uses it for the loading background before mount. See
|
|
218
|
+
[`examples/basic.py`](examples/basic.py) for the complete switcher.
|
|
163
219
|
|
|
164
220
|
### Custom loader markup
|
|
165
221
|
|
|
@@ -241,11 +297,18 @@ configure(
|
|
|
241
297
|
)
|
|
242
298
|
```
|
|
243
299
|
|
|
244
|
-
The plugin preserves Dash's normal index template, injects
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
300
|
+
The plugin preserves Dash's normal index template, injects its stylesheet before
|
|
301
|
+
`</head>`, and inserts the overlay followed by its startup script directly after
|
|
302
|
+
the opening `<body>` tag. The source CSS and JavaScript remain package resources
|
|
303
|
+
for maintainability, but the browser receives them inline and makes no separate
|
|
304
|
+
request for either file. The index hook uses priority `100`, so it runs before
|
|
305
|
+
lower-priority index hooks. If multiple hooks share the same priority, Dash does
|
|
306
|
+
not guarantee their relative order.
|
|
307
|
+
|
|
308
|
+
Because the resources are inline, applications with a strict Content Security
|
|
309
|
+
Policy must permit the injected style and script, or add nonce support in a
|
|
310
|
+
customized deployment. The previous external-resource form is more suitable
|
|
311
|
+
when a policy forbids all inline code.
|
|
249
312
|
|
|
250
313
|
## Scope and process model
|
|
251
314
|
|
|
@@ -294,15 +357,108 @@ Run the tests and example:
|
|
|
294
357
|
|
|
295
358
|
```bash
|
|
296
359
|
uv run pytest
|
|
297
|
-
uv run python examples/basic.py
|
|
360
|
+
uv run --with dash-ant-design python examples/basic.py
|
|
298
361
|
```
|
|
299
362
|
|
|
363
|
+
The example uses `dash_antd_components.ConfigProvider` to demonstrate native
|
|
364
|
+
theme persistence, so `dash-ant-design` is an example-only dependency rather
|
|
365
|
+
than a runtime dependency of the plugin.
|
|
366
|
+
|
|
300
367
|
Build the source distribution and wheel:
|
|
301
368
|
|
|
302
369
|
```bash
|
|
303
370
|
uv build
|
|
304
371
|
```
|
|
305
372
|
|
|
373
|
+
## Publishing to PyPI
|
|
374
|
+
|
|
375
|
+
PyPI does not allow replacing an existing distribution file. Confirm that the
|
|
376
|
+
version in `pyproject.toml` is new before building, then create the artifacts
|
|
377
|
+
from a clean `dist/` directory:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
rm -rf build dist src/*.egg-info
|
|
381
|
+
uv lock
|
|
382
|
+
uv run --extra test pytest
|
|
383
|
+
uvx ruff check src tests examples
|
|
384
|
+
uv build
|
|
385
|
+
uvx twine check dist/*
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Inspect the resulting files before upload:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
ls -lh dist/
|
|
392
|
+
unzip -l dist/dash_startup_loading_plugin-0.2.0-py3-none-any.whl
|
|
393
|
+
tar -tzf dist/dash_startup_loading_plugin-0.2.0.tar.gz
|
|
394
|
+
uv publish --dry-run dist/*
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### TestPyPI
|
|
398
|
+
|
|
399
|
+
Create a TestPyPI API token, then upload to its legacy upload endpoint:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
export UV_PUBLISH_TOKEN="pypi-your-test-token"
|
|
403
|
+
uv publish \
|
|
404
|
+
--publish-url https://test.pypi.org/legacy/ \
|
|
405
|
+
--check-url https://test.pypi.org/simple/ \
|
|
406
|
+
dist/*
|
|
407
|
+
unset UV_PUBLISH_TOKEN
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Test installation in an isolated environment:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
uv run --isolated \
|
|
414
|
+
--index-url https://test.pypi.org/simple/ \
|
|
415
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
416
|
+
--with dash-startup-loading-plugin==0.2.0 \
|
|
417
|
+
python -c "import dash_startup_loading_plugin; print('import ok')"
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The extra PyPI index is needed because TestPyPI may not contain the plugin's
|
|
421
|
+
runtime dependencies.
|
|
422
|
+
|
|
423
|
+
### PyPI with an API token
|
|
424
|
+
|
|
425
|
+
Create a project-scoped PyPI token and publish the checked artifacts:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
export UV_PUBLISH_TOKEN="pypi-your-project-token"
|
|
429
|
+
uv publish dist/*
|
|
430
|
+
unset UV_PUBLISH_TOKEN
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Do not place tokens in `pyproject.toml`, shell history, source control, or CI
|
|
434
|
+
logs.
|
|
435
|
+
|
|
436
|
+
### PyPI Trusted Publishing
|
|
437
|
+
|
|
438
|
+
For CI releases, configure the repository and workflow as a Trusted Publisher
|
|
439
|
+
in the PyPI project settings. The publishing job needs the
|
|
440
|
+
`id-token: write` permission and can upload without a long-lived token:
|
|
441
|
+
|
|
442
|
+
```yaml
|
|
443
|
+
permissions:
|
|
444
|
+
contents: read
|
|
445
|
+
id-token: write
|
|
446
|
+
|
|
447
|
+
steps:
|
|
448
|
+
- uses: actions/checkout@v4
|
|
449
|
+
- uses: astral-sh/setup-uv@v8
|
|
450
|
+
- run: uv build
|
|
451
|
+
- run: uv publish --trusted-publishing always
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
After publishing, verify the release:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
uv run --isolated \
|
|
458
|
+
--with dash-startup-loading-plugin==0.2.0 \
|
|
459
|
+
python -c "import dash_startup_loading_plugin; print('import ok')"
|
|
460
|
+
```
|
|
461
|
+
|
|
306
462
|
## License
|
|
307
463
|
|
|
308
464
|
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -11,4 +11,5 @@ src/dash_startup_loading_plugin.egg-info/requires.txt
|
|
|
11
11
|
src/dash_startup_loading_plugin.egg-info/top_level.txt
|
|
12
12
|
src/dash_startup_loading_plugin/resources/startup-loading.css
|
|
13
13
|
src/dash_startup_loading_plugin/resources/startup-loading.js
|
|
14
|
+
src/dash_startup_loading_plugin/resources/startup-theme.js
|
|
14
15
|
tests/test_plugin.py
|
{dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/tests/test_plugin.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import html
|
|
2
2
|
import json
|
|
3
3
|
import re
|
|
4
|
+
from importlib.resources import files
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
from dash import Dash
|
|
@@ -27,8 +28,16 @@ def test_injects_overlay_after_body_with_custom_attributes():
|
|
|
27
28
|
|
|
28
29
|
result = _inject_overlay(index)
|
|
29
30
|
|
|
31
|
+
assert '<style data-dash-startup-loading-resource="style">' in result
|
|
32
|
+
assert result.index("<style") < result.index("</head>")
|
|
33
|
+
assert '<script data-dash-startup-loading-resource="theme">' in result
|
|
34
|
+
assert result.index('data-dash-startup-loading-resource="theme"') < result.index("</head>")
|
|
30
35
|
assert '<body class="app"><div id="dash-startup-loading"' in result
|
|
31
|
-
assert
|
|
36
|
+
assert '<script data-dash-startup-loading-resource="script">' in result
|
|
37
|
+
assert result.index('id="dash-startup-loading"') < result.index(
|
|
38
|
+
'<script data-dash-startup-loading-resource="script">'
|
|
39
|
+
)
|
|
40
|
+
assert result.count(" data-dash-startup-loading ") == 1
|
|
32
41
|
assert '<span class="dash-startup-loading__spinner"' in result
|
|
33
42
|
assert _data_config(result)["requiredSelectors"] == ["#react-entry-point"]
|
|
34
43
|
|
|
@@ -81,12 +90,53 @@ def test_configuration_validation():
|
|
|
81
90
|
configure(required_selectors="#header")
|
|
82
91
|
with pytest.raises(ValueError, match="timeout_ms"):
|
|
83
92
|
configure(timeout_ms=-1)
|
|
93
|
+
with pytest.raises(ValueError, match="theme_mode"):
|
|
94
|
+
configure(theme_mode="sepia")
|
|
95
|
+
with pytest.raises(ValueError, match="dash_theme_component_id"):
|
|
96
|
+
configure(dash_theme_component_id="")
|
|
84
97
|
with pytest.raises(TypeError, match="Unknown"):
|
|
85
98
|
configure(unknown=True)
|
|
86
99
|
assert get_config().required_selectors == ("#react-entry-point",)
|
|
87
100
|
|
|
88
101
|
|
|
89
|
-
def
|
|
102
|
+
def test_resolved_startup_theme_controls_overlay_colors():
|
|
103
|
+
css = (
|
|
104
|
+
files("dash_startup_loading_plugin")
|
|
105
|
+
.joinpath("resources/startup-loading.css")
|
|
106
|
+
.read_text(encoding="utf-8")
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assert 'html[data-dash-startup-loading-theme="light"] .dash-startup-loading' in css
|
|
110
|
+
assert 'html[data-dash-startup-loading-theme="dark"] .dash-startup-loading' in css
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_theme_bootstrap_supports_dash_tailwind_and_mantine_conventions():
|
|
114
|
+
script = (
|
|
115
|
+
files("dash_startup_loading_plugin")
|
|
116
|
+
.joinpath("resources/startup-theme.js")
|
|
117
|
+
.read_text(encoding="utf-8")
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert "_dash_persistence." in script
|
|
121
|
+
assert 'classList.contains("dark")' in script
|
|
122
|
+
assert 'data-mantine-color-scheme' in script
|
|
123
|
+
assert 'mantine-color-scheme-value' in script
|
|
124
|
+
assert 'data-dash-startup-loading-theme' in script
|
|
125
|
+
assert 'name === "system" || name === "auto"' in script
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_theme_bootstrap_serializes_component_id_and_explicit_mode():
|
|
129
|
+
configure(theme_mode="dark", dash_theme_component_id="theme-provider")
|
|
130
|
+
|
|
131
|
+
result = _inject_overlay("<html><head></head><body></body></html>")
|
|
132
|
+
|
|
133
|
+
assert (
|
|
134
|
+
'window.__dashStartupLoadingThemeConfig={"themeMode":"dark",'
|
|
135
|
+
'"dashThemeComponentId":"theme-provider"};'
|
|
136
|
+
) in result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_dash_index_contains_overlay_and_inline_resources():
|
|
90
140
|
app = Dash(__name__)
|
|
91
141
|
app.layout = dash_html.Div("Ready", id="ready")
|
|
92
142
|
|
|
@@ -96,9 +146,8 @@ def test_dash_index_contains_overlay_and_packaged_resources():
|
|
|
96
146
|
|
|
97
147
|
assert response.status_code == 200
|
|
98
148
|
assert "data-dash-startup-loading" in index
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assert
|
|
102
|
-
assert
|
|
103
|
-
assert
|
|
104
|
-
assert client.get(js_url.group(1)).status_code == 200
|
|
149
|
+
assert '<style data-dash-startup-loading-resource="style">' in index
|
|
150
|
+
assert '<script data-dash-startup-loading-resource="theme">' in index
|
|
151
|
+
assert '<script data-dash-startup-loading-resource="script">' in index
|
|
152
|
+
assert "resources/startup-loading.css" not in index
|
|
153
|
+
assert "resources/startup-loading.js" not in index
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|