mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
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
138 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|