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

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 (126) 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 +51 -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/AreaChecklistHistoryLogs.vue +51 -44
  23. package/components/AreaChecklistHistoryMain.vue +6 -2
  24. package/components/AreaMain.vue +9 -17
  25. package/components/AttendanceMain.vue +4 -3
  26. package/components/Avatar/Main.vue +54 -2
  27. package/components/BillingMain.vue +26 -30
  28. package/components/BuildingManagement/buildings.vue +27 -38
  29. package/components/BuildingManagement/units.vue +26 -42
  30. package/components/BulletinBoardManagement.vue +23 -10
  31. package/components/CameraWall.vue +1034 -0
  32. package/components/CameraWallTile.vue +818 -0
  33. package/components/CardToolbar.vue +37 -0
  34. package/components/CleaningScheduleMain.vue +29 -46
  35. package/components/ClientMain.vue +90 -78
  36. package/components/DashboardEmptyState.vue +11 -1
  37. package/components/DashboardMain.vue +349 -275
  38. package/components/DashboardPanel.vue +1 -1
  39. package/components/DocumentManagement.vue +129 -93
  40. package/components/EntryPassInformation.vue +4 -4
  41. package/components/EntryPassMain.vue +49 -27
  42. package/components/EquipmentItemMain.vue +4 -6
  43. package/components/EquipmentManagementMain.vue +9 -7
  44. package/components/FeedbackMain.vue +42 -56
  45. package/components/FormDialog.vue +60 -21
  46. package/components/HidAccessLogDashboard.vue +119 -44
  47. package/components/HidCameraCaptureDialog.vue +199 -0
  48. package/components/HidIntercomManagement.vue +825 -202
  49. package/components/HidQrCodeConfiguration.vue +927 -93
  50. package/components/HidServiceSettingsPanel.vue +20 -1
  51. package/components/HidUserEnrollment.vue +454 -106
  52. package/components/InvitationMain.vue +46 -61
  53. package/components/Layout/Header.vue +289 -37
  54. package/components/Layout/NavigationDrawer.vue +340 -30
  55. package/components/ManageChecklistMain.vue +17 -17
  56. package/components/MemberMain.vue +79 -60
  57. package/components/MyAttendanceMain.vue +3 -5
  58. package/components/NavigationItem.vue +9 -0
  59. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  60. package/components/NotificationSettings.vue +361 -0
  61. package/components/OnlineFormConfigurationForm.vue +620 -224
  62. package/components/OnlineFormFill.vue +36 -8
  63. package/components/OnlineFormsConfiguration.vue +7 -3
  64. package/components/PageHeader.vue +43 -0
  65. package/components/PassInformation.vue +104 -45
  66. package/components/QrTemplate/CreditCardLandscape.vue +19 -12
  67. package/components/QrTemplate/PrintDialog.vue +209 -196
  68. package/components/RolePermissionMain.vue +184 -91
  69. package/components/ScanVisitorQRCode.vue +35 -7
  70. package/components/ScheduleAreaMain.vue +15 -49
  71. package/components/ScheduleTaskMain.vue +23 -26
  72. package/components/ScheduleTastTicketMain.vue +18 -43
  73. package/components/ServiceProviderInvitationPrompt.vue +150 -0
  74. package/components/ServiceProviderMain.vue +346 -157
  75. package/components/SiteSettings.vue +25 -10
  76. package/components/SiteSettingsWorkOrder.vue +61 -30
  77. package/components/SpecificAttr.vue +10 -2
  78. package/components/StatusChip.vue +93 -0
  79. package/components/StockCard.vue +17 -12
  80. package/components/SwitchContext.vue +33 -1
  81. package/components/TableHygiene.vue +29 -46
  82. package/components/TableList.vue +1 -1
  83. package/components/TableListSecondary.vue +1 -1
  84. package/components/TableMain.vue +161 -77
  85. package/components/TableV3.vue +61 -66
  86. package/components/UnitMain.vue +9 -17
  87. package/components/VehicleManagement.vue +20 -9
  88. package/components/VisitorForm.vue +294 -78
  89. package/components/VisitorManagement.vue +221 -71
  90. package/components/VisitorSocketPopUp.vue +56 -2
  91. package/components/WorkOrder/Main.vue +42 -51
  92. package/composables/useComment.ts +3 -3
  93. package/composables/useHidAmico.ts +151 -0
  94. package/composables/useHidNavigation.ts +0 -7
  95. package/composables/useLocalAuth.ts +9 -36
  96. package/composables/useNotificationPreference.ts +87 -0
  97. package/composables/useOnlineFormPages.ts +2 -0
  98. package/composables/useOrg.ts +1 -1
  99. package/composables/useRole.ts +22 -0
  100. package/composables/useServiceProviderInvitation.ts +145 -0
  101. package/composables/useSipWebPhone.ts +336 -0
  102. package/composables/useSiteSettings.ts +50 -0
  103. package/composables/useThemePreference.ts +63 -0
  104. package/composables/useVisitor.ts +6 -1
  105. package/composables/useVisitorSocket.ts +26 -0
  106. package/composables/useWebUsb.ts +127 -37
  107. package/nuxt.config.ts +31 -0
  108. package/package.json +4 -1
  109. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  110. package/pages/notification-settings.vue +19 -0
  111. package/plugins/secure-member.client.ts +21 -56
  112. package/plugins/vuetify.ts +68 -9
  113. package/test/visitor-socket.test.mjs +36 -0
  114. package/types/org.d.ts +2 -0
  115. package/types/site.d.ts +71 -0
  116. package/utils/camera-wall.test.ts +571 -0
  117. package/utils/camera-wall.ts +926 -0
  118. package/utils/nav-sections.test.ts +190 -0
  119. package/utils/nav-sections.ts +151 -0
  120. package/utils/status.test.ts +77 -0
  121. package/utils/status.ts +90 -0
  122. package/utils/theme.test.ts +534 -0
  123. package/utils/theme.ts +294 -0
  124. package/components/HidIdentityMapping.vue +0 -975
  125. package/middleware/member.ts +0 -4
  126. package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
@@ -1,67 +1,45 @@
1
1
  <template>
2
- <v-row no-gutters>
3
- <v-col cols="12" class="mb-2">
4
- <v-row no-gutters>
5
- <v-btn
6
- class="text-none mr-2"
7
- rounded="pill"
8
- variant="tonal"
9
- size="large"
10
- @click="inviteMember()"
11
- v-if="props.inviteMember"
12
- >
13
- Invite member
14
- </v-btn>
15
- </v-row>
16
- </v-col>
2
+ <div>
3
+ <!-- The design's page scaffold: a title with the action on the right of
4
+ that line, then ONE row inside the card - the two tabs on the left,
5
+ refresh, the range and the pager on the right. The tabs route exactly
6
+ as they did; only where they sit has changed. -->
7
+ <PageHeader title="Invitations">
8
+ <template v-if="props.inviteMember" #actions>
9
+ <AppButton @click="inviteMember()">Invite member</AppButton>
10
+ </template>
11
+ </PageHeader>
12
+
13
+ <v-row no-gutters>
17
14
  <v-col cols="12">
18
- <v-card width="100%" variant="outlined" border="thin" rounded="lg">
19
- <v-toolbar density="compact" color="grey-lighten-4">
20
- <template #prepend>
21
- <v-btn
22
- fab
23
- icon
24
- density="comfortable"
25
- @click="getVerifications()"
15
+ <AppCard class="table-card">
16
+ <CardToolbar :count="pageRange">
17
+ <template #tabs>
18
+ <NuxtLink
19
+ v-for="tab in invitationTabs"
20
+ :key="tab.status"
21
+ class="app-tab"
22
+ :class="{ 'app-tab--active': props.status === tab.status }"
23
+ :to="{ name: props.route, params: { status: tab.status } }"
26
24
  >
27
- <v-icon>mdi-refresh</v-icon>
28
- </v-btn>
25
+ {{ tab.label }}
26
+ </NuxtLink>
29
27
  </template>
30
28
 
31
- <template #append>
32
- <v-row no-gutters justify="end" align="center">
33
- <span class="mr-2 text-caption text-fontgray">
34
- {{ pageRange }}
35
- </span>
36
- <local-pagination
37
- v-model="page"
38
- :length="pages"
39
- @update:value="getVerifications"
40
- />
41
- </v-row>
42
- </template>
43
-
44
- <template #extension>
45
- <v-tabs>
46
- <v-tab
47
- :to="{
48
- name: props.route,
49
- params: { status: 'pending' },
50
- }"
51
- >
52
- Pending
53
- </v-tab>
54
- <v-tab
55
- :to="{
56
- name: props.route,
57
- params: { status: 'expired' },
58
- }"
59
- >
60
- Expired
61
- </v-tab>
62
- </v-tabs>
29
+ <template #right>
30
+ <AppButton
31
+ variant="icon"
32
+ icon="mdi-refresh"
33
+ aria-label="Refresh"
34
+ @click="getVerifications()"
35
+ />
36
+ <local-pagination
37
+ v-model="page"
38
+ :length="pages"
39
+ @update:value="getVerifications"
40
+ />
63
41
  </template>
64
- </v-toolbar>
42
+ </CardToolbar>
65
43
 
66
44
  <v-data-table
67
45
  :headers="headers"
@@ -77,7 +55,7 @@
77
55
  <span class="text-caption font-weight-bold text-capitalize">
78
56
  permissions
79
57
  </span>
80
- <v-chip>{{ value.length }}</v-chip>
58
+ <StatusChip :dot="false" tone="info" :label="String(value.length)" />
81
59
  </template>
82
60
  <template #item.createdAt="{ item }">
83
61
  {{ item.createdAt ? formatDate(item.createdAt) : "" }}
@@ -103,7 +81,7 @@
103
81
  {{ formatStatus(value) }}
104
82
  </template>
105
83
  </v-data-table>
106
- </v-card>
84
+ </AppCard>
107
85
  </v-col>
108
86
 
109
87
  <ConfirmDialog
@@ -153,10 +131,17 @@
153
131
  :app="props.app"
154
132
  />
155
133
  </v-dialog>
156
- </v-row>
134
+ </v-row>
135
+ </div>
157
136
  </template>
158
137
 
159
138
  <script setup lang="ts">
139
+ /** The same two tabs, same labels, same routes. */
140
+ const invitationTabs = [
141
+ { status: "pending", label: "Pending" },
142
+ { status: "expired", label: "Expired" },
143
+ ];
144
+
160
145
  const props = defineProps({
161
146
  app: {
162
147
  type: String,
@@ -1,43 +1,87 @@
1
1
  <template>
2
- <v-app-bar scroll-behavior="elevate" scroll-threshold="200">
2
+ <!--
3
+ PHASE 2 - THE 58px TOP BAR.
4
+
5
+ Same bar, same hamburger, same theme toggle, same profile menu with the
6
+ same two actions. What changes is its height (the design's 58px), the
7
+ breadcrumb it now carries, and the shape of the menu it opens.
8
+
9
+ The breadcrumb READS state that is already in the page - the site the
10
+ application has loaded and the route the router is on. It fetches nothing
11
+ and navigates nowhere; it is a label.
12
+ -->
13
+ <v-app-bar
14
+ :height="58"
15
+ flat
16
+ scroll-behavior="elevate"
17
+ scroll-threshold="200"
18
+ class="app-topbar-shell"
19
+ >
3
20
  <v-app-bar-nav-icon v-if="!props.hideNavIcon" @click="drawer = !drawer"></v-app-bar-nav-icon>
4
21
 
22
+ <div class="app-topbar-shell__crumb">
23
+ <span v-if="siteName" class="app-topbar-shell__site">{{ siteName }}</span>
24
+ <span v-if="siteName && moduleName" class="app-topbar-shell__sep">/</span>
25
+ <span class="app-topbar-shell__module">{{ moduleName }}</span>
26
+ </div>
27
+
5
28
  <template #append>
6
- <v-btn icon="mdi-theme-light-dark" variant="text" class="mx-2" @click="toggleTheme" />
29
+ <!--
30
+ The icon now says which theme you would GET, and the choice is
31
+ remembered - see `composables/useThemePreference.ts`. Same button, same
32
+ place, same one click.
33
+ -->
34
+ <!-- The design's 36px bordered icon button. Same one click, same icon. -->
35
+ <v-btn
36
+ :icon="current === 'dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'"
37
+ :aria-label="current === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'"
38
+ variant="text"
39
+ size="36"
40
+ class="app-topbar-shell__icon-btn ms-2"
41
+ @click="toggleTheme"
42
+ />
7
43
 
8
44
  <v-menu offset="10px" :close-on-content-click="false">
9
45
  <template #activator="{ props }">
10
- <v-btn fab variant="text" icon v-bind="props" class="mx-2">
46
+ <v-btn fab variant="text" icon v-bind="props" class="ms-2 me-1">
11
47
  <AvatarMain :image-src="currentUser?.profile" :name="name" />
12
48
  </v-btn>
13
49
  </template>
14
50
 
15
- <v-card width="350" max-height="600px" elevation="2" rounded="xl" class="pa-4">
16
- <v-row no-gutters>
17
- <v-col cols="12">
18
- <v-row no-gutters justify="center">
19
- <AvatarMain :image-src="currentUser?.profile" :name="name" />
20
- </v-row>
21
- </v-col>
22
-
23
- <v-col cols="12" class="text-center mt-2 mb-4">
24
- {{ name }}
25
- </v-col>
26
-
27
- <v-col cols="12" class="mb-3">
28
- <v-btn v-if="APP_NAME.toLowerCase() !== 'account'" block rounded="xl" variant="tonal" size="x-large"
29
- class="text-none text-subtitle-1 font-weight-regular" @click="redirect(APP_ACCOUNT, 'home')">
30
- Manage Account
31
- </v-btn>
32
- </v-col>
33
-
34
- <v-col cols="12">
35
- <v-btn block rounded="xl" variant="tonal" size="x-large"
36
- class="text-none text-subtitle-1 font-weight-regular" @click="logout()">
37
- Logout
38
- </v-btn>
39
- </v-col>
40
- </v-row>
51
+ <!--
52
+ 240px, 14px radius and the design's menu shadow. The two actions and
53
+ the condition on the first are exactly as they were.
54
+ -->
55
+ <v-card :width="240" elevation="0" class="app-profile-menu">
56
+ <div class="app-profile-menu__head">
57
+ <AvatarMain :image-src="currentUser?.profile" :name="name" />
58
+
59
+ <div class="app-profile-menu__who">
60
+ <div class="app-profile-menu__name">{{ name }}</div>
61
+ <div v-if="siteName" class="app-profile-menu__site">{{ siteName }}</div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="app-profile-menu__rows">
66
+ <button
67
+ v-if="APP_NAME.toLowerCase() !== 'account'"
68
+ type="button"
69
+ class="app-profile-menu__row"
70
+ @click="redirect(APP_ACCOUNT, 'home')"
71
+ >
72
+ <v-icon icon="mdi-account-cog-outline" size="18" />
73
+ Manage Account
74
+ </button>
75
+
76
+ <button
77
+ type="button"
78
+ class="app-profile-menu__row app-profile-menu__row--danger"
79
+ @click="logout()"
80
+ >
81
+ <v-icon icon="mdi-logout" size="18" />
82
+ Logout
83
+ </button>
84
+ </div>
41
85
  </v-card>
42
86
  </v-menu>
43
87
  </template>
@@ -45,8 +89,6 @@
45
89
  </template>
46
90
 
47
91
  <script setup lang="ts">
48
- import { useTheme } from "vuetify";
49
-
50
92
  const props = defineProps({
51
93
  hideNavIcon: {
52
94
  type: Boolean,
@@ -64,14 +106,76 @@ const { redirect } = useUtils();
64
106
 
65
107
  const { APP_ACCOUNT, APP_MAIN, APP_NAME } = useRuntimeConfig().public;
66
108
 
67
- const theme = useTheme();
68
-
69
- function toggleTheme() {
70
- theme.global.name.value = theme.global.current.value.dark ? "light" : "dark";
71
- }
109
+ /**
110
+ * This used to be `theme.global.name.value = ...` and nothing else, so the
111
+ * choice was gone on the next reload - and, because these are eleven separate
112
+ * applications, gone again on every hop between them. The composable writes the
113
+ * layer's shared `theme` cookie, and `plugins/vuetify.ts` boots on it.
114
+ */
115
+ const { current, toggleTheme } = useThemePreference();
72
116
 
73
117
  const { currentUser } = useLocalAuth();
74
118
 
119
+ /**
120
+ * THE BREADCRUMB: `Site name / Current module`.
121
+ *
122
+ * Both halves are read from what the page already holds - the shared `site`
123
+ * state the applications populate, and the router's own current route. NO
124
+ * request is made from this component; if the site has not been loaded on a
125
+ * given screen the site half is simply omitted and the module stands alone.
126
+ */
127
+ const { site } = useSite();
128
+
129
+ /**
130
+ * The shared `site` state is only populated by the screens that load a site
131
+ * document, so on most screens - the dashboard included - the bar had no name
132
+ * to print. `SwitchContext` mirrors the label it is ALREADY showing in the
133
+ * rail into this state, so the bar falls back to the very name the user can
134
+ * see two inches to the left. Still no request from this component.
135
+ */
136
+ const shellSiteName = useState<string>("shell-context-site-name", () => "");
137
+
138
+ const siteName = computed(() =>
139
+ String(site.value?.name || shellSiteName.value || "").trim()
140
+ );
141
+
142
+ const route = useRoute();
143
+
144
+ /**
145
+ * The module's name comes off the route name, which the applications build
146
+ * from their own folder structure: `org-site-work-orders` is Work Orders.
147
+ * Leading parameter segments (`org`, `site`) are skipped and the name stops at
148
+ * the first one that follows, so a detail route (`...-feedbacks-id`) and a
149
+ * status route (`...-members-status`) both name their module rather than their
150
+ * argument.
151
+ */
152
+ const ACRONYMS: Record<string, string> = {
153
+ cctv: "CCTV",
154
+ hid: "HID",
155
+ dob: "DOB",
156
+ anpr: "ANPR",
157
+ nfc: "NFC",
158
+ qr: "QR",
159
+ soa: "SOA",
160
+ sp: "SP",
161
+ id: "ID",
162
+ };
163
+
164
+ const moduleName = computed(() => {
165
+ const segments = String(route.name ?? "").split("-").filter(Boolean);
166
+ const params = new Set(Object.keys(route.params ?? {}));
167
+
168
+ let i = 0;
169
+ while (i < segments.length && params.has(segments[i])) i++;
170
+
171
+ const words: string[] = [];
172
+ while (i < segments.length && !params.has(segments[i])) words.push(segments[i++]);
173
+
174
+ return words
175
+ .map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1))
176
+ .join(" ");
177
+ });
178
+
75
179
  const profile = computed(() => {
76
180
  return `/api/public/${currentUser.value?.profile}`;
77
181
  });
@@ -95,8 +199,11 @@ const name = computed(() => {
95
199
 
96
200
  if(full) return full;
97
201
 
202
+ /* A member invited by email has no name stored yet, which left the avatar's
203
+ initials empty and the circle blank. The address is what the product shows
204
+ that member everywhere else, so it stands in here too. */
98
205
  const alternative = user?.name?.trim();
99
- return alternative || ""
206
+ return alternative || user?.email?.trim() || ""
100
207
 
101
208
  })
102
209
 
@@ -105,6 +212,151 @@ const { getNameInitials } = useUtils();
105
212
  </script>
106
213
 
107
214
  <style scoped>
215
+ /* ---------------------------------------------------------------- */
216
+ /* The top bar. */
217
+ /* ---------------------------------------------------------------- */
218
+
219
+ /**
220
+ * The design's bar is 58px INCLUDING its rule. A real `border-bottom` is added
221
+ * to the 58px Vuetify lays out, so the bar measures 59 and every page below it
222
+ * starts a pixel low; an inset shadow draws the same 1px line inside the 58.
223
+ */
224
+ .app-topbar-shell,
225
+ .app-profile-menu {
226
+ /* The shell is the first surface to wear the design's typeface - see the
227
+ note in NavigationDrawer.vue. */
228
+ font-family: var(--font-sans);
229
+ }
230
+
231
+ .app-topbar-shell {
232
+ box-shadow: inset 0 -1px 0 var(--border);
233
+ }
234
+
235
+ .app-topbar-shell__crumb {
236
+ flex: 1 1 auto;
237
+ min-width: 0;
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 6px;
241
+ font-size: var(--fs-breadcrumb);
242
+ /* Truncates rather than wraps: two lines would break a 58px bar, and the
243
+ site name is the half worth losing first. */
244
+ overflow: hidden;
245
+ white-space: nowrap;
246
+ }
247
+
248
+ .app-topbar-shell__site,
249
+ .app-topbar-shell__sep {
250
+ color: var(--muted);
251
+ overflow: hidden;
252
+ text-overflow: ellipsis;
253
+ }
254
+
255
+ .app-topbar-shell__module {
256
+ color: var(--text);
257
+ font-weight: 700;
258
+ flex: 0 0 auto;
259
+ }
260
+
261
+ /**
262
+ * THE TOP-BAR AVATAR is the design's accent-soft circle with accent-text
263
+ * initials, and only this one. `AvatarMain` gives every person a colour of
264
+ * their own from their name, which is right in a list of people - it is how
265
+ * you tell two rows apart - and wrong for the one avatar that is always the
266
+ * same person. `!important` because that per-person colour arrives as an
267
+ * inline style, and the rule is confined to the bar and its own menu.
268
+ */
269
+ .app-topbar-shell :deep(.v-avatar),
270
+ .app-profile-menu :deep(.v-avatar) {
271
+ background-color: var(--accent-soft) !important;
272
+ /* The initials' colour arrives inline as well, and in dark mode it is the
273
+ per-person colour ON a blue wash: 1.26:1, invisible. */
274
+ color: var(--accent-text) !important;
275
+ font-weight: 800;
276
+ }
277
+
278
+ /* 36px, 1px border, 10px radius - the design's icon button. */
279
+ .app-topbar-shell__icon-btn {
280
+ border: 1px solid var(--border);
281
+ border-radius: var(--r-inner);
282
+ }
283
+
284
+ /* ---------------------------------------------------------------- */
285
+ /* The 240px profile menu. */
286
+ /* ---------------------------------------------------------------- */
287
+
288
+ .app-profile-menu {
289
+ border-radius: var(--r-menu);
290
+ box-shadow: var(--shadow-menu);
291
+ border: 1px solid var(--border);
292
+ overflow: hidden;
293
+ }
294
+
295
+ .app-profile-menu__head {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 10px;
299
+ padding: 14px;
300
+ border-bottom: 1px solid var(--border);
301
+ }
302
+
303
+ .app-profile-menu__who {
304
+ min-width: 0;
305
+ }
306
+
307
+ .app-profile-menu__name {
308
+ font-size: 13.5px;
309
+ font-weight: 800;
310
+ line-height: 1.3;
311
+ color: var(--text);
312
+ overflow: hidden;
313
+ white-space: nowrap;
314
+ text-overflow: ellipsis;
315
+ }
316
+
317
+ .app-profile-menu__site {
318
+ font-size: 11.5px;
319
+ font-weight: 600;
320
+ line-height: 1.3;
321
+ color: var(--muted);
322
+ overflow: hidden;
323
+ white-space: nowrap;
324
+ text-overflow: ellipsis;
325
+ }
326
+
327
+ .app-profile-menu__rows {
328
+ padding: 6px;
329
+ }
330
+
331
+ .app-profile-menu__row {
332
+ width: 100%;
333
+ display: flex;
334
+ align-items: center;
335
+ gap: 10px;
336
+ padding: 9px 10px;
337
+ border-radius: var(--r-inner-sm);
338
+ font-size: var(--fs-btn);
339
+ font-weight: var(--fw-btn);
340
+ color: var(--text2);
341
+ text-align: start;
342
+ cursor: pointer;
343
+ background: transparent;
344
+ }
345
+
346
+ .app-profile-menu__row:hover {
347
+ background: var(--hover);
348
+ }
349
+
350
+ /* Logout is the design's red row: the colour lives on the label, and the
351
+ soft error background is what the hover paints. */
352
+ .app-profile-menu__row--danger {
353
+ color: var(--err);
354
+ }
355
+
356
+ .app-profile-menu__row--danger:hover {
357
+ background: var(--err-bg);
358
+ }
359
+
108
360
  .APP_NAME {
109
361
  display: inline-block;
110
362
  word-wrap: break-word;