@7365admin1/layer-common 3.2.1 → 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 (123) hide show
  1. package/.changeset/camera-wall-gated-endpoint.md +47 -0
  2. package/.changeset/camera-wall-plain-english.md +60 -0
  3. package/.changeset/camera-wall-selection-and-guidance.md +65 -0
  4. package/.changeset/dark-primary-contrast.md +39 -0
  5. package/.changeset/dashboard-dark-theme-contrast.md +15 -0
  6. package/.changeset/notification-preferences.md +17 -0
  7. package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
  8. package/.changeset/service-provider-invitation-actions.md +28 -0
  9. package/assets/css/primitives.css +680 -0
  10. package/assets/css/screens.css +541 -0
  11. package/assets/css/shell.css +258 -0
  12. package/assets/css/tokens.css +220 -0
  13. package/components/AccessCardQrTagging.vue +1 -1
  14. package/components/AccessManagement.vue +53 -72
  15. package/components/AppButton.vue +52 -0
  16. package/components/AppCard.vue +20 -0
  17. package/components/AppDateField.vue +35 -0
  18. package/components/AppField.vue +46 -0
  19. package/components/AppKpiTile.vue +65 -0
  20. package/components/AppSegmented.vue +38 -0
  21. package/components/AppSelect.vue +203 -0
  22. package/components/AreaChecklistHistoryMain.vue +6 -2
  23. package/components/AreaMain.vue +9 -17
  24. package/components/AttendanceMain.vue +4 -3
  25. package/components/Avatar/Main.vue +54 -2
  26. package/components/BillingMain.vue +26 -30
  27. package/components/BuildingManagement/buildings.vue +27 -38
  28. package/components/BuildingManagement/units.vue +26 -42
  29. package/components/BulletinBoardManagement.vue +23 -10
  30. package/components/CameraWall.vue +1034 -0
  31. package/components/CameraWallTile.vue +818 -0
  32. package/components/CardToolbar.vue +37 -0
  33. package/components/CleaningScheduleMain.vue +29 -46
  34. package/components/ClientMain.vue +90 -78
  35. package/components/DashboardEmptyState.vue +11 -1
  36. package/components/DashboardMain.vue +349 -275
  37. package/components/DashboardPanel.vue +1 -1
  38. package/components/DocumentManagement.vue +129 -93
  39. package/components/EntryPassInformation.vue +4 -4
  40. package/components/EntryPassMain.vue +49 -27
  41. package/components/EquipmentItemMain.vue +4 -6
  42. package/components/EquipmentManagementMain.vue +9 -7
  43. package/components/FeedbackMain.vue +42 -56
  44. package/components/FormDialog.vue +60 -21
  45. package/components/HidAccessLogDashboard.vue +119 -44
  46. package/components/HidCameraCaptureDialog.vue +199 -0
  47. package/components/HidIntercomManagement.vue +825 -202
  48. package/components/HidQrCodeConfiguration.vue +927 -93
  49. package/components/HidServiceSettingsPanel.vue +20 -1
  50. package/components/HidUserEnrollment.vue +454 -106
  51. package/components/InvitationMain.vue +46 -61
  52. package/components/Layout/Header.vue +289 -37
  53. package/components/Layout/NavigationDrawer.vue +340 -30
  54. package/components/ManageChecklistMain.vue +17 -17
  55. package/components/MemberMain.vue +79 -60
  56. package/components/MyAttendanceMain.vue +3 -5
  57. package/components/NavigationItem.vue +9 -0
  58. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  59. package/components/NotificationSettings.vue +361 -0
  60. package/components/OnlineFormConfigurationForm.vue +620 -224
  61. package/components/OnlineFormFill.vue +36 -8
  62. package/components/OnlineFormsConfiguration.vue +7 -3
  63. package/components/PageHeader.vue +43 -0
  64. package/components/PassInformation.vue +104 -45
  65. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  66. package/components/QrTemplate/PrintDialog.vue +209 -196
  67. package/components/RolePermissionMain.vue +184 -91
  68. package/components/ScanVisitorQRCode.vue +35 -7
  69. package/components/ScheduleAreaMain.vue +15 -49
  70. package/components/ScheduleTaskMain.vue +23 -26
  71. package/components/ScheduleTastTicketMain.vue +18 -43
  72. package/components/ServiceProviderInvitationPrompt.vue +150 -0
  73. package/components/ServiceProviderMain.vue +346 -157
  74. package/components/SiteSettings.vue +25 -10
  75. package/components/SpecificAttr.vue +10 -2
  76. package/components/StatusChip.vue +93 -0
  77. package/components/SwitchContext.vue +33 -1
  78. package/components/TableHygiene.vue +29 -46
  79. package/components/TableList.vue +1 -1
  80. package/components/TableListSecondary.vue +1 -1
  81. package/components/TableMain.vue +161 -77
  82. package/components/TableV3.vue +61 -66
  83. package/components/UnitMain.vue +9 -17
  84. package/components/VehicleManagement.vue +20 -9
  85. package/components/VisitorForm.vue +294 -78
  86. package/components/VisitorManagement.vue +221 -71
  87. package/components/VisitorSocketPopUp.vue +56 -2
  88. package/components/WorkOrder/Main.vue +42 -51
  89. package/composables/useComment.ts +3 -3
  90. package/composables/useHidAmico.ts +151 -0
  91. package/composables/useHidNavigation.ts +0 -7
  92. package/composables/useLocalAuth.ts +9 -36
  93. package/composables/useNotificationPreference.ts +87 -0
  94. package/composables/useOnlineFormPages.ts +2 -0
  95. package/composables/useOrg.ts +1 -1
  96. package/composables/useRole.ts +22 -0
  97. package/composables/useServiceProviderInvitation.ts +145 -0
  98. package/composables/useSipWebPhone.ts +336 -0
  99. package/composables/useSiteSettings.ts +50 -0
  100. package/composables/useThemePreference.ts +63 -0
  101. package/composables/useVisitor.ts +6 -1
  102. package/composables/useVisitorSocket.ts +26 -0
  103. package/composables/useWebUsb.ts +127 -37
  104. package/nuxt.config.ts +31 -0
  105. package/package.json +4 -1
  106. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  107. package/pages/notification-settings.vue +19 -0
  108. package/plugins/secure-member.client.ts +21 -56
  109. package/plugins/vuetify.ts +68 -9
  110. package/test/visitor-socket.test.mjs +36 -0
  111. package/types/org.d.ts +2 -0
  112. package/types/site.d.ts +71 -0
  113. package/utils/camera-wall.test.ts +571 -0
  114. package/utils/camera-wall.ts +926 -0
  115. package/utils/nav-sections.test.ts +190 -0
  116. package/utils/nav-sections.ts +151 -0
  117. package/utils/status.test.ts +77 -0
  118. package/utils/status.ts +90 -0
  119. package/utils/theme.test.ts +534 -0
  120. package/utils/theme.ts +294 -0
  121. package/components/HidIdentityMapping.vue +0 -975
  122. package/middleware/member.ts +0 -4
  123. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -0,0 +1,37 @@
1
+ <!--
2
+ THE IN-CARD TOP ROW.
3
+
4
+ The design does NOT put a screen's tabs above its card - they are the card's
5
+ own first row, with the pagination on the opposite end of that same line, and
6
+ the tab underline meets the card's rule. Today those tabs sit in a
7
+ `v-toolbar`'s extension slot, which stacks them BELOW a separate bar and
8
+ costs a whole row of height.
9
+
10
+ Two shapes, both from the handoff: with tabs (`0 20px`, the tabs supply the
11
+ vertical padding) and without (`10px 14px`, a refresh button on the left).
12
+ This component only places things - the tabs, the count and the pager are
13
+ passed in and keep their own behaviour.
14
+ -->
15
+ <template>
16
+ <div class="app-toolbar" :class="{ 'app-toolbar--plain': !$slots.tabs }">
17
+ <div v-if="$slots.tabs" class="app-tabs" role="tablist">
18
+ <slot name="tabs" />
19
+ </div>
20
+
21
+ <slot name="left" />
22
+
23
+ <div class="app-toolbar__spacer" />
24
+
25
+ <div class="app-toolbar__right">
26
+ <span v-if="count" class="app-toolbar__count">{{ count }}</span>
27
+ <slot name="right" />
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ defineProps({
34
+ /** The design's `1-1 of 1` range, printed as the screen already computes it. */
35
+ count: { type: String, default: "" },
36
+ });
37
+ </script>
@@ -17,11 +17,7 @@
17
17
  >
18
18
  <template #extension>
19
19
  <v-row no-gutters class="w-100 d-flex flex-column">
20
- <v-card
21
- class="w-100 px-3 d-flex align-center ga-5 py-2"
22
- flat
23
- :height="60"
24
- >
20
+ <div class="filter-bar">
25
21
  <InputDateTimePicker
26
22
  v-model:utc="startDate"
27
23
  density="compact"
@@ -47,7 +43,7 @@
47
43
  item-value="value"
48
44
  label="Status"
49
45
  />
50
- </v-card>
46
+ </div>
51
47
  </v-row>
52
48
  </template>
53
49
 
@@ -60,25 +56,23 @@
60
56
  : ""
61
57
  }}
62
58
  </template>
59
+ <!--
60
+ A countdown, not a status word, so it passes the tone the screen has
61
+ already worked out from the time left rather than a word to map.
62
+ -->
63
63
  <template #item.closeIn="{ item }">
64
- <v-chip
64
+ <StatusChip
65
65
  class="text-capitalize"
66
- variant="flat"
67
- :color="getCloseInColor(item._id)"
68
- pill
69
- >
70
- {{ remainingTime[item._id] || "00h 00m" }}
71
- </v-chip>
66
+ :tone="getCloseInTone(item._id)"
67
+ :label="remainingTime[item._id] || '00h 00m'"
68
+ />
72
69
  </template>
73
70
  <template #item.status="{ value }">
74
- <v-chip
71
+ <StatusChip
75
72
  class="text-capitalize"
76
- :color="getStatusColor(value)"
77
- variant="flat"
78
- pill
79
- >
80
- {{ value || "No Status" }}
81
- </v-chip>
73
+ :status="value"
74
+ :label="value || 'No Status'"
75
+ />
82
76
  </template>
83
77
  <template #item.completedAt="{ value }">
84
78
  {{
@@ -159,33 +153,22 @@ const downloadingId = ref<string | null>(null);
159
153
  const { getCleaningSchedules, downloadChecklistPdf } = useCleaningSchedules();
160
154
  const { formatDate } = useUtils();
161
155
 
162
- const getStatusColor = (status: unknown): string => {
163
- if (!status) return "grey";
164
-
165
- const normalized = String(status).toLowerCase();
166
-
167
- switch (normalized) {
168
- case "open":
169
- return "grey";
170
- case "ongoing":
171
- return "primary";
172
- case "completed":
173
- case "accepted":
174
- return "success";
175
- case "closed":
176
- case "rejected":
177
- return "error";
178
- default:
179
- return "secondary";
180
- }
181
- };
182
-
183
- const getCloseInColor = (id: string): string => {
156
+ /**
157
+ * The status word now picks its own colour, from the one shared mapping in
158
+ * `utils/status.ts` - which carries every word this screen used to switch on,
159
+ * with the tone it used to give it. The screen no longer keeps a private
160
+ * palette that only it agrees with.
161
+ *
162
+ * The countdown is not a status word, so the thresholds stay here; only the
163
+ * NAMES change, from Vuetify colours to the design's tones. Same three bands,
164
+ * same boundaries.
165
+ */
166
+ const getCloseInTone = (id: string): "info" | "warn" | "err" => {
184
167
  const secs = remainingSeconds.value[id];
185
- if (secs === undefined) return "primary";
186
- if (secs <= 0) return "error";
187
- if (secs < 3 * 3600) return "warning";
188
- return "primary";
168
+ if (secs === undefined) return "info";
169
+ if (secs <= 0) return "err";
170
+ if (secs < 3 * 3600) return "warn";
171
+ return "info";
189
172
  };
190
173
 
191
174
  const canDownloadSchedule = computed(() => props.canDownloadSchedule);
@@ -632,7 +632,7 @@ onUnmounted(() => {
632
632
  .page-wrap {
633
633
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
634
634
  font-size: 14px;
635
- color: #111827;
635
+ color: var(--text);
636
636
  background: transparent;
637
637
  }
638
638
 
@@ -643,17 +643,21 @@ onUnmounted(() => {
643
643
  gap: 12px;
644
644
  padding: 12px 0;
645
645
  background: transparent;
646
+ /* The title, the 260px search and the invite button do not fit a phone on
647
+ one line, and without this the whole page scrolled sideways. */
648
+ flex-wrap: wrap;
646
649
  }
647
650
  .topbar-right {
648
651
  margin-left: auto;
649
652
  display: flex;
650
653
  align-items: center;
651
654
  gap: 12px;
655
+ flex-wrap: wrap;
652
656
  }
653
657
  .btn-invite {
654
- background: #fff;
655
- color: #111827;
656
- border: 1px solid #D1D5DB;
658
+ background: var(--card);
659
+ color: var(--text);
660
+ border: 1px solid var(--border);
657
661
  border-radius: 24px;
658
662
  padding: 10px 22px;
659
663
  font-size: 14px;
@@ -662,26 +666,27 @@ onUnmounted(() => {
662
666
  white-space: nowrap;
663
667
  transition: background 0.15s;
664
668
  }
665
- .btn-invite:hover { background: #F9FAFB; }
669
+ .btn-invite:hover { background: var(--hover); }
666
670
 
667
671
  .search-wrap {
668
672
  position: relative;
669
673
  width: 260px;
674
+ max-width: 100%;
670
675
  flex-shrink: 0;
671
676
  }
672
677
  .search-input {
673
678
  width: 100%;
674
679
  height: 36px;
675
- border: 1px solid #E5E7EB;
680
+ border: 1px solid var(--border);
676
681
  border-radius: 8px;
677
682
  padding: 0 34px 0 12px;
678
683
  font-size: 13px;
679
- color: #111827;
680
- background: #fff;
684
+ color: var(--text);
685
+ background: var(--card);
681
686
  outline: none;
682
687
  }
683
- .search-input:focus { border-color: #9CA3AF; }
684
- .search-input::placeholder { color: #9CA3AF; }
688
+ .search-input:focus { border-color: var(--muted); }
689
+ .search-input::placeholder { color: var(--muted); }
685
690
  .search-icon {
686
691
  position: absolute;
687
692
  right: 12px;
@@ -698,18 +703,25 @@ onUnmounted(() => {
698
703
  transform: translateY(-50%);
699
704
  border: none;
700
705
  background: transparent;
701
- color: #9CA3AF;
706
+ color: var(--muted);
702
707
  font-size: 16px;
703
708
  cursor: pointer;
704
709
  line-height: 1;
705
710
  padding: 0;
706
711
  }
707
- .clear-btn:hover { color: #374151; }
712
+ .clear-btn:hover { color: var(--text2); }
708
713
 
709
714
  .filter-wrap {
710
715
  display: flex;
711
716
  gap: 10px;
712
- flex-shrink: 0;
717
+ /* Three filters do not fit a phone beside the search box. `flex-shrink: 0`
718
+ kept this row at its 463px content width, so it never wrapped and the
719
+ whole page scrolled sideways at 360. */
720
+ flex-wrap: wrap;
721
+ min-width: 0;
722
+ }
723
+ .filter-wrap > * {
724
+ max-width: 100%;
713
725
  }
714
726
  .filter-field {
715
727
  position: relative;
@@ -721,8 +733,8 @@ onUnmounted(() => {
721
733
  top: -8px;
722
734
  left: 9px;
723
735
  font-size: 10px;
724
- color: #6B7280;
725
- background: #fff;
736
+ color: var(--muted);
737
+ background: var(--card);
726
738
  padding: 0 3px;
727
739
  pointer-events: none;
728
740
  z-index: 1;
@@ -730,18 +742,18 @@ onUnmounted(() => {
730
742
  }
731
743
  .filter-field select {
732
744
  height: 36px;
733
- border: 1px solid #E5E7EB;
745
+ border: 1px solid var(--border);
734
746
  border-radius: 8px;
735
747
  padding: 0 28px 0 10px;
736
748
  font-size: 13px;
737
- color: #111827;
738
- background: #fff;
749
+ color: var(--text);
750
+ background: var(--card);
739
751
  appearance: none;
740
752
  cursor: pointer;
741
753
  outline: none;
742
754
  min-width: 110px;
743
755
  }
744
- .filter-field select:focus { border-color: #9CA3AF; }
756
+ .filter-field select:focus { border-color: var(--muted); }
745
757
  .chevron {
746
758
  position: absolute;
747
759
  right: 9px;
@@ -755,8 +767,8 @@ onUnmounted(() => {
755
767
  /* ===== TABLE CARD ===== */
756
768
  .table-card {
757
769
  margin: 12px 0 0 0;
758
- background: #fff;
759
- border: 1px solid #E5E7EB;
770
+ background: var(--card);
771
+ border: 1px solid var(--border);
760
772
  border-radius: 10px;
761
773
  overflow: hidden;
762
774
  }
@@ -767,8 +779,8 @@ onUnmounted(() => {
767
779
  align-items: center;
768
780
  justify-content: space-between;
769
781
  padding: 6px 12px;
770
- background: #fff;
771
- border-bottom: 1px solid #F3F4F6;
782
+ background: var(--card);
783
+ border-bottom: 1px solid var(--hover);
772
784
  }
773
785
  .toolbar-left { display: flex; align-items: center; }
774
786
  .toolbar-right { display: flex; align-items: center; gap: 6px; }
@@ -777,7 +789,7 @@ onUnmounted(() => {
777
789
  background: none;
778
790
  border: none;
779
791
  cursor: pointer;
780
- color: #6B7280;
792
+ color: var(--muted);
781
793
  width: 28px;
782
794
  height: 28px;
783
795
  border-radius: 6px;
@@ -786,14 +798,14 @@ onUnmounted(() => {
786
798
  justify-content: center;
787
799
  transition: background 0.15s;
788
800
  }
789
- .refresh-btn:hover { background: #F3F4F6; }
801
+ .refresh-btn:hover { background: var(--hover); }
790
802
  .refresh-btn.spinning svg { animation: spin 0.8s linear infinite; }
791
803
  @keyframes spin { to { transform: rotate(360deg); } }
792
804
 
793
- .page-range-text { font-size: 12px; color: #6B7280; }
805
+ .page-range-text { font-size: 12px; color: var(--muted); }
794
806
  .page-btn {
795
807
  background: none;
796
- border: 1px solid #E5E7EB;
808
+ border: 1px solid var(--border);
797
809
  border-radius: 5px;
798
810
  width: 24px;
799
811
  height: 24px;
@@ -801,22 +813,22 @@ onUnmounted(() => {
801
813
  align-items: center;
802
814
  justify-content: center;
803
815
  cursor: pointer;
804
- color: #374151;
816
+ color: var(--text2);
805
817
  transition: background 0.1s;
806
818
  }
807
- .page-btn:hover:not(:disabled) { background: #F3F4F6; }
819
+ .page-btn:hover:not(:disabled) { background: var(--hover); }
808
820
  .page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
809
821
 
810
822
  /* ===== TABS ===== */
811
823
  .tabs-row {
812
824
  display: flex;
813
- border-bottom: 1px solid #E5E7EB;
825
+ border-bottom: 1px solid var(--border);
814
826
  padding: 0 16px;
815
827
  }
816
828
  .tab {
817
829
  padding: 10px 14px;
818
830
  font-size: 13px;
819
- color: #6B7280;
831
+ color: var(--muted);
820
832
  background: none;
821
833
  border: none;
822
834
  border-bottom: 2px solid transparent;
@@ -824,15 +836,15 @@ onUnmounted(() => {
824
836
  margin-bottom: -1px;
825
837
  transition: color 0.15s, border-color 0.15s;
826
838
  }
827
- .tab:hover { color: #374151; }
828
- .tab.active { color: #111827; border-bottom-color: #111827; font-weight: 500; }
839
+ .tab:hover { color: var(--text2); }
840
+ .tab.active { color: var(--text); border-bottom-color: var(--text); font-weight: 500; }
829
841
 
830
842
  /* ===== LOADING BAR ===== */
831
- .loading-bar { height: 2px; background: #F3F4F6; overflow: hidden; }
843
+ .loading-bar { height: 2px; background: var(--hover); overflow: hidden; }
832
844
  .loading-bar-fill {
833
845
  height: 100%;
834
846
  width: 35%;
835
- background: #6B7280;
847
+ background: var(--muted);
836
848
  animation: slide 1.1s ease-in-out infinite;
837
849
  }
838
850
  @keyframes slide {
@@ -848,34 +860,34 @@ thead th {
848
860
  text-align: left;
849
861
  font-weight: 600;
850
862
  font-size: 13px;
851
- color: #374151;
852
- border-bottom: 1px solid #E5E7EB;
863
+ color: var(--text2);
864
+ border-bottom: 1px solid var(--border);
853
865
  white-space: nowrap;
854
- background: #fff;
866
+ background: var(--card);
855
867
  }
856
- tbody tr { border-bottom: 1px solid #F3F4F6; transition: background 0.1s; }
868
+ tbody tr { border-bottom: 1px solid var(--hover); transition: background 0.1s; }
857
869
  tbody tr:last-child { border-bottom: none; }
858
- tbody tr:hover { background: #FAFAFA; }
870
+ tbody tr:hover { background: var(--thead); }
859
871
  tbody td { padding: 14px 16px; vertical-align: middle; }
860
872
 
861
- .empty { text-align: center; color: #9CA3AF; padding: 48px 0 !important; }
862
- .td-name { font-weight: 500; color: #111827; }
863
- .td-muted { color: #6B7280; font-size: 13px; }
873
+ .empty { text-align: center; color: var(--muted); padding: 48px 0 !important; }
874
+ .td-name { font-weight: 500; color: var(--text); }
875
+ .td-muted { color: var(--muted); font-size: 13px; }
864
876
 
865
877
  .sites-badge {
866
878
  display: inline-block;
867
- background: #F3F4F6;
879
+ background: var(--hover);
868
880
  border-radius: 20px;
869
881
  padding: 3px 12px;
870
882
  font-size: 12px;
871
- color: #374151;
883
+ color: var(--text2);
872
884
  }
873
885
  .date-cell {
874
886
  display: inline-flex;
875
887
  align-items: center;
876
888
  gap: 5px;
877
889
  font-size: 13px;
878
- color: #374151;
890
+ color: var(--text2);
879
891
  }
880
892
 
881
893
  /* ===== STATUS CHIPS ===== */
@@ -889,12 +901,12 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
889
901
  font-weight: 500;
890
902
  }
891
903
  .status-chip::before { content: ''; width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
892
- .status-chip.active { background: #D1FAE5; color: #065F46; }
893
- .status-chip.active::before { background: #10B981; }
894
- .status-chip.suspended { background: #FEE2E2; color: #991B1B; }
895
- .status-chip.suspended::before { background: #EF4444; }
896
- .status-chip.pending { background: #FEF9C3; color: #854D0E; }
897
- .status-chip.pending::before { background: #EAB308; }
904
+ .status-chip.active { background: var(--ok-bg); color: var(--ok); }
905
+ .status-chip.active::before { background: currentColor; }
906
+ .status-chip.suspended { background: var(--err-bg); color: var(--err); }
907
+ .status-chip.suspended::before { background: currentColor; }
908
+ .status-chip.pending { background: var(--warn-bg); color: var(--warn); }
909
+ .status-chip.pending::before { background: currentColor; }
898
910
 
899
911
  /* ===== ACTION MENU ===== */
900
912
  .action-wrap { position: relative; }
@@ -907,16 +919,16 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
907
919
  border-radius: 6px;
908
920
  padding: 6px 8px;
909
921
  cursor: pointer;
910
- color: #9CA3AF;
922
+ color: var(--muted);
911
923
  transition: background 0.15s;
912
924
  }
913
- .dots-btn:hover { background: #F3F4F6; color: #374151; }
925
+ .dots-btn:hover { background: var(--hover); color: var(--text2); }
914
926
  .dots-btn span { display: block; width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
915
927
 
916
928
  .dropdown {
917
929
  position: fixed;
918
- background: #fff;
919
- border: 1px solid #E5E7EB;
930
+ background: var(--card);
931
+ border: 1px solid var(--border);
920
932
  border-radius: 8px;
921
933
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
922
934
  min-width: 130px;
@@ -932,12 +944,12 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
932
944
  background: none;
933
945
  border: none;
934
946
  cursor: pointer;
935
- color: #374151;
947
+ color: var(--text2);
936
948
  transition: background 0.1s;
937
949
  }
938
- .dd-item:hover { background: #F9FAFB; }
939
- .dd-item.danger { color: #DC2626; }
940
- .dd-item.danger:hover { background: #FEF2F2; }
950
+ .dd-item:hover { background: var(--hover); }
951
+ .dd-item.danger { color: var(--err); }
952
+ .dd-item.danger:hover { background: var(--err-bg); }
941
953
 
942
954
  .cancel-btn {
943
955
  display: inline-flex;
@@ -945,27 +957,27 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
945
957
  gap: 6px;
946
958
  padding: 5px 12px;
947
959
  font-size: 12px;
948
- color: #DC2626;
949
- background: #FEF2F2;
950
- border: 1px solid #FECACA;
960
+ color: var(--err);
961
+ background: var(--err-bg);
962
+ border: 1px solid var(--err-bg);
951
963
  border-radius: 6px;
952
964
  cursor: pointer;
953
965
  transition: background 0.15s;
954
966
  }
955
- .cancel-btn:hover { background: #FEE2E2; }
967
+ .cancel-btn:hover { background: var(--err-bg); }
956
968
  .cancel-btn:disabled { opacity: 0.5; cursor: not-allowed; }
957
969
  .spinner {
958
970
  display: inline-block;
959
971
  width: 11px;
960
972
  height: 11px;
961
- border: 1.5px solid #FECACA;
962
- border-top-color: #DC2626;
973
+ border: 1.5px solid var(--err-bg);
974
+ border-top-color: var(--err);
963
975
  border-radius: 50%;
964
976
  animation: spin 0.7s linear infinite;
965
977
  }
966
978
  .spinner-light {
967
979
  border: 1.5px solid rgba(255,255,255,0.5);
968
- border-top-color: #fff;
980
+ border-top-color: var(--card);
969
981
  }
970
982
 
971
983
  .client-detail {
@@ -982,7 +994,7 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
982
994
  z-index: 1000;
983
995
  }
984
996
  .dialog-box {
985
- background: #fff;
997
+ background: var(--card);
986
998
  border-radius: 12px;
987
999
  max-width: 500px;
988
1000
  width: 90%;
@@ -992,7 +1004,7 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
992
1004
 
993
1005
  /* ===== SUSPEND CONFIRMATION DIALOG ===== */
994
1006
  .confirm-box {
995
- background: #fff;
1007
+ background: var(--card);
996
1008
  border-radius: 12px;
997
1009
  max-width: 380px;
998
1010
  width: 90%;
@@ -1002,12 +1014,12 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
1002
1014
  .confirm-title {
1003
1015
  font-size: 16px;
1004
1016
  font-weight: 600;
1005
- color: #111827;
1017
+ color: var(--text);
1006
1018
  margin-bottom: 8px;
1007
1019
  }
1008
1020
  .confirm-text {
1009
1021
  font-size: 13.5px;
1010
- color: #4B5563;
1022
+ color: var(--text2);
1011
1023
  line-height: 1.5;
1012
1024
  margin-bottom: 20px;
1013
1025
  }
@@ -1020,14 +1032,14 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
1020
1032
  padding: 8px 16px;
1021
1033
  font-size: 13px;
1022
1034
  font-weight: 500;
1023
- border: 1px solid #D1D5DB;
1035
+ border: 1px solid var(--border);
1024
1036
  border-radius: 8px;
1025
- background: #fff;
1026
- color: #374151;
1037
+ background: var(--card);
1038
+ color: var(--text2);
1027
1039
  cursor: pointer;
1028
1040
  transition: background 0.15s;
1029
1041
  }
1030
- .btn-cancel:hover { background: #F9FAFB; }
1042
+ .btn-cancel:hover { background: var(--hover); }
1031
1043
  .btn-confirm-danger {
1032
1044
  display: inline-flex;
1033
1045
  align-items: center;
@@ -1039,11 +1051,11 @@ tbody td { padding: 14px 16px; vertical-align: middle; }
1039
1051
  font-weight: 500;
1040
1052
  border: none;
1041
1053
  border-radius: 8px;
1042
- background: #DC2626;
1043
- color: #fff;
1054
+ background: var(--err);
1055
+ color: var(--card);
1044
1056
  cursor: pointer;
1045
1057
  transition: background 0.15s;
1046
1058
  }
1047
- .btn-confirm-danger:hover:not(:disabled) { background: #B91C1C; }
1059
+ .btn-confirm-danger:hover:not(:disabled) { background: var(--err); }
1048
1060
  .btn-confirm-danger:disabled { opacity: 0.7; cursor: not-allowed; }
1049
1061
  </style>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="d-flex flex-column align-center justify-center text-center text-blue-grey-lighten-1"
3
+ class="d-flex flex-column align-center justify-center text-center dashboard-empty"
4
4
  :style="{ minHeight: compact ? '72px' : '140px', gap: '8px' }"
5
5
  >
6
6
  <v-icon icon="mdi-database-search-outline" size="28" />
@@ -16,3 +16,13 @@ defineProps({
16
16
  },
17
17
  });
18
18
  </script>
19
+
20
+ <style scoped>
21
+ /* Was `text-blue-grey-lighten-1`, a fixed Vuetify grey that measured 2.4:1 in
22
+ LIGHT mode - the empty state was the hardest thing on the page to read. The
23
+ muted token is the design's own and follows the theme. */
24
+ .dashboard-empty {
25
+ font-family: var(--font-sans);
26
+ color: var(--muted);
27
+ }
28
+ </style>