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}
-
- {/if}
- {title}
+
+
+ {#if Glyph}
+
+ {/if}
+ {title}
+
+ {#each lines as line (line)}
+ {line}
+ {/each}
{/if}
{#if children}
diff --git a/web/src/lib/components/settings/SettingsSaveBar.svelte b/web/src/lib/components/settings/SettingsSaveBar.svelte
new file mode 100644
index 00000000..48c45a08
--- /dev/null
+++ b/web/src/lib/components/settings/SettingsSaveBar.svelte
@@ -0,0 +1,23 @@
+
+
+
+
+ {#if dirty}
+ Reset
+ Unsaved changes
+ {/if}
+ Save
+
diff --git a/web/src/lib/components/settings/SettingsSection.svelte b/web/src/lib/components/settings/SettingsSection.svelte
index 84e17a4f..e51c4750 100644
--- a/web/src/lib/components/settings/SettingsSection.svelte
+++ b/web/src/lib/components/settings/SettingsSection.svelte
@@ -1,24 +1,31 @@
-
+
{#if title}
{title}
{/if}
{#if framed}
-
+
{@render children()}
{:else}
diff --git a/web/src/lib/components/settings/tabs/EmailsTab.svelte b/web/src/lib/components/settings/tabs/EmailsTab.svelte
index 03931c8b..05adeb95 100644
--- a/web/src/lib/components/settings/tabs/EmailsTab.svelte
+++ b/web/src/lib/components/settings/tabs/EmailsTab.svelte
@@ -1,7 +1,7 @@
-{#snippet actions()}
-
- {#if dirty}
- Reset
- Unsaved changes
- {/if}
- Save
-
-{/snippet}
-
Profile
- {@render actions()}
+
@@ -163,5 +152,5 @@
- {@render actions()}
+
diff --git a/web/src/lib/components/settings/tabs/SitesTab.svelte b/web/src/lib/components/settings/tabs/SitesTab.svelte
index c8a554ce..a87588b7 100644
--- a/web/src/lib/components/settings/tabs/SitesTab.svelte
+++ b/web/src/lib/components/settings/tabs/SitesTab.svelte
@@ -1,7 +1,7 @@
+
+
+
+
+
+ {@render children()}
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/+page.svelte b/web/src/routes/[handle]/[repo]/settings/+page.svelte
new file mode 100644
index 00000000..008ce5ef
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/+page.svelte
@@ -0,0 +1,177 @@
+
+
+
+
General
+
+
+
+
+
+
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/access/+page.svelte b/web/src/routes/[handle]/[repo]/settings/access/+page.svelte
new file mode 100644
index 00000000..90a42a42
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/access/+page.svelte
@@ -0,0 +1,69 @@
+
+
+
+
Access
+
+
+
+ {#snippet action()}
+ Add collaborator
+ {/snippet}
+
+
+ {#each collaborators as person (person.handle)}
+
+ {#snippet label()}
+
+
+
+ {person.handle}
+
+ {person.role}
+
+ {/snippet}
+ {#if !person.owner}
+ remove(person.handle)}>
+ Remove collaborator
+
+ {/if}
+
+ {/each}
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte b/web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte
new file mode 100644
index 00000000..38e07bdc
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ Add
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/hooks/+page.svelte b/web/src/routes/[handle]/[repo]/settings/hooks/+page.svelte
new file mode 100644
index 00000000..af0dfd29
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/hooks/+page.svelte
@@ -0,0 +1,97 @@
+
+
+
+
Hooks
+
+
+
+ {#snippet action()}
+ New webhook
+ {/snippet}
+
+ {#if hooks.length === 0}
+
+ {:else}
+
+ {#each hooks as hook (hook.id)}
+
+
+
+
{hook.url}
+
+
+ {hook.events}
+
+
+ {hook.added}
+
+ {hook.by}
+
+
+
+
+
+ Deliveries
+ Edit
+ remove(hook.id)}>Delete
+
+
+
+ {/each}
+
+ {/if}
+
diff --git a/web/src/routes/[handle]/[repo]/settings/hooks/new/+page.svelte b/web/src/routes/[handle]/[repo]/settings/hooks/new/+page.svelte
new file mode 100644
index 00000000..3e95e147
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/hooks/new/+page.svelte
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each EVENTS as event (event.id)}
+ {event.label}
+ {/each}
+
+
+
+
+
+ Cancel
+ Add
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte b/web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte
new file mode 100644
index 00000000..cccdeccc
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/labels/new/+page.svelte
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+ Basic
+ Key-value
+
+
+
+
+
+
+
+
+
+ Issues
+ Pull requests
+
+
+
+
+
+ {#each COLORS as swatch (swatch)}
+ (color = swatch)}
+ aria-label="Use colour {swatch}"
+ aria-pressed={color === swatch}
+ class="size-5 cursor-pointer rounded-full transition-shadow {color === swatch
+ ? 'ring-2 ring-foreground-default ring-offset-2 ring-offset-background-default'
+ : ''}"
+ style="background-color: {swatch}"
+ >
+ {/each}
+
+
+
+
+
+ Cancel
+ Add
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte b/web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte
new file mode 100644
index 00000000..e6cb2013
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/pipelines/+page.svelte
@@ -0,0 +1,96 @@
+
+
+
+
Pipelines
+
+
+
+ {#snippet action()}
+
+ {/snippet}
+
+
+
+
+ {#each spindles as option (option)}
+ {option}
+ {/each}
+
+
+
+
+
+
+ {#snippet action()}
+ Add secret
+ {/snippet}
+
+
+ {#each secrets as secret (secret.name)}
+
+ {#snippet label()}
+
+
+ {secret.name}
+
+
+ {secret.added}
+
+ {secret.by}
+
+
+ {/snippet}
+ remove(secret.name)}>Delete
+
+ {/each}
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/pipelines/secrets/new/+page.svelte b/web/src/routes/[handle]/[repo]/settings/pipelines/secrets/new/+page.svelte
new file mode 100644
index 00000000..38944be7
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/pipelines/secrets/new/+page.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ Add
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/rename/+page.svelte b/web/src/routes/[handle]/[repo]/settings/rename/+page.svelte
new file mode 100644
index 00000000..dc38e7b0
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/rename/+page.svelte
@@ -0,0 +1,78 @@
+
+
+
+
+ {#each stableUrls as url (url)}
+
+
+ {url}
+
+ copy(url)}
+ aria-label="Copy {url}"
+ class="cursor-pointer text-foreground-muted hover:text-foreground-default"
+ >
+ {#if copied === url}
+
+ {:else}
+
+ {/if}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+ Cancel
+ Add
+
+
diff --git a/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte b/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte
new file mode 100644
index 00000000..c94a462d
--- /dev/null
+++ b/web/src/routes/[handle]/[repo]/settings/sites/+page.svelte
@@ -0,0 +1,140 @@
+
+
+
+
Sites
+
+
+
+
+
+ Serve a static site directly from this repository. Choose a branch and the directory containing
+ your index.html.
+ Only repository owners can configure sites.
+
+
+ {#if deployed}
+
+ Live at {domain}
+ Open
+
+ {/if}
+
+
+
+
+
+ {#each branches as branch (branch.value)}
+ {branch.label}
+ {/each}
+
+
+
+
+
+
+
+
+
+
+ Index site
+ {domain}
+
+
+ Sub-path site
+ {subPath}
+
+
+
+
+ {#if deployed}
+
+
+
+ {/if}
+
+
+
+
+ 0}>
+ {#if deploys.length === 0}
+
+ {:else}
+ {#each deploys as deploy (deploy.branch)}
+
+ {#snippet label()}
+
+
+ {deploy.branch}
+
+ {deploy.status}
+ {deploy.reason}
+
+ {/snippet}
+ {deploy.when}
+
+ {/each}
+ {/if}
+