dash-startup-loading-plugin 1.0.4__tar.gz → 1.1.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 (43) hide show
  1. dash_startup_loading_plugin-1.1.0/MANIFEST.in +2 -0
  2. dash_startup_loading_plugin-1.1.0/PKG-INFO +17 -0
  3. {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/pyproject.toml +8 -7
  4. {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin/__init__.py +3 -9
  5. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/plugin.py +416 -0
  6. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/py.typed +0 -0
  7. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/LOADING-UI-LICENSE +21 -0
  8. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/loading-ui.js +775 -0
  9. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/loading.css +164 -0
  10. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/loading.js +45 -0
  11. {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin/resources/theme.js +51 -19
  12. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/PKG-INFO +17 -0
  13. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/SOURCES.txt +17 -0
  14. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/entry_points.txt +2 -0
  15. dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/requires.txt +2 -0
  16. dash_startup_loading_plugin-1.0.4/MANIFEST.in +0 -2
  17. dash_startup_loading_plugin-1.0.4/PKG-INFO +0 -303
  18. dash_startup_loading_plugin-1.0.4/README.md +0 -289
  19. dash_startup_loading_plugin-1.0.4/README.zh-CN.md +0 -274
  20. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/README.md +0 -289
  21. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/README.zh-CN.md +0 -274
  22. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/__init__.py +0 -5
  23. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/__main__.py +0 -5
  24. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/antd.py +0 -109
  25. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/basic.py +0 -49
  26. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/cli.py +0 -89
  27. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/fac.py +0 -110
  28. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/mantine.py +0 -79
  29. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/shared.py +0 -33
  30. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/plugin.py +0 -296
  31. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/resources/loading.css +0 -64
  32. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/resources/loading.js +0 -177
  33. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/PKG-INFO +0 -303
  34. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/SOURCES.txt +0 -29
  35. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/entry_points.txt +0 -5
  36. dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/requires.txt +0 -1
  37. dash_startup_loading_plugin-1.0.4/tests/test_documentation.py +0 -75
  38. dash_startup_loading_plugin-1.0.4/tests/test_examples.py +0 -150
  39. dash_startup_loading_plugin-1.0.4/tests/test_plugin.py +0 -270
  40. {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/LICENSE +0 -0
  41. {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/setup.cfg +0 -0
  42. {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin.egg-info/dependency_links.txt +0 -0
  43. {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin.egg-info/top_level.txt +0 -0
@@ -0,0 +1,2 @@
1
+ prune tests
2
+ global-exclude *.md
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: dash-startup-loading-plugin
3
+ Version: 1.1.0
4
+ Summary: A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin.
5
+ Author-email: Ethan Zhang <ethan.zhang2016@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/c0debeez/dash-startup-loading-plugin
8
+ Keywords: dash,plotly,loading,plugin
9
+ Classifier: Framework :: Dash
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/plain
12
+ License-File: LICENSE
13
+ Requires-Dist: dash>=3.0.3
14
+ Requires-Dist: typing-extensions>=4.11
15
+ Dynamic: license-file
16
+
17
+ An installable full-screen startup loading overlay for Dash apps.
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dash-startup-loading-plugin"
7
- version = "1.0.4"
7
+ version = "1.1.0"
8
8
  description = "A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin."
9
- readme = "README.md"
9
+ readme = { text = "An installable full-screen startup loading overlay for Dash apps.", content-type = "text/plain" }
10
10
  requires-python = ">=3.9"
11
11
  authors = [
12
12
  { name = "Ethan Zhang", email = "ethan.zhang2016@gmail.com" }
@@ -17,16 +17,17 @@ license = "MIT"
17
17
  license-files = ["LICENSE"]
18
18
  dependencies = [
19
19
  "dash>=3.0.3",
20
+ "typing-extensions>=4.11",
20
21
  ]
21
22
 
23
+ [project.urls]
24
+ Repository = "https://github.com/c0debeez/dash-startup-loading-plugin"
25
+
22
26
  [dependency-groups]
23
27
  test = [
24
28
  "pytest>=8",
25
29
  ]
26
30
 
27
- [project.scripts]
28
- dash-startup-loading-plugin = "dash_startup_loading_plugin.examples:main"
29
-
30
31
  [project.entry-points."dash_hooks"]
31
32
  dash_startup_loading_plugin = "dash_startup_loading_plugin"
32
33
 
@@ -39,10 +40,10 @@ where = ["src"]
39
40
 
40
41
  [tool.setuptools.package-data]
41
42
  dash_startup_loading_plugin = [
42
- "README.md",
43
- "README.zh-CN.md",
44
43
  "resources/*.css",
45
44
  "resources/*.js",
45
+ "resources/LOADING-UI-LICENSE",
46
+ "py.typed",
46
47
  ]
47
48
 
48
49
  [tool.pytest.ini_options]
@@ -5,14 +5,11 @@ 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__ = "1.0.4"
8
+ __version__ = "1.1.0"
9
9
 
10
10
  from .plugin import (
11
11
  StartupLoadingConfig,
12
- configure,
13
- configure_dac,
14
- configure_fac,
15
- configure_dmc,
12
+ setup,
16
13
  get_config,
17
14
  reset_config,
18
15
  )
@@ -20,10 +17,7 @@ from .plugin import (
20
17
  __all__ = [
21
18
  "StartupLoadingConfig",
22
19
  "__version__",
23
- "configure",
24
- "configure_dac",
25
- "configure_fac",
26
- "configure_dmc",
20
+ "setup",
27
21
  "get_config",
28
22
  "reset_config",
29
23
  ]
@@ -0,0 +1,416 @@
1
+ """Dash Hooks registration and startup-overlay configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass, fields, replace
8
+ from functools import lru_cache
9
+ from html import escape
10
+ from importlib.resources import files
11
+ from threading import RLock
12
+ from typing import Any, Literal, TypedDict, cast, get_args
13
+ from weakref import WeakKeyDictionary, ref
14
+
15
+ from dash import get_app, hooks
16
+ from dash.exceptions import AppNotFoundError
17
+ from flask import current_app, has_app_context, request
18
+ from typing_extensions import Unpack
19
+
20
+ _OVERLAY_MARKER = "data-dash-loading"
21
+ _BODY_PATTERN = re.compile(r"<body(?:\s[^>]*)?>", flags=re.IGNORECASE)
22
+ _HEAD_END_PATTERN = re.compile(r"</head\s*>", flags=re.IGNORECASE)
23
+ _CONFIG_LOCK = RLock()
24
+ _DASH_APPS_BY_SERVER: WeakKeyDictionary[Any, list[Any]] = WeakKeyDictionary()
25
+ _explicit_options: frozenset[str] = frozenset()
26
+
27
+ LoaderName = Literal[
28
+ "default",
29
+ "antd",
30
+ "accordion-loader",
31
+ "analyzing-image",
32
+ "arc",
33
+ "bars",
34
+ "bobbing-dots",
35
+ "bouncing-dots",
36
+ "classic",
37
+ "clock-ring",
38
+ "comet-spinner",
39
+ "concentric-ring",
40
+ "conveyor-loop",
41
+ "dash-ring",
42
+ "diamond",
43
+ "dots",
44
+ "dots-ring",
45
+ "dual-arc",
46
+ "fade-arc",
47
+ "infinity",
48
+ "infinity-square-snake",
49
+ "infinity-track",
50
+ "morphing-infinity",
51
+ "orbit-ring",
52
+ "pulsating-dots",
53
+ "pulse",
54
+ "pulse-dot",
55
+ "quarter-ring",
56
+ "ring",
57
+ "ripple",
58
+ "satellite-ring",
59
+ "skeleton",
60
+ "spiral",
61
+ "spokes",
62
+ "square-accordion",
63
+ "square-grid",
64
+ "square-snake",
65
+ "swirling",
66
+ "symmetric-wave",
67
+ "terminal",
68
+ "text-blink",
69
+ "text-dots",
70
+ "text-shimmer",
71
+ "text-shimmer-wave",
72
+ "triple-dot-spinner",
73
+ "twin-orbit",
74
+ "typing",
75
+ "wandering-eyes",
76
+ "wave",
77
+ ]
78
+ ThemeMode = Literal["auto", "light", "dark"]
79
+ _LOADING_UI_LOADERS = frozenset(get_args(LoaderName)) - {"default", "antd"}
80
+
81
+
82
+ @hooks.setup(priority=100)
83
+ def _register_dash_app(app: Any) -> None:
84
+ _DASH_APPS_BY_SERVER.setdefault(app.server, []).append(ref(app))
85
+
86
+
87
+ class SetupOptions(TypedDict, total=False):
88
+ enabled: bool
89
+ aria_label: str
90
+ z_index: int
91
+ background: str
92
+ dark_background: str
93
+ loader_color: str | None
94
+ loader_dark_color: str | None
95
+ theme_mode: ThemeMode
96
+ loader: LoaderName
97
+ loader_text: str
98
+ loader_size: int | None
99
+ loader_stroke_width: int
100
+ custom_loader_html: str | None
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class StartupLoadingConfig:
105
+ """Configuration serialized into the startup overlay.
106
+
107
+ ``custom_loader_html`` is inserted verbatim and must only contain trusted
108
+ HTML supplied by the application author.
109
+ """
110
+
111
+ enabled: bool = True
112
+ aria_label: str = "Loading"
113
+ z_index: int = 9999
114
+ background: str = "#ffffff"
115
+ dark_background: str = "#121212"
116
+ loader_color: str | None = None
117
+ loader_dark_color: str | None = None
118
+ theme_mode: ThemeMode = "auto"
119
+ loader: LoaderName = "default"
120
+ loader_text: str = "Loading"
121
+ loader_size: int | None = 12
122
+ loader_stroke_width: int = 2
123
+ custom_loader_html: str | None = None
124
+
125
+
126
+ _DEFAULT_CONFIG = StartupLoadingConfig()
127
+ _config = _DEFAULT_CONFIG
128
+
129
+
130
+ def _validate(config: StartupLoadingConfig) -> StartupLoadingConfig:
131
+ if not isinstance(config.enabled, bool):
132
+ raise TypeError("enabled must be a boolean")
133
+ if config.theme_mode not in {"auto", "light", "dark"}:
134
+ raise ValueError("theme_mode must be 'auto', 'light', or 'dark'")
135
+ if config.loader not in _LOADING_UI_LOADERS | {"default", "antd"}:
136
+ raise ValueError("loader must be a Loading UI loader name, 'default', or 'antd'")
137
+ if not isinstance(config.loader_text, str) or not config.loader_text.strip():
138
+ raise ValueError("loader_text must be a non-empty string")
139
+ for name in ("loader_color", "loader_dark_color"):
140
+ value = getattr(config, name)
141
+ if value is not None and (not isinstance(value, str) or not value.strip()):
142
+ raise ValueError(f"{name} must be None or a non-empty CSS color")
143
+ if config.loader_size is not None and config.loader_size < 0:
144
+ raise ValueError("loader_size must be greater than or equal to zero")
145
+ if config.loader_stroke_width < 0:
146
+ raise ValueError("loader_stroke_width must be greater than or equal to zero")
147
+ return config
148
+
149
+
150
+ def setup(**changes: Unpack[SetupOptions]) -> StartupLoadingConfig:
151
+ """Update the process-wide plugin configuration.
152
+
153
+ Call this before creating ``dash.Dash``. The Dash hooks registry is
154
+ process-wide, so one configuration is shared by all apps in the process.
155
+ """
156
+
157
+ valid_names = {field.name for field in fields(StartupLoadingConfig)}
158
+ unknown = set(changes).difference(valid_names)
159
+ if unknown:
160
+ names = ", ".join(sorted(unknown))
161
+ raise TypeError(f"Unknown startup loading option(s): {names}")
162
+ global _config, _explicit_options
163
+ with _CONFIG_LOCK:
164
+ _config = _validate(replace(_config, **changes))
165
+ _explicit_options = _explicit_options.union(changes)
166
+ return _config
167
+
168
+
169
+ def get_config() -> StartupLoadingConfig:
170
+ """Return the active immutable configuration."""
171
+
172
+ with _CONFIG_LOCK:
173
+ return _config
174
+
175
+
176
+ def _configuration_state() -> tuple[StartupLoadingConfig, frozenset[str]]:
177
+ with _CONFIG_LOCK:
178
+ return _config, _explicit_options
179
+
180
+
181
+ def reset_config() -> StartupLoadingConfig:
182
+ """Restore the default configuration, primarily for tests."""
183
+
184
+ global _config, _explicit_options
185
+ with _CONFIG_LOCK:
186
+ _config = _DEFAULT_CONFIG
187
+ _explicit_options = frozenset()
188
+ return _config
189
+
190
+
191
+ def _mantine_forced_color_scheme(layout: Any) -> str | None:
192
+ """Return a static MantineProvider color scheme without rendering the layout."""
193
+
194
+ if layout is None or callable(layout):
195
+ return None
196
+ pending = [layout]
197
+ seen: set[int] = set()
198
+ while pending:
199
+ component = pending.pop()
200
+ if component is None or id(component) in seen:
201
+ continue
202
+ seen.add(id(component))
203
+ if (
204
+ getattr(component, "_namespace", None) == "dash_mantine_components"
205
+ and getattr(component, "_type", None) == "MantineProvider"
206
+ ):
207
+ scheme = getattr(component, "forceColorScheme", None)
208
+ if scheme in {"light", "dark"}:
209
+ return scheme
210
+ children = getattr(component, "children", None)
211
+ if isinstance(children, (list, tuple)):
212
+ pending.extend(children)
213
+ elif children is not None:
214
+ pending.append(children)
215
+ return None
216
+
217
+
218
+ def _theme_config(
219
+ config: StartupLoadingConfig,
220
+ app_index: str,
221
+ layout: Any = None,
222
+ ) -> dict[str, Any]:
223
+ values: dict[str, Any] = {
224
+ "themeMode": config.theme_mode,
225
+ "mantineBundle": "dash_mantine_components" in app_index,
226
+ }
227
+ forced_scheme = _mantine_forced_color_scheme(layout)
228
+ if forced_scheme:
229
+ values["mantineForcedColorScheme"] = forced_scheme
230
+ return values
231
+
232
+
233
+ def _resolved_loader_colors(
234
+ config: StartupLoadingConfig,
235
+ loader: LoaderName,
236
+ ) -> tuple[str, str]:
237
+ defaults = ("#1677ff", "#4096ff") if loader == "antd" else ("#000", "#fff")
238
+ return (
239
+ config.loader_color or defaults[0],
240
+ config.loader_dark_color or defaults[1],
241
+ )
242
+
243
+
244
+ def _overlay_html(
245
+ config: StartupLoadingConfig,
246
+ loader: LoaderName | None = None,
247
+ ) -> str:
248
+ aria_label = escape(config.aria_label, quote=True)
249
+ class_name = "dash-loading"
250
+ selected_loader = loader or config.loader
251
+ light_color, dark_color = _resolved_loader_colors(config, selected_loader)
252
+ if config.loader_size is not None:
253
+ spinner_size = f"{config.loader_size}px"
254
+ spinner_scale = config.loader_size / 20
255
+ else:
256
+ spinner_size = "20px"
257
+ spinner_scale = 1
258
+ loading_ui_stroke = (
259
+ config.loader_stroke_width / spinner_scale
260
+ if spinner_scale > 0
261
+ else config.loader_stroke_width
262
+ )
263
+ styles = {
264
+ "--dash-loading-background": config.background,
265
+ "--dash-loading-dark-background": config.dark_background,
266
+ "--dash-loading-loader-color": light_color,
267
+ "--dash-loading-loader-dark-color": dark_color,
268
+ "--dash-loading-size": spinner_size,
269
+ "--dash-loading-scale": f"{spinner_scale:g}",
270
+ "--dash-loading-ui-display": "none" if spinner_scale == 0 else "inline-flex",
271
+ "--dash-loading-stroke": f"{config.loader_stroke_width}px",
272
+ "--dash-loading-ui-stroke": f"{loading_ui_stroke:g}px",
273
+ "--dash-loading-z-index": str(config.z_index),
274
+ }
275
+ if config.background != _DEFAULT_CONFIG.background:
276
+ styles["--dash-loading-mantine-light-background"] = config.background
277
+ if config.dark_background != _DEFAULT_CONFIG.dark_background:
278
+ styles["--dash-loading-mantine-dark-background"] = config.dark_background
279
+ style = escape(";".join(f"{name}:{value}" for name, value in styles.items()), quote=True)
280
+ ring = (
281
+ '<svg class="dash-loading__ring" viewBox="0 0 24 24" fill="none" '
282
+ 'aria-hidden="true" xmlns="http://www.w3.org/2000/svg">'
283
+ '<path d="M21 12.0004C20.9999 13.901 20.3981 15.7528 19.2809 17.2904'
284
+ 'C18.1637 18.8279 16.5885 19.9723 14.7809 20.5596'
285
+ 'C12.9733 21.1469 11.0262 21.1468 9.21864 20.5594'
286
+ 'C7.41109 19.9721 5.83588 18.8276 4.71876 17.29'
287
+ 'C3.60165 15.7523 2.99999 13.9005 3 11.9999'
288
+ 'C3.00001 10.0993 3.60171 8.24755 4.71884 6.70994'
289
+ 'C5.83598 5.17233 7.4112 4.02785 9.21877 3.44052'
290
+ 'C11.0263 2.85319 12.9734 2.85316 14.781 3.44044" '
291
+ 'stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>'
292
+ '</svg>'
293
+ )
294
+ loader_html = config.custom_loader_html
295
+ if loader_html is None:
296
+ if selected_loader == "default":
297
+ loader_html = '<span class="dash-loading__spinner" aria-hidden="true"></span>'
298
+ elif selected_loader == "antd":
299
+ loader_html = (
300
+ '<span class="dash-loading__antd-spinner" aria-hidden="true">'
301
+ '<span class="dash-loading__antd-dot">'
302
+ '<i></i><i></i><i></i><i></i>'
303
+ '</span></span>'
304
+ )
305
+ elif selected_loader == "ring":
306
+ loader_html = ring
307
+ else:
308
+ name = escape(selected_loader, quote=True)
309
+ loader_text = escape(config.loader_text, quote=True)
310
+ loader_html = (
311
+ f'<span class="dash-loading__loading-ui" data-dash-loading-ui="{name}" '
312
+ f'data-dash-loading-text="{loader_text}" '
313
+ 'aria-hidden="true"></span>'
314
+ )
315
+
316
+ content_class = "dash-loading__content"
317
+ if config.custom_loader_html is None and selected_loader in _LOADING_UI_LOADERS:
318
+ content_class += " dash-loading__content--loading-ui"
319
+ loader_html = f'<div class="dash-loading__spinner-region">{loader_html}</div>'
320
+
321
+ return (
322
+ f'<div class="{class_name}" {_OVERLAY_MARKER} '
323
+ 'role="status" aria-live="polite" '
324
+ f'aria-label="{aria_label}" aria-busy="true" style="{style}">'
325
+ f'<div class="{content_class}">{loader_html}</div>'
326
+ "</div>"
327
+ )
328
+
329
+
330
+ @lru_cache(maxsize=4)
331
+ def _resource_text(name: str) -> str:
332
+ return files("dash_startup_loading_plugin").joinpath("resources").joinpath(name).read_text(encoding="utf-8").strip()
333
+
334
+
335
+ def _inline_head_resources(
336
+ app_index: str,
337
+ config: StartupLoadingConfig,
338
+ layout: Any = None,
339
+ ) -> str:
340
+ theme_config = json.dumps(
341
+ _theme_config(config, app_index, layout),
342
+ ensure_ascii=False,
343
+ separators=(",", ":"),
344
+ )
345
+ resources = (
346
+ '<style data-dash-loading-resource="style">'
347
+ f"{_resource_text('loading.css')}"
348
+ "</style>"
349
+ '<script data-dash-loading-resource="theme">'
350
+ f"window.__dashLoadingThemeConfig={theme_config};"
351
+ f"{_resource_text('theme.js')}"
352
+ "</script>"
353
+ )
354
+ head_end = _HEAD_END_PATTERN.search(app_index)
355
+ if head_end is not None:
356
+ return app_index[: head_end.start()] + resources + app_index[head_end.start() :]
357
+ return resources + app_index
358
+
359
+
360
+ def _inject_overlay(app_index: str, layout: Any = None) -> str:
361
+ config, explicit_options = _configuration_state()
362
+ if not config.enabled or _OVERLAY_MARKER in app_index:
363
+ return app_index
364
+
365
+ body_match = _BODY_PATTERN.search(app_index)
366
+ if body_match is None:
367
+ return app_index
368
+ app_index = _inline_head_resources(app_index, config, layout)
369
+ body_match = _BODY_PATTERN.search(app_index)
370
+ assert body_match is not None
371
+ position = body_match.end()
372
+ dash_antd_bundle = "dash_antd_components" in app_index
373
+ loader = (
374
+ "antd"
375
+ if dash_antd_bundle and "loader" not in explicit_options
376
+ else config.loader
377
+ )
378
+ scripts = ""
379
+ if config.custom_loader_html is None and loader in _LOADING_UI_LOADERS - {"ring"}:
380
+ scripts += (
381
+ '<script data-dash-loading-resource="loading-ui">'
382
+ f'{_resource_text("loading-ui.js")}'
383
+ '</script>'
384
+ )
385
+ scripts += f'<script data-dash-loading-resource="script">{_resource_text("loading.js")}</script>'
386
+ return app_index[:position] + _overlay_html(config, loader) + scripts + app_index[position:]
387
+
388
+
389
+ def _current_app_layout() -> Any:
390
+ if has_app_context():
391
+ server = cast(Any, current_app)._get_current_object()
392
+ registered = [app_ref() for app_ref in _DASH_APPS_BY_SERVER.get(server, [])]
393
+ apps = [app for app in registered if app is not None]
394
+ if apps:
395
+ path = request.path
396
+ matches = [
397
+ app
398
+ for app in apps
399
+ if path.startswith(app.config.routes_pathname_prefix)
400
+ ]
401
+ selected = max(
402
+ matches or apps,
403
+ key=lambda app: len(app.config.routes_pathname_prefix),
404
+ )
405
+ return selected.layout
406
+ try:
407
+ return get_app().layout
408
+ except AppNotFoundError:
409
+ return None
410
+
411
+
412
+ @hooks.index(priority=100)
413
+ def inject_startup_loading(app_index: str) -> str:
414
+ """Inject the pre-React overlay into the final HTML document."""
415
+
416
+ return _inject_overlay(app_index, _current_app_layout())
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bartosz Zagrodzki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.