refactor(ui): replace hand-picked z-index values with one named scale and lint it

Regression LIT-6143 (the policy Flow Builder painting its guardrail dropdown
underneath a position: fixed shell at z-index 1000) was one instance of a
class of bug: pages picking their own z-index numbers above the portalled
popup layer. This removes the class.

- globals.css defines the only z-index values in the dashboard as Tailwind
  utilities: z-raised, z-chrome, z-sticky, z-sticky-pinned, z-floating,
  z-overlay, z-popup; every numeric, arbitrary and inline z-index across
  src is migrated onto them and tailwind-merge learns the tokens
- new local/no-ad-hoc-z-index ESLint rule bans z-<n>, z-[...], z-(...) and
  inline zIndex everywhere, and reserves z-popup for the portalled
  primitives in components/ui (and the DataTable menus)
- the Flow Builder renders in the dashboard content area instead of as a
  fixed full-screen overlay, so it has no stacking level at all
- the guardrail content-filter Add keyword / Add pattern / Custom pattern
  dialogs drop the leftover z-[1100] (renamed from ABOVE_ANTD_MODAL when
  antd was removed) that hid their own Action select and pattern combobox
  behind the dialog, the same bug as LIT-6143
This commit is contained in:
ryan-crabbe-berri 2026-08-25 17:02:35 -07:00
parent 50a42ba38e
commit 6385b6d801
58 changed files with 397 additions and 129 deletions

View file

@ -82,9 +82,22 @@ const eslintConfig = [
"no-restricted-syntax": "off",
},
},
{
files: ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"],
rules: { "local/no-ad-hoc-z-index": "error" },
},
{
files: [
"src/components/ui/**/*.{ts,tsx}",
"src/components/shared/DataTable/**/*.{ts,tsx}",
"src/**/*.test.{ts,tsx}",
"tests/**/*.{ts,tsx}",
],
rules: { "local/no-ad-hoc-z-index": ["error", { allowPopupLayer: true }] },
},
{
files: ["tests/eslint-rules/**/*.{ts,tsx}"],
rules: { "local/no-noop-hover-variant": "off" },
rules: { "local/no-noop-hover-variant": "off", "local/no-ad-hoc-z-index": "off" },
},
{
files: ["src/**/*.test.{ts,tsx}", "tests/**/*.{ts,tsx}"],

View file

@ -3,6 +3,7 @@ import noLongConditionChain from "./no-long-condition-chain.mjs";
import noComplexJsxArrow from "./no-complex-jsx-arrow.mjs";
import filenamePascalCase from "./filename-pascal-case.mjs";
import noNoopHoverVariant from "./no-noop-hover-variant.mjs";
import noAdHocZIndex from "./no-ad-hoc-z-index.mjs";
const plugin = {
rules: {
@ -11,6 +12,7 @@ const plugin = {
"no-complex-jsx-arrow": noComplexJsxArrow,
"filename-pascal-case": filenamePascalCase,
"no-noop-hover-variant": noNoopHoverVariant,
"no-ad-hoc-z-index": noAdHocZIndex,
},
};

View file

@ -0,0 +1,71 @@
const AD_HOC_Z = /^(?:[\w-]+:)*-?z-(?:\d+|\[[^\]]*\]|\([^)]*\))$/;
const POPUP_Z = /^(?:[\w-]+:)*z-popup$/;
const classify = (token, allowPopupLayer) => {
if (AD_HOC_Z.test(token)) return "adHoc";
if (!allowPopupLayer && POPUP_Z.test(token)) return "popupReserved";
return null;
};
const offendingTokens = (value, allowPopupLayer) =>
value
.split(/\s+/)
.filter(Boolean)
.map((token) => ({ token, messageId: classify(token, allowPopupLayer) }))
.filter(({ messageId }) => messageId !== null);
const propertyName = (key) => {
if (key.type === "Identifier") return key.name;
if (key.type === "Literal" && typeof key.value === "string") return key.value;
return null;
};
const rule = {
meta: {
type: "problem",
docs: {
description:
"Disallow hand-picked z-index values (numeric or arbitrary z-* classes, inline zIndex styles). Use the named scale defined in src/app/globals.css so nothing can stack above the portalled popup layer.",
},
schema: [
{
type: "object",
properties: { allowPopupLayer: { type: "boolean" } },
additionalProperties: false,
},
],
messages: {
adHoc:
"`{{token}}` is a hand-picked z-index. Use the scale from globals.css: z-raised, z-chrome, z-sticky, z-sticky-pinned, z-floating, z-overlay (z-popup is reserved for portalled primitives).",
popupReserved:
"`{{token}}` is reserved for the portalled primitives in src/components/ui. Page content must stay below the popup layer; use z-overlay or lower.",
inlineZIndex:
"Inline `zIndex` styles bypass the z-index scale. Use a class from globals.css (z-raised, z-chrome, z-sticky, z-sticky-pinned, z-floating, z-overlay) instead.",
},
},
create(context) {
const allowPopupLayer = context.options[0]?.allowPopupLayer ?? false;
const check = (node, value) => {
if (typeof value !== "string" || !value.includes("z-")) return;
for (const { token, messageId } of offendingTokens(value, allowPopupLayer)) {
context.report({ node, messageId, data: { token } });
}
};
return {
Literal(node) {
check(node, node.value);
},
TemplateElement(node) {
check(node, node.value.cooked);
},
Property(node) {
const name = propertyName(node.key);
if (name === "zIndex" || name === "z-index") {
context.report({ node, messageId: "inlineZIndex" });
}
},
};
},
};
export default rule;

View file

@ -758,7 +758,7 @@ type ConfirmDialogProps = {
function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDialogProps) {
const isApprove = action === "approve";
return (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-overlay">
<div className="bg-card rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center mb-4 ${

View file

@ -4,7 +4,6 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ACTION_ITEMS } from "./action_options";
import { NESTED_DIALOG_LAYER } from "./dialog_layering";
interface CustomPatternModalProps {
visible: boolean;
@ -31,7 +30,7 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
}) => {
return (
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${NESTED_DIALOG_LAYER}`}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px]">
<DialogHeader>
<DialogTitle>Add custom regex pattern</DialogTitle>
</DialogHeader>

View file

@ -88,4 +88,13 @@ describe("KeywordModal", () => {
expect(screen.queryByText("Add blocked keyword")).not.toBeInTheDocument();
});
it("should not raise the dialog above the portalled popup layer its Action select renders into", async () => {
renderModal();
await screen.findByText("Add blocked keyword");
const content = document.querySelector('[data-slot="dialog-content"]');
expect(content).not.toBeNull();
expect(Array.from(content!.classList).filter((cls) => cls.startsWith("z-"))).toEqual(["z-popup"]);
});
});

View file

@ -5,7 +5,6 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ACTION_ITEMS } from "./action_options";
import { NESTED_DIALOG_LAYER } from "./dialog_layering";
interface KeywordModalProps {
visible: boolean;
@ -32,7 +31,7 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
}) => {
return (
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${NESTED_DIALOG_LAYER}`}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px]">
<DialogHeader>
<DialogTitle>Add blocked keyword</DialogTitle>
</DialogHeader>

View file

@ -142,4 +142,13 @@ describe("PatternModal", () => {
expect(screen.queryByText("Add prebuilt pattern")).not.toBeInTheDocument();
});
it("should not raise the dialog above the portalled popup layer its pattern combobox renders into", async () => {
renderModal();
await screen.findByText("Add prebuilt pattern");
const content = document.querySelector('[data-slot="dialog-content"]');
expect(content).not.toBeNull();
expect(Array.from(content!.classList).filter((cls) => cls.startsWith("z-"))).toEqual(["z-popup"]);
});
});

View file

@ -14,7 +14,6 @@ import {
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ACTION_ITEMS } from "./action_options";
import { NESTED_DIALOG_LAYER } from "./dialog_layering";
interface PrebuiltPattern {
name: string;
@ -66,7 +65,7 @@ const PatternModal: React.FC<PatternModalProps> = ({
return (
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className={`max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px] ${NESTED_DIALOG_LAYER}`}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px]">
<DialogHeader>
<DialogTitle>Add prebuilt pattern</DialogTitle>
</DialogHeader>

View file

@ -1 +0,0 @@
export const NESTED_DIALOG_LAYER = "z-[1100]";

View file

@ -522,7 +522,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
variant="ghost"
size="icon-xs"
onClick={() => copyToClipboard(guardrailData.guardrail_id, "guardrail-id")}
className={`left-2 z-10 transition-all duration-200 ${
className={`left-2 z-raised transition-all duration-200 ${
copiedStates["guardrail-id"]
? "text-success bg-success/10 border-success/20"
: "text-muted-foreground hover:text-foreground hover:bg-muted"

View file

@ -79,7 +79,7 @@ function ConfirmDialog({ action, serverName, isCurrentlyActive, onConfirm, onCan
? "This server is currently live. Rejecting it will immediately remove it from the proxy runtime."
: "This will mark the submission as rejected.";
return (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-overlay">
<div className="bg-card rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center mb-4 ${

View file

@ -164,7 +164,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
variant="ghost"
size="icon-xs"
onClick={() => copyToClipboard(code, copyKey)}
className={`absolute top-2 right-2 z-10 transition-all duration-200 ${
className={`absolute top-2 right-2 z-raised transition-all duration-200 ${
copiedStates[copyKey]
? "text-success bg-success/10 border-success/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"

View file

@ -100,7 +100,7 @@ export function ComparisonPanel({
{/* Close button in top right */}
<button
onClick={handleClosePopover}
className="absolute top-0 right-0 p-1 hover:bg-accent rounded-sm transition-colors text-muted-foreground hover:text-foreground z-10"
className="absolute top-0 right-0 p-1 hover:bg-accent rounded-sm transition-colors text-muted-foreground hover:text-foreground z-raised"
>
<X size={14} />
</button>

View file

@ -757,7 +757,7 @@ export default function ComplianceUI({
<ChevronDown className="w-4 h-4 text-muted-foreground" />
</button>
{showGuardrailDropdown && (
<div className="absolute z-30 top-full left-0 right-0 mt-1 bg-card border border-border rounded-lg shadow-lg py-1 max-h-52 overflow-y-auto">
<div className="absolute z-floating top-full left-0 right-0 mt-1 bg-card border border-border rounded-lg shadow-lg py-1 max-h-52 overflow-y-auto">
{guardrailOptions.length === 0 ? (
<div className="px-3 py-2 text-xs text-muted-foreground">
No guardrails available. Create guardrails in the Guardrails page.

View file

@ -198,6 +198,12 @@ describe("AddPolicyForm", () => {
renderWithProviders(<AddPolicyForm {...defaultProps} onClose={onClose} onOpenFlowBuilder={onOpenFlowBuilder} />);
await user.click(await screen.findByText("Flow Builder"));
expect(
screen.getByText("You'll be taken to the Flow Builder to design your policy logic visually."),
).toBeInTheDocument();
expect(screen.queryByText(/full-screen/i)).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Continue to Builder" }));
expect(onOpenFlowBuilder).toHaveBeenCalledTimes(1);

View file

@ -342,9 +342,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
{selectedMode === "flow_builder" && (
<Alert variant="info" className="mt-4 border border-info/20 bg-info/10">
<AlertTitle>
You&apos;ll be redirected to the full-screen Flow Builder to design your policy logic visually.
</AlertTitle>
<AlertTitle>You&apos;ll be taken to the Flow Builder to design your policy logic visually.</AlertTitle>
</Alert>
)}

View file

@ -2,7 +2,7 @@ import React from "react";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "@/../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import PoliciesPanel from "./index";
/**
@ -52,7 +52,11 @@ vi.mock("./policy_templates", () => ({
}));
vi.mock("./pipeline_flow_builder", () => ({
FlowBuilderPage: () => null,
FlowBuilderPage: ({ onBack }: { onBack: () => void }) => (
<button type="button" onClick={onBack}>
Back to policies
</button>
),
}));
vi.mock("./policy_info", () => ({
@ -159,3 +163,48 @@ describe("PoliciesPanel attachment delete", () => {
});
});
});
describe("PoliciesPanel flow builder", () => {
const POLICY_ID = "pol-11111111-2222-3333-4444-555555555555";
beforeEach(() => {
vi.clearAllMocks();
networkingMocks.getPoliciesList.mockResolvedValue({
policies: [
{
policy_id: POLICY_ID,
policy_name: "pii-policy",
inherit: null,
description: null,
guardrails_add: [],
guardrails_remove: [],
condition: null,
definition_location: "db",
},
],
});
});
afterEach(() => {
networkingMocks.getPoliciesList.mockResolvedValue({ policies: [] });
});
it("replaces the tabs and policy table with the flow builder while editing, then restores them on back", async () => {
const user = userEvent.setup();
renderWithProviders(<PoliciesPanel accessToken="test-token" userRole="Admin" />);
await user.click(screen.getByRole("tab", { name: /^policies$/i }));
await user.click(await screen.findByTestId(`policy-actions-${POLICY_ID}`));
await user.click(await screen.findByTestId("policy-action-edit"));
expect(await screen.findByRole("button", { name: "Back to policies" })).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: /^policies$/i })).not.toBeInTheDocument();
expect(screen.queryByText("pii-policy")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Back to policies" }));
expect(await screen.findByText("pii-policy")).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /^policies$/i })).toHaveAttribute("aria-selected", "true");
expect(screen.queryByRole("button", { name: "Back to policies" })).not.toBeInTheDocument();
});
});

View file

@ -406,6 +406,37 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({ accessToken, userRole })
setTemplateQueueProgress(null);
};
if (showFlowBuilder) {
return (
<FlowBuilderPage
onBack={() => {
setShowFlowBuilder(false);
setEditingPolicy(null);
}}
onSuccess={() => {
fetchPolicies();
setEditingPolicy(null);
}}
accessToken={accessToken}
editingPolicy={editingPolicy}
availableGuardrails={guardrailsList}
createPolicy={createPolicyCall}
updatePolicy={updatePolicyCall}
onVersionCreated={(newPolicy) => {
setEditingPolicy(newPolicy);
fetchPolicies();
}}
onSelectVersion={(policy) => {
setEditingPolicy(policy);
}}
onVersionStatusUpdated={(updatedPolicy) => {
setEditingPolicy(updatedPolicy);
fetchPolicies();
}}
/>
);
}
return (
<div className="m-8 mx-auto w-full flex-auto overflow-y-auto p-2">
<Tabs value={activeTab} onValueChange={setActiveTab}>
@ -628,35 +659,6 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({ accessToken, userRole })
accessToken={accessToken}
allTemplates={loadedTemplates}
/>
{showFlowBuilder && (
<FlowBuilderPage
onBack={() => {
setShowFlowBuilder(false);
setEditingPolicy(null);
}}
onSuccess={() => {
fetchPolicies();
setEditingPolicy(null);
}}
accessToken={accessToken}
editingPolicy={editingPolicy}
availableGuardrails={guardrailsList}
createPolicy={createPolicyCall}
updatePolicy={updatePolicyCall}
onVersionCreated={(newPolicy) => {
setEditingPolicy(newPolicy);
fetchPolicies();
}}
onSelectVersion={(policy) => {
setEditingPolicy(policy);
}}
onVersionStatusUpdated={(updatedPolicy) => {
setEditingPolicy(updatedPolicy);
fetchPolicies();
}}
/>
)}
</div>
);
};

View file

@ -169,8 +169,7 @@ describe("PipelineFlowBuilder", () => {
});
describe("FlowBuilderPage", () => {
it("stacks its full-screen shell below the portalled popup layer", () => {
const portalLayerZIndex = 50;
it("renders its shell in flow with no stacking level, so it can never cover the portalled popup layer", () => {
const { container } = renderWithProviders(
<FlowBuilderPage
onBack={vi.fn()}
@ -183,8 +182,12 @@ describe("FlowBuilderPage", () => {
);
const shell = container.firstElementChild as HTMLElement;
const shellClasses = shell.className.split(/\s+/);
expect(shell).toHaveStyle({ position: "fixed" });
expect(Number(shell.style.zIndex)).toBeLessThan(portalLayerZIndex);
expect(shell).toContainElement(screen.getByPlaceholderText("Policy name..."));
expect(shell).not.toHaveStyle({ position: "fixed" });
expect(shellClasses).not.toContain("fixed");
expect(window.getComputedStyle(shell).zIndex).not.toMatch(/\d/);
expect(shellClasses.filter((cls) => /^-?z-/.test(cls))).toEqual([]);
});
});

View file

@ -227,7 +227,7 @@ const Connector: React.FC<ConnectorProps> = ({ onInsert }) => (
<div style={{ width: 1, flex: 1, backgroundColor: "var(--color-border)" }} />
<button
onClick={onInsert}
className="flex items-center justify-center"
className="z-raised flex items-center justify-center"
style={{
width: 24,
height: 24,
@ -235,7 +235,6 @@ const Connector: React.FC<ConnectorProps> = ({ onInsert }) => (
border: "1px solid var(--color-border)",
backgroundColor: "var(--color-card)",
cursor: "pointer",
zIndex: 1,
transition: "all 0.15s ease",
}}
onMouseEnter={(e) => {
@ -1623,20 +1622,7 @@ export const FlowBuilderPage: React.FC<FlowBuilderPageProps> = ({
};
return (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "var(--color-muted)",
zIndex: 40,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<div className="flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden bg-muted">
{/* Header bar */}
<div
style={{

View file

@ -86,7 +86,7 @@ const VersionHistorySidePanel: React.FC<VersionHistorySidePanelProps> = ({
role="dialog"
aria-modal={false}
aria-labelledby="version-history-title"
className="fixed inset-y-0 right-0 z-50 flex w-[400px] max-w-full flex-col gap-4 border-l border-border bg-popover text-popover-foreground shadow-lg"
className="fixed inset-y-0 right-0 z-overlay flex w-[400px] max-w-full flex-col gap-4 border-l border-border bg-popover text-popover-foreground shadow-lg"
>
<Button type="button" variant="ghost" size="icon-sm" className="absolute top-4 right-4" onClick={onClose}>
<XIcon />

View file

@ -195,7 +195,7 @@ const PromptInfoView: React.FC<PromptInfoProps> = ({ promptId, onClose, accessTo
variant="ghost"
size="icon-xs"
onClick={() => copyToClipboard(basePromptId, "prompt-id")}
className={`left-2 z-10 transition-all duration-200 ${
className={`left-2 z-raised transition-all duration-200 ${
copiedStates["prompt-id"]
? "text-success bg-success/10 border-success/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"

View file

@ -239,7 +239,7 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
return (
<div
data-testid="usage-ai-chat-panel"
className={`fixed top-0 right-0 h-full bg-card border-l border-border shadow-2xl z-50 flex flex-col transition-transform duration-300 ease-in-out ${
className={`fixed top-0 right-0 h-full bg-card border-l border-border shadow-2xl z-overlay flex flex-col transition-transform duration-300 ease-in-out ${
open ? "translate-x-0" : "translate-x-full"
}`}
style={{ width: 420 }}

View file

@ -416,7 +416,7 @@ export default function UserInfoView({
variant="ghost"
size="icon-xs"
onClick={() => copyToClipboard(userData.user_id, "user-id")}
className={`left-2 z-10 transition-all duration-200 ${
className={`left-2 z-raised transition-all duration-200 ${
copiedStates["user-id"]
? "text-success bg-success/10 border-success/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
@ -598,7 +598,7 @@ export default function UserInfoView({
variant="ghost"
size="icon-xs"
onClick={() => copyToClipboard(userData.user_id, "user-id")}
className={`left-2 z-10 transition-all duration-200 ${
className={`left-2 z-raised transition-all duration-200 ${
copiedStates["user-id"]
? "text-success bg-success/10 border-success/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"

View file

@ -585,7 +585,7 @@ export default function ChatConversationPage() {
}
}
}}
className="absolute bottom-[100px] left-1/2 -translate-x-1/2 z-10 rounded-full border bg-background/75 text-muted-foreground shadow-sm backdrop-blur-md hover:bg-background/95"
className="absolute bottom-[100px] left-1/2 -translate-x-1/2 z-chrome rounded-full border bg-background/75 text-muted-foreground shadow-sm backdrop-blur-md hover:bg-background/95"
aria-label="Scroll to bottom"
>
<ChevronDown className="h-3 w-3" />

View file

@ -27,6 +27,30 @@
}
}
/* The only z-index values in the dashboard. Portalled popups (src/components/ui) own the top of the
scale; everything else must sit below them. Enforced by the local/no-ad-hoc-z-index ESLint rule. */
@utility z-raised {
z-index: 1;
}
@utility z-chrome {
z-index: 10;
}
@utility z-sticky {
z-index: 20;
}
@utility z-sticky-pinned {
z-index: 25;
}
@utility z-floating {
z-index: 30;
}
@utility z-overlay {
z-index: 40;
}
@utility z-popup {
z-index: 50;
}
/* Verbatim end-edge subset of the shadcn scroll-fade utility (shadcn@4.17.0 dist/tailwind.css),
vendored so the CLI package is not a build dependency. Re-copy from upstream to update. */
@property --scroll-fade-e {

View file

@ -23,7 +23,7 @@ const CodeBlock = ({ code, language }: CodeBlockProps) => {
<div className="relative rounded-lg border border-border overflow-hidden">
<button
onClick={copyToClipboard}
className="absolute top-3 right-3 p-2 rounded-md bg-muted hover:bg-accent text-muted-foreground z-10"
className="absolute top-3 right-3 p-2 rounded-md bg-muted hover:bg-accent text-muted-foreground z-raised"
aria-label="Copy code"
>
{copied ? <CheckIcon size={16} /> : <ClipboardIcon size={16} />}

View file

@ -102,7 +102,7 @@ export const HelpIcon: React.FC<HelpIconProps> = ({ content, learnMoreHref, lear
</button>
{showTooltip && (
<div
className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-50 bg-gray-900 text-white p-3 rounded-lg text-xs shadow-lg w-64"
className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 z-floating bg-gray-900 text-white p-3 rounded-lg text-xs shadow-lg w-64"
style={{ pointerEvents: "none" }}
>
<div className="mb-2">{content}</div>
@ -178,7 +178,7 @@ export const DocsMenu: React.FC<DocsMenuProps> = ({ items, children = "Docs", cl
</button>
{isOpen && (
<div className="absolute right-0 mt-1 w-56 bg-card rounded-lg shadow-lg border border-border py-1 z-50">
<div className="absolute right-0 mt-1 w-56 bg-card rounded-lg shadow-lg border border-border py-1 z-floating">
{items.map((item, index) => (
<a
key={index}

View file

@ -134,7 +134,7 @@ const PassThroughSettings: React.FC<PassThroughSettingsProps> = ({ accessToken,
/>
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="fixed z-overlay inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>

View file

@ -92,7 +92,7 @@ export function FallbackGroupConfig({
</div>
{/* Visual Connection */}
<div className="flex items-center justify-center -my-4 z-10">
<div className="flex items-center justify-center -my-4 z-raised">
<div className="bg-indigo-50 text-indigo-500 px-4 py-1 rounded-full text-xs font-bold border border-indigo-100 flex items-center gap-2 shadow-xs dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-900">
<ArrowDown className="w-4 h-4" />
IF FAILS, TRY...

View file

@ -221,7 +221,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
customTooltip={(props) => {
const item = props.payload?.[0]?.payload;
return (
<div className="relative z-50 p-3 bg-black/90 shadow-lg rounded-lg text-white max-w-xs">
<div className="relative z-floating p-3 bg-black/90 shadow-lg rounded-lg text-white max-w-xs">
<div className="space-y-1.5">
<div className="text-sm">
<span className="text-muted-foreground">Key Alias: </span>
@ -246,7 +246,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
)}
{isModalOpen && selectedKey && keyData && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={handleOutsideClick}>
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-overlay" onClick={handleOutsideClick}>
<div className="bg-card rounded-lg shadow-xl relative w-11/12 max-w-6xl max-h-[90vh] overflow-y-auto min-h-[750px]">
{/* Close button */}
<button

View file

@ -93,7 +93,7 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents, defaultOpenKeys }: MCPEven
{toolsEvent.item?.tools?.map((tool, index) => (
<div
key={index}
className="relative z-[1] bg-card font-mono text-[13px] leading-[18px] text-muted-foreground"
className="relative z-raised bg-card font-mono text-[13px] leading-[18px] text-muted-foreground"
>
{tool.name}
</div>
@ -113,7 +113,7 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents, defaultOpenKeys }: MCPEven
onOpenChange={(open) => toggleKey(key, open)}
>
<div>
<div className="relative z-[1] mb-3 bg-card last:mb-0">
<div className="relative z-raised mb-3 bg-card last:mb-0">
<div className="mb-1 text-[13px] font-medium text-muted-foreground">Request</div>
<div className="rounded-md border border-border bg-muted p-2 text-xs">
{callEvent.item?.arguments && (
@ -124,7 +124,7 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents, defaultOpenKeys }: MCPEven
</div>
</div>
<div className="relative z-[1] mb-3 bg-card last:mb-0">
<div className="relative z-raised mb-3 bg-card last:mb-0">
<div className="flex items-center text-[13px] text-muted-foreground">
<span className="mr-1.5 font-bold text-success" aria-hidden="true">
@ -134,7 +134,7 @@ function MCPEventsPanels({ toolsEvent, mcpCallEvents, defaultOpenKeys }: MCPEven
</div>
{callEvent.item?.output && (
<div className="relative z-[1] mb-3 bg-card last:mb-0">
<div className="relative z-raised mb-3 bg-card last:mb-0">
<div className="mb-1 text-[13px] font-medium text-muted-foreground">Response</div>
<div className="whitespace-pre-wrap font-mono text-[13px] leading-normal text-foreground">
{callEvent.item.output}

View file

@ -604,7 +604,7 @@ export default function ModelInfoView({
size="icon-xs"
aria-label="Copy model ID"
onClick={() => copyToClipboard(modelData.model_info.id, "model-id")}
className={`left-2 z-10 transition-all duration-200 ${
className={`left-2 z-raised transition-all duration-200 ${
copiedStates["model-id"]
? "text-success bg-success/10 border-success/20"
: "text-muted-foreground hover:text-foreground hover:bg-muted"

View file

@ -66,7 +66,7 @@ const Navbar: React.FC<NavbarProps> = ({
};
return (
<nav className="sticky top-0 z-10 border-b border-border bg-card">
<nav className="sticky top-0 z-chrome border-b border-border bg-card">
<div className="w-full">
<div className="flex h-14 items-center px-4">
<div className="flex shrink-0 items-center">
@ -111,7 +111,7 @@ const Navbar: React.FC<NavbarProps> = ({
🌑
</span>
)}
<Badge variant="outline" className="relative z-10 cursor-pointer text-xs font-medium">
<Badge variant="outline" className="relative z-raised cursor-pointer text-xs font-medium">
<a
href="https://docs.litellm.ai/release_notes"
target="_blank"

View file

@ -153,7 +153,7 @@ export const OrgSettingsForm = ({
</FormField>
</FieldGroup>
<div className="sticky z-10 bg-card p-4 border-t border-border -bottom-6 -inset-x-6 mt-6">
<div className="sticky z-chrome bg-card p-4 border-t border-border -bottom-6 -inset-x-6 mt-6">
<div className="flex justify-end items-center gap-2">
<Button type="button" variant="outline" onClick={onCancel} disabled={mutation.isPending}>
Cancel

View file

@ -105,14 +105,14 @@ function buildRowModels<TData>(
};
}
function stickyZIndex(isPinned: boolean, isHeader: boolean): number {
function stickyLayer(isPinned: boolean, isHeader: boolean): string {
if (isPinned && isHeader) {
return 30;
return "z-sticky-pinned";
}
if (isHeader) {
return 20;
return "z-sticky";
}
return 10;
return "z-raised";
}
function pinnedShadow(pinned: false | ColumnPinnedSide): string {
@ -141,13 +141,15 @@ function computeStickyStyle<TData, TValue>(
const style: React.CSSProperties = {
position: "sticky",
zIndex: stickyZIndex(pinned !== false, isHeader),
...(stickyTop ? { top: 0 } : {}),
...(left !== undefined ? { left } : {}),
...(right !== undefined ? { right } : {}),
};
return { style, className: cn(pinned ? "bg-background" : "", pinnedShadow(pinned)) };
return {
style,
className: cn(stickyLayer(pinned !== false, isHeader), pinned ? "bg-background" : "", pinnedShadow(pinned)),
};
}
function widthStyle<TData, TValue>(
@ -595,7 +597,7 @@ export function DataTable<TData extends RowData, TValue>(props: DataTableProps<T
style={maxBodyHeight !== undefined ? { maxHeight: maxBodyHeight } : undefined}
>
<TableRoot className={enableColumnResizing ? "table-fixed" : ""} style={tableStyle}>
<TableHeader className={cn(stickyHeader ? "sticky top-0 z-20" : "", fill.header)}>
<TableHeader className={cn(stickyHeader ? "sticky top-0 z-sticky" : "", fill.header)}>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-muted/50">
{headerGroup.headers.map((header) => (

View file

@ -63,7 +63,7 @@ export function DataTableSortHeader<TData, TValue>({
}
/>
<Menu.Portal>
<Menu.Positioner side="bottom" align="start" sideOffset={4} className="isolate z-50">
<Menu.Positioner side="bottom" align="start" sideOffset={4} className="isolate z-popup">
<Menu.Popup className="min-w-[9rem] rounded-md bg-popover p-1 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden">
<Menu.Item className={MENU_ITEM_CLASS} onClick={() => column.toggleSorting(false)}>
<ChevronUp className="size-3.5" /> Ascending
@ -167,7 +167,7 @@ export function DataTableMultiSortHeader<TData>({ table, fields, className }: Da
}
/>
<Menu.Portal>
<Menu.Positioner side="bottom" align="start" sideOffset={4} className="isolate z-50">
<Menu.Positioner side="bottom" align="start" sideOffset={4} className="isolate z-popup">
<Menu.Popup className="min-w-[9rem] rounded-md bg-popover p-1 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden">
{options.map((option) => {
const isActive = activeField?.id === option.id && activeField.desc === option.desc;

View file

@ -30,7 +30,7 @@ export function DataTableViewOptions<TData>({ table, label = "View", className }
}
/>
<Menu.Portal>
<Menu.Positioner side="bottom" align="end" sideOffset={4} className="isolate z-50">
<Menu.Positioner side="bottom" align="end" sideOffset={4} className="isolate z-popup">
<Menu.Popup className="min-w-[12rem] rounded-md bg-popover p-1 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden">
{hideableColumns.map((column) => (
<Menu.CheckboxItem

View file

@ -311,7 +311,7 @@ const AdvancedDatePicker: React.FC<AdvancedDatePickerProps> = ({
data-slot="advanced-date-picker-panel"
data-align={align}
className={cn(
"absolute top-full z-9999 min-w-[600px] mt-1 bg-card border border-border rounded-lg shadow-xl",
"absolute top-full z-floating min-w-[600px] mt-1 bg-card border border-border rounded-lg shadow-xl",
align === "left" ? "left-0" : "right-0",
)}
>

View file

@ -1689,7 +1689,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</FormField>
</FieldGroup>
<div className="sticky z-10 -inset-x-6 -bottom-6 border-t border-border bg-card p-4 pr-0">
<div className="sticky z-chrome -inset-x-6 -bottom-6 border-t border-border bg-card p-4 pr-0">
<div className="flex items-center justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setIsEditing(false)} disabled={isTeamSaving}>
Cancel
@ -1921,7 +1921,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
variant="ghost"
size="icon-xs"
onClick={() => copyToClipboard(info.team_id, "team-id")}
className={`left-2 z-10 transition-all duration-200 ${
className={`left-2 z-raised transition-all duration-200 ${
copiedStates["team-id"]
? "text-success bg-success/10 border-success/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"

View file

@ -872,7 +872,7 @@ export function KeyEditView({
</div>
</FieldGroup>
<div className="sticky z-10 bg-background p-4 border-t border-border -bottom-6 -inset-x-6">
<div className="sticky z-chrome bg-background p-4 border-t border-border -bottom-6 -inset-x-6">
<div className="flex justify-end items-center gap-2">
<Button type="button" variant="secondary" onClick={onCancel} disabled={isKeySaving}>
Cancel

View file

@ -23,7 +23,7 @@ function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdr
<AlertDialogPrimitive.Backdrop
data-slot="alert-dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
"fixed inset-0 isolate z-popup bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
@ -45,7 +45,7 @@ function AlertDialogContent({
data-slot="alert-dialog-content"
data-size={size}
className={cn(
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"group/alert-dialog-content fixed top-1/2 left-1/2 z-popup grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}

View file

@ -6,7 +6,7 @@ import { cn } from "@/lib/cva.config";
import { Separator } from "@/components/ui/separator";
const buttonGroupVariants = cva(
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-raised has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {

View file

@ -101,7 +101,7 @@ function ComboboxContent({
alignOffset={alignOffset}
collisionAvoidance={collisionAvoidance}
anchor={anchor}
className="isolate z-50"
className="isolate z-popup"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"

View file

@ -28,7 +28,7 @@ function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props)
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
"fixed inset-0 isolate z-popup bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
@ -50,7 +50,7 @@ function DialogContent({
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"fixed top-1/2 left-1/2 z-popup grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}

View file

@ -29,7 +29,7 @@ function DropdownMenuContent({
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
className="isolate z-popup outline-none"
align={align}
alignOffset={alignOffset}
side={side}
@ -38,7 +38,7 @@ function DropdownMenuContent({
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn(
"z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95",
"z-popup max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}

View file

@ -28,12 +28,12 @@ function HoverCardContent({
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
className="isolate z-popup"
>
<PreviewCardPrimitive.Popup
data-slot="hover-card-content"
className={cn(
"z-50 w-64 origin-(--transform-origin) rounded-lg bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"z-popup w-64 origin-(--transform-origin) rounded-lg bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}

View file

@ -29,12 +29,12 @@ function PopoverContent({
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
className="isolate z-popup"
>
<PopoverPrimitive.Popup
data-slot="popover-content"
className={cn(
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-md bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"z-popup flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-md bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}

View file

@ -61,13 +61,13 @@ function SelectContent({
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
className="isolate z-popup"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn(
"relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"relative isolate z-popup max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
@ -128,7 +128,7 @@ function SelectScrollUpButton({ className, ...props }: React.ComponentProps<type
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
"top-0 z-raised flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@ -143,7 +143,7 @@ function SelectScrollDownButton({ className, ...props }: React.ComponentProps<ty
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
"bottom-0 z-raised flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View file

@ -28,7 +28,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
"fixed inset-0 z-popup bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className,
)}
{...props}
@ -53,7 +53,7 @@ function SheetContent({
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
"fixed z-popup flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className,
)}
{...props}

View file

@ -35,18 +35,18 @@ function TooltipContent({
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
className="isolate z-popup"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"z-popup inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-popup **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
<TooltipPrimitive.Arrow className="z-popup size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>

View file

@ -46,13 +46,13 @@ export function DrawerHeader({
return (
<div
className="z-chrome"
style={{
padding: DRAWER_HEADER_PADDING,
borderBottom: `1px solid ${COLOR_BORDER}`,
backgroundColor: COLOR_BACKGROUND,
position: "sticky",
top: 0,
zIndex: 10,
}}
>
{/* Row 0: Model + Provider with Logo */}

View file

@ -317,7 +317,7 @@ export function LogDetailsDrawer({
variant="ghost"
size="icon-sm"
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 left-2 z-20 bg-card! border! border-border! rounded-md!"
className="absolute top-2 left-2 z-raised bg-card! border! border-border! rounded-md!"
aria-label="Collapse trace sidebar"
>
<ChevronLeft className="size-4" />
@ -327,7 +327,7 @@ export function LogDetailsDrawer({
variant="ghost"
size="icon-sm"
onClick={() => setIsSidebarCollapsed(false)}
className="absolute top-2 left-2 z-20 bg-card! border! border-border! rounded-md!"
className="absolute top-2 left-2 z-raised bg-card! border! border-border! rounded-md!"
aria-label="Expand trace sidebar"
>
<ChevronRight className="size-4" />

View file

@ -126,7 +126,7 @@ const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userSpend, userMaxBudget,
{/* <div className="ml-auto">
<Accordion>
<AccordionHeader><Text>Team Models</Text></AccordionHeader>
<AccordionBody className="absolute right-0 z-10 bg-card p-2 shadow-lg max-w-xs">
<AccordionBody className="absolute right-0 z-floating bg-card p-2 shadow-lg max-w-xs">
<List>
{modelsToDisplay.map((model: string) => (
<ListItem key={model}>

View file

@ -0,0 +1,21 @@
/* eslint-disable local/no-ad-hoc-z-index -- exercises how cn merges the banned numeric classes against the tokens */
import { describe, expect, it } from "vitest";
import { cn } from "./cva.config";
describe("cn z-index token merging", () => {
it("keeps only the last z token when two tokens conflict", () => {
expect(cn("z-raised", "z-overlay")).toBe("z-overlay");
});
it("lets a z token override a numeric z class", () => {
expect(cn("z-50", "z-popup")).toBe("z-popup");
});
it("lets a numeric z class override a z token", () => {
expect(cn("z-popup", "z-50")).toBe("z-50");
});
it("does not merge z tokens with unrelated classes", () => {
expect(cn("z-chrome", "sticky", "top-0")).toBe("z-chrome sticky top-0");
});
});

View file

@ -1,8 +1,16 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { extendTailwindMerge } from "tailwind-merge";
export { cva, type VariantProps } from "class-variance-authority";
const twMerge = extendTailwindMerge({
extend: {
classGroups: {
z: [{ z: ["raised", "chrome", "sticky", "sticky-pinned", "floating", "overlay", "popup"] }],
},
},
});
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export const cx = cn;

View file

@ -0,0 +1,69 @@
import { RuleTester } from "eslint";
import rule from "../../scripts/eslint-rules/no-ad-hoc-z-index.mjs";
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
parserOptions: { ecmaFeatures: { jsx: true } },
},
});
ruleTester.run("no-ad-hoc-z-index", rule as never, {
valid: [
'const c = "sticky top-0 z-chrome border-b";',
'const c = "absolute top-full z-floating mt-1";',
'const c = "fixed inset-0 z-overlay flex";',
'const c = "relative z-raised";',
'const c = "sticky top-0 z-sticky";',
'const c = "z-sticky-pinned";',
'const c = "z-auto";',
'const c = "team-xyz-789";',
'const c = "bg-gray-50 text-gray-900";',
"const c = `flex ${open ? 'z-overlay' : ''}`;",
"const el = <div className=\"absolute z-floating\" />;",
"const style = { position: 'fixed', top: 0 };",
{ code: 'const c = "isolate z-popup";', options: [{ allowPopupLayer: true }] },
{ code: 'const c = "data-[side=top]:z-popup";', options: [{ allowPopupLayer: true }] },
],
invalid: [
{ code: 'const c = "fixed inset-0 z-50";', errors: [{ messageId: "adHoc", data: { token: "z-50" } }] },
{ code: 'const c = "z-10";', errors: [{ messageId: "adHoc", data: { token: "z-10" } }] },
{ code: 'const c = "z-0";', errors: [{ messageId: "adHoc", data: { token: "z-0" } }] },
{ code: 'const c = "-z-10";', errors: [{ messageId: "adHoc", data: { token: "-z-10" } }] },
{ code: 'const c = "z-[1100]";', errors: [{ messageId: "adHoc", data: { token: "z-[1100]" } }] },
{ code: 'const c = "z-9999";', errors: [{ messageId: "adHoc", data: { token: "z-9999" } }] },
{ code: 'const c = "z-(--my-z)";', errors: [{ messageId: "adHoc", data: { token: "z-(--my-z)" } }] },
{ code: 'const c = "md:z-50";', errors: [{ messageId: "adHoc", data: { token: "md:z-50" } }] },
{ code: 'const c = "hover:md:z-[2]";', errors: [{ messageId: "adHoc", data: { token: "hover:md:z-[2]" } }] },
{
code: "const c = `max-h-full ${extra} z-[1100]`;",
errors: [{ messageId: "adHoc", data: { token: "z-[1100]" } }],
},
{
code: "const el = <div className=\"fixed inset-0 z-50\" />;",
errors: [{ messageId: "adHoc", data: { token: "z-50" } }],
},
{
code: 'const c = "z-10 md:z-20";',
errors: [{ messageId: "adHoc" }, { messageId: "adHoc" }],
},
{ code: 'const c = "isolate z-popup";', errors: [{ messageId: "popupReserved", data: { token: "z-popup" } }] },
{
code: 'const c = "isolate z-popup";',
options: [{ allowPopupLayer: false }],
errors: [{ messageId: "popupReserved", data: { token: "z-popup" } }],
},
{ code: "const style = { position: 'fixed', zIndex: 1000 };", errors: [{ messageId: "inlineZIndex" }] },
{ code: "const style = { 'z-index': 1000 };", errors: [{ messageId: "inlineZIndex" }] },
{
code: "const el = <div style={{ zIndex: 40 }} />;",
errors: [{ messageId: "inlineZIndex" }],
},
{
code: 'const c = "z-50";',
options: [{ allowPopupLayer: true }],
errors: [{ messageId: "adHoc", data: { token: "z-50" } }],
},
],
});