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.
Files changed (17) hide show
  1. {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
  2. {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/README.md +172 -16
  3. {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/pyproject.toml +1 -1
  4. {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin/__init__.py +1 -1
  5. {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/src/dash_startup_loading_plugin/plugin.py +55 -20
  6. {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
  7. dash_startup_loading_plugin-0.2.0/src/dash_startup_loading_plugin/resources/startup-theme.js +102 -0
  8. {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
  9. {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
  10. {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/tests/test_plugin.py +57 -8
  11. {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/LICENSE +0 -0
  12. {dash_startup_loading_plugin-0.1.0 → dash_startup_loading_plugin-0.2.0}/setup.cfg +0 -0
  13. {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
  14. {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
  15. {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
  16. {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
  17. {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.1.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 is injected into the final HTML document with `hooks.index`, so it
24
- is visible before Dash and React mount the application layout. The packaged CSS
25
- and JavaScript are registered with `hooks.stylesheet` and `hooks.script`; an app
26
- does not need to copy assets or replace Dash's `index_string`.
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
- - Supports light and dark themes, reduced-motion preferences, custom colors,
36
- custom spinner geometry, and trusted custom loader markup.
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
- The default stylesheet recognizes `html.dark` and `html.light`. When neither
161
- class forces a theme, it follows `prefers-color-scheme`. It also adjusts its
162
- animation when the user enables `prefers-reduced-motion`.
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 the overlay directly
245
- after the opening `<body>` tag, and registers its versioned package assets via
246
- Dash Hooks. The index hook uses priority `100`, so it runs before lower-priority
247
- index hooks. If multiple hooks share the same priority, Dash does not guarantee
248
- their relative order.
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 is injected into the final HTML document with `hooks.index`, so it
8
- is visible before Dash and React mount the application layout. The packaged CSS
9
- and JavaScript are registered with `hooks.stylesheet` and `hooks.script`; an app
10
- does not need to copy assets or replace Dash's `index_string`.
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
- - Supports light and dark themes, reduced-motion preferences, custom colors,
20
- custom spinner geometry, and trusted custom loader markup.
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
- The default stylesheet recognizes `html.dark` and `html.light`. When neither
145
- class forces a theme, it follows `prefers-color-scheme`. It also adjusts its
146
- animation when the user enables `prefers-reduced-motion`.
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 the overlay directly
229
- after the opening `<body>` tag, and registers its versioned package assets via
230
- Dash Hooks. The index hook uses priority `100`, so it runs before lower-priority
231
- index hooks. If multiple hooks share the same priority, Dash does not guarantee
232
- their relative order.
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.1.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.1.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
- return app_index[:position] + _overlay_html(config) + app_index[position:]
173
-
174
-
175
- hooks.stylesheet(
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.dark .dash-startup-loading {
41
- color: var(--dash-startup-loading-dark-color, #4096ff);
42
- background: var(--dash-startup-loading-dark-background, #0f0f0f);
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
- @media (prefers-color-scheme: dark) {
46
- html:not(.light) .dash-startup-loading {
47
- color: var(--dash-startup-loading-dark-color, #4096ff);
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.1.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 is injected into the final HTML document with `hooks.index`, so it
24
- is visible before Dash and React mount the application layout. The packaged CSS
25
- and JavaScript are registered with `hooks.stylesheet` and `hooks.script`; an app
26
- does not need to copy assets or replace Dash's `index_string`.
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
- - Supports light and dark themes, reduced-motion preferences, custom colors,
36
- custom spinner geometry, and trusted custom loader markup.
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
- The default stylesheet recognizes `html.dark` and `html.light`. When neither
161
- class forces a theme, it follows `prefers-color-scheme`. It also adjusts its
162
- animation when the user enables `prefers-reduced-motion`.
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 the overlay directly
245
- after the opening `<body>` tag, and registers its versioned package assets via
246
- Dash Hooks. The index hook uses priority `100`, so it runs before lower-priority
247
- index hooks. If multiple hooks share the same priority, Dash does not guarantee
248
- their relative order.
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
@@ -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 result.count("data-dash-startup-loading") == 1
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 test_dash_index_contains_overlay_and_packaged_resources():
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
- css_url = re.search(r'href="([^"]*dash_startup_loading_plugin/[^"]*startup-loading[^\"]+\.css)"', index)
100
- js_url = re.search(r'src="([^"]*dash_startup_loading_plugin/[^"]*startup-loading[^\"]+\.js)"', index)
101
- assert css_url is not None
102
- assert js_url is not None
103
- assert client.get(css_url.group(1)).status_code == 200
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