workspaces 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/DESIGN.md +33 -9
- data/README.md +21 -0
- data/lib/workspaces/config.rb +13 -0
- data/lib/workspaces/coordinator.rb +23 -10
- data/lib/workspaces/dashboard/app.js +60 -8
- data/lib/workspaces/dashboard/arb-palette.css +82 -0
- data/lib/workspaces/dashboard/arb-previews-favicon.svg +11 -0
- data/lib/workspaces/dashboard/card.html.erb +2 -1
- data/lib/workspaces/dashboard/environment.js +2 -2
- data/lib/workspaces/dashboard/styles.css +237 -132
- data/lib/workspaces/dashboard/template.html.erb +2 -2
- data/lib/workspaces/dashboard_view.rb +24 -13
- data/lib/workspaces/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b215ed643db37f770915a5da966e4afd5c052ccf8a1bc271ae371f95aed5851
|
|
4
|
+
data.tar.gz: a6c97523fd83b4ccfa72e1d007c60fce8f98e16bcf0ecfcbe6fb7891bc4ae4f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54a95866acd2a0b6ec251e06996ff1b366d72d885ac7d87ae4cb2e3c68af2a19f59dc92c435ed61a0aac8278f9e816451cb6c4f7fa866cd0748242d7059f2644
|
|
7
|
+
data.tar.gz: 469c1c9aaed1ff6aabbbe3d515d8b6bd99b0e1e1089f6c7a8f7d90278e37c23a07aea8f0cc067d1b6107fd25f234794c145d358cb56ebcbd6116bba6f5ac06f5
|
data/DESIGN.md
CHANGED
|
@@ -8,17 +8,39 @@ GET is read-only. All creation and lifecycle controls submit same-origin POST fo
|
|
|
8
8
|
|
|
9
9
|
## Visual direction
|
|
10
10
|
|
|
11
|
-
Restrained tooling UI: a
|
|
11
|
+
Restrained tooling UI built on the **Arb design system, Native v1** (`~/code/arb-design-system`, commit `88765ca`): a deep-teal page in the dark theme and limestone in the light one, one slightly lighter card surface with a hairline border and no shadow, a compact spacing rhythm, clear status affordances, and no decorative animation. Every text pairing is at least 4.5:1 against its surface, and `spec/workspaces/arb_design_system_spec.rb` measures each one in both themes.
|
|
12
|
+
|
|
13
|
+
Only colour changed in this migration. Type, spacing, radii and motion timing are the ones the dashboard already had, because the Arb kit defines none of them. No state, label, handler, route or permission changed.
|
|
12
14
|
|
|
13
15
|
## Tokens used
|
|
14
16
|
|
|
15
17
|
### Color
|
|
16
18
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
`dashboard/arb-palette.css` is the kit's `tokens/palette.css` copied unchanged (a two-line note on top says where from). It defines 28 `--arb-*` tokens per theme, selected by `data-theme` on `<html>`. **The theme follows the visitor's operating-system setting, with dark as the default**; a one-line script in the `<head>` sets it before anything paints, and a page without script stays dark. There is no theme switch, because the kit says to follow the host's existing behaviour and the dashboard had none.
|
|
20
|
+
|
|
21
|
+
`styles.css` names no colour of its own, which a spec enforces (no hex, `rgb`, `hsl`, `color-mix` or faded opacity). Its `:root` maps a short list of roles onto the palette, and everything below uses the roles or `--arb-*` directly:
|
|
22
|
+
|
|
23
|
+
| Role in `styles.css` | Arb token | Used for |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `--bg` | `bg` | the page |
|
|
26
|
+
| `--bg-elevated` | `surface` | panels, the card, inputs, log blocks |
|
|
27
|
+
| `--arb-elevated` | `elevated` | the progress track, neutral pill fill |
|
|
28
|
+
| `--text`, `--muted` | `text`, `muted` | body and supporting text (the old strong, cell and placeholder greys are gone: one text colour, one muted) |
|
|
29
|
+
| `--border` | `border` | every edge and divider, including control edges, so controls now meet 3:1 (the old control edge was about 2:1) |
|
|
30
|
+
| `--accent`, `--accent-hover`, `--accent-pressed`, `--on-accent` | `accent`, `action_hover`, `action_pressed`, `on_accent` | primary buttons and links |
|
|
31
|
+
| `--focus` | `focus` | the focus ring |
|
|
32
|
+
| `--danger` / `--arb-error*`, `--arb-on-error` | `error`, `error_hover`, `error_pressed`, `on_error` | Delete, the setup error callout, the danger zone, failed steps |
|
|
33
|
+
| `--warning`, `--success` | `warning`, `success` | slow-progress fill, vault note, completed step icon |
|
|
34
|
+
| `--arb-selected-bg`, `--arb-selected-border` | `selected_bg`, `selected_border` | the chosen preset, the preset badge, the log tab you are on |
|
|
35
|
+
| `--arb-surface-hover`, `--arb-surface-pressed` | `surface_hover`, `surface_pressed` | hover and pressed on subtle buttons, rows, tabs |
|
|
36
|
+
| `--arb-disabled-*` | `disabled_bg`, `disabled_fg`, `disabled_border` | disabled buttons and fields, at full opacity |
|
|
37
|
+
|
|
38
|
+
- **Focus** is a 2px `focus` ring with a 2px gap, as the kit specifies. The gap is the outline's own offset, which is transparent and so shows the surface actually behind the control.
|
|
39
|
+
- **Selected** is never colour alone: the chosen preset and the current log tab carry a check mark, because the light theme's selected and pressed fills are the same colour.
|
|
40
|
+
- **Disabled** controls use the disabled tokens at full opacity, not a faded copy of the enabled style.
|
|
41
|
+
- **Status pills** pair a glyph with the word the server already sends: ready is a check on `success_bg`, error a cross on `error_bg`, interrupted an exclamation mark on `warning_bg` (a run that was cut off, not one that failed), preparing a ringed dot in `accent`. Idle, stopped and sleeping have no palette role of their own, so they are told apart by glyph and word (empty ring, filled square, half ring) in `steel` on a neutral fill. The earlier design gave each its own hue; the palette has no roles to carry that, and the guide forbids inventing near-matches.
|
|
42
|
+
- **Motion**: the dots that pulsed forever (next to the current step, and on a busy pill) are gone, because a pulse that never stops says "working" whether or not anything is. The bar, the step name and its number carry progress. The step status icon no longer pulses either.
|
|
43
|
+
- **Forced colors**: shapes that carry meaning get a real border so they survive the system palette.
|
|
22
44
|
|
|
23
45
|
### Radius and depth
|
|
24
46
|
|
|
@@ -39,7 +61,7 @@ Restrained tooling UI: a near-black page, one slightly lighter card surface with
|
|
|
39
61
|
|
|
40
62
|
- **Creation form**: four labelled fields in one wrapping row (`flex: 1 1 220px`, 16px gap), each with a real `<label for>`, then a `type="submit"` button on its own line.
|
|
41
63
|
- **Workspace inventory**: a centered column up to 1180px. Rows are ordered by last startup, newest first; workspaces that never started (or whose start time cannot be read) come last, and ties fall back to the id so the list never shuffles between loads. Moments are compared as moments, not text. The page header carries "N managed worktrees · none running" (or "· K running"), counted from the data. The table sits in an `overflow-x: auto` wrapper with `min-width: 820px`, so a phone scrolls it sideways instead of cramming or hiding columns. Rows have 16px padding; workspace names are monospaced links with no underline until hover. Times read "Oct 27, 10:48", are UTC (the headers say so) and keep the full ISO value in a `title`. A missing start reads "Never" and a missing creation time "Unavailable", in the muted colour.
|
|
42
|
-
- **Status pill**: a
|
|
64
|
+
- **Status pill**: a glyph and one word (13px, 600, fully rounded), never colour alone. A busy workspace adds "(busy)" to the word; nothing pulses.
|
|
43
65
|
- **Setup progress**: names and ordered states come from repository run steps; failures are highlighted. Restart shows the background launch and later readiness commands.
|
|
44
66
|
- **Actions by state**: idle/stopped offers Start; active operations show progress without lifecycle buttons; failure offers Retry; ready offers Open workspace plus rebuild/restart/stop.
|
|
45
67
|
- **PR source update**: a secondary Update from PR form only for PR-sourced workspaces; it fetches the latest PR head, fast-forwards the checkout, and preserves local edits without rebuilding or restarting. A persistent result message explains when rebuild is needed or when a retained stash needs manual recovery.
|
|
@@ -79,12 +101,14 @@ Restrained tooling UI: a near-black page, one slightly lighter card surface with
|
|
|
79
101
|
- **Editable environment**: labelled monospace multiline `NAME=value` control for non-secret recipe defaults and added workspace settings; busy state disables the fieldset and save button, values are escaped in HTML, other legacy overrides remain hidden. Omitted default names revert to recipe defaults; omitted added names are unset.
|
|
80
102
|
- **Environment presets (JS-enhanced)**: optional preset selector is hidden by default and revealed only when JavaScript is active and presets exist. Selecting a preset replaces the entire draft editor content and announces unsaved state without submitting. Draft edits remain local until Save & restart.
|
|
81
103
|
- **Danger Zone**: isolated section on the detail page with explicit loss-of-uncommitted-and-untracked-work copy, an exact-workspace-ID confirmation field, and a destructive action. Deletion remains possible without JavaScript, is disabled while setup is busy, and never appears as a list-row action.
|
|
82
|
-
- **Workspace favicon**:
|
|
104
|
+
- **Workspace favicon**: the Arb Previews icon (`dashboard/arb-previews-favicon.svg`, copied unchanged from the design system's `arb-previews/favicon.svg`), served by the dashboard host for local and hosted origins alike. The earlier blue-for-localhost, gold-for-hosted tab signal is gone: the icon is one mark and is not recoloured.
|
|
83
105
|
|
|
84
106
|
## Accessibility constraints
|
|
85
107
|
|
|
86
108
|
- Semantic sections/headings for all regions
|
|
87
|
-
- Focus-visible outlines on actionable controls
|
|
109
|
+
- Focus-visible outlines on actionable controls: a 2px ring with a 2px gap, in the palette's focus colour
|
|
110
|
+
- Selected controls carry a check mark as well as a fill; statuses carry a glyph as well as a colour
|
|
111
|
+
- Forced-colors and reduced-motion are handled
|
|
88
112
|
- `aria-live` limited to phase-change and connection warnings (not full log stream)
|
|
89
113
|
- No-JS refresh path always available
|
|
90
114
|
|
data/README.md
CHANGED
|
@@ -95,6 +95,27 @@ workspace's directory with the workspace's environment, and exits with the comma
|
|
|
95
95
|
restart, rebuild, PR update or removal holding the lock still takes the preview offline, since
|
|
96
96
|
those replace the server or its files.
|
|
97
97
|
|
|
98
|
+
### Log tails
|
|
99
|
+
|
|
100
|
+
The dashboard shows the end of each log, not all of it. Normally that is the last 6 KB or 80
|
|
101
|
+
lines of a step's output, and 8 KB or 100 lines of the setup and web server logs, which keeps
|
|
102
|
+
every poll small.
|
|
103
|
+
|
|
104
|
+
When something has failed (a step failed, or a run was interrupted) the page gets far more of
|
|
105
|
+
what matters: the failed step's output, and the setup and server logs, are sent as the last
|
|
106
|
+
**128 KB or 2000 lines**. That is enough for the error and the backtrace after it, which the
|
|
107
|
+
everyday tail would cut off. Steps that did not fail keep the small tail, and a step that is
|
|
108
|
+
merely running does too. The full files stay on disk under the workspaces home (`logs/`), and
|
|
109
|
+
every tail is secret-scrubbed first.
|
|
110
|
+
|
|
111
|
+
### Copying from a log
|
|
112
|
+
|
|
113
|
+
The page refreshes itself, and replacing a log's text drops any selection inside it. So it never
|
|
114
|
+
rewrites a log whose text has not changed, and while part of a log (a step's output, the full
|
|
115
|
+
setup log, the web server log or the error panel) is selected, its new text waits and is applied
|
|
116
|
+
once, when the selection ends. Only the log being read waits; everything else keeps updating. A
|
|
117
|
+
step that finishes while you have text selected in it stays open until you let go.
|
|
118
|
+
|
|
98
119
|
### Step timings
|
|
99
120
|
|
|
100
121
|
Per-step durations are persisted in seconds in workspace step status JSON, so they
|
data/lib/workspaces/config.rb
CHANGED
|
@@ -13,6 +13,13 @@ module Workspaces
|
|
|
13
13
|
# Each step shows its own tail, so the per-step bounds are smaller than the combined log's.
|
|
14
14
|
STEP_LOG_BYTES = 6 * 1024
|
|
15
15
|
STEP_LOG_LINES = 80
|
|
16
|
+
# What the page gets once something has failed. The error is at the end of the output, but a failing
|
|
17
|
+
# command prints a backtrace of a hundred lines or more after it, and bundler, yarn and rails print
|
|
18
|
+
# their progress before it, so the everyday tail cuts the message off. Only the step that failed (and
|
|
19
|
+
# the setup and server logs of a workspace that failed) get this; everything else stays small, because
|
|
20
|
+
# every poll sends these.
|
|
21
|
+
FAILURE_LOG_BYTES = 128 * 1024
|
|
22
|
+
FAILURE_LOG_LINES = 2000
|
|
16
23
|
DEFAULT_HOME = File.expand_path('~/.workspaces')
|
|
17
24
|
DEFAULT_SUBDOMAIN_PREFIX = 'ws-'
|
|
18
25
|
DEFAULT_FRONT_DOOR_PORT = 4747
|
|
@@ -144,6 +151,12 @@ module Workspaces
|
|
|
144
151
|
"#{public_origin}/workspaces/#{validate_id!(id)}"
|
|
145
152
|
end
|
|
146
153
|
|
|
154
|
+
# The list of every workspace. A card on a preview host is a different origin from the dashboard, so it
|
|
155
|
+
# needs the whole URL; a card on the dashboard host can use the path.
|
|
156
|
+
def self.index_url
|
|
157
|
+
"#{public_origin}/workspaces"
|
|
158
|
+
end
|
|
159
|
+
|
|
147
160
|
def self.settings_url(id)
|
|
148
161
|
"#{dashboard_url(id)}/settings"
|
|
149
162
|
end
|
|
@@ -274,9 +274,10 @@ module Workspaces
|
|
|
274
274
|
'steps' => entry.fetch('steps', []), 'total_seconds' => entry['total_seconds'],
|
|
275
275
|
'resume' => resume_info(id, entry),
|
|
276
276
|
'estimates' => entry.fetch('estimates', {}),
|
|
277
|
-
'step_logs' => include_log && log == 'setup' ? step_logs(id, entry.fetch('steps', [])) : {},
|
|
277
|
+
'step_logs' => include_log && log == 'setup' ? step_logs(id, entry.fetch('steps', []), status) : {},
|
|
278
278
|
'backend_log_available' => backend_log_available?(id),
|
|
279
|
-
'log_tail' => include_log ? log_tail(id, log) : '', 'log_source' => log,
|
|
279
|
+
'log_tail' => include_log ? log_tail(id, log, failed: failed_status?(status)) : '', 'log_source' => log,
|
|
280
|
+
'port' => entry['port'],
|
|
280
281
|
'environment_keys' => environment_keys(id),
|
|
281
282
|
'editable_environment' => editable_environment(id),
|
|
282
283
|
'secret_names' => secret_names(id),
|
|
@@ -434,30 +435,42 @@ module Workspaces
|
|
|
434
435
|
WorkspaceLock.held?(id)
|
|
435
436
|
end
|
|
436
437
|
|
|
437
|
-
|
|
438
|
+
# Something went wrong, so the logs are worth reading in full: the larger tail is for these states.
|
|
439
|
+
def failed_status?(status)
|
|
440
|
+
%w(error interrupted).include?(status)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def log_tail(id, log, failed: false)
|
|
438
444
|
path = log == 'backend' ? Config.backend_log_path(id, create: false) : Config.setup_log_path(id, create: false)
|
|
439
445
|
return '' unless path.file?
|
|
440
446
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
end
|
|
445
|
-
scrub(raw, id: id).lines.last(Config::SAFE_LOG_LINES).join
|
|
447
|
+
bytes = failed ? Config::FAILURE_LOG_BYTES : Config::SAFE_LOG_BYTES
|
|
448
|
+
lines = failed ? Config::FAILURE_LOG_LINES : Config::SAFE_LOG_LINES
|
|
449
|
+
tail_file(path, id, bytes: bytes, lines: lines).fetch('text')
|
|
446
450
|
end
|
|
447
451
|
|
|
448
452
|
# Output for each recipe step, keyed by step id. Only steps that have written something
|
|
449
453
|
# appear, so a pending step has no entry and the page can show it as not started.
|
|
450
|
-
def step_logs(id, steps)
|
|
454
|
+
def step_logs(id, steps, status = nil)
|
|
451
455
|
steps.each_with_object({}) do |step, logs|
|
|
452
456
|
path = Config.step_log_path(id, step['key'], create: false)
|
|
453
457
|
next unless path.file?
|
|
454
458
|
|
|
455
|
-
|
|
459
|
+
failed = failed_step?(step, status)
|
|
460
|
+
logs[step['key']] = tail_file(path, id, bytes: failed ? Config::FAILURE_LOG_BYTES : Config::STEP_LOG_BYTES,
|
|
461
|
+
lines: failed ? Config::FAILURE_LOG_LINES : Config::STEP_LOG_LINES)
|
|
456
462
|
end
|
|
457
463
|
rescue ArgumentError
|
|
458
464
|
{}
|
|
459
465
|
end
|
|
460
466
|
|
|
467
|
+
# The step that failed, or the one a run was cut off in. A step that is merely running is not one.
|
|
468
|
+
def failed_step?(step, status)
|
|
469
|
+
return true if step['state'] == 'failed'
|
|
470
|
+
|
|
471
|
+
status == 'interrupted' && step['state'] == 'active'
|
|
472
|
+
end
|
|
473
|
+
|
|
461
474
|
def backend_log_available?(id)
|
|
462
475
|
path = Config.backend_log_path(id, create: false)
|
|
463
476
|
path.file? && !path.zero?
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
const output = details.querySelector('pre');
|
|
108
108
|
output.setAttribute('aria-label', step.label + ' output');
|
|
109
109
|
const entry = stepLogs && stepLogs[step.key];
|
|
110
|
-
if (entry
|
|
110
|
+
if (entry) writeLog(output, entry.text);
|
|
111
111
|
applyStepOpenState(details, step);
|
|
112
112
|
});
|
|
113
113
|
}
|
|
@@ -164,11 +164,11 @@
|
|
|
164
164
|
: 'everything, in order';
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
// A step that finished is closed by the page, which hides its log, and text selected inside a hidden
|
|
168
|
+
// element is lost. So a close waits while part of that step is selected, and happens when the selection ends.
|
|
169
|
+
const pendingClose = new Set();
|
|
170
|
+
|
|
171
|
+
function setStepOpen(details, shouldOpen) {
|
|
172
172
|
if (details.open === shouldOpen) return;
|
|
173
173
|
details.dataset.programmatic = 'true';
|
|
174
174
|
details.open = shouldOpen;
|
|
@@ -176,13 +176,63 @@
|
|
|
176
176
|
setTimeout(() => { details.dataset.programmatic = 'false'; }, 0);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function applyStepOpenState(details, step) {
|
|
180
|
+
const previous = stepState[step.key];
|
|
181
|
+
stepState[step.key] = step.state;
|
|
182
|
+
if (previous === step.state || userToggled.has(step.key)) return;
|
|
183
|
+
const shouldOpen = step.state === 'active' || step.state === 'failed';
|
|
184
|
+
if (shouldOpen) pendingClose.delete(details);
|
|
185
|
+
if (!shouldOpen && details.open && hasSelectionIn(details)) {
|
|
186
|
+
pendingClose.add(details);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
pendingClose.delete(details);
|
|
190
|
+
setStepOpen(details, shouldOpen);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Replacing a log's text drops any selection inside it, which made copying out of a log that keeps
|
|
194
|
+
// updating a race against the next poll. So: text that has not changed is never rewritten, and while
|
|
195
|
+
// part of a log is selected its new text is held back and applied, once, when the selection ends.
|
|
196
|
+
// Only the log being read waits; every other element keeps updating.
|
|
197
|
+
const heldText = new Map();
|
|
198
|
+
|
|
199
|
+
function hasSelectionIn(element) {
|
|
200
|
+
const selection = window.getSelection && window.getSelection();
|
|
201
|
+
if (!selection || selection.isCollapsed) return false;
|
|
202
|
+
if (element.contains(selection.anchorNode) || element.contains(selection.focusNode)) return true;
|
|
203
|
+
// A selection that starts above a log and ends below it contains the log without either end in it.
|
|
204
|
+
return typeof selection.containsNode === 'function' && selection.containsNode(element, true);
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
// Writing a log must not yank a reader who scrolled up; follow the tail only if already there.
|
|
180
208
|
function writeLog(pre, text) {
|
|
209
|
+
const next = text || '';
|
|
210
|
+
if (pre.textContent === next) {
|
|
211
|
+
heldText.delete(pre);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (hasSelectionIn(pre)) {
|
|
215
|
+
heldText.set(pre, next);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
heldText.delete(pre);
|
|
181
219
|
const atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 18;
|
|
182
|
-
pre.textContent =
|
|
220
|
+
pre.textContent = next;
|
|
183
221
|
if (atBottom) pre.scrollTop = pre.scrollHeight;
|
|
184
222
|
}
|
|
185
223
|
|
|
224
|
+
// When the selection moves or is cleared, catch up any log that was waiting on it.
|
|
225
|
+
document.addEventListener('selectionchange', () => {
|
|
226
|
+
heldText.forEach((text, pre) => {
|
|
227
|
+
if (!hasSelectionIn(pre)) writeLog(pre, text);
|
|
228
|
+
});
|
|
229
|
+
pendingClose.forEach((details) => {
|
|
230
|
+
if (hasSelectionIn(details)) return;
|
|
231
|
+
pendingClose.delete(details);
|
|
232
|
+
if (!userToggled.has(details.dataset.stepLog)) setStepOpen(details, false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
186
236
|
// Durations read the way people say them: 0.5s, 12.3s, 1m 10s, 1h 5m 3s. This mirrors
|
|
187
237
|
// Workspaces::DurationFormat in Ruby; a spec runs both over the same inputs.
|
|
188
238
|
function formatDuration(seconds) {
|
|
@@ -258,6 +308,8 @@
|
|
|
258
308
|
}
|
|
259
309
|
|
|
260
310
|
function renderLead(view) {
|
|
311
|
+
const lead = node('lead');
|
|
312
|
+
if (lead && view.state) lead.dataset.state = view.state;
|
|
261
313
|
node('status').textContent = view.headline || view.title;
|
|
262
314
|
node('summary').textContent = view.summary || '';
|
|
263
315
|
const note = node('sleep-note');
|
|
@@ -335,7 +387,7 @@
|
|
|
335
387
|
function renderStatus(view) {
|
|
336
388
|
node('preview').hidden = !view.ready;
|
|
337
389
|
reloadWhenReady(view);
|
|
338
|
-
node('error')
|
|
390
|
+
writeLog(node('error'), view.error || '');
|
|
339
391
|
node('error-panel').hidden = !view.error;
|
|
340
392
|
if (window.WorkspaceEnvironment)
|
|
341
393
|
window.WorkspaceEnvironment.updateStatus(view.busy);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* Arb design system, Native v1: tokens/palette.css from ~/code/arb-design-system (commit 88765ca), copied unchanged below this note.
|
|
2
|
+
Do not edit by hand. To update, copy the new palette.css over everything after this comment. */
|
|
3
|
+
/* Source: palette.tokens.json. Opaque sRGB pairings only; see README.md. */
|
|
4
|
+
:root {
|
|
5
|
+
--arb-mark-shell: #123A3D;
|
|
6
|
+
--arb-mark-river: #58D9C4;
|
|
7
|
+
--arb-mark-brackets: #F4F1E8;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
[data-theme="dark"] {
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
--arb-bg: #0B171A;
|
|
13
|
+
--arb-surface: #12262B;
|
|
14
|
+
--arb-elevated: #1B343B;
|
|
15
|
+
--arb-text: #F4F1E8;
|
|
16
|
+
--arb-muted: #B4C3C8;
|
|
17
|
+
--arb-accent: #58D9C4;
|
|
18
|
+
--arb-on-accent: #073A35;
|
|
19
|
+
--arb-steel: #ABC8D2;
|
|
20
|
+
--arb-border: #6F8991;
|
|
21
|
+
--arb-focus: #76CCE5;
|
|
22
|
+
--arb-success: #9DD48E;
|
|
23
|
+
--arb-warning: #F1C56B;
|
|
24
|
+
--arb-error: #FF9B96;
|
|
25
|
+
--arb-action-hover: #78E3D0;
|
|
26
|
+
--arb-action-pressed: #3CBDA9;
|
|
27
|
+
--arb-surface-hover: #1B343B;
|
|
28
|
+
--arb-surface-pressed: #203A41;
|
|
29
|
+
--arb-selected-bg: #173E40;
|
|
30
|
+
--arb-selected-border: #58D9C4;
|
|
31
|
+
--arb-disabled-bg: #1B343B;
|
|
32
|
+
--arb-disabled-fg: #91A5AC;
|
|
33
|
+
--arb-disabled-border: #6F8991;
|
|
34
|
+
--arb-success-bg: #173322;
|
|
35
|
+
--arb-warning-bg: #3E321A;
|
|
36
|
+
--arb-error-bg: #40262A;
|
|
37
|
+
--arb-error-hover: #FFB4AE;
|
|
38
|
+
--arb-error-pressed: #EE817D;
|
|
39
|
+
--arb-on-error: #351415;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[data-theme="light"] {
|
|
43
|
+
color-scheme: light;
|
|
44
|
+
--arb-bg: #F5F2E9;
|
|
45
|
+
--arb-surface: #FFFFFF;
|
|
46
|
+
--arb-elevated: #E8EEEC;
|
|
47
|
+
--arb-text: #123A3D;
|
|
48
|
+
--arb-muted: #4B676E;
|
|
49
|
+
--arb-accent: #0A766F;
|
|
50
|
+
--arb-on-accent: #FFFFFF;
|
|
51
|
+
--arb-steel: #496B79;
|
|
52
|
+
--arb-border: #71868B;
|
|
53
|
+
--arb-focus: #246C89;
|
|
54
|
+
--arb-success: #2D6D45;
|
|
55
|
+
--arb-warning: #8A5A10;
|
|
56
|
+
--arb-error: #B64344;
|
|
57
|
+
--arb-action-hover: #08655F;
|
|
58
|
+
--arb-action-pressed: #07554F;
|
|
59
|
+
--arb-surface-hover: #E8EEEC;
|
|
60
|
+
--arb-surface-pressed: #E1EFEA;
|
|
61
|
+
--arb-selected-bg: #E1EFEA;
|
|
62
|
+
--arb-selected-border: #0A766F;
|
|
63
|
+
--arb-disabled-bg: #E1E7E5;
|
|
64
|
+
--arb-disabled-fg: #4E656B;
|
|
65
|
+
--arb-disabled-border: #71868B;
|
|
66
|
+
--arb-success-bg: #E5F1E6;
|
|
67
|
+
--arb-warning-bg: #FBF0D7;
|
|
68
|
+
--arb-error-bg: #FBE9E7;
|
|
69
|
+
--arb-error-hover: #A03638;
|
|
70
|
+
--arb-error-pressed: #8B2C2E;
|
|
71
|
+
--arb-on-error: #FFFFFF;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Optional descriptive aliases. Canonical names mirror JSON keys. */
|
|
75
|
+
[data-theme] {
|
|
76
|
+
--arb-canvas: var(--arb-bg);
|
|
77
|
+
--arb-raised: var(--arb-elevated);
|
|
78
|
+
--arb-text-secondary: var(--arb-muted);
|
|
79
|
+
--arb-river: var(--arb-accent);
|
|
80
|
+
--arb-on-river: var(--arb-on-accent);
|
|
81
|
+
--arb-boundary: var(--arb-border);
|
|
82
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Arb Previews</title>
|
|
3
|
+
<desc id="desc">A browser window with two preview panes. Original vector artwork in the Arb Native palette. Optically tuned for 16 pixels.</desc>
|
|
4
|
+
<rect id="tile" width="16" height="16" rx="4" fill="#123A3D"/>
|
|
5
|
+
<g id="preview-window" fill="none" stroke-linejoin="round">
|
|
6
|
+
<rect id="window-frame" x="2.5" y="3.5" width="11" height="9" rx="1.5" stroke="#F4F1E8" stroke-width="1.5"/>
|
|
7
|
+
<path id="browser-header" d="M2.5 6.5H13.5" stroke="#F4F1E8" stroke-width="1.5"/>
|
|
8
|
+
<path id="preview-divider" d="M8 7.25V11.75" stroke="#58D9C4" stroke-width="1.5"/>
|
|
9
|
+
<g id="browser-controls" fill="#58D9C4"><circle cx="4.5" cy="5" r="0.5"/><circle cx="6.5" cy="5" r="0.5"/></g>
|
|
10
|
+
</g>
|
|
11
|
+
</svg>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
<section class="card" aria-labelledby="status">
|
|
3
3
|
<header class="card__header">
|
|
4
4
|
<p class="card__name"><span class="eyebrow">Workspace</span><span class="card__id"><%= @id %></span></p>
|
|
5
|
-
<a class="icon-link" href="<%= escape(
|
|
5
|
+
<span class="card__links"><a class="icon-link" href="<%= escape(index_href) %>" aria-label="All workspaces" title="All workspaces"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/></svg></a>
|
|
6
|
+
<a class="icon-link" href="<%= escape(settings_href) %>" aria-label="Settings and details" title="Settings and details"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" d="M10.52 5.57L10.69 2.69L13.31 2.69L13.48 5.57A6.6 6.6 0 0 1 15.50 6.40L17.66 4.49L19.51 6.34L17.60 8.50A6.6 6.6 0 0 1 18.43 10.52L21.31 10.69L21.31 13.31L18.43 13.48A6.6 6.6 0 0 1 17.60 15.50L19.51 17.66L17.66 19.51L15.50 17.60A6.6 6.6 0 0 1 13.48 18.43L13.31 21.31L10.69 21.31L10.52 18.43A6.6 6.6 0 0 1 8.50 17.60L6.34 19.51L4.49 17.66L6.40 15.50A6.6 6.6 0 0 1 5.57 13.48L2.69 13.31L2.69 10.69L5.57 10.52A6.6 6.6 0 0 1 6.40 8.50L4.49 6.34L6.34 4.49L8.50 6.40A6.6 6.6 0 0 1 10.52 5.57ZM9.20 12.00a2.8 2.8 0 1 0 5.6 0a2.8 2.8 0 1 0 -5.6 0Z"/></svg></a></span>
|
|
6
7
|
</header>
|
|
7
8
|
<div class="card__text">
|
|
8
9
|
<h1 id="status" role="status"><%= escape(@presentation['headline']) %></h1>
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
presetButtons.forEach((button) => {
|
|
87
87
|
const isSelected = !!selectedPreset && button.dataset.environmentPreset === selectedPreset;
|
|
88
88
|
button.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
|
|
89
|
-
if (isSelected) button.classList.add('button--
|
|
90
|
-
else button.classList.remove('button--
|
|
89
|
+
if (isSelected) button.classList.add('button--selected');
|
|
90
|
+
else button.classList.remove('button--selected');
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
/*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
--
|
|
6
|
-
--
|
|
7
|
-
--border
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
--
|
|
11
|
-
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--accent-hover: #8dbbff;
|
|
19
|
-
--on-accent: #06101f;
|
|
20
|
-
--success: #2fbf8f;
|
|
21
|
-
--danger: #ff6f74;
|
|
22
|
-
--warning: #f1bb53;
|
|
2
|
+
/* Colours come from the Arb palette (arb-palette.css), per theme, and only through these roles. Nothing
|
|
3
|
+
below this block names a colour: add a role here, never a literal. A role the palette lacks is a gap to
|
|
4
|
+
raise with the palette's owner, not something to approximate with a mix or an opacity. */
|
|
5
|
+
--bg: var(--arb-bg);
|
|
6
|
+
--bg-elevated: var(--arb-surface);
|
|
7
|
+
--border: var(--arb-border);
|
|
8
|
+
--text: var(--arb-text);
|
|
9
|
+
--muted: var(--arb-muted);
|
|
10
|
+
--accent: var(--arb-accent);
|
|
11
|
+
--accent-hover: var(--arb-action-hover);
|
|
12
|
+
--accent-pressed: var(--arb-action-pressed);
|
|
13
|
+
--on-accent: var(--arb-on-accent);
|
|
14
|
+
--success: var(--arb-success);
|
|
15
|
+
--danger: var(--arb-error);
|
|
16
|
+
--warning: var(--arb-warning);
|
|
17
|
+
--focus: var(--arb-focus);
|
|
23
18
|
--radius: 12px;
|
|
24
19
|
--radius-control: 8px;
|
|
25
20
|
--control-height: 44px;
|
|
26
|
-
--shadow: none;
|
|
27
21
|
--font-sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
28
22
|
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
29
23
|
--space-1: 4px;
|
|
@@ -37,6 +31,12 @@
|
|
|
37
31
|
--panel-padding: 28px;
|
|
38
32
|
}
|
|
39
33
|
|
|
34
|
+
/* Focus: a 2px ring in the palette's focus colour, with a 2px gap. The gap is required: the ring is only approved
|
|
35
|
+
against surfaces, not directly against a button's fill. It is the outline's own offset, which is transparent, so
|
|
36
|
+
it shows whatever surface the control actually sits on (the page or a panel) and needs no colour of its own. The
|
|
37
|
+
ring takes no space and moves nothing. */
|
|
38
|
+
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
39
|
+
|
|
40
40
|
* {
|
|
41
41
|
box-sizing: border-box;
|
|
42
42
|
}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
which tag was used. Reset them once; every gap is declared by a stack rule below
|
|
46
46
|
using the --space-* scale. */
|
|
47
47
|
h1, h2, h3, h4, p, ul, ol, dl, dd, pre, fieldset, figure { margin: 0; }
|
|
48
|
-
h2 { font-size: 20px; font-weight: 600; line-height: 1.3; color: var(--text
|
|
48
|
+
h2 { font-size: 20px; font-weight: 600; line-height: 1.3; color: var(--text); }
|
|
49
49
|
label { font-size: 14px; font-weight: 600; }
|
|
50
50
|
ol, ul { padding-inline-start: var(--space-6); }
|
|
51
51
|
legend { padding: 0; }
|
|
@@ -67,27 +67,27 @@ input:not([type='checkbox']):not([type='radio']), select {
|
|
|
67
67
|
max-inline-size: 100%;
|
|
68
68
|
block-size: var(--control-height);
|
|
69
69
|
padding-inline: var(--space-3);
|
|
70
|
-
color: var(--text
|
|
70
|
+
color: var(--text);
|
|
71
71
|
background: var(--bg);
|
|
72
|
-
border: 1px solid var(--
|
|
72
|
+
border: 1px solid var(--border);
|
|
73
73
|
border-radius: var(--radius-control);
|
|
74
74
|
font-family: var(--font-mono);
|
|
75
75
|
font-size: 14px;
|
|
76
76
|
}
|
|
77
77
|
select { font-family: var(--font-sans); font-size: 15px; }
|
|
78
|
-
::placeholder { color: var(--
|
|
79
|
-
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
78
|
+
::placeholder { color: var(--muted); opacity: 1; }
|
|
80
79
|
summary { cursor: pointer; color: var(--muted); }
|
|
81
|
-
summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
|
|
82
80
|
.workspace-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }
|
|
83
81
|
/* The server-rendered action forms live in a wrapper; let them flow with the sibling button. */
|
|
84
82
|
#actions { display: contents; }
|
|
85
|
-
|
|
86
|
-
.setup-error
|
|
83
|
+
/* Two classes, so it beats the shared panel rule further down, which would otherwise put the full outline back. */
|
|
84
|
+
.error-panel.setup-error { --panel-padding: var(--space-4); padding: var(--panel-padding); border: 0; border-inline-start: 4px solid var(--arb-error); border-radius: var(--radius-control); background: var(--arb-error-bg); }
|
|
85
|
+
.setup-error h3, .setup-error h2 { color: var(--arb-error); margin-block-start: 0; }
|
|
86
|
+
.setup-error pre { background: var(--bg); }
|
|
87
87
|
.step-outcome { margin-inline-start: auto; font-size: 12px; }
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
/* The dot beside the current step is dropped: a pulse that never stops says "working" whether or not anything is
|
|
89
|
+
happening. The step's name, its number and the bar are what show progress. */
|
|
90
|
+
.activity-dot { display: none; }
|
|
91
91
|
dd, h1, code { overflow-wrap: anywhere; }
|
|
92
92
|
dt + dd { margin-block-start: var(--space-1); }
|
|
93
93
|
dd + dt { margin-block-start: var(--space-3); }
|
|
@@ -115,6 +115,8 @@ a {
|
|
|
115
115
|
gap: var(--space-6);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
.dashboard--narrow { inline-size: min(100% - var(--space-6), 60rem); }
|
|
119
|
+
|
|
118
120
|
.dashboard__header {
|
|
119
121
|
display: flex;
|
|
120
122
|
flex-wrap: wrap;
|
|
@@ -134,7 +136,7 @@ a {
|
|
|
134
136
|
|
|
135
137
|
h1 {
|
|
136
138
|
margin: 0;
|
|
137
|
-
color: var(--text
|
|
139
|
+
color: var(--text);
|
|
138
140
|
font-size: 32px;
|
|
139
141
|
font-weight: 700;
|
|
140
142
|
line-height: 1.2;
|
|
@@ -163,10 +165,9 @@ h1 {
|
|
|
163
165
|
.log-panel,
|
|
164
166
|
.noscript-note {
|
|
165
167
|
background: var(--bg-elevated);
|
|
166
|
-
border: 1px solid var(--
|
|
168
|
+
border: 1px solid var(--arb-elevated);
|
|
167
169
|
border-radius: var(--radius);
|
|
168
170
|
padding: var(--panel-padding);
|
|
169
|
-
box-shadow: var(--shadow);
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
/* Disclosure panels keep their native layout; only the open state adds spacing below. */
|
|
@@ -189,13 +190,36 @@ h1 {
|
|
|
189
190
|
.lead { gap: var(--space-4); padding-block: var(--space-8); }
|
|
190
191
|
.lead__text { display: grid; gap: var(--space-2); }
|
|
191
192
|
.lead h2 { font-size: 24px; line-height: 1.25; }
|
|
192
|
-
.lead__summary { max-inline-size: 38rem; color: var(--
|
|
193
|
+
.lead__summary { max-inline-size: 38rem; color: var(--text); font-size: 16px; line-height: 1.5; }
|
|
194
|
+
/* A run that failed or was cut off says so three ways: the words, a glyph beside the headline, and an accent
|
|
195
|
+
bar on the panel. The colour is the accent, never the only signal. Interrupted is a warning, not an error. */
|
|
196
|
+
.lead[data-state='error'], .lead[data-state='interrupted'] { border-inline-start: 4px solid var(--arb-error); }
|
|
197
|
+
.lead[data-state='interrupted'] { border-inline-start-color: var(--arb-warning); }
|
|
198
|
+
.lead[data-state='error'] h2::before, .lead[data-state='interrupted'] h2::before {
|
|
199
|
+
content: '!';
|
|
200
|
+
content: '!' / '';
|
|
201
|
+
display: inline-grid;
|
|
202
|
+
place-items: center;
|
|
203
|
+
inline-size: 1.2em;
|
|
204
|
+
block-size: 1.2em;
|
|
205
|
+
margin-inline-end: var(--space-3);
|
|
206
|
+
border: 2px solid currentColor;
|
|
207
|
+
border-radius: 999px;
|
|
208
|
+
color: var(--arb-error);
|
|
209
|
+
font-size: 0.8em;
|
|
210
|
+
font-weight: 800;
|
|
211
|
+
line-height: 1;
|
|
212
|
+
vertical-align: 0.08em;
|
|
213
|
+
}
|
|
214
|
+
.lead[data-state='interrupted'] h2::before { color: var(--arb-warning); }
|
|
215
|
+
.lead[data-state='interrupted'] .error-panel.setup-error { border-inline-start-color: var(--arb-warning); background: var(--arb-warning-bg); }
|
|
216
|
+
.lead[data-state='interrupted'] .setup-error h3, .lead[data-state='interrupted'] .setup-error h2 { color: var(--arb-warning); }
|
|
193
217
|
.lead__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }
|
|
194
218
|
.lead__actions:empty { display: none; }
|
|
195
219
|
|
|
196
220
|
/* The build progress bar: a plain track and fill, then one line saying which step and how long. */
|
|
197
221
|
.build-progress { display: grid; gap: var(--space-3); }
|
|
198
|
-
.progress-bar { block-size: 10px; border-radius: 999px; background:
|
|
222
|
+
.progress-bar { block-size: 10px; border-radius: 999px; background: var(--arb-elevated); border: 1px solid var(--border); overflow: hidden; }
|
|
199
223
|
.progress-bar__fill { display: block; block-size: 100%; border-radius: inherit; background: var(--accent); transition: inline-size 600ms ease; }
|
|
200
224
|
.progress-bar[data-slow='true'] .progress-bar__fill { background: var(--warning); }
|
|
201
225
|
.progress-meta { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: var(--space-1) var(--space-4); }
|
|
@@ -214,31 +238,31 @@ h1 {
|
|
|
214
238
|
align-items: baseline;
|
|
215
239
|
gap: var(--space-1) var(--space-3);
|
|
216
240
|
padding-block-end: var(--space-4);
|
|
217
|
-
border-block-end: 1px solid var(--
|
|
241
|
+
border-block-end: 1px solid var(--arb-elevated);
|
|
218
242
|
}
|
|
219
|
-
.section-heading h2 { margin: 0; font-size: 1.
|
|
220
|
-
.section-heading p { margin: 0; font-size:
|
|
243
|
+
.section-heading h2 { margin: 0; font-size: 1.25rem; font-weight: 700; }
|
|
244
|
+
.section-heading p { margin: 0; font-size: 14px; }
|
|
221
245
|
/* Which preset is active, readable at a glance. Same tint as a pressed preset button. */
|
|
222
246
|
.badge {
|
|
223
247
|
display: inline-flex;
|
|
224
248
|
align-items: center;
|
|
225
249
|
gap: var(--space-2);
|
|
226
250
|
padding: var(--space-1) var(--space-3);
|
|
227
|
-
border: 1px solid
|
|
251
|
+
border: 1px solid var(--arb-selected-border);
|
|
228
252
|
border-radius: 999px;
|
|
229
|
-
background:
|
|
253
|
+
background: var(--arb-selected-bg);
|
|
230
254
|
color: var(--text);
|
|
231
255
|
font-size: 12px;
|
|
232
256
|
font-weight: 600;
|
|
233
257
|
line-height: 1.4;
|
|
234
258
|
white-space: nowrap;
|
|
235
259
|
}
|
|
236
|
-
.badge::before { content: ''; inline-size: 6px; block-size: 6px; border-radius: 999px; background: var(--
|
|
260
|
+
.badge::before { content: ''; inline-size: 6px; block-size: 6px; border-radius: 999px; background: var(--arb-selected-border); }
|
|
237
261
|
#technical-section .technical > section:not(.danger-zone) { padding: 0; border: 0; box-shadow: none; background: none; }
|
|
238
|
-
#technical-section .danger-zone { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--
|
|
262
|
+
#technical-section .danger-zone { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--arb-error); background: var(--arb-error-bg); border-radius: var(--radius); }
|
|
239
263
|
#technical-section .log-panel { padding: 0; }
|
|
240
264
|
.log-panel > #log { margin-block-start: 0; }
|
|
241
|
-
#technical-section .danger-zone h3 { color: var(--
|
|
265
|
+
#technical-section .danger-zone h3 { color: var(--arb-error); font-size: 1rem; }
|
|
242
266
|
#technical-section h3 { font-size: 1rem; }
|
|
243
267
|
|
|
244
268
|
.status-row {
|
|
@@ -265,8 +289,10 @@ h1 {
|
|
|
265
289
|
|
|
266
290
|
/* Step logs, in the style of GitHub Actions: one row per recipe step with a status icon,
|
|
267
291
|
its name, how long it took, and a chevron. Open a row to read just that step's output. */
|
|
268
|
-
.step-logs { display: grid; border: 1px solid var(--
|
|
269
|
-
.step-log
|
|
292
|
+
.step-logs { display: grid; border: 1px solid var(--arb-elevated); border-radius: 10px; overflow: hidden; }
|
|
293
|
+
.step-log { border-inline-start: 4px solid transparent; }
|
|
294
|
+
.step-log + .step-log { border-block-start: 1px solid var(--arb-elevated); }
|
|
295
|
+
.step-log[data-step-state='failed'] { border-inline-start-color: var(--arb-error); background: var(--arb-error-bg); }
|
|
270
296
|
.step-log > summary {
|
|
271
297
|
display: flex;
|
|
272
298
|
align-items: center;
|
|
@@ -280,8 +306,8 @@ h1 {
|
|
|
280
306
|
transition: background-color 120ms ease;
|
|
281
307
|
}
|
|
282
308
|
.step-log > summary::-webkit-details-marker { display: none; }
|
|
283
|
-
.step-log > summary:hover { background:
|
|
284
|
-
.step-log > summary:focus-visible { outline
|
|
309
|
+
.step-log > summary:hover { background: var(--arb-surface-hover); }
|
|
310
|
+
.step-log > summary:focus-visible { outline-offset: -4px; }
|
|
285
311
|
.step-log > summary::after {
|
|
286
312
|
content: '';
|
|
287
313
|
flex: none;
|
|
@@ -295,7 +321,7 @@ h1 {
|
|
|
295
321
|
transition: transform 160ms ease;
|
|
296
322
|
}
|
|
297
323
|
.step-log[open] > summary::after { margin-block-start: -3px; transform: rotate(45deg); }
|
|
298
|
-
.step-log[open] > summary { border-block-end: 1px solid var(--
|
|
324
|
+
.step-log[open] > summary { border-block-end: 1px solid var(--arb-elevated); }
|
|
299
325
|
.step-log__name { flex: 1; min-inline-size: 0; overflow-wrap: anywhere; font-weight: 600; }
|
|
300
326
|
.step-log__outcome { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
301
327
|
.step-log--full > summary { color: var(--muted); }
|
|
@@ -310,17 +336,17 @@ h1 {
|
|
|
310
336
|
|
|
311
337
|
/* The status icon: a ring for waiting, a pulsing ring while running, a check, a cross. */
|
|
312
338
|
.step-log__status { flex: none; inline-size: 16px; block-size: 16px; border: 2px solid var(--muted); border-radius: 999px; position: relative; }
|
|
313
|
-
.step-log[data-step-state='active'] .step-log__status { border-color: var(--accent); background:
|
|
339
|
+
.step-log[data-step-state='active'] .step-log__status { border-color: var(--accent); background: radial-gradient(circle, var(--accent) 0 3px, transparent 3.5px); }
|
|
314
340
|
.step-log[data-step-state='complete'] .step-log__status { border-color: var(--success); background: var(--success); }
|
|
315
341
|
.step-log[data-step-state='complete'] .step-log__status::after { content: ''; position: absolute; inset-inline-start: 4px; inset-block-start: 1px; inline-size: 4px; block-size: 8px; border-inline-end: 2px solid var(--bg); border-block-end: 2px solid var(--bg); transform: rotate(40deg); }
|
|
316
|
-
.step-log[data-step-state='failed'] .step-log__status { border-color: var(--
|
|
342
|
+
.step-log[data-step-state='failed'] .step-log__status { border-color: var(--arb-error); background: var(--arb-error); }
|
|
317
343
|
.step-log[data-step-state='failed'] .step-log__status::before,
|
|
318
344
|
.step-log[data-step-state='failed'] .step-log__status::after { content: ''; position: absolute; inset-inline-start: 5px; inset-block-start: 2px; inline-size: 2px; block-size: 8px; background: var(--bg); transform: rotate(45deg); }
|
|
319
345
|
.step-log[data-step-state='failed'] .step-log__status::after { transform: rotate(-45deg); }
|
|
320
|
-
.step-log[data-step-state='failed'] > summary .step-log__name { color:
|
|
346
|
+
.step-log[data-step-state='failed'] > summary .step-log__name { color: var(--arb-error); }
|
|
321
347
|
.step-log[data-step-state='pending'] > summary .step-log__name { color: var(--muted); font-weight: 400; }
|
|
322
348
|
.step-log[data-step-state='unknown'] .step-log__status { border-style: dashed; }
|
|
323
|
-
@media (prefers-reduced-motion: reduce) { .step-log > summary, .step-log > summary::after
|
|
349
|
+
@media (prefers-reduced-motion: reduce) { .step-log > summary, .step-log > summary::after { transition: none; } }
|
|
324
350
|
|
|
325
351
|
.env-panel {
|
|
326
352
|
display: grid;
|
|
@@ -328,23 +354,25 @@ h1 {
|
|
|
328
354
|
}
|
|
329
355
|
|
|
330
356
|
.env-fieldset {
|
|
331
|
-
border:
|
|
332
|
-
border-radius: 10px;
|
|
357
|
+
border: 0;
|
|
333
358
|
margin: 0;
|
|
334
|
-
padding:
|
|
359
|
+
padding: 0;
|
|
335
360
|
display: grid;
|
|
336
|
-
gap: var(--space-
|
|
361
|
+
gap: var(--space-5);
|
|
337
362
|
}
|
|
338
363
|
|
|
339
364
|
.env-legend {
|
|
340
|
-
color: var(--
|
|
341
|
-
font-size:
|
|
342
|
-
|
|
365
|
+
color: var(--text);
|
|
366
|
+
font-size: 1rem;
|
|
367
|
+
font-weight: 700;
|
|
368
|
+
padding: 0;
|
|
369
|
+
margin-block-end: var(--space-1);
|
|
343
370
|
}
|
|
344
371
|
|
|
345
372
|
.env-input-label {
|
|
346
373
|
display: block;
|
|
347
374
|
font-size: 14px;
|
|
375
|
+
font-weight: 600;
|
|
348
376
|
}
|
|
349
377
|
|
|
350
378
|
.env-fieldset textarea {
|
|
@@ -353,9 +381,9 @@ h1 {
|
|
|
353
381
|
min-block-size: 12rem;
|
|
354
382
|
resize: vertical;
|
|
355
383
|
padding: var(--space-3);
|
|
356
|
-
color: var(--text
|
|
384
|
+
color: var(--text);
|
|
357
385
|
background: var(--bg);
|
|
358
|
-
border: 1px solid var(--
|
|
386
|
+
border: 1px solid var(--border);
|
|
359
387
|
border-radius: var(--radius-control);
|
|
360
388
|
font-family: var(--font-mono);
|
|
361
389
|
line-height: 1.5;
|
|
@@ -378,9 +406,9 @@ h1 {
|
|
|
378
406
|
min-inline-size: 0;
|
|
379
407
|
block-size: var(--control-height);
|
|
380
408
|
padding: 0 var(--space-3);
|
|
381
|
-
color: var(--text
|
|
409
|
+
color: var(--text);
|
|
382
410
|
background: var(--bg);
|
|
383
|
-
border: 1px solid var(--
|
|
411
|
+
border: 1px solid var(--border);
|
|
384
412
|
border-radius: var(--radius-control);
|
|
385
413
|
font-family: var(--font-mono);
|
|
386
414
|
font-size: 14px;
|
|
@@ -388,9 +416,40 @@ h1 {
|
|
|
388
416
|
|
|
389
417
|
.env-row[data-secret] .env-row__value { border-style: dashed; }
|
|
390
418
|
.env-row__stored { display: inline-flex; align-items: center; gap: var(--space-2); font-size: 13px; }
|
|
391
|
-
.env-row__secret { display: inline-flex; align-items: center; gap: var(--space-
|
|
419
|
+
.env-row__secret { display: inline-flex; align-items: center; gap: var(--space-2); min-block-size: var(--space-6); font-size: 14px; white-space: nowrap; }
|
|
420
|
+
/* A checkbox drawn from the palette, so every state is one we measured and none depends on the browser's own look
|
|
421
|
+
(a disabled native one ignores the palette and fades to about 1.5:1 on a light panel). Unchecked: the control
|
|
422
|
+
edge on the field colour. Checked: the action fill with its label colour for the tick. Disabled: the disabled
|
|
423
|
+
tokens, tick included, so a stored secret still reads as checked. */
|
|
424
|
+
input[type='checkbox'] {
|
|
425
|
+
appearance: none;
|
|
426
|
+
flex: none;
|
|
427
|
+
display: inline-grid;
|
|
428
|
+
place-content: center;
|
|
429
|
+
inline-size: 18px;
|
|
430
|
+
block-size: 18px;
|
|
431
|
+
margin: 0;
|
|
432
|
+
border: 2px solid var(--border);
|
|
433
|
+
border-radius: 4px;
|
|
434
|
+
background: var(--bg);
|
|
435
|
+
}
|
|
436
|
+
input[type='checkbox']:checked { background: var(--accent); border-color: var(--accent); }
|
|
437
|
+
input[type='checkbox']:checked::after {
|
|
438
|
+
content: '';
|
|
439
|
+
inline-size: 4px;
|
|
440
|
+
block-size: 9px;
|
|
441
|
+
margin-block-start: -2px;
|
|
442
|
+
border-inline-end: 2px solid var(--on-accent);
|
|
443
|
+
border-block-end: 2px solid var(--on-accent);
|
|
444
|
+
transform: rotate(40deg);
|
|
445
|
+
}
|
|
446
|
+
input[type='checkbox']:disabled, input[type='checkbox']:disabled:checked { background: var(--arb-disabled-bg); border-color: var(--arb-disabled-border); }
|
|
447
|
+
input[type='checkbox']:disabled:checked::after { border-color: var(--arb-disabled-fg); }
|
|
392
448
|
.env-row-actions { display: flex; gap: var(--space-2); }
|
|
393
449
|
.env-vault-note { color: var(--warning); }
|
|
450
|
+
/* Instructions are body copy, so they take the primary text colour; muted stays for metadata and hints. */
|
|
451
|
+
.muted.env-secret-help, .muted#environment-preset-help { color: var(--text); }
|
|
452
|
+
.muted.env-vault-note { color: var(--text); border-inline-start: 3px solid var(--arb-warning); padding-inline-start: var(--space-3); }
|
|
394
453
|
|
|
395
454
|
@media (max-width: 560px) {
|
|
396
455
|
.env-row { grid-template-columns: 1fr 1fr; }
|
|
@@ -403,8 +462,9 @@ h1 {
|
|
|
403
462
|
gap: var(--space-3);
|
|
404
463
|
}
|
|
405
464
|
|
|
406
|
-
fieldset:disabled {
|
|
407
|
-
|
|
465
|
+
fieldset:disabled { color: var(--arb-disabled-fg); }
|
|
466
|
+
fieldset:disabled input, fieldset:disabled textarea, fieldset:disabled select {
|
|
467
|
+
background: var(--arb-disabled-bg); color: var(--arb-disabled-fg); border-color: var(--arb-disabled-border);
|
|
408
468
|
}
|
|
409
469
|
|
|
410
470
|
.controls-panel {
|
|
@@ -439,10 +499,15 @@ fieldset:disabled {
|
|
|
439
499
|
padding-inline: var(--space-3);
|
|
440
500
|
}
|
|
441
501
|
|
|
442
|
-
.env-preset-button[aria-pressed='true']
|
|
443
|
-
|
|
444
|
-
|
|
502
|
+
.env-preset-button[aria-pressed='true'],
|
|
503
|
+
.button--selected {
|
|
504
|
+
border-color: var(--arb-selected-border);
|
|
505
|
+
background: var(--arb-selected-bg);
|
|
506
|
+
color: var(--text);
|
|
445
507
|
}
|
|
508
|
+
/* The light theme's selected and pressed surfaces are the same colour, so a selected control needs its own
|
|
509
|
+
persistent marker rather than relying on the fill: a check, beside the label. */
|
|
510
|
+
.env-preset-button[aria-pressed='true']::before { content: '\2713'; margin-inline-end: var(--space-2); font-weight: 700; }
|
|
446
511
|
|
|
447
512
|
.button {
|
|
448
513
|
justify-self: start;
|
|
@@ -459,33 +524,21 @@ fieldset:disabled {
|
|
|
459
524
|
background: transparent;
|
|
460
525
|
color: var(--text);
|
|
461
526
|
cursor: pointer;
|
|
462
|
-
transition:
|
|
527
|
+
transition: border-color 120ms ease, background-color 120ms ease;
|
|
463
528
|
}
|
|
529
|
+
.button:hover:not(:disabled) { background: var(--arb-surface-hover); }
|
|
530
|
+
.button:active:not(:disabled) { background: var(--arb-surface-pressed); }
|
|
464
531
|
|
|
465
|
-
.button:hover,
|
|
466
|
-
.button:focus-visible {
|
|
467
|
-
transform: translateY(-1px);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
.button:focus-visible {
|
|
471
|
-
outline: 2px solid var(--accent);
|
|
472
|
-
outline-offset: 1px;
|
|
473
|
-
}
|
|
474
532
|
|
|
475
533
|
.button--primary {
|
|
476
534
|
background: var(--accent);
|
|
477
535
|
color: var(--on-accent);
|
|
478
536
|
}
|
|
479
537
|
.button--primary:hover:not(:disabled) { background: var(--accent-hover); }
|
|
480
|
-
|
|
481
|
-
.button--primary:disabled {
|
|
482
|
-
opacity: 0.55;
|
|
483
|
-
cursor: not-allowed;
|
|
484
|
-
transform: none;
|
|
485
|
-
}
|
|
538
|
+
.button--primary:active:not(:disabled) { background: var(--accent-pressed); }
|
|
486
539
|
|
|
487
540
|
.button--subtle {
|
|
488
|
-
border-color: var(--
|
|
541
|
+
border-color: var(--border);
|
|
489
542
|
}
|
|
490
543
|
|
|
491
544
|
.button--small { min-block-size: 32px; padding: 4px 10px; font-size: 12px; }
|
|
@@ -495,43 +548,62 @@ fieldset:disabled {
|
|
|
495
548
|
.step-log__actions form { margin: 0; }
|
|
496
549
|
|
|
497
550
|
.button--danger {
|
|
498
|
-
background: var(--
|
|
499
|
-
color: var(--
|
|
551
|
+
background: var(--arb-error);
|
|
552
|
+
color: var(--arb-on-error);
|
|
500
553
|
}
|
|
554
|
+
.button--danger:hover:not(:disabled) { background: var(--arb-error-hover); }
|
|
555
|
+
.button--danger:active:not(:disabled) { background: var(--arb-error-pressed); }
|
|
501
556
|
|
|
502
|
-
|
|
557
|
+
/* Disabled is its own set of colours at full opacity, never a faded version of the enabled button. */
|
|
558
|
+
.button:disabled {
|
|
559
|
+
background: var(--arb-disabled-bg);
|
|
560
|
+
color: var(--arb-disabled-fg);
|
|
561
|
+
border-color: var(--arb-disabled-border);
|
|
562
|
+
cursor: not-allowed;
|
|
563
|
+
}
|
|
503
564
|
|
|
504
|
-
.danger-zone { border-color: var(--
|
|
505
|
-
.danger-zone h2 { color: var(--
|
|
565
|
+
.danger-zone { border-color: var(--arb-error); }
|
|
566
|
+
.danger-zone h2 { color: var(--arb-error); }
|
|
506
567
|
.danger-zone form { display: grid; justify-items: start; gap: var(--space-3); }
|
|
507
568
|
|
|
508
569
|
.table-scroll { overflow-x: auto; }
|
|
509
570
|
.workspace-table { inline-size: 100%; min-inline-size: 820px; border-collapse: collapse; }
|
|
510
571
|
.workspace-table th,
|
|
511
|
-
.workspace-table td { padding: 16px var(--space-4); text-align: start; vertical-align: middle; border-block-end: 1px solid var(--
|
|
572
|
+
.workspace-table td { padding: 16px var(--space-4); text-align: start; vertical-align: middle; border-block-end: 1px solid var(--border); overflow-wrap: anywhere; }
|
|
512
573
|
.workspace-table th:first-child,
|
|
513
574
|
.workspace-table td:first-child { padding-inline-start: 0; }
|
|
514
575
|
.workspace-table th:last-child,
|
|
515
576
|
.workspace-table td:last-child { padding-inline-end: 0; }
|
|
516
|
-
.workspace-table thead th { padding-block: 0 12px; color: var(--muted); font-size: 12px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; border-block-end-color: var(--border
|
|
577
|
+
.workspace-table thead th { padding-block: 0 12px; color: var(--muted); font-size: 12px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; border-block-end-color: var(--border); }
|
|
517
578
|
.workspace-table tbody tr:last-child > * { border-block-end: 0; }
|
|
518
579
|
.workspace-table tbody th { font-family: var(--font-mono); font-size: 14px; font-weight: 600; }
|
|
519
|
-
.workspace-table tbody th a { color: var(--text
|
|
580
|
+
.workspace-table tbody th a { color: var(--text); text-decoration: none; }
|
|
520
581
|
.workspace-table tbody th a:hover { color: var(--accent-hover); text-decoration: underline; }
|
|
521
|
-
.workspace-table
|
|
522
|
-
.workspace-table td { color: var(--text-cell); }
|
|
582
|
+
.workspace-table td { color: var(--text); }
|
|
523
583
|
.workspace-table__date { font-size: 14px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
524
584
|
.workspace-table__date .never { color: var(--muted); }
|
|
525
585
|
|
|
526
|
-
/* A status is a
|
|
527
|
-
|
|
586
|
+
/* A status is a glyph and a word, never colour alone. One rule per status the product reports (the words
|
|
587
|
+
come from the server and are unchanged); the colours come from the palette's status roles.
|
|
588
|
+
|
|
589
|
+
ready success on success_bg check
|
|
590
|
+
error error on error_bg cross
|
|
591
|
+
interrupted warning on warning_bg exclamation (a run that was cut off, not one that failed)
|
|
592
|
+
preparing accent on a surface ring with a dot
|
|
593
|
+
idle steel on a surface empty ring
|
|
594
|
+
stopped steel on a surface filled square
|
|
595
|
+
sleeping steel on a surface half-filled ring
|
|
596
|
+
anything else neutral, empty ring
|
|
597
|
+
|
|
598
|
+
idle, stopped and sleeping are told apart by their shape and their word, not by a colour the palette has no
|
|
599
|
+
role for. Where there is a tinted background it is the matching *_bg; elsewhere the pill sits on a general surface. */
|
|
528
600
|
.status-pill {
|
|
529
|
-
--pill-bg:
|
|
601
|
+
--pill-bg: var(--arb-elevated); --pill-text: var(--text); --pill-glyph: var(--arb-steel);
|
|
530
602
|
display: inline-flex;
|
|
531
603
|
align-items: center;
|
|
532
|
-
gap:
|
|
604
|
+
gap: var(--space-2);
|
|
533
605
|
margin: 0;
|
|
534
|
-
padding: 3px 10px;
|
|
606
|
+
padding: 3px 10px 3px 8px;
|
|
535
607
|
border-radius: 999px;
|
|
536
608
|
background: var(--pill-bg);
|
|
537
609
|
color: var(--pill-text);
|
|
@@ -540,16 +612,33 @@ fieldset:disabled {
|
|
|
540
612
|
line-height: 1.5;
|
|
541
613
|
white-space: nowrap;
|
|
542
614
|
}
|
|
543
|
-
.status-dot {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
615
|
+
.status-dot {
|
|
616
|
+
flex: none;
|
|
617
|
+
display: inline-grid;
|
|
618
|
+
place-items: center;
|
|
619
|
+
inline-size: 14px;
|
|
620
|
+
block-size: 14px;
|
|
621
|
+
box-sizing: border-box;
|
|
622
|
+
border-radius: 999px;
|
|
623
|
+
border: 2px solid var(--pill-glyph);
|
|
624
|
+
color: var(--pill-glyph);
|
|
625
|
+
font-size: 11px;
|
|
626
|
+
font-weight: 800;
|
|
627
|
+
line-height: 1;
|
|
628
|
+
}
|
|
629
|
+
.status-pill[data-status='ready'] { --pill-bg: var(--arb-success-bg); --pill-text: var(--arb-success); --pill-glyph: var(--arb-success); }
|
|
630
|
+
.status-pill[data-status='ready'] .status-dot { border-color: transparent; }
|
|
631
|
+
.status-pill[data-status='ready'] .status-dot::before { content: '\2713'; }
|
|
632
|
+
.status-pill[data-status='error'] { --pill-bg: var(--arb-error-bg); --pill-text: var(--arb-error); --pill-glyph: var(--arb-error); }
|
|
633
|
+
.status-pill[data-status='error'] .status-dot { border-color: transparent; }
|
|
634
|
+
.status-pill[data-status='error'] .status-dot::before { content: '\2715'; }
|
|
635
|
+
.status-pill[data-status='interrupted'] { --pill-bg: var(--arb-warning-bg); --pill-text: var(--arb-warning); --pill-glyph: var(--arb-warning); }
|
|
636
|
+
.status-pill[data-status='interrupted'] .status-dot { border-color: transparent; }
|
|
637
|
+
.status-pill[data-status='interrupted'] .status-dot::before { content: '!'; }
|
|
638
|
+
.status-pill[data-status='preparing'] { --pill-glyph: var(--arb-accent); }
|
|
639
|
+
.status-pill[data-status='preparing'] .status-dot { background: radial-gradient(circle, var(--pill-glyph) 0 2px, transparent 2.5px); }
|
|
640
|
+
.status-pill[data-status='stopped'] .status-dot { border-radius: 2px; background: var(--pill-glyph); }
|
|
641
|
+
.status-pill[data-status='sleeping'] .status-dot { background: linear-gradient(90deg, var(--pill-glyph) 50%, transparent 50%); }
|
|
553
642
|
|
|
554
643
|
/* The create form: four fields in one wrapping row, the button on its own line below. */
|
|
555
644
|
.create-fields { display: flex; flex-wrap: wrap; gap: var(--space-4); }
|
|
@@ -560,10 +649,6 @@ fieldset:disabled {
|
|
|
560
649
|
|
|
561
650
|
ol > li + li { margin-block-start: var(--space-2); }
|
|
562
651
|
|
|
563
|
-
.button--success {
|
|
564
|
-
background: color-mix(in oklab, var(--success) 22%, transparent);
|
|
565
|
-
border-color: color-mix(in oklab, var(--success) 55%, var(--border));
|
|
566
|
-
}
|
|
567
652
|
|
|
568
653
|
.muted {
|
|
569
654
|
margin: 0;
|
|
@@ -583,9 +668,9 @@ ol > li + li { margin-block-start: var(--space-2); }
|
|
|
583
668
|
margin: 0;
|
|
584
669
|
padding: var(--space-3);
|
|
585
670
|
border-radius: 10px;
|
|
586
|
-
border:
|
|
671
|
+
border: 0;
|
|
587
672
|
background: var(--bg);
|
|
588
|
-
color: var(--text
|
|
673
|
+
color: var(--text);
|
|
589
674
|
font-family: var(--font-mono);
|
|
590
675
|
font-size: 12px;
|
|
591
676
|
line-height: 1.4;
|
|
@@ -614,21 +699,22 @@ ol > li + li { margin-block-start: var(--space-2); }
|
|
|
614
699
|
.log-switch a {
|
|
615
700
|
color: var(--muted);
|
|
616
701
|
padding: var(--space-2) var(--space-3);
|
|
617
|
-
border: 1px solid var(--
|
|
702
|
+
border: 1px solid var(--border);
|
|
618
703
|
border-radius: var(--radius-control);
|
|
619
704
|
text-decoration: none;
|
|
620
705
|
}
|
|
706
|
+
.log-switch a:hover:not([aria-current='page']) { background: var(--arb-surface-hover); color: var(--text); }
|
|
621
707
|
|
|
622
708
|
.log-switch a[aria-current='page'] {
|
|
623
709
|
color: var(--text);
|
|
624
|
-
|
|
710
|
+
background: var(--arb-selected-bg);
|
|
711
|
+
border-color: var(--arb-selected-border);
|
|
712
|
+
font-weight: 600;
|
|
625
713
|
}
|
|
714
|
+
/* The tab you are on says so in words for assistive technology (aria-current) and in a marker for everyone,
|
|
715
|
+
because the light theme's selected fill is the same colour as its pressed fill. */
|
|
716
|
+
.log-switch a[aria-current='page']::before { content: '\2713'; margin-inline-end: var(--space-2); }
|
|
626
717
|
|
|
627
|
-
.log-switch a:focus-visible,
|
|
628
|
-
.detail-link:focus-visible {
|
|
629
|
-
outline: 2px solid var(--accent);
|
|
630
|
-
outline-offset: 2px;
|
|
631
|
-
}
|
|
632
718
|
|
|
633
719
|
.detail-link {
|
|
634
720
|
color: var(--muted);
|
|
@@ -681,13 +767,14 @@ p:empty:not([role]):not([aria-live]) { display: none; }
|
|
|
681
767
|
|
|
682
768
|
/* The card: the default page for a workspace. One small square in the middle of the screen. */
|
|
683
769
|
.card-page { min-block-size: 100dvb; display: grid; place-items: center; padding: var(--space-6) var(--space-3); }
|
|
684
|
-
.card { inline-size: min(100%, 26rem); display: grid; gap: var(--space-5); align-content: start; padding: var(--panel-padding); background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius);
|
|
770
|
+
.card { inline-size: min(100%, 26rem); display: grid; gap: var(--space-5); align-content: start; padding: var(--panel-padding); background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius); }
|
|
685
771
|
.card__header { display: flex; justify-content: space-between; align-items: start; gap: var(--space-3); }
|
|
772
|
+
.card__links { display: flex; align-items: center; gap: var(--space-1); flex: none; }
|
|
686
773
|
.card__name { display: grid; gap: var(--space-1); min-inline-size: 0; }
|
|
687
|
-
.card__id { overflow-wrap: anywhere; font-family: var(--font-mono); font-size: 14px; font-weight: 600; color: var(--text
|
|
774
|
+
.card__id { overflow-wrap: anywhere; font-family: var(--font-mono); font-size: 14px; font-weight: 600; color: var(--text); }
|
|
688
775
|
.card__text { display: grid; gap: var(--space-2); }
|
|
689
776
|
.card h1 { font-size: 24px; line-height: 1.25; }
|
|
690
|
-
.card__summary { color: var(--
|
|
777
|
+
.card__summary { color: var(--text); line-height: 1.5; }
|
|
691
778
|
.card__actions { display: grid; gap: var(--space-3); }
|
|
692
779
|
.card__actions:not(:has(*:not([hidden]))) { display: none; }
|
|
693
780
|
.card__actions .button, .card__actions form, .card__start .button { inline-size: 100%; text-align: center; }
|
|
@@ -696,5 +783,23 @@ p:empty:not([role]):not([aria-live]) { display: none; }
|
|
|
696
783
|
.card .setup-error pre { max-block-size: 8rem; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
697
784
|
.card select { inline-size: 100%; }
|
|
698
785
|
.icon-link { display: inline-flex; padding: var(--space-2); border-radius: var(--radius-control); color: var(--muted); }
|
|
699
|
-
.icon-link:hover { color: var(--text); background:
|
|
700
|
-
|
|
786
|
+
.icon-link:hover { color: var(--text); background: var(--arb-surface-hover); }
|
|
787
|
+
|
|
788
|
+
/* A selected control keeps its fill while hovered or pressed; only an unselected one takes the hover surface. */
|
|
789
|
+
.env-preset-button[aria-pressed='true']:hover:not(:disabled),
|
|
790
|
+
.env-preset-button[aria-pressed='true']:active:not(:disabled),
|
|
791
|
+
.button--selected:hover:not(:disabled),
|
|
792
|
+
.button--selected:active:not(:disabled) { background: var(--arb-selected-bg); }
|
|
793
|
+
|
|
794
|
+
/* High-contrast and forced-colors modes replace our colours with the system's. Anything drawn only with a
|
|
795
|
+
background colour would then vanish, so the shapes that carry meaning get a real border. Text and focus
|
|
796
|
+
rings keep working because they were never colour-only. */
|
|
797
|
+
@media (forced-colors: active) {
|
|
798
|
+
.button, .badge, .status-pill, .progress-bar, .env-preset-button { border: 1px solid ButtonText; }
|
|
799
|
+
.progress-bar__fill { background: Highlight; }
|
|
800
|
+
.step-log__status, .status-dot { forced-color-adjust: none; border-color: ButtonText; color: ButtonText; }
|
|
801
|
+
.env-preset-button[aria-pressed='true'], .button--selected { border-width: 2px; }
|
|
802
|
+
:focus-visible { outline-color: Highlight; }
|
|
803
|
+
.lead[data-state='error'], .lead[data-state='interrupted'], .setup-error, .step-log[data-step-state='failed'] { border-inline-start-color: ButtonText; }
|
|
804
|
+
.lead h2::before { forced-color-adjust: none; color: ButtonText; }
|
|
805
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<main class="dashboard" id="workspace" data-id="<%= @id %>" data-status="<%= field('status') %>" data-started="<%= field('prepare_started_at') %>" data-completed="<%= field('completed_at') %>" data-busy="<%= @presentation['busy'] %>">
|
|
1
|
+
<main class="dashboard dashboard--narrow" id="workspace" data-id="<%= @id %>" data-status="<%= field('status') %>" data-started="<%= field('prepare_started_at') %>" data-completed="<%= field('completed_at') %>" data-busy="<%= @presentation['busy'] %>">
|
|
2
2
|
<header class="dashboard__header">
|
|
3
3
|
<div class="dashboard__title"><p class="eyebrow">Managed workspace</p><h1 class="mono-title"><%= @id %></h1></div>
|
|
4
4
|
<div class="header-actions"><a class="button button--subtle" href="/workspaces/<%= @id %>">Back to workspace</a><a class="button button--subtle" href="/workspaces">All workspaces</a></div>
|
|
5
5
|
</header>
|
|
6
|
-
<section class="status-panel lead" id="lead" data-section="lead" aria-labelledby="status" <%= 'hidden' unless section?('lead') %>>
|
|
6
|
+
<section class="status-panel lead" id="lead" data-section="lead" data-state="<%= escape(@presentation['state']) %>" aria-labelledby="status" <%= 'hidden' unless section?('lead') %>>
|
|
7
7
|
<div class="lead__text">
|
|
8
8
|
<h2 id="status" role="status"><%= escape(@presentation['headline']) %></h2>
|
|
9
9
|
<p id="summary" class="lead__summary"><%= escape(@presentation['summary']) %></p>
|
|
@@ -40,6 +40,12 @@ module Workspaces
|
|
|
40
40
|
|
|
41
41
|
PREVIEW_STATUS_KEYS = %w(state busy ready headline summary progress lead_action current_preset sleep open).freeze
|
|
42
42
|
PREVIEW_ERROR = 'Setup failed. Open settings for the full log.'.freeze
|
|
43
|
+
# The Arb palette (dashboard/arb-palette.css) defines its colours per theme, selected by data-theme on the root
|
|
44
|
+
# element. The theme follows the visitor's operating-system setting, with dark when there is none. A tiny script
|
|
45
|
+
# in the head sets it before anything paints, so a light-mode visitor never sees a dark flash; a page without
|
|
46
|
+
# script stays dark, which the palette and the stylesheet are both tested in.
|
|
47
|
+
THEME_SCRIPT = 'document.documentElement.dataset.theme=' \
|
|
48
|
+
"window.matchMedia&&window.matchMedia('(prefers-color-scheme: light)').matches?'light':'dark'".freeze
|
|
43
49
|
|
|
44
50
|
# What the card polls for on a preview host, where the workspace's own pages share the origin.
|
|
45
51
|
# Only what the card draws: no log, no steps, no environment and no error text.
|
|
@@ -58,18 +64,15 @@ module Workspaces
|
|
|
58
64
|
new({}).render_checkout_authentication_error(path)
|
|
59
65
|
end
|
|
60
66
|
|
|
67
|
+
# The Arb Previews icon from the Arb design system (arb-previews/favicon.svg, unchanged), tuned for 16 pixels.
|
|
68
|
+
# It carries no environment signal: the old icon was blue on localhost and gold when hosted, and the kit
|
|
69
|
+
# forbids recolouring or badging its marks, so the page itself (the address bar, the host in the URL) is how
|
|
70
|
+
# a localhost dashboard is told from a hosted one.
|
|
71
|
+
# The Arb Previews icon, from the design system (arb-previews/favicon.svg, copied unchanged). It is the 16 px
|
|
72
|
+
# tuned vector, so it is served as it is: the kit forbids recolouring or badging it, which means the tab no
|
|
73
|
+
# longer shows whether this dashboard is the local one or a hosted one.
|
|
61
74
|
def self.favicon
|
|
62
|
-
|
|
63
|
-
color = local ? '#4f8dff' : '#f1bb53'
|
|
64
|
-
<<~SVG
|
|
65
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Workspaces">
|
|
66
|
-
<rect x="3" y="3" width="58" height="58" rx="13" fill="#0f1524"/>
|
|
67
|
-
<rect x="11" y="13" width="42" height="38" rx="5" fill="none" stroke="#{color}" stroke-width="4"/>
|
|
68
|
-
<path d="M11 25h42M32 25v26" fill="none" stroke="#{color}" stroke-width="4"/>
|
|
69
|
-
<circle cx="18" cy="19" r="2" fill="#{color}"/>
|
|
70
|
-
<circle cx="25" cy="19" r="2" fill="#{color}"/>
|
|
71
|
-
</svg>
|
|
72
|
-
SVG
|
|
75
|
+
File.read(File.join(__dir__, 'dashboard/arb-previews-favicon.svg'), encoding: 'UTF-8')
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def initialize(snapshot, preview: false)
|
|
@@ -110,11 +113,14 @@ module Workspaces
|
|
|
110
113
|
end
|
|
111
114
|
|
|
112
115
|
def document(title, body)
|
|
113
|
-
|
|
116
|
+
palette = File.read(File.join(__dir__, 'dashboard/arb-palette.css'), encoding: 'UTF-8')
|
|
117
|
+
css = palette + File.read(File.join(__dir__, 'dashboard/styles.css'), encoding: 'UTF-8')
|
|
114
118
|
script = File.read(File.join(__dir__, 'dashboard/app.js'), encoding: 'UTF-8') +
|
|
115
119
|
File.read(File.join(__dir__, 'dashboard/environment.js'), encoding: 'UTF-8')
|
|
116
|
-
'<!doctype html><html lang="en"><head><meta charset="utf-8">' \
|
|
120
|
+
'<!doctype html><html lang="en" data-theme="dark"><head><meta charset="utf-8">' \
|
|
117
121
|
'<meta name="viewport" content="width=device-width, initial-scale=1">' \
|
|
122
|
+
'<meta name="color-scheme" content="dark light">' \
|
|
123
|
+
"<script>#{THEME_SCRIPT}</script>" \
|
|
118
124
|
'<link rel="icon" type="image/svg+xml" href="/favicon.svg">' \
|
|
119
125
|
"<title>#{escape(title)}</title><style>#{css}</style></head>" \
|
|
120
126
|
"<body>#{body}<script>#{script}</script></body></html>"
|
|
@@ -319,6 +325,11 @@ module Workspaces
|
|
|
319
325
|
@preview ? Config.settings_url(@id) : "/workspaces/#{@id}/settings"
|
|
320
326
|
end
|
|
321
327
|
|
|
328
|
+
# Back to the list of every workspace, from the card.
|
|
329
|
+
def index_href
|
|
330
|
+
@preview ? Config.index_url : '/workspaces'
|
|
331
|
+
end
|
|
332
|
+
|
|
322
333
|
# Where the card's start form posts. A shared card with presets saves the chosen one through the
|
|
323
334
|
# environment form; a preview card starts through its own origin.
|
|
324
335
|
def start_form_action
|
data/lib/workspaces/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: workspaces
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Workspaces contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -79,6 +79,8 @@ files:
|
|
|
79
79
|
- lib/workspaces/config.rb
|
|
80
80
|
- lib/workspaces/coordinator.rb
|
|
81
81
|
- lib/workspaces/dashboard/app.js
|
|
82
|
+
- lib/workspaces/dashboard/arb-palette.css
|
|
83
|
+
- lib/workspaces/dashboard/arb-previews-favicon.svg
|
|
82
84
|
- lib/workspaces/dashboard/card.html.erb
|
|
83
85
|
- lib/workspaces/dashboard/checkout_authentication_error.html.erb
|
|
84
86
|
- lib/workspaces/dashboard/environment.js
|