@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,76 @@
1
+ <!--
2
+ THE TEXT FIELD.
3
+
4
+ 40px (34px compact), 10px radius, a 1px border, 13.5px/500 - and the label
5
+ OUTSIDE the box as a small muted word, not Vuetify's outlined field with a
6
+ label that floats up through the border. A native `<input>`, so the browser
7
+ keeps the keyboard, autofill and accessibility for free.
8
+ -->
9
+ <template>
10
+ <label
11
+ v-bind="rootAttrs"
12
+ class="app-field"
13
+ :class="[{ 'app-field--compact': compact, 'app-field--search': search }, klass]"
14
+ >
15
+ <v-icon
16
+ v-if="icon || search"
17
+ class="app-field__icon"
18
+ :icon="icon || 'mdi-magnify'"
19
+ size="18"
20
+ />
21
+ <span v-if="label" class="app-field__label">{{ label }}</span>
22
+ <input
23
+ class="app-field__input"
24
+ v-bind="inputAttrs"
25
+ :type="type"
26
+ :value="model"
27
+ :placeholder="placeholder"
28
+ :disabled="disabled"
29
+ :readonly="readonly"
30
+ @input="model = ($event.target as HTMLInputElement).value"
31
+ />
32
+ <!-- A trailing control INSIDE the box: the password reveal on the HID PIN,
33
+ SIP password and reader password fields. The handoff draws no password
34
+ field at all, so the eye is this product's, not the design's - but it
35
+ belongs in one place rather than being re-hacked in three screens. -->
36
+ <slot name="trailing" />
37
+ </label>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { computed, useAttrs } from "vue";
42
+
43
+ /**
44
+ * The root is a `<label>`, so an `aria-label`, `inputmode` or `autocomplete`
45
+ * left to fall through would land on the LABEL and never reach the input -
46
+ * silently costing the field its accessible name. `class` and `style` still
47
+ * belong on the root (that is what every existing caller styles), so the two
48
+ * are split rather than moved wholesale.
49
+ */
50
+ defineOptions({ inheritAttrs: false });
51
+
52
+ const attrs = useAttrs();
53
+ const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }));
54
+ const inputAttrs = computed(() => {
55
+ const { class: _c, style: _s, ...rest } = attrs;
56
+ return rest;
57
+ });
58
+
59
+ const model = defineModel({ type: String, default: "" });
60
+
61
+ defineProps({
62
+ label: { type: String, default: "" },
63
+ placeholder: { type: String, default: "" },
64
+ icon: { type: String, default: "" },
65
+ type: { type: String, default: "text" },
66
+ /** The design's search field: a magnifier and `flex:1` in a filter row. */
67
+ search: { type: Boolean, default: false },
68
+ compact: { type: Boolean, default: false },
69
+ disabled: { type: Boolean, default: false },
70
+ /** A field that prints a derived value - a preview, a computed reference -
71
+ and is not typed into. Kept distinct from `disabled`, which also greys
72
+ the box and takes it out of the tab order. */
73
+ readonly: { type: Boolean, default: false },
74
+ klass: { type: String, default: "" },
75
+ });
76
+ </script>
@@ -0,0 +1,65 @@
1
+ <!--
2
+ THE KPI TILE.
3
+
4
+ The handoff's tile is three rows: the soft icon square AND the uppercase
5
+ label together on the first, the big tabular number with its percentage pill
6
+ BESIDE it on the second, the sub-line on the third. The product had the icon
7
+ alone on a row of its own, the label under it, and the pill stranded at the
8
+ bottom next to the sub-line - the same figures in a different shape.
9
+
10
+ It draws what it is given and nothing else: no fetch, no formatting, no
11
+ threshold logic. `#action` is the far end of the first row (a filter menu, a
12
+ "View All"); `#default`, when given, replaces the figure rows entirely for
13
+ the tiles that list items instead of counting them.
14
+ -->
15
+ <template>
16
+ <AppCard class="app-kpi d-flex flex-column flex-grow-1">
17
+ <div class="app-kpi__head">
18
+ <span
19
+ v-if="icon"
20
+ class="app-kpi__icon"
21
+ :style="tint ? { backgroundColor: tint } : undefined"
22
+ >
23
+ <v-icon :icon="icon" :color="color || undefined" size="19" />
24
+ </span>
25
+
26
+ <span class="app-kpi__label">{{ label }}</span>
27
+
28
+ <span v-if="$slots.action" class="app-kpi__action">
29
+ <slot name="action" />
30
+ </span>
31
+ </div>
32
+
33
+ <slot>
34
+ <div class="app-kpi__figure">
35
+ <span class="app-kpi__value">{{ value }}</span>
36
+
37
+ <span
38
+ v-if="chip"
39
+ class="app-kpi__chip"
40
+ :class="{ 'app-kpi__chip--down': down }"
41
+ >
42
+ {{ chip }}
43
+ </span>
44
+ </div>
45
+
46
+ <div v-if="sub" class="app-kpi__sub">{{ sub }}</div>
47
+ </slot>
48
+ </AppCard>
49
+ </template>
50
+
51
+ <script setup lang="ts">
52
+ defineProps({
53
+ icon: { type: String, default: "" },
54
+ /** The icon square's soft fill - a token-derived colour from the caller. */
55
+ tint: { type: String, default: "" },
56
+ color: { type: String, default: "" },
57
+ label: { type: String, default: "" },
58
+ value: { type: [String, Number], default: "" },
59
+ /** The percentage pill's text, already formatted by the caller. */
60
+ chip: { type: String, default: "" },
61
+ /** Draws the pill in the error tone rather than the success one. */
62
+ down: { type: Boolean, default: false },
63
+ sub: { type: String, default: "" },
64
+ });
65
+ </script>
@@ -0,0 +1,38 @@
1
+ <!--
2
+ THE SEGMENTED CONTROL - `Today | Week | Month`.
3
+
4
+ A tray of plain `<button>`s, not a `v-btn-toggle`. The toggle brings its own
5
+ height scale, dividers, ripple and overlay layer, and its selected state is
6
+ drawn from `on-surface` - which is why the product's selected segment reads
7
+ grey where the design fills it with the accent.
8
+
9
+ Selection only: it writes the same `v-model` the toggle wrote, and the value
10
+ set is whatever the caller passes. No behaviour is added here.
11
+ -->
12
+ <template>
13
+ <div class="app-seg" role="group">
14
+ <button
15
+ v-for="option in options"
16
+ :key="String(option.value)"
17
+ type="button"
18
+ class="app-seg__item"
19
+ :class="{ 'app-seg__item--on': model === option.value }"
20
+ :aria-pressed="model === option.value"
21
+ @click="model = option.value"
22
+ >
23
+ {{ option.label }}
24
+ </button>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ defineProps({
30
+ /** `{ label, value }` - the same option list the toggle was given. */
31
+ options: {
32
+ type: Array as PropType<Array<{ label: string; value: any }>>,
33
+ default: () => [],
34
+ },
35
+ });
36
+
37
+ const model = defineModel<any>({ default: "" });
38
+ </script>
@@ -0,0 +1,247 @@
1
+ <!--
2
+ THE SELECT.
3
+
4
+ The handoff draws it as the text field with the filter's name in muted 12px
5
+ on the left, the chosen value next to it and a chevron on the right. Both
6
+ single and multiple values use the same themed popover, so the option list
7
+ does not fall back to the browser/OS styling.
8
+
9
+ `multiple` is the ONE case where native is the wrong answer. A native
10
+ `<select multiple>` is not a dropdown at all - it is an always-open scrolling
11
+ LIST BOX - so adopting it would turn the daily filter bars into list boxes
12
+ and change how the staff work. So multi opens a POPOVER in the same field
13
+ chrome, with the chosen values as the design's soft removable pills.
14
+
15
+ A single-value consumer that does not use `return-object` still emits the
16
+ same string that the old native `<select>` emitted.
17
+ -->
18
+ <template>
19
+ <v-menu
20
+ v-if="!multiple"
21
+ :close-on-content-click="true"
22
+ :disabled="disabled"
23
+ location="bottom start"
24
+ offset="4"
25
+ >
26
+ <template #activator="{ props: activator }">
27
+ <div
28
+ v-bind="{ ...activator, ...$attrs }"
29
+ class="app-field app-select app-select--single"
30
+ :class="{ 'app-field--compact': compact, 'app-select--disabled': disabled }"
31
+ role="button"
32
+ :aria-disabled="disabled"
33
+ :tabindex="disabled ? -1 : 0"
34
+ >
35
+ <span v-if="label" class="app-field__label">{{ label }}</span>
36
+ <span
37
+ class="app-select__single-value"
38
+ :class="{ 'app-select__placeholder': !hasSingleValue }"
39
+ >
40
+ {{ singleDisplay }}
41
+ </span>
42
+ <button
43
+ v-if="clearable && hasSingleValue"
44
+ type="button"
45
+ class="app-select__clear"
46
+ aria-label="Clear"
47
+ @click.stop="clearSingle"
48
+ >
49
+ <v-icon icon="mdi-close-circle" size="15" />
50
+ </button>
51
+ <v-icon class="app-field__icon" icon="mdi-chevron-down" size="17" />
52
+ </div>
53
+ </template>
54
+
55
+ <div class="app-select__menu" role="listbox">
56
+ <button
57
+ v-for="opt in normalized"
58
+ :key="String(opt.value)"
59
+ type="button"
60
+ class="app-select__option"
61
+ :class="{ 'app-select__option--selected': isSingleOn(opt) }"
62
+ role="option"
63
+ :aria-selected="isSingleOn(opt)"
64
+ @click="selectSingle(opt)"
65
+ >
66
+ <span class="app-select__option-text">{{ opt.title }}</span>
67
+ <v-icon v-if="isSingleOn(opt)" icon="mdi-check" size="17" />
68
+ </button>
69
+ </div>
70
+ </v-menu>
71
+
72
+ <v-menu
73
+ v-else
74
+ :close-on-content-click="false"
75
+ :disabled="disabled"
76
+ location="bottom start"
77
+ offset="4"
78
+ >
79
+ <template #activator="{ props: activator }">
80
+ <div
81
+ v-bind="{ ...activator, ...$attrs }"
82
+ class="app-field app-select"
83
+ :class="{ 'app-field--compact': compact, 'app-select--disabled': disabled }"
84
+ role="button"
85
+ :aria-disabled="disabled"
86
+ :tabindex="disabled ? -1 : 0"
87
+ >
88
+ <span v-if="label" class="app-field__label">{{ label }}</span>
89
+ <span class="app-select__values">
90
+ <StatusChip
91
+ v-for="opt in selectedOptions"
92
+ :key="String(opt.value)"
93
+ tone="neutral"
94
+ pill
95
+ :dot="false"
96
+ >
97
+ {{ opt.title }}
98
+ <button
99
+ type="button"
100
+ class="app-select__x"
101
+ :aria-label="`Remove ${opt.title}`"
102
+ @click.stop="toggle(opt)"
103
+ >
104
+ <v-icon icon="mdi-close" size="12" />
105
+ </button>
106
+ </StatusChip>
107
+ <span v-if="!selectedOptions.length" class="app-select__placeholder">
108
+ {{ placeholder }}
109
+ </span>
110
+ </span>
111
+ <button
112
+ v-if="clearable && selectedOptions.length"
113
+ type="button"
114
+ class="app-select__clear"
115
+ aria-label="Clear"
116
+ @click.stop="clear"
117
+ >
118
+ <v-icon icon="mdi-close-circle" size="15" />
119
+ </button>
120
+ <v-icon class="app-field__icon" icon="mdi-chevron-down" size="17" />
121
+ </div>
122
+ </template>
123
+
124
+ <div class="app-select__menu">
125
+ <button
126
+ v-for="opt in normalized"
127
+ :key="String(opt.value)"
128
+ type="button"
129
+ class="app-select__option"
130
+ :aria-pressed="isOn(opt)"
131
+ @click="toggle(opt)"
132
+ >
133
+ <v-icon
134
+ class="app-select__box"
135
+ :class="{ 'app-select__box--on': isOn(opt) }"
136
+ :icon="isOn(opt) ? 'mdi-checkbox-marked' : 'mdi-checkbox-blank-outline'"
137
+ size="17"
138
+ />
139
+ <span class="app-select__option-text">{{ opt.title }}</span>
140
+ </button>
141
+ </div>
142
+ </v-menu>
143
+ </template>
144
+
145
+ <script setup lang="ts">
146
+ /* `v-menu` has no single root node, so attributes and classes have to be put
147
+ on each activator by hand. */
148
+ defineOptions({ inheritAttrs: false });
149
+
150
+ /*
151
+ * One model for both shapes: a string when single, an array when multiple.
152
+ * The default stays `""` so an existing single consumer that never sets its
153
+ * v-model starts on the same empty string it starts on today.
154
+ */
155
+ const model = defineModel<any>({ default: "" });
156
+
157
+ const props = defineProps({
158
+ label: { type: String, default: "" },
159
+ /** Strings, or the `{ title, value }` shape `v-select` already takes. */
160
+ items: { type: Array as PropType<any[]>, default: () => [] },
161
+ itemTitle: { type: String, default: "title" },
162
+ itemValue: { type: String, default: "value" },
163
+ compact: { type: Boolean, default: false },
164
+ disabled: { type: Boolean, default: false },
165
+ /** Popover multi-select with removable pills. Never a native list box. */
166
+ multiple: { type: Boolean, default: false },
167
+ /** Emit the source item rather than its `itemValue`. */
168
+ returnObject: { type: Boolean, default: false },
169
+ clearable: { type: Boolean, default: false },
170
+ placeholder: { type: String, default: "" },
171
+ });
172
+
173
+ const normalized = computed(() =>
174
+ props.items.map((item: any) =>
175
+ typeof item === "object" && item !== null
176
+ ? { title: item[props.itemTitle], value: item[props.itemValue], raw: item }
177
+ : { title: String(item), value: item, raw: item }
178
+ )
179
+ );
180
+
181
+ /* A model entry is either the plain value or - under `return-object` - the
182
+ source item, so everything compares on this one key. */
183
+ const keyOf = (v: any) =>
184
+ String(v !== null && typeof v === "object" ? v[props.itemValue] : v);
185
+
186
+ /* ---- single ---------------------------------------------------------- */
187
+
188
+ const hasSingleValue = computed(
189
+ () => model.value !== "" && model.value !== null && model.value !== undefined
190
+ );
191
+
192
+ const selectedSingleOption = computed(() =>
193
+ normalized.value.find((opt) => String(opt.value) === keyOf(model.value))
194
+ );
195
+
196
+ const singleDisplay = computed(() =>
197
+ hasSingleValue.value
198
+ ? selectedSingleOption.value?.title ?? keyOf(model.value)
199
+ : props.placeholder
200
+ );
201
+
202
+ const isSingleOn = (opt: any) =>
203
+ hasSingleValue.value && String(opt.value) === keyOf(model.value);
204
+
205
+ function selectSingle(opt: any) {
206
+ if (props.disabled) return;
207
+ model.value = props.returnObject ? opt.raw : String(opt.value);
208
+ }
209
+
210
+ function clearSingle() {
211
+ if (props.disabled) return;
212
+ model.value = "";
213
+ }
214
+
215
+ /* ---- multiple -------------------------------------------------------- */
216
+
217
+ const selected = computed<any[]>(() =>
218
+ Array.isArray(model.value) ? model.value : []
219
+ );
220
+
221
+ const selectedKeys = computed(() => new Set(selected.value.map(keyOf)));
222
+
223
+ const isOn = (opt: any) => selectedKeys.value.has(String(opt.value));
224
+
225
+ /* Fall back to the raw key when a chosen value is not in `items` - an async
226
+ item list can arrive after the model does, and the chip must still print. */
227
+ const selectedOptions = computed(() =>
228
+ selected.value.map(
229
+ (v) =>
230
+ normalized.value.find((o) => String(o.value) === keyOf(v)) ?? {
231
+ title: keyOf(v),
232
+ value: keyOf(v),
233
+ }
234
+ )
235
+ );
236
+
237
+ function toggle(opt: any) {
238
+ if (props.disabled) return;
239
+ model.value = isOn(opt)
240
+ ? selected.value.filter((v) => keyOf(v) !== String(opt.value))
241
+ : [...selected.value, props.returnObject ? opt.raw : opt.value];
242
+ }
243
+
244
+ function clear() {
245
+ model.value = [];
246
+ }
247
+ </script>
@@ -1,28 +1,27 @@
1
1
  <template>
2
2
  <v-row no-gutters align="center" justify="center">
3
3
  <v-col cols="12">
4
- <v-card class="mx-auto">
5
- <v-toolbar
6
- density="compact"
7
- color="grey-lighten-4"
8
- class="d-flex align-center px-4 py-3"
9
- >
10
- <v-btn icon variant="plain" @click="back()" class="mr-2">
11
- <v-icon>mdi-chevron-left</v-icon>
12
- </v-btn>
13
- <span class="text-h6 font-weight-bold">Area Checklist</span>
14
- </v-toolbar>
15
-
16
- <v-divider />
17
-
18
- <v-card-text class="pa-0">
4
+ <!-- The screen's name and its Back move out of the `grey-lighten-4` bar -
5
+ a slab that stayed white in dark mode - onto the design's title row.
6
+ All five service apps mount this component bare, so the layer is the
7
+ only place the page name can come from. -->
8
+ <PageHeader title="Area Checklist">
9
+ <template #actions>
10
+ <AppButton variant="ghost" icon="mdi-chevron-left" @click="back()">
11
+ Back
12
+ </AppButton>
13
+ </template>
14
+ </PageHeader>
15
+
16
+ <AppCard>
17
+ <div class="pa-0">
19
18
  <v-row no-gutters>
20
19
  <v-col
21
20
  cols="12"
22
21
  md="6"
23
22
  lg="5"
24
23
  xl="4"
25
- style="border-right: 1px solid rgba(0, 0, 0, 0.12)"
24
+ class="checklist-detail__split"
26
25
  >
27
26
  <v-row no-gutters class="pa-6">
28
27
  <v-col cols="12" class="mb-4">
@@ -47,11 +46,11 @@
47
46
 
48
47
  <v-col cols="12" class="mb-3">
49
48
  <div class="text-body-2 mb-1">Status:</div>
50
- <v-chip
51
- class="text-capitalize text-body-2 font-weight-medium"
52
- >
53
- {{ status || "N/A" }}
54
- </v-chip>
49
+ <StatusChip
50
+ class="text-capitalize"
51
+ :status="status"
52
+ :label="status || 'N/A'"
53
+ />
55
54
  </v-col>
56
55
  </v-row>
57
56
  </v-col>
@@ -119,15 +118,15 @@
119
118
  class="text-subtitle-2 font-weight-bold px-0"
120
119
  >
121
120
  Set {{ checklistSet.set }}
122
- <v-chip
121
+ <StatusChip
123
122
  v-if="checklistSet.isScheduleTask"
124
- size="x-small"
125
- color="info"
126
- variant="tonal"
127
- class="text-none ml-2"
123
+ tone="info"
124
+ :dot="false"
125
+ pill
126
+ class="ml-2"
128
127
  >
129
128
  Schedule Task
130
- </v-chip>
129
+ </StatusChip>
131
130
  </v-list-subheader>
132
131
 
133
132
  <v-list-item
@@ -161,22 +160,14 @@
161
160
  </div>
162
161
  </v-col>
163
162
  <v-col cols="12" sm="2" class="text-right">
164
- <v-chip
165
- :color="
166
- unit.status?.toLowerCase() === 'completed'
167
- ? 'success'
168
- : 'grey'
169
- "
170
- :variant="
171
- unit.status?.toLowerCase() === 'completed'
172
- ? 'flat'
173
- : 'outlined'
174
- "
175
- size="small"
163
+ <!-- Same word decides the colour, only now through the
164
+ shared status map, so "Completed" here is the same
165
+ green as on every other screen. -->
166
+ <StatusChip
176
167
  class="text-capitalize"
177
- >
178
- {{ unit.status || "Open" }}
179
- </v-chip>
168
+ :status="unit.status || 'Open'"
169
+ :label="unit.status || 'Open'"
170
+ />
180
171
  </v-col>
181
172
  </v-row>
182
173
  </v-list-item>
@@ -191,8 +182,8 @@
191
182
  </v-row>
192
183
  </v-col>
193
184
  </v-row>
194
- </v-card-text>
195
- </v-card>
185
+ </div>
186
+ </AppCard>
196
187
  </v-col>
197
188
  </v-row>
198
189
  </template>
@@ -242,3 +233,19 @@ const timeOut = ref<string>("N/A");
242
233
  const status = ref<string>("N/A");
243
234
  const signatureUrl = ref<string>("");
244
235
  </script>
236
+
237
+ <style scoped>
238
+ /* Was an inline `rgba(0,0,0,.12)`, which is the same near-black rule in dark
239
+ mode. The token follows the theme. At `md` and below the two halves stack, so
240
+ the rule moves under the first one. */
241
+ .checklist-detail__split {
242
+ border-bottom: 1px solid var(--border);
243
+ }
244
+
245
+ @media (min-width: 960px) {
246
+ .checklist-detail__split {
247
+ border-bottom: none;
248
+ border-right: 1px solid var(--border);
249
+ }
250
+ }
251
+ </style>
@@ -32,11 +32,12 @@
32
32
 
33
33
  <template #extension>
34
34
  <v-row no-gutters class="w-100 d-flex flex-column">
35
- <v-card
36
- class="w-100 px-3 d-flex align-center ga-5 py-2"
37
- flat
38
- :height="60"
39
- >
35
+ <!--
36
+ A bare `v-card` here paints Vuetify's own `surface`, a shade off
37
+ the table card it sits inside. A plain row on the card's own
38
+ background, with both filters on the design's field.
39
+ -->
40
+ <div class="w-100 filter-bar d-flex align-center ga-5">
40
41
  <v-text-field
41
42
  v-model="searchInput"
42
43
  density="compact"
@@ -45,6 +46,7 @@
45
46
  max-width="300"
46
47
  append-inner-icon="mdi-magnify"
47
48
  hide-details
49
+ class="filter-field"
48
50
  />
49
51
  <v-select
50
52
  v-model="status"
@@ -55,8 +57,9 @@
55
57
  clearable
56
58
  hide-details
57
59
  max-width="200"
60
+ class="filter-field"
58
61
  />
59
- </v-card>
62
+ </div>
60
63
  </v-row>
61
64
  </template>
62
65
 
@@ -80,7 +83,11 @@
80
83
  </div>
81
84
  </template>
82
85
  <template #item.status="{ value }">
83
- <v-chip class="text-capitalize">{{ value || "No Status" }}</v-chip>
86
+ <StatusChip
87
+ class="text-capitalize"
88
+ :status="value"
89
+ :label="value || 'No Status'"
90
+ />
84
91
  </template>
85
92
  </TableMain></v-col
86
93
  >