From b8ea7abcc77dd3ace86f3a047a30ed8e048fa2e3 Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Tue, 22 Sep 2026 19:52:35 -0700 Subject: [PATCH] =?UTF-8?q?A=20=C3=97=20closes=20a=20detail=20pane=20on=20?= =?UTF-8?q?a=20wide=20screen:=20Docker,=20Services,=20Cron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a phone the list gives way to the pane and Back returns; on a desktop the list stays beside it and there was no way to put the pane away. Co-Authored-By: Claude Fable 5.1 --- web/src/Cron.tsx | 3 +++ web/src/Docker.tsx | 6 ++++-- web/src/Services.tsx | 1 + web/src/styles.css | 8 +++++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/web/src/Cron.tsx b/web/src/Cron.tsx index a309e43..3df34d2 100644 --- a/web/src/Cron.tsx +++ b/web/src/Cron.tsx @@ -48,6 +48,7 @@ function SourceEditor({ name, onClose, onSaved }: { name: string; onClose: () => } return (
+

{srcLabel(name)}

{!editable &&
Read-only: writing this file needs passwordless sudo on the host.
} @@ -98,6 +99,7 @@ function JobDetail({ job, logging, onChanged, onBack, onEdit }: { job: CronJob; return (
+

{job.command}

@@ -150,6 +152,7 @@ function TimerDetail({ t, onChanged, onBack }: { t: CronTimer; onChanged: () => const B = ({ a, label }: { a: string; label: string }) => return (
+

{t.unit}

{t.description}
diff --git a/web/src/Docker.tsx b/web/src/Docker.tsx index 81144aa..75292a3 100644 --- a/web/src/Docker.tsx +++ b/web/src/Docker.tsx @@ -60,6 +60,7 @@ function Detail({ c, live, onBack, onChanged }: { c: DockerContainer; live: Reco const mounts = (info?.mounts as { Source: string; Destination: string; RW: boolean }[] | undefined) ?? [] return (
+
{c.name}
Image{c.image}
@@ -140,10 +141,11 @@ function StackDetail({ name, startTask, onBack, onChanged, onDuplicate }: { name } const duplicate = async () => { if (!st) return; try { onDuplicate({ name, text: file === 0 ? orig : (await api.stackFile(name, 0)).text, hasEnv: !!st.has_env, files: st.host_files?.length ?? 1 }) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } const run = (a: 'deploy' | 'update' | 'restart' | 'stop' | 'down') => async () => { try { setTask((await api.stackAction(name, a)).id) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } - if (err && !st) return
{err}
+ if (err && !st) return
{err}
if (!st) return
return (
+
{st.name}{st.running}/{st.containers.length} running
{file === 'env' ? (st.host_file.replace(/[^/]+$/, '') + '.env') : (st.host_files?.[file] ?? st.host_file)}{st.held_by && · kept on {st.held_by}, deployed to this machine over {st.remote}}{st.via && · compose runs inside {st.via}, as it was deployed}{st.has_env && ' · has a .env beside it'}
{((st.host_files?.length ?? 0) > 1 || st.has_env) &&
{st.host_files!.map((f, i) => )}{st.has_env && }
} @@ -264,7 +266,7 @@ export default function Docker() { )}
- {shownTab === 'containers' && !cur && sel && !selStack &&
There is no container called {sel} on this machine. It may have been removed, renamed or recreated under another name.
} + {shownTab === 'containers' && !cur && sel && !selStack &&
There is no container called {sel} on this machine. It may have been removed, renamed or recreated under another name.
} {shownTab === 'containers' && (cur ? setSel('')} onChanged={load} /> : (sel && !selStack ? null :
Select a container.
))} {making && { setMaking(''); setDup(null) }} onCreated={async (name, deploy) => { setMaking(''); setDup(null); setTab('stacks'); if (deploy) { try { setStartTask({ name, id: (await api.stackAction(name, 'deploy')).id }) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } else setStartTask(null); await load(); setSel('stack:' + name) }} />} {shownTab === 'stacks' && (selStack ? setSel('')} onChanged={load} onDuplicate={(f) => { setDup(f); setMaking('stack') }} /> :
Select a stack to edit its compose file.
)} diff --git a/web/src/Services.tsx b/web/src/Services.tsx index 2a2cd0d..59b147b 100644 --- a/web/src/Services.tsx +++ b/web/src/Services.tsx @@ -137,6 +137,7 @@ function Detail({ name, onChanged, onBack }: { name: string; onChanged: () => vo return (
+
diff --git a/web/src/styles.css b/web/src/styles.css index 4c2fa1d..cc089d7 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -149,6 +149,12 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi What is meant to give way (.logs, .stack-editor) says so itself, further down. */ .svc-detail > *, .cron-detail > * { flex-shrink: 0; } .svc-detail .back { display: none; align-self: flex-start; } +/* The × that closes a detail pane on a wide screen, where the list stays beside it and there is + no Back. Sits in the pane's top-right corner; the header keeps clear of it. */ +.svc-detail, .cron-detail { position: relative; } +.detail-close { position: absolute; top: 8px; right: 10px; z-index: 1; width: 28px; height: 28px; line-height: 28px; font-size: 20px; color: var(--muted); border-radius: 6px; } +.detail-close:hover { color: var(--fg); background: var(--bg3); } +.svc-detail > .svc-head, .cron-detail > h2 { padding-right: 36px; } /* Nothing selected: the list has the window to itself rather than a 360px column beside an empty pane. The names, descriptions and commands that get an ellipsis are what it's for. Picking something puts the list back at its own width, beside the detail. A phone shows one @@ -174,7 +180,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi .log.err .lm { color: var(--bad); } .log.warn .lm { color: var(--s2); } .log.debug .lm { color: var(--muted); } @media (max-width: 800px) { .svc-list { width: 100%; border-right: 0; } .has-sel .svc-list { display: none; } .svc-empty { display: none; } - .svc-detail .back { display: inline-block; } .props { grid-template-columns: 1fr; } + .svc-detail .back { display: inline-block; } .props { grid-template-columns: 1fr; } .detail-close { display: none; } } /* cron */