diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 7760f002dd..1b81a551f1 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -8833,7 +8833,9 @@ title = "Sources" connectSource = "Connect source" [portal.sources.builder] +advanced = "Advanced" back = "Back to sources" +backToSource = "Back to source setup" backToTypes = "All source types" cancel = "Cancel" chooseHint = "Choose where documents come from. Greyed-out connectors are on the way." @@ -8843,7 +8845,6 @@ create = "Create source" createTitle = "Connect a source" delete = "Delete" editTitle = "Edit source" -enabled = "Enabled" save = "Save changes" [portal.sources.builder.folderAccess] @@ -8863,23 +8864,24 @@ total = "Connections" unused = "Unused" [portal.sources.networkFields.connection] -helperText = "The stored connection with the server address and credentials. Reused by every source that references it." +helperText = "The saved connection with the server address and credentials. Shared by every source that uses it." label = "Connection" [portal.sources.networkFields.directory] -helperText = "Folder on the server to poll, relative to the login home or share root. Leave blank for the root." +helperText = "The folder on the server to watch, relative to the login home or share root." label = "Folder" -placeholder = "incoming/" +placeholder = "Login home or share root" [portal.sources.networkFields.mode] -helperText = "Consume removes each file from the server once every policy has processed it." -label = "Read mode" +helperText = "Whether to leave the original file in place after it has been processed. If the original file is left in place, it will be re-processed next time the pipeline scans the source." +label = "After processing" [portal.sources.networkFields.mode.options] -consume = "Consume: process each file once" -snapshot = "Snapshot: re-read the folder every run" +consume = "Delete the file" +snapshot = "Leave it in place" [portal.sources.networkFields.recursive] +helperText = "Include subfolders if your files are organised into nested folders, or watch only the top level." label = "Folder depth" [portal.sources.networkFields.recursive.options] @@ -8920,26 +8922,28 @@ description = "Watch a directory on the server for new documents." label = "Folder" [portal.sources.types.folder.fields.directory] -helperText = "Absolute path Stirling watches for files to process." -label = "Directory path" +helperText = "The absolute path to the folder to watch for new files." +label = "Folder" placeholder = "/data/incoming" [portal.sources.types.folder.fields.identity] -helperText = "Content check reads each changed file, so renames and touches that don't alter content are not reprocessed." +helperText = "Using the file contents for change detection is slower but more accurate." label = "Change detection" [portal.sources.types.folder.fields.identity.options] -hash = "Size, date and content check" -stat = "Size and date modified" +hash = "File Metadata & Content" +stat = "File Metadata" [portal.sources.types.folder.fields.mode] -label = "Read mode" +helperText = "Whether to leave the original file in place after it has been processed. If the original file is left in place, it will be re-processed next time the pipeline scans the source." +label = "After processing" [portal.sources.types.folder.fields.mode.options] -consume = "Consume: process each file once" -snapshot = "Snapshot: re-read the folder every run" +consume = "Delete the file" +snapshot = "Leave it in place" [portal.sources.types.folder.fields.recursive] +helperText = "Include subfolders if your files are organised into nested folders, or watch only the top level." label = "Folder depth" [portal.sources.types.folder.fields.recursive.options] @@ -8967,21 +8971,21 @@ description = "Pull documents from an Amazon S3 or S3-compatible bucket." label = "Amazon S3" [portal.sources.types.s3.fields.connection] -helperText = "The stored connection holding the bucket and credentials. Reused by every source and pipeline output that references it." +helperText = "The saved connection with the bucket and credentials. Shared by every source and pipeline output that uses it." label = "Connection" [portal.sources.types.s3.fields.mode] -helperText = "Consume removes each object from the bucket once every policy has processed it." -label = "Read mode" +helperText = "Whether to leave the original object in place after it has been processed. If the original object is left in place, it will be re-processed next time the pipeline scans the source." +label = "After processing" [portal.sources.types.s3.fields.mode.options] -consume = "Consume: process each object once" -snapshot = "Snapshot: re-read the bucket every run" +consume = "Delete the object" +snapshot = "Leave it in place" [portal.sources.types.s3.fields.prefix] -helperText = "Only objects whose keys start with this prefix are processed." -label = "Key prefix" -placeholder = "incoming/" +helperText = "The folder (key prefix) within the bucket to watch." +label = "Folder" +placeholder = "Whole bucket" [portal.sources.types.sftp] description = "Poll an SFTP drop folder for new documents." diff --git a/frontend/editor/src/core/theme/colors.css b/frontend/editor/src/core/theme/colors.css index 8e416af124..31f9666271 100644 --- a/frontend/editor/src/core/theme/colors.css +++ b/frontend/editor/src/core/theme/colors.css @@ -1,5 +1,5 @@ /* COLORS — canonical semantic core (~21 --c-* tokens) mapped from primitives.css per theme; compat.css aliases legacy names onto these. Editor: always data-app-theme="custom" + data-mantine-color-scheme=light|dark. Portal/Storybook: data-theme=light|dark → LIGHT/MIDNIGHT. */ -/* Surface elevation: --c-bg (canvas) < --c-bg-raised (sidebars) < --c-surface (cards) < --c-surface-raised < --c-surface-sunken; plus --c-input-bg, --c-hover, --c-active, --c-overlay. */ +/* Surface elevation: --c-bg (canvas) < --c-bg-raised (sidebars) < --c-surface (cards) < --c-surface-raised < --c-surface-sunken; plus --c-modal-surface (modal panels), --c-input-bg, --c-hover, --c-active, --c-overlay. */ /* ── LIGHT ───────────────────────────────────────────────────────────────── */ :root, @@ -11,6 +11,7 @@ html[data-app-theme="light"] { --c-surface-raised: var(--p-white); --c-surface-sunken: var(--p-gray-100); --c-input-bg: var(--p-white); + --c-modal-surface: var(--p-white); --c-hover: var(--p-gray-50); --c-active: var(--p-gray-100); --c-overlay: rgba(0, 0, 0, 0.5); @@ -22,7 +23,7 @@ html[data-app-theme="light"] { --c-btn-solid: var(--c-text); --c-btn-inverse: var(--p-snow); - --c-btn-secondary: var(--c-btn-inverse); + --c-btn-secondary: var(--c-surface-raised); --c-btn-secondary-border: var(--c-border); --c-border: var(--p-c-f0f0f0); @@ -144,7 +145,8 @@ html[data-app-theme="midnight"] { --c-surface: var(--p-c-1a1a1d); --c-surface-raised: var(--p-zinc-650); --c-surface-sunken: var(--p-zinc-850); - --c-input-bg: var(--p-zinc-650); + --c-input-bg: var(--c-surface); + --c-modal-surface: var(--c-surface); --c-hover: var(--p-gray-800); --c-active: var(--p-gray-800); --c-overlay: rgba(0, 0, 0, 0.6); @@ -360,7 +362,7 @@ html[data-app-theme="custom"][data-mantine-color-scheme="dark"] { var(--c-primary) 8%, var(--p-zinc-900) ); - --c-input-bg: color-mix(in srgb, var(--c-primary) 7%, var(--p-zinc-900)); + --c-input-bg: var(--c-surface); --c-hover: color-mix(in srgb, var(--c-primary) 12%, var(--p-zinc-750)); --c-active: color-mix(in srgb, var(--c-primary) 15%, var(--p-zinc-700)); --c-border: color-mix(in srgb, var(--c-primary) 14%, var(--p-c-28282d)); @@ -424,7 +426,7 @@ html[data-app-theme="custom"][data-accent="default"][data-mantine-color-scheme=" --c-surface: var(--p-c-1a1a1d); --c-surface-raised: var(--p-zinc-775); --c-surface-sunken: var(--p-zinc-900); - --c-input-bg: var(--p-zinc-900); + --c-input-bg: var(--c-surface); --c-hover: var(--p-zinc-750); --c-active: var(--p-zinc-700); --c-border: var(--p-c-28282d); diff --git a/frontend/editor/src/core/ui/Collapsible.css b/frontend/editor/src/core/ui/Collapsible.css index 6f7164dea5..1565464810 100644 --- a/frontend/editor/src/core/ui/Collapsible.css +++ b/frontend/editor/src/core/ui/Collapsible.css @@ -1,51 +1,41 @@ .sui-collapsible { + display: flex; + flex-direction: column; overflow: hidden; } +/* A bare chevron-led toggle: no box, sized to its label, left-aligned. */ .sui-collapsible__head { - display: flex; + align-self: flex-start; + display: inline-flex; align-items: center; - gap: 0.5rem; - width: 100%; - padding: 0.75rem 0.875rem; - background: var(--c-surface); + gap: 0.375rem; + padding: 0; border: none; + background: none; cursor: pointer; - text-align: left; - transition: background-color 0.15s ease; + font: inherit; + font-size: 0.8125rem; + font-weight: 600; + color: var(--c-text-muted); } .sui-collapsible__head:hover { - background: var(--c-hover); -} - -.sui-collapsible__head-main { - display: flex; - align-items: center; - gap: 0.5rem; - flex: 1; - min-width: 0; - flex-wrap: wrap; -} - -.sui-collapsible__head-end { - display: inline-flex; - align-items: center; - gap: 0.625rem; - margin-left: auto; - flex-shrink: 0; + color: var(--c-text); } +/* Points right when closed, rotates down when open. */ .sui-collapsible__chevron { color: var(--c-text-subtle); + transform: rotate(-90deg); transition: transform 0.2s ease; } .sui-collapsible__chevron[data-open="true"] { - transform: rotate(180deg); + transform: rotate(0deg); } -/* Grid-rows disclosure: 0fr → 1fr, no magic max-height */ +/* Grid-rows disclosure: 0fr → 1fr, no magic max-height. */ .sui-collapsible__body { display: grid; grid-template-rows: 0fr; @@ -56,6 +46,8 @@ grid-template-rows: 1fr; } +/* Gap between the toggle and the revealed content, no side indent. */ .sui-collapsible__body-inner { overflow: hidden; + padding-top: 0.875rem; } diff --git a/frontend/editor/src/core/ui/Collapsible.stories.tsx b/frontend/editor/src/core/ui/Collapsible.stories.tsx index 36dffb527b..b2c1c6c5a4 100644 --- a/frontend/editor/src/core/ui/Collapsible.stories.tsx +++ b/frontend/editor/src/core/ui/Collapsible.stories.tsx @@ -19,25 +19,17 @@ export const Default: Story = { setOpen((o) => !o)} - header={Section title} - aside={3 items} + header="Advanced" > -
- Body content revealed when the section is open. -
+ Body content revealed when the section is open.
); }, }; -// Stacked instances form an accordion; each section toggles independently. -export const Accordion: Story = { +// Several independent disclosures stacked in a form-like column. +export const Stacked: Story = { render: () => { const [open, setOpen] = useState(0); const sections = ["Trial", "Quote", "Agreement"]; @@ -47,7 +39,7 @@ export const Accordion: Story = { maxWidth: "40rem", display: "flex", flexDirection: "column", - gap: "0.75rem", + gap: "0.875rem", }} > {sections.map((label, i) => ( @@ -55,16 +47,9 @@ export const Accordion: Story = { key={label} open={open === i} onToggle={() => setOpen(open === i ? null : i)} - header={{label}} + header={label} > -
- {label} details. -
+ {label} details. ))} diff --git a/frontend/editor/src/core/ui/Collapsible.tsx b/frontend/editor/src/core/ui/Collapsible.tsx index 229b2d2ef1..12992c2668 100644 --- a/frontend/editor/src/core/ui/Collapsible.tsx +++ b/frontend/editor/src/core/ui/Collapsible.tsx @@ -1,38 +1,31 @@ import type { ReactNode } from "react"; -import "@app/ui/Surface.css"; import "@app/ui/Collapsible.css"; export interface CollapsibleProps { /** Whether the section is expanded. Controlled — pair with `onToggle`. */ open: boolean; onToggle: () => void; - /** Header content (left/main side); the chevron is appended automatically. */ + /** The toggle's label, shown after the chevron. */ header: ReactNode; - /** Right-aligned header content shown before the chevron (a count, a label). */ - aside?: ReactNode; children: ReactNode; className?: string; } /** - * A single bordered disclosure section: an always-visible header that toggles, - * and a body that animates open/closed via a grid-rows transition (no magic - * max-height). Stack several to build an accordion. Header content is fully - * caller-supplied, so it suits both terse and rich (icon + chips + count) rows. + * A lightweight inline disclosure: a chevron-led label that toggles, and a body + * that animates open/closed via a grid-rows transition (no magic max-height). + * No surface or box - it sits inline within a form or panel. */ export function Collapsible({ open, onToggle, header, - aside, children, className, }: CollapsibleProps) { return (
{children}
diff --git a/frontend/editor/src/core/ui/ColorInput.tsx b/frontend/editor/src/core/ui/ColorInput.tsx index cf33331d7d..40e61470fa 100644 --- a/frontend/editor/src/core/ui/ColorInput.tsx +++ b/frontend/editor/src/core/ui/ColorInput.tsx @@ -12,7 +12,7 @@ import { useInputAria } from "@app/ui/ariaForwarding"; import "@app/ui/MantineForms.css"; const SUI_INPUT_VARS = { - "--input-bg": "var(--c-surface)", + "--input-bg": "var(--c-input-bg)", "--input-bd": "var(--c-border)", "--input-bd-focus": "var(--c-primary)", "--input-radius": "var(--radius-md)", diff --git a/frontend/editor/src/core/ui/FormField.css b/frontend/editor/src/core/ui/FormField.css index 792845c255..f7aaf61131 100644 --- a/frontend/editor/src/core/ui/FormField.css +++ b/frontend/editor/src/core/ui/FormField.css @@ -5,6 +5,12 @@ min-width: 0; } +.sui-field__label-row { + display: flex; + align-items: center; + gap: 0.25rem; +} + .sui-field__label { font-size: 0.75rem; font-weight: 600; @@ -13,6 +19,30 @@ color: var(--color-section-label); } +/* An (i) affordance beside the label: the explanation lives in its tooltip + rather than as permanent subtext under the control. */ +.sui-field__info { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + background: none; + color: var(--c-text-subtle); + cursor: pointer; + line-height: 0; +} + +.sui-field__info:hover { + color: var(--c-text); +} + +.sui-field__info:focus-visible { + outline: 2px solid var(--c-primary); + outline-offset: 2px; + border-radius: 999px; +} + .sui-field__required { /* The base red is a fill colour; as text on the form background it only reaches 3.4:1. */ diff --git a/frontend/editor/src/core/ui/FormField.tsx b/frontend/editor/src/core/ui/FormField.tsx index 65eec64eb8..473ca82472 100644 --- a/frontend/editor/src/core/ui/FormField.tsx +++ b/frontend/editor/src/core/ui/FormField.tsx @@ -5,12 +5,18 @@ import { type ReactElement, type ReactNode, } from "react"; +import { Tooltip } from "@mantine/core"; import "@app/ui/FormField.css"; export interface FormFieldProps { label?: ReactNode; /** Helper text shown under the control. Replaced by `error` when present. */ helperText?: ReactNode; + /** + * Supplementary explanation shown behind an (i) affordance on the label, + * instead of taking up permanent space under the control like `helperText`. + */ + info?: ReactNode; /** Error string. Causes the control + helper region to swap to the error tone. */ error?: ReactNode; required?: boolean; @@ -32,6 +38,7 @@ export interface FormFieldProps { export function FormField({ label, helperText, + info, error, required, children, @@ -57,16 +64,54 @@ export function FormField({ .filter(Boolean) .join(" ")} > - {label && ( -