@7365admin1/layer-common 3.2.2-staging.118 → 3.2.2-staging.121
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/assets/css/primitives.css +168 -0
- package/assets/css/screens.css +76 -1
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AddPassKeyToVisitor.vue +95 -73
- package/components/AppDateField.vue +15 -1
- package/components/AppField.vue +25 -0
- package/components/BuildingUnitFormEdit.vue +5 -1
- package/components/Card/DeleteConfirmation.vue +5 -2
- package/components/HidAccessLogDashboard.vue +288 -447
- package/components/HidIntercomManagement.vue +387 -521
- package/components/HidQrCodeConfiguration.vue +293 -369
- package/components/HidReaderManagement.vue +197 -320
- package/components/HidServiceSettingsPanel.vue +38 -26
- package/components/HidUserEnrollment.vue +239 -344
- package/components/IncidentReport/Authorities.vue +30 -18
- package/components/Nfc/NFCTagForm.vue +5 -1
- package/components/ScanVisitorQRCode.vue +64 -28
- package/components/SearchVehicleNumberUser.vue +39 -37
- package/components/TableMain.vue +50 -1
- package/components/VehicleAddSelection.vue +48 -32
- package/components/VehicleForm.vue +112 -129
- package/components/VehicleUpdateMoreAction.vue +59 -46
- package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
- package/components/VisitorFormSelection.vue +40 -37
- package/components/VisitorPassKeyQRScanner.vue +65 -18
- package/components/VisitorSocketPopUp.vue +21 -13
- package/components/VisitorsReportPreview.vue +44 -29
- package/package.json +1 -1
- package/tools/render-harness/baselines.json +95 -0
- package/tools/render-harness/harness-init.ps1 +4 -0
- package/utils/status.test.ts +33 -0
- package/utils/status.ts +36 -0
- package/utils/theme-aa-ledger.ts +142 -0
- package/utils/theme.test.ts +95 -34
- package/utils/theme.ts +55 -28
|
@@ -41,6 +41,57 @@
|
|
|
41
41
|
font-family: var(--font-sans);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* ...and the places Vuetify hard-codes Roboto, which BEAT the rule above
|
|
46
|
+
* because they name the element rather than inherit into it. In
|
|
47
|
+
* vuetify 3.12 that is exactly 81 declarations, in three files:
|
|
48
|
+
*
|
|
49
|
+
* main.css `html` (1) and the typography utilities .text-h1 ...
|
|
50
|
+
* .text-overline (13), each repeated for the five responsive
|
|
51
|
+
* breakpoints .text-sm-* ... .text-xxl-* (65) = 79
|
|
52
|
+
* VBadge.css `.v-badge__badge`
|
|
53
|
+
* VKbd.css `.v-kbd`
|
|
54
|
+
*
|
|
55
|
+
* None of them carries `!important`, so specificity alone re-points them:
|
|
56
|
+
* `:root` (0,1,0) beats `html` (0,0,1), and a descendant of `.v-application`
|
|
57
|
+
* (0,2,0) beats a bare `.text-h1` (0,1,0). No `!important` here either -
|
|
58
|
+
* a screen that wants a different face must still be able to say so.
|
|
59
|
+
*
|
|
60
|
+
* This lives HERE, once, and not in each of the eleven apps: the utilities
|
|
61
|
+
* are Vuetify's, the token is this layer's, and a per-app copy would drift.
|
|
62
|
+
*
|
|
63
|
+
* The var() fallback keeps Vuetify's own stack if a build ever resolves a
|
|
64
|
+
* pre-redesign layer-common in which `--font-sans` does not exist.
|
|
65
|
+
*/
|
|
66
|
+
:root {
|
|
67
|
+
font-family: var(--font-sans, Roboto, sans-serif);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.v-application
|
|
71
|
+
:is(
|
|
72
|
+
[class*="text-h"],
|
|
73
|
+
[class*="text-subtitle"],
|
|
74
|
+
[class*="text-body"],
|
|
75
|
+
[class*="text-button"],
|
|
76
|
+
[class*="text-caption"],
|
|
77
|
+
[class*="text-overline"],
|
|
78
|
+
.v-badge__badge,
|
|
79
|
+
.v-kbd
|
|
80
|
+
),
|
|
81
|
+
.v-overlay-container
|
|
82
|
+
:is(
|
|
83
|
+
[class*="text-h"],
|
|
84
|
+
[class*="text-subtitle"],
|
|
85
|
+
[class*="text-body"],
|
|
86
|
+
[class*="text-button"],
|
|
87
|
+
[class*="text-caption"],
|
|
88
|
+
[class*="text-overline"],
|
|
89
|
+
.v-badge__badge,
|
|
90
|
+
.v-kbd
|
|
91
|
+
) {
|
|
92
|
+
font-family: var(--font-sans, Roboto, sans-serif);
|
|
93
|
+
}
|
|
94
|
+
|
|
44
95
|
/* ------------------------------------------------------------------ */
|
|
45
96
|
/* PageHeader - the row every screen opens with. */
|
|
46
97
|
/* ------------------------------------------------------------------ */
|
|
@@ -398,6 +449,23 @@
|
|
|
398
449
|
opacity: 1;
|
|
399
450
|
}
|
|
400
451
|
|
|
452
|
+
/* The `trailing` slot's control - same muted weight as the leading icon, so a
|
|
453
|
+
reveal toggle reads as field chrome rather than as a second action. */
|
|
454
|
+
.app-field__trailing {
|
|
455
|
+
display: grid;
|
|
456
|
+
place-items: center;
|
|
457
|
+
flex: 0 0 auto;
|
|
458
|
+
border: none;
|
|
459
|
+
background: transparent;
|
|
460
|
+
padding: 0;
|
|
461
|
+
color: var(--muted);
|
|
462
|
+
cursor: pointer;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.app-field__trailing:hover {
|
|
466
|
+
color: var(--text2);
|
|
467
|
+
}
|
|
468
|
+
|
|
401
469
|
/* A `<select>` keeps the native menu; only the chrome is ours. */
|
|
402
470
|
select.app-field__input {
|
|
403
471
|
font-weight: 600;
|
|
@@ -574,6 +642,106 @@ select.app-field__input {
|
|
|
574
642
|
color: var(--on-accent-strong);
|
|
575
643
|
}
|
|
576
644
|
|
|
645
|
+
/* ------------------------------------------------------------------ */
|
|
646
|
+
/* Switch - the settings toggle. */
|
|
647
|
+
/* ------------------------------------------------------------------ */
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* THE DESIGN'S TOGGLE, WRITTEN ONCE.
|
|
651
|
+
*
|
|
652
|
+
* The handoff draws it on the Settings screen's ANPR Camera section and then
|
|
653
|
+
* says, in the prototype's own words, that every other module's
|
|
654
|
+
* "toggles, form fields and tables use the same styling as the ANPR Camera
|
|
655
|
+
* section" - so this IS the spec for the HID panels, not an interpretation:
|
|
656
|
+
*
|
|
657
|
+
* track 38x22, `border-radius:999px`; ON `var(--accent)`, OFF `var(--hover)`
|
|
658
|
+
* thumb 18x18 circle at `top:2px`; ON `left:18px` `#ffffff`,
|
|
659
|
+
* OFF `left:2px` `var(--muted)`
|
|
660
|
+
* label 13.5px/700; the OFF label is `var(--text2)`
|
|
661
|
+
*
|
|
662
|
+
* `--toggle-w` / `--toggle-h` were put in `tokens.css` in Phase 1 for exactly
|
|
663
|
+
* this and had no rule to serve until now.
|
|
664
|
+
*
|
|
665
|
+
* Vuetify slides `.v-selection-control__input` and leaves the thumb static
|
|
666
|
+
* inside it; the design slides the THUMB inside a fixed track. So the input's
|
|
667
|
+
* transform is reset and the thumb is positioned directly - otherwise the two
|
|
668
|
+
* movements compound and the knob leaves the track.
|
|
669
|
+
*
|
|
670
|
+
* Three HID panels each carried their own copy of this with hardcoded hexes
|
|
671
|
+
* (`#8b9297` / `#2eaf55` / `#e0e0e0`), which is why a disabled toggle read as
|
|
672
|
+
* light grey on the dark theme's dark card. One rule, both themes, no hex.
|
|
673
|
+
*/
|
|
674
|
+
.app-switch .v-selection-control__wrapper {
|
|
675
|
+
position: relative;
|
|
676
|
+
flex: 0 0 auto;
|
|
677
|
+
width: var(--toggle-w);
|
|
678
|
+
height: var(--toggle-h);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.app-switch .v-switch__track {
|
|
682
|
+
position: absolute;
|
|
683
|
+
inset: 0;
|
|
684
|
+
width: var(--toggle-w);
|
|
685
|
+
height: var(--toggle-h);
|
|
686
|
+
border-radius: var(--r-pill);
|
|
687
|
+
background: var(--hover);
|
|
688
|
+
opacity: 1;
|
|
689
|
+
transition: background var(--motion);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.app-switch .v-selection-control--dirty .v-switch__track {
|
|
693
|
+
background: var(--accent);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.app-switch .v-selection-control__input {
|
|
697
|
+
position: absolute;
|
|
698
|
+
inset: 0;
|
|
699
|
+
width: var(--toggle-w);
|
|
700
|
+
height: var(--toggle-h);
|
|
701
|
+
/* Vuetify's own slide. The design moves the thumb, not this box. */
|
|
702
|
+
transform: none;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* The 40px hover/ripple disc is bigger than the whole control in this shape. */
|
|
706
|
+
.app-switch .v-selection-control__input::before,
|
|
707
|
+
.app-switch .v-selection-control__input .v-ripple__container {
|
|
708
|
+
display: none;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.app-switch .v-switch__thumb {
|
|
712
|
+
position: absolute;
|
|
713
|
+
top: 2px;
|
|
714
|
+
left: 2px;
|
|
715
|
+
width: 18px;
|
|
716
|
+
height: 18px;
|
|
717
|
+
border-radius: 50%;
|
|
718
|
+
background: var(--muted);
|
|
719
|
+
box-shadow: none;
|
|
720
|
+
transition: left var(--motion), background var(--motion);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.app-switch .v-selection-control--dirty .v-switch__thumb {
|
|
724
|
+
left: 18px;
|
|
725
|
+
background: #ffffff;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.app-switch .v-label {
|
|
729
|
+
padding-inline-start: 12px;
|
|
730
|
+
color: var(--text2);
|
|
731
|
+
font-family: var(--font-sans);
|
|
732
|
+
font-size: 13.5px;
|
|
733
|
+
font-weight: 700;
|
|
734
|
+
opacity: 1;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.app-switch .v-selection-control--dirty .v-label {
|
|
738
|
+
color: var(--text);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.app-switch.v-input--disabled {
|
|
742
|
+
opacity: 0.55;
|
|
743
|
+
}
|
|
744
|
+
|
|
577
745
|
/* ------------------------------------------------------------------ */
|
|
578
746
|
/* KpiTile - the dashboard's headline figure. */
|
|
579
747
|
/* ------------------------------------------------------------------ */
|
package/assets/css/screens.css
CHANGED
|
@@ -360,6 +360,19 @@
|
|
|
360
360
|
color: var(--on-accent-strong);
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
/*
|
|
364
|
+
* `min-width: 0` above is what lets a button shrink to its label, and it is
|
|
365
|
+
* one class more specific than Vuetify's `.v-btn--block { min-width: 100% }` -
|
|
366
|
+
* so it silently CANCELLED `block`. Measured on the ten create/edit forms:
|
|
367
|
+
* a `block` Cancel sat 80.8px wide inside a 672px half-column instead of
|
|
368
|
+
* filling it, on every width and both themes. Restoring it needs the block
|
|
369
|
+
* class named, so this reaches only the buttons that asked to be full width.
|
|
370
|
+
*/
|
|
371
|
+
.v-btn.v-btn--block.screen-btn-primary,
|
|
372
|
+
.v-btn.v-btn--block.screen-btn-ghost {
|
|
373
|
+
min-width: 100%;
|
|
374
|
+
}
|
|
375
|
+
|
|
363
376
|
/* Vuetify paints `variant="tonal"` with an overlay layer ABOVE the button's own
|
|
364
377
|
background, so without this the accent fill shows through a wash. */
|
|
365
378
|
.screen-btn-primary .v-btn__overlay,
|
|
@@ -534,6 +547,11 @@
|
|
|
534
547
|
/* The header row and the rows under it. */
|
|
535
548
|
/* ------------------------------------------------------------------ */
|
|
536
549
|
|
|
550
|
+
/**
|
|
551
|
+
* `padding: 12px 20px`, not 10px - read off the prototype's own header row,
|
|
552
|
+
* which is `padding:12px 20px;font-size:11px;font-weight:800;letter-spacing:
|
|
553
|
+
* .07em;text-transform:uppercase;color:var(--muted);background:var(--thead)`.
|
|
554
|
+
*/
|
|
537
555
|
.table-card thead th {
|
|
538
556
|
background: var(--thead) !important;
|
|
539
557
|
color: var(--muted) !important;
|
|
@@ -541,21 +559,78 @@
|
|
|
541
559
|
font-weight: var(--fw-table-head) !important;
|
|
542
560
|
letter-spacing: var(--ls-table-head);
|
|
543
561
|
text-transform: uppercase;
|
|
544
|
-
padding:
|
|
562
|
+
padding: 12px var(--cellpad-x) !important;
|
|
545
563
|
height: auto !important;
|
|
546
564
|
border-bottom: 1px solid var(--border) !important;
|
|
547
565
|
white-space: nowrap;
|
|
548
566
|
}
|
|
549
567
|
|
|
568
|
+
/**
|
|
569
|
+
* ROW HEIGHT IS THE LINE BOX, NOT THE PADDING.
|
|
570
|
+
*
|
|
571
|
+
* The padding was already the design's `15px 20px`, and the type was already
|
|
572
|
+
* `13.5px / 600` - measured, both correct. The row was still too tall, because
|
|
573
|
+
* Vuetify puts `line-height: 1.5` on a cell: a 20.25px line box around 13.5px
|
|
574
|
+
* type, which makes a text-only row 51.25px.
|
|
575
|
+
*
|
|
576
|
+
* The prototype sets NO line-height at all, so its rows draw at the font's own
|
|
577
|
+
* normal leading - so this sets `normal` rather than a number, which is the
|
|
578
|
+
* same thing said out loud. Measured: a text-only row is then exactly 49.0px,
|
|
579
|
+
* the design's figure, and a row carrying a chip or the design's 26-28px avatar
|
|
580
|
+
* grows past it on its own - which is where the design's taller rows come from.
|
|
581
|
+
* Height follows content, exactly as the prototype does it, and is pinned to
|
|
582
|
+
* nothing.
|
|
583
|
+
*/
|
|
550
584
|
.table-card tbody td {
|
|
551
585
|
font-size: var(--fs-cell);
|
|
552
586
|
font-weight: var(--fw-cell);
|
|
553
587
|
color: var(--text);
|
|
554
588
|
padding: var(--cellpad) var(--cellpad-x) !important;
|
|
555
589
|
height: auto !important;
|
|
590
|
+
/* `!important` for the same reason the padding needs it: Vuetify's own rule
|
|
591
|
+
is five levels of descendant, which no two-class selector can outrank. */
|
|
592
|
+
line-height: normal !important;
|
|
556
593
|
font-variant-numeric: tabular-nums;
|
|
557
594
|
}
|
|
558
595
|
|
|
596
|
+
/**
|
|
597
|
+
* The design bolds the name column. Every table in the prototype that leads
|
|
598
|
+
* with a name draws it `font-weight:700` against the row's own 600 - it is what
|
|
599
|
+
* makes a row scannable, and it was missing from every table here.
|
|
600
|
+
*
|
|
601
|
+
* `:first-child` rather than a prop: the caller's own `headers` decide what the
|
|
602
|
+
* first column is, so no caller changes and no new API. A cell whose CONTENT
|
|
603
|
+
* sets its own weight - a chip, a link, an avatar block - still wins, because
|
|
604
|
+
* this only sets what the cell's children inherit.
|
|
605
|
+
*/
|
|
606
|
+
.table-card tbody td:first-child {
|
|
607
|
+
font-weight: 700;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* THE DESIGN'S FIXED COLUMN PROPORTIONS - opt-in, because they cannot be
|
|
612
|
+
* derived. The prototype lays every table out as a CSS grid with a per-table
|
|
613
|
+
* `grid-template-columns` (`0.9fr 1.5fr 1.7fr 0.8fr 1fr 1.1fr` on Feedbacks,
|
|
614
|
+
* `2fr 1fr 1fr` on Units, and so on). Those proportions are a design decision
|
|
615
|
+
* per screen; nothing in a caller's `headers` array says what they should be,
|
|
616
|
+
* so a shared component cannot invent them.
|
|
617
|
+
*
|
|
618
|
+
* What `TableMain` can do is honour them when a caller states them: a `weight`
|
|
619
|
+
* on a header becomes that column's share, and only then does the table switch
|
|
620
|
+
* to `table-layout: fixed` so the widths are obeyed rather than treated as
|
|
621
|
+
* hints. A caller that sets no weights gets exactly today's automatic layout.
|
|
622
|
+
*/
|
|
623
|
+
.table-card .table-card__fixed table {
|
|
624
|
+
table-layout: fixed;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* Fixed layout means a long value can no longer widen its column, so it has to
|
|
628
|
+
be told what to do instead of overflowing the cell. */
|
|
629
|
+
.table-card .table-card__fixed tbody td {
|
|
630
|
+
overflow: hidden;
|
|
631
|
+
text-overflow: ellipsis;
|
|
632
|
+
}
|
|
633
|
+
|
|
559
634
|
.table-card tbody tr:hover > td {
|
|
560
635
|
background: var(--hover);
|
|
561
636
|
}
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters align="center" style="gap: 8px">
|
|
3
3
|
<span class="text-caption text-medium-emphasis mr-1">Available:</span>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
<!--
|
|
5
|
+
These were `v-chip variant="tonal"` on Vuetify's `primary` / `secondary`.
|
|
6
|
+
`secondary` is never defined in this layer's theme, so the QR chip was
|
|
7
|
+
painting itself Vuetify's BUILT-IN default `#48a9a6` - a colour the design
|
|
8
|
+
never chose, and one that measures 2.58:1 on the page background.
|
|
9
|
+
|
|
10
|
+
`StatusChip`'s `tone` prop is the documented route for a chip that is a
|
|
11
|
+
COUNT rather than a status word: the screen has already worked out the
|
|
12
|
+
meaning and only needs the design's tone.
|
|
13
|
+
|
|
14
|
+
Both take `neutral`, and that is the design system's own rule rather than
|
|
15
|
+
a preference - `utils/status.ts`: a value the design never named must NOT
|
|
16
|
+
be guessed into a colour. The handoff draws no "available cards" stat, so
|
|
17
|
+
neither count has a colour of its own to honour. They are told apart by
|
|
18
|
+
their icon and label. `info` was measured here too and reads 3.88:1 on
|
|
19
|
+
the page background, which would have ADDED a sub-AA pair to buy a
|
|
20
|
+
distinction the design never asked for.
|
|
21
|
+
-->
|
|
22
|
+
<StatusChip tone="neutral" pill :dot="false">
|
|
23
|
+
<v-icon icon="mdi-card-account-details" size="14" />
|
|
11
24
|
NFC: <strong class="ml-1">{{ availablePhysical }}</strong>
|
|
12
|
-
</
|
|
13
|
-
<
|
|
14
|
-
size="
|
|
15
|
-
variant="tonal"
|
|
16
|
-
color="secondary"
|
|
17
|
-
prepend-icon="mdi-qrcode"
|
|
18
|
-
:loading="loading"
|
|
19
|
-
>
|
|
25
|
+
</StatusChip>
|
|
26
|
+
<StatusChip tone="neutral" pill :dot="false">
|
|
27
|
+
<v-icon icon="mdi-qrcode" size="14" />
|
|
20
28
|
QR Code: <strong class="ml-1">{{ availableNonPhysical }}</strong>
|
|
21
|
-
</
|
|
29
|
+
</StatusChip>
|
|
22
30
|
</v-row>
|
|
23
31
|
</template>
|
|
24
32
|
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-card width="100%" :loading="processing">
|
|
3
|
-
<
|
|
4
|
-
<v-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<v-row no-gutters class="ga-1">
|
|
2
|
+
<v-card width="100%" class="screen-modal passkey" :loading="processing">
|
|
3
|
+
<div class="passkey__head">
|
|
4
|
+
<v-card-title>{{ isEditMode ? "Edit" : "Assign" }} Pass & Keys</v-card-title>
|
|
5
|
+
<AppButton
|
|
6
|
+
variant="icon"
|
|
7
|
+
icon="mdi-close"
|
|
8
|
+
aria-label="Close"
|
|
9
|
+
@click="emit('close')"
|
|
10
|
+
/>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="screen-modal__body passkey__body">
|
|
14
|
+
<!-- `.filter-field` is the design's input, and it is a descendant
|
|
15
|
+
selector, so both autocompletes get it from this one class. -->
|
|
16
|
+
<v-row no-gutters class="ga-1 filter-field">
|
|
18
17
|
<v-col cols="12">
|
|
19
|
-
<span class="
|
|
20
|
-
Name: {{ prop.visitor.name }}
|
|
21
|
-
</span>
|
|
18
|
+
<span class="passkey__name">Name: {{ prop.visitor.name }}</span>
|
|
22
19
|
</v-col>
|
|
23
20
|
|
|
24
21
|
<!-- Pass section -->
|
|
@@ -27,15 +24,11 @@
|
|
|
27
24
|
|
|
28
25
|
<!-- Existing pass -->
|
|
29
26
|
<div v-if="existingPass" class="d-flex align-center ga-2 mb-2">
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
color="blue"
|
|
33
|
-
variant="tonal"
|
|
34
|
-
size="small"
|
|
35
|
-
>
|
|
27
|
+
<StatusChip tone="info" :dot="false">
|
|
28
|
+
<v-icon icon="mdi-card-bulleted-outline" size="13" />
|
|
36
29
|
{{ existingPass.prefixAndName }}
|
|
37
|
-
</
|
|
38
|
-
<span class="
|
|
30
|
+
</StatusChip>
|
|
31
|
+
<span class="passkey__current">(current)</span>
|
|
39
32
|
</div>
|
|
40
33
|
|
|
41
34
|
<!-- New pass selector -->
|
|
@@ -79,21 +72,26 @@
|
|
|
79
72
|
v-if="existingKeys.length > 0"
|
|
80
73
|
class="d-flex flex-wrap ga-1 mb-2"
|
|
81
74
|
>
|
|
82
|
-
|
|
75
|
+
<!-- The design's tag chip, keeping the remove control the product
|
|
76
|
+
already had: the tone and the shape are the shared ones, only
|
|
77
|
+
the close button is drawn inside rather than by Vuetify. -->
|
|
78
|
+
<StatusChip
|
|
83
79
|
v-for="key in existingKeys.filter((k) => k.status != 'Removed')"
|
|
84
80
|
:key="key.keyId"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
variant="tonal"
|
|
88
|
-
size="small"
|
|
89
|
-
closable
|
|
90
|
-
@click:close="removeExistingKey(key)"
|
|
81
|
+
tone="warn"
|
|
82
|
+
:dot="false"
|
|
91
83
|
>
|
|
84
|
+
<v-icon icon="mdi-key" size="13" />
|
|
92
85
|
{{ key.prefixAndName }}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
<button
|
|
87
|
+
class="passkey__remove"
|
|
88
|
+
:aria-label="`Remove ${key.prefixAndName}`"
|
|
89
|
+
@click="removeExistingKey(key)"
|
|
90
|
+
>
|
|
91
|
+
<v-icon icon="mdi-close-circle" size="14" />
|
|
92
|
+
</button>
|
|
93
|
+
</StatusChip>
|
|
94
|
+
<span class="passkey__current align-self-center">(current)</span>
|
|
97
95
|
</div>
|
|
98
96
|
|
|
99
97
|
<!-- New keys selector -->
|
|
@@ -132,41 +130,16 @@
|
|
|
132
130
|
</v-row>
|
|
133
131
|
|
|
134
132
|
<v-row v-if="errorMessage" no-gutters class="mt-2">
|
|
135
|
-
<p class="
|
|
136
|
-
{{ errorMessage }}
|
|
137
|
-
</p>
|
|
138
|
-
</v-row>
|
|
139
|
-
</v-card-text>
|
|
140
|
-
|
|
141
|
-
<v-toolbar density="compact">
|
|
142
|
-
<v-row no-gutters>
|
|
143
|
-
<v-col cols="6">
|
|
144
|
-
<v-btn
|
|
145
|
-
tile
|
|
146
|
-
block
|
|
147
|
-
variant="text"
|
|
148
|
-
class="text-none"
|
|
149
|
-
size="48"
|
|
150
|
-
text="Cancel"
|
|
151
|
-
@click="emit('close')"
|
|
152
|
-
/>
|
|
153
|
-
</v-col>
|
|
154
|
-
<v-col cols="6">
|
|
155
|
-
<v-btn
|
|
156
|
-
tile
|
|
157
|
-
block
|
|
158
|
-
variant="flat"
|
|
159
|
-
color="black"
|
|
160
|
-
class="text-none"
|
|
161
|
-
size="48"
|
|
162
|
-
:text="isEditMode ? 'Save' : 'Assign'"
|
|
163
|
-
:disabled="!canSubmit"
|
|
164
|
-
:loading="processing"
|
|
165
|
-
@click="handleSubmit"
|
|
166
|
-
/>
|
|
167
|
-
</v-col>
|
|
133
|
+
<p class="passkey__error">{{ errorMessage }}</p>
|
|
168
134
|
</v-row>
|
|
169
|
-
</
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="passkey__footer">
|
|
138
|
+
<AppButton variant="ghost" @click="emit('close')">Cancel</AppButton>
|
|
139
|
+
<AppButton :disabled="!canSubmit" @click="handleSubmit">
|
|
140
|
+
{{ isEditMode ? "Save" : "Assign" }}
|
|
141
|
+
</AppButton>
|
|
142
|
+
</div>
|
|
170
143
|
</v-card>
|
|
171
144
|
</template>
|
|
172
145
|
|
|
@@ -448,4 +421,53 @@ async function handleSubmit() {
|
|
|
448
421
|
}
|
|
449
422
|
</script>
|
|
450
423
|
|
|
451
|
-
<style scoped
|
|
424
|
+
<style scoped>
|
|
425
|
+
.passkey__head {
|
|
426
|
+
display: flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
justify-content: space-between;
|
|
429
|
+
padding-right: 12px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.passkey__body {
|
|
433
|
+
padding: 4px 20px 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.passkey__name,
|
|
437
|
+
.passkey__current {
|
|
438
|
+
font-size: var(--fs-cell);
|
|
439
|
+
color: var(--text2);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.passkey__error {
|
|
443
|
+
width: 100%;
|
|
444
|
+
margin: 0;
|
|
445
|
+
text-align: center;
|
|
446
|
+
font-size: var(--fs-cell);
|
|
447
|
+
color: var(--err);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* The chip's own remove control - `currentColor`, so it stays on whatever
|
|
451
|
+
tone the chip carries rather than picking a second colour. */
|
|
452
|
+
.passkey__remove {
|
|
453
|
+
display: inline-flex;
|
|
454
|
+
margin-left: 2px;
|
|
455
|
+
padding: 0;
|
|
456
|
+
border: none;
|
|
457
|
+
background: none;
|
|
458
|
+
color: inherit;
|
|
459
|
+
cursor: pointer;
|
|
460
|
+
opacity: 0.75;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.passkey__remove:hover {
|
|
464
|
+
opacity: 1;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.passkey__footer {
|
|
468
|
+
display: flex;
|
|
469
|
+
justify-content: flex-end;
|
|
470
|
+
gap: 8px;
|
|
471
|
+
padding: 16px 20px 20px;
|
|
472
|
+
}
|
|
473
|
+
</style>
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
accessible and free.
|
|
8
8
|
-->
|
|
9
9
|
<template>
|
|
10
|
-
<label class="app-field" :class="{ 'app-field--compact': compact }">
|
|
10
|
+
<label v-bind="rootAttrs" class="app-field" :class="{ 'app-field--compact': compact }">
|
|
11
11
|
<v-icon class="app-field__icon" icon="mdi-calendar-month-outline" size="17" />
|
|
12
12
|
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
13
13
|
<input
|
|
14
14
|
class="app-field__input"
|
|
15
|
+
v-bind="inputAttrs"
|
|
15
16
|
type="date"
|
|
16
17
|
:value="model"
|
|
17
18
|
:min="min || undefined"
|
|
@@ -23,6 +24,19 @@
|
|
|
23
24
|
</template>
|
|
24
25
|
|
|
25
26
|
<script setup lang="ts">
|
|
27
|
+
import { computed, useAttrs } from "vue";
|
|
28
|
+
|
|
29
|
+
/** Same split as `AppField`, and for the same reason: the root is a `<label>`,
|
|
30
|
+
so an `aria-label` left to fall through would never reach the input. */
|
|
31
|
+
defineOptions({ inheritAttrs: false });
|
|
32
|
+
|
|
33
|
+
const attrs = useAttrs();
|
|
34
|
+
const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }));
|
|
35
|
+
const inputAttrs = computed(() => {
|
|
36
|
+
const { class: _c, style: _s, ...rest } = attrs;
|
|
37
|
+
return rest;
|
|
38
|
+
});
|
|
39
|
+
|
|
26
40
|
const model = defineModel({ type: String, default: "" });
|
|
27
41
|
|
|
28
42
|
defineProps({
|
package/components/AppField.vue
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
-->
|
|
9
9
|
<template>
|
|
10
10
|
<label
|
|
11
|
+
v-bind="rootAttrs"
|
|
11
12
|
class="app-field"
|
|
12
13
|
:class="[{ 'app-field--compact': compact, 'app-field--search': search }, klass]"
|
|
13
14
|
>
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
21
22
|
<input
|
|
22
23
|
class="app-field__input"
|
|
24
|
+
v-bind="inputAttrs"
|
|
23
25
|
:type="type"
|
|
24
26
|
:value="model"
|
|
25
27
|
:placeholder="placeholder"
|
|
@@ -27,10 +29,33 @@
|
|
|
27
29
|
:readonly="readonly"
|
|
28
30
|
@input="model = ($event.target as HTMLInputElement).value"
|
|
29
31
|
/>
|
|
32
|
+
<!-- A trailing control INSIDE the box: the password reveal on the HID PIN,
|
|
33
|
+
SIP password and reader password fields. The handoff draws no password
|
|
34
|
+
field at all, so the eye is this product's, not the design's - but it
|
|
35
|
+
belongs in one place rather than being re-hacked in three screens. -->
|
|
36
|
+
<slot name="trailing" />
|
|
30
37
|
</label>
|
|
31
38
|
</template>
|
|
32
39
|
|
|
33
40
|
<script setup lang="ts">
|
|
41
|
+
import { computed, useAttrs } from "vue";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The root is a `<label>`, so an `aria-label`, `inputmode` or `autocomplete`
|
|
45
|
+
* left to fall through would land on the LABEL and never reach the input -
|
|
46
|
+
* silently costing the field its accessible name. `class` and `style` still
|
|
47
|
+
* belong on the root (that is what every existing caller styles), so the two
|
|
48
|
+
* are split rather than moved wholesale.
|
|
49
|
+
*/
|
|
50
|
+
defineOptions({ inheritAttrs: false });
|
|
51
|
+
|
|
52
|
+
const attrs = useAttrs();
|
|
53
|
+
const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }));
|
|
54
|
+
const inputAttrs = computed(() => {
|
|
55
|
+
const { class: _c, style: _s, ...rest } = attrs;
|
|
56
|
+
return rest;
|
|
57
|
+
});
|
|
58
|
+
|
|
34
59
|
const model = defineModel({ type: String, default: "" });
|
|
35
60
|
|
|
36
61
|
defineProps({
|
|
@@ -263,8 +263,12 @@
|
|
|
263
263
|
|
|
264
264
|
<v-col v-if="canUpdateUnit || canDeleteUnit" cols="6" class="pa-0">
|
|
265
265
|
<v-menu>
|
|
266
|
+
<!-- Was `color="black"`: a fixed slab that stayed black on the dark
|
|
267
|
+
card. `primary-button` is the theme's FILL token, and its pair
|
|
268
|
+
`on-primary-button` supplies the white label - unlike
|
|
269
|
+
`primary`, which is a FOREGROUND here. -->
|
|
266
270
|
<template #activator="{ props }">
|
|
267
|
-
<v-btn block variant="flat" color="
|
|
271
|
+
<v-btn block variant="flat" color="primary-button" class="text-none" height="48" v-bind="props" tile
|
|
268
272
|
:disabled="!canUpdateUnit && !canDeleteUnit">
|
|
269
273
|
More actions
|
|
270
274
|
</v-btn>
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
<!-- `text-error` is Vuetify's own class over `--v-theme-error`,
|
|
7
7
|
which is the design's `--err` in both themes - already a token,
|
|
8
8
|
so it stays. -->
|
|
9
|
-
|
|
9
|
+
<!-- A `div`, as in the Dialog/ twin: on an inline `span` the `mt-2`
|
|
10
|
+
does nothing and the warning ran straight into the title with
|
|
11
|
+
no space - "delete this?This removes the record...". -->
|
|
12
|
+
<div v-if="message" class="text-error mt-2">
|
|
10
13
|
{{ message }} Do you want to delete anyway?
|
|
11
|
-
</
|
|
14
|
+
</div>
|
|
12
15
|
</v-card-text>
|
|
13
16
|
|
|
14
17
|
<!-- The design's footer: two equal-width buttons, ghost then primary,
|