# RouteProgress

> A thin progress bar along the top edge for navigation and background jobs. It creeps while work runs, jumps to full when it ends, and a hook counts overlapping jobs.

Source: https://docs.nasaqui.com/components/route-progress

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/route-progress.json
```

A two pixel bar on the top edge of the screen, the kind that shows a page is on its way. It does not know how long
the work takes, so it creeps forward, slows down near the end and finishes when you tell it to. It draws only. The
router, the fetch or the job queue decides when work starts and stops.

## When to use

- A route change or a data refetch that takes longer than a moment.
- Several small background jobs (saves, syncs) that should share one signal.

## When not to use

- A task with a known length or a place inside a card: use [`Progress`](https://docs.nasaqui.com/components/progress).
- A single button waiting: use the button's `loading` state.

## Import

```tsx
import { RouteProgress, useRouteProgress } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { RouteProgress, useRouteProgress } from "@fadymondy/nasaq/web";

export function Shell({ children }: { children: React.ReactNode }) {
  const jobs = useRouteProgress();
  return (
    <>
      <RouteProgress active={jobs.active} />
      <button onClick={() => jobs.run(fetch("/api/report"))}>Refresh</button>
      {children}
    </>
  );
}
```

With a router, call `jobs.start()` when navigation begins and the function it returns when it ends.

## Anatomy

```
RouteProgress        data-slot="route-progress" (data-state), role="progressbar"
└─ bar               data-slot="route-progress-bar", fills from the inline start
```

## API

Every `div` prop except `children`, plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `active` | `boolean` | `false` | Work is running. Creeps to 94%, then finishes at 100% when it turns false. |
| `value` | `number` | | Pin an exact percentage. Overrides `active`. Hidden at 0 and at 100. |
| `tone` | `"default" \| "info" \| "success" \| "warning" \| "danger"` | `"default"` | Fill colour. |
| `placement` | `"fixed" \| "absolute"` | `"fixed"` | Screen top, or the top of a `relative` parent. |
| `interval` | `number` | `250` | Milliseconds between creeps. |
| `labels` | `Partial<RouteProgressLabels>` | | Override the accessible name. |

**useRouteProgress()** returns `{ active, count, start, run }`. `start()` returns a finisher, safe to call twice.
`run(promiseOrFn)` wraps a promise and always finishes. **nextTrickle(value)** is the pure step function.

## Examples

**Pinned to a real upload**

```tsx
<RouteProgress value={(uploaded / total) * 100} tone="info" />
```

## Accessibility

- `role="progressbar"` with an accessible name and the current value. While idle it is `aria-hidden`.
- The motion is a width change only, and it is switched off under `prefers-reduced-motion`.
- The bar never carries information alone: pair it with visible content that changes, like a skeleton.

## RTL & i18n

- The fill starts at the inline start, so it grows from the right in Arabic. The accessible name is in English or Arabic by locale.

## Styling & tokens

- Uses `bg-primary` and the `nq-info`, `nq-success`, `nq-warning`, `nq-danger` fills. Target `[data-slot="route-progress"]`.

## Do / Don't

- Do mount one bar per app and count jobs with `useRouteProgress`.
- Don't start it for work under about 150 ms: show it after a short delay in your router glue.
- Don't use it as a progress meter for a known task.

## Related

- [`Progress`](https://docs.nasaqui.com/components/progress)
- [`Spinner`](https://docs.nasaqui.com/components/spinner)
- [`Toast`](https://docs.nasaqui.com/components/toast)

## Code

### React

```tsx
import { RouteProgress, useRouteProgress } from "@fadymondy/nasaq/web";

export function Shell({ children }: { children: React.ReactNode }) {
  const jobs = useRouteProgress();
  return (
    <>
      <RouteProgress active={jobs.active} />
      <button onClick={() => jobs.run(fetch("/api/report"))}>Refresh</button>
      {children}
    </>
  );
}
```

### shadcn

```tsx
import { RouteProgress, useRouteProgress } from "@/components/ui/route-progress";

export function Shell({ children }: { children: React.ReactNode }) {
  const jobs = useRouteProgress();
  return (
    <>
      <RouteProgress active={jobs.active} />
      <button onClick={() => jobs.run(fetch("/api/report"))}>Refresh</button>
      {children}
    </>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqRouteProgress, useRouteProgress } from "@fadymondy/nasaq/vue";

const jobs = useRouteProgress();
const refresh = () => jobs.run(new Promise((resolve) => setTimeout(resolve, 2000)));
</script>

<template>
  <div class="relative">
    <NqRouteProgress :active="jobs.active.value" placement="absolute" />
    <NqButton @click="refresh">Refresh</NqButton>
  </div>
</template>
```

### Blade

```blade
<div class="relative" x-data="{ loading: false, refresh() { this.loading = true; setTimeout(() => this.loading = false, 2000) } }">
    <x-nq::route-progress x-modelable="active" x-model="loading" :active="false" placement="absolute" />
    <x-nq::button x-on:click="refresh()">Refresh</x-nq::button>
</div>
```

### HTML + Alpine

```html
<div class="relative" x-data="{ loading: false, refresh() { this.loading = true; setTimeout(() => this.loading = false, 2000) } }">
    <div data-slot="route-progress" data-state="idle"
    x-data="nqRouteProgress(false, null, 250)" x-bind="root"
    role="progressbar" aria-label="Loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
     aria-hidden="true"     x-modelable="active" x-model="loading" class="pointer-events-none inset-x-0 top-0 z-[60] h-0.5 overflow-hidden transition-opacity duration-200 ease-nq motion-reduce:transition-none absolute opacity-0">
    <div data-slot="route-progress-bar" x-bind="bar" style="inline-size: 0%"
        class="h-full rounded-e-full transition-[inline-size] duration-200 ease-nq motion-reduce:transition-none bg-primary"></div>
</div>
    <button data-slot="button"
     type="button"                         x-on:click="refresh()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Refresh</button>
</div>
```
