Metadata-Version: 2.4
Name: dash-startup-loading-plugin
Version: 0.3.0
Summary: A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin.
Author-email: Ethan Zhang <ethan.zhang2016@gmail.com>
License-Expression: MIT
Keywords: dash,plotly,loading,plugin
Classifier: Framework :: Dash
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: dash==4.4.1
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Provides-Extra: mantine
Requires-Dist: dash-mantine-components>=2.6.0; extra == "mantine"
Dynamic: license-file

# dash-startup-loading-plugin

`dash-startup-loading-plugin` is an installable [Dash Hooks plugin](https://dash.plotly.com/dash-plugins-using-hooks)
that displays a full-screen loading overlay while a Dash application performs
its initial browser-side startup.

The overlay, CSS, and JavaScript are injected into the final HTML document with
`hooks.index`, so they are available before Dash and React mount the application
layout without additional startup resource requests. An app does not need to
copy assets or replace Dash's `index_string`.

## Features

- Appears before the Dash renderer starts, avoiding a blank startup page.
- Is discovered automatically through Dash's `dash_hooks` entry point.
- Waits for real rendered content and optional app-specific readiness selectors.
- Can wait for lazy-loading placeholders to disappear.
- Includes a timeout fallback, minimum display time, and fade-out transition.
- Inlines its small startup CSS and JavaScript, avoiding two additional requests.
- Resolves light and dark themes before first paint from Dash component
  persistence, Tailwind root classes, Mantine color-scheme state, or the
  operating-system preference.
- Exposes a small browser API and emits a completion event.
- Requires no changes to the app layout or callbacks.

## Requirements

- Python 3.9 or later
- Dash 3.0.3 or later

Dash Hooks were introduced in Dash 3.0. The plugin uses automatic hook
discovery, resource hooks, and the index hook described in the official
[Dash plugin documentation](https://dash.plotly.com/dash-plugins-using-hooks).

## Installation

```bash
pip install "dash-startup-loading-plugin>=0.3.0"
```

The package declares the following entry point:

```toml
[project.entry-points."dash_hooks"]
dash_startup_loading_plugin = "dash_startup_loading_plugin"
```

Dash imports registered `dash_hooks` packages automatically. Installing the
package therefore enables the default startup overlay for Dash applications in
that Python environment; an explicit import is only needed when changing its
configuration or using its Python API.

## What's new in 0.3.0

- Adds `configure_antd()` with matching `#ffffff` and `#121212` loading
  backgrounds plus explicit light/dark overrides.
- Adds `configure_mantine()` with theme defaults sourced from Dash Mantine
  Components and its pre-render color-scheme hook.
- Updates the Dash Ant Design example for the structured
  `ConfigProvider.theme.algorithm` API.
- Adds a Dash Mantine Components example with persisted theme switching.

## What's new in 0.2.0

- Resolves the loading background before first paint from a persisted
  `dash_antd_components.ConfigProvider.theme`.
- Supports Tailwind's `html.dark` and `html.light` classes.
- Supports Dash Mantine Components' `data-mantine-color-scheme` attribute and
  `mantine-color-scheme-value` localStorage entry.
- Adds `theme_mode` and `dash_theme_component_id` configuration.
- Keeps the startup CSS and JavaScript inline, so theme detection does not add
  network requests before Dash mounts.

## Quick start

The defaults work without any plugin-specific code:

```python
from dash import Dash, html

app = Dash(__name__)
app.layout = html.Main(
    [
        html.H1("My Dash app"),
        html.P("The overlay disappears after this layout is mounted."),
    ]
)

if __name__ == "__main__":
    app.run(debug=True)
```

To customize the readiness conditions or appearance, call `configure()` before
creating the `Dash` instance:

```python
from dash import Dash, html
from dash_startup_loading_plugin import configure

configure(
    required_selectors=["#usage-header", "#usage-sidebar-menu"],
    pending_selector="[data-dac-async-placeholder]",
    timeout_ms=6000,
    fade_duration_ms=160,
)

app = Dash(__name__)
app.layout = html.Div(
    [
        html.Header("Dashboard", id="usage-header"),
        html.Nav("Navigation", id="usage-sidebar-menu"),
    ]
)
```

Runnable examples:

- [`examples/basic.py`](examples/basic.py): Dash Ant Design theme persistence.
- [`examples/mantine_theme.py`](examples/mantine_theme.py):
  Dash Mantine Components with a persisted light/dark theme switch.

Run the Mantine example with:

```bash
pip install -e ".[mantine]"
python examples/mantine_theme.py
```

## Readiness behavior

The overlay closes with the `ready` reason after all of these conditions are
true:

1. `root_selector` exists.
2. The root no longer contains Dash's initial `._dash-loading` element.
3. The root contains an element or non-empty text node.
4. Every CSS selector in `required_selectors` exists in the document.
5. No node matching `pending_selector` remains under the root.
6. The conditions stay true for two consecutive animation frames.

A `MutationObserver` rechecks these conditions as the page changes. The
two-frame confirmation prevents the overlay from disappearing during an
intermediate render.

`timeout_ms` is a safety fallback and dismisses the overlay even when the
readiness contract is not satisfied. Set it to `None` to disable forced
dismissal. A timeout is not delayed by `minimum_display_ms`.

## Configuration reference

`configure(**changes)` updates only the supplied values and returns the active,
immutable `StartupLoadingConfig` instance.

| Option | Default | Description |
|---|---:|---|
| `enabled` | `True` | Inject the startup overlay. Set to `False` to disable it. |
| `overlay_id` | `"dash-startup-loading"` | HTML `id` of the injected overlay; also used by the browser API. |
| `aria_label` | `"Loading"` | Accessible label on the overlay's `role="status"` element. |
| `root_selector` | `"#react-entry-point"` | Dash renderer root observed for mounted content. |
| `required_selectors` | `("#react-entry-point",)` | Iterable of document-level CSS selectors that must all exist. A single string is not accepted. |
| `pending_selector` | `"[data-dac-async-placeholder]"` | Selector for placeholders under the root that delay dismissal. Use `None` to disable this check. |
| `timeout_ms` | `6000` | Forced-dismiss timeout in milliseconds. Use `None` to disable it. |
| `minimum_display_ms` | `0` | Minimum display time for ready or manual dismissal. |
| `fade_duration_ms` | `160` | Opacity transition duration before the overlay is removed. |
| `z_index` | `9999` | Overlay stacking order. |
| `background` | `"#ffffff"` | Light-theme background color. |
| `dark_background` | `"#0f0f0f"` | Dark-theme background color. |
| `color` | `"#1677ff"` | Light-theme spinner/current color. |
| `dark_color` | `"#4096ff"` | Dark-theme spinner/current color. |
| `theme_mode` | `"auto"` | Startup theme policy: `"auto"`, `"light"`, or `"dark"`. Auto follows persisted or active app state, then the operating-system preference. |
| `dash_theme_component_id` | `None` | Preferred Dash component ID when reading a persisted `theme` prop. When omitted, the first compatible persisted theme is used. |
| `spinner_size_px` | `28` | Default spinner width and height in pixels. |
| `spinner_stroke_px` | `3` | Default spinner stroke width in pixels. |
| `hide_default_loading` | `True` | Hide Dash's built-in initial `._dash-loading` indicator while the overlay is present. |
| `custom_loader_html` | `None` | Trusted HTML that replaces the default spinner. |

With `theme_mode="auto"`, the inline head bootstrap resolves the startup theme
in this order:

1. The persisted Dash `theme` for `dash_theme_component_id`, when configured.
2. An existing `html.dark` or `html.light` class, including Tailwind's manual
   dark-mode convention.
3. `data-theme`, `data-color-scheme`, or `data-mantine-color-scheme` on
   `<html>`.
4. Other Dash local persistence entries for a component's `theme` prop. Set
   `dash_theme_component_id` when an app contains more than one persisted
   theme-capable component.
5. Dash Mantine Components' `mantine-color-scheme-value` localStorage entry,
   followed by the common `theme`, `color-theme`, and `color-scheme` keys.
6. `prefers-color-scheme`.

The result is written to `data-dash-startup-loading-theme` before the body is
parsed. This isolates the overlay styling without changing classes or data
attributes owned by Tailwind, Dash Mantine Components, or the application.
The stylesheet also adjusts its animation for `prefers-reduced-motion`.

### Dash Mantine Components

Dash Mantine Components 2.6 and later includes `ColorSchemeToggle`, which
switches the active color scheme without a callback and persists it in
`mantine-color-scheme-value`. The startup plugin reads that value before Dash
and React mount. To make the loading transition seamless, use Mantine's current
default dark body color for the overlay:

```python
import dash_mantine_components as dmc
from dash_startup_loading_plugin import configure_mantine

configure_mantine()

app.layout = dmc.MantineProvider(
    [
        dmc.ColorSchemeToggle(
            lightIcon=dmc.Text("☀"),
            darkIcon=dmc.Text("☾"),
        ),
        dmc.Text("Application content", id="app-ready"),
    ],
    defaultColorScheme="auto",
)
```

See
[`examples/mantine_theme.py`](examples/mantine_theme.py)
for the complete runnable example.

`configure_mantine()` keeps DMC optional. When used, it registers DMC's
pre-render color-scheme hook, disables the Dash Ant Design placeholder check,
and derives the light and dark loading backgrounds from DMC's active default
theme. Any explicit configuration still takes precedence:

```python
configure_mantine(dark_background="#202020")
```

### Dash Ant Design persistence

Current `dash_antd_components.ConfigProvider.theme` accepts a structured theme
configuration whose `algorithm` is `"default"`, `"dark"`, `"compact"`, or an
array of those values. Persist a user-facing system/light/dark selector, resolve
the system preference in a clientside callback, and store the semantic choice
in the small conventional `localStorage["theme"]` value that the startup plugin
reads before Dash mounts:

```python
import dash_antd_components as dac
from dash import Input, Output, clientside_callback
from dash_startup_loading_plugin import configure_antd

configure_antd()

provider = dac.ConfigProvider(
    [
        app_content,
        dac.Segmented(
            id="theme-mode",
            options=["system", "light", "dark"],
            value="system",
            persistence=True,
            persisted_props=["value"],
            persistence_type="local",
        ),
    ],
    id="theme-provider",
    theme={"algorithm": "default"},
)

clientside_callback(
    """function(mode) {
        localStorage.setItem("theme", JSON.stringify(mode));
        const dark = mode === "dark" || (
            mode === "system"
            && window.matchMedia("(prefers-color-scheme: dark)").matches
        );
        return {algorithm: dark ? "dark" : "default"};
    }""",
    Output("theme-provider", "theme"),
    Input("theme-mode", "value"),
)
```

The selector owns the semantic `"system"` state; `ConfigProvider` receives only
the algorithms allowed by its current API. `configure_antd()` uses `#ffffff`
and `#121212`, matching the component library's default light and dark
backgrounds. If the application customizes either background, pass the same
values explicitly:

```python
configure_antd(
    background="#f5f5f5",
    dark_background="#202020",
)
```

See
[`examples/basic.py`](examples/basic.py) for the complete switcher.

### Custom loader markup

```python
from dash_startup_loading_plugin import configure

configure(
    aria_label="Loading dashboard",
    custom_loader_html="""
    <div class="brand-loader" aria-hidden="true">
      <span></span><span></span><span></span>
    </div>
    """,
)
```

Add the matching `.brand-loader` rules to the app's normal `assets` directory.
`custom_loader_html` is inserted verbatim and must be trusted application
configuration. Never populate it with user input.

## Python API

```python
from dash_startup_loading_plugin import (
    StartupLoadingConfig,
    configure,
    configure_antd,
    configure_mantine,
    get_config,
    reset_config,
)
```

- `configure(**changes)` validates and applies a partial configuration update.
- `configure_antd(**changes)` matches the loading backgrounds to Dash Ant
  Design's light and dark themes and accepts explicit overrides.
- `configure_mantine(**changes)` applies DMC theme defaults and registers DMC's
  pre-render color-scheme hook.
- `get_config()` returns the current immutable configuration.
- `reset_config()` restores all defaults. It is primarily useful in tests.
- `StartupLoadingConfig` is the frozen dataclass containing all options.

Unknown option names raise `TypeError`. Invalid selector collections and
negative timing or spinner values are rejected rather than silently ignored.

## Browser API and events

The plugin exposes two methods for integrations that need explicit control:

```javascript
// Recheck the configured readiness conditions.
window.dashStartupLoading.check();

// Begin a manual dismissal.
window.dashStartupLoading.finish();

// A custom overlay_id can be supplied to either method.
window.dashStartupLoading.finish("my-loading-overlay");
```

Before fading out, the overlay dispatches a bubbling
`dash-startup-loading:ready` event. Its `detail.reason` is `"ready"`,
`"timeout"`, or `"manual"`:

```javascript
document.addEventListener("dash-startup-loading:ready", function (event) {
    console.log("Startup overlay finished:", event.detail.reason);
});
```

## Migrating from a custom `index_string`

If an app previously injected a loader into a hand-written `index_string`, move
its readiness contract into the plugin and remove the `app.index_string = ...`
assignment:

```python
from dash_startup_loading_plugin import configure

configure(
    required_selectors=["#usage-header", "#usage-sidebar-menu"],
    pending_selector="[data-dac-async-placeholder]",
    timeout_ms=6000,
    fade_duration_ms=160,
)
```

The plugin preserves Dash's normal index template, injects its stylesheet before
`</head>`, and inserts the overlay followed by its startup script directly after
the opening `<body>` tag. The source CSS and JavaScript remain package resources
for maintainability, but the browser receives them inline and makes no separate
request for either file. The index hook uses priority `100`, so it runs before
lower-priority index hooks. If multiple hooks share the same priority, Dash does
not guarantee their relative order.

Because the resources are inline, applications with a strict Content Security
Policy must permit the injected style and script, or add nonce support in a
customized deployment. The previous external-resource form is more suitable
when a policy forbids all inline code.

## Scope and process model

Dash's hook registry is process-wide. `configure()` therefore affects every
Dash app created in the same Python process. Use one shared configuration per
process, or set `enabled=False` when the overlay should not be injected.

This plugin handles initial application startup only. It does not show a
full-screen overlay for later callback execution; use `dcc.Loading` or another
callback-specific loading pattern for that use case.

## Troubleshooting

### The overlay only disappears after the timeout

One of the configured selectors is probably never becoming ready. Check that:

- `root_selector` matches the actual Dash renderer root.
- Every `required_selectors` entry exists in the final document.
- `pending_selector` does not match a placeholder that remains permanently.
- Custom selectors are valid CSS selectors.

Temporarily keep the fallback enabled and listen for the completion event. A
`"timeout"` reason confirms that the readiness contract was not met.

### The overlay disappears too quickly

Set `minimum_display_ms` to keep it visible for a predictable minimum duration,
or add stable application elements to `required_selectors`.

### The overlay appears in every app in the environment

This is expected with automatic `dash_hooks` discovery. Use a dedicated virtual
environment, uninstall the package where it is not wanted, or call
`configure(enabled=False)` before constructing those Dash apps.

## License

MIT. See [`LICENSE`](LICENSE).
