@7365admin1/layer-common 3.2.2-staging.120 → 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/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/Nfc/NFCTagForm.vue +5 -1
- package/components/TableMain.vue +50 -1
- package/components/VisitorsReportPreview.vue +4 -1
- 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
|
@@ -1,276 +1,229 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
HID QR CODE CONFIGURATION - the reader's own settings, inside Site Settings.
|
|
3
|
+
|
|
4
|
+
It is mounted in `SiteSettings.vue`'s "HID Face Reader" expansion panel, next
|
|
5
|
+
to `HidServiceSettingsPanel`, so it takes the shape the handoff gives a
|
|
6
|
+
settings module BODY: section labels at 13px/700, explanatory lines at
|
|
7
|
+
12-13px/600 in `--muted`, the 38x22 toggle, 40px fields and 38px buttons.
|
|
8
|
+
|
|
9
|
+
Two pieces of legacy chrome are gone rather than restyled:
|
|
10
|
+
* the `#082e43` navy title bar. The design puts NO card inside an accordion
|
|
11
|
+
body - the accordion head ("HID Face Reader") is the card, and a second
|
|
12
|
+
titled panel inside it is a header the design does not draw. Its title is
|
|
13
|
+
kept as the body's first section label.
|
|
14
|
+
* the chevron button that sat on that bar. It had no `@click` and no model:
|
|
15
|
+
it looked like a collapse control and did nothing.
|
|
16
|
+
|
|
17
|
+
Every field, toggle, dialog, handler and save call is otherwise unchanged.
|
|
18
|
+
-->
|
|
1
19
|
<template>
|
|
2
20
|
<section class="hid-qr-config">
|
|
3
|
-
<
|
|
4
|
-
<div class="
|
|
21
|
+
<div class="config-body">
|
|
22
|
+
<div class="section-label config-body__title">HID Configuration</div>
|
|
23
|
+
|
|
24
|
+
<div class="online-mode-row">
|
|
5
25
|
<div>
|
|
6
|
-
<
|
|
26
|
+
<h3>Activate Online Mode</h3>
|
|
27
|
+
<p>Enables real-time server communication, synchronizing live credential verification and access data across the network.</p>
|
|
7
28
|
</div>
|
|
8
|
-
<v-
|
|
29
|
+
<v-switch
|
|
30
|
+
v-model="form.onlineMode"
|
|
31
|
+
class="app-switch"
|
|
32
|
+
hide-details
|
|
33
|
+
:loading="savingSection === 'online-mode'"
|
|
34
|
+
:disabled="Boolean(savingSection) || !form.readerId"
|
|
35
|
+
@update:model-value="saveOnlineMode"
|
|
36
|
+
/>
|
|
9
37
|
</div>
|
|
10
38
|
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
<v-divider />
|
|
40
|
+
|
|
41
|
+
<div class="form-section identification-section">
|
|
42
|
+
<div class="section-label">Enable/disable reader identification method</div>
|
|
43
|
+
<p class="section-description">
|
|
44
|
+
Toggles multi-factor access methods, allowing users to authenticate via biometric, physical card, or credential inputs.
|
|
45
|
+
</p>
|
|
46
|
+
<div class="methods-grid">
|
|
47
|
+
<div
|
|
48
|
+
v-for="method in identificationMethodOptions"
|
|
49
|
+
:key="method.key"
|
|
50
|
+
class="method-control"
|
|
51
|
+
:class="{ 'method-control--disabled': !form.onlineMode }"
|
|
52
|
+
>
|
|
53
|
+
<span>{{ method.label }}</span>
|
|
54
|
+
<v-switch
|
|
55
|
+
v-model="form.identificationMethods[method.key]"
|
|
56
|
+
class="app-switch"
|
|
57
|
+
hide-details
|
|
58
|
+
density="compact"
|
|
59
|
+
:loading="savingSection === `method-${method.key}`"
|
|
60
|
+
:disabled="Boolean(savingSection) || !form.onlineMode"
|
|
61
|
+
@update:model-value="saveIdentificationMethod(method.key)"
|
|
62
|
+
/>
|
|
16
63
|
</div>
|
|
17
|
-
<v-switch
|
|
18
|
-
v-model="form.onlineMode"
|
|
19
|
-
class="hid-config-switch"
|
|
20
|
-
color="success"
|
|
21
|
-
hide-details
|
|
22
|
-
inset
|
|
23
|
-
:loading="savingSection === 'online-mode'"
|
|
24
|
-
:disabled="Boolean(savingSection) || !form.readerId"
|
|
25
|
-
@update:model-value="saveOnlineMode"
|
|
26
|
-
/>
|
|
27
64
|
</div>
|
|
65
|
+
</div>
|
|
28
66
|
|
|
29
|
-
|
|
67
|
+
<v-divider />
|
|
68
|
+
|
|
69
|
+
<div class="form-section">
|
|
70
|
+
<div class="section-label">Printer Setup</div>
|
|
71
|
+
<div class="printer-grid">
|
|
72
|
+
<AppField v-model="form.printer.vendorId" placeholder="Vendor" />
|
|
73
|
+
<AppField v-model="form.printer.productId" placeholder="Product Id" />
|
|
74
|
+
<AppButton variant="ghost" icon="mdi-magnify" @click="scanPrinter">
|
|
75
|
+
Scan Printer
|
|
76
|
+
</AppButton>
|
|
77
|
+
<AppButton
|
|
78
|
+
variant="ghost"
|
|
79
|
+
icon="mdi-printer-check"
|
|
80
|
+
:disabled="!hasPrinterConfig || testingPrinter"
|
|
81
|
+
@click="testPrint"
|
|
82
|
+
>
|
|
83
|
+
Test & Print
|
|
84
|
+
</AppButton>
|
|
85
|
+
<AppButton
|
|
86
|
+
icon="mdi-printer"
|
|
87
|
+
:disabled="savingSection === 'printer'"
|
|
88
|
+
@click="saveSection('printer')"
|
|
89
|
+
>
|
|
90
|
+
Save Settings
|
|
91
|
+
</AppButton>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
30
94
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
hide-details
|
|
49
|
-
inset
|
|
50
|
-
density="compact"
|
|
51
|
-
:loading="savingSection === `method-${method.key}`"
|
|
52
|
-
:disabled="Boolean(savingSection) || !form.onlineMode"
|
|
53
|
-
@update:model-value="saveIdentificationMethod(method.key)"
|
|
54
|
-
/>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
95
|
+
<v-divider />
|
|
96
|
+
|
|
97
|
+
<div class="form-section">
|
|
98
|
+
<div class="section-label">HID QR Code Template</div>
|
|
99
|
+
<div class="template-grid">
|
|
100
|
+
<AppField v-model="form.template.header" placeholder="Header" />
|
|
101
|
+
<AppField v-model="form.template.subtext" placeholder="Subtext" />
|
|
102
|
+
<AppButton variant="ghost" icon="mdi-eye" @click="previewDialog = true">
|
|
103
|
+
Preview
|
|
104
|
+
</AppButton>
|
|
105
|
+
<AppButton
|
|
106
|
+
icon="mdi-qrcode-scan"
|
|
107
|
+
:disabled="savingSection === 'template'"
|
|
108
|
+
@click="saveSection('template')"
|
|
109
|
+
>
|
|
110
|
+
Save
|
|
111
|
+
</AppButton>
|
|
57
112
|
</div>
|
|
113
|
+
</div>
|
|
58
114
|
|
|
59
|
-
|
|
115
|
+
<v-divider />
|
|
60
116
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
v-model="form.printer.productId"
|
|
73
|
-
placeholder="Product Id"
|
|
74
|
-
variant="outlined"
|
|
75
|
-
density="compact"
|
|
76
|
-
hide-details
|
|
77
|
-
/>
|
|
78
|
-
<v-btn
|
|
79
|
-
class="text-none utility-button"
|
|
80
|
-
variant="outlined"
|
|
81
|
-
prepend-icon="mdi-magnify"
|
|
82
|
-
height="40"
|
|
83
|
-
@click="scanPrinter"
|
|
84
|
-
>
|
|
85
|
-
Scan Printer
|
|
86
|
-
</v-btn>
|
|
87
|
-
<v-btn
|
|
88
|
-
class="text-none test-button"
|
|
89
|
-
variant="outlined"
|
|
90
|
-
prepend-icon="mdi-printer-check"
|
|
91
|
-
height="40"
|
|
92
|
-
:loading="testingPrinter"
|
|
93
|
-
:disabled="!hasPrinterConfig"
|
|
94
|
-
@click="testPrint"
|
|
95
|
-
>
|
|
96
|
-
Test & Print
|
|
97
|
-
</v-btn>
|
|
98
|
-
<v-btn
|
|
99
|
-
class="text-none save-button"
|
|
100
|
-
variant="flat"
|
|
101
|
-
prepend-icon="mdi-printer"
|
|
102
|
-
height="40"
|
|
103
|
-
:loading="savingSection === 'printer'"
|
|
104
|
-
@click="saveSection('printer')"
|
|
105
|
-
>
|
|
106
|
-
Save Settings
|
|
107
|
-
</v-btn>
|
|
108
|
-
</div>
|
|
117
|
+
<div class="form-section">
|
|
118
|
+
<div class="section-label">HID QR Code Validity</div>
|
|
119
|
+
<div class="validity-grid">
|
|
120
|
+
<AppField v-model="validityInput" type="number" placeholder="Validity (minutes)" />
|
|
121
|
+
<AppButton
|
|
122
|
+
icon="mdi-timer-outline"
|
|
123
|
+
:disabled="savingSection === 'validity'"
|
|
124
|
+
@click="saveSection('validity')"
|
|
125
|
+
>
|
|
126
|
+
Save
|
|
127
|
+
</AppButton>
|
|
109
128
|
</div>
|
|
129
|
+
</div>
|
|
110
130
|
|
|
111
|
-
|
|
131
|
+
<v-divider />
|
|
112
132
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<div class="
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
density="compact"
|
|
121
|
-
hide-details
|
|
122
|
-
/>
|
|
123
|
-
<v-text-field
|
|
124
|
-
v-model="form.template.subtext"
|
|
125
|
-
placeholder="Subtext"
|
|
126
|
-
variant="outlined"
|
|
127
|
-
density="compact"
|
|
128
|
-
hide-details
|
|
129
|
-
/>
|
|
130
|
-
<v-btn
|
|
131
|
-
class="text-none preview-button"
|
|
132
|
-
variant="flat"
|
|
133
|
-
prepend-icon="mdi-eye"
|
|
134
|
-
height="40"
|
|
135
|
-
@click="previewDialog = true"
|
|
136
|
-
>
|
|
137
|
-
Preview
|
|
138
|
-
</v-btn>
|
|
139
|
-
<v-btn
|
|
140
|
-
class="text-none save-button"
|
|
141
|
-
variant="flat"
|
|
142
|
-
prepend-icon="mdi-qrcode-scan"
|
|
143
|
-
height="40"
|
|
144
|
-
:loading="savingSection === 'template'"
|
|
145
|
-
@click="saveSection('template')"
|
|
146
|
-
>
|
|
147
|
-
Save
|
|
148
|
-
</v-btn>
|
|
133
|
+
<div v-if="isPermissionManager" class="permission-section">
|
|
134
|
+
<div class="permission-location">
|
|
135
|
+
<div class="permission-copy">
|
|
136
|
+
<div class="section-label">Permission Location</div>
|
|
137
|
+
<p class="section-description">
|
|
138
|
+
Permissions are scoped to the location configured on the HID Face Reader.
|
|
139
|
+
</p>
|
|
149
140
|
</div>
|
|
141
|
+
<AppSelect
|
|
142
|
+
v-model="permissionReaderId"
|
|
143
|
+
class="permission-location__select"
|
|
144
|
+
:items="permissionLocations"
|
|
145
|
+
item-title="label"
|
|
146
|
+
item-value="readerId"
|
|
147
|
+
placeholder="Select a Face Reader location"
|
|
148
|
+
:disabled="permissionLocations.length === 0"
|
|
149
|
+
@update:model-value="loadPermissions"
|
|
150
|
+
/>
|
|
150
151
|
</div>
|
|
151
152
|
|
|
153
|
+
<v-alert
|
|
154
|
+
v-if="permissionLocations.length === 0"
|
|
155
|
+
class="mb-4"
|
|
156
|
+
type="warning"
|
|
157
|
+
variant="tonal"
|
|
158
|
+
density="compact"
|
|
159
|
+
>
|
|
160
|
+
Add a Location to a Face Reader before assigning HID permissions.
|
|
161
|
+
</v-alert>
|
|
162
|
+
|
|
152
163
|
<v-divider />
|
|
153
164
|
|
|
154
|
-
<div class="
|
|
155
|
-
<div class="
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
prepend-icon="mdi-timer-outline"
|
|
170
|
-
height="40"
|
|
171
|
-
:loading="savingSection === 'validity'"
|
|
172
|
-
@click="saveSection('validity')"
|
|
165
|
+
<div class="permission-row">
|
|
166
|
+
<div class="permission-copy">
|
|
167
|
+
<div class="section-label">Access Permission</div>
|
|
168
|
+
<p class="section-description">
|
|
169
|
+
Select who can use the enabled HID identification methods at this site.
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="permission-actions">
|
|
173
|
+
<AppButton
|
|
174
|
+
v-for="option in accessPermissionOptions"
|
|
175
|
+
:key="option.category"
|
|
176
|
+
variant="ghost"
|
|
177
|
+
class="permission-button"
|
|
178
|
+
:disabled="!form.onlineMode || !permissionReaderId"
|
|
179
|
+
@click="openPermissionDialog(option.category)"
|
|
173
180
|
>
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
<span class="permission-count">{{ permissionCount(option.category) }}</span>
|
|
182
|
+
<span>{{ option.label }}</span>
|
|
183
|
+
</AppButton>
|
|
176
184
|
</div>
|
|
177
185
|
</div>
|
|
178
186
|
|
|
179
187
|
<v-divider />
|
|
180
188
|
|
|
181
|
-
<div
|
|
182
|
-
<div class="permission-
|
|
183
|
-
<div class="
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
</p>
|
|
188
|
-
</div>
|
|
189
|
-
<v-select
|
|
190
|
-
v-model="permissionReaderId"
|
|
191
|
-
class="permission-location__select"
|
|
192
|
-
:items="permissionLocations"
|
|
193
|
-
item-title="label"
|
|
194
|
-
item-value="readerId"
|
|
195
|
-
placeholder="Select a Face Reader location"
|
|
196
|
-
variant="outlined"
|
|
197
|
-
density="compact"
|
|
198
|
-
hide-details
|
|
199
|
-
:disabled="permissionLocations.length === 0"
|
|
200
|
-
@update:model-value="loadPermissions"
|
|
201
|
-
/>
|
|
189
|
+
<div class="permission-row permission-row--intercom">
|
|
190
|
+
<div class="permission-copy">
|
|
191
|
+
<div class="section-label">Access Intercom</div>
|
|
192
|
+
<p class="section-description">
|
|
193
|
+
Choose which authorized people and providers may use HID intercom access.
|
|
194
|
+
</p>
|
|
202
195
|
</div>
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<v-divider />
|
|
215
|
-
|
|
216
|
-
<div class="permission-row">
|
|
217
|
-
<div class="permission-copy">
|
|
218
|
-
<div class="section-label">Access Permission</div>
|
|
219
|
-
<p class="section-description">
|
|
220
|
-
Select who can use the enabled HID identification methods at this site.
|
|
221
|
-
</p>
|
|
222
|
-
</div>
|
|
223
|
-
<div class="permission-actions">
|
|
224
|
-
<v-btn
|
|
225
|
-
v-for="option in accessPermissionOptions"
|
|
226
|
-
:key="option.category"
|
|
227
|
-
class="permission-button text-none"
|
|
228
|
-
variant="outlined"
|
|
229
|
-
:disabled="!form.onlineMode || !permissionReaderId"
|
|
230
|
-
@click="openPermissionDialog(option.category)"
|
|
231
|
-
>
|
|
232
|
-
<span class="permission-count">{{ permissionCount(option.category) }}</span>
|
|
233
|
-
<span>{{ option.label }}</span>
|
|
234
|
-
</v-btn>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
|
|
238
|
-
<v-divider />
|
|
239
|
-
|
|
240
|
-
<div class="permission-row permission-row--intercom">
|
|
241
|
-
<div class="permission-copy">
|
|
242
|
-
<div class="section-label">Access Intercom</div>
|
|
243
|
-
<p class="section-description">
|
|
244
|
-
Choose which authorized people and providers may use HID intercom access.
|
|
245
|
-
</p>
|
|
246
|
-
</div>
|
|
247
|
-
<div class="permission-actions permission-actions--single">
|
|
248
|
-
<v-btn
|
|
249
|
-
class="permission-button text-none"
|
|
250
|
-
variant="outlined"
|
|
251
|
-
:disabled="!form.onlineMode || !permissionReaderId || permissionState.assignments.length === 0"
|
|
252
|
-
@click="openPermissionDialog('intercom')"
|
|
253
|
-
>
|
|
254
|
-
<span class="permission-count">{{ permissionState.counts.intercom }}</span>
|
|
255
|
-
<span>Intercom Permissions</span>
|
|
256
|
-
</v-btn>
|
|
257
|
-
</div>
|
|
196
|
+
<div class="permission-actions permission-actions--single">
|
|
197
|
+
<AppButton
|
|
198
|
+
variant="ghost"
|
|
199
|
+
class="permission-button"
|
|
200
|
+
:disabled="!form.onlineMode || !permissionReaderId || permissionState.assignments.length === 0"
|
|
201
|
+
@click="openPermissionDialog('intercom')"
|
|
202
|
+
>
|
|
203
|
+
<span class="permission-count">{{ permissionState.counts.intercom }}</span>
|
|
204
|
+
<span>Intercom Permissions</span>
|
|
205
|
+
</AppButton>
|
|
258
206
|
</div>
|
|
259
207
|
</div>
|
|
260
208
|
</div>
|
|
261
|
-
</
|
|
209
|
+
</div>
|
|
262
210
|
|
|
263
211
|
<v-dialog v-model="previewDialog" max-width="420">
|
|
264
212
|
<v-card class="preview-shell">
|
|
265
|
-
<
|
|
213
|
+
<AppButton
|
|
214
|
+
variant="icon"
|
|
266
215
|
class="preview-close"
|
|
267
216
|
icon="mdi-close"
|
|
268
|
-
variant="text"
|
|
269
|
-
density="comfortable"
|
|
270
217
|
@click="previewDialog = false"
|
|
271
218
|
/>
|
|
272
219
|
<v-card-text>
|
|
273
220
|
<div class="preview-title">QR code Preview</div>
|
|
221
|
+
<!--
|
|
222
|
+
THE PRINTED PASS. Deliberately NOT themed: this is a preview of a
|
|
223
|
+
piece of card that comes out of a printer, so it is white with black
|
|
224
|
+
ink on both themes, the way the physical pass is. Everything AROUND
|
|
225
|
+
it sits on the dialog scrim, which is dark in both themes.
|
|
226
|
+
-->
|
|
274
227
|
<div class="pass-preview">
|
|
275
228
|
<div class="pass-heading">{{ form.template.header || "Header" }}</div>
|
|
276
229
|
<div class="pass-subtext">{{ form.template.subtext || "Subtext" }}</div>
|
|
@@ -302,25 +255,19 @@
|
|
|
302
255
|
<div class="permission-dialog__title">{{ permissionDialogTitle }}</div>
|
|
303
256
|
<div class="permission-dialog__subtitle">{{ permissionDialogDescription }}</div>
|
|
304
257
|
</div>
|
|
305
|
-
<
|
|
258
|
+
<AppButton
|
|
259
|
+
variant="icon"
|
|
306
260
|
icon="mdi-close"
|
|
307
|
-
variant="text"
|
|
308
|
-
density="comfortable"
|
|
309
261
|
aria-label="Close permission dialog"
|
|
310
262
|
@click="closePermissionDialog"
|
|
311
263
|
/>
|
|
312
264
|
</v-card-title>
|
|
313
265
|
|
|
314
266
|
<v-card-text class="permission-dialog__body">
|
|
315
|
-
<
|
|
267
|
+
<AppField
|
|
316
268
|
v-model="permissionSearch"
|
|
317
|
-
|
|
318
|
-
prepend-inner-icon="mdi-magnify"
|
|
269
|
+
search
|
|
319
270
|
placeholder="Search by name or details"
|
|
320
|
-
variant="outlined"
|
|
321
|
-
density="compact"
|
|
322
|
-
hide-details
|
|
323
|
-
clearable
|
|
324
271
|
/>
|
|
325
272
|
|
|
326
273
|
<div class="permission-list-summary" aria-live="polite">
|
|
@@ -332,7 +279,7 @@
|
|
|
332
279
|
|
|
333
280
|
<div class="permission-list" :class="{ 'permission-list--loading': loadingPermissionCandidates }">
|
|
334
281
|
<div v-if="loadingPermissionCandidates" class="permission-empty">
|
|
335
|
-
<v-progress-circular indeterminate
|
|
282
|
+
<v-progress-circular indeterminate class="permission-spinner" size="28" />
|
|
336
283
|
<span>Loading permissions...</span>
|
|
337
284
|
</div>
|
|
338
285
|
|
|
@@ -352,7 +299,6 @@
|
|
|
352
299
|
class="permission-list-item__checkbox"
|
|
353
300
|
:model-value="selectedPermissionIds.has(candidate.subjectId)"
|
|
354
301
|
:aria-label="`Select ${candidate.name}`"
|
|
355
|
-
color="success"
|
|
356
302
|
@update:model-value="togglePermissionCandidate(candidate.subjectId, Boolean($event))"
|
|
357
303
|
/>
|
|
358
304
|
</label>
|
|
@@ -366,17 +312,13 @@
|
|
|
366
312
|
</v-card-text>
|
|
367
313
|
|
|
368
314
|
<v-card-actions class="permission-dialog__actions">
|
|
369
|
-
<
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
variant="flat"
|
|
373
|
-
min-width="140"
|
|
374
|
-
:loading="savingPermissions"
|
|
375
|
-
:disabled="loadingPermissionCandidates"
|
|
315
|
+
<AppButton variant="ghost" @click="closePermissionDialog">Cancel</AppButton>
|
|
316
|
+
<AppButton
|
|
317
|
+
:disabled="loadingPermissionCandidates || savingPermissions"
|
|
376
318
|
@click="savePermissions"
|
|
377
319
|
>
|
|
378
320
|
Save Permissions
|
|
379
|
-
</
|
|
321
|
+
</AppButton>
|
|
380
322
|
</v-card-actions>
|
|
381
323
|
</v-card>
|
|
382
324
|
</v-dialog>
|
|
@@ -385,6 +327,7 @@
|
|
|
385
327
|
</section>
|
|
386
328
|
</template>
|
|
387
329
|
|
|
330
|
+
|
|
388
331
|
<script setup lang="ts">
|
|
389
332
|
type HidIdentificationMethod = keyof NonNullable<THidQrCodePassConfig["identificationMethods"]>;
|
|
390
333
|
|
|
@@ -536,6 +479,19 @@ const siteLabel = computed(() => siteData.value?.code || siteData.value?.block |
|
|
|
536
479
|
const validityText = computed(() =>
|
|
537
480
|
form.validityMinutes ? `${form.validityMinutes} minutes` : "Not set",
|
|
538
481
|
);
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* `AppField` is a native `<input>` and its model is a string, where
|
|
485
|
+
* `form.validityMinutes` is a `number | null` on the config type. This keeps
|
|
486
|
+
* the field's value exactly the number `v-model.number` used to put there -
|
|
487
|
+
* empty stays `null`, not `NaN` and not `""`.
|
|
488
|
+
*/
|
|
489
|
+
const validityInput = computed({
|
|
490
|
+
get: () => (form.validityMinutes === null || form.validityMinutes === undefined ? "" : String(form.validityMinutes)),
|
|
491
|
+
set: (value: string) => {
|
|
492
|
+
form.validityMinutes = value === "" ? null : Number(value);
|
|
493
|
+
},
|
|
494
|
+
});
|
|
539
495
|
const visiblePermissionCandidates = computed(() => {
|
|
540
496
|
const search = permissionSearch.value.trim().toLowerCase();
|
|
541
497
|
if (!search) return permissionCandidates.value;
|
|
@@ -904,38 +860,32 @@ function showMessage(text: string, color: string) {
|
|
|
904
860
|
}
|
|
905
861
|
</script>
|
|
906
862
|
|
|
863
|
+
|
|
907
864
|
<style scoped>
|
|
865
|
+
/*
|
|
866
|
+
EVERY COLOUR IS A TOKEN NOW.
|
|
867
|
+
|
|
868
|
+
This file carried 38 distinct literal hexes - a navy `#082e43` bar, greens
|
|
869
|
+
`#2eaf55` / `#35b84b` / `#34b84a` / `#2da643`, greys `#233545` / `#677280` /
|
|
870
|
+
`#334454` / `#8b9297` / `#dce2e7` / `#e6eaee` and a dozen more. A literal is
|
|
871
|
+
the same colour on both themes, which is why this screen had no dark theme at
|
|
872
|
+
all. The LAYOUT (the grids, the breakpoints, the 42px method boxes) is the
|
|
873
|
+
screen's own and is kept; only the palette moved.
|
|
874
|
+
|
|
875
|
+
The one deliberate exception is `.pass-preview` - see the template comment.
|
|
876
|
+
*/
|
|
908
877
|
.hid-qr-config {
|
|
909
878
|
width: 100%;
|
|
910
879
|
min-width: 0;
|
|
911
880
|
padding: 8px 0 28px;
|
|
912
881
|
}
|
|
913
882
|
|
|
914
|
-
.config-
|
|
915
|
-
|
|
916
|
-
border-radius: 6px;
|
|
917
|
-
overflow: hidden;
|
|
918
|
-
background: #ffffff;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
.panel-header {
|
|
922
|
-
display: flex;
|
|
923
|
-
align-items: center;
|
|
924
|
-
justify-content: space-between;
|
|
925
|
-
min-height: 48px;
|
|
926
|
-
padding: 0 18px;
|
|
927
|
-
background: #082e43;
|
|
928
|
-
color: #ffffff;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
.panel-header h2 {
|
|
932
|
-
margin: 0;
|
|
933
|
-
font-size: 13px;
|
|
934
|
-
font-weight: 700;
|
|
883
|
+
.config-body {
|
|
884
|
+
padding: 4px 0 0;
|
|
935
885
|
}
|
|
936
886
|
|
|
937
|
-
.config-
|
|
938
|
-
|
|
887
|
+
.config-body__title {
|
|
888
|
+
margin-bottom: 14px;
|
|
939
889
|
}
|
|
940
890
|
|
|
941
891
|
.online-mode-row {
|
|
@@ -946,10 +896,13 @@ function showMessage(text: string, color: string) {
|
|
|
946
896
|
padding: 0 0 20px;
|
|
947
897
|
}
|
|
948
898
|
|
|
899
|
+
/* The prototype's settings body: a 13px/700 label over a 12-13px/600 muted
|
|
900
|
+
line. */
|
|
949
901
|
.online-mode-row h3,
|
|
950
902
|
.section-label {
|
|
951
903
|
margin: 0 0 6px;
|
|
952
|
-
color:
|
|
904
|
+
color: var(--text);
|
|
905
|
+
font-family: var(--font-sans);
|
|
953
906
|
font-size: 13px;
|
|
954
907
|
font-weight: 700;
|
|
955
908
|
}
|
|
@@ -957,8 +910,10 @@ function showMessage(text: string, color: string) {
|
|
|
957
910
|
.online-mode-row p,
|
|
958
911
|
.section-description {
|
|
959
912
|
margin: 0;
|
|
960
|
-
color:
|
|
913
|
+
color: var(--muted);
|
|
914
|
+
font-family: var(--font-sans);
|
|
961
915
|
font-size: 12px;
|
|
916
|
+
font-weight: 600;
|
|
962
917
|
line-height: 1.5;
|
|
963
918
|
}
|
|
964
919
|
|
|
@@ -984,10 +939,12 @@ function showMessage(text: string, color: string) {
|
|
|
984
939
|
min-width: 0;
|
|
985
940
|
min-height: 42px;
|
|
986
941
|
padding: 0 10px 0 14px;
|
|
987
|
-
border: 1px solid
|
|
988
|
-
border-radius:
|
|
989
|
-
color:
|
|
942
|
+
border: 1px solid var(--border);
|
|
943
|
+
border-radius: var(--r-inner);
|
|
944
|
+
color: var(--text2);
|
|
945
|
+
font-family: var(--font-sans);
|
|
990
946
|
font-size: 12px;
|
|
947
|
+
font-weight: 700;
|
|
991
948
|
}
|
|
992
949
|
|
|
993
950
|
.method-control span {
|
|
@@ -997,8 +954,8 @@ function showMessage(text: string, color: string) {
|
|
|
997
954
|
}
|
|
998
955
|
|
|
999
956
|
.method-control--disabled {
|
|
1000
|
-
background-color:
|
|
1001
|
-
color:
|
|
957
|
+
background-color: var(--hover);
|
|
958
|
+
color: var(--muted);
|
|
1002
959
|
}
|
|
1003
960
|
|
|
1004
961
|
.method-control :deep(.v-switch) {
|
|
@@ -1009,33 +966,15 @@ function showMessage(text: string, color: string) {
|
|
|
1009
966
|
min-height: 32px;
|
|
1010
967
|
}
|
|
1011
968
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
.hid-config-switch :deep(.v-selection-control--dirty .v-switch__track) {
|
|
1018
|
-
background-color: #2eaf55 !important;
|
|
1019
|
-
color: #2eaf55 !important;
|
|
1020
|
-
opacity: 1;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
.hid-config-switch :deep(.v-switch__thumb) {
|
|
1024
|
-
background-color: #fff !important;
|
|
1025
|
-
color: #fff !important;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
.hid-config-switch.v-input--disabled :deep(.v-switch__track) {
|
|
1029
|
-
background-color: #aeb4b8 !important;
|
|
1030
|
-
color: #aeb4b8 !important;
|
|
1031
|
-
opacity: 1;
|
|
1032
|
-
}
|
|
969
|
+
/* The three `.hid-config-switch` rules are gone: `.app-switch` in
|
|
970
|
+
primitives.css is the handoff's toggle, and it is shared with the other two
|
|
971
|
+
HID panels that have one. */
|
|
1033
972
|
|
|
1034
973
|
.printer-grid {
|
|
1035
974
|
display: grid;
|
|
1036
975
|
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr) minmax(140px, 1fr);
|
|
1037
976
|
gap: 14px;
|
|
1038
|
-
align-items:
|
|
977
|
+
align-items: end;
|
|
1039
978
|
}
|
|
1040
979
|
|
|
1041
980
|
.template-grid {
|
|
@@ -1044,16 +983,11 @@ function showMessage(text: string, color: string) {
|
|
|
1044
983
|
gap: 14px;
|
|
1045
984
|
}
|
|
1046
985
|
|
|
1047
|
-
.template-grid .preview-button,
|
|
1048
|
-
.template-grid .save-button {
|
|
1049
|
-
width: 100%;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
986
|
.validity-grid {
|
|
1053
987
|
display: grid;
|
|
1054
988
|
grid-template-columns: minmax(180px, 260px) minmax(140px, 260px);
|
|
1055
989
|
gap: 14px;
|
|
1056
|
-
align-items:
|
|
990
|
+
align-items: end;
|
|
1057
991
|
}
|
|
1058
992
|
|
|
1059
993
|
.permission-section {
|
|
@@ -1099,12 +1033,11 @@ function showMessage(text: string, color: string) {
|
|
|
1099
1033
|
justify-content: end;
|
|
1100
1034
|
}
|
|
1101
1035
|
|
|
1036
|
+
/* A ghost button carrying a count badge. `AppButton --ghost` supplies the
|
|
1037
|
+
height, radius, border and type; only the alignment is local. */
|
|
1102
1038
|
.permission-button {
|
|
1103
1039
|
justify-content: flex-start;
|
|
1104
1040
|
min-height: 42px;
|
|
1105
|
-
padding-inline: 12px;
|
|
1106
|
-
border-color: #dce2e7;
|
|
1107
|
-
color: #334454;
|
|
1108
1041
|
}
|
|
1109
1042
|
|
|
1110
1043
|
.permission-count {
|
|
@@ -1114,20 +1047,21 @@ function showMessage(text: string, color: string) {
|
|
|
1114
1047
|
height: 20px;
|
|
1115
1048
|
margin-right: 8px;
|
|
1116
1049
|
border-radius: 50%;
|
|
1117
|
-
background:
|
|
1118
|
-
color:
|
|
1050
|
+
background: var(--accent-soft);
|
|
1051
|
+
color: var(--accent-text);
|
|
1119
1052
|
font-size: 11px;
|
|
1120
1053
|
font-weight: 700;
|
|
1121
1054
|
}
|
|
1122
1055
|
|
|
1123
1056
|
.permission-button:not(:disabled):hover {
|
|
1124
|
-
border-color:
|
|
1125
|
-
color:
|
|
1057
|
+
border-color: var(--accent);
|
|
1058
|
+
color: var(--accent-text);
|
|
1126
1059
|
}
|
|
1127
1060
|
|
|
1128
1061
|
.permission-dialog {
|
|
1129
1062
|
overflow: hidden;
|
|
1130
|
-
border-radius:
|
|
1063
|
+
border-radius: var(--r-modal);
|
|
1064
|
+
background: var(--card);
|
|
1131
1065
|
}
|
|
1132
1066
|
|
|
1133
1067
|
.permission-dialog__header {
|
|
@@ -1136,19 +1070,21 @@ function showMessage(text: string, color: string) {
|
|
|
1136
1070
|
justify-content: space-between;
|
|
1137
1071
|
gap: 16px;
|
|
1138
1072
|
padding: 16px 18px 14px;
|
|
1139
|
-
border-bottom: 1px solid
|
|
1073
|
+
border-bottom: 1px solid var(--border);
|
|
1140
1074
|
}
|
|
1141
1075
|
|
|
1142
1076
|
.permission-dialog__title {
|
|
1143
|
-
color:
|
|
1077
|
+
color: var(--text);
|
|
1078
|
+
font-family: var(--font-sans);
|
|
1144
1079
|
font-size: 18px;
|
|
1145
|
-
font-weight:
|
|
1080
|
+
font-weight: 800;
|
|
1146
1081
|
}
|
|
1147
1082
|
|
|
1148
1083
|
.permission-dialog__subtitle {
|
|
1149
1084
|
margin-top: 4px;
|
|
1150
|
-
color:
|
|
1085
|
+
color: var(--muted);
|
|
1151
1086
|
font-size: 12px;
|
|
1087
|
+
font-weight: 600;
|
|
1152
1088
|
white-space: normal;
|
|
1153
1089
|
}
|
|
1154
1090
|
|
|
@@ -1158,21 +1094,18 @@ function showMessage(text: string, color: string) {
|
|
|
1158
1094
|
padding: 14px 18px 16px;
|
|
1159
1095
|
}
|
|
1160
1096
|
|
|
1161
|
-
.permission-search :deep(.v-field) {
|
|
1162
|
-
min-height: 44px;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1097
|
.permission-list-summary {
|
|
1166
1098
|
display: flex;
|
|
1167
1099
|
align-items: center;
|
|
1168
1100
|
justify-content: space-between;
|
|
1169
1101
|
min-height: 22px;
|
|
1170
|
-
color:
|
|
1102
|
+
color: var(--muted);
|
|
1171
1103
|
font-size: 11px;
|
|
1104
|
+
font-weight: 600;
|
|
1172
1105
|
}
|
|
1173
1106
|
|
|
1174
1107
|
.permission-list-summary__selected {
|
|
1175
|
-
color:
|
|
1108
|
+
color: var(--ok);
|
|
1176
1109
|
font-weight: 700;
|
|
1177
1110
|
}
|
|
1178
1111
|
|
|
@@ -1180,9 +1113,9 @@ function showMessage(text: string, color: string) {
|
|
|
1180
1113
|
min-height: 180px;
|
|
1181
1114
|
max-height: min(360px, 48vh);
|
|
1182
1115
|
overflow-y: auto;
|
|
1183
|
-
border: 1px solid
|
|
1184
|
-
border-radius:
|
|
1185
|
-
background:
|
|
1116
|
+
border: 1px solid var(--border);
|
|
1117
|
+
border-radius: var(--r-inner);
|
|
1118
|
+
background: var(--card);
|
|
1186
1119
|
}
|
|
1187
1120
|
|
|
1188
1121
|
.permission-list--loading {
|
|
@@ -1197,9 +1130,9 @@ function showMessage(text: string, color: string) {
|
|
|
1197
1130
|
align-items: center;
|
|
1198
1131
|
min-height: 56px;
|
|
1199
1132
|
padding: 7px 12px;
|
|
1200
|
-
border-bottom: 1px solid
|
|
1133
|
+
border-bottom: 1px solid var(--border);
|
|
1201
1134
|
cursor: pointer;
|
|
1202
|
-
transition: background-color
|
|
1135
|
+
transition: background-color var(--motion) ease;
|
|
1203
1136
|
}
|
|
1204
1137
|
|
|
1205
1138
|
.permission-list-item:last-child {
|
|
@@ -1207,15 +1140,12 @@ function showMessage(text: string, color: string) {
|
|
|
1207
1140
|
}
|
|
1208
1141
|
|
|
1209
1142
|
.permission-list-item:hover {
|
|
1210
|
-
background:
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
.permission-list-item--selected {
|
|
1214
|
-
background: #f1f8f3;
|
|
1143
|
+
background: var(--hover);
|
|
1215
1144
|
}
|
|
1216
1145
|
|
|
1146
|
+
.permission-list-item--selected,
|
|
1217
1147
|
.permission-list-item--selected:hover {
|
|
1218
|
-
background:
|
|
1148
|
+
background: var(--accent-soft);
|
|
1219
1149
|
}
|
|
1220
1150
|
|
|
1221
1151
|
.permission-list-item__content {
|
|
@@ -1232,23 +1162,25 @@ function showMessage(text: string, color: string) {
|
|
|
1232
1162
|
}
|
|
1233
1163
|
|
|
1234
1164
|
.permission-list-item__content strong {
|
|
1235
|
-
color:
|
|
1165
|
+
color: var(--text);
|
|
1236
1166
|
font-size: 13px;
|
|
1167
|
+
font-weight: 700;
|
|
1237
1168
|
line-height: 1.35;
|
|
1238
1169
|
}
|
|
1239
1170
|
|
|
1240
1171
|
.permission-list-item__content small,
|
|
1241
1172
|
.permission-list-item__group {
|
|
1242
|
-
color:
|
|
1173
|
+
color: var(--muted);
|
|
1243
1174
|
font-size: 11px;
|
|
1175
|
+
font-weight: 600;
|
|
1244
1176
|
}
|
|
1245
1177
|
|
|
1246
1178
|
.permission-list-item__group {
|
|
1247
1179
|
max-width: 170px;
|
|
1248
1180
|
overflow: hidden;
|
|
1249
1181
|
padding: 4px 9px;
|
|
1250
|
-
border-radius:
|
|
1251
|
-
background:
|
|
1182
|
+
border-radius: var(--r-tag);
|
|
1183
|
+
background: var(--hover);
|
|
1252
1184
|
text-overflow: ellipsis;
|
|
1253
1185
|
white-space: nowrap;
|
|
1254
1186
|
}
|
|
@@ -1269,37 +1201,31 @@ function showMessage(text: string, color: string) {
|
|
|
1269
1201
|
gap: 10px;
|
|
1270
1202
|
min-height: 180px;
|
|
1271
1203
|
padding: 24px;
|
|
1272
|
-
color:
|
|
1204
|
+
color: var(--muted);
|
|
1273
1205
|
font-size: 13px;
|
|
1274
1206
|
text-align: center;
|
|
1275
1207
|
}
|
|
1276
1208
|
|
|
1209
|
+
.permission-spinner {
|
|
1210
|
+
color: var(--accent);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1277
1213
|
.permission-dialog__actions {
|
|
1278
1214
|
justify-content: flex-end;
|
|
1279
1215
|
gap: 8px;
|
|
1280
1216
|
padding: 12px 18px;
|
|
1281
|
-
border-top: 1px solid
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
.save-button {
|
|
1285
|
-
background: #082e43;
|
|
1286
|
-
color: #ffffff;
|
|
1217
|
+
border-top: 1px solid var(--border);
|
|
1287
1218
|
}
|
|
1288
1219
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
color: #ffffff;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
.test-button {
|
|
1295
|
-
border-color: #34b84a;
|
|
1296
|
-
color: #2da643;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
.utility-button {
|
|
1300
|
-
color: #1f3448;
|
|
1301
|
-
}
|
|
1220
|
+
/*
|
|
1221
|
+
THE PRINTED PASS - the one thing here that is NOT themed.
|
|
1302
1222
|
|
|
1223
|
+
It previews a physical card that comes out of a badge printer, so it stays
|
|
1224
|
+
white with dark ink on both themes; theming it would show the operator a
|
|
1225
|
+
preview of something the printer cannot produce. Everything around it sits on
|
|
1226
|
+
the dialog scrim, which is dark on both themes - which is why the title above
|
|
1227
|
+
it is white.
|
|
1228
|
+
*/
|
|
1303
1229
|
.preview-shell {
|
|
1304
1230
|
position: relative;
|
|
1305
1231
|
overflow: visible;
|
|
@@ -1312,13 +1238,15 @@ function showMessage(text: string, color: string) {
|
|
|
1312
1238
|
top: 38px;
|
|
1313
1239
|
right: 18px;
|
|
1314
1240
|
z-index: 2;
|
|
1241
|
+
color: #ffffff;
|
|
1315
1242
|
}
|
|
1316
1243
|
|
|
1317
1244
|
.preview-title {
|
|
1318
1245
|
margin: 0 0 12px;
|
|
1319
1246
|
color: #ffffff;
|
|
1247
|
+
font-family: var(--font-sans);
|
|
1320
1248
|
font-size: 18px;
|
|
1321
|
-
font-weight:
|
|
1249
|
+
font-weight: 800;
|
|
1322
1250
|
}
|
|
1323
1251
|
|
|
1324
1252
|
.pass-preview {
|
|
@@ -1408,10 +1336,6 @@ function showMessage(text: string, color: string) {
|
|
|
1408
1336
|
}
|
|
1409
1337
|
|
|
1410
1338
|
@media (max-width: 700px) {
|
|
1411
|
-
.config-body {
|
|
1412
|
-
padding: 22px 16px;
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
1339
|
.online-mode-row,
|
|
1416
1340
|
.printer-grid,
|
|
1417
1341
|
.template-grid,
|