Files
Stirling-PDF/frontend/shared/components/SettingsShell.tsx
T
Reece Browne 2b05865a84 Portal: unified-design surfaces (Policies, Users, Components, Agent Builder, Editor deploy) + Settings rebuild (#6696)
Builds the remaining developer-portal surfaces from the unified design
and rebuilds Settings, on top of the portal scaffold merged in #6686.
All tier-aware, mock-driven (MSW), componentised with Storybook
coverage. Touches only `frontend/portal` + `frontend/shared` — the
editor is untouched.

## New surfaces
- **Policies** — org-wide governance across the five categories
(Ingestion / Security / Compliance / Routing / Retention) with a
designer + per-doc-type overrides
- **Users** — members, roles, invite, tier-scaled SSO/SCIM access
- **Components** — embeddable `@stirling/*` SDK catalogue with
per-action pricing
- **Getting Started** — three-step funnel (use case → analyse a document
→ API key + snippets)
- **Agent Builder** — agent lifecycle (scenarios, tool modes,
evals/golden-sets, versions), reached from Sources
- **Editor deployment** — deploy/pair/operate the editor (targets,
pairing, health, credential rotation, air-gapped bundle), reached from
Infrastructure

## Reworks
- **Documents** → review/approval queue (confidence, extractions, audit
drawer, zero-standing-access elevation); the doc-type catalogue is
retained as a second tab
- **Pipelines** → golden-set pass column + "Promoted from the Editor"
section
- **Infrastructure** → new **Models** tab; deeper **Security** (managed
/ BYOK / HYOK + SOC 2 / ISO 27001 / HIPAA / GDPR / PCI attestations)
- **Home** → "What runs on your PDFs" policy summary + tier-aware
processing-status strip + pipeline-fork wizard

## Settings & shared
- New shared **`SettingsShell`** (grouped left-nav + content pane),
modelled on the editor's account-settings modal so both apps can
converge on one layout
- Portal **Settings** rebuilt on it as scoped sections — Account /
Workspace / Admin (Authentication, Active sessions, Early access)

## Brand
- Adopt the editor's brand mark + favicon; sidebar reads **Stirling
Processor**; app-switcher labels the active app "Processor"

## Mock contract
- Every surface follows the 3-layer pattern (typed `api/*` → MSW handler
→ fixtures); new endpoints documented in `MOCKS.md`. The read contract
is backend-ready; writes are marked `// TODO(backend): <METHOD> <path>`.

## Verification
- tsc (portal + shared) ✓ · eslint ✓ · dpdm (no circular) ✓ ·
`build:portal` ✓ · `storybook:build` ✓ · Prettier ✓

## Deferred (noted, not in scope)
- Unified shell / auth / role→surface routing / Workspace=Plan
(architectural epic)
- Tier rename (Editor / Processor / Bespoke) and the Usage flat-pricing
+ PAYG quick-amounts + Bespoke modal
- Editor adopting the shared `SettingsShell`; converting marked
write-stubs into live `api/` seams
2026-06-18 10:28:03 +00:00

138 lines
4.3 KiB
TypeScript

import type { ReactNode } from "react";
import "@shared/components/SettingsShell.css";
export interface SettingsNavItem {
key: string;
label: string;
/** Optional leading glyph. */
icon?: ReactNode;
/** Optional trailing badge (e.g. a plan gate or count). */
badge?: ReactNode;
disabled?: boolean;
}
export interface SettingsNavSection {
/** Uppercase group heading above its items. */
title: string;
items: SettingsNavItem[];
}
export interface SettingsShellProps {
sections: SettingsNavSection[];
activeKey: string;
onSelect: (key: string) => void;
/** Heading for the content pane — usually the active item's label. */
title: ReactNode;
/** Renders a close button at the top-right of the content header. */
onClose?: () => void;
/** Extra header controls (e.g. a search field) left of the close button. */
headerActions?: ReactNode;
/** Sticky footer, e.g. Save / Cancel. */
footer?: ReactNode;
/** Active section content. */
children: ReactNode;
className?: string;
}
/**
* Two-pane settings layout: a grouped left navigation rail and a content pane
* with a sticky header (active title + actions) and an optional sticky footer.
*
* Layout chrome only — the caller owns section state and renders the active
* panel as `children`. Host it inside any modal/dialog frame (it fills its
* container's height and scrolls the two panes independently). Shared so the
* portal and the editor can present account settings the same way.
*/
export function SettingsShell({
sections,
activeKey,
onSelect,
title,
onClose,
headerActions,
footer,
children,
className,
}: SettingsShellProps) {
return (
<div
className={["sui-settings-shell", className ?? ""]
.filter(Boolean)
.join(" ")}
>
<nav className="sui-settings-shell__nav" aria-label="Settings sections">
{sections.map((section) => (
<div key={section.title} className="sui-settings-shell__group">
<span className="sui-settings-shell__group-title">
{section.title}
</span>
{section.items.map((item) => (
<button
key={item.key}
type="button"
className={
"sui-settings-shell__item" +
(item.key === activeKey ? " is-active" : "")
}
aria-current={item.key === activeKey ? "page" : undefined}
disabled={item.disabled}
onClick={() => onSelect(item.key)}
>
{item.icon && (
<span className="sui-settings-shell__item-icon" aria-hidden>
{item.icon}
</span>
)}
<span className="sui-settings-shell__item-label">
{item.label}
</span>
{item.badge && (
<span className="sui-settings-shell__item-badge">
{item.badge}
</span>
)}
</button>
))}
</div>
))}
</nav>
<div className="sui-settings-shell__content">
<header className="sui-settings-shell__header">
<span className="sui-settings-shell__title">{title}</span>
<div className="sui-settings-shell__header-actions">
{headerActions}
{onClose && (
<button
type="button"
className="sui-settings-shell__close"
onClick={onClose}
aria-label="Close"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
)}
</div>
</header>
<div className="sui-settings-shell__body">{children}</div>
{footer && <div className="sui-settings-shell__footer">{footer}</div>}
</div>
</div>
);
}