@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
@@ -1,107 +1,105 @@
1
+ <!--
2
+ HID READER MANAGEMENT - the readers on this site's doors.
3
+
4
+ Was a bespoke screen: an above-card toolbar, a raw `v-table` and about
5
+ twenty-five hardcoded hexes. Those hexes are why this screen had no dark
6
+ theme - `#111827` on the dark card measures **1.06:1**, and `#475467` 2.17:1.
7
+
8
+ It is the design's standard table screen, so it is drawn with the shared one:
9
+ `TableMain` supplies the page heading, the card, the in-card toolbar, the
10
+ uppercase header row, the 49px rows and the pager. Every column, action,
11
+ dialog and handler is the same - the table's own `filteredReaders` still
12
+ feeds it, the overflow menu still opens the same five dialogs.
13
+
14
+ The handoff draws no HID table, so it states no column proportions for it -
15
+ the table takes the shared automatic layout every other `TableMain` caller
16
+ uses, and scrolls inside the card at 360/768 the way the other wide tables
17
+ do. See `readerHeaders` for why the screen's own fixed percentages were
18
+ measured and dropped rather than carried over.
19
+ -->
1
20
  <template>
2
21
  <section class="hid-reader-management">
3
- <div class="toolbar">
4
- <v-btn
5
- rounded="xl"
6
- class="text-none add-button"
7
- variant="flat"
8
- prepend-icon="mdi-plus"
9
- @click="openAdd"
10
- >
11
- Add HID Reader
12
- </v-btn>
13
-
14
- <div class="toolbar-spacer" />
15
-
16
- <v-text-field
17
- v-model="search"
18
- placeholder="Search"
19
- variant="outlined"
20
- density="compact"
21
- hide-details
22
- class="search-input"
23
- prepend-inner-icon="mdi-magnify"
24
- />
25
-
26
- <v-select
27
- v-model="status"
28
- :items="statusOptions"
29
- variant="outlined"
30
- density="compact"
31
- hide-details
32
- class="status-input"
33
- />
34
- </div>
35
-
36
- <v-card flat border class="table-card">
37
- <div class="table-refresh">
38
- <v-btn icon="mdi-refresh" variant="text" density="comfortable" :loading="loading" @click="loadReaders" />
39
- </div>
40
-
41
- <v-table class="reader-table">
42
- <thead>
43
- <tr>
44
- <th class="name-column">Name</th>
45
- <th class="location-column">Location</th>
46
- <th class="device-column">Device ID</th>
47
- <th class="status-column">Status</th>
48
- <th class="sync-at-column">Last sync at</th>
49
- <th class="message-column">Last sync message</th>
50
- <th class="action-column"></th>
51
- </tr>
52
- </thead>
53
- <tbody>
54
- <tr v-for="reader in filteredReaders" :key="reader._id">
55
- <td class="name-cell">
56
- <span class="cell-strong">{{ reader.name || "N/A" }}</span>
57
- </td>
58
- <td class="location-cell">{{ reader.location || "N/A" }}</td>
59
- <td class="device-cell">{{ reader.deviceId || "N/A" }}</td>
60
- <td class="status-cell">
61
- <v-chip
62
- size="small"
63
- variant="flat"
64
- class="status-chip"
65
- :class="getStatusClass(resolveReaderStatus(reader))"
66
- >
67
- {{ formatReaderStatus(resolveReaderStatus(reader)) }}
68
- </v-chip>
69
- </td>
70
- <td class="sync-at-cell">{{ formatDate(reader.lastSyncAt) }}</td>
71
- <td class="message-cell">
72
- <span class="message-text" :title="reader.lastSyncMessage || 'N/A'">
73
- {{ reader.lastSyncMessage || "N/A" }}
74
- </span>
75
- </td>
76
- <td class="action-cell">
77
- <v-menu>
78
- <template #activator="{ props: menuProps }">
79
- <v-btn
80
- v-bind="menuProps"
81
- icon="mdi-dots-vertical"
82
- variant="text"
83
- density="comfortable"
84
- class="action-menu-button"
85
- />
86
- </template>
87
-
88
- <v-list density="compact" min-width="180">
89
- <v-list-item title="View" @click="openView(reader)" />
90
- <v-list-item title="Sync HID Reader" @click="openSyncConfirm(reader)" />
91
- <v-list-item title="Test Connection" @click="openTestConfirm(reader)" />
92
- <v-list-item title="Edit" @click="openEdit(reader)" />
93
- <v-list-item title="Delete" class="text-error" @click="openDeleteConfirm(reader)" />
94
- </v-list>
95
- </v-menu>
96
- </td>
97
- </tr>
98
- </tbody>
99
- </v-table>
100
-
101
- <div v-if="!filteredReaders.length" class="empty-state">
102
- No HID devices added yet.
103
- </div>
104
- </v-card>
22
+ <TableMain
23
+ title="HID Readers"
24
+ :headers="readerHeaders"
25
+ :items="filteredReaders"
26
+ :loading="loading"
27
+ :items-per-page="-1"
28
+ :page-range="readerRange"
29
+ show-header
30
+ @refresh="loadReaders"
31
+ >
32
+ <template #actions>
33
+ <AppButton icon="mdi-plus" @click="openAdd">Add HID Reader</AppButton>
34
+ </template>
35
+
36
+ <!-- The design puts filters on their own full-width row under the
37
+ toolbar, which is what `extension` is. -->
38
+ <template #extension>
39
+ <div class="app-filter-row hid-reader-filters">
40
+ <AppField v-model="search" search placeholder="Search" />
41
+ <AppSelect v-model="status" :items="statusOptions" />
42
+ </div>
43
+ </template>
44
+
45
+ <template #[`item.name`]="{ item }">
46
+ <span class="app-cell--strong">{{ item.name || "N/A" }}</span>
47
+ </template>
48
+
49
+ <template #[`item.location`]="{ item }">
50
+ <span class="app-cell--muted">{{ item.location || "N/A" }}</span>
51
+ </template>
52
+
53
+ <template #[`item.deviceId`]="{ item }">
54
+ <span class="app-cell--muted">{{ item.deviceId || "N/A" }}</span>
55
+ </template>
56
+
57
+ <!-- The shared chip, so "Active" is the same green here as on a work
58
+ order. `utils/status.ts` is the mapping; this screen's own
59
+ `getStatusClass` was a fourth private copy of it. -->
60
+ <template #[`item.status`]="{ item }">
61
+ <StatusChip
62
+ :status="resolveReaderStatus(item)"
63
+ :label="formatReaderStatus(resolveReaderStatus(item))"
64
+ />
65
+ </template>
66
+
67
+ <template #[`item.lastSyncAt`]="{ item }">
68
+ <span class="app-cell--num">{{ formatDate(item.lastSyncAt) }}</span>
69
+ </template>
70
+
71
+ <template #[`item.lastSyncMessage`]="{ item }">
72
+ <span class="app-cell--muted" :title="item.lastSyncMessage || 'N/A'">
73
+ {{ item.lastSyncMessage || "N/A" }}
74
+ </span>
75
+ </template>
76
+
77
+ <template #[`item.action-table`]="{ item }">
78
+ <v-menu>
79
+ <template #activator="{ props: menuProps }">
80
+ <AppButton v-bind="menuProps" variant="row" icon="mdi-dots-vertical" />
81
+ </template>
82
+
83
+ <v-list density="compact" min-width="180">
84
+ <v-list-item title="View" @click="openView(item)" />
85
+ <v-list-item title="Sync HID Reader" @click="openSyncConfirm(item)" />
86
+ <v-list-item title="Test Connection" @click="openTestConfirm(item)" />
87
+ <v-list-item title="Edit" @click="openEdit(item)" />
88
+ <v-list-item title="Delete" class="text-error" @click="openDeleteConfirm(item)" />
89
+ </v-list>
90
+ </v-menu>
91
+ </template>
92
+
93
+ <!-- The screen's own sentence, kept: the shared empty state says "No data
94
+ available", which is less useful on a screen whose whole point is
95
+ that no device has been added yet. -->
96
+ <template #no-data>
97
+ <div class="table-card__empty">
98
+ <v-icon icon="mdi-card-account-details-outline" size="32" />
99
+ <span>No HID devices added yet.</span>
100
+ </div>
101
+ </template>
102
+ </TableMain>
105
103
 
106
104
  <HidReaderForm
107
105
  v-model="formDialog"
@@ -113,7 +111,7 @@
113
111
 
114
112
  <v-dialog v-model="viewDialog" max-width="390">
115
113
  <v-card rounded="lg">
116
- <v-card-title class="small-title">{{ selectedReader?.name || "HID Reader" }}</v-card-title>
114
+ <v-card-title class="screen-card-title">{{ selectedReader?.name || "HID Reader" }}</v-card-title>
117
115
  <v-card-text>
118
116
  <div class="detail-grid">
119
117
  <span>HID reader name</span><strong>{{ selectedReader?.name || "N/A" }}</strong>
@@ -126,35 +124,35 @@
126
124
  </div>
127
125
 
128
126
  <div class="dialog-actions-inline">
129
- <v-btn rounded="xl" class="text-none" variant="tonal" @click="openTestConfirm(selectedReader)">Test Connection</v-btn>
130
- <v-btn rounded="xl" class="text-none" variant="tonal" @click="openSyncConfirm(selectedReader)">Sync Reader</v-btn>
127
+ <AppButton variant="ghost" @click="openTestConfirm(selectedReader)">Test Connection</AppButton>
128
+ <AppButton variant="ghost" @click="openSyncConfirm(selectedReader)">Sync Reader</AppButton>
131
129
  </div>
132
130
  </v-card-text>
133
131
  <v-card-actions>
134
132
  <v-spacer />
135
- <v-btn class="text-none" variant="text" @click="viewDialog = false">Close</v-btn>
133
+ <AppButton variant="ghost" @click="viewDialog = false">Close</AppButton>
136
134
  </v-card-actions>
137
135
  </v-card>
138
136
  </v-dialog>
139
137
 
140
138
  <v-dialog v-model="confirmDialog" max-width="390" persistent>
141
139
  <v-card rounded="lg" class="reader-action-dialog">
142
- <v-card-title class="small-title">{{ confirmTitle }}</v-card-title>
140
+ <v-card-title class="screen-card-title">{{ confirmTitle }}</v-card-title>
143
141
 
144
142
  <v-card-text v-if="pendingAction === 'test'" class="reader-action-content">
145
- <v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
143
+ <v-icon icon="mdi-database-sync-outline" class="dialog-mark" size="48" />
146
144
  <h3>Test Device Connection</h3>
147
145
  <p>You are about to test the connection for {{ selectedReader?.name || "this HID Reader" }}.</p>
148
146
  </v-card-text>
149
147
 
150
148
  <v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'confirm'" class="reader-action-content">
151
- <v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
149
+ <v-icon icon="mdi-database-sync-outline" class="dialog-mark" size="48" />
152
150
  <h3>Sync Data</h3>
153
151
  <p>You're about to sync data with {{ selectedReader?.name || "this HID Reader" }}.</p>
154
152
  </v-card-text>
155
153
 
156
154
  <v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'review'" class="reader-action-content reader-sync-review">
157
- <v-icon icon="mdi-database-sync-outline" color="#1976d2" size="48" />
155
+ <v-icon icon="mdi-database-sync-outline" class="dialog-mark" size="48" />
158
156
  <h3>Ready to Synchronize</h3>
159
157
  <div class="sync-summary-list">
160
158
  <div>
@@ -182,7 +180,7 @@
182
180
  </v-card-text>
183
181
 
184
182
  <v-card-text v-else-if="pendingAction === 'sync' && actionStage === 'loading'" class="reader-action-content">
185
- <v-progress-circular indeterminate color="#8a8f98" size="42" width="5" />
183
+ <v-progress-circular indeterminate class="dialog-spinner" size="42" width="5" />
186
184
  <h3>Sync Data to {{ selectedReader?.name || "HID Reader" }}...</h3>
187
185
  </v-card-text>
188
186
 
@@ -208,7 +206,7 @@
208
206
 
209
207
  <v-dialog v-model="resultDialog" max-width="390">
210
208
  <v-card rounded="lg">
211
- <v-card-title v-if="resultReaderName" class="small-title">{{ resultReaderName }}</v-card-title>
209
+ <v-card-title v-if="resultReaderName" class="screen-card-title">{{ resultReaderName }}</v-card-title>
212
210
  <v-card-text class="result-content" :class="{ 'result-with-stats': resultStats }">
213
211
  <v-icon :icon="resultSuccess ? 'mdi-check-circle' : 'mdi-alert-circle'" :color="resultSuccess ? 'success' : 'warning'" size="42" />
214
212
  <h3>{{ resultTitle }}</h3>
@@ -284,6 +282,37 @@ const resultStats = ref<typeof syncPreviewStats.value | null>(null);
284
282
 
285
283
  const statusOptions = ["Status", "active", "inactive", "deleted", "offline"];
286
284
 
285
+ /**
286
+ * The columns. NO `weight`, on purpose.
287
+ *
288
+ * The old CSS pinned them at 19/14/15/12/15/20/5 % with `table-layout: fixed`,
289
+ * and `TableMain`'s `weight` would say exactly that - but it was measured and
290
+ * it is worse than the automatic layout, in two ways the design cares about:
291
+ *
292
+ * * at 360 the six percentages resolve against a ~330px card, so every cell
293
+ * truncates to "Mai...", the headers overlap and the status chip is cut in
294
+ * half. The old fixed grid only worked because it also carried
295
+ * `min-width: 920px`, which is the automatic layout's behaviour anyway.
296
+ * * a 5% action column is ~29px of content once the shared 20px cell padding
297
+ * is taken out, so the 30px row button overflows by half a pixel and the
298
+ * fixed-layout ellipsis rule paints a "..." beside every button.
299
+ *
300
+ * The handoff states `fr` proportions for the tables it draws (Feedbacks,
301
+ * Units, Pass & Key) and draws no HID table at all, so there is no design
302
+ * number to honour here. Automatic is what the other 39 `TableMain` callers
303
+ * use. `sortable: false` keeps the previous behaviour: the raw `v-table` never
304
+ * sorted.
305
+ */
306
+ const readerHeaders = [
307
+ { title: "Name", value: "name", sortable: false },
308
+ { title: "Location", value: "location", sortable: false },
309
+ { title: "Device ID", value: "deviceId", sortable: false },
310
+ { title: "Status", value: "status", sortable: false },
311
+ { title: "Last sync at", value: "lastSyncAt", sortable: false },
312
+ { title: "Last sync message", value: "lastSyncMessage", sortable: false },
313
+ { title: "", value: "action-table", sortable: false },
314
+ ];
315
+
287
316
  const filteredReaders = computed(() => {
288
317
  const query = search.value.trim().toLowerCase();
289
318
  return readers.value.filter((reader) => {
@@ -301,6 +330,15 @@ const filteredReaders = computed(() => {
301
330
  });
302
331
  });
303
332
 
333
+ /**
334
+ * The design's `1-6 of 6` count on the toolbar row. This screen loads every
335
+ * reader in one call and has never paged, so the range is simply what is on
336
+ * screen - stated rather than left as `TableMain`'s `-- - -- of --` placeholder.
337
+ */
338
+ const readerRange = computed(() =>
339
+ filteredReaders.value.length ? `1 - ${filteredReaders.value.length} of ${filteredReaders.value.length}` : "0 of 0"
340
+ );
341
+
304
342
  const confirmTitle = computed(() => {
305
343
  if (pendingAction.value === "sync" && actionStage.value === "loading") return "Test Connection...";
306
344
  if (pendingAction.value === "sync") return "Sync Data";
@@ -916,13 +954,6 @@ function formatDate(value?: string) {
916
954
  return Number.isNaN(date.getTime()) ? "N/A" : date.toLocaleString();
917
955
  }
918
956
 
919
- function getStatusClass(value?: string) {
920
- if (value === "active") return "status-success";
921
- if (value === "inactive") return "status-warning";
922
- if (value === "deleted") return "status-error";
923
- return "status-muted";
924
- }
925
-
926
957
  function resolveReaderStatus(reader?: Record<string, any> | null) {
927
958
  if (!reader) return "";
928
959
  if (reader.lastSyncStatus === "failed" && reader.status === "active") return "offline";
@@ -943,208 +974,49 @@ function formatReaderStatus(value?: string) {
943
974
  }
944
975
  </script>
945
976
 
977
+
946
978
  <style scoped>
979
+ /*
980
+ WHAT IS LEFT AFTER THE CARD AND TABLE MOVED TO `TableMain`.
981
+
982
+ The toolbar, the card, the `reader-table` grid, the seven column widths, the
983
+ header band, the row height, the hover, the empty state and the four
984
+ `status-*` chip palettes are all gone - they were a private copy of the shared
985
+ table, drawn in about twenty-five literal hexes. What remains is the two
986
+ dialogs, and every colour in them is now a token, so the dark theme reaches
987
+ them for the first time.
988
+ */
947
989
  .hid-reader-management {
948
990
  padding: 24px 0;
949
991
  width: 100%;
950
992
  min-width: 0;
951
993
  }
952
994
 
953
- .toolbar {
954
- display: flex;
955
- flex-wrap: wrap;
956
- gap: 12px;
957
- align-items: center;
958
- margin-bottom: 16px;
959
- }
960
-
961
- .toolbar-spacer {
962
- flex: 1;
963
- }
964
-
965
- .add-button {
966
- min-width: 170px;
967
- background: #e4e4e4;
968
- color: #0a2638;
995
+ .hid-reader-filters {
996
+ /* The filter row sits INSIDE the card's extension, so it needs the card's
997
+ own side padding rather than the row's default bottom margin. */
998
+ margin: 0;
999
+ padding: 10px 14px;
969
1000
  }
970
1001
 
971
- .search-input {
1002
+ .hid-reader-filters :deep(.app-field) {
972
1003
  max-width: 240px;
973
1004
  }
974
1005
 
975
- .status-input {
1006
+ .hid-reader-filters :deep(.app-select) {
976
1007
  max-width: 160px;
977
1008
  }
978
1009
 
979
- .table-card {
980
- min-height: 430px;
981
- overflow-x: auto;
982
- overflow-y: hidden;
983
- }
984
-
985
- .reader-table :deep(table) {
986
- table-layout: fixed;
987
- width: 100%;
988
- min-width: 920px;
989
- border-collapse: separate;
990
- border-spacing: 0;
991
- }
992
-
993
- .reader-table :deep(th) {
994
- height: 44px;
995
- background: #f8fafc;
996
- color: #344054;
997
- font-size: 12px;
998
- font-weight: 700;
999
- letter-spacing: 0;
1000
- text-transform: none;
1001
- border-bottom: 1px solid #e7ebef;
1002
- }
1003
-
1004
- .reader-table :deep(td) {
1005
- height: 64px;
1006
- color: #1f2937;
1007
- font-size: 13px;
1008
- border-bottom: 1px solid #edf0f3;
1009
- vertical-align: middle;
1010
- }
1011
-
1012
- .reader-table :deep(tbody tr) {
1013
- transition: background-color 0.15s ease;
1014
- }
1015
-
1016
- .reader-table :deep(tbody tr:hover) {
1017
- background: #fbfcfe;
1018
- }
1019
-
1020
- .reader-table :deep(th),
1021
- .reader-table :deep(td) {
1022
- padding: 0 14px !important;
1023
- white-space: nowrap;
1024
- }
1025
-
1026
- .name-column,
1027
- .name-cell {
1028
- width: 19%;
1029
- }
1030
-
1031
- .location-column,
1032
- .location-cell {
1033
- width: 14%;
1034
- }
1035
-
1036
- .device-column,
1037
- .device-cell {
1038
- width: 15%;
1039
- }
1040
-
1041
- .status-column,
1042
- .status-cell {
1043
- width: 12%;
1044
- }
1045
-
1046
- .sync-at-column,
1047
- .sync-at-cell {
1048
- width: 15%;
1049
- }
1050
-
1051
- .message-column,
1052
- .message-cell {
1053
- width: 20%;
1054
- min-width: 0;
1055
- }
1056
-
1057
- .action-column,
1058
- .action-cell {
1059
- width: 5%;
1060
- text-align: right;
1061
- }
1062
-
1063
- .cell-strong {
1064
- display: block;
1065
- overflow: hidden;
1066
- color: #111827;
1067
- font-weight: 600;
1068
- text-overflow: ellipsis;
1069
- }
1070
-
1071
- .location-cell,
1072
- .device-cell,
1073
- .sync-at-cell {
1074
- overflow: hidden;
1075
- color: #475467;
1076
- text-overflow: ellipsis;
1077
- }
1078
-
1079
- .message-text {
1080
- display: block;
1081
- max-width: 100%;
1082
- overflow: hidden;
1083
- color: #475467;
1084
- line-height: 1.35;
1085
- text-overflow: ellipsis;
1086
- }
1087
-
1088
- .action-menu-button {
1089
- color: #344054;
1090
- }
1091
-
1092
- .table-refresh {
1093
- display: flex;
1094
- justify-content: flex-start;
1095
- padding: 10px 14px;
1096
- border-bottom: 1px solid #e7ebef;
1097
- background: #ffffff;
1098
- }
1099
-
1100
- .empty-state {
1101
- min-height: 280px;
1102
- display: grid;
1103
- place-items: center;
1104
- color: #687382;
1105
- font-size: 13px;
1106
- }
1107
-
1108
- .status-chip {
1109
- min-width: 86px;
1110
- justify-content: center;
1111
- font-weight: 600;
1112
- }
1113
-
1114
- .status-success {
1115
- background: #dff7e7 !important;
1116
- color: #1b7f3a !important;
1117
- }
1118
-
1119
- .status-warning {
1120
- background: #fff0cc !important;
1121
- color: #9a6700 !important;
1122
- }
1123
-
1124
- .status-error {
1125
- background: #fde2e1 !important;
1126
- color: #b42318 !important;
1127
- }
1128
-
1129
- .status-muted {
1130
- background: #e9edf2 !important;
1131
- color: #475467 !important;
1132
- }
1133
-
1134
- .small-title {
1135
- font-size: 14px;
1136
- font-weight: 700;
1137
- }
1138
-
1139
1010
  .detail-grid {
1140
1011
  display: grid;
1141
1012
  grid-template-columns: 1fr 1fr;
1142
1013
  gap: 8px 16px;
1143
1014
  font-size: 13px;
1015
+ color: var(--text);
1144
1016
  }
1145
1017
 
1146
1018
  .detail-grid span {
1147
- color: #667085;
1019
+ color: var(--muted);
1148
1020
  }
1149
1021
 
1150
1022
  .dialog-actions-inline {
@@ -1169,7 +1041,7 @@ function formatReaderStatus(value?: string) {
1169
1041
 
1170
1042
  .reader-action-content h3 {
1171
1043
  margin: 4px 0 0;
1172
- color: #111827;
1044
+ color: var(--text);
1173
1045
  font-size: 15px;
1174
1046
  font-weight: 700;
1175
1047
  line-height: 1.3;
@@ -1177,11 +1049,21 @@ function formatReaderStatus(value?: string) {
1177
1049
 
1178
1050
  .reader-action-content p {
1179
1051
  margin: 0;
1180
- color: #667085;
1052
+ color: var(--muted);
1181
1053
  font-size: 13px;
1182
1054
  line-height: 1.45;
1183
1055
  }
1184
1056
 
1057
+ /* Was `color="#1976d2"` and `color="#8a8f98"` on the elements themselves - the
1058
+ same blue and the same grey on both themes. */
1059
+ .dialog-mark {
1060
+ color: var(--accent);
1061
+ }
1062
+
1063
+ .dialog-spinner {
1064
+ color: var(--muted);
1065
+ }
1066
+
1185
1067
  .reader-sync-review {
1186
1068
  justify-items: stretch;
1187
1069
  text-align: left;
@@ -1205,24 +1087,32 @@ function formatReaderStatus(value?: string) {
1205
1087
  align-items: center;
1206
1088
  justify-content: space-between;
1207
1089
  gap: 16px;
1208
- color: #667085;
1090
+ color: var(--muted);
1209
1091
  font-size: 13px;
1210
1092
  }
1211
1093
 
1212
1094
  .sync-summary-list strong {
1213
- color: #111827;
1095
+ color: var(--text);
1214
1096
  font-weight: 700;
1215
1097
  }
1216
1098
 
1099
+ /* The split footer bar. The handoff does not draw one, so the SHAPE is the
1100
+ screen's own; only the colours change - `#062d42` was a fixed navy that sat
1101
+ on the dark card unchanged. */
1217
1102
  .action-cancel,
1218
1103
  .action-submit {
1219
1104
  flex: 1;
1220
1105
  border-radius: 0;
1221
1106
  }
1222
1107
 
1108
+ .action-cancel {
1109
+ background: var(--card);
1110
+ color: var(--text2);
1111
+ }
1112
+
1223
1113
  .action-submit {
1224
- background: #062d42;
1225
- color: #ffffff;
1114
+ background: var(--accent-strong);
1115
+ color: var(--on-accent-strong);
1226
1116
  }
1227
1117
 
1228
1118
  .result-content {
@@ -1246,24 +1136,11 @@ function formatReaderStatus(value?: string) {
1246
1136
  }
1247
1137
 
1248
1138
  @media (max-width: 960px) {
1249
- .toolbar {
1250
- align-items: stretch;
1251
- }
1252
-
1253
- .toolbar-spacer {
1254
- display: none;
1255
- }
1256
-
1257
- .add-button,
1258
- .search-input,
1259
- .status-input {
1139
+ .hid-reader-filters :deep(.app-field),
1140
+ .hid-reader-filters :deep(.app-select) {
1260
1141
  max-width: none;
1261
1142
  width: 100%;
1262
1143
  }
1263
-
1264
- .table-card {
1265
- min-height: 360px;
1266
- }
1267
1144
  }
1268
1145
 
1269
1146
  @media (max-width: 600px) {
@@ -1275,7 +1152,7 @@ function formatReaderStatus(value?: string) {
1275
1152
  grid-template-columns: 1fr;
1276
1153
  }
1277
1154
 
1278
- .dialog-actions-inline .v-btn {
1155
+ .dialog-actions-inline .app-btn {
1279
1156
  width: 100%;
1280
1157
  }
1281
1158