mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
parent
50a42ba38e
commit
6385b6d801
58 changed files with 397 additions and 129 deletions
|
|
@ -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}"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 ${
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const NESTED_DIALOG_LAYER = "z-[1100]";
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ${
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'll be redirected to the full-screen Flow Builder to design your policy logic visually.
|
||||
</AlertTitle>
|
||||
<AlertTitle>You'll be taken to the Flow Builder to design your policy logic visually.</AlertTitle>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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} />}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
21
ui/litellm-dashboard/src/lib/cva.config.test.ts
Normal file
21
ui/litellm-dashboard/src/lib/cva.config.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue