From 9d07c428df3c320862478977d99d568e1fbc5d3f Mon Sep 17 00:00:00 2001 From: Nico Ritschel Date: Tue, 13 Oct 2026 19:50:36 -0700 Subject: [PATCH] Polish gallery layout, forms, and navigation Slim the detail header to 44px with tabs that sit on its border, show the open Schedule or Settings view as a tab, and mark historical revisions with a chip and a checked Versions menu. Import project becomes its own view. Content buttons share one outlined style, inputs match selects, code text uses a shared monospace token, and disclosures use the custom chevron. Library rows stay one line with status dots, Worker apps get a status summary, schedule runs show status dots without repeated UTC labels, secret removal asks for confirmation, and the light focus ring meets 3:1 contrast. --- e2e/gallery-drafts.test.ts | 4 +- e2e/gallery-navigation.test.ts | 12 ++- e2e/gallery-secrets.test.ts | 2 + e2e/gallery-transfer.test.ts | 12 +-- src/gallery/client.tsx | 56 +++++++++----- src/gallery/execution-controls.tsx | 16 ++-- src/gallery/hosted.tsx | 2 +- src/gallery/library-navigation.tsx | 15 +++- src/gallery/move.tsx | 2 +- src/gallery/native-apps.tsx | 57 +++++++++----- src/gallery/project-transfer.tsx | 22 +++--- src/gallery/remix.tsx | 2 +- src/gallery/secrets.tsx | 11 ++- src/gallery/source-editor.tsx | 2 +- src/gallery/styles.ts | 120 +++++++++++++++++------------ src/gallery/theme.ts | 11 ++- src/gallery/workspace.tsx | 4 +- src/runtime/mcp-file-editor.tsx | 2 +- 18 files changed, 221 insertions(+), 131 deletions(-) diff --git a/e2e/gallery-drafts.test.ts b/e2e/gallery-drafts.test.ts index cb417a8..ca7e748 100644 --- a/e2e/gallery-drafts.test.ts +++ b/e2e/gallery-drafts.test.ts @@ -69,13 +69,13 @@ test("gallery retains complete unsaved projects and new scripts and offers confl await page.getByRole("textbox", { name: "Project dependencies" }).fill("{}"); await page.getByRole("button", { name: "More actions", exact: true }).click(); await page.getByRole("menuitem", { name: "Versions", exact: true }).click(); - await page.getByRole("menuitem", { name: /^Revision 1 ·/ }).click(); + await page.getByRole("menuitemradio", { name: /^Revision 1 ·/ }).click(); await page.getByRole("button", { name: "Restore and deploy", exact: true }).waitFor(); expect(await client.innerText()).toBe("historical client"); expect(await page.locator(".live-data-note").innerText()).toContain("Historical code uses the current database and files and can change them"); await page.getByRole("button", { name: "More actions", exact: true }).click(); await page.getByRole("menuitem", { name: "Versions", exact: true }).click(); - await page.getByRole("menuitem", { name: "Working copy", exact: true }).click(); + await page.getByRole("menuitemradio", { name: "Working copy", exact: true }).click(); expect(await client.innerText()).toBe("unsaved client"); await page.getByRole("button", { name: "Save and deploy", exact: true }).click(); await page.getByRole("button", { name: "Compare saved project" }).waitFor(); diff --git a/e2e/gallery-navigation.test.ts b/e2e/gallery-navigation.test.ts index 5c95ed1..68fa86a 100644 --- a/e2e/gallery-navigation.test.ts +++ b/e2e/gallery-navigation.test.ts @@ -154,17 +154,24 @@ test("gallery filters, previews revisions, and preserves mobile library navigati await screenshot(desktop, "gallery-desktop-source"); await desktop.getByRole("button", { name: "More actions", exact: true }).click(); await desktop.getByRole("menuitem", { name: "Versions", exact: true }).click(); - await desktop.getByRole("menuitem", { name: /^Revision 1 ·/ }).click(); + expect(await desktop.getByRole("menuitemradio", { name: "Working copy", exact: true }).getAttribute("aria-checked")).toBe("true"); + await desktop.getByRole("menuitemradio", { name: /^Revision 1 ·/ }).click(); await desktop.getByRole("button", { name: "Restore and deploy", exact: true }).waitFor(); + expect(await desktop.locator(".detail-header .revision-chip").innerText()).toStartWith("Revision 1"); await desktop.waitForFunction(() => document.querySelector('[role="textbox"][aria-label="Campaign overview.artifact.tsx"]')?.textContent?.includes("Campaign revision 1")); expect(await source.getAttribute("aria-readonly")).toBe("true"); await desktop.getByRole("tablist", { name: "Artifact view" }).getByRole("tab", { name: "Preview", exact: true }).click(); await desktop.frameLocator('iframe[title="Preview of Campaign overview"]').getByRole("heading", { name: "Campaign revision 1" }).waitFor(); expect(new URL(await desktop.getByTitle("Preview of Campaign overview").getAttribute("src") ?? "", server.url).searchParams.get("version")).toBe("campaign-revision-1"); + await screenshot(desktop, "gallery-desktop-revision"); await desktop.getByRole("button", { name: "More actions", exact: true }).click(); await desktop.getByRole("menuitem", { name: "Versions", exact: true }).click(); - await desktop.getByRole("menuitem", { name: "Working copy", exact: true }).click(); + expect(await desktop.getByRole("menuitemradio", { name: /^Revision 1 ·/ }).getAttribute("aria-checked")).toBe("true"); + await desktop.getByRole("menuitemradio", { name: /^Revision 1 ·/ }).click(); + await desktop.getByRole("menuitemradio", { name: /^Revision 1 ·/ }).waitFor({ state: "hidden" }); + await desktop.getByRole("button", { name: "Return to working copy", exact: true }).click(); await desktop.frameLocator('iframe[title="Preview of Campaign overview"]').getByRole("heading", { name: "Campaign performance" }).waitFor(); + expect(await desktop.locator(".detail-header .revision-chip").count()).toBe(0); await desktop.getByRole("button", { name: "Share", exact: true }).click(); await desktop.getByRole("textbox", { name: "URL slug", exact: true }).fill("campaign-report"); expect(calls).toEqual([]); @@ -234,6 +241,7 @@ test("gallery filters, previews revisions, and preserves mobile library navigati await desktop.getByRole("button", { name: "More actions", exact: true }).click(); await desktop.getByRole("menuitem", { name: "Schedule", exact: true }).click(); await desktop.getByRole("status").filter({ hasText: /^Enabled$/ }).waitFor(); + expect(await desktop.getByRole("tablist", { name: "Artifact view" }).getByRole("tab", { name: "Schedule", exact: true }).getAttribute("aria-selected")).toBe("true"); await desktop.getByText("Last result: succeeded", { exact: false }).waitFor(); expect(await desktop.getByLabel("Schedule headers").isVisible()).toBe(false); expect(await desktop.getByLabel("Schedule path").inputValue()).toBe("/tick"); diff --git a/e2e/gallery-secrets.test.ts b/e2e/gallery-secrets.test.ts index ff16725..483b2f1 100644 --- a/e2e/gallery-secrets.test.ts +++ b/e2e/gallery-secrets.test.ts @@ -54,6 +54,8 @@ test("gallery exposes server secret names and writes for artifacts and scripts w expect(calls.at(-1)).toEqual({ name: `${kind}_secrets`, arguments: { name, secrets: { TOKEN: "typed-fixture-value" } } }); if (kind === "artifact" && process.env.GALLERY_SCREENSHOT_DIR) await page.screenshot({ path: `${process.env.GALLERY_SCREENSHOT_DIR}/artifact-secrets.png`, fullPage: true }); await page.getByRole("button", { name: "Remove secret", exact: true }).click(); + expect(calls.at(-1)!.arguments).toEqual({ name, secrets: { TOKEN: "typed-fixture-value" } }); + await page.getByRole("button", { name: "Remove", exact: true }).click(); await page.getByText("Secret removed", { exact: true }).waitFor(); expect(calls.at(-1)).toEqual({ name: `${kind}_secrets`, arguments: { name, secrets: { TOKEN: null } } }); } diff --git a/e2e/gallery-transfer.test.ts b/e2e/gallery-transfer.test.ts index 1a622e7..c117e59 100644 --- a/e2e/gallery-transfer.test.ts +++ b/e2e/gallery-transfer.test.ts @@ -20,14 +20,16 @@ test("gallery imports a fresh complete project and exports its saved snapshot th await page.getByRole("menuitem", { name: "Import project", exact: true }).click(); await page.getByLabel("Project archive", { exact: true }).setInputFiles({ name: "portable.artifact-project.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify(archive)) }); await page.waitForFunction(() => (document.querySelector('[aria-label="Imported project name"]') as HTMLInputElement)?.value === "portable-import"); - expect(await page.getByText("Includes backend source", { exact: false }).innerText()).toContain("1 helper files"); - await page.getByRole("button", { name: "Import as new project", exact: true }).click(); - await page.getByRole("status").filter({ hasText: "Project imported." }).waitFor(); + expect(await page.getByText("Includes backend source", { exact: false }).innerText()).toContain("1 helper file · 0 dependencies"); + expect(await page.getByRole("heading", { level: 1 }).innerText()).toBe("Import project"); + expect(await page.getByRole("tab").count()).toBe(0); await page.getByRole("button", { name: "Import as new project", exact: true }).hover(); expect(await page.getByRole("button", { name: "Import as new project", exact: true }).evaluate(element => getComputedStyle(element).backgroundColor)).toBe("rgb(42, 39, 35)"); - expect(await page.getByRole("heading", { level: 1 }).innerText()).toBe("portable-import"); if (process.env.GALLERY_SCREENSHOT_DIR) { await mkdir(process.env.GALLERY_SCREENSHOT_DIR, { recursive: true }); await page.screenshot({ path: join(process.env.GALLERY_SCREENSHOT_DIR, "gallery-project-import.png"), fullPage: true }); } - await page.getByRole("button", { name: "Close import", exact: true }).click(); + await page.getByRole("button", { name: "Import as new project", exact: true }).click(); + await page.getByRole("status").filter({ hasText: "Project imported." }).waitFor(); + expect(await page.getByRole("heading", { level: 1 }).innerText()).toBe("portable-import"); + expect(await page.getByRole("button", { name: "Import as new project", exact: true }).count()).toBe(0); await page.getByRole("button", { name: "More actions", exact: true }).click(); const [download] = await Promise.all([page.waitForEvent("download"), page.getByRole("menuitem", { name: "Download", exact: true }).click()]); expect(download.suggestedFilename()).toBe("portable-import.artifact-project.json"); diff --git a/src/gallery/client.tsx b/src/gallery/client.tsx index 26ce7a0..0c19e75 100644 --- a/src/gallery/client.tsx +++ b/src/gallery/client.tsx @@ -118,6 +118,7 @@ function App() { const [showLinks, setShowLinks] = useState(false); const [showMove, setShowMove] = useState(false); const [showImport, setShowImport] = useState(false); + const [notice, setNotice] = useState(""); const [sourceVisited, setSourceVisited] = useState(null); const [mobileDetail, setMobileDetail] = useState(() => workerSelection() !== null); const [narrowLayout, setNarrowLayout] = useState(() => window.matchMedia("(max-width: 760px)").matches); @@ -292,6 +293,8 @@ function App() { useEffect(() => { setSelectedVersion(resolvedVersion); }, [resolvedVersion]); + const viewingItem = !creatingKind && !showImport && !!selectedArtifact && !!resolvedVersion; + const viewedRevision = resolvedVersion !== WORKING_VERSION ? sortedVersions.find(version => version.id === resolvedVersion) : undefined; useEffect(() => { if (activeTab === "source" && selectedArtifact && resolvedVersion) setSourceVisited(`${selectedArtifact.key}:${resolvedVersion}`); @@ -406,6 +409,8 @@ function App() { setShowLinks(false); setShowMove(false); setCreatingKind(null); + setShowImport(false); + setNotice(""); setRemixing(false); setSelectedKey(artifact.key); setSelectedVersion(artifact.working ? WORKING_VERSION : [...artifact.versions].sort((a, b) => b.revision - a.revision)[0]?.id ?? null); @@ -438,6 +443,7 @@ function App() { setCreatingKind(kind); setTab("source"); setShowImport(false); + setNotice(""); setRemixing(false); setShowLinks(false); setShowMove(false); @@ -543,7 +549,7 @@ function App() { startCreating("artifact")}>New artifact {gallery?.capabilities?.scripts ? startCreating("script")}>New script : null} {gallery?.capabilities?.nativeApps ? selectWorker({ workspace: gallery.workspace, name: null })}>New Worker app : null} - { if (!leaveWorker()) return; setShowImport(true); setCreatingKind(null); setMobileDetail(true); }}>Import project + { if (!leaveWorker()) return; setShowImport(true); setCreatingKind(null); setShowLinks(false); setShowMove(false); setRemixing(false); setNotice(""); setMobileDetail(true); }}>Import project @@ -555,7 +561,7 @@ function App() { const url = new URL(window.location.href); url.searchParams.set("library", event.target.value); window.location.assign(url.href); - }}> : null} + }}> : null} {user ?
{user.name} @@ -569,7 +575,7 @@ function App() {
[...drafts.entries()].some(([key, draft]) => key.startsWith(artifact.key + ":") && draft.dirty)} loading={loading} error={galleryError} onRetry={() => void loadGallery()} connected={liveConnected} @@ -583,18 +589,22 @@ function App() {
-

{creatingKind ? `New ${creatingKind}` : selectedArtifact?.name ?? "Your artifacts"}

+

{showImport ? "Import project" : creatingKind ? `New ${creatingKind}` : selectedArtifact?.name ?? "Your artifacts"}

+ {viewingItem && viewedRevision ? Revision {viewedRevision.revision} + {selectedArtifact.working ? : null} + : null}
- {!creatingKind && selectedArtifact && resolvedVersion ? <> + {viewingItem ? <> - {(selectedArtifact.kind === "script" - ? [["source", "Source"], ["requests", "Requests"]] as const - : [["preview", "Preview"], ["source", "Source"]] as const - ).map(([value, label]) => {label})} + {([ + ...(selectedArtifact.kind === "script" ? [["source", "Source"], ["requests", "Requests"]] : [["preview", "Preview"], ["source", "Source"]]), + // Schedule and Settings open from More actions; while one is open it shows here so the header says where you are. + ...(activeTab === "schedule" ? [["schedule", "Schedule"]] : activeTab === "settings" ? [["settings", "Settings"]] : []), + ] as [DetailTab, string][]).map(([value, label]) => {label})} : null} - {!creatingKind && selectedArtifact && resolvedVersion ?
- + {viewingItem ?
+ {selectedArtifact.kind !== "script" && selectedArtifact.url ? Open : null} @@ -606,8 +616,10 @@ function App() { Versions - {selectedArtifact.working ? setSelectedVersion(WORKING_VERSION)}>Working copy : null} - {sortedVersions.map(version => setSelectedVersion(version.id)}>Revision {version.revision} · {formatDate(version.createdAt)})} + setSelectedVersion(value as string)}> + {selectedArtifact.working ? Working copy : null} + {sortedVersions.map(version => Revision {version.revision} · {formatDate(version.createdAt)})} + {gallery?.capabilities?.moves && gallery.libraryScope ? { setShowMove(true); setShowLinks(false); setRemixing(false); }}>Move : null} @@ -618,14 +630,16 @@ function App() {
: null}
- {showImport && gallery ?
setShowImport(false)} onSaved={async (name, kind) => { createdRemix.current = { name, kind, workspace: gallery.workspace }; await loadGallery(); setSelectedVersion("working"); setTab("source"); setQuery(""); setKindFilter("all"); }} />
: null} - - {!creatingKind && selectedArtifact?.kind !== "script" && selectedArtifact && resolvedVersion !== "working" && gallery?.capabilities?.links ?

Live data · Historical code uses the current database and files and can change them. Restore deploys this code while keeping current data.

: null} - {showMove && !creatingKind && selectedArtifact && gallery?.libraryScope && gallery.capabilities?.moves ?
setShowMove(false)} />
: null} - {remixing && !creatingKind && selectedArtifact && resolvedVersion ?
setRemixing(false)} onSaved={async name => { createdRemix.current = {name, workspace:selectedArtifact.workspace, kind:selectedArtifact.kind ?? "artifact"}; await loadGallery(); setSelectedVersion("working"); setQuery(""); setKindFilter("all"); setRemixing(false); }} />
: null} - {!creatingKind && selectedArtifact && resolvedVersion ? : null} - - {creatingKind === "artifact" ? setCreatingKind(null)} onSaved={name => createdItem(name, "artifact")} /> + {notice ?

{notice}

: null} + {viewingItem && selectedArtifact.kind !== "script" && resolvedVersion !== "working" && gallery?.capabilities?.links ?

Live data · Historical code uses the current database and files and can change them. Restore deploys this code while keeping current data.

: null} + {viewingItem && showMove && gallery?.libraryScope && gallery.capabilities?.moves ?
setShowMove(false)} />
: null} + {viewingItem && remixing ?
setRemixing(false)} onSaved={async name => { createdRemix.current = {name, workspace:selectedArtifact.workspace, kind:selectedArtifact.kind ?? "artifact"}; await loadGallery(); setSelectedVersion("working"); setQuery(""); setKindFilter("all"); setRemixing(false); }} />
: null} + {viewingItem ? : null} + + {showImport && gallery ? setShowImport(false)} + onSaved={async (name, kind) => { createdRemix.current = { name, kind, workspace: gallery.workspace }; await loadGallery(); setSelectedVersion("working"); setTab("source"); setQuery(""); setKindFilter("all"); }} + onImported={message => { setShowImport(false); setNotice(message); }} /> + : creatingKind === "artifact" ? setCreatingKind(null)} onSaved={name => createdItem(name, "artifact")} /> : creatingKind === "script" ? setCreatingKind(null)} onSaved={name => createdItem(name, "script")} /> : selectedArtifact?.kind === "script" ? { setSelectedVersion("working"); await loadGallery(); }} /> : !selectedArtifact && gallery?.capabilities?.links ? startCreating("artifact")} /> diff --git a/src/gallery/execution-controls.tsx b/src/gallery/execution-controls.tsx index 12ae16d..c6efddc 100644 --- a/src/gallery/execution-controls.tsx +++ b/src/gallery/execution-controls.tsx @@ -57,7 +57,9 @@ export function ExecutionControls({ workspace, name, kind = "script", active = t if (!active || loaded) return; void perform(async () => { await control("get"); await loadRuns(); }); }, [active, workspace, name]); - const displayTime = (value: number | string) => new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(new Date(value)) + " (UTC)"; + const utcTime = (value: number | string) => new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(new Date(value)); + const displayTime = (value: number | string) => `${utcTime(value)} (UTC)`; + const runTone = (status: string) => status === "succeeded" ? "success" : status === "running" ? "warning" : "error"; let preview = ""; if (loaded) { try { preview = `Next run if saved now: ${displayTime(nextOccurrence(timing()))}`; } catch (error) { preview = error instanceof Error ? error.message : String(error); } } let frequency = ""; @@ -72,12 +74,14 @@ export function ExecutionControls({ workspace, name, kind = "script", active = t

Schedule

{!loaded ?

{busy ? "Loading schedule…" : "Load current settings before editing."}

: <> -

{schedule ? schedule.paused ? "Paused" : "Enabled" : "Not configured"}

+

{schedule ?

{schedule.cron ? `${schedule.cron} · ${schedule.timezone ?? "UTC"}` : frequency} · {schedule.next_run_at === null ? "No next run" : `Next run: ${displayTime(schedule.next_run_at)}`}

: null} {runs[0] ?

Last result: {runs[0].status} · {displayTime(runs[0].started_at)}

: null} } - - {schedule ? <> : null} +
+ + {schedule ? <> : null} +
{loaded && !hasServer ?

This artifact has no validated server. Add and deploy a server handler in Source to schedule requests.

:
{schedule ? "Edit schedule" : "Create schedule"}
{ event.preventDefault(); if (loaded && !busy) void perform(() => control("set")); }}> @@ -104,8 +108,8 @@ export function ExecutionControls({ workspace, name, kind = "script", active = t

Recent runs

{runsLoaded && !runs.length ?

No runs recorded yet.

: null} -

HTTP-handler outcomes; background tasks and streamed response completion are separate. Latest 100 of up to 1,000 retained runs.

-
{kind === "script" ? : null}{runs.map(run => {kind === "script" ? : null})}
Started (UTC)TriggerStatusHTTPDurationRevisionLogs
{displayTime(run.started_at)}{run.trigger}{run.status}{run.http_status ?? "—"}{run.duration_ms === null ? "—" : `${run.duration_ms} ms`}{run.revision.slice(0, 12)}
+
{kind === "script" ? : null}{runs.map(run => {kind === "script" ? : null})}
Started (UTC)TriggerStatusHTTPDurationRevisionLogs
{utcTime(run.started_at)}{run.trigger}{run.http_status ?? "—"}{run.duration_ms === null ? "—" : `${run.duration_ms} ms`}{run.revision.slice(0, 12)}
+

Status reflects the HTTP response; background tasks and streamed bodies can finish later. Showing the latest 100 of up to 1,000 retained runs.

{logs ?
{logs}
: null}
; diff --git a/src/gallery/hosted.tsx b/src/gallery/hosted.tsx index bf4ac3b..a1dff10 100644 --- a/src/gallery/hosted.tsx +++ b/src/gallery/hosted.tsx @@ -69,7 +69,7 @@ export function LinkSettings({ artifact, onSaved, localUrl, openLink }: { artifa {localUrl ?

This link works only on this Mac while the local server is running. Publish the artifact to a hosted library to share it with other people.

: <> - +

Private links use the library’s access. Public links allow anyone through the app’s access check; deployment authentication may still apply. This setting does not move the item between libraries.

} {status} diff --git a/src/gallery/library-navigation.tsx b/src/gallery/library-navigation.tsx index 6715084..f2d24e0 100644 --- a/src/gallery/library-navigation.tsx +++ b/src/gallery/library-navigation.tsx @@ -4,6 +4,10 @@ import type { GalleryArtifact, GalleryData, GalleryItem, GalleryWorker } from ". export type LibraryKindFilter = "all" | "artifact" | "script" | "worker"; +export function workerStatusTone(status: string): "success" | "warning" | "error" | undefined { + return status === "active" ? "success" : status === "deploying" ? "warning" : status === "recovery-required" ? "error" : undefined; +} + /** Shared navigation for the web gallery and the chat workspace. */ export function LibraryNavigation({ gallery, artifacts, query, onQueryChange, kindFilter, onKindFilterChange, selectedKey, onSelect, onSelectWorker, isDirty, loading, error, onRetry, connected = true, panelRef, searchRef, hidden, footer, @@ -41,11 +45,14 @@ export function LibraryNavigation({ gallery, artifacts, query, onQueryChange, ki title={`${artifact.workspace}/${artifact.name}`} aria-current={artifact.key === selectedKey ? "true" : undefined} onClick={() => artifact.kind === "worker" ? onSelectWorker?.(artifact) : onSelect(artifact)} onFocus={event => event.currentTarget.scrollIntoView({ block: "nearest" })}>
{query || kindFilter !== "all" ?
{artifacts.length} of {libraryItems.length} items
: null} diff --git a/src/gallery/move.tsx b/src/gallery/move.tsx index 65b8e6c..b316055 100644 --- a/src/gallery/move.tsx +++ b/src/gallery/move.tsx @@ -32,7 +32,7 @@ export function MovePanel({ artifact, library, onCancel }: { }}>

Move {artifact.name} to {target}?

{destination === "team" ? "Everyone with team library access will be able to view and edit it, including its data and secrets." : "It will leave the team library. Only you will have personal library access."} Its history, data, secrets, schedules, and URL stay with it. Public links remain public.

-
+
{error ?

{error}

: null} ; } diff --git a/src/gallery/native-apps.tsx b/src/gallery/native-apps.tsx index e6c5cba..53ab37b 100644 --- a/src/gallery/native-apps.tsx +++ b/src/gallery/native-apps.tsx @@ -3,6 +3,7 @@ import { GalleryToolError, galleryTool } from "./hosted"; import { ProjectEditor, editableProject, emptyEditableProject, type EditableProject } from "./project-editor"; import { SecretControls } from "./secrets"; import { Select } from "./select"; +import { workerStatusTone } from "./library-navigation"; import { Tabs } from "@base-ui/react/tabs"; import type { AppManifest } from "../../cloudflare/native-worker/manifest"; @@ -208,6 +209,16 @@ export function NativeAppEditor({ workspace, appName, providers, onNavigate, onS .worker-workspace fieldset { min-width: 0; } .worker-workspace .script-fields > label { max-width: 100%; } .worker-workspace .detail-actions .open-link { display: inline-flex; } + .worker-workspace .worker-content > :is(.worker-summary, .detail-notice) { margin: 0; } + .worker-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; padding: 10px 16px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 12px; } + .worker-summary > span + span::before { content: "·"; margin-right: 8px; color: var(--subtle); } + .worker-summary .worker-state { display: inline-flex; align-items: center; gap: 6px; color: var(--text); font-weight: 500; } + .worker-workspace [role="tabpanel"]:not(.worker-source-panel) { max-width: 760px; } + .worker-workspace [role="tabpanel"] > h2 { margin: 0 0 6px; font-size: 14px; font-weight: 600; } + .worker-workspace [role="tabpanel"] > h3 { margin: 24px 0 6px; font-size: 13px; font-weight: 600; } + .worker-workspace .worker-content > h3 { font-size: 13px; font-weight: 600; } + .worker-workspace legend { padding: 0; margin-bottom: 8px; font-size: 12px; font-weight: 600; } + .worker-manifest { display: block; width: 100%; min-height: 240px; margin-top: 6px; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font: 12px/1.6 var(--font-mono); } @media (max-width: 1100px) { .worker-workspace .detail-header { flex-wrap: wrap; gap: 8px; } .worker-workspace .view-control { order: 3; width: 100%; } @@ -224,12 +235,27 @@ export function NativeAppEditor({ workspace, appName, providers, onNavigate, onS {appUrl ? : null}
+ {detail ? ( +

+ {selected ? ( + + + ) : ( + New draft + )} + {provider} + {selected ? Saved {selected.revision_token.slice(0, 12)} : null} + {selected?.active_revision ? Live {selected.active_revision.slice(0, 12)} : null} +

+ ) : null} {error ? (

{error}

) : null} - {status ?

{status}

: null} + {status ?

{status}

: null} {appName && !selected && busy ?

Loading Worker app…

: null} {selected && !providerAvailable ? (

@@ -312,19 +338,6 @@ export function NativeAppEditor({ workspace, appName, providers, onNavigate, onS ) : null} {detail ? ( <> -

- {provider} - {selected ? ( - - Deployment: {selected.status.replaceAll("-", " ")} - {` · saved ${selected.revision_token.slice(0, 12)}`} - {selected.desired_revision ? ` · desired ${selected.desired_revision.slice(0, 12)}` : ""} - {selected.active_revision ? ` · active ${selected.active_revision.slice(0, 12)}` : ""} - - ) : ( - New draft - )} -
{selected?.error ?

{selected.error}

: null}

- Desired revision: {selected.desired_revision?.slice(0, 12) || "None"} · Active revision:{" "} - {selected.active_revision?.slice(0, 12) || "None"} + Desired revision {selected.desired_revision?.slice(0, 12) || "none"} · Active revision{" "} + {selected.active_revision?.slice(0, 12) || "none"}

- +
+ + +
{error ?

{error}

: null} {diagnostics ?
Import diagnostics
{JSON.stringify(diagnostics, null, 2)}
: null} - {status ?

{status}

: null}
; } diff --git a/src/gallery/remix.tsx b/src/gallery/remix.tsx index 0642aad..00d8513 100644 --- a/src/gallery/remix.tsx +++ b/src/gallery/remix.tsx @@ -22,7 +22,7 @@ export function RemixPanel({ artifact, version, onSaved, onCancel }: { }}> Fresh data and secrets. Hosted URLs start private. - + {error ? {error} : null} ; diff --git a/src/gallery/secrets.tsx b/src/gallery/secrets.tsx index 1d392d2..e652977 100644 --- a/src/gallery/secrets.tsx +++ b/src/gallery/secrets.tsx @@ -11,6 +11,7 @@ export function SecretControls({ workspace, name, kind, onSaved, active = true } const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [loaded, setLoaded] = useState(false); + const [confirmingRemove, setConfirmingRemove] = useState(false); async function update(secrets?: Record) { setBusy(true); setError(""); setStatus(""); try { @@ -36,19 +37,23 @@ export function SecretControls({ workspace, name, kind, onSaved, active = true }

Available to this {kind === "script" ? "script" : kind === "app" ? "Worker app" : "artifact backend"} on the server. Saved values stay hidden.

Saved secrets

    - {names.map(name =>
  • {name}
  • )} + {names.map(name =>
  • {name}
  • )} {!names.length ?
  • {loaded ? "No secrets saved yet." : "Loading secrets…"}
  • : null}
{ event.preventDefault(); void update({ [secretName]: secretValue }); }}>
{names.includes(secretName) ? "Replace secret" : "Add a secret"}
- +
- + {confirmingRemove ? <> + Remove {secretName}? + + + : }
diff --git a/src/gallery/source-editor.tsx b/src/gallery/source-editor.tsx index d0f60b9..aa2efd5 100644 --- a/src/gallery/source-editor.tsx +++ b/src/gallery/source-editor.tsx @@ -20,7 +20,7 @@ const colors = HighlightStyle.define([ const theme = EditorView.theme({ "&": { height: "100%", backgroundColor: "transparent", color: "var(--text)", fontSize: "13px" }, "&.cm-focused": { outline: "none" }, - ".cm-scroller": { overflow: "auto", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", lineHeight: "1.65" }, + ".cm-scroller": { overflow: "auto", fontFamily: "var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)", lineHeight: "1.65" }, ".cm-content": { padding: "16px 0", caretColor: "var(--text)" }, ".cm-line": { padding: "0 20px 0 12px" }, ".cm-gutters": { backgroundColor: "transparent", color: "var(--subtle)", border: "none", padding: "0 8px 0 12px" }, diff --git a/src/gallery/styles.ts b/src/gallery/styles.ts index 1548606..7808ac8 100644 --- a/src/gallery/styles.ts +++ b/src/gallery/styles.ts @@ -13,10 +13,16 @@ export const galleryStyles = ` button, select, input { height: 32px; min-height: 32px; line-height: 20px; border: 1px solid transparent; border-radius: 8px; background: transparent; padding: 5px 10px; } button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; } select, input { border-color: var(--line); min-width: 0; } - input { background: var(--page); } + input { background: var(--panel); } + pre, code, kbd, samp { font-family: var(--font-mono); font-size: 12px; } button:disabled, select:disabled { opacity: .45; cursor: default; } :is(button, input, select, textarea, summary, a):focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; } button, a { -webkit-tap-highlight-color: transparent; } + .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } + .status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--subtle); } + .status-dot[data-tone="success"] { background: var(--success); } + .status-dot[data-tone="warning"] { background: var(--warning); } + .status-dot[data-tone="error"] { background: var(--error); } .select-control { position: relative; display: inline-flex; min-width: 0; vertical-align: middle; } .select-control select { appearance: none; width: 100%; background: var(--panel); padding-right: 30px; text-overflow: ellipsis; } .select-chevron { position: absolute; pointer-events: none; right: 12px; top: 50%; width: 6px; height: 6px; margin-top: -4px; border-right: 1px solid var(--muted); border-bottom: 1px solid var(--muted); transform: rotate(45deg); } @@ -30,6 +36,8 @@ export const galleryStyles = ` .select-control option:hover, .select-control option:focus { background: var(--raised); outline: none; } .select-control option::checkmark { color: var(--muted); } } + input[type=file] { height: auto; min-height: 0; padding: 0; border: 0; border-radius: 0; background: transparent; color: var(--muted); } + input[type=file]::file-selector-button { height: 32px; margin-right: 10px; padding: 5px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font: inherit; cursor: pointer; } .gallery-app { display: flex; flex-direction: column; height: 100dvh; overflow: hidden; } .app-header { display: flex; align-items: center; gap: 16px; min-height: 48px; padding: 8px 16px; flex-shrink: 0; } .library-heading { display: flex; align-items: center; justify-content: space-between; width: 224px; flex-shrink: 0; } @@ -72,36 +80,40 @@ export const galleryStyles = ` .artifact-row[aria-current="true"] { background: var(--selected); } .artifact-row-copy { min-width: 0; flex: 1; } .artifact-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; } - .row-kind { flex-shrink: 0; color: var(--subtle); font-size: 11px; } - .workspace-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--subtle); font-size: 11px; } + .row-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; color: var(--subtle); font-size: 11px; } + .unsaved-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text); flex-shrink: 0; } .library-footer { padding: 8px 12px; color: var(--subtle); font-size: 11px; } .library-count { padding: 8px 12px; color: var(--subtle); font-size: 11px; } - .artifact-detail { display: flex; flex-direction: column; min-width: 0; min-height: 0; margin: 0 6px 6px 0; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } - .detail-header { display: flex; gap: 16px; align-items: center; justify-content: space-between; padding: 10px 16px; flex-shrink: 0; flex-wrap: nowrap; border-bottom: 1px solid var(--line); } + .artifact-detail { display: flex; flex-direction: column; min-width: 0; min-height: 0; margin: 0 8px 8px 0; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } + /* Tabs fill the bar height so the active underline sits on its bottom border. */ + .detail-header { display: flex; gap: 16px; align-items: stretch; justify-content: space-between; min-height: 44px; padding: 0 8px 0 16px; flex-shrink: 0; flex-wrap: nowrap; border-bottom: 1px solid var(--line); } + .detail-header > :not(.view-control) { align-self: center; } .detail-title { min-width: 0; flex: 1; } + .detail-title:has(.revision-chip) { display: flex; align-items: center; gap: 8px; } .detail-eyebrow { margin: 0 0 2px; color: var(--muted); font-size: 11px; } - .detail-title h1 { margin: 0; font-size: 15px; line-height: 1.3; font-weight: 600; letter-spacing: -.35px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .detail-title h1 { min-width: 0; margin: 0; font-size: 14px; line-height: 1.3; font-weight: 600; letter-spacing: -.3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .detail-title h1:focus { outline: none; } - .detail-actions { display: flex; align-items: center; gap: 4px; flex-wrap: nowrap; justify-content: flex-end; flex-shrink: 0; } + .revision-chip { display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; height: 22px; padding: 0 8px; border-radius: 999px; background: var(--raised); color: var(--muted); font-size: 12px; white-space: nowrap; } + .revision-chip:has(button) { padding-right: 2px; } + .revision-chip button { position: relative; width: 18px; height: 18px; min-height: 18px; padding: 0; border: 0; border-radius: 50%; color: var(--muted); font-size: 14px; line-height: 1; } + .revision-chip button::after { content: ""; position: absolute; inset: -8px; } + .detail-actions { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; justify-content: flex-end; flex-shrink: 0; } .detail-actions > :is(button, a) { white-space: nowrap; } .download-link { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; min-height: 32px; line-height: 20px; padding: 5px 10px; border: 1px solid transparent; border-radius: 8px; text-decoration: none; } - .detail-actions .open-link { margin-left: 6px; border-color: var(--line); } - .view-control { display: flex; align-items: center; gap: 2px; overflow-x: auto; min-width: 0; } - .view-control button { padding: 5px 10px; color: var(--muted); flex-shrink: 0; border-radius: 0; border-bottom: 2px solid transparent; height: 40px; } + .detail-actions :is(.share-action, .open-link) { border-color: var(--line); } + .view-control { display: flex; align-items: stretch; gap: 2px; overflow-x: auto; min-width: 0; margin-bottom: -1px; } + .view-control button { height: auto; min-height: 40px; padding: 2px 10px 0; color: var(--muted); flex-shrink: 0; border: 0; border-bottom: 2px solid transparent; border-radius: 0; } .view-control button[data-active] { color: var(--text); border-bottom-color: var(--text); background: transparent; } - .revision-control { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted); } - .version-select { width: 140px; max-width: 140px; } - .version-select select { border-color: transparent; background: transparent; color: var(--text); } .detail-actions button[aria-expanded="true"] { background: var(--raised); } .back-library { display: none; } + .detail-notice { display: flex; align-items: center; gap: 8px; margin: 0; padding: 8px 16px; border-bottom: 1px solid var(--line); color: var(--muted); } .detail-disclosure { flex-shrink: 0; max-height: 42vh; overflow: auto; background: var(--panel); border-bottom: 1px solid var(--line); } .link-settings, .script-fields { display: flex; flex-wrap: wrap; align-items: end; gap: 8px; } .link-settings { padding: 12px 20px; } .link-settings label, .script-fields label { display: flex; flex-direction: column; align-items: stretch; gap: 4px; min-width: 0; color: var(--muted); font-size: 12px; } - .link-settings :is(button, .download-link), .script-panel button, .secret-controls button { border-color: var(--line); background: var(--panel); } + .link-settings .download-link { border-color: var(--line); } .library-move { padding: 12px 20px; } .library-move p { margin: 0 0 10px; max-width: 720px; } - .library-move button { border-color: var(--line); } .link-note { flex-basis: 100%; margin: 0; color: var(--muted); font-size: 12px; } .link-settings [role="status"] { align-self: center; } .link-settings .muted { font-size: 12px; } @@ -112,10 +124,11 @@ export const galleryStyles = ` .source-actions p { margin: 0; color: var(--muted); } .gallery-app button.primary-action { background: var(--primary); color: var(--primary-text); border-color: var(--primary); font-weight: 600; } button.primary-action:disabled { opacity: .45; } - .deployment-status { margin: 6px 0 0; color: var(--muted); } + /* Content actions are outlined; header, tab, and menu controls stay quiet. */ + :where(.artifact-stage, .detail-disclosure, .worker-content, .library-empty, .empty-detail, .save-conflict) button:where(:not(.cm-editor button)) { border-color: var(--line); background: var(--panel); } .save-feedback { padding: 8px 16px; flex-shrink: 0; max-height: 30vh; overflow: auto; } .save-feedback p { margin: 0 0 6px; } - .save-feedback button { height: auto; text-align: left; text-decoration: underline; } + .save-feedback button { height: auto; border-color: transparent; background: transparent; text-align: left; text-decoration: underline; } .save-feedback pre { white-space: pre-wrap; overflow-wrap: anywhere; } .agent-onboarding { width: 100%; max-width: 560px; flex-shrink: 0; margin: auto; padding: 32px 28px; } .agent-onboarding h2 { font-size: 22px; font-weight: 600; line-height: 1.3; letter-spacing: -.6px; margin: 0 0 8px; text-wrap: balance; } @@ -127,48 +140,56 @@ export const galleryStyles = ` .agent-connection > p { font-size: 12px; } .agent-onboarding label { display: block; margin: 18px 0 6px; color: var(--muted); font-size: 12px; } .onboarding-url-row { display: flex; align-items: center; gap: 8px; } - .onboarding-url-row input { flex: 1; min-width: 0; height: 36px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; } + .onboarding-url-row input { flex: 1; min-width: 0; height: 36px; font: 12px/1.5 var(--font-mono); text-overflow: ellipsis; } .agent-onboarding textarea { display: block; width: 100%; height: 120px; border: 1px solid var(--line); border-radius: 8px; padding: 12px; background: var(--page); line-height: 1.6; resize: none; } - .agent-connection button { flex-shrink: 0; border-color: var(--line); min-height: 36px; } + .agent-connection button { flex-shrink: 0; border-color: var(--line); background: var(--panel); min-height: 36px; } .onboarding-copy-row { display: flex; align-items: center; gap: 12px; margin-top: 10px; } .onboarding-feedback { height: 36px; overflow: auto; display: flex; align-items: center; color: var(--muted); font-size: 12px; } .live-data-note { padding: 8px 16px; margin: 0; border-bottom: 1px solid var(--line); color: var(--muted); } .artifact-execution fieldset { border: 0; padding: 0; margin: 0; min-width: 0; } - .project-import { padding: 16px 20px; max-height: 50vh; overflow: auto; } - .project-import label { display: block; margin: 8px 0; } - .project-import h2 { margin: 0 0 8px; font-size: 16px; } - .project-import input { background: var(--raised); } - .project-import input[type=file] { height: auto; } + .project-import { flex: 1; min-width: 0; min-height: 0; overflow: auto; padding: 20px 20px 28px; } + .project-import > :not(details) { max-width: 640px; } + .project-import p { margin: 0 0 12px; color: var(--muted); line-height: 1.6; } + .project-import label { display: flex; flex-direction: column; align-items: stretch; gap: 6px; max-width: 420px; margin-top: 16px; color: var(--muted); font-size: 12px; } + .project-import label input:not([type=file]) { width: 100%; color: var(--text); font-size: 13px; } + .project-import .import-summary { margin: 12px 0 0; color: var(--text); } + .project-import .error-message { margin-top: 12px; color: var(--error); } + .import-actions { display: flex; align-items: center; gap: 8px; margin-top: 20px; } .project-import pre { max-height: 160px; overflow: auto; white-space: pre-wrap; } .source-actions .source-readonly { margin-right: auto; } .save-conflict { flex-shrink: 0; padding: 8px 16px; max-height: 35vh; overflow: auto; } - .save-conflict button { border-color: var(--line); margin-right: 8px; } + .save-conflict button { margin-right: 8px; } .script-panel label { color: var(--muted); font-size: 12px; } .script-panel :is(input, select) { color: var(--text); } .project-editor { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; } .project-file-bar { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; padding: 6px 12px; border-bottom: 1px solid var(--line); flex-shrink: 0; } - .project-file-bar button { background: transparent; border-color: transparent; font: 12px/20px "SFMono-Regular", Consolas, monospace; } + .project-file-bar button { background: transparent; border-color: transparent; font: 12px/20px var(--font-mono); } .project-file-bar button[aria-pressed="true"] { background: var(--raised); } .source-code-editor { display: flex; flex-direction: column; flex: 1; min-height: 180px; min-width: 0; } .source-code-surface { flex: 1; min-height: 0; min-width: 0; } .source-code-status { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 4px 16px; border-top: 1px solid var(--line); color: var(--subtle); font-size: 11px; flex-shrink: 0; } + .source-code-status button { border-color: transparent; background: transparent; color: var(--muted); } .project-file-management, .project-dependencies { flex-shrink: 0; border-top: 1px solid var(--line); padding: 0 16px; max-height: 35vh; overflow: auto; } .project-file-management > div { padding-bottom: 10px; } .project-file-management details { margin: 0; } .project-file-management p { margin: 6px 0; } - .script-panel textarea, .artifact-execution textarea { display: block; width: 100%; min-height: 80px; margin: 6px 0 12px; color: var(--text); background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px; font: 13px/1.6 "SFMono-Regular", Consolas, monospace; resize: vertical; } - summary { cursor: pointer; min-height: 32px; padding-block: 5px; color: var(--text); } - .script-panel pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 12px; border: 1px solid var(--line); background: var(--panel); } + .script-panel textarea, .artifact-execution textarea { display: block; width: 100%; min-height: 80px; margin: 6px 0 12px; color: var(--text); background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px; font: 12px/1.6 var(--font-mono); resize: vertical; } + summary { display: flex; align-items: center; gap: 8px; list-style: none; cursor: pointer; min-height: 32px; padding-block: 5px; color: var(--text); } + summary::-webkit-details-marker { display: none; } + summary::before { content: ""; width: 5px; height: 5px; margin: 0 3px 0 2px; flex-shrink: 0; border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted); transform: rotate(-45deg); } + details[open] > summary::before { transform: rotate(45deg); } + .script-panel pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); } .script-activity, .artifact-execution { flex: 1; min-height: 0; overflow: auto; padding: 16px 20px; } .script-activity p { color: var(--muted); } .script-activity label { display: block; } .script-activity .script-fields label { display: flex; } + .script-activity label:has(> textarea) { margin-top: 12px; } .script-activity details + details { border-top: 1px solid var(--line); margin-top: 16px; padding-top: 8px; } .artifact-execution label { display: inline-flex; flex-direction: column; gap: 4px; margin: 8px 12px 8px 0; } .artifact-execution label:has(textarea) { display: flex; margin-right: 0; } - .artifact-execution button { border-color: var(--line); } .artifact-execution table { border-collapse: collapse; font-variant-numeric: tabular-nums; font-size: 12px; width: 100%; } .artifact-execution :is(td, th) { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--line); } + .artifact-execution tbody tr:last-child td { border-bottom: 0; } .artifact-execution p { color: var(--muted); } .script-activity, .artifact-execution { font-size: 12px; line-height: 1.6; } .script-activity :is(h2, h3), .artifact-execution :is(h2, h3) { margin: 0 0 8px; font-size: 14px; font-weight: 600; } @@ -176,13 +197,16 @@ export const galleryStyles = ` .script-activity > form, .schedule-form form { max-width: 680px; } .schedule-summary { padding-bottom: 12px; border-bottom: 1px solid var(--line); } .schedule-summary h2, .run-history h2 { margin: 0 0 8px; font-size: 15px; } - .schedule-summary button + button { margin-left: 8px; } - .schedule-form { max-width: 680px; padding: 12px 0; border-top: 1px solid var(--line); } + .schedule-summary .schedule-state { display: flex; align-items: center; gap: 8px; color: var(--text); font-size: 13px; font-weight: 600; } + .button-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } + .schedule-form { max-width: 680px; padding: 12px 0; } .schedule-form > summary { font-weight: 600; padding-bottom: 12px; } .run-history { padding: 20px 0; margin-top: 24px; overflow-x: auto; border-top: 1px solid var(--line); } .run-table { width: 100%; overflow: auto; margin-top: 16px; border: 1px solid var(--line); border-radius: 8px; } .run-table table { min-width: 640px; } .run-table th { color: var(--muted); font-weight: 500; background: var(--page); } + .run-status { display: inline-flex; align-items: center; gap: 6px; } + .artifact-execution .run-note { margin: 10px 0 0; color: var(--subtle); font-size: 11px; } .muted { color: var(--muted); } .artifact-stage { position: relative; display: flex; flex: 1; min-width: 0; min-height: 0; overflow: hidden; } .source-stage { display: flex; flex: 1; min-width: 0; min-height: 0; } @@ -192,7 +216,6 @@ export const galleryStyles = ` .state-message { margin: 0; padding: 12px; color: var(--muted); } .library-empty { padding: 20px 12px; } .library-empty p { margin: 0 0 8px; color: var(--muted); } - .library-empty button { border-color: var(--line); } .empty-detail { margin: auto; max-width: 400px; padding: 24px; text-align: center; } .empty-detail h2 { margin: 0 0 8px; font-size: 18px; font-weight: 500; } .empty-detail p { margin: 0; color: var(--muted); } @@ -200,18 +223,17 @@ export const galleryStyles = ` .source-form > .error-message, .script-panel > .error-message { margin: 0; padding: 8px 16px; flex-shrink: 0; } .refresh-error { padding: 10px 16px; margin: 0; border-bottom: 1px solid var(--line); background: var(--panel); } @media (hover: hover) and (pointer: fine) { - button:hover:not(:disabled), .download-link:hover { background: var(--raised); } + button:hover:not(:disabled), .download-link:hover, input[type=file]:not(:disabled)::file-selector-button:hover { background: var(--raised); } .gallery-app button.primary-action:hover:not(:disabled) { background: var(--primary-hover); } .artifact-row[aria-current="true"]:hover { background: var(--selected); } .create-item:hover { background: var(--raised); color: var(--text); } + .view-control button:hover { background: transparent; } .view-control button:hover:not([data-active]), .library-filters button:hover:not([aria-pressed="true"]) { color: var(--text); background: transparent; } } @media (max-width: 1100px) { .gallery-layout { grid-template-columns: 232px minmax(0, 1fr); } .library-heading { width: 200px; } - .detail-header { gap: 8px; padding-inline: 12px; } - .detail-actions { max-width: 300px; } - .revision-control > span:not(.select-control) { display: none; } + .detail-header { gap: 8px; padding-left: 12px; } .account-name { display: none; } } @media (max-width: 760px) { @@ -223,7 +245,7 @@ export const galleryStyles = ` .create-item { width: 44px; height: 44px; } .app-header { flex-wrap: wrap; } .header-actions { flex-wrap: wrap; } - .artifact-detail { margin: 0 4px 4px; } + .artifact-detail { margin: 0 8px 8px; } .header-context { display: none; } .header-actions { gap: 4px; } .header-actions .select-control { max-width: 130px; } @@ -234,17 +256,14 @@ export const galleryStyles = ` .artifact-row { min-height: 44px; } .artifact-name { font-size: 14px; } .back-library { display: inline-flex; padding: 0; width: 28px; flex-shrink: 0; color: var(--muted); } - .detail-header { padding: 4px 8px; gap: 4px; } - .detail-actions { max-width: none; } + .detail-header { padding: 0 4px 0 8px; gap: 4px; } .detail-title h1 { font-size: 13px; } + .detail-actions { gap: 2px; } .detail-actions .open-link { display: none; } .detail-header .view-control button { padding-inline: 6px; font-size: 12px; } .detail-actions > button { padding-inline: 8px; } - .version-select { width: 150px; max-width: 150px; } - .version-select select { padding-left: 6px; } .view-control { gap: 0; } .view-control button { padding-inline: 8px; } - .revision-control { min-width: 0; margin-left: auto; } .link-settings { padding: 12px; } .script-fields > :is(input, label, .select-control) { max-width: 100%; } .link-settings label { flex: 1; min-width: 0; } @@ -255,13 +274,16 @@ export const galleryStyles = ` .source-code-status { padding-inline: 12px; } .source-code-status .editor-shortcuts { display: none; } .script-activity, .artifact-execution { padding: 12px; } + .project-import { padding: 16px; } } .more-actions { width: 32px; padding: 0; font-size: 24px; line-height: 1; } .mobile-open { display: none !important; } @media (max-width: 760px) { .mobile-open { display: block !important; } } .action-menu { padding: 4px; min-width: 170px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); color: var(--text); box-shadow: 0 8px 28px #0002; } - .action-menu [role=menuitem] { display: block; padding: 8px 12px; border-radius: 6px; text-decoration: none; cursor: pointer; } + .action-menu :is([role=menuitem], [role=menuitemradio]) { display: block; padding: 8px 12px; border-radius: 6px; text-decoration: none; cursor: pointer; } + .action-menu [role=menuitemradio] { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .action-menu [data-highlighted] { background: var(--raised); outline: none; } + .menu-check { width: 9px; height: 5px; margin-top: -3px; flex-shrink: 0; border-left: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted); transform: rotate(-45deg); } .settings-panel { width: 100%; overflow: auto; } .settings-title { margin: 16px 20px 0; font-size: 15px; } .secret-controls { width: 100%; max-width: 680px; padding: 16px 20px 28px; } @@ -278,9 +300,11 @@ export const galleryStyles = ` .settings-fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; } .settings-fields label { display: flex; flex-direction: column; gap: 6px; color: var(--muted); font-size: 12px; } .settings-fields input { width: 100%; height: 36px; color: var(--text); } - .settings-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; } + .settings-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 14px; } .settings-actions button { min-height: 36px; } .settings-actions .destructive-action { color: var(--error); border-color: transparent; background: transparent; } + .gallery-app .settings-actions .destructive-action.confirm { color: var(--panel); border-color: var(--error); background: var(--error); font-weight: 600; } + .settings-actions .confirm-prompt { color: var(--text); font-size: 12px; } .settings-feedback { min-height: 20px; margin: 12px 0 0; color: var(--muted); font-size: 12px; } .script-panel .secret-controls { flex: 1; min-height: 0; overflow: auto; } @media (max-width: 760px) { @@ -288,15 +312,13 @@ export const galleryStyles = ` .settings-fields { grid-template-columns: minmax(0, 1fr); } .settings-fields input { height: 44px; font-size: 16px; } .settings-actions button, .secret-list-header button, .secret-list button { min-height: 44px; } - } - @media (max-width: 760px) { .detail-actions { margin-left: 0; } - .revision-control { padding-bottom: 4px; } } @media (pointer: coarse) { button, input, select, .download-link, summary { height: auto; min-height: 44px; } + input[type=file] { min-height: 0; } input, select, textarea, .script-panel textarea { font-size: 16px; } - .search-input { padding-block: 11px; } + .search-input, select { padding-block: 11px; } .clear-search { height: 44px; } } `; diff --git a/src/gallery/theme.ts b/src/gallery/theme.ts index 0986a73..2455bff 100644 --- a/src/gallery/theme.ts +++ b/src/gallery/theme.ts @@ -1,13 +1,16 @@ // Matches sidequery-mono/apps/web/app/app.css. Keep app chrome and editor // on the same tokens; artifact previews retain their own authored styling. +// Light --focus is darker than the app.css amber so the ring keeps 3:1 +// contrast against panel, page, and selected rows. export const themeStyles = ` :root { color-scheme: light; --page: #f6f5f1; --panel: #ffffff; --raised: #f0eee8; --selected: #e9e4d9; --line: #e7e4dd; --text: #1b1917; - --muted: #6d665d; --subtle: #8b8378; --focus: #f1bd5b; + --muted: #6d665d; --subtle: #8b8378; --focus: #a86f00; --primary: #1b1917; --primary-text: #ffffff; --primary-hover: #2a2723; - --error: #b91c1c; + --error: #b91c1c; --success: #2f7a4b; --warning: #b06a00; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --syntax-keyword: #8250a0; --syntax-string: #326a42; --syntax-number: #98621b; --syntax-type: #216e78; --syntax-function: #654fb0; } @@ -15,9 +18,9 @@ export const themeStyles = ` color-scheme: dark; --page: #141312; --panel: #1c1a18; --raised: #25231f; --selected: #2e2b27; --line: #2e2b27; --text: #f4f2ee; - --muted: #b5aea3; --subtle: #958c80; + --muted: #b5aea3; --subtle: #958c80; --focus: #f1bd5b; --primary: #f4f2ee; --primary-text: #141312; --primary-hover: #ffffff; - --error: #f2a5a5; + --error: #f2a5a5; --success: #8fcca4; --warning: #f1bd5b; --syntax-keyword: #c4afe5; --syntax-string: #b6c6a0; --syntax-number: #d1b58e; --syntax-type: #a8c9c4; --syntax-function: #c6bfd5; } diff --git a/src/gallery/workspace.tsx b/src/gallery/workspace.tsx index ea01631..27c0d4a 100644 --- a/src/gallery/workspace.tsx +++ b/src/gallery/workspace.tsx @@ -385,8 +385,8 @@ const workspaceStyles = ` .mcp-gallery .artifact-detail { margin: 0; border: 0; border-radius: 0; background: transparent; } .mcp-gallery .detail-header { min-height: 48px; padding: 4px 12px; gap: 8px; background: transparent; } .mcp-gallery .detail-title h1 { font-size: 13px; letter-spacing: normal; } - .mcp-gallery .view-control { gap: 2px; } - .mcp-gallery .view-control button { height: 32px; border: 0; border-radius: 6px; font-size: 12px; } + .mcp-gallery .view-control { align-self: center; align-items: center; gap: 2px; margin-bottom: 0; } + .mcp-gallery .view-control button { height: 32px; min-height: 32px; padding: 5px 10px; border: 0; border-radius: 6px; font-size: 12px; } .mcp-gallery .view-control button[data-active] { background: var(--raised); } .mcp-gallery .workspace-library-toggle { width: 32px; padding: 0; flex-shrink: 0; color: var(--muted); } .library-toggle-icon { display: block; width: 16px; height: 14px; border: 1.5px solid currentColor; border-radius: 3px; position: relative; } diff --git a/src/runtime/mcp-file-editor.tsx b/src/runtime/mcp-file-editor.tsx index e9acef2..262a972 100644 --- a/src/runtime/mcp-file-editor.tsx +++ b/src/runtime/mcp-file-editor.tsx @@ -144,7 +144,7 @@ export function mountArtifactFileEditor(root: HTMLElement, extensions: OpenAIExt const fileEditorStyles = ` .mcp-file-editor { height: 100%; min-height: 300px; background: var(--panel); color: var(--text); } -.mcp-file-editor .detail-header { flex-wrap: wrap; } +.mcp-file-editor .detail-header { flex-wrap: wrap; padding-block: 6px; } .mcp-file-editor .file-status { margin: 8px 16px; color: var(--muted); } .mcp-file-editor .file-source { flex: 1; min-height: 0; overflow: auto; } .mcp-file-editor .source-code-editor { display: flex; flex-direction: column; height: 100%; min-height: 180px; min-width: 0; }