@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.
- package/assets/css/screens.css +471 -0
- package/components/AttendanceMain.vue +4 -3
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/InvitationMain.vue +10 -8
- package/components/MemberMain.vue +9 -6
- package/components/MyAttendanceMain.vue +3 -5
- package/components/RolePermissionMain.vue +15 -10
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ServiceProviderMain.vue +17 -42
- package/components/StatusChip.vue +16 -2
- package/components/TableMain.vue +0 -133
- package/package.json +1 -1
- package/plugins/vuetify.ts +1 -0
- package/utils/status.test.ts +36 -0
- package/utils/status.ts +36 -0
|
@@ -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="
|
|
21
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
64
|
+
<StatusChip
|
|
65
65
|
class="text-capitalize"
|
|
66
|
-
|
|
67
|
-
:
|
|
68
|
-
|
|
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
|
-
<
|
|
71
|
+
<StatusChip
|
|
75
72
|
class="text-capitalize"
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 "
|
|
186
|
-
if (secs <= 0) return "
|
|
187
|
-
if (secs < 3 * 3600) return "
|
|
188
|
-
return "
|
|
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
|
-
|
|
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="
|
|
19
|
-
<v-toolbar
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
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="
|
|
23
|
-
<v-toolbar
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
21
|
-
|
|
22
|
-
rounded="lg"
|
|
18
|
+
variant="flat"
|
|
19
|
+
class="table-card"
|
|
23
20
|
:loading="loading"
|
|
24
21
|
>
|
|
25
|
-
<v-toolbar
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
<
|
|
69
|
-
:color="value === 'active' ? 'success' : 'default'"
|
|
70
|
+
<StatusChip
|
|
70
71
|
class="text-capitalize"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
<
|
|
274
|
-
:color="
|
|
275
|
-
selectedTask.status === 'active' ? 'success' : 'default'
|
|
276
|
-
"
|
|
277
|
-
size="small"
|
|
274
|
+
<StatusChip
|
|
278
275
|
class="text-capitalize"
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
63
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
157
|
-
|
|
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="
|
|
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
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
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(() =>
|
|
43
|
+
const toneClass = computed(() =>
|
|
44
|
+
props.tone ? `tone-${props.tone}` : statusToneClass(props.status)
|
|
45
|
+
);
|
|
32
46
|
</script>
|
|
33
47
|
|
|
34
48
|
<style scoped>
|
package/components/TableMain.vue
CHANGED
|
@@ -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
package/plugins/vuetify.ts
CHANGED
package/utils/status.test.ts
CHANGED
|
@@ -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
|
/**
|