Files
hindsight/Screenshot 2026-07-27 at 14.45.25.png
Nicolò Boschi b651f43f22 fix(control-plane): knowledge-pages polish (#3080)
* fix(control-plane): stop next-intl parsing 'type:<x>' tag hint as an unclosed tag

The Tags hint used 'type:<x>', which next-intl reads as an unclosed rich-text
tag (INVALID_MESSAGE: UNCLOSED_TAG), crashing the create/edit page dialog.
Replace the angle-bracket placeholder with 'type:…' across all locales.

* refactor(control-plane): replace loading-emoji hourglasses with a shared Spinner

The UI used a spinning  emoji for loading in several places — inconsistent and
not accessible. Add a shared <Spinner> (wrapping lucide Loader2, sized xs–xl,
role=status) and use it for all 7 loading states: the large centered
'loading…' placeholders (documents / document / chunks / chunk-modal) and the
inline save-button spinners (tags / content / edit-memory).

* feat(control-plane): branded tumbling-logo spinner for loading states

Add a LogoSpinner that renders the Hindsight mark doing a looping 2D
'tumble' (crouch → hop + 360° flip → land squash), ported from the motion
lab. The hop is expressed as a % of the element so one keyframe scales
across sizes, and reduced-motion falls back to an opacity pulse.

Use it for the prominent centered loading states (documents list, document/
chunk modal); the compact lucide Spinner stays for tiny inline spots.

* feat(control-plane): make the tumbling logo the shared spinner everywhere

Fold LogoSpinner into the shared Spinner so every loading state renders the
Hindsight mark, and swap all remaining ad-hoc spinners (lucide Loader2, the
hand-rolled ring-<div> spinners, the sonner toast loader, and the two
RefreshCw-as-loader stand-ins) over to it — ~65 sites across 22 files.

Spinner now has two motions:
- variant "flip" (default): an in-place 360° flip + squash, safe inline and in
  buttons (only a tiny vertical bob, stays on the text baseline).
- variant "jump": the full tumble (crouch + hop + flip), for prominent centered
  loaders that have vertical room.

RefreshCw icons that spin only while refreshing are left as-is — a spinning
refresh arrow reads as "refreshing", which is distinct from a generic loader.

* fix(control-plane): show the loader (not empty state) on Documents, and finish the spinner sweep

Documents: the mount fetch is debounced, so the empty state ("No documents
found" / "0 documents") flashed before the spinner. Track a `loaded` flag and
gate the empty state + count on it, so the loader shows until the first fetch
resolves. Also swap the 📄/📊 emoji empty states for lucide icons.

Finish converting the loaders the first sweep missed (they used non-spinner
patterns, so grepping for Loader2/animate-spin didn't find them):
- pulsing Clock full-view loaders → jump Spinner (stats page, bank profile)
- emoji + text-only loaders (entities graph/list/linked-memories, llm-requests
  and audit-logs chart loaders) → Spinner
- emoji empty states (no chunks / no entities / no data) → lucide icons

* fix(control-plane): render the Documents loader on first paint (hard refresh)

On a hard refresh, bank-context starts currentBank=null and only resolves it
from the URL in an effect (after the first paint, before hydration + theme), so
the previous `!!currentBank` guard made the empty state win the very first
render — you'd see "No documents found" flash before anything else.

Gate the loader on `!loaded` alone. currentBank always resolves on a
/banks/[id] route and the fetch's finally flips `loaded` (even on an invalid
bank whose fetch errors), so it can't get stuck. Verified: the SSR HTML now
renders the loading Spinner, not the empty state.

* fix(control-plane): apply theme before first paint (no light flash on hard refresh)

ThemeProvider only reads the saved/system theme in a useEffect (after paint), so
a dark-mode user saw a light flash on every hard refresh. Add a tiny blocking
inline script as the first <body> child that sets the .dark class synchronously
before the content paints — the standard anti-FOUC pattern (what next-themes does
internally). <html> already has suppressHydrationWarning for the class mutation.
Logic mirrors lib/theme-context.tsx exactly (saved || system).

* feat(control-plane): add the tumbling mascot to the no-bank welcome screen

Loop the jump Spinner above 'Welcome to Hindsight' as a friendly greeting on
the dashboard shown when no bank is selected.

* feat(control-plane): spin the header logo on sidebar navigation

Clicking a sidebar item now gives the header Hindsight logo a one-shot 'spin
round' — a little playful nav feedback. The sidebar dispatches a
'hindsight:logo-spin' window event (decoupled, like DOCUMENTS_REFRESH_EVENT) and
the header listens and toggles a one-shot animation, cleared on animationEnd.

The logo is a wide lockup, so a 2D rotate would swing it vertical and overflow
the header — use a rotateY card-flip that stays within its footprint instead.

* feat(control-plane): spin only the logo mark (not the wordmark) on navigation

Split the header lockup into two pieces: the octopus mark (favicon.png, a
standalone image so it can rotate freely) and the 'Hindsight' wordmark (the
right slice of the full logo.png, shown via a cropped background). Their widths
sum to the full logo so they butt together seamlessly. Only the mark carries the
one-shot spin, so the wordmark stays put.

Because the mark alone is ~square, the spin is now a clean 2D rotate (reverted
the rotateY card-flip workaround that the wide full lockup had required).

* feat(control-plane): soften logo nav feedback from a spin to a subtle wiggle

A full 360 rotate was too much. Replace it with a small tilt that springs back
(logo-wiggle, ~450ms) on the mark — less impactful but still a bit of life.

* chore(control-plane): drop the dead logo-spin-once reduced-motion block

Leftover from the rename to logo-wiggle.

* style(control-plane): apply prettier formatting to the spinner-sweep files

Ran scripts/hooks/lint.sh — the earlier commits were eslint-clean but not
prettier-formatted, which tripped verify-generated-files.
2026-07-30 19:04:56 +02:00

152 KiB
1268x767px