cogworker 0.1.0 → 0.2.1
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/lib/cogworker/attempts.rb +47 -0
- data/lib/cogworker/basic_fetch.rb +25 -2
- data/lib/cogworker/heartbeat.rb +4 -2
- data/lib/cogworker/history/storage.rb +58 -22
- data/lib/cogworker/history.rb +57 -7
- data/lib/cogworker/manager.rb +4 -0
- data/lib/cogworker/periodic/ticker.rb +16 -1
- data/lib/cogworker/process.rb +13 -5
- data/lib/cogworker/processor.rb +4 -0
- data/lib/cogworker/queue.rb +17 -0
- data/lib/cogworker/redis_keys.rb +5 -0
- data/lib/cogworker/throughput.rb +51 -0
- data/lib/cogworker/version.rb +1 -1
- data/lib/cogworker/web/assets/nocturne/fonts/inter-latin.woff2 +0 -0
- data/lib/cogworker/web/assets/nocturne/styles.css +582 -0
- data/lib/cogworker/web/assets/phosphor/Phosphor.woff2 +0 -0
- data/lib/cogworker/web/assets/phosphor/style.css +4627 -0
- data/lib/cogworker/web/layout.rb +232 -141
- data/lib/cogworker/web/routes/history.rb +86 -32
- data/lib/cogworker/web/routes/jobs.rb +469 -0
- data/lib/cogworker/web/routes/overview.rb +746 -0
- data/lib/cogworker/web/routes/schedules.rb +197 -0
- data/lib/cogworker/web/routes/workers.rb +227 -0
- data/lib/cogworker/web.rb +4 -3
- metadata +11 -9
- data/lib/cogworker/web/assets/tailwind.css +0 -1
- data/lib/cogworker/web/routes/busy.rb +0 -99
- data/lib/cogworker/web/routes/dead.rb +0 -93
- data/lib/cogworker/web/routes/periodic.rb +0 -67
- data/lib/cogworker/web/routes/queues.rb +0 -101
- data/lib/cogworker/web/routes/retries.rb +0 -89
- data/lib/cogworker/web/routes/scheduled.rb +0 -49
- data/lib/cogworker/web/routes/stats.rb +0 -298
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/* Nocturne — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
|
|
2
|
+
/* Self-hosted Inter (latin subset, weights 400/500/600/700 in one variable
|
|
3
|
+
woff2 — Google serves the same file for each weight, so one @font-face
|
|
4
|
+
per weight is correct, not a duplicate download) instead of the
|
|
5
|
+
upstream fonts.googleapis.com @import: this gem's Web UI ships with zero
|
|
6
|
+
external network dependency (see "Fully offline" in CLAUDE.md), same
|
|
7
|
+
policy as htmx/Tailwind/AG Grid/Chart.js. */
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: 'Inter';
|
|
10
|
+
font-style: normal;
|
|
11
|
+
font-weight: 400;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
src: url('fonts/inter-latin.woff2') format('woff2');
|
|
14
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
15
|
+
}
|
|
16
|
+
@font-face {
|
|
17
|
+
font-family: 'Inter';
|
|
18
|
+
font-style: normal;
|
|
19
|
+
font-weight: 500;
|
|
20
|
+
font-display: swap;
|
|
21
|
+
src: url('fonts/inter-latin.woff2') format('woff2');
|
|
22
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
23
|
+
}
|
|
24
|
+
@font-face {
|
|
25
|
+
font-family: 'Inter';
|
|
26
|
+
font-style: normal;
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
font-display: swap;
|
|
29
|
+
src: url('fonts/inter-latin.woff2') format('woff2');
|
|
30
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
31
|
+
}
|
|
32
|
+
@font-face {
|
|
33
|
+
font-family: 'Inter';
|
|
34
|
+
font-style: normal;
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
font-display: swap;
|
|
37
|
+
src: url('fonts/inter-latin.woff2') format('woff2');
|
|
38
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:root {
|
|
42
|
+
--color-bg: #161826;
|
|
43
|
+
--color-surface: #232532;
|
|
44
|
+
--color-text: #e9e9ed;
|
|
45
|
+
--color-accent: #9184d9; /* review round 7 (Barron): the accent moves to
|
|
46
|
+
the product's blurple — OKLCH hue 289.2, the hue of the app's own Pro
|
|
47
|
+
accent (#9a8fe0, --om-accent-pro) — at the same lightness the steel
|
|
48
|
+
accent held (L 0.660, so the documented ratios survive) and chroma
|
|
49
|
+
in the product blurple's own range (its token sits at C 0.118; the
|
|
50
|
+
accent takes 0.125, from the old steel's 0.030):
|
|
51
|
+
the accent now reads as an accent rather than another neutral. */
|
|
52
|
+
--color-accent-2: #a7a1db; /* the machine-derived stand-in follows: same
|
|
53
|
+
hue, same L 0.734, chroma at the same 2:3 proportion it held */
|
|
54
|
+
--color-divider: color-mix(in srgb, #e9e9ed 16%, transparent);
|
|
55
|
+
|
|
56
|
+
/* Tonal ramps — generated in OKLCH on one shared lightness scale, so the
|
|
57
|
+
same step of any role matches the others in visual value. */
|
|
58
|
+
--color-neutral-100: #f3f5fe;
|
|
59
|
+
--color-neutral-200: #e4e7f5;
|
|
60
|
+
--color-neutral-300: #cfd3e5;
|
|
61
|
+
--color-neutral-400: #b2b6ca;
|
|
62
|
+
--color-neutral-500: #9397ab;
|
|
63
|
+
--color-neutral-600: #75798c;
|
|
64
|
+
--color-neutral-700: #595d6c;
|
|
65
|
+
--color-neutral-800: #3f424d;
|
|
66
|
+
--color-neutral-900: #292b31;
|
|
67
|
+
|
|
68
|
+
--color-accent-100: #f5f4ff;
|
|
69
|
+
--color-accent-200: #e7e5fe;
|
|
70
|
+
--color-accent-300: #d2cefd;
|
|
71
|
+
--color-accent-400: #b5abfc;
|
|
72
|
+
--color-accent-500: #968ae0;
|
|
73
|
+
--color-accent-600: #796cbf;
|
|
74
|
+
--color-accent-700: #5d5294;
|
|
75
|
+
--color-accent-800: #423a6a;
|
|
76
|
+
--color-accent-900: #2b2741;
|
|
77
|
+
|
|
78
|
+
--color-accent-2-100: #f5f4ff;
|
|
79
|
+
--color-accent-2-200: #e7e5fe;
|
|
80
|
+
--color-accent-2-300: #d2cefd;
|
|
81
|
+
--color-accent-2-400: #b5afe8;
|
|
82
|
+
--color-accent-2-500: #9690c9;
|
|
83
|
+
--color-accent-2-600: #7972a9;
|
|
84
|
+
--color-accent-2-700: #5c5783;
|
|
85
|
+
--color-accent-2-800: #423e5d;
|
|
86
|
+
--color-accent-2-900: #2b293a;
|
|
87
|
+
|
|
88
|
+
/* Semantic (warning/danger/success) ramps — nocturne's own token set is
|
|
89
|
+
mono (neutral + one accent, see readme's Color section); the three
|
|
90
|
+
hues below were picked ad hoc in the "Relay" concept mock for
|
|
91
|
+
retrying/failed-dead/healthy status (amber/rose/mint, chosen there as
|
|
92
|
+
literal hex, not tokens) and are formalized here as full ramps so
|
|
93
|
+
usage sites reference a role, not a hardcoded hex. Built the same way
|
|
94
|
+
as the accent-2 stand-in: each ramp keeps the mock's hue, is anchored
|
|
95
|
+
at the ramp's shared step-500 lightness, and reuses the accent ramp's
|
|
96
|
+
own chroma taper (proportional to its C at each step) scaled to that
|
|
97
|
+
hue's measured chroma — so a semantic ramp carries the same "vivid,
|
|
98
|
+
tapering at the extremes" character as the brand accent, unlike the
|
|
99
|
+
low-chroma neutral ramp. --color-warning/-danger/-success (the bare,
|
|
100
|
+
unindexed role, parallel to --color-accent) keep the mock's original
|
|
101
|
+
hex exactly, so nothing that already matched the concept shifts. */
|
|
102
|
+
--color-warning: #d9b884;
|
|
103
|
+
--color-warning-100: #f9f5ef;
|
|
104
|
+
--color-warning-200: #f0e7d9;
|
|
105
|
+
--color-warning-300: #e3d2b7;
|
|
106
|
+
--color-warning-400: #d0b283;
|
|
107
|
+
--color-warning-500: #b29360;
|
|
108
|
+
--color-warning-600: #937543;
|
|
109
|
+
--color-warning-700: #725931;
|
|
110
|
+
--color-warning-800: #513f22;
|
|
111
|
+
--color-warning-900: #332a1a;
|
|
112
|
+
|
|
113
|
+
--color-danger: #d98494;
|
|
114
|
+
--color-danger-100: #fef2f4;
|
|
115
|
+
--color-danger-200: #fae1e4;
|
|
116
|
+
--color-danger-300: #f5c6cd;
|
|
117
|
+
--color-danger-400: #ed9dab;
|
|
118
|
+
--color-danger-500: #d07c8c;
|
|
119
|
+
--color-danger-600: #af5e6e;
|
|
120
|
+
--color-danger-700: #874754;
|
|
121
|
+
--color-danger-800: #61323c;
|
|
122
|
+
--color-danger-900: #3c2327;
|
|
123
|
+
|
|
124
|
+
--color-success: #7fc0a0;
|
|
125
|
+
--color-success-100: #f0f8f3;
|
|
126
|
+
--color-success-200: #dcede4;
|
|
127
|
+
--color-success-300: #bdddcc;
|
|
128
|
+
--color-success-400: #8cc7a9;
|
|
129
|
+
--color-success-500: #68a889;
|
|
130
|
+
--color-success-600: #4a896c;
|
|
131
|
+
--color-success-700: #376a52;
|
|
132
|
+
--color-success-800: #274b3a;
|
|
133
|
+
--color-success-900: #1d3027;
|
|
134
|
+
|
|
135
|
+
/* Deck section-divider ground — review round 3: saturation as presence.
|
|
136
|
+
Dividers take a saturated deep indigo (hue 277, the neutral ramp's own
|
|
137
|
+
family; C 0.094 — written when the accent held C 0.030, before the
|
|
138
|
+
round-7 blurple raised it past the ground's) with a lighter bloom and
|
|
139
|
+
a ghost tint of the same hue, while the slide grounds stay
|
|
140
|
+
desaturated. Deck-scale fills only — not interface colors. */
|
|
141
|
+
--color-section: #262a60;
|
|
142
|
+
--color-section-glow: #353b80;
|
|
143
|
+
--color-section-ghost: #4c5397;
|
|
144
|
+
|
|
145
|
+
--font-heading: "Inter", system-ui, sans-serif;
|
|
146
|
+
--font-heading-weight: 500;
|
|
147
|
+
--font-body: "Inter", system-ui, sans-serif;
|
|
148
|
+
|
|
149
|
+
--space-1: 2.8px;
|
|
150
|
+
--space-2: 5.6px;
|
|
151
|
+
--space-3: 8.4px;
|
|
152
|
+
--space-4: 11.2px;
|
|
153
|
+
--space-6: 16.8px;
|
|
154
|
+
--space-8: 22.4px;
|
|
155
|
+
|
|
156
|
+
--radius-sm: 4px;
|
|
157
|
+
--radius-md: 8px;
|
|
158
|
+
--radius-lg: 14px;
|
|
159
|
+
|
|
160
|
+
/* Elevation — derived from the ground: soft ink-tinted shadows on a
|
|
161
|
+
light theme, a hairline edge + ambient darkness on a dark one. */
|
|
162
|
+
--shadow-sm: 0 0 0 1px #3f424d;
|
|
163
|
+
--shadow-md: 0 0 0 1px #595d6c, 0 6px 18px rgba(0,0,0,0.55);
|
|
164
|
+
--shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0,0,0,0.65);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Light variant — not part of the exported design system (nocturne's own
|
|
168
|
+
readme describes a dark-only "ground"); authored here so Cogworker's Web
|
|
169
|
+
UI can keep following the OS's `prefers-color-scheme`, same as it did
|
|
170
|
+
under the previous Tailwind theme, instead of dropping light mode.
|
|
171
|
+
Every *ramp* step keeps its shared, role-independent lightness schedule
|
|
172
|
+
(100 lightest … 900 darkest) but is reassigned to the mirror-image step
|
|
173
|
+
of its dark-theme value (light's 100 = dark's 900, light's 200 = dark's
|
|
174
|
+
800, … light's 500 stays put). This way every component rule in this
|
|
175
|
+
file that reaches for a specific step — written with a dark ground in
|
|
176
|
+
mind, e.g. "800 for a subtle tinted border" — keeps producing the
|
|
177
|
+
equivalently subtle result on a light ground too, with no component CSS
|
|
178
|
+
duplicated or forked per theme; only these token values change. The
|
|
179
|
+
four *role* tokens (bg/surface/text/divider) and the two brand-accent
|
|
180
|
+
anchors aren't ramp steps, so they're set directly below, not mirrored:
|
|
181
|
+
bg/surface stay near-white rather than literally inverting to mid-grey,
|
|
182
|
+
text goes near-black, and --color-accent/--color-accent-2 anchor at
|
|
183
|
+
what was the dark ramp's own 600 step — a darker point on the same hue,
|
|
184
|
+
needed for the ~3:1 contrast readme's Color section calls for once the
|
|
185
|
+
ground is light instead of dark (the untouched, lighter #9184d9 reads
|
|
186
|
+
fine on #161826 but washes out against a near-white surface). Nothing
|
|
187
|
+
here (never pure black/white, low chroma outside the accent, no bolded
|
|
188
|
+
headings) breaks the system's own Do/Don't rules. */
|
|
189
|
+
@media (prefers-color-scheme: light) {
|
|
190
|
+
:root {
|
|
191
|
+
--color-bg: #f5f6fe;
|
|
192
|
+
--color-surface: #fdfdff;
|
|
193
|
+
--color-text: #1a1a1f;
|
|
194
|
+
--color-accent: #796cbf;
|
|
195
|
+
--color-accent-2: #7972a9;
|
|
196
|
+
--color-divider: color-mix(in srgb, #1a1a1f 12%, transparent);
|
|
197
|
+
|
|
198
|
+
--color-neutral-100: #292b31;
|
|
199
|
+
--color-neutral-200: #3f424d;
|
|
200
|
+
--color-neutral-300: #595d6c;
|
|
201
|
+
--color-neutral-400: #75798c;
|
|
202
|
+
--color-neutral-500: #9397ab;
|
|
203
|
+
--color-neutral-600: #b2b6ca;
|
|
204
|
+
--color-neutral-700: #cfd3e5;
|
|
205
|
+
--color-neutral-800: #e4e7f5;
|
|
206
|
+
--color-neutral-900: #f3f5fe;
|
|
207
|
+
|
|
208
|
+
--color-accent-100: #2b2741;
|
|
209
|
+
--color-accent-200: #423a6a;
|
|
210
|
+
--color-accent-300: #5d5294;
|
|
211
|
+
--color-accent-400: #796cbf;
|
|
212
|
+
--color-accent-500: #968ae0;
|
|
213
|
+
--color-accent-600: #b5abfc;
|
|
214
|
+
--color-accent-700: #d2cefd;
|
|
215
|
+
--color-accent-800: #e7e5fe;
|
|
216
|
+
--color-accent-900: #f5f4ff;
|
|
217
|
+
|
|
218
|
+
--color-accent-2-100: #2b293a;
|
|
219
|
+
--color-accent-2-200: #423e5d;
|
|
220
|
+
--color-accent-2-300: #5c5783;
|
|
221
|
+
--color-accent-2-400: #7972a9;
|
|
222
|
+
--color-accent-2-500: #9690c9;
|
|
223
|
+
--color-accent-2-600: #b5afe8;
|
|
224
|
+
--color-accent-2-700: #d2cefd;
|
|
225
|
+
--color-accent-2-800: #e7e5fe;
|
|
226
|
+
--color-accent-2-900: #f5f4ff;
|
|
227
|
+
|
|
228
|
+
--color-warning: #937543;
|
|
229
|
+
--color-warning-100: #332a1a;
|
|
230
|
+
--color-warning-200: #513f22;
|
|
231
|
+
--color-warning-300: #725931;
|
|
232
|
+
--color-warning-400: #937543;
|
|
233
|
+
--color-warning-500: #b29360;
|
|
234
|
+
--color-warning-600: #d0b283;
|
|
235
|
+
--color-warning-700: #e3d2b7;
|
|
236
|
+
--color-warning-800: #f0e7d9;
|
|
237
|
+
--color-warning-900: #f9f5ef;
|
|
238
|
+
|
|
239
|
+
--color-danger: #af5e6e;
|
|
240
|
+
--color-danger-100: #3c2327;
|
|
241
|
+
--color-danger-200: #61323c;
|
|
242
|
+
--color-danger-300: #874754;
|
|
243
|
+
--color-danger-400: #af5e6e;
|
|
244
|
+
--color-danger-500: #d07c8c;
|
|
245
|
+
--color-danger-600: #ed9dab;
|
|
246
|
+
--color-danger-700: #f5c6cd;
|
|
247
|
+
--color-danger-800: #fae1e4;
|
|
248
|
+
--color-danger-900: #fef2f4;
|
|
249
|
+
|
|
250
|
+
--color-success: #4a896c;
|
|
251
|
+
--color-success-100: #1d3027;
|
|
252
|
+
--color-success-200: #274b3a;
|
|
253
|
+
--color-success-300: #376a52;
|
|
254
|
+
--color-success-400: #4a896c;
|
|
255
|
+
--color-success-500: #68a889;
|
|
256
|
+
--color-success-600: #8cc7a9;
|
|
257
|
+
--color-success-700: #bdddcc;
|
|
258
|
+
--color-success-800: #dcede4;
|
|
259
|
+
--color-success-900: #f0f8f3;
|
|
260
|
+
|
|
261
|
+
--shadow-sm: 0 0 0 1px #e4e7f5;
|
|
262
|
+
--shadow-md: 0 0 0 1px #cfd3e5, 0 6px 18px rgba(0,0,0,0.12);
|
|
263
|
+
--shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0,0,0,0.18);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
body {
|
|
268
|
+
background: var(--color-bg);
|
|
269
|
+
color: var(--color-text);
|
|
270
|
+
font-family: var(--font-body);
|
|
271
|
+
}
|
|
272
|
+
h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }
|
|
273
|
+
|
|
274
|
+
.lighten{mix-blend-mode:lighten;background-color:transparent}
|
|
275
|
+
|
|
276
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
277
|
+
Components — built with the tokens above. Plain CSS
|
|
278
|
+
on plain HTML: no JavaScript, no build step. Each class is documented in
|
|
279
|
+
readme.md and demonstrated in foundations/ and components/.
|
|
280
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
281
|
+
|
|
282
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
283
|
+
body { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }
|
|
284
|
+
h1, h2, h3, h4, h5, h6 {
|
|
285
|
+
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
|
286
|
+
line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
|
|
287
|
+
}
|
|
288
|
+
h1 { font-size: 42px; }
|
|
289
|
+
h2 { font-size: 32px; }
|
|
290
|
+
h3 { font-size: 25px; }
|
|
291
|
+
h4 { font-size: 20px; }
|
|
292
|
+
h5 { font-size: 16px; }
|
|
293
|
+
h6 { font-size: 13px; }
|
|
294
|
+
h6 { letter-spacing: 0.08em; text-transform: uppercase; }
|
|
295
|
+
p { margin: 0 0 var(--space-3); }
|
|
296
|
+
a { color: var(--color-accent); text-underline-offset: 3px; }
|
|
297
|
+
img { display: block; max-width: 100%; }
|
|
298
|
+
figure { margin: 0; }
|
|
299
|
+
figcaption {
|
|
300
|
+
font-size: 11px; margin-top: var(--space-1);
|
|
301
|
+
color: color-mix(in srgb, var(--color-text) 55%, transparent);
|
|
302
|
+
}
|
|
303
|
+
.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
|
|
304
|
+
/* Not part of the exported system — the "Relay" concept mock defined this
|
|
305
|
+
inline in its own <style> block for JIDs/class names; formalized here so
|
|
306
|
+
every consuming page shares one definition instead of repeating it. */
|
|
307
|
+
.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
|
|
308
|
+
:focus { outline: none; }
|
|
309
|
+
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
|
310
|
+
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }
|
|
311
|
+
|
|
312
|
+
/* — rules — */
|
|
313
|
+
/* Rules fade to transparent at both ends instead of stopping cleanly — a
|
|
314
|
+
Nocturne signature. The ramp is 48px an end (one deck baseline unit),
|
|
315
|
+
painted from the same divider token. Freestanding rules fade; box
|
|
316
|
+
outlines, in-control separators, and short accent marks stay solid. */
|
|
317
|
+
.hr {
|
|
318
|
+
height: 1px; border: 0; margin: var(--space-4) 0;
|
|
319
|
+
background: linear-gradient(to right,
|
|
320
|
+
transparent, var(--color-divider) 48px,
|
|
321
|
+
var(--color-divider) calc(100% - 48px), transparent);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* — buttons — */
|
|
325
|
+
.btn {
|
|
326
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
327
|
+
cursor: pointer; text-decoration: none; white-space: nowrap;
|
|
328
|
+
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
|
329
|
+
font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —
|
|
330
|
+
the pair sits side by side in sign-up rows */
|
|
331
|
+
background: transparent; border: 1px solid transparent;
|
|
332
|
+
padding: var(--space-2) calc(var(--space-3) * 1.2);
|
|
333
|
+
border-radius: var(--radius-md);
|
|
334
|
+
}
|
|
335
|
+
.btn svg { display: block; }
|
|
336
|
+
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
337
|
+
.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }
|
|
338
|
+
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
|
|
339
|
+
.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
|
|
340
|
+
.btn-secondary { border-color: var(--color-divider); }
|
|
341
|
+
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
|
|
342
|
+
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
|
|
343
|
+
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
|
|
344
|
+
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
|
|
345
|
+
.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
|
|
346
|
+
.btn-icon { width: 36px; height: 36px; padding: 0; }
|
|
347
|
+
.btn-block { width: 100%; margin-top: var(--space-2); }
|
|
348
|
+
/* Not part of the exported system (only .btn-primary/-secondary/-ghost/-icon/
|
|
349
|
+
-block are documented) — added for actions that are specifically a warning,
|
|
350
|
+
a destructive/danger action, or a positive/resume action (Cogworker's
|
|
351
|
+
Busy/Retries/Dead/Overview/Workers tabs have these), following the exact
|
|
352
|
+
same outline-only shape as .btn-primary, just anchored on the warning/
|
|
353
|
+
danger/success ramps instead of the accent. */
|
|
354
|
+
.btn-warning { color: var(--color-warning); border-color: var(--color-warning); }
|
|
355
|
+
.btn-warning:hover { background: color-mix(in srgb, var(--color-warning) 12%, transparent); }
|
|
356
|
+
.btn-warning:active { background: color-mix(in srgb, var(--color-warning) 22%, transparent); }
|
|
357
|
+
.btn-danger { color: var(--color-danger); border-color: var(--color-danger); }
|
|
358
|
+
.btn-danger:hover { background: color-mix(in srgb, var(--color-danger) 12%, transparent); }
|
|
359
|
+
.btn-danger:active { background: color-mix(in srgb, var(--color-danger) 22%, transparent); }
|
|
360
|
+
.btn-success { color: var(--color-success); border-color: var(--color-success); }
|
|
361
|
+
.btn-success:hover { background: color-mix(in srgb, var(--color-success) 12%, transparent); }
|
|
362
|
+
.btn-success:active { background: color-mix(in srgb, var(--color-success) 22%, transparent); }
|
|
363
|
+
|
|
364
|
+
/* — forms — */
|
|
365
|
+
.field > label {
|
|
366
|
+
display: block; font-size: 12px; margin-bottom: 5px;
|
|
367
|
+
color: color-mix(in srgb, var(--color-text) 70%, transparent);
|
|
368
|
+
}
|
|
369
|
+
.input {
|
|
370
|
+
width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;
|
|
371
|
+
font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);
|
|
372
|
+
background: var(--color-surface);
|
|
373
|
+
border: 1px solid var(--color-divider); border-radius: var(--radius-md);
|
|
374
|
+
}
|
|
375
|
+
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
|
|
376
|
+
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
|
|
377
|
+
textarea.input { min-height: 90px; resize: vertical; }
|
|
378
|
+
.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }
|
|
379
|
+
.radio input, .seg-opt input {
|
|
380
|
+
position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
|
|
381
|
+
}
|
|
382
|
+
.radio .dot {
|
|
383
|
+
width: 16px; height: 16px; flex: none; border-radius: 50%;
|
|
384
|
+
border: 1.5px solid var(--color-divider);
|
|
385
|
+
}
|
|
386
|
+
.radio:hover .dot { border-color: var(--color-accent); }
|
|
387
|
+
.radio input:checked + .dot {
|
|
388
|
+
border-color: var(--color-accent); background: var(--color-accent);
|
|
389
|
+
box-shadow: inset 0 0 0 4px var(--color-bg);
|
|
390
|
+
}
|
|
391
|
+
.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
|
|
392
|
+
/* `align-self: flex-start` guards against a real bug: when a `.seg` ends
|
|
393
|
+
up a *direct* child of a `display: flex; flex-direction: column` ancestor
|
|
394
|
+
(e.g. History's status filter, a direct child of `<main>` — unlike every
|
|
395
|
+
other `.seg` usage, nested a level or two deeper inside its own
|
|
396
|
+
`page_header` div) the flex spec "blockifies" its `inline-flex` to plain
|
|
397
|
+
`flex`, and the ancestor's default `align-items: stretch` then stretches
|
|
398
|
+
it to the ancestor's full width — a mostly-empty, oddly wide bordered box
|
|
399
|
+
with the actual segment labels bunched at the left. `align-self` opts
|
|
400
|
+
this element back out of stretching on the cross axis regardless of
|
|
401
|
+
where it ends up, and is a no-op everywhere `.seg` isn't a flex item at
|
|
402
|
+
all (nested usages), so it's safe to set unconditionally. */
|
|
403
|
+
.seg {
|
|
404
|
+
display: inline-flex; overflow: hidden; align-self: flex-start;
|
|
405
|
+
border: 1px solid var(--color-divider); border-radius: var(--radius-md);
|
|
406
|
+
}
|
|
407
|
+
.seg-opt {
|
|
408
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
409
|
+
padding: 7px 12px; font-size: 13px; cursor: pointer;
|
|
410
|
+
}
|
|
411
|
+
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
|
|
412
|
+
/* `.seg` clips its children to its own rounded corner via `overflow:
|
|
413
|
+
hidden` — a checked end segment's own corner is otherwise square, so its
|
|
414
|
+
`box-shadow: inset` ring gets cut off right at the parent's curve instead
|
|
415
|
+
of following it, a visible break in the ring exactly at the rounded
|
|
416
|
+
corner. Matching the end segment's own radius to the parent's (minus the
|
|
417
|
+
1px border) keeps the ring's corner following the same curve as the clip. */
|
|
418
|
+
.seg-opt:first-child { border-radius: calc(var(--radius-md) - 1px) 0 0 calc(var(--radius-md) - 1px); }
|
|
419
|
+
.seg-opt:last-child { border-radius: 0 calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0; }
|
|
420
|
+
.seg-opt:only-child { border-radius: calc(var(--radius-md) - 1px); }
|
|
421
|
+
.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
|
|
422
|
+
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
|
|
423
|
+
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }
|
|
424
|
+
|
|
425
|
+
/* — cards — */
|
|
426
|
+
.card {
|
|
427
|
+
display: flex; flex-direction: column; gap: var(--space-2);
|
|
428
|
+
padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-surface);
|
|
429
|
+
}
|
|
430
|
+
.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
|
|
431
|
+
.card-title {
|
|
432
|
+
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
|
433
|
+
font-size: 17px; line-height: 1.2;
|
|
434
|
+
}
|
|
435
|
+
.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }
|
|
436
|
+
.card-meta {
|
|
437
|
+
display: flex; align-items: center; gap: 6px; font-size: 11px;
|
|
438
|
+
color: color-mix(in srgb, var(--color-text) 50%, transparent);
|
|
439
|
+
}
|
|
440
|
+
.elev-sm { box-shadow: var(--shadow-sm); }
|
|
441
|
+
.elev-md { box-shadow: var(--shadow-md); }
|
|
442
|
+
.elev-lg { box-shadow: var(--shadow-lg); }
|
|
443
|
+
|
|
444
|
+
/* — tags — */
|
|
445
|
+
.tag {
|
|
446
|
+
display: inline-flex; align-items: center; font-size: 11px;
|
|
447
|
+
letter-spacing: 0.02em; padding: 3px 10px;
|
|
448
|
+
border-radius: calc(var(--radius-md) * 0.75);
|
|
449
|
+
}
|
|
450
|
+
.tag-accent { background: var(--color-accent-800); color: var(--color-accent-100); }
|
|
451
|
+
.tag-accent-2 { background: var(--color-accent-2-800); color: var(--color-accent-2-100); }
|
|
452
|
+
.tag-neutral { background: var(--color-neutral-800); color: var(--color-neutral-100); }
|
|
453
|
+
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }
|
|
454
|
+
/* Same addition as .btn-warning/-danger above, mirroring .tag-accent's own
|
|
455
|
+
shape (dark-ramp-800 fill, light-ramp-100 text) for the three semantic
|
|
456
|
+
ramps instead of the brand accent. */
|
|
457
|
+
.tag-warning { background: var(--color-warning-800); color: var(--color-warning-100); }
|
|
458
|
+
.tag-danger { background: var(--color-danger-800); color: var(--color-danger-100); }
|
|
459
|
+
.tag-success { background: var(--color-success-800); color: var(--color-success-100); }
|
|
460
|
+
|
|
461
|
+
/* — navigation — */
|
|
462
|
+
.nav {
|
|
463
|
+
display: flex; align-items: center; gap: var(--space-4);
|
|
464
|
+
padding: var(--space-3) var(--space-4);
|
|
465
|
+
border-bottom: none;
|
|
466
|
+
}
|
|
467
|
+
.nav-brand {
|
|
468
|
+
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
|
469
|
+
font-size: 18px; margin-right: auto;
|
|
470
|
+
}
|
|
471
|
+
.nav a { color: inherit; text-decoration: none; font-size: 14px; }
|
|
472
|
+
.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }
|
|
473
|
+
|
|
474
|
+
/* — layout — */
|
|
475
|
+
/* Not part of the exported design system — the mock's own canvas has no
|
|
476
|
+
equivalent of a real app's page content ever needing to run edge to edge.
|
|
477
|
+
`.cw-main--wide` (toggled by `Layout.wide_layout_toggle_button`/
|
|
478
|
+
`wide_layout_script`) lifts the cap for anyone working with wide tables
|
|
479
|
+
(History's AG Grid, the Overview queue-first layout) on a large monitor.
|
|
480
|
+
`contain: inline-size` fixes a real bug: `body`'s own `min-width: max-
|
|
481
|
+
content` (see `Layout.wrap`'s comment) is meant to be driven purely by
|
|
482
|
+
the header — but without this, `.cw-main`'s *own* max-content
|
|
483
|
+
contribution (a wide table/card grid, capped at 1480px by the max-width
|
|
484
|
+
above) also factors into that same calculation, and since 1480px is
|
|
485
|
+
bigger than the header's own true minimum (~1090px), `body` ends up
|
|
486
|
+
floored at 1480 instead — the header (and `main` riding along with it)
|
|
487
|
+
then sit frozen at 1480px for the *entire* practical resize range
|
|
488
|
+
instead of continuously tracking the window down to their real minimum,
|
|
489
|
+
caught by hand: dragging the window narrower did nothing at all until
|
|
490
|
+
crossing below 1480px, only then jumping straight to the scrollbar.
|
|
491
|
+
`contain: inline-size` tells the browser this element's own width
|
|
492
|
+
sizing doesn't depend on (and its content-based size shouldn't leak
|
|
493
|
+
into) its container's — main still gets its width entirely normally,
|
|
494
|
+
from the flex `align-items: stretch` cross-size body ends up resolving
|
|
495
|
+
to (via the header) and its own `max-width` above; it just stops being
|
|
496
|
+
counted as a contributor to what that resolved size floors out at. */
|
|
497
|
+
.cw-main { max-width: 1480px; width: 100%; margin: 0 auto; box-sizing: border-box; contain: inline-size; }
|
|
498
|
+
.cw-main.cw-main--wide { max-width: none; }
|
|
499
|
+
|
|
500
|
+
/* — tables — */
|
|
501
|
+
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
502
|
+
.table th {
|
|
503
|
+
text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
|
|
504
|
+
color: color-mix(in srgb, var(--color-text) 60%, transparent);
|
|
505
|
+
padding: var(--space-2); border-bottom: 1px solid transparent; /* keeps layout; the row paints the fading rule */
|
|
506
|
+
}
|
|
507
|
+
.table td {
|
|
508
|
+
padding: var(--space-2);
|
|
509
|
+
border-bottom: 1px solid transparent;
|
|
510
|
+
}
|
|
511
|
+
/* Row rules as row-level strips so the end-fade spans the row, not each cell. */
|
|
512
|
+
.table thead tr {
|
|
513
|
+
background: linear-gradient(to right,
|
|
514
|
+
transparent, var(--color-divider) 48px,
|
|
515
|
+
var(--color-divider) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
|
|
516
|
+
}
|
|
517
|
+
.table tbody tr {
|
|
518
|
+
background: linear-gradient(to right,
|
|
519
|
+
transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
|
|
520
|
+
color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
|
|
521
|
+
}
|
|
522
|
+
.table tbody tr:hover {
|
|
523
|
+
/* the hover tint layers over the row rule, which keeps painting */
|
|
524
|
+
background:
|
|
525
|
+
linear-gradient(color-mix(in srgb, var(--color-text) 4%, transparent),
|
|
526
|
+
color-mix(in srgb, var(--color-text) 4%, transparent)) no-repeat 0 0 / 100% 100%,
|
|
527
|
+
linear-gradient(to right,
|
|
528
|
+
transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
|
|
529
|
+
color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* — dialog — */
|
|
533
|
+
.dialog-backdrop {
|
|
534
|
+
position: fixed; inset: 0; display: grid; place-items: center;
|
|
535
|
+
padding: var(--space-4);
|
|
536
|
+
background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
|
|
537
|
+
}
|
|
538
|
+
/* `color: var(--color-text)` here is as load-bearing as `border: none`
|
|
539
|
+
below: a native `<dialog>`'s UA stylesheet also sets `color: CanvasText`
|
|
540
|
+
directly on the element, and since that's a declared value (not merely
|
|
541
|
+
inherited), it wins over the light `--color-text` that would otherwise
|
|
542
|
+
flow down from `body` — without overriding it here, every descendant
|
|
543
|
+
that doesn't set its own `color` (the title, the `<pre>` backtrace body)
|
|
544
|
+
renders near-black text on `--color-surface`'s dark background, unreadable. */
|
|
545
|
+
.dialog {
|
|
546
|
+
width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
|
|
547
|
+
padding: var(--space-4); border-radius: var(--radius-lg); border: none;
|
|
548
|
+
background: var(--color-surface); color: var(--color-text); box-shadow: var(--shadow-lg);
|
|
549
|
+
}
|
|
550
|
+
.dialog-title {
|
|
551
|
+
font-family: var(--font-heading); font-weight: var(--font-heading-weight);
|
|
552
|
+
font-size: 20px;
|
|
553
|
+
}
|
|
554
|
+
.dialog-body { font-size: 14px; opacity: 0.85; }
|
|
555
|
+
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }
|
|
556
|
+
/* Not part of the exported system — the native <dialog>/showModal() a
|
|
557
|
+
consuming page reaches for (e.g. History's backtrace viewer) paints its
|
|
558
|
+
own browser-default ::backdrop, not the .dialog-backdrop div above (that
|
|
559
|
+
one's for a JS-managed overlay <div>). Same tint, so either path looks
|
|
560
|
+
like the same system. .dialog itself needs one explicit override to work
|
|
561
|
+
as a native <dialog>'s own box styling, not just "unmodified" — see the
|
|
562
|
+
`border: none` above: `<dialog>` ships a UA-default `border-style: solid`
|
|
563
|
+
that neither `.dialog`'s `border-radius` nor `box-shadow` overrides on
|
|
564
|
+
their own, so without resetting it a real browser paints its own default
|
|
565
|
+
border (often a stark, unthemed gray/silver rectangle, square-cornered
|
|
566
|
+
on top of the rounded card underneath) around every native dialog on the
|
|
567
|
+
page — a real bug, caught by hand on History's backtrace viewer. */
|
|
568
|
+
dialog.dialog::backdrop {
|
|
569
|
+
background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
|
|
570
|
+
}
|
|
571
|
+
/* `.dialog`'s own `display: flex` (needed for the JS-managed
|
|
572
|
+
`.dialog-backdrop` div pattern above) has the same specificity as the
|
|
573
|
+
browser's built-in `dialog:not([open]) { display: none }` — and author
|
|
574
|
+
styles win a same-specificity tie over user-agent ones, so without this,
|
|
575
|
+
`.dialog` applied straight to a native `<dialog>` element defeats its
|
|
576
|
+
own native hide-until-`showModal()` behavior and renders permanently
|
|
577
|
+
open. Element-qualifying restores it without touching `.dialog`'s
|
|
578
|
+
existing div-overlay behavior elsewhere. */
|
|
579
|
+
dialog.dialog:not([open]) {
|
|
580
|
+
display: none;
|
|
581
|
+
}
|
|
582
|
+
|
|
Binary file
|