@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.
Files changed (35) hide show
  1. package/assets/css/primitives.css +168 -0
  2. package/assets/css/screens.css +76 -1
  3. package/components/AccessCard/AvailableStats.vue +24 -16
  4. package/components/AddPassKeyToVisitor.vue +95 -73
  5. package/components/AppDateField.vue +15 -1
  6. package/components/AppField.vue +25 -0
  7. package/components/BuildingUnitFormEdit.vue +5 -1
  8. package/components/Card/DeleteConfirmation.vue +5 -2
  9. package/components/HidAccessLogDashboard.vue +288 -447
  10. package/components/HidIntercomManagement.vue +387 -521
  11. package/components/HidQrCodeConfiguration.vue +293 -369
  12. package/components/HidReaderManagement.vue +197 -320
  13. package/components/HidServiceSettingsPanel.vue +38 -26
  14. package/components/HidUserEnrollment.vue +239 -344
  15. package/components/IncidentReport/Authorities.vue +30 -18
  16. package/components/Nfc/NFCTagForm.vue +5 -1
  17. package/components/ScanVisitorQRCode.vue +64 -28
  18. package/components/SearchVehicleNumberUser.vue +39 -37
  19. package/components/TableMain.vue +50 -1
  20. package/components/VehicleAddSelection.vue +48 -32
  21. package/components/VehicleForm.vue +112 -129
  22. package/components/VehicleUpdateMoreAction.vue +59 -46
  23. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  24. package/components/VisitorFormSelection.vue +40 -37
  25. package/components/VisitorPassKeyQRScanner.vue +65 -18
  26. package/components/VisitorSocketPopUp.vue +21 -13
  27. package/components/VisitorsReportPreview.vue +44 -29
  28. package/package.json +1 -1
  29. package/tools/render-harness/baselines.json +95 -0
  30. package/tools/render-harness/harness-init.ps1 +4 -0
  31. package/utils/status.test.ts +33 -0
  32. package/utils/status.ts +36 -0
  33. package/utils/theme-aa-ledger.ts +142 -0
  34. package/utils/theme.test.ts +95 -34
  35. 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
  /* ------------------------------------------------------------------ */
@@ -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: 10px var(--cellpad-x) !important;
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
- <v-chip
5
- size="small"
6
- variant="tonal"
7
- color="primary"
8
- prepend-icon="mdi-card-account-details"
9
- :loading="loading"
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
- </v-chip>
13
- <v-chip
14
- size="small"
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
- </v-chip>
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
- <v-toolbar>
4
- <v-row
5
- no-gutters
6
- class="fill-height px-6 d-flex justify-space-between align-center"
7
- align="center"
8
- >
9
- <span class="font-weight-bold text-subtitle-1"
10
- >{{ isEditMode ? "Edit" : "Assign" }} Pass &amp; Keys</span
11
- >
12
- <ButtonClose @click="emit('close')" />
13
- </v-row>
14
- </v-toolbar>
15
-
16
- <v-card-text>
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 &amp; 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="text-subtitle-2 text-medium-emphasis">
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
- <v-chip
31
- prepend-icon="mdi-card-bulleted-outline"
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
- </v-chip>
38
- <span class="text-caption text-medium-emphasis">(current)</span>
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
- <v-chip
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
- prepend-icon="mdi-key"
86
- color="orange"
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
- </v-chip>
94
- <span class="text-caption text-medium-emphasis align-self-center">
95
- (current)
96
- </span>
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="text-error text-subtitle-2 w-100 text-center">
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
- </v-toolbar>
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></style>
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({
@@ -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="black" class="text-none" height="48" v-bind="props" tile
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
- <span v-if="message" class="text-error mt-2">
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
- </span>
14
+ </div>
12
15
  </v-card-text>
13
16
 
14
17
  <!-- The design's footer: two equal-width buttons, ghost then primary,