diff --git a/ui/litellm-dashboard/eslint.config.mjs b/ui/litellm-dashboard/eslint.config.mjs
index 6d7ca2ad071..23cc5096bb1 100644
--- a/ui/litellm-dashboard/eslint.config.mjs
+++ b/ui/litellm-dashboard/eslint.config.mjs
@@ -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}"],
diff --git a/ui/litellm-dashboard/scripts/eslint-rules/index.mjs b/ui/litellm-dashboard/scripts/eslint-rules/index.mjs
index 983399ae4a3..750b8df4e27 100644
--- a/ui/litellm-dashboard/scripts/eslint-rules/index.mjs
+++ b/ui/litellm-dashboard/scripts/eslint-rules/index.mjs
@@ -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,
},
};
diff --git a/ui/litellm-dashboard/scripts/eslint-rules/no-ad-hoc-z-index.mjs b/ui/litellm-dashboard/scripts/eslint-rules/no-ad-hoc-z-index.mjs
new file mode 100644
index 00000000000..fbe0fdcf7e5
--- /dev/null
+++ b/ui/litellm-dashboard/scripts/eslint-rules/no-ad-hoc-z-index.mjs
@@ -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;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx
index fae21f8dfc4..dc1223264bb 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx
@@ -758,7 +758,7 @@ type ConfirmDialogProps = {
function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDialogProps) {
const isApprove = action === "approve";
return (
-
+
= ({
}) => {
return (