From 9fd53bd6a02311d6f055748fca95dbb02a12d289 Mon Sep 17 00:00:00 2001 From: eti Date: Thu, 30 Jul 2026 18:30:32 +0200 Subject: [PATCH] web/settings: add the repository settings screens from the redesign Completes the desktop half of the Settings Redesign Figma (TFhrx7FBh2Ue5SGH8CUeBU) that the user-settings pass left out: General, Access, Pipelines, Hooks and Sites, plus the rename, custom-label, collaborator, secret and webhook drill-downs. There was no repository settings surface in the rewrite at all, so this adds the route tree under [handle]/[repo]/settings and a Settings tab on RepoTabs. The tab is unconditional for now -- gating it needs the viewer's push access, which the repo layout load doesn't resolve yet. The sidebar card, drill-down shell and row primitives are reused from the user settings. Three of them grew to cover patterns that only appear on the repo pages: SettingsList's gap is now named in spacing units because the designs use all of 8/12/16px, SettingsRow takes a muted description, and SettingsBlock is the new title+description+action header that Access and Pipelines put above each card. Multi-line copy is passed as an array of lines rather than a string with escapes, which reads better and keeps the templates free of useless mustaches. SettingsEmpty replaces EmptyState inside settings: Figma's empty state there is a single 52px row, not the tall italic box the rest of the app uses. SettingsSaveBar factors out the Reset / "Unsaved changes" / Save trio now that General, Sites and Profile all carry it. Verified light and dark against the Figma renders; navigation, card and surface tokens plus nav width and heading size all match. Refs TAN-575. Signed-off-by: eti --- web/src/lib/components/repo/RepoTabs.svelte | 6 +- .../lib/components/settings/DrillDown.svelte | 13 +- .../lib/components/settings/FormRow.svelte | 40 +++- .../components/settings/SettingsBlock.svelte | 60 ++++++ .../components/settings/SettingsEmpty.svelte | 15 ++ .../components/settings/SettingsList.svelte | 12 +- .../components/settings/SettingsRow.svelte | 38 +++- .../settings/SettingsSaveBar.svelte | 23 +++ .../settings/SettingsSection.svelte | 15 +- .../components/settings/tabs/EmailsTab.svelte | 4 +- .../components/settings/tabs/KeysTab.svelte | 4 +- .../components/settings/tabs/KnotsTab.svelte | 4 +- .../settings/tabs/ProfileTab.svelte | 17 +- .../components/settings/tabs/SitesTab.svelte | 4 +- .../settings/tabs/SpindlesTab.svelte | 4 +- .../[handle]/[repo]/settings/+layout.svelte | 36 ++++ .../[handle]/[repo]/settings/+page.svelte | 177 ++++++++++++++++++ .../[repo]/settings/access/+page.svelte | 69 +++++++ .../[repo]/settings/access/new/+page.svelte | 40 ++++ .../[repo]/settings/hooks/+page.svelte | 97 ++++++++++ .../[repo]/settings/hooks/new/+page.svelte | 81 ++++++++ .../[repo]/settings/labels/new/+page.svelte | 91 +++++++++ .../[repo]/settings/pipelines/+page.svelte | 96 ++++++++++ .../pipelines/secrets/new/+page.svelte | 44 +++++ .../[repo]/settings/rename/+page.svelte | 78 ++++++++ .../[repo]/settings/sites/+page.svelte | 140 ++++++++++++++ 26 files changed, 1155 insertions(+), 53 deletions(-) create mode 100644 web/src/lib/components/settings/SettingsBlock.svelte create mode 100644 web/src/lib/components/settings/SettingsEmpty.svelte create mode 100644 web/src/lib/components/settings/SettingsSaveBar.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/+layout.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/access/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/hooks/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/hooks/new/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/pipelines/secrets/new/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/rename/+page.svelte create mode 100644 web/src/routes/[handle]/[repo]/settings/sites/+page.svelte diff --git a/web/src/lib/components/repo/RepoTabs.svelte b/web/src/lib/components/repo/RepoTabs.svelte index 0e927d0b..4483be61 100644 --- a/web/src/lib/components/repo/RepoTabs.svelte +++ b/web/src/lib/components/repo/RepoTabs.svelte @@ -3,6 +3,7 @@ import CircleDot from "$icon/circle-dot"; import GitPullRequest from "$icon/git-pull-request"; import Layers2 from "$icon/layers-2"; + import Settings from "$icon/settings"; import Tabs, { type TabDef } from "$lib/components/ui/Tabs.svelte"; import type { RepoCounts, RepoInfo } from "./types"; @@ -32,7 +33,10 @@ count: counts.pulls, href: `${base}/pulls` }, - { id: "pipelines", label: "Pipelines", icon: Layers2, href: `${base}/pipelines` } + { id: "pipelines", label: "Pipelines", icon: Layers2, href: `${base}/pipelines` }, + // todo: only show this to collaborators — needs the viewer's push access on + // RepoInfo, which the layout load doesn't resolve yet + { id: "settings", label: "Settings", icon: Settings, href: `${base}/settings` } ]); diff --git a/web/src/lib/components/settings/DrillDown.svelte b/web/src/lib/components/settings/DrillDown.svelte index f1ba0981..35694911 100644 --- a/web/src/lib/components/settings/DrillDown.svelte +++ b/web/src/lib/components/settings/DrillDown.svelte @@ -8,11 +8,16 @@ backLabel: string; backHref: string; title: string; - description?: string; + /** pass an array to get one line each */ + description?: string | string[]; children: Snippet; } let { backLabel, backHref, title, description, children }: Props = $props(); + + const lines = $derived( + description === undefined ? [] : Array.isArray(description) ? description : [description] + ); +
+
+ {#if title} + + {title} + + {/if} + {#each lines as line (line)} +

{line}

+ {/each} +
+ {#if action} +
{@render action()}
+ {/if} +
+ {/if} + {#if separator} +
+ {/if} + {#if children} + {@render children()} + {/if} + diff --git a/web/src/lib/components/settings/SettingsEmpty.svelte b/web/src/lib/components/settings/SettingsEmpty.svelte new file mode 100644 index 00000000..910433b7 --- /dev/null +++ b/web/src/lib/components/settings/SettingsEmpty.svelte @@ -0,0 +1,15 @@ + + + +
+ {message} +
diff --git a/web/src/lib/components/settings/SettingsList.svelte b/web/src/lib/components/settings/SettingsList.svelte index 270c02d9..6f72428e 100644 --- a/web/src/lib/components/settings/SettingsList.svelte +++ b/web/src/lib/components/settings/SettingsList.svelte @@ -6,10 +6,12 @@ variants: { // Figma spaces rows with a gap and drops the hairline in the middle of it, // so the divider is split evenly across the two rows it separates rather - // than sitting flush against one of them. + // than sitting flush against one of them. Named in Tailwind spacing units + // because the designs use all three of 8 / 12 / 16px. gap: { - md: "[&>*+*]:mt-2 [&>*+*]:border-t [&>*+*]:border-border-default [&>*+*]:pt-2", - sm: "[&>*+*]:mt-1 [&>*+*]:border-t [&>*+*]:border-border-default [&>*+*]:pt-1" + "2": "[&>*+*]:mt-1 [&>*+*]:border-t [&>*+*]:border-border-default [&>*+*]:pt-1", + "3": "[&>*+*]:mt-1.5 [&>*+*]:border-t [&>*+*]:border-border-default [&>*+*]:pt-1.5", + "4": "[&>*+*]:mt-2 [&>*+*]:border-t [&>*+*]:border-border-default [&>*+*]:pt-2" }, padding: { default: "px-4 py-3", @@ -17,7 +19,7 @@ } }, defaultVariants: { - gap: "md", + gap: "4", padding: "default" } }); @@ -35,7 +37,7 @@ children: Snippet; } - let { gap = "md", padding = "default", class: className, children }: Props = $props(); + let { gap = "4", padding = "default", class: className, children }: Props = $props(); const classes = $derived(settingsList({ gap, padding, class: className })); diff --git a/web/src/lib/components/settings/SettingsRow.svelte b/web/src/lib/components/settings/SettingsRow.svelte index ee2e2b50..74bb6d9a 100644 --- a/web/src/lib/components/settings/SettingsRow.svelte +++ b/web/src/lib/components/settings/SettingsRow.svelte @@ -5,28 +5,52 @@ interface Props { /** plain-text label — omit and render whatever you need through `label` */ title?: string; + /** muted explanation under the title; pass an array to get one line each */ + description?: string | string[]; icon?: Component; /** replaces `title`/`icon` when the leading side is more than a label */ label?: Snippet; /** trailing controls: buttons, toggles, code chips */ children?: Snippet; + /** top-align the control instead of centring it, for tall right-hand content */ + align?: "center" | "start"; class?: string; } - let { title, icon, label, children, class: className }: Props = $props(); + let { + title, + description, + icon, + label, + children, + align = "center", + class: className + }: Props = $props(); const Glyph = $derived(icon); + const lines = $derived( + description === undefined ? [] : Array.isArray(description) ? description : [description] + ); -
+
{#if label} {@render label()} {:else} - - {#if Glyph} -