refactor(e10): move the platform components into components/ui/ and enforce the boundary

The app HAS a platform layer -- AnchoredPopover (22 importers, with
arrows/typeahead/Escape/anchor-flip), QueryState (21), Modal (17),
PageShell, DataTable, Tabs, Tooltip and ~26 more -- but it sat flat among
~160 feature components with nothing marking it as one. Several of those
files describe themselves as 'the one place' or 'canonical' and were
still bypassed by hand-rolled markup, because a layer nobody can see is a
layer nobody uses; three separate inventory passes of this codebase
misread it the same way.

Moves the 33 domain-free modules into components/ui/, rewrites the
imports in 89 files, and makes the boundary a lint rule rather than a
convention: inside ui/ a one-level-up import (a feature component) and
any lib/api slice are errors. The rule was verified to fire on both and
to leave generic lib/ helpers alone. README.md states which side of the
line a component belongs on and why.

No behaviour change: 3x tsc, eslint, knip, prettier and 687 vitest tests
all green; no backend file touched.
This commit is contained in:
2026-09-05 16:58:29 +02:00
parent 31fa554b87
commit 5bfcf5d83a
138 changed files with 262 additions and 189 deletions
+58
View File
@@ -0,0 +1,58 @@
import { useState, useSyncExternalStore } from "react";
import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
import { hintsEnabled, subscribeHints } from "../../lib/hints";
type Side = "top" | "bottom" | "left" | "right";
/** Wrap any element to show a short glass hint caption on hover — by default only while the
* app-wide hints toggle (Settings → Appearance) is on. Anchored by the R8 popover platform, so it
* is never clipped by an overflow/stacking ancestor, flips when the preferred side runs out of room
* and slides back inside the viewport near an edge.
* `always` bypasses the hints toggle (for a label that IS the primary affordance, e.g. the slim nav
* rail's flyout names); `openDelay` waits before showing (a rest-on-the-icon feel). */
export default function Tooltip({
hint,
side = "top",
always = false,
openDelay = 0,
children,
}: {
hint: string;
side?: Side;
always?: boolean;
openDelay?: number;
children: React.ReactNode;
}) {
const hintsOn = useSyncExternalStore(subscribeHints, hintsEnabled, hintsEnabled);
const enabled = always || hintsOn;
const [open, setOpen] = useState(false);
const pop = useAnchoredPopover({
open: open && enabled && !!hint,
onOpenChange: setOpen,
mode: "hover",
placement: side,
offset: 8,
// Show/hide on contact by default (as the pre-platform tooltip did); `openDelay` adds a
// rest-before-show for labels that shouldn't flash on a fast sweep across an icon row.
hoverDelay: { open: openDelay, close: 0 },
});
if (!enabled || !hint) return <>{children}</>;
return (
<>
<span ref={pop.refs.setReference} {...pop.getReferenceProps()} className="inline-flex">
{children}
</span>
{/* Passive caption: it must not swallow a click aimed at a neighbouring control that it
overlaps while the hover-close delay runs out. */}
<PopoverSurface
popover={pop}
passthrough
className="glass w-max max-w-[240px] px-2.5 py-1.5 rounded-lg text-xs leading-snug text-fg font-normal normal-case tracking-normal animate-[fadeIn_0.12s_ease]"
>
{hint}
</PopoverSurface>
</>
);
}