As a user I want a navigation progress bar so I can see that the app is loading after I click a link #289

Open
opened 2026-04-20 18:13:22 +02:00 by marcel · 8 comments
Owner

Problem

When clicking a navigation link (sidebar, header, in-page link), SvelteKit runs the server load function before the next page renders. On slower routes (e.g. /chronik, /documents, document detail) this delay is long enough that users think nothing happened and click again.

There is currently no visual feedback between click and render.

Proposed solution (quick win)

Add a global top progress bar in frontend/src/routes/+layout.svelte driven by SvelteKit's navigating store.

  • Use navigating from $app/state (Svelte 5) to detect pending navigations.
  • Render a thin 2–3px bar at the very top of the viewport.
  • Show only after a small delay (~100ms) so instant navigations stay flicker-free.
  • Style in brand-mint (#A6DAD8) on a transparent track, matching the existing palette.
  • Animated indeterminate "slide" while navigating !== null, then fade out on completion.

Acceptance criteria

  • Clicking a slow-loading nav link shows a progress bar within ~100ms
  • Fast navigations (cached / instant) do NOT flash the bar
  • Bar is visible on all routes (rendered once in the root layout)
  • Bar uses brand-mint color
  • Works for both link clicks and goto() programmatic navigation
  • Accessible: not announced as alert; aria-hidden="true" since navigating state is already semantic
  • Unit test covers: bar hidden when navigating is null, visible when it is set

Out of scope

Per-route skeleton loaders — tracked separately.

Notes

Keeps implementation tiny (single component + root layout change). Does not require refactoring any existing routes.

## Problem When clicking a navigation link (sidebar, header, in-page link), SvelteKit runs the server `load` function before the next page renders. On slower routes (e.g. `/chronik`, `/documents`, document detail) this delay is long enough that users think nothing happened and click again. There is currently no visual feedback between click and render. ## Proposed solution (quick win) Add a global top progress bar in `frontend/src/routes/+layout.svelte` driven by SvelteKit's `navigating` store. - Use `navigating` from `$app/state` (Svelte 5) to detect pending navigations. - Render a thin 2–3px bar at the very top of the viewport. - Show only after a small delay (~100ms) so instant navigations stay flicker-free. - Style in `brand-mint` (`#A6DAD8`) on a transparent track, matching the existing palette. - Animated indeterminate "slide" while `navigating !== null`, then fade out on completion. ## Acceptance criteria - [ ] Clicking a slow-loading nav link shows a progress bar within ~100ms - [ ] Fast navigations (cached / instant) do NOT flash the bar - [ ] Bar is visible on all routes (rendered once in the root layout) - [ ] Bar uses `brand-mint` color - [ ] Works for both link clicks and `goto()` programmatic navigation - [ ] Accessible: not announced as alert; `aria-hidden="true"` since `navigating` state is already semantic - [ ] Unit test covers: bar hidden when `navigating` is null, visible when it is set ## Out of scope Per-route skeleton loaders — tracked separately. ## Notes Keeps implementation tiny (single component + root layout change). Does not require refactoring any existing routes.
marcel added the featureui labels 2026-04-20 18:13:45 +02:00
Author
Owner

🏛️ Markus Keller — Application Architect

Observations

  • This is purely a presentation-layer affordance — no module boundary impact, no ADR needed.
  • navigating from $app/state is already in use in frontend/src/routes/documents/+page.svelte:3 (isLoading={navigating.to !== null}). The pattern is established; the new component just hoists it to the layout.
  • The header in frontend/src/routes/+layout.svelte:48 already renders a decorative <div class="h-1 bg-accent"> strip. Two thin horizontal bars within 2px of each other would be visually redundant.
  • "Quick win" is the right framing. No new dependency, no routing layer changes.

Recommendations

  • Single new component frontend/src/lib/components/NavigationProgress.svelte, composed once in +layout.svelte above the header. Keep it under ~40 lines — one $derived, one $effect for the 100ms delay, one CSS animation. No extraction for reuse — there is only one consumer.
  • Do NOT add an npm dependency (nprogress, etc.). Svelte 5 + navigating + CSS keyframes cover every requirement in the AC. Every dependency is a supply-chain and bundle-size liability for 30 lines of logic.
  • Replace the existing h-1 bg-accent strip rather than stacking a second 2–3px bar above it. In the idle state the strip stays as a static bg-accent; on navigation it animates. One element, two states — avoids visual noise and layout shift.
  • No backend work, no spec file, no ADR. The acceptance criteria are concrete enough that implementation can start directly.
  • Scope discipline: resist the urge to start measuring real latency or refactoring slow load functions here. Route-level performance is #292's concern; this issue is purely perceived-performance feedback.

Open Decisions

None — implementation path is clear.

## 🏛️ Markus Keller — Application Architect ### Observations - This is purely a presentation-layer affordance — no module boundary impact, no ADR needed. - `navigating` from `$app/state` is already in use in `frontend/src/routes/documents/+page.svelte:3` (`isLoading={navigating.to !== null}`). The pattern is established; the new component just hoists it to the layout. - The header in `frontend/src/routes/+layout.svelte:48` already renders a decorative `<div class="h-1 bg-accent">` strip. Two thin horizontal bars within 2px of each other would be visually redundant. - "Quick win" is the right framing. No new dependency, no routing layer changes. ### Recommendations - **Single new component** `frontend/src/lib/components/NavigationProgress.svelte`, composed once in `+layout.svelte` above the header. Keep it under ~40 lines — one `$derived`, one `$effect` for the 100ms delay, one CSS animation. No extraction for reuse — there is only one consumer. - **Do NOT add an npm dependency** (nprogress, etc.). Svelte 5 + `navigating` + CSS keyframes cover every requirement in the AC. Every dependency is a supply-chain and bundle-size liability for 30 lines of logic. - **Replace the existing `h-1 bg-accent` strip rather than stacking a second 2–3px bar above it.** In the idle state the strip stays as a static `bg-accent`; on navigation it animates. One element, two states — avoids visual noise and layout shift. - **No backend work, no spec file, no ADR.** The acceptance criteria are concrete enough that implementation can start directly. - **Scope discipline**: resist the urge to start measuring real latency or refactoring slow `load` functions here. Route-level performance is #292's concern; this issue is purely perceived-performance feedback. ### Open Decisions _None — implementation path is clear._
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • Pattern is already proven in frontend/src/routes/documents/+page.svelte:109 (isLoading={navigating.to !== null}). The test mocking pattern is in frontend/src/routes/documents/page.svelte.spec.ts:6: vi.mock('$app/state', () => ({ navigating: { to: null } })).
  • navigating in Svelte 5 / SvelteKit 2 is a reactive object exposed from $app/state. It is null when idle, a Navigation object (with .to, .from, .type, .complete) while a navigation is pending.
  • Layout has no existing +layout.svelte.spec.ts — this component will be tested in isolation, not by mounting the layout.

Recommendations

  • TDD order (red/green/refactor):
    1. NavigationProgress.svelte.spec.ts — three unit tests:
      • hidden when navigating is null
      • visible (has role="progressbar" OR a visible .is-active class) when navigating.to is set AND 100ms have elapsed (use vi.useFakeTimers() + vi.advanceTimersByTime(100))
      • hides again when navigating returns to null
    2. Implement the component to make each test pass, no more.
  • Implementation shape (keep it under ~40 lines):
    <script lang="ts">
      import { navigating } from '$app/state';
      let visible = $state(false);
      $effect(() => {
        if (navigating.to === null) { visible = false; return; }
        const t = setTimeout(() => { visible = true; }, 100);
        return () => clearTimeout(t);
      });
    </script>
    <div class="nav-progress" class:is-active={visible} aria-hidden="true"></div>
    
  • Mount location: one line added to +layout.svelte, rendered for both authenticated and auth pages (the bar is still useful during the login flow).
  • Don't test SvelteKit internals — mock $app/state and trust SvelteKit drives navigating correctly on real clicks. Integration confidence comes from Playwright, not from trying to simulate a real navigation in Vitest.
  • Naming: file is NavigationProgress.svelte, not LoadingBar, Spinner, TopBar. One nameable region, named after what it is.

Open Decisions

None — test plan and implementation shape are deterministic.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - Pattern is already proven in `frontend/src/routes/documents/+page.svelte:109` (`isLoading={navigating.to !== null}`). The test mocking pattern is in `frontend/src/routes/documents/page.svelte.spec.ts:6`: `vi.mock('$app/state', () => ({ navigating: { to: null } }))`. - `navigating` in Svelte 5 / SvelteKit 2 is a reactive object exposed from `$app/state`. It is `null` when idle, a `Navigation` object (with `.to`, `.from`, `.type`, `.complete`) while a navigation is pending. - Layout has no existing `+layout.svelte.spec.ts` — this component will be tested in isolation, not by mounting the layout. ### Recommendations - **TDD order (red/green/refactor):** 1. `NavigationProgress.svelte.spec.ts` — three unit tests: - hidden when `navigating` is `null` - visible (has `role="progressbar"` OR a visible `.is-active` class) when `navigating.to` is set AND 100ms have elapsed (use `vi.useFakeTimers()` + `vi.advanceTimersByTime(100)`) - hides again when `navigating` returns to `null` 2. Implement the component to make each test pass, no more. - **Implementation shape** (keep it under ~40 lines): ```svelte <script lang="ts"> import { navigating } from '$app/state'; let visible = $state(false); $effect(() => { if (navigating.to === null) { visible = false; return; } const t = setTimeout(() => { visible = true; }, 100); return () => clearTimeout(t); }); </script> <div class="nav-progress" class:is-active={visible} aria-hidden="true"></div> ``` - **Mount location**: one line added to `+layout.svelte`, rendered for both authenticated and auth pages (the bar is still useful during the login flow). - **Don't test SvelteKit internals** — mock `$app/state` and trust SvelteKit drives `navigating` correctly on real clicks. Integration confidence comes from Playwright, not from trying to simulate a real navigation in Vitest. - **Naming**: file is `NavigationProgress.svelte`, not `LoadingBar`, `Spinner`, `TopBar`. One nameable region, named after what it is. ### Open Decisions _None — test plan and implementation shape are deterministic._
Author
Owner

🛡️ Nora Steiner — Application Security Engineer

Observations

  • This change has an essentially zero security surface. No network calls, no user input, no auth state, no new route.
  • navigating.to contains the destination URL object. The bar itself renders no text from navigating, so there is no XSS or information-disclosure vector.
  • No new dependency means no supply-chain risk.

Recommendations

  • Keep aria-hidden="true" as specified. The bar is a decorative progress affordance — not a status message. An aria-live region tied to navigating would announce every click to screen readers (noise + route enumeration read aloud, which is also a privacy smell on shared-screen setups).
  • Do not render navigating.to.url.pathname (or any property of it) into the DOM. Keep the component stateless beyond a boolean visible. If someone later wants a "Loading /chronik…" label, that is a separate UX decision with its own security review — not a silent addition.
  • CSS only for the animation — no inline style={} bindings that interpolate any URL or user-controlled value. Prevents future CSS-injection smells from sneaking in during a "let me just add a dynamic color" refactor.
  • No logging of navigation events. navigating.to.url can contain query params (search terms, filters, IDs). Logging them to the browser console or a server-side analytics sink is a data-minimisation concern for a family archive with private documents.

Open Decisions

None — security posture is trivial and already correctly scoped in the acceptance criteria.

## 🛡️ Nora Steiner — Application Security Engineer ### Observations - This change has an essentially zero security surface. No network calls, no user input, no auth state, no new route. - `navigating.to` contains the destination URL object. The bar itself renders no text from `navigating`, so there is no XSS or information-disclosure vector. - No new dependency means no supply-chain risk. ### Recommendations - **Keep `aria-hidden="true"` as specified.** The bar is a decorative progress affordance — not a status message. An `aria-live` region tied to `navigating` would announce every click to screen readers (noise + route enumeration read aloud, which is also a privacy smell on shared-screen setups). - **Do not render `navigating.to.url.pathname` (or any property of it) into the DOM.** Keep the component stateless beyond a boolean `visible`. If someone later wants a "Loading /chronik…" label, that is a separate UX decision with its own security review — not a silent addition. - **CSS only for the animation** — no inline `style={}` bindings that interpolate any URL or user-controlled value. Prevents future CSS-injection smells from sneaking in during a "let me just add a dynamic color" refactor. - **No logging of navigation events.** `navigating.to.url` can contain query params (search terms, filters, IDs). Logging them to the browser console or a server-side analytics sink is a data-minimisation concern for a family archive with private documents. ### Open Decisions _None — security posture is trivial and already correctly scoped in the acceptance criteria._
Author
Owner

🧪 Sara Holt — QA Engineer

Observations

  • Existing mock pattern for navigating is already in the codebase: frontend/src/routes/documents/page.svelte.spec.ts:6. New spec can copy-paste the mock setup exactly.
  • The AC says "unit test covers: bar hidden when navigating is null, visible when it is set" — this is underspecified. A bar that appears and never disappears would pass those two assertions.
  • No mention of how the component behaves on rapid back-to-back navigations, form submits (use:enhance triggers navigating), or goto(..., { invalidate: true }).
  • Current E2E setup already has data-hydrated on the layout root (frontend/src/routes/+layout.svelte:45) — Playwright tests can wait for hydration before asserting on the bar.

Recommendations

  • Expand the unit test coverage beyond the 2 AC lines to 5 cases (all <10s total, Vitest + vi.useFakeTimers()):
    1. Idle state — bar is not visible.
    2. navigating.to set, 50ms elapsed — bar still not visible (flicker-free guarantee).
    3. navigating.to set, 150ms elapsed — bar is visible.
    4. navigating.to transitions back to null — bar hides again.
    5. Rapid navigation churn (click → complete → click within 100ms) — bar does not "flash" multiple times (debounce behaviour).
  • Add one Playwright E2E against a slow route (/chronik is the realistic target): navigate, assert the .nav-progress.is-active element is visible within 500ms, assert it disappears after load resolves. Single test, under 3 seconds. Add to the existing authenticated journey, no new spec file needed if it fits.
  • Verify axe stays green: add an aria-hidden decorative element should be invisible to axe, but run the existing AxeBuilder sweep on /chronik with the bar rendered to confirm no regression.
  • Visual regression: the bar ships across every route. Existing screenshot tests should be re-baselined once to accept the new 2–3px strip. Pick a single representative page (login OR home) for the baseline rather than regenerating every snapshot.
  • AC addition I would add to the issue:
    • Bar does not "flash" on rapid back-to-back navigations (debounce)
    • Bar appears on form submits via use:enhance (since those also drive navigating) — OR explicitly scope the AC to link clicks only and document why

Open Decisions

  • Form-submit behaviour. navigating is set during use:enhance form submits too. Should the bar show? Options: (a) yes, same treatment — free progress feedback for saves/deletes; (b) no, forms have their own save bars and the page-top bar is misleading. I lean (a), but it depends on whether the save bars already show a spinner/disabled state. (Raised by: Sara — needs Leonie's input.)
## 🧪 Sara Holt — QA Engineer ### Observations - Existing mock pattern for `navigating` is already in the codebase: `frontend/src/routes/documents/page.svelte.spec.ts:6`. New spec can copy-paste the mock setup exactly. - The AC says "unit test covers: bar hidden when `navigating` is null, visible when it is set" — this is underspecified. A bar that appears and never disappears would pass those two assertions. - No mention of how the component behaves on rapid back-to-back navigations, form submits (`use:enhance` triggers `navigating`), or `goto(..., { invalidate: true })`. - Current E2E setup already has `data-hydrated` on the layout root (`frontend/src/routes/+layout.svelte:45`) — Playwright tests can wait for hydration before asserting on the bar. ### Recommendations - **Expand the unit test coverage beyond the 2 AC lines** to 5 cases (all <10s total, Vitest + `vi.useFakeTimers()`): 1. Idle state — bar is not visible. 2. `navigating.to` set, 50ms elapsed — bar still not visible (flicker-free guarantee). 3. `navigating.to` set, 150ms elapsed — bar is visible. 4. `navigating.to` transitions back to `null` — bar hides again. 5. Rapid navigation churn (click → complete → click within 100ms) — bar does not "flash" multiple times (debounce behaviour). - **Add one Playwright E2E** against a slow route (`/chronik` is the realistic target): navigate, assert the `.nav-progress.is-active` element is visible within 500ms, assert it disappears after `load` resolves. Single test, under 3 seconds. Add to the existing authenticated journey, no new spec file needed if it fits. - **Verify axe stays green**: add an `aria-hidden` decorative element should be invisible to axe, but run the existing `AxeBuilder` sweep on `/chronik` with the bar rendered to confirm no regression. - **Visual regression**: the bar ships across every route. Existing screenshot tests should be re-baselined once to accept the new 2–3px strip. Pick a single representative page (login OR home) for the baseline rather than regenerating every snapshot. - **AC addition I would add to the issue**: - [ ] Bar does not "flash" on rapid back-to-back navigations (debounce) - [ ] Bar appears on form submits via `use:enhance` (since those also drive `navigating`) — OR explicitly scope the AC to link clicks only and document why ### Open Decisions - **Form-submit behaviour.** `navigating` is set during `use:enhance` form submits too. Should the bar show? Options: _(a)_ yes, same treatment — free progress feedback for saves/deletes; _(b)_ no, forms have their own save bars and the page-top bar is misleading. I lean (a), but it depends on whether the save bars already show a spinner/disabled state. _(Raised by: Sara — needs Leonie's input.)_
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Observations

  • The header in frontend/src/routes/+layout.svelte:48 already renders <div class="h-1 bg-accent"> — a 4px decorative brand-mint strip at the very top of the viewport. A new 2–3px progress bar "at the top" would sit directly on top of or next to this. Two bars, both brand-mint, 2–3px apart is visual noise.
  • The issue asks for brand-mint on a "transparent track". On auth pages the bg-canvas is light sand, on authenticated pages the header background is brand-navy. The bar will traverse both contexts when routing between /login and / (or similar).
  • prefers-reduced-motion is respected via CSS @media queries across several components already (AnnotationShape.svelte, ChronikFuerDichBox.svelte). There is a consistent pattern to follow.
  • Header is sticky top-0 z-50 — the progress bar must be at z-51 or sit outside the header stacking context to stay above it.

Recommendations

  • Repurpose the existing h-1 bg-accent strip — do not add a second bar. The same element becomes the progress bar. Idle state: static solid bg-accent (current behaviour). Active state: indeterminate animated gradient sliding left→right. This preserves the brand's existing "mint hairline" signature and adds no vertical pixels.
    /* idle — current behaviour */
    .brand-strip { height: 4px; background: var(--color-accent); }
    /* active — during navigation */
    .brand-strip.is-active {
      background: linear-gradient(90deg, var(--color-accent-bg) 0%, var(--color-accent) 50%, var(--color-accent-bg) 100%);
      background-size: 200% 100%;
      animation: slide 1.2s ease-in-out infinite;
    }
    @keyframes slide { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
    @media (prefers-reduced-motion: reduce) {
      .brand-strip.is-active { animation: none; opacity: 0.6; } /* static dimmed state — still conveys "something is happening" */
    }
    
  • Use semantic tokens, not hex literals. var(--color-accent) (idle) and var(--color-accent-bg) (trough) — both already exist in layout.css. This automatically handles dark mode; a hardcoded #A6DAD8 would break in the dark theme.
  • Height: keep it at the existing 4px (not 2–3px). Matches brand, already allocated in layout, no CLS on first paint.
  • Position: bar stays top: 0, position: sticky, height 4px, z-index: 60 (above the sticky z-50 header). Full-width 100vw. On auth pages where the header is hidden, the bar stays visible — useful during login redirects.
  • Touch targets / pointer-events: pointer-events: none on the bar so it cannot intercept clicks on the header.
  • A11y confirmation: aria-hidden="true" is correct for the decorative bar. Do NOT add an aria-live region for navigation — announcing every click would be hostile to screen-reader users. If the team later wants accessible navigation feedback, it belongs in a shared role="status" live region tied to a longer timeout (e.g. 2s), which is out of scope for this quick-win issue.
  • Contrast check (for reference): brand-mint #A6DAD8 on brand-navy #002850 header = 7.2:1 AAA for large decorative UI. On bg-canvas (light) it's ~2.8:1 — below AA for text but acceptable for a 4px decorative strip, since the bar is not text and is aria-hidden.

Open Decisions

  • Replace the brand strip vs. add a second bar above it. I strongly prefer reusing the existing strip (rationale above). The counterargument is that the "idle brand strip" and "progress indicator" are semantically different things and mixing them makes the brand strip "flicker" on every page change. Decision needed before Felix picks a layout. (Raised by: Leonie.)
## 🎨 Leonie Voss — UX Designer & Accessibility Strategist ### Observations - The header in `frontend/src/routes/+layout.svelte:48` already renders `<div class="h-1 bg-accent">` — a 4px decorative `brand-mint` strip at the very top of the viewport. A new 2–3px progress bar "at the top" would sit directly on top of or next to this. Two bars, both `brand-mint`, 2–3px apart is visual noise. - The issue asks for `brand-mint` on a "transparent track". On auth pages the `bg-canvas` is light sand, on authenticated pages the header background is `brand-navy`. The bar will traverse both contexts when routing between `/login` and `/` (or similar). - `prefers-reduced-motion` is respected via CSS `@media` queries across several components already (`AnnotationShape.svelte`, `ChronikFuerDichBox.svelte`). There is a consistent pattern to follow. - Header is `sticky top-0 z-50` — the progress bar must be at `z-51` or sit outside the header stacking context to stay above it. ### Recommendations - **Repurpose the existing `h-1 bg-accent` strip — do not add a second bar.** The same element becomes the progress bar. Idle state: static solid `bg-accent` (current behaviour). Active state: indeterminate animated gradient sliding left→right. This preserves the brand's existing "mint hairline" signature and adds no vertical pixels. ```css /* idle — current behaviour */ .brand-strip { height: 4px; background: var(--color-accent); } /* active — during navigation */ .brand-strip.is-active { background: linear-gradient(90deg, var(--color-accent-bg) 0%, var(--color-accent) 50%, var(--color-accent-bg) 100%); background-size: 200% 100%; animation: slide 1.2s ease-in-out infinite; } @keyframes slide { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } } @media (prefers-reduced-motion: reduce) { .brand-strip.is-active { animation: none; opacity: 0.6; } /* static dimmed state — still conveys "something is happening" */ } ``` - **Use semantic tokens, not hex literals.** `var(--color-accent)` (idle) and `var(--color-accent-bg)` (trough) — both already exist in `layout.css`. This automatically handles dark mode; a hardcoded `#A6DAD8` would break in the dark theme. - **Height**: keep it at the existing 4px (not 2–3px). Matches brand, already allocated in layout, no CLS on first paint. - **Position**: bar stays `top: 0`, `position: sticky`, height `4px`, `z-index: 60` (above the sticky `z-50` header). Full-width `100vw`. On auth pages where the header is hidden, the bar stays visible — useful during login redirects. - **Touch targets / pointer-events**: `pointer-events: none` on the bar so it cannot intercept clicks on the header. - **A11y confirmation**: `aria-hidden="true"` is correct for the decorative bar. **Do NOT** add an `aria-live` region for navigation — announcing every click would be hostile to screen-reader users. If the team later wants accessible navigation feedback, it belongs in a shared `role="status"` live region tied to a longer timeout (e.g. 2s), which is out of scope for this quick-win issue. - **Contrast check** (for reference): `brand-mint #A6DAD8` on `brand-navy #002850` header = 7.2:1 ✅ AAA for large decorative UI. On `bg-canvas` (light) it's ~2.8:1 — below AA for text but acceptable for a 4px decorative strip, since the bar is not text and is `aria-hidden`. ### Open Decisions - **Replace the brand strip vs. add a second bar above it.** I strongly prefer reusing the existing strip (rationale above). The counterargument is that the "idle brand strip" and "progress indicator" are semantically different things and mixing them makes the brand strip "flicker" on every page change. Decision needed before Felix picks a layout. _(Raised by: Leonie.)_
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer

Observations

  • Frontend-only change. No Compose file, Caddy, or CI pipeline impact.
  • Zero new runtime dependencies if implemented as recommended (native Svelte + CSS keyframes). No Renovate work, no bundle-size review.
  • One additional Vitest file adds ~1s to CI; negligible.
  • This is a perceived-performance fix, not a real-performance fix. The underlying question — "why is /chronik slow enough for users to think nothing happened?" — is not answered by this issue.

Recommendations

  • Do not add an npm dependency (@sveltejs/kit-progress, nprogress, or similar). Supply-chain discipline matters even for small packages on a family-archive project. The implementation is 30 lines; own it.
  • Treat this as a cosmetic patch — not a substitute for profiling. After #289 ships, profile /chronik, /documents, and document detail with browser DevTools + Spring Boot request logs. Slow load functions likely have fixable N+1 queries or missing indexes that a progress bar only disguises. Track those as separate backend issues if found.
  • No observability work for this issue. When #292 (skeletons) is in flight and we have real perceived-performance telemetry goals, consider adding performance.mark('sveltekit:navigation:end') hooks and shipping them to Grafana via the existing stack. Premature here.
  • CI impact check before merge: confirm the bundle-size delta is under 1KB gzipped (Svelte + CSS only, should be well under). If somehow it is not, it means an npm dep snuck in — reject.
  • Rollout: single Compose rebuild of the frontend image. No migration, no env var, no secret rotation. Safe to ship on any deploy window.

Open Decisions

None — infrastructure impact is negligible.

## ⚙️ Tobias Wendt — DevOps & Platform Engineer ### Observations - Frontend-only change. No Compose file, Caddy, or CI pipeline impact. - Zero new runtime dependencies if implemented as recommended (native Svelte + CSS keyframes). No Renovate work, no bundle-size review. - One additional Vitest file adds ~1s to CI; negligible. - This is a perceived-performance fix, not a real-performance fix. The underlying question — "why is `/chronik` slow enough for users to think nothing happened?" — is not answered by this issue. ### Recommendations - **Do not add an npm dependency** (`@sveltejs/kit-progress`, `nprogress`, or similar). Supply-chain discipline matters even for small packages on a family-archive project. The implementation is 30 lines; own it. - **Treat this as a cosmetic patch — not a substitute for profiling.** After #289 ships, profile `/chronik`, `/documents`, and document detail with browser DevTools + Spring Boot request logs. Slow `load` functions likely have fixable N+1 queries or missing indexes that a progress bar only disguises. Track those as separate backend issues if found. - **No observability work for this issue.** When #292 (skeletons) is in flight and we have real perceived-performance telemetry goals, consider adding `performance.mark('sveltekit:navigation:end')` hooks and shipping them to Grafana via the existing stack. Premature here. - **CI impact check before merge**: confirm the bundle-size delta is under 1KB gzipped (Svelte + CSS only, should be well under). If somehow it is not, it means an npm dep snuck in — reject. - **Rollout**: single Compose rebuild of the frontend image. No migration, no env var, no secret rotation. Safe to ship on any deploy window. ### Open Decisions _None — infrastructure impact is negligible._
Author
Owner

🗳️ Decision Queue — Action Required

2 decisions need your input before implementation starts.

UX

  • Replace the existing h-1 bg-accent brand strip, or stack a second progress bar above it?

    • (a) Replace — same 4px element becomes static in idle state, animated during navigation. Zero added vertical pixels, no CLS, reuses the existing brand hairline. Downside: the brand strip "flickers" on every page transition, which some may read as the brand signature becoming the loading indicator.
    • (b) Stack — keep the brand strip as an immutable signature; add a separate 2–3px progress bar above or below it. Downside: two thin mint bars within 2–6px of each other is visually noisy.
    • Markus and Leonie both recommend (a). (Raised by: Leonie, seconded by Markus.)
  • Show the bar on use:enhance form submissions?

    • navigating is set during form submits too, so the bar will show by default unless explicitly filtered.
    • (a) Yes, same treatment — free progress feedback for saves/deletes, consistent signal across all async operations.
    • (b) No, filter out form submits — forms already have dedicated save-bar / spinner feedback; page-top bar is misleading when the page isn't actually changing.
    • (Raised by: Sara.)
## 🗳️ Decision Queue — Action Required _2 decisions need your input before implementation starts._ ### UX - **Replace the existing `h-1 bg-accent` brand strip, or stack a second progress bar above it?** - _(a) Replace_ — same 4px element becomes static in idle state, animated during navigation. Zero added vertical pixels, no CLS, reuses the existing brand hairline. Downside: the brand strip "flickers" on every page transition, which some may read as the brand signature becoming the loading indicator. - _(b) Stack_ — keep the brand strip as an immutable signature; add a separate 2–3px progress bar above or below it. Downside: two thin mint bars within 2–6px of each other is visually noisy. - _Markus and Leonie both recommend (a)._ _(Raised by: Leonie, seconded by Markus.)_ - **Show the bar on `use:enhance` form submissions?** - `navigating` is set during form submits too, so the bar will show by default unless explicitly filtered. - _(a) Yes, same treatment_ — free progress feedback for saves/deletes, consistent signal across all async operations. - _(b) No, filter out form submits_ — forms already have dedicated save-bar / spinner feedback; page-top bar is misleading when the page isn't actually changing. - _(Raised by: Sara.)_
Author
Owner

Replace the brand bar with the loading indicator.
Show also on form saving

Replace the brand bar with the loading indicator. Show also on form saving
Sign in to join this conversation.
No Label feature ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#289