@7365admin1/layer-common 3.2.2-staging.85 → 3.2.2-staging.86

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.
@@ -0,0 +1,471 @@
1
+ /**
2
+ * PHASE 4 - THE MODULE-SCREEN PATTERNS.
3
+ *
4
+ * Phase 1 wrote the tokens, Phase 2 the shell, Phase 3 the table card. What is
5
+ * left on a module screen is everything AROUND that table: the filter bar over
6
+ * it, the tabs that switch it, the dialogs it opens, the plain cards and the
7
+ * settings accordion. Those patterns are drawn once, here, and the screens opt
8
+ * in by class name - so a screen's diff is class names and chips, not a block
9
+ * of CSS repeated twenty times.
10
+ *
11
+ * OPT-IN, NOT GLOBAL. Every rule below is under a class this layer's own
12
+ * components add to their own markup. Nothing here selects a bare Vuetify class
13
+ * on its own, so no application page is repainted by a stylesheet it never
14
+ * asked for - the eleven apps get these surfaces through the components they
15
+ * already mount, and their own markup is Phases 6 and 7.
16
+ *
17
+ * VISUAL ONLY. No rule here hides, moves, reorders, disables or reveals a
18
+ * control: everything is colour, type, radius, border, shadow and spacing.
19
+ */
20
+
21
+ /* ------------------------------------------------------------------ */
22
+ /* 1. The filter bar. */
23
+ /* ------------------------------------------------------------------ */
24
+
25
+ /**
26
+ * The row of filters that sits over a table. On these screens it is a
27
+ * `v-card flat` with a fixed 60px height and `ga-5`, which on a phone puts
28
+ * three 300px fields on one 60px line and clips them. This is the same fields
29
+ * in the same order, on the design's surface, wrapping instead of clipping.
30
+ */
31
+ .filter-bar {
32
+ display: flex;
33
+ flex-wrap: wrap;
34
+ align-items: center;
35
+ gap: var(--grid-gap);
36
+ width: 100%;
37
+ padding: 10px var(--cellpad-x);
38
+ background: transparent;
39
+ font-family: var(--font-sans);
40
+ }
41
+
42
+ /* 40px, 10px radius, 13.5px - the design's input, whatever Vuetify's density
43
+ would otherwise make of it. `.filter-field` is the same input on its own,
44
+ for the screens whose one search box sits in a toolbar rather than a row. */
45
+ .filter-bar .v-field,
46
+ .filter-field .v-field {
47
+ border-radius: var(--r-inner);
48
+ font-size: var(--fs-cell);
49
+ font-family: var(--font-sans);
50
+ }
51
+
52
+ .filter-bar .v-field__input,
53
+ .filter-field .v-field__input {
54
+ min-height: var(--input-h);
55
+ font-size: var(--fs-cell);
56
+ }
57
+
58
+ .filter-bar .v-label,
59
+ .filter-field .v-label,
60
+ .filter-bar .v-field__input input::placeholder,
61
+ .filter-field .v-field__input input::placeholder {
62
+ font-size: var(--fs-cell);
63
+ font-family: var(--font-sans);
64
+ }
65
+
66
+ /* A field is at least readable-wide and then shares what is left. Below 600
67
+ there is no room for two, so they stack - the same rule `.filter-row` uses. */
68
+ .filter-bar > * {
69
+ flex: 1 1 200px;
70
+ min-width: 0;
71
+ max-width: 100%;
72
+ }
73
+
74
+ .filter-bar > .filter-bar__fit {
75
+ flex: 0 0 auto;
76
+ }
77
+
78
+ @media (max-width: 599.98px) {
79
+ .filter-bar {
80
+ flex-direction: column;
81
+ align-items: stretch;
82
+ }
83
+
84
+ .filter-bar > * {
85
+ flex: 1 1 auto;
86
+ width: 100%;
87
+ }
88
+ }
89
+
90
+ /* ------------------------------------------------------------------ */
91
+ /* 2. Tabs. */
92
+ /* ------------------------------------------------------------------ */
93
+
94
+ /**
95
+ * 13px, the active one at 800 in `--accent-text` over a 2px accent underline.
96
+ * Vuetify's own tab label is uppercase 14px/500 with wide tracking and it puts
97
+ * `text-uppercase` on it as a utility, so these need the same weight of hand
98
+ * the nav items needed in Phase 2.
99
+ */
100
+ .screen-tabs .v-tab {
101
+ font-family: var(--font-sans);
102
+ font-size: var(--fs-nav) !important;
103
+ font-weight: var(--fw-cell) !important;
104
+ letter-spacing: 0 !important;
105
+ text-transform: none !important;
106
+ color: var(--muted);
107
+ min-width: 0;
108
+ }
109
+
110
+ .screen-tabs .v-tab.v-tab--selected {
111
+ font-weight: var(--fw-table-head) !important;
112
+ color: var(--accent-text);
113
+ }
114
+
115
+ .screen-tabs .v-tab .v-tab__slider {
116
+ height: 2px;
117
+ background: var(--accent);
118
+ }
119
+
120
+ /* ------------------------------------------------------------------ */
121
+ /* 3. Cards, section headings and empty states. */
122
+ /* ------------------------------------------------------------------ */
123
+
124
+ /** The card contract, for the surfaces that are not the table card. */
125
+ .screen-card {
126
+ font-family: var(--font-sans);
127
+ background: var(--card);
128
+ border: 1px solid var(--border);
129
+ border-radius: var(--r-card);
130
+ box-shadow: var(--shadow);
131
+ }
132
+
133
+ .screen-title {
134
+ font-family: var(--font-sans);
135
+ font-size: var(--fs-h1);
136
+ font-weight: var(--fw-h1);
137
+ letter-spacing: var(--ls-h1);
138
+ color: var(--text);
139
+ }
140
+
141
+ .screen-card-title {
142
+ font-family: var(--font-sans);
143
+ font-size: var(--fs-card-title) !important;
144
+ font-weight: var(--fw-card-title) !important;
145
+ letter-spacing: 0 !important;
146
+ color: var(--text);
147
+ }
148
+
149
+ .screen-empty {
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ justify-content: center;
154
+ gap: 8px;
155
+ padding: 52px 16px;
156
+ font-family: var(--font-sans);
157
+ font-size: var(--fs-cell);
158
+ color: var(--muted);
159
+ text-align: center;
160
+ }
161
+
162
+ .screen-empty .v-icon {
163
+ color: var(--muted);
164
+ }
165
+
166
+ /**
167
+ * The one-line explanation under a setting's name. It was
168
+ * `text-grey-darken-1` - a fixed Vuetify grey, i.e. the same dark grey on the
169
+ * dark page - so the sentence that explains a switch was the hardest thing on
170
+ * the screen to read. `--muted` is the design's own, and it is a token, so it
171
+ * follows the theme.
172
+ */
173
+ .screen-hint {
174
+ font-family: var(--font-sans);
175
+ color: var(--muted);
176
+ }
177
+
178
+ /* ------------------------------------------------------------------ */
179
+ /* 4. Dialogs. */
180
+ /* ------------------------------------------------------------------ */
181
+
182
+ /**
183
+ * THE ONE RULE HERE THAT IS NOT OPT-IN, AND WHY.
184
+ *
185
+ * The product opens its dialogs from more than sixty different form and dialog
186
+ * components, not from the twenty-three module screens. Giving the design's
187
+ * 16px corner to a modal by editing sixty files would be sixty chances to
188
+ * break a dialog for a corner radius. So the SHAPE is stated once, for any
189
+ * card that Vuetify has put inside an overlay - shape and shadow only. It
190
+ * repaints no colour, moves no control and changes no type, and it is the same
191
+ * `.v-overlay__content > .v-card` selector Phase 3 already had to out-specify.
192
+ */
193
+ .v-overlay__content > .v-card,
194
+ .v-overlay__content > form > .v-card {
195
+ border-radius: var(--r-modal) !important;
196
+ box-shadow: var(--shadow-modal) !important;
197
+ }
198
+
199
+ /**
200
+ * `.v-overlay__content > .v-card` is MORE SPECIFIC than a class on the card
201
+ * (Phase 3 measured a dialog stuck at its factory 4px radius because of it),
202
+ * so the radius has to be `!important` to be reached at all.
203
+ *
204
+ * The body is what scrolls, not the whole card: a modal whose card scrolls
205
+ * takes its Cancel/Submit footer off screen with it. `.screen-modal__body` is
206
+ * only applied where a screen's dialog already has a distinct body element.
207
+ */
208
+ .screen-modal {
209
+ font-family: var(--font-sans);
210
+ border-radius: var(--r-modal) !important;
211
+ box-shadow: var(--shadow-modal) !important;
212
+ background: var(--card) !important;
213
+ max-height: calc(100vh - 32px);
214
+ }
215
+
216
+ .screen-modal .v-card-title {
217
+ font-family: var(--font-sans);
218
+ font-size: var(--fs-card-title) !important;
219
+ font-weight: var(--fw-card-title) !important;
220
+ letter-spacing: 0 !important;
221
+ color: var(--text);
222
+ }
223
+
224
+ .screen-modal__body {
225
+ overflow-y: auto;
226
+ font-size: var(--fs-cell);
227
+ }
228
+
229
+ /* The design's two buttons, for the dialogs and toolbars that draw their own. */
230
+ .screen-btn-primary {
231
+ height: var(--btn-h);
232
+ border-radius: var(--r-inner);
233
+ padding: 0 16px;
234
+ font-family: var(--font-sans);
235
+ font-size: var(--fs-btn);
236
+ font-weight: var(--fw-btn-strong);
237
+ letter-spacing: 0;
238
+ text-transform: none;
239
+ background: var(--accent-strong);
240
+ color: var(--on-accent-strong);
241
+ }
242
+
243
+ /* Vuetify paints `variant="tonal"` with an overlay layer ABOVE the button's own
244
+ background, so without this the accent fill shows through a wash. */
245
+ .screen-btn-primary .v-btn__overlay,
246
+ .screen-btn-ghost .v-btn__overlay {
247
+ opacity: 0;
248
+ }
249
+
250
+ /**
251
+ * The icon-only actions next to a primary button (download template, import,
252
+ * export). The design draws an icon button as a bordered square the height of
253
+ * the input next to it - the same button Phase 2 gave the top bar - not a
254
+ * tinted pill.
255
+ */
256
+ .screen-icon-btn {
257
+ height: var(--btn-h);
258
+ width: var(--btn-h);
259
+ min-width: var(--btn-h);
260
+ border-radius: var(--r-inner);
261
+ border: 1px solid var(--border);
262
+ background: var(--card);
263
+ color: var(--text2);
264
+ }
265
+
266
+ .screen-icon-btn .v-btn__overlay {
267
+ opacity: 0;
268
+ }
269
+
270
+ .screen-btn-ghost {
271
+ height: var(--btn-h);
272
+ border-radius: var(--r-inner);
273
+ padding: 0 16px;
274
+ font-family: var(--font-sans);
275
+ font-size: var(--fs-btn);
276
+ font-weight: var(--fw-btn);
277
+ letter-spacing: 0;
278
+ text-transform: none;
279
+ color: var(--text2);
280
+ border-color: var(--border);
281
+ background: var(--card);
282
+ }
283
+
284
+ /* ------------------------------------------------------------------ */
285
+ /* 5. The settings accordion. */
286
+ /* ------------------------------------------------------------------ */
287
+
288
+ /**
289
+ * The design draws settings as a stack of bordered sections with one open at a
290
+ * time. The "one at a time" half is BEHAVIOUR and is left exactly as the panel
291
+ * has it (`multiple`, on the site settings screen); this is the drawn half:
292
+ * the border, the radius, the title type and the open section's surface.
293
+ */
294
+ .screen-accordion .v-expansion-panel {
295
+ font-family: var(--font-sans);
296
+ background: var(--card);
297
+ border: 1px solid var(--border);
298
+ border-radius: var(--r-card) !important;
299
+ box-shadow: none !important;
300
+ margin-bottom: 10px;
301
+ }
302
+
303
+ .screen-accordion .v-expansion-panel::after {
304
+ display: none;
305
+ }
306
+
307
+ .screen-accordion .v-expansion-panel-title {
308
+ font-family: var(--font-sans);
309
+ font-size: var(--fs-card-title);
310
+ font-weight: var(--fw-card-title);
311
+ color: var(--text);
312
+ min-height: 56px;
313
+ }
314
+
315
+ .screen-accordion .v-expansion-panel-title--active {
316
+ color: var(--text);
317
+ border-bottom: 1px solid var(--border);
318
+ }
319
+
320
+ .screen-accordion .v-expansion-panel-text {
321
+ font-size: var(--fs-cell);
322
+ color: var(--text2);
323
+ }
324
+
325
+ .screen-accordion .v-expansion-panel-title__overlay {
326
+ background: transparent;
327
+ }
328
+
329
+ /* ------------------------------------------------------------------ */
330
+ /* 6. The table card (moved here from TableMain, unchanged). */
331
+ /* ------------------------------------------------------------------ */
332
+
333
+ /**
334
+ * Phase 3 drew this inside `TableMain`'s own scoped block. It is moved here,
335
+ * rule for rule, because it is not TableMain's alone any more: `BillingMain`
336
+ * draws the same card from its own copy of that markup, and a second copy of
337
+ * the CSS is how the two drift apart. `TableMain` still puts the same classes
338
+ * on the same elements - only the stylesheet moved, so nothing it renders
339
+ * changes.
340
+ */
341
+
342
+ /* ------------------------------------------------------------------ */
343
+ /* The card. */
344
+ /* ------------------------------------------------------------------ */
345
+
346
+ .table-card {
347
+ font-family: var(--font-sans);
348
+ background: var(--card);
349
+ border: 1px solid var(--border);
350
+ border-radius: var(--r-card);
351
+ box-shadow: var(--shadow);
352
+ overflow: hidden;
353
+ }
354
+
355
+ /**
356
+ * The toolbar was `color="grey-lighten-4"` - a literal Vuetify grey, which is
357
+ * a near-white bar sitting on a #1c1e24 card in dark mode. It is the card's
358
+ * own surface now, with the design's rule under it.
359
+ */
360
+ .table-card__toolbar {
361
+ border-bottom: 1px solid var(--border);
362
+ }
363
+
364
+ .table-card__range {
365
+ font-size: 12px;
366
+ font-weight: 600;
367
+ color: var(--muted);
368
+ font-variant-numeric: tabular-nums;
369
+ }
370
+
371
+ /* ------------------------------------------------------------------ */
372
+ /* The header row and the rows under it. */
373
+ /* ------------------------------------------------------------------ */
374
+
375
+ .table-card thead th {
376
+ background: var(--thead) !important;
377
+ color: var(--muted) !important;
378
+ font-size: var(--fs-table-head) !important;
379
+ font-weight: var(--fw-table-head) !important;
380
+ letter-spacing: var(--ls-table-head);
381
+ text-transform: uppercase;
382
+ padding: 10px var(--cellpad-x) !important;
383
+ height: auto !important;
384
+ border-bottom: 1px solid var(--border) !important;
385
+ white-space: nowrap;
386
+ }
387
+
388
+ .table-card tbody td {
389
+ font-size: var(--fs-cell);
390
+ font-weight: var(--fw-cell);
391
+ color: var(--text);
392
+ padding: var(--cellpad) var(--cellpad-x) !important;
393
+ height: auto !important;
394
+ font-variant-numeric: tabular-nums;
395
+ }
396
+
397
+ .table-card tbody tr:hover > td {
398
+ background: var(--hover);
399
+ }
400
+
401
+ /* ------------------------------------------------------------------ */
402
+ /* Empty state, in-card search, buttons and in-card tabs. */
403
+ /* ------------------------------------------------------------------ */
404
+
405
+ .table-card__empty {
406
+ display: flex;
407
+ flex-direction: column;
408
+ align-items: center;
409
+ justify-content: center;
410
+ gap: 8px;
411
+ padding: 52px 16px;
412
+ font-size: var(--fs-cell);
413
+ color: var(--muted);
414
+ }
415
+
416
+ .table-card__empty .v-icon {
417
+ color: var(--muted);
418
+ }
419
+
420
+ /* The in-card variant: 34px, 10px radius, as drawn. */
421
+ .table-card__search .v-field {
422
+ border-radius: var(--r-inner);
423
+ font-size: var(--fs-cell);
424
+ }
425
+
426
+ .table-card__primary {
427
+ height: var(--btn-h);
428
+ border-radius: var(--r-inner);
429
+ padding: 0 16px;
430
+ font-size: var(--fs-btn);
431
+ font-weight: var(--fw-btn-strong);
432
+ letter-spacing: 0;
433
+ background: var(--accent-strong);
434
+ color: var(--on-accent-strong);
435
+ }
436
+
437
+ .table-card__ghost {
438
+ height: var(--btn-h);
439
+ border-radius: var(--r-inner);
440
+ padding: 0 16px;
441
+ font-size: var(--fs-btn);
442
+ font-weight: var(--fw-btn);
443
+ letter-spacing: 0;
444
+ color: var(--text2);
445
+ border-color: var(--border);
446
+ background: var(--card);
447
+ }
448
+
449
+ /**
450
+ * Tabs that a screen puts in this card's extension slot: 13px, the active one
451
+ * at 800 in `--accent-text` over a 2px accent underline. Tabs elsewhere in the
452
+ * product are their own screens' and are restyled with them.
453
+ */
454
+ .table-card .v-tab {
455
+ font-size: var(--fs-nav);
456
+ font-weight: var(--fw-cell);
457
+ letter-spacing: 0;
458
+ text-transform: none;
459
+ color: var(--muted);
460
+ min-width: 0;
461
+ }
462
+
463
+ .table-card .v-tab.v-tab--selected {
464
+ font-weight: var(--fw-table-head);
465
+ color: var(--accent-text);
466
+ }
467
+
468
+ .table-card .v-tab .v-tab__slider {
469
+ height: 2px;
470
+ background: var(--accent);
471
+ }
@@ -15,11 +15,12 @@
15
15
  @row-click="handleRowClick"
16
16
  >
17
17
  <template #actions>
18
+ <!-- Was `color="black"`: a black slab in both themes, and the only
19
+ black surface on the page. Same button, the design's ghost. -->
18
20
  <v-btn
19
21
  v-if="canManageAttendanceSettings"
20
- variant="flat"
21
- color="black"
22
- class="text-none"
22
+ variant="outlined"
23
+ class="text-none screen-btn-ghost"
23
24
  @click="dialogShowSettings = true"
24
25
  >
25
26
  <v-icon class="mr-2">mdi-cog</v-icon>
@@ -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);
@@ -3,10 +3,8 @@
3
3
  <v-col cols="12" class="mb-2">
4
4
  <v-row no-gutters>
5
5
  <v-btn
6
- class="text-none mr-2"
7
- rounded="pill"
8
- variant="tonal"
9
- size="large"
6
+ class="text-none mr-2 screen-btn-primary"
7
+ variant="flat"
10
8
  @click="inviteMember()"
11
9
  v-if="props.inviteMember"
12
10
  >
@@ -15,8 +13,12 @@
15
13
  </v-row>
16
14
  </v-col>
17
15
  <v-col cols="12">
18
- <v-card width="100%" variant="outlined" border="thin" rounded="lg">
19
- <v-toolbar density="compact" color="grey-lighten-4">
16
+ <v-card width="100%" variant="flat" class="table-card">
17
+ <v-toolbar
18
+ density="compact"
19
+ color="transparent"
20
+ class="table-card__toolbar"
21
+ >
20
22
  <template #prepend>
21
23
  <v-btn
22
24
  fab
@@ -30,7 +32,7 @@
30
32
 
31
33
  <template #append>
32
34
  <v-row no-gutters justify="end" align="center">
33
- <span class="mr-2 text-caption text-fontgray">
35
+ <span class="mr-2 table-card__range">
34
36
  {{ pageRange }}
35
37
  </span>
36
38
  <local-pagination
@@ -77,7 +79,7 @@
77
79
  <span class="text-caption font-weight-bold text-capitalize">
78
80
  permissions
79
81
  </span>
80
- <v-chip>{{ value.length }}</v-chip>
82
+ <StatusChip :dot="false" tone="info" :label="String(value.length)" />
81
83
  </template>
82
84
  <template #item.createdAt="{ item }">
83
85
  {{ item.createdAt ? formatDate(item.createdAt) : "" }}
@@ -4,9 +4,8 @@
4
4
  <v-row no-gutters>
5
5
  <v-btn
6
6
  v-if="props.seatManagement !== 'index'"
7
- class="text-none"
8
- rounded="pill"
9
- variant="tonal"
7
+ class="text-none screen-btn-primary"
8
+ variant="flat"
10
9
  :to="{
11
10
  name: props.seatManagement,
12
11
  params: { organization: props.orgId },
@@ -19,8 +18,12 @@
19
18
  </v-col>
20
19
 
21
20
  <v-col cols="12">
22
- <v-card width="100%" variant="outlined" border="thin" rounded="lg">
23
- <v-toolbar density="compact" color="grey-lighten-4">
21
+ <v-card width="100%" variant="flat" class="table-card">
22
+ <v-toolbar
23
+ density="compact"
24
+ color="transparent"
25
+ class="table-card__toolbar"
26
+ >
24
27
  <template #prepend>
25
28
  <v-btn fab icon density="comfortable" @click="getAll()">
26
29
  <v-icon>mdi-refresh</v-icon>
@@ -29,7 +32,7 @@
29
32
 
30
33
  <template #append>
31
34
  <v-row no-gutters justify="end" align="center">
32
- <span class="mr-2 text-caption text-font gray">
35
+ <span class="mr-2 table-card__range">
33
36
  {{ pageRange }}
34
37
  </span>
35
38
  <local-pagination
@@ -17,10 +17,8 @@
17
17
  <v-row no-gutters align="center" class="w-100">
18
18
  <v-col cols="auto" v-if="canCheckInOut">
19
19
  <v-btn
20
- class="text-none"
21
- rounded="pill"
22
- variant="tonal"
23
- size="large"
20
+ class="text-none screen-btn-primary"
21
+ variant="flat"
24
22
  @click="onCreateItem"
25
23
  >
26
24
  {{ hasCheckedInToday ? "Check Out" : "Check In" }}
@@ -28,7 +26,7 @@
28
26
  </v-col>
29
27
  <v-spacer />
30
28
 
31
- <v-col cols="auto">
29
+ <v-col cols="auto" class="filter-field">
32
30
  <v-text-field
33
31
  v-model="searchInput"
34
32
  density="compact"
@@ -3,11 +3,9 @@
3
3
  <v-col cols="12" class="mb-2">
4
4
  <v-row no-gutters>
5
5
  <v-btn
6
- class="text-none"
7
- rounded="pill"
8
- variant="tonal"
6
+ class="text-none screen-btn-primary"
7
+ variant="flat"
9
8
  @click="createDialog = true"
10
- size="large"
11
9
  v-if="canCreateRole"
12
10
  >
13
11
  Create role
@@ -17,12 +15,15 @@
17
15
  <v-col cols="12">
18
16
  <v-card
19
17
  width="100%"
20
- variant="outlined"
21
- border="thin"
22
- rounded="lg"
18
+ variant="flat"
19
+ class="table-card"
23
20
  :loading="loading"
24
21
  >
25
- <v-toolbar density="compact" color="grey-lighten-4">
22
+ <v-toolbar
23
+ density="compact"
24
+ color="transparent"
25
+ class="table-card__toolbar"
26
+ >
26
27
  <template #prepend>
27
28
  <v-btn fab icon density="comfortable" @click="getRoles()">
28
29
  <v-icon>mdi-refresh</v-icon>
@@ -31,7 +32,7 @@
31
32
 
32
33
  <template #append>
33
34
  <v-row no-gutters justify="end" align="center">
34
- <span class="mr-2 text-caption text-fontgray">
35
+ <span class="mr-2 table-card__range">
35
36
  {{ pageRange }}
36
37
  </span>
37
38
  <local-pagination
@@ -58,7 +59,11 @@
58
59
  <span class="text-caption font-weight-bold text-capitalize">
59
60
  permissions
60
61
  </span>
61
- <v-chip>{{ getPermissionCount(value) }}</v-chip>
62
+ <StatusChip
63
+ :dot="false"
64
+ tone="info"
65
+ :label="String(getPermissionCount(value))"
66
+ />
62
67
  </template>
63
68
 
64
69
  <template #item.action-table="{ item }" v-if="canDeleteRole">
@@ -21,10 +21,8 @@
21
21
  <v-row no-gutters align="center" class="w-100">
22
22
  <v-col cols="auto" v-if="canCreateScheduleTask">
23
23
  <v-btn
24
- class="text-none"
25
- rounded="pill"
26
- variant="tonal"
27
- size="large"
24
+ class="text-none screen-btn-primary"
25
+ variant="flat"
28
26
  @click="openFormDialog"
29
27
  >
30
28
  Create Schedule Task
@@ -33,7 +31,7 @@
33
31
 
34
32
  <v-spacer />
35
33
 
36
- <v-col cols="auto">
34
+ <v-col cols="auto" class="filter-field">
37
35
  <v-text-field
38
36
  v-model="searchInput"
39
37
  density="compact"
@@ -49,28 +47,31 @@
49
47
 
50
48
  <template #item.areas="{ value }">
51
49
  <div v-if="value && value.length > 0">
52
- <v-chip
50
+ <StatusChip
53
51
  v-for="(area, idx) in value.slice(0, 2)"
54
52
  :key="idx"
55
- size="small"
56
53
  class="mr-1 mb-1"
57
- >
58
- {{ area.name }}
59
- </v-chip>
60
- <v-chip v-if="value.length > 2" size="small" class="mb-1">
61
- +{{ value.length - 2 }} more
62
- </v-chip>
54
+ :dot="false"
55
+ tone="neutral"
56
+ :label="area.name"
57
+ />
58
+ <StatusChip
59
+ v-if="value.length > 2"
60
+ class="mb-1"
61
+ :dot="false"
62
+ tone="neutral"
63
+ :label="`+${value.length - 2} more`"
64
+ />
63
65
  </div>
64
66
  <span v-else>N/A</span>
65
67
  </template>
66
68
 
67
69
  <template #item.status="{ value }">
68
- <v-chip
69
- :color="value === 'active' ? 'success' : 'default'"
70
+ <StatusChip
70
71
  class="text-capitalize"
71
- >
72
- {{ value || "N/A" }}
73
- </v-chip>
72
+ :status="value"
73
+ :label="value || 'N/A'"
74
+ />
74
75
  </template>
75
76
  </TableMain>
76
77
 
@@ -270,15 +271,11 @@
270
271
  Status:
271
272
  </v-col>
272
273
  <v-col cols="7" class="text-subtitle-2">
273
- <v-chip
274
- :color="
275
- selectedTask.status === 'active' ? 'success' : 'default'
276
- "
277
- size="small"
274
+ <StatusChip
278
275
  class="text-capitalize"
279
- >
280
- {{ selectedTask.status || "N/A" }}
281
- </v-chip>
276
+ :status="selectedTask.status"
277
+ :label="selectedTask.status || 'N/A'"
278
+ />
282
279
  </v-col>
283
280
  </v-row>
284
281
 
@@ -5,20 +5,16 @@
5
5
  <div class="d-flex flex-wrap align-center">
6
6
  <v-btn
7
7
  v-if="showAddButton && canAddServiceProvider"
8
- class="text-none mr-2"
9
- rounded="pill"
10
- variant="tonal"
11
- size="large"
8
+ class="text-none mr-2 screen-btn-primary"
9
+ variant="flat"
12
10
  @click="(createDialog = true), setServiceProvider({ mode: 'add' })"
13
11
  >
14
12
  Add
15
13
  </v-btn>
16
14
  <v-btn
17
15
  v-if="canInviteServiceProvider"
18
- class="text-none mr-2"
19
- rounded="pill"
20
- variant="tonal"
21
- size="large"
16
+ class="text-none mr-2 screen-btn-primary"
17
+ variant="flat"
22
18
  @click="
23
19
  (createInviteDialog = true),
24
20
  setServiceProvider({ mode: 'invite' })
@@ -28,10 +24,8 @@
28
24
  </v-btn>
29
25
  <v-btn
30
26
  v-if="showBillingButton"
31
- class="text-none mr-2"
32
- rounded="pill"
33
- variant="tonal"
34
- size="large"
27
+ class="text-none mr-2 screen-btn-primary"
28
+ variant="flat"
35
29
  @click="
36
30
  useRouter().push({
37
31
  name: 'org-site-service-provider-mgmt-billing',
@@ -59,9 +53,8 @@
59
53
  <v-col cols="12">
60
54
  <v-card
61
55
  width="100%"
62
- variant="outlined"
63
- border="thin"
64
- rounded="lg"
56
+ variant="flat"
57
+ class="table-card"
65
58
  :loading="loading"
66
59
  >
67
60
  <div class="sp-table-header">
@@ -101,7 +94,7 @@
101
94
  v-model="listTab"
102
95
  color="primary"
103
96
  density="comfortable"
104
- class="sp-tabs"
97
+ class="sp-tabs screen-tabs"
105
98
  align-tabs="start"
106
99
  >
107
100
  <v-tab value="active" class="text-none">Service Providers</v-tab>
@@ -147,14 +140,11 @@
147
140
  4.5:1 a 12px label needs. Filled puts the colour in the
148
141
  background and lets Vuetify pick a readable foreground for it.
149
142
  -->
150
- <v-chip
151
- :color="statusChipColor(item.status, item.statusRaw)"
152
- size="small"
153
- :variant="item.source === 'invite' ? 'flat' : 'tonal'"
143
+ <StatusChip
154
144
  class="text-none"
155
- >
156
- {{ item.status }}
157
- </v-chip>
145
+ :status="item.statusRaw || item.status"
146
+ :label="item.status"
147
+ />
158
148
  <div
159
149
  v-if="item.rejectionReason"
160
150
  class="text-caption text-medium-emphasis mt-1 sp-reject-reason"
@@ -520,7 +510,7 @@
520
510
 
521
511
  <v-dialog v-model="createInviteDialog" width="580" persistent>
522
512
  <v-card width="100%" rounded="lg" class="sp-invite-card">
523
- <v-toolbar flat color="grey-lighten-4" height="76">
513
+ <v-toolbar flat color="transparent" class="table-card__toolbar" height="76">
524
514
  <v-row no-gutters class="fill-height px-6" align="center">
525
515
  <span class="font-weight-bold text-h5">
526
516
  Invite Service Provider
@@ -1147,24 +1137,9 @@ function statusDisplay(raw: string, source: TableRow["source"]) {
1147
1137
  return raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase();
1148
1138
  }
1149
1139
 
1150
- const INVITE_STATUS_COLOURS: Record<string, string> = {
1151
- "awaiting-approval": "warning",
1152
- pending: "info",
1153
- complete: "success",
1154
- rejected: "error",
1155
- declined: "default",
1156
- cancelled: "default",
1157
- expired: "default",
1158
- };
1159
-
1160
- function statusChipColor(statusLabel: string, statusRaw?: string) {
1161
- if (statusRaw) return INVITE_STATUS_COLOURS[statusRaw] ?? "primary";
1162
- const s = statusLabel.toLowerCase();
1163
- if (s === "active") return "success";
1164
- if (s === "pending") return "warning";
1165
- if (s === "in-active" || s === "inactive") return "default";
1166
- return "primary";
1167
- }
1140
+ /* The status chips read their tone from the shared mapping in
1141
+ `utils/status.ts` now - which carries every raw invitation status this map
1142
+ carried, with the same tone. */
1168
1143
 
1169
1144
  // async function loadInviteRoles() {
1170
1145
  // loadingInviteRoles.value = true;
@@ -12,7 +12,10 @@
12
12
  <template>
13
13
  <span class="status-chip" :class="[toneClass, { 'status-chip--plain': !dot }]">
14
14
  <span v-if="dot" class="status-chip__dot" />
15
- {{ label ?? status }}
15
+ <!-- The slot is for the chips that already carried an icon next to their
16
+ word - a padlock on "Closed - View Only" - so adopting the design's
17
+ pill does not quietly drop it. -->
18
+ <slot>{{ label ?? status }}</slot>
16
19
  </span>
17
20
  </template>
18
21
 
@@ -26,9 +29,20 @@ const props = defineProps({
26
29
  label: { type: String, default: undefined },
27
30
  /** The design's tag chips (pass IDs, role names) carry no leading dot. */
28
31
  dot: { type: Boolean, default: true },
32
+ /**
33
+ * For the chips that are not a status WORD at all - a countdown, a rating, a
34
+ * count - where the screen has already worked out the meaning and only needs
35
+ * the design's tone. Given, it wins over the word mapping.
36
+ */
37
+ tone: {
38
+ type: String as PropType<"ok" | "warn" | "err" | "info" | "neutral" | "">,
39
+ default: "",
40
+ },
29
41
  });
30
42
 
31
- const toneClass = computed(() => statusToneClass(props.status));
43
+ const toneClass = computed(() =>
44
+ props.tone ? `tone-${props.tone}` : statusToneClass(props.status)
45
+ );
32
46
  </script>
33
47
 
34
48
  <style scoped>
@@ -216,136 +216,3 @@ watch(
216
216
  }
217
217
  );
218
218
  </script>
219
-
220
- <style scoped>
221
- /* ------------------------------------------------------------------ */
222
- /* The card. */
223
- /* ------------------------------------------------------------------ */
224
-
225
- .table-card {
226
- font-family: var(--font-sans);
227
- background: var(--card);
228
- border: 1px solid var(--border);
229
- border-radius: var(--r-card);
230
- box-shadow: var(--shadow);
231
- overflow: hidden;
232
- }
233
-
234
- /**
235
- * The toolbar was `color="grey-lighten-4"` - a literal Vuetify grey, which is
236
- * a near-white bar sitting on a #1c1e24 card in dark mode. It is the card's
237
- * own surface now, with the design's rule under it.
238
- */
239
- .table-card__toolbar {
240
- border-bottom: 1px solid var(--border);
241
- }
242
-
243
- .table-card__range {
244
- font-size: 12px;
245
- font-weight: 600;
246
- color: var(--muted);
247
- font-variant-numeric: tabular-nums;
248
- }
249
-
250
- /* ------------------------------------------------------------------ */
251
- /* The header row and the rows under it. */
252
- /* ------------------------------------------------------------------ */
253
-
254
- .table-card :deep(thead th) {
255
- background: var(--thead) !important;
256
- color: var(--muted) !important;
257
- font-size: var(--fs-table-head) !important;
258
- font-weight: var(--fw-table-head) !important;
259
- letter-spacing: var(--ls-table-head);
260
- text-transform: uppercase;
261
- padding: 10px var(--cellpad-x) !important;
262
- height: auto !important;
263
- border-bottom: 1px solid var(--border) !important;
264
- white-space: nowrap;
265
- }
266
-
267
- .table-card :deep(tbody td) {
268
- font-size: var(--fs-cell);
269
- font-weight: var(--fw-cell);
270
- color: var(--text);
271
- padding: var(--cellpad) var(--cellpad-x) !important;
272
- height: auto !important;
273
- font-variant-numeric: tabular-nums;
274
- }
275
-
276
- .table-card :deep(tbody tr:hover > td) {
277
- background: var(--hover);
278
- }
279
-
280
- /* ------------------------------------------------------------------ */
281
- /* Empty state, in-card search, buttons and in-card tabs. */
282
- /* ------------------------------------------------------------------ */
283
-
284
- .table-card__empty {
285
- display: flex;
286
- flex-direction: column;
287
- align-items: center;
288
- justify-content: center;
289
- gap: 8px;
290
- padding: 52px 16px;
291
- font-size: var(--fs-cell);
292
- color: var(--muted);
293
- }
294
-
295
- .table-card__empty :deep(.v-icon) {
296
- color: var(--muted);
297
- }
298
-
299
- /* The in-card variant: 34px, 10px radius, as drawn. */
300
- .table-card__search :deep(.v-field) {
301
- border-radius: var(--r-inner);
302
- font-size: var(--fs-cell);
303
- }
304
-
305
- .table-card__primary {
306
- height: var(--btn-h);
307
- border-radius: var(--r-inner);
308
- padding: 0 16px;
309
- font-size: var(--fs-btn);
310
- font-weight: var(--fw-btn-strong);
311
- letter-spacing: 0;
312
- background: var(--accent-strong);
313
- color: var(--on-accent-strong);
314
- }
315
-
316
- .table-card__ghost {
317
- height: var(--btn-h);
318
- border-radius: var(--r-inner);
319
- padding: 0 16px;
320
- font-size: var(--fs-btn);
321
- font-weight: var(--fw-btn);
322
- letter-spacing: 0;
323
- color: var(--text2);
324
- border-color: var(--border);
325
- background: var(--card);
326
- }
327
-
328
- /**
329
- * Tabs that a screen puts in this card's extension slot: 13px, the active one
330
- * at 800 in `--accent-text` over a 2px accent underline. Tabs elsewhere in the
331
- * product are their own screens' and are restyled with them.
332
- */
333
- .table-card :deep(.v-tab) {
334
- font-size: var(--fs-nav);
335
- font-weight: var(--fw-cell);
336
- letter-spacing: 0;
337
- text-transform: none;
338
- color: var(--muted);
339
- min-width: 0;
340
- }
341
-
342
- .table-card :deep(.v-tab.v-tab--selected) {
343
- font-weight: var(--fw-table-head);
344
- color: var(--accent-text);
345
- }
346
-
347
- .table-card :deep(.v-tab .v-tab__slider) {
348
- height: 2px;
349
- background: var(--accent);
350
- }
351
- </style>
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.85",
5
+ "version": "3.2.2-staging.86",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -53,6 +53,7 @@ import type { CookieOptions } from "#app";
53
53
  */
54
54
  import "../assets/css/tokens.css";
55
55
  import "../assets/css/shell.css";
56
+ import "../assets/css/screens.css";
56
57
 
57
58
  export default defineNuxtPlugin((app) => {
58
59
  /**
@@ -35,6 +35,42 @@ test("an unmapped status comes out neutral rather than guessed", () => {
35
35
  }
36
36
  });
37
37
 
38
+ /**
39
+ * PHASE 4. Each of these words is on a module screen today with a colour a
40
+ * per-screen `getStatusColor` gave it. The list has to keep giving it the SAME
41
+ * tone, or adopting the shared chip would quietly repaint a status.
42
+ */
43
+ test("the product's own status words keep the tone the screens already gave them", () => {
44
+ for (const s of ["Accepted", "Approved", "Resolved", "Active"]) {
45
+ assert.equal(statusTone(s), "ok", s);
46
+ }
47
+
48
+ for (const s of ["Rejected", "Deleted", "Suspended"]) {
49
+ assert.equal(statusTone(s), "err", s);
50
+ }
51
+
52
+ for (const s of ["In Progress", "In-Progress", "To-Do", "Awaiting Approval"]) {
53
+ assert.equal(statusTone(s), "warn", s);
54
+ }
55
+
56
+ for (const s of ["Ongoing", "For Review"]) {
57
+ assert.equal(statusTone(s), "info", s);
58
+ }
59
+
60
+ for (const s of ["Inactive", "Expired"]) {
61
+ assert.equal(statusTone(s), "neutral", s);
62
+ }
63
+ });
64
+
65
+ /**
66
+ * The one place the handoff and the old per-screen colour disagree: `Open` was
67
+ * grey on the cleaning schedules and the design names it `ok`. Pinned so the
68
+ * disagreement is a decision on the record rather than a surprise.
69
+ */
70
+ test("Open follows the design, not the old grey", () => {
71
+ assert.equal(statusTone("open"), "ok");
72
+ });
73
+
38
74
  test("the class name matches what tokens.css actually defines", () => {
39
75
  assert.equal(statusToneClass("Paid"), "tone-ok");
40
76
  assert.equal(statusToneClass("Whatever"), "tone-neutral");
package/utils/status.ts CHANGED
@@ -34,6 +34,42 @@ const TONES: Record<string, TStatusTone> = {
34
34
 
35
35
  role: "info",
36
36
  info: "info",
37
+
38
+ /**
39
+ * PHASE 4 - THE WORDS THIS PRODUCT ACTUALLY USES.
40
+ *
41
+ * The handoff names ten words; the module screens show more than that, and
42
+ * each of them is ALREADY painted a colour today by a per-screen
43
+ * `getStatusColor`. So these are not new opinions - each one is the tone the
44
+ * screens already give that word, moved into the one list so that "Ongoing"
45
+ * is the same blue on a cleaning schedule and on a scheduled area. Where the
46
+ * old per-screen colour and the design disagree the design wins, and there
47
+ * is exactly one of those: `Open` was grey and the handoff names it `ok`.
48
+ *
49
+ * Anything still unlisted stays neutral, deliberately - see above.
50
+ */
51
+ ongoing: "info", // was `primary`
52
+ "in progress": "warn", // was #FB8C00, orange
53
+ "for review": "info", // was `primary`
54
+ "to do": "warn", // was `orange`
55
+
56
+ accepted: "ok", // was `success`
57
+ approved: "ok",
58
+ resolved: "ok", // was #4CAF50, green
59
+ active: "ok", // was `success`
60
+
61
+ rejected: "err", // was `error`
62
+ deleted: "err", // was `error`
63
+ suspended: "err", // was `error`
64
+
65
+ complete: "ok",
66
+
67
+ inactive: "neutral", // was `grey`
68
+ "in active": "neutral", // the same word, printed "In-Active"
69
+ declined: "neutral", // was Vuetify's `default`
70
+ expired: "neutral", // was `grey`
71
+
72
+ "awaiting approval": "warn", // a waiting state, like Pending
37
73
  };
38
74
 
39
75
  /**