@graphty/visual-review 0.2.1 → 0.2.2

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.
@@ -8,6 +8,8 @@
8
8
  --ok: #1d7a3a;
9
9
  --bad: #b3261e;
10
10
  --warn: #8a5a00;
11
+ --wait: #f0f3f7;
12
+ --on-accent: #ffffff;
11
13
  color-scheme: light dark;
12
14
  }
13
15
 
@@ -22,6 +24,9 @@
22
24
  --ok: #5cc27d;
23
25
  --bad: #ff7a70;
24
26
  --warn: #e0b04c;
27
+ --wait: #262a30;
28
+ /* Dark text on the light blue: white on it is 2.5:1, this is over 7:1. */
29
+ --on-accent: #16181c;
25
30
  }
26
31
  }
27
32
 
@@ -29,15 +34,27 @@
29
34
  box-sizing: border-box;
30
35
  }
31
36
 
32
- /* The page is one screen: the header on top, and main scrolling under it. */
37
+ /*
38
+ * The page is one screen that never scrolls: the sticky stack (header, the screen's own bar, the
39
+ * status row) on top, and main under it. On the story screen only the panes scroll, so nothing
40
+ * (iPad Safari's on-screen keyboard included) can push the decision buttons off the screen.
41
+ */
42
+ html,
43
+ body {
44
+ height: 100%;
45
+ overflow: hidden;
46
+ }
47
+
33
48
  body {
34
49
  display: flex;
35
50
  flex-direction: column;
36
51
  height: 100vh;
37
52
  height: 100dvh;
38
53
  margin: 0;
54
+ padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
55
+ /* In rem, so the browser's own text size setting scales the page. */
39
56
  font:
40
- 14px/1.4 system-ui,
57
+ 0.875rem/1.4 system-ui,
41
58
  sans-serif;
42
59
  background: var(--bg);
43
60
  color: var(--fg);
@@ -45,30 +62,166 @@ body {
45
62
 
46
63
  header {
47
64
  display: flex;
48
- gap: 16px;
65
+ gap: 10px;
49
66
  align-items: center;
50
- padding: 8px 16px;
67
+ min-height: 3.15em;
68
+ padding: max(4px, env(safe-area-inset-top)) 16px 4px;
51
69
  border-bottom: 1px solid var(--line);
52
70
  background: var(--card);
53
71
  flex: none;
54
72
  }
55
73
 
74
+ header h1 {
75
+ margin: 0;
76
+ font-size: inherit;
77
+ flex: none;
78
+ }
79
+
56
80
  #home {
57
81
  font-weight: 700;
82
+ white-space: nowrap;
58
83
  }
59
84
 
60
- #home,
85
+ .sr-only {
86
+ position: absolute;
87
+ width: 1px;
88
+ height: 1px;
89
+ overflow: hidden;
90
+ clip-path: inset(50%);
91
+ white-space: nowrap;
92
+ }
93
+
94
+ #keys-button,
61
95
  #copy-link {
62
96
  flex: none;
63
97
  white-space: nowrap;
64
98
  }
65
99
 
66
- #status {
100
+ /* The pickers shrink first when the window is narrow; a native menu truncates its own label. */
101
+ .picker {
102
+ display: flex;
103
+ gap: 4px;
104
+ align-items: center;
105
+ flex: 0 1 16em;
106
+ min-width: 5em;
107
+ color: var(--muted);
108
+ }
109
+
110
+ .picker select {
111
+ min-width: 0;
112
+ width: 100%;
113
+ flex: 1 1 auto;
114
+ }
115
+
116
+ .picker[hidden] {
117
+ display: none;
118
+ }
119
+
120
+ /* Finish never shrinks: the pickers give up their width first, so its count is always whole. */
121
+ #finish-slot {
122
+ display: flex;
123
+ gap: 8px;
124
+ align-items: center;
67
125
  margin-left: auto;
126
+ flex: none;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ #finish-slot .finish {
131
+ flex: none;
132
+ }
133
+
134
+ /* Narrow: the pickers' labels and the tail of Finish's reason keep their text for screen readers
135
+ (a press on Finish says the reason too), and give the width to the menus. */
136
+ @media (max-width: 1049.98px) {
137
+ .picker > span,
138
+ #finish-slot .wide-only {
139
+ position: absolute;
140
+ width: 1px;
141
+ height: 1px;
142
+ overflow: hidden;
143
+ clip-path: inset(50%);
144
+ }
145
+
146
+ /* An iPad held upright, Split View or a zoomed page: the menus take a row of their own, half
147
+ each, so a project's name is readable; nothing in the header runs past the screen's edge. */
148
+ header {
149
+ flex-wrap: wrap;
150
+ row-gap: 6px;
151
+ }
152
+
153
+ .picker {
154
+ order: 1;
155
+ flex: 1 1 calc(50% - 5px);
156
+ }
157
+ }
158
+
159
+ #bar {
160
+ flex: none;
161
+ background: var(--card);
162
+ border-bottom: 1px solid var(--line);
163
+ }
164
+
165
+ #bar:empty {
166
+ display: none;
167
+ }
168
+
169
+ /* One line tall from 1050 px, two below, whatever it says: a message never moves anything. */
170
+ #status-row {
171
+ flex: none;
172
+ display: flex;
173
+ gap: 6px;
174
+ align-items: flex-start;
175
+ height: calc(1.4em + 10px);
176
+ padding: 5px 16px;
177
+ overflow: hidden;
68
178
  color: var(--muted);
179
+ border-bottom: 1px solid var(--line);
180
+ }
181
+
182
+ #status-row > span {
183
+ min-width: 0;
184
+ }
185
+
186
+ #elapsed,
187
+ #status-more {
188
+ flex: none;
189
+ }
190
+
191
+ /* More: the whole message, however long, until the next one. */
192
+ #status-row.open {
193
+ height: auto;
194
+ }
195
+
196
+ #status-row.open > span {
197
+ -webkit-line-clamp: unset;
198
+ line-clamp: unset;
199
+ }
200
+
201
+ #status-row > span {
202
+ display: -webkit-box;
203
+ -webkit-box-orient: vertical;
204
+ -webkit-line-clamp: 1;
205
+ line-clamp: 1;
206
+ overflow: hidden;
207
+ }
208
+
209
+ #status-row > span:empty {
210
+ display: none;
69
211
  }
70
212
 
71
- #status.error,
213
+ @media (max-width: 1049.98px) {
214
+ #status-row {
215
+ height: calc(2.8em + 10px);
216
+ }
217
+
218
+ #status-row > span {
219
+ -webkit-line-clamp: 2;
220
+ line-clamp: 2;
221
+ }
222
+ }
223
+
224
+ #alert,
72
225
  .error {
73
226
  color: var(--bad);
74
227
  }
@@ -80,18 +233,56 @@ main {
80
233
  padding: 16px;
81
234
  }
82
235
 
236
+ main.story-screen {
237
+ overflow: hidden;
238
+ padding: 0 16px 8px;
239
+ }
240
+
83
241
  a {
84
242
  color: var(--accent);
85
243
  }
86
244
 
87
- button {
245
+ button,
246
+ select,
247
+ input {
88
248
  font: inherit;
249
+ }
250
+
251
+ button,
252
+ select {
253
+ min-height: 2.15em;
89
254
  padding: 4px 10px;
90
255
  border: 1px solid var(--line);
91
256
  border-radius: 4px;
92
257
  background: var(--card);
93
258
  color: var(--fg);
94
259
  cursor: pointer;
260
+ touch-action: manipulation;
261
+ }
262
+
263
+ input[type="text"],
264
+ input[type="search"] {
265
+ padding: 4px 8px;
266
+ border: 1px solid var(--line);
267
+ border-radius: 4px;
268
+ background: var(--card);
269
+ color: var(--fg);
270
+ min-height: 2.15em;
271
+ }
272
+
273
+ :focus-visible {
274
+ outline: 2px solid var(--accent);
275
+ outline-offset: 3px;
276
+ }
277
+
278
+ kbd {
279
+ font: inherit;
280
+ font-size: 0.85em;
281
+ padding: 0 4px;
282
+ margin-left: 4px;
283
+ border: 1px solid var(--line);
284
+ border-radius: 3px;
285
+ color: var(--muted);
95
286
  }
96
287
 
97
288
  button[aria-pressed="true"] {
@@ -99,53 +290,254 @@ button[aria-pressed="true"] {
99
290
  box-shadow: inset 0 0 0 1px var(--accent);
100
291
  }
101
292
 
102
- button:disabled {
103
- opacity: 0.5;
104
- cursor: default;
105
- }
106
-
107
293
  button.link {
108
294
  border: none;
109
295
  background: none;
110
296
  padding: 0;
297
+ min-height: 0;
111
298
  color: var(--accent);
112
299
  }
113
300
 
114
301
  button.primary {
115
302
  background: var(--accent);
116
303
  border-color: var(--accent);
117
- color: #fff;
304
+ color: var(--on-accent);
118
305
  }
119
306
 
120
- /* The page's own confirmation (ask in review.js), in place of the browser's confirm(). */
121
- dialog.ask {
122
- max-width: min(640px, calc(100vw - 32px));
307
+ button.primary[aria-disabled="true"],
308
+ button.primary:disabled {
309
+ background: var(--card);
310
+ color: var(--muted);
311
+ border-color: var(--line);
312
+ }
313
+
314
+ button.accept {
315
+ border-color: var(--ok);
316
+ color: var(--ok);
317
+ }
318
+
319
+ button.reject {
320
+ border-color: var(--bad);
321
+ color: var(--bad);
322
+ }
323
+
324
+ /* Unavailable: muted text at full opacity (4.5:1), never removed, so nothing beside it moves. */
325
+ button:disabled,
326
+ button[aria-disabled="true"],
327
+ button.accept[aria-disabled="true"],
328
+ button.reject[aria-disabled="true"] {
329
+ color: var(--muted);
330
+ border-color: var(--line);
331
+ border-style: dashed;
332
+ background: transparent;
333
+ cursor: default;
334
+ }
335
+
336
+ button[aria-disabled="true"][aria-pressed="true"] {
337
+ color: var(--fg);
338
+ border-color: var(--accent);
339
+ }
340
+
341
+ /* A Reject or Exclude waiting for its reason. */
342
+ button.waiting {
343
+ outline: 2px dashed currentColor;
344
+ outline-offset: -5px;
345
+ }
346
+
347
+ .spinner {
348
+ display: inline-block;
349
+ width: 0.9em;
350
+ height: 0.9em;
351
+ margin-right: 6px;
352
+ vertical-align: -0.1em;
353
+ border: 2px solid currentColor;
354
+ border-right-color: transparent;
355
+ border-radius: 50%;
356
+ animation: spin 0.8s linear infinite;
357
+ }
358
+
359
+ @keyframes spin {
360
+ to {
361
+ transform: rotate(360deg);
362
+ }
363
+ }
364
+
365
+ @media (prefers-reduced-motion: reduce) {
366
+ .spinner,
367
+ .placeholder {
368
+ animation: none;
369
+ }
370
+ }
371
+
372
+ /* The page's own dialogs: questions (ask in review.js), Finish's sheet and the key overlay. */
373
+ dialog.ask,
374
+ dialog.keys {
375
+ max-width: min(680px, calc(100vw - 32px));
376
+ max-height: calc(100dvh - 32px);
123
377
  border: 1px solid var(--line);
124
378
  border-radius: 6px;
125
379
  background: var(--card);
126
380
  color: var(--fg);
381
+ padding: 16px;
127
382
  }
128
383
 
129
- dialog.ask p:first-child {
384
+ dialog[open].ask,
385
+ dialog[open].keys {
386
+ display: flex;
387
+ flex-direction: column;
388
+ }
389
+
390
+ .ask-text {
130
391
  white-space: pre-wrap;
392
+ overflow: auto;
393
+ min-height: 0;
394
+ }
395
+
396
+ .ask-text p {
397
+ margin: 0 0 8px;
131
398
  }
132
399
 
133
- dialog.ask .actions {
400
+ .ask-text h2 {
401
+ font-size: 1rem;
402
+ margin: 0 0 8px;
403
+ }
404
+
405
+ .ask .actions {
134
406
  display: flex;
135
407
  gap: 8px;
136
408
  justify-content: flex-end;
409
+ flex: none;
410
+ margin: 8px 0 0;
137
411
  }
138
412
 
139
- button.accept {
140
- border-color: var(--ok);
141
- color: var(--ok);
413
+ /*
414
+ * The wait box: a wait that blocks what the reader can do, in the middle of the screen over
415
+ * everything. One width, and every line keeps its height whether it says anything or not, so
416
+ * nothing in the box moves while its counts and its time change.
417
+ */
418
+ dialog.wait {
419
+ width: min(480px, calc(100vw - 32px));
420
+ max-height: calc(100dvh - 32px);
421
+ margin: auto;
422
+ border: 1px solid var(--line);
423
+ border-radius: 8px;
424
+ background: var(--card);
425
+ color: var(--fg);
426
+ padding: 20px;
427
+ font-size: 1.05rem;
428
+ box-shadow: 0 8px 32px rgb(0 0 0 / 0.3);
142
429
  }
143
430
 
144
- button.reject {
145
- border-color: var(--bad);
431
+ dialog.wait::backdrop {
432
+ background: rgb(0 0 0 / 0.45);
433
+ }
434
+
435
+ dialog.wait[open] {
436
+ display: flex;
437
+ flex-direction: column;
438
+ gap: 8px;
439
+ }
440
+
441
+ dialog.wait h2 {
442
+ font-size: 1.2rem;
443
+ margin: 0;
444
+ }
445
+
446
+ dialog.wait p {
447
+ margin: 0;
448
+ min-height: 1.4em;
449
+ overflow-wrap: anywhere;
450
+ }
451
+
452
+ /* A retry under way and a failure never show together: they share one two-line place. */
453
+ #wait-notes {
454
+ display: grid;
455
+ min-height: 2.8em;
456
+ }
457
+
458
+ #wait-notes > p {
459
+ grid-area: 1 / 1;
460
+ }
461
+
462
+ #wait-net {
463
+ color: var(--warn);
464
+ }
465
+
466
+ #wait-error {
146
467
  color: var(--bad);
147
468
  }
148
469
 
470
+ /* The box itself takes focus so that no key presses Cancel by accident; it needs no ring. */
471
+ dialog.wait:focus {
472
+ outline: none;
473
+ }
474
+
475
+ #wait-body {
476
+ overflow: auto;
477
+ min-height: 0;
478
+ }
479
+
480
+ #wait-body ol {
481
+ margin: 0;
482
+ padding-left: 20px;
483
+ }
484
+
485
+ #wait-body li.done {
486
+ color: var(--muted);
487
+ }
488
+
489
+ #wait-body li.now {
490
+ font-weight: 600;
491
+ }
492
+
493
+ #wait-bar {
494
+ width: 100%;
495
+ height: 10px;
496
+ }
497
+
498
+ /* A box that failed keeps its bar's place, empty. */
499
+ dialog.wait.failed #wait-bar {
500
+ visibility: hidden;
501
+ }
502
+
503
+ dialog.wait .actions {
504
+ display: flex;
505
+ gap: 8px;
506
+ align-items: center;
507
+ justify-content: flex-end;
508
+ margin-top: 4px;
509
+ }
510
+
511
+ /* An action that does not apply keeps its place, so the box never changes size. */
512
+ dialog.wait .actions button.unused {
513
+ visibility: hidden;
514
+ }
515
+
516
+ #wait-elapsed {
517
+ margin-right: auto;
518
+ color: var(--muted);
519
+ font-variant-numeric: tabular-nums;
520
+ }
521
+
522
+ /* Finish's notes scroll inside the sheet; the summary above and the buttons below stay put. */
523
+ .notes {
524
+ max-height: 30vh;
525
+ overflow: auto;
526
+ margin: 0 0 8px;
527
+ padding-left: 20px;
528
+ }
529
+
530
+ .keys table td:first-child {
531
+ white-space: nowrap;
532
+ font-weight: 600;
533
+ }
534
+
535
+ .keys .recent {
536
+ max-height: 30vh;
537
+ overflow: auto;
538
+ color: var(--muted);
539
+ }
540
+
149
541
  .card {
150
542
  background: var(--card);
151
543
  border: 1px solid var(--line);
@@ -156,7 +548,12 @@ button.reject {
156
548
 
157
549
  .card h2 {
158
550
  margin: 0 0 4px;
159
- font-size: 16px;
551
+ font-size: 1rem;
552
+ }
553
+
554
+ .card.placeholder {
555
+ min-height: 140px;
556
+ background: var(--wait);
160
557
  }
161
558
 
162
559
  .meta {
@@ -164,6 +561,14 @@ button.reject {
164
561
  margin: 4px 0;
165
562
  }
166
563
 
564
+ .listline {
565
+ display: flex;
566
+ gap: 12px;
567
+ align-items: center;
568
+ margin: 0 0 12px;
569
+ color: var(--muted);
570
+ }
571
+
167
572
  table {
168
573
  border-collapse: collapse;
169
574
  width: 100%;
@@ -182,8 +587,9 @@ td {
182
587
  margin: 0 4px;
183
588
  border: 1px solid var(--line);
184
589
  border-radius: 10px;
185
- font-size: 12px;
590
+ font-size: 0.75rem;
186
591
  font-weight: 400;
592
+ white-space: nowrap;
187
593
  }
188
594
 
189
595
  .badge.warn,
@@ -193,35 +599,135 @@ td {
193
599
  color: var(--warn);
194
600
  }
195
601
 
196
- .badge.accept,
197
- .badge.new {
198
- border-color: var(--ok);
199
- color: var(--ok);
602
+ .badge.accept,
603
+ .badge.new {
604
+ border-color: var(--ok);
605
+ color: var(--ok);
606
+ }
607
+
608
+ .badge.reject,
609
+ .badge.removed {
610
+ border-color: var(--bad);
611
+ color: var(--bad);
612
+ }
613
+
614
+ .badge.changed,
615
+ .badge.moved,
616
+ .badge.exclude {
617
+ border-color: var(--accent);
618
+ color: var(--accent);
619
+ }
620
+
621
+ .warning {
622
+ color: var(--warn);
623
+ margin: 4px 0;
624
+ }
625
+
626
+ .spacer {
627
+ flex: 1;
628
+ }
629
+
630
+ /* ---------------------------------------------------------------- the grid */
631
+
632
+ .gridbar {
633
+ display: flex;
634
+ gap: 8px;
635
+ align-items: center;
636
+ padding: 6px 16px;
637
+ min-height: calc(2.15em + 14px);
638
+ white-space: nowrap;
639
+ }
640
+
641
+ .gridbar > .row > *,
642
+ .gridbar [role="group"] > * {
643
+ flex: none;
644
+ }
645
+
646
+ #more-filters {
647
+ flex: 0 1 auto;
648
+ min-width: 7em;
649
+ }
650
+
651
+ .gridbar .row {
652
+ display: contents;
653
+ }
654
+
655
+ .gridbar [role="group"] {
656
+ display: flex;
657
+ gap: 4px;
658
+ align-items: center;
659
+ }
660
+
661
+ .gridbar label {
662
+ display: flex;
663
+ gap: 6px;
664
+ align-items: center;
665
+ color: var(--muted);
666
+ }
667
+
668
+ #find {
669
+ width: 14em;
200
670
  }
201
671
 
202
- .badge.reject,
203
- .badge.removed {
204
- border-color: var(--bad);
205
- color: var(--bad);
672
+ /* Two rows below 1300 px (an iPad either way up): what to look at, then what to do. */
673
+ @media (max-width: 1299.98px) {
674
+ .gridbar {
675
+ flex-direction: column;
676
+ align-items: stretch;
677
+ }
678
+
679
+ .gridbar .row {
680
+ display: flex;
681
+ flex-wrap: wrap;
682
+ gap: 8px;
683
+ align-items: center;
684
+ }
685
+
686
+ #find {
687
+ width: 100%;
688
+ min-width: 8em;
689
+ }
690
+
691
+ .gridbar > .row > label {
692
+ flex: 1 1 auto;
693
+ min-width: 0;
694
+ }
206
695
  }
207
696
 
208
- .badge.changed,
209
- .badge.moved,
210
- .badge.exclude {
211
- border-color: var(--accent);
212
- color: var(--accent);
697
+ /* A small menu: a native details element, opened by its summary. */
698
+ details.menu {
699
+ position: relative;
213
700
  }
214
701
 
215
- .toolbar {
702
+ details.menu > summary {
703
+ list-style: none;
704
+ padding: 4px 10px;
705
+ min-height: 2.15em;
216
706
  display: flex;
217
- flex-wrap: wrap;
218
- gap: 8px;
219
707
  align-items: center;
220
- margin-bottom: 12px;
708
+ border: 1px solid var(--line);
709
+ border-radius: 4px;
710
+ cursor: pointer;
221
711
  }
222
712
 
223
- .spacer {
224
- flex: 1;
713
+ details.menu > summary::-webkit-details-marker {
714
+ display: none;
715
+ }
716
+
717
+ details.menu > div {
718
+ position: absolute;
719
+ right: 0;
720
+ top: calc(100% + 4px);
721
+ z-index: 10;
722
+ display: flex;
723
+ flex-direction: column;
724
+ gap: 4px;
725
+ min-width: 14em;
726
+ padding: 8px;
727
+ background: var(--card);
728
+ border: 1px solid var(--line);
729
+ border-radius: 6px;
730
+ box-shadow: 0 4px 16px #0003;
225
731
  }
226
732
 
227
733
  .component {
@@ -233,7 +739,11 @@ td {
233
739
  gap: 12px;
234
740
  align-items: center;
235
741
  margin: 0 0 8px;
236
- font-size: 15px;
742
+ font-size: 0.9375rem;
743
+ }
744
+
745
+ .component h3 button {
746
+ font-weight: 400;
237
747
  }
238
748
 
239
749
  .stories {
@@ -251,7 +761,7 @@ td {
251
761
  }
252
762
 
253
763
  .story-name {
254
- font-size: 12px;
764
+ font-size: 0.75rem;
255
765
  font-weight: 600;
256
766
  margin-bottom: 4px;
257
767
  word-break: break-all;
@@ -273,16 +783,26 @@ td {
273
783
  }
274
784
 
275
785
  .tile img,
276
- .tile .noimage {
786
+ .tile .noimage,
787
+ .tile .thumb-wait {
277
788
  width: 100%;
278
789
  aspect-ratio: 4 / 3;
279
790
  object-fit: contain;
280
791
  background: repeating-conic-gradient(#8882 0 25%, transparent 0 50%) 0 0 / 16px 16px;
281
792
  }
282
793
 
283
- .tile .noimage {
794
+ .tile .noimage,
795
+ .tile .thumb-wait {
284
796
  display: grid;
285
797
  place-items: center;
798
+ color: var(--muted);
799
+ }
800
+
801
+ .tile .thumb-wait {
802
+ background: var(--wait);
803
+ }
804
+
805
+ .tile .noimage {
286
806
  color: var(--warn);
287
807
  }
288
808
 
@@ -303,7 +823,7 @@ td {
303
823
  flex-wrap: wrap;
304
824
  gap: 2px 6px;
305
825
  align-items: baseline;
306
- font-size: 12px;
826
+ font-size: 0.75rem;
307
827
  font-weight: 600;
308
828
  }
309
829
 
@@ -328,14 +848,14 @@ td {
328
848
 
329
849
  .decision button {
330
850
  padding: 0 8px;
331
- font-size: 12px;
851
+ min-height: 0;
852
+ font-size: 0.75rem;
332
853
  font-weight: 400;
333
854
  }
334
855
 
335
856
  .decision .meta {
336
857
  margin: 0;
337
858
  font-weight: 400;
338
- white-space: nowrap;
339
859
  }
340
860
 
341
861
  .error-list .decision .what {
@@ -344,7 +864,7 @@ td {
344
864
  }
345
865
 
346
866
  .tile .name {
347
- font-size: 12px;
867
+ font-size: 0.75rem;
348
868
  word-break: break-all;
349
869
  }
350
870
 
@@ -353,11 +873,11 @@ td {
353
873
  font-variant-numeric: tabular-nums;
354
874
  }
355
875
 
356
- /* The story last opened, when the grid comes back. */
876
+ /* The story last opened, when the grid comes back: flush, unlike the focus ring. */
357
877
  .tile.current,
358
878
  .error-list li.current {
359
879
  outline: 3px solid var(--accent);
360
- outline-offset: 2px;
880
+ outline-offset: 0;
361
881
  opacity: 1;
362
882
  }
363
883
 
@@ -368,9 +888,10 @@ td {
368
888
  margin-bottom: 20px;
369
889
  }
370
890
 
371
- .errors h3 {
891
+ .errors > summary {
372
892
  color: var(--bad);
373
- margin: 0 0 4px;
893
+ font-weight: 600;
894
+ cursor: pointer;
374
895
  }
375
896
 
376
897
  .error-list li {
@@ -381,38 +902,296 @@ td {
381
902
  color: var(--bad);
382
903
  }
383
904
 
384
- #filter,
385
- #goto {
386
- min-width: 220px;
387
- font: inherit;
388
- padding: 4px 8px;
389
- border: 1px solid var(--line);
390
- border-radius: 4px;
391
- background: var(--card);
392
- color: var(--fg);
905
+ .tile .moved-from {
906
+ font-size: 0.75rem;
907
+ word-break: break-all;
908
+ color: var(--accent);
393
909
  }
394
910
 
395
- /* The story screen fills main: its controls, then the two panes taking all the height left. */
396
- .story-view {
911
+ /* ---------------------------------------------------------------- the story */
912
+
913
+ /*
914
+ * The decision bar: every button always present, each in a cell of fixed width, so a button's
915
+ * box is the same on every item. One row from 1280 px; below, the cells share the window's width
916
+ * (never more), in two rows, or three (decisions, movement, the note) on an iPad held upright and
917
+ * below 900 px, and four columns below 600 px (Split View, a zoomed page), so Undo and the note
918
+ * box are always on screen.
919
+ */
920
+ .decisionbar {
921
+ display: grid;
922
+ grid-template-columns: 6.2em 6em 12em 6em 7em 7em 7.5em 6.5em minmax(8em, 1fr);
923
+ grid-template-areas: "grid prev pos next accept reject exclude undo note";
924
+ gap: 6px;
925
+ align-items: center;
926
+ padding: 4px 16px;
927
+ }
928
+
929
+ .decisionbar > button {
930
+ width: 100%;
931
+ min-height: 44px;
932
+ white-space: nowrap;
933
+ overflow: hidden;
934
+ text-overflow: ellipsis;
935
+ }
936
+
937
+ #to-grid {
938
+ grid-area: grid;
939
+ }
940
+
941
+ #prev {
942
+ grid-area: prev;
943
+ }
944
+
945
+ #position {
946
+ grid-area: pos;
947
+ overflow: hidden;
948
+ white-space: nowrap;
949
+ text-overflow: ellipsis;
950
+ text-align: center;
951
+ font-variant-numeric: tabular-nums;
952
+ }
953
+
954
+ #next {
955
+ grid-area: next;
956
+ }
957
+
958
+ #accept {
959
+ grid-area: accept;
960
+ }
961
+
962
+ #reject {
963
+ grid-area: reject;
964
+ }
965
+
966
+ #exclude {
967
+ grid-area: exclude;
968
+ }
969
+
970
+ #undo {
971
+ grid-area: undo;
972
+ }
973
+
974
+ .notebox {
975
+ grid-area: note;
397
976
  display: flex;
398
- flex-direction: column;
399
- height: 100%;
977
+ gap: 6px;
978
+ align-items: center;
979
+ min-width: 0;
980
+ }
981
+
982
+ .notebox label {
983
+ color: var(--muted);
984
+ }
985
+
986
+ #note {
987
+ flex: 1;
988
+ min-width: 0;
989
+ min-height: 44px;
990
+ }
991
+
992
+ @media (max-width: 1279.98px) {
993
+ .decisionbar {
994
+ grid-template-columns: repeat(6, minmax(0, 1fr));
995
+ grid-template-areas:
996
+ "grid accept reject exclude undo ."
997
+ "prev pos pos next note note";
998
+ }
999
+ }
1000
+
1001
+ /* Three rows on an iPad held upright, or any window under 900 px: a short landscape window keeps
1002
+ two, so its panes keep their height. */
1003
+ @media (max-width: 899.98px), (max-width: 1049.98px) and (orientation: portrait) {
1004
+ .decisionbar {
1005
+ grid-template-columns: repeat(5, minmax(0, 1fr));
1006
+ grid-template-areas:
1007
+ "grid accept reject exclude undo"
1008
+ "prev pos pos pos next"
1009
+ "note note note note note";
1010
+ }
1011
+ }
1012
+
1013
+ @media (max-width: 599.98px) {
1014
+ .decisionbar {
1015
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1016
+ grid-template-areas:
1017
+ "accept reject exclude undo"
1018
+ "grid prev pos next"
1019
+ "note note note note";
1020
+ }
1021
+
1022
+ /* The keys are in the Keys list; here the words need the width. */
1023
+ .decisionbar > button {
1024
+ padding: 4px;
1025
+ }
1026
+
1027
+ .decisionbar > button kbd {
1028
+ display: none;
1029
+ }
1030
+ }
1031
+
1032
+ /* Images loading: the spinner sits in Accept's left padding, so its label and key chip keep
1033
+ their place and are never cut. */
1034
+ .decisionbar > button {
1035
+ position: relative;
1036
+ }
1037
+
1038
+ .decisionbar > button > .spinner {
1039
+ position: absolute;
1040
+ left: 3px;
1041
+ top: calc(50% - 0.35em);
1042
+ width: 0.7em;
1043
+ height: 0.7em;
1044
+ margin: 0;
1045
+ border-width: 1.5px;
400
1046
  }
401
1047
 
402
- .story-view > * {
1048
+ /* The item line: two lines, clamped; tapping it shows it all (the reader's choice to move the panes). */
1049
+ .itemline {
403
1050
  flex: none;
1051
+ height: calc(2 * 1.5em + 8px);
1052
+ padding: 4px 0;
1053
+ overflow: hidden;
1054
+ cursor: pointer;
404
1055
  }
405
1056
 
406
- .story-view h2 {
407
- margin: 0 0 4px;
408
- font-size: 18px;
1057
+ .itemline.open {
1058
+ height: auto;
1059
+ max-height: 40vh;
1060
+ overflow: auto;
1061
+ }
1062
+
1063
+ .itemline h2 {
1064
+ margin: 0;
1065
+ font-size: 0.9375rem;
1066
+ line-height: 1.5em;
1067
+ white-space: nowrap;
1068
+ overflow: hidden;
1069
+ text-overflow: ellipsis;
1070
+ }
1071
+
1072
+ .itemline p {
1073
+ margin: 0;
1074
+ line-height: 1.5em;
1075
+ color: var(--muted);
1076
+ white-space: nowrap;
1077
+ overflow: hidden;
1078
+ text-overflow: ellipsis;
1079
+ }
1080
+
1081
+ .itemline.open h2,
1082
+ .itemline.open p {
1083
+ white-space: normal;
1084
+ }
1085
+
1086
+ /* The view bar: one row from 1280 px; below (an iPad either way up) always two, the views on the
1087
+ first, so the panes start at the same height whichever view and options an item shows. */
1088
+ .viewbar {
1089
+ flex: none;
1090
+ display: flex;
1091
+ flex-wrap: wrap;
1092
+ gap: 6px 8px;
1093
+ align-items: center;
1094
+ padding: 2px 0 8px;
1095
+ white-space: nowrap;
1096
+ }
1097
+
1098
+ /* A finger needs 44 px: every control, in every bar and dialog. */
1099
+ @media (pointer: coarse) {
1100
+ button,
1101
+ button.link,
1102
+ select,
1103
+ input[type="text"],
1104
+ input[type="search"],
1105
+ summary,
1106
+ details.menu > summary {
1107
+ min-height: 44px;
1108
+ }
1109
+ }
1110
+
1111
+ .viewbar [role="group"] {
1112
+ display: flex;
1113
+ flex: none;
1114
+ }
1115
+
1116
+ .viewbar [role="group"] > button {
1117
+ border-radius: 0;
1118
+ margin-left: -1px;
1119
+ }
1120
+
1121
+ .viewbar [role="group"] > button:first-child {
1122
+ border-radius: 4px 0 0 4px;
1123
+ margin-left: 0;
1124
+ }
1125
+
1126
+ .viewbar [role="group"] > button:last-child {
1127
+ border-radius: 0 4px 4px 0;
1128
+ }
1129
+
1130
+ .viewbar > * {
1131
+ flex: none;
1132
+ }
1133
+
1134
+ .viewbar > .row-break {
1135
+ display: none;
1136
+ }
1137
+
1138
+ /* Fixed widths for what changes between items and views: the bar wraps the same way on all. */
1139
+ .viewbar > .view-option {
1140
+ width: 10.5em;
1141
+ }
1142
+
1143
+ .viewbar > #box-count {
1144
+ width: 5.5em;
1145
+ }
1146
+
1147
+ #ask-alert:empty {
1148
+ display: none;
1149
+ }
1150
+
1151
+ @media (max-width: 1279.98px) {
1152
+ .viewbar > .row-break {
1153
+ display: block;
1154
+ flex-basis: 100%;
1155
+ height: 0;
1156
+ }
1157
+ }
1158
+
1159
+ .details-pop {
1160
+ position: relative;
1161
+ }
1162
+
1163
+ .details-pop > div {
1164
+ position: fixed;
1165
+ right: 16px;
1166
+ z-index: 10;
1167
+ max-width: min(640px, calc(100vw - 32px));
1168
+ max-height: 50vh;
1169
+ overflow: auto;
1170
+ padding: 8px 12px;
1171
+ white-space: normal;
1172
+ background: var(--card);
1173
+ border: 1px solid var(--line);
1174
+ border-radius: 6px;
1175
+ box-shadow: 0 4px 16px #0003;
1176
+ }
1177
+
1178
+ .details-pop > summary {
1179
+ cursor: pointer;
1180
+ padding: 4px 6px;
1181
+ }
1182
+
1183
+ /* The story screen fills main: item line, view bar, then the panes taking all the height left. */
1184
+ .story-view {
1185
+ display: flex;
1186
+ flex-direction: column;
1187
+ height: 100%;
409
1188
  }
410
1189
 
411
1190
  .stage {
412
1191
  display: flex;
413
1192
  gap: 16px;
414
1193
  flex: 1 1 0;
415
- min-height: 200px;
1194
+ min-height: 120px;
416
1195
  }
417
1196
 
418
1197
  /* Two panes of the same size, whatever each holds. */
@@ -443,6 +1222,25 @@ td {
443
1222
  border-style: dashed;
444
1223
  }
445
1224
 
1225
+ .frame .wait,
1226
+ .frame .failed {
1227
+ margin: auto;
1228
+ padding: 16px;
1229
+ text-align: center;
1230
+ color: var(--muted);
1231
+ }
1232
+
1233
+ .frame .failed {
1234
+ color: var(--bad);
1235
+ }
1236
+
1237
+ .frame pre.console {
1238
+ margin: 0;
1239
+ flex: 1;
1240
+ overflow: auto;
1241
+ border: none;
1242
+ }
1243
+
446
1244
  /* Centred while it fits; auto margins drop to 0 when it overflows, so every part scrolls in. */
447
1245
  .sheet {
448
1246
  position: relative;
@@ -485,16 +1283,6 @@ td {
485
1283
  text-overflow: ellipsis;
486
1284
  }
487
1285
 
488
- .console-block {
489
- margin-top: 8px;
490
- }
491
-
492
- .console-block .console {
493
- max-height: 30vh;
494
- overflow: auto;
495
- margin: 4px 0 0;
496
- }
497
-
498
1286
  .console {
499
1287
  background: var(--card);
500
1288
  border: 1px solid var(--line);
@@ -502,31 +1290,56 @@ td {
502
1290
  white-space: pre-wrap;
503
1291
  }
504
1292
 
505
- .actions {
1293
+ /* The end of a pass, in place of the panes. */
1294
+ .endcard {
1295
+ flex: 1 1 0;
1296
+ min-height: 0;
1297
+ overflow: auto;
1298
+ border: 1px solid var(--line);
1299
+ border-radius: 6px;
1300
+ padding: 16px;
1301
+ background: var(--card);
1302
+ }
1303
+
1304
+ .endcard h2 {
1305
+ margin: 0 0 12px;
1306
+ font-size: 1rem;
1307
+ }
1308
+
1309
+ .offers {
506
1310
  display: flex;
507
1311
  flex-wrap: wrap;
508
1312
  gap: 8px;
509
- align-items: center;
510
- padding-top: 8px;
1313
+ margin: 0 0 12px;
511
1314
  }
512
1315
 
513
- #reason {
514
- flex: 1;
515
- min-width: 240px;
516
- font: inherit;
517
- padding: 4px 8px;
518
- border: 1px solid var(--line);
519
- border-radius: 4px;
520
- background: var(--card);
521
- color: var(--fg);
1316
+ .offers button {
1317
+ min-height: 44px;
522
1318
  }
523
1319
 
524
- .tile .moved-from {
525
- font-size: 12px;
526
- word-break: break-all;
527
- color: var(--accent);
1320
+ /* Finish: its steps while it runs, and its result. */
1321
+ .finish-panel ol {
1322
+ padding-left: 20px;
1323
+ }
1324
+
1325
+ .finish-panel li {
1326
+ margin: 4px 0;
1327
+ }
1328
+
1329
+ .finish-panel li.done {
1330
+ color: var(--muted);
1331
+ }
1332
+
1333
+ .finish-panel li.now {
1334
+ font-weight: 600;
1335
+ }
1336
+
1337
+ .finish-panel progress {
1338
+ margin-left: 8px;
1339
+ vertical-align: middle;
528
1340
  }
529
1341
 
530
- #app:focus {
1342
+ #app:focus,
1343
+ #app:focus-visible {
531
1344
  outline: none;
532
1345
  }