@7365admin1/layer-common 3.2.1-staging.75 → 3.2.2-staging.100

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 (87) hide show
  1. package/.changeset/notification-preferences.md +17 -0
  2. package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
  3. package/CHANGELOG.md +6 -0
  4. package/assets/css/primitives.css +680 -0
  5. package/assets/css/screens.css +541 -0
  6. package/assets/css/shell.css +258 -0
  7. package/assets/css/tokens.css +220 -0
  8. package/components/AccessCardQrTagging.vue +1 -1
  9. package/components/AccessManagement.vue +53 -72
  10. package/components/AppButton.vue +52 -0
  11. package/components/AppCard.vue +20 -0
  12. package/components/AppDateField.vue +35 -0
  13. package/components/AppField.vue +46 -0
  14. package/components/AppKpiTile.vue +65 -0
  15. package/components/AppSegmented.vue +38 -0
  16. package/components/AppSelect.vue +203 -0
  17. package/components/AreaChecklistHistoryMain.vue +6 -2
  18. package/components/AreaMain.vue +9 -17
  19. package/components/AttendanceMain.vue +4 -3
  20. package/components/Avatar/Main.vue +54 -2
  21. package/components/BillingMain.vue +26 -30
  22. package/components/BuildingManagement/buildings.vue +27 -38
  23. package/components/BuildingManagement/units.vue +26 -42
  24. package/components/BulletinBoardManagement.vue +23 -10
  25. package/components/CardToolbar.vue +37 -0
  26. package/components/CleaningScheduleMain.vue +29 -46
  27. package/components/ClientMain.vue +90 -78
  28. package/components/DashboardEmptyState.vue +11 -1
  29. package/components/DashboardMain.vue +285 -207
  30. package/components/DocumentManagement.vue +129 -93
  31. package/components/EntryPassMain.vue +49 -27
  32. package/components/EquipmentItemMain.vue +4 -6
  33. package/components/EquipmentManagementMain.vue +9 -7
  34. package/components/FeedbackMain.vue +35 -49
  35. package/components/FormDialog.vue +60 -21
  36. package/components/HidIntercomManagement.vue +40 -24
  37. package/components/HidQrCodeConfiguration.vue +24 -2
  38. package/components/HidUserEnrollment.vue +68 -31
  39. package/components/InvitationMain.vue +46 -61
  40. package/components/Layout/Header.vue +289 -37
  41. package/components/Layout/NavigationDrawer.vue +299 -30
  42. package/components/ManageChecklistMain.vue +17 -17
  43. package/components/MemberMain.vue +79 -60
  44. package/components/MyAttendanceMain.vue +3 -5
  45. package/components/Nfc/NFCPatrolRouteMain.vue +64 -96
  46. package/components/NotificationSettings.vue +361 -0
  47. package/components/OnlineFormsConfiguration.vue +1 -1
  48. package/components/PageHeader.vue +43 -0
  49. package/components/PassInformation.vue +104 -45
  50. package/components/RolePermissionMain.vue +184 -91
  51. package/components/ScanVisitorQRCode.vue +35 -7
  52. package/components/ScheduleAreaMain.vue +15 -49
  53. package/components/ScheduleTaskMain.vue +23 -26
  54. package/components/ScheduleTastTicketMain.vue +18 -43
  55. package/components/ServiceProviderMain.vue +113 -168
  56. package/components/SiteSettings.vue +21 -8
  57. package/components/SpecificAttr.vue +10 -2
  58. package/components/StatusChip.vue +93 -0
  59. package/components/SwitchContext.vue +33 -1
  60. package/components/TableHygiene.vue +29 -46
  61. package/components/TableList.vue +1 -1
  62. package/components/TableListSecondary.vue +1 -1
  63. package/components/TableMain.vue +161 -77
  64. package/components/TableV3.vue +61 -66
  65. package/components/UnitMain.vue +9 -17
  66. package/components/VehicleManagement.vue +20 -9
  67. package/components/VisitorForm.vue +100 -7
  68. package/components/VisitorManagement.vue +219 -68
  69. package/components/WorkOrder/Main.vue +42 -51
  70. package/composables/useHidAmico.ts +26 -2
  71. package/composables/useNotificationPreference.ts +87 -0
  72. package/composables/useOrg.ts +1 -1
  73. package/composables/useRole.ts +22 -0
  74. package/composables/useSipWebPhone.ts +28 -3
  75. package/composables/useThemePreference.ts +63 -0
  76. package/composables/useVisitor.ts +6 -1
  77. package/nuxt.config.ts +31 -0
  78. package/package.json +3 -2
  79. package/pages/notification-settings.vue +19 -0
  80. package/plugins/vuetify.ts +32 -1
  81. package/types/org.d.ts +2 -0
  82. package/utils/nav-sections.test.ts +190 -0
  83. package/utils/nav-sections.ts +151 -0
  84. package/utils/status.test.ts +77 -0
  85. package/utils/status.ts +90 -0
  86. package/utils/theme.test.ts +325 -7
  87. package/utils/theme.ts +268 -137
@@ -203,44 +203,36 @@
203
203
 
204
204
  <section v-else class="unified-dashboard pb-8">
205
205
  <!-- ── Header ──────────────────────────────────────────────────────────── -->
206
- <div
207
- class="d-flex flex-column flex-md-row align-md-start justify-space-between ga-5 mb-6"
206
+ <PageHeader
207
+ :title="dashboardTitle"
208
+ :subtitle="currentSiteName || 'Selected site'"
209
+ subtitle-icon="mdi-map-marker"
208
210
  >
209
- <div>
210
- <h1 class="text-h5 font-weight-black text-high-emphasis mb-0">
211
- {{ dashboardTitle }}
212
- </h1>
213
- <div class="d-flex align-center text-medium-emphasis ga-1 mt-2 text-caption">
214
- <v-icon icon="mdi-map-marker" size="16" />
215
- <span>{{ currentSiteName || "Selected site" }}</span>
216
- </div>
217
- </div>
218
-
219
- <div
220
- v-if="showHeaderActions"
221
- class="d-flex flex-wrap align-center justify-md-end ga-3"
222
- >
223
- <v-btn
224
- variant="outlined"
225
- class="text-none font-weight-bold"
226
- prepend-icon="mdi-tune-variant"
211
+ <!--
212
+ The design's three actions: two 38px ghost buttons and the accent
213
+ segmented control. Same dialog, same export menu, same range model -
214
+ `Customize Dashboard` and `Export` shorten to the labels the prototype
215
+ prints, and the export menu keeps both of its formats.
216
+ -->
217
+ <template v-if="showHeaderActions" #actions>
218
+ <AppButton
219
+ variant="ghost"
220
+ icon="mdi-tune-variant"
227
221
  @click="openCustomizeDialog"
228
222
  >
229
- Customize Dashboard
230
- </v-btn>
223
+ Customize
224
+ </AppButton>
231
225
 
232
226
  <v-menu>
233
227
  <template #activator="{ props: menuProps }">
234
- <v-btn
228
+ <AppButton
235
229
  v-bind="menuProps"
236
- variant="outlined"
237
- class="text-none font-weight-bold"
238
- append-icon="mdi-chevron-down"
239
- :loading="isExporting"
230
+ variant="ghost"
231
+ icon="mdi-download-outline"
240
232
  :disabled="isExporting"
241
233
  >
242
234
  Export
243
- </v-btn>
235
+ </AppButton>
244
236
  </template>
245
237
 
246
238
  <v-list density="compact" border rounded="lg" elevation="3">
@@ -249,36 +241,9 @@
249
241
  </v-list>
250
242
  </v-menu>
251
243
 
252
- <v-btn-toggle
253
- v-model="selectedRange"
254
- mandatory
255
- density="comfortable"
256
- divided
257
- variant="outlined"
258
- class="bg-surface"
259
- style="border-radius: 8px"
260
- >
261
- <v-btn
262
- v-for="range in rangeOptions"
263
- :key="range.value"
264
- :value="range.value"
265
- class="text-none"
266
- size="small"
267
- :style="
268
- selectedRange === range.value
269
- ? {
270
- backgroundColor: 'rgba(var(--v-theme-on-surface), 0.12)',
271
- color: 'rgb(var(--v-theme-on-surface))',
272
- fontWeight: '600',
273
- }
274
- : { backgroundColor: 'transparent', color: 'rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))' }
275
- "
276
- >
277
- {{ range.label }}
278
- </v-btn>
279
- </v-btn-toggle>
280
- </div>
281
- </div>
244
+ <AppSegmented v-model="selectedRange" :options="rangeOptions" />
245
+ </template>
246
+ </PageHeader>
282
247
 
283
248
  <v-progress-linear
284
249
  v-if="finalLoading"
@@ -298,22 +263,24 @@
298
263
  lg="3"
299
264
  class="d-flex"
300
265
  >
301
- <v-card
302
- flat
303
- border
304
- rounded="lg"
305
- class="pa-4 flex-grow-1"
306
- style="min-height: 158px"
266
+ <!--
267
+ The same figures the tile has always shown, in the arrangement the
268
+ handoff draws them: icon and label on one row, the number with its
269
+ percentage pill beside it, the sub-line under both. The filter menu
270
+ and the facility "View All" keep their place at the end of the first
271
+ row and do exactly what they did.
272
+ -->
273
+ <AppKpiTile
274
+ :icon="card.icon"
275
+ :tint="card.tint"
276
+ :color="card.color"
277
+ :label="card.title"
278
+ :value="card.value"
279
+ :chip="kpiChipText(card)"
280
+ :down="Number(card.percentage) < 0"
281
+ :sub="card.meta"
307
282
  >
308
- <div class="d-flex align-start justify-space-between">
309
- <div
310
- class="d-inline-flex align-center justify-center rounded-lg"
311
- style="height: 28px; width: 28px; flex-shrink: 0"
312
- :style="{ backgroundColor: card.tint }"
313
- >
314
- <v-icon :icon="card.icon" :color="card.color" size="15" />
315
- </div>
316
-
283
+ <template v-if="card.filter || card.key === 'facilityBookings'" #action>
317
284
  <v-menu v-if="card.filter" v-model="filterMenus[card.key]">
318
285
  <template #activator="{ props: menuProps }">
319
286
  <v-btn
@@ -357,61 +324,24 @@
357
324
  >
358
325
  View All
359
326
  </button>
360
- </div>
327
+ </template>
361
328
 
362
- <div class="mt-6 w-100">
363
- <p
364
- class="text-caption font-weight-black text-uppercase text-medium-emphasis mb-3"
365
- >
366
- {{ card.title }}
367
- </p>
368
- <template
369
- v-if="
370
- card.key === 'supplyAlert' && Array.isArray(card.raw?.items)
371
- "
372
- >
373
- <div class="d-flex flex-column ga-2 mt-2">
374
- <div
375
- v-for="item in card.raw.items"
376
- :key="item._id || item.name"
377
- class="d-flex justify-space-between text-body-2 text-high-emphasis font-weight-medium"
378
- >
379
- <span>{{ item.name }}</span>
380
- <span class="text-medium-emphasis">Balance: {{ item.qty }}</span>
381
- </div>
382
- </div>
383
- </template>
384
- <template v-else>
385
- <div class="d-flex align-center mb-3" style="min-height: 31px">
386
- <span class="text-h5 font-weight-black text-high-emphasis">
387
- {{ card.value }}
388
- </span>
389
- </div>
390
- <div class="d-flex align-center flex-wrap ga-2">
391
- <span v-if="card.meta" class="text-caption text-medium-emphasis">{{
392
- card.meta
393
- }}</span>
394
- <v-chip
395
- :color="card.percentage >= 0 ? 'success' : 'error'"
396
- size="x-small"
397
- variant="flat"
398
- class="font-weight-bold"
399
- >
400
- <v-icon
401
- :icon="
402
- card.percentage >= 0
403
- ? 'mdi-arrow-up-right'
404
- : 'mdi-arrow-down-right'
405
- "
406
- size="13"
407
- start
408
- />
409
- {{ Math.abs(card.percentage) }}%
410
- </v-chip>
329
+ <!-- The supply tile lists items instead of counting one figure. -->
330
+ <template
331
+ v-if="card.key === 'supplyAlert' && Array.isArray(card.raw?.items)"
332
+ >
333
+ <div class="d-flex flex-column ga-2">
334
+ <div
335
+ v-for="item in card.raw.items"
336
+ :key="item._id || item.name"
337
+ class="d-flex justify-space-between text-body-2 text-high-emphasis font-weight-medium"
338
+ >
339
+ <span>{{ item.name }}</span>
340
+ <span class="text-medium-emphasis">Balance: {{ item.qty }}</span>
411
341
  </div>
412
- </template>
413
- </div>
414
- </v-card>
342
+ </div>
343
+ </template>
344
+ </AppKpiTile>
415
345
  </v-col>
416
346
  </v-row>
417
347
 
@@ -419,12 +349,10 @@
419
349
  <v-card
420
350
  v-if="isWidgetVisible('activity')"
421
351
  flat
422
- border
423
- rounded="lg"
424
- class="px-5 pt-4 pb-3 mt-3"
352
+ class="dashboard-card px-5 pt-4 pb-3 mt-3"
425
353
  >
426
354
  <div class="d-flex align-center justify-space-between">
427
- <h2 class="text-body-1 font-weight-black text-high-emphasis mb-0">
355
+ <h2 class="dashboard-card-title mb-0">
428
356
  {{ activityTitle }}
429
357
  </h2>
430
358
  </div>
@@ -556,9 +484,7 @@
556
484
  v-for="item in taskScheduleItems"
557
485
  :key="item.id"
558
486
  flat
559
- border
560
- rounded="lg"
561
- class="pa-3"
487
+ class="dashboard-row-card pa-3"
562
488
  >
563
489
  <div class="d-flex align-center flex-wrap ga-3">
564
490
  <div class="flex-grow-1" style="min-width: 0">
@@ -622,9 +548,7 @@
622
548
  v-for="item in staffStatusItems"
623
549
  :key="item.id"
624
550
  flat
625
- border
626
- rounded="lg"
627
- class="pa-3"
551
+ class="dashboard-row-card pa-3"
628
552
  >
629
553
  <div class="d-flex align-start ga-3">
630
554
  <AvatarMain :name="item.name" :size="32" />
@@ -663,7 +587,7 @@
663
587
  <div
664
588
  v-for="item in feedbackItems"
665
589
  :key="item.id"
666
- class="d-flex align-center justify-space-between"
590
+ class="d-flex align-center justify-space-between dashboard-feedback-row"
667
591
  style="
668
592
  padding: 16px 20px;
669
593
  border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
@@ -964,7 +888,7 @@
964
888
  <v-col v-if="isWidgetVisible('activePatrol')" cols="12" lg="6">
965
889
  <div class="dashboard-card dashboard-panel figma-panel">
966
890
  <div class="figma-panel-header">
967
- <h2>ACTIVE PATROL</h2>
891
+ <h2>Active Patrol</h2>
968
892
  <button
969
893
  class="figma-panel-action"
970
894
  type="button"
@@ -1044,7 +968,7 @@
1044
968
  <v-col v-if="isWidgetVisible('staffStatus')" cols="12" lg="6">
1045
969
  <div class="dashboard-card dashboard-panel figma-panel">
1046
970
  <div class="figma-panel-header">
1047
- <h2>STAFF STATUS</h2>
971
+ <h2>Staff Status</h2>
1048
972
  <button
1049
973
  class="figma-panel-action"
1050
974
  type="button"
@@ -1128,7 +1052,7 @@
1128
1052
  <div
1129
1053
  v-for="item in feedbackItems"
1130
1054
  :key="item.id"
1131
- class="d-flex align-center justify-space-between"
1055
+ class="d-flex align-center justify-space-between dashboard-feedback-row"
1132
1056
  style="
1133
1057
  padding: 16px 20px;
1134
1058
  border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
@@ -2501,6 +2425,17 @@ const visibleKpiCards = computed(() =>
2501
2425
  kpiCards.value.filter((card) => isWidgetVisible(card.key))
2502
2426
  );
2503
2427
 
2428
+ /**
2429
+ * The percentage pill's label. Same number the chip has always shown, written
2430
+ * the way the handoff prints it - `+0%` / `-6.4%`, the sign carrying what the
2431
+ * arrow icon used to. A card without a percentage gets no pill.
2432
+ */
2433
+ function kpiChipText(card: Record<string, any>): string {
2434
+ const value = Number(card?.percentage);
2435
+ if (!Number.isFinite(value)) return "";
2436
+ return `${value < 0 ? "-" : "+"}${Math.abs(value)}%`;
2437
+ }
2438
+
2504
2439
  // ─── Dashboard Title ──────────────────────────────────────────────────────────
2505
2440
 
2506
2441
  const dashboardTitle = computed(() => {
@@ -3250,31 +3185,37 @@ function getInitials(name?: string) {
3250
3185
  function getStatusPillStyle(status: any) {
3251
3186
  const tone = getStatusTone(status);
3252
3187
  const map: Record<string, { backgroundColor: string; color: string }> = {
3253
- success: { backgroundColor: "#72e3a1", color: "#0f5132" },
3254
- warning: { backgroundColor: "#fde68a", color: "#92400e" },
3255
- error: { backgroundColor: "#fecaca", color: "#b91c1c" },
3256
- neutral: { backgroundColor: "#e2e8f0", color: "#64748b" },
3188
+ success: { backgroundColor: "var(--ok-bg)", color: "var(--ok)" },
3189
+ warning: { backgroundColor: "var(--warn-bg)", color: "var(--warn)" },
3190
+ error: { backgroundColor: "var(--err-bg)", color: "var(--err)" },
3191
+ neutral: { backgroundColor: "var(--hover)", color: "var(--text2)" },
3257
3192
  };
3258
3193
  return map[tone] || map.neutral;
3259
3194
  }
3260
3195
 
3196
+ /**
3197
+ * The pills are drawn by inline style, so the literal colours lived here rather
3198
+ * than in the stylesheet - which is why they survived the dark-mode repair and
3199
+ * still measured 3.86:1. Same three cases, same words, the design's tones.
3200
+ */
3261
3201
  function getPatrolStatusStyle(status: any) {
3262
3202
  const s = String(status || "").toLowerCase();
3263
3203
  if (s === "completed" || s === "complete")
3264
- return { backgroundColor: "#72e3a1", color: "#0f5132" };
3204
+ return { backgroundColor: "var(--ok-bg)", color: "var(--ok)" };
3265
3205
  if (s === "on paused" || s === "paused")
3266
- return { backgroundColor: "#dcdcdc", color: "#64748b" };
3267
- return { backgroundColor: "#e2e8f0", color: "#64748b" };
3206
+ return { backgroundColor: "var(--hover)", color: "var(--text2)" };
3207
+ return { backgroundColor: "var(--hover)", color: "var(--text2)" };
3268
3208
  }
3269
3209
 
3270
3210
  function getStaffStatusPillStyle(status: any) {
3271
3211
  const s = String(status || "").toLowerCase();
3272
3212
  if (s === "on duty" || s === "on-duty")
3273
- return { backgroundColor: "#72e3a1", color: "#0f5132" };
3213
+ return { backgroundColor: "var(--ok-bg)", color: "var(--ok)" };
3274
3214
  if (s === "on break" || s === "on-break")
3275
- return { backgroundColor: "#fde68a", color: "#92400e" };
3276
- if (s === "absent") return { backgroundColor: "#fecaca", color: "#b91c1c" };
3277
- return { backgroundColor: "#e2e8f0", color: "#64748b" };
3215
+ return { backgroundColor: "var(--warn-bg)", color: "var(--warn)" };
3216
+ if (s === "absent")
3217
+ return { backgroundColor: "var(--err-bg)", color: "var(--err)" };
3218
+ return { backgroundColor: "var(--hover)", color: "var(--text2)" };
3278
3219
  }
3279
3220
 
3280
3221
  function getStatusProgressStyle(item: { percent: number; color: string }) {
@@ -4154,37 +4095,98 @@ function formatDashboardTime(value?: string) {
4154
4095
  text-transform: capitalize;
4155
4096
  }
4156
4097
 
4098
+ /* Was five pairs of literal pastels - the same pale green on the dark page,
4099
+ and none of them the tones the rest of the product uses. */
4157
4100
  .badge-success {
4158
- background-color: #d1fae5;
4159
- color: #065f46;
4101
+ background-color: var(--ok-bg);
4102
+ color: var(--ok);
4160
4103
  }
4161
4104
 
4162
4105
  .badge-info {
4163
- background-color: #e0f2fe;
4164
- color: #075985;
4106
+ background-color: var(--info-bg);
4107
+ color: var(--info);
4165
4108
  }
4166
4109
 
4167
4110
  .badge-danger,
4168
4111
  .badge-error {
4169
- background-color: #fee2e2;
4170
- color: #991b1b;
4112
+ background-color: var(--err-bg);
4113
+ color: var(--err);
4171
4114
  }
4172
4115
 
4173
4116
  .badge-warning {
4174
- background-color: #fef3c7;
4175
- color: #92400e;
4117
+ background-color: var(--warn-bg);
4118
+ color: var(--warn);
4176
4119
  }
4177
4120
 
4178
4121
  .badge-neutral {
4179
- background-color: #f1f5f9;
4180
- color: #475569;
4122
+ background-color: var(--hover);
4123
+ color: var(--text2);
4181
4124
  }
4182
4125
 
4126
+ /* ------------------------------------------------------------------ */
4127
+ /* PHASE 5 - the design's card, type and semantic tones. */
4128
+ /* */
4129
+ /* The dark-mode repair earlier today moved these surfaces off literal */
4130
+ /* white onto the Vuetify theme; this keeps that and takes the last */
4131
+ /* step, onto the design tokens themselves - so the dashboard's cards, */
4132
+ /* headings and pills are the same 14px card, the same Manrope and the */
4133
+ /* same four status tones as every other screen. */
4134
+ /* ------------------------------------------------------------------ */
4135
+
4183
4136
  .dashboard-card {
4184
- background: rgb(var(--v-theme-surface));
4185
- border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
4186
- border-radius: 8px;
4187
- box-shadow: 0 1px 2px rgba(15, 35, 54, 0.04);
4137
+ background: var(--card);
4138
+ border: 1px solid var(--border);
4139
+ border-radius: var(--r-card);
4140
+ box-shadow: var(--shadow);
4141
+ font-family: var(--font-sans);
4142
+ }
4143
+
4144
+ /* The KPI tile and the rows inside a panel are the same card, one step in. */
4145
+ .dashboard-kpi-card,
4146
+ .dashboard-row-card {
4147
+ background: var(--card);
4148
+ border: 1px solid var(--border);
4149
+ border-radius: var(--r-card);
4150
+ box-shadow: var(--shadow);
4151
+ font-family: var(--font-sans);
4152
+ }
4153
+
4154
+ .dashboard-title {
4155
+ font-family: var(--font-sans);
4156
+ font-size: var(--fs-h1);
4157
+ font-weight: var(--fw-h1);
4158
+ letter-spacing: var(--ls-h1);
4159
+ color: var(--text);
4160
+ }
4161
+
4162
+ .dashboard-card-title {
4163
+ font-family: var(--font-sans);
4164
+ font-size: var(--fs-card-title);
4165
+ font-weight: var(--fw-card-title);
4166
+ letter-spacing: 0;
4167
+ color: var(--text);
4168
+ }
4169
+
4170
+ /* 11px/800 uppercase over a 30px/800 number, tabular so the columns of
4171
+ figures line up - the design's KPI, and the same header type the table
4172
+ card already uses. */
4173
+ .dashboard-kpi-label {
4174
+ font-family: var(--font-sans);
4175
+ font-size: var(--fs-table-head);
4176
+ font-weight: var(--fw-table-head);
4177
+ letter-spacing: var(--ls-table-head);
4178
+ text-transform: uppercase;
4179
+ color: var(--muted);
4180
+ }
4181
+
4182
+ .dashboard-kpi-value {
4183
+ font-family: var(--font-sans);
4184
+ font-size: var(--fs-kpi);
4185
+ font-weight: var(--fw-kpi);
4186
+ letter-spacing: var(--ls-kpi);
4187
+ line-height: 1.1;
4188
+ color: var(--text);
4189
+ font-variant-numeric: tabular-nums;
4188
4190
  }
4189
4191
 
4190
4192
  .dashboard-section-grid {
@@ -4219,14 +4221,17 @@ function formatDashboardTime(value?: string) {
4219
4221
  min-height: 18px;
4220
4222
  }
4221
4223
 
4224
+ /* The panel heading is the design's card title: 15.5px/800, sentence case. It
4225
+ was a 12px/900 uppercase label from the earlier Figma. */
4222
4226
  .figma-panel-header h2 {
4223
- color: rgb(var(--v-theme-on-surface));
4224
- font-size: 12px;
4225
- font-weight: 900;
4227
+ color: var(--text);
4228
+ font-family: var(--font-sans);
4229
+ font-size: var(--fs-card-title);
4230
+ font-weight: var(--fw-card-title);
4226
4231
  letter-spacing: 0;
4227
4232
  line-height: 1.2;
4228
4233
  margin: 0;
4229
- text-transform: uppercase;
4234
+ text-transform: none;
4230
4235
  }
4231
4236
 
4232
4237
  .figma-panel-action,
@@ -4373,30 +4378,30 @@ function formatDashboardTime(value?: string) {
4373
4378
 
4374
4379
  .figma-event-dot-info,
4375
4380
  .figma-event-dot-neutral {
4376
- background: #c9e6ff;
4377
- border: 2px solid #77bef9;
4381
+ background: var(--info-bg);
4382
+ border: 2px solid var(--info);
4378
4383
  }
4379
4384
 
4380
4385
  .figma-event-dot-success {
4381
- background: #2fb84d;
4382
- border: 2px solid #c9efd4;
4386
+ background: var(--ok-bg);
4387
+ border: 2px solid var(--ok);
4383
4388
  }
4384
4389
 
4385
4390
  .figma-event-dot-danger {
4386
- background: #ffd9d6;
4387
- border: 2px solid #f9a6a0;
4391
+ background: var(--err-bg);
4392
+ border: 2px solid var(--err);
4388
4393
  }
4389
4394
 
4390
4395
  .figma-event-dot-warning {
4391
- background: #ffe9c7;
4392
- border: 2px solid #f9c97c;
4396
+ background: var(--warn-bg);
4397
+ border: 2px solid var(--warn);
4393
4398
  }
4394
4399
 
4395
4400
  .active-patrol-row {
4396
4401
  align-items: center;
4397
- background-color: rgb(var(--v-theme-surface-bright));
4398
- border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
4399
- border-radius: 8px;
4402
+ background-color: var(--card);
4403
+ border: 1px solid var(--border);
4404
+ border-radius: var(--r-inner-lg);
4400
4405
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
4401
4406
  display: grid;
4402
4407
  gap: 10px;
@@ -4449,6 +4454,21 @@ function formatDashboardTime(value?: string) {
4449
4454
  padding: 2px 8px 2px 3px;
4450
4455
  }
4451
4456
 
4457
+ /**
4458
+ * Carried from Phase 1: `AvatarMain` colours each person from their NAME, and
4459
+ * writes WHITE initials on whatever tint it picks - 2.34:1 on this dashboard.
4460
+ * That is right in a members list, where the colour tells two people apart,
4461
+ * and wrong on a dashboard, where the same face appears in four panels.
4462
+ *
4463
+ * The component is left alone - Phase 2 took exactly this route for the top
4464
+ * bar's own avatar - and the dashboard paints every avatar it draws with the
4465
+ * design's accent pair instead. One rule, all seven call sites.
4466
+ */
4467
+ .unified-dashboard :deep(.v-avatar) {
4468
+ background: var(--accent-soft) !important;
4469
+ color: var(--accent-text) !important;
4470
+ }
4471
+
4452
4472
  .active-patrol-assignee span {
4453
4473
  color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
4454
4474
  font-size: 11px;
@@ -4503,11 +4523,11 @@ function formatDashboardTime(value?: string) {
4503
4523
  }
4504
4524
 
4505
4525
  .active-patrol-tooltip::after {
4506
- background: #0f2638;
4507
- border-radius: 6px;
4526
+ background: var(--text);
4527
+ border-radius: var(--r-inner-sm);
4508
4528
  bottom: calc(100% + 8px);
4509
- box-shadow: 0 8px 24px rgba(15, 38, 56, 0.18);
4510
- color: #ffffff;
4529
+ box-shadow: var(--shadow-menu);
4530
+ color: var(--card);
4511
4531
  content: attr(data-tooltip);
4512
4532
  font-size: 11px;
4513
4533
  font-weight: 600;
@@ -4537,6 +4557,28 @@ function formatDashboardTime(value?: string) {
4537
4557
  }
4538
4558
  }
4539
4559
 
4560
+ /**
4561
+ * Carried from Phase 1: the dashboard scrolled sideways at 360. This row is a
4562
+ * four-column grid whose last two columns are `max-content`, so on a phone the
4563
+ * assignee pill, the status pill and the action button cannot fit beside a
4564
+ * 120px title and the row pushes ~39px past the viewport.
4565
+ *
4566
+ * Below 600 the four columns become one. Nothing is hidden, dropped or
4567
+ * reordered - the same four blocks, stacked in the same order.
4568
+ */
4569
+ @media (max-width: 599.98px) {
4570
+ .active-patrol-row {
4571
+ grid-template-columns: minmax(0, 1fr);
4572
+ }
4573
+
4574
+ .active-patrol-action,
4575
+ .active-patrol-statuses,
4576
+ .active-patrol-assignees {
4577
+ justify-self: start;
4578
+ text-align: left;
4579
+ }
4580
+ }
4581
+
4540
4582
  .figma-icon-badge {
4541
4583
  align-items: center;
4542
4584
  border-radius: 50%;
@@ -4548,28 +4590,28 @@ function formatDashboardTime(value?: string) {
4548
4590
  }
4549
4591
 
4550
4592
  .figma-icon-badge-danger {
4551
- background: #ffe0de;
4552
- color: #d72f2f;
4593
+ background: var(--err-bg);
4594
+ color: var(--err);
4553
4595
  }
4554
4596
 
4555
4597
  .figma-icon-badge-info {
4556
- background: #e8e2ff;
4557
- color: #6d4bd8;
4598
+ background: var(--info-bg);
4599
+ color: var(--info);
4558
4600
  }
4559
4601
 
4560
4602
  .figma-icon-badge-success {
4561
- background: #ddf7e6;
4562
- color: #229852;
4603
+ background: var(--ok-bg);
4604
+ color: var(--ok);
4563
4605
  }
4564
4606
 
4565
4607
  .figma-icon-badge-warning {
4566
- background: #ffeed4;
4567
- color: #c57916;
4608
+ background: var(--warn-bg);
4609
+ color: var(--warn);
4568
4610
  }
4569
4611
 
4570
4612
  .figma-icon-badge-neutral {
4571
- background: #edf2f6;
4572
- color: #607386;
4613
+ background: var(--hover);
4614
+ color: var(--text2);
4573
4615
  }
4574
4616
 
4575
4617
  .figma-status-stack {
@@ -4620,9 +4662,9 @@ function formatDashboardTime(value?: string) {
4620
4662
 
4621
4663
  .feedback-row {
4622
4664
  align-items: center;
4623
- background: rgb(var(--v-theme-surface-bright));
4624
- border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
4625
- border-radius: 8px;
4665
+ background: var(--card);
4666
+ border: 1px solid var(--border);
4667
+ border-radius: var(--r-inner-lg);
4626
4668
  display: grid;
4627
4669
  gap: 16px;
4628
4670
  grid-template-columns: minmax(180px, 225px) minmax(130px, 175px) 100px auto;
@@ -4631,6 +4673,39 @@ function formatDashboardTime(value?: string) {
4631
4673
  transition: border-color 0.16s ease, box-shadow 0.16s ease;
4632
4674
  }
4633
4675
 
4676
+ /**
4677
+ * The property-mode feedback row is four flex children that each ask for at
4678
+ * least 140px, so on a 360px phone the status pill and the View button were
4679
+ * clipped by the card edge - visible, but cut in half. It wraps now, and below
4680
+ * 600 each block takes the full width: same four blocks, same order.
4681
+ */
4682
+ .dashboard-feedback-row {
4683
+ flex-wrap: wrap;
4684
+ gap: 12px;
4685
+ }
4686
+
4687
+ @media (max-width: 599.98px) {
4688
+ .dashboard-feedback-row > div {
4689
+ flex: 1 1 100% !important;
4690
+ justify-content: flex-start !important;
4691
+ text-align: left !important;
4692
+ }
4693
+ }
4694
+
4695
+ /* Same story as the patrol row: four fixed-ish columns need ~600px, so on a
4696
+ phone the status pill and the View button were clipped by the card edge.
4697
+ One column below 600, same blocks, same order. */
4698
+ @media (max-width: 599.98px) {
4699
+ .feedback-row {
4700
+ grid-template-columns: minmax(0, 1fr);
4701
+ }
4702
+
4703
+ .feedback-status-pill,
4704
+ .feedback-action {
4705
+ justify-self: start;
4706
+ }
4707
+ }
4708
+
4634
4709
  .feedback-row:hover {
4635
4710
  border-color: rgba(var(--v-border-color), var(--v-border-opacity));
4636
4711
  box-shadow: 0 6px 14px rgba(15, 44, 69, 0.04);
@@ -4675,11 +4750,13 @@ function formatDashboardTime(value?: string) {
4675
4750
  white-space: nowrap;
4676
4751
  }
4677
4752
 
4753
+ /* Carried from Phase 1: the initials measured 1.26:1 in dark mode, because the
4754
+ tint was a literal light blue whatever the theme. */
4678
4755
  .feedback-avatar {
4679
4756
  align-items: center;
4680
- background: #d1e8ff;
4757
+ background: var(--accent-soft);
4681
4758
  border-radius: 50%;
4682
- color: #1d5f99;
4759
+ color: var(--accent-text);
4683
4760
  display: inline-flex;
4684
4761
  flex-shrink: 0;
4685
4762
  font-size: 11px;
@@ -4698,24 +4775,25 @@ function formatDashboardTime(value?: string) {
4698
4775
  white-space: nowrap;
4699
4776
  }
4700
4777
 
4778
+ /* The other two carried Phase 1 items: hard-coded chips. */
4701
4779
  .feedback-status-pill-completed {
4702
- background: #72e3a1;
4703
- color: #0f5132;
4780
+ background: var(--ok-bg);
4781
+ color: var(--ok);
4704
4782
  }
4705
4783
 
4706
4784
  .feedback-status-pill-progress {
4707
- background: #fde68a;
4708
- color: #92400e;
4785
+ background: var(--warn-bg);
4786
+ color: var(--warn);
4709
4787
  }
4710
4788
 
4711
4789
  .feedback-status-pill-danger {
4712
- background: #fecaca;
4713
- color: #b91c1c;
4790
+ background: var(--err-bg);
4791
+ color: var(--err);
4714
4792
  }
4715
4793
 
4716
4794
  .feedback-status-pill-todo {
4717
- background: #e2e8f0;
4718
- color: #64748b;
4795
+ background: var(--hover);
4796
+ color: var(--text2);
4719
4797
  }
4720
4798
 
4721
4799
  .feedback-view-button {