Metadata-Version: 2.4
Name: dash-startup-loading-plugin
Version: 0.1.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,hooks
Classifier: Framework :: Dash
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: dash>=3.0.3
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
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 is injected into the final HTML document with `hooks.index`, so it
is visible before Dash and React mount the application layout. The packaged CSS
and JavaScript are registered with `hooks.stylesheet` and `hooks.script`; 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.
- Supports light and dark themes, reduced-motion preferences, custom colors,
  custom spinner geometry, and trusted custom loader markup.
- 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
```

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.

## 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=["#page-header", "#sidebar-menu"],
    pending_selector="[data-dac-async-placeholder]",
    timeout_ms=6000,
    minimum_display_ms=200,
    fade_duration_ms=160,
    color="#1677ff",
    dark_color="#4096ff",
)

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

See [`examples/basic.py`](examples/basic.py) for a runnable example.

## 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. |
| `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. |

The default stylesheet recognizes `html.dark` and `html.light`. When neither
class forces a theme, it follows `prefers-color-scheme`. It also adjusts its
animation when the user enables `prefers-reduced-motion`.

### 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,
    get_config,
    reset_config,
)
```

- `configure(**changes)` validates and applies a partial configuration update.
- `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 the overlay directly
after the opening `<body>` tag, and registers its versioned package assets via
Dash Hooks. 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.

## 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.

## Development

Clone the repository, then install the project and test dependencies:

```bash
uv sync --extra test
```

Run the tests and example:

```bash
uv run pytest
uv run python examples/basic.py
```

Build the source distribution and wheel:

```bash
uv build
```

## License

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