6ee34b4e
feat(web): the console speaks the site's language
a73x 2026-08-07 15:12
Commit message
web/src/lib/Console.svelte
| Old | New | ||
|---|---|---|---|
| @@ -146,11 +146,9 @@ | |||
| 146 | 146 | ||
| 147 | <style> | 147 | <style> |
| 148 | .console { | 148 | .console { |
| 149 | background: #15171c; | 149 | border: 1px solid var(--hairline); |
| 150 | border: 1px solid #2a2e37; | 150 | padding: 0.6em 0.8em; |
| 151 | border-radius: 6px; | 151 | margin-top: 0.8em; |
| 152 | padding: 0.6rem 0.8rem; | ||
| 153 | margin-top: 0.8rem; | ||
| 154 | } | 152 | } |
| 155 | .console header { | 153 | .console header { |
| 156 | display: flex; | 154 | display: flex; |
| @@ -159,29 +157,30 @@ | |||
| 159 | } | 157 | } |
| 160 | .console h3 { | 158 | .console h3 { |
| 161 | margin: 0; | 159 | margin: 0; |
| 162 | font-size: 13px; | 160 | font-size: inherit; |
| 163 | } | 161 | } |
| 162 | /* The terminal itself stays a dark surface—it is a terminal—but its frame | ||
| 163 | is the console's own hairline. */ | ||
| 164 | .term { | 164 | .term { |
| 165 | min-height: 320px; | 165 | min-height: 320px; |
| 166 | background: #000; | 166 | background: #000; |
| 167 | padding: 4px; | 167 | padding: 4px; |
| 168 | border-radius: 4px; | 168 | margin-top: 0.5em; |
| 169 | margin-top: 0.5rem; | ||
| 170 | /* xterm renders a fixed 80-col canvas (~640px): scroll it inside the | 169 | /* xterm renders a fixed 80-col canvas (~640px): scroll it inside the |
| 171 | card instead of overflowing narrow viewports. */ | 170 | panel instead of overflowing narrow viewports. */ |
| 172 | overflow-x: auto; | 171 | overflow-x: auto; |
| 173 | } | 172 | } |
| 174 | .term.hidden { | 173 | .term.hidden { |
| 175 | display: none; | 174 | display: none; |
| 176 | } | 175 | } |
| 177 | .err { | 176 | .err { |
| 178 | color: #ffb4b4; | 177 | color: var(--bad); |
| 179 | margin: 0.4rem 0 0; | 178 | margin: 0.4em 0 0; |
| 180 | } | 179 | } |
| 181 | /* color comes from the global .hint rule (+layout.svelte); this one just | 180 | /* colour comes from the global .hint rule (+layout.svelte); this one just |
| 182 | wants a smaller size and top margin. */ | 181 | wants a smaller size and top margin. */ |
| 183 | .hint { | 182 | .hint { |
| 184 | font-size: 0.85em; | 183 | font-size: 0.85em; |
| 185 | margin: 0.4rem 0 0; | 184 | margin: 0.4em 0 0; |
| 186 | } | 185 | } |
| 187 | </style> | 186 | </style> |
web/src/lib/ResourceBar.svelte
| Old | New | ||
|---|---|---|---|
| @@ -27,43 +27,38 @@ | |||
| 27 | 27 | ||
| 28 | <style> | 28 | <style> |
| 29 | .bar { | 29 | .bar { |
| 30 | margin: 0.35rem 0; | 30 | margin: 0.35em 0; |
| 31 | } | 31 | } |
| 32 | .head { | 32 | .head { |
| 33 | display: flex; | 33 | display: flex; |
| 34 | justify-content: space-between; | 34 | justify-content: space-between; |
| 35 | gap: 1em; | ||
| 35 | margin-bottom: 2px; | 36 | margin-bottom: 2px; |
| 36 | } | 37 | } |
| 37 | .label { | 38 | .label { |
| 38 | color: #8b919c; | 39 | color: var(--faint); |
| 39 | } | 40 | } |
| 40 | .nums { | 41 | .nums { |
| 41 | color: #d6d9df; | 42 | font-variant-numeric: tabular-nums; |
| 42 | } | ||
| 43 | .free { | ||
| 44 | color: #6b7280; | ||
| 45 | } | 43 | } |
| 44 | .free, | ||
| 46 | .dim { | 45 | .dim { |
| 47 | color: #6b7280; | 46 | color: var(--faint); |
| 48 | } | 47 | } |
| 48 | /* The same meter the fleet table uses, stretched to the panel: a wash track | ||
| 49 | with an ink fill. The numbers above it are the reading; the bar is the | ||
| 50 | glance. Past 90% the fill turns, because that is a wall you can hit. */ | ||
| 49 | .track { | 51 | .track { |
| 50 | height: 8px; | 52 | height: 3px; |
| 51 | background: #20242c; | 53 | background: var(--wash); |
| 52 | border-radius: 4px; | ||
| 53 | overflow: hidden; | 54 | overflow: hidden; |
| 54 | } | 55 | } |
| 55 | .fill { | 56 | .fill { |
| 56 | height: 100%; | 57 | height: 100%; |
| 57 | border-radius: 4px; | 58 | background: var(--ink); |
| 58 | transition: width 0.3s; | 59 | transition: width 0.3s; |
| 59 | } | 60 | } |
| 60 | .fill.ok { | ||
| 61 | background: #34d399; | ||
| 62 | } | ||
| 63 | .fill.warm { | ||
| 64 | background: #f5b945; | ||
| 65 | } | ||
| 66 | .fill.hot { | 61 | .fill.hot { |
| 67 | background: #f87171; | 62 | background: var(--bad); |
| 68 | } | 63 | } |
| 69 | </style> | 64 | </style> |
web/src/routes/+layout.svelte
| Old | New | ||
|---|---|---|---|
| @@ -71,126 +71,181 @@ | |||
| 71 | </main> | 71 | </main> |
| 72 | 72 | ||
| 73 | <style> | 73 | <style> |
| 74 | :global(body) { | 74 | /* The console is derived from the site (site/style.css) and softened for an |
| 75 | app: the same monospace and the same square unfilled boxes, but rules | ||
| 76 | instead of a full cell grid. The vocabulary is named once here — ink, | ||
| 77 | paper, hairline, faint, wash — so the rest of the console inherits it | ||
| 78 | instead of carrying literals. */ | ||
| 79 | :global(:root) { | ||
| 80 | color-scheme: light dark; | ||
| 81 | --ink: CanvasText; | ||
| 82 | --paper: Canvas; | ||
| 83 | --hairline: color-mix(in srgb, CanvasText 25%, transparent); | ||
| 84 | --faint: color-mix(in srgb, CanvasText 55%, transparent); | ||
| 85 | --wash: color-mix(in srgb, CanvasText 6%, transparent); | ||
| 86 | --ok: #3a9e4f; | ||
| 87 | --bad: #c2503a; | ||
| 88 | } | ||
| 89 | :global(*) { | ||
| 75 | margin: 0; | 90 | margin: 0; |
| 76 | font-family: ui-monospace, SFMono-Regular, Menlo, monospace; | 91 | padding: 0; |
| 77 | background: #0c0d10; | 92 | box-sizing: border-box; |
| 78 | color: #d6d9df; | 93 | } |
| 79 | font-size: 13px; | 94 | :global(body) { |
| 95 | font-family: monospace; | ||
| 96 | line-height: 1.5; | ||
| 97 | color: var(--ink); | ||
| 98 | background: var(--paper); | ||
| 80 | } | 99 | } |
| 81 | :global(a) { | 100 | :global(a) { |
| 82 | color: #6cb6ff; | 101 | color: inherit; |
| 83 | text-decoration: none; | 102 | } |
| 103 | :global(h1) { | ||
| 104 | margin-bottom: 0.5em; | ||
| 84 | } | 105 | } |
| 85 | :global(a:hover) { | 106 | :global(h2) { |
| 86 | text-decoration: underline; | 107 | margin: 1.5em 0 0.5em; |
| 87 | } | 108 | } |
| 109 | :global(h3) { | ||
| 110 | margin: 1em 0 0.5em; | ||
| 111 | } | ||
| 112 | :global(p, ul, ol) { | ||
| 113 | margin-bottom: 1em; | ||
| 114 | } | ||
| 115 | :global(ul, ol) { | ||
| 116 | padding-left: 2em; | ||
| 117 | } | ||
| 118 | :global(li) { | ||
| 119 | margin-bottom: 0.25em; | ||
| 120 | } | ||
| 121 | :global(code) { | ||
| 122 | word-break: break-all; | ||
| 123 | } | ||
| 124 | /* One strong rule under the head, a hairline between rows, and a wash under | ||
| 125 | the pointer: enough structure to track a row across nine columns without | ||
| 126 | boxing every cell. */ | ||
| 88 | :global(table) { | 127 | :global(table) { |
| 89 | border-collapse: collapse; | 128 | border-collapse: collapse; |
| 90 | width: 100%; | 129 | margin: 0.5em 0 1.5em; |
| 91 | margin: 0.5rem 0 1.5rem; | ||
| 92 | } | 130 | } |
| 93 | :global(th, td) { | 131 | :global(th) { |
| 132 | border-bottom: 1px solid var(--ink); | ||
| 133 | padding: 0.35em 0.9em 0.35em 0; | ||
| 94 | text-align: left; | 134 | text-align: left; |
| 95 | padding: 0.4rem 0.6rem; | 135 | font-weight: bold; |
| 96 | border-bottom: 1px solid #20242c; | ||
| 97 | white-space: nowrap; | 136 | white-space: nowrap; |
| 98 | } | 137 | } |
| 99 | :global(th) { | 138 | :global(td) { |
| 100 | color: #8b919c; | 139 | border-bottom: 1px solid var(--hairline); |
| 101 | font-weight: 600; | 140 | padding: 0.45em 0.9em 0.45em 0; |
| 102 | border-bottom: 1px solid #2a2e37; | 141 | white-space: nowrap; |
| 103 | } | 142 | } |
| 143 | :global(tr:hover td) { | ||
| 144 | background: var(--wash); | ||
| 145 | } | ||
| 146 | /* A key/value table has no head, so its keys are labels rather than | ||
| 147 | headings: faint, unemphasised, and ruled like any other row. */ | ||
| 148 | :global(.kv th) { | ||
| 149 | border-bottom: 1px solid var(--hairline); | ||
| 150 | color: var(--faint); | ||
| 151 | font-weight: normal; | ||
| 152 | vertical-align: top; | ||
| 153 | width: 20ch; | ||
| 154 | } | ||
| 155 | :global(td.num, th.num) { | ||
| 156 | text-align: right; | ||
| 157 | font-variant-numeric: tabular-nums; | ||
| 158 | } | ||
| 159 | /* A status light is a glyph, not a swatch: filled and green for on, hollow | ||
| 160 | and faint for off. Only the glyph takes the colour — the label beside it | ||
| 161 | stays ink, so the state reads without it. */ | ||
| 104 | :global(.dot) { | 162 | :global(.dot) { |
| 105 | width: 8px; | ||
| 106 | height: 8px; | ||
| 107 | border-radius: 50%; | ||
| 108 | display: inline-block; | 163 | display: inline-block; |
| 164 | margin-right: 0.25em; | ||
| 109 | } | 165 | } |
| 110 | :global(.dot.on) { | 166 | :global(.dot.on) { |
| 111 | background: #34d399; | 167 | color: var(--ok); |
| 112 | } | 168 | } |
| 113 | :global(.dot.off) { | 169 | :global(.dot.off) { |
| 114 | background: #f87171; | 170 | color: var(--faint); |
| 171 | } | ||
| 172 | :global(.dot.on::before) { | ||
| 173 | content: '●'; | ||
| 174 | } | ||
| 175 | :global(.dot.off::before) { | ||
| 176 | content: '○'; | ||
| 115 | } | 177 | } |
| 116 | :global(.hint) { | 178 | :global(.hint) { |
| 117 | color: #6b7280; | 179 | color: var(--faint); |
| 118 | } | 180 | } |
| 119 | header { | 181 | header { |
| 120 | display: flex; | 182 | display: flex; |
| 121 | align-items: center; | 183 | align-items: center; |
| 122 | gap: 0.6rem; | 184 | gap: 0.75em; |
| 123 | padding: 0.6rem 1rem; | 185 | padding: 1em; |
| 124 | background: #15171c; | 186 | border-bottom: 1px solid var(--hairline); |
| 125 | border-bottom: 1px solid #23262d; | ||
| 126 | } | 187 | } |
| 127 | .brand { | 188 | .brand { |
| 128 | font-weight: 700; | 189 | font-weight: bold; |
| 129 | color: #fff; | 190 | text-decoration: none; |
| 130 | font-size: 15px; | ||
| 131 | } | 191 | } |
| 132 | .fleet-label { | 192 | .fleet-label, |
| 133 | color: #6b7280; | 193 | .conn, |
| 194 | .email { | ||
| 195 | color: var(--faint); | ||
| 134 | } | 196 | } |
| 135 | .spacer { | 197 | .spacer { |
| 136 | flex: 1; | 198 | flex: 1; |
| 137 | } | 199 | } |
| 138 | .conn { | 200 | /* Controls are real but quiet: a hairline box that firms up to ink when you |
| 139 | color: #9aa0aa; | 201 | reach for it, and to red when what it does cannot be undone. */ |
| 140 | margin-right: 0.5rem; | 202 | :global(input, select, textarea) { |
| 141 | } | 203 | font: inherit; |
| 142 | .email { | 204 | color: inherit; |
| 143 | color: #9aa0aa; | 205 | background: none; |
| 144 | } | 206 | border: 1px solid var(--hairline); |
| 145 | :global(input, button, select, textarea) { | 207 | border-radius: 0; |
| 146 | background: #0c0d10; | 208 | padding: 0.05em 0.6em; |
| 147 | color: #d6d9df; | ||
| 148 | border: 1px solid #2a2e37; | ||
| 149 | border-radius: 4px; | ||
| 150 | padding: 0.3rem 0.5rem; | ||
| 151 | font-family: inherit; | ||
| 152 | font-size: 12px; | ||
| 153 | box-sizing: border-box; | 209 | box-sizing: border-box; |
| 154 | } | 210 | } |
| 211 | :global(input:focus, select:focus, textarea:focus) { | ||
| 212 | border-color: var(--ink); | ||
| 213 | } | ||
| 155 | :global(button) { | 214 | :global(button) { |
| 215 | font: inherit; | ||
| 216 | color: inherit; | ||
| 217 | background: none; | ||
| 218 | border: 1px solid var(--hairline); | ||
| 219 | border-radius: 0; | ||
| 220 | padding: 0.05em 0.6em; | ||
| 156 | cursor: pointer; | 221 | cursor: pointer; |
| 157 | background: #1f6feb; | ||
| 158 | border-color: #1f6feb; | ||
| 159 | color: #fff; | ||
| 160 | } | 222 | } |
| 161 | :global(button:hover) { | 223 | :global(button:hover) { |
| 162 | background: #2b7bf5; | 224 | border-color: var(--ink); |
| 163 | } | 225 | } |
| 164 | :global(button.ghost) { | 226 | :global(button.danger:hover) { |
| 165 | background: transparent; | 227 | border-color: var(--bad); |
| 166 | border-color: #2a2e37; | 228 | color: var(--bad); |
| 167 | color: #d6d9df; | ||
| 168 | } | 229 | } |
| 169 | :global(button.danger) { | 230 | /* A tick box is the one control the browser draws better than we can. */ |
| 170 | background: #b9333f; | 231 | :global(input[type='checkbox'], input[type='radio']) { |
| 171 | border-color: #b9333f; | 232 | border: 0; |
| 233 | padding: 0; | ||
| 172 | } | 234 | } |
| 235 | /* Wide enough for a full fleet table; the site's 72ch measure is for prose, | ||
| 236 | and it is the language that carries over, not the measure. */ | ||
| 173 | main { | 237 | main { |
| 174 | padding: 1rem; | 238 | padding: 1em; |
| 175 | max-width: 1100px; | 239 | max-width: 170ch; |
| 240 | margin: 0 auto; | ||
| 176 | } | 241 | } |
| 177 | .error { | 242 | .error { |
| 178 | background: #3a1518; | ||
| 179 | color: #ffb4b4; | ||
| 180 | padding: 0.5rem 1rem; | ||
| 181 | border-bottom: 1px solid #5a2025; | ||
| 182 | display: flex; | 243 | display: flex; |
| 183 | align-items: center; | 244 | align-items: center; |
| 184 | justify-content: space-between; | 245 | justify-content: space-between; |
| 185 | gap: 0.6rem; | 246 | gap: 0.75em; |
| 186 | } | 247 | padding: 0.5em 1em; |
| 187 | .error .dismiss { | 248 | border-bottom: 1px solid var(--hairline); |
| 188 | background: transparent; | 249 | color: var(--bad); |
| 189 | border: 1px solid #5a2025; | ||
| 190 | color: #ffb4b4; | ||
| 191 | padding: 0.1rem 0.45rem; | ||
| 192 | } | ||
| 193 | .error .dismiss:hover { | ||
| 194 | background: #5a2025; | ||
| 195 | } | 250 | } |
| 196 | </style> | 251 | </style> |
web/src/routes/+page.svelte
| Old | New | ||
|---|---|---|---|
| @@ -210,59 +210,70 @@ | |||
| 210 | {/if} | 210 | {/if} |
| 211 | 211 | ||
| 212 | {#if fleet.hosts.length > 0 && shownHosts.length > 0} | 212 | {#if fleet.hosts.length > 0 && shownHosts.length > 0} |
| 213 | <table> | 213 | <div class="scroll"> |
| 214 | <thead> | 214 | <table> |
| 215 | <tr><th>Name</th><th>Status</th><th>OS</th><th>Version</th><th>Load</th><th>VMs</th><th>Guest network</th><th>Used / total (vCPU · mem · disk)</th><th></th></tr> | 215 | <thead> |
| 216 | </thead> | ||
| 217 | <tbody> | ||
| 218 | {#each shownHosts as h (h.id)} | ||
| 219 | {@const load = loadRatio(h)} | ||
| 220 | <tr> | 216 | <tr> |
| 221 | <td><a href="/hosts/{h.id}">{h.name}</a></td> | 217 | <th rowspan="2">Name</th> |
| 222 | <td> | 218 | <th rowspan="2">Status</th> |
| 223 | <span class="dot {h.online ? 'on' : 'off'}"></span> | 219 | <th rowspan="2">OS</th> |
| 224 | {h.status}{h.online ? '' : ' · offline'} | 220 | <th rowspan="2">Version</th> |
| 225 | </td> | 221 | <th rowspan="2" class="num">Load</th> |
| 226 | <td title={osTitle(h)}>{osLabel(h)}</td> | 222 | <th rowspan="2" class="num">VMs</th> |
| 227 | <td> | 223 | <th rowspan="2">Guest network</th> |
| 228 | {h.agent_version || '—'} | 224 | <th colspan="3" class="group">Used / total</th> |
| 229 | {#if h.agent_update_available} | 225 | <th rowspan="2"></th> |
| 230 | <button class="ghost upgrade" onclick={() => upgrade(h)}>↑ {fleet.latest_version}</button> | ||
| 231 | {/if} | ||
| 232 | </td> | ||
| 233 | <td> | ||
| 234 | {#if load !== null} | ||
| 235 | <span class="loadcell" title="load1 {h.metrics?.load1.toFixed(2)} · {h.capacity.vcpus} vCPU"> | ||
| 236 | <span | ||
| 237 | class="loadbar" | ||
| 238 | class:calm={load <= 0.7} | ||
| 239 | class:warm={load > 0.7 && load <= 1} | ||
| 240 | class:hot={load > 1} | ||
| 241 | ><span style="width:{Math.min(load, 1) * 100}%"></span></span> | ||
| 242 | {load.toFixed(2)} | ||
| 243 | </span> | ||
| 244 | {:else} | ||
| 245 | <span class="hint">—</span> | ||
| 246 | {/if} | ||
| 247 | </td> | ||
| 248 | <td>{vmCountByHost.get(h.id) ?? 0}</td> | ||
| 249 | <td>{h.bridge_cidr || '—'}</td> | ||
| 250 | <td> | ||
| 251 | {h.allocated.vcpus}/{h.capacity.vcpus || '?'}c · | ||
| 252 | {h.allocated.mem_mb}/{h.capacity.mem_mb || '?'}MB · | ||
| 253 | {h.allocated.disk_gb}/{h.capacity.disk_gb || '?'}GB | ||
| 254 | </td> | ||
| 255 | <td> | ||
| 256 | {#if h.status !== 'decommissioning'} | ||
| 257 | <button class="danger" onclick={() => decommission(h.id, h.name)}>Decommission</button> | ||
| 258 | {:else} | ||
| 259 | <span class="hint">draining…</span> | ||
| 260 | {/if} | ||
| 261 | </td> | ||
| 262 | </tr> | 226 | </tr> |
| 263 | {/each} | 227 | <tr><th class="num">vCPU</th><th class="num">mem MB</th><th class="num">disk GB</th></tr> |
| 264 | </tbody> | 228 | </thead> |
| 265 | </table> | 229 | <tbody> |
| 230 | {#each shownHosts as h (h.id)} | ||
| 231 | {@const load = loadRatio(h)} | ||
| 232 | <tr> | ||
| 233 | <td><a href="/hosts/{h.id}">{h.name}</a></td> | ||
| 234 | <td> | ||
| 235 | <span class="dot {h.online ? 'on' : 'off'}"></span> | ||
| 236 | {h.status}{h.online ? '' : ' · offline'} | ||
| 237 | </td> | ||
| 238 | <td title={osTitle(h)}>{osLabel(h)}</td> | ||
| 239 | <td> | ||
| 240 | {h.agent_version || '—'} | ||
| 241 | {#if h.agent_update_available} | ||
| 242 | <button class="ghost upgrade" onclick={() => upgrade(h)}>↑ {fleet.latest_version}</button> | ||
| 243 | {/if} | ||
| 244 | </td> | ||
| 245 | <td class="num"> | ||
| 246 | {#if load !== null} | ||
| 247 | <span | ||
| 248 | class="load" | ||
| 249 | class:over={load > 1} | ||
| 250 | title="load1 {h.metrics?.load1.toFixed(2)} · {h.capacity.vcpus} vCPU" | ||
| 251 | > | ||
| 252 | {load.toFixed(2)}<span class="meter" | ||
| 253 | ><i style="width:{Math.min(load, 1) * 100}%"></i></span | ||
| 254 | > | ||
| 255 | </span> | ||
| 256 | {:else} | ||
| 257 | <span class="hint">—</span> | ||
| 258 | {/if} | ||
| 259 | </td> | ||
| 260 | <td class="num">{vmCountByHost.get(h.id) ?? 0}</td> | ||
| 261 | <td>{h.bridge_cidr || '—'}</td> | ||
| 262 | <td class="num">{h.allocated.vcpus}/{h.capacity.vcpus || '?'}</td> | ||
| 263 | <td class="num">{h.allocated.mem_mb}/{h.capacity.mem_mb || '?'}</td> | ||
| 264 | <td class="num">{h.allocated.disk_gb}/{h.capacity.disk_gb || '?'}</td> | ||
| 265 | <td> | ||
| 266 | {#if h.status !== 'decommissioning'} | ||
| 267 | <button class="danger" onclick={() => decommission(h.id, h.name)}>Decommission</button> | ||
| 268 | {:else} | ||
| 269 | <span class="hint">draining…</span> | ||
| 270 | {/if} | ||
| 271 | </td> | ||
| 272 | </tr> | ||
| 273 | {/each} | ||
| 274 | </tbody> | ||
| 275 | </table> | ||
| 276 | </div> | ||
| 266 | {/if} | 277 | {/if} |
| 267 | </section> | 278 | </section> |
| 268 | 279 | ||
| @@ -277,38 +288,40 @@ | |||
| 277 | {:else if shownVMs.length === 0} | 288 | {:else if shownVMs.length === 0} |
| 278 | <p class="hint">No VMs match “{filter}”.</p> | 289 | <p class="hint">No VMs match “{filter}”.</p> |
| 279 | {:else} | 290 | {:else} |
| 280 | <table> | 291 | <div class="scroll"> |
| 281 | <thead> | 292 | <table> |
| 282 | <tr><th>Name</th><th>Host</th><th>Allocation</th><th>Status</th><th>Power</th><th></th></tr> | 293 | <thead> |
| 283 | </thead> | 294 | <tr><th>Name</th><th>Host</th><th class="num">Allocation</th><th>Status</th><th>Power</th><th></th></tr> |
| 284 | <tbody> | 295 | </thead> |
| 285 | {#each shownVMs as v (v.id)} | 296 | <tbody> |
| 286 | <tr> | 297 | {#each shownVMs as v (v.id)} |
| 287 | <td><a href="/vms/{v.id}">{v.name}</a></td> | 298 | <tr> |
| 288 | <td>{hostName(v.host_id)}</td> | 299 | <td><a href="/vms/{v.id}">{v.name}</a></td> |
| 289 | <td>{v.vcpus}c · {v.mem_mb}MB · {v.disk_gb}GB</td> | 300 | <td>{hostName(v.host_id)}</td> |
| 290 | <td> | 301 | <td class="num">{v.vcpus}c · {v.mem_mb}MB · {v.disk_gb}GB</td> |
| 291 | {#if v.deleted}<span class="teardown">deleting—undo available</span>{:else}{vmStatus(v)}{/if}{v.last_error | 302 | <td> |
| 292 | ? ` · ${v.last_error}` | 303 | {#if v.deleted}<span class="teardown">deleting—undo available</span>{:else}{vmStatus(v)}{/if}{v.last_error |
| 293 | : ''} | 304 | ? ` · ${v.last_error}` |
| 294 | </td> | 305 | : ''} |
| 295 | <td>{vmPower(v)}</td> | 306 | </td> |
| 296 | <td class="actions"> | 307 | <td>{vmPower(v)}</td> |
| 297 | {#if v.deleted} | 308 | <td class="actions"> |
| 298 | <button class="restore" onclick={() => restore(v.id)}>Undo delete</button> | 309 | {#if v.deleted} |
| 299 | {:else} | 310 | <button class="restore" onclick={() => restore(v.id)}>Undo delete</button> |
| 300 | {#if vmIsRunning(v)} | ||
| 301 | <button class="ghost" onclick={() => power(v.id, 'stopped')}>Stop</button> | ||
| 302 | {:else} | 311 | {:else} |
| 303 | <button class="ghost" onclick={() => power(v.id, 'running')}>Start</button> | 312 | {#if vmIsRunning(v)} |
| 313 | <button class="danger" onclick={() => power(v.id, 'stopped')}>Stop</button> | ||
| 314 | {:else} | ||
| 315 | <button class="ghost" onclick={() => power(v.id, 'running')}>Start</button> | ||
| 316 | {/if} | ||
| 317 | <button class="danger" onclick={() => remove(v.id)}>Delete</button> | ||
| 304 | {/if} | 318 | {/if} |
| 305 | <button class="danger" onclick={() => remove(v.id)}>Delete</button> | 319 | </td> |
| 306 | {/if} | 320 | </tr> |
| 307 | </td> | 321 | {/each} |
| 308 | </tr> | 322 | </tbody> |
| 309 | {/each} | 323 | </table> |
| 310 | </tbody> | 324 | </div> |
| 311 | </table> | ||
| 312 | {/if} | 325 | {/if} |
| 313 | </section> | 326 | </section> |
| 314 | 327 | ||
| @@ -362,118 +375,115 @@ | |||
| 362 | .row { | 375 | .row { |
| 363 | display: flex; | 376 | display: flex; |
| 364 | align-items: center; | 377 | align-items: center; |
| 365 | gap: 0.8rem; | 378 | gap: 0.75em; |
| 366 | } | 379 | } |
| 367 | .row.end { | 380 | .row.end { |
| 368 | justify-content: flex-end; | 381 | justify-content: flex-end; |
| 369 | margin-top: 0.5rem; | 382 | margin-top: 0.5em; |
| 370 | } | 383 | } |
| 371 | h2 { | 384 | h2 { |
| 372 | font-size: 14px; | 385 | font-size: inherit; |
| 373 | margin: 1rem 0 0; | 386 | margin: 1.5em 0 0.5em; |
| 374 | } | 387 | } |
| 375 | /* base .dot rule and .on/.off colors are global (+layout.svelte); this | 388 | h3 { |
| 376 | page's dot just wants a bit more breathing room before the text. */ | 389 | font-size: inherit; |
| 377 | .dot { | 390 | } |
| 378 | margin-right: 3px; | 391 | /* Every table keeps its natural width and scrolls sideways inside its own |
| 379 | } | 392 | box. Nothing is squeezed and nothing is truncated: a CIDR or a host name |
| 380 | .actions { | 393 | that does not fit is scrolled to, never elided into a half-truth. */ |
| 381 | display: flex; | 394 | .scroll { |
| 382 | gap: 0.3rem; | 395 | overflow-x: auto; |
| 396 | } | ||
| 397 | .scroll table { | ||
| 398 | min-width: 100%; | ||
| 399 | } | ||
| 400 | /* The heading that spans the three capacity columns. Numbers align on their | ||
| 401 | digits (td.num/th.num, global) so capacity reads down the column instead | ||
| 402 | of drifting with the width of each value. */ | ||
| 403 | .group { | ||
| 404 | text-align: center; | ||
| 405 | } | ||
| 406 | /* Normalized-load pressure: the number is the reading, and the meter beside | ||
| 407 | it is the same value at a glance. A host past 1.0 sets in bold. */ | ||
| 408 | .load { | ||
| 409 | display: inline-block; | ||
| 410 | font-variant-numeric: tabular-nums; | ||
| 383 | } | 411 | } |
| 384 | /* Normalized-load pressure gauge: the number is the source of truth, the | 412 | .load.over { |
| 385 | bar + colour are a supplementary at-a-glance signal (calm/warm/hot). */ | 413 | font-weight: bold; |
| 386 | .loadcell { | ||
| 387 | display: inline-flex; | ||
| 388 | align-items: center; | ||
| 389 | gap: 0.4rem; | ||
| 390 | white-space: nowrap; | ||
| 391 | } | 414 | } |
| 392 | .loadbar { | 415 | .meter { |
| 393 | display: inline-block; | 416 | display: inline-block; |
| 394 | width: 34px; | 417 | width: 6ch; |
| 395 | height: 6px; | 418 | height: 3px; |
| 396 | border-radius: 3px; | 419 | background: var(--wash); |
| 397 | background: #2a2e37; | 420 | vertical-align: middle; |
| 398 | overflow: hidden; | 421 | margin-left: 0.6ch; |
| 399 | } | 422 | } |
| 400 | .loadbar > span { | 423 | .meter > i { |
| 401 | display: block; | 424 | display: block; |
| 402 | height: 100%; | 425 | height: 100%; |
| 403 | border-radius: 3px; | 426 | background: var(--ink); |
| 404 | } | ||
| 405 | .loadbar.calm > span { | ||
| 406 | background: #8fe3a0; | ||
| 407 | } | 427 | } |
| 408 | .loadbar.warm > span { | 428 | .upgrade { |
| 409 | background: #e3c98f; | 429 | margin-left: 0.5em; |
| 410 | } | 430 | } |
| 411 | .loadbar.hot > span { | 431 | .actions { |
| 412 | background: #e38f8f; | 432 | display: flex; |
| 433 | gap: 0.5em; | ||
| 413 | } | 434 | } |
| 414 | .enroll { | 435 | .enroll { |
| 415 | background: #15171c; | 436 | border: 1px solid var(--hairline); |
| 416 | border: 1px solid #2a2e37; | 437 | padding: 1em; |
| 417 | border-radius: 4px; | 438 | margin: 0.5em 0; |
| 418 | padding: 0.5rem; | 439 | overflow-x: auto; |
| 419 | margin: 0.5rem 0; | ||
| 420 | } | ||
| 421 | .update-banner { | ||
| 422 | background: #15171c; | ||
| 423 | border: 1px solid #2a2e37; | ||
| 424 | border-radius: 4px; | ||
| 425 | padding: 0.5rem 0.8rem; | ||
| 426 | margin: 0.8rem 0 0; | ||
| 427 | color: #9aa0aa; | ||
| 428 | } | ||
| 429 | .upgrade { | ||
| 430 | margin-left: 0.4rem; | ||
| 431 | padding: 0.1rem 0.45rem; | ||
| 432 | font-size: 12px; | ||
| 433 | } | 440 | } |
| 434 | .enroll code { | 441 | .enroll code { |
| 435 | display: block; | 442 | display: block; |
| 436 | margin-top: 0.3rem; | ||
| 437 | color: #8fe3a0; | ||
| 438 | word-break: break-all; | 443 | word-break: break-all; |
| 439 | } | 444 | } |
| 445 | .update-banner { | ||
| 446 | display: inline-block; | ||
| 447 | border: 1px solid var(--hairline); | ||
| 448 | padding: 0.25em 0.75em; | ||
| 449 | margin: 1em 0 0; | ||
| 450 | } | ||
| 440 | .modal { | 451 | .modal { |
| 441 | position: fixed; | 452 | position: fixed; |
| 442 | inset: 0; | 453 | inset: 0; |
| 443 | background: rgba(0, 0, 0, 0.6); | 454 | background: rgba(0, 0, 0, 0.5); |
| 444 | display: flex; | 455 | display: flex; |
| 445 | align-items: center; | 456 | align-items: center; |
| 446 | justify-content: center; | 457 | justify-content: center; |
| 447 | } | 458 | } |
| 448 | .card { | 459 | .card { |
| 449 | background: #15171c; | 460 | background: var(--paper); |
| 450 | border: 1px solid #2a2e37; | 461 | color: var(--ink); |
| 451 | border-radius: 8px; | 462 | border: 1px solid var(--ink); |
| 452 | padding: 1.2rem; | 463 | padding: 1em; |
| 453 | width: 420px; | 464 | width: 52ch; |
| 454 | max-width: 92vw; | 465 | max-width: 92vw; |
| 455 | display: flex; | 466 | display: flex; |
| 456 | flex-direction: column; | 467 | flex-direction: column; |
| 457 | gap: 0.5rem; | 468 | gap: 0.5em; |
| 458 | } | 469 | } |
| 459 | .card h3 { | 470 | .card h3 { |
| 460 | margin: 0 0 0.3rem; | 471 | margin: 0 0 0.25em; |
| 461 | } | 472 | } |
| 462 | label { | 473 | label { |
| 463 | display: flex; | 474 | display: flex; |
| 464 | flex-direction: column; | 475 | flex-direction: column; |
| 465 | gap: 0.2rem; | 476 | gap: 0.1em; |
| 466 | color: #9aa0aa; | ||
| 467 | } | 477 | } |
| 468 | label.checkbox { | 478 | label.checkbox { |
| 469 | flex-direction: row; | 479 | flex-direction: row; |
| 470 | align-items: center; | 480 | align-items: center; |
| 471 | gap: 0.4rem; | 481 | gap: 0.5em; |
| 472 | } | 482 | } |
| 473 | .grid { | 483 | .grid { |
| 474 | display: grid; | 484 | display: grid; |
| 475 | grid-template-columns: 1fr 1fr 1fr; | 485 | grid-template-columns: 1fr 1fr 1fr; |
| 476 | gap: 0.5rem; | 486 | gap: 0.5em; |
| 477 | } | 487 | } |
| 478 | /* Let grid columns shrink to their 1fr share; number inputs have an | 488 | /* Let grid columns shrink to their 1fr share; number inputs have an |
| 479 | intrinsic min-width that otherwise pushes the third column past the card. */ | 489 | intrinsic min-width that otherwise pushes the third column past the card. */ |
| @@ -485,24 +495,28 @@ | |||
| 485 | .card textarea { | 495 | .card textarea { |
| 486 | width: 100%; | 496 | width: 100%; |
| 487 | } | 497 | } |
| 498 | /* A tick box keeps its own size; only the fields fill the card. */ | ||
| 499 | .card input[type='checkbox'] { | ||
| 500 | width: auto; | ||
| 501 | } | ||
| 488 | .filter { | 502 | .filter { |
| 489 | margin-top: 0.8rem; | 503 | margin-top: 1em; |
| 490 | width: 260px; | 504 | width: 28ch; |
| 491 | } | 505 | } |
| 492 | .teardown { | 506 | .teardown { |
| 493 | color: #f0b429; | 507 | font-weight: bold; |
| 494 | font-weight: 600; | ||
| 495 | } | 508 | } |
| 496 | .onboard { | 509 | .onboard { |
| 497 | background: #15171c; | 510 | border: 1px solid var(--hairline); |
| 498 | border: 1px solid #2a2e37; | 511 | padding: 1em; |
| 499 | border-radius: 8px; | 512 | margin: 0.5em 0; |
| 500 | padding: 1.2rem; | 513 | max-width: 72ch; |
| 501 | margin: 0.5rem 0; | ||
| 502 | max-width: 620px; | ||
| 503 | } | 514 | } |
| 504 | .onboard h3 { | 515 | .onboard h3 { |
| 505 | margin: 0 0 0.4rem; | 516 | margin: 0 0 0.5em; |
| 517 | } | ||
| 518 | .onboard p { | ||
| 519 | margin-bottom: 0.5em; | ||
| 506 | } | 520 | } |
| 507 | .onboard .enroll { | 521 | .onboard .enroll { |
| 508 | margin-bottom: 0; | 522 | margin-bottom: 0; |
web/src/routes/hosts/[id]/+page.svelte
| Old | New | ||
|---|---|---|---|
| @@ -135,28 +135,17 @@ | |||
| 135 | 135 | ||
| 136 | <style> | 136 | <style> |
| 137 | h2 { | 137 | h2 { |
| 138 | font-size: 14px; | 138 | font-size: inherit; |
| 139 | } | 139 | } |
| 140 | .resources { | 140 | .resources { |
| 141 | max-width: 460px; | 141 | max-width: 60ch; |
| 142 | } | 142 | } |
| 143 | .note { | 143 | .note { |
| 144 | color: #6b7280; | 144 | color: var(--faint); |
| 145 | max-width: 600px; | 145 | max-width: 72ch; |
| 146 | margin: 0.3rem 0 1rem; | 146 | margin: 0.3em 0 1em; |
| 147 | } | ||
| 148 | .kv th { | ||
| 149 | color: #8b919c; | ||
| 150 | width: 140px; | ||
| 151 | } | 147 | } |
| 152 | .upgrade { | 148 | .upgrade { |
| 153 | margin-left: 0.4rem; | 149 | margin-left: 0.5em; |
| 154 | padding: 0.1rem 0.45rem; | ||
| 155 | font-size: 12px; | ||
| 156 | } | ||
| 157 | /* base .dot rule and .on/.off colors are global (+layout.svelte); this | ||
| 158 | page's dot just wants a bit more breathing room before the text. */ | ||
| 159 | .dot { | ||
| 160 | margin-right: 4px; | ||
| 161 | } | 150 | } |
| 162 | </style> | 151 | </style> |
web/src/routes/settings/+page.svelte
| Old | New | ||
|---|---|---|---|
| @@ -129,7 +129,7 @@ | |||
| 129 | <h2>Identity</h2> | 129 | <h2>Identity</h2> |
| 130 | </div> | 130 | </div> |
| 131 | {#if fleet.me} | 131 | {#if fleet.me} |
| 132 | <table> | 132 | <table class="kv"> |
| 133 | <tbody> | 133 | <tbody> |
| 134 | <tr><th>Email</th><td>{fleet.me.email}</td></tr> | 134 | <tr><th>Email</th><td>{fleet.me.email}</td></tr> |
| 135 | <tr><th>Tenant</th><td>{fleet.me.tenant}</td></tr> | 135 | <tr><th>Tenant</th><td>{fleet.me.tenant}</td></tr> |
| @@ -244,87 +244,82 @@ | |||
| 244 | .row { | 244 | .row { |
| 245 | display: flex; | 245 | display: flex; |
| 246 | align-items: center; | 246 | align-items: center; |
| 247 | gap: 0.8rem; | 247 | gap: 0.75em; |
| 248 | } | 248 | } |
| 249 | h2 { | 249 | h2 { |
| 250 | font-size: 14px; | 250 | font-size: inherit; |
| 251 | margin: 1rem 0 0; | 251 | margin: 1.5em 0 0.5em; |
| 252 | } | 252 | } |
| 253 | .actions { | 253 | .actions { |
| 254 | display: flex; | 254 | display: flex; |
| 255 | gap: 0.3rem; | 255 | gap: 0.5em; |
| 256 | } | 256 | } |
| 257 | .enroll { | 257 | .enroll { |
| 258 | background: #15171c; | 258 | border: 1px solid var(--hairline); |
| 259 | border: 1px solid #2a2e37; | 259 | padding: 1em; |
| 260 | border-radius: 4px; | 260 | margin: 0.5em 0; |
| 261 | padding: 0.5rem; | 261 | overflow-x: auto; |
| 262 | margin: 0.5rem 0; | ||
| 263 | } | 262 | } |
| 264 | .enroll code { | 263 | .enroll code { |
| 265 | display: block; | 264 | display: block; |
| 266 | margin-top: 0.3rem; | ||
| 267 | color: #8fe3a0; | ||
| 268 | word-break: break-all; | 265 | word-break: break-all; |
| 269 | } | 266 | } |
| 270 | .pat-form { | 267 | .pat-form { |
| 271 | display: flex; | 268 | display: flex; |
| 272 | gap: 0.5rem; | 269 | gap: 0.5em; |
| 273 | align-items: flex-end; | 270 | align-items: flex-end; |
| 274 | flex-wrap: wrap; | 271 | flex-wrap: wrap; |
| 275 | margin-top: 0.5rem; | 272 | margin-top: 0.5em; |
| 276 | } | 273 | } |
| 277 | .pat-form label { | 274 | .pat-form label { |
| 278 | display: flex; | 275 | display: flex; |
| 279 | flex-direction: column; | 276 | flex-direction: column; |
| 280 | gap: 0.2rem; | 277 | gap: 0.1em; |
| 281 | color: #9aa0aa; | 278 | color: var(--faint); |
| 282 | } | 279 | } |
| 280 | /* The one secret the console ever shows, and it shows it once: ink border, | ||
| 281 | like the modal, so it does not read as another panel. */ | ||
| 283 | .secret-box { | 282 | .secret-box { |
| 284 | background: #15171c; | 283 | border: 1px solid var(--ink); |
| 285 | border: 1px solid #2a5a35; | 284 | padding: 0.8em; |
| 286 | border-radius: 6px; | 285 | margin: 0.6em 0; |
| 287 | padding: 0.8rem; | 286 | max-width: 72ch; |
| 288 | margin: 0.6rem 0; | ||
| 289 | } | 287 | } |
| 290 | .secret-head { | 288 | .secret-head { |
| 291 | display: flex; | 289 | display: flex; |
| 292 | align-items: center; | 290 | align-items: center; |
| 293 | justify-content: space-between; | 291 | justify-content: space-between; |
| 294 | gap: 0.6rem; | 292 | gap: 0.75em; |
| 295 | } | 293 | } |
| 296 | .secret-box .warn { | 294 | .secret-box .warn { |
| 297 | color: #f0b429; | 295 | color: var(--bad); |
| 298 | margin: 0.4rem 0; | 296 | margin: 0.4em 0; |
| 299 | } | 297 | } |
| 300 | .secret-value { | 298 | .secret-value { |
| 301 | display: flex; | 299 | display: flex; |
| 302 | align-items: center; | 300 | align-items: center; |
| 303 | gap: 0.5rem; | 301 | gap: 0.5em; |
| 304 | } | 302 | } |
| 305 | .secret-value code { | 303 | .secret-value code { |
| 306 | flex: 1; | 304 | flex: 1; |
| 307 | background: #0c0d10; | 305 | border: 1px solid var(--hairline); |
| 308 | border: 1px solid #2a2e37; | 306 | padding: 0.05em 0.6em; |
| 309 | border-radius: 4px; | ||
| 310 | padding: 0.3rem 0.5rem; | ||
| 311 | color: #8fe3a0; | ||
| 312 | word-break: break-all; | 307 | word-break: break-all; |
| 313 | } | 308 | } |
| 314 | .ca-add { | 309 | .ca-add { |
| 315 | display: flex; | 310 | display: flex; |
| 316 | gap: 0.5rem; | 311 | gap: 0.5em; |
| 317 | align-items: flex-end; | 312 | align-items: flex-end; |
| 318 | flex-wrap: wrap; | 313 | flex-wrap: wrap; |
| 319 | margin-top: 0.5rem; | 314 | margin-top: 0.5em; |
| 320 | } | 315 | } |
| 321 | .ca-add label { | 316 | .ca-add label { |
| 322 | flex: 1; | 317 | flex: 1; |
| 323 | min-width: 200px; | 318 | min-width: 25ch; |
| 324 | display: flex; | 319 | display: flex; |
| 325 | flex-direction: column; | 320 | flex-direction: column; |
| 326 | gap: 0.2rem; | 321 | gap: 0.1em; |
| 327 | color: #9aa0aa; | 322 | color: var(--faint); |
| 328 | } | 323 | } |
| 329 | .ca-add input { | 324 | .ca-add input { |
| 330 | width: 100%; | 325 | width: 100%; |
web/src/routes/vms/[id]/+page.svelte
| Old | New | ||
|---|---|---|---|
| @@ -258,7 +258,7 @@ | |||
| 258 | {:else} | 258 | {:else} |
| 259 | <span class="hint">host address not yet known · :{e.host_port}</span> | 259 | <span class="hint">host address not yet known · :{e.host_port}</span> |
| 260 | {/if} | 260 | {/if} |
| 261 | <span class="state {exposureState(e)}">● {exposureState(e)}</span> | 261 | <span class="state {exposureState(e)}">●</span> {exposureState(e)} |
| 262 | {#if e.reason && !tearingDown}<span class="err">{e.reason}</span>{/if} | 262 | {#if e.reason && !tearingDown}<span class="err">{e.reason}</span>{/if} |
| 263 | </td> | 263 | </td> |
| 264 | <td> | 264 | <td> |
| @@ -306,11 +306,9 @@ | |||
| 306 | 306 | ||
| 307 | <style> | 307 | <style> |
| 308 | h2 { | 308 | h2 { |
| 309 | font-size: 14px; | 309 | font-size: inherit; |
| 310 | } | 310 | } |
| 311 | .kv th { | 311 | .kv th { |
| 312 | color: #8b919c; | ||
| 313 | width: 140px; | ||
| 314 | vertical-align: top; | 312 | vertical-align: top; |
| 315 | } | 313 | } |
| 316 | .wrap { | 314 | .wrap { |
| @@ -318,83 +316,69 @@ | |||
| 318 | white-space: normal; | 316 | white-space: normal; |
| 319 | } | 317 | } |
| 320 | .err { | 318 | .err { |
| 321 | color: #ffb4b4; | 319 | color: var(--bad); |
| 322 | } | 320 | } |
| 323 | .actions { | 321 | .actions { |
| 324 | display: flex; | 322 | display: flex; |
| 325 | gap: 0.4rem; | 323 | gap: 0.5em; |
| 326 | margin-top: 0.8rem; | 324 | margin-top: 0.8em; |
| 327 | } | 325 | } |
| 326 | /* A VM on its way out is worth a box of its own, so the undo is not one | ||
| 327 | button among several. Ink border, like the modal: this one is loud. */ | ||
| 328 | .teardown-callout { | 328 | .teardown-callout { |
| 329 | background: #2a2213; | 329 | border: 1px solid var(--ink); |
| 330 | border: 1px solid #7a5c12; | 330 | padding: 0.7em 0.9em; |
| 331 | border-radius: 6px; | 331 | margin-top: 0.9em; |
| 332 | padding: 0.7rem 0.9rem; | 332 | max-width: 72ch; |
| 333 | margin-top: 0.9rem; | ||
| 334 | } | 333 | } |
| 335 | .teardown-callout h3 { | 334 | .teardown-callout h3 { |
| 336 | margin: 0 0 0.3rem; | 335 | margin: 0 0 0.3em; |
| 337 | font-size: 13px; | 336 | font-size: inherit; |
| 338 | color: #f0b429; | ||
| 339 | } | 337 | } |
| 340 | .teardown-callout p { | 338 | .teardown-callout p { |
| 341 | margin: 0 0 0.6rem; | 339 | margin: 0 0 0.6em; |
| 342 | color: #d6c48a; | ||
| 343 | } | ||
| 344 | :global(button.restore) { | ||
| 345 | background: #2f9e44; | ||
| 346 | border-color: #2f9e44; | ||
| 347 | font-weight: 600; | ||
| 348 | } | ||
| 349 | :global(button.restore:hover) { | ||
| 350 | background: #37b24d; | ||
| 351 | } | 340 | } |
| 352 | .exposures { | 341 | .exposures { |
| 353 | margin-top: 1.2rem; | 342 | margin-top: 1.2em; |
| 354 | } | 343 | } |
| 355 | .exposures h3 { | 344 | .exposures h3 { |
| 356 | margin: 0 0 0.2rem; | 345 | margin: 0 0 0.2em; |
| 357 | font-size: 13px; | 346 | font-size: inherit; |
| 358 | } | 347 | } |
| 359 | .exposures .kv th { | 348 | .exposures .kv th { |
| 360 | color: #8b919c; | 349 | width: 20ch; |
| 361 | width: 140px; | ||
| 362 | font-weight: 400; | ||
| 363 | white-space: nowrap; | ||
| 364 | } | 350 | } |
| 351 | /* The glyph takes the state's colour; the word beside it stays ink, so the | ||
| 352 | state reads without it. */ | ||
| 365 | .state { | 353 | .state { |
| 366 | margin-left: 0.5rem; | 354 | margin-left: 0.5em; |
| 367 | font-size: 12px; | ||
| 368 | } | 355 | } |
| 369 | .state.active { | 356 | .state.active { |
| 370 | color: #51cf66; | 357 | color: var(--ok); |
| 371 | } | 358 | } |
| 372 | .state.failed { | 359 | .state.failed { |
| 373 | color: #ffb4b4; | 360 | color: var(--bad); |
| 374 | } | 361 | } |
| 375 | .state.pending, | 362 | .state.pending, |
| 376 | .state.deleting { | 363 | .state.deleting { |
| 377 | color: #8b919c; | 364 | color: var(--faint); |
| 378 | } | 365 | } |
| 379 | .expose-form { | 366 | .expose-form { |
| 380 | display: flex; | 367 | display: flex; |
| 381 | gap: 0.4rem; | 368 | gap: 0.5em; |
| 382 | margin-top: 0.5rem; | 369 | margin-top: 0.5em; |
| 383 | } | 370 | } |
| 384 | .expose-form input { | 371 | .expose-form input { |
| 385 | width: 10rem; | 372 | width: 24ch; |
| 386 | } | 373 | } |
| 387 | .timeline { | 374 | .timeline { |
| 388 | margin-top: 1.2rem; | 375 | margin-top: 1.2em; |
| 389 | } | 376 | } |
| 390 | .timeline h3 { | 377 | .timeline h3 { |
| 391 | margin: 0 0 0.2rem; | 378 | margin: 0 0 0.2em; |
| 392 | font-size: 13px; | 379 | font-size: inherit; |
| 393 | } | 380 | } |
| 394 | .timeline .kv th { | 381 | .timeline .kv th { |
| 395 | color: #8b919c; | 382 | width: 30ch; |
| 396 | width: 220px; | ||
| 397 | font-weight: 400; | ||
| 398 | white-space: nowrap; | ||
| 399 | } | 383 | } |
| 400 | </style> | 384 | </style> |