@7365admin1/layer-common 3.2.1-staging.64 → 3.2.1-staging.65
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/components/CameraWall.vue +326 -16
- package/components/CameraWallTile.vue +478 -29
- package/composables/useSiteSettings.ts +22 -0
- package/package.json +1 -1
- package/utils/camera-wall.test.ts +266 -10
- package/utils/camera-wall.ts +554 -47
|
@@ -119,18 +119,48 @@
|
|
|
119
119
|
}"
|
|
120
120
|
>
|
|
121
121
|
<CameraWallTile
|
|
122
|
-
v-for="
|
|
123
|
-
:key="camera
|
|
122
|
+
v-for="camera in tiles"
|
|
123
|
+
:key="camera._id"
|
|
124
124
|
:camera="camera"
|
|
125
125
|
:dense="dense"
|
|
126
126
|
:online="online"
|
|
127
|
-
:
|
|
127
|
+
:health="healthById[camera._id]"
|
|
128
|
+
:now="now"
|
|
129
|
+
:focused="camera._id === focusedId"
|
|
128
130
|
@focus="focusedId = $event._id"
|
|
129
131
|
/>
|
|
130
132
|
</div>
|
|
131
133
|
</div>
|
|
132
134
|
</div>
|
|
133
135
|
|
|
136
|
+
<!--
|
|
137
|
+
THE PAGER.
|
|
138
|
+
|
|
139
|
+
Fixed under the wall rather than scrolling with the page, because it must
|
|
140
|
+
not scroll away from the wall it pages. Drawn only when there is more than
|
|
141
|
+
one page — a site with four cameras on a 2x2 wall does not need to be told
|
|
142
|
+
it is on page 1 of 1.
|
|
143
|
+
-->
|
|
144
|
+
<div v-if="pages > 1" class="vms__pager">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
class="vms__tool"
|
|
148
|
+
:disabled="page === 0"
|
|
149
|
+
@click="page = Math.max(0, page - 1)"
|
|
150
|
+
>
|
|
151
|
+
Previous
|
|
152
|
+
</button>
|
|
153
|
+
<span class="vms__pager-label">Page {{ page + 1 }} of {{ pages }}</span>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
class="vms__tool"
|
|
157
|
+
:disabled="page >= pages - 1"
|
|
158
|
+
@click="page = Math.min(pages - 1, page + 1)"
|
|
159
|
+
>
|
|
160
|
+
Next
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
134
164
|
<!-- ------------------------------------------------------- capabilities -->
|
|
135
165
|
<!--
|
|
136
166
|
THE CONTROLS THAT ARE NOT SWITCHED ON YET.
|
|
@@ -141,6 +171,43 @@
|
|
|
141
171
|
-->
|
|
142
172
|
<div v-if="focused" class="vms__caps">
|
|
143
173
|
<p class="vms__caps-head">{{ focused.name || focused._id }}</p>
|
|
174
|
+
|
|
175
|
+
<!--
|
|
176
|
+
HEALTH — the recorder's own state, which is a different fact from the
|
|
177
|
+
tile badge above it (that one is about the video page loading). They can
|
|
178
|
+
disagree, and when they do a supervisor needs both, not a merged verdict.
|
|
179
|
+
|
|
180
|
+
Firmware and the device clock are blank on this estate and the server
|
|
181
|
+
says why, in its own words. An empty field with no explanation reads as
|
|
182
|
+
"checked, and fine", which would be the opposite of the truth.
|
|
183
|
+
-->
|
|
184
|
+
<!--
|
|
185
|
+
WHAT THE BADGE ON THE TILE ACTUALLY MEANS, said once here rather than
|
|
186
|
+
nine times across the wall. "Not verified" is a word a supervisor has
|
|
187
|
+
every right to challenge, and the answer — the picture belongs to
|
|
188
|
+
another origin's page and a browser cannot look inside it — is too long
|
|
189
|
+
for a tile and too important to leave out.
|
|
190
|
+
-->
|
|
191
|
+
<p v-if="embedsPlayer" class="vms__badge-note">{{ UNVERIFIED_DETAIL }}</p>
|
|
192
|
+
|
|
193
|
+
<div class="vms__health">
|
|
194
|
+
<span class="vms__health-state">
|
|
195
|
+
<span class="vms__health-dot" :class="`vms__health-dot--${focusedHealth.tone}`" />
|
|
196
|
+
{{ focusedHealth.label }}
|
|
197
|
+
</span>
|
|
198
|
+
<span class="vms__health-item">
|
|
199
|
+
Last picture this server received:
|
|
200
|
+
<b>{{ focusedHealth.lastFrame || "none yet" }}</b>
|
|
201
|
+
</span>
|
|
202
|
+
<span v-if="focusedHealth.detail" class="vms__health-item">
|
|
203
|
+
{{ focusedHealth.detail }}
|
|
204
|
+
</span>
|
|
205
|
+
<span v-if="focusedHealth.deviceDetail" class="vms__health-item">
|
|
206
|
+
{{ focusedHealth.deviceDetail }}
|
|
207
|
+
</span>
|
|
208
|
+
<span v-if="healthFailed" class="vms__health-item">{{ healthFailed }}</span>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
144
211
|
<div class="vms__caps-row">
|
|
145
212
|
<div
|
|
146
213
|
v-for="c in WALL_CAPABILITY_CONTROLS"
|
|
@@ -150,11 +217,26 @@
|
|
|
150
217
|
>
|
|
151
218
|
<p class="vms__cap-title">
|
|
152
219
|
{{ c.title }}
|
|
153
|
-
<span
|
|
220
|
+
<span
|
|
221
|
+
class="vms__cap-tag"
|
|
222
|
+
:class="{ 'vms__cap-tag--on': capability(c.key).available }"
|
|
223
|
+
>
|
|
154
224
|
{{ capability(c.key).tag }}
|
|
155
225
|
</span>
|
|
156
226
|
</p>
|
|
157
|
-
|
|
227
|
+
<!-- Working: what it does HERE. Off: the server's sentence, verbatim. -->
|
|
228
|
+
<p class="vms__cap-detail">
|
|
229
|
+
{{ capability(c.key).available ? c.ready : capability(c.key).detail }}
|
|
230
|
+
</p>
|
|
231
|
+
<!--
|
|
232
|
+
RULE 5. The line the server cannot write, because it does not know
|
|
233
|
+
who the reader can go and ask. Somebody read "Not set up yet" on this
|
|
234
|
+
screen and asked how to set it up — which is a fair question that the
|
|
235
|
+
screen was not answering.
|
|
236
|
+
-->
|
|
237
|
+
<p v-if="capability(c.key).nextStep" class="vms__cap-next">
|
|
238
|
+
{{ capability(c.key).nextStep }}
|
|
239
|
+
</p>
|
|
158
240
|
</div>
|
|
159
241
|
</div>
|
|
160
242
|
</div>
|
|
@@ -166,14 +248,21 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
|
166
248
|
|
|
167
249
|
import {
|
|
168
250
|
capabilityView,
|
|
251
|
+
clampPage,
|
|
169
252
|
DEFAULT_WALL_LAYOUT,
|
|
253
|
+
healthView,
|
|
170
254
|
isDenseLayout,
|
|
171
255
|
wallCameras,
|
|
172
256
|
wallLayout,
|
|
257
|
+
wallPageCount,
|
|
258
|
+
wallRows,
|
|
173
259
|
wallTiles,
|
|
260
|
+
tileView,
|
|
261
|
+
UNVERIFIED_DETAIL,
|
|
174
262
|
WALL_CAPABILITY_CONTROLS,
|
|
175
263
|
WALL_LAYOUTS,
|
|
176
264
|
type TWallCamera,
|
|
265
|
+
type TWallCameraHealth,
|
|
177
266
|
type TWallLayoutKey,
|
|
178
267
|
} from "../utils/camera-wall";
|
|
179
268
|
|
|
@@ -215,11 +304,17 @@ const props = defineProps<{ site: string }>();
|
|
|
215
304
|
|
|
216
305
|
/* ---------------------------------------------------------------- the data */
|
|
217
306
|
|
|
218
|
-
const { getSiteWall } = useSiteSettings();
|
|
307
|
+
const { getSiteWall, getSiteHealth } = useSiteSettings();
|
|
219
308
|
|
|
220
309
|
const cameras = ref<TWallCamera[]>([]);
|
|
221
310
|
const loading = ref(true);
|
|
222
311
|
const failed = ref("");
|
|
312
|
+
/**
|
|
313
|
+
* The server's own tuning. `healthCacheSeconds` is how long IT caches a
|
|
314
|
+
* recorder probe, so it is exactly how often there is any point asking — and
|
|
315
|
+
* asking faster would spend round trips to be handed the same cached answer.
|
|
316
|
+
*/
|
|
317
|
+
const config = ref<Record<string, any> | null>(null);
|
|
223
318
|
|
|
224
319
|
async function load() {
|
|
225
320
|
loading.value = true;
|
|
@@ -230,6 +325,7 @@ async function load() {
|
|
|
230
325
|
// an ANPR unit must never reach a monitoring wall even if that ever changes.
|
|
231
326
|
const res = await getSiteWall(props.site);
|
|
232
327
|
cameras.value = wallCameras(res?.cameras);
|
|
328
|
+
config.value = res?.config ?? null;
|
|
233
329
|
} catch (error: any) {
|
|
234
330
|
// The raw message is often a stack trace and rarely something an operator
|
|
235
331
|
// can act on — but "you may not see this site" and "your connection
|
|
@@ -246,12 +342,15 @@ async function load() {
|
|
|
246
342
|
: "The list of cameras did not load. Check your connection and try again.";
|
|
247
343
|
} finally {
|
|
248
344
|
loading.value = false;
|
|
345
|
+
restartHealth();
|
|
249
346
|
}
|
|
250
347
|
}
|
|
251
348
|
|
|
252
349
|
onMounted(load);
|
|
253
350
|
watch(() => props.site, () => {
|
|
254
351
|
selectedIds.value = [];
|
|
352
|
+
page.value = 0;
|
|
353
|
+
healthById.value = {};
|
|
255
354
|
load();
|
|
256
355
|
});
|
|
257
356
|
|
|
@@ -259,13 +358,42 @@ watch(() => props.site, () => {
|
|
|
259
358
|
|
|
260
359
|
const layoutKey = ref<TWallLayoutKey>(DEFAULT_WALL_LAYOUT);
|
|
261
360
|
const layout = computed(() => wallLayout(layoutKey.value));
|
|
262
|
-
const rows = computed(() => Math.ceil(layout.value.tiles / layout.value.columns));
|
|
263
361
|
const dense = computed(() => isDenseLayout(layoutKey.value));
|
|
264
362
|
|
|
265
363
|
const selectedIds = ref<string[]>([]);
|
|
266
364
|
const showPicker = ref(false);
|
|
267
365
|
|
|
268
|
-
const
|
|
366
|
+
const page = ref(0);
|
|
367
|
+
const pages = computed(() =>
|
|
368
|
+
wallPageCount(cameras.value, selectedIds.value, layoutKey.value)
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const tiles = computed(() =>
|
|
372
|
+
wallTiles(cameras.value, selectedIds.value, layoutKey.value, page.value)
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* THE GRID PACKS TO WHAT IS ON IT.
|
|
377
|
+
*
|
|
378
|
+
* Five cameras on a nine-tile wall used to draw five pictures and four dashed
|
|
379
|
+
* boxes reading "No camera in this position", with the pictures squeezed into
|
|
380
|
+
* two thirds of the height to leave room for the holes. The layout picker means
|
|
381
|
+
* "at most this many, at this size" — it does not mean the screen owes anybody
|
|
382
|
+
* four empty rectangles. Nothing can be done with an empty cell from here
|
|
383
|
+
* anyway: cameras are added in Site Settings.
|
|
384
|
+
*/
|
|
385
|
+
const rows = computed(() => wallRows(tiles.value.length, layoutKey.value));
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A layout change resizes the page, so the old page NUMBER no longer means
|
|
389
|
+
* anything — going to 3x3 from page 2 of a 2x2 wall would land somewhere
|
|
390
|
+
* arbitrary, or past the end. Clamped rather than reset to 0: dropping from
|
|
391
|
+
* 3x3 to 2x2 keeps you roughly where you were instead of throwing you back to
|
|
392
|
+
* the front of a wall you were part-way through.
|
|
393
|
+
*/
|
|
394
|
+
watch([layoutKey, pages], () => {
|
|
395
|
+
page.value = clampPage(page.value, pages.value);
|
|
396
|
+
});
|
|
269
397
|
|
|
270
398
|
function isPicked(id: string) {
|
|
271
399
|
return selectedIds.value.includes(id);
|
|
@@ -281,13 +409,90 @@ function togglePick(id: string) {
|
|
|
281
409
|
|
|
282
410
|
const focusedId = ref("");
|
|
283
411
|
const focused = computed(
|
|
284
|
-
() => tiles.value.find((c) => c
|
|
412
|
+
() => tiles.value.find((c) => c._id === focusedId.value) || tiles.value[0] || null
|
|
285
413
|
);
|
|
286
414
|
|
|
287
415
|
function capability(key: string) {
|
|
288
416
|
return capabilityView(focused.value, key);
|
|
289
417
|
}
|
|
290
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Whether this camera embeds a player at all — i.e. whether the "Not verified"
|
|
421
|
+
* badge is something its tile can ever show. A camera the server has already
|
|
422
|
+
* refused has a reason of its own on the tile and does not need this one too.
|
|
423
|
+
*/
|
|
424
|
+
const embedsPlayer = computed(
|
|
425
|
+
() => tileView(focused.value, { online: online.value }).showPlayer
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
/* ------------------------------------------------------------------ health */
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* THE RECORDER'S OWN STATE, asked for separately and slowly.
|
|
432
|
+
*
|
|
433
|
+
* This is the only thing on this screen that can cause the server to touch a
|
|
434
|
+
* device, so three things bound it and all three matter:
|
|
435
|
+
*
|
|
436
|
+
* - **only the tiles on screen** are asked about, so paging to another page
|
|
437
|
+
* does not keep probing the page you left;
|
|
438
|
+
* - **the interval is the server's own cache lifetime**, because asking faster
|
|
439
|
+
* than the cache is round trips in exchange for the identical answer;
|
|
440
|
+
* - **a failure is never retried and never breaks the wall.** Health is
|
|
441
|
+
* supporting information; a wall that goes blank because a probe timed out
|
|
442
|
+
* would be a worse screen than one with no health on it at all.
|
|
443
|
+
*/
|
|
444
|
+
const healthById = ref<Record<string, TWallCameraHealth>>({});
|
|
445
|
+
const healthFailed = ref("");
|
|
446
|
+
/** Ticked with the sweep so "4 min ago" ages instead of freezing. */
|
|
447
|
+
const now = ref(Date.now());
|
|
448
|
+
|
|
449
|
+
const focusedHealth = computed(() =>
|
|
450
|
+
healthView(focused.value ? healthById.value[focused.value._id] : null, now.value)
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const visibleIds = computed(() => tiles.value.map((c) => c._id));
|
|
454
|
+
|
|
455
|
+
let healthTimer: ReturnType<typeof setInterval> | null = null;
|
|
456
|
+
|
|
457
|
+
async function sweepHealth() {
|
|
458
|
+
const ids = visibleIds.value;
|
|
459
|
+
now.value = Date.now();
|
|
460
|
+
if (!ids.length || !online.value) return;
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const res = await getSiteHealth(props.site, ids);
|
|
464
|
+
const next: Record<string, TWallCameraHealth> = {};
|
|
465
|
+
for (const item of (res?.cameras ?? []) as any[]) {
|
|
466
|
+
if (item?._id) next[String(item._id)] = item as TWallCameraHealth;
|
|
467
|
+
}
|
|
468
|
+
healthById.value = next;
|
|
469
|
+
healthFailed.value = "";
|
|
470
|
+
} catch {
|
|
471
|
+
// Kept, not cleared: the last known health is still the most useful thing
|
|
472
|
+
// we have, and "the recorder was fine a moment ago" is a real answer.
|
|
473
|
+
healthFailed.value = "The camera health check did not answer this time.";
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function restartHealth() {
|
|
478
|
+
if (healthTimer) clearInterval(healthTimer);
|
|
479
|
+
const seconds = Number(config.value?.healthCacheSeconds);
|
|
480
|
+
const intervalMs = (Number.isFinite(seconds) && seconds > 0 ? seconds : 20) * 1000;
|
|
481
|
+
healthTimer = setInterval(sweepHealth, intervalMs);
|
|
482
|
+
sweepHealth();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Re-swept when the visible set changes, so a supervisor who pages or changes
|
|
486
|
+
// layout is not reading the previous page's health under the new page's names.
|
|
487
|
+
watch(visibleIds, (ids, previous) => {
|
|
488
|
+
if (ids.join(",") !== (previous ?? []).join(",")) sweepHealth();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
onBeforeUnmount(() => {
|
|
492
|
+
if (healthTimer) clearInterval(healthTimer);
|
|
493
|
+
healthTimer = null;
|
|
494
|
+
});
|
|
495
|
+
|
|
291
496
|
/* ------------------------------------------------------- browser condition */
|
|
292
497
|
|
|
293
498
|
const online = ref(true);
|
|
@@ -339,7 +544,13 @@ async function toggleFullscreen() {
|
|
|
339
544
|
--vms-scrim-solid: rgba(7, 9, 12, 0.82);
|
|
340
545
|
--vms-text: #e8ecf1;
|
|
341
546
|
--vms-text-dim: #94a0b0;
|
|
342
|
-
|
|
547
|
+
/*
|
|
548
|
+
#5c6675 measured 2.98:1 against the chrome — under the 3:1 a status
|
|
549
|
+
indicator needs, and this token IS a status indicator (the "not checked"
|
|
550
|
+
dot and the inactive dot in the camera list). #7a8697 measures 4.68:1 there
|
|
551
|
+
and 5.01:1 on a tile. Invented value; the rest of the ramp is unchanged.
|
|
552
|
+
*/
|
|
553
|
+
--vms-text-faint: #7a8697;
|
|
343
554
|
--vms-accent: #5b9be0;
|
|
344
555
|
--vms-radius: 3px;
|
|
345
556
|
|
|
@@ -538,6 +749,80 @@ async function toggleFullscreen() {
|
|
|
538
749
|
color: var(--vms-text-dim);
|
|
539
750
|
}
|
|
540
751
|
|
|
752
|
+
/* ------------------------------------------------------------------ pager */
|
|
753
|
+
|
|
754
|
+
.vms__pager {
|
|
755
|
+
display: flex;
|
|
756
|
+
align-items: center;
|
|
757
|
+
justify-content: center;
|
|
758
|
+
gap: 10px;
|
|
759
|
+
padding: 6px;
|
|
760
|
+
background: var(--vms-chrome);
|
|
761
|
+
border-top: 1px solid var(--vms-line);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.vms__pager-label {
|
|
765
|
+
font-size: 12px;
|
|
766
|
+
color: var(--vms-text-dim);
|
|
767
|
+
font-variant-numeric: tabular-nums;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/* ----------------------------------------------------------- badge meaning */
|
|
771
|
+
|
|
772
|
+
.vms__badge-note {
|
|
773
|
+
margin: 0 0 8px;
|
|
774
|
+
max-width: 92ch;
|
|
775
|
+
font-size: 12px;
|
|
776
|
+
line-height: 17px;
|
|
777
|
+
color: var(--vms-text-dim);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* ----------------------------------------------------------------- health */
|
|
781
|
+
|
|
782
|
+
.vms__health {
|
|
783
|
+
display: flex;
|
|
784
|
+
flex-wrap: wrap;
|
|
785
|
+
align-items: baseline;
|
|
786
|
+
gap: 6px 16px;
|
|
787
|
+
margin: 0 0 10px;
|
|
788
|
+
font-size: 12px;
|
|
789
|
+
line-height: 17px;
|
|
790
|
+
color: var(--vms-text-dim);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.vms__health-state {
|
|
794
|
+
display: inline-flex;
|
|
795
|
+
align-items: center;
|
|
796
|
+
gap: 6px;
|
|
797
|
+
color: var(--vms-text);
|
|
798
|
+
font-weight: 600;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.vms__health-item b {
|
|
802
|
+
color: var(--vms-text);
|
|
803
|
+
font-weight: 600;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.vms__health-dot {
|
|
807
|
+
width: 7px;
|
|
808
|
+
height: 7px;
|
|
809
|
+
border-radius: 50%;
|
|
810
|
+
flex: none;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.vms__health-dot--ok {
|
|
814
|
+
background: #4caf50;
|
|
815
|
+
}
|
|
816
|
+
.vms__health-dot--warn {
|
|
817
|
+
background: #fb8c00;
|
|
818
|
+
}
|
|
819
|
+
.vms__health-dot--down {
|
|
820
|
+
background: #e0241c;
|
|
821
|
+
}
|
|
822
|
+
.vms__health-dot--unknown {
|
|
823
|
+
background: var(--vms-text-faint);
|
|
824
|
+
}
|
|
825
|
+
|
|
541
826
|
/* ------------------------------------------------------------ capabilities */
|
|
542
827
|
|
|
543
828
|
.vms__caps {
|
|
@@ -598,15 +883,40 @@ async function toggleFullscreen() {
|
|
|
598
883
|
}
|
|
599
884
|
|
|
600
885
|
/*
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
same as no reason at all.
|
|
886
|
+
"Ready" is the one tag that is good news, and it is the accent rather than
|
|
887
|
+
green: green on this screen means "this camera is up", and a control being
|
|
888
|
+
available is not the same claim.
|
|
605
889
|
*/
|
|
606
|
-
.vms__cap--
|
|
607
|
-
|
|
890
|
+
.vms__cap-tag--on {
|
|
891
|
+
color: var(--vms-accent);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/*
|
|
895
|
+
RULE 5's second line: quieter than the server's sentence above it, because it
|
|
896
|
+
is advice rather than fact — but NOT dimmed with the rest of a switched-off
|
|
897
|
+
panel. It is the one line on an unavailable control that is actionable, and
|
|
898
|
+
fading the only actionable line is the whole mistake this fixes.
|
|
899
|
+
*/
|
|
900
|
+
.vms__cap-next {
|
|
901
|
+
margin: 6px 0 0;
|
|
902
|
+
padding-top: 6px;
|
|
903
|
+
border-top: 1px solid var(--vms-line);
|
|
904
|
+
font-size: 11px;
|
|
905
|
+
line-height: 16px;
|
|
906
|
+
color: var(--vms-text-dim);
|
|
608
907
|
}
|
|
609
908
|
|
|
909
|
+
/*
|
|
910
|
+
THE FADE ON A SWITCHED-OFF REASON IS GONE, AND THE OLD COMMENT WAS WRONG.
|
|
911
|
+
|
|
912
|
+
It claimed 0.55 opacity measured "about 5.2:1". Measured properly — the dim
|
|
913
|
+
ramp #94a0b0 composited at 0.55 over the chrome #161b22 — it is 2.88:1,
|
|
914
|
+
well under the 4.5:1 a sentence needs, and the reason a supervisor is meant
|
|
915
|
+
to read was the least legible text on the panel. Removing the fade entirely
|
|
916
|
+
leaves #94a0b0 on #161b22 at 6.52:1. The panel is already outlined rather
|
|
917
|
+
than filled, which is what says "not switched on"; the fade was saying it a
|
|
918
|
+
second time at the cost of the words.
|
|
919
|
+
*/
|
|
610
920
|
.vms__cap-detail {
|
|
611
921
|
margin: 0;
|
|
612
922
|
font-size: 12px;
|