From 2f153020690a34f1248f5d22e76dd00400c0a00f Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Jun 2026 01:03:16 +0100 Subject: [PATCH 1/9] change policies ui --- .../public/locales/en-US/translation.toml | 2 + .../components/policies/PoliciesSidebar.tsx | 5 + .../components/shared/PanelHeaderPill.css | 167 ++++++++++++++++++ .../components/shared/PanelHeaderPill.tsx | 138 +++++++++++++++ .../core/components/tools/RightSidebar.tsx | 130 +++++++------- .../src/core/components/tools/ToolPanel.css | 62 +------ .../proprietary/components/chat/ChatPanel.css | 101 +---------- .../proprietary/components/chat/ChatPanel.tsx | 64 +++---- .../components/policies/Policies.css | 48 ++++- .../policies/PoliciesSidebar.test.tsx | 4 +- .../components/policies/PoliciesSidebar.tsx | 134 ++++++++------ .../components/policies/PolicyDetailPanel.tsx | 9 +- .../components/policies/PolicySetupWizard.tsx | 35 ++-- .../components/policies/policyStatus.ts | 9 +- 14 files changed, 552 insertions(+), 356 deletions(-) create mode 100644 frontend/editor/src/core/components/shared/PanelHeaderPill.css create mode 100644 frontend/editor/src/core/components/shared/PanelHeaderPill.tsx diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 5b22c30644..bd93b5913f 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -5837,6 +5837,7 @@ routing = "Routing" security = "Security" [policies.detail] +close = "Close" editSettings = "Edit Settings" enforces = "Enforces" managedByOrg = "Managed by your organization. Contact a team leader to change this policy." @@ -5890,6 +5891,7 @@ allDocTypesTitle = "All document types" back = "Back" builderDesc = "Build the sequence of tools this policy runs on each document." clear = "Clear" +close = "Close" continue = "Continue" docTypesLabel = "Document types" edit = "Edit" diff --git a/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx index d787c6435f..9d9cbed2e2 100644 --- a/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx +++ b/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx @@ -14,6 +14,11 @@ export function usePoliciesEnabled(): boolean { return false; } +/** Whether the Policies list should appear for the current user. False in core. */ +export function usePoliciesVisible(): boolean { + return false; +} + /** * Whether a policy is open (its detail should take over the rail). Always false * in core; proprietary bridges to the policy-selection store. diff --git a/frontend/editor/src/core/components/shared/PanelHeaderPill.css b/frontend/editor/src/core/components/shared/PanelHeaderPill.css new file mode 100644 index 0000000000..d9257653db --- /dev/null +++ b/frontend/editor/src/core/components/shared/PanelHeaderPill.css @@ -0,0 +1,167 @@ +/* ===================== PanelHeaderPill ===================== */ +/* The rounded pill header shared by the active-tool panel, the AI chat panel */ +/* and the Policies detail/wizard. Mirrors the AI chat header treatment so the */ +/* pill reads well in both light and dark mode (thin border, no heavy fill). */ + +.sui-pillhdr { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 1rem 1rem 0.75rem; + flex-shrink: 0; +} + +.sui-pillhdr__pill { + display: inline-flex; + align-items: center; + gap: 0.5rem; + flex: 1; + min-width: 0; + padding: 0.4rem 0.75rem 0.4rem 0.4rem; + border: 1px solid var(--border-subtle); + border-radius: 9999px; + background: var(--mantine-color-body); + text-align: left; + color: inherit; +} + +/* Only the menu-trigger variant is interactive. */ +button.sui-pillhdr__pill { + cursor: pointer; + transition: + background 120ms ease-out, + border-color 120ms ease-out; +} + +button.sui-pillhdr__pill:hover { + background: var(--mantine-color-default-hover); +} + +.sui-pillhdr__icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 9999px; + background: var(--mantine-color-blue-light); + color: var(--mantine-color-blue-filled); + flex-shrink: 0; +} + +.sui-pillhdr__icon svg { + font-size: 1rem; + width: 1rem; + height: 1rem; +} + +/* ToolIcon wraps its glyph in .tool-button-icon with its own margin/transform; + reset them so the glyph sits dead-centre in the circular badge. */ +.sui-pillhdr__icon .tool-button-icon { + margin: 0 !important; + transform: none !important; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.sui-pillhdr__label { + flex: 1; + min-width: 0; + font-size: 0.9rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-primary); +} + +.sui-pillhdr__chevron { + flex-shrink: 0; + color: var(--text-muted); +} + +.sui-pillhdr__trail { + display: inline-flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} + +/* Running / in-progress status dot, anchored to the pill icon. */ +.sui-pillhdr__dot { + position: absolute; + bottom: 0; + right: 0; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--mantine-color-blue-5); + border: 1.5px solid var(--mantine-color-body); + animation: sui-pillhdr-dot-pulse 2.4s ease-in-out infinite; + pointer-events: none; +} + +@keyframes sui-pillhdr-dot-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.45; + } +} + +@media (prefers-reduced-motion: reduce) { + .sui-pillhdr__dot { + animation: none; + } +} + +.sui-pillhdr__pill--loading { + border-color: color-mix( + in srgb, + var(--mantine-color-blue-5) 60%, + var(--border-subtle) + ); +} + +/* Dark mode: let the pill blend into the rail — just a thin border, no fill — + so it doesn't read as a clashing lighter card on the dark toolbar. */ +[data-mantine-color-scheme="dark"] .sui-pillhdr__pill { + background: transparent; + border-color: var(--border-subtle); +} + +[data-mantine-color-scheme="dark"] button.sui-pillhdr__pill:hover { + background: rgba(255, 255, 255, 0.04); +} + +[data-mantine-color-scheme="dark"] .sui-pillhdr__pill--loading { + border-color: color-mix( + in srgb, + var(--mantine-color-blue-4) 55%, + var(--border-subtle) + ); +} + +[data-mantine-color-scheme="dark"] .sui-pillhdr__icon { + background: color-mix( + in srgb, + var(--mantine-color-blue-filled) 18%, + transparent + ); + color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled)); +} + +/* Dark mode: the subtle gray close button is too dim against the dark rail — + brighten it to a clearly-visible light grey (near-white on hover). */ +[data-mantine-color-scheme="dark"] .sui-pillhdr__close { + color: var(--mantine-color-gray-4); +} + +[data-mantine-color-scheme="dark"] .sui-pillhdr__close:hover { + color: var(--mantine-color-gray-2); +} diff --git a/frontend/editor/src/core/components/shared/PanelHeaderPill.tsx b/frontend/editor/src/core/components/shared/PanelHeaderPill.tsx new file mode 100644 index 0000000000..817968a567 --- /dev/null +++ b/frontend/editor/src/core/components/shared/PanelHeaderPill.tsx @@ -0,0 +1,138 @@ +import type { ReactNode } from "react"; +import { ActionIcon, Menu } from "@mantine/core"; +import CloseIcon from "@mui/icons-material/Close"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import "@app/components/shared/PanelHeaderPill.css"; + +export interface PanelHeaderPillMenuItem { + /** Stable key; falls back to the item index. */ + key?: string; + /** Optional leading glyph. */ + icon?: ReactNode; + label: ReactNode; + onClick: () => void; + disabled?: boolean; +} + +export interface PanelHeaderPillProps { + /** Glyph rendered in the tinted circular badge at the pill's leading edge. */ + icon: ReactNode; + /** Pill label. */ + title: ReactNode; + /** Close (X) handler. The trailing close button renders only when supplied. */ + onClose?: () => void; + /** aria-label for the close button. */ + closeLabel?: string; + /** + * When provided, the pill becomes a dropdown trigger: a disclosure chevron is + * shown and clicking the pill opens a menu of these items (e.g. "Clear chat"). + */ + menuItems?: PanelHeaderPillMenuItem[]; + /** aria-label for the pill when it acts as a menu trigger. */ + menuLabel?: string; + /** Shows a pulsing status dot on the icon + a tinted border (e.g. AI running). */ + loading?: boolean; + /** Right-aligned content rendered before the close button (e.g. a status badge). */ + actions?: ReactNode; + /** Applied to the pill element itself — e.g. to set a view-transition-name. */ + pillClassName?: string; + /** Applied to the outer header container. */ + className?: string; +} + +/** + * The rounded "pill" header shared by the rail surfaces — the active tool panel, + * the AI chat panel, and the Policies detail/wizard. A tinted icon badge + title + * sit in a pill, with an optional dropdown menu and a trailing close button. The + * pill styling mirrors the AI chat header so it stays legible in dark mode (thin + * border, no heavy fill) across every surface. + */ +export function PanelHeaderPill({ + icon, + title, + onClose, + closeLabel, + menuItems, + menuLabel, + loading = false, + actions, + pillClassName, + className, +}: PanelHeaderPillProps) { + const hasMenu = menuItems != null && menuItems.length > 0; + + const pillClasses = [ + "sui-pillhdr__pill", + loading ? "sui-pillhdr__pill--loading" : "", + pillClassName ?? "", + ] + .filter(Boolean) + .join(" "); + + const pillBody = ( + <> + + {icon} + {loading && } + + {title} + {hasMenu && ( + + )} + + ); + + return ( +
+ {hasMenu ? ( + + + + + + {(menuItems ?? []).map((item, i) => ( + + {item.label} + + ))} + + + ) : ( +
{pillBody}
+ )} + + {(actions != null || onClose != null) && ( +
+ {actions} + {onClose && ( + + + + )} +
+ )} +
+ ); +} diff --git a/frontend/editor/src/core/components/tools/RightSidebar.tsx b/frontend/editor/src/core/components/tools/RightSidebar.tsx index d705709af0..9991199a84 100644 --- a/frontend/editor/src/core/components/tools/RightSidebar.tsx +++ b/frontend/editor/src/core/components/tools/RightSidebar.tsx @@ -13,6 +13,7 @@ import { PoliciesSection, PolicyDetailTakeover, usePoliciesEnabled, + usePoliciesVisible, usePolicyDetailActive, } from "@app/components/policies/PoliciesSidebar"; import { PolicyAutoRunController } from "@app/components/policies/PolicyAutoRunController"; @@ -20,6 +21,7 @@ import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems"; import { useToolSections } from "@app/hooks/useToolSections"; import type { SubcategoryGroup } from "@app/hooks/useToolSections"; import { ToolIcon } from "@app/components/shared/ToolIcon"; +import { PanelHeaderPill } from "@app/components/shared/PanelHeaderPill"; import { Tooltip as AppTooltip } from "@app/components/shared/Tooltip"; import { withViewTransition } from "@app/utils/viewTransition"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; @@ -67,6 +69,7 @@ export default function RightSidebar() { } = useToolWorkflow(); const policiesEnabled = usePoliciesEnabled(); + const policiesVisible = usePoliciesVisible(); const rawPolicyDetailActive = usePolicyDetailActive(); const fullscreenExpanded = useIsFullscreenExpanded(); const fullscreenGeometry = useToolPanelGeometry({ @@ -118,9 +121,14 @@ export default function RightSidebar() { const inToolView = leftPanelView !== "toolPicker"; // Show X (close) button only when there's somewhere to go back to. const showCloseButton = inToolView || allToolsView; - // Policies sit above the tool list in the default tool-picker view. + // Policies sit above the tool list in the default tool-picker view — but only + // when the current user actually has policies to see (see usePoliciesVisible), + // so regular users with none get the plain tool picker with no empty block. const showPolicies = - policiesEnabled && !allToolsView && leftPanelView === "toolPicker"; + policiesEnabled && + policiesVisible && + !allToolsView && + leftPanelView === "toolPicker"; // When Policies are shown, the search moves OUT of the header to sit between // the Policies and Tools sections (separating them); otherwise it stays in the // header. Show the header search when there's a close button, or in the @@ -284,65 +292,67 @@ export default function RightSidebar() { ) : ( <> - {!showPolicies && ( -
- {activeTool ? ( -
- - - - - {activeTool.name} - -
- ) : showHeaderSearch ? ( -
- -
- ) : null} - {showCloseButton ? ( - - - - ) : ( - - - - )} -
- )} + } + title={activeTool.name} + onClose={handleHeaderBack} + closeLabel={ + inToolView + ? t("toolPanel.backToAllTools", "Back to all tools") + : t("toolPanel.goBack", "Go back") + } + /> + ) : ( +
+ {showHeaderSearch ? ( +
+ +
+ ) : null} + {showCloseButton ? ( + + + + ) : ( + + + + )} +
+ ))} {showPolicies && ( -
- - - - - - } - onClick={clearChat} - disabled={messages.length === 0 && !isLoading} - > - {t("chat.header.clearChat", "Clear chat")} - - - - - - -
+ } + title={t("agents.stirling_name", "Stirling")} + loading={isLoading} + className="chat-panel__header" + pillClassName="chat-panel__agent-pill-vt" + menuLabel={t("chat.header.agentMenu", "Stirling agent options")} + menuItems={[ + { + key: "clear-chat", + icon: , + label: t("chat.header.clearChat", "Clear chat"), + onClick: clearChat, + disabled: messages.length === 0 && !isLoading, + }, + ]} + onClose={onBack} + closeLabel={backLabel} + /> {showQuickActions && (
diff --git a/frontend/editor/src/proprietary/components/policies/Policies.css b/frontend/editor/src/proprietary/components/policies/Policies.css index 637e30d380..5a2d1f93a2 100644 --- a/frontend/editor/src/proprietary/components/policies/Policies.css +++ b/frontend/editor/src/proprietary/components/policies/Policies.css @@ -8,6 +8,27 @@ /* scaffolding + the collapsed rail; spacing snaps to the SUI --space-* */ /* scale and colour to the SUI token set so it reads as one product. */ +/* Dark mode only: remap SUI surface/border tokens to the app's neutral-grey values so policy cards read as one product with the rail; accent tokens are left alone. */ +[data-theme="dark"] .pol-list, +[data-theme="dark"] .pol-takeover, +[data-theme="dark"] .pol-detail, +[data-theme="dark"] .pol-crail { + --color-bg: var(--bg-toolbar); + --color-bg-alt: var(--bg-toolbar); + --color-bg-subtle: var(--bg-toolbar); + --color-surface: var(--bg-surface); + --color-surface-alt: #323942; + --color-bg-hover: #323942; + --color-bg-muted: var(--bg-surface); + --color-border: var(--border-default); + --color-border-light: var(--border-subtle); + --color-border-input: var(--border-strong); + --color-border-hover: var(--border-strong); + --color-divider: var(--border-subtle); + --color-dropdown-bg: var(--bg-surface); + --color-dropdown-border: var(--border-default); +} + /* ---- List ---- */ .pol-list { width: 100%; @@ -126,20 +147,28 @@ text-decoration: underline; } -/* Locked "Coming soon" row — muted, not interactive. */ +/* Enterprise-only ("coming soon") row — shown to admins / team leaders but not + available on the current plan, so the whole box is dimmed to read as disabled. + The row itself isn't a button; its trailing "Upgrade to enterprise" link is. */ .pol-row--soon { - opacity: 0.55; cursor: default; + opacity: 0.55; } .pol-row--soon:hover { background: transparent; } -.pol-row-soon { +/* Trailing "Upgrade to enterprise" link → contact us. Greyed to match the + disabled row; still clickable for admins who want to enquire. */ +.pol-row-upgrade { font-size: 0.6875rem; - font-weight: 500; + font-weight: 600; color: var(--color-text-4); + text-decoration: none; white-space: nowrap; } +.pol-row-upgrade:hover { + text-decoration: underline; +} /* In-progress activity icon spins gently. */ .pol-spin { @@ -192,9 +221,17 @@ /* ---- Step indicator (wraps a SUI StepIndicator) ---- */ .pol-steps { - padding: var(--space-3) var(--space-5); + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0 var(--space-5) var(--space-3); border-bottom: 1px solid var(--color-border); } +.pol-step-label { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-4); +} /* ---- Scroll body ---- */ .pol-scroll { @@ -211,6 +248,7 @@ line-height: 1.5; color: var(--color-text-4); margin: 0; + margin-bottom: var(--space-3); } .pol-section-label { font-size: 0.6875rem; diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx index 55e0231dbc..48606be6d8 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx @@ -165,11 +165,11 @@ describe("Policies right-sidebar surface", () => { expect(await screen.findByText("No activity yet")).toBeInTheDocument(); }); - it("returns to the list via the back button", () => { + it("returns to the list via the close button", () => { renderHost(); fireEvent.click(screen.getByText("Security")); expect(screen.getByText("Enforces")).toBeInTheDocument(); - fireEvent.click(screen.getByLabelText("Back")); + fireEvent.click(screen.getByLabelText("Close")); expect(screen.getByText("Policies")).toBeInTheDocument(); }); }); diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx index 8b6e551cde..9762701b00 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx @@ -61,6 +61,15 @@ export function usePoliciesEnabled(): boolean { return POLICIES_ENABLED; } +/** Admins always see the list; others only see it when they have configured policies, hiding the section entirely when none are set up. */ +export function usePoliciesVisible(): boolean { + const pol = usePolicies(); + const { categories } = usePolicyCatalog(); + if (!POLICIES_ENABLED) return false; + if (pol.canConfigure) return true; + return categories.some((c) => pol.policies[c.id]?.configured); +} + /** * Whether the current user is a guest who can't open or configure policies — * an anonymous user on a login-enabled deployment (i.e. a SaaS sign-up prompt @@ -122,6 +131,12 @@ export function PoliciesSection({ if (!POLICIES_ENABLED) return null; + // Admins see the full catalogue; others see only configured policies — section disappears when none are configured. + const visibleCategories = pol.canConfigure + ? categories + : categories.filter((c) => pol.policies[c.id]?.configured); + if (visibleCategories.length === 0) return null; + // The header tally counts every CONFIGURED policy (active + paused), not just // the active ones. const configuredCount = categories.filter( @@ -170,14 +185,10 @@ export function PoliciesSection({ {expanded && ( <>
- {categories.map((cat) => { + {visibleCategories.map((cat) => { if (cat.comingSoon) { return ( -
+
{cat.icon} @@ -185,12 +196,17 @@ export function PoliciesSection({ {t(`policies.catalog.${cat.id}`, cat.label)} - + {t( "policies.sidebar.upgradeToEnterprise", "Upgrade to enterprise", )} - +
); @@ -457,58 +473,64 @@ export function PoliciesCollapsedButton({ if (!POLICIES_ENABLED) return null; + // Coming-soon policies are excluded; admins see all real policies, others only see configured ones — renders nothing when empty. + const railCategories = categories.filter((cat) => { + if (cat.comingSoon) return false; + if (pol.canConfigure) return true; + return pol.policies[cat.id]?.configured; + }); + if (railCategories.length === 0) return null; + return ( <>
- {categories - .filter((cat) => !cat.comingSoon) - .map((cat) => { - const status = deriveRowStatus(pol.policies[cat.id]); - const label = t(`policies.catalog.${cat.id}`, cat.label); - const statusLabel = t( - `policies.status.${status}`, - STATUS_LABEL[status], - ); - const suffix = - status === "active" - ? t("policies.sidebar.railSuffixActive", " (Active)") - : status === "paused" - ? t("policies.sidebar.railSuffixPaused", " (Paused)") - : ""; - return ( - { + const status = deriveRowStatus(pol.policies[cat.id]); + const label = t(`policies.catalog.${cat.id}`, cat.label); + const statusLabel = t( + `policies.status.${status}`, + STATUS_LABEL[status], + ); + const suffix = + status === "active" + ? t("policies.sidebar.railSuffixActive", " (Active)") + : status === "paused" + ? t("policies.sidebar.railSuffixPaused", " (Paused)") + : ""; + return ( + + - - ); - })} + {cat.icon} + {(status === "active" || status === "paused") && ( + + )} + + + ); + })}
diff --git a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx index 114f1ac16b..01879fea0e 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx @@ -9,8 +9,7 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import AutorenewIcon from "@mui/icons-material/Autorenew"; import LockIcon from "@mui/icons-material/Lock"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined"; -import { PanelHeader } from "@shared/components/PanelHeader"; -import { ROW_ACCENT } from "@app/components/policies/policyStatus"; +import { PanelHeaderPill } from "@app/components/shared/PanelHeaderPill"; import { Card } from "@shared/components/Card"; import { ChipFlow } from "@shared/components/ChipFlow"; import { StatusBadge } from "@shared/components/StatusBadge"; @@ -130,11 +129,11 @@ export function PolicyDetailPanel({ }; return (
- -
- } - /> - } + onClose={onCancel} + closeLabel={t("cancel", "Cancel")} />
+ + {t("policies.wizard.stepOf", "Step {{step}} of {{total}}", { + step, + total: TOTAL_STEPS, + })} +
diff --git a/frontend/editor/src/proprietary/components/policies/policyStatus.ts b/frontend/editor/src/proprietary/components/policies/policyStatus.ts index d3c3d8ea7b..9e014e87e3 100644 --- a/frontend/editor/src/proprietary/components/policies/policyStatus.ts +++ b/frontend/editor/src/proprietary/components/policies/policyStatus.ts @@ -17,11 +17,10 @@ export const STATUS_LABEL: Record = { setup: "Set up", }; -/** A soft tinted icon tile per category — gives each policy a calm identity colour. */ export const ROW_ACCENT: Record = { ingestion: "blue", - security: "purple", - compliance: "green", - routing: "amber", - retention: "red", + security: "blue", + compliance: "blue", + routing: "blue", + retention: "blue", }; From d2001d3e5e9c6785ccc8ff1d67804aead267895b Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Jun 2026 16:20:52 +0100 Subject: [PATCH 2/9] PanelHeaderPill -> PanelHeader --- .../{PanelHeaderPill.css => PanelHeader.css} | 54 +++++++------- .../{PanelHeaderPill.tsx => PanelHeader.tsx} | 74 +++++++++---------- .../core/components/tools/RightSidebar.tsx | 4 +- .../proprietary/components/chat/ChatPanel.tsx | 6 +- .../components/policies/PolicyDetailPanel.tsx | 4 +- .../components/policies/PolicySetupWizard.tsx | 6 +- 6 files changed, 73 insertions(+), 75 deletions(-) rename frontend/editor/src/core/components/shared/{PanelHeaderPill.css => PanelHeader.css} (68%) rename frontend/editor/src/core/components/shared/{PanelHeaderPill.tsx => PanelHeader.tsx} (56%) diff --git a/frontend/editor/src/core/components/shared/PanelHeaderPill.css b/frontend/editor/src/core/components/shared/PanelHeader.css similarity index 68% rename from frontend/editor/src/core/components/shared/PanelHeaderPill.css rename to frontend/editor/src/core/components/shared/PanelHeader.css index d9257653db..786176981a 100644 --- a/frontend/editor/src/core/components/shared/PanelHeaderPill.css +++ b/frontend/editor/src/core/components/shared/PanelHeader.css @@ -1,9 +1,9 @@ -/* ===================== PanelHeaderPill ===================== */ -/* The rounded pill header shared by the active-tool panel, the AI chat panel */ -/* and the Policies detail/wizard. Mirrors the AI chat header treatment so the */ -/* pill reads well in both light and dark mode (thin border, no heavy fill). */ +/* ===================== PanelHeader ===================== */ +/* The header shared by the active-tool panel, the AI chat panel and the */ +/* Policies detail/wizard. Mirrors the AI chat header treatment so it reads well */ +/* in both light and dark mode (thin border, no heavy fill). */ -.sui-pillhdr { +.sui-panelhdr { display: flex; align-items: center; gap: 0.5rem; @@ -11,7 +11,7 @@ flex-shrink: 0; } -.sui-pillhdr__pill { +.sui-panelhdr__bar { display: inline-flex; align-items: center; gap: 0.5rem; @@ -26,18 +26,18 @@ } /* Only the menu-trigger variant is interactive. */ -button.sui-pillhdr__pill { +button.sui-panelhdr__bar { cursor: pointer; transition: background 120ms ease-out, border-color 120ms ease-out; } -button.sui-pillhdr__pill:hover { +button.sui-panelhdr__bar:hover { background: var(--mantine-color-default-hover); } -.sui-pillhdr__icon { +.sui-panelhdr__icon { position: relative; display: inline-flex; align-items: center; @@ -50,7 +50,7 @@ button.sui-pillhdr__pill:hover { flex-shrink: 0; } -.sui-pillhdr__icon svg { +.sui-panelhdr__icon svg { font-size: 1rem; width: 1rem; height: 1rem; @@ -58,7 +58,7 @@ button.sui-pillhdr__pill:hover { /* ToolIcon wraps its glyph in .tool-button-icon with its own margin/transform; reset them so the glyph sits dead-centre in the circular badge. */ -.sui-pillhdr__icon .tool-button-icon { +.sui-panelhdr__icon .tool-button-icon { margin: 0 !important; transform: none !important; display: inline-flex; @@ -67,7 +67,7 @@ button.sui-pillhdr__pill:hover { line-height: 1; } -.sui-pillhdr__label { +.sui-panelhdr__label { flex: 1; min-width: 0; font-size: 0.9rem; @@ -78,20 +78,20 @@ button.sui-pillhdr__pill:hover { color: var(--text-primary); } -.sui-pillhdr__chevron { +.sui-panelhdr__chevron { flex-shrink: 0; color: var(--text-muted); } -.sui-pillhdr__trail { +.sui-panelhdr__trail { display: inline-flex; align-items: center; gap: 0.5rem; flex-shrink: 0; } -/* Running / in-progress status dot, anchored to the pill icon. */ -.sui-pillhdr__dot { +/* Running / in-progress status dot, anchored to the header icon. */ +.sui-panelhdr__dot { position: absolute; bottom: 0; right: 0; @@ -100,11 +100,11 @@ button.sui-pillhdr__pill:hover { border-radius: 50%; background: var(--mantine-color-blue-5); border: 1.5px solid var(--mantine-color-body); - animation: sui-pillhdr-dot-pulse 2.4s ease-in-out infinite; + animation: sui-panelhdr-dot-pulse 2.4s ease-in-out infinite; pointer-events: none; } -@keyframes sui-pillhdr-dot-pulse { +@keyframes sui-panelhdr-dot-pulse { 0%, 100% { opacity: 1; @@ -115,12 +115,12 @@ button.sui-pillhdr__pill:hover { } @media (prefers-reduced-motion: reduce) { - .sui-pillhdr__dot { + .sui-panelhdr__dot { animation: none; } } -.sui-pillhdr__pill--loading { +.sui-panelhdr__bar--loading { border-color: color-mix( in srgb, var(--mantine-color-blue-5) 60%, @@ -128,18 +128,18 @@ button.sui-pillhdr__pill:hover { ); } -/* Dark mode: let the pill blend into the rail — just a thin border, no fill — +/* Dark mode: let the header blend into the rail — just a thin border, no fill — so it doesn't read as a clashing lighter card on the dark toolbar. */ -[data-mantine-color-scheme="dark"] .sui-pillhdr__pill { +[data-mantine-color-scheme="dark"] .sui-panelhdr__bar { background: transparent; border-color: var(--border-subtle); } -[data-mantine-color-scheme="dark"] button.sui-pillhdr__pill:hover { +[data-mantine-color-scheme="dark"] button.sui-panelhdr__bar:hover { background: rgba(255, 255, 255, 0.04); } -[data-mantine-color-scheme="dark"] .sui-pillhdr__pill--loading { +[data-mantine-color-scheme="dark"] .sui-panelhdr__bar--loading { border-color: color-mix( in srgb, var(--mantine-color-blue-4) 55%, @@ -147,7 +147,7 @@ button.sui-pillhdr__pill:hover { ); } -[data-mantine-color-scheme="dark"] .sui-pillhdr__icon { +[data-mantine-color-scheme="dark"] .sui-panelhdr__icon { background: color-mix( in srgb, var(--mantine-color-blue-filled) 18%, @@ -158,10 +158,10 @@ button.sui-pillhdr__pill:hover { /* Dark mode: the subtle gray close button is too dim against the dark rail — brighten it to a clearly-visible light grey (near-white on hover). */ -[data-mantine-color-scheme="dark"] .sui-pillhdr__close { +[data-mantine-color-scheme="dark"] .sui-panelhdr__close { color: var(--mantine-color-gray-4); } -[data-mantine-color-scheme="dark"] .sui-pillhdr__close:hover { +[data-mantine-color-scheme="dark"] .sui-panelhdr__close:hover { color: var(--mantine-color-gray-2); } diff --git a/frontend/editor/src/core/components/shared/PanelHeaderPill.tsx b/frontend/editor/src/core/components/shared/PanelHeader.tsx similarity index 56% rename from frontend/editor/src/core/components/shared/PanelHeaderPill.tsx rename to frontend/editor/src/core/components/shared/PanelHeader.tsx index 817968a567..c6bdd13ed1 100644 --- a/frontend/editor/src/core/components/shared/PanelHeaderPill.tsx +++ b/frontend/editor/src/core/components/shared/PanelHeader.tsx @@ -2,9 +2,9 @@ import type { ReactNode } from "react"; import { ActionIcon, Menu } from "@mantine/core"; import CloseIcon from "@mui/icons-material/Close"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import "@app/components/shared/PanelHeaderPill.css"; +import "@app/components/shared/PanelHeader.css"; -export interface PanelHeaderPillMenuItem { +export interface PanelHeaderMenuItem { /** Stable key; falls back to the item index. */ key?: string; /** Optional leading glyph. */ @@ -14,40 +14,40 @@ export interface PanelHeaderPillMenuItem { disabled?: boolean; } -export interface PanelHeaderPillProps { - /** Glyph rendered in the tinted circular badge at the pill's leading edge. */ +export interface PanelHeaderProps { + /** Glyph rendered in the tinted circular badge at the header's leading edge. */ icon: ReactNode; - /** Pill label. */ + /** Header title. */ title: ReactNode; /** Close (X) handler. The trailing close button renders only when supplied. */ onClose?: () => void; /** aria-label for the close button. */ closeLabel?: string; /** - * When provided, the pill becomes a dropdown trigger: a disclosure chevron is - * shown and clicking the pill opens a menu of these items (e.g. "Clear chat"). + * When provided, the header becomes a dropdown trigger: a disclosure chevron is + * shown and clicking it opens a menu of these items (e.g. "Clear chat"). */ - menuItems?: PanelHeaderPillMenuItem[]; - /** aria-label for the pill when it acts as a menu trigger. */ + menuItems?: PanelHeaderMenuItem[]; + /** aria-label for the header when it acts as a menu trigger. */ menuLabel?: string; /** Shows a pulsing status dot on the icon + a tinted border (e.g. AI running). */ loading?: boolean; /** Right-aligned content rendered before the close button (e.g. a status badge). */ actions?: ReactNode; - /** Applied to the pill element itself — e.g. to set a view-transition-name. */ - pillClassName?: string; + /** Applied to the inner header element — e.g. to set a view-transition-name. */ + barClassName?: string; /** Applied to the outer header container. */ className?: string; } /** - * The rounded "pill" header shared by the rail surfaces — the active tool panel, - * the AI chat panel, and the Policies detail/wizard. A tinted icon badge + title - * sit in a pill, with an optional dropdown menu and a trailing close button. The - * pill styling mirrors the AI chat header so it stays legible in dark mode (thin - * border, no heavy fill) across every surface. + * The header shared by the rail surfaces — the active tool panel, the AI chat + * panel, and the Policies detail/wizard. A tinted icon badge + title sit in a + * rounded bar, with an optional dropdown menu and a trailing close button. The + * styling stays legible in dark mode (thin border, no heavy fill) across every + * surface. */ -export function PanelHeaderPill({ +export function PanelHeader({ icon, title, onClose, @@ -56,29 +56,29 @@ export function PanelHeaderPill({ menuLabel, loading = false, actions, - pillClassName, + barClassName, className, -}: PanelHeaderPillProps) { +}: PanelHeaderProps) { const hasMenu = menuItems != null && menuItems.length > 0; - const pillClasses = [ - "sui-pillhdr__pill", - loading ? "sui-pillhdr__pill--loading" : "", - pillClassName ?? "", + const barClasses = [ + "sui-panelhdr__bar", + loading ? "sui-panelhdr__bar--loading" : "", + barClassName ?? "", ] .filter(Boolean) .join(" "); - const pillBody = ( + const barBody = ( <> - + {icon} - {loading && } + {loading && } - {title} + {title} {hasMenu && ( )} @@ -86,16 +86,14 @@ export function PanelHeaderPill({ ); return ( -
+
{hasMenu ? ( - @@ -112,15 +110,15 @@ export function PanelHeaderPill({ ) : ( -
{pillBody}
+
{barBody}
)} {(actions != null || onClose != null) && ( -
+
{actions} {onClose && ( {!showPolicies && (activeTool ? ( - - } title={t("agents.stirling_name", "Stirling")} loading={isLoading} className="chat-panel__header" - pillClassName="chat-panel__agent-pill-vt" + barClassName="chat-panel__agent-pill-vt" menuLabel={t("chat.header.agentMenu", "Stirling agent options")} menuItems={[ { diff --git a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx index 01879fea0e..751488a6c7 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx @@ -9,7 +9,7 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import AutorenewIcon from "@mui/icons-material/Autorenew"; import LockIcon from "@mui/icons-material/Lock"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined"; -import { PanelHeaderPill } from "@app/components/shared/PanelHeaderPill"; +import { PanelHeader } from "@app/components/shared/PanelHeader"; import { Card } from "@shared/components/Card"; import { ChipFlow } from "@shared/components/ChipFlow"; import { StatusBadge } from "@shared/components/StatusBadge"; @@ -129,7 +129,7 @@ export function PolicyDetailPanel({ }; return (
- - - Date: Tue, 16 Jun 2026 17:06:57 +0100 Subject: [PATCH 3/9] colors --- .../components/policies/Policies.css | 13 ++++++++++++ .../components/policies/policyStatus.ts | 13 ++++++++---- frontend/shared/components/IconBadge.css | 21 ++++++++++--------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/frontend/editor/src/proprietary/components/policies/Policies.css b/frontend/editor/src/proprietary/components/policies/Policies.css index 5a2d1f93a2..07fcb1865f 100644 --- a/frontend/editor/src/proprietary/components/policies/Policies.css +++ b/frontend/editor/src/proprietary/components/policies/Policies.css @@ -90,6 +90,19 @@ outline: 2px solid var(--color-blue); outline-offset: -2px; } +/* Policy icons are colourless at rest; hovering or focusing the row reveals the + category colour (blue/purple/green/amber/red — see ROW_ACCENT). The accent + class sets --ib-base; we neutralise --ib-accent here and restore it on hover. */ +.pol-row .sui-iconbadge { + --ib-accent: var(--color-text-3); + transition: + color var(--motion-fast), + background var(--motion-fast); +} +.pol-row:hover .sui-iconbadge, +.pol-row:focus-visible .sui-iconbadge { + --ib-accent: var(--ib-base); +} .pol-row-label { flex: 1; min-width: 0; diff --git a/frontend/editor/src/proprietary/components/policies/policyStatus.ts b/frontend/editor/src/proprietary/components/policies/policyStatus.ts index 9e014e87e3..c9f2b000fc 100644 --- a/frontend/editor/src/proprietary/components/policies/policyStatus.ts +++ b/frontend/editor/src/proprietary/components/policies/policyStatus.ts @@ -17,10 +17,15 @@ export const STATUS_LABEL: Record = { setup: "Set up", }; +/** + * Per-category accent colour. The single source of truth shared by: the list + * icons (which render colourless at rest and reveal this colour on hover/focus), + * and a file's post-run glow + shield badge (via usePolicyFileBadges' ACCENT_VAR). + */ export const ROW_ACCENT: Record = { ingestion: "blue", - security: "blue", - compliance: "blue", - routing: "blue", - retention: "blue", + security: "purple", + compliance: "green", + routing: "amber", + retention: "red", }; diff --git a/frontend/shared/components/IconBadge.css b/frontend/shared/components/IconBadge.css index 2a510b282f..10e2d28915 100644 --- a/frontend/shared/components/IconBadge.css +++ b/frontend/shared/components/IconBadge.css @@ -4,6 +4,12 @@ justify-content: center; border-radius: var(--radius-md); flex-shrink: 0; + /* Resolved tint. Each accent class sets --ib-base; --ib-accent defaults to it + but a consumer can override --ib-accent alone (e.g. to neutralise the badge + until hover) without losing the per-accent base. */ + --ib-accent: var(--ib-base, var(--color-blue)); + color: var(--ib-accent); + background: color-mix(in srgb, var(--ib-accent) 14%, transparent); } .sui-iconbadge--sm { width: 1.75rem; @@ -14,22 +20,17 @@ height: 2rem; } .sui-iconbadge--blue { - color: var(--color-blue); - background: color-mix(in srgb, var(--color-blue) 14%, transparent); + --ib-base: var(--color-blue); } .sui-iconbadge--purple { - color: var(--color-purple); - background: color-mix(in srgb, var(--color-purple) 14%, transparent); + --ib-base: var(--color-purple); } .sui-iconbadge--green { - color: var(--color-green); - background: color-mix(in srgb, var(--color-green) 14%, transparent); + --ib-base: var(--color-green); } .sui-iconbadge--amber { - color: var(--color-amber); - background: color-mix(in srgb, var(--color-amber) 14%, transparent); + --ib-base: var(--color-amber); } .sui-iconbadge--red { - color: var(--color-red); - background: color-mix(in srgb, var(--color-red) 14%, transparent); + --ib-base: var(--color-red); } From cf8eb25bc094bc40b112d4d7ae5c05324e5bfd3e Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Tue, 16 Jun 2026 18:07:39 +0100 Subject: [PATCH 4/9] CI fix --- .../core/components/shared/PanelHeader.tsx | 21 +++++++++++++++++-- .../components/policies/PolicyDetailPanel.tsx | 2 ++ .../components/policies/PolicySetupWizard.tsx | 3 +++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/frontend/editor/src/core/components/shared/PanelHeader.tsx b/frontend/editor/src/core/components/shared/PanelHeader.tsx index c6bdd13ed1..fc024ac8fe 100644 --- a/frontend/editor/src/core/components/shared/PanelHeader.tsx +++ b/frontend/editor/src/core/components/shared/PanelHeader.tsx @@ -1,7 +1,8 @@ -import type { ReactNode } from "react"; +import type { CSSProperties, ReactNode } from "react"; import { ActionIcon, Menu } from "@mantine/core"; import CloseIcon from "@mui/icons-material/Close"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import type { IconBadgeAccent } from "@shared/components/IconBadge"; import "@app/components/shared/PanelHeader.css"; export interface PanelHeaderMenuItem { @@ -30,6 +31,11 @@ export interface PanelHeaderProps { menuItems?: PanelHeaderMenuItem[]; /** aria-label for the header when it acts as a menu trigger. */ menuLabel?: string; + /** + * Tints the icon badge with a category colour (blue/purple/green/amber/red). + * Defaults to the standard blue when omitted (tool + AI chat headers). + */ + accent?: IconBadgeAccent; /** Shows a pulsing status dot on the icon + a tinted border (e.g. AI running). */ loading?: boolean; /** Right-aligned content rendered before the close button (e.g. a status badge). */ @@ -54,6 +60,7 @@ export function PanelHeader({ closeLabel, menuItems, menuLabel, + accent, loading = false, actions, barClassName, @@ -61,6 +68,16 @@ export function PanelHeader({ }: PanelHeaderProps) { const hasMenu = menuItems != null && menuItems.length > 0; + // Tint the icon badge with the category colour when an accent is given. Inline + // so it wins over the default blue treatment in both light and dark mode; the + // --color-* tokens are theme-aware and match the badge tint used elsewhere. + const iconStyle: CSSProperties | undefined = accent + ? { + color: `var(--color-${accent})`, + background: `color-mix(in srgb, var(--color-${accent}) 14%, transparent)`, + } + : undefined; + const barClasses = [ "sui-panelhdr__bar", loading ? "sui-panelhdr__bar--loading" : "", @@ -71,7 +88,7 @@ export function PanelHeader({ const barBody = ( <> - + {icon} {loading && } diff --git a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx index 751488a6c7..5ac0ea82cb 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx @@ -10,6 +10,7 @@ import AutorenewIcon from "@mui/icons-material/Autorenew"; import LockIcon from "@mui/icons-material/Lock"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined"; import { PanelHeader } from "@app/components/shared/PanelHeader"; +import { ROW_ACCENT } from "@app/components/policies/policyStatus"; import { Card } from "@shared/components/Card"; import { ChipFlow } from "@shared/components/ChipFlow"; import { StatusBadge } from "@shared/components/StatusBadge"; @@ -131,6 +132,7 @@ export function PolicyDetailPanel({
Date: Tue, 16 Jun 2026 18:59:43 +0100 Subject: [PATCH 5/9] Policies: keep live policies visible to non-admins, hide only coming-soon rows Non-admins (login on) must still see and open live policies like Security (into the read-only locked state) per policy-admin-gate.spec.ts. The list was filtering out every unconfigured policy and hiding the whole section, which removed Security too. Filter on comingSoon instead so regular users see the live policies and only the enterprise 'Upgrade to enterprise' rows are hidden. --- .../components/policies/PoliciesSidebar.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx index 9762701b00..45d6d37510 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx @@ -61,13 +61,18 @@ export function usePoliciesEnabled(): boolean { return POLICIES_ENABLED; } -/** Admins always see the list; others only see it when they have configured policies, hiding the section entirely when none are set up. */ +/** + * Whether the right rail should show the Policies section. Everyone sees it when + * the feature is on: admins / team leads get the full catalogue (coming-soon rows + * greyed), regular users get the live policies only. It only disappears if there + * are no rows to show for the current user — which can't happen while a live + * (non-coming-soon) policy like Security exists, but is handled defensively. + */ export function usePoliciesVisible(): boolean { const pol = usePolicies(); const { categories } = usePolicyCatalog(); if (!POLICIES_ENABLED) return false; - if (pol.canConfigure) return true; - return categories.some((c) => pol.policies[c.id]?.configured); + return pol.canConfigure || categories.some((c) => !c.comingSoon); } /** @@ -131,10 +136,12 @@ export function PoliciesSection({ if (!POLICIES_ENABLED) return null; - // Admins see the full catalogue; others see only configured policies — section disappears when none are configured. + // Admins / team leads see the full catalogue (coming-soon rows greyed as an + // enterprise upsell); regular users only see the live policies — the + // coming-soon "Upgrade to enterprise" rows are hidden from them. const visibleCategories = pol.canConfigure ? categories - : categories.filter((c) => pol.policies[c.id]?.configured); + : categories.filter((c) => !c.comingSoon); if (visibleCategories.length === 0) return null; // The header tally counts every CONFIGURED policy (active + paused), not just From 743dd1dc9c71c0229eb181b7d16b88796d380487 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 11:59:00 +0100 Subject: [PATCH 6/9] Move PanelHeader into the shared design-system so the portal can share it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PanelHeader was editor-only (core/components/shared); the portal couldn't use it. The shared lib and the portal both use Mantine, so there's no barrier — relocate it to frontend/shared/components, re-theming its three editor-only CSS tokens (--border-subtle/--text-primary/--text-muted) to Mantine equivalents so it renders in the portal too. Replaces the dead old shared PanelHeader (no importers) and repoints the editor usages to @shared/components/PanelHeader. --- .../core/components/shared/PanelHeader.css | 167 ---------------- .../core/components/shared/PanelHeader.tsx | 153 --------------- .../core/components/tools/RightSidebar.tsx | 2 +- .../proprietary/components/chat/ChatPanel.tsx | 2 +- .../components/policies/PolicyDetailPanel.tsx | 2 +- .../components/policies/PolicySetupWizard.tsx | 2 +- frontend/shared/components/PanelHeader.css | 179 ++++++++++++++---- .../shared/components/PanelHeader.stories.tsx | 71 +++---- frontend/shared/components/PanelHeader.tsx | 178 ++++++++++++----- 9 files changed, 315 insertions(+), 441 deletions(-) delete mode 100644 frontend/editor/src/core/components/shared/PanelHeader.css delete mode 100644 frontend/editor/src/core/components/shared/PanelHeader.tsx diff --git a/frontend/editor/src/core/components/shared/PanelHeader.css b/frontend/editor/src/core/components/shared/PanelHeader.css deleted file mode 100644 index 786176981a..0000000000 --- a/frontend/editor/src/core/components/shared/PanelHeader.css +++ /dev/null @@ -1,167 +0,0 @@ -/* ===================== PanelHeader ===================== */ -/* The header shared by the active-tool panel, the AI chat panel and the */ -/* Policies detail/wizard. Mirrors the AI chat header treatment so it reads well */ -/* in both light and dark mode (thin border, no heavy fill). */ - -.sui-panelhdr { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 1rem 1rem 0.75rem; - flex-shrink: 0; -} - -.sui-panelhdr__bar { - display: inline-flex; - align-items: center; - gap: 0.5rem; - flex: 1; - min-width: 0; - padding: 0.4rem 0.75rem 0.4rem 0.4rem; - border: 1px solid var(--border-subtle); - border-radius: 9999px; - background: var(--mantine-color-body); - text-align: left; - color: inherit; -} - -/* Only the menu-trigger variant is interactive. */ -button.sui-panelhdr__bar { - cursor: pointer; - transition: - background 120ms ease-out, - border-color 120ms ease-out; -} - -button.sui-panelhdr__bar:hover { - background: var(--mantine-color-default-hover); -} - -.sui-panelhdr__icon { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.75rem; - height: 1.75rem; - border-radius: 9999px; - background: var(--mantine-color-blue-light); - color: var(--mantine-color-blue-filled); - flex-shrink: 0; -} - -.sui-panelhdr__icon svg { - font-size: 1rem; - width: 1rem; - height: 1rem; -} - -/* ToolIcon wraps its glyph in .tool-button-icon with its own margin/transform; - reset them so the glyph sits dead-centre in the circular badge. */ -.sui-panelhdr__icon .tool-button-icon { - margin: 0 !important; - transform: none !important; - display: inline-flex; - align-items: center; - justify-content: center; - line-height: 1; -} - -.sui-panelhdr__label { - flex: 1; - min-width: 0; - font-size: 0.9rem; - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: var(--text-primary); -} - -.sui-panelhdr__chevron { - flex-shrink: 0; - color: var(--text-muted); -} - -.sui-panelhdr__trail { - display: inline-flex; - align-items: center; - gap: 0.5rem; - flex-shrink: 0; -} - -/* Running / in-progress status dot, anchored to the header icon. */ -.sui-panelhdr__dot { - position: absolute; - bottom: 0; - right: 0; - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--mantine-color-blue-5); - border: 1.5px solid var(--mantine-color-body); - animation: sui-panelhdr-dot-pulse 2.4s ease-in-out infinite; - pointer-events: none; -} - -@keyframes sui-panelhdr-dot-pulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.45; - } -} - -@media (prefers-reduced-motion: reduce) { - .sui-panelhdr__dot { - animation: none; - } -} - -.sui-panelhdr__bar--loading { - border-color: color-mix( - in srgb, - var(--mantine-color-blue-5) 60%, - var(--border-subtle) - ); -} - -/* Dark mode: let the header blend into the rail — just a thin border, no fill — - so it doesn't read as a clashing lighter card on the dark toolbar. */ -[data-mantine-color-scheme="dark"] .sui-panelhdr__bar { - background: transparent; - border-color: var(--border-subtle); -} - -[data-mantine-color-scheme="dark"] button.sui-panelhdr__bar:hover { - background: rgba(255, 255, 255, 0.04); -} - -[data-mantine-color-scheme="dark"] .sui-panelhdr__bar--loading { - border-color: color-mix( - in srgb, - var(--mantine-color-blue-4) 55%, - var(--border-subtle) - ); -} - -[data-mantine-color-scheme="dark"] .sui-panelhdr__icon { - background: color-mix( - in srgb, - var(--mantine-color-blue-filled) 18%, - transparent - ); - color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled)); -} - -/* Dark mode: the subtle gray close button is too dim against the dark rail — - brighten it to a clearly-visible light grey (near-white on hover). */ -[data-mantine-color-scheme="dark"] .sui-panelhdr__close { - color: var(--mantine-color-gray-4); -} - -[data-mantine-color-scheme="dark"] .sui-panelhdr__close:hover { - color: var(--mantine-color-gray-2); -} diff --git a/frontend/editor/src/core/components/shared/PanelHeader.tsx b/frontend/editor/src/core/components/shared/PanelHeader.tsx deleted file mode 100644 index fc024ac8fe..0000000000 --- a/frontend/editor/src/core/components/shared/PanelHeader.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import type { CSSProperties, ReactNode } from "react"; -import { ActionIcon, Menu } from "@mantine/core"; -import CloseIcon from "@mui/icons-material/Close"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import type { IconBadgeAccent } from "@shared/components/IconBadge"; -import "@app/components/shared/PanelHeader.css"; - -export interface PanelHeaderMenuItem { - /** Stable key; falls back to the item index. */ - key?: string; - /** Optional leading glyph. */ - icon?: ReactNode; - label: ReactNode; - onClick: () => void; - disabled?: boolean; -} - -export interface PanelHeaderProps { - /** Glyph rendered in the tinted circular badge at the header's leading edge. */ - icon: ReactNode; - /** Header title. */ - title: ReactNode; - /** Close (X) handler. The trailing close button renders only when supplied. */ - onClose?: () => void; - /** aria-label for the close button. */ - closeLabel?: string; - /** - * When provided, the header becomes a dropdown trigger: a disclosure chevron is - * shown and clicking it opens a menu of these items (e.g. "Clear chat"). - */ - menuItems?: PanelHeaderMenuItem[]; - /** aria-label for the header when it acts as a menu trigger. */ - menuLabel?: string; - /** - * Tints the icon badge with a category colour (blue/purple/green/amber/red). - * Defaults to the standard blue when omitted (tool + AI chat headers). - */ - accent?: IconBadgeAccent; - /** Shows a pulsing status dot on the icon + a tinted border (e.g. AI running). */ - loading?: boolean; - /** Right-aligned content rendered before the close button (e.g. a status badge). */ - actions?: ReactNode; - /** Applied to the inner header element — e.g. to set a view-transition-name. */ - barClassName?: string; - /** Applied to the outer header container. */ - className?: string; -} - -/** - * The header shared by the rail surfaces — the active tool panel, the AI chat - * panel, and the Policies detail/wizard. A tinted icon badge + title sit in a - * rounded bar, with an optional dropdown menu and a trailing close button. The - * styling stays legible in dark mode (thin border, no heavy fill) across every - * surface. - */ -export function PanelHeader({ - icon, - title, - onClose, - closeLabel, - menuItems, - menuLabel, - accent, - loading = false, - actions, - barClassName, - className, -}: PanelHeaderProps) { - const hasMenu = menuItems != null && menuItems.length > 0; - - // Tint the icon badge with the category colour when an accent is given. Inline - // so it wins over the default blue treatment in both light and dark mode; the - // --color-* tokens are theme-aware and match the badge tint used elsewhere. - const iconStyle: CSSProperties | undefined = accent - ? { - color: `var(--color-${accent})`, - background: `color-mix(in srgb, var(--color-${accent}) 14%, transparent)`, - } - : undefined; - - const barClasses = [ - "sui-panelhdr__bar", - loading ? "sui-panelhdr__bar--loading" : "", - barClassName ?? "", - ] - .filter(Boolean) - .join(" "); - - const barBody = ( - <> - - {icon} - {loading && } - - {title} - {hasMenu && ( - - )} - - ); - - return ( -
- {hasMenu ? ( - - - - - - {(menuItems ?? []).map((item, i) => ( - - {item.label} - - ))} - - - ) : ( -
{barBody}
- )} - - {(actions != null || onClose != null) && ( -
- {actions} - {onClose && ( - - - - )} -
- )} -
- ); -} diff --git a/frontend/editor/src/core/components/tools/RightSidebar.tsx b/frontend/editor/src/core/components/tools/RightSidebar.tsx index a32f7ab0f3..17672f521f 100644 --- a/frontend/editor/src/core/components/tools/RightSidebar.tsx +++ b/frontend/editor/src/core/components/tools/RightSidebar.tsx @@ -21,7 +21,7 @@ import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems"; import { useToolSections } from "@app/hooks/useToolSections"; import type { SubcategoryGroup } from "@app/hooks/useToolSections"; import { ToolIcon } from "@app/components/shared/ToolIcon"; -import { PanelHeader } from "@app/components/shared/PanelHeader"; +import { PanelHeader } from "@shared/components/PanelHeader"; import { Tooltip as AppTooltip } from "@app/components/shared/Tooltip"; import { withViewTransition } from "@app/utils/viewTransition"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; diff --git a/frontend/editor/src/proprietary/components/chat/ChatPanel.tsx b/frontend/editor/src/proprietary/components/chat/ChatPanel.tsx index 54a9747eb5..76bde0664e 100644 --- a/frontend/editor/src/proprietary/components/chat/ChatPanel.tsx +++ b/frontend/editor/src/proprietary/components/chat/ChatPanel.tsx @@ -42,7 +42,7 @@ import { formatRelativeTime } from "@app/utils/timeUtils"; import { useTranslatedToolCatalog } from "@app/data/useTranslatedToolRegistry"; import { StirlingLogoAnimated } from "@app/components/agents/StirlingLogoAnimated"; import { StirlingLogoOutline } from "@app/components/agents/StirlingLogoOutline"; -import { PanelHeader } from "@app/components/shared/PanelHeader"; +import { PanelHeader } from "@shared/components/PanelHeader"; import { ChatQuickActions } from "@app/components/chat/ChatQuickActions"; import "@app/components/chat/ChatPanel.css"; diff --git a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx index 5ac0ea82cb..aed4062869 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx @@ -9,7 +9,7 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import AutorenewIcon from "@mui/icons-material/Autorenew"; import LockIcon from "@mui/icons-material/Lock"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutlined"; -import { PanelHeader } from "@app/components/shared/PanelHeader"; +import { PanelHeader } from "@shared/components/PanelHeader"; import { ROW_ACCENT } from "@app/components/policies/policyStatus"; import { Card } from "@shared/components/Card"; import { ChipFlow } from "@shared/components/ChipFlow"; diff --git a/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx b/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx index cb11f47e07..e2548f373f 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx @@ -1,7 +1,7 @@ import { useState, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import { PanelHeader } from "@app/components/shared/PanelHeader"; +import { PanelHeader } from "@shared/components/PanelHeader"; import { ROW_ACCENT } from "@app/components/policies/policyStatus"; import { Card } from "@shared/components/Card"; import { Button } from "@shared/components/Button"; diff --git a/frontend/shared/components/PanelHeader.css b/frontend/shared/components/PanelHeader.css index dabffb27d5..f19808b290 100644 --- a/frontend/shared/components/PanelHeader.css +++ b/frontend/shared/components/PanelHeader.css @@ -1,54 +1,167 @@ +/* ===================== PanelHeader ===================== */ +/* The header shared by the active-tool panel, the AI chat panel and the */ +/* Policies detail/wizard. Mirrors the AI chat header treatment so it reads well */ +/* in both light and dark mode (thin border, no heavy fill). */ + .sui-panelhdr { display: flex; align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem 1.25rem; - border-bottom: 1px solid var(--color-border); + gap: 0.5rem; + padding: 1rem 1rem 0.75rem; + flex-shrink: 0; } -.sui-panelhdr__left { - display: flex; + +.sui-panelhdr__bar { + display: inline-flex; align-items: center; - gap: 0.75rem; + gap: 0.5rem; + flex: 1; min-width: 0; + padding: 0.4rem 0.75rem 0.4rem 0.4rem; + border: 1px solid var(--mantine-color-default-border); + border-radius: 9999px; + background: var(--mantine-color-body); + text-align: left; + color: inherit; +} + +/* Only the menu-trigger variant is interactive. */ +button.sui-panelhdr__bar { + cursor: pointer; + transition: + background 120ms ease-out, + border-color 120ms ease-out; } -.sui-panelhdr__back { + +button.sui-panelhdr__bar:hover { + background: var(--mantine-color-default-hover); +} + +.sui-panelhdr__icon { + position: relative; display: inline-flex; align-items: center; justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 9999px; + background: var(--mantine-color-blue-light); + color: var(--mantine-color-blue-filled); flex-shrink: 0; - width: 1.875rem; - height: 1.875rem; - border: 1px solid var(--color-border); - border-radius: 50%; - background: var(--color-surface); - color: var(--color-text-2); - cursor: pointer; - transition: - background var(--motion-fast), - color var(--motion-fast), - border-color var(--motion-fast); } -.sui-panelhdr__back:hover { - background: var(--color-bg-hover); - border-color: var(--color-text-4); - color: var(--color-text-1); + +.sui-panelhdr__icon svg { + font-size: 1rem; + width: 1rem; + height: 1rem; } -.sui-panelhdr__text { - min-width: 0; + +/* ToolIcon wraps its glyph in .tool-button-icon with its own margin/transform; + reset them so the glyph sits dead-centre in the circular badge. */ +.sui-panelhdr__icon .tool-button-icon { + margin: 0 !important; + transform: none !important; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; } -.sui-panelhdr__title { - font-size: 1.0625rem; + +.sui-panelhdr__label { + flex: 1; + min-width: 0; + font-size: 0.9rem; font-weight: 600; - color: var(--color-text-1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--mantine-color-text); } -.sui-panelhdr__sub { - font-size: 0.8125rem; - color: var(--color-text-4); - margin-top: 0.125rem; + +.sui-panelhdr__chevron { + flex-shrink: 0; + color: var(--mantine-color-dimmed); } -.sui-panelhdr__actions { + +.sui-panelhdr__trail { display: inline-flex; align-items: center; gap: 0.5rem; + flex-shrink: 0; +} + +/* Running / in-progress status dot, anchored to the header icon. */ +.sui-panelhdr__dot { + position: absolute; + bottom: 0; + right: 0; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--mantine-color-blue-5); + border: 1.5px solid var(--mantine-color-body); + animation: sui-panelhdr-dot-pulse 2.4s ease-in-out infinite; + pointer-events: none; +} + +@keyframes sui-panelhdr-dot-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.45; + } +} + +@media (prefers-reduced-motion: reduce) { + .sui-panelhdr__dot { + animation: none; + } +} + +.sui-panelhdr__bar--loading { + border-color: color-mix( + in srgb, + var(--mantine-color-blue-5) 60%, + var(--mantine-color-default-border) + ); +} + +/* Dark mode: let the header blend into the rail — just a thin border, no fill — + so it doesn't read as a clashing lighter card on the dark toolbar. */ +[data-mantine-color-scheme="dark"] .sui-panelhdr__bar { + background: transparent; + border-color: var(--mantine-color-default-border); +} + +[data-mantine-color-scheme="dark"] button.sui-panelhdr__bar:hover { + background: rgba(255, 255, 255, 0.04); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__bar--loading { + border-color: color-mix( + in srgb, + var(--mantine-color-blue-4) 55%, + var(--mantine-color-default-border) + ); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__icon { + background: color-mix( + in srgb, + var(--mantine-color-blue-filled) 18%, + transparent + ); + color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled)); +} + +/* Dark mode: the subtle gray close button is too dim against the dark rail — + brighten it to a clearly-visible light grey (near-white on hover). */ +[data-mantine-color-scheme="dark"] .sui-panelhdr__close { + color: var(--mantine-color-gray-4); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__close:hover { + color: var(--mantine-color-gray-2); } diff --git a/frontend/shared/components/PanelHeader.stories.tsx b/frontend/shared/components/PanelHeader.stories.tsx index 1aa37b9556..02cf26360a 100644 --- a/frontend/shared/components/PanelHeader.stories.tsx +++ b/frontend/shared/components/PanelHeader.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined"; +import DeleteSweepIcon from "@mui/icons-material/DeleteSweep"; import { PanelHeader } from "@shared/components/PanelHeader"; -import { Button } from "@shared/components/Button"; import { StatusBadge } from "@shared/components/StatusBadge"; const meta: Meta = { @@ -9,53 +10,53 @@ const meta: Meta = { tags: ["autodocs"], parameters: { layout: "padded" }, args: { - title: "Pipeline detail", - subtitle: "COI Compliance · us-east-1", + icon: , + title: "Security", + closeLabel: "Close", }, - argTypes: { onBack: { action: "back" } }, + argTypes: { onClose: { action: "close" } }, }; export default meta; type Story = StoryObj; -/** Toggle title / subtitle / onBack / actions in controls. */ +/** Plain header pill with a trailing close button. */ export const Playground: Story = {}; -export const WithActions: Story = { +/** Category-accented icon badge (blue / purple / green / amber / red). */ +export const Accented: Story = { + args: { accent: "purple" }, +}; + +/** Dropdown trigger — a disclosure chevron appears and clicking the pill opens + * the menu (e.g. the chat header's "Clear chat"). */ +export const WithMenu: Story = { args: { - subtitle: "Last deploy 14m ago · golden set 48/48", - actions: ( - <> - - Healthy - - - - - ), + title: "Stirling", + menuLabel: "Stirling agent options", + menuItems: [ + { + key: "clear", + icon: , + label: "Clear chat", + onClick: () => {}, + }, + ], }, }; -export const Everything: Story = { +/** Loading state — pulsing status dot on the icon + a tinted border. */ +export const Loading: Story = { + args: { title: "Stirling", loading: true }, +}; + +/** Right-aligned actions rendered before the close button. */ +export const WithActions: Story = { args: { - title: "Pipeline detail — COI Compliance", - subtitle: "Forked from Compliance Pack · 1,287 docs / 24h", - onBack: () => {}, + accent: "purple", actions: ( - <> - - Healthy - - - - + + Active + ), }, }; diff --git a/frontend/shared/components/PanelHeader.tsx b/frontend/shared/components/PanelHeader.tsx index e65b21f376..83eb8ef37e 100644 --- a/frontend/shared/components/PanelHeader.tsx +++ b/frontend/shared/components/PanelHeader.tsx @@ -1,73 +1,153 @@ -import type { ReactNode } from "react"; +import type { CSSProperties, ReactNode } from "react"; +import { ActionIcon, Menu } from "@mantine/core"; +import CloseIcon from "@mui/icons-material/Close"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import type { IconBadgeAccent } from "@shared/components/IconBadge"; import "@shared/components/PanelHeader.css"; -import { IconBadge } from "@shared/components/IconBadge"; + +export interface PanelHeaderMenuItem { + /** Stable key; falls back to the item index. */ + key?: string; + /** Optional leading glyph. */ + icon?: ReactNode; + label: ReactNode; + onClick: () => void; + disabled?: boolean; +} export interface PanelHeaderProps { + /** Glyph rendered in the tinted circular badge at the header's leading edge. */ + icon: ReactNode; + /** Header title. */ title: ReactNode; - /** Sub-heading below the title. */ - subtitle?: ReactNode; - /** Show a back chevron and trigger this callback when clicked. */ - onBack?: () => void; - /** Optional leading visual (e.g. a category glyph) shown in a tinted box. */ - icon?: ReactNode; - /** Accent tint for the leading icon box. Defaults to blue. */ - iconAccent?: "blue" | "purple" | "green" | "amber" | "red"; - /** Right-aligned action buttons / chips. */ + /** Close (X) handler. The trailing close button renders only when supplied. */ + onClose?: () => void; + /** aria-label for the close button. */ + closeLabel?: string; + /** + * When provided, the header becomes a dropdown trigger: a disclosure chevron is + * shown and clicking it opens a menu of these items (e.g. "Clear chat"). + */ + menuItems?: PanelHeaderMenuItem[]; + /** aria-label for the header when it acts as a menu trigger. */ + menuLabel?: string; + /** + * Tints the icon badge with a category colour (blue/purple/green/amber/red). + * Defaults to the standard blue when omitted (tool + AI chat headers). + */ + accent?: IconBadgeAccent; + /** Shows a pulsing status dot on the icon + a tinted border (e.g. AI running). */ + loading?: boolean; + /** Right-aligned content rendered before the close button (e.g. a status badge). */ actions?: ReactNode; + /** Applied to the inner header element — e.g. to set a view-transition-name. */ + barClassName?: string; + /** Applied to the outer header container. */ className?: string; } /** - * Header strip used by drill-down panels (admin tabs, agent detail, settings - * sub-pages). Back chevron renders only when `onBack` is supplied; an optional - * leading `icon` renders in a tinted box before the title. + * The header shared by the rail surfaces — the active tool panel, the AI chat + * panel, and the Policies detail/wizard. A tinted icon badge + title sit in a + * rounded bar, with an optional dropdown menu and a trailing close button. The + * styling stays legible in dark mode (thin border, no heavy fill) across every + * surface. */ export function PanelHeader({ - title, - subtitle, - onBack, icon, - iconAccent = "blue", + title, + onClose, + closeLabel, + menuItems, + menuLabel, + accent, + loading = false, actions, + barClassName, className, }: PanelHeaderProps) { + const hasMenu = menuItems != null && menuItems.length > 0; + + // Tint the icon badge with the category colour when an accent is given. Inline + // so it wins over the default blue treatment in both light and dark mode; the + // --color-* tokens are theme-aware and match the badge tint used elsewhere. + const iconStyle: CSSProperties | undefined = accent + ? { + color: `var(--color-${accent})`, + background: `color-mix(in srgb, var(--color-${accent}) 14%, transparent)`, + } + : undefined; + + const barClasses = [ + "sui-panelhdr__bar", + loading ? "sui-panelhdr__bar--loading" : "", + barClassName ?? "", + ] + .filter(Boolean) + .join(" "); + + const barBody = ( + <> + + {icon} + {loading && } + + {title} + {hasMenu && ( + + )} + + ); + return (
-
- {onBack && ( - - )} - {icon && ( - - {icon} - - )} -
-
{title}
- {subtitle &&
{subtitle}
} + + + )}
-
- {actions &&
{actions}
} + )}
); } From 984b0e424af0bd74697bb3cadc3fd1322ada27d3 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 12:20:52 +0100 Subject: [PATCH 7/9] PanelHeader: render actions inside the bar (e.g. the policy status pill) The status badge now sits inside the rounded header pill, right-aligned after the title, instead of in the trail beside the close button. --- frontend/shared/components/PanelHeader.css | 5 ++-- frontend/shared/components/PanelHeader.tsx | 35 +++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/shared/components/PanelHeader.css b/frontend/shared/components/PanelHeader.css index f19808b290..d6a46330e1 100644 --- a/frontend/shared/components/PanelHeader.css +++ b/frontend/shared/components/PanelHeader.css @@ -83,10 +83,11 @@ button.sui-panelhdr__bar:hover { color: var(--mantine-color-dimmed); } -.sui-panelhdr__trail { +/* Right-aligned content inside the bar (e.g. a status badge); the flexible + label pushes it to the trailing edge of the pill. */ +.sui-panelhdr__actions { display: inline-flex; align-items: center; - gap: 0.5rem; flex-shrink: 0; } diff --git a/frontend/shared/components/PanelHeader.tsx b/frontend/shared/components/PanelHeader.tsx index 83eb8ef37e..1fcedd88ac 100644 --- a/frontend/shared/components/PanelHeader.tsx +++ b/frontend/shared/components/PanelHeader.tsx @@ -38,7 +38,8 @@ export interface PanelHeaderProps { accent?: IconBadgeAccent; /** Shows a pulsing status dot on the icon + a tinted border (e.g. AI running). */ loading?: boolean; - /** Right-aligned content rendered before the close button (e.g. a status badge). */ + /** Right-aligned content rendered inside the header bar, after the title + * (e.g. a status badge). */ actions?: ReactNode; /** Applied to the inner header element — e.g. to set a view-transition-name. */ barClassName?: string; @@ -93,6 +94,9 @@ export function PanelHeader({ {loading && } {title} + {actions != null && ( + {actions} + )} {hasMenu && ( {barBody}
)} - {(actions != null || onClose != null) && ( -
- {actions} - {onClose && ( - - - - )} -
+ {onClose && ( + + + )}
); From 96943c92d8503a5e696c8790fc3ea252713efb84 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 12:31:03 +0100 Subject: [PATCH 8/9] PanelHeader: grey header border in the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --mantine-color-default-border isn't emitted in the main app's Mantine scope (only inside the ChatFAB provider), so the bare var fell back to currentColor — a black outline in light mode. Use --border-subtle first (defined app-wide) with the Mantine token as the portal fallback, matching FormFieldSidebar. --- frontend/shared/components/PanelHeader.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/shared/components/PanelHeader.css b/frontend/shared/components/PanelHeader.css index d6a46330e1..9869b56be3 100644 --- a/frontend/shared/components/PanelHeader.css +++ b/frontend/shared/components/PanelHeader.css @@ -18,7 +18,7 @@ flex: 1; min-width: 0; padding: 0.4rem 0.75rem 0.4rem 0.4rem; - border: 1px solid var(--mantine-color-default-border); + border: 1px solid var(--border-subtle, var(--mantine-color-default-border)); border-radius: 9999px; background: var(--mantine-color-body); text-align: left; @@ -125,7 +125,7 @@ button.sui-panelhdr__bar:hover { border-color: color-mix( in srgb, var(--mantine-color-blue-5) 60%, - var(--mantine-color-default-border) + var(--border-subtle, var(--mantine-color-default-border)) ); } @@ -133,7 +133,7 @@ button.sui-panelhdr__bar:hover { so it doesn't read as a clashing lighter card on the dark toolbar. */ [data-mantine-color-scheme="dark"] .sui-panelhdr__bar { background: transparent; - border-color: var(--mantine-color-default-border); + border-color: var(--border-subtle, var(--mantine-color-default-border)); } [data-mantine-color-scheme="dark"] button.sui-panelhdr__bar:hover { @@ -144,7 +144,7 @@ button.sui-panelhdr__bar:hover { border-color: color-mix( in srgb, var(--mantine-color-blue-4) 55%, - var(--mantine-color-default-border) + var(--border-subtle, var(--mantine-color-default-border)) ); } From 978507b605b9321967b562d048b1451e2bc89191 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Jun 2026 12:51:11 +0100 Subject: [PATCH 9/9] tidy comments --- frontend/editor/src/core/components/tools/ToolPanel.css | 3 --- .../editor/src/proprietary/components/chat/ChatPanel.css | 3 --- .../src/proprietary/components/policies/PoliciesSidebar.tsx | 6 +----- .../src/proprietary/components/policies/policyStatus.ts | 4 +--- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/frontend/editor/src/core/components/tools/ToolPanel.css b/frontend/editor/src/core/components/tools/ToolPanel.css index 9919e3f19c..e5949e365a 100644 --- a/frontend/editor/src/core/components/tools/ToolPanel.css +++ b/frontend/editor/src/core/components/tools/ToolPanel.css @@ -294,9 +294,6 @@ background-color: var(--bg-muted); } -/* The active-tool header now renders via the shared PanelHeaderPill - (see PanelHeaderPill.css). */ - ::view-transition-old(tool-rail) { animation: vt-rail-fade-out 180ms ease-out forwards; } diff --git a/frontend/editor/src/proprietary/components/chat/ChatPanel.css b/frontend/editor/src/proprietary/components/chat/ChatPanel.css index 1a81fed560..19b6cdbfc0 100644 --- a/frontend/editor/src/proprietary/components/chat/ChatPanel.css +++ b/frontend/editor/src/proprietary/components/chat/ChatPanel.css @@ -25,9 +25,6 @@ flex-shrink: 0; } -/* The agent pill renders via the shared PanelHeaderPill (see PanelHeaderPill.css - for its visual styling). This class only carries the view-transition name so - the pill still morphs in/out with the chat panel. */ .chat-panel__agent-pill-vt { view-transition-name: stirling-agent; } diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx index 45d6d37510..0e87d9f982 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx @@ -62,11 +62,7 @@ export function usePoliciesEnabled(): boolean { } /** - * Whether the right rail should show the Policies section. Everyone sees it when - * the feature is on: admins / team leads get the full catalogue (coming-soon rows - * greyed), regular users get the live policies only. It only disappears if there - * are no rows to show for the current user — which can't happen while a live - * (non-coming-soon) policy like Security exists, but is handled defensively. + * Whether the right rail should show the Policies section. */ export function usePoliciesVisible(): boolean { const pol = usePolicies(); diff --git a/frontend/editor/src/proprietary/components/policies/policyStatus.ts b/frontend/editor/src/proprietary/components/policies/policyStatus.ts index c9f2b000fc..fab615ab25 100644 --- a/frontend/editor/src/proprietary/components/policies/policyStatus.ts +++ b/frontend/editor/src/proprietary/components/policies/policyStatus.ts @@ -18,9 +18,7 @@ export const STATUS_LABEL: Record = { }; /** - * Per-category accent colour. The single source of truth shared by: the list - * icons (which render colourless at rest and reveal this colour on hover/focus), - * and a file's post-run glow + shield badge (via usePolicyFileBadges' ACCENT_VAR). + * Per-category accent colour */ export const ROW_ACCENT: Record = { ingestion: "blue",