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

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 {
if (!isRecord(value)) {
return false;
@@ -530,11 +542,21 @@ function CredentialRow({
onCopy: () => void;
}) {
return (
<label className="ow-field-block">
<span className="ow-field-label">{label}</span>
<div className="ow-copy-row">
<input readOnly value={value ?? placeholder} className="ow-input ow-mono" onClick={(event) => event.currentTarget.select()} />
<button type="button" className="ow-btn-icon" disabled={!canCopy} onClick={onCopy}>
<label className="grid gap-2">
<span className="px-0.5 text-[0.67rem] font-bold uppercase tracking-[0.11em] text-slate-500">{label}</span>
<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="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"}
</button>
</div>
@@ -1375,7 +1397,7 @@ export function CloudControlPanel() {
) : null}
{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">
<div className="ow-nav-group">
<p className="ow-nav-label">Workspace</p>
@@ -1412,8 +1434,12 @@ export function CloudControlPanel() {
<p className="ow-section-title">Workers</p>
<p className="ow-caption">Pick a worker to see details.</p>
</div>
<button type="button" className="ow-btn-secondary ow-btn-compact" onClick={() => setShowLaunchForm((current) => !current)}>
{showLaunchForm ? "Hide launch" : "Launch worker"}
<button
type="button"
className="ow-btn-primary ow-btn-compact ow-btn-primary-inline"
onClick={() => setShowLaunchForm((current) => !current)}
>
{showLaunchForm ? "Close" : "+ New Worker"}
</button>
</div>
@@ -1501,9 +1527,12 @@ export function CloudControlPanel() {
<div className="ow-worker-head">
<div>
<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>
{item.isMine ? <span className="ow-badge">Yours</span> : null}
</div>
</button>
</li>
@@ -1550,7 +1579,7 @@ export function CloudControlPanel() {
<div className="ow-inline-actions">
<button
type="button"
className="ow-btn-primary ow-btn-primary-inline"
className="ow-btn-primary ow-btn-primary-inline ow-open-btn"
onClick={() => {
if (!openworkDeepLink) {
return;
@@ -1563,6 +1592,26 @@ export function CloudControlPanel() {
</button>
</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) ? (
<div className="ow-note-box">
{!openworkDeepLink ? <p className="ow-caption">Getting connection details ready...</p> : null}
@@ -1572,7 +1621,7 @@ export function CloudControlPanel() {
) : null}
<details className="ow-howto">
<summary>Copy details manually</summary>
<summary>Manual connect details</summary>
<p className="ow-howto-copy">
If one-click open doesn't work, copy these into OpenWork: Add a worker &gt; Connect remote.
</p>
@@ -1581,26 +1630,22 @@ export function CloudControlPanel() {
value={openworkConnectUrl}
placeholder="URL appears once ready"
canCopy={Boolean(openworkConnectUrl)}
copied={copiedField === "openwork-url"}
onCopy={() => void copyToClipboard("openwork-url", openworkConnectUrl)}
copied={copiedField === "manual-openwork-url"}
onCopy={() => void copyToClipboard("manual-openwork-url", openworkConnectUrl)}
/>
<CredentialRow
label="Access token"
value={activeWorker?.clientToken ?? null}
placeholder="Use more options to refresh"
placeholder="Use Worker actions to refresh"
canCopy={Boolean(activeWorker?.clientToken)}
copied={copiedField === "access-token"}
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 className="ow-howto">
<summary>More options</summary>
<summary>Worker actions</summary>
<div className="ow-inline-actions">
<button
type="button"
@@ -1630,7 +1675,7 @@ export function CloudControlPanel() {
</details>
<details className="ow-howto">
<summary>Technical details</summary>
<summary>Advanced details</summary>
<CredentialRow
label="Worker host URL"
value={activeWorker?.instanceUrl ?? null}