@7365admin1/layer-common 3.2.2-staging.84 → 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);