@7365admin1/layer-common 3.2.7 → 3.2.8-staging.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/assets/css/primitives.css +1113 -0
  2. package/assets/css/screens.css +1097 -0
  3. package/assets/css/shell.css +258 -0
  4. package/assets/css/tokens.css +236 -0
  5. package/components/AcceptDialog.vue +17 -18
  6. package/components/AccessCard/AvailableStats.vue +24 -16
  7. package/components/AccessCardAddForm.vue +42 -42
  8. package/components/AccessCardAssignToUnitForm.vue +13 -21
  9. package/components/AccessCardDeleteDialog.vue +41 -39
  10. package/components/AccessCardDetailsDialog.vue +125 -79
  11. package/components/AccessCardPreviewDialog.vue +58 -84
  12. package/components/AccessCardQrTagging.vue +60 -63
  13. package/components/AccessCardReplaceForm.vue +27 -23
  14. package/components/AccessManagement.vue +86 -85
  15. package/components/AddEqupmentForm.vue +18 -22
  16. package/components/AddPassKeyToVisitor.vue +95 -73
  17. package/components/AppButton.vue +62 -0
  18. package/components/AppCard.vue +20 -0
  19. package/components/AppDateField.vue +49 -0
  20. package/components/AppField.vue +76 -0
  21. package/components/AppKpiTile.vue +65 -0
  22. package/components/AppSegmented.vue +38 -0
  23. package/components/AppSelect.vue +247 -0
  24. package/components/AreaChecklistHistoryLogs.vue +51 -44
  25. package/components/AreaChecklistHistoryMain.vue +14 -7
  26. package/components/AreaFormDialog.vue +71 -59
  27. package/components/AreaMain.vue +73 -107
  28. package/components/AttachmentsViewer.vue +84 -22
  29. package/components/AttendanceCheckInOutDialog.vue +85 -88
  30. package/components/AttendanceDetailsDialog.vue +57 -63
  31. package/components/AttendanceMain.vue +4 -3
  32. package/components/AttendanceMapSearchDialog.vue +42 -44
  33. package/components/AttendanceSettingsDialog.vue +79 -63
  34. package/components/Avatar/Main.vue +54 -2
  35. package/components/BillingMain.vue +26 -30
  36. package/components/BtnUploadFile.vue +53 -11
  37. package/components/BuildingForm.vue +34 -27
  38. package/components/BuildingManagement/buildings.vue +39 -39
  39. package/components/BuildingManagement/units.vue +26 -42
  40. package/components/BuildingUnitFormAdd.vue +20 -23
  41. package/components/BuildingUnitFormEdit.vue +8 -4
  42. package/components/BulletinBoardForm.vue +19 -24
  43. package/components/BulletinBoardManagement.vue +49 -57
  44. package/components/BulletinBoardView.vue +9 -6
  45. package/components/BulletinExpirationChip.vue +13 -4
  46. package/components/Button/Close.vue +2 -2
  47. package/components/CameraForm.vue +21 -22
  48. package/components/CameraMain.vue +50 -49
  49. package/components/Card/DeleteConfirmation.vue +24 -22
  50. package/components/Card/MemberInfoSummary.vue +28 -3
  51. package/components/Card/Toggle.vue +15 -3
  52. package/components/CardToolbar.vue +37 -0
  53. package/components/Carousel.vue +56 -36
  54. package/components/Chat/Bubbles.vue +43 -14
  55. package/components/Chat/Information.vue +49 -32
  56. package/components/Chat/ListCard.vue +38 -9
  57. package/components/Chat/Message.vue +41 -15
  58. package/components/Chat/Navigation.vue +27 -15
  59. package/components/Chat/SkeletonLoader.vue +9 -2
  60. package/components/ChecklistItemRow.vue +25 -1
  61. package/components/CleaningScheduleMain.vue +44 -49
  62. package/components/ClientDetailForm.vue +81 -56
  63. package/components/ClientMain.vue +92 -82
  64. package/components/ConfirmDialog.vue +14 -8
  65. package/components/Container/Standard.vue +9 -7
  66. package/components/CreateSubsciptionPlan.vue +148 -89
  67. package/components/DashboardEmptyState.vue +48 -3
  68. package/components/DashboardMain.vue +924 -676
  69. package/components/DashboardPanel.vue +84 -21
  70. package/components/DeliveryCompany.vue +34 -20
  71. package/components/Dialog/DeleteConfirmation.vue +20 -21
  72. package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
  73. package/components/Dialog/ReusablePrompt.vue +30 -29
  74. package/components/Dialog/UpdateMoreAction.vue +62 -59
  75. package/components/DocumentForm.vue +14 -21
  76. package/components/DocumentManagement.vue +162 -108
  77. package/components/DocumentViewer.vue +29 -12
  78. package/components/DrawImage.vue +29 -21
  79. package/components/EntryPass/QrTemplatePreview.vue +27 -16
  80. package/components/EntryPassInformation.vue +40 -10
  81. package/components/EntryPassMain.vue +49 -27
  82. package/components/EquipmentItemMain.vue +84 -97
  83. package/components/EquipmentManagementMain.vue +81 -103
  84. package/components/Facility/BookingSetup.vue +43 -52
  85. package/components/Feedback/Form.vue +26 -12
  86. package/components/FeedbackMain.vue +35 -49
  87. package/components/FileInput.vue +75 -58
  88. package/components/FileInputWithList.vue +59 -12
  89. package/components/FormDialog.vue +60 -21
  90. package/components/HidAccessLogDashboard.vue +512 -490
  91. package/components/HidCameraCaptureDialog.vue +216 -0
  92. package/components/HidEnabledGate.vue +67 -9
  93. package/components/HidIntercomManagement.vue +1322 -653
  94. package/components/HidQrCodeConfiguration.vue +1030 -258
  95. package/components/HidReaderForm.vue +142 -59
  96. package/components/HidReaderManagement.vue +211 -323
  97. package/components/HidServiceSettingsPanel.vue +44 -13
  98. package/components/HidUserEnrollment.vue +1185 -412
  99. package/components/HygieneUpdateMoreAction.vue +44 -44
  100. package/components/IncidentReport/Authorities.vue +30 -18
  101. package/components/IncidentReport/IncidentInformation.vue +10 -10
  102. package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
  103. package/components/IncidentReport/affectedEntities.vue +41 -20
  104. package/components/Input/Date.vue +3 -0
  105. package/components/Input/DatePicker.vue +1 -2
  106. package/components/Input/DateTimePicker.vue +1 -1
  107. package/components/Input/File.vue +55 -10
  108. package/components/Input/FileV2.vue +39 -6
  109. package/components/Input/InputPhoneNumberV2.vue +2 -1
  110. package/components/Input/ListGroupSelection.vue +56 -8
  111. package/components/Input/NRICNumber.vue +1 -0
  112. package/components/Input/NewDate.vue +1 -0
  113. package/components/Input/Number.vue +1 -0
  114. package/components/Input/Password.vue +1 -0
  115. package/components/Input/PhoneNumber.vue +21 -1
  116. package/components/Input/VehicleNumber.vue +1 -0
  117. package/components/InputLabel.vue +2 -2
  118. package/components/InvitationClientForm.vue +21 -19
  119. package/components/InvitationForm.vue +15 -21
  120. package/components/InvitationMain.vue +46 -61
  121. package/components/Layout/Header.vue +263 -36
  122. package/components/Layout/NavigationDrawer.vue +300 -30
  123. package/components/ListItem.vue +33 -3
  124. package/components/LocalPagination.vue +27 -3
  125. package/components/ManageChecklistMain.vue +136 -86
  126. package/components/MemberInformation.vue +32 -16
  127. package/components/MemberMain.vue +113 -93
  128. package/components/MyAttendanceMain.vue +3 -5
  129. package/components/NavigationItem.vue +83 -3
  130. package/components/Nfc/NFCPatrolReportMain.vue +47 -33
  131. package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
  132. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  133. package/components/Nfc/NFCPatrolSettings.vue +14 -4
  134. package/components/Nfc/NFCTagForm.vue +22 -8
  135. package/components/Nfc/NFCTagMain.vue +41 -43
  136. package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
  137. package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
  138. package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
  139. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
  140. package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
  141. package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
  142. package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
  143. package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
  144. package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
  145. package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
  146. package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
  147. package/components/NotificationSettings.vue +373 -0
  148. package/components/NumberSettingField.vue +9 -2
  149. package/components/OnlineFormConfigurationForm.vue +725 -237
  150. package/components/OnlineFormFill.vue +213 -106
  151. package/components/OnlineFormRenderPreview.vue +25 -30
  152. package/components/OnlineFormsConfiguration.vue +113 -109
  153. package/components/OrgViewDialog.vue +34 -16
  154. package/components/OvernightParkingAvailability.vue +49 -26
  155. package/components/OvernightParkingManagement.vue +78 -18
  156. package/components/PageHeader.vue +43 -0
  157. package/components/PassInformation.vue +109 -53
  158. package/components/PasswordConfirmation.vue +44 -26
  159. package/components/PeopleForm.vue +41 -20
  160. package/components/PhotoUpload.vue +110 -48
  161. package/components/PlaceholderComponent.vue +35 -19
  162. package/components/PlanActionsMenu.vue +12 -2
  163. package/components/PlanStatusBadge.vue +7 -8
  164. package/components/PlateNumberDisplay.vue +9 -6
  165. package/components/QrTemplate/PrintDialog.vue +66 -43
  166. package/components/RolePermissionFormCreate.vue +104 -79
  167. package/components/RolePermissionFormPreviewUpdate.vue +111 -109
  168. package/components/RolePermissionMain.vue +183 -81
  169. package/components/ScanVisitorQRCode.vue +99 -35
  170. package/components/ScheduleAreaMain.vue +34 -59
  171. package/components/ScheduleTaskForm.vue +61 -42
  172. package/components/ScheduleTaskMain.vue +56 -69
  173. package/components/SearchVehicleNumberUser.vue +39 -37
  174. package/components/ServiceProviderFormCreate.vue +29 -20
  175. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  176. package/components/ServiceProviderMain.vue +601 -432
  177. package/components/Signature.vue +40 -34
  178. package/components/SignaturePad.vue +5 -29
  179. package/components/SiteSettings.vue +41 -11
  180. package/components/SiteSettingsWorkOrder.vue +61 -30
  181. package/components/SlideCardGroup.vue +35 -11
  182. package/components/Snackbar.vue +36 -3
  183. package/components/SpecificAttr.vue +18 -8
  184. package/components/StatusChip.vue +156 -0
  185. package/components/StockCard.vue +17 -12
  186. package/components/SubscriptionPlanTable.vue +207 -140
  187. package/components/SwitchContext.vue +201 -9
  188. package/components/TableHygiene.vue +36 -46
  189. package/components/TableList.vue +10 -10
  190. package/components/TableListSecondary.vue +105 -54
  191. package/components/TableMain.vue +251 -78
  192. package/components/TableV3.vue +61 -70
  193. package/components/TiptapEditor.vue +33 -40
  194. package/components/TiptapEditorQr.vue +5 -2
  195. package/components/Tooltip/Info.vue +69 -24
  196. package/components/UnitMain.vue +40 -60
  197. package/components/UnitPersonCard.vue +42 -13
  198. package/components/VehicleAddSelection.vue +48 -32
  199. package/components/VehicleForm.vue +112 -129
  200. package/components/VehicleManagement.vue +26 -12
  201. package/components/VehicleUpdateMoreAction.vue +59 -46
  202. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  203. package/components/VisitorForm.vue +774 -210
  204. package/components/VisitorFormSelection.vue +40 -37
  205. package/components/VisitorManagement.vue +239 -82
  206. package/components/VisitorPassKeyQRScanner.vue +65 -18
  207. package/components/VisitorSearchResultSummary.vue +60 -16
  208. package/components/VisitorSocketPopUp.vue +22 -14
  209. package/components/VisitorsReportPreview.vue +44 -29
  210. package/components/WorkOrder/Create.vue +26 -17
  211. package/components/WorkOrder/ListView.vue +38 -7
  212. package/components/WorkOrder/Main.vue +62 -59
  213. package/composables/useFeedback.ts +3 -3
  214. package/composables/useHidAmico.ts +269 -0
  215. package/composables/useHidNavigation.ts +2 -9
  216. package/composables/useMember.ts +3 -3
  217. package/composables/useNFCPatrolDailyReport.ts +13 -0
  218. package/composables/useNFCPatrolMonthlyReport.ts +22 -56
  219. package/composables/useNFCPatrolReport.ts +13 -0
  220. package/composables/useNotificationPreference.ts +87 -0
  221. package/composables/useOnlineFormPages.ts +2 -0
  222. package/composables/useRole.ts +22 -0
  223. package/composables/useServiceProvider.ts +3 -3
  224. package/composables/useServiceProviderInvitation.ts +145 -0
  225. package/composables/useSipWebPhone.ts +336 -0
  226. package/composables/useSubscriptionPlan.ts +62 -0
  227. package/composables/useThemePreference.ts +63 -0
  228. package/composables/useVisitor.ts +6 -1
  229. package/composables/useWebUsb.ts +127 -37
  230. package/composables/useWorkOrder.ts +17 -3
  231. package/error.vue +30 -6
  232. package/nuxt.config.ts +36 -0
  233. package/package.json +25 -4
  234. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
  235. package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
  236. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  237. package/pages/notification-settings.vue +19 -0
  238. package/pages/payment-method-linked.vue +34 -10
  239. package/pages/require-customer.vue +48 -10
  240. package/pages/require-organization-membership.vue +32 -8
  241. package/pages/unauthorized.vue +29 -4
  242. package/plugins/secure-member.client.ts +21 -56
  243. package/plugins/vuetify.ts +69 -2
  244. package/types/nfc-patrol-report.ts +3 -2
  245. package/types/site.d.ts +71 -0
  246. package/types/subscription.ts +18 -11
  247. package/utils/breadcrumb.test.ts +108 -0
  248. package/utils/breadcrumb.ts +82 -0
  249. package/utils/dashboard.test.ts +118 -0
  250. package/utils/dashboard.ts +53 -0
  251. package/utils/data.test.ts +95 -0
  252. package/utils/data.ts +40 -2
  253. package/utils/hid-discover.test.ts +102 -0
  254. package/utils/list-page.test.ts +60 -0
  255. package/utils/list-page.ts +38 -0
  256. package/utils/materialSymbols.ts +363 -0
  257. package/utils/nav-sections.test.ts +190 -0
  258. package/utils/nav-sections.ts +151 -0
  259. package/utils/nfc-patrol-report.test.ts +130 -0
  260. package/utils/nfc-patrol-report.ts +101 -0
  261. package/utils/role.test.ts +69 -0
  262. package/utils/role.ts +10 -0
  263. package/utils/route-name.test.ts +85 -0
  264. package/utils/route-name.ts +24 -0
  265. package/utils/status.test.ts +110 -0
  266. package/utils/status.ts +135 -0
  267. package/utils/table-card.test.ts +84 -0
  268. package/utils/theme-aa-ledger.ts +148 -0
  269. package/utils/theme.test.ts +485 -7
  270. package/utils/theme.ts +295 -110
  271. package/.changeset/README.md +0 -8
  272. package/.changeset/config.json +0 -11
  273. package/.editorconfig +0 -12
  274. package/.github/workflows/main.yml +0 -17
  275. package/.github/workflows/publish-staging.yml +0 -47
  276. package/.github/workflows/publish.yml +0 -39
  277. package/PUBLISHING.md +0 -269
  278. package/components/HidIdentityMapping.vue +0 -975
  279. package/components/ScheduleTaskAreaFormDialog.vue +0 -144
  280. package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
  281. package/components/ScheduleTastTicketMain.vue +0 -182
  282. package/components/TableWithButton.vue +0 -94
  283. package/test/visitor-socket.test.mjs +0 -36
@@ -0,0 +1,135 @@
1
+ /**
2
+ * THE SEMANTIC STATUS MAPPING, IN ONE PLACE.
3
+ *
4
+ * The design assigns a colour to a status WORD rather than to a module, so the
5
+ * same word has to look the same wherever it appears - "Open" on a Daily
6
+ * Occurrence Book and "Open" on a work order are the same green. Keeping the
7
+ * list here rather than in each screen is what makes that true, and is what
8
+ * Phases 3 and 4 read when they restyle the chips.
9
+ *
10
+ * Straight from the handoff:
11
+ * Completed / Paid / Available / Open -> ok
12
+ * Pending -> warn
13
+ * In Use / Closed / Checkout -> err
14
+ * Role and info tags -> info
15
+ *
16
+ * Anything not listed is `neutral` on purpose. A status this product has and
17
+ * the design did not name must NOT be guessed into a colour - a wrong green on
18
+ * a failed job is worse than a grey one - so it comes out neutral and visible,
19
+ * which is how it gets noticed and added.
20
+ */
21
+ export type TStatusTone = "ok" | "warn" | "err" | "info" | "neutral";
22
+
23
+ const TONES: Record<string, TStatusTone> = {
24
+ completed: "ok",
25
+ paid: "ok",
26
+ available: "ok",
27
+ open: "ok",
28
+
29
+ pending: "warn",
30
+
31
+ "in use": "err",
32
+ closed: "err",
33
+ checkout: "err",
34
+
35
+ role: "info",
36
+ info: "info",
37
+
38
+ /**
39
+ * PHASE 4 - THE WORDS THIS PRODUCT ACTUALLY USES.
40
+ *
41
+ * The handoff names ten words; the module screens show more than that, and
42
+ * each of them is ALREADY painted a colour today by a per-screen
43
+ * `getStatusColor`. So these are not new opinions - each one is the tone the
44
+ * screens already give that word, moved into the one list so that "Ongoing"
45
+ * is the same blue on a cleaning schedule and on a scheduled area. Where the
46
+ * old per-screen colour and the design disagree the design wins, and there
47
+ * is exactly one of those: `Open` was grey and the handoff names it `ok`.
48
+ *
49
+ * Anything still unlisted stays neutral, deliberately - see above.
50
+ */
51
+ ongoing: "info", // was `primary`
52
+ "in progress": "warn", // was #FB8C00, orange
53
+ "for review": "info", // was `primary`
54
+ "to do": "warn", // was `orange`
55
+
56
+ accepted: "ok", // was `success`
57
+ approved: "ok",
58
+ resolved: "ok", // was #4CAF50, green
59
+ active: "ok", // was `success`
60
+
61
+ // An online-form submission sent back to the resident to correct and send
62
+ // again. Unlisted it would come out neutral, which would make "needs your
63
+ // attention" look the same as "nothing is happening".
64
+ resubmission: "warn",
65
+
66
+ assigned: "info", // was `primary`, on the access-card details dialog
67
+ replaced: "warn", // was `orange`, same screen
68
+ returned: "warn", // was `orange`, same screen - the card came back
69
+
70
+ rejected: "err", // was `error`
71
+ deleted: "err", // was `error`
72
+ suspended: "err", // was `error`
73
+
74
+ complete: "ok",
75
+
76
+ inactive: "neutral", // was `grey`
77
+ "in active": "neutral", // the same word, printed "In-Active"
78
+ declined: "neutral", // was Vuetify's `default`
79
+ expired: "neutral", // was `grey`
80
+
81
+ "awaiting approval": "warn", // a waiting state, like Pending
82
+
83
+ /**
84
+ * HID enrolment: is this reader identity tied to a real person record yet?
85
+ * Same rule as the Phase 4 block above - not a new opinion, but the tone
86
+ * `HidUserEnrollment` already painted these two words, moved into the one
87
+ * list. Left unlisted they would BOTH come out neutral, which would erase
88
+ * the only distinction the column exists to draw.
89
+ */
90
+ mapped: "ok", // was `status-success`, #1b7f3a on #dff7e7
91
+ unmapped: "warn", // was `status-warning`, #9a6700 on #fff0cc
92
+
93
+ /**
94
+ * HID intercom SIP state, from `HidIntercomManagement`'s own
95
+ * `getStatusClass`. Same rule again: the tone the screen already gave the
96
+ * word, moved into the one list. "Disabled" and "Not connected" are NOT
97
+ * failures - the operator turned SIP off, or never set it up - so they stay
98
+ * neutral, which is what `status-muted` painted them.
99
+ */
100
+ /**
101
+ * HID access authorization. These arrive with `b0667a7` on `staging`
102
+ * ("Call the Authorized status API from Amico via Access Logs"), which
103
+ * replaces the Access Logs' Mapped/Unmapped column with these three words.
104
+ * Listed here AHEAD of that merge so the shared chip already knows them -
105
+ * unlisted they would all come out neutral, silently erasing the difference
106
+ * between a granted and a denied door. Tones are that commit's own:
107
+ * `mapped-chip` green, `unmapped-chip` red, `unknown-chip` amber.
108
+ */
109
+ authorized: "ok",
110
+ "not authorized": "err",
111
+ unknown: "warn",
112
+
113
+ connected: "ok", // was `status-success`
114
+ connecting: "warn", // was `status-warning`, the fall-through case
115
+ "not connected": "neutral", // was `status-muted`
116
+ disabled: "neutral", // was `status-muted`
117
+ failed: "err", // was `status-error`
118
+ };
119
+
120
+ /**
121
+ * The tone for a status word. Case- and space-insensitive, because the same
122
+ * status arrives as "In Use", "in_use" and "IN USE" from different endpoints.
123
+ */
124
+ export function statusTone(status: string | null | undefined): TStatusTone {
125
+ if (!status) return "neutral";
126
+
127
+ const key = String(status).trim().toLowerCase().replace(/[_-]+/g, " ");
128
+
129
+ return TONES[key] ?? "neutral";
130
+ }
131
+
132
+ /** The class `assets/css/tokens.css` paints that tone with. */
133
+ export function statusToneClass(status: string | null | undefined): string {
134
+ return `tone-${statusTone(status)}`;
135
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * REGRESSION GUARDS FOR THE TABLE CARD'S BUSY SIGNAL.
3
+ *
4
+ * Both faults here are a CSS declaration and a template binding, not a pure
5
+ * function, and this repo has no component test runner (adding one is a
6
+ * dependency change). So these assert the SOURCE CONTRACT: the exact
7
+ * declarations whose absence caused the defect. They fail the moment someone
8
+ * deletes the anchor or the disabled binding again.
9
+ *
10
+ * The behavioural proof is the driven browser measurement recorded on the PR -
11
+ * bar 1440x2 at `top: 0` of the WINDOW before, 1406x2 at the card's own top
12
+ * edge after; refresh button `false -> false -> false` before,
13
+ * `false -> true -> false` after.
14
+ */
15
+ import { describe, it } from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { readFileSync } from "node:fs";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const read = (rel: string) =>
21
+ readFileSync(fileURLToPath(new URL(`../${rel}`, import.meta.url)), "utf8");
22
+
23
+ /** The declaration block for a selector, comments stripped. */
24
+ const ruleFor = (css: string, selector: string) => {
25
+ const at = css.indexOf(`\n${selector} {`);
26
+ assert.notEqual(at, -1, `no rule for "${selector}"`);
27
+ const open = css.indexOf("{", at);
28
+ const close = css.indexOf("}", open);
29
+ return css
30
+ .slice(open + 1, close)
31
+ .replace(/\/\*[\s\S]*?\*\//g, "")
32
+ .trim();
33
+ };
34
+
35
+ describe("the loading bar's containing block", () => {
36
+ const screens = read("assets/css/screens.css");
37
+
38
+ it("anchors .table-card__loading to its card, not the viewport", () => {
39
+ // The bar is absolute. Without this the nearest positioned ancestor is
40
+ // the initial containing block, i.e. the browser window: the bar was
41
+ // measured 1440x2 at top:0 while its card sat 61.5px lower.
42
+ assert.match(ruleFor(screens, ".table-card"), /position:\s*relative/);
43
+ });
44
+
45
+ it("still positions the bar out of flow, so a load causes no 2px jump", () => {
46
+ assert.match(ruleFor(screens, ".table-card__loading"), /position:\s*absolute/);
47
+ });
48
+ });
49
+
50
+ describe("TableMain's refresh button", () => {
51
+ const sfc = read("components/TableMain.vue");
52
+ // The one `v-btn` carrying the refresh emit.
53
+ const refreshBtn = (() => {
54
+ const at = sfc.indexOf(`@click="emits('refresh')"`);
55
+ assert.notEqual(at, -1, "no refresh button in TableMain");
56
+ const open = sfc.lastIndexOf("<v-btn", at);
57
+ return sfc.slice(open, at);
58
+ })();
59
+
60
+ it("is disabled while the caller's request is in flight", () => {
61
+ // Without this, clicking refresh on a list that comes back identical
62
+ // changed zero pixels - the Work Orders / Duty Officer Book report.
63
+ assert.match(refreshBtn, /:disabled="loading"/);
64
+ });
65
+
66
+ it("reads the same `loading` prop the bar reads, so the two cannot diverge", () => {
67
+ assert.match(sfc, /v-if="loading"[\s\S]{0,200}table-card__loading/);
68
+ assert.match(sfc, /loading:\s*\{\s*type:\s*Boolean,\s*default:\s*false,?\s*\}/);
69
+ });
70
+ });
71
+
72
+ describe(".app-switch's thumb", () => {
73
+ const primitives = read("assets/css/primitives.css");
74
+
75
+ it("beats Vuetify's 3- and 4-class slide on the input", () => {
76
+ // `.v-locale--is-ltr .v-switch .v-selection-control__input` and its
77
+ // `--dirty` twin outrank a 2-class rule, so the plain reset never
78
+ // applied and every thumb sat 10px out of its track.
79
+ assert.match(
80
+ ruleFor(primitives, ".app-switch .v-selection-control__input"),
81
+ /transform:\s*none\s*!important/,
82
+ );
83
+ });
84
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * THE LIGHT THEME'S KNOWN AA FAILURES, MEASURED.
3
+ *
4
+ * The owner's 2026-08-14 decision is 100% design fidelity over WCAG AA, so the
5
+ * six light values that had been darkened are back on the handoff and the light
6
+ * theme no longer clears 4.5:1 everywhere. This file is the ledger of exactly
7
+ * what that costs: every pair, its measured ratio, and what it needs.
8
+ *
9
+ * It is data, not prose - `theme.test.ts` asserts each entry still measures what
10
+ * it says, so a later change that drifts a colour (in either direction) fails
11
+ * the build and has to come back here and say so.
12
+ *
13
+ * `run` prints the table: `node --experimental-strip-types utils/theme-aa-ledger.ts`
14
+ */
15
+ import { PALETTE, LIGHT_THEME, DARK_THEME } from "./theme.ts";
16
+
17
+ const luminance = (hex: string): number => {
18
+ const c = [0, 2, 4]
19
+ .map((i) => parseInt(hex.slice(1 + i, 3 + i), 16) / 255)
20
+ .map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
21
+ return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
22
+ };
23
+
24
+ export const contrast = (a: string, b: string): number => {
25
+ const [x, y] = [luminance(a), luminance(b)];
26
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
27
+ };
28
+
29
+ export const over = (fg: string, bg: string, alpha: number): string =>
30
+ "#" +
31
+ [0, 2, 4]
32
+ .map((i) =>
33
+ Math.round(
34
+ parseInt(fg.slice(1 + i, 3 + i), 16) * alpha +
35
+ parseInt(bg.slice(1 + i, 3 + i), 16) * (1 - alpha),
36
+ )
37
+ .toString(16)
38
+ .padStart(2, "0"),
39
+ )
40
+ .join("");
41
+
42
+ const L = PALETTE.light;
43
+
44
+ /** `--thead` is a 2% ink wash over the card, not a colour of its own. */
45
+ export const thead = over("#14161a", L.card, 0.02);
46
+
47
+ /**
48
+ * `--hover` is a 5% ink wash. On the page background it is the fill a DISABLED
49
+ * control sits on (the HID reader identification methods, greyed until Online
50
+ * Mode is on), so `--muted` label on `--hover` is a pair a person actually reads.
51
+ */
52
+ export const hoverOnBg = over("#14161a", L.bg, 0.05);
53
+
54
+
55
+ /** The soft chip backgrounds, exactly as the design states them. */
56
+ export const chip = {
57
+ ok: over("#0f8a62", L.card, 0.1),
58
+ warn: over("#be8c19", L.card, 0.13),
59
+ err: over("#cf4b4b", L.card, 0.1),
60
+ info: over("#3b6fd4", L.card, 0.1),
61
+ };
62
+
63
+ /** `--accent-soft` is transparent, so it takes the colour of what is behind it. */
64
+ export const accentSoft = {
65
+ sidebar: over(L.accent, L.sidebar, 0.12),
66
+ card: over(L.accent, L.card, 0.12),
67
+ };
68
+
69
+ /**
70
+ * Every LIGHT pair a person actually reads. `need` is 4.5 for body text and 3
71
+ * for large text (>=18.66px, or >=14px at 700+) and for non-text edges.
72
+ */
73
+ export const LIGHT_PAIRS: Array<{
74
+ what: string;
75
+ fg: string;
76
+ bg: string;
77
+ need: number;
78
+ }> = [
79
+ { what: "muted on a card (KPI sub-lines, field labels)", fg: L.muted, bg: L.card, need: 4.5 },
80
+ { what: "muted on the page background", fg: L.muted, bg: L.bg, need: 4.5 },
81
+ /*
82
+ * `muted on a table header band` used to sit here at 3.10:1. `.table-card
83
+ * thead th` now takes `--text2` (9.22:1 light / 9.57:1 dark), and nothing
84
+ * else paints `--muted` on a `--thead` band - `.table-card__group`'s label
85
+ * is Vuetify's `text-medium-emphasis`. The pair no longer occurs, so it is
86
+ * no longer carried as accepted debt.
87
+ */
88
+ { what: "muted on the sidebar", fg: L.muted, bg: L.sidebar, need: 4.5 },
89
+ { what: "muted on a disabled control's hover fill", fg: L.muted, bg: hoverOnBg, need: 4.5 },
90
+ { what: "ok label on its own chip", fg: L.ok, bg: chip.ok, need: 4.5 },
91
+ { what: "warn label on its own chip", fg: L.warn, bg: chip.warn, need: 4.5 },
92
+ { what: "err label on its own chip", fg: L.err, bg: chip.err, need: 4.5 },
93
+ { what: "info label on its own chip", fg: L.info, bg: chip.info, need: 4.5 },
94
+ { what: "ok as plain text on a card", fg: L.ok, bg: L.card, need: 4.5 },
95
+ { what: "warn as plain text on a card", fg: L.warn, bg: L.card, need: 4.5 },
96
+ { what: "err as plain text on a card (the Logout row)", fg: L.err, bg: L.card, need: 4.5 },
97
+ { what: "info as plain text on a card", fg: L.info, bg: L.card, need: 4.5 },
98
+ { what: "accent-text as a link on a card", fg: L.accentText, bg: L.card, need: 4.5 },
99
+ { what: "accent-text on accent-soft over the sidebar (active nav item)", fg: L.accentText, bg: accentSoft.sidebar, need: 4.5 },
100
+ { what: "accent-text on accent-soft over a card (active tab, avatar initials)", fg: L.accentText, bg: accentSoft.card, need: 4.5 },
101
+ { what: "white on the filled success colour", fg: "#ffffff", bg: L.ok, need: 4.5 },
102
+ { what: "white on the filled warning colour", fg: "#ffffff", bg: L.warn, need: 4.5 },
103
+ { what: "white on the filled error colour", fg: "#ffffff", bg: L.err, need: 4.5 },
104
+ { what: "white on the filled info colour", fg: "#ffffff", bg: L.info, need: 4.5 },
105
+ { what: "white on primary-button (accent-strong) - NOT reverted", fg: "#ffffff", bg: L.accentStrong, need: 4.5 },
106
+ { what: "text on a card", fg: L.text, bg: L.card, need: 4.5 },
107
+ { what: "text2 on a card", fg: L.text2, bg: L.card, need: 4.5 },
108
+ ];
109
+
110
+ export const measure = () =>
111
+ LIGHT_PAIRS.map((p) => ({
112
+ ...p,
113
+ ratio: Math.round(contrast(p.fg, p.bg) * 100) / 100,
114
+ passes: contrast(p.fg, p.bg) >= p.need,
115
+ }));
116
+
117
+ /** The dark theme was exact as handed over and clears AA - this proves it still does. */
118
+ export const DARK_PAIRS = () => {
119
+ const D = PALETTE.dark;
120
+ const dthead = over("#ffffff", D.card, 0.02);
121
+ return [
122
+ { what: "muted on a card", fg: D.muted, bg: D.card, need: 4.5 },
123
+ { what: "muted on the page background", fg: D.muted, bg: D.bg, need: 4.5 },
124
+ { what: "muted on a --thead band (the tint itself, no longer the header text)", fg: D.muted, bg: dthead, need: 4.5 },
125
+ { what: "muted on the sidebar", fg: D.muted, bg: D.sidebar, need: 4.5 },
126
+ { what: "text on a card", fg: D.text, bg: D.card, need: 4.5 },
127
+ { what: "text2 on a card", fg: D.text2, bg: D.card, need: 4.5 },
128
+ { what: "ok on its chip", fg: D.ok, bg: over("#41c795", D.card, 0.12), need: 4.5 },
129
+ { what: "warn on its chip", fg: D.warn, bg: over("#dcaf4e", D.card, 0.13), need: 4.5 },
130
+ { what: "err on its chip", fg: D.err, bg: over("#e57373", D.card, 0.12), need: 4.5 },
131
+ { what: "info on its chip", fg: D.info, bg: over("#7da6ec", D.card, 0.12), need: 4.5 },
132
+ { what: "accent-text on accent-soft over the sidebar", fg: D.accentText, bg: over(D.accent, D.sidebar, 0.12), need: 4.5 },
133
+ { what: "accent-text on accent-soft over a card", fg: D.accentText, bg: over(D.accent, D.card, 0.12), need: 4.5 },
134
+ ].map((p) => ({ ...p, ratio: Math.round(contrast(p.fg, p.bg) * 100) / 100, passes: contrast(p.fg, p.bg) >= p.need }));
135
+ };
136
+
137
+ if (process.argv[1]?.endsWith("theme-aa-ledger.ts")) {
138
+ const row = (r: { what: string; fg: string; bg: string; ratio: number; need: number; passes: boolean }) =>
139
+ `${r.passes ? "PASS" : "FAIL"} ${String(r.ratio).padStart(5)}:1 / ${r.need} ${r.fg} on ${r.bg} ${r.what}`;
140
+ console.log("LIGHT");
141
+ for (const r of measure()) console.log(" " + row(r));
142
+ console.log("\nDARK");
143
+ for (const r of DARK_PAIRS()) console.log(" " + row(r));
144
+ const f = measure().filter((r) => !r.passes).length;
145
+ console.log(`\n${f} of ${LIGHT_PAIRS.length} light pairs below AA; ${DARK_PAIRS().filter((r) => !r.passes).length} dark.`);
146
+ void LIGHT_THEME;
147
+ void DARK_THEME;
148
+ }