@7365admin1/layer-common 3.2.6 → 3.2.8-staging.200
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/CHANGELOG.md +6 -0
- package/assets/css/primitives.css +1113 -0
- package/assets/css/screens.css +1097 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +236 -0
- package/components/AcceptDialog.vue +17 -18
- package/components/AccessCard/AvailableStats.vue +24 -16
- package/components/AccessCardAddForm.vue +42 -42
- package/components/AccessCardAssignToUnitForm.vue +13 -21
- package/components/AccessCardDeleteDialog.vue +41 -39
- package/components/AccessCardDetailsDialog.vue +125 -79
- package/components/AccessCardPreviewDialog.vue +58 -84
- package/components/AccessCardQrTagging.vue +60 -63
- package/components/AccessCardReplaceForm.vue +27 -23
- package/components/AccessManagement.vue +86 -85
- package/components/AddEqupmentForm.vue +18 -22
- package/components/AddPassKeyToVisitor.vue +95 -73
- package/components/AppButton.vue +62 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +49 -0
- package/components/AppField.vue +76 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +247 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +14 -7
- package/components/AreaFormDialog.vue +71 -59
- package/components/AreaMain.vue +73 -107
- package/components/AttachmentsViewer.vue +84 -22
- package/components/AttendanceCheckInOutDialog.vue +85 -88
- package/components/AttendanceDetailsDialog.vue +57 -63
- package/components/AttendanceMain.vue +4 -3
- package/components/AttendanceMapSearchDialog.vue +42 -44
- package/components/AttendanceSettingsDialog.vue +79 -63
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BtnUploadFile.vue +53 -11
- package/components/BuildingForm.vue +34 -27
- package/components/BuildingManagement/buildings.vue +39 -39
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BuildingUnitFormAdd.vue +20 -23
- package/components/BuildingUnitFormEdit.vue +8 -4
- package/components/BulletinBoardForm.vue +19 -24
- package/components/BulletinBoardManagement.vue +49 -57
- package/components/BulletinBoardView.vue +9 -6
- package/components/BulletinExpirationChip.vue +13 -4
- package/components/Button/Close.vue +2 -2
- package/components/CameraForm.vue +21 -22
- package/components/CameraMain.vue +50 -49
- package/components/Card/DeleteConfirmation.vue +24 -22
- package/components/Card/MemberInfoSummary.vue +28 -3
- package/components/Card/Toggle.vue +15 -3
- package/components/CardToolbar.vue +37 -0
- package/components/Carousel.vue +56 -36
- package/components/Chat/Bubbles.vue +43 -14
- package/components/Chat/Information.vue +49 -32
- package/components/Chat/ListCard.vue +38 -9
- package/components/Chat/Message.vue +41 -15
- package/components/Chat/Navigation.vue +27 -15
- package/components/Chat/SkeletonLoader.vue +9 -2
- package/components/ChecklistItemRow.vue +25 -1
- package/components/CleaningScheduleMain.vue +44 -49
- package/components/ClientDetailForm.vue +81 -56
- package/components/ClientMain.vue +92 -82
- package/components/ConfirmDialog.vue +14 -8
- package/components/Container/Standard.vue +9 -7
- package/components/CreateSubsciptionPlan.vue +148 -89
- package/components/DashboardEmptyState.vue +48 -3
- package/components/DashboardMain.vue +924 -676
- package/components/DashboardPanel.vue +84 -21
- package/components/DeliveryCompany.vue +34 -20
- package/components/Dialog/DeleteConfirmation.vue +20 -21
- package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
- package/components/Dialog/ReusablePrompt.vue +30 -29
- package/components/Dialog/UpdateMoreAction.vue +62 -59
- package/components/DocumentForm.vue +14 -21
- package/components/DocumentManagement.vue +162 -108
- package/components/DocumentViewer.vue +29 -12
- package/components/DrawImage.vue +29 -21
- package/components/EntryPass/QrTemplatePreview.vue +27 -16
- package/components/EntryPassInformation.vue +40 -10
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +84 -97
- package/components/EquipmentManagementMain.vue +81 -103
- package/components/Facility/BookingSetup.vue +43 -52
- package/components/Feedback/Form.vue +26 -12
- package/components/FeedbackMain.vue +35 -49
- package/components/FileInput.vue +75 -58
- package/components/FileInputWithList.vue +59 -12
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +512 -490
- package/components/HidCameraCaptureDialog.vue +216 -0
- package/components/HidEnabledGate.vue +67 -9
- package/components/HidIntercomManagement.vue +1322 -653
- package/components/HidQrCodeConfiguration.vue +1030 -258
- package/components/HidReaderForm.vue +142 -59
- package/components/HidReaderManagement.vue +211 -323
- package/components/HidServiceSettingsPanel.vue +44 -13
- package/components/HidUserEnrollment.vue +1185 -412
- package/components/HygieneUpdateMoreAction.vue +44 -44
- package/components/IncidentReport/Authorities.vue +30 -18
- package/components/IncidentReport/IncidentInformation.vue +10 -10
- package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
- package/components/IncidentReport/affectedEntities.vue +41 -20
- package/components/Input/Date.vue +3 -0
- package/components/Input/DatePicker.vue +1 -2
- package/components/Input/DateTimePicker.vue +1 -1
- package/components/Input/File.vue +55 -10
- package/components/Input/FileV2.vue +39 -6
- package/components/Input/InputPhoneNumberV2.vue +2 -1
- package/components/Input/ListGroupSelection.vue +56 -8
- package/components/Input/NRICNumber.vue +1 -0
- package/components/Input/NewDate.vue +1 -0
- package/components/Input/Number.vue +1 -0
- package/components/Input/Password.vue +1 -0
- package/components/Input/PhoneNumber.vue +21 -1
- package/components/Input/VehicleNumber.vue +1 -0
- package/components/InputLabel.vue +2 -2
- package/components/InvitationClientForm.vue +21 -19
- package/components/InvitationForm.vue +15 -21
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +263 -36
- package/components/Layout/NavigationDrawer.vue +300 -30
- package/components/ListItem.vue +33 -3
- package/components/LocalPagination.vue +27 -3
- package/components/ManageChecklistMain.vue +136 -86
- package/components/MemberInformation.vue +32 -16
- package/components/MemberMain.vue +113 -93
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +83 -3
- package/components/Nfc/NFCPatrolReportMain.vue +47 -33
- package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/Nfc/NFCPatrolSettings.vue +14 -4
- package/components/Nfc/NFCTagForm.vue +22 -8
- package/components/Nfc/NFCTagMain.vue +41 -43
- package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
- package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
- package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
- package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
- package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
- package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
- package/components/NotificationSettings.vue +373 -0
- package/components/NumberSettingField.vue +9 -2
- package/components/OnlineFormConfigurationForm.vue +725 -237
- package/components/OnlineFormFill.vue +213 -106
- package/components/OnlineFormRenderPreview.vue +25 -30
- package/components/OnlineFormsConfiguration.vue +113 -109
- package/components/OrgViewDialog.vue +34 -16
- package/components/OvernightParkingAvailability.vue +49 -26
- package/components/OvernightParkingManagement.vue +78 -18
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +109 -53
- package/components/PasswordConfirmation.vue +44 -26
- package/components/PeopleForm.vue +41 -20
- package/components/PhotoUpload.vue +110 -48
- package/components/PlaceholderComponent.vue +35 -19
- package/components/PlanActionsMenu.vue +12 -2
- package/components/PlanStatusBadge.vue +7 -8
- package/components/PlateNumberDisplay.vue +9 -6
- package/components/QrTemplate/PrintDialog.vue +66 -43
- package/components/RolePermissionFormCreate.vue +104 -79
- package/components/RolePermissionFormPreviewUpdate.vue +111 -109
- package/components/RolePermissionMain.vue +183 -81
- package/components/ScanVisitorQRCode.vue +99 -35
- package/components/ScheduleAreaMain.vue +34 -59
- package/components/ScheduleTaskForm.vue +61 -42
- package/components/ScheduleTaskMain.vue +56 -69
- package/components/SearchVehicleNumberUser.vue +39 -37
- package/components/ServiceProviderFormCreate.vue +29 -20
- package/components/ServiceProviderInvitationPrompt.vue +155 -0
- package/components/ServiceProviderMain.vue +601 -432
- package/components/Signature.vue +40 -34
- package/components/SignaturePad.vue +5 -29
- package/components/SiteSettings.vue +41 -11
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SlideCardGroup.vue +35 -11
- package/components/Snackbar.vue +36 -3
- package/components/SpecificAttr.vue +18 -8
- package/components/StatusChip.vue +156 -0
- package/components/StockCard.vue +17 -12
- package/components/SubscriptionPlanTable.vue +207 -140
- package/components/SwitchContext.vue +201 -9
- package/components/TableHygiene.vue +36 -46
- package/components/TableList.vue +10 -10
- package/components/TableListSecondary.vue +105 -54
- package/components/TableMain.vue +241 -79
- package/components/TableV3.vue +61 -70
- package/components/TiptapEditor.vue +33 -40
- package/components/TiptapEditorQr.vue +5 -2
- package/components/Tooltip/Info.vue +69 -24
- package/components/UnitMain.vue +40 -60
- package/components/UnitPersonCard.vue +42 -13
- package/components/VehicleAddSelection.vue +48 -32
- package/components/VehicleForm.vue +112 -129
- package/components/VehicleManagement.vue +26 -12
- package/components/VehicleUpdateMoreAction.vue +59 -46
- package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
- package/components/VisitorForm.vue +774 -210
- package/components/VisitorFormSelection.vue +40 -37
- package/components/VisitorManagement.vue +239 -82
- package/components/VisitorPassKeyQRScanner.vue +65 -18
- package/components/VisitorSearchResultSummary.vue +60 -16
- package/components/VisitorSocketPopUp.vue +22 -14
- package/components/VisitorsReportPreview.vue +44 -29
- package/components/WorkOrder/Create.vue +26 -17
- package/components/WorkOrder/ListView.vue +38 -7
- package/components/WorkOrder/Main.vue +62 -59
- package/composables/useFeedback.ts +3 -3
- package/composables/useHidAmico.ts +269 -0
- package/composables/useHidNavigation.ts +2 -9
- package/composables/useMember.ts +3 -3
- package/composables/useNFCPatrolDailyReport.ts +13 -0
- package/composables/useNFCPatrolMonthlyReport.ts +22 -56
- package/composables/useNFCPatrolReport.ts +13 -0
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProvider.ts +3 -3
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSubscriptionPlan.ts +62 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useWebUsb.ts +127 -37
- package/composables/useWorkOrder.ts +17 -3
- package/error.vue +30 -6
- package/nuxt.config.ts +36 -0
- package/package.json +25 -4
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/pages/payment-method-linked.vue +34 -10
- package/pages/require-customer.vue +48 -10
- package/pages/require-organization-membership.vue +32 -8
- package/pages/unauthorized.vue +29 -4
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +69 -2
- package/types/nfc-patrol-report.ts +3 -2
- package/types/site.d.ts +71 -0
- package/types/subscription.ts +18 -11
- package/utils/breadcrumb.test.ts +108 -0
- package/utils/breadcrumb.ts +82 -0
- package/utils/dashboard.test.ts +118 -0
- package/utils/dashboard.ts +53 -0
- package/utils/data.test.ts +95 -0
- package/utils/data.ts +40 -2
- package/utils/hid-discover.test.ts +102 -0
- package/utils/list-page.test.ts +60 -0
- package/utils/list-page.ts +38 -0
- package/utils/materialSymbols.ts +363 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/nfc-patrol-report.test.ts +130 -0
- package/utils/nfc-patrol-report.ts +101 -0
- package/utils/role.test.ts +69 -0
- package/utils/role.ts +10 -0
- package/utils/route-name.test.ts +85 -0
- package/utils/route-name.ts +24 -0
- package/utils/status.test.ts +110 -0
- package/utils/status.ts +135 -0
- package/utils/table-card.test.ts +84 -0
- package/utils/theme-aa-ledger.ts +148 -0
- package/utils/theme.test.ts +485 -7
- package/utils/theme.ts +295 -110
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.editorconfig +0 -12
- package/.github/workflows/main.yml +0 -17
- package/.github/workflows/publish-staging.yml +0 -47
- package/.github/workflows/publish.yml +0 -39
- package/PUBLISHING.md +0 -269
- package/components/HidIdentityMapping.vue +0 -975
- package/components/ScheduleTaskAreaFormDialog.vue +0 -144
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
- package/components/ScheduleTastTicketMain.vue +0 -182
- package/components/TableWithButton.vue +0 -94
- package/test/visitor-socket.test.mjs +0 -36
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLATFORM REDESIGN - THE PAGE PRIMITIVES.
|
|
3
|
+
*
|
|
4
|
+
* Phases 1-5 tokenised what Vuetify draws. This file is the other half: the
|
|
5
|
+
* pieces the design draws that Vuetify has no component for, taken as literal
|
|
6
|
+
* CSS out of the handoff prototype
|
|
7
|
+
* (`design_handoff_platform_redesign/iService365 Redesign.dc.html`) rather
|
|
8
|
+
* than re-derived by eye. Every declaration below has a line in that file.
|
|
9
|
+
*
|
|
10
|
+
* The rule for a screen adopting these: MARKUP, not a theme override. A
|
|
11
|
+
* `.v-btn` restyled into a pill is still a `.v-btn` with Vuetify's ripple,
|
|
12
|
+
* overlay, min-width and uppercase tracking fighting it at a higher
|
|
13
|
+
* specificity; a `<button class="app-btn">` simply is the button in the
|
|
14
|
+
* design. Where Vuetify owns the behaviour (the data table, the menu, the
|
|
15
|
+
* dialog) it stays - only the chrome around it is replaced.
|
|
16
|
+
*
|
|
17
|
+
* Numbers, for review against the handoff:
|
|
18
|
+
* page title 22px/800/-0.01em · card radius 14px, 1px border, --shadow
|
|
19
|
+
* in-card toolbar 0 20px, tabs 14px 2px 12px, 2px accent underline
|
|
20
|
+
* fields 40px (34px compact), 10px radius, 13.5px/500
|
|
21
|
+
* primary button 40px, 10px radius, 13.5px/800 · ghost 38px, 13px/700
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* ------------------------------------------------------------------ */
|
|
25
|
+
/* The typeface. */
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Phase 1 loaded Manrope and deliberately did not apply it, so that the
|
|
30
|
+
* product would not sit half-migrated between two typefaces. The page
|
|
31
|
+
* scaffold is the migration, so the font goes on now - on Vuetify's own
|
|
32
|
+
* application root, which is every rendered page in all eleven apps.
|
|
33
|
+
*
|
|
34
|
+
* `.v-application` and not `body`: Vuetify sets `font-family` on that class
|
|
35
|
+
* itself, so a rule on `body` loses to it and the product would keep Roboto
|
|
36
|
+
* everywhere while the new primitives alone changed - the exact
|
|
37
|
+
* half-migrated look this was avoiding.
|
|
38
|
+
*/
|
|
39
|
+
.v-application,
|
|
40
|
+
.v-overlay-container {
|
|
41
|
+
font-family: var(--font-sans);
|
|
42
|
+
}
|
|
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
|
+
):not(.material-symbols-outlined),
|
|
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
|
+
):not(.material-symbols-outlined) {
|
|
92
|
+
font-family: var(--font-sans, Roboto, sans-serif);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
96
|
+
/* The icon face. */
|
|
97
|
+
/* ------------------------------------------------------------------ */
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The handoff draws every icon with Google's `Material Symbols Outlined`
|
|
101
|
+
* variable font, requested in `nuxt.config.ts` alongside Manrope. The icon
|
|
102
|
+
* set in `plugins/vuetify.ts` turns each `mdi-*` name into that font's
|
|
103
|
+
* ligature and puts this class on the element; this rule is what makes the
|
|
104
|
+
* ligature resolve.
|
|
105
|
+
*
|
|
106
|
+
* One class, no `!important`. `.v-icon` already gives us `font-feature-
|
|
107
|
+
* settings: "liga"`, `letter-spacing: normal`, `line-height: 1` and the 1em
|
|
108
|
+
* box, and Vuetify never sets `font-family` on it - the old family arrived
|
|
109
|
+
* through `.mdi:before`, which we no longer emit. Only `text-transform` has
|
|
110
|
+
* to be stated: `.v-btn` uppercases its contents, and an uppercased "close"
|
|
111
|
+
* matches no ligature, so the icon would render as the word.
|
|
112
|
+
*/
|
|
113
|
+
.material-symbols-outlined {
|
|
114
|
+
font-family: "Material Symbols Outlined";
|
|
115
|
+
font-style: normal;
|
|
116
|
+
font-variation-settings:
|
|
117
|
+
"FILL" 0,
|
|
118
|
+
"GRAD" 0,
|
|
119
|
+
"opsz" 24;
|
|
120
|
+
text-transform: none;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
word-wrap: normal;
|
|
123
|
+
direction: ltr;
|
|
124
|
+
-webkit-font-smoothing: antialiased;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The icon face, re-asserted at (0,3,0).
|
|
129
|
+
*
|
|
130
|
+
* A `<v-icon class="text-h6">` carries BOTH the icon class and a typography
|
|
131
|
+
* utility, so the re-point above (a descendant of `.v-application`, (0,2,0))
|
|
132
|
+
* would otherwise beat the single-class rule (0,1,0) and the ligature would
|
|
133
|
+
* fall back to Manrope - drawing the literal word "local_shipping" instead of
|
|
134
|
+
* a truck. The `:not()` guard above stops that here; this block stops it
|
|
135
|
+
* anywhere an app ships its own copy of the re-point, whatever the stylesheet
|
|
136
|
+
* order. Repeating the class is how the weight is raised - no `!important`,
|
|
137
|
+
* same precedent as the re-point itself, so a screen can still override.
|
|
138
|
+
*/
|
|
139
|
+
.v-application .material-symbols-outlined.material-symbols-outlined,
|
|
140
|
+
.v-overlay-container .material-symbols-outlined.material-symbols-outlined {
|
|
141
|
+
font-family: "Material Symbols Outlined";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* mdi's spin modifier, kept working now the glyph is not a `:before`.
|
|
145
|
+
`@keyframes mdi-spin` still comes from `@mdi/font`, which stays loaded so
|
|
146
|
+
that any icon name without a mapping keeps drawing its old glyph. */
|
|
147
|
+
.material-symbols-outlined.mdi-spin {
|
|
148
|
+
animation: mdi-spin 2s infinite linear;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ------------------------------------------------------------------ */
|
|
152
|
+
/* PageHeader - the row every screen opens with. */
|
|
153
|
+
/* ------------------------------------------------------------------ */
|
|
154
|
+
|
|
155
|
+
.app-page-head {
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: space-between;
|
|
159
|
+
gap: 16px;
|
|
160
|
+
margin-bottom: 18px;
|
|
161
|
+
flex-wrap: wrap;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.app-page-head__title {
|
|
165
|
+
margin: 0;
|
|
166
|
+
font-size: var(--fs-h1);
|
|
167
|
+
font-weight: var(--fw-h1);
|
|
168
|
+
letter-spacing: var(--ls-h1);
|
|
169
|
+
line-height: 1.25;
|
|
170
|
+
color: var(--text);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.app-page-head__sub {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 5px;
|
|
177
|
+
margin-top: 4px;
|
|
178
|
+
font-size: 12.5px;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
color: var(--muted);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.app-page-head__actions {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 10px;
|
|
187
|
+
flex-wrap: wrap;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ------------------------------------------------------------------ */
|
|
191
|
+
/* AppCard - the surface everything else sits on. */
|
|
192
|
+
/* ------------------------------------------------------------------ */
|
|
193
|
+
|
|
194
|
+
.app-card {
|
|
195
|
+
background: var(--card);
|
|
196
|
+
border: 1px solid var(--border);
|
|
197
|
+
border-radius: var(--r-card);
|
|
198
|
+
box-shadow: var(--shadow);
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.app-card--flush {
|
|
203
|
+
overflow: visible;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ------------------------------------------------------------------ */
|
|
207
|
+
/* CardToolbar - the in-card top row. */
|
|
208
|
+
/* ------------------------------------------------------------------ */
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Two shapes in the handoff, one component. With tabs the row is `0 20px` and
|
|
212
|
+
* the tabs supply their own vertical padding, so the underline meets the
|
|
213
|
+
* card's rule; without tabs (a refresh button on the left) it is `10px 14px`.
|
|
214
|
+
*/
|
|
215
|
+
.app-toolbar {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
/* One row at desktop widths, as drawn. On a phone the tabs and the
|
|
219
|
+
search/pager group drop onto their own lines rather than overflow. */
|
|
220
|
+
flex-wrap: wrap;
|
|
221
|
+
gap: 22px;
|
|
222
|
+
padding: 0 var(--cellpad-x);
|
|
223
|
+
border-bottom: 1px solid var(--border);
|
|
224
|
+
min-height: 48px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.app-toolbar--plain {
|
|
228
|
+
gap: 8px;
|
|
229
|
+
padding: 10px 14px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.app-toolbar__spacer {
|
|
233
|
+
flex: 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.app-toolbar__right {
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 8px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* The list toolbar's own search box. `--search` alone stretches (it is written
|
|
243
|
+
for the filter row), which would push the pager off the end of the row. */
|
|
244
|
+
.app-toolbar__search.app-field--search {
|
|
245
|
+
flex: 0 0 auto;
|
|
246
|
+
width: 200px;
|
|
247
|
+
min-width: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.app-toolbar__count {
|
|
251
|
+
font-size: 12.5px;
|
|
252
|
+
font-weight: 600;
|
|
253
|
+
color: var(--muted);
|
|
254
|
+
font-variant-numeric: tabular-nums;
|
|
255
|
+
white-space: nowrap;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.app-tabs {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
gap: 22px;
|
|
262
|
+
min-width: 0;
|
|
263
|
+
overflow-x: auto;
|
|
264
|
+
scrollbar-width: none;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.app-tabs::-webkit-scrollbar {
|
|
268
|
+
display: none;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.app-tab {
|
|
272
|
+
padding: 14px 2px 12px 2px;
|
|
273
|
+
border: none;
|
|
274
|
+
background: transparent;
|
|
275
|
+
border-bottom: 2px solid transparent;
|
|
276
|
+
font-family: inherit;
|
|
277
|
+
font-size: var(--fs-nav);
|
|
278
|
+
font-weight: 600;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
white-space: nowrap;
|
|
281
|
+
cursor: pointer;
|
|
282
|
+
/* A tab is a <button> on the screens that switch a list in place and a link
|
|
283
|
+
on the ones that route (Invitations), so the underline has to be ours. */
|
|
284
|
+
text-decoration: none;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.app-tab:hover {
|
|
288
|
+
color: var(--text2);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.app-tab--active {
|
|
292
|
+
font-weight: 800;
|
|
293
|
+
color: var(--accent-text);
|
|
294
|
+
border-bottom-color: var(--accent);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ------------------------------------------------------------------ */
|
|
298
|
+
/* The table, drawn as a grid. */
|
|
299
|
+
/* ------------------------------------------------------------------ */
|
|
300
|
+
|
|
301
|
+
.app-thead {
|
|
302
|
+
display: grid;
|
|
303
|
+
gap: 12px;
|
|
304
|
+
padding: 12px var(--cellpad-x);
|
|
305
|
+
font-size: var(--fs-table-head);
|
|
306
|
+
font-weight: var(--fw-table-head);
|
|
307
|
+
letter-spacing: var(--ls-table-head);
|
|
308
|
+
text-transform: uppercase;
|
|
309
|
+
color: var(--muted);
|
|
310
|
+
background: var(--thead);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.app-trow {
|
|
314
|
+
display: grid;
|
|
315
|
+
gap: 12px;
|
|
316
|
+
align-items: center;
|
|
317
|
+
padding: var(--cellpad) var(--cellpad-x);
|
|
318
|
+
border-top: 1px solid var(--border);
|
|
319
|
+
font-size: 13px;
|
|
320
|
+
font-weight: 600;
|
|
321
|
+
color: var(--text);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.app-trow:hover {
|
|
325
|
+
background: var(--hover);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.app-cell--strong {
|
|
329
|
+
font-weight: 700;
|
|
330
|
+
overflow: hidden;
|
|
331
|
+
text-overflow: ellipsis;
|
|
332
|
+
white-space: nowrap;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.app-cell--muted {
|
|
336
|
+
color: var(--text2);
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
text-overflow: ellipsis;
|
|
339
|
+
white-space: nowrap;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.app-cell--num {
|
|
343
|
+
color: var(--text2);
|
|
344
|
+
font-variant-numeric: tabular-nums;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.app-empty {
|
|
348
|
+
display: flex;
|
|
349
|
+
flex-direction: column;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 8px;
|
|
352
|
+
padding: 52px var(--cellpad-x);
|
|
353
|
+
border-top: 1px solid var(--border);
|
|
354
|
+
font-size: 13px;
|
|
355
|
+
font-weight: 600;
|
|
356
|
+
color: var(--muted);
|
|
357
|
+
text-align: center;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ------------------------------------------------------------------ */
|
|
361
|
+
/* AppButton. */
|
|
362
|
+
/* ------------------------------------------------------------------ */
|
|
363
|
+
|
|
364
|
+
.app-btn {
|
|
365
|
+
display: inline-flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
justify-content: center;
|
|
368
|
+
gap: 6px;
|
|
369
|
+
height: var(--btn-h);
|
|
370
|
+
padding: 0 16px;
|
|
371
|
+
border: none;
|
|
372
|
+
border-radius: var(--r-inner);
|
|
373
|
+
/* `--accent` at 3.23:1 cannot carry a white label - see Phase 1. The fill,
|
|
374
|
+
and only the fill, uses the 18%-darker `--accent-strong`; the underline,
|
|
375
|
+
the links and every soft background stay on `--accent`. */
|
|
376
|
+
background: var(--accent-strong);
|
|
377
|
+
color: var(--on-accent-strong);
|
|
378
|
+
font-family: inherit;
|
|
379
|
+
font-size: var(--fs-btn);
|
|
380
|
+
font-weight: var(--fw-btn-strong);
|
|
381
|
+
line-height: 1;
|
|
382
|
+
white-space: nowrap;
|
|
383
|
+
cursor: pointer;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.app-btn:hover {
|
|
387
|
+
filter: brightness(1.06);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/*
|
|
391
|
+
* THE `type="button"` TIE, AND WHY THIS BLOCK EXISTS.
|
|
392
|
+
*
|
|
393
|
+
* Vuetify ships `[type="button"] { color: inherit }`. That is an ATTRIBUTE
|
|
394
|
+
* selector at (0,1,0) - an exact tie with every `.app-btn` rule above, which is
|
|
395
|
+
* also (0,1,0). A tie is broken by INJECTION ORDER, so in a document where
|
|
396
|
+
* Vuetify's stylesheet lands after this one, the background from the rules above
|
|
397
|
+
* still applies and the `color` does not: the button keeps its accent fill and
|
|
398
|
+
* takes the page ink instead of its own.
|
|
399
|
+
*
|
|
400
|
+
* `AppButton` renders `<button :type="type">` with `type` defaulting to
|
|
401
|
+
* "button", so EVERY variant carries the attribute the tie needs.
|
|
402
|
+
*
|
|
403
|
+
* MEASURED, 2026-08-16. Injecting `[type="button"] { color: inherit }` last, on
|
|
404
|
+
* a real primary in the harness:
|
|
405
|
+
*
|
|
406
|
+
* before fill rgb(75,116,196) ink rgb(255,255,255) 4.57:1
|
|
407
|
+
* after fill rgb(75,116,196) ink rgb(25,27,31) 3.77:1
|
|
408
|
+
*
|
|
409
|
+
* `rgb(25,27,31)` is `--text` on the light theme - the button had stopped
|
|
410
|
+
* choosing its own ink at all.
|
|
411
|
+
*
|
|
412
|
+
* Naming the ELEMENT makes each of these (0,1,1), which beats an attribute
|
|
413
|
+
* selector WHATEVER the order. No `!important`: a screen that wants a different
|
|
414
|
+
* label colour must still be able to say so.
|
|
415
|
+
*
|
|
416
|
+
* This is an ADDITIONAL block rather than an edit to the rules above, and
|
|
417
|
+
* deliberately so: `AppButton` renders a `NuxtLink` when `to` is given, which is
|
|
418
|
+
* an `<a>` with no `type` attribute. Putting `button` on the original selectors
|
|
419
|
+
* would have taken the colour away from that path entirely. The values below are
|
|
420
|
+
* copied verbatim from the rules above; if one changes, change both.
|
|
421
|
+
*/
|
|
422
|
+
button.app-btn {
|
|
423
|
+
color: var(--on-accent-strong);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
button.app-btn--ghost {
|
|
427
|
+
color: var(--text2);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
button.app-btn--icon {
|
|
431
|
+
color: var(--text2);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
button.app-btn--row {
|
|
435
|
+
color: var(--muted);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
button.app-btn--ok {
|
|
439
|
+
color: var(--on-ok);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
button.app-btn--err {
|
|
443
|
+
color: var(--err);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.app-btn:disabled {
|
|
447
|
+
opacity: 0.5;
|
|
448
|
+
cursor: default;
|
|
449
|
+
filter: none;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* The design draws the ghost at the SAME size as the primary it stands next to
|
|
453
|
+
- Bulk Upload beside Add Unit, Scan QR Code beside Add Visitor are both
|
|
454
|
+
`height:40px; padding:0 16px; font-size:13.5px`, and only the fill, the
|
|
455
|
+
border and the weight differ. 38px / 0 14px / 13px was a smaller button than
|
|
456
|
+
the handoff asks for, and the 2px mismatch shows on every screen that draws
|
|
457
|
+
the pair. Height, padding and type therefore come from `.app-btn`. */
|
|
458
|
+
.app-btn--ghost {
|
|
459
|
+
border: 1px solid var(--border);
|
|
460
|
+
background: var(--card);
|
|
461
|
+
color: var(--text2);
|
|
462
|
+
font-weight: var(--fw-btn);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.app-btn--ghost:hover {
|
|
466
|
+
background: var(--hover);
|
|
467
|
+
filter: none;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.app-btn--icon {
|
|
471
|
+
width: 32px;
|
|
472
|
+
height: 32px;
|
|
473
|
+
padding: 0;
|
|
474
|
+
border: none;
|
|
475
|
+
border-radius: 8px;
|
|
476
|
+
background: transparent;
|
|
477
|
+
color: var(--text2);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.app-btn--icon:hover {
|
|
481
|
+
background: var(--hover);
|
|
482
|
+
filter: none;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* The 30px bordered row action - the `more_vert` at the end of a member row. */
|
|
486
|
+
.app-btn--row {
|
|
487
|
+
width: 30px;
|
|
488
|
+
height: 30px;
|
|
489
|
+
padding: 0;
|
|
490
|
+
border: 1px solid var(--border);
|
|
491
|
+
border-radius: 8px;
|
|
492
|
+
background: transparent;
|
|
493
|
+
color: var(--muted);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.app-btn--row:hover {
|
|
497
|
+
background: var(--hover);
|
|
498
|
+
filter: none;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* ------------------------------------------------------------------ */
|
|
502
|
+
/* The check-in / check-out pair. */
|
|
503
|
+
/* ------------------------------------------------------------------ */
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* NEW DESIGN, approved by the owner on 2026-08-14. The handoff draws no
|
|
507
|
+
* check-in and no check-out button, so there is nothing to match here; the
|
|
508
|
+
* gatehouse screens had been carrying Vuetify's `color="success"` and a
|
|
509
|
+
* hardcoded `color="red"` instead.
|
|
510
|
+
*
|
|
511
|
+
* Both are `.app-btn` first - the same 40px box, the same 10px radius, the
|
|
512
|
+
* same 13.5px/800 - so only the fill, the ink and the edge differ and the pair
|
|
513
|
+
* sits level with every other button in the product. The 16px icon at an 8px
|
|
514
|
+
* gap is the only metric either adds.
|
|
515
|
+
*
|
|
516
|
+
* CHECK IN is SOLID. It is the affirmative action a guard hits hundreds of
|
|
517
|
+
* times a shift and should be the heaviest thing on its row.
|
|
518
|
+
*
|
|
519
|
+
* CHECK OUT is TONAL, and that is measured rather than preferred: white on
|
|
520
|
+
* `--err` reads 2.99:1 in the dark theme, which is why batch C built a solid
|
|
521
|
+
* danger button, measured it and reverted it. `--err-bg` fill + `--err` ink +
|
|
522
|
+
* a 24%/28% `--err` edge is the construction the status chips already use, it
|
|
523
|
+
* passes in both themes, and it keeps a routine action from reading as a
|
|
524
|
+
* destructive one.
|
|
525
|
+
*/
|
|
526
|
+
.app-btn--ok,
|
|
527
|
+
.app-btn--err {
|
|
528
|
+
gap: 8px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* The label is `--on-ok`, not a literal white - see tokens.css. White on the
|
|
532
|
+
DARK theme's `--ok` measures 2.14:1; `--on-ok` is the on-fill pair the theme
|
|
533
|
+
already carries, so the green stays exactly the green the design names. */
|
|
534
|
+
.app-btn--ok {
|
|
535
|
+
border: 1px solid var(--ok);
|
|
536
|
+
background: var(--ok);
|
|
537
|
+
color: var(--on-ok);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.app-btn--err {
|
|
541
|
+
border: 1px solid var(--err-border);
|
|
542
|
+
background: var(--err-bg);
|
|
543
|
+
color: var(--err);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* A tint cannot be hovered with `brightness()` - above 1 it fades in the light
|
|
547
|
+
theme, which is the wrong direction. Layering the theme's own `--hover` over
|
|
548
|
+
the fill darkens in light and lightens in dark, with no new token. */
|
|
549
|
+
.app-btn--err:hover {
|
|
550
|
+
background: linear-gradient(var(--hover), var(--hover)), var(--err-bg);
|
|
551
|
+
filter: none;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* ------------------------------------------------------------------ */
|
|
555
|
+
/* AppField / AppSelect / AppDateField. */
|
|
556
|
+
/* ------------------------------------------------------------------ */
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Flat and compact, with the label OUTSIDE the box as a small muted word -
|
|
560
|
+
* not Vuetify's outlined field with a label that floats through the border.
|
|
561
|
+
* A native `<input>`/`<select>`, so the browser keeps the keyboard, the date
|
|
562
|
+
* picker and the accessibility for free.
|
|
563
|
+
*/
|
|
564
|
+
.app-field {
|
|
565
|
+
display: flex;
|
|
566
|
+
align-items: center;
|
|
567
|
+
gap: 8px;
|
|
568
|
+
height: var(--input-h);
|
|
569
|
+
padding: 0 12px;
|
|
570
|
+
border: 1px solid var(--border);
|
|
571
|
+
border-radius: var(--r-inner);
|
|
572
|
+
background: var(--card);
|
|
573
|
+
font-size: var(--fs-nav);
|
|
574
|
+
font-weight: 600;
|
|
575
|
+
color: var(--text);
|
|
576
|
+
min-width: 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.app-field--compact {
|
|
580
|
+
height: var(--input-h-compact);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.app-field:focus-within {
|
|
584
|
+
border-color: var(--accent);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.app-field__label {
|
|
588
|
+
font-size: 12px;
|
|
589
|
+
font-weight: 600;
|
|
590
|
+
color: var(--muted);
|
|
591
|
+
white-space: nowrap;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.app-field__icon {
|
|
595
|
+
color: var(--muted);
|
|
596
|
+
flex: 0 0 auto;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.app-field__input {
|
|
600
|
+
flex: 1;
|
|
601
|
+
min-width: 0;
|
|
602
|
+
height: 100%;
|
|
603
|
+
border: none;
|
|
604
|
+
outline: none;
|
|
605
|
+
background: transparent;
|
|
606
|
+
font-family: inherit;
|
|
607
|
+
font-size: var(--fs-cell);
|
|
608
|
+
font-weight: 500;
|
|
609
|
+
color: var(--text);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.app-field__input::placeholder {
|
|
613
|
+
color: var(--muted);
|
|
614
|
+
opacity: 1;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/*
|
|
618
|
+
* A native `<select>`'s option list is painted by the BROWSER, not by the page.
|
|
619
|
+
* The control above sets `color`, and an `<option>` inherits it - but it gets
|
|
620
|
+
* no background of its own, so the browser paints its own popup behind the
|
|
621
|
+
* theme's ink. In dark mode that popup is white on Windows/Chrome, and the
|
|
622
|
+
* theme's near-white ink on it measures 1.14:1: the list reads as blank, with
|
|
623
|
+
* only the highlighted row legible. QA reported exactly that on two apps.
|
|
624
|
+
*
|
|
625
|
+
* Giving the option the card's own fill and ink is what makes the open list
|
|
626
|
+
* belong to the theme. `--card`/`--text` and not a literal, so it follows the
|
|
627
|
+
* `v-theme--*` subtree the select is actually in - a dialog that forces a
|
|
628
|
+
* theme included. Declared once here rather than per component, so every
|
|
629
|
+
* native select this layer renders is covered, in every consuming app.
|
|
630
|
+
*
|
|
631
|
+
* The FRAME around the list is browser chrome and is not ours to paint; a
|
|
632
|
+
* light strip may remain around the options in some browsers.
|
|
633
|
+
*/
|
|
634
|
+
select option {
|
|
635
|
+
background-color: var(--card);
|
|
636
|
+
color: var(--text);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/* The `trailing` slot's control - same muted weight as the leading icon, so a
|
|
640
|
+
reveal toggle reads as field chrome rather than as a second action. */
|
|
641
|
+
.app-field__trailing {
|
|
642
|
+
display: grid;
|
|
643
|
+
place-items: center;
|
|
644
|
+
flex: 0 0 auto;
|
|
645
|
+
border: none;
|
|
646
|
+
background: transparent;
|
|
647
|
+
padding: 0;
|
|
648
|
+
color: var(--muted);
|
|
649
|
+
cursor: pointer;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.app-field__trailing:hover {
|
|
653
|
+
color: var(--text2);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/*
|
|
657
|
+
* AppSelect wears the same field chrome as a button and opens a themed
|
|
658
|
+
* popover. Multi values are `StatusChip pill tone-neutral`, the soft pill the
|
|
659
|
+
* design already uses and the one whose contrast is measured.
|
|
660
|
+
*/
|
|
661
|
+
.app-select {
|
|
662
|
+
cursor: pointer;
|
|
663
|
+
overflow: hidden;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.app-select--disabled {
|
|
667
|
+
cursor: default;
|
|
668
|
+
opacity: 0.55;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.app-select--single {
|
|
672
|
+
min-width: 180px;
|
|
673
|
+
font-family: inherit;
|
|
674
|
+
text-align: left;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.app-select__single-value {
|
|
678
|
+
flex: 1;
|
|
679
|
+
min-width: 0;
|
|
680
|
+
overflow: hidden;
|
|
681
|
+
font-size: var(--fs-nav);
|
|
682
|
+
font-weight: 600;
|
|
683
|
+
color: var(--text);
|
|
684
|
+
text-overflow: ellipsis;
|
|
685
|
+
white-space: nowrap;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/*
|
|
689
|
+
* The SAME step for the typed field, which never had one. `AppField` and
|
|
690
|
+
* `AppDateField` both take a `disabled` prop and pass it to the native
|
|
691
|
+
* `<input>` - and `AppField`'s prop doc says disabled "also greys the box" -
|
|
692
|
+
* but `.app-field` pins `background: var(--card)` and `.app-field__input` pins
|
|
693
|
+
* `color: var(--text)`, which overrides the greying a browser gives a disabled
|
|
694
|
+
* input for free. Measured 2026-08-16 on `AccessManagement`: enabled and
|
|
695
|
+
* disabled came back identical on background, colour, border, opacity and
|
|
696
|
+
* filter, on BOTH themes. The field was genuinely dead and read as live.
|
|
697
|
+
*
|
|
698
|
+
* Keyed off the input's own `:disabled` rather than a new modifier class, so
|
|
699
|
+
* it covers `AppField` and `AppDateField` without two templates having to
|
|
700
|
+
* remember to add it. 0.55 is
|
|
701
|
+
* `.app-select--disabled`'s number, not a second one.
|
|
702
|
+
*/
|
|
703
|
+
.app-field:has(.app-field__input:disabled) {
|
|
704
|
+
cursor: default;
|
|
705
|
+
opacity: 0.55;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.app-select__values {
|
|
709
|
+
display: flex;
|
|
710
|
+
align-items: center;
|
|
711
|
+
gap: 6px;
|
|
712
|
+
flex: 1;
|
|
713
|
+
min-width: 0;
|
|
714
|
+
overflow: hidden;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.app-select__placeholder {
|
|
718
|
+
font-size: var(--fs-cell);
|
|
719
|
+
font-weight: 500;
|
|
720
|
+
color: var(--muted);
|
|
721
|
+
white-space: nowrap;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/* The x that removes one pill, and the one that empties the field. */
|
|
725
|
+
.app-select__x,
|
|
726
|
+
.app-select__clear {
|
|
727
|
+
display: inline-flex;
|
|
728
|
+
align-items: center;
|
|
729
|
+
color: inherit;
|
|
730
|
+
opacity: 0.7;
|
|
731
|
+
background: none;
|
|
732
|
+
border: none;
|
|
733
|
+
padding: 0;
|
|
734
|
+
cursor: pointer;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.app-select__x:hover,
|
|
738
|
+
.app-select__clear:hover {
|
|
739
|
+
opacity: 1;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.app-select__clear {
|
|
743
|
+
color: var(--muted);
|
|
744
|
+
flex: 0 0 auto;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.app-select__menu {
|
|
748
|
+
min-width: 200px;
|
|
749
|
+
max-height: 280px;
|
|
750
|
+
overflow-y: auto;
|
|
751
|
+
padding: 6px;
|
|
752
|
+
border: 1px solid var(--border);
|
|
753
|
+
border-radius: var(--r-inner-lg);
|
|
754
|
+
background: var(--card);
|
|
755
|
+
box-shadow: var(--shadow-menu);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.app-select__option {
|
|
759
|
+
display: flex;
|
|
760
|
+
align-items: center;
|
|
761
|
+
gap: 8px;
|
|
762
|
+
width: 100%;
|
|
763
|
+
padding: 7px 8px;
|
|
764
|
+
border: none;
|
|
765
|
+
border-radius: var(--r-inner-sm);
|
|
766
|
+
background: none;
|
|
767
|
+
font-family: var(--font-sans);
|
|
768
|
+
font-size: var(--fs-cell);
|
|
769
|
+
font-weight: 500;
|
|
770
|
+
color: var(--text);
|
|
771
|
+
text-align: left;
|
|
772
|
+
cursor: pointer;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.app-select__option:hover {
|
|
776
|
+
background: var(--hover);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.app-select__option--selected {
|
|
780
|
+
background: var(--accent-soft);
|
|
781
|
+
color: var(--accent-text);
|
|
782
|
+
font-weight: 600;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.app-select__option--selected:hover {
|
|
786
|
+
background: var(--accent-soft);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.app-select__box {
|
|
790
|
+
color: var(--muted);
|
|
791
|
+
flex: 0 0 auto;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.app-select__box--on {
|
|
795
|
+
color: var(--accent);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.app-select__option-text {
|
|
799
|
+
flex: 1;
|
|
800
|
+
min-width: 0;
|
|
801
|
+
overflow: hidden;
|
|
802
|
+
text-overflow: ellipsis;
|
|
803
|
+
white-space: nowrap;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.app-field--search {
|
|
807
|
+
flex: 1;
|
|
808
|
+
min-width: 220px;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.app-filter-row {
|
|
812
|
+
display: flex;
|
|
813
|
+
gap: var(--grid-gap);
|
|
814
|
+
margin-bottom: 14px;
|
|
815
|
+
flex-wrap: wrap;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
@media (max-width: 599.98px) {
|
|
819
|
+
.app-filter-row > * {
|
|
820
|
+
flex: 1 1 100%;
|
|
821
|
+
min-width: 0;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/* ------------------------------------------------------------------ */
|
|
826
|
+
/* SegmentedControl - Today | Week | Month. */
|
|
827
|
+
/* ------------------------------------------------------------------ */
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* The handoff draws this as a 3px-padded bordered tray holding 8px-radius
|
|
831
|
+
* segments; the SELECTED one is an ACCENT FILL, not a grey one. Vuetify's
|
|
832
|
+
* `v-btn-toggle` cannot be talked into that shape (its own height scale, its
|
|
833
|
+
* dividers, its overlay layer), so the tray is written as markup.
|
|
834
|
+
*
|
|
835
|
+
* The fill is `--accent-strong`, not `--accent`: white on `--accent` measures
|
|
836
|
+
* 3.23:1, the same Phase-1 rule every other filled label follows.
|
|
837
|
+
*/
|
|
838
|
+
.app-seg {
|
|
839
|
+
display: flex;
|
|
840
|
+
gap: 2px;
|
|
841
|
+
padding: 3px;
|
|
842
|
+
border: 1px solid var(--border);
|
|
843
|
+
border-radius: var(--r-inner);
|
|
844
|
+
background: var(--card);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.app-seg__item {
|
|
848
|
+
padding: 6px 12px;
|
|
849
|
+
border: none;
|
|
850
|
+
border-radius: 8px;
|
|
851
|
+
background: transparent;
|
|
852
|
+
color: var(--muted);
|
|
853
|
+
font-family: inherit;
|
|
854
|
+
font-size: 12.5px;
|
|
855
|
+
font-weight: 700;
|
|
856
|
+
line-height: 1.2;
|
|
857
|
+
white-space: nowrap;
|
|
858
|
+
cursor: pointer;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.app-seg__item:hover {
|
|
862
|
+
background: var(--hover);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.app-seg__item--on,
|
|
866
|
+
.app-seg__item--on:hover {
|
|
867
|
+
background: var(--accent-strong);
|
|
868
|
+
color: var(--on-accent-strong);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/* ------------------------------------------------------------------ */
|
|
872
|
+
/* Switch - the settings toggle. */
|
|
873
|
+
/* ------------------------------------------------------------------ */
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* THE DESIGN'S TOGGLE, WRITTEN ONCE.
|
|
877
|
+
*
|
|
878
|
+
* The handoff draws it on the Settings screen's ANPR Camera section and then
|
|
879
|
+
* says, in the prototype's own words, that every other module's
|
|
880
|
+
* "toggles, form fields and tables use the same styling as the ANPR Camera
|
|
881
|
+
* section" - so this IS the spec for the HID panels, not an interpretation:
|
|
882
|
+
*
|
|
883
|
+
* track 38x22, `border-radius:999px`; ON `var(--accent)`, OFF `var(--hover)`
|
|
884
|
+
* thumb 18x18 circle at `top:2px`; ON `left:18px` `#ffffff`,
|
|
885
|
+
* OFF `left:2px` `var(--muted)`
|
|
886
|
+
* label 13.5px/700; the OFF label is `var(--text2)`
|
|
887
|
+
*
|
|
888
|
+
* `--toggle-w` / `--toggle-h` were put in `tokens.css` in Phase 1 for exactly
|
|
889
|
+
* this and had no rule to serve until now.
|
|
890
|
+
*
|
|
891
|
+
* Vuetify slides `.v-selection-control__input` and leaves the thumb static
|
|
892
|
+
* inside it; the design slides the THUMB inside a fixed track. So the input's
|
|
893
|
+
* transform is reset and the thumb is positioned directly - otherwise the two
|
|
894
|
+
* movements compound and the knob leaves the track.
|
|
895
|
+
*
|
|
896
|
+
* Three HID panels each carried their own copy of this with hardcoded hexes
|
|
897
|
+
* (`#8b9297` / `#2eaf55` / `#e0e0e0`), which is why a disabled toggle read as
|
|
898
|
+
* light grey on the dark theme's dark card. One rule, both themes, no hex.
|
|
899
|
+
*/
|
|
900
|
+
.app-switch .v-selection-control__wrapper {
|
|
901
|
+
position: relative;
|
|
902
|
+
flex: 0 0 auto;
|
|
903
|
+
width: var(--toggle-w);
|
|
904
|
+
height: var(--toggle-h);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.app-switch .v-switch__track {
|
|
908
|
+
position: absolute;
|
|
909
|
+
inset: 0;
|
|
910
|
+
width: var(--toggle-w);
|
|
911
|
+
height: var(--toggle-h);
|
|
912
|
+
border-radius: var(--r-pill);
|
|
913
|
+
background: var(--hover);
|
|
914
|
+
opacity: 1;
|
|
915
|
+
transition: background var(--motion);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.app-switch .v-selection-control--dirty .v-switch__track {
|
|
919
|
+
background: var(--accent);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.app-switch .v-selection-control__input {
|
|
923
|
+
position: absolute;
|
|
924
|
+
inset: 0;
|
|
925
|
+
width: var(--toggle-w);
|
|
926
|
+
height: var(--toggle-h);
|
|
927
|
+
/*
|
|
928
|
+
* Vuetify's own slide. The design moves the thumb, not this box - but the
|
|
929
|
+
* reset never landed: Vuetify ships it as
|
|
930
|
+
* `.v-locale--is-ltr .v-switch .v-selection-control__input` (THREE classes)
|
|
931
|
+
* and, when checked, `.v-locale--is-ltr .v-switch .v-selection-control--dirty
|
|
932
|
+
* .v-selection-control__input` (FOUR), both of which outrank this two-class
|
|
933
|
+
* rule. So every `.app-switch` in the layer drew its thumb 10px out: measured
|
|
934
|
+
* OFF thumb at `track.x - 8` (design: +2) and ON at `track.x + 28`
|
|
935
|
+
* (design: +18). `!important` rather than stacking four `.app-switch`
|
|
936
|
+
* selectors to out-specify it.
|
|
937
|
+
*/
|
|
938
|
+
transform: none !important;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/* The 40px hover/ripple disc is bigger than the whole control in this shape. */
|
|
942
|
+
.app-switch .v-selection-control__input::before,
|
|
943
|
+
.app-switch .v-selection-control__input .v-ripple__container {
|
|
944
|
+
display: none;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.app-switch .v-switch__thumb {
|
|
948
|
+
position: absolute;
|
|
949
|
+
top: 2px;
|
|
950
|
+
left: 2px;
|
|
951
|
+
width: 18px;
|
|
952
|
+
height: 18px;
|
|
953
|
+
border-radius: 50%;
|
|
954
|
+
background: var(--muted);
|
|
955
|
+
box-shadow: none;
|
|
956
|
+
transition: left var(--motion), background var(--motion);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.app-switch .v-selection-control--dirty .v-switch__thumb {
|
|
960
|
+
left: 18px;
|
|
961
|
+
background: #ffffff;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.app-switch .v-label {
|
|
965
|
+
padding-inline-start: 12px;
|
|
966
|
+
color: var(--text2);
|
|
967
|
+
font-family: var(--font-sans);
|
|
968
|
+
font-size: 13.5px;
|
|
969
|
+
font-weight: 700;
|
|
970
|
+
opacity: 1;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.app-switch .v-selection-control--dirty .v-label {
|
|
974
|
+
color: var(--text);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.app-switch.v-input--disabled {
|
|
978
|
+
opacity: 0.55;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/* ------------------------------------------------------------------ */
|
|
982
|
+
/* KpiTile - the dashboard's headline figure. */
|
|
983
|
+
/* ------------------------------------------------------------------ */
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Three rows, in this order, straight from the handoff:
|
|
987
|
+
* 1. a 34px soft icon square and the uppercase label, side by side
|
|
988
|
+
* 2. the 30px tabular number with its percentage pill BESIDE it
|
|
989
|
+
* 3. the sub-line
|
|
990
|
+
* The product had the icon alone on row 1 and the pill stranded at the
|
|
991
|
+
* bottom, which is why the tile read as a different component.
|
|
992
|
+
*/
|
|
993
|
+
.app-kpi {
|
|
994
|
+
padding: 18px;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.app-kpi__head {
|
|
998
|
+
display: flex;
|
|
999
|
+
align-items: center;
|
|
1000
|
+
gap: 10px;
|
|
1001
|
+
margin-bottom: 14px;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.app-kpi__icon {
|
|
1005
|
+
display: flex;
|
|
1006
|
+
align-items: center;
|
|
1007
|
+
justify-content: center;
|
|
1008
|
+
width: 34px;
|
|
1009
|
+
height: 34px;
|
|
1010
|
+
flex: 0 0 34px;
|
|
1011
|
+
border-radius: var(--r-inner);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.app-kpi__label {
|
|
1015
|
+
font-size: var(--fs-table-head);
|
|
1016
|
+
font-weight: var(--fw-table-head);
|
|
1017
|
+
letter-spacing: var(--ls-table-head);
|
|
1018
|
+
text-transform: uppercase;
|
|
1019
|
+
color: var(--muted);
|
|
1020
|
+
min-width: 0;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.app-kpi__action {
|
|
1024
|
+
margin-left: auto;
|
|
1025
|
+
flex: 0 0 auto;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.app-kpi__figure {
|
|
1029
|
+
display: flex;
|
|
1030
|
+
align-items: center;
|
|
1031
|
+
gap: 10px;
|
|
1032
|
+
flex-wrap: wrap;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.app-kpi__value {
|
|
1036
|
+
font-size: var(--fs-kpi);
|
|
1037
|
+
font-weight: var(--fw-kpi);
|
|
1038
|
+
letter-spacing: var(--ls-kpi);
|
|
1039
|
+
line-height: 1.1;
|
|
1040
|
+
color: var(--text);
|
|
1041
|
+
font-variant-numeric: tabular-nums;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.app-kpi__chip {
|
|
1045
|
+
display: inline-flex;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
padding: 3px 8px;
|
|
1048
|
+
border-radius: 999px;
|
|
1049
|
+
font-size: 11.5px;
|
|
1050
|
+
font-weight: 800;
|
|
1051
|
+
line-height: 1.2;
|
|
1052
|
+
background: var(--ok-bg);
|
|
1053
|
+
color: var(--ok);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
.app-kpi__chip--down {
|
|
1057
|
+
background: var(--err-bg);
|
|
1058
|
+
color: var(--err);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.app-kpi__sub {
|
|
1062
|
+
margin-top: 6px;
|
|
1063
|
+
font-size: 12.5px;
|
|
1064
|
+
font-weight: 600;
|
|
1065
|
+
color: var(--muted);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/* ------------------------------------------------------------------ */
|
|
1069
|
+
/* Responsive - the prototype has no reference, this follows Phase 1. */
|
|
1070
|
+
/* ------------------------------------------------------------------ */
|
|
1071
|
+
|
|
1072
|
+
@media (max-width: 767.98px) {
|
|
1073
|
+
.app-page-head {
|
|
1074
|
+
align-items: flex-start;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/* The grid table keeps its columns and scrolls sideways inside the card,
|
|
1078
|
+
deliberately not restacked into key/value blocks: the columns carry
|
|
1079
|
+
meaning by position. Same decision as Phase 1's tables. */
|
|
1080
|
+
.app-table-scroll {
|
|
1081
|
+
overflow-x: auto;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.app-table-scroll > * {
|
|
1085
|
+
min-width: 720px;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/* ------------------------------------------------------------------ */
|
|
1090
|
+
/* Field label (`components/InputLabel.vue`, 125 caller files). */
|
|
1091
|
+
/* */
|
|
1092
|
+
/* Every form label in the product rendered Vuetify's `text-subtitle-2`:*/
|
|
1093
|
+
/* 14px/500 at 0.0071em tracking, declaring `font-family: Roboto`. */
|
|
1094
|
+
/* The handoff's own label - the Create Role modal - is 12.5px/700 with*/
|
|
1095
|
+
/* a 6px gap under it and the asterisk on `--err`. */
|
|
1096
|
+
/* */
|
|
1097
|
+
/* `margin-bottom` is deliberately NOT `!important`, so the ~40 callers*/
|
|
1098
|
+
/* that already pass a Vuetify spacing utility (`mb-1`, `mb-2`) keep */
|
|
1099
|
+
/* winning - those classes carry `!important` - and only the labels */
|
|
1100
|
+
/* that state no spacing take the design's 6px. */
|
|
1101
|
+
/* ------------------------------------------------------------------ */
|
|
1102
|
+
.app-input-label {
|
|
1103
|
+
font-family: var(--font-sans);
|
|
1104
|
+
font-size: 12.5px;
|
|
1105
|
+
font-weight: 700;
|
|
1106
|
+
letter-spacing: 0;
|
|
1107
|
+
color: var(--text);
|
|
1108
|
+
margin-bottom: 6px;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.app-input-label__req {
|
|
1112
|
+
color: var(--err);
|
|
1113
|
+
}
|