@7365admin1/layer-common 3.2.2-staging.80 → 3.2.2-staging.82

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.
@@ -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,65 @@ 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
+ const siteName = computed(() => String(site.value?.name ?? "").trim());
130
+
131
+ const route = useRoute();
132
+
133
+ /**
134
+ * The module's name comes off the route name, which the applications build
135
+ * from their own folder structure: `org-site-work-orders` is Work Orders.
136
+ * Leading parameter segments (`org`, `site`) are skipped and the name stops at
137
+ * the first one that follows, so a detail route (`...-feedbacks-id`) and a
138
+ * status route (`...-members-status`) both name their module rather than their
139
+ * argument.
140
+ */
141
+ const ACRONYMS: Record<string, string> = {
142
+ cctv: "CCTV",
143
+ hid: "HID",
144
+ dob: "DOB",
145
+ anpr: "ANPR",
146
+ nfc: "NFC",
147
+ qr: "QR",
148
+ soa: "SOA",
149
+ sp: "SP",
150
+ id: "ID",
151
+ };
152
+
153
+ const moduleName = computed(() => {
154
+ const segments = String(route.name ?? "").split("-").filter(Boolean);
155
+ const params = new Set(Object.keys(route.params ?? {}));
156
+
157
+ let i = 0;
158
+ while (i < segments.length && params.has(segments[i])) i++;
159
+
160
+ const words: string[] = [];
161
+ while (i < segments.length && !params.has(segments[i])) words.push(segments[i++]);
162
+
163
+ return words
164
+ .map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1))
165
+ .join(" ");
166
+ });
167
+
75
168
  const profile = computed(() => {
76
169
  return `/api/public/${currentUser.value?.profile}`;
77
170
  });
@@ -105,6 +198,151 @@ const { getNameInitials } = useUtils();
105
198
  </script>
106
199
 
107
200
  <style scoped>
201
+ /* ---------------------------------------------------------------- */
202
+ /* The top bar. */
203
+ /* ---------------------------------------------------------------- */
204
+
205
+ /**
206
+ * The design's bar is 58px INCLUDING its rule. A real `border-bottom` is added
207
+ * to the 58px Vuetify lays out, so the bar measures 59 and every page below it
208
+ * starts a pixel low; an inset shadow draws the same 1px line inside the 58.
209
+ */
210
+ .app-topbar-shell,
211
+ .app-profile-menu {
212
+ /* The shell is the first surface to wear the design's typeface - see the
213
+ note in NavigationDrawer.vue. */
214
+ font-family: var(--font-sans);
215
+ }
216
+
217
+ .app-topbar-shell {
218
+ box-shadow: inset 0 -1px 0 var(--border);
219
+ }
220
+
221
+ .app-topbar-shell__crumb {
222
+ flex: 1 1 auto;
223
+ min-width: 0;
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 6px;
227
+ font-size: var(--fs-breadcrumb);
228
+ /* Truncates rather than wraps: two lines would break a 58px bar, and the
229
+ site name is the half worth losing first. */
230
+ overflow: hidden;
231
+ white-space: nowrap;
232
+ }
233
+
234
+ .app-topbar-shell__site,
235
+ .app-topbar-shell__sep {
236
+ color: var(--muted);
237
+ overflow: hidden;
238
+ text-overflow: ellipsis;
239
+ }
240
+
241
+ .app-topbar-shell__module {
242
+ color: var(--text);
243
+ font-weight: 700;
244
+ flex: 0 0 auto;
245
+ }
246
+
247
+ /**
248
+ * THE TOP-BAR AVATAR is the design's accent-soft circle with accent-text
249
+ * initials, and only this one. `AvatarMain` gives every person a colour of
250
+ * their own from their name, which is right in a list of people - it is how
251
+ * you tell two rows apart - and wrong for the one avatar that is always the
252
+ * same person. `!important` because that per-person colour arrives as an
253
+ * inline style, and the rule is confined to the bar and its own menu.
254
+ */
255
+ .app-topbar-shell :deep(.v-avatar),
256
+ .app-profile-menu :deep(.v-avatar) {
257
+ background-color: var(--accent-soft) !important;
258
+ /* The initials' colour arrives inline as well, and in dark mode it is the
259
+ per-person colour ON a blue wash: 1.26:1, invisible. */
260
+ color: var(--accent-text) !important;
261
+ font-weight: 800;
262
+ }
263
+
264
+ /* 36px, 1px border, 10px radius - the design's icon button. */
265
+ .app-topbar-shell__icon-btn {
266
+ border: 1px solid var(--border);
267
+ border-radius: var(--r-inner);
268
+ }
269
+
270
+ /* ---------------------------------------------------------------- */
271
+ /* The 240px profile menu. */
272
+ /* ---------------------------------------------------------------- */
273
+
274
+ .app-profile-menu {
275
+ border-radius: var(--r-menu);
276
+ box-shadow: var(--shadow-menu);
277
+ border: 1px solid var(--border);
278
+ overflow: hidden;
279
+ }
280
+
281
+ .app-profile-menu__head {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 10px;
285
+ padding: 14px;
286
+ border-bottom: 1px solid var(--border);
287
+ }
288
+
289
+ .app-profile-menu__who {
290
+ min-width: 0;
291
+ }
292
+
293
+ .app-profile-menu__name {
294
+ font-size: 13.5px;
295
+ font-weight: 800;
296
+ line-height: 1.3;
297
+ color: var(--text);
298
+ overflow: hidden;
299
+ white-space: nowrap;
300
+ text-overflow: ellipsis;
301
+ }
302
+
303
+ .app-profile-menu__site {
304
+ font-size: 11.5px;
305
+ font-weight: 600;
306
+ line-height: 1.3;
307
+ color: var(--muted);
308
+ overflow: hidden;
309
+ white-space: nowrap;
310
+ text-overflow: ellipsis;
311
+ }
312
+
313
+ .app-profile-menu__rows {
314
+ padding: 6px;
315
+ }
316
+
317
+ .app-profile-menu__row {
318
+ width: 100%;
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 10px;
322
+ padding: 9px 10px;
323
+ border-radius: var(--r-inner-sm);
324
+ font-size: var(--fs-btn);
325
+ font-weight: var(--fw-btn);
326
+ color: var(--text2);
327
+ text-align: start;
328
+ cursor: pointer;
329
+ background: transparent;
330
+ }
331
+
332
+ .app-profile-menu__row:hover {
333
+ background: var(--hover);
334
+ }
335
+
336
+ /* Logout is the design's red row: the colour lives on the label, and the
337
+ soft error background is what the hover paints. */
338
+ .app-profile-menu__row--danger {
339
+ color: var(--err);
340
+ }
341
+
342
+ .app-profile-menu__row--danger:hover {
343
+ background: var(--err-bg);
344
+ }
345
+
108
346
  .APP_NAME {
109
347
  display: inline-block;
110
348
  word-wrap: break-word;