staysfixed 0.2.2 → 0.3.0
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.
- package/package.json +1 -1
- package/src/core/events.js +674 -0
- package/src/guard/api.js +168 -12
- package/src/guard/run.js +104 -6
- package/src/picture/capture.js +198 -3
- package/src/picture/run.js +157 -18
- package/src/types.js +25 -1
- package/src/watch/panel.js +836 -168
package/src/watch/panel.js
CHANGED
|
@@ -217,6 +217,10 @@ export function panelHtml(plan = {}) {
|
|
|
217
217
|
|
|
218
218
|
// --- every check, one quiet line each ----------------------------------
|
|
219
219
|
'<div class="scroll" id="scroll">',
|
|
220
|
+
// The working for whatever is on the glass. It belongs in the scrolling
|
|
221
|
+
// column, not inside the picture's own box — put there it stole the
|
|
222
|
+
// picture's height and squashed it to a sliver.
|
|
223
|
+
'<section class="work-here" id="work" hidden></section>',
|
|
220
224
|
'<section class="group" id="groupScreens" hidden>',
|
|
221
225
|
'<p class="grouplabel">Screens<span class="mono" id="countScreens"></span></p>',
|
|
222
226
|
'<div class="items" id="listScreens"></div>',
|
|
@@ -300,57 +304,97 @@ export function panelHtml(plan = {}) {
|
|
|
300
304
|
|
|
301
305
|
const STYLE = `
|
|
302
306
|
/* --------------------------------------------------------------------------
|
|
303
|
-
Tokens.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
307
|
+
Tokens — graphite and signal.
|
|
308
|
+
|
|
309
|
+
The page is graphite. Ground, cards, glass, the picture's well and every
|
|
310
|
+
piece of furniture on it are one family of greys, separated only by how much
|
|
311
|
+
light they carry and by a hairline where two of them meet. Type does the
|
|
312
|
+
rest: one monospace for names and numbers, one text face for sentences, one
|
|
313
|
+
scale of five sizes.
|
|
314
|
+
|
|
315
|
+
Colour is not decoration here and it is not a mood. It is reserved, entirely,
|
|
316
|
+
for the three things a person has to be told: a screen MOVED, a guard BROKE,
|
|
317
|
+
or something is WAITING for them to look at it. A check that held is not
|
|
318
|
+
coloured — it is simply present, in grey. A check that is running is not
|
|
319
|
+
coloured either; it is only lit. That is why the colours can be this pure:
|
|
320
|
+
they are the only ones on the page, so one amber dot in a column of graphite
|
|
321
|
+
is unmissable from across a desk.
|
|
322
|
+
|
|
323
|
+
The five states are also a ladder of lightness, in even steps of about
|
|
324
|
+
1.46:1, so they can be told apart with no colour vision at all — and each
|
|
325
|
+
one carries its own silhouette as well: a hollow ring for a check nobody ran,
|
|
326
|
+
a plain disc for one that held, a disc inside a crisp ring for one waiting,
|
|
327
|
+
a disc in a soft halo for one that moved, a diamond for one that broke.
|
|
328
|
+
|
|
329
|
+
Dark is the design — this window sits beside an editor all day. Light is
|
|
330
|
+
quarried from the same rock: a stone ground with cards floating a shade
|
|
331
|
+
above it, never a white page.
|
|
315
332
|
-------------------------------------------------------------------------- */
|
|
316
333
|
:root {
|
|
317
334
|
color-scheme: dark;
|
|
318
335
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
--
|
|
327
|
-
--
|
|
328
|
-
--
|
|
329
|
-
|
|
330
|
-
--
|
|
331
|
-
|
|
332
|
-
--
|
|
333
|
-
--
|
|
334
|
-
|
|
335
|
-
--
|
|
336
|
-
|
|
337
|
-
--
|
|
338
|
-
--
|
|
339
|
-
--
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
--
|
|
336
|
+
/* Borrowed simplicity, not a borrowed skin.
|
|
337
|
+
The app this sits beside is a flat neutral grey with hairline borders and
|
|
338
|
+
small radii, and copying that wholesale made this look like a piece of it
|
|
339
|
+
rather than a thing of its own. So: the same restraint, a different surface.
|
|
340
|
+
A degree or two cooler and a shade deeper, so the panel reads as the quiet
|
|
341
|
+
instrument next to the window rather than as more of the window. */
|
|
342
|
+
--ground: #14161a;
|
|
343
|
+
--lift: #1b1e23;
|
|
344
|
+
--card: rgba(200, 220, 255, 0.035);
|
|
345
|
+
--card-hover: rgba(200, 220, 255, 0.06);
|
|
346
|
+
--well: #0f1114;
|
|
347
|
+
--glass: rgba(20, 22, 26, 0.9);
|
|
348
|
+
|
|
349
|
+
--ink: #e8eaee;
|
|
350
|
+
--soft: #aeb3bb;
|
|
351
|
+
--faint: #8b9098;
|
|
352
|
+
--faintest: #6b7079;
|
|
353
|
+
|
|
354
|
+
--line: rgba(255, 255, 255, 0.05);
|
|
355
|
+
--line-firm: rgba(255, 255, 255, 0.17);
|
|
356
|
+
--sheen: rgba(255, 255, 255, 0.03);
|
|
357
|
+
--shadow: rgba(0, 0, 0, 0.5);
|
|
358
|
+
|
|
359
|
+
/* Activity, not state. Something is happening — the shutter, a running dot,
|
|
360
|
+
a live segment of the meter. Deliberately colourless: "happening" is not
|
|
361
|
+
news, and the moment it has news it has a colour instead. */
|
|
362
|
+
--accent: #9a9a9a;
|
|
363
|
+
|
|
364
|
+
/* Held is a grey. It is the whole idea: nothing to say, so nothing is said. */
|
|
365
|
+
--held: #6b7079;
|
|
366
|
+
|
|
367
|
+
/* The three that have something to say. Pure, because they are alone. */
|
|
368
|
+
--broke: #ff4438;
|
|
369
|
+
/* Not the blue next door — a teal, so "this one wants you" is unmistakably
|
|
370
|
+
this panel talking and not the app it is watching. */
|
|
371
|
+
--wait: #3fb9a6;
|
|
372
|
+
--moved: #ffc24d;
|
|
373
|
+
|
|
374
|
+
/* Nobody ran it. Barely there on purpose. */
|
|
375
|
+
--resting: rgba(255, 255, 255, 0.09);
|
|
376
|
+
--skip: var(--faintest);
|
|
377
|
+
--pending: var(--faintest);
|
|
378
|
+
--running: var(--accent);
|
|
379
|
+
--waiting: var(--wait);
|
|
380
|
+
|
|
381
|
+
/* Small radii, flat surfaces. The app this sits beside uses 8px and almost no
|
|
382
|
+
shadow; a panel with 20px corners and floating cards read as a different
|
|
383
|
+
product bolted on to the side of it. */
|
|
384
|
+
--radius: 10px;
|
|
385
|
+
--radius-sm: 8px;
|
|
386
|
+
--radius-xs: 6px;
|
|
387
|
+
|
|
388
|
+
--pad: 16px;
|
|
346
389
|
--tint: var(--accent);
|
|
347
390
|
|
|
348
391
|
/* The type scale. Nothing on this page is set at a size that is not here. */
|
|
349
392
|
--t-label: 10px;
|
|
350
393
|
--t-meta: 10.5px;
|
|
394
|
+
--t-small: 11px;
|
|
351
395
|
--t-body: 11.5px;
|
|
352
396
|
--t-name: 12px;
|
|
353
|
-
--t-lead:
|
|
397
|
+
--t-lead: 18px;
|
|
354
398
|
|
|
355
399
|
/* One curve. Everything that moves, moves on this. */
|
|
356
400
|
--ease: cubic-bezier(0.22, 0.72, 0.24, 1);
|
|
@@ -366,32 +410,41 @@ const STYLE = `
|
|
|
366
410
|
* panel came up pale on a dark desktop, which is exactly the thing it was asked not to
|
|
367
411
|
* do. The look is therefore stated on the html element rather than sniffed: dark unless
|
|
368
412
|
* somebody asks for light, and 'system' for anyone who does want the browser's opinion.
|
|
413
|
+
*
|
|
414
|
+
* The same rock, lit from the other side: a stone ground, cards a shade brighter
|
|
415
|
+
* than it rather than the other way round, and the three signal colours taken
|
|
416
|
+
* down until they hold on paper. The ladder inverts with the ground — the
|
|
417
|
+
* quietest state is now the lightest — and the steps stay wide enough to read
|
|
418
|
+
* with no colour vision.
|
|
369
419
|
*/
|
|
370
420
|
:root[data-theme='light'],
|
|
371
421
|
:root[data-theme='system'] {
|
|
372
422
|
color-scheme: light;
|
|
373
423
|
|
|
374
|
-
--ground: #
|
|
375
|
-
--lift: #
|
|
376
|
-
--card: rgba(255, 255, 255, 0.
|
|
377
|
-
--card-hover:
|
|
378
|
-
--well: #
|
|
379
|
-
--glass: rgba(
|
|
424
|
+
--ground: #d9dade;
|
|
425
|
+
--lift: #eef0f3;
|
|
426
|
+
--card: rgba(255, 255, 255, 0.6);
|
|
427
|
+
--card-hover: #ffffff;
|
|
428
|
+
--well: #c7c9cf;
|
|
429
|
+
--glass: rgba(232, 234, 238, 0.9);
|
|
430
|
+
|
|
431
|
+
--ink: #14161a;
|
|
432
|
+
--soft: #545a62;
|
|
433
|
+
--faint: #5e646c;
|
|
434
|
+
--faintest: #6f757d;
|
|
380
435
|
|
|
381
|
-
--
|
|
382
|
-
--
|
|
383
|
-
--
|
|
436
|
+
--line: rgba(20, 22, 26, 0.11);
|
|
437
|
+
--line-firm: rgba(20, 22, 26, 0.24);
|
|
438
|
+
--sheen: rgba(255, 255, 255, 0.9);
|
|
439
|
+
--shadow: rgba(26, 30, 38, 0.34);
|
|
384
440
|
|
|
385
|
-
--
|
|
386
|
-
--
|
|
387
|
-
--
|
|
388
|
-
--
|
|
441
|
+
--accent: #474d56;
|
|
442
|
+
--held: #757b83;
|
|
443
|
+
--broke: #7e1105;
|
|
444
|
+
--wait: #0e5588;
|
|
445
|
+
--moved: #8a5f06;
|
|
389
446
|
|
|
390
|
-
--
|
|
391
|
-
--held: #1f7a4d;
|
|
392
|
-
--moved: #94620a;
|
|
393
|
-
--broke: #b52f23;
|
|
394
|
-
--resting: rgba(28, 24, 18, 0.1);
|
|
447
|
+
--resting: rgba(20, 22, 26, 0.13);
|
|
395
448
|
}
|
|
396
449
|
|
|
397
450
|
* { box-sizing: border-box; }
|
|
@@ -415,14 +468,17 @@ img { display: block; }
|
|
|
415
468
|
button { font: inherit; color: inherit; }
|
|
416
469
|
.glyph { width: 16px; height: 16px; flex: 0 0 auto; }
|
|
417
470
|
|
|
418
|
-
/*
|
|
419
|
-
|
|
420
|
-
|
|
471
|
+
/* One light from above, one shadow below, and a thin sheen along the very top
|
|
472
|
+
edge of the window. Three flat washes, no pattern and no edge you could point
|
|
473
|
+
at — it is what stops a page of greys reading as paint. The light takes the
|
|
474
|
+
colour of the worst thing that has happened, which on a run where everything
|
|
475
|
+
held is no colour at all: only a little more light. */
|
|
421
476
|
.aura {
|
|
422
477
|
position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
|
423
478
|
background:
|
|
424
|
-
radial-gradient(
|
|
425
|
-
radial-gradient(
|
|
479
|
+
radial-gradient(132% 44% at 50% -12%, color-mix(in srgb, var(--tint) 14%, transparent), transparent 68%),
|
|
480
|
+
radial-gradient(74% 26% at 50% -4%, var(--sheen), transparent 76%),
|
|
481
|
+
radial-gradient(120% 62% at 50% 114%, var(--shadow), transparent 64%);
|
|
426
482
|
transition: background 620ms var(--ease);
|
|
427
483
|
}
|
|
428
484
|
|
|
@@ -437,24 +493,29 @@ button { font: inherit; color: inherit; }
|
|
|
437
493
|
flex: 0 0 auto;
|
|
438
494
|
padding: 16px var(--pad) 15px;
|
|
439
495
|
background: var(--glass);
|
|
440
|
-
backdrop-filter: blur(22px) saturate(
|
|
441
|
-
-webkit-backdrop-filter: blur(22px) saturate(
|
|
496
|
+
backdrop-filter: blur(22px) saturate(120%);
|
|
497
|
+
-webkit-backdrop-filter: blur(22px) saturate(120%);
|
|
498
|
+
/* Separated from the list by a hairline and nothing else. */
|
|
499
|
+
box-shadow: 0 1px 0 var(--line);
|
|
442
500
|
}
|
|
443
501
|
.brand { display: flex; align-items: center; gap: 9px; }
|
|
502
|
+
/* The mark sits on a milled square of the same graphite, lit along its top
|
|
503
|
+
edge. It is the product's own face, so it is never one of the three colours
|
|
504
|
+
that mean something is wrong. */
|
|
444
505
|
.badge {
|
|
445
506
|
flex: 0 0 auto;
|
|
446
507
|
display: flex; align-items: center; justify-content: center;
|
|
447
508
|
width: 24px; height: 24px;
|
|
448
509
|
border-radius: 8px;
|
|
449
|
-
color: var(--
|
|
450
|
-
background:
|
|
451
|
-
box-shadow: inset 0 0 0 1px
|
|
510
|
+
color: var(--ink);
|
|
511
|
+
background: var(--lift);
|
|
512
|
+
box-shadow: inset 0 0 0 1px var(--line-firm);
|
|
452
513
|
}
|
|
453
514
|
.badge .glyph { width: 15px; height: 15px; }
|
|
454
515
|
.wordmark {
|
|
455
516
|
flex: 1 1 auto; min-width: 0;
|
|
456
517
|
font-size: var(--t-label); font-weight: 600;
|
|
457
|
-
letter-spacing: 0.
|
|
518
|
+
letter-spacing: 0.22em; text-transform: uppercase;
|
|
458
519
|
color: var(--soft);
|
|
459
520
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
460
521
|
}
|
|
@@ -467,22 +528,45 @@ button { font: inherit; color: inherit; }
|
|
|
467
528
|
white-space: nowrap; overflow: hidden;
|
|
468
529
|
}
|
|
469
530
|
.target .mono { color: var(--ink); flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
|
|
470
|
-
.target .sep { color: var(--
|
|
531
|
+
.target .sep { color: var(--faintest); flex: 0 0 auto; }
|
|
471
532
|
.target .app { color: var(--faint); flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; font-size: var(--t-body); }
|
|
472
533
|
|
|
473
534
|
/* The sentence. The largest text on the page after the picture, because it is
|
|
474
|
-
the one thing a person reads from four feet away.
|
|
535
|
+
the one thing a person reads from four feet away. It is white while the news
|
|
536
|
+
is good and takes a colour only when there is news. */
|
|
475
537
|
.state {
|
|
476
|
-
margin-top:
|
|
477
|
-
font-size: var(--t-lead); font-weight:
|
|
478
|
-
letter-spacing: -0.
|
|
538
|
+
margin-top: 10px;
|
|
539
|
+
font-size: var(--t-lead); font-weight: 600; line-height: 1.3;
|
|
540
|
+
letter-spacing: -0.014em;
|
|
479
541
|
overflow-wrap: anywhere;
|
|
480
542
|
transition: color var(--slow) var(--ease);
|
|
481
543
|
}
|
|
482
|
-
.state.held { color: var(--ink); }
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
544
|
+
.state.held, .state.moved, .state.broke, .state.wait { color: var(--ink); }
|
|
545
|
+
/* The sentence stays white; a signal sits in front of it. It is the same mark
|
|
546
|
+
the row it is about carries — a haloed disc for a screen that moved, a disc
|
|
547
|
+
in a ring for one waiting on a person, a diamond for a guard that broke — so
|
|
548
|
+
the top of the window and the list underneath speak one language. The words
|
|
549
|
+
are what you read from four feet away; the mark is what you see before you
|
|
550
|
+
have read anything at all. */
|
|
551
|
+
.state.moved::before, .state.wait::before, .state.broke::before {
|
|
552
|
+
content: ''; display: inline-block;
|
|
553
|
+
width: 10px; height: 10px; border-radius: 50%;
|
|
554
|
+
margin-right: 13px; vertical-align: 0.1em;
|
|
555
|
+
}
|
|
556
|
+
.state.moved::before {
|
|
557
|
+
background: var(--moved);
|
|
558
|
+
box-shadow: 0 0 0 5px color-mix(in srgb, var(--moved) 17%, transparent);
|
|
559
|
+
}
|
|
560
|
+
.state.wait::before {
|
|
561
|
+
background: var(--wait);
|
|
562
|
+
box-shadow:
|
|
563
|
+
0 0 0 2px color-mix(in srgb, var(--wait) 42%, transparent),
|
|
564
|
+
0 0 0 5px color-mix(in srgb, var(--wait) 12%, transparent);
|
|
565
|
+
}
|
|
566
|
+
.state.broke::before {
|
|
567
|
+
background: var(--broke); border-radius: 2px; transform: rotate(45deg);
|
|
568
|
+
box-shadow: 0 0 0 5px color-mix(in srgb, var(--broke) 17%, transparent);
|
|
569
|
+
}
|
|
486
570
|
/* The verdict arrives rather than appearing: it is the one sentence the whole
|
|
487
571
|
run was for, and a run that ends by swapping a word looks like a page that
|
|
488
572
|
was not paying attention. */
|
|
@@ -492,8 +576,8 @@ button { font: inherit; color: inherit; }
|
|
|
492
576
|
to { opacity: 1; transform: none; filter: blur(0); }
|
|
493
577
|
}
|
|
494
578
|
.what {
|
|
495
|
-
margin:
|
|
496
|
-
line-height: 1.
|
|
579
|
+
margin: 7px 0 0; font-size: var(--t-small); color: var(--faint);
|
|
580
|
+
line-height: 1.5; overflow-wrap: anywhere;
|
|
497
581
|
}
|
|
498
582
|
.rverdict {
|
|
499
583
|
flex: 0 0 auto; font-size: var(--t-small); color: var(--faint);
|
|
@@ -502,13 +586,15 @@ button { font: inherit; color: inherit; }
|
|
|
502
586
|
.item.held .rverdict { color: var(--faint); }
|
|
503
587
|
.item.moved .rverdict { color: var(--moved); }
|
|
504
588
|
.item.broke .rverdict { color: var(--broke); }
|
|
589
|
+
.item.wait .rverdict { color: var(--wait); }
|
|
505
590
|
.item.waiting .rverdict { color: var(--waiting); }
|
|
506
|
-
.item.pending .rverdict, .item.skip .rverdict { color: var(--faintest
|
|
507
|
-
.note { margin-top:
|
|
591
|
+
.item.pending .rverdict, .item.skip .rverdict { color: var(--faintest); }
|
|
592
|
+
.note { margin-top: 6px; font-size: var(--t-body); color: var(--faint); overflow-wrap: anywhere; }
|
|
508
593
|
|
|
509
594
|
/* One hairline, not a row of blocks. Each finished check adds its own slice of
|
|
510
595
|
colour to a single continuous line, so progress and outcome are the same
|
|
511
|
-
object and there is one fewer thing on the page.
|
|
596
|
+
object and there is one fewer thing on the page. A run where everything held
|
|
597
|
+
finishes as one unbroken grey rule — which is the point. */
|
|
512
598
|
.meter { display: flex; align-items: center; gap: 10px; margin-top: 15px; }
|
|
513
599
|
.track {
|
|
514
600
|
flex: 1 1 auto; min-width: 0;
|
|
@@ -525,8 +611,8 @@ button { font: inherit; color: inherit; }
|
|
|
525
611
|
.slice.held { background: var(--held); }
|
|
526
612
|
.slice.moved { background: var(--moved); }
|
|
527
613
|
.slice.broke { background: var(--broke); }
|
|
528
|
-
.slice.wait { background: var(--
|
|
529
|
-
.slice.skip { background: color-mix(in srgb, var(--ink)
|
|
614
|
+
.slice.wait { background: var(--wait); }
|
|
615
|
+
.slice.skip { background: color-mix(in srgb, var(--ink) 13%, transparent); }
|
|
530
616
|
.slice.running {
|
|
531
617
|
background: var(--accent);
|
|
532
618
|
animation: breathe 1.9s var(--ease) infinite;
|
|
@@ -534,7 +620,7 @@ button { font: inherit; color: inherit; }
|
|
|
534
620
|
/* A segment lights as it lands, once, and then it is just a colour. */
|
|
535
621
|
.slice.land { animation: land 520ms var(--ease) 1; }
|
|
536
622
|
@keyframes land { 0% { filter: brightness(2.1); } 100% { filter: none; } }
|
|
537
|
-
@keyframes breathe { 0%, 100% { opacity: 0.
|
|
623
|
+
@keyframes breathe { 0%, 100% { opacity: 0.34; } 50% { opacity: 1; } }
|
|
538
624
|
.counts {
|
|
539
625
|
flex: 0 1 auto; min-width: 0; max-width: 66%;
|
|
540
626
|
font-size: var(--t-meta); color: var(--faint); letter-spacing: 0.02em;
|
|
@@ -565,9 +651,7 @@ button { font: inherit; color: inherit; }
|
|
|
565
651
|
/* No border. The picture is the hero, so nothing is drawn around it that
|
|
566
652
|
could compete with it — only a soft floor shadow and the faintest rim to
|
|
567
653
|
keep a white screenshot from bleeding into a light ground. */
|
|
568
|
-
box-shadow:
|
|
569
|
-
0 0 0 1px var(--line),
|
|
570
|
-
0 26px 50px -30px var(--shadow);
|
|
654
|
+
box-shadow: 0 0 0 1px var(--line);
|
|
571
655
|
transition: box-shadow var(--slow) var(--ease);
|
|
572
656
|
}
|
|
573
657
|
.shot.empty { cursor: default; }
|
|
@@ -587,29 +671,27 @@ button { font: inherit; color: inherit; }
|
|
|
587
671
|
text-align: center; padding: 0 30px; max-width: 260px;
|
|
588
672
|
}
|
|
589
673
|
|
|
590
|
-
/* The shutter, made visible. One pass of
|
|
674
|
+
/* The shutter, made visible. One pass of plain light across the glass for
|
|
591
675
|
exactly as long as the app is really being photographed — it starts when the
|
|
592
|
-
screen starts and it is gone the instant the picture lands
|
|
593
|
-
|
|
676
|
+
screen starts and it is gone the instant the picture lands. It is light, not
|
|
677
|
+
colour: nothing has been found out yet, so there is nothing to say. */
|
|
594
678
|
.sweep {
|
|
595
679
|
position: absolute; inset: 0;
|
|
596
680
|
pointer-events: none; opacity: 0;
|
|
597
681
|
background:
|
|
598
682
|
linear-gradient(100deg,
|
|
599
683
|
transparent 4%,
|
|
600
|
-
color-mix(in srgb, var(--accent)
|
|
601
|
-
color-mix(in srgb, var(--accent)
|
|
602
|
-
color-mix(in srgb, var(--accent)
|
|
603
|
-
color-mix(in srgb, var(--accent)
|
|
604
|
-
color-mix(in srgb, var(--accent)
|
|
684
|
+
color-mix(in srgb, var(--accent) 8%, transparent) 30%,
|
|
685
|
+
color-mix(in srgb, var(--accent) 28%, transparent) 47%,
|
|
686
|
+
color-mix(in srgb, var(--accent) 72%, transparent) 50%,
|
|
687
|
+
color-mix(in srgb, var(--accent) 28%, transparent) 53%,
|
|
688
|
+
color-mix(in srgb, var(--accent) 8%, transparent) 70%,
|
|
605
689
|
transparent 96%);
|
|
606
690
|
transform: translateX(-100%);
|
|
607
691
|
}
|
|
608
692
|
.shot.scanning .sweep { opacity: 1; animation: sweep 1.6s var(--ease) infinite; }
|
|
609
693
|
.shot.scanning {
|
|
610
|
-
box-shadow:
|
|
611
|
-
0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent),
|
|
612
|
-
0 26px 50px -30px var(--shadow);
|
|
694
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 34%, transparent);
|
|
613
695
|
}
|
|
614
696
|
@keyframes sweep {
|
|
615
697
|
from { transform: translateX(-100%); }
|
|
@@ -621,8 +703,9 @@ button { font: inherit; color: inherit; }
|
|
|
621
703
|
position: absolute; right: 10px; bottom: 10px;
|
|
622
704
|
font-size: var(--t-label); letter-spacing: 0.02em;
|
|
623
705
|
color: var(--ink);
|
|
624
|
-
background: color-mix(in srgb, var(--ground)
|
|
706
|
+
background: color-mix(in srgb, var(--ground) 76%, transparent);
|
|
625
707
|
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
|
708
|
+
box-shadow: inset 0 0 0 1px var(--line-firm);
|
|
626
709
|
border-radius: 999px; padding: 4px 10px;
|
|
627
710
|
opacity: 0; transform: translateY(4px);
|
|
628
711
|
transition: opacity var(--quick) var(--ease), transform var(--quick) var(--ease);
|
|
@@ -639,11 +722,12 @@ button { font: inherit; color: inherit; }
|
|
|
639
722
|
.shotout { flex: 1 1 auto; min-width: 0; font-size: var(--t-body); color: var(--soft); text-align: right; overflow-wrap: anywhere; }
|
|
640
723
|
.shotout.moved { color: var(--moved); }
|
|
641
724
|
.shotout.broke { color: var(--broke); }
|
|
642
|
-
.shotout.wait { color: var(--
|
|
725
|
+
.shotout.wait { color: var(--wait); }
|
|
643
726
|
|
|
644
727
|
/* Approved / now / difference. Three quiet words with a line that slides
|
|
645
728
|
between them — no pill, no box, because a screen that moved already has
|
|
646
|
-
enough asking for the eye.
|
|
729
|
+
enough asking for the eye. The line under the chosen word is the one place
|
|
730
|
+
the screen's own colour appears in the furniture. */
|
|
647
731
|
.switch { display: flex; gap: 2px; margin-top: 10px; }
|
|
648
732
|
.switch button {
|
|
649
733
|
position: relative;
|
|
@@ -657,7 +741,7 @@ button { font: inherit; color: inherit; }
|
|
|
657
741
|
}
|
|
658
742
|
.switch button::after {
|
|
659
743
|
content: ''; position: absolute; left: 50%; right: 50%; bottom: 0; height: 1.5px;
|
|
660
|
-
border-radius: 2px; background: var(--
|
|
744
|
+
border-radius: 2px; background: var(--ink);
|
|
661
745
|
transition: left var(--calm) var(--ease), right var(--calm) var(--ease), opacity var(--quick) var(--ease);
|
|
662
746
|
opacity: 0;
|
|
663
747
|
}
|
|
@@ -671,7 +755,7 @@ button { font: inherit; color: inherit; }
|
|
|
671
755
|
overflow-y: auto;
|
|
672
756
|
overscroll-behavior: contain;
|
|
673
757
|
scrollbar-width: thin;
|
|
674
|
-
scrollbar-color: color-mix(in srgb, var(--soft)
|
|
758
|
+
scrollbar-color: color-mix(in srgb, var(--soft) 20%, transparent) transparent;
|
|
675
759
|
padding: 18px 5px 14px 0;
|
|
676
760
|
/* The list slides away under the header rather than being cut off by a box. */
|
|
677
761
|
mask-image: linear-gradient(to bottom, transparent 0, #000 16px, #000 calc(100% - 12px), transparent 100%);
|
|
@@ -679,38 +763,42 @@ button { font: inherit; color: inherit; }
|
|
|
679
763
|
}
|
|
680
764
|
.scroll::-webkit-scrollbar { width: 10px; }
|
|
681
765
|
.scroll::-webkit-scrollbar-thumb {
|
|
682
|
-
background: color-mix(in srgb, var(--soft)
|
|
766
|
+
background: color-mix(in srgb, var(--soft) 20%, transparent);
|
|
683
767
|
border-radius: 8px; border: 3px solid transparent; background-clip: padding-box;
|
|
684
768
|
}
|
|
685
769
|
.scroll::-webkit-scrollbar-track { background: transparent; }
|
|
686
770
|
|
|
687
|
-
.group + .group { margin-top:
|
|
771
|
+
.group + .group { margin-top: 22px; }
|
|
688
772
|
.grouplabel {
|
|
689
773
|
display: flex; align-items: baseline; gap: 9px;
|
|
690
774
|
padding: 0 6px 9px 4px;
|
|
691
775
|
font-size: var(--t-label); font-weight: 600;
|
|
692
|
-
letter-spacing: 0.
|
|
776
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
693
777
|
color: var(--faint);
|
|
694
778
|
}
|
|
695
|
-
.grouplabel .mono { font-size: var(--t-label); letter-spacing: 0.
|
|
779
|
+
.grouplabel .mono { font-size: var(--t-label); letter-spacing: 0.06em; color: var(--faintest); }
|
|
696
780
|
|
|
781
|
+
/* A card, floating: a shade of graphite above the ground, lit along its top
|
|
782
|
+
edge, one hairline round it and a soft floor under it. Nothing but light
|
|
783
|
+
separates it from the page. */
|
|
697
784
|
.items {
|
|
698
785
|
border-radius: var(--radius);
|
|
699
786
|
background: var(--card);
|
|
700
787
|
overflow: hidden;
|
|
788
|
+
box-shadow: inset 0 0 0 1px var(--line);
|
|
701
789
|
}
|
|
702
790
|
.item + .item { box-shadow: inset 0 1px 0 var(--line); }
|
|
703
791
|
.item {
|
|
704
792
|
transition: background var(--calm) var(--ease);
|
|
705
793
|
}
|
|
706
|
-
.item.attention { background: color-mix(in srgb, var(--tone, var(--accent)) 5%, transparent); }
|
|
707
|
-
.item.running { background: color-mix(in srgb, var(--accent)
|
|
794
|
+
.item.attention { background: color-mix(in srgb, var(--tone, var(--accent)) 3.5%, transparent); }
|
|
795
|
+
.item.running { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
708
796
|
/* Something broke. It is said once, with weight, and then it sits still — a
|
|
709
797
|
panel that keeps flashing beside your work is a panel you turn off. */
|
|
710
798
|
.item.alarm { animation: alarm 900ms var(--ease) 1; }
|
|
711
799
|
@keyframes alarm {
|
|
712
|
-
0% { background: color-mix(in srgb, var(--tone, var(--broke))
|
|
713
|
-
100% { background: color-mix(in srgb, var(--tone, var(--broke)) 5%, transparent); }
|
|
800
|
+
0% { background: color-mix(in srgb, var(--tone, var(--broke)) 22%, transparent); }
|
|
801
|
+
100% { background: color-mix(in srgb, var(--tone, var(--broke)) 3.5%, transparent); }
|
|
714
802
|
}
|
|
715
803
|
|
|
716
804
|
/* Big, quiet, touch-sized rows. At rest a check is a dot, a name and a time —
|
|
@@ -727,8 +815,12 @@ button { font: inherit; color: inherit; }
|
|
|
727
815
|
.row:hover { background: var(--card-hover); }
|
|
728
816
|
.row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
729
817
|
.item.plain .row { cursor: default; }
|
|
730
|
-
.item.showing .row { box-shadow: inset 2px 0 0 var(--line-firm); }
|
|
731
|
-
.item.showing .rname { color: var(--ink); }
|
|
818
|
+
.item.showing .row, .item.working .row { box-shadow: inset 2px 0 0 var(--line-firm); }
|
|
819
|
+
.item.showing .rname, .item.working .rname { color: var(--ink); }
|
|
820
|
+
/* A check that wants a person is marked with a line down its edge in its own
|
|
821
|
+
colour, not a wash across it. A wash over graphite is a stain; a line is a
|
|
822
|
+
flag, and it reads at the same glance as the dot beside the name. */
|
|
823
|
+
.item.attention .row { box-shadow: inset 2px 0 0 var(--tone, var(--accent)); }
|
|
732
824
|
.item.plain .row:hover { background: transparent; }
|
|
733
825
|
/* Rows settle in rather than appearing, and they do it one after another, which
|
|
734
826
|
is what makes a list of eleven screens read as one thing arriving. */
|
|
@@ -738,23 +830,36 @@ button { font: inherit; color: inherit; }
|
|
|
738
830
|
to { opacity: 1; transform: none; }
|
|
739
831
|
}
|
|
740
832
|
|
|
833
|
+
/* The mark beside a name. Five states, five silhouettes, five rungs of a
|
|
834
|
+
lightness ladder — so it can be read with no colour vision, in a photocopy,
|
|
835
|
+
or out of the corner of an eye:
|
|
836
|
+
nobody ran it a hollow ring, barely there
|
|
837
|
+
it held a plain disc, grey, nothing around it
|
|
838
|
+
it is waiting a disc inside a crisp ring
|
|
839
|
+
it moved a disc in a soft wide halo
|
|
840
|
+
it broke a diamond in a soft wide halo
|
|
841
|
+
*/
|
|
741
842
|
.dot {
|
|
742
843
|
flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
|
|
743
844
|
background: var(--resting);
|
|
744
845
|
transition: background var(--calm) var(--ease), box-shadow var(--calm) var(--ease);
|
|
745
846
|
}
|
|
746
|
-
.item.held .dot {
|
|
747
|
-
.item.
|
|
748
|
-
.item.
|
|
749
|
-
.item.
|
|
750
|
-
|
|
847
|
+
.item.held .dot { width: 7px; height: 7px; background: var(--held); box-shadow: none; }
|
|
848
|
+
.item.wait .dot { background: var(--wait); box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--wait) 42%, transparent), 0 0 0 4px color-mix(in srgb, var(--wait) 12%, transparent); }
|
|
849
|
+
.item.moved .dot { background: var(--moved); box-shadow: 0 0 0 4px color-mix(in srgb, var(--moved) 17%, transparent); }
|
|
850
|
+
.item.broke .dot {
|
|
851
|
+
background: var(--broke);
|
|
852
|
+
border-radius: 2px; transform: rotate(45deg);
|
|
853
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--broke) 17%, transparent);
|
|
854
|
+
}
|
|
855
|
+
.item.skip .dot, .item.pending .dot { background: transparent; box-shadow: inset 0 0 0 1.5px var(--resting); }
|
|
751
856
|
.item.running .dot {
|
|
752
857
|
background: transparent;
|
|
753
858
|
box-shadow: inset 0 0 0 2px var(--accent);
|
|
754
859
|
animation: ping 1.8s var(--ease) infinite;
|
|
755
860
|
}
|
|
756
861
|
@keyframes ping {
|
|
757
|
-
0% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 0 color-mix(in srgb, var(--accent)
|
|
862
|
+
0% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 0 color-mix(in srgb, var(--accent) 34%, transparent); }
|
|
758
863
|
70%, 100% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 7px color-mix(in srgb, var(--accent) 0%, transparent); }
|
|
759
864
|
}
|
|
760
865
|
.item.running .rname { color: var(--ink); }
|
|
@@ -772,11 +877,11 @@ button { font: inherit; color: inherit; }
|
|
|
772
877
|
flex: 0 0 auto;
|
|
773
878
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
774
879
|
font-variant-numeric: tabular-nums;
|
|
775
|
-
font-size: var(--t-meta); color: var(--
|
|
880
|
+
font-size: var(--t-meta); color: var(--faintest);
|
|
776
881
|
}
|
|
777
882
|
.row .chev {
|
|
778
883
|
flex: 0 0 auto; width: 14px; height: 14px;
|
|
779
|
-
color: var(--
|
|
884
|
+
color: var(--faintest); opacity: 0;
|
|
780
885
|
transition: transform var(--calm) var(--ease), opacity var(--quick) var(--ease);
|
|
781
886
|
}
|
|
782
887
|
.row:hover .chev, .item.open .chev { opacity: 1; }
|
|
@@ -791,22 +896,135 @@ button { font: inherit; color: inherit; }
|
|
|
791
896
|
}
|
|
792
897
|
.item.open .detail { animation: unfold 240ms var(--ease) both; }
|
|
793
898
|
@keyframes unfold { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
|
|
794
|
-
.detail .why { color: var(--
|
|
899
|
+
.detail .why { color: var(--faint); }
|
|
900
|
+
/* Grey, whatever happened. The row above it has already said so in colour and
|
|
901
|
+
the working below colours the step that did it — a page that says the same
|
|
902
|
+
thing three times in the same colour is a page nobody scans. */
|
|
795
903
|
.detail .out { color: var(--soft); margin-top: 3px; }
|
|
796
|
-
.detail .out.moved { color: var(--moved); }
|
|
797
|
-
.detail .out.broke { color: var(--broke); }
|
|
798
|
-
.detail .out.wait { color: var(--accent); }
|
|
799
904
|
.detail .claim {
|
|
800
905
|
margin-top: 9px; padding: 9px 12px;
|
|
801
906
|
border-radius: var(--radius-xs);
|
|
802
907
|
background: var(--well);
|
|
803
|
-
box-shadow: inset 2px 0 0 var(--broke);
|
|
908
|
+
box-shadow: inset 2px 0 0 var(--broke), inset 0 0 0 1px var(--line);
|
|
804
909
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
805
910
|
font-size: var(--t-body); color: var(--ink);
|
|
806
911
|
}
|
|
807
912
|
.detail .claim em { font-style: normal; color: var(--faint); }
|
|
808
913
|
.detail .story { margin-top: 7px; color: var(--faint); }
|
|
809
914
|
|
|
915
|
+
/* The working, shown.
|
|
916
|
+
A verdict and a stopwatch do not tell anybody what was actually done to a
|
|
917
|
+
screen — which is exactly what the first person to look at this panel said.
|
|
918
|
+
So every step of a picture check is written out here in the order it
|
|
919
|
+
happened, numbered, with the number behind it on the right. It reads like a
|
|
920
|
+
receipt: quiet, aligned, and only ever seen by somebody who opened the row.
|
|
921
|
+
The spine down the left is built one segment per step, so the step that
|
|
922
|
+
moved or broke colours its own piece of it — the same idea as the meter at
|
|
923
|
+
the top of the window. */
|
|
924
|
+
.work-here { padding: 2px var(--pad) 10px; }
|
|
925
|
+
.work-here .worklabel { margin: 0 0 6px; }
|
|
926
|
+
.detail .worklabel { margin: 11px 0 6px; }
|
|
927
|
+
/* The heading of a working list: what it is, and — quietly, on the right —
|
|
928
|
+
which check it belongs to. The name matters now that the list is live: a
|
|
929
|
+
guard's claims can be ticking off while the last screen's picture is still
|
|
930
|
+
on the glass, and a heading with no name would leave a person guessing
|
|
931
|
+
whose working they are reading. */
|
|
932
|
+
.worklabel {
|
|
933
|
+
display: flex; align-items: baseline; gap: 8px;
|
|
934
|
+
font-size: var(--t-label); font-weight: 600;
|
|
935
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
936
|
+
color: var(--faint);
|
|
937
|
+
}
|
|
938
|
+
/* The heading never wraps: three words that never change. Anything that has to
|
|
939
|
+
give is the name beside it, which ellipses. */
|
|
940
|
+
.worklabel .worktitle { flex: 0 0 auto; white-space: nowrap; }
|
|
941
|
+
.worklabel .workwho {
|
|
942
|
+
flex: 1 1 auto; min-width: 0;
|
|
943
|
+
font-weight: 500; letter-spacing: 0.02em; text-transform: none;
|
|
944
|
+
text-align: right;
|
|
945
|
+
color: var(--faintest);
|
|
946
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
947
|
+
}
|
|
948
|
+
/* Said only while a list is empty, so a check that has just started does not
|
|
949
|
+
look like a check that found nothing to do. */
|
|
950
|
+
.workwait {
|
|
951
|
+
padding: 4px 0 3px 11px;
|
|
952
|
+
box-shadow: inset 1px 0 0 var(--line);
|
|
953
|
+
font-size: var(--t-body); color: var(--faint);
|
|
954
|
+
}
|
|
955
|
+
.work { list-style: none; margin: 0; padding: 0; }
|
|
956
|
+
.step {
|
|
957
|
+
display: flex; align-items: baseline; gap: 9px;
|
|
958
|
+
padding: 4px 0 4px 11px;
|
|
959
|
+
box-shadow: inset 1px 0 0 var(--line);
|
|
960
|
+
}
|
|
961
|
+
.step.warn { box-shadow: inset 1.5px 0 0 var(--moved); }
|
|
962
|
+
.step.bad { box-shadow: inset 1.5px 0 0 var(--broke); }
|
|
963
|
+
.step.skipped, .step.ended { box-shadow: inset 1px 0 0 var(--resting); }
|
|
964
|
+
.step.running { box-shadow: inset 1.5px 0 0 color-mix(in srgb, var(--accent) 66%, transparent); }
|
|
965
|
+
/* A line arrives once, quietly, and then never moves again. */
|
|
966
|
+
.step.arrive { animation: stepin 240ms var(--ease) both; }
|
|
967
|
+
@keyframes stepin { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
968
|
+
|
|
969
|
+
/* The tick. This is the thing a person actually watches: each line takes its
|
|
970
|
+
mark the moment it settles, so a screen being checked reads as a list
|
|
971
|
+
ticking off rather than a table that appears when it is all over. A step
|
|
972
|
+
still running carries a soft dot that breathes — an honest "this one is
|
|
973
|
+
happening", with nothing that spins on after the step it belongs to.
|
|
974
|
+
A tick is grey. Twenty grey ticks and one red cross is a page you can read
|
|
975
|
+
in a glance; twenty green ticks and one red cross is a Christmas tree. */
|
|
976
|
+
.stepmark {
|
|
977
|
+
/* Level with the first line of the label, never floating between the label
|
|
978
|
+
and the number under it. */
|
|
979
|
+
flex: 0 0 13px; align-self: flex-start;
|
|
980
|
+
display: flex; align-items: center; justify-content: center;
|
|
981
|
+
height: 18px; color: var(--faint);
|
|
982
|
+
transition: color var(--calm) var(--ease);
|
|
983
|
+
}
|
|
984
|
+
.stepmark .glyph { width: 13px; height: 13px; }
|
|
985
|
+
.step.ok .stepmark { color: var(--held); }
|
|
986
|
+
.step.skipped .stepmark, .step.ended .stepmark { color: var(--faintest); }
|
|
987
|
+
.step.running .stepmark { color: var(--accent); }
|
|
988
|
+
.step.running .stepmark .glyph { animation: markpulse 1.6s var(--ease) infinite; }
|
|
989
|
+
@keyframes markpulse { 0%, 100% { opacity: 0.34; } 50% { opacity: 1; } }
|
|
990
|
+
.step.running .stepwhat { color: var(--ink); }
|
|
991
|
+
|
|
992
|
+
/* Guards: the claims are what a guard is for, so they carry the weight, and
|
|
993
|
+
the actions that get the app into position sit back a little.
|
|
994
|
+
Named 'stepclaim' rather than 'claim' on purpose — a bare 'claim' inside a
|
|
995
|
+
detail is already the boxed expectation that failed, and these lines would
|
|
996
|
+
have been swept up by it. */
|
|
997
|
+
.step.stepclaim .stepwhat { color: var(--ink); font-weight: 560; }
|
|
998
|
+
.step.stepact .stepwhat { color: var(--soft); }
|
|
999
|
+
.step.stepact .stepno, .step.stepact .stepnum { color: var(--faintest); }
|
|
1000
|
+
.step.stepact .stepmark { opacity: 0.8; }
|
|
1001
|
+
/* The numeral keeps its own column whatever happens to the right of it — a
|
|
1002
|
+
number that has been left behind on a line of its own is not a sequence any
|
|
1003
|
+
more, which is what a long label does to it on a panel dragged narrow. */
|
|
1004
|
+
.stepno { flex: 0 0 15px; text-align: right; font-size: var(--t-label); color: var(--faintest); }
|
|
1005
|
+
/* Label on one line, its number under it.
|
|
1006
|
+
They used to share a line and wrap to a second, right-aligned one whenever the
|
|
1007
|
+
number was long — which at 460px was most of them, and the column came out
|
|
1008
|
+
ragged. Stacked, it reads down the page like a receipt: what was done, then
|
|
1009
|
+
what it came to. */
|
|
1010
|
+
.stepbody { flex: 1 1 auto; min-width: 0; }
|
|
1011
|
+
/* One size, whether the working is beside the picture or under a row: the same
|
|
1012
|
+
size the check names are set at, so the two lists read as one thing. */
|
|
1013
|
+
.stepwhat { color: var(--soft); font-size: var(--t-name); line-height: 1.5; }
|
|
1014
|
+
.stepnum {
|
|
1015
|
+
display: block; margin-top: 1px;
|
|
1016
|
+
font-size: var(--t-meta); color: var(--faint);
|
|
1017
|
+
overflow-wrap: anywhere;
|
|
1018
|
+
}
|
|
1019
|
+
/* A step that wants a person is the one thing in the list that is allowed to
|
|
1020
|
+
be read from across the row: its words go to full strength, its number takes
|
|
1021
|
+
the colour of what happened. */
|
|
1022
|
+
.step.warn .stepwhat, .step.bad .stepwhat { color: var(--ink); font-weight: 560; }
|
|
1023
|
+
.step.warn .stepno, .step.warn .stepnum, .step.warn .stepmark { color: var(--moved); }
|
|
1024
|
+
.step.bad .stepno, .step.bad .stepnum, .step.bad .stepmark { color: var(--broke); }
|
|
1025
|
+
.step.skipped .stepwhat, .step.skipped .stepno, .step.skipped .stepnum,
|
|
1026
|
+
.step.ended .stepwhat, .step.ended .stepno, .step.ended .stepnum { color: var(--faintest); }
|
|
1027
|
+
|
|
810
1028
|
.nothing { padding: 26px 16px; color: var(--faint); font-size: var(--t-body); line-height: 1.7; text-align: center; }
|
|
811
1029
|
.nothing .mono { color: var(--soft); }
|
|
812
1030
|
|
|
@@ -816,8 +1034,8 @@ button { font: inherit; color: inherit; }
|
|
|
816
1034
|
padding: 12px var(--pad) 13px;
|
|
817
1035
|
border-top: 1px solid var(--line);
|
|
818
1036
|
background: var(--glass);
|
|
819
|
-
backdrop-filter: blur(22px) saturate(
|
|
820
|
-
-webkit-backdrop-filter: blur(22px) saturate(
|
|
1037
|
+
backdrop-filter: blur(22px) saturate(120%);
|
|
1038
|
+
-webkit-backdrop-filter: blur(22px) saturate(120%);
|
|
821
1039
|
}
|
|
822
1040
|
.tophead {
|
|
823
1041
|
display: flex; align-items: center; gap: 8px;
|
|
@@ -828,7 +1046,7 @@ button { font: inherit; color: inherit; }
|
|
|
828
1046
|
.tlabel {
|
|
829
1047
|
flex: 1 1 auto;
|
|
830
1048
|
font-size: var(--t-label); font-weight: 600;
|
|
831
|
-
letter-spacing: 0.
|
|
1049
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
832
1050
|
color: var(--faint);
|
|
833
1051
|
}
|
|
834
1052
|
.ttotal {
|
|
@@ -841,7 +1059,7 @@ button { font: inherit; color: inherit; }
|
|
|
841
1059
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
842
1060
|
font-variant-numeric: tabular-nums;
|
|
843
1061
|
}
|
|
844
|
-
.tophead .chev { color: var(--
|
|
1062
|
+
.tophead .chev { color: var(--faintest); width: 13px; height: 13px; transition: transform var(--calm) var(--ease); }
|
|
845
1063
|
.tophead[aria-expanded='true'] .chev { transform: rotate(180deg); }
|
|
846
1064
|
.tophead:hover .tlabel, .tophead:hover .ttotal, .tophead:hover .chev { color: var(--soft); }
|
|
847
1065
|
|
|
@@ -861,32 +1079,34 @@ button { font: inherit; color: inherit; }
|
|
|
861
1079
|
|
|
862
1080
|
/* --- follow ------------------------------------------------------------- */
|
|
863
1081
|
.follow {
|
|
864
|
-
|
|
1082
|
+
/* Clear of the pinned strip at the bottom, not on top of it. At 16px it sat
|
|
1083
|
+
squarely over the timing line and covered the words it floated above. */
|
|
1084
|
+
position: fixed; left: 50%; bottom: 62px; transform: translateX(-50%);
|
|
865
1085
|
z-index: 4;
|
|
866
1086
|
font-size: var(--t-body);
|
|
867
|
-
color: var(--
|
|
1087
|
+
color: var(--soft);
|
|
868
1088
|
background: var(--lift);
|
|
869
|
-
border:
|
|
870
|
-
padding:
|
|
871
|
-
box-shadow: 0
|
|
1089
|
+
border: 0; border-radius: 999px;
|
|
1090
|
+
padding: 7px 15px; cursor: pointer;
|
|
1091
|
+
box-shadow: inset 0 0 0 1px var(--line-firm);
|
|
872
1092
|
animation: settle 240ms var(--ease) both;
|
|
873
|
-
transition:
|
|
1093
|
+
transition: color var(--quick) var(--ease), background var(--quick) var(--ease);
|
|
874
1094
|
}
|
|
875
|
-
.follow:hover {
|
|
1095
|
+
.follow:hover { color: var(--ink); background: var(--card-hover); }
|
|
876
1096
|
|
|
877
1097
|
/* --- the one thing left to do ------------------------------------------- */
|
|
878
1098
|
.foot {
|
|
879
1099
|
flex: 0 0 auto;
|
|
880
1100
|
padding: 14px var(--pad) 16px;
|
|
881
1101
|
background: var(--glass);
|
|
882
|
-
backdrop-filter: blur(22px) saturate(
|
|
883
|
-
-webkit-backdrop-filter: blur(22px) saturate(
|
|
1102
|
+
backdrop-filter: blur(22px) saturate(120%);
|
|
1103
|
+
-webkit-backdrop-filter: blur(22px) saturate(120%);
|
|
884
1104
|
border-top: 1px solid var(--line);
|
|
885
1105
|
animation: arrive 340ms var(--ease) both;
|
|
886
1106
|
}
|
|
887
1107
|
.nextlabel {
|
|
888
1108
|
font-size: var(--t-label); font-weight: 600;
|
|
889
|
-
letter-spacing: 0.
|
|
1109
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
890
1110
|
color: var(--faint);
|
|
891
1111
|
margin-bottom: 8px;
|
|
892
1112
|
}
|
|
@@ -896,22 +1116,26 @@ button { font: inherit; color: inherit; }
|
|
|
896
1116
|
display: flex; align-items: center;
|
|
897
1117
|
background: var(--well);
|
|
898
1118
|
border-radius: var(--radius-xs);
|
|
1119
|
+
box-shadow: inset 0 0 0 1px var(--line);
|
|
899
1120
|
padding: 9px 11px; font-size: var(--t-body);
|
|
900
1121
|
color: var(--ink);
|
|
901
1122
|
overflow-wrap: anywhere;
|
|
902
1123
|
user-select: all;
|
|
903
1124
|
}
|
|
904
|
-
.prompt { flex: 0 0 auto; color: var(--
|
|
1125
|
+
.prompt { flex: 0 0 auto; color: var(--faint); user-select: none; }
|
|
905
1126
|
.copy {
|
|
906
1127
|
flex: 0 0 auto; width: 36px;
|
|
907
1128
|
display: flex; align-items: center; justify-content: center;
|
|
908
1129
|
color: var(--soft); background: var(--well);
|
|
909
1130
|
border: 0; border-radius: var(--radius-xs);
|
|
1131
|
+
box-shadow: inset 0 0 0 1px var(--line);
|
|
910
1132
|
cursor: pointer;
|
|
911
1133
|
transition: color var(--quick) var(--ease), background var(--quick) var(--ease);
|
|
912
1134
|
}
|
|
913
1135
|
.copy:hover { color: var(--ink); background: var(--card-hover); }
|
|
914
|
-
.
|
|
1136
|
+
/* Copied. Not a colour — colour means something is wrong — but a hard invert,
|
|
1137
|
+
which is louder than any tint and gone again in a second. */
|
|
1138
|
+
.copy.done { color: var(--ground); background: var(--ink); }
|
|
915
1139
|
|
|
916
1140
|
/* --------------------------------------------------------------------------
|
|
917
1141
|
The viewer.
|
|
@@ -925,7 +1149,7 @@ button { font: inherit; color: inherit; }
|
|
|
925
1149
|
.viewer {
|
|
926
1150
|
position: fixed; inset: 0; z-index: 20;
|
|
927
1151
|
display: flex; flex-direction: column;
|
|
928
|
-
background: color-mix(in srgb, var(--ground)
|
|
1152
|
+
background: color-mix(in srgb, var(--ground) 88%, #000);
|
|
929
1153
|
animation: viewerin 220ms var(--ease) both;
|
|
930
1154
|
}
|
|
931
1155
|
@keyframes viewerin { from { opacity: 0; } to { opacity: 1; } }
|
|
@@ -959,7 +1183,7 @@ button { font: inherit; color: inherit; }
|
|
|
959
1183
|
}
|
|
960
1184
|
.vmodes button::after {
|
|
961
1185
|
content: ''; position: absolute; left: 50%; right: 50%; bottom: 1px; height: 1.5px;
|
|
962
|
-
border-radius: 2px; background: var(--
|
|
1186
|
+
border-radius: 2px; background: var(--ink); opacity: 0;
|
|
963
1187
|
transition: left var(--calm) var(--ease), right var(--calm) var(--ease), opacity var(--quick) var(--ease);
|
|
964
1188
|
}
|
|
965
1189
|
.vmodes button:hover { color: var(--soft); }
|
|
@@ -1035,14 +1259,16 @@ button { font: inherit; color: inherit; }
|
|
|
1035
1259
|
padding: 10px var(--pad) 15px;
|
|
1036
1260
|
text-align: center;
|
|
1037
1261
|
font-size: var(--t-label); letter-spacing: 0.03em;
|
|
1038
|
-
color: var(--
|
|
1262
|
+
color: var(--faintest);
|
|
1039
1263
|
}
|
|
1040
|
-
.vside { flex: 0 0 auto; font-size: var(--t-label); letter-spacing: 0.
|
|
1264
|
+
.vside { flex: 0 0 auto; font-size: var(--t-label); letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint); }
|
|
1265
|
+
/* Grey at the approved end, the screen's own amber at the new one: the slider
|
|
1266
|
+
itself says which way is "what changed". */
|
|
1041
1267
|
.vblend {
|
|
1042
1268
|
flex: 1 1 auto; min-width: 0;
|
|
1043
1269
|
-webkit-appearance: none; appearance: none;
|
|
1044
1270
|
height: 3px; border-radius: 999px;
|
|
1045
|
-
background: linear-gradient(to right, var(--
|
|
1271
|
+
background: linear-gradient(to right, var(--line-firm), var(--moved));
|
|
1046
1272
|
outline: none; cursor: pointer;
|
|
1047
1273
|
}
|
|
1048
1274
|
.vblend::-webkit-slider-thumb {
|
|
@@ -1058,7 +1284,7 @@ button { font: inherit; color: inherit; }
|
|
|
1058
1284
|
|
|
1059
1285
|
/* Dragged narrow. Everything stays, nothing wraps into a mess. */
|
|
1060
1286
|
@media (max-width: 330px) {
|
|
1061
|
-
:root { --pad: 12px; --radius: 16px; --t-lead:
|
|
1287
|
+
:root { --pad: 12px; --radius: 16px; --t-lead: 16px; }
|
|
1062
1288
|
.target .app, .target .sep { display: none; }
|
|
1063
1289
|
.row { gap: 9px; padding: 9px 11px; }
|
|
1064
1290
|
.detail { padding: 0 11px 12px 28px; }
|
|
@@ -1075,7 +1301,7 @@ button { font: inherit; color: inherit; }
|
|
|
1075
1301
|
/* Pulled wide. Two columns rather than one very long thin one, so the picture
|
|
1076
1302
|
gets the room and the list stays beside it instead of below the fold. */
|
|
1077
1303
|
@media (min-width: 720px) {
|
|
1078
|
-
:root { --pad: 24px; --t-lead:
|
|
1304
|
+
:root { --pad: 24px; --t-lead: 20px; }
|
|
1079
1305
|
.body { flex-direction: row; gap: 28px; padding-bottom: 4px; }
|
|
1080
1306
|
.stage { flex: 1 1 54%; min-width: 0; align-self: center; padding-top: 0; }
|
|
1081
1307
|
.scroll { flex: 1 1 46%; min-width: 0; padding-top: 22px; }
|
|
@@ -1120,7 +1346,7 @@ const SCRIPT = `
|
|
|
1120
1346
|
var ui = {
|
|
1121
1347
|
clock: el('clock'), project: el('project'), app: el('app'), targetsep: el('targetsep'),
|
|
1122
1348
|
state: el('state'), note: el('note'),
|
|
1123
|
-
track: el('track'), fill: el('fill'), counts: el('counts'), what: el('what'),
|
|
1349
|
+
track: el('track'), fill: el('fill'), counts: el('counts'), what: el('what'), work: el('work'),
|
|
1124
1350
|
stage: el('stage'), shot: el('shot'), layerA: el('layerA'), layerB: el('layerB'), blank: el('blank'),
|
|
1125
1351
|
caption: el('caption'), shotname: el('shotname'), shotout: el('shotout'), tabs: el('tabs'),
|
|
1126
1352
|
scroll: el('scroll'), follow: el('follow'), nothing: el('nothing'),
|
|
@@ -1406,7 +1632,12 @@ const SCRIPT = `
|
|
|
1406
1632
|
// what makes the whole run browsable afterwards instead of only the last
|
|
1407
1633
|
// screen photographed.
|
|
1408
1634
|
row.addEventListener('click', function () {
|
|
1409
|
-
|
|
1635
|
+
// Its picture, or its working, or both — whichever this check has. A
|
|
1636
|
+
// guard has no picture and its claims are the whole point of it, so a
|
|
1637
|
+
// guard row brings its own working back exactly like a screen does.
|
|
1638
|
+
var forward =
|
|
1639
|
+
(entry.pics && !entry.root.classList.contains('showing')) ||
|
|
1640
|
+
((hasWork(entry) || entry.running) && workOwner !== entry);
|
|
1410
1641
|
if (forward) {
|
|
1411
1642
|
recall(entry);
|
|
1412
1643
|
setOpen(entry, true);
|
|
@@ -1454,7 +1685,21 @@ const SCRIPT = `
|
|
|
1454
1685
|
out.textContent = entry.outText;
|
|
1455
1686
|
d.appendChild(out);
|
|
1456
1687
|
}
|
|
1457
|
-
|
|
1688
|
+
// The working. Everything that was really done to this check, in the order
|
|
1689
|
+
// it happened, numbered so it reads as a sequence rather than a bag of
|
|
1690
|
+
// facts. The heading and the list are the SAME two elements every time —
|
|
1691
|
+
// built once for this row and put back — so a redraw in the middle of a
|
|
1692
|
+
// run never tears down lines that are still ticking.
|
|
1693
|
+
if (hasWork(entry)) {
|
|
1694
|
+
var ol = detailWork(entry);
|
|
1695
|
+
d.appendChild(entry.workLabel);
|
|
1696
|
+
d.appendChild(ol);
|
|
1697
|
+
renderWork(ol, entry.checks, entry.kind);
|
|
1698
|
+
}
|
|
1699
|
+
// The expectation that failed, boxed — unless the working above already
|
|
1700
|
+
// ends on it, which it does now that a guard shows its claims. Nothing on
|
|
1701
|
+
// this page is said twice.
|
|
1702
|
+
if (entry.failedAt && !inWork(entry, entry.failedAt)) {
|
|
1458
1703
|
var claim = document.createElement('div');
|
|
1459
1704
|
claim.className = 'claim';
|
|
1460
1705
|
var label = document.createElement('em');
|
|
@@ -1475,20 +1720,387 @@ const SCRIPT = `
|
|
|
1475
1720
|
if (!entry.hasDetail) setOpen(entry, false);
|
|
1476
1721
|
}
|
|
1477
1722
|
|
|
1478
|
-
|
|
1723
|
+
// -------------------------------------------------------------------------
|
|
1724
|
+
// The working, ticked off live.
|
|
1725
|
+
//
|
|
1726
|
+
// A table that appears once a screen has already finished tells nobody what
|
|
1727
|
+
// is happening — which is exactly what the first person to watch this said.
|
|
1728
|
+
// So the run now announces each thing as it reaches it, and this is the part
|
|
1729
|
+
// of the panel that draws them: the names of what is being checked on the
|
|
1730
|
+
// check that is running right now, each one taking its mark as it settles.
|
|
1731
|
+
//
|
|
1732
|
+
// Two rules hold it up. Every line is found again by its own id rather than
|
|
1733
|
+
// redrawn, so a list that is already right is never torn down and rebuilt —
|
|
1734
|
+
// that is what stops it flickering when the verdict arrives carrying the
|
|
1735
|
+
// whole list again. And a step whose outcome needs a person shows the number
|
|
1736
|
+
// behind it the moment it lands, because that is the moment somebody is
|
|
1737
|
+
// looking.
|
|
1738
|
+
// -------------------------------------------------------------------------
|
|
1739
|
+
|
|
1740
|
+
// The marks, drawn here like every other icon on this page. A tick for a
|
|
1741
|
+
// thing that held, a cross for one that broke, a stroke for a thing that was
|
|
1742
|
+
// deliberately not done, and a soft dot for the one happening now.
|
|
1743
|
+
var MARKS = {
|
|
1744
|
+
ok: '<svg class="glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.4 12.6l4.3 4.3 8.9-9.8"></path></svg>',
|
|
1745
|
+
warn: '<svg class="glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M12 5.4v8.4"></path><path d="M12 18.3h.01"></path></svg>',
|
|
1746
|
+
bad: '<svg class="glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6.6 6.6l10.8 10.8M17.4 6.6L6.6 17.4"></path></svg>',
|
|
1747
|
+
skipped: '<svg class="glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6.4 12h11.2"></path></svg>',
|
|
1748
|
+
ended: '<svg class="glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6.4 12h11.2"></path></svg>',
|
|
1749
|
+
running: '<svg class="glyph" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="4.1" fill="currentColor"></circle></svg>'
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
var STEP_STATES = { running: 1, ok: 1, warn: 1, bad: 1, skipped: 1, ended: 1 };
|
|
1753
|
+
|
|
1754
|
+
function labelOf(step) { return String(step && step.label == null ? '' : step.label).trim(); }
|
|
1755
|
+
function detailOf(step) { return String(step && step.detail == null ? '' : step.detail).trim(); }
|
|
1756
|
+
function stateOf(step) {
|
|
1757
|
+
var s = step && step.state;
|
|
1758
|
+
return STEP_STATES[s] ? s : 'ok';
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// How a step is found again. The run gives a stable id to anything it will
|
|
1762
|
+
// announce twice — once as it starts, once as it settles — and without one
|
|
1763
|
+
// the words themselves have to do it.
|
|
1764
|
+
function idOf(step) {
|
|
1765
|
+
var key = step && step.key != null ? String(step.key).trim() : '';
|
|
1766
|
+
return key ? 'k:' + key : 'l:' + labelOf(step);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Guards only. A guard is a promise about behaviour, and the promises are
|
|
1770
|
+
// the claims it makes — 'the sidebar column is gone' — with actions in
|
|
1771
|
+
// between to get the app into position. The claims are what the guard is
|
|
1772
|
+
// for, so they are the ones that carry the weight.
|
|
1773
|
+
var CLAIM_KEY = /^(expect|claim|assert|expected)([^a-z0-9]|$)/i;
|
|
1774
|
+
var ACT_KEY = /^(do|did|act|action|step|fresh|run|ran|open|click|type|press|hover|scroll|wait|read|note)([^a-z0-9]|$)/i;
|
|
1775
|
+
// A step that had to be tried twice is the same step, and its id says so.
|
|
1776
|
+
var RETRY = /^try[0-9]+[^a-z0-9]/i;
|
|
1777
|
+
var ACT_WORD = /^(opened|clicked|typed|pressed|hovered|scrolled|waited|went|ran|read|reached|took|started|closed|filled|chose|picked|dragged|reloaded|moved|set|gave)([^a-z0-9]|$)/i;
|
|
1778
|
+
|
|
1779
|
+
function weightOf(kind, step) {
|
|
1780
|
+
if (kind !== 'guard') return '';
|
|
1781
|
+
var key = step && step.key != null ? String(step.key).trim() : '';
|
|
1782
|
+
if (RETRY.test(key)) key = key.replace(RETRY, '');
|
|
1783
|
+
if (CLAIM_KEY.test(key)) return 'stepclaim';
|
|
1784
|
+
if (ACT_KEY.test(key)) return 'stepact';
|
|
1785
|
+
return ACT_WORD.test(labelOf(step)) ? 'stepact' : 'stepclaim';
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/** One line, built once and then only ever updated in place. */
|
|
1789
|
+
function makeLine() {
|
|
1790
|
+
var li = document.createElement('li');
|
|
1791
|
+
li.className = 'step';
|
|
1792
|
+
var no = document.createElement('span');
|
|
1793
|
+
no.className = 'stepno mono';
|
|
1794
|
+
var mark = document.createElement('span');
|
|
1795
|
+
mark.className = 'stepmark';
|
|
1796
|
+
var body = document.createElement('span');
|
|
1797
|
+
body.className = 'stepbody';
|
|
1798
|
+
var what = document.createElement('span');
|
|
1799
|
+
what.className = 'stepwhat';
|
|
1800
|
+
var num = document.createElement('span');
|
|
1801
|
+
num.className = 'stepnum mono';
|
|
1802
|
+
num.hidden = true;
|
|
1803
|
+
body.appendChild(what);
|
|
1804
|
+
body.appendChild(num);
|
|
1805
|
+
li.appendChild(no);
|
|
1806
|
+
li.appendChild(mark);
|
|
1807
|
+
li.appendChild(body);
|
|
1808
|
+
li.__no = no; li.__mark = mark; li.__what = what; li.__num = num;
|
|
1809
|
+
li.__state = ''; li.__weight = '';
|
|
1810
|
+
return li;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function fillLine(li, step, n, kind, fresh) {
|
|
1814
|
+
var state = stateOf(step);
|
|
1815
|
+
var weight = weightOf(kind, step);
|
|
1816
|
+
var said = labelOf(step);
|
|
1817
|
+
var behind = detailOf(step);
|
|
1818
|
+
|
|
1819
|
+
if (li.__no.textContent !== String(n)) li.__no.textContent = String(n);
|
|
1820
|
+
if (li.__state !== state) {
|
|
1821
|
+
li.__state = state;
|
|
1822
|
+
li.__mark.innerHTML = MARKS[state] || MARKS.ok;
|
|
1823
|
+
}
|
|
1824
|
+
li.__weight = weight;
|
|
1825
|
+
li.className = 'step ' + state + (weight ? ' ' + weight : '') + (fresh ? ' arrive' : '');
|
|
1826
|
+
if (li.__what.textContent !== said) li.__what.textContent = said;
|
|
1827
|
+
// The number behind a step is shown the instant the step has one — a line
|
|
1828
|
+
// that has just gone amber or red is the one moment somebody needs it.
|
|
1829
|
+
if (li.__num.textContent !== behind) li.__num.textContent = behind;
|
|
1830
|
+
li.__num.hidden = !behind;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* Draw a list of steps into a list element, reconciling by id.
|
|
1835
|
+
*
|
|
1836
|
+
* Lines already on the page are found and updated; new ones settle in at
|
|
1837
|
+
* their place; anything the authoritative list no longer mentions goes. So
|
|
1838
|
+
* the same function serves a step arriving on its own and the whole array
|
|
1839
|
+
* that comes with the verdict, and the second one changes nothing a watcher
|
|
1840
|
+
* already had right.
|
|
1841
|
+
*
|
|
1842
|
+
* @param {any} ol
|
|
1843
|
+
* @param {any} checks
|
|
1844
|
+
* @param {string} kind
|
|
1845
|
+
* @returns {number} How many lines are shown.
|
|
1846
|
+
*/
|
|
1847
|
+
function renderWork(ol, checks, kind) {
|
|
1848
|
+
var steps = Array.isArray(checks) ? checks : [];
|
|
1849
|
+
var lines = ol.__lines || (ol.__lines = Object.create(null));
|
|
1850
|
+
var seen = Object.create(null);
|
|
1851
|
+
var n = 0;
|
|
1852
|
+
for (var i = 0; i < steps.length; i++) {
|
|
1853
|
+
var step = steps[i];
|
|
1854
|
+
if (!step || typeof step !== 'object') continue;
|
|
1855
|
+
if (!labelOf(step)) continue;
|
|
1856
|
+
n++;
|
|
1857
|
+
var id = idOf(step);
|
|
1858
|
+
// Two steps that answer to the same name in one list still get a line
|
|
1859
|
+
// each, rather than one of them quietly replacing the other.
|
|
1860
|
+
if (seen[id]) id = id + '#' + n;
|
|
1861
|
+
seen[id] = true;
|
|
1862
|
+
|
|
1863
|
+
var li = lines[id];
|
|
1864
|
+
var fresh = false;
|
|
1865
|
+
if (!li) { li = makeLine(); lines[id] = li; fresh = !CALM; }
|
|
1866
|
+
fillLine(li, step, n, kind, fresh);
|
|
1867
|
+
if (fresh) forget(li);
|
|
1868
|
+
var at = ol.children[n - 1];
|
|
1869
|
+
if (at !== li) ol.insertBefore(li, at || null);
|
|
1870
|
+
}
|
|
1871
|
+
for (var gone in lines) {
|
|
1872
|
+
if (seen[gone]) continue;
|
|
1873
|
+
if (lines[gone].parentNode === ol) ol.removeChild(lines[gone]);
|
|
1874
|
+
delete lines[gone];
|
|
1875
|
+
}
|
|
1876
|
+
return n;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// The arriving animation is worn once and then taken off, so a line that is
|
|
1880
|
+
// later moved or updated never plays it again.
|
|
1881
|
+
function forget(li) {
|
|
1882
|
+
setTimeout(function () { li.classList.remove('arrive'); }, 420);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function hasWork(entry) {
|
|
1886
|
+
return !!(entry && Array.isArray(entry.checks) && entry.checks.length);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/** Is this sentence already one of the lines of the working? */
|
|
1890
|
+
function inWork(entry, said) {
|
|
1891
|
+
var want = String(said == null ? '' : said).trim();
|
|
1892
|
+
if (!want || !hasWork(entry)) return false;
|
|
1893
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
1894
|
+
if (labelOf(entry.checks[i]) === want) return true;
|
|
1895
|
+
}
|
|
1896
|
+
return false;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/** This row's own copy of the list, for under the row. Built once. */
|
|
1900
|
+
function detailWork(entry) {
|
|
1901
|
+
if (!entry.workOl) {
|
|
1902
|
+
var label = document.createElement('p');
|
|
1903
|
+
label.className = 'worklabel';
|
|
1904
|
+
var title = document.createElement('span');
|
|
1905
|
+
title.className = 'worktitle';
|
|
1906
|
+
label.appendChild(title);
|
|
1907
|
+
entry.workLabel = label;
|
|
1908
|
+
entry.workTitle = title;
|
|
1909
|
+
entry.workOl = document.createElement('ol');
|
|
1910
|
+
entry.workOl.className = 'work';
|
|
1911
|
+
}
|
|
1912
|
+
entry.workTitle.textContent = entry.running ? 'What is being checked' : 'What was done';
|
|
1913
|
+
return entry.workOl;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// -------------------------------------------------------------------------
|
|
1917
|
+
// The working on the glass: whichever check the panel is showing.
|
|
1918
|
+
// -------------------------------------------------------------------------
|
|
1919
|
+
|
|
1920
|
+
var workOwner = null;
|
|
1921
|
+
// Whether this run says anything as it goes. An older Stays Fixed only ever
|
|
1922
|
+
// hands over the whole list at the end, and a heading promising that things
|
|
1923
|
+
// will appear as they are checked would be a promise nobody kept.
|
|
1924
|
+
var sawSteps = false;
|
|
1925
|
+
|
|
1926
|
+
// Built once, into the section that already sits at the top of the scrolling
|
|
1927
|
+
// column. It has to stay there: put inside the picture's own box it steals
|
|
1928
|
+
// the picture's height and squashes the screenshot to a sliver.
|
|
1929
|
+
var workShell = (function () {
|
|
1930
|
+
var label = document.createElement('p');
|
|
1931
|
+
label.className = 'worklabel';
|
|
1932
|
+
var title = document.createElement('span');
|
|
1933
|
+
title.className = 'worktitle';
|
|
1934
|
+
var who = document.createElement('span');
|
|
1935
|
+
who.className = 'workwho mono';
|
|
1936
|
+
label.appendChild(title);
|
|
1937
|
+
label.appendChild(who);
|
|
1938
|
+
var waiting = document.createElement('p');
|
|
1939
|
+
waiting.className = 'workwait';
|
|
1940
|
+
waiting.hidden = true;
|
|
1941
|
+
var list = document.createElement('ol');
|
|
1942
|
+
list.className = 'work';
|
|
1943
|
+
ui.work.appendChild(label);
|
|
1944
|
+
ui.work.appendChild(waiting);
|
|
1945
|
+
ui.work.appendChild(list);
|
|
1946
|
+
return { title: title, who: who, waiting: waiting, list: list };
|
|
1947
|
+
})();
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Bring one check's working onto the glass.
|
|
1951
|
+
*
|
|
1952
|
+
* A bring is for a check that has just STARTED: the list is about to tick off
|
|
1953
|
+
* and it is no use doing that below the fold, so the column goes back to the
|
|
1954
|
+
* top where the working lives — unless the person has taken hold of the list,
|
|
1955
|
+
* in which case nothing moves under them, like everywhere else on this page.
|
|
1956
|
+
* A click never brings: somebody who reached for a row is already looking at
|
|
1957
|
+
* it, and the row opening under their finger is the answer.
|
|
1958
|
+
*/
|
|
1959
|
+
function openWork(entry, bring) {
|
|
1960
|
+
if (!entry) return;
|
|
1961
|
+
if (workOwner && workOwner !== entry) workOwner.root.classList.remove('working');
|
|
1962
|
+
workOwner = entry;
|
|
1963
|
+
entry.root.classList.add('working');
|
|
1964
|
+
syncWork(entry);
|
|
1965
|
+
if (bring && following && ui.scroll.scrollTop > 0) {
|
|
1966
|
+
ui.scroll.scrollTo({ top: 0, behavior: CALM ? 'auto' : 'smooth' });
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/** Redraw wherever this check's working is on show. */
|
|
1971
|
+
function syncWork(entry) {
|
|
1972
|
+
if (!entry) return;
|
|
1973
|
+
if (workOwner === entry) {
|
|
1974
|
+
var n = renderWork(workShell.list, entry.checks, entry.kind);
|
|
1975
|
+
workShell.title.textContent = entry.running ? 'What is being checked' : 'What was done';
|
|
1976
|
+
workShell.who.textContent = entry.name || '';
|
|
1977
|
+
workShell.waiting.textContent = entry.running
|
|
1978
|
+
? 'Each thing appears here as it is checked.'
|
|
1979
|
+
: 'Nothing was written down for this one.';
|
|
1980
|
+
workShell.waiting.hidden = n > 0 || !sawSteps;
|
|
1981
|
+
workShell.list.hidden = n === 0;
|
|
1982
|
+
// Nothing to show and nothing promised: the section stays out of the way.
|
|
1983
|
+
ui.work.hidden = n === 0 && !sawSteps;
|
|
1984
|
+
}
|
|
1985
|
+
// Under the row. Cheap once the list is on the page; the first step of a
|
|
1986
|
+
// check has to go the long way round, because that is what turns a row
|
|
1987
|
+
// with nothing under it into one worth opening.
|
|
1988
|
+
if (entry.workOl && entry.workOl.parentNode) {
|
|
1989
|
+
detailWork(entry);
|
|
1990
|
+
renderWork(entry.workOl, entry.checks, entry.kind);
|
|
1991
|
+
} else if (hasWork(entry)) {
|
|
1992
|
+
redraw(entry);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* One step, as it happens. Announced as running, found again by its id and
|
|
1998
|
+
* settled when it is done.
|
|
1999
|
+
*/
|
|
2000
|
+
function applyStep(entry, step) {
|
|
2001
|
+
if (!entry || !step || typeof step !== 'object') return;
|
|
2002
|
+
if (!labelOf(step)) return;
|
|
2003
|
+
if (!Array.isArray(entry.checks)) entry.checks = [];
|
|
2004
|
+
var id = idOf(step);
|
|
2005
|
+
var kept = {
|
|
2006
|
+
label: labelOf(step),
|
|
2007
|
+
detail: detailOf(step),
|
|
2008
|
+
state: stateOf(step),
|
|
2009
|
+
key: step.key == null ? '' : String(step.key)
|
|
2010
|
+
};
|
|
2011
|
+
var found = -1;
|
|
2012
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
2013
|
+
if (idOf(entry.checks[i]) === id) { found = i; break; }
|
|
2014
|
+
}
|
|
2015
|
+
if (found >= 0) entry.checks[found] = kept;
|
|
2016
|
+
else entry.checks.push(kept);
|
|
2017
|
+
syncWork(entry);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Take the authoritative list that arrives with a verdict.
|
|
2022
|
+
*
|
|
2023
|
+
* Steps keep the id they were announced with wherever the final list does not
|
|
2024
|
+
* carry one, so a line a watcher has already been ticking is found again and
|
|
2025
|
+
* updated rather than thrown away and drawn a second time. That is what makes
|
|
2026
|
+
* the moment a check finishes look like nothing happened at all.
|
|
2027
|
+
*/
|
|
2028
|
+
function adopt(entry, checks) {
|
|
2029
|
+
var live = Array.isArray(entry.checks) ? entry.checks : [];
|
|
2030
|
+
var known = Object.create(null);
|
|
2031
|
+
var i;
|
|
2032
|
+
for (i = 0; i < live.length; i++) {
|
|
2033
|
+
if (live[i] && live[i].key) known[labelOf(live[i])] = String(live[i].key);
|
|
2034
|
+
}
|
|
2035
|
+
var out = [];
|
|
2036
|
+
for (i = 0; i < checks.length; i++) {
|
|
2037
|
+
var step = checks[i];
|
|
2038
|
+
if (!step || typeof step !== 'object') continue;
|
|
2039
|
+
var said = labelOf(step);
|
|
2040
|
+
if (!said) continue;
|
|
2041
|
+
var key = step.key == null ? '' : String(step.key);
|
|
2042
|
+
out.push({
|
|
2043
|
+
label: said,
|
|
2044
|
+
detail: detailOf(step),
|
|
2045
|
+
state: stateOf(step),
|
|
2046
|
+
key: key || known[said] || ''
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
entry.checks = out;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/**
|
|
2053
|
+
* A check has stopped. Anything still marked as happening is not happening
|
|
2054
|
+
* any more, and a mark that keeps breathing next to a finished result is a
|
|
2055
|
+
* lie — so those lines settle to a plain, colourless end.
|
|
2056
|
+
*/
|
|
2057
|
+
function hush(entry) {
|
|
2058
|
+
if (!entry || !Array.isArray(entry.checks)) return;
|
|
2059
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
2060
|
+
var step = entry.checks[i];
|
|
2061
|
+
if (stateOf(step) !== 'running') continue;
|
|
2062
|
+
entry.checks[i] = { label: labelOf(step), detail: detailOf(step), state: 'ended', key: step.key || '' };
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* The claim a guard died on, as the last line of its working.
|
|
2068
|
+
*
|
|
2069
|
+
* A guard that failed has to show every claim that held before the one that
|
|
2070
|
+
* did not, and this makes sure the one that did not is there even when the
|
|
2071
|
+
* run only reported it in the verdict.
|
|
2072
|
+
*/
|
|
2073
|
+
function markFailedClaim(entry, claim) {
|
|
2074
|
+
var said = String(claim == null ? '' : claim).trim();
|
|
2075
|
+
if (!said) return;
|
|
2076
|
+
if (!Array.isArray(entry.checks)) entry.checks = [];
|
|
2077
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
2078
|
+
if (labelOf(entry.checks[i]) !== said) continue;
|
|
2079
|
+
entry.checks[i] = {
|
|
2080
|
+
label: said, detail: detailOf(entry.checks[i]), state: 'bad', key: entry.checks[i].key || ''
|
|
2081
|
+
};
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
entry.checks.push({ label: said, detail: '', state: 'bad', key: 'claim:failed' });
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/** Put a check back on the glass: its pictures if it has any, its working always. */
|
|
1479
2088
|
function recall(entry) {
|
|
1480
2089
|
var p = entry.pics;
|
|
1481
|
-
if (
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2090
|
+
if (p) {
|
|
2091
|
+
var showed = false;
|
|
2092
|
+
if (p.status === 'changed') showed = comparePictures(p);
|
|
2093
|
+
if (!showed && nowOf(p)) {
|
|
2094
|
+
singlePicture(p, entry.name);
|
|
2095
|
+
showed = true;
|
|
2096
|
+
}
|
|
2097
|
+
if (showed) {
|
|
2098
|
+
nameTheShot(entry.name);
|
|
2099
|
+
sayOutcome(entry.tone, outcomeShort(p));
|
|
2100
|
+
markShowing(entry);
|
|
2101
|
+
}
|
|
1487
2102
|
}
|
|
1488
|
-
|
|
1489
|
-
nameTheShot(entry.name);
|
|
1490
|
-
sayOutcome(entry.tone, outcomeShort(p));
|
|
1491
|
-
markShowing(entry);
|
|
2103
|
+
openWork(entry);
|
|
1492
2104
|
}
|
|
1493
2105
|
|
|
1494
2106
|
function setOpen(entry, open) {
|
|
@@ -1633,7 +2245,16 @@ const SCRIPT = `
|
|
|
1633
2245
|
|
|
1634
2246
|
var runningItem = null;
|
|
1635
2247
|
function markRunning(entry) {
|
|
2248
|
+
if (runningItem && runningItem !== entry) {
|
|
2249
|
+
runningItem.running = false;
|
|
2250
|
+
hush(runningItem);
|
|
2251
|
+
syncWork(runningItem);
|
|
2252
|
+
}
|
|
1636
2253
|
runningItem = entry;
|
|
2254
|
+
entry.running = true;
|
|
2255
|
+
// A check that is starting again starts with an empty list, so nothing is
|
|
2256
|
+
// left over from the last time it ran.
|
|
2257
|
+
entry.checks = [];
|
|
1637
2258
|
setTone(entry, 'running');
|
|
1638
2259
|
if (following) entry.root.scrollIntoView({ block: 'nearest', behavior: CALM ? 'auto' : 'smooth' });
|
|
1639
2260
|
}
|
|
@@ -2306,11 +2927,26 @@ const SCRIPT = `
|
|
|
2306
2927
|
// photographed yet, so renaming the frame now would put this screen's
|
|
2307
2928
|
// name under the last screen's picture — a caption that lies for a
|
|
2308
2929
|
// second is worse than one that is a second behind.
|
|
2309
|
-
|
|
2930
|
+
var starting = ensureItem('picture', String(ev.name || ''), ev.describe);
|
|
2931
|
+
markRunning(starting);
|
|
2932
|
+
// The working area clears and takes this screen's name, so the list that
|
|
2933
|
+
// ticks off from here is unmistakably about the screen being checked now.
|
|
2934
|
+
openWork(starting, true);
|
|
2310
2935
|
scanning(true);
|
|
2311
2936
|
return;
|
|
2312
2937
|
}
|
|
2313
2938
|
|
|
2939
|
+
// One thing, the moment it happens. This is the whole point of the panel:
|
|
2940
|
+
// the names of what is being checked right now, each taking its mark as it
|
|
2941
|
+
// settles, instead of a table that lands when it is already over.
|
|
2942
|
+
if (type === 'screen:step' || type === 'guard:step') {
|
|
2943
|
+
sawSteps = true;
|
|
2944
|
+
var stepKind = type === 'guard:step' ? 'guard' : 'picture';
|
|
2945
|
+
var stepping = ensureItem(stepKind, String(ev.name || ''), ev.describe);
|
|
2946
|
+
applyStep(stepping, ev.step);
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2314
2950
|
if (type === 'screen:shot') {
|
|
2315
2951
|
scanning(false);
|
|
2316
2952
|
var shotRow = ev.name ? ensureItem('picture', String(ev.name), ev.describe) : null;
|
|
@@ -2348,6 +2984,9 @@ const SCRIPT = `
|
|
|
2348
2984
|
// in the list, because the picture is where the person is looking and
|
|
2349
2985
|
// that line is what tells them there is something to decide.
|
|
2350
2986
|
sayOutcome(tone, outcomeShort(ev));
|
|
2987
|
+
// And what was actually done to it, in the list beside the picture,
|
|
2988
|
+
// while the person is looking at it.
|
|
2989
|
+
openWork(done);
|
|
2351
2990
|
}
|
|
2352
2991
|
tint(worstTone());
|
|
2353
2992
|
return;
|
|
@@ -2360,7 +2999,11 @@ const SCRIPT = `
|
|
|
2360
2999
|
var guards = ev.total || expected.guard || countOf(plan.guards);
|
|
2361
3000
|
setState(guards ? 'running ' + commas(guards) + ' ' + plural(guards, 'guard', 'guards') : 'running the guards');
|
|
2362
3001
|
}
|
|
2363
|
-
|
|
3002
|
+
var guarding = ensureItem('guard', String(ev.name || ''), ev.describe);
|
|
3003
|
+
markRunning(guarding);
|
|
3004
|
+
// Guards have working too: the claims this one makes, ticking off as
|
|
3005
|
+
// each is checked. Shown the same way a screen's steps are.
|
|
3006
|
+
openWork(guarding, true);
|
|
2364
3007
|
return;
|
|
2365
3008
|
}
|
|
2366
3009
|
|
|
@@ -2371,6 +3014,12 @@ const SCRIPT = `
|
|
|
2371
3014
|
}
|
|
2372
3015
|
|
|
2373
3016
|
if (type === 'run:done') {
|
|
3017
|
+
// Nothing is happening any more, so nothing may still say it is.
|
|
3018
|
+
for (var q = 0; q < order.length; q++) {
|
|
3019
|
+
order[q].running = false;
|
|
3020
|
+
hush(order[q]);
|
|
3021
|
+
syncWork(order[q]);
|
|
3022
|
+
}
|
|
2374
3023
|
runningItem = null;
|
|
2375
3024
|
ui.follow.hidden = true;
|
|
2376
3025
|
scanning(false);
|
|
@@ -2418,6 +3067,16 @@ const SCRIPT = `
|
|
|
2418
3067
|
// picture that is already decoded and the swap never flashes.
|
|
2419
3068
|
preloadAll(entry.pics);
|
|
2420
3069
|
}
|
|
3070
|
+
// What was actually done to this check. The list that comes with the
|
|
3071
|
+
// verdict is the one that is right — a watcher that opened halfway through,
|
|
3072
|
+
// or missed an event, ends up correct here whatever it saw on the way.
|
|
3073
|
+
entry.running = false;
|
|
3074
|
+
if (Array.isArray(ev.checks)) adopt(entry, ev.checks);
|
|
3075
|
+
// Anything the run left mid-flight stops looking like it is still going.
|
|
3076
|
+
hush(entry);
|
|
3077
|
+
// And a guard that broke shows the claim it broke on, under every claim
|
|
3078
|
+
// that held before it.
|
|
3079
|
+
if (kind === 'guard' && ev.status === 'failed') markFailedClaim(entry, ev.failedAt);
|
|
2421
3080
|
entry.outText = outcomeText(kind, ev);
|
|
2422
3081
|
if (entry.verdict) entry.verdict.textContent = shortOutcome(kind, ev);
|
|
2423
3082
|
entry.failedAt = (kind === 'guard' && ev.status === 'failed' && ev.failedAt) ? ev.failedAt : '';
|
|
@@ -2425,6 +3084,7 @@ const SCRIPT = `
|
|
|
2425
3084
|
if (typeof ev.durationMs === 'number') tweenTo(entry.time, ev.durationMs, fmt);
|
|
2426
3085
|
entry.tone = tone;
|
|
2427
3086
|
redraw(entry);
|
|
3087
|
+
syncWork(entry);
|
|
2428
3088
|
setTone(entry, tone);
|
|
2429
3089
|
// Only the checks that want a person open themselves. Everything else stays
|
|
2430
3090
|
// one line, which is what keeps a clean run quiet. A row that has just
|
|
@@ -2487,7 +3147,10 @@ const SCRIPT = `
|
|
|
2487
3147
|
handle(ev);
|
|
2488
3148
|
} catch (err) {
|
|
2489
3149
|
// A watch window must never be the reason a run looks broken. Whatever
|
|
2490
|
-
// this event was, the next one still has to land.
|
|
3150
|
+
// this event was, the next one still has to land. It is still said out
|
|
3151
|
+
// loud where anyone would look for it — a panel that swallows its own
|
|
3152
|
+
// mistakes in silence is a panel nobody can mend.
|
|
3153
|
+
if (window.console && console.error) console.error('stays fixed: ' + (err && err.message ? err.message : err));
|
|
2491
3154
|
}
|
|
2492
3155
|
};
|
|
2493
3156
|
|
|
@@ -2496,6 +3159,11 @@ const SCRIPT = `
|
|
|
2496
3159
|
window.__staysfixed_detach = function () {
|
|
2497
3160
|
stopClock();
|
|
2498
3161
|
scanning(false);
|
|
3162
|
+
if (runningItem) {
|
|
3163
|
+
runningItem.running = false;
|
|
3164
|
+
hush(runningItem);
|
|
3165
|
+
syncWork(runningItem);
|
|
3166
|
+
}
|
|
2499
3167
|
runningItem = null;
|
|
2500
3168
|
};
|
|
2501
3169
|
|