staysfixed 0.2.3 → 0.3.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.
- package/package.json +1 -1
- package/src/core/events.js +174 -18
- package/src/guard/api.js +168 -12
- package/src/guard/run.js +104 -6
- package/src/picture/capture.js +134 -14
- package/src/picture/run.js +67 -1
- package/src/types.js +9 -2
- package/src/watch/panel.js +806 -255
package/src/watch/panel.js
CHANGED
|
@@ -304,57 +304,105 @@ export function panelHtml(plan = {}) {
|
|
|
304
304
|
|
|
305
305
|
const STYLE = `
|
|
306
306
|
/* --------------------------------------------------------------------------
|
|
307
|
-
Tokens.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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.
|
|
319
332
|
-------------------------------------------------------------------------- */
|
|
320
333
|
:root {
|
|
321
334
|
color-scheme: dark;
|
|
322
335
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
--
|
|
327
|
-
--
|
|
328
|
-
--
|
|
329
|
-
|
|
330
|
-
--
|
|
331
|
-
--
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
--
|
|
335
|
-
--
|
|
336
|
-
--
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
--
|
|
341
|
-
--
|
|
342
|
-
--
|
|
343
|
-
--
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
336
|
+
/* Neutral black. No warm cast, no cool cast — every tint I tried made the
|
|
337
|
+
panel look like it was lit by something that was not in the room. The
|
|
338
|
+
character comes from the three state colours and nothing else. */
|
|
339
|
+
--ground: #101010;
|
|
340
|
+
--lift: #191919;
|
|
341
|
+
--card: rgba(255, 255, 255, 0.035);
|
|
342
|
+
--card-hover: rgba(255, 255, 255, 0.06);
|
|
343
|
+
--well: #0a0a0a;
|
|
344
|
+
--glass: rgba(16, 16, 16, 0.9);
|
|
345
|
+
|
|
346
|
+
--ink: #ededed;
|
|
347
|
+
--soft: #b2b2b2;
|
|
348
|
+
--faint: #8d8d8d;
|
|
349
|
+
--faintest: #6d6d6d;
|
|
350
|
+
/* Not sure yet: it wobbled, it needed a second go, or the answer is soft. */
|
|
351
|
+
--doubt: #e8b85c;
|
|
352
|
+
|
|
353
|
+
--line: rgba(255, 255, 255, 0.055);
|
|
354
|
+
--line-firm: rgba(255, 255, 255, 0.17);
|
|
355
|
+
--sheen: rgba(255, 255, 255, 0.03);
|
|
356
|
+
--shadow: rgba(0, 0, 0, 0.5);
|
|
357
|
+
|
|
358
|
+
/* Activity, not state. Something is happening — the shutter, a running dot,
|
|
359
|
+
a live segment of the meter. Deliberately colourless: "happening" is not
|
|
360
|
+
news, and the moment it has news it has a colour instead. */
|
|
361
|
+
/* The brand mark, the thing that is running, and the check that wants a person
|
|
362
|
+
are all this one colour — so on a page that is otherwise grey, this means
|
|
363
|
+
"you". A clear sky blue: bright enough to find at a glance, cool enough not
|
|
364
|
+
to be mistaken for the amber that means something moved. */
|
|
365
|
+
--accent: #4fb3f0;
|
|
366
|
+
|
|
367
|
+
/* Held is green, and visibly so.
|
|
368
|
+
It was a grey at first, on the argument that a check which passed has
|
|
369
|
+
nothing to say. True of a whole run — the verdict line and the meter are
|
|
370
|
+
quiet when all is well — but wrong of the individual check: a person
|
|
371
|
+
scanning a list of thirty things wants to SEE that twenty-nine of them
|
|
372
|
+
held, not infer it from an absence. Green passed, red broke, amber not
|
|
373
|
+
sure. Grey is only for what never ran. */
|
|
374
|
+
--held: #25d366;
|
|
375
|
+
|
|
376
|
+
/* The three that have something to say. Pure, because they are alone. */
|
|
377
|
+
--broke: #ff4438;
|
|
378
|
+
/* Waiting for a person is the brand colour itself. */
|
|
379
|
+
--wait: #4fb3f0;
|
|
380
|
+
--moved: #ffc24d;
|
|
381
|
+
|
|
382
|
+
/* Nobody ran it. Barely there on purpose. */
|
|
383
|
+
--resting: rgba(255, 255, 255, 0.09);
|
|
384
|
+
--skip: var(--faintest);
|
|
385
|
+
--pending: var(--faintest);
|
|
386
|
+
--running: var(--accent);
|
|
387
|
+
--waiting: var(--wait);
|
|
388
|
+
|
|
389
|
+
/* Small radii, flat surfaces. The app this sits beside uses 8px and almost no
|
|
390
|
+
shadow; a panel with 20px corners and floating cards read as a different
|
|
391
|
+
product bolted on to the side of it. */
|
|
392
|
+
--radius: 10px;
|
|
393
|
+
--radius-sm: 8px;
|
|
394
|
+
--radius-xs: 6px;
|
|
395
|
+
|
|
396
|
+
--pad: 16px;
|
|
350
397
|
--tint: var(--accent);
|
|
351
398
|
|
|
352
399
|
/* The type scale. Nothing on this page is set at a size that is not here. */
|
|
353
400
|
--t-label: 10px;
|
|
354
401
|
--t-meta: 10.5px;
|
|
402
|
+
--t-small: 11px;
|
|
355
403
|
--t-body: 11.5px;
|
|
356
404
|
--t-name: 12px;
|
|
357
|
-
--t-lead:
|
|
405
|
+
--t-lead: 18px;
|
|
358
406
|
|
|
359
407
|
/* One curve. Everything that moves, moves on this. */
|
|
360
408
|
--ease: cubic-bezier(0.22, 0.72, 0.24, 1);
|
|
@@ -370,32 +418,41 @@ const STYLE = `
|
|
|
370
418
|
* panel came up pale on a dark desktop, which is exactly the thing it was asked not to
|
|
371
419
|
* do. The look is therefore stated on the html element rather than sniffed: dark unless
|
|
372
420
|
* somebody asks for light, and 'system' for anyone who does want the browser's opinion.
|
|
421
|
+
*
|
|
422
|
+
* The same rock, lit from the other side: a stone ground, cards a shade brighter
|
|
423
|
+
* than it rather than the other way round, and the three signal colours taken
|
|
424
|
+
* down until they hold on paper. The ladder inverts with the ground — the
|
|
425
|
+
* quietest state is now the lightest — and the steps stay wide enough to read
|
|
426
|
+
* with no colour vision.
|
|
373
427
|
*/
|
|
374
428
|
:root[data-theme='light'],
|
|
375
429
|
:root[data-theme='system'] {
|
|
376
430
|
color-scheme: light;
|
|
377
431
|
|
|
378
|
-
--ground: #
|
|
379
|
-
--lift: #
|
|
380
|
-
--card: rgba(255, 255, 255, 0.
|
|
381
|
-
--card-hover:
|
|
382
|
-
--well: #
|
|
383
|
-
--glass: rgba(
|
|
432
|
+
--ground: #d9dade;
|
|
433
|
+
--lift: #eef0f3;
|
|
434
|
+
--card: rgba(255, 255, 255, 0.6);
|
|
435
|
+
--card-hover: #ffffff;
|
|
436
|
+
--well: #c7c9cf;
|
|
437
|
+
--glass: rgba(232, 234, 238, 0.9);
|
|
384
438
|
|
|
385
|
-
--ink: #
|
|
386
|
-
--soft: #
|
|
387
|
-
--faint: #
|
|
439
|
+
--ink: #14161a;
|
|
440
|
+
--soft: #545a62;
|
|
441
|
+
--faint: #5e646c;
|
|
442
|
+
--faintest: #6f757d;
|
|
388
443
|
|
|
389
|
-
--line: rgba(
|
|
390
|
-
--line-firm: rgba(
|
|
391
|
-
--sheen: rgba(255, 255, 255, 0.
|
|
392
|
-
--shadow: rgba(
|
|
444
|
+
--line: rgba(20, 22, 26, 0.11);
|
|
445
|
+
--line-firm: rgba(20, 22, 26, 0.24);
|
|
446
|
+
--sheen: rgba(255, 255, 255, 0.9);
|
|
447
|
+
--shadow: rgba(26, 30, 38, 0.34);
|
|
393
448
|
|
|
394
|
-
--accent: #
|
|
395
|
-
--held: #
|
|
396
|
-
--
|
|
397
|
-
--
|
|
398
|
-
--
|
|
449
|
+
--accent: #474d56;
|
|
450
|
+
--held: #757b83;
|
|
451
|
+
--broke: #7e1105;
|
|
452
|
+
--wait: #0e5588;
|
|
453
|
+
--moved: #8a5f06;
|
|
454
|
+
|
|
455
|
+
--resting: rgba(20, 22, 26, 0.13);
|
|
399
456
|
}
|
|
400
457
|
|
|
401
458
|
* { box-sizing: border-box; }
|
|
@@ -419,14 +476,17 @@ img { display: block; }
|
|
|
419
476
|
button { font: inherit; color: inherit; }
|
|
420
477
|
.glyph { width: 16px; height: 16px; flex: 0 0 auto; }
|
|
421
478
|
|
|
422
|
-
/*
|
|
423
|
-
|
|
424
|
-
|
|
479
|
+
/* One light above, one shadow below. Nothing else.
|
|
480
|
+
There was a five-bloom mesh here with warm and coloured washes in it; on a
|
|
481
|
+
neutral black ground it read as a stain rather than as depth. What is left is
|
|
482
|
+
the least that stops a page of greys looking like flat paint, plus the tint of
|
|
483
|
+
the worst thing that has happened — which on a run where everything held is no
|
|
484
|
+
colour at all. */
|
|
425
485
|
.aura {
|
|
426
486
|
position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
|
427
487
|
background:
|
|
428
|
-
radial-gradient(
|
|
429
|
-
radial-gradient(
|
|
488
|
+
radial-gradient(126% 42% at 50% -12%, color-mix(in srgb, var(--tint) 12%, transparent), transparent 70%),
|
|
489
|
+
radial-gradient(128% 56% at 50% 116%, var(--shadow), transparent 62%);
|
|
430
490
|
transition: background 620ms var(--ease);
|
|
431
491
|
}
|
|
432
492
|
|
|
@@ -441,24 +501,29 @@ button { font: inherit; color: inherit; }
|
|
|
441
501
|
flex: 0 0 auto;
|
|
442
502
|
padding: 16px var(--pad) 15px;
|
|
443
503
|
background: var(--glass);
|
|
444
|
-
backdrop-filter: blur(22px) saturate(
|
|
445
|
-
-webkit-backdrop-filter: blur(22px) saturate(
|
|
504
|
+
backdrop-filter: blur(22px) saturate(120%);
|
|
505
|
+
-webkit-backdrop-filter: blur(22px) saturate(120%);
|
|
506
|
+
/* Separated from the list by a hairline and nothing else. */
|
|
507
|
+
box-shadow: 0 1px 0 var(--line);
|
|
446
508
|
}
|
|
447
509
|
.brand { display: flex; align-items: center; gap: 9px; }
|
|
510
|
+
/* The mark sits on a milled square of the same graphite, lit along its top
|
|
511
|
+
edge. It is the product's own face, so it is never one of the three colours
|
|
512
|
+
that mean something is wrong. */
|
|
448
513
|
.badge {
|
|
449
514
|
flex: 0 0 auto;
|
|
450
515
|
display: flex; align-items: center; justify-content: center;
|
|
451
516
|
width: 24px; height: 24px;
|
|
452
517
|
border-radius: 8px;
|
|
453
|
-
color: var(--
|
|
454
|
-
background:
|
|
455
|
-
box-shadow: inset 0 0 0 1px
|
|
518
|
+
color: var(--ink);
|
|
519
|
+
background: var(--lift);
|
|
520
|
+
box-shadow: inset 0 0 0 1px var(--line-firm);
|
|
456
521
|
}
|
|
457
522
|
.badge .glyph { width: 15px; height: 15px; }
|
|
458
523
|
.wordmark {
|
|
459
524
|
flex: 1 1 auto; min-width: 0;
|
|
460
525
|
font-size: var(--t-label); font-weight: 600;
|
|
461
|
-
letter-spacing: 0.
|
|
526
|
+
letter-spacing: 0.22em; text-transform: uppercase;
|
|
462
527
|
color: var(--soft);
|
|
463
528
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
464
529
|
}
|
|
@@ -471,22 +536,45 @@ button { font: inherit; color: inherit; }
|
|
|
471
536
|
white-space: nowrap; overflow: hidden;
|
|
472
537
|
}
|
|
473
538
|
.target .mono { color: var(--ink); flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
|
|
474
|
-
.target .sep { color: var(--
|
|
539
|
+
.target .sep { color: var(--faintest); flex: 0 0 auto; }
|
|
475
540
|
.target .app { color: var(--faint); flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; font-size: var(--t-body); }
|
|
476
541
|
|
|
477
542
|
/* The sentence. The largest text on the page after the picture, because it is
|
|
478
|
-
the one thing a person reads from four feet away.
|
|
543
|
+
the one thing a person reads from four feet away. It is white while the news
|
|
544
|
+
is good and takes a colour only when there is news. */
|
|
479
545
|
.state {
|
|
480
|
-
margin-top:
|
|
481
|
-
font-size: var(--t-lead); font-weight:
|
|
482
|
-
letter-spacing: -0.
|
|
546
|
+
margin-top: 10px;
|
|
547
|
+
font-size: var(--t-lead); font-weight: 600; line-height: 1.3;
|
|
548
|
+
letter-spacing: -0.014em;
|
|
483
549
|
overflow-wrap: anywhere;
|
|
484
550
|
transition: color var(--slow) var(--ease);
|
|
485
551
|
}
|
|
486
|
-
.state.held { color: var(--ink); }
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
552
|
+
.state.held, .state.moved, .state.broke, .state.wait { color: var(--ink); }
|
|
553
|
+
/* The sentence stays white; a signal sits in front of it. It is the same mark
|
|
554
|
+
the row it is about carries — a haloed disc for a screen that moved, a disc
|
|
555
|
+
in a ring for one waiting on a person, a diamond for a guard that broke — so
|
|
556
|
+
the top of the window and the list underneath speak one language. The words
|
|
557
|
+
are what you read from four feet away; the mark is what you see before you
|
|
558
|
+
have read anything at all. */
|
|
559
|
+
.state.moved::before, .state.wait::before, .state.broke::before {
|
|
560
|
+
content: ''; display: inline-block;
|
|
561
|
+
width: 10px; height: 10px; border-radius: 50%;
|
|
562
|
+
margin-right: 13px; vertical-align: 0.1em;
|
|
563
|
+
}
|
|
564
|
+
.state.moved::before {
|
|
565
|
+
background: var(--moved);
|
|
566
|
+
box-shadow: 0 0 0 5px color-mix(in srgb, var(--moved) 17%, transparent);
|
|
567
|
+
}
|
|
568
|
+
.state.wait::before {
|
|
569
|
+
background: var(--wait);
|
|
570
|
+
box-shadow:
|
|
571
|
+
0 0 0 2px color-mix(in srgb, var(--wait) 42%, transparent),
|
|
572
|
+
0 0 0 5px color-mix(in srgb, var(--wait) 12%, transparent);
|
|
573
|
+
}
|
|
574
|
+
.state.broke::before {
|
|
575
|
+
background: var(--broke); border-radius: 2px; transform: rotate(45deg);
|
|
576
|
+
box-shadow: 0 0 0 5px color-mix(in srgb, var(--broke) 17%, transparent);
|
|
577
|
+
}
|
|
490
578
|
/* The verdict arrives rather than appearing: it is the one sentence the whole
|
|
491
579
|
run was for, and a run that ends by swapping a word looks like a page that
|
|
492
580
|
was not paying attention. */
|
|
@@ -496,8 +584,8 @@ button { font: inherit; color: inherit; }
|
|
|
496
584
|
to { opacity: 1; transform: none; filter: blur(0); }
|
|
497
585
|
}
|
|
498
586
|
.what {
|
|
499
|
-
margin:
|
|
500
|
-
line-height: 1.
|
|
587
|
+
margin: 7px 0 0; font-size: var(--t-small); color: var(--faint);
|
|
588
|
+
line-height: 1.5; overflow-wrap: anywhere;
|
|
501
589
|
}
|
|
502
590
|
.rverdict {
|
|
503
591
|
flex: 0 0 auto; font-size: var(--t-small); color: var(--faint);
|
|
@@ -506,13 +594,15 @@ button { font: inherit; color: inherit; }
|
|
|
506
594
|
.item.held .rverdict { color: var(--faint); }
|
|
507
595
|
.item.moved .rverdict { color: var(--moved); }
|
|
508
596
|
.item.broke .rverdict { color: var(--broke); }
|
|
597
|
+
.item.wait .rverdict { color: var(--wait); }
|
|
509
598
|
.item.waiting .rverdict { color: var(--waiting); }
|
|
510
|
-
.item.pending .rverdict, .item.skip .rverdict { color: var(--faintest
|
|
511
|
-
.note { margin-top:
|
|
599
|
+
.item.pending .rverdict, .item.skip .rverdict { color: var(--faintest); }
|
|
600
|
+
.note { margin-top: 6px; font-size: var(--t-body); color: var(--faint); overflow-wrap: anywhere; }
|
|
512
601
|
|
|
513
602
|
/* One hairline, not a row of blocks. Each finished check adds its own slice of
|
|
514
603
|
colour to a single continuous line, so progress and outcome are the same
|
|
515
|
-
object and there is one fewer thing on the page.
|
|
604
|
+
object and there is one fewer thing on the page. A run where everything held
|
|
605
|
+
finishes as one unbroken grey rule — which is the point. */
|
|
516
606
|
.meter { display: flex; align-items: center; gap: 10px; margin-top: 15px; }
|
|
517
607
|
.track {
|
|
518
608
|
flex: 1 1 auto; min-width: 0;
|
|
@@ -529,8 +619,8 @@ button { font: inherit; color: inherit; }
|
|
|
529
619
|
.slice.held { background: var(--held); }
|
|
530
620
|
.slice.moved { background: var(--moved); }
|
|
531
621
|
.slice.broke { background: var(--broke); }
|
|
532
|
-
.slice.wait { background: var(--
|
|
533
|
-
.slice.skip { background: color-mix(in srgb, var(--ink)
|
|
622
|
+
.slice.wait { background: var(--wait); }
|
|
623
|
+
.slice.skip { background: color-mix(in srgb, var(--ink) 13%, transparent); }
|
|
534
624
|
.slice.running {
|
|
535
625
|
background: var(--accent);
|
|
536
626
|
animation: breathe 1.9s var(--ease) infinite;
|
|
@@ -538,7 +628,7 @@ button { font: inherit; color: inherit; }
|
|
|
538
628
|
/* A segment lights as it lands, once, and then it is just a colour. */
|
|
539
629
|
.slice.land { animation: land 520ms var(--ease) 1; }
|
|
540
630
|
@keyframes land { 0% { filter: brightness(2.1); } 100% { filter: none; } }
|
|
541
|
-
@keyframes breathe { 0%, 100% { opacity: 0.
|
|
631
|
+
@keyframes breathe { 0%, 100% { opacity: 0.34; } 50% { opacity: 1; } }
|
|
542
632
|
.counts {
|
|
543
633
|
flex: 0 1 auto; min-width: 0; max-width: 66%;
|
|
544
634
|
font-size: var(--t-meta); color: var(--faint); letter-spacing: 0.02em;
|
|
@@ -569,9 +659,7 @@ button { font: inherit; color: inherit; }
|
|
|
569
659
|
/* No border. The picture is the hero, so nothing is drawn around it that
|
|
570
660
|
could compete with it — only a soft floor shadow and the faintest rim to
|
|
571
661
|
keep a white screenshot from bleeding into a light ground. */
|
|
572
|
-
box-shadow:
|
|
573
|
-
0 0 0 1px var(--line),
|
|
574
|
-
0 26px 50px -30px var(--shadow);
|
|
662
|
+
box-shadow: 0 0 0 1px var(--line);
|
|
575
663
|
transition: box-shadow var(--slow) var(--ease);
|
|
576
664
|
}
|
|
577
665
|
.shot.empty { cursor: default; }
|
|
@@ -591,29 +679,27 @@ button { font: inherit; color: inherit; }
|
|
|
591
679
|
text-align: center; padding: 0 30px; max-width: 260px;
|
|
592
680
|
}
|
|
593
681
|
|
|
594
|
-
/* The shutter, made visible. One pass of
|
|
682
|
+
/* The shutter, made visible. One pass of plain light across the glass for
|
|
595
683
|
exactly as long as the app is really being photographed — it starts when the
|
|
596
|
-
screen starts and it is gone the instant the picture lands
|
|
597
|
-
|
|
684
|
+
screen starts and it is gone the instant the picture lands. It is light, not
|
|
685
|
+
colour: nothing has been found out yet, so there is nothing to say. */
|
|
598
686
|
.sweep {
|
|
599
687
|
position: absolute; inset: 0;
|
|
600
688
|
pointer-events: none; opacity: 0;
|
|
601
689
|
background:
|
|
602
690
|
linear-gradient(100deg,
|
|
603
691
|
transparent 4%,
|
|
604
|
-
color-mix(in srgb, var(--accent)
|
|
605
|
-
color-mix(in srgb, var(--accent)
|
|
606
|
-
color-mix(in srgb, var(--accent)
|
|
607
|
-
color-mix(in srgb, var(--accent)
|
|
608
|
-
color-mix(in srgb, var(--accent)
|
|
692
|
+
color-mix(in srgb, var(--accent) 8%, transparent) 30%,
|
|
693
|
+
color-mix(in srgb, var(--accent) 28%, transparent) 47%,
|
|
694
|
+
color-mix(in srgb, var(--accent) 72%, transparent) 50%,
|
|
695
|
+
color-mix(in srgb, var(--accent) 28%, transparent) 53%,
|
|
696
|
+
color-mix(in srgb, var(--accent) 8%, transparent) 70%,
|
|
609
697
|
transparent 96%);
|
|
610
698
|
transform: translateX(-100%);
|
|
611
699
|
}
|
|
612
700
|
.shot.scanning .sweep { opacity: 1; animation: sweep 1.6s var(--ease) infinite; }
|
|
613
701
|
.shot.scanning {
|
|
614
|
-
box-shadow:
|
|
615
|
-
0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent),
|
|
616
|
-
0 26px 50px -30px var(--shadow);
|
|
702
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 34%, transparent);
|
|
617
703
|
}
|
|
618
704
|
@keyframes sweep {
|
|
619
705
|
from { transform: translateX(-100%); }
|
|
@@ -625,8 +711,9 @@ button { font: inherit; color: inherit; }
|
|
|
625
711
|
position: absolute; right: 10px; bottom: 10px;
|
|
626
712
|
font-size: var(--t-label); letter-spacing: 0.02em;
|
|
627
713
|
color: var(--ink);
|
|
628
|
-
background: color-mix(in srgb, var(--ground)
|
|
714
|
+
background: color-mix(in srgb, var(--ground) 76%, transparent);
|
|
629
715
|
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
|
716
|
+
box-shadow: inset 0 0 0 1px var(--line-firm);
|
|
630
717
|
border-radius: 999px; padding: 4px 10px;
|
|
631
718
|
opacity: 0; transform: translateY(4px);
|
|
632
719
|
transition: opacity var(--quick) var(--ease), transform var(--quick) var(--ease);
|
|
@@ -643,11 +730,12 @@ button { font: inherit; color: inherit; }
|
|
|
643
730
|
.shotout { flex: 1 1 auto; min-width: 0; font-size: var(--t-body); color: var(--soft); text-align: right; overflow-wrap: anywhere; }
|
|
644
731
|
.shotout.moved { color: var(--moved); }
|
|
645
732
|
.shotout.broke { color: var(--broke); }
|
|
646
|
-
.shotout.wait { color: var(--
|
|
733
|
+
.shotout.wait { color: var(--wait); }
|
|
647
734
|
|
|
648
735
|
/* Approved / now / difference. Three quiet words with a line that slides
|
|
649
736
|
between them — no pill, no box, because a screen that moved already has
|
|
650
|
-
enough asking for the eye.
|
|
737
|
+
enough asking for the eye. The line under the chosen word is the one place
|
|
738
|
+
the screen's own colour appears in the furniture. */
|
|
651
739
|
.switch { display: flex; gap: 2px; margin-top: 10px; }
|
|
652
740
|
.switch button {
|
|
653
741
|
position: relative;
|
|
@@ -661,7 +749,7 @@ button { font: inherit; color: inherit; }
|
|
|
661
749
|
}
|
|
662
750
|
.switch button::after {
|
|
663
751
|
content: ''; position: absolute; left: 50%; right: 50%; bottom: 0; height: 1.5px;
|
|
664
|
-
border-radius: 2px; background: var(--
|
|
752
|
+
border-radius: 2px; background: var(--ink);
|
|
665
753
|
transition: left var(--calm) var(--ease), right var(--calm) var(--ease), opacity var(--quick) var(--ease);
|
|
666
754
|
opacity: 0;
|
|
667
755
|
}
|
|
@@ -675,7 +763,7 @@ button { font: inherit; color: inherit; }
|
|
|
675
763
|
overflow-y: auto;
|
|
676
764
|
overscroll-behavior: contain;
|
|
677
765
|
scrollbar-width: thin;
|
|
678
|
-
scrollbar-color: color-mix(in srgb, var(--soft)
|
|
766
|
+
scrollbar-color: color-mix(in srgb, var(--soft) 20%, transparent) transparent;
|
|
679
767
|
padding: 18px 5px 14px 0;
|
|
680
768
|
/* The list slides away under the header rather than being cut off by a box. */
|
|
681
769
|
mask-image: linear-gradient(to bottom, transparent 0, #000 16px, #000 calc(100% - 12px), transparent 100%);
|
|
@@ -683,38 +771,42 @@ button { font: inherit; color: inherit; }
|
|
|
683
771
|
}
|
|
684
772
|
.scroll::-webkit-scrollbar { width: 10px; }
|
|
685
773
|
.scroll::-webkit-scrollbar-thumb {
|
|
686
|
-
background: color-mix(in srgb, var(--soft)
|
|
774
|
+
background: color-mix(in srgb, var(--soft) 20%, transparent);
|
|
687
775
|
border-radius: 8px; border: 3px solid transparent; background-clip: padding-box;
|
|
688
776
|
}
|
|
689
777
|
.scroll::-webkit-scrollbar-track { background: transparent; }
|
|
690
778
|
|
|
691
|
-
.group + .group { margin-top:
|
|
779
|
+
.group + .group { margin-top: 22px; }
|
|
692
780
|
.grouplabel {
|
|
693
781
|
display: flex; align-items: baseline; gap: 9px;
|
|
694
782
|
padding: 0 6px 9px 4px;
|
|
695
783
|
font-size: var(--t-label); font-weight: 600;
|
|
696
|
-
letter-spacing: 0.
|
|
784
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
697
785
|
color: var(--faint);
|
|
698
786
|
}
|
|
699
|
-
.grouplabel .mono { font-size: var(--t-label); letter-spacing: 0.
|
|
787
|
+
.grouplabel .mono { font-size: var(--t-label); letter-spacing: 0.06em; color: var(--faintest); }
|
|
700
788
|
|
|
789
|
+
/* A card, floating: a shade of graphite above the ground, lit along its top
|
|
790
|
+
edge, one hairline round it and a soft floor under it. Nothing but light
|
|
791
|
+
separates it from the page. */
|
|
701
792
|
.items {
|
|
702
793
|
border-radius: var(--radius);
|
|
703
794
|
background: var(--card);
|
|
704
795
|
overflow: hidden;
|
|
796
|
+
box-shadow: inset 0 0 0 1px var(--line);
|
|
705
797
|
}
|
|
706
798
|
.item + .item { box-shadow: inset 0 1px 0 var(--line); }
|
|
707
799
|
.item {
|
|
708
800
|
transition: background var(--calm) var(--ease);
|
|
709
801
|
}
|
|
710
|
-
.item.attention { background: color-mix(in srgb, var(--tone, var(--accent)) 5%, transparent); }
|
|
711
|
-
.item.running { background: color-mix(in srgb, var(--accent)
|
|
802
|
+
.item.attention { background: color-mix(in srgb, var(--tone, var(--accent)) 3.5%, transparent); }
|
|
803
|
+
.item.running { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
712
804
|
/* Something broke. It is said once, with weight, and then it sits still — a
|
|
713
805
|
panel that keeps flashing beside your work is a panel you turn off. */
|
|
714
806
|
.item.alarm { animation: alarm 900ms var(--ease) 1; }
|
|
715
807
|
@keyframes alarm {
|
|
716
|
-
0% { background: color-mix(in srgb, var(--tone, var(--broke))
|
|
717
|
-
100% { background: color-mix(in srgb, var(--tone, var(--broke)) 5%, transparent); }
|
|
808
|
+
0% { background: color-mix(in srgb, var(--tone, var(--broke)) 22%, transparent); }
|
|
809
|
+
100% { background: color-mix(in srgb, var(--tone, var(--broke)) 3.5%, transparent); }
|
|
718
810
|
}
|
|
719
811
|
|
|
720
812
|
/* Big, quiet, touch-sized rows. At rest a check is a dot, a name and a time —
|
|
@@ -731,8 +823,12 @@ button { font: inherit; color: inherit; }
|
|
|
731
823
|
.row:hover { background: var(--card-hover); }
|
|
732
824
|
.row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
733
825
|
.item.plain .row { cursor: default; }
|
|
734
|
-
.item.showing .row { box-shadow: inset 2px 0 0 var(--line-firm); }
|
|
735
|
-
.item.showing .rname { color: var(--ink); }
|
|
826
|
+
.item.showing .row, .item.working .row { box-shadow: inset 2px 0 0 var(--line-firm); }
|
|
827
|
+
.item.showing .rname, .item.working .rname { color: var(--ink); }
|
|
828
|
+
/* A check that wants a person is marked with a line down its edge in its own
|
|
829
|
+
colour, not a wash across it. A wash over graphite is a stain; a line is a
|
|
830
|
+
flag, and it reads at the same glance as the dot beside the name. */
|
|
831
|
+
.item.attention .row { box-shadow: inset 2px 0 0 var(--tone, var(--accent)); }
|
|
736
832
|
.item.plain .row:hover { background: transparent; }
|
|
737
833
|
/* Rows settle in rather than appearing, and they do it one after another, which
|
|
738
834
|
is what makes a list of eleven screens read as one thing arriving. */
|
|
@@ -742,23 +838,37 @@ button { font: inherit; color: inherit; }
|
|
|
742
838
|
to { opacity: 1; transform: none; }
|
|
743
839
|
}
|
|
744
840
|
|
|
841
|
+
/* The mark beside a name. Five states, five silhouettes, five rungs of a
|
|
842
|
+
lightness ladder — so it can be read with no colour vision, in a photocopy,
|
|
843
|
+
or out of the corner of an eye:
|
|
844
|
+
nobody ran it a hollow ring, barely there
|
|
845
|
+
it held a plain disc, grey, nothing around it
|
|
846
|
+
it is waiting a disc inside a crisp ring
|
|
847
|
+
it moved a disc in a soft wide halo
|
|
848
|
+
it broke a diamond in a soft wide halo
|
|
849
|
+
*/
|
|
745
850
|
.dot {
|
|
746
851
|
flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
|
|
747
852
|
background: var(--resting);
|
|
748
853
|
transition: background var(--calm) var(--ease), box-shadow var(--calm) var(--ease);
|
|
749
854
|
}
|
|
750
|
-
.item.held .dot {
|
|
751
|
-
.item.
|
|
752
|
-
.item.
|
|
753
|
-
.item.
|
|
754
|
-
.item.
|
|
855
|
+
.item.held .dot { width: 7px; height: 7px; background: var(--held); box-shadow: none; }
|
|
856
|
+
.item.held .rverdict { color: color-mix(in srgb, var(--held) 78%, var(--faint)); }
|
|
857
|
+
.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); }
|
|
858
|
+
.item.moved .dot { background: var(--moved); box-shadow: 0 0 0 4px color-mix(in srgb, var(--moved) 17%, transparent); }
|
|
859
|
+
.item.broke .dot {
|
|
860
|
+
background: var(--broke);
|
|
861
|
+
border-radius: 2px; transform: rotate(45deg);
|
|
862
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--broke) 17%, transparent);
|
|
863
|
+
}
|
|
864
|
+
.item.skip .dot, .item.pending .dot { background: transparent; box-shadow: inset 0 0 0 1.5px var(--resting); }
|
|
755
865
|
.item.running .dot {
|
|
756
866
|
background: transparent;
|
|
757
867
|
box-shadow: inset 0 0 0 2px var(--accent);
|
|
758
868
|
animation: ping 1.8s var(--ease) infinite;
|
|
759
869
|
}
|
|
760
870
|
@keyframes ping {
|
|
761
|
-
0% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 0 color-mix(in srgb, var(--accent)
|
|
871
|
+
0% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 0 color-mix(in srgb, var(--accent) 34%, transparent); }
|
|
762
872
|
70%, 100% { box-shadow: inset 0 0 0 2px var(--accent), 0 0 0 7px color-mix(in srgb, var(--accent) 0%, transparent); }
|
|
763
873
|
}
|
|
764
874
|
.item.running .rname { color: var(--ink); }
|
|
@@ -776,11 +886,11 @@ button { font: inherit; color: inherit; }
|
|
|
776
886
|
flex: 0 0 auto;
|
|
777
887
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
778
888
|
font-variant-numeric: tabular-nums;
|
|
779
|
-
font-size: var(--t-meta); color: var(--
|
|
889
|
+
font-size: var(--t-meta); color: var(--faintest);
|
|
780
890
|
}
|
|
781
891
|
.row .chev {
|
|
782
892
|
flex: 0 0 auto; width: 14px; height: 14px;
|
|
783
|
-
color: var(--
|
|
893
|
+
color: var(--faintest); opacity: 0;
|
|
784
894
|
transition: transform var(--calm) var(--ease), opacity var(--quick) var(--ease);
|
|
785
895
|
}
|
|
786
896
|
.row:hover .chev, .item.open .chev { opacity: 1; }
|
|
@@ -795,16 +905,16 @@ button { font: inherit; color: inherit; }
|
|
|
795
905
|
}
|
|
796
906
|
.item.open .detail { animation: unfold 240ms var(--ease) both; }
|
|
797
907
|
@keyframes unfold { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
|
|
798
|
-
.detail .why { color: var(--
|
|
908
|
+
.detail .why { color: var(--faint); }
|
|
909
|
+
/* Grey, whatever happened. The row above it has already said so in colour and
|
|
910
|
+
the working below colours the step that did it — a page that says the same
|
|
911
|
+
thing three times in the same colour is a page nobody scans. */
|
|
799
912
|
.detail .out { color: var(--soft); margin-top: 3px; }
|
|
800
|
-
.detail .out.moved { color: var(--moved); }
|
|
801
|
-
.detail .out.broke { color: var(--broke); }
|
|
802
|
-
.detail .out.wait { color: var(--accent); }
|
|
803
913
|
.detail .claim {
|
|
804
914
|
margin-top: 9px; padding: 9px 12px;
|
|
805
915
|
border-radius: var(--radius-xs);
|
|
806
916
|
background: var(--well);
|
|
807
|
-
box-shadow: inset 2px 0 0 var(--broke);
|
|
917
|
+
box-shadow: inset 2px 0 0 var(--broke), inset 0 0 0 1px var(--line);
|
|
808
918
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
809
919
|
font-size: var(--t-body); color: var(--ink);
|
|
810
920
|
}
|
|
@@ -822,32 +932,110 @@ button { font: inherit; color: inherit; }
|
|
|
822
932
|
the top of the window. */
|
|
823
933
|
.work-here { padding: 2px var(--pad) 10px; }
|
|
824
934
|
.work-here .worklabel { margin: 0 0 6px; }
|
|
825
|
-
.detail .worklabel {
|
|
826
|
-
|
|
935
|
+
.detail .worklabel { margin: 11px 0 6px; }
|
|
936
|
+
/* The heading of a working list: what it is, and — quietly, on the right —
|
|
937
|
+
which check it belongs to. The name matters now that the list is live: a
|
|
938
|
+
guard's claims can be ticking off while the last screen's picture is still
|
|
939
|
+
on the glass, and a heading with no name would leave a person guessing
|
|
940
|
+
whose working they are reading. */
|
|
941
|
+
.worklabel {
|
|
942
|
+
display: flex; align-items: baseline; gap: 8px;
|
|
827
943
|
font-size: var(--t-label); font-weight: 600;
|
|
828
|
-
letter-spacing: 0.
|
|
944
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
829
945
|
color: var(--faint);
|
|
830
946
|
}
|
|
947
|
+
/* The heading never wraps: three words that never change. Anything that has to
|
|
948
|
+
give is the name beside it, which ellipses. */
|
|
949
|
+
.worklabel .worktitle { flex: 0 0 auto; white-space: nowrap; }
|
|
950
|
+
.worklabel .workwho {
|
|
951
|
+
flex: 1 1 auto; min-width: 0;
|
|
952
|
+
font-weight: 500; letter-spacing: 0.02em; text-transform: none;
|
|
953
|
+
text-align: right;
|
|
954
|
+
color: var(--faintest);
|
|
955
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
956
|
+
}
|
|
957
|
+
/* Said only while a list is empty, so a check that has just started does not
|
|
958
|
+
look like a check that found nothing to do. */
|
|
959
|
+
.workwait {
|
|
960
|
+
padding: 4px 0 3px 11px;
|
|
961
|
+
box-shadow: inset 1px 0 0 var(--line);
|
|
962
|
+
font-size: var(--t-body); color: var(--faint);
|
|
963
|
+
}
|
|
831
964
|
.work { list-style: none; margin: 0; padding: 0; }
|
|
832
965
|
.step {
|
|
833
966
|
display: flex; align-items: baseline; gap: 9px;
|
|
834
967
|
padding: 4px 0 4px 11px;
|
|
835
968
|
box-shadow: inset 1px 0 0 var(--line);
|
|
836
969
|
}
|
|
837
|
-
.step.warn { box-shadow: inset 1.5px 0 0 var(--
|
|
970
|
+
.step.warn { box-shadow: inset 1.5px 0 0 var(--doubt); }
|
|
838
971
|
.step.bad { box-shadow: inset 1.5px 0 0 var(--broke); }
|
|
839
|
-
.step.skipped { box-shadow: inset 1px 0 0 var(--resting); }
|
|
972
|
+
.step.skipped, .step.ended { box-shadow: inset 1px 0 0 var(--resting); }
|
|
973
|
+
.step.running { box-shadow: inset 1.5px 0 0 color-mix(in srgb, var(--accent) 66%, transparent); }
|
|
974
|
+
/* A line arrives once, quietly, and then never moves again. */
|
|
975
|
+
.step.arrive { animation: stepin 240ms var(--ease) both; }
|
|
976
|
+
@keyframes stepin { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
977
|
+
|
|
978
|
+
/* The tick. This is the thing a person actually watches: each line takes its
|
|
979
|
+
mark the moment it settles, so a screen being checked reads as a list
|
|
980
|
+
ticking off rather than a table that appears when it is all over. A step
|
|
981
|
+
still running carries a soft dot that breathes — an honest "this one is
|
|
982
|
+
happening", with nothing that spins on after the step it belongs to.
|
|
983
|
+
A check that passed wears a filled green disc with a white tick in it. The
|
|
984
|
+
first version drew a bare grey tick on the argument that twenty green ticks
|
|
985
|
+
is a Christmas tree — true of a colour used for decoration, wrong here: a
|
|
986
|
+
person scanning the list wants to SEE that these held, and a disc reads as
|
|
987
|
+
done from further away than a hairline tick ever will. */
|
|
988
|
+
.stepmark {
|
|
989
|
+
/* Level with the first line of the label, never floating between the label
|
|
990
|
+
and the number under it. */
|
|
991
|
+
flex: 0 0 16px; align-self: flex-start;
|
|
992
|
+
display: flex; align-items: center; justify-content: center;
|
|
993
|
+
width: 16px; height: 18px; color: var(--faint);
|
|
994
|
+
transition: color var(--calm) var(--ease);
|
|
995
|
+
}
|
|
996
|
+
.stepmark .glyph { width: 13px; height: 13px; }
|
|
997
|
+
|
|
998
|
+
/* The disc. Only for a settled verdict — a step still running keeps its bare
|
|
999
|
+
mark, because a filled circle would say it had finished. */
|
|
1000
|
+
.step.ok .stepmark, .step.warn .stepmark, .step.bad .stepmark {
|
|
1001
|
+
width: 16px; height: 16px; margin-top: 1px;
|
|
1002
|
+
border-radius: 50%;
|
|
1003
|
+
color: #0b1a10;
|
|
1004
|
+
}
|
|
1005
|
+
.step.ok .stepmark .glyph, .step.warn .stepmark .glyph, .step.bad .stepmark .glyph {
|
|
1006
|
+
width: 11px; height: 11px;
|
|
1007
|
+
}
|
|
1008
|
+
.step.ok .stepmark { background: var(--held); color: #06230f; }
|
|
1009
|
+
.step.warn .stepmark { background: var(--doubt); color: #2a1c00; }
|
|
1010
|
+
.step.bad .stepmark { background: var(--broke); color: #2a0703; }
|
|
1011
|
+
.step.skipped .stepmark, .step.ended .stepmark { color: var(--faintest); }
|
|
1012
|
+
.step.running .stepmark { color: var(--accent); }
|
|
1013
|
+
.step.running .stepmark .glyph { animation: markpulse 1.6s var(--ease) infinite; }
|
|
1014
|
+
@keyframes markpulse { 0%, 100% { opacity: 0.34; } 50% { opacity: 1; } }
|
|
1015
|
+
.step.running .stepwhat { color: var(--ink); }
|
|
1016
|
+
|
|
1017
|
+
/* Guards: the claims are what a guard is for, so they carry the weight, and
|
|
1018
|
+
the actions that get the app into position sit back a little.
|
|
1019
|
+
Named 'stepclaim' rather than 'claim' on purpose — a bare 'claim' inside a
|
|
1020
|
+
detail is already the boxed expectation that failed, and these lines would
|
|
1021
|
+
have been swept up by it. */
|
|
1022
|
+
.step.stepclaim .stepwhat { color: var(--ink); font-weight: 560; }
|
|
1023
|
+
.step.stepact .stepwhat { color: var(--soft); }
|
|
1024
|
+
.step.stepact .stepno, .step.stepact .stepnum { color: var(--faintest); }
|
|
1025
|
+
.step.stepact .stepmark { opacity: 0.8; }
|
|
840
1026
|
/* The numeral keeps its own column whatever happens to the right of it — a
|
|
841
1027
|
number that has been left behind on a line of its own is not a sequence any
|
|
842
1028
|
more, which is what a long label does to it on a panel dragged narrow. */
|
|
843
|
-
.stepno { flex: 0 0 15px; text-align: right; font-size: var(--t-label); color: var(--
|
|
1029
|
+
.stepno { flex: 0 0 15px; text-align: right; font-size: var(--t-label); color: var(--faintest); }
|
|
844
1030
|
/* Label on one line, its number under it.
|
|
845
1031
|
They used to share a line and wrap to a second, right-aligned one whenever the
|
|
846
1032
|
number was long — which at 460px was most of them, and the column came out
|
|
847
1033
|
ragged. Stacked, it reads down the page like a receipt: what was done, then
|
|
848
1034
|
what it came to. */
|
|
849
1035
|
.stepbody { flex: 1 1 auto; min-width: 0; }
|
|
850
|
-
|
|
1036
|
+
/* One size, whether the working is beside the picture or under a row: the same
|
|
1037
|
+
size the check names are set at, so the two lists read as one thing. */
|
|
1038
|
+
.stepwhat { color: var(--soft); font-size: var(--t-name); line-height: 1.5; }
|
|
851
1039
|
.stepnum {
|
|
852
1040
|
display: block; margin-top: 1px;
|
|
853
1041
|
font-size: var(--t-meta); color: var(--faint);
|
|
@@ -856,10 +1044,11 @@ button { font: inherit; color: inherit; }
|
|
|
856
1044
|
/* A step that wants a person is the one thing in the list that is allowed to
|
|
857
1045
|
be read from across the row: its words go to full strength, its number takes
|
|
858
1046
|
the colour of what happened. */
|
|
859
|
-
.step.warn .stepwhat, .step.bad .stepwhat { color: var(--ink); }
|
|
860
|
-
.step.warn .stepno, .step.warn .stepnum { color: var(--
|
|
1047
|
+
.step.warn .stepwhat, .step.bad .stepwhat { color: var(--ink); font-weight: 560; }
|
|
1048
|
+
.step.warn .stepno, .step.warn .stepnum { color: var(--doubt); }
|
|
861
1049
|
.step.bad .stepno, .step.bad .stepnum { color: var(--broke); }
|
|
862
|
-
.step.skipped .stepwhat, .step.skipped .stepno, .step.skipped .stepnum
|
|
1050
|
+
.step.skipped .stepwhat, .step.skipped .stepno, .step.skipped .stepnum,
|
|
1051
|
+
.step.ended .stepwhat, .step.ended .stepno, .step.ended .stepnum { color: var(--faintest); }
|
|
863
1052
|
|
|
864
1053
|
.nothing { padding: 26px 16px; color: var(--faint); font-size: var(--t-body); line-height: 1.7; text-align: center; }
|
|
865
1054
|
.nothing .mono { color: var(--soft); }
|
|
@@ -870,8 +1059,8 @@ button { font: inherit; color: inherit; }
|
|
|
870
1059
|
padding: 12px var(--pad) 13px;
|
|
871
1060
|
border-top: 1px solid var(--line);
|
|
872
1061
|
background: var(--glass);
|
|
873
|
-
backdrop-filter: blur(22px) saturate(
|
|
874
|
-
-webkit-backdrop-filter: blur(22px) saturate(
|
|
1062
|
+
backdrop-filter: blur(22px) saturate(120%);
|
|
1063
|
+
-webkit-backdrop-filter: blur(22px) saturate(120%);
|
|
875
1064
|
}
|
|
876
1065
|
.tophead {
|
|
877
1066
|
display: flex; align-items: center; gap: 8px;
|
|
@@ -882,7 +1071,7 @@ button { font: inherit; color: inherit; }
|
|
|
882
1071
|
.tlabel {
|
|
883
1072
|
flex: 1 1 auto;
|
|
884
1073
|
font-size: var(--t-label); font-weight: 600;
|
|
885
|
-
letter-spacing: 0.
|
|
1074
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
886
1075
|
color: var(--faint);
|
|
887
1076
|
}
|
|
888
1077
|
.ttotal {
|
|
@@ -895,7 +1084,7 @@ button { font: inherit; color: inherit; }
|
|
|
895
1084
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
896
1085
|
font-variant-numeric: tabular-nums;
|
|
897
1086
|
}
|
|
898
|
-
.tophead .chev { color: var(--
|
|
1087
|
+
.tophead .chev { color: var(--faintest); width: 13px; height: 13px; transition: transform var(--calm) var(--ease); }
|
|
899
1088
|
.tophead[aria-expanded='true'] .chev { transform: rotate(180deg); }
|
|
900
1089
|
.tophead:hover .tlabel, .tophead:hover .ttotal, .tophead:hover .chev { color: var(--soft); }
|
|
901
1090
|
|
|
@@ -915,32 +1104,34 @@ button { font: inherit; color: inherit; }
|
|
|
915
1104
|
|
|
916
1105
|
/* --- follow ------------------------------------------------------------- */
|
|
917
1106
|
.follow {
|
|
918
|
-
|
|
1107
|
+
/* Clear of the pinned strip at the bottom, not on top of it. At 16px it sat
|
|
1108
|
+
squarely over the timing line and covered the words it floated above. */
|
|
1109
|
+
position: fixed; left: 50%; bottom: 62px; transform: translateX(-50%);
|
|
919
1110
|
z-index: 4;
|
|
920
1111
|
font-size: var(--t-body);
|
|
921
|
-
color: var(--
|
|
1112
|
+
color: var(--soft);
|
|
922
1113
|
background: var(--lift);
|
|
923
|
-
border:
|
|
924
|
-
padding:
|
|
925
|
-
box-shadow: 0
|
|
1114
|
+
border: 0; border-radius: 999px;
|
|
1115
|
+
padding: 7px 15px; cursor: pointer;
|
|
1116
|
+
box-shadow: inset 0 0 0 1px var(--line-firm);
|
|
926
1117
|
animation: settle 240ms var(--ease) both;
|
|
927
|
-
transition:
|
|
1118
|
+
transition: color var(--quick) var(--ease), background var(--quick) var(--ease);
|
|
928
1119
|
}
|
|
929
|
-
.follow:hover {
|
|
1120
|
+
.follow:hover { color: var(--ink); background: var(--card-hover); }
|
|
930
1121
|
|
|
931
1122
|
/* --- the one thing left to do ------------------------------------------- */
|
|
932
1123
|
.foot {
|
|
933
1124
|
flex: 0 0 auto;
|
|
934
1125
|
padding: 14px var(--pad) 16px;
|
|
935
1126
|
background: var(--glass);
|
|
936
|
-
backdrop-filter: blur(22px) saturate(
|
|
937
|
-
-webkit-backdrop-filter: blur(22px) saturate(
|
|
1127
|
+
backdrop-filter: blur(22px) saturate(120%);
|
|
1128
|
+
-webkit-backdrop-filter: blur(22px) saturate(120%);
|
|
938
1129
|
border-top: 1px solid var(--line);
|
|
939
1130
|
animation: arrive 340ms var(--ease) both;
|
|
940
1131
|
}
|
|
941
1132
|
.nextlabel {
|
|
942
1133
|
font-size: var(--t-label); font-weight: 600;
|
|
943
|
-
letter-spacing: 0.
|
|
1134
|
+
letter-spacing: 0.2em; text-transform: uppercase;
|
|
944
1135
|
color: var(--faint);
|
|
945
1136
|
margin-bottom: 8px;
|
|
946
1137
|
}
|
|
@@ -950,22 +1141,26 @@ button { font: inherit; color: inherit; }
|
|
|
950
1141
|
display: flex; align-items: center;
|
|
951
1142
|
background: var(--well);
|
|
952
1143
|
border-radius: var(--radius-xs);
|
|
1144
|
+
box-shadow: inset 0 0 0 1px var(--line);
|
|
953
1145
|
padding: 9px 11px; font-size: var(--t-body);
|
|
954
1146
|
color: var(--ink);
|
|
955
1147
|
overflow-wrap: anywhere;
|
|
956
1148
|
user-select: all;
|
|
957
1149
|
}
|
|
958
|
-
.prompt { flex: 0 0 auto; color: var(--
|
|
1150
|
+
.prompt { flex: 0 0 auto; color: var(--faint); user-select: none; }
|
|
959
1151
|
.copy {
|
|
960
1152
|
flex: 0 0 auto; width: 36px;
|
|
961
1153
|
display: flex; align-items: center; justify-content: center;
|
|
962
1154
|
color: var(--soft); background: var(--well);
|
|
963
1155
|
border: 0; border-radius: var(--radius-xs);
|
|
1156
|
+
box-shadow: inset 0 0 0 1px var(--line);
|
|
964
1157
|
cursor: pointer;
|
|
965
1158
|
transition: color var(--quick) var(--ease), background var(--quick) var(--ease);
|
|
966
1159
|
}
|
|
967
1160
|
.copy:hover { color: var(--ink); background: var(--card-hover); }
|
|
968
|
-
.
|
|
1161
|
+
/* Copied. Not a colour — colour means something is wrong — but a hard invert,
|
|
1162
|
+
which is louder than any tint and gone again in a second. */
|
|
1163
|
+
.copy.done { color: var(--ground); background: var(--ink); }
|
|
969
1164
|
|
|
970
1165
|
/* --------------------------------------------------------------------------
|
|
971
1166
|
The viewer.
|
|
@@ -979,7 +1174,7 @@ button { font: inherit; color: inherit; }
|
|
|
979
1174
|
.viewer {
|
|
980
1175
|
position: fixed; inset: 0; z-index: 20;
|
|
981
1176
|
display: flex; flex-direction: column;
|
|
982
|
-
background: color-mix(in srgb, var(--ground)
|
|
1177
|
+
background: color-mix(in srgb, var(--ground) 88%, #000);
|
|
983
1178
|
animation: viewerin 220ms var(--ease) both;
|
|
984
1179
|
}
|
|
985
1180
|
@keyframes viewerin { from { opacity: 0; } to { opacity: 1; } }
|
|
@@ -1013,7 +1208,7 @@ button { font: inherit; color: inherit; }
|
|
|
1013
1208
|
}
|
|
1014
1209
|
.vmodes button::after {
|
|
1015
1210
|
content: ''; position: absolute; left: 50%; right: 50%; bottom: 1px; height: 1.5px;
|
|
1016
|
-
border-radius: 2px; background: var(--
|
|
1211
|
+
border-radius: 2px; background: var(--ink); opacity: 0;
|
|
1017
1212
|
transition: left var(--calm) var(--ease), right var(--calm) var(--ease), opacity var(--quick) var(--ease);
|
|
1018
1213
|
}
|
|
1019
1214
|
.vmodes button:hover { color: var(--soft); }
|
|
@@ -1089,14 +1284,16 @@ button { font: inherit; color: inherit; }
|
|
|
1089
1284
|
padding: 10px var(--pad) 15px;
|
|
1090
1285
|
text-align: center;
|
|
1091
1286
|
font-size: var(--t-label); letter-spacing: 0.03em;
|
|
1092
|
-
color: var(--
|
|
1287
|
+
color: var(--faintest);
|
|
1093
1288
|
}
|
|
1094
|
-
.vside { flex: 0 0 auto; font-size: var(--t-label); letter-spacing: 0.
|
|
1289
|
+
.vside { flex: 0 0 auto; font-size: var(--t-label); letter-spacing: 0.16em; text-transform: uppercase; color: var(--faint); }
|
|
1290
|
+
/* Grey at the approved end, the screen's own amber at the new one: the slider
|
|
1291
|
+
itself says which way is "what changed". */
|
|
1095
1292
|
.vblend {
|
|
1096
1293
|
flex: 1 1 auto; min-width: 0;
|
|
1097
1294
|
-webkit-appearance: none; appearance: none;
|
|
1098
1295
|
height: 3px; border-radius: 999px;
|
|
1099
|
-
background: linear-gradient(to right, var(--
|
|
1296
|
+
background: linear-gradient(to right, var(--line-firm), var(--moved));
|
|
1100
1297
|
outline: none; cursor: pointer;
|
|
1101
1298
|
}
|
|
1102
1299
|
.vblend::-webkit-slider-thumb {
|
|
@@ -1112,7 +1309,7 @@ button { font: inherit; color: inherit; }
|
|
|
1112
1309
|
|
|
1113
1310
|
/* Dragged narrow. Everything stays, nothing wraps into a mess. */
|
|
1114
1311
|
@media (max-width: 330px) {
|
|
1115
|
-
:root { --pad: 12px; --radius: 16px; --t-lead:
|
|
1312
|
+
:root { --pad: 12px; --radius: 16px; --t-lead: 16px; }
|
|
1116
1313
|
.target .app, .target .sep { display: none; }
|
|
1117
1314
|
.row { gap: 9px; padding: 9px 11px; }
|
|
1118
1315
|
.detail { padding: 0 11px 12px 28px; }
|
|
@@ -1129,7 +1326,7 @@ button { font: inherit; color: inherit; }
|
|
|
1129
1326
|
/* Pulled wide. Two columns rather than one very long thin one, so the picture
|
|
1130
1327
|
gets the room and the list stays beside it instead of below the fold. */
|
|
1131
1328
|
@media (min-width: 720px) {
|
|
1132
|
-
:root { --pad: 24px; --t-lead:
|
|
1329
|
+
:root { --pad: 24px; --t-lead: 20px; }
|
|
1133
1330
|
.body { flex-direction: row; gap: 28px; padding-bottom: 4px; }
|
|
1134
1331
|
.stage { flex: 1 1 54%; min-width: 0; align-self: center; padding-top: 0; }
|
|
1135
1332
|
.scroll { flex: 1 1 46%; min-width: 0; padding-top: 22px; }
|
|
@@ -1290,7 +1487,7 @@ const SCRIPT = `
|
|
|
1290
1487
|
return 'broken again';
|
|
1291
1488
|
}
|
|
1292
1489
|
switch (status) {
|
|
1293
|
-
case 'passed': return '
|
|
1490
|
+
case 'passed': return 'unchanged';
|
|
1294
1491
|
case 'changed':
|
|
1295
1492
|
var n = ev.diffPixels || 0;
|
|
1296
1493
|
return commas(n) + ' ' + plural(n, 'pixel', 'pixels') + ' moved';
|
|
@@ -1460,7 +1657,12 @@ const SCRIPT = `
|
|
|
1460
1657
|
// what makes the whole run browsable afterwards instead of only the last
|
|
1461
1658
|
// screen photographed.
|
|
1462
1659
|
row.addEventListener('click', function () {
|
|
1463
|
-
|
|
1660
|
+
// Its picture, or its working, or both — whichever this check has. A
|
|
1661
|
+
// guard has no picture and its claims are the whole point of it, so a
|
|
1662
|
+
// guard row brings its own working back exactly like a screen does.
|
|
1663
|
+
var forward =
|
|
1664
|
+
(entry.pics && !entry.root.classList.contains('showing')) ||
|
|
1665
|
+
((hasWork(entry) || entry.running) && workOwner !== entry);
|
|
1464
1666
|
if (forward) {
|
|
1465
1667
|
recall(entry);
|
|
1466
1668
|
setOpen(entry, true);
|
|
@@ -1508,18 +1710,21 @@ const SCRIPT = `
|
|
|
1508
1710
|
out.textContent = entry.outText;
|
|
1509
1711
|
d.appendChild(out);
|
|
1510
1712
|
}
|
|
1511
|
-
// The working. Everything that was really done to this
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
d.appendChild(
|
|
1520
|
-
|
|
1713
|
+
// The working. Everything that was really done to this check, in the order
|
|
1714
|
+
// it happened, numbered so it reads as a sequence rather than a bag of
|
|
1715
|
+
// facts. The heading and the list are the SAME two elements every time —
|
|
1716
|
+
// built once for this row and put back — so a redraw in the middle of a
|
|
1717
|
+
// run never tears down lines that are still ticking.
|
|
1718
|
+
if (hasWork(entry)) {
|
|
1719
|
+
var ol = detailWork(entry);
|
|
1720
|
+
d.appendChild(entry.workLabel);
|
|
1721
|
+
d.appendChild(ol);
|
|
1722
|
+
renderWork(ol, entry.checks, entry.kind);
|
|
1521
1723
|
}
|
|
1522
|
-
|
|
1724
|
+
// The expectation that failed, boxed — unless the working above already
|
|
1725
|
+
// ends on it, which it does now that a guard shows its claims. Nothing on
|
|
1726
|
+
// this page is said twice.
|
|
1727
|
+
if (entry.failedAt && !inWork(entry, entry.failedAt)) {
|
|
1523
1728
|
var claim = document.createElement('div');
|
|
1524
1729
|
claim.className = 'claim';
|
|
1525
1730
|
var label = document.createElement('em');
|
|
@@ -1540,95 +1745,387 @@ const SCRIPT = `
|
|
|
1540
1745
|
if (!entry.hasDetail) setOpen(entry, false);
|
|
1541
1746
|
}
|
|
1542
1747
|
|
|
1748
|
+
// -------------------------------------------------------------------------
|
|
1749
|
+
// The working, ticked off live.
|
|
1750
|
+
//
|
|
1751
|
+
// A table that appears once a screen has already finished tells nobody what
|
|
1752
|
+
// is happening — which is exactly what the first person to watch this said.
|
|
1753
|
+
// So the run now announces each thing as it reaches it, and this is the part
|
|
1754
|
+
// of the panel that draws them: the names of what is being checked on the
|
|
1755
|
+
// check that is running right now, each one taking its mark as it settles.
|
|
1756
|
+
//
|
|
1757
|
+
// Two rules hold it up. Every line is found again by its own id rather than
|
|
1758
|
+
// redrawn, so a list that is already right is never torn down and rebuilt —
|
|
1759
|
+
// that is what stops it flickering when the verdict arrives carrying the
|
|
1760
|
+
// whole list again. And a step whose outcome needs a person shows the number
|
|
1761
|
+
// behind it the moment it lands, because that is the moment somebody is
|
|
1762
|
+
// looking.
|
|
1763
|
+
// -------------------------------------------------------------------------
|
|
1764
|
+
|
|
1765
|
+
// The marks, drawn here like every other icon on this page. A tick for a
|
|
1766
|
+
// thing that held, a cross for one that broke, a stroke for a thing that was
|
|
1767
|
+
// deliberately not done, and a soft dot for the one happening now.
|
|
1768
|
+
var MARKS = {
|
|
1769
|
+
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>',
|
|
1770
|
+
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>',
|
|
1771
|
+
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>',
|
|
1772
|
+
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>',
|
|
1773
|
+
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>',
|
|
1774
|
+
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>'
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
var STEP_STATES = { running: 1, ok: 1, warn: 1, bad: 1, skipped: 1, ended: 1 };
|
|
1778
|
+
|
|
1779
|
+
function labelOf(step) { return String(step && step.label == null ? '' : step.label).trim(); }
|
|
1780
|
+
function detailOf(step) { return String(step && step.detail == null ? '' : step.detail).trim(); }
|
|
1781
|
+
function stateOf(step) {
|
|
1782
|
+
var s = step && step.state;
|
|
1783
|
+
return STEP_STATES[s] ? s : 'ok';
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// How a step is found again. The run gives a stable id to anything it will
|
|
1787
|
+
// announce twice — once as it starts, once as it settles — and without one
|
|
1788
|
+
// the words themselves have to do it.
|
|
1789
|
+
function idOf(step) {
|
|
1790
|
+
var key = step && step.key != null ? String(step.key).trim() : '';
|
|
1791
|
+
return key ? 'k:' + key : 'l:' + labelOf(step);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Guards only. A guard is a promise about behaviour, and the promises are
|
|
1795
|
+
// the claims it makes — 'the sidebar column is gone' — with actions in
|
|
1796
|
+
// between to get the app into position. The claims are what the guard is
|
|
1797
|
+
// for, so they are the ones that carry the weight.
|
|
1798
|
+
var CLAIM_KEY = /^(expect|claim|assert|expected)([^a-z0-9]|$)/i;
|
|
1799
|
+
var ACT_KEY = /^(do|did|act|action|step|fresh|run|ran|open|click|type|press|hover|scroll|wait|read|note)([^a-z0-9]|$)/i;
|
|
1800
|
+
// A step that had to be tried twice is the same step, and its id says so.
|
|
1801
|
+
var RETRY = /^try[0-9]+[^a-z0-9]/i;
|
|
1802
|
+
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;
|
|
1803
|
+
|
|
1804
|
+
function weightOf(kind, step) {
|
|
1805
|
+
if (kind !== 'guard') return '';
|
|
1806
|
+
var key = step && step.key != null ? String(step.key).trim() : '';
|
|
1807
|
+
if (RETRY.test(key)) key = key.replace(RETRY, '');
|
|
1808
|
+
if (CLAIM_KEY.test(key)) return 'stepclaim';
|
|
1809
|
+
if (ACT_KEY.test(key)) return 'stepact';
|
|
1810
|
+
return ACT_WORD.test(labelOf(step)) ? 'stepact' : 'stepclaim';
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/** One line, built once and then only ever updated in place. */
|
|
1814
|
+
function makeLine() {
|
|
1815
|
+
var li = document.createElement('li');
|
|
1816
|
+
li.className = 'step';
|
|
1817
|
+
var no = document.createElement('span');
|
|
1818
|
+
no.className = 'stepno mono';
|
|
1819
|
+
var mark = document.createElement('span');
|
|
1820
|
+
mark.className = 'stepmark';
|
|
1821
|
+
var body = document.createElement('span');
|
|
1822
|
+
body.className = 'stepbody';
|
|
1823
|
+
var what = document.createElement('span');
|
|
1824
|
+
what.className = 'stepwhat';
|
|
1825
|
+
var num = document.createElement('span');
|
|
1826
|
+
num.className = 'stepnum mono';
|
|
1827
|
+
num.hidden = true;
|
|
1828
|
+
body.appendChild(what);
|
|
1829
|
+
body.appendChild(num);
|
|
1830
|
+
li.appendChild(no);
|
|
1831
|
+
li.appendChild(mark);
|
|
1832
|
+
li.appendChild(body);
|
|
1833
|
+
li.__no = no; li.__mark = mark; li.__what = what; li.__num = num;
|
|
1834
|
+
li.__state = ''; li.__weight = '';
|
|
1835
|
+
return li;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function fillLine(li, step, n, kind, fresh) {
|
|
1839
|
+
var state = stateOf(step);
|
|
1840
|
+
var weight = weightOf(kind, step);
|
|
1841
|
+
var said = labelOf(step);
|
|
1842
|
+
var behind = detailOf(step);
|
|
1843
|
+
|
|
1844
|
+
if (li.__no.textContent !== String(n)) li.__no.textContent = String(n);
|
|
1845
|
+
if (li.__state !== state) {
|
|
1846
|
+
li.__state = state;
|
|
1847
|
+
li.__mark.innerHTML = MARKS[state] || MARKS.ok;
|
|
1848
|
+
}
|
|
1849
|
+
li.__weight = weight;
|
|
1850
|
+
li.className = 'step ' + state + (weight ? ' ' + weight : '') + (fresh ? ' arrive' : '');
|
|
1851
|
+
if (li.__what.textContent !== said) li.__what.textContent = said;
|
|
1852
|
+
// The number behind a step is shown the instant the step has one — a line
|
|
1853
|
+
// that has just gone amber or red is the one moment somebody needs it.
|
|
1854
|
+
if (li.__num.textContent !== behind) li.__num.textContent = behind;
|
|
1855
|
+
li.__num.hidden = !behind;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1543
1858
|
/**
|
|
1544
|
-
*
|
|
1859
|
+
* Draw a list of steps into a list element, reconciling by id.
|
|
1545
1860
|
*
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1548
|
-
*
|
|
1861
|
+
* Lines already on the page are found and updated; new ones settle in at
|
|
1862
|
+
* their place; anything the authoritative list no longer mentions goes. So
|
|
1863
|
+
* the same function serves a step arriving on its own and the whole array
|
|
1864
|
+
* that comes with the verdict, and the second one changes nothing a watcher
|
|
1865
|
+
* already had right.
|
|
1549
1866
|
*
|
|
1867
|
+
* @param {any} ol
|
|
1550
1868
|
* @param {any} checks
|
|
1551
|
-
* @
|
|
1869
|
+
* @param {string} kind
|
|
1870
|
+
* @returns {number} How many lines are shown.
|
|
1552
1871
|
*/
|
|
1553
|
-
function
|
|
1554
|
-
var steps = Array.isArray(checks) ? checks :
|
|
1555
|
-
|
|
1556
|
-
var
|
|
1557
|
-
|
|
1558
|
-
var
|
|
1559
|
-
|
|
1560
|
-
var step = steps[k];
|
|
1872
|
+
function renderWork(ol, checks, kind) {
|
|
1873
|
+
var steps = Array.isArray(checks) ? checks : [];
|
|
1874
|
+
var lines = ol.__lines || (ol.__lines = Object.create(null));
|
|
1875
|
+
var seen = Object.create(null);
|
|
1876
|
+
var n = 0;
|
|
1877
|
+
for (var i = 0; i < steps.length; i++) {
|
|
1878
|
+
var step = steps[i];
|
|
1561
1879
|
if (!step || typeof step !== 'object') continue;
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
var
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
what.className = 'stepwhat';
|
|
1578
|
-
what.textContent = said;
|
|
1579
|
-
body.appendChild(what);
|
|
1580
|
-
|
|
1581
|
-
var behind = String(step.detail == null ? '' : step.detail).trim();
|
|
1582
|
-
if (behind) {
|
|
1583
|
-
var num = document.createElement('span');
|
|
1584
|
-
num.className = 'stepnum mono';
|
|
1585
|
-
num.textContent = behind;
|
|
1586
|
-
body.appendChild(num);
|
|
1587
|
-
}
|
|
1588
|
-
line.appendChild(no);
|
|
1589
|
-
line.appendChild(body);
|
|
1590
|
-
work.appendChild(line);
|
|
1880
|
+
if (!labelOf(step)) continue;
|
|
1881
|
+
n++;
|
|
1882
|
+
var id = idOf(step);
|
|
1883
|
+
// Two steps that answer to the same name in one list still get a line
|
|
1884
|
+
// each, rather than one of them quietly replacing the other.
|
|
1885
|
+
if (seen[id]) id = id + '#' + n;
|
|
1886
|
+
seen[id] = true;
|
|
1887
|
+
|
|
1888
|
+
var li = lines[id];
|
|
1889
|
+
var fresh = false;
|
|
1890
|
+
if (!li) { li = makeLine(); lines[id] = li; fresh = !CALM; }
|
|
1891
|
+
fillLine(li, step, n, kind, fresh);
|
|
1892
|
+
if (fresh) forget(li);
|
|
1893
|
+
var at = ol.children[n - 1];
|
|
1894
|
+
if (at !== li) ol.insertBefore(li, at || null);
|
|
1591
1895
|
}
|
|
1592
|
-
|
|
1896
|
+
for (var gone in lines) {
|
|
1897
|
+
if (seen[gone]) continue;
|
|
1898
|
+
if (lines[gone].parentNode === ol) ol.removeChild(lines[gone]);
|
|
1899
|
+
delete lines[gone];
|
|
1900
|
+
}
|
|
1901
|
+
return n;
|
|
1593
1902
|
}
|
|
1594
1903
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
if (!
|
|
1904
|
+
// The arriving animation is worn once and then taken off, so a line that is
|
|
1905
|
+
// later moved or updated never plays it again.
|
|
1906
|
+
function forget(li) {
|
|
1907
|
+
setTimeout(function () { li.classList.remove('arrive'); }, 420);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function hasWork(entry) {
|
|
1911
|
+
return !!(entry && Array.isArray(entry.checks) && entry.checks.length);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/** Is this sentence already one of the lines of the working? */
|
|
1915
|
+
function inWork(entry, said) {
|
|
1916
|
+
var want = String(said == null ? '' : said).trim();
|
|
1917
|
+
if (!want || !hasWork(entry)) return false;
|
|
1918
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
1919
|
+
if (labelOf(entry.checks[i]) === want) return true;
|
|
1920
|
+
}
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/** This row's own copy of the list, for under the row. Built once. */
|
|
1925
|
+
function detailWork(entry) {
|
|
1926
|
+
if (!entry.workOl) {
|
|
1927
|
+
var label = document.createElement('p');
|
|
1928
|
+
label.className = 'worklabel';
|
|
1929
|
+
var title = document.createElement('span');
|
|
1930
|
+
title.className = 'worktitle';
|
|
1931
|
+
label.appendChild(title);
|
|
1932
|
+
entry.workLabel = label;
|
|
1933
|
+
entry.workTitle = title;
|
|
1934
|
+
entry.workOl = document.createElement('ol');
|
|
1935
|
+
entry.workOl.className = 'work';
|
|
1936
|
+
}
|
|
1937
|
+
entry.workTitle.textContent = entry.running ? 'What is being checked' : 'What was done';
|
|
1938
|
+
return entry.workOl;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// -------------------------------------------------------------------------
|
|
1942
|
+
// The working on the glass: whichever check the panel is showing.
|
|
1943
|
+
// -------------------------------------------------------------------------
|
|
1944
|
+
|
|
1945
|
+
var workOwner = null;
|
|
1946
|
+
// Whether this run says anything as it goes. An older Stays Fixed only ever
|
|
1947
|
+
// hands over the whole list at the end, and a heading promising that things
|
|
1948
|
+
// will appear as they are checked would be a promise nobody kept.
|
|
1949
|
+
var sawSteps = false;
|
|
1950
|
+
|
|
1951
|
+
// Built once, into the section that already sits at the top of the scrolling
|
|
1952
|
+
// column. It has to stay there: put inside the picture's own box it steals
|
|
1953
|
+
// the picture's height and squashes the screenshot to a sliver.
|
|
1954
|
+
var workShell = (function () {
|
|
1609
1955
|
var label = document.createElement('p');
|
|
1610
1956
|
label.className = 'worklabel';
|
|
1611
|
-
|
|
1957
|
+
var title = document.createElement('span');
|
|
1958
|
+
title.className = 'worktitle';
|
|
1959
|
+
var who = document.createElement('span');
|
|
1960
|
+
who.className = 'workwho mono';
|
|
1961
|
+
label.appendChild(title);
|
|
1962
|
+
label.appendChild(who);
|
|
1963
|
+
var waiting = document.createElement('p');
|
|
1964
|
+
waiting.className = 'workwait';
|
|
1965
|
+
waiting.hidden = true;
|
|
1966
|
+
var list = document.createElement('ol');
|
|
1967
|
+
list.className = 'work';
|
|
1612
1968
|
ui.work.appendChild(label);
|
|
1969
|
+
ui.work.appendChild(waiting);
|
|
1613
1970
|
ui.work.appendChild(list);
|
|
1614
|
-
|
|
1971
|
+
return { title: title, who: who, waiting: waiting, list: list };
|
|
1972
|
+
})();
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* Bring one check's working onto the glass.
|
|
1976
|
+
*
|
|
1977
|
+
* A bring is for a check that has just STARTED: the list is about to tick off
|
|
1978
|
+
* and it is no use doing that below the fold, so the column goes back to the
|
|
1979
|
+
* top where the working lives — unless the person has taken hold of the list,
|
|
1980
|
+
* in which case nothing moves under them, like everywhere else on this page.
|
|
1981
|
+
* A click never brings: somebody who reached for a row is already looking at
|
|
1982
|
+
* it, and the row opening under their finger is the answer.
|
|
1983
|
+
*/
|
|
1984
|
+
function openWork(entry, bring) {
|
|
1985
|
+
if (!entry) return;
|
|
1986
|
+
if (workOwner && workOwner !== entry) workOwner.root.classList.remove('working');
|
|
1987
|
+
workOwner = entry;
|
|
1988
|
+
entry.root.classList.add('working');
|
|
1989
|
+
syncWork(entry);
|
|
1990
|
+
if (bring && following && ui.scroll.scrollTop > 0) {
|
|
1991
|
+
ui.scroll.scrollTo({ top: 0, behavior: CALM ? 'auto' : 'smooth' });
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/** Redraw wherever this check's working is on show. */
|
|
1996
|
+
function syncWork(entry) {
|
|
1997
|
+
if (!entry) return;
|
|
1998
|
+
if (workOwner === entry) {
|
|
1999
|
+
var n = renderWork(workShell.list, entry.checks, entry.kind);
|
|
2000
|
+
workShell.title.textContent = entry.running ? 'What is being checked' : 'What was done';
|
|
2001
|
+
workShell.who.textContent = entry.name || '';
|
|
2002
|
+
workShell.waiting.textContent = entry.running
|
|
2003
|
+
? 'Each thing appears here as it is checked.'
|
|
2004
|
+
: 'Nothing was written down for this one.';
|
|
2005
|
+
workShell.waiting.hidden = n > 0 || !sawSteps;
|
|
2006
|
+
workShell.list.hidden = n === 0;
|
|
2007
|
+
// Nothing to show and nothing promised: the section stays out of the way.
|
|
2008
|
+
ui.work.hidden = n === 0 && !sawSteps;
|
|
2009
|
+
}
|
|
2010
|
+
// Under the row. Cheap once the list is on the page; the first step of a
|
|
2011
|
+
// check has to go the long way round, because that is what turns a row
|
|
2012
|
+
// with nothing under it into one worth opening.
|
|
2013
|
+
if (entry.workOl && entry.workOl.parentNode) {
|
|
2014
|
+
detailWork(entry);
|
|
2015
|
+
renderWork(entry.workOl, entry.checks, entry.kind);
|
|
2016
|
+
} else if (hasWork(entry)) {
|
|
2017
|
+
redraw(entry);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* One step, as it happens. Announced as running, found again by its id and
|
|
2023
|
+
* settled when it is done.
|
|
2024
|
+
*/
|
|
2025
|
+
function applyStep(entry, step) {
|
|
2026
|
+
if (!entry || !step || typeof step !== 'object') return;
|
|
2027
|
+
if (!labelOf(step)) return;
|
|
2028
|
+
if (!Array.isArray(entry.checks)) entry.checks = [];
|
|
2029
|
+
var id = idOf(step);
|
|
2030
|
+
var kept = {
|
|
2031
|
+
label: labelOf(step),
|
|
2032
|
+
detail: detailOf(step),
|
|
2033
|
+
state: stateOf(step),
|
|
2034
|
+
key: step.key == null ? '' : String(step.key)
|
|
2035
|
+
};
|
|
2036
|
+
var found = -1;
|
|
2037
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
2038
|
+
if (idOf(entry.checks[i]) === id) { found = i; break; }
|
|
2039
|
+
}
|
|
2040
|
+
if (found >= 0) entry.checks[found] = kept;
|
|
2041
|
+
else entry.checks.push(kept);
|
|
2042
|
+
syncWork(entry);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Take the authoritative list that arrives with a verdict.
|
|
2047
|
+
*
|
|
2048
|
+
* Steps keep the id they were announced with wherever the final list does not
|
|
2049
|
+
* carry one, so a line a watcher has already been ticking is found again and
|
|
2050
|
+
* updated rather than thrown away and drawn a second time. That is what makes
|
|
2051
|
+
* the moment a check finishes look like nothing happened at all.
|
|
2052
|
+
*/
|
|
2053
|
+
function adopt(entry, checks) {
|
|
2054
|
+
var live = Array.isArray(entry.checks) ? entry.checks : [];
|
|
2055
|
+
var known = Object.create(null);
|
|
2056
|
+
var i;
|
|
2057
|
+
for (i = 0; i < live.length; i++) {
|
|
2058
|
+
if (live[i] && live[i].key) known[labelOf(live[i])] = String(live[i].key);
|
|
2059
|
+
}
|
|
2060
|
+
var out = [];
|
|
2061
|
+
for (i = 0; i < checks.length; i++) {
|
|
2062
|
+
var step = checks[i];
|
|
2063
|
+
if (!step || typeof step !== 'object') continue;
|
|
2064
|
+
var said = labelOf(step);
|
|
2065
|
+
if (!said) continue;
|
|
2066
|
+
var key = step.key == null ? '' : String(step.key);
|
|
2067
|
+
out.push({
|
|
2068
|
+
label: said,
|
|
2069
|
+
detail: detailOf(step),
|
|
2070
|
+
state: stateOf(step),
|
|
2071
|
+
key: key || known[said] || ''
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
entry.checks = out;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* A check has stopped. Anything still marked as happening is not happening
|
|
2079
|
+
* any more, and a mark that keeps breathing next to a finished result is a
|
|
2080
|
+
* lie — so those lines settle to a plain, colourless end.
|
|
2081
|
+
*/
|
|
2082
|
+
function hush(entry) {
|
|
2083
|
+
if (!entry || !Array.isArray(entry.checks)) return;
|
|
2084
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
2085
|
+
var step = entry.checks[i];
|
|
2086
|
+
if (stateOf(step) !== 'running') continue;
|
|
2087
|
+
entry.checks[i] = { label: labelOf(step), detail: detailOf(step), state: 'ended', key: step.key || '' };
|
|
2088
|
+
}
|
|
1615
2089
|
}
|
|
1616
2090
|
|
|
1617
|
-
/**
|
|
2091
|
+
/**
|
|
2092
|
+
* The claim a guard died on, as the last line of its working.
|
|
2093
|
+
*
|
|
2094
|
+
* A guard that failed has to show every claim that held before the one that
|
|
2095
|
+
* did not, and this makes sure the one that did not is there even when the
|
|
2096
|
+
* run only reported it in the verdict.
|
|
2097
|
+
*/
|
|
2098
|
+
function markFailedClaim(entry, claim) {
|
|
2099
|
+
var said = String(claim == null ? '' : claim).trim();
|
|
2100
|
+
if (!said) return;
|
|
2101
|
+
if (!Array.isArray(entry.checks)) entry.checks = [];
|
|
2102
|
+
for (var i = 0; i < entry.checks.length; i++) {
|
|
2103
|
+
if (labelOf(entry.checks[i]) !== said) continue;
|
|
2104
|
+
entry.checks[i] = {
|
|
2105
|
+
label: said, detail: detailOf(entry.checks[i]), state: 'bad', key: entry.checks[i].key || ''
|
|
2106
|
+
};
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
entry.checks.push({ label: said, detail: '', state: 'bad', key: 'claim:failed' });
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
/** Put a check back on the glass: its pictures if it has any, its working always. */
|
|
1618
2113
|
function recall(entry) {
|
|
1619
2114
|
var p = entry.pics;
|
|
1620
|
-
if (
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2115
|
+
if (p) {
|
|
2116
|
+
var showed = false;
|
|
2117
|
+
if (p.status === 'changed') showed = comparePictures(p);
|
|
2118
|
+
if (!showed && nowOf(p)) {
|
|
2119
|
+
singlePicture(p, entry.name);
|
|
2120
|
+
showed = true;
|
|
2121
|
+
}
|
|
2122
|
+
if (showed) {
|
|
2123
|
+
nameTheShot(entry.name);
|
|
2124
|
+
sayOutcome(entry.tone, outcomeShort(p));
|
|
2125
|
+
markShowing(entry);
|
|
2126
|
+
}
|
|
1626
2127
|
}
|
|
1627
|
-
|
|
1628
|
-
nameTheShot(entry.name);
|
|
1629
|
-
sayOutcome(entry.tone, outcomeShort(p));
|
|
1630
|
-
showWork(entry.checks);
|
|
1631
|
-
markShowing(entry);
|
|
2128
|
+
openWork(entry);
|
|
1632
2129
|
}
|
|
1633
2130
|
|
|
1634
2131
|
function setOpen(entry, open) {
|
|
@@ -1751,10 +2248,13 @@ const SCRIPT = `
|
|
|
1751
2248
|
// scrolls for themselves. Nothing is more annoying than a list that yanks
|
|
1752
2249
|
// itself back while you are reading it.
|
|
1753
2250
|
var following = true;
|
|
2251
|
+
var runOver = false;
|
|
1754
2252
|
function stopFollowing() {
|
|
1755
2253
|
if (!following) return;
|
|
1756
2254
|
following = false;
|
|
1757
|
-
|
|
2255
|
+
// Nothing to follow once the run has ended, so the pill would be an offer
|
|
2256
|
+
// to do nothing — and it sits over the list while it makes it.
|
|
2257
|
+
ui.follow.hidden = runOver;
|
|
1758
2258
|
}
|
|
1759
2259
|
['wheel', 'touchmove', 'keydown'].forEach(function (name) {
|
|
1760
2260
|
ui.scroll.addEventListener(name, stopFollowing, { passive: true });
|
|
@@ -1773,7 +2273,16 @@ const SCRIPT = `
|
|
|
1773
2273
|
|
|
1774
2274
|
var runningItem = null;
|
|
1775
2275
|
function markRunning(entry) {
|
|
2276
|
+
if (runningItem && runningItem !== entry) {
|
|
2277
|
+
runningItem.running = false;
|
|
2278
|
+
hush(runningItem);
|
|
2279
|
+
syncWork(runningItem);
|
|
2280
|
+
}
|
|
1776
2281
|
runningItem = entry;
|
|
2282
|
+
entry.running = true;
|
|
2283
|
+
// A check that is starting again starts with an empty list, so nothing is
|
|
2284
|
+
// left over from the last time it ran.
|
|
2285
|
+
entry.checks = [];
|
|
1777
2286
|
setTone(entry, 'running');
|
|
1778
2287
|
if (following) entry.root.scrollIntoView({ block: 'nearest', behavior: CALM ? 'auto' : 'smooth' });
|
|
1779
2288
|
}
|
|
@@ -2446,11 +2955,26 @@ const SCRIPT = `
|
|
|
2446
2955
|
// photographed yet, so renaming the frame now would put this screen's
|
|
2447
2956
|
// name under the last screen's picture — a caption that lies for a
|
|
2448
2957
|
// second is worse than one that is a second behind.
|
|
2449
|
-
|
|
2958
|
+
var starting = ensureItem('picture', String(ev.name || ''), ev.describe);
|
|
2959
|
+
markRunning(starting);
|
|
2960
|
+
// The working area clears and takes this screen's name, so the list that
|
|
2961
|
+
// ticks off from here is unmistakably about the screen being checked now.
|
|
2962
|
+
openWork(starting, true);
|
|
2450
2963
|
scanning(true);
|
|
2451
2964
|
return;
|
|
2452
2965
|
}
|
|
2453
2966
|
|
|
2967
|
+
// One thing, the moment it happens. This is the whole point of the panel:
|
|
2968
|
+
// the names of what is being checked right now, each taking its mark as it
|
|
2969
|
+
// settles, instead of a table that lands when it is already over.
|
|
2970
|
+
if (type === 'screen:step' || type === 'guard:step') {
|
|
2971
|
+
sawSteps = true;
|
|
2972
|
+
var stepKind = type === 'guard:step' ? 'guard' : 'picture';
|
|
2973
|
+
var stepping = ensureItem(stepKind, String(ev.name || ''), ev.describe);
|
|
2974
|
+
applyStep(stepping, ev.step);
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2454
2978
|
if (type === 'screen:shot') {
|
|
2455
2979
|
scanning(false);
|
|
2456
2980
|
var shotRow = ev.name ? ensureItem('picture', String(ev.name), ev.describe) : null;
|
|
@@ -2488,9 +3012,9 @@ const SCRIPT = `
|
|
|
2488
3012
|
// in the list, because the picture is where the person is looking and
|
|
2489
3013
|
// that line is what tells them there is something to decide.
|
|
2490
3014
|
sayOutcome(tone, outcomeShort(ev));
|
|
2491
|
-
// And what was actually done to it,
|
|
2492
|
-
// person is looking at it.
|
|
2493
|
-
|
|
3015
|
+
// And what was actually done to it, in the list beside the picture,
|
|
3016
|
+
// while the person is looking at it.
|
|
3017
|
+
openWork(done);
|
|
2494
3018
|
}
|
|
2495
3019
|
tint(worstTone());
|
|
2496
3020
|
return;
|
|
@@ -2503,7 +3027,11 @@ const SCRIPT = `
|
|
|
2503
3027
|
var guards = ev.total || expected.guard || countOf(plan.guards);
|
|
2504
3028
|
setState(guards ? 'running ' + commas(guards) + ' ' + plural(guards, 'guard', 'guards') : 'running the guards');
|
|
2505
3029
|
}
|
|
2506
|
-
|
|
3030
|
+
var guarding = ensureItem('guard', String(ev.name || ''), ev.describe);
|
|
3031
|
+
markRunning(guarding);
|
|
3032
|
+
// Guards have working too: the claims this one makes, ticking off as
|
|
3033
|
+
// each is checked. Shown the same way a screen's steps are.
|
|
3034
|
+
openWork(guarding, true);
|
|
2507
3035
|
return;
|
|
2508
3036
|
}
|
|
2509
3037
|
|
|
@@ -2514,7 +3042,14 @@ const SCRIPT = `
|
|
|
2514
3042
|
}
|
|
2515
3043
|
|
|
2516
3044
|
if (type === 'run:done') {
|
|
3045
|
+
// Nothing is happening any more, so nothing may still say it is.
|
|
3046
|
+
for (var q = 0; q < order.length; q++) {
|
|
3047
|
+
order[q].running = false;
|
|
3048
|
+
hush(order[q]);
|
|
3049
|
+
syncWork(order[q]);
|
|
3050
|
+
}
|
|
2517
3051
|
runningItem = null;
|
|
3052
|
+
runOver = true;
|
|
2518
3053
|
ui.follow.hidden = true;
|
|
2519
3054
|
scanning(false);
|
|
2520
3055
|
var summary = ev.summary || null;
|
|
@@ -2561,9 +3096,16 @@ const SCRIPT = `
|
|
|
2561
3096
|
// picture that is already decoded and the swap never flashes.
|
|
2562
3097
|
preloadAll(entry.pics);
|
|
2563
3098
|
}
|
|
2564
|
-
// What was actually done to this
|
|
2565
|
-
// the one
|
|
2566
|
-
|
|
3099
|
+
// What was actually done to this check. The list that comes with the
|
|
3100
|
+
// verdict is the one that is right — a watcher that opened halfway through,
|
|
3101
|
+
// or missed an event, ends up correct here whatever it saw on the way.
|
|
3102
|
+
entry.running = false;
|
|
3103
|
+
if (Array.isArray(ev.checks)) adopt(entry, ev.checks);
|
|
3104
|
+
// Anything the run left mid-flight stops looking like it is still going.
|
|
3105
|
+
hush(entry);
|
|
3106
|
+
// And a guard that broke shows the claim it broke on, under every claim
|
|
3107
|
+
// that held before it.
|
|
3108
|
+
if (kind === 'guard' && ev.status === 'failed') markFailedClaim(entry, ev.failedAt);
|
|
2567
3109
|
entry.outText = outcomeText(kind, ev);
|
|
2568
3110
|
if (entry.verdict) entry.verdict.textContent = shortOutcome(kind, ev);
|
|
2569
3111
|
entry.failedAt = (kind === 'guard' && ev.status === 'failed' && ev.failedAt) ? ev.failedAt : '';
|
|
@@ -2571,6 +3113,7 @@ const SCRIPT = `
|
|
|
2571
3113
|
if (typeof ev.durationMs === 'number') tweenTo(entry.time, ev.durationMs, fmt);
|
|
2572
3114
|
entry.tone = tone;
|
|
2573
3115
|
redraw(entry);
|
|
3116
|
+
syncWork(entry);
|
|
2574
3117
|
setTone(entry, tone);
|
|
2575
3118
|
// Only the checks that want a person open themselves. Everything else stays
|
|
2576
3119
|
// one line, which is what keeps a clean run quiet. A row that has just
|
|
@@ -2633,7 +3176,10 @@ const SCRIPT = `
|
|
|
2633
3176
|
handle(ev);
|
|
2634
3177
|
} catch (err) {
|
|
2635
3178
|
// A watch window must never be the reason a run looks broken. Whatever
|
|
2636
|
-
// this event was, the next one still has to land.
|
|
3179
|
+
// this event was, the next one still has to land. It is still said out
|
|
3180
|
+
// loud where anyone would look for it — a panel that swallows its own
|
|
3181
|
+
// mistakes in silence is a panel nobody can mend.
|
|
3182
|
+
if (window.console && console.error) console.error('stays fixed: ' + (err && err.message ? err.message : err));
|
|
2637
3183
|
}
|
|
2638
3184
|
};
|
|
2639
3185
|
|
|
@@ -2642,6 +3188,11 @@ const SCRIPT = `
|
|
|
2642
3188
|
window.__staysfixed_detach = function () {
|
|
2643
3189
|
stopClock();
|
|
2644
3190
|
scanning(false);
|
|
3191
|
+
if (runningItem) {
|
|
3192
|
+
runningItem.running = false;
|
|
3193
|
+
hush(runningItem);
|
|
3194
|
+
syncWork(runningItem);
|
|
3195
|
+
}
|
|
2645
3196
|
runningItem = null;
|
|
2646
3197
|
};
|
|
2647
3198
|
|