@7365admin1/layer-common 3.2.2-staging.90 → 3.2.2-staging.91
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/primitives.css +430 -0
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +46 -0
- package/components/AppSelect.vue +47 -0
- package/components/CardToolbar.vue +37 -0
- package/components/MemberMain.vue +79 -63
- package/components/PageHeader.vue +40 -0
- package/components/StatusChip.vue +17 -2
- package/package.json +1 -1
- package/plugins/vuetify.ts +1 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLATFORM REDESIGN - THE PAGE PRIMITIVES.
|
|
3
|
+
*
|
|
4
|
+
* Phases 1-5 tokenised what Vuetify draws. This file is the other half: the
|
|
5
|
+
* pieces the design draws that Vuetify has no component for, taken as literal
|
|
6
|
+
* CSS out of the handoff prototype
|
|
7
|
+
* (`design_handoff_platform_redesign/iService365 Redesign.dc.html`) rather
|
|
8
|
+
* than re-derived by eye. Every declaration below has a line in that file.
|
|
9
|
+
*
|
|
10
|
+
* The rule for a screen adopting these: MARKUP, not a theme override. A
|
|
11
|
+
* `.v-btn` restyled into a pill is still a `.v-btn` with Vuetify's ripple,
|
|
12
|
+
* overlay, min-width and uppercase tracking fighting it at a higher
|
|
13
|
+
* specificity; a `<button class="app-btn">` simply is the button in the
|
|
14
|
+
* design. Where Vuetify owns the behaviour (the data table, the menu, the
|
|
15
|
+
* dialog) it stays - only the chrome around it is replaced.
|
|
16
|
+
*
|
|
17
|
+
* Numbers, for review against the handoff:
|
|
18
|
+
* page title 22px/800/-0.01em · card radius 14px, 1px border, --shadow
|
|
19
|
+
* in-card toolbar 0 20px, tabs 14px 2px 12px, 2px accent underline
|
|
20
|
+
* fields 40px (34px compact), 10px radius, 13.5px/500
|
|
21
|
+
* primary button 40px, 10px radius, 13.5px/800 · ghost 38px, 13px/700
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* ------------------------------------------------------------------ */
|
|
25
|
+
/* The typeface. */
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Phase 1 loaded Manrope and deliberately did not apply it, so that the
|
|
30
|
+
* product would not sit half-migrated between two typefaces. The page
|
|
31
|
+
* scaffold is the migration, so the font goes on now - on Vuetify's own
|
|
32
|
+
* application root, which is every rendered page in all eleven apps.
|
|
33
|
+
*
|
|
34
|
+
* `.v-application` and not `body`: Vuetify sets `font-family` on that class
|
|
35
|
+
* itself, so a rule on `body` loses to it and the product would keep Roboto
|
|
36
|
+
* everywhere while the new primitives alone changed - the exact
|
|
37
|
+
* half-migrated look this was avoiding.
|
|
38
|
+
*/
|
|
39
|
+
.v-application,
|
|
40
|
+
.v-overlay-container {
|
|
41
|
+
font-family: var(--font-sans);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ------------------------------------------------------------------ */
|
|
45
|
+
/* PageHeader - the row every screen opens with. */
|
|
46
|
+
/* ------------------------------------------------------------------ */
|
|
47
|
+
|
|
48
|
+
.app-page-head {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
gap: 16px;
|
|
53
|
+
margin-bottom: 18px;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.app-page-head__title {
|
|
58
|
+
margin: 0;
|
|
59
|
+
font-size: var(--fs-h1);
|
|
60
|
+
font-weight: var(--fw-h1);
|
|
61
|
+
letter-spacing: var(--ls-h1);
|
|
62
|
+
line-height: 1.25;
|
|
63
|
+
color: var(--text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.app-page-head__sub {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 5px;
|
|
70
|
+
margin-top: 4px;
|
|
71
|
+
font-size: 12.5px;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.app-page-head__actions {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 10px;
|
|
80
|
+
flex-wrap: wrap;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ------------------------------------------------------------------ */
|
|
84
|
+
/* AppCard - the surface everything else sits on. */
|
|
85
|
+
/* ------------------------------------------------------------------ */
|
|
86
|
+
|
|
87
|
+
.app-card {
|
|
88
|
+
background: var(--card);
|
|
89
|
+
border: 1px solid var(--border);
|
|
90
|
+
border-radius: var(--r-card);
|
|
91
|
+
box-shadow: var(--shadow);
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.app-card--flush {
|
|
96
|
+
overflow: visible;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ------------------------------------------------------------------ */
|
|
100
|
+
/* CardToolbar - the in-card top row. */
|
|
101
|
+
/* ------------------------------------------------------------------ */
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Two shapes in the handoff, one component. With tabs the row is `0 20px` and
|
|
105
|
+
* the tabs supply their own vertical padding, so the underline meets the
|
|
106
|
+
* card's rule; without tabs (a refresh button on the left) it is `10px 14px`.
|
|
107
|
+
*/
|
|
108
|
+
.app-toolbar {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
gap: 22px;
|
|
112
|
+
padding: 0 var(--cellpad-x);
|
|
113
|
+
border-bottom: 1px solid var(--border);
|
|
114
|
+
min-height: 48px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.app-toolbar--plain {
|
|
118
|
+
gap: 8px;
|
|
119
|
+
padding: 10px 14px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.app-toolbar__spacer {
|
|
123
|
+
flex: 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.app-toolbar__right {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 8px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.app-toolbar__count {
|
|
133
|
+
font-size: 12.5px;
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
color: var(--muted);
|
|
136
|
+
font-variant-numeric: tabular-nums;
|
|
137
|
+
white-space: nowrap;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.app-tabs {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 22px;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
overflow-x: auto;
|
|
146
|
+
scrollbar-width: none;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.app-tabs::-webkit-scrollbar {
|
|
150
|
+
display: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.app-tab {
|
|
154
|
+
padding: 14px 2px 12px 2px;
|
|
155
|
+
border: none;
|
|
156
|
+
background: transparent;
|
|
157
|
+
border-bottom: 2px solid transparent;
|
|
158
|
+
font-family: inherit;
|
|
159
|
+
font-size: var(--fs-nav);
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
color: var(--muted);
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.app-tab:hover {
|
|
167
|
+
color: var(--text2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.app-tab--active {
|
|
171
|
+
font-weight: 800;
|
|
172
|
+
color: var(--accent-text);
|
|
173
|
+
border-bottom-color: var(--accent);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ------------------------------------------------------------------ */
|
|
177
|
+
/* The table, drawn as a grid. */
|
|
178
|
+
/* ------------------------------------------------------------------ */
|
|
179
|
+
|
|
180
|
+
.app-thead {
|
|
181
|
+
display: grid;
|
|
182
|
+
gap: 12px;
|
|
183
|
+
padding: 12px var(--cellpad-x);
|
|
184
|
+
font-size: var(--fs-table-head);
|
|
185
|
+
font-weight: var(--fw-table-head);
|
|
186
|
+
letter-spacing: var(--ls-table-head);
|
|
187
|
+
text-transform: uppercase;
|
|
188
|
+
color: var(--muted);
|
|
189
|
+
background: var(--thead);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.app-trow {
|
|
193
|
+
display: grid;
|
|
194
|
+
gap: 12px;
|
|
195
|
+
align-items: center;
|
|
196
|
+
padding: var(--cellpad) var(--cellpad-x);
|
|
197
|
+
border-top: 1px solid var(--border);
|
|
198
|
+
font-size: 13px;
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
color: var(--text);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.app-trow:hover {
|
|
204
|
+
background: var(--hover);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.app-cell--strong {
|
|
208
|
+
font-weight: 700;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
text-overflow: ellipsis;
|
|
211
|
+
white-space: nowrap;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.app-cell--muted {
|
|
215
|
+
color: var(--text2);
|
|
216
|
+
overflow: hidden;
|
|
217
|
+
text-overflow: ellipsis;
|
|
218
|
+
white-space: nowrap;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.app-cell--num {
|
|
222
|
+
color: var(--text2);
|
|
223
|
+
font-variant-numeric: tabular-nums;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.app-empty {
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
align-items: center;
|
|
230
|
+
gap: 8px;
|
|
231
|
+
padding: 52px var(--cellpad-x);
|
|
232
|
+
border-top: 1px solid var(--border);
|
|
233
|
+
font-size: 13px;
|
|
234
|
+
font-weight: 600;
|
|
235
|
+
color: var(--muted);
|
|
236
|
+
text-align: center;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* ------------------------------------------------------------------ */
|
|
240
|
+
/* AppButton. */
|
|
241
|
+
/* ------------------------------------------------------------------ */
|
|
242
|
+
|
|
243
|
+
.app-btn {
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
justify-content: center;
|
|
247
|
+
gap: 6px;
|
|
248
|
+
height: var(--btn-h);
|
|
249
|
+
padding: 0 16px;
|
|
250
|
+
border: none;
|
|
251
|
+
border-radius: var(--r-inner);
|
|
252
|
+
/* `--accent` at 3.23:1 cannot carry a white label - see Phase 1. The fill,
|
|
253
|
+
and only the fill, uses the 18%-darker `--accent-strong`; the underline,
|
|
254
|
+
the links and every soft background stay on `--accent`. */
|
|
255
|
+
background: var(--accent-strong);
|
|
256
|
+
color: var(--on-accent-strong);
|
|
257
|
+
font-family: inherit;
|
|
258
|
+
font-size: var(--fs-btn);
|
|
259
|
+
font-weight: var(--fw-btn-strong);
|
|
260
|
+
line-height: 1;
|
|
261
|
+
white-space: nowrap;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.app-btn:hover {
|
|
266
|
+
filter: brightness(1.06);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.app-btn:disabled {
|
|
270
|
+
opacity: 0.5;
|
|
271
|
+
cursor: default;
|
|
272
|
+
filter: none;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.app-btn--ghost {
|
|
276
|
+
height: 38px;
|
|
277
|
+
padding: 0 14px;
|
|
278
|
+
border: 1px solid var(--border);
|
|
279
|
+
background: var(--card);
|
|
280
|
+
color: var(--text2);
|
|
281
|
+
font-size: var(--fs-nav);
|
|
282
|
+
font-weight: var(--fw-btn);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.app-btn--ghost:hover {
|
|
286
|
+
background: var(--hover);
|
|
287
|
+
filter: none;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.app-btn--icon {
|
|
291
|
+
width: 32px;
|
|
292
|
+
height: 32px;
|
|
293
|
+
padding: 0;
|
|
294
|
+
border: none;
|
|
295
|
+
border-radius: 8px;
|
|
296
|
+
background: transparent;
|
|
297
|
+
color: var(--text2);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.app-btn--icon:hover {
|
|
301
|
+
background: var(--hover);
|
|
302
|
+
filter: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* The 30px bordered row action - the `more_vert` at the end of a member row. */
|
|
306
|
+
.app-btn--row {
|
|
307
|
+
width: 30px;
|
|
308
|
+
height: 30px;
|
|
309
|
+
padding: 0;
|
|
310
|
+
border: 1px solid var(--border);
|
|
311
|
+
border-radius: 8px;
|
|
312
|
+
background: transparent;
|
|
313
|
+
color: var(--muted);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.app-btn--row:hover {
|
|
317
|
+
background: var(--hover);
|
|
318
|
+
filter: none;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* ------------------------------------------------------------------ */
|
|
322
|
+
/* AppField / AppSelect / AppDateField. */
|
|
323
|
+
/* ------------------------------------------------------------------ */
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Flat and compact, with the label OUTSIDE the box as a small muted word -
|
|
327
|
+
* not Vuetify's outlined field with a label that floats through the border.
|
|
328
|
+
* A native `<input>`/`<select>`, so the browser keeps the keyboard, the date
|
|
329
|
+
* picker and the accessibility for free.
|
|
330
|
+
*/
|
|
331
|
+
.app-field {
|
|
332
|
+
display: flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
gap: 8px;
|
|
335
|
+
height: var(--input-h);
|
|
336
|
+
padding: 0 12px;
|
|
337
|
+
border: 1px solid var(--border);
|
|
338
|
+
border-radius: var(--r-inner);
|
|
339
|
+
background: var(--card);
|
|
340
|
+
font-size: var(--fs-nav);
|
|
341
|
+
font-weight: 600;
|
|
342
|
+
color: var(--text);
|
|
343
|
+
min-width: 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.app-field--compact {
|
|
347
|
+
height: var(--input-h-compact);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.app-field:focus-within {
|
|
351
|
+
border-color: var(--accent);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.app-field__label {
|
|
355
|
+
font-size: 12px;
|
|
356
|
+
font-weight: 600;
|
|
357
|
+
color: var(--muted);
|
|
358
|
+
white-space: nowrap;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.app-field__icon {
|
|
362
|
+
color: var(--muted);
|
|
363
|
+
flex: 0 0 auto;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.app-field__input {
|
|
367
|
+
flex: 1;
|
|
368
|
+
min-width: 0;
|
|
369
|
+
height: 100%;
|
|
370
|
+
border: none;
|
|
371
|
+
outline: none;
|
|
372
|
+
background: transparent;
|
|
373
|
+
font-family: inherit;
|
|
374
|
+
font-size: var(--fs-cell);
|
|
375
|
+
font-weight: 500;
|
|
376
|
+
color: var(--text);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.app-field__input::placeholder {
|
|
380
|
+
color: var(--muted);
|
|
381
|
+
opacity: 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* A `<select>` keeps the native menu; only the chrome is ours. */
|
|
385
|
+
select.app-field__input {
|
|
386
|
+
font-weight: 600;
|
|
387
|
+
font-size: var(--fs-nav);
|
|
388
|
+
appearance: none;
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.app-field--search {
|
|
393
|
+
flex: 1;
|
|
394
|
+
min-width: 220px;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.app-filter-row {
|
|
398
|
+
display: flex;
|
|
399
|
+
gap: var(--grid-gap);
|
|
400
|
+
margin-bottom: 14px;
|
|
401
|
+
flex-wrap: wrap;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@media (max-width: 599.98px) {
|
|
405
|
+
.app-filter-row > * {
|
|
406
|
+
flex: 1 1 100%;
|
|
407
|
+
min-width: 0;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ------------------------------------------------------------------ */
|
|
412
|
+
/* Responsive - the prototype has no reference, this follows Phase 1. */
|
|
413
|
+
/* ------------------------------------------------------------------ */
|
|
414
|
+
|
|
415
|
+
@media (max-width: 767.98px) {
|
|
416
|
+
.app-page-head {
|
|
417
|
+
align-items: flex-start;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* The grid table keeps its columns and scrolls sideways inside the card,
|
|
421
|
+
deliberately not restacked into key/value blocks: the columns carry
|
|
422
|
+
meaning by position. Same decision as Phase 1's tables. */
|
|
423
|
+
.app-table-scroll {
|
|
424
|
+
overflow-x: auto;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.app-table-scroll > * {
|
|
428
|
+
min-width: 720px;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE BUTTON.
|
|
3
|
+
|
|
4
|
+
A `<button>`, not a restyled `v-btn`. Vuetify's button carries an overlay
|
|
5
|
+
layer, a ripple, a min-width, uppercase letter-spacing and its own height
|
|
6
|
+
scale, all at a specificity a class has to out-shout one declaration at a
|
|
7
|
+
time - which is why the product's primary buttons still read as Vuetify grey
|
|
8
|
+
under a correct token set.
|
|
9
|
+
|
|
10
|
+
Variants, straight from the handoff:
|
|
11
|
+
primary 40px pill-radius accent fill, 13.5px/800, white label
|
|
12
|
+
ghost 38px, 1px border, card fill, 13px/700 in `--text2`
|
|
13
|
+
icon 32px transparent square, 8px radius
|
|
14
|
+
row 30px bordered square - the `more_vert` at the end of a table row
|
|
15
|
+
-->
|
|
16
|
+
<template>
|
|
17
|
+
<component
|
|
18
|
+
:is="to ? resolveLink : 'button'"
|
|
19
|
+
:to="to || undefined"
|
|
20
|
+
:type="to ? undefined : type"
|
|
21
|
+
:disabled="disabled || undefined"
|
|
22
|
+
class="app-btn"
|
|
23
|
+
:class="variant === 'primary' ? '' : `app-btn--${variant}`"
|
|
24
|
+
>
|
|
25
|
+
<v-icon v-if="icon" :icon="icon" :size="iconSize" />
|
|
26
|
+
<slot />
|
|
27
|
+
</component>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { resolveComponent } from "vue";
|
|
32
|
+
|
|
33
|
+
const props = defineProps({
|
|
34
|
+
variant: {
|
|
35
|
+
type: String as PropType<"primary" | "ghost" | "icon" | "row">,
|
|
36
|
+
default: "primary",
|
|
37
|
+
},
|
|
38
|
+
icon: { type: String, default: "" },
|
|
39
|
+
type: { type: String, default: "button" },
|
|
40
|
+
disabled: { type: Boolean, default: false },
|
|
41
|
+
/** Given, the button routes - same `to` a `v-btn` would have taken. */
|
|
42
|
+
to: { type: [String, Object] as PropType<any>, default: undefined },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const resolveLink = resolveComponent("NuxtLink");
|
|
46
|
+
|
|
47
|
+
const iconSize = computed(() => {
|
|
48
|
+
if (props.variant === "primary") return 18;
|
|
49
|
+
if (props.variant === "row") return 17;
|
|
50
|
+
return 18;
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE CARD.
|
|
3
|
+
|
|
4
|
+
14px radius, a 1px `--border` rule and the design's one-pixel shadow, on
|
|
5
|
+
`--card`. A plain element rather than a `v-card`, because `v-card` brings its
|
|
6
|
+
own elevation, radius and surface colour at a specificity that a class has to
|
|
7
|
+
fight - which is why the radius kept coming back as 4px in Phase 3.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<div class="app-card" :class="{ 'app-card--flush': flush }">
|
|
11
|
+
<slot />
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
defineProps({
|
|
17
|
+
/** Let content (a menu, a dropdown) escape the card's rounded corners. */
|
|
18
|
+
flush: { type: Boolean, default: false },
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE DATE FIELD.
|
|
3
|
+
|
|
4
|
+
The same 40px box with a calendar glyph, as the handoff draws the two date
|
|
5
|
+
ends of a filter row. `<input type="date">` - the browser's own picker, which
|
|
6
|
+
is the one a phone user already knows and the one that is localised, keyboard
|
|
7
|
+
accessible and free.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<label class="app-field" :class="{ 'app-field--compact': compact }">
|
|
11
|
+
<v-icon class="app-field__icon" icon="mdi-calendar-month-outline" size="17" />
|
|
12
|
+
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
13
|
+
<input
|
|
14
|
+
class="app-field__input"
|
|
15
|
+
type="date"
|
|
16
|
+
:value="model"
|
|
17
|
+
:min="min || undefined"
|
|
18
|
+
:max="max || undefined"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
@input="model = ($event.target as HTMLInputElement).value"
|
|
21
|
+
/>
|
|
22
|
+
</label>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
const model = defineModel({ type: String, default: "" });
|
|
27
|
+
|
|
28
|
+
defineProps({
|
|
29
|
+
label: { type: String, default: "" },
|
|
30
|
+
min: { type: String, default: "" },
|
|
31
|
+
max: { type: String, default: "" },
|
|
32
|
+
compact: { type: Boolean, default: false },
|
|
33
|
+
disabled: { type: Boolean, default: false },
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE TEXT FIELD.
|
|
3
|
+
|
|
4
|
+
40px (34px compact), 10px radius, a 1px border, 13.5px/500 - and the label
|
|
5
|
+
OUTSIDE the box as a small muted word, not Vuetify's outlined field with a
|
|
6
|
+
label that floats up through the border. A native `<input>`, so the browser
|
|
7
|
+
keeps the keyboard, autofill and accessibility for free.
|
|
8
|
+
-->
|
|
9
|
+
<template>
|
|
10
|
+
<label
|
|
11
|
+
class="app-field"
|
|
12
|
+
:class="[{ 'app-field--compact': compact, 'app-field--search': search }, klass]"
|
|
13
|
+
>
|
|
14
|
+
<v-icon
|
|
15
|
+
v-if="icon || search"
|
|
16
|
+
class="app-field__icon"
|
|
17
|
+
:icon="icon || 'mdi-magnify'"
|
|
18
|
+
size="18"
|
|
19
|
+
/>
|
|
20
|
+
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
21
|
+
<input
|
|
22
|
+
class="app-field__input"
|
|
23
|
+
:type="type"
|
|
24
|
+
:value="model"
|
|
25
|
+
:placeholder="placeholder"
|
|
26
|
+
:disabled="disabled"
|
|
27
|
+
@input="model = ($event.target as HTMLInputElement).value"
|
|
28
|
+
/>
|
|
29
|
+
</label>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
const model = defineModel({ type: String, default: "" });
|
|
34
|
+
|
|
35
|
+
defineProps({
|
|
36
|
+
label: { type: String, default: "" },
|
|
37
|
+
placeholder: { type: String, default: "" },
|
|
38
|
+
icon: { type: String, default: "" },
|
|
39
|
+
type: { type: String, default: "text" },
|
|
40
|
+
/** The design's search field: a magnifier and `flex:1` in a filter row. */
|
|
41
|
+
search: { type: Boolean, default: false },
|
|
42
|
+
compact: { type: Boolean, default: false },
|
|
43
|
+
disabled: { type: Boolean, default: false },
|
|
44
|
+
klass: { type: String, default: "" },
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE SELECT.
|
|
3
|
+
|
|
4
|
+
The handoff draws it as the text field with the filter's name in muted 12px
|
|
5
|
+
on the left, the chosen value next to it and a chevron on the right. A native
|
|
6
|
+
`<select>` under our chrome: the option list, the keyboard and the mobile
|
|
7
|
+
wheel are the browser's, so there is no menu to build and nothing to get
|
|
8
|
+
wrong on a phone.
|
|
9
|
+
-->
|
|
10
|
+
<template>
|
|
11
|
+
<label class="app-field" :class="{ 'app-field--compact': compact }">
|
|
12
|
+
<span v-if="label" class="app-field__label">{{ label }}</span>
|
|
13
|
+
<select
|
|
14
|
+
class="app-field__input"
|
|
15
|
+
:value="model"
|
|
16
|
+
:disabled="disabled"
|
|
17
|
+
@change="model = ($event.target as HTMLSelectElement).value"
|
|
18
|
+
>
|
|
19
|
+
<option v-for="opt in normalized" :key="opt.value" :value="opt.value">
|
|
20
|
+
{{ opt.title }}
|
|
21
|
+
</option>
|
|
22
|
+
</select>
|
|
23
|
+
<v-icon class="app-field__icon" icon="mdi-chevron-down" size="17" />
|
|
24
|
+
</label>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
const model = defineModel({ type: String, default: "" });
|
|
29
|
+
|
|
30
|
+
const props = defineProps({
|
|
31
|
+
label: { type: String, default: "" },
|
|
32
|
+
/** Strings, or the `{ title, value }` shape `v-select` already takes. */
|
|
33
|
+
items: { type: Array as PropType<any[]>, default: () => [] },
|
|
34
|
+
itemTitle: { type: String, default: "title" },
|
|
35
|
+
itemValue: { type: String, default: "value" },
|
|
36
|
+
compact: { type: Boolean, default: false },
|
|
37
|
+
disabled: { type: Boolean, default: false },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const normalized = computed(() =>
|
|
41
|
+
props.items.map((item: any) =>
|
|
42
|
+
typeof item === "object" && item !== null
|
|
43
|
+
? { title: item[props.itemTitle], value: item[props.itemValue] }
|
|
44
|
+
: { title: String(item), value: item }
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE IN-CARD TOP ROW.
|
|
3
|
+
|
|
4
|
+
The design does NOT put a screen's tabs above its card - they are the card's
|
|
5
|
+
own first row, with the pagination on the opposite end of that same line, and
|
|
6
|
+
the tab underline meets the card's rule. Today those tabs sit in a
|
|
7
|
+
`v-toolbar`'s extension slot, which stacks them BELOW a separate bar and
|
|
8
|
+
costs a whole row of height.
|
|
9
|
+
|
|
10
|
+
Two shapes, both from the handoff: with tabs (`0 20px`, the tabs supply the
|
|
11
|
+
vertical padding) and without (`10px 14px`, a refresh button on the left).
|
|
12
|
+
This component only places things - the tabs, the count and the pager are
|
|
13
|
+
passed in and keep their own behaviour.
|
|
14
|
+
-->
|
|
15
|
+
<template>
|
|
16
|
+
<div class="app-toolbar" :class="{ 'app-toolbar--plain': !$slots.tabs }">
|
|
17
|
+
<div v-if="$slots.tabs" class="app-tabs" role="tablist">
|
|
18
|
+
<slot name="tabs" />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<slot name="left" />
|
|
22
|
+
|
|
23
|
+
<div class="app-toolbar__spacer" />
|
|
24
|
+
|
|
25
|
+
<div class="app-toolbar__right">
|
|
26
|
+
<span v-if="count" class="app-toolbar__count">{{ count }}</span>
|
|
27
|
+
<slot name="right" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
defineProps({
|
|
34
|
+
/** The design's `1-1 of 1` range, printed as the screen already computes it. */
|
|
35
|
+
count: { type: String, default: "" },
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
@@ -1,68 +1,65 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
<div>
|
|
3
|
+
<!--
|
|
4
|
+
THE DESIGN'S PAGE SCAFFOLD.
|
|
5
|
+
|
|
6
|
+
The handoff opens Members with a 22px title row and puts the tabs INSIDE
|
|
7
|
+
the card's first row, with the pagination on the far end of that same
|
|
8
|
+
line. Before this the screen had no title at all - the page name existed
|
|
9
|
+
only in the rail and the browser tab - and the tabs sat in a toolbar
|
|
10
|
+
extension, i.e. a second stacked bar below a mostly empty one.
|
|
11
|
+
|
|
12
|
+
Nothing here changes what the screen does: the same two tabs route the
|
|
13
|
+
same way, the same refresh and pager call the same `getAll()`, the same
|
|
14
|
+
`headers` prop drives the same columns, and the same menu carries the
|
|
15
|
+
same four actions behind the same permission conditions.
|
|
16
|
+
-->
|
|
17
|
+
<PageHeader :title="props.title">
|
|
18
|
+
<template v-if="props.seatManagement !== 'index'" #actions>
|
|
19
|
+
<AppButton
|
|
9
20
|
:to="{
|
|
10
21
|
name: props.seatManagement,
|
|
11
22
|
params: { organization: props.orgId },
|
|
12
23
|
}"
|
|
13
|
-
size="large"
|
|
14
24
|
>
|
|
15
25
|
Manage seats
|
|
16
|
-
</
|
|
17
|
-
</
|
|
18
|
-
</
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
>
|
|
54
|
-
<v-tab
|
|
55
|
-
v-for="(tab, i) in tabOptions"
|
|
56
|
-
:value="tab.status"
|
|
57
|
-
:key="i"
|
|
58
|
-
class="text-capitalize"
|
|
59
|
-
>
|
|
60
|
-
{{ tab.name }}
|
|
61
|
-
</v-tab>
|
|
62
|
-
</v-tabs>
|
|
63
|
-
</template>
|
|
64
|
-
</v-toolbar>
|
|
65
|
-
|
|
26
|
+
</AppButton>
|
|
27
|
+
</template>
|
|
28
|
+
</PageHeader>
|
|
29
|
+
|
|
30
|
+
<AppCard class="table-card">
|
|
31
|
+
<CardToolbar :count="pageRange">
|
|
32
|
+
<template #tabs>
|
|
33
|
+
<button
|
|
34
|
+
v-for="(tab, i) in tabOptions"
|
|
35
|
+
:key="i"
|
|
36
|
+
type="button"
|
|
37
|
+
role="tab"
|
|
38
|
+
class="app-tab"
|
|
39
|
+
:class="{ 'app-tab--active': selectedStatus === tab.status }"
|
|
40
|
+
:aria-selected="selectedStatus === tab.status"
|
|
41
|
+
@click="toRoute(tab.status)"
|
|
42
|
+
>
|
|
43
|
+
{{ tab.name }}
|
|
44
|
+
</button>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<template #right>
|
|
48
|
+
<AppButton
|
|
49
|
+
variant="icon"
|
|
50
|
+
icon="mdi-refresh"
|
|
51
|
+
aria-label="Refresh"
|
|
52
|
+
@click="getAll()"
|
|
53
|
+
/>
|
|
54
|
+
<local-pagination
|
|
55
|
+
v-model="page"
|
|
56
|
+
:length="pages"
|
|
57
|
+
@update:value="getAll()"
|
|
58
|
+
/>
|
|
59
|
+
</template>
|
|
60
|
+
</CardToolbar>
|
|
61
|
+
|
|
62
|
+
<div class="app-table-scroll">
|
|
66
63
|
<v-data-table
|
|
67
64
|
:headers="props.headers"
|
|
68
65
|
:items="items"
|
|
@@ -76,6 +73,13 @@
|
|
|
76
73
|
<template #item.nature="{ item }">
|
|
77
74
|
{{ replaceMatch(item.nature, "_", " ") }}
|
|
78
75
|
</template>
|
|
76
|
+
<!-- The role is a soft pill in the design, not a plain word. Same
|
|
77
|
+
text, same column, same source field. -->
|
|
78
|
+
<template #item.roleName="{ item }">
|
|
79
|
+
<StatusChip v-if="item.roleName" :dot="false" pill tone="info">
|
|
80
|
+
{{ item.roleName }}
|
|
81
|
+
</StatusChip>
|
|
82
|
+
</template>
|
|
79
83
|
<template #item.dateInvited="{ item }">
|
|
80
84
|
{{ formatDateInvited(item.dateInvited) }}
|
|
81
85
|
</template>
|
|
@@ -92,8 +96,13 @@
|
|
|
92
96
|
offset-y
|
|
93
97
|
width="150"
|
|
94
98
|
>
|
|
95
|
-
<template v-slot:activator="{ props }">
|
|
96
|
-
<
|
|
99
|
+
<template v-slot:activator="{ props: menuProps }">
|
|
100
|
+
<AppButton
|
|
101
|
+
v-bind="menuProps"
|
|
102
|
+
variant="row"
|
|
103
|
+
icon="mdi-dots-vertical"
|
|
104
|
+
aria-label="Member actions"
|
|
105
|
+
/>
|
|
97
106
|
</template>
|
|
98
107
|
<v-list>
|
|
99
108
|
<v-list-item
|
|
@@ -131,8 +140,9 @@
|
|
|
131
140
|
</v-menu>
|
|
132
141
|
</template>
|
|
133
142
|
</v-data-table>
|
|
134
|
-
</
|
|
135
|
-
</
|
|
143
|
+
</div>
|
|
144
|
+
</AppCard>
|
|
145
|
+
|
|
136
146
|
<ConfirmDialog
|
|
137
147
|
v-model="confirmDialog"
|
|
138
148
|
:loading="updateLoading"
|
|
@@ -235,11 +245,17 @@
|
|
|
235
245
|
</v-card-text>
|
|
236
246
|
</v-card>
|
|
237
247
|
</v-dialog>
|
|
238
|
-
</
|
|
248
|
+
</div>
|
|
239
249
|
</template>
|
|
240
250
|
|
|
241
251
|
<script setup lang="ts">
|
|
242
252
|
const props = defineProps({
|
|
253
|
+
/** The design's page title. Every app that mounts this screen calls it
|
|
254
|
+
Members; a caller that names it otherwise passes its own. */
|
|
255
|
+
title: {
|
|
256
|
+
type: String,
|
|
257
|
+
default: "Members",
|
|
258
|
+
},
|
|
243
259
|
orgId: {
|
|
244
260
|
type: String,
|
|
245
261
|
default: "",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
THE PAGE TITLE ROW.
|
|
3
|
+
|
|
4
|
+
The design opens EVERY screen with this row: the page's name at 22px/800,
|
|
5
|
+
and the screen's primary action on the right of the same line. The product
|
|
6
|
+
has never had it - the page name lived only in the browser tab and in the
|
|
7
|
+
navigation rail - which is the single most visible difference between the
|
|
8
|
+
staging site and the handoff.
|
|
9
|
+
|
|
10
|
+
It is a label and a slot. It fetches nothing, decides nothing and navigates
|
|
11
|
+
nowhere; the action passed into it is the screen's own button.
|
|
12
|
+
-->
|
|
13
|
+
<template>
|
|
14
|
+
<div class="app-page-head">
|
|
15
|
+
<div>
|
|
16
|
+
<h1 class="app-page-head__title">
|
|
17
|
+
<slot name="title">{{ title }}</slot>
|
|
18
|
+
</h1>
|
|
19
|
+
|
|
20
|
+
<!-- The design's second line: a location, a count, a date range. Only
|
|
21
|
+
drawn when a screen supplies one. -->
|
|
22
|
+
<div v-if="subtitle || $slots.subtitle" class="app-page-head__sub">
|
|
23
|
+
<v-icon v-if="subtitleIcon" :icon="subtitleIcon" size="15" />
|
|
24
|
+
<slot name="subtitle">{{ subtitle }}</slot>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div v-if="$slots.actions" class="app-page-head__actions">
|
|
29
|
+
<slot name="actions" />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
defineProps({
|
|
36
|
+
title: { type: String, default: "" },
|
|
37
|
+
subtitle: { type: String, default: "" },
|
|
38
|
+
subtitleIcon: { type: String, default: "" },
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
translate or shorten a status.
|
|
11
11
|
-->
|
|
12
12
|
<template>
|
|
13
|
-
<span
|
|
13
|
+
<span
|
|
14
|
+
class="status-chip"
|
|
15
|
+
:class="[toneClass, { 'status-chip--plain': !dot && !pill, 'status-chip--pill': pill }]"
|
|
16
|
+
>
|
|
14
17
|
<span v-if="dot" class="status-chip__dot" />
|
|
15
18
|
<!-- The slot is for the chips that already carried an icon next to their
|
|
16
19
|
word - a padlock on "Closed - View Only" - so adopting the design's
|
|
@@ -27,8 +30,14 @@ const props = defineProps({
|
|
|
27
30
|
status: { type: String, default: "" },
|
|
28
31
|
/** What to PRINT, when that differs from the word being matched on. */
|
|
29
32
|
label: { type: String, default: undefined },
|
|
30
|
-
/** The design's tag chips (pass IDs,
|
|
33
|
+
/** The design's tag chips (pass IDs, counts) carry no leading dot. */
|
|
31
34
|
dot: { type: Boolean, default: true },
|
|
35
|
+
/**
|
|
36
|
+
* The handoff draws TWO dotless chips, and they are not the same shape: the
|
|
37
|
+
* 7px-radius 11px tag on a pass, and the 999px 11.5px pill a role name sits
|
|
38
|
+
* in on the Members screen. `pill` picks the second.
|
|
39
|
+
*/
|
|
40
|
+
pill: { type: Boolean, default: false },
|
|
32
41
|
/**
|
|
33
42
|
* For the chips that are not a status WORD at all - a countdown, a rating, a
|
|
34
43
|
* count - where the screen has already worked out the meaning and only needs
|
|
@@ -68,6 +77,12 @@ const toneClass = computed(() =>
|
|
|
68
77
|
font-size: var(--fs-chip);
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
/* The role tag: `padding:3px 10px;border-radius:999px;font-size:11.5px`. */
|
|
81
|
+
.status-chip--pill {
|
|
82
|
+
padding: 3px 10px;
|
|
83
|
+
font-size: var(--fs-chip);
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
.status-chip__dot {
|
|
72
87
|
width: 6px;
|
|
73
88
|
height: 6px;
|
package/package.json
CHANGED
package/plugins/vuetify.ts
CHANGED