@globalbrain/sefirot 4.9.0 → 4.10.0
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.
|
@@ -7,7 +7,7 @@ import IconMinusCircle from '~icons/ph/minus-circle'
|
|
|
7
7
|
import IconXCircle from '~icons/ph/x-circle'
|
|
8
8
|
import { computed } from 'vue'
|
|
9
9
|
|
|
10
|
-
export type Size = 'nano' | 'mini' | 'small' | 'medium' | 'large' | 'jumbo'
|
|
10
|
+
export type Size = 'nano' | 'mini' | 'small' | 'medium' | 'large' | 'jumbo' | 'fill'
|
|
11
11
|
export type State = 'pending' | 'ready' | 'queued' | 'running' | 'completed' | 'failed' | 'aborted'
|
|
12
12
|
export type Mode = 'colored' | 'monochrome'
|
|
13
13
|
|
|
@@ -65,6 +65,7 @@ const classes = computed(() => [
|
|
|
65
65
|
.SIndicator.medium { width: 32px; height: 32px; }
|
|
66
66
|
.SIndicator.large { width: 40px; height: 40px; }
|
|
67
67
|
.SIndicator.jumbo { width: 48px; height: 48px; }
|
|
68
|
+
.SIndicator.fill { width: 100%; height: 100%; }
|
|
68
69
|
|
|
69
70
|
.SIndicator.queued {
|
|
70
71
|
animation: indicator-blink 1.5s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
|
|
@@ -1,18 +1,42 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="T extends ModelType = 'file'">
|
|
2
|
+
import { type ValidationRuleWithParams } from '@vuelidate/core'
|
|
3
|
+
import { useDropZone } from '@vueuse/core'
|
|
2
4
|
import { type Component, computed, ref } from 'vue'
|
|
3
5
|
import { useTrans } from '../composables/Lang'
|
|
4
6
|
import { type Validatable } from '../composables/Validation'
|
|
5
7
|
import { formatSize } from '../support/File'
|
|
6
|
-
import SButton from './SButton.vue'
|
|
8
|
+
import SButton, { type Mode as ButtonMode } from './SButton.vue'
|
|
7
9
|
import SCard from './SCard.vue'
|
|
8
10
|
import SCardBlock from './SCardBlock.vue'
|
|
11
|
+
import { type State as IndicatorState } from './SIndicator.vue'
|
|
9
12
|
import SInputBase from './SInputBase.vue'
|
|
10
13
|
import SInputFileUploadItem from './SInputFileUploadItem.vue'
|
|
14
|
+
import STrans from './STrans.vue'
|
|
11
15
|
|
|
12
16
|
export type Size = 'mini' | 'small' | 'medium'
|
|
13
17
|
export type Color = 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
export type ModelType = 'file' | 'object'
|
|
20
|
+
export type ModelValue<T extends ModelType> = T extends 'file' ? File : FileObject
|
|
21
|
+
|
|
22
|
+
export interface FileObject {
|
|
23
|
+
file: File
|
|
24
|
+
indicatorState?: IndicatorState | null
|
|
25
|
+
canRemove?: boolean
|
|
26
|
+
action?: Action
|
|
27
|
+
errorMessage?: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Action {
|
|
31
|
+
mode?: ButtonMode
|
|
32
|
+
icon?: Component
|
|
33
|
+
leadIcon?: Component
|
|
34
|
+
trailIcon?: Component
|
|
35
|
+
label?: string
|
|
36
|
+
onClick(): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const props = withDefaults(defineProps<{
|
|
16
40
|
size?: Size
|
|
17
41
|
label?: string
|
|
18
42
|
info?: string
|
|
@@ -26,15 +50,20 @@ const props = defineProps<{
|
|
|
26
50
|
checkIcon?: Component
|
|
27
51
|
checkText?: string
|
|
28
52
|
checkColor?: Color
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
droppable?: boolean
|
|
54
|
+
value?: ModelValue<T>[]
|
|
55
|
+
modelType?: T
|
|
56
|
+
modelValue?: ModelValue<T>[]
|
|
57
|
+
rules?: Record<string, ValidationRuleWithParams>
|
|
32
58
|
validation?: Validatable
|
|
33
|
-
|
|
59
|
+
hideError?: boolean
|
|
60
|
+
}>(), {
|
|
61
|
+
modelType: 'file' as any // `ModelType` doesn't work so stubbing it.
|
|
62
|
+
})
|
|
34
63
|
|
|
35
64
|
const emit = defineEmits<{
|
|
36
|
-
'update:model-value': [files:
|
|
37
|
-
'change': [files:
|
|
65
|
+
'update:model-value': [files: ModelValue<T>[]]
|
|
66
|
+
'change': [files: ModelValue<T>[]]
|
|
38
67
|
}>()
|
|
39
68
|
|
|
40
69
|
const { t } = useTrans({
|
|
@@ -50,28 +79,46 @@ const { t } = useTrans({
|
|
|
50
79
|
}
|
|
51
80
|
})
|
|
52
81
|
|
|
82
|
+
const dropZoneEl = ref<HTMLDivElement | null>(null)
|
|
83
|
+
|
|
84
|
+
const { isOverDropZone } = useDropZone(dropZoneEl, {
|
|
85
|
+
multiple: true,
|
|
86
|
+
onDrop: (files) => onDrop(files)
|
|
87
|
+
})
|
|
88
|
+
|
|
53
89
|
const _value = computed(() => {
|
|
54
90
|
return props.modelValue !== undefined
|
|
55
91
|
? props.modelValue
|
|
56
|
-
: props.value !== undefined ? props.value : []
|
|
92
|
+
: props.value !== undefined ? props.value : [] as ModelValue<T>[]
|
|
57
93
|
})
|
|
58
94
|
|
|
59
95
|
const input = ref<HTMLInputElement | null>(null)
|
|
60
96
|
|
|
61
|
-
const classes = computed(() => [
|
|
97
|
+
const classes = computed(() => [
|
|
98
|
+
props.size ?? 'small',
|
|
99
|
+
{ droppable: props.droppable },
|
|
100
|
+
{ 'is-over-drop-zone': isOverDropZone.value }
|
|
101
|
+
])
|
|
62
102
|
|
|
63
103
|
const totalFileCountText = computed(() => {
|
|
64
104
|
return t.selected_files(_value.value.length)
|
|
65
105
|
})
|
|
66
106
|
|
|
67
107
|
const totalFileSizeText = computed(() => {
|
|
68
|
-
|
|
108
|
+
const files = _value.value.map((file) => file instanceof File ? file : file.file)
|
|
109
|
+
return formatSize(files)
|
|
69
110
|
})
|
|
70
111
|
|
|
71
112
|
function open() {
|
|
72
113
|
input.value!.click()
|
|
73
114
|
}
|
|
74
115
|
|
|
116
|
+
function onDrop(files: File[] | null) {
|
|
117
|
+
if (files !== null && files.length > 0) {
|
|
118
|
+
emitChange(append(files))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
75
122
|
function onChange(e: Event) {
|
|
76
123
|
const files = Array.from((e.target as HTMLInputElement).files ?? [])
|
|
77
124
|
|
|
@@ -81,25 +128,35 @@ function onChange(e: Event) {
|
|
|
81
128
|
return
|
|
82
129
|
}
|
|
83
130
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
emit('update:model-value', newFiles)
|
|
87
|
-
emit('change', newFiles)
|
|
88
|
-
|
|
89
|
-
props.validation?.$touch()
|
|
131
|
+
emitChange(append(files))
|
|
90
132
|
}
|
|
91
133
|
|
|
92
134
|
function onRemove(index: number) {
|
|
93
135
|
const files = _value.value.filter((_, i) => i !== index)
|
|
136
|
+
emitChange(files)
|
|
137
|
+
}
|
|
94
138
|
|
|
139
|
+
function emitChange(files: ModelValue<T>[]) {
|
|
95
140
|
emit('update:model-value', files)
|
|
96
141
|
emit('change', files)
|
|
142
|
+
props.validation?.$touch()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function append(files: File[]) {
|
|
146
|
+
return [
|
|
147
|
+
..._value.value,
|
|
148
|
+
...(props.modelType === 'file' ? files : toFileObjects(files))
|
|
149
|
+
] as ModelValue<T>[]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function toFileObjects(files: File[]) {
|
|
153
|
+
return files.map((file) => ({ file } as ModelValue<T>))
|
|
97
154
|
}
|
|
98
155
|
</script>
|
|
99
156
|
|
|
100
157
|
<template>
|
|
101
158
|
<SInputBase
|
|
102
|
-
class="
|
|
159
|
+
class="SInputFileUpload"
|
|
103
160
|
:class="classes"
|
|
104
161
|
:label="label"
|
|
105
162
|
:note="note"
|
|
@@ -121,8 +178,29 @@ function onRemove(index: number) {
|
|
|
121
178
|
@change="onChange"
|
|
122
179
|
>
|
|
123
180
|
<SCard :mode="hasError ? 'danger' : undefined">
|
|
124
|
-
<SCardBlock class="
|
|
181
|
+
<SCardBlock v-if="droppable" class="drop-zone" ref="dropZoneEl" @click="open">
|
|
182
|
+
<div class="drop-zone-box">
|
|
183
|
+
<STrans lang="en">
|
|
184
|
+
<div class="drop-zone-text">
|
|
185
|
+
Drag and drop files here, or
|
|
186
|
+
</div>
|
|
187
|
+
<div class="drop-zone-action">
|
|
188
|
+
<SButton size="mini" label="Select files" />
|
|
189
|
+
</div>
|
|
190
|
+
</STrans>
|
|
191
|
+
<STrans lang="ja">
|
|
192
|
+
<div class="drop-zone-text">
|
|
193
|
+
ファイルをドラック&ドロップ、または
|
|
194
|
+
</div>
|
|
195
|
+
<div class="drop-zone-action">
|
|
196
|
+
<SButton size="mini" label="ファイルを選択" />
|
|
197
|
+
</div>
|
|
198
|
+
</STrans>
|
|
199
|
+
</div>
|
|
200
|
+
</SCardBlock>
|
|
201
|
+
<SCardBlock v-if="!droppable || placeholder" class="header">
|
|
125
202
|
<SButton
|
|
203
|
+
v-if="!droppable"
|
|
126
204
|
size="small"
|
|
127
205
|
:label="text ?? t.button_text"
|
|
128
206
|
@click="open"
|
|
@@ -134,8 +212,9 @@ function onRemove(index: number) {
|
|
|
134
212
|
<template v-if="_value.length">
|
|
135
213
|
<SInputFileUploadItem
|
|
136
214
|
v-for="file, i in _value"
|
|
137
|
-
:key="
|
|
215
|
+
:key="i"
|
|
138
216
|
:file="file"
|
|
217
|
+
:rules="rules"
|
|
139
218
|
@remove="() => { onRemove(i) }"
|
|
140
219
|
/>
|
|
141
220
|
</template>
|
|
@@ -172,6 +251,35 @@ function onRemove(index: number) {
|
|
|
172
251
|
display: none;
|
|
173
252
|
}
|
|
174
253
|
|
|
254
|
+
.drop-zone {
|
|
255
|
+
padding: 12px;
|
|
256
|
+
|
|
257
|
+
&:hover .drop-zone-box {
|
|
258
|
+
border-color: var(--c-border-info-1);
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.drop-zone-box {
|
|
264
|
+
display: flex;
|
|
265
|
+
flex-direction: column;
|
|
266
|
+
justify-content: center;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 16px;
|
|
269
|
+
border: 1px dashed var(--c-border-mute-1);
|
|
270
|
+
border-radius: 3px;
|
|
271
|
+
padding: 24px 0;
|
|
272
|
+
min-height: 192px;
|
|
273
|
+
text-align: center;
|
|
274
|
+
transition: border-color 0.25s;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.drop-zone-text {
|
|
278
|
+
text-align: center;
|
|
279
|
+
font-size: 14px;
|
|
280
|
+
color: var(--c-text-2);
|
|
281
|
+
}
|
|
282
|
+
|
|
175
283
|
.header {
|
|
176
284
|
display: flex;
|
|
177
285
|
align-items: center;
|
|
@@ -226,4 +334,16 @@ function onRemove(index: number) {
|
|
|
226
334
|
width: 32px;
|
|
227
335
|
height: 32px;
|
|
228
336
|
}
|
|
337
|
+
|
|
338
|
+
.SInputFileUpload.droppable {
|
|
339
|
+
.header {
|
|
340
|
+
padding-left: 16px;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.SInputFileUpload.is-over-drop-zone {
|
|
345
|
+
.drop-zone-box {
|
|
346
|
+
border-color: var(--c-border-info-1);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
229
349
|
</style>
|
|
@@ -1,38 +1,99 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import IconFileText from '~icons/ph/file-text'
|
|
3
3
|
import IconTrash from '~icons/ph/trash'
|
|
4
|
-
import {
|
|
4
|
+
import { type ValidationRuleWithParams } from '@vuelidate/core'
|
|
5
|
+
import { type Component, computed } from 'vue'
|
|
6
|
+
import { useValidation } from '../composables/Validation'
|
|
5
7
|
import { formatSize } from '../support/File'
|
|
6
|
-
import SButton from './SButton.vue'
|
|
8
|
+
import SButton, { type Mode as ButtonMode } from './SButton.vue'
|
|
7
9
|
import SCardBlock from './SCardBlock.vue'
|
|
10
|
+
import SIndicator, { type State as IndicatorState } from './SIndicator.vue'
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
export interface FileObject {
|
|
10
13
|
file: File
|
|
14
|
+
indicatorState?: IndicatorState | null
|
|
15
|
+
canRemove?: boolean
|
|
16
|
+
action?: Action | null
|
|
17
|
+
errorMessage?: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Action {
|
|
21
|
+
mode?: ButtonMode
|
|
22
|
+
icon?: Component
|
|
23
|
+
leadIcon?: Component
|
|
24
|
+
trailIcon?: Component
|
|
25
|
+
label?: string
|
|
26
|
+
onClick(): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = defineProps<{
|
|
30
|
+
file: File | FileObject
|
|
31
|
+
rules?: Record<string, ValidationRuleWithParams>
|
|
11
32
|
}>()
|
|
12
33
|
|
|
13
34
|
defineEmits<{
|
|
14
35
|
'remove': []
|
|
15
36
|
}>()
|
|
16
37
|
|
|
17
|
-
const
|
|
18
|
-
|
|
38
|
+
const _file = computed(() => ({
|
|
39
|
+
name: props.file instanceof File ? props.file.name : props.file.file.name,
|
|
40
|
+
file: props.file instanceof File ? props.file : props.file.file,
|
|
41
|
+
size: formatSize(props.file instanceof File ? props.file : props.file.file),
|
|
42
|
+
indicatorState: props.file instanceof File ? null : props.file.indicatorState,
|
|
43
|
+
canRemove: props.file instanceof File ? true : props.file.canRemove ?? true,
|
|
44
|
+
action: props.file instanceof File ? null : props.file.action,
|
|
45
|
+
errorMessage: props.file instanceof File ? null : props.file.errorMessage
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
const { validation } = useValidation(() => ({
|
|
49
|
+
file: _file.value.file
|
|
50
|
+
}), {
|
|
51
|
+
file: props.rules ?? {}
|
|
19
52
|
})
|
|
53
|
+
|
|
54
|
+
validation.value.$touch()
|
|
20
55
|
</script>
|
|
21
56
|
|
|
22
57
|
<template>
|
|
23
58
|
<SCardBlock class="SInputFileUploadItem">
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
<div class="name">
|
|
60
|
+
<div class="name-label">
|
|
61
|
+
<div class="name-icon">
|
|
62
|
+
<IconFileText v-if="_file.indicatorState == null" class="name-icon-svg" />
|
|
63
|
+
<SIndicator size="fill" v-else :state="_file.indicatorState" />
|
|
64
|
+
</div>
|
|
65
|
+
<p class="name-text">{{ _file.name }}</p>
|
|
66
|
+
</div>
|
|
67
|
+
<p v-if="_file.errorMessage" class="error">{{ _file.errorMessage }}</p>
|
|
68
|
+
<p v-else-if="validation.$errors.length" class="error">{{ validation.$errors[0]?.$message }}</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div v-if="_file.action" class="action">
|
|
71
|
+
<SButton
|
|
72
|
+
type="text"
|
|
73
|
+
size="small"
|
|
74
|
+
:mode="_file.action.mode"
|
|
75
|
+
:icon="_file.action.icon"
|
|
76
|
+
:lead-icon="_file.action.leadIcon"
|
|
77
|
+
:trail-icon="_file.action.trailIcon"
|
|
78
|
+
:label="_file.action.label"
|
|
79
|
+
@click="_file.action.onClick"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="meta">
|
|
83
|
+
<div class="size">
|
|
84
|
+
{{ _file.size }}
|
|
85
|
+
</div>
|
|
86
|
+
<div class="delete">
|
|
87
|
+
<SButton
|
|
88
|
+
size="small"
|
|
89
|
+
type="text"
|
|
90
|
+
mode="mute"
|
|
91
|
+
:icon="IconTrash"
|
|
92
|
+
:disabled="!_file.canRemove"
|
|
93
|
+
@click="$emit('remove')"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
36
97
|
</SCardBlock>
|
|
37
98
|
</template>
|
|
38
99
|
|
|
@@ -40,12 +101,20 @@ const fileSize = computed(() => {
|
|
|
40
101
|
.SInputFileUploadItem {
|
|
41
102
|
display: flex;
|
|
42
103
|
align-items: center;
|
|
43
|
-
gap:
|
|
44
|
-
|
|
45
|
-
padding: 0 8px 0 16px;
|
|
104
|
+
gap: 16px;
|
|
105
|
+
padding: 8px 8px 8px 16px;
|
|
46
106
|
}
|
|
47
107
|
|
|
48
108
|
.name {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
111
|
+
flex-grow: 1;
|
|
112
|
+
white-space: nowrap;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
text-overflow: ellipsis;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.name-label {
|
|
49
118
|
display: flex;
|
|
50
119
|
align-items: center;
|
|
51
120
|
gap: 8px;
|
|
@@ -56,6 +125,12 @@ const fileSize = computed(() => {
|
|
|
56
125
|
}
|
|
57
126
|
|
|
58
127
|
.name-icon {
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
width: 16px;
|
|
130
|
+
height: 16px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.name-icon-svg {
|
|
59
134
|
width: 16px;
|
|
60
135
|
height: 16px;
|
|
61
136
|
color: var(--c-text-2);
|
|
@@ -70,9 +145,35 @@ const fileSize = computed(() => {
|
|
|
70
145
|
text-overflow: ellipsis;
|
|
71
146
|
}
|
|
72
147
|
|
|
148
|
+
.error {
|
|
149
|
+
padding-left: 24px;
|
|
150
|
+
line-height: 20px;
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
color: var(--c-text-danger-1);
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
text-overflow: ellipsis;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.action {
|
|
159
|
+
flex-shrink: 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.meta {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
flex-shrink: 0;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
}
|
|
168
|
+
|
|
73
169
|
.size {
|
|
170
|
+
flex-shrink: 0;
|
|
74
171
|
line-height: 24px;
|
|
75
172
|
font-size: 12px;
|
|
76
173
|
color: var(--c-text-2);
|
|
77
174
|
}
|
|
175
|
+
|
|
176
|
+
.delete {
|
|
177
|
+
flex-shrink: 0;
|
|
178
|
+
}
|
|
78
179
|
</style>
|
package/package.json
CHANGED