@7365admin1/layer-common 3.2.2-staging.126 → 3.2.2-staging.127
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/components/AreaFormDialog.vue +71 -59
- package/components/AttachmentsViewer.vue +84 -22
- package/components/FileInput.vue +75 -58
- package/components/PhotoUpload.vue +110 -48
- package/package.json +1 -1
|
@@ -1,30 +1,40 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-dialog v-model="showDialog" max-width="450">
|
|
3
|
-
<v-card>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
<v-card class="screen-modal">
|
|
4
|
+
<!-- Was a bare `v-toolbar`, which Vuetify paints from its own `surface` -
|
|
5
|
+
a grey slab across the top of the card, visibly lighter than the card
|
|
6
|
+
on the dark theme - carrying a `text-h6` (20px/500 Roboto) heading.
|
|
7
|
+
The design's modal heading is the `v-card-title` row on the card's own
|
|
8
|
+
surface, and it carries the design's own title size and weight. The
|
|
9
|
+
`header` slot keeps its title/close payload unchanged. -->
|
|
10
|
+
<template v-if="$slots.header">
|
|
11
|
+
<slot
|
|
12
|
+
name="header"
|
|
13
|
+
:title="
|
|
14
|
+
prop.mode === 'edit'
|
|
15
|
+
? `Edit ${prop.entityType}`
|
|
16
|
+
: `Add New ${prop.entityType}`
|
|
17
|
+
"
|
|
18
|
+
:close="close"
|
|
19
|
+
/>
|
|
20
|
+
</template>
|
|
21
|
+
<v-card-title v-else class="d-flex align-center text-capitalize">
|
|
22
|
+
{{
|
|
23
|
+
prop.mode === "edit"
|
|
24
|
+
? `Edit ${prop.entityType}`
|
|
25
|
+
: `Add New ${prop.entityType}`
|
|
26
|
+
}}
|
|
27
|
+
<v-spacer />
|
|
28
|
+
<v-btn
|
|
29
|
+
variant="text"
|
|
30
|
+
icon="mdi-close"
|
|
31
|
+
class="screen-modal__close"
|
|
32
|
+
aria-label="Close"
|
|
33
|
+
@click="close"
|
|
34
|
+
/>
|
|
35
|
+
</v-card-title>
|
|
26
36
|
|
|
27
|
-
<v-card-text class="
|
|
37
|
+
<v-card-text class="screen-modal__body">
|
|
28
38
|
<v-form ref="formRef" v-model="valid">
|
|
29
39
|
<v-row no-gutters class="pa-0">
|
|
30
40
|
<v-col v-if="prop.showType" cols="12" class="pa-0 mb-2">
|
|
@@ -35,7 +45,7 @@
|
|
|
35
45
|
density="comfortable"
|
|
36
46
|
variant="outlined"
|
|
37
47
|
hide-details
|
|
38
|
-
class="w-100 mb-0"
|
|
48
|
+
class="filter-field w-100 mb-0"
|
|
39
49
|
/>
|
|
40
50
|
</v-col>
|
|
41
51
|
|
|
@@ -49,7 +59,7 @@
|
|
|
49
59
|
<v-text-field
|
|
50
60
|
v-model="model"
|
|
51
61
|
:rules="[requiredRule]"
|
|
52
|
-
class="mb-0"
|
|
62
|
+
class="filter-field mb-0"
|
|
53
63
|
/>
|
|
54
64
|
</v-col>
|
|
55
65
|
|
|
@@ -68,18 +78,21 @@
|
|
|
68
78
|
chips
|
|
69
79
|
closable-chips
|
|
70
80
|
placeholder="Select units"
|
|
71
|
-
class="mb-0"
|
|
81
|
+
class="filter-field mb-0"
|
|
72
82
|
/>
|
|
73
83
|
</v-col>
|
|
74
84
|
|
|
75
85
|
<v-col v-if="prop.showOptional" cols="12" class="mb-4">
|
|
76
86
|
<v-row dense justify="center">
|
|
77
87
|
<v-col cols="6" class="pa-0">
|
|
88
|
+
<!-- `color="primary"` on a `v-btn` is the design's FOREGROUND
|
|
89
|
+
token; this is a plain text toggle, so it takes the
|
|
90
|
+
accent's INK form, which the theme test already holds at
|
|
91
|
+
>= 4.5:1 on the card. -->
|
|
78
92
|
<v-btn
|
|
79
93
|
block
|
|
80
|
-
color="primary"
|
|
81
94
|
variant="text"
|
|
82
|
-
class="text-none font-weight-bold"
|
|
95
|
+
class="text-none font-weight-bold area-form-dialog__optional"
|
|
83
96
|
text="Optional"
|
|
84
97
|
@click="show = !show"
|
|
85
98
|
></v-btn>
|
|
@@ -101,6 +114,7 @@
|
|
|
101
114
|
density="comfortable"
|
|
102
115
|
type="number"
|
|
103
116
|
min="1"
|
|
117
|
+
class="filter-field"
|
|
104
118
|
></v-text-field>
|
|
105
119
|
</v-col>
|
|
106
120
|
</v-row>
|
|
@@ -111,36 +125,24 @@
|
|
|
111
125
|
</v-form>
|
|
112
126
|
</v-card-text>
|
|
113
127
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
variant="flat"
|
|
133
|
-
color="black"
|
|
134
|
-
class="text-none font-weight-bold rounded-0"
|
|
135
|
-
height="48"
|
|
136
|
-
:loading="submitting"
|
|
137
|
-
@click="submit"
|
|
138
|
-
>
|
|
139
|
-
{{ prop.mode === "edit" ? "Save" : "Create" }}
|
|
140
|
-
</v-btn>
|
|
141
|
-
</v-col>
|
|
142
|
-
</v-row>
|
|
143
|
-
</v-toolbar>
|
|
128
|
+
<!-- Was a second `v-toolbar` holding two square 48px bars - the primary a
|
|
129
|
+
hardcoded `color="black"`, which is neither theme's accent and reads
|
|
130
|
+
as a hole in the dark modal - with `rounded-0` forcing 0px corners.
|
|
131
|
+
This is the design's shared modal footer: a bordered strip with a
|
|
132
|
+
ghost secondary and the primary beside it, both 40px at 10px. -->
|
|
133
|
+
<div class="screen-modal__footer">
|
|
134
|
+
<v-btn class="text-none screen-btn-ghost" variant="outlined" @click="close">
|
|
135
|
+
Cancel
|
|
136
|
+
</v-btn>
|
|
137
|
+
<v-btn
|
|
138
|
+
class="text-none screen-btn-primary"
|
|
139
|
+
variant="flat"
|
|
140
|
+
:loading="submitting"
|
|
141
|
+
@click="submit"
|
|
142
|
+
>
|
|
143
|
+
{{ prop.mode === "edit" ? "Save" : "Create" }}
|
|
144
|
+
</v-btn>
|
|
145
|
+
</div>
|
|
144
146
|
</v-card>
|
|
145
147
|
</v-dialog>
|
|
146
148
|
</template>
|
|
@@ -268,3 +270,13 @@ async function submit() {
|
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
</script>
|
|
273
|
+
|
|
274
|
+
<style scoped>
|
|
275
|
+
/* The "Optional" reveal is plain text, not a filled button, so it takes the
|
|
276
|
+
accent's INK form. `--accent-strong` is the FILL that a white label sits on;
|
|
277
|
+
`--accent-text` is the one `utils/theme.test.ts` holds at >= 4.5:1 against
|
|
278
|
+
the card in both themes. */
|
|
279
|
+
.area-form-dialog__optional {
|
|
280
|
+
color: var(--accent-text);
|
|
281
|
+
}
|
|
282
|
+
</style>
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
<!-- Loading Skeleton -->
|
|
8
8
|
<template v-if="loading && files?.length > 0">
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
<!-- `border="sm black"` painted a literal black hairline on
|
|
10
|
+
both themes; the design's rule is `--border`. -->
|
|
11
|
+
<v-card v-for="(file, index) in files" :key="index" height="50px" flat
|
|
12
|
+
class="att-skeleton mb-2">
|
|
11
13
|
<v-skeleton-loader type="list-item" class="mb-2" />
|
|
12
14
|
</v-card>
|
|
13
15
|
</template>
|
|
@@ -17,7 +19,7 @@
|
|
|
17
19
|
|
|
18
20
|
<!-- Videos -->
|
|
19
21
|
<template v-if="videosArray.length > 0">
|
|
20
|
-
<div v-if="showFileTypeTitle" class="
|
|
22
|
+
<div v-if="showFileTypeTitle" class="att-group mb-1">Videos ({{
|
|
21
23
|
videosArray.length }})</div>
|
|
22
24
|
<v-list density="compact" class="pa-0 rounded-lg border mb-2">
|
|
23
25
|
<template v-for="(item, i) in videosArray" :key="item._id">
|
|
@@ -26,11 +28,11 @@
|
|
|
26
28
|
:subtitle="item.mimeType" class="cursor-pointer"
|
|
27
29
|
@click="openMediaViewer(item._id, item.mimeType)">
|
|
28
30
|
<template #prepend>
|
|
29
|
-
<v-icon :
|
|
31
|
+
<v-icon :class="[getFileIconClass(item.mimeType), 'mr-3']">{{
|
|
30
32
|
getFileIcon(item.mimeType) }}</v-icon>
|
|
31
33
|
</template>
|
|
32
34
|
<template #append>
|
|
33
|
-
<v-icon size="16"
|
|
35
|
+
<v-icon size="16" class="att-icon--muted">mdi-play-circle-outline</v-icon>
|
|
34
36
|
</template>
|
|
35
37
|
</v-list-item>
|
|
36
38
|
</template>
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
|
|
40
42
|
<!-- Photos -->
|
|
41
43
|
<template v-if="photosArray.length > 0">
|
|
42
|
-
<div v-if="showFileTypeTitle" class="
|
|
44
|
+
<div v-if="showFileTypeTitle" class="att-group mb-1"
|
|
43
45
|
:class="{ 'mt-3': videosArray.length > 0 }">
|
|
44
46
|
Photos ({{ photosArray.length }})</div>
|
|
45
47
|
<v-list density="compact" class="pa-0 rounded-lg border mb-2">
|
|
@@ -50,16 +52,16 @@
|
|
|
50
52
|
<v-avatar size="36" rounded="sm" class="mr-3">
|
|
51
53
|
<v-img :src="imageCover(item._id)" cover>
|
|
52
54
|
<template #error>
|
|
53
|
-
<v-icon
|
|
55
|
+
<v-icon size="20" class="att-icon--ok">mdi-image</v-icon>
|
|
54
56
|
</template>
|
|
55
57
|
</v-img>
|
|
56
58
|
</v-avatar>
|
|
57
59
|
</template>
|
|
58
|
-
<v-list-item-title class="
|
|
59
|
-
<v-list-item-subtitle class="
|
|
60
|
+
<v-list-item-title class="att-name">{{ item.file.name }}</v-list-item-title>
|
|
61
|
+
<v-list-item-subtitle class="att-meta">{{ item.mimeType
|
|
60
62
|
}}</v-list-item-subtitle>
|
|
61
63
|
<template #append>
|
|
62
|
-
<v-icon size="16"
|
|
64
|
+
<v-icon size="16" class="att-icon--muted">mdi-eye-outline</v-icon>
|
|
63
65
|
</template>
|
|
64
66
|
</v-list-item>
|
|
65
67
|
</template>
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
|
|
69
71
|
<!-- Documents & Other Files -->
|
|
70
72
|
<template v-if="filesArray.length > 0">
|
|
71
|
-
<div v-if="showFileTypeTitle" class="
|
|
73
|
+
<div v-if="showFileTypeTitle" class="att-group mb-1"
|
|
72
74
|
:class="{ 'mt-3': videosArray.length > 0 || photosArray.length > 0 }">Files ({{
|
|
73
75
|
filesArray.length }})</div>
|
|
74
76
|
<v-list density="compact" class="pa-0 rounded-lg border mb-2">
|
|
@@ -77,14 +79,14 @@
|
|
|
77
79
|
<v-list-item class="cursor-pointer"
|
|
78
80
|
@click="openDocumentViewer(item._id, item.file.name, item.mimeType)">
|
|
79
81
|
<template #prepend>
|
|
80
|
-
<v-icon :
|
|
82
|
+
<v-icon :class="[getFileIconClass(item.mimeType), 'mr-3']">{{
|
|
81
83
|
getFileIcon(item.mimeType) }}</v-icon>
|
|
82
84
|
</template>
|
|
83
|
-
<v-list-item-title class="
|
|
84
|
-
<v-list-item-subtitle class="
|
|
85
|
+
<v-list-item-title class="att-name">{{ item.file.name }}</v-list-item-title>
|
|
86
|
+
<v-list-item-subtitle class="att-meta">{{ item.mimeType
|
|
85
87
|
}}</v-list-item-subtitle>
|
|
86
88
|
<template #append>
|
|
87
|
-
<v-icon size="16"
|
|
89
|
+
<v-icon size="16" class="att-icon--muted">mdi-open-in-new</v-icon>
|
|
88
90
|
</template>
|
|
89
91
|
</v-list-item>
|
|
90
92
|
</template>
|
|
@@ -185,13 +187,20 @@ function getFileIcon(mimeType: string): string {
|
|
|
185
187
|
return 'mdi-file-outline';
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Was Vuetify's raw palette names - `red`, `blue`, `green`, `orange`, `purple`,
|
|
192
|
+
* `grey` - which are the same six hexes in both themes and none of them a token.
|
|
193
|
+
* The handoff draws NO file-type icon palette, so this keeps the distinctions
|
|
194
|
+
* the screen already made and states each one in the token that already carries
|
|
195
|
+
* that tone. `purple` has no counterpart at all, so video takes the muted tone
|
|
196
|
+
* with the rest; that one IS a change of meaning and is recorded as such.
|
|
197
|
+
*/
|
|
198
|
+
function getFileIconClass(mimeType: string): string {
|
|
199
|
+
if (mimeType === 'application/pdf') return 'att-icon--err';
|
|
200
|
+
if (mimeType.includes('word') || mimeType.includes('document')) return 'att-icon--info';
|
|
201
|
+
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'att-icon--ok';
|
|
202
|
+
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'att-icon--warn';
|
|
203
|
+
return 'att-icon--muted';
|
|
195
204
|
}
|
|
196
205
|
|
|
197
206
|
function getByType(prefix: 'image/' | 'video/' | 'other') {
|
|
@@ -311,3 +320,56 @@ function inferMimeType(ext?: string): string {
|
|
|
311
320
|
return mimeMap[ext] || 'application/octet-stream';
|
|
312
321
|
}
|
|
313
322
|
</script>
|
|
323
|
+
|
|
324
|
+
<style scoped>
|
|
325
|
+
/* The group heading was `text-caption text-medium-emphasis` - Vuetify's 12px
|
|
326
|
+
Roboto at 60% alpha. The design's sub-line is `--muted` at the card's own
|
|
327
|
+
caption size. */
|
|
328
|
+
.att-group {
|
|
329
|
+
font-family: var(--font-sans);
|
|
330
|
+
font-size: 12px;
|
|
331
|
+
font-weight: 700;
|
|
332
|
+
color: var(--muted);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Row title and meta were `text-body-2` / `text-caption`, i.e. Roboto again.
|
|
336
|
+
These take the design's cell type, the same pair every converted table row
|
|
337
|
+
uses. */
|
|
338
|
+
.att-name {
|
|
339
|
+
font-family: var(--font-sans);
|
|
340
|
+
font-size: 13.5px;
|
|
341
|
+
font-weight: 600;
|
|
342
|
+
color: var(--text);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.att-meta {
|
|
346
|
+
font-family: var(--font-sans);
|
|
347
|
+
font-size: 12px;
|
|
348
|
+
color: var(--muted);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.att-skeleton {
|
|
352
|
+
border: 1px solid var(--border);
|
|
353
|
+
border-radius: var(--r-inner);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.att-icon--err {
|
|
357
|
+
color: var(--err);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.att-icon--info {
|
|
361
|
+
color: var(--info);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.att-icon--ok {
|
|
365
|
+
color: var(--ok);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.att-icon--warn {
|
|
369
|
+
color: var(--warn);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.att-icon--muted {
|
|
373
|
+
color: var(--muted);
|
|
374
|
+
}
|
|
375
|
+
</style>
|
package/components/FileInput.vue
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
accept="image/*"
|
|
7
7
|
:prepend-icon="prependIcon"
|
|
8
8
|
:loading="isFileUploading"
|
|
9
|
+
variant="outlined"
|
|
10
|
+
density="comfortable"
|
|
11
|
+
class="filter-field"
|
|
9
12
|
hide-details
|
|
10
13
|
chips
|
|
11
14
|
multiple
|
|
@@ -16,7 +19,16 @@
|
|
|
16
19
|
>
|
|
17
20
|
<template v-slot:append v-if="hasLabel">
|
|
18
21
|
<slot name="append">
|
|
19
|
-
|
|
22
|
+
<!-- `color="primary"` on a `v-btn` is the design's FOREGROUND token,
|
|
23
|
+
so this drew accent-on-accent; the fill is `primary-button`, which
|
|
24
|
+
is what `.screen-btn-primary` states. `height="50px"` was also
|
|
25
|
+
10px taller than the design's 40px button. -->
|
|
26
|
+
<v-btn
|
|
27
|
+
class="text-none screen-btn-primary"
|
|
28
|
+
variant="flat"
|
|
29
|
+
aria-label="Take a picture"
|
|
30
|
+
@click="openCameraDialog"
|
|
31
|
+
>
|
|
20
32
|
<v-icon>mdi-camera</v-icon>
|
|
21
33
|
</v-btn>
|
|
22
34
|
</slot>
|
|
@@ -32,64 +44,69 @@
|
|
|
32
44
|
persistent
|
|
33
45
|
@after-enter="startCamera"
|
|
34
46
|
>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
<v-card
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
<!-- Was a `v-container` wrapping a factory 4px `v-card` with a bare
|
|
48
|
+
`v-toolbar` across the top - Vuetify paints that toolbar from its own
|
|
49
|
+
`surface`, a grey slab visibly lighter than the card on dark. The
|
|
50
|
+
design's modal is the card itself: 16px radius on `--card`, a
|
|
51
|
+
`v-card-title` row, and the borderless 32px close. -->
|
|
52
|
+
<v-card class="screen-modal d-flex flex-column align-center">
|
|
53
|
+
<v-card-title class="d-flex align-center w-100">
|
|
54
|
+
Take a Picture
|
|
55
|
+
<v-spacer />
|
|
56
|
+
<v-btn
|
|
57
|
+
variant="text"
|
|
58
|
+
icon="mdi-close"
|
|
59
|
+
class="screen-modal__close"
|
|
60
|
+
aria-label="Close camera"
|
|
61
|
+
@click="closeCameraDialog"
|
|
62
|
+
/>
|
|
63
|
+
</v-card-title>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
id="reader"
|
|
67
|
+
class="d-flex justify-center align-center"
|
|
68
|
+
style="
|
|
69
|
+
position: relative;
|
|
70
|
+
width: 500px;
|
|
71
|
+
min-width: 400px;
|
|
72
|
+
height: 400px;
|
|
73
|
+
"
|
|
74
|
+
>
|
|
75
|
+
<video
|
|
76
|
+
ref="video"
|
|
77
|
+
style="flex: 1; height: 400px; min-width: 300px"
|
|
78
|
+
class="video-shutter"
|
|
79
|
+
autoplay
|
|
80
|
+
></video>
|
|
81
|
+
<canvas
|
|
82
|
+
ref="canvas"
|
|
83
|
+
style="flex: 1; height: 400px; min-width: 300px; display: none"
|
|
84
|
+
></canvas>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Was a two-column `v-row` of Vuetify `primary` / `secondary` icon
|
|
88
|
+
buttons - `secondary` is not a design token at all, and `primary` is
|
|
89
|
+
the foreground one. The design's icon button is the 36px bordered
|
|
90
|
+
square, and the capture action is the primary of the pair, so it
|
|
91
|
+
sits on the shared modal footer in the primary's place. -->
|
|
92
|
+
<div class="screen-modal__footer w-100">
|
|
93
|
+
<v-btn
|
|
94
|
+
class="text-none screen-btn-ghost"
|
|
95
|
+
variant="outlined"
|
|
96
|
+
aria-label="Switch camera"
|
|
97
|
+
@click="switchCamera"
|
|
98
|
+
>
|
|
99
|
+
<v-icon>mdi-camera-switch</v-icon>
|
|
100
|
+
</v-btn>
|
|
101
|
+
<v-btn
|
|
102
|
+
class="text-none screen-btn-primary"
|
|
103
|
+
variant="flat"
|
|
104
|
+
@click="captureImageFromCamera"
|
|
61
105
|
>
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
autoplay
|
|
67
|
-
></video>
|
|
68
|
-
<canvas
|
|
69
|
-
ref="canvas"
|
|
70
|
-
style="flex: 1; height: 400px; min-width: 300px; display: none"
|
|
71
|
-
></canvas>
|
|
72
|
-
</div>
|
|
73
|
-
|
|
74
|
-
<v-row align="center" justify="center">
|
|
75
|
-
<v-col cols="6">
|
|
76
|
-
<v-btn color="primary" icon class="mt-4" @click="switchCamera">
|
|
77
|
-
<v-icon>mdi-camera-switch</v-icon>
|
|
78
|
-
</v-btn>
|
|
79
|
-
</v-col>
|
|
80
|
-
<v-col cols="6">
|
|
81
|
-
<v-btn
|
|
82
|
-
color="secondary"
|
|
83
|
-
icon
|
|
84
|
-
class="mt-4"
|
|
85
|
-
@click="captureImageFromCamera"
|
|
86
|
-
>
|
|
87
|
-
<v-icon large>mdi-camera-outline</v-icon>
|
|
88
|
-
</v-btn>
|
|
89
|
-
</v-col>
|
|
90
|
-
</v-row>
|
|
91
|
-
</v-card>
|
|
92
|
-
</v-container>
|
|
106
|
+
<v-icon>mdi-camera-outline</v-icon>
|
|
107
|
+
</v-btn>
|
|
108
|
+
</div>
|
|
109
|
+
</v-card>
|
|
93
110
|
</v-dialog>
|
|
94
111
|
</div>
|
|
95
112
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters class="mb-6">
|
|
3
3
|
<v-col cols="12">
|
|
4
|
-
<div class="
|
|
4
|
+
<div class="photo-upload__title mb-3">
|
|
5
5
|
{{ title }}
|
|
6
|
-
<span v-if="required" class="
|
|
6
|
+
<span v-if="required" class="photo-upload__req">*</span>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
9
|
<v-sheet
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
<v-icon
|
|
18
18
|
:icon="uploading ? 'mdi-loading mdi-spin' : 'mdi-camera-plus-outline'"
|
|
19
19
|
size="56"
|
|
20
|
-
:color="uploading ? 'grey' : 'primary'"
|
|
21
20
|
class="mb-3"
|
|
21
|
+
:class="uploading ? 'photo-upload__icon--wait' : 'photo-upload__icon'"
|
|
22
22
|
/>
|
|
23
|
-
<div class="
|
|
23
|
+
<div class="photo-upload__lead mb-1">
|
|
24
24
|
{{ uploading ? "Uploading Photo..." : "Add Photo" }}
|
|
25
25
|
</div>
|
|
26
|
-
<div class="
|
|
26
|
+
<div class="photo-upload__hint">
|
|
27
27
|
{{ uploading ? "Please wait" : "Take a photo or upload from device" }}
|
|
28
28
|
</div>
|
|
29
29
|
</v-sheet>
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
icon="mdi-close"
|
|
51
51
|
size="x-small"
|
|
52
52
|
variant="tonal"
|
|
53
|
-
|
|
54
|
-
class="delete-btn"
|
|
53
|
+
aria-label="Remove photo"
|
|
54
|
+
class="delete-btn photo-upload__remove"
|
|
55
55
|
@click.stop="removePhoto(index)"
|
|
56
56
|
style="position: absolute; top: -8px; right: -8px"
|
|
57
57
|
/>
|
|
@@ -60,11 +60,13 @@
|
|
|
60
60
|
</v-row>
|
|
61
61
|
|
|
62
62
|
<v-dialog v-model="showPhotoPreview" max-width="900">
|
|
63
|
-
<v-card>
|
|
63
|
+
<v-card class="screen-modal">
|
|
64
64
|
<v-card-actions class="pa-2 justify-end">
|
|
65
65
|
<v-btn
|
|
66
66
|
icon="mdi-close"
|
|
67
67
|
variant="text"
|
|
68
|
+
class="screen-modal__close"
|
|
69
|
+
aria-label="Close photo preview"
|
|
68
70
|
@click="showPhotoPreview = false"
|
|
69
71
|
/>
|
|
70
72
|
</v-card-actions>
|
|
@@ -75,46 +77,47 @@
|
|
|
75
77
|
</v-dialog>
|
|
76
78
|
|
|
77
79
|
<v-dialog v-model="photoOptionsDialog" max-width="420">
|
|
78
|
-
<v-card>
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
<v-card class="screen-modal">
|
|
81
|
+
<!-- `text-h6` is 20px/500 Roboto; the design's modal heading is the
|
|
82
|
+
`v-card-title` row's own size and weight. -->
|
|
83
|
+
<v-card-title class="d-flex align-center">
|
|
84
|
+
Add Photo
|
|
81
85
|
<v-spacer />
|
|
82
86
|
<v-btn
|
|
83
87
|
icon="mdi-close"
|
|
84
88
|
variant="text"
|
|
85
|
-
|
|
89
|
+
class="screen-modal__close"
|
|
90
|
+
aria-label="Close"
|
|
86
91
|
@click="photoOptionsDialog = false"
|
|
87
92
|
/>
|
|
88
93
|
</v-card-title>
|
|
89
94
|
|
|
90
|
-
<v-
|
|
91
|
-
|
|
92
|
-
<v-card-text class="pa-4">
|
|
95
|
+
<v-card-text class="screen-modal__body">
|
|
93
96
|
<v-list class="pa-0" bg-color="transparent">
|
|
94
97
|
<v-list-item class="rounded-lg mb-2" @click="openCameraFromOptions">
|
|
95
98
|
<template v-slot:prepend>
|
|
96
|
-
<v-avatar
|
|
97
|
-
<v-icon icon="mdi-camera"
|
|
99
|
+
<v-avatar variant="flat" size="48" class="photo-upload__avatar">
|
|
100
|
+
<v-icon icon="mdi-camera" />
|
|
98
101
|
</v-avatar>
|
|
99
102
|
</template>
|
|
100
|
-
<v-list-item-title class="
|
|
103
|
+
<v-list-item-title class="photo-upload__lead mb-1">
|
|
101
104
|
Take a Photo
|
|
102
105
|
</v-list-item-title>
|
|
103
|
-
<v-list-item-subtitle class="
|
|
106
|
+
<v-list-item-subtitle class="photo-upload__hint">
|
|
104
107
|
Use device camera to capture an image
|
|
105
108
|
</v-list-item-subtitle>
|
|
106
109
|
</v-list-item>
|
|
107
110
|
|
|
108
111
|
<v-list-item class="rounded-lg" @click="triggerFileInput">
|
|
109
112
|
<template v-slot:prepend>
|
|
110
|
-
<v-avatar
|
|
111
|
-
<v-icon icon="mdi-upload"
|
|
113
|
+
<v-avatar variant="flat" size="48" class="photo-upload__avatar">
|
|
114
|
+
<v-icon icon="mdi-upload" />
|
|
112
115
|
</v-avatar>
|
|
113
116
|
</template>
|
|
114
|
-
<v-list-item-title class="
|
|
117
|
+
<v-list-item-title class="photo-upload__lead mb-1">
|
|
115
118
|
Upload a Photo
|
|
116
119
|
</v-list-item-title>
|
|
117
|
-
<v-list-item-subtitle class="
|
|
120
|
+
<v-list-item-subtitle class="photo-upload__hint">
|
|
118
121
|
Select an image from your device
|
|
119
122
|
</v-list-item-subtitle>
|
|
120
123
|
</v-list-item>
|
|
@@ -124,17 +127,22 @@
|
|
|
124
127
|
</v-dialog>
|
|
125
128
|
|
|
126
129
|
<v-dialog v-model="showCameraDialog" max-width="720">
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
<!-- Was a bare `v-toolbar`, which Vuetify paints from its own `surface` -
|
|
131
|
+
a grey slab across the top of the card, lighter than the card on the
|
|
132
|
+
dark theme. -->
|
|
133
|
+
<v-card class="screen-modal">
|
|
134
|
+
<v-card-title class="d-flex align-center">
|
|
135
|
+
Camera
|
|
132
136
|
<v-spacer />
|
|
133
|
-
<v-btn
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
<v-btn
|
|
138
|
+
icon="mdi-close"
|
|
139
|
+
variant="text"
|
|
140
|
+
class="screen-modal__close"
|
|
141
|
+
aria-label="Close camera"
|
|
142
|
+
@click="closeCameraDialog"
|
|
143
|
+
/>
|
|
144
|
+
</v-card-title>
|
|
145
|
+
<v-card-text class="screen-modal__body">
|
|
138
146
|
<div class="d-flex flex-column align-center">
|
|
139
147
|
<div style="width: 100%; max-width: 640px">
|
|
140
148
|
<video
|
|
@@ -146,33 +154,32 @@
|
|
|
146
154
|
/>
|
|
147
155
|
</div>
|
|
148
156
|
<canvas ref="canvasRef" style="display: none"></canvas>
|
|
149
|
-
<div class="
|
|
157
|
+
<div class="screen-hint mt-2">
|
|
150
158
|
Tip: Allow camera permissions to use this feature.
|
|
151
159
|
</div>
|
|
152
160
|
</div>
|
|
153
161
|
</v-card-text>
|
|
154
|
-
|
|
162
|
+
<!-- Was a `v-card-actions` holding a plain text CANCEL and a 56px
|
|
163
|
+
`color="black"` bar forced to `rounded-0` - black is neither theme's
|
|
164
|
+
accent and reads as a hole in the dark modal. This is the design's
|
|
165
|
+
shared modal footer: ghost secondary, then the primary. -->
|
|
166
|
+
<div class="screen-modal__footer">
|
|
155
167
|
<v-btn
|
|
156
|
-
text
|
|
168
|
+
class="text-none screen-btn-ghost"
|
|
169
|
+
variant="outlined"
|
|
157
170
|
@click="closeCameraDialog"
|
|
158
|
-
class="text-none font-weight-medium"
|
|
159
171
|
>
|
|
160
|
-
|
|
172
|
+
Cancel
|
|
161
173
|
</v-btn>
|
|
162
|
-
<v-spacer />
|
|
163
174
|
<v-btn
|
|
164
|
-
|
|
175
|
+
class="text-none screen-btn-primary"
|
|
165
176
|
variant="flat"
|
|
166
|
-
color="black"
|
|
167
|
-
class="text-none font-weight-bold rounded-0"
|
|
168
|
-
height="56"
|
|
169
|
-
size="large"
|
|
170
177
|
@click="capturePhoto"
|
|
171
178
|
>
|
|
172
179
|
<v-icon class="mr-2">mdi-camera</v-icon>
|
|
173
|
-
|
|
180
|
+
Capture
|
|
174
181
|
</v-btn>
|
|
175
|
-
</
|
|
182
|
+
</div>
|
|
176
183
|
</v-card>
|
|
177
184
|
</v-dialog>
|
|
178
185
|
|
|
@@ -393,13 +400,68 @@ onBeforeUnmount(() => {
|
|
|
393
400
|
</script>
|
|
394
401
|
|
|
395
402
|
<style scoped>
|
|
403
|
+
/* Was `rgb(var(--v-theme-primary))`, i.e. the design's FOREGROUND token used
|
|
404
|
+
as a rule and a wash. `--accent-strong` is the accent's fill form, which is
|
|
405
|
+
what a hover state on a drop zone is. */
|
|
396
406
|
.upload-area {
|
|
397
407
|
&:hover:not(.upload-disabled) {
|
|
398
|
-
border-color:
|
|
399
|
-
background-color:
|
|
408
|
+
border-color: var(--accent-strong) !important;
|
|
409
|
+
background-color: var(--accent-soft);
|
|
400
410
|
}
|
|
401
411
|
}
|
|
402
412
|
|
|
413
|
+
/* The section title was `text-subtitle-2 font-weight-bold` and the two leads
|
|
414
|
+
`text-body-1 font-weight-medium` - Vuetify Roboto at Vuetify's sizes. These
|
|
415
|
+
take the design's card-title and cell type. */
|
|
416
|
+
.photo-upload__title {
|
|
417
|
+
font-family: var(--font-sans);
|
|
418
|
+
font-size: var(--fs-card-title);
|
|
419
|
+
font-weight: var(--fw-card-title);
|
|
420
|
+
color: var(--text);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.photo-upload__req {
|
|
424
|
+
color: var(--err);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.photo-upload__lead {
|
|
428
|
+
font-family: var(--font-sans);
|
|
429
|
+
font-size: 13.5px;
|
|
430
|
+
font-weight: 600;
|
|
431
|
+
color: var(--text);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Was `text-caption text-medium-emphasis` / `grey--text`, Vuetify's 60% alpha.
|
|
435
|
+
`--muted` is the design's sub-line and follows the theme. */
|
|
436
|
+
.photo-upload__hint {
|
|
437
|
+
font-family: var(--font-sans);
|
|
438
|
+
font-size: 12px;
|
|
439
|
+
color: var(--muted);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* The drop-zone glyph was `color="primary"`, the FOREGROUND token; the
|
|
443
|
+
accent's ink form is the one held at >= 4.5:1 on the card. */
|
|
444
|
+
.photo-upload__icon {
|
|
445
|
+
color: var(--accent-text);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.photo-upload__icon--wait {
|
|
449
|
+
color: var(--muted);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* The two option tiles were `v-avatar color="primary" variant="tonal"` with a
|
|
453
|
+
`primary` glyph inside - accent on accent. */
|
|
454
|
+
.photo-upload__avatar {
|
|
455
|
+
background: var(--accent-soft);
|
|
456
|
+
color: var(--accent-text);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* `color="error"` is Vuetify's palette red, not the theme's `--err`. */
|
|
460
|
+
.photo-upload__remove {
|
|
461
|
+
background: var(--err-bg);
|
|
462
|
+
color: var(--err);
|
|
463
|
+
}
|
|
464
|
+
|
|
403
465
|
.upload-disabled {
|
|
404
466
|
cursor: not-allowed !important;
|
|
405
467
|
opacity: 0.6;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.2.2-staging.
|
|
5
|
+
"version": "3.2.2-staging.127",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|