mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 18:44:05 +02:00
# Description of Changes ## What & why This PR introduces the **Stirling developer portal** — a new control-plane frontend that sits alongside the existing PDF editor — plus the shared design system and workspace structure needed to host both apps in one frontend. The portal is the parent product surface: where users connect sources, compose pipelines, wire agents, and manage usage / billing / infrastructure, with the PDF editor as one capability inside it. This PR lays the **foundation** — workspace reshape, design system, app shell, navigation, and a mock-driven home — rather than wiring real backends (those surfaces are placeholders for follow-up phases). ## What's in this PR **1. Frontend repo reshape (`frontend/src/` → `frontend/editor/`)** The existing editor app moved under `frontend/editor/`, so `editor`, `portal`, and `shared` are siblings in one workspace. All references were updated accordingly: `LICENSE`, `.dockerignore`, `.gitignore`, build/sign shell scripts, the GH language-check script, the Taskfile, and Docker config. **No editor source logic changed — path references only.** **2. New shared design system (`frontend/shared/`)** - **Design tokens** in `tokens.css` as the single runtime source of truth (light/dark, category accents, gradients). `tokens.ts` now holds only the `Tier` type — the old JS palette mirror was removed (nothing consumed it and it had drifted). - ~30 framework-light **components** (Card, Button, Input, Select, Tabs, Modal, Drawer, Toast, MetricCard, StatusBadge, Skeleton, EmptyState, …) with Storybook stories. - **Typed data catalogues**: `endpoints.ts` (10 verticals / 64 endpoints) and `ops.ts`. **3. New developer portal app (`frontend/portal/`)** - App shell: `Header`, `Sidebar`, `AssistantPanel`, search modal, notifications, tier switcher, theme toggle, MSW toggle. - **Tier-aware** home (free / pay-as-you-go / enterprise): KPI strip, 30-day usage chart, onboarding checklist, quick actions, recent activity, region health, product grid, and a curated **"Popular use cases"** teaser. - **Documents** view hosting the full, tab-filterable endpoint catalogue. - Placeholder views for Sources / Pipelines / Agents / Editor / Infrastructure / Usage & Billing / Developer Docs / Settings (follow-up phases). - **MSW-mocked** API layer: `api/*` issues real `fetch`, intercepted by mocks in dev/Storybook; pointing at a real backend is just a matter of not registering MSW. `react-router` URLs; Tier / View / UI contexts. **4. Tooling & guardrails** - ESLint extended to `portal` + `shared`, with **layering-boundary rules**: `shared/` may depend only on third-party packages and itself (no `@app` / `@portal` / `@core` / `@proprietary` / Tauri), so it stays cleanly extractable into a standalone package later. - `dpdm` circular-dependency check now walks editor + portal + shared (the old glob matched only 2 files). - New **devDependencies only** — Storybook (+ a11y/docs/themes addons), MSW. No runtime dependencies added. - New tasks: `frontend:dev:portal`, `frontend:build:portal`. ## Testing done locally - `tsc` for both `portal` and `shared` projects — clean - `eslint --max-warnings=0` across the whole frontend — clean - `dpdm` circular-dependency check — no cycles - Editor builds clean: `vite build editor --mode core` (✓ built, only the pre-existing >500 kB chunk-size advisory) - Editor runs in dev (core mode) with **zero console errors**; portal runs in dev across all three tiers ## Notes for reviewers - The change is overwhelmingly **additive**: `shared/` and `portal/` are brand-new; the existing editor is path-reference changes only. - The portal is intentionally **mock-driven** at this stage — real backends and the remaining views land in follow-up phases. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
304 lines
7.9 KiB
TypeScript
304 lines
7.9 KiB
TypeScript
import { useState } from "react";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import { FormField } from "@shared/components/FormField";
|
|
import { Input } from "@shared/components/Input";
|
|
import { Select } from "@shared/components/Select";
|
|
import { Checkbox } from "@shared/components/Checkbox";
|
|
import { RadioGroup } from "@shared/components/Radio";
|
|
import { Slider } from "@shared/components/Slider";
|
|
import { Stack } from "@shared/components/Stack";
|
|
import { Inline } from "@shared/components/Inline";
|
|
|
|
// Inline icon to avoid a cross-layer import; shared/ must not depend on portal/.
|
|
function SearchIcon({ size = 14 }: { size?: number }) {
|
|
return (
|
|
<svg
|
|
width={size}
|
|
height={size}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth={1.75}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<circle cx="11" cy="11" r="7" />
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
const meta: Meta = {
|
|
title: "Primitives/Forms",
|
|
parameters: { layout: "padded" },
|
|
decorators: [
|
|
(S) => (
|
|
<div style={{ width: "28rem" }}>
|
|
<S />
|
|
</div>
|
|
),
|
|
],
|
|
};
|
|
export default meta;
|
|
type Story = StoryObj;
|
|
|
|
export const Input_Default: Story = {
|
|
render: () => (
|
|
<FormField
|
|
label="Pipeline name"
|
|
required
|
|
helperText="Lowercase letters and dashes only."
|
|
>
|
|
<Input placeholder="e.g. coi-compliance" />
|
|
</FormField>
|
|
),
|
|
};
|
|
|
|
export const Input_WithIcon: Story = {
|
|
render: () => (
|
|
<FormField label="Search">
|
|
<Input
|
|
leadingIcon={<SearchIcon size={14} />}
|
|
placeholder="Search Stirling…"
|
|
/>
|
|
</FormField>
|
|
),
|
|
};
|
|
|
|
export const Input_Error: Story = {
|
|
render: () => (
|
|
<FormField label="Email" error="Must be a valid email address" required>
|
|
<Input value="not-an-email" onChange={() => {}} />
|
|
</FormField>
|
|
),
|
|
};
|
|
|
|
export const Select_Default: Story = {
|
|
render: () => (
|
|
<FormField label="Retention period">
|
|
<Select
|
|
defaultValue="90"
|
|
options={[
|
|
{ value: "30", label: "30 days" },
|
|
{ value: "60", label: "60 days" },
|
|
{ value: "90", label: "90 days (default)" },
|
|
{ value: "180", label: "180 days" },
|
|
{ value: "never", label: "Never expire" },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
),
|
|
};
|
|
|
|
export const Checkbox_Single: Story = {
|
|
render: () => (
|
|
<Stack gap="2">
|
|
<Checkbox
|
|
defaultChecked
|
|
label="Notify on pipeline failure (webhook + email)"
|
|
/>
|
|
<Checkbox
|
|
label="Send low-confidence docs to review queue"
|
|
description="Confidence below 0.85 routes to a reviewer."
|
|
/>
|
|
<Checkbox indeterminate label="Mixed state" />
|
|
<Checkbox disabled label="Disabled (off)" />
|
|
<Checkbox defaultChecked disabled label="Disabled (on)" />
|
|
</Stack>
|
|
),
|
|
};
|
|
|
|
export const Checkbox_GridOfCategories: Story = {
|
|
render: () => (
|
|
<FormField label="PII categories">
|
|
<Inline gap="2">
|
|
{["SSN", "DOB", "Accounts", "Contacts", "Names", "Addresses"].map(
|
|
(c) => (
|
|
<Checkbox key={c} defaultChecked label={c} />
|
|
),
|
|
)}
|
|
</Inline>
|
|
</FormField>
|
|
),
|
|
};
|
|
|
|
export const Radio_Group: Story = {
|
|
render: () => {
|
|
function Bound() {
|
|
const [mode, setMode] = useState<"stirling" | "byok" | "hyok">(
|
|
"stirling",
|
|
);
|
|
return (
|
|
<FormField label="Key mode">
|
|
<RadioGroup
|
|
name="keymode"
|
|
value={mode}
|
|
onChange={setMode}
|
|
options={[
|
|
{
|
|
value: "stirling",
|
|
label: "Stirling-managed",
|
|
description: "Stirling generates and rotates keys.",
|
|
},
|
|
{
|
|
value: "byok",
|
|
label: "BYOK (AWS KMS)",
|
|
description: "Bring your own KMS key.",
|
|
},
|
|
{
|
|
value: "hyok",
|
|
label: "HYOK (Enterprise)",
|
|
description: "Stirling never sees the key material.",
|
|
},
|
|
]}
|
|
/>
|
|
</FormField>
|
|
);
|
|
}
|
|
return <Bound />;
|
|
},
|
|
};
|
|
|
|
export const Radio_Horizontal: Story = {
|
|
render: () => {
|
|
function Bound() {
|
|
const [v, setV] = useState("us");
|
|
return (
|
|
<FormField label="Region">
|
|
<RadioGroup
|
|
name="region"
|
|
direction="horizontal"
|
|
value={v}
|
|
onChange={setV}
|
|
options={[
|
|
{ value: "us", label: "US" },
|
|
{ value: "eu", label: "EU" },
|
|
{ value: "apac", label: "APAC" },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
);
|
|
}
|
|
return <Bound />;
|
|
},
|
|
};
|
|
|
|
export const Slider_Confidence: Story = {
|
|
render: () => {
|
|
function Bound() {
|
|
const [v, setV] = useState(0.85);
|
|
return (
|
|
<FormField
|
|
label="Minimum confidence"
|
|
helperText="Default 0.85 — gates downstream ops below this threshold."
|
|
>
|
|
<Slider
|
|
value={v}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
onChange={setV}
|
|
formatValue={(x) => x.toFixed(2)}
|
|
/>
|
|
</FormField>
|
|
);
|
|
}
|
|
return <Bound />;
|
|
},
|
|
};
|
|
|
|
export const Slider_Retention: Story = {
|
|
render: () => {
|
|
function Bound() {
|
|
const [days, setDays] = useState(90);
|
|
return (
|
|
<FormField label="Retain artifacts for">
|
|
<Slider
|
|
value={days}
|
|
min={7}
|
|
max={365}
|
|
step={1}
|
|
onChange={setDays}
|
|
formatValue={(d) => `${d} days`}
|
|
/>
|
|
</FormField>
|
|
);
|
|
}
|
|
return <Bound />;
|
|
},
|
|
};
|
|
|
|
export const FullForm: Story = {
|
|
render: () => {
|
|
function Form() {
|
|
const [name, setName] = useState("");
|
|
const [retention, setRetention] = useState("90");
|
|
const [mode, setMode] = useState<"stirling" | "byok" | "hyok">(
|
|
"stirling",
|
|
);
|
|
const [conf, setConf] = useState(0.85);
|
|
const [notify, setNotify] = useState(true);
|
|
const [review, setReview] = useState(false);
|
|
return (
|
|
<Stack gap="4">
|
|
<FormField label="Pipeline name" required>
|
|
<Input
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g. coi-compliance"
|
|
/>
|
|
</FormField>
|
|
<FormField label="Retention">
|
|
<Select
|
|
value={retention}
|
|
onChange={(e) => setRetention(e.target.value)}
|
|
options={[
|
|
{ value: "30", label: "30 days" },
|
|
{ value: "90", label: "90 days" },
|
|
{ value: "365", label: "1 year" },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Key mode">
|
|
<RadioGroup
|
|
name="km"
|
|
value={mode}
|
|
onChange={setMode}
|
|
options={[
|
|
{ value: "stirling", label: "Stirling-managed" },
|
|
{ value: "byok", label: "BYOK" },
|
|
{ value: "hyok", label: "HYOK" },
|
|
]}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Confidence gate" helperText="Default 0.85.">
|
|
<Slider
|
|
value={conf}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
onChange={setConf}
|
|
formatValue={(v) => v.toFixed(2)}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Alerts">
|
|
<Stack gap="2">
|
|
<Checkbox
|
|
checked={notify}
|
|
onChange={(e) => setNotify(e.target.checked)}
|
|
label="Notify on failure"
|
|
/>
|
|
<Checkbox
|
|
checked={review}
|
|
onChange={(e) => setReview(e.target.checked)}
|
|
label="Send low-confidence to review"
|
|
/>
|
|
</Stack>
|
|
</FormField>
|
|
</Stack>
|
|
);
|
|
}
|
|
return <Form />;
|
|
},
|
|
};
|