refactor(web): polish cloud workers shell hierarchy

This commit is contained in:
Benjamin Shafii
2026-02-22 15:38:02 -08:00
parent c7fd67e47c
commit 4c3edc3477
2 changed files with 252 additions and 138 deletions

View File

@@ -134,12 +134,16 @@ body {
width: calc(100vw - 3rem); width: calc(100vw - 3rem);
max-width: none; max-width: none;
min-height: calc(100vh - 8rem); min-height: calc(100vh - 8rem);
border-radius: 1.15rem; border-radius: 2rem;
background: transparent;
border-color: transparent;
box-shadow: none;
backdrop-filter: none;
} }
.ow-card-shell .ow-card-body { .ow-card-shell .ow-card-body {
min-height: calc(100vh - 8rem); min-height: calc(100vh - 8rem);
padding: 1rem; padding: 0;
} }
.ow-card-shell .ow-app-shell { .ow-card-shell .ow-app-shell {
@@ -275,20 +279,21 @@ body {
} }
.ow-btn-secondary { .ow-btn-secondary {
border-radius: 0.78rem; border-radius: 0.82rem;
border: 1px solid #ced8ef; border: 1px solid #cbd5e1;
background: #fff; background: #fff;
color: #1f3a8a; color: #334155;
font-size: 0.84rem; font-size: 0.84rem;
font-weight: 600; font-weight: 600;
padding: 0.72rem 0.84rem; padding: 0.66rem 0.84rem;
transition: border-color 120ms ease, background-color 120ms ease; transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
} }
.ow-btn-secondary:hover:not(:disabled), .ow-btn-secondary:hover:not(:disabled),
.ow-btn-icon:hover:not(:disabled) { .ow-btn-icon:hover:not(:disabled) {
border-color: #9fb2eb; border-color: rgba(27, 41, 255, 0.35);
background: #f5f8ff; color: #1b29ff;
background: rgba(27, 41, 255, 0.06);
} }
.ow-btn-secondary:disabled, .ow-btn-secondary:disabled,
@@ -306,12 +311,13 @@ body {
.ow-app-shell { .ow-app-shell {
display: grid; display: grid;
gap: 0.7rem; gap: 1rem;
align-content: start;
} }
@media (min-width: 980px) { @media (min-width: 980px) {
.ow-app-shell { .ow-app-shell {
grid-template-columns: 7.4rem minmax(0, 0.95fr) minmax(0, 1.05fr); grid-template-columns: 16rem 21rem minmax(0, 1fr);
align-items: start; align-items: start;
} }
@@ -321,109 +327,119 @@ body {
} }
.ow-app-nav { .ow-app-nav {
border: 1px solid #dbe3f4; border: 1px solid #e2e8f0;
border-radius: 0.9rem; border-radius: 2rem;
background: #f9fbff; background: #fff;
padding: 0.5rem; padding: 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 1rem;
box-shadow: 0 6px 24px rgba(15, 23, 42, 0.05);
} }
.ow-nav-group { .ow-nav-group {
display: grid; display: grid;
gap: 0.34rem; gap: 0.45rem;
} }
.ow-nav-label { .ow-nav-label {
margin: 0; margin: 0;
font-size: 0.66rem; font-size: 0.62rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.12em;
text-transform: uppercase; text-transform: uppercase;
color: #64748b; color: #94a3b8;
padding-left: 0.22rem; padding-left: 0.35rem;
} }
.ow-nav-item { .ow-nav-item {
border: 1px solid #dbe3f4; border: 1px solid transparent;
border-radius: 0.72rem; border-radius: 0.92rem;
background: #fff; background: transparent;
color: #334155; color: #475569;
font-size: 0.8rem; font-size: 0.85rem;
font-weight: 700; font-weight: 600;
padding: 0.58rem 0.5rem; padding: 0.66rem 0.75rem;
text-align: left; text-align: left;
transition: border-color 140ms ease, background-color 140ms ease, color 140ms ease;
}
.ow-nav-item:hover {
border-color: #e2e8f0;
background: #f8fafc;
} }
.ow-nav-item.is-active { .ow-nav-item.is-active {
border-color: #9fb2eb; border-color: rgba(27, 41, 255, 0.24);
color: #1e3a8a; color: #1b29ff;
background: #edf3ff; background: rgba(27, 41, 255, 0.08);
} }
.ow-app-nav-footer { .ow-app-nav-footer {
margin-top: auto; margin-top: auto;
border-top: 1px solid #dbe3f4; border-top: 1px solid #e2e8f0;
padding-top: 0.58rem; padding-top: 0.9rem;
display: grid; display: grid;
gap: 0.34rem; gap: 0.42rem;
} }
.ow-nav-email { .ow-nav-email {
overflow-wrap: anywhere; overflow-wrap: anywhere;
font-size: 0.8rem;
color: #475569;
} }
.ow-pane { .ow-pane {
border: 1px solid #dbe3f4; border: 1px solid #e2e8f0;
border-radius: 0.92rem; border-radius: 2rem;
background: #fff; background: #fff;
padding: 0.72rem; padding: 1.2rem;
display: grid; display: grid;
gap: 0.6rem; gap: 0.9rem;
align-content: start; align-content: start;
box-shadow: 0 6px 24px rgba(15, 23, 42, 0.05);
} }
.ow-pane-head { .ow-pane-head {
display: grid; display: grid;
gap: 0.16rem; gap: 0.3rem;
} }
.ow-pane-head-row { .ow-pane-head-row {
display: flex; display: flex;
align-items: flex-start; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.6rem; gap: 0.75rem;
} }
.ow-pane-block { .ow-pane-block {
border: 1px solid #e3e9f7; border: 1px solid #e2e8f0;
border-radius: 0.86rem; border-radius: 1.1rem;
background: #fbfcff; background: #f8fafc;
padding: 0.62rem; padding: 0.85rem;
display: grid; display: grid;
gap: 0.55rem; gap: 0.65rem;
} }
.ow-btn-compact { .ow-btn-compact {
font-size: 0.78rem; font-size: 0.79rem;
padding: 0.54rem 0.72rem; padding: 0.58rem 0.88rem;
} }
.ow-filter-row { .ow-filter-row {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
gap: 0.5rem; gap: 0.55rem;
} }
.ow-select { .ow-select {
border-radius: 0.78rem; border-radius: 0.9rem;
border: 1px solid #d9e1f2; border: 1px solid #cbd5e1;
background: #fff; background: #fff;
color: #334155; color: #334155;
padding: 0.76rem 0.8rem; padding: 0.74rem 0.82rem;
font-size: 0.86rem; font-size: 0.86rem;
font-weight: 600; font-weight: 500;
} }
.ow-select:focus-visible { .ow-select:focus-visible {
@@ -434,7 +450,7 @@ body {
.ow-overview-grid { .ow-overview-grid {
display: grid; display: grid;
gap: 0.5rem; gap: 0.65rem;
} }
@media (min-width: 700px) { @media (min-width: 700px) {
@@ -444,28 +460,28 @@ body {
} }
.ow-overview-card { .ow-overview-card {
border: 1px solid #e3e9f7; border: 1px solid #e2e8f0;
border-radius: 0.86rem; border-radius: 1.1rem;
background: #fbfcff; background: #fff;
padding: 0.64rem; padding: 0.9rem;
display: grid; display: grid;
gap: 0.22rem; gap: 0.32rem;
} }
.ow-overview-label { .ow-overview-label {
margin: 0; margin: 0;
font-size: 0.7rem; font-size: 0.65rem;
font-weight: 700; font-weight: 700;
color: #64748b; color: #94a3b8;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.06em; letter-spacing: 0.1em;
} }
.ow-overview-value { .ow-overview-value {
margin: 0; margin: 0;
font-size: 0.94rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
color: #1e293b; color: #0f172a;
} }
.ow-overview-value.is-ready { .ow-overview-value.is-ready {
@@ -488,26 +504,28 @@ body {
} }
.ow-empty-detail { .ow-empty-detail {
border: 1px dashed #d1ddef; border: 1px dashed #cbd5e1;
border-radius: 0.82rem; border-radius: 1.1rem;
background: #f9fbff; background: #f8fafc;
padding: 0.9rem; padding: 1.2rem;
display: grid; display: grid;
gap: 0.35rem; gap: 0.42rem;
} }
@media (max-width: 979px) { @media (max-width: 979px) {
.ow-app-nav { .ow-app-nav {
flex-direction: row; flex-direction: row;
align-items: center; align-items: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
border-radius: 1.35rem;
padding: 0.8rem;
} }
.ow-app-nav-footer { .ow-app-nav-footer {
margin-top: 0; margin-top: 0;
margin-left: auto; margin-left: auto;
border-top: 0; border-top: 0;
padding-top: 0; padding-top: 0.1rem;
align-items: end; align-items: end;
} }
@@ -528,13 +546,13 @@ body {
.ow-caption { .ow-caption {
margin: 0; margin: 0;
color: #64748b; color: #64748b;
font-size: 0.76rem; font-size: 0.78rem;
} }
.ow-link { .ow-link {
background: transparent; background: transparent;
color: #1e3a8a; color: #1b29ff;
font-size: 0.78rem; font-size: 0.79rem;
font-weight: 700; font-weight: 700;
padding: 0; padding: 0;
text-decoration: underline; text-decoration: underline;
@@ -550,10 +568,10 @@ body {
.ow-paywall-box, .ow-paywall-box,
.ow-lookup-box, .ow-lookup-box,
.ow-log-box { .ow-log-box {
border-radius: 0.92rem; border-radius: 1rem;
border: 1px solid #dbe3f4; border: 1px solid #e2e8f0;
background: #f9fbff; background: #f8fafc;
padding: 0.78rem; padding: 0.9rem;
} }
.ow-note-box p, .ow-note-box p,
@@ -566,7 +584,7 @@ body {
.ow-note-box { .ow-note-box {
display: grid; display: grid;
gap: 0.42rem; gap: 0.42rem;
color: #334155; color: #475569;
font-size: 0.82rem; font-size: 0.82rem;
} }
@@ -598,17 +616,17 @@ body {
} }
.ow-howto { .ow-howto {
border: 1px solid #dbe3f4; border: 1px solid #e2e8f0;
border-radius: 0.75rem; border-radius: 1rem;
background: #fff; background: #fff;
padding: 0.5rem; padding: 0.75rem;
} }
.ow-howto summary { .ow-howto summary {
cursor: pointer; cursor: pointer;
font-size: 0.78rem; font-size: 0.82rem;
font-weight: 700; font-weight: 700;
color: #1e3a8a; color: #0f172a;
} }
.ow-howto[open] { .ow-howto[open] {
@@ -662,8 +680,8 @@ body {
.ow-step-title { .ow-step-title {
margin: 0; margin: 0;
font-size: 0.78rem; font-size: 0.86rem;
color: #1e293b; color: #0f172a;
font-weight: 700; font-weight: 700;
} }
@@ -676,9 +694,9 @@ body {
.ow-paywall-title, .ow-paywall-title,
.ow-section-title { .ow-section-title {
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
font-size: 0.78rem; font-size: 0.95rem;
font-weight: 700; font-weight: 700;
color: #1e3a8a; color: #0f172a;
} }
.ow-full { .ow-full {
@@ -711,9 +729,9 @@ body {
} }
.ow-worker-list-panel { .ow-worker-list-panel {
max-height: 17rem; max-height: 58vh;
overflow-y: auto; overflow-y: auto;
padding-right: 0.1rem; padding-right: 0.2rem;
} }
.ow-worker-select { .ow-worker-select {
@@ -722,22 +740,24 @@ body {
font: inherit; font: inherit;
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
border: 1px solid #dbe3f4; border: 1px solid #e2e8f0;
border-radius: 0.76rem; border-radius: 1.2rem;
background: #fff; background: #fff;
padding: 0.56rem; padding: 0.82rem;
display: grid; display: grid;
gap: 0.4rem; gap: 0.45rem;
transition: border-color 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
} }
.ow-worker-select:hover { .ow-worker-select:hover {
border-color: #9fb2eb; border-color: #cbd5e1;
background: #f5f8ff; background: #f8fafc;
} }
.ow-worker-select.is-active { .ow-worker-select.is-active {
border-color: #9fb2eb; border-color: #1b29ff;
background: #f8fbff; background: rgba(27, 41, 255, 0.04);
box-shadow: inset 3px 0 0 #1b29ff, 0 8px 22px rgba(27, 41, 255, 0.12);
} }
.ow-worker-detail { .ow-worker-detail {
@@ -773,37 +793,76 @@ body {
.ow-worker-head { .ow-worker-head {
display: flex; display: flex;
align-items: flex-start; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.4rem; gap: 0.75rem;
} }
.ow-worker-meta { .ow-worker-meta {
margin: 0; margin: 0;
font-size: 0.72rem; font-size: 0.72rem;
color: #475569; color: #64748b;
word-break: break-all; word-break: break-all;
font-family: var(--font-ibm-plex-mono), "JetBrains Mono", "SF Mono", monospace;
} }
.ow-badge { .ow-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 999px; border-radius: 0.45rem;
border: 1px solid #b8c8ec; border: 1px solid #e2e8f0;
background: #edf3ff; background: #f8fafc;
color: #1e3a8a; color: #64748b;
font-size: 0.64rem; font-size: 0.62rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.05em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
padding: 0.2rem 0.42rem; padding: 0.2rem 0.45rem;
}
.ow-worker-badges {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.ow-status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 0.22rem 0.5rem;
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.ow-status-pill.is-ready {
background: #dcfce7;
color: #166534;
}
.ow-status-pill.is-starting {
background: #fef3c7;
color: #92400e;
}
.ow-status-pill.is-attention {
background: #ffe4e6;
color: #9f1239;
}
.ow-status-pill.is-other {
background: #e2e8f0;
color: #475569;
} }
.ow-inline-actions { .ow-inline-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.48rem; gap: 0.58rem;
} }
.ow-inline-actions > .ow-input { .ow-inline-actions > .ow-input {
@@ -813,24 +872,34 @@ body {
.ow-copy-row { .ow-copy-row {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
gap: 0.46rem; gap: 0.55rem;
align-items: center;
} }
.ow-btn-icon { .ow-btn-icon {
border-radius: 0.76rem; border-radius: 0.75rem;
border: 1px solid #ced8ef; border: 1px solid #cbd5e1;
background: #fff; background: #fff;
color: #1f3a8a; color: #334155;
font-size: 0.74rem; font-size: 0.74rem;
font-weight: 700; font-weight: 600;
padding: 0.66rem 0.75rem; padding: 0.6rem 0.75rem;
min-width: 4.2rem; min-width: 4.2rem;
} }
.ow-btn-primary-inline { .ow-btn-primary-inline {
width: auto; width: auto;
min-width: 12.5rem; min-width: 0;
box-shadow: none; }
.ow-open-btn {
min-width: 12.8rem;
box-shadow: 0 10px 24px rgba(27, 41, 255, 0.22);
}
.ow-connection-block {
display: grid;
gap: 0.38rem;
} }
.ow-log-box { .ow-log-box {

View File

@@ -280,6 +280,18 @@ function getWorkerStatusCopy(status: string): string {
} }
} }
function getWorkerAddressLabel(item: WorkerListItem): string {
if (!item.instanceUrl) {
return shortValue(item.workerId);
}
try {
return new URL(item.instanceUrl).host;
} catch {
return shortValue(item.instanceUrl);
}
}
function isWorkerLaunch(value: unknown): value is WorkerLaunch { function isWorkerLaunch(value: unknown): value is WorkerLaunch {
if (!isRecord(value)) { if (!isRecord(value)) {
return false; return false;
@@ -530,11 +542,21 @@ function CredentialRow({
onCopy: () => void; onCopy: () => void;
}) { }) {
return ( return (
<label className="ow-field-block"> <label className="grid gap-2">
<span className="ow-field-label">{label}</span> <span className="px-0.5 text-[0.67rem] font-bold uppercase tracking-[0.11em] text-slate-500">{label}</span>
<div className="ow-copy-row"> <div className="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 p-1.5">
<input readOnly value={value ?? placeholder} className="ow-input ow-mono" onClick={(event) => event.currentTarget.select()} /> <input
<button type="button" className="ow-btn-icon" disabled={!canCopy} onClick={onCopy}> readOnly
value={value ?? placeholder}
className="min-w-0 flex-1 border-none bg-transparent px-2 py-1.5 font-mono text-xs text-slate-700 outline-none"
onClick={(event) => event.currentTarget.select()}
/>
<button
type="button"
className="rounded-lg border border-slate-200 bg-white px-2.5 py-1 text-xs font-medium text-slate-600 transition hover:border-[#1B29FF] hover:text-[#1B29FF] disabled:cursor-not-allowed disabled:opacity-50"
disabled={!canCopy}
onClick={onCopy}
>
{copied ? "Copied" : canCopy ? "Copy" : "N/A"} {copied ? "Copied" : canCopy ? "Copy" : "N/A"}
</button> </button>
</div> </div>
@@ -1375,7 +1397,7 @@ export function CloudControlPanel() {
) : null} ) : null}
{step === 2 ? ( {step === 2 ? (
<div className="ow-app-shell"> <div className="ow-app-shell bg-[#F4F5F7] p-3 md:p-4 rounded-[32px]">
<aside className="ow-app-nav"> <aside className="ow-app-nav">
<div className="ow-nav-group"> <div className="ow-nav-group">
<p className="ow-nav-label">Workspace</p> <p className="ow-nav-label">Workspace</p>
@@ -1412,8 +1434,12 @@ export function CloudControlPanel() {
<p className="ow-section-title">Workers</p> <p className="ow-section-title">Workers</p>
<p className="ow-caption">Pick a worker to see details.</p> <p className="ow-caption">Pick a worker to see details.</p>
</div> </div>
<button type="button" className="ow-btn-secondary ow-btn-compact" onClick={() => setShowLaunchForm((current) => !current)}> <button
{showLaunchForm ? "Hide launch" : "Launch worker"} type="button"
className="ow-btn-primary ow-btn-compact ow-btn-primary-inline"
onClick={() => setShowLaunchForm((current) => !current)}
>
{showLaunchForm ? "Close" : "+ New Worker"}
</button> </button>
</div> </div>
@@ -1501,9 +1527,12 @@ export function CloudControlPanel() {
<div className="ow-worker-head"> <div className="ow-worker-head">
<div> <div>
<p className="ow-step-title">{item.workerName}</p> <p className="ow-step-title">{item.workerName}</p>
<p className="ow-step-detail">{meta.label}</p> <p className="ow-worker-meta">{getWorkerAddressLabel(item)}</p>
</div>
<div className="ow-worker-badges">
<span className={`ow-status-pill is-${meta.bucket}`}>{meta.label}</span>
{item.isMine ? <span className="ow-badge">Yours</span> : null}
</div> </div>
{item.isMine ? <span className="ow-badge">Yours</span> : null}
</div> </div>
</button> </button>
</li> </li>
@@ -1550,7 +1579,7 @@ export function CloudControlPanel() {
<div className="ow-inline-actions"> <div className="ow-inline-actions">
<button <button
type="button" type="button"
className="ow-btn-primary ow-btn-primary-inline" className="ow-btn-primary ow-btn-primary-inline ow-open-btn"
onClick={() => { onClick={() => {
if (!openworkDeepLink) { if (!openworkDeepLink) {
return; return;
@@ -1563,6 +1592,26 @@ export function CloudControlPanel() {
</button> </button>
</div> </div>
<div className="ow-connection-block">
<p className="ow-field-label">Connection URL</p>
<div className="ow-copy-row">
<input
readOnly
value={openworkConnectUrl ?? "Connection URL is still preparing..."}
className="ow-input ow-mono"
onClick={(event) => event.currentTarget.select()}
/>
<button
type="button"
className="ow-btn-icon"
disabled={!openworkConnectUrl}
onClick={() => void copyToClipboard("openwork-url", openworkConnectUrl)}
>
{copiedField === "openwork-url" ? "Copied" : "Copy"}
</button>
</div>
</div>
{!openworkDeepLink || !openworkConnectUrl || (!hasWorkspaceScopedUrl && openworkConnectUrl) ? ( {!openworkDeepLink || !openworkConnectUrl || (!hasWorkspaceScopedUrl && openworkConnectUrl) ? (
<div className="ow-note-box"> <div className="ow-note-box">
{!openworkDeepLink ? <p className="ow-caption">Getting connection details ready...</p> : null} {!openworkDeepLink ? <p className="ow-caption">Getting connection details ready...</p> : null}
@@ -1572,7 +1621,7 @@ export function CloudControlPanel() {
) : null} ) : null}
<details className="ow-howto"> <details className="ow-howto">
<summary>Copy details manually</summary> <summary>Manual connect details</summary>
<p className="ow-howto-copy"> <p className="ow-howto-copy">
If one-click open doesn't work, copy these into OpenWork: Add a worker &gt; Connect remote. If one-click open doesn't work, copy these into OpenWork: Add a worker &gt; Connect remote.
</p> </p>
@@ -1581,26 +1630,22 @@ export function CloudControlPanel() {
value={openworkConnectUrl} value={openworkConnectUrl}
placeholder="URL appears once ready" placeholder="URL appears once ready"
canCopy={Boolean(openworkConnectUrl)} canCopy={Boolean(openworkConnectUrl)}
copied={copiedField === "openwork-url"} copied={copiedField === "manual-openwork-url"}
onCopy={() => void copyToClipboard("openwork-url", openworkConnectUrl)} onCopy={() => void copyToClipboard("manual-openwork-url", openworkConnectUrl)}
/> />
<CredentialRow <CredentialRow
label="Access token" label="Access token"
value={activeWorker?.clientToken ?? null} value={activeWorker?.clientToken ?? null}
placeholder="Use more options to refresh" placeholder="Use Worker actions to refresh"
canCopy={Boolean(activeWorker?.clientToken)} canCopy={Boolean(activeWorker?.clientToken)}
copied={copiedField === "access-token"} copied={copiedField === "access-token"}
onCopy={() => void copyToClipboard("access-token", activeWorker?.clientToken ?? null)} onCopy={() => void copyToClipboard("access-token", activeWorker?.clientToken ?? null)}
/> />
<figure className="ow-connect-shot">
<img src="/connect-remote-menu.png" alt="OpenWork Add a worker menu with Connect remote option" />
</figure>
</details> </details>
<details className="ow-howto"> <details className="ow-howto">
<summary>More options</summary> <summary>Worker actions</summary>
<div className="ow-inline-actions"> <div className="ow-inline-actions">
<button <button
type="button" type="button"
@@ -1630,7 +1675,7 @@ export function CloudControlPanel() {
</details> </details>
<details className="ow-howto"> <details className="ow-howto">
<summary>Technical details</summary> <summary>Advanced details</summary>
<CredentialRow <CredentialRow
label="Worker host URL" label="Worker host URL"
value={activeWorker?.instanceUrl ?? null} value={activeWorker?.instanceUrl ?? null}