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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
@@ -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-<1–9>` (+ font weights) | 9 | Radix type scale |
|
||||
| Radius | `rounded-<1–6>` | 6 | Radix "Medium" radius |
|
||||
| Shadow | `shadow-<1–6>`, `inset-shadow-<1–3>` | 6 / 3 | Radix elevation |
|
||||
| Z-index | `z-<1–6>` | 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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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";
|
||||
|
||||
56
packages/ui/src/v2/theme/z-index.css
Normal file
56
packages/ui/src/v2/theme/z-index.css
Normal 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 1–6)
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user