Add z-index scale and overview redirect

Introduce a v2 z-1…z-6 stacking scale so portaled
menus sit above in-page media, and redirect the
legacy /overview trust-app URL to home.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-20 18:58:12 +02:00
parent 2db37660d3
commit 311639ae98
17 changed files with 117 additions and 19 deletions

View File

@@ -25,7 +25,7 @@ Detailed guides for specific subsystems live in `contrib/claude/`:
- [`contrib/claude/hooks.md`](contrib/claude/hooks.md) — Custom hooks (_lib placement, awaitable useMutation primitive, auto error handling)
- [`contrib/claude/react-components.md`](contrib/claude/react-components.md) — React component shape (file/export, props, configure vs data via hooks, naming/suffix taxonomy, error props)
- [`contrib/claude/ui.md`](contrib/claude/ui.md) — @probo/ui v2 kit (Base UI headless, Tailwind, tailwind-variants, flat folders, bundle-safe skeletons)
- [`contrib/claude/v2-tokens.md`](contrib/claude/v2-tokens.md) — v2 design tokens (color, typography, radius, shadow scales; native spacing)
- [`contrib/claude/v2-tokens.md`](contrib/claude/v2-tokens.md) — v2 design tokens (color, typography, radius, shadow, z-index scales; native spacing)
- [`contrib/claude/forms.md`](contrib/claude/forms.md) — Frontend forms (Base UI Field/Form tiers, native vs zod vs react-hook-form, server errors)
- [`contrib/claude/routing.md`](contrib/claude/routing.md) — Frontend routing (@probo/routes, navigation, typed params, URL state, auth/protected routes)
- [`contrib/claude/state-management.md`](contrib/claude/state-management.md) — Client state decision order (Relay, URL, local, context, zustand)

View File

@@ -28,7 +28,7 @@ import { backdropCard } from "./variants";
interface BackdropCardProps {
// Centered content shown above the backdrop (an icon or a logo box). The node
// owns its own sizing/color; position it above the backdrop with `relative
// z-10`.
// z-1`.
media: ReactNode;
// When set, a blurred, magnified copy of this image becomes the backdrop;
// otherwise a dotted texture is shown.

View File

@@ -25,6 +25,6 @@ import { tv } from "tailwind-variants/lite";
// BackdropCard; this slot only styles the icon container.
export const commitmentCard = tv({
slots: {
icon: "relative z-10 flex size-8 items-center justify-center text-gold-9",
icon: "relative z-1 flex size-8 items-center justify-center text-gold-9",
},
});

View File

@@ -32,7 +32,7 @@ export const mediaTile = tv({
backdrop: "pointer-events-none absolute inset-0",
blurBackdrop: "pointer-events-none absolute inset-0 size-full scale-150 object-cover opacity-10 blur-lg",
backdropFade: "pointer-events-none absolute inset-0 bg-linear-to-b from-sand-1/0 to-sand-1",
mediaContent: "relative z-10 flex size-16 items-center justify-center [&_img]:size-full [&_img]:object-contain",
mediaContent: "relative z-1 flex size-16 items-center justify-center [&_img]:size-full [&_img]:object-contain",
caption: "flex w-full items-center justify-center px-4 py-3",
},
variants: {

View File

@@ -27,7 +27,7 @@ export const poweredBy = tv({
root: "relative flex w-full items-center justify-center overflow-hidden bg-sand-2 py-6 text-sand-11",
backdrop: "pointer-events-none absolute inset-0",
backdropFade: "pointer-events-none absolute inset-0 bg-linear-to-b from-sand-2 to-sand-2/0",
content: "relative z-10 flex items-center justify-center gap-2",
content: "relative z-1 flex items-center justify-center gap-2",
logo: "h-6 w-auto text-sand-9",
},
});

View File

@@ -113,14 +113,14 @@ export function DocumentEntry({
? (
<RouterLink
to={viewHref}
className="absolute inset-0 z-10 hidden max-sm:block"
className="absolute inset-0 z-1 hidden max-sm:block"
aria-label={mobileHitLabel}
/>
)
: (
<button
type="button"
className="absolute inset-0 z-10 hidden max-sm:block"
className="absolute inset-0 z-1 hidden max-sm:block"
aria-label={mobileHitLabel}
disabled={isRequesting}
onClick={onGetAccess}

View File

@@ -25,7 +25,7 @@ import { tv } from "tailwind-variants/lite";
// the body. The card frame and backdrop live in BackdropCard.
export const subprocessorListItem = tv({
slots: {
logo: "relative z-10 flex size-10 items-center justify-center overflow-hidden rounded-2 bg-sand-1",
logo: "relative z-1 flex size-10 items-center justify-center overflow-hidden rounded-2 bg-sand-1",
logoImage: "size-full object-cover",
logoFallbackIcon: "text-sand-9",
region: "flex items-start gap-1",

View File

@@ -20,7 +20,8 @@
import { lazy } from "@probo/react-lazy";
import { type AppRoute, routeFromAppRoute } from "@probo/routes";
import { createBrowserRouter } from "react-router";
import { Fragment } from "react";
import { createBrowserRouter, redirect } from "react-router";
import { PageErrorBoundary } from "#/components/errors/PageErrorBoundary";
import { RootErrorBoundary } from "#/components/errors/RootErrorBoundary";
@@ -52,6 +53,15 @@ const routes = [
Fallback: HomePageSkeleton,
Component: lazy(() => import("#/pages/HomePageLoader")),
},
// Legacy trust-app URL; home now lives at the index route.
{
path: "overview",
loader: () => {
// eslint-disable-next-line
throw redirect("/");
},
Component: Fragment,
},
...documentRoutes,
...subprocessorRoutes,
...updateRoutes,

View File

@@ -9,7 +9,7 @@ These rules are the **source of truth**. The legacy tree (`Atoms/`, `Molecules/`
| Topic | Guide |
|-------|--------|
| Component shape, props, naming/suffixes | [`contrib/claude/react-components.md`](react-components.md) |
| v2 design tokens (color, type, radius, shadow, spacing) | [`contrib/claude/v2-tokens.md`](v2-tokens.md) |
| v2 design tokens (color, type, radius, shadow, z-index, spacing) | [`contrib/claude/v2-tokens.md`](v2-tokens.md) |
| App folder layout and special folders | [`contrib/claude/app-arborescence.md`](app-arborescence.md) |
| Error boundaries and error/fallback props | [`contrib/claude/error-handling.md`](error-handling.md) |
| Relay data loading | [`contrib/claude/relay.md`](relay.md) |
@@ -20,7 +20,7 @@ These rules are the **source of truth**. The legacy tree (`Atoms/`, `Molecules/`
|------|------------|
| Package | **`@probo/ui`** — v2 components under `src/v2`. Apps opt into v2 by importing the v2 theme (see [`v2-tokens.md`](v2-tokens.md)). |
| Styling | **Tailwind v4** with the Radix-scale tokens (`bg-sand-3`, `text-sand-12`, `rounded-3`, `text-4`, …). |
| Variants API | **`tailwind-variants/lite`** only — `import { tv } from "tailwind-variants/lite"`. The `/lite` entrypoint ships **without `tailwind-merge`**, which is required: the numbered scales (`text-1…9`, `rounded-1…6`, `shadow-1…6`) collide with the color/utility namespaces and tailwind-merge would silently drop the scale class (e.g. `text-3` next to `text-sand-11`). The legacy v1 kit stays on `tailwind-variants` (with merge). |
| Variants API | **`tailwind-variants/lite`** only — `import { tv } from "tailwind-variants/lite"`. The `/lite` entrypoint ships **without `tailwind-merge`**, which is required: the numbered scales (`text-1…9`, `rounded-1…6`, `shadow-1…6`, `z-1…6`) collide with the color/utility namespaces and tailwind-merge would silently drop the scale class (e.g. `text-3` next to `text-sand-11`). The legacy v1 kit stays on `tailwind-variants` (with merge). |
| Class composition | **Do not use `clsx` or `tailwind-merge`.** All conditional styling goes through `tv` variants and slots. |
| Headless primitives | **Base UI** (`@base-ui/react`). We **style** these primitives; we do not re-implement their behavior. |

View File

@@ -2,7 +2,7 @@
The v2 UI kit is built on a small set of **numbered token scales** sourced from Radix and the "Probo Radix UI" Figma file. Every token family follows the same convention as color — a numbered scale exposed as Tailwind utilities — so the kit speaks one consistent visual language: `bg-sand-3`, `text-4`, `rounded-3`, `shadow-2`.
Theme entry: [`packages/ui/src/v2/theme.css`](../../packages/ui/src/v2/theme.css), aggregating `theme/colors.css`, `theme/typography.css`, `theme/radius.css`, `theme/shadows.css`.
Theme entry: [`packages/ui/src/v2/theme.css`](../../packages/ui/src/v2/theme.css), aggregating `theme/colors.css`, `theme/typography.css`, `theme/radius.css`, `theme/shadows.css`, `theme/z-index.css`.
| Family | Utilities | Steps | Source |
|--------|-----------|-------|--------|
@@ -10,6 +10,7 @@ Theme entry: [`packages/ui/src/v2/theme.css`](../../packages/ui/src/v2/theme.css
| Typography | `text-<19>` (+ font weights) | 9 | Radix type scale |
| Radius | `rounded-<16>` | 6 | Radix "Medium" radius |
| Shadow | `shadow-<16>`, `inset-shadow-<13>` | 6 / 3 | Radix elevation |
| Z-index | `z-<16>` | 6 | UI layer roles |
| Spacing | Tailwind native (`p-4`, `gap-2`, …) | — | not tokenized |
Each `--<family>-*` is reset to `initial` in its theme layer, so a v2 build exposes **only** the numbered scales — Tailwind's default palette, t-shirt type sizes, and `shadow-sm/md/lg` are intentionally unavailable. Use the numbered token; never reintroduce an ad-hoc value.
@@ -257,6 +258,33 @@ Theme file: [`packages/ui/src/v2/theme/shadows.css`](../../packages/ui/src/v2/th
<div className="shadow-md dark:shadow-none"></div>
```
# Z-index (`z-1` … `z-6`)
Stacking scale by **UI layer role**, not arbitrary elevation. Prefer the step that matches the element type; never invent ad-hoc values (`z-10`, `z-[999]`) in a v2 build.
Theme file: [`packages/ui/src/v2/theme/z-index.css`](../../packages/ui/src/v2/theme/z-index.css)
| Step | Value | Role |
|------|-------|------|
| 1 | 1 | Local stacking inside a component (media over a decorative backdrop) |
| 2 | 10 | Sticky / fixed page chrome (top bars, sticky toolbars) |
| 3 | 30 | Floating menus (select, dropdown, tooltip, popover) |
| 4 | 40 | Overlay backdrops (dimmers under modals / drawers) |
| 5 | 50 | Modals and drawers |
| 6 | 60 | Toasts / global notifications (always on top) |
Put the token on the **portaled root** that participates in the document stacking context (e.g. a menu `Positioner`), not only an inner popup. A local `z-1` in the page will paint above a portaled popup that has no z-index of its own.
```tsx
// Good — local media above its own backdrop; menu sits in the popover layer
<div className="relative z-1">logo</div>
<Menu.Positioner className="z-3"></Menu.Positioner>
// Bad — Tailwind default / arbitrary z-index (wiped or discouraged in v2)
<div className="z-10"></div>
<div className="z-[999]"></div>
```
# Spacing (Tailwind native)
Spacing is **not** tokenized — use Tailwind's native spacing scale (`p-4`, `gap-2`, `mt-6`, `size-8`). Do not invent a numbered spacing scale or use arbitrary pixel values where a native step fits.

View File

@@ -26,12 +26,12 @@ import { tv } from "tailwind-variants/lite";
export const dialog = tv({
slots: {
backdrop: [
"fixed inset-0 z-50 bg-sand-12/40",
"fixed inset-0 z-4 bg-sand-12/40",
"transition-opacity duration-150",
"data-starting-style:opacity-0 data-ending-style:opacity-0",
],
popup: [
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
"fixed left-1/2 top-1/2 z-5 -translate-x-1/2 -translate-y-1/2",
"flex w-[calc(100vw-2rem)] max-w-[600px] flex-col gap-4",
"max-h-[calc(100vh-2rem)] overflow-y-auto overflow-x-clip",
"rounded-5 border border-sand-6 bg-sand-1 py-6 shadow-6 outline-none",

View File

@@ -25,11 +25,11 @@ import { tv } from "tailwind-variants/lite";
export const drawer = tv({
slots: {
backdrop: [
"fixed inset-0 z-50 bg-sand-12/40",
"fixed inset-0 z-4 bg-sand-12/40",
"transition-opacity duration-200",
"data-starting-style:opacity-0 data-ending-style:opacity-0",
],
viewport: "fixed inset-0 z-50 flex",
viewport: "fixed inset-0 z-5 flex",
popup: [
"relative flex flex-col bg-sand-1 shadow-6 outline-none",
"transition-transform duration-200",

View File

@@ -49,7 +49,8 @@ export function DropdownPopup(props: DropdownPopupProps) {
return (
<Menu.Portal>
<Menu.Positioner side={side} align={align} sideOffset={sideOffset}>
{/* z-3 on the Positioner so the portaled root wins over in-page z-1. */}
<Menu.Positioner className="z-3" side={side} align={align} sideOffset={sideOffset}>
<Menu.Popup className={dropdownPopup({ className })} {...popupProps}>
<DropdownProvider value={{ size, variant, highContrast }}>
{children}

View File

@@ -43,7 +43,8 @@ export function SelectPopup(props: SelectPopupProps) {
return (
<BaseSelect.Portal>
<BaseSelect.Positioner side={side} align={align} sideOffset={sideOffset}>
{/* z-3 on the Positioner so the portaled root wins over in-page z-1. */}
<BaseSelect.Positioner className="z-3" side={side} align={align} sideOffset={sideOffset}>
<BaseSelect.Popup className={selectPopup({ className })} {...popupProps}>
{children}
</BaseSelect.Popup>

View File

@@ -27,7 +27,7 @@ import { tv } from "tailwind-variants/lite";
export const toaster = tv({
slots: {
viewport: [
"fixed bottom-0 right-0 z-60 flex w-[380px] max-w-[calc(100vw-2rem)] flex-col gap-2 p-4",
"fixed bottom-0 right-0 z-6 flex w-[380px] max-w-[calc(100vw-2rem)] flex-col gap-2 p-4",
"outline-none",
],
root: [

View File

@@ -30,6 +30,7 @@
* typography.css Inter font + type scale (text-1…9) + font weights
* radius.css radius scale (rounded-1…6)
* shadows.css elevation scale (shadow-1…6)
* z-index.css stacking scale (z-1…6) by UI layer
*
* Spacing intentionally stays on Tailwind's native scale (not tokenized).
* See contrib/claude/v2-tokens.md for the full usage guide.
@@ -38,3 +39,4 @@
@import "./theme/typography.css";
@import "./theme/radius.css";
@import "./theme/shadows.css";
@import "./theme/z-index.css";

View File

@@ -0,0 +1,56 @@
/* Copyright (c) 2026 Probo Inc <hello@probo.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* ---------------------------------------------------------------------------
* Z-index — stacking scale by UI layer (steps 16)
*
* Numbered like color / type / radius / shadow. Each step is a layer role, not
* an arbitrary elevation. Values are spaced so a future mid-layer can land
* between them without renumbering consumers.
*
* Step value Role
* ───── ────── ────────────────────────────────────────────────────────
* 1 1 Local stacking inside a component (media over backdrop)
* 2 10 Sticky / fixed page chrome (top bars, sticky toolbars)
* 3 30 Floating menus (select, dropdown, tooltip, popover)
* 4 40 Overlay backdrops (dimmers under modals / drawers)
* 5 50 Modals and drawers
* 6 60 Toasts / global notifications (always on top)
*
* Put the token on the portaled root that participates in the document
* stacking context (e.g. the Positioner for menus, not only the inner Popup).
* A local z-1 inside the page will otherwise paint above a portaled popup
* that has no z-index of its own.
*
* --z-index-* is reset to initial so the v2 build exposes only this numeric
* scale (not Tailwind's z-10 / z-20 / … defaults). z-auto stays available as
* a static utility.
* ------------------------------------------------------------------------- */
@theme {
--z-index-*: initial;
--z-index-1: 1;
--z-index-2: 10;
--z-index-3: 30;
--z-index-4: 40;
--z-index-5: 50;
--z-index-6: 60;
}