@7365admin1/layer-common 3.2.2-staging.94 → 3.2.2-staging.95

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.
@@ -55,6 +55,39 @@ const initials = computed(() => {
55
55
 
56
56
  const fontSize = computed(() => Math.round(props.size * 0.5))
57
57
 
58
+ /**
59
+ * WHITE INITIALS ON A HASHED COLOUR - THE ESTATE'S WORST CONTRAST DEFECT.
60
+ *
61
+ * The circle's fill is derived from the name, and Vuetify then picks a WHITE
62
+ * label for it. Measured across the whole space this generator can produce
63
+ * (360 hues x 20 saturations x 15 lightnesses): **70.6% of names fail AA**,
64
+ * and the worst - a yellow, `hsl(60,79%,59%)` - reads **1.30:1**. The 3.12:1
65
+ * seen on one screen was a mild sample, not the floor.
66
+ *
67
+ * The name's colour is its identity, so the HUE and SATURATION are kept
68
+ * exactly; only the lightness is walked down until white clears 4.5:1 on it.
69
+ * A yellow therefore darkens a long way and a blue barely moves, which is what
70
+ * makes a single fixed lightness impossible here.
71
+ */
72
+ const AA = 4.5
73
+
74
+ const srgb = (c: number) => {
75
+ const v = c / 255
76
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
77
+ }
78
+
79
+ /** Relative luminance of an `hsl()` triple, without going through the DOM. */
80
+ function luminance(h: number, s: number, l: number): number {
81
+ const sat = s / 100
82
+ const light = l / 100
83
+ const a = sat * Math.min(light, 1 - light)
84
+ const f = (n: number) => {
85
+ const k = (n + h / 30) % 12
86
+ return light - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)))
87
+ }
88
+ return 0.2126 * srgb(f(0) * 255) + 0.7152 * srgb(f(8) * 255) + 0.0722 * srgb(f(4) * 255)
89
+ }
90
+
58
91
  function getColor(str?: string): string {
59
92
  if(!str) return "grey"
60
93
 
@@ -66,7 +99,14 @@ function getColor(str?: string): string {
66
99
 
67
100
  const hue = Math.abs(hash) % 360
68
101
  const saturation = 60 + (Math.abs((hash >> 3) % 20)) // 60–80%
69
- const lightness = 45 + (Math.abs((hash >> 5) % 15)) // 45–60%
102
+ let lightness = 45 + (Math.abs((hash >> 5) % 15)) // 45–60%
103
+
104
+ // ponytail: linear 1% walk, not a solve - the range is at most 45 steps and
105
+ // this runs once per avatar. Lightness bottoms out at 0, where every hue is
106
+ // black and white is 21:1, so the loop always terminates on a pass.
107
+ while (lightness > 0 && 1.05 / (luminance(hue, saturation, lightness) + 0.05) < AA) {
108
+ lightness -= 1
109
+ }
70
110
 
71
111
  return `hsl(${hue}, ${saturation}%, ${lightness}%)`
72
112
  }
@@ -1,28 +1,27 @@
1
1
  <template>
2
- <v-row no-gutters>
3
- <v-col cols="12">
4
- <v-card width="100%" variant="flat" class="table-card" :loading="false">
5
- <v-toolbar
6
- density="compact"
7
- color="transparent"
8
- class="table-card__toolbar"
9
- >
10
- <template #prepend>
11
- <v-btn fab icon density="comfortable">
12
- <v-icon>mdi-refresh</v-icon>
13
- </v-btn>
14
- </template>
15
- <template #append>
16
- <v-row no-gutters justify="end" align="center">
17
- <span class="mr-2 table-card__range">
18
- {{ pageRange }}
19
- </span>
20
- <local-pagination v-model="page" :length="pages" />
21
- </v-row>
22
- </template>
23
- </v-toolbar>
2
+ <div>
3
+ <!--
4
+ THE DESIGN'S PAGE SCAFFOLD (same shape as Service Providers).
5
+
6
+ Placement only: the same refresh control, the same range text, the same
7
+ pager bound to the same `page`, and the table, its columns and its cell
8
+ templates are untouched. The screen simply gains the page title it has
9
+ never had, and its card + toolbar are the design's rather than Vuetify's.
10
+ -->
11
+ <PageHeader title="Billing" />
12
+
13
+ <AppCard class="table-card">
14
+ <CardToolbar :count="pageRange">
15
+ <template #left>
16
+ <AppButton variant="icon" icon="mdi-refresh" aria-label="Refresh" />
17
+ </template>
18
+
19
+ <template #right>
20
+ <local-pagination v-model="page" :length="pages" />
21
+ </template>
22
+ </CardToolbar>
24
23
 
25
- <v-data-table
24
+ <v-data-table
26
25
  :headers="headers"
27
26
  :items="items"
28
27
  item-value="_id"
@@ -44,10 +43,9 @@
44
43
  {{ item.nature }}
45
44
  </span>
46
45
  </template>
47
- </v-data-table>
48
- </v-card>
49
- </v-col>
50
- </v-row>
46
+ </v-data-table>
47
+ </AppCard>
48
+ </div>
51
49
  </template>
52
50
 
53
51
  <script setup lang="ts">
@@ -5,28 +5,41 @@
5
5
  :height="'200px'"
6
6
  v-if="loading"
7
7
  />
8
- <v-row no-gutters v-if="!loading">
9
- <v-col cols="12" elevation="3" class="screen-card">
10
- <v-tabs v-model="activeTab" color="primary" class="px-2 screen-tabs">
11
- <v-tab value="general">
12
- <v-icon start>mdi-cog-outline</v-icon>
13
- General
14
- </v-tab>
15
- <v-tab value="qrcode">
16
- <v-icon start>mdi-qrcode</v-icon>
17
- QR Code
18
- </v-tab>
19
- <v-tab value="printer">
20
- <v-icon start>mdi-printer</v-icon>
21
- Printer
22
- </v-tab>
23
- <v-tab value="entrypass">
24
- <v-icon start>mdi-link-variant</v-icon>
25
- Entrypass
26
- </v-tab>
27
- </v-tabs>
28
-
29
- <v-divider />
8
+ <div v-if="!loading">
9
+ <!--
10
+ THE DESIGN'S CARD + TAB ROW - AND DELIBERATELY NO `PageHeader`.
11
+
12
+ This screen owns no table; it is a settings screen of switches and fields,
13
+ so it takes `AppCard` + `CardToolbar`'s tabs row and no pager, count or
14
+ refresh, because it has never had any.
15
+
16
+ It gets NO page title, unlike the other screens in this pass: its only
17
+ caller mounts it INSIDE a `v-expansion-panel` on property-management's
18
+ Settings page whose panel header already reads "Entry Pass". A
19
+ `PageHeader` here would print that title twice.
20
+
21
+ The same four tabs switch the same `v-window` on the same `activeTab`,
22
+ keeping their icons; every switch, field, button and save handler is
23
+ untouched.
24
+ -->
25
+ <AppCard class="screen-card">
26
+ <CardToolbar>
27
+ <template #tabs>
28
+ <button
29
+ v-for="tab in entryPassTabs"
30
+ :key="tab.value"
31
+ type="button"
32
+ role="tab"
33
+ class="app-tab"
34
+ :class="{ 'app-tab--active': activeTab === tab.value }"
35
+ :aria-selected="activeTab === tab.value"
36
+ @click="activeTab = tab.value"
37
+ >
38
+ <v-icon start size="16">{{ tab.icon }}</v-icon>
39
+ {{ tab.label }}
40
+ </button>
41
+ </template>
42
+ </CardToolbar>
30
43
 
31
44
  <v-window v-model="activeTab">
32
45
  <!-- ─── Tab 1: General ─────────────────────────────────── -->
@@ -379,7 +392,7 @@
379
392
  </v-row>
380
393
  </v-window-item>
381
394
  </v-window>
382
- </v-col>
395
+ </AppCard>
383
396
 
384
397
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
385
398
 
@@ -388,7 +401,7 @@
388
401
  :header="currentEntryPassSettings.qrCodeTemplateHeader"
389
402
  :sub-text="currentEntryPassSettings.qrCodeTemplateHeaderSubText"
390
403
  />
391
- </v-row>
404
+ </div>
392
405
  </template>
393
406
 
394
407
  <script lang="ts" setup>
@@ -400,6 +413,15 @@ const prop = defineProps({
400
413
 
401
414
  const activeTab = ref("general");
402
415
 
416
+ /** The same four tabs, in the same order, moved out of the template so the
417
+ design's tab row can `v-for` them. Values unchanged - they key `v-window`. */
418
+ const entryPassTabs = [
419
+ { value: "general", label: "General", icon: "mdi-cog-outline" },
420
+ { value: "qrcode", label: "QR Code", icon: "mdi-qrcode" },
421
+ { value: "printer", label: "Printer", icon: "mdi-printer" },
422
+ { value: "entrypass", label: "Entrypass", icon: "mdi-link-variant" },
423
+ ];
424
+
403
425
  const currentEntryPassSettings = ref<Record<string, any>>({
404
426
  _id: "",
405
427
  site: "",
@@ -1,50 +1,46 @@
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 screen-btn-primary"
7
- variant="flat"
8
- @click="createDialog = true"
9
- v-if="canCreateRole"
10
- >
2
+ <div>
3
+ <!--
4
+ THE DESIGN'S PAGE SCAFFOLD (same shape as Service Providers).
5
+
6
+ `Create role` moves from a bare row above the card onto the title line,
7
+ on the right, where the design puts a screen's primary action. It is the
8
+ same button under the same `canCreateRole` permission opening the same
9
+ dialog. The refresh, the range, the pager and the table are unchanged.
10
+ -->
11
+ <PageHeader title="Roles and Permissions">
12
+ <template #actions>
13
+ <AppButton v-if="canCreateRole" @click="createDialog = true">
11
14
  Create role
12
- </v-btn>
13
- </v-row>
14
- </v-col>
15
- <v-col cols="12">
16
- <v-card
17
- width="100%"
18
- variant="flat"
19
- class="table-card"
20
- :loading="loading"
21
- >
22
- <v-toolbar
23
- density="compact"
24
- color="transparent"
25
- class="table-card__toolbar"
26
- >
27
- <template #prepend>
28
- <v-btn fab icon density="comfortable" @click="getRoles()">
29
- <v-icon>mdi-refresh</v-icon>
30
- </v-btn>
31
- </template>
32
-
33
- <template #append>
34
- <v-row no-gutters justify="end" align="center">
35
- <span class="mr-2 table-card__range">
36
- {{ pageRange }}
37
- </span>
38
- <local-pagination
39
- v-model="page"
40
- :length="pages"
41
- @update:value="getRoles()"
42
- />
43
- </v-row>
44
- </template>
45
- </v-toolbar>
46
-
47
- <v-data-table
15
+ </AppButton>
16
+ </template>
17
+ </PageHeader>
18
+
19
+ <AppCard class="table-card">
20
+ <!-- `v-card`'s `:loading` bar, drawn explicitly: a loading screen must
21
+ still say so now that the card is not a `v-card`. -->
22
+ <v-progress-linear v-if="loading" indeterminate height="2" />
23
+
24
+ <CardToolbar :count="pageRange">
25
+ <template #left>
26
+ <AppButton
27
+ variant="icon"
28
+ icon="mdi-refresh"
29
+ aria-label="Refresh"
30
+ @click="getRoles()"
31
+ />
32
+ </template>
33
+
34
+ <template #right>
35
+ <local-pagination
36
+ v-model="page"
37
+ :length="pages"
38
+ @update:value="getRoles()"
39
+ />
40
+ </template>
41
+ </CardToolbar>
42
+
43
+ <v-data-table
48
44
  :headers="props.headers"
49
45
  :items="items"
50
46
  item-value="_id"
@@ -78,9 +74,8 @@
78
74
  </v-list>
79
75
  </v-menu>
80
76
  </template>
81
- </v-data-table>
82
- </v-card>
83
- </v-col>
77
+ </v-data-table>
78
+ </AppCard>
84
79
 
85
80
  <!-- dialogs -->
86
81
  <v-dialog v-model="createDialog" width="500" persistent>
@@ -196,7 +191,7 @@
196
191
  </v-dialog>
197
192
 
198
193
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
199
- </v-row>
194
+ </div>
200
195
  </template>
201
196
 
202
197
  <script setup lang="ts">
@@ -32,13 +32,18 @@
32
32
  </template>
33
33
 
34
34
  <template #item.createdBy="{ item }">
35
- <v-avatar
36
- size="32"
37
- class="mr-2"
38
- :color="getAvatarColor(item.createdBy)"
39
- >
40
- <span class="white--text">{{ getInitials(item.createdBy) }}</span>
41
- </v-avatar>
35
+ <!--
36
+ THE 3.12:1 `HS` CHIP.
37
+
38
+ This was a hand-rolled copy of `AvatarMain` with two problems: its
39
+ fill was Vuetify's MATERIAL `blue`/`purple`/`grey` (not a theme
40
+ token, so no `on-*` pair applied to it) and its `white--text` is
41
+ Vuetify 2 syntax that does nothing in Vuetify 3 - leaving Vuetify to
42
+ derive WHITE, which reads 3.12:1 on that blue in BOTH themes.
43
+ `AvatarMain` draws the same circle with the same initials and now
44
+ guarantees its label clears 4.5:1.
45
+ -->
46
+ <AvatarMain :name="item.createdBy" :size="32" class="mr-2" />
42
47
  {{ item.createdBy || "N/A" }}
43
48
  </template>
44
49
 
@@ -131,19 +136,6 @@ const headers = [
131
136
 
132
137
  const { formatDate } = useUtils();
133
138
 
134
- function getInitials(name: string) {
135
- if (!name) return "";
136
- return name
137
- .split(" ")
138
- .map((n) => n[0])
139
- .join("")
140
- .toUpperCase();
141
- }
142
- function getAvatarColor(name: string) {
143
- if (name === "Demo Smith") return "purple";
144
- if (name === "Hygiene SP") return "blue";
145
- return "grey";
146
- }
147
139
  function acceptTask(item: any) {
148
140
  selectedTask.value = item;
149
141
  showAcceptDialog.value = true;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.2.2-staging.94",
5
+ "version": "3.2.2-staging.95",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {