Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/editor/public/locales/en-US/translation.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
167 changes: 167 additions & 0 deletions frontend/editor/src/core/components/shared/PanelHeader.css
Original file line number Diff line number Diff line change
@@ -0,0 +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;
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);
}
153 changes: 153 additions & 0 deletions frontend/editor/src/core/components/shared/PanelHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { CSSProperties, ReactNode } from "react";
Comment thread
EthanHealy01 marked this conversation as resolved.
Outdated
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 = (
<>
<span className="sui-panelhdr__icon" style={iconStyle}>
{icon}
{loading && <span className="sui-panelhdr__dot" />}
</span>
<span className="sui-panelhdr__label">{title}</span>
{hasMenu && (
<KeyboardArrowDownIcon
className="sui-panelhdr__chevron"
sx={{ fontSize: 18 }}
/>
)}
</>
);

return (
<div
className={["sui-panelhdr", className ?? ""].filter(Boolean).join(" ")}
>
{hasMenu ? (
<Menu shadow="md" width={220} position="bottom-start" withinPortal>
<Menu.Target>
<button type="button" className={barClasses} aria-label={menuLabel}>
{barBody}
</button>
</Menu.Target>
<Menu.Dropdown>
{(menuItems ?? []).map((item, i) => (
<Menu.Item
key={item.key ?? i}
leftSection={item.icon}
onClick={item.onClick}
disabled={item.disabled}
>
{item.label}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
) : (
<div className={barClasses}>{barBody}</div>
)}

{(actions != null || onClose != null) && (
<div className="sui-panelhdr__trail">
{actions}
{onClose && (
<ActionIcon
className="sui-panelhdr__close"
variant="subtle"
color="gray"
radius="xl"
size="md"
onClick={onClose}
aria-label={closeLabel}
>
<CloseIcon sx={{ fontSize: 18 }} />
</ActionIcon>
)}
</div>
)}
</div>
);
}
Loading
Loading