From 28ae878bd04599c3d236764f62a0e6f3ee13a91e Mon Sep 17 00:00:00 2001 From: Zach Dunn Date: Mon, 3 Aug 2026 14:50:02 -0400 Subject: [PATCH] feat(web): split workspace settings into comment + storage pages Implements the Claude Design "Workspace Settings" project. - `/settings` is now GitHub-comment defaults only; storage and the workspace details summary move to `/settings/storage`, reached from a nested sub-nav under the settings tab. One shared render function backs both the server render and the client-side switcher repaint. - Comment defaults become label/description rows with segmented controls, and the per-field source badges move inline onto the row they describe (with the row dimmed when a repo's `.uploads.yml` pins it), replacing the detached badge strip. - The live preview moves into a widened right rail via a new `preview` slot on `WorkspaceLayout`, so the controls and their result sit side by side. - The storage page leads with a three-step summary of what connecting a bucket involves; the existing wizard replaces it on Connect. Behavior, including the empty-workspace gate and the disconnect guards, is unchanged. - Preview fallback fixtures now use purpose-drawn generic wireframes instead of the site's own OG image, and the before/after pair are two visibly different drawings rather than the same file twice. --- apps/api/src/comment-preview-fixtures.ts | 63 +- apps/api/src/routes/me.test.ts | 8 +- apps/web/public/preview/comment-dashboard.svg | 20 + .../public/preview/comment-settings-after.svg | 24 + .../preview/comment-settings-before.svg | 20 + apps/web/src/layouts/AccountLayout.astro | 31 +- apps/web/src/layouts/WorkspaceLayout.astro | 44 +- apps/web/src/lib/workspaces-nav.ts | 63 +- .../account/workspaces/[name]/settings.astro | 1400 +++++++---------- .../workspaces/[name]/settings/storage.astro | 778 +++++++++ .../src/pages/account/workspaces/new.astro | 4 +- apps/web/src/pages/docs/byo-bucket.astro | 4 +- apps/web/src/styles/account-shell.css | 38 + 13 files changed, 1594 insertions(+), 903 deletions(-) create mode 100644 apps/web/public/preview/comment-dashboard.svg create mode 100644 apps/web/public/preview/comment-settings-after.svg create mode 100644 apps/web/public/preview/comment-settings-before.svg create mode 100644 apps/web/src/pages/account/workspaces/[name]/settings/storage.astro diff --git a/apps/api/src/comment-preview-fixtures.ts b/apps/api/src/comment-preview-fixtures.ts index a6fcad14..9ae5d2fd 100644 --- a/apps/api/src/comment-preview-fixtures.ts +++ b/apps/api/src/comment-preview-fixtures.ts @@ -1,17 +1,24 @@ /** * Static fallback items for the comment-settings preview endpoint (issue * #307, Task 6) — used whenever a workspace has no recent `gh/`-prefixed - * attachments to render a realistic preview from. All three point at the - * same real, always-loadable static asset (`og/home.png`, the site-wide OG - * fallback served by apps/web) so the preview never depends on a workspace's - * own storage; only the filenames vary, since the renderer's per-item width - * heuristic (`attachmentImageWidth`) keys off filename patterns - * (landscape/portrait) and the before/after pairing keys off `meta.state`. + * attachments to render a realistic preview from. + * + * The images are purpose-drawn generic wireframes (`/preview/*.svg`, served + * by apps/web) rather than screenshots of uploads.sh itself: the preview's + * job is to show how the *comment* lays out, and a picture of our own UI + * reads as if it were the reader's own attachment. They're always-loadable + * static assets, so the preview never depends on a workspace's own storage. + * The before/after pair are two visibly different drawings — reusing one + * image for both made the pairing look broken. * * Three items (one wide dashboard + one before/after pair) land in the * sparse density tier so the settings preview shows readable image sizes * and path/state `` captions — matching what a typical PR looks like. - * `pageUrl: null` throughout — fixtures never claim a real `/f/` file page. + * Filenames stay `.png` — they stand in for what a real uploaded screenshot + * is called, and the renderer's per-item width heuristic + * (`attachmentImageWidth`) keys off filename patterns while the before/after + * pairing keys off `meta.state`. `pageUrl: null` throughout — fixtures never + * claim a real `/f/` file page. */ import type { AttachmentItem } from "./github-comment-render"; import { webOrigin } from "./web-url"; @@ -19,33 +26,25 @@ import { webOrigin } from "./web-url"; /** * Build the fixture items against `env.WEB_ORIGIN` (via `webOrigin`, the * same single source of truth `filePageUrl` uses) instead of a hardcoded - * production origin, so the preview's fixture image resolves correctly in - * local/staging environments too. `/og/home.png` is the one path — only the - * origin varies. + * production origin, so the preview's fixture images resolve correctly in + * local/staging environments too — only the origin varies. */ export function previewFixtureItems(env: Env): AttachmentItem[] { - const fixtureImageUrl = `${webOrigin(env)}/og/home.png`; + const asset = (name: string): string => `${webOrigin(env)}/preview/${name}.svg`; + const item = ( + filename: string, + assetName: string, + meta: AttachmentItem["meta"], + ): AttachmentItem => ({ + key: `gh/preview/pull/0/${filename}`, + url: asset(assetName), + embedUrl: asset(assetName), + pageUrl: null, + meta, + }); return [ - { - key: "gh/preview/pull/0/dashboard-overview.png", - url: fixtureImageUrl, - embedUrl: fixtureImageUrl, - pageUrl: null, - meta: { path: "/dashboard", state: "after" }, - }, - { - key: "gh/preview/pull/0/settings-before.png", - url: fixtureImageUrl, - embedUrl: fixtureImageUrl, - pageUrl: null, - meta: { path: "/settings", state: "before" }, - }, - { - key: "gh/preview/pull/0/settings-after.png", - url: fixtureImageUrl, - embedUrl: fixtureImageUrl, - pageUrl: null, - meta: { path: "/settings", state: "after" }, - }, + item("dashboard-overview.png", "comment-dashboard", { path: "/dashboard", state: "after" }), + item("settings-before.png", "comment-settings-before", { path: "/settings", state: "before" }), + item("settings-after.png", "comment-settings-after", { path: "/settings", state: "after" }), ]; } diff --git a/apps/api/src/routes/me.test.ts b/apps/api/src/routes/me.test.ts index b9cc9bdb..da8291c0 100644 --- a/apps/api/src/routes/me.test.ts +++ b/apps/api/src/routes/me.test.ts @@ -2254,8 +2254,12 @@ describe("GET /me/workspaces/:name/comment-preview", () => { expect(body.body).toContain("dashboard-overview.png"); // Fixture image origin is derived from WEB_ORIGIN (previewEnv sets // "https://uploads.test"), not hardcoded to production's uploads.sh. - expect(body.body).toContain('src="https://uploads.test/og/home.png"'); - expect(body.body).not.toContain("uploads.sh/og/home.png"); + expect(body.body).toContain('src="https://uploads.test/preview/comment-dashboard.svg"'); + expect(body.body).not.toContain("uploads.sh/preview/"); + // The before/after pair are two distinct drawings, not one image twice — + // a repeated image made the pairing look broken. + expect(body.body).toContain("/preview/comment-settings-before.svg"); + expect(body.body).toContain("/preview/comment-settings-after.svg"); }); it("(e) repo not linked to this workspace -> 404", async () => { diff --git a/apps/web/public/preview/comment-dashboard.svg b/apps/web/public/preview/comment-dashboard.svg new file mode 100644 index 00000000..28587adf --- /dev/null +++ b/apps/web/public/preview/comment-dashboard.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/preview/comment-settings-after.svg b/apps/web/public/preview/comment-settings-after.svg new file mode 100644 index 00000000..dee122de --- /dev/null +++ b/apps/web/public/preview/comment-settings-after.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/preview/comment-settings-before.svg b/apps/web/public/preview/comment-settings-before.svg new file mode 100644 index 00000000..d8afd67a --- /dev/null +++ b/apps/web/public/preview/comment-settings-before.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/layouts/AccountLayout.astro b/apps/web/src/layouts/AccountLayout.astro index 1fa8c04d..239bea87 100644 --- a/apps/web/src/layouts/AccountLayout.astro +++ b/apps/web/src/layouts/AccountLayout.astro @@ -16,7 +16,9 @@ import { signedInCsp, } from "../lib/signed-in-page"; import { + renderSettingsSubnavHtml, WORKSPACE_NAV_TABS, + workspaceSettingsSubpageFromPathname, workspaceTabFromPathname, type WorkspaceNavTab, } from "../lib/workspaces-nav"; @@ -38,9 +40,11 @@ interface Props { creating?: boolean; /** Optional document title override (e.g. workspace name). */ title?: string; + /** Widen the right rail (~430px) for the comment-settings 3-column layout. */ + railWide?: boolean; } -const { section, workspace = "", creating = false, title } = Astro.props; +const { section, workspace = "", creating = false, title, railWide = false } = Astro.props; const hasRail = Astro.slots.has("rail"); const titles: Record = { @@ -54,6 +58,8 @@ const docTitle = `${pageHeading} · uploads.sh`; const activeTab: WorkspaceNavTab | "" = workspace ? workspaceTabFromPathname(Astro.url.pathname) || "files" : ""; +const settingsSubpage = + activeTab === "settings" ? workspaceSettingsSubpageFromPathname(Astro.url.pathname) : ""; const workspaceBase = workspace ? `/account/workspaces/${encodeURIComponent(workspace)}` : ""; const switcherHeading = workspace || "workspaces"; @@ -90,7 +96,7 @@ applyAuthSecurityHeaders(Astro.response.headers, signedInCsp(authOrigin, apiOrig
-
+
-
- - +
+
+
+ max inline images + +
+

Attachments beyond this collapse into a "more files" link.

+
+
+ +
-
- - +
+
+
+ metadata captions + +
+

Filename, dimensions, and size under each image.

+
+
+
+ + + +
+
-
- - + -
- +
+
+ note + + 0/500 +
+

Optional line appended to every managed comment.

-

0/500

+ rows="2" + placeholder="e.g. Screenshots update on every push.">
- +
+ - - - - +

+ Rendered from your saved settings — GitHub's own styling may differ slightly. +

+ + diff --git a/apps/web/src/pages/account/workspaces/[name]/settings/storage.astro b/apps/web/src/pages/account/workspaces/[name]/settings/storage.astro new file mode 100644 index 00000000..cb56ac0b --- /dev/null +++ b/apps/web/src/pages/account/workspaces/[name]/settings/storage.astro @@ -0,0 +1,778 @@ +--- +/** + * Workspace settings — storage sub-page: the workspace details summary + * (slug + base URL) and the Storage / BYO-bucket panel, split out of + * settings.astro (issue #365 redesign). Behavior is unchanged from before + * the split — this is the same markup, styles, and script logic, just + * without the `
` disclosure around the summary (it's its own page + * now, so there's nothing to collapse it under) and without the GitHub + * comment block, which stays on the parent settings page. + */ +import WorkspaceLayout from "../../../../../layouts/WorkspaceLayout.astro"; +import { isBrowseWorkspace } from "../../../../../lib/workspace-browse-url"; + +export const prerender = false; + +const nameParam = Astro.params.name ?? ""; +const workspace = isBrowseWorkspace(nameParam) ? nameParam : ""; +if (!workspace) return Astro.redirect("/account/workspaces"); +--- + + +
+
+

Workspace details

+
+
+ + +
+ + + + +
diff --git a/apps/web/src/pages/account/workspaces/new.astro b/apps/web/src/pages/account/workspaces/new.astro index abded2c8..22df69ff 100644 --- a/apps/web/src/pages/account/workspaces/new.astro +++ b/apps/web/src/pages/account/workspaces/new.astro @@ -242,10 +242,10 @@ export const prerender = false; } if (wantsByo) { // Drop straight into the storage wizard for the new (still - // empty) workspace — settings.astro's `loadStorage()` opens it + // empty) workspace — storage.astro's `loadStorage()` opens it // automatically on `#storage` once it confirms // `byoBucketEnabled` for real. - location.href = `/account/workspaces/${encodeURIComponent(name)}/settings#storage`; + location.href = `/account/workspaces/${encodeURIComponent(name)}/settings/storage#storage`; return; } input.value = ""; diff --git a/apps/web/src/pages/docs/byo-bucket.astro b/apps/web/src/pages/docs/byo-bucket.astro index e3bc92a7..53c067f5 100644 --- a/apps/web/src/pages/docs/byo-bucket.astro +++ b/apps/web/src/pages/docs/byo-bucket.astro @@ -1,8 +1,8 @@ --- // uploads.sh — docs / bring your own bucket. Point a workspace at your own // Cloudflare R2 bucket. Wizard copy on -// `/account/workspaces/[name]/settings.astro` (storage-step-1) is the -// source of truth for dashboard paths — keep this page in sync with it. +// `/account/workspaces/[name]/settings/storage.astro` (storage-step-1) is +// the source of truth for dashboard paths — keep this page in sync with it. import DocsLayout from "../../layouts/DocsLayout.astro"; const TOC = [ diff --git a/apps/web/src/styles/account-shell.css b/apps/web/src/styles/account-shell.css index 4bf5de14..4275fb82 100644 --- a/apps/web/src/styles/account-shell.css +++ b/apps/web/src/styles/account-shell.css @@ -234,6 +234,44 @@ display: none; } +/* Settings sub-nav (github comment / storage) — indented under the settings + tab with a left hairline, shown only while a settings route is active. */ +.account-shell nav.side .ws-settings-subnav { + display: grid; + gap: 2px; + margin: 2px 0 2px 12px; + padding-left: 10px; + border-left: 1px solid var(--line); +} +.account-shell nav.side .ws-settings-subnav__link { + display: block; + padding: 5px 8px; + border-radius: 6px; + color: var(--muted); + font-size: 12px; + text-decoration: none; +} +.account-shell nav.side .ws-settings-subnav__link:hover, +.account-shell nav.side .ws-settings-subnav__link:focus-visible { + color: var(--fg); + outline: none; +} +.account-shell nav.side .ws-settings-subnav__link[aria-current] { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 10%, transparent); +} + +/* Widened rail for the comment-settings 3-column layout (nav · content · + preview). Only above --bp-rail (1080px, see signed-in-shell.css) — the + mid-width and stack breakpoints below already collapse `.layout` back to + two/one columns via the plain (lower-specificity) selector, and this rule + must stay out of their way rather than out-specificity them. */ +@media (min-width: 1081px) { + .account-shell .layout[data-rail-wide] { + grid-template-columns: 180px minmax(420px, 1fr) 430px; + } +} + /* Right rail — always a grid track; content from the `rail` slot. */ .account-shell aside.rail { display: grid;