@globalbrain/sefirot 4.8.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.
- package/lib/components/SIndicator.vue +14 -6
- package/lib/components/SInputFileUpload.vue +141 -21
- package/lib/components/SInputFileUploadItem.vue +121 -20
- package/lib/components/STableCell.vue +11 -0
- package/lib/components/STableCellIndicator.vue +38 -0
- package/lib/components/STableCellPath.vue +71 -0
- package/lib/composables/Table.ts +29 -6
- package/package.json +1 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import IconCheckCircleFill from '~icons/ph/check-circle-fill'
|
|
3
|
-
import IconCircle from '~icons/ph/circle
|
|
4
|
-
import IconCircleDashed from '~icons/ph/circle-dashed
|
|
5
|
-
import IconCircleNotch from '~icons/ph/circle-notch
|
|
6
|
-
import
|
|
3
|
+
import IconCircle from '~icons/ph/circle'
|
|
4
|
+
import IconCircleDashed from '~icons/ph/circle-dashed'
|
|
5
|
+
import IconCircleNotch from '~icons/ph/circle-notch'
|
|
6
|
+
import IconMinusCircle from '~icons/ph/minus-circle'
|
|
7
|
+
import IconXCircle from '~icons/ph/x-circle'
|
|
7
8
|
import { computed } from 'vue'
|
|
8
9
|
|
|
9
|
-
export type Size = 'nano' | 'mini' | 'small' | 'medium' | 'large' | 'jumbo'
|
|
10
|
-
export type State = 'pending' | 'ready' | 'queued' | 'running' | 'completed' | 'failed'
|
|
10
|
+
export type Size = 'nano' | 'mini' | 'small' | 'medium' | 'large' | 'jumbo' | 'fill'
|
|
11
|
+
export type State = 'pending' | 'ready' | 'queued' | 'running' | 'completed' | 'failed' | 'aborted'
|
|
11
12
|
export type Mode = 'colored' | 'monochrome'
|
|
12
13
|
|
|
13
14
|
const props = withDefaults(defineProps<{
|
|
@@ -33,6 +34,7 @@ const classes = computed(() => [
|
|
|
33
34
|
<IconCircleNotch v-if="props.state === 'running'" class="icon" />
|
|
34
35
|
<IconCheckCircleFill v-if="props.state === 'completed'" class="icon" />
|
|
35
36
|
<IconXCircle v-if="props.state === 'failed'" class="icon" />
|
|
37
|
+
<IconMinusCircle v-if="props.state === 'aborted'" class="icon aborted" />
|
|
36
38
|
</div>
|
|
37
39
|
</template>
|
|
38
40
|
|
|
@@ -51,6 +53,10 @@ const classes = computed(() => [
|
|
|
51
53
|
.icon {
|
|
52
54
|
width: 100%;
|
|
53
55
|
height: 100%;
|
|
56
|
+
|
|
57
|
+
&.aborted {
|
|
58
|
+
transform: rotate(-45deg);
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
.SIndicator.nano { width: 20px; height: 20px; }
|
|
@@ -59,6 +65,7 @@ const classes = computed(() => [
|
|
|
59
65
|
.SIndicator.medium { width: 32px; height: 32px; }
|
|
60
66
|
.SIndicator.large { width: 40px; height: 40px; }
|
|
61
67
|
.SIndicator.jumbo { width: 48px; height: 48px; }
|
|
68
|
+
.SIndicator.fill { width: 100%; height: 100%; }
|
|
62
69
|
|
|
63
70
|
.SIndicator.queued {
|
|
64
71
|
animation: indicator-blink 1.5s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
|
|
@@ -75,6 +82,7 @@ const classes = computed(() => [
|
|
|
75
82
|
&.running { color: var(--c-fg-info-1); }
|
|
76
83
|
&.completed { color: var(--c-fg-success-1); }
|
|
77
84
|
&.failed { color: var(--c-fg-danger-1); }
|
|
85
|
+
&.aborted { color: var(--c-fg-gray-1); }
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
.SIndicator.monochrome {
|
|
@@ -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>
|
|
@@ -7,7 +7,9 @@ import STableCellAvatars from './STableCellAvatars.vue'
|
|
|
7
7
|
import STableCellCustom from './STableCellCustom.vue'
|
|
8
8
|
import STableCellDay from './STableCellDay.vue'
|
|
9
9
|
import STableCellEmpty from './STableCellEmpty.vue'
|
|
10
|
+
import STableCellIndicator from './STableCellIndicator.vue'
|
|
10
11
|
import STableCellNumber from './STableCellNumber.vue'
|
|
12
|
+
import STableCellPath from './STableCellPath.vue'
|
|
11
13
|
import STableCellPill from './STableCellPill.vue'
|
|
12
14
|
import STableCellPills from './STableCellPills.vue'
|
|
13
15
|
import STableCellState from './STableCellState.vue'
|
|
@@ -56,6 +58,10 @@ const computedCell = computed<TableCell | undefined>(() =>
|
|
|
56
58
|
:icon-color="computedCell.iconColor"
|
|
57
59
|
:on-click="computedCell.onClick"
|
|
58
60
|
/>
|
|
61
|
+
<STableCellPath
|
|
62
|
+
v-else-if="computedCell.type === 'path'"
|
|
63
|
+
:segments="computedCell.segments"
|
|
64
|
+
/>
|
|
59
65
|
<STableCellDay
|
|
60
66
|
v-else-if="computedCell.type === 'day'"
|
|
61
67
|
:align="computedCell.align"
|
|
@@ -77,6 +83,11 @@ const computedCell = computed<TableCell | undefined>(() =>
|
|
|
77
83
|
:state="computedCell.label"
|
|
78
84
|
:mode="computedCell.mode"
|
|
79
85
|
/>
|
|
86
|
+
<STableCellIndicator
|
|
87
|
+
v-else-if="computedCell.type === 'indicator'"
|
|
88
|
+
:state="computedCell.state"
|
|
89
|
+
:label="computedCell.label"
|
|
90
|
+
/>
|
|
80
91
|
<STableCellAvatar
|
|
81
92
|
v-else-if="computedCell.type === 'avatar'"
|
|
82
93
|
:value="value"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import SIndicator, { type State } from './SIndicator.vue'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
state: State
|
|
6
|
+
label?: string | null
|
|
7
|
+
}>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="STableCellIndicator" :class="[state]">
|
|
12
|
+
<SIndicator size="nano" :state="state" />
|
|
13
|
+
<div v-if="label" class="text">{{ label }}</div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<style scoped lang="postcss">
|
|
18
|
+
.STableCellIndicator {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 8px;
|
|
22
|
+
padding: 0 16px;
|
|
23
|
+
min-height: 40px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.text {
|
|
27
|
+
line-height: 24px;
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.STableCellIndicator.pending .text { color: var(--c-text-1); }
|
|
32
|
+
.STableCellIndicator.ready .text { color: var(--c-text-1); }
|
|
33
|
+
.STableCellIndicator.queued .text { color: var(--c-text-1); }
|
|
34
|
+
.STableCellIndicator.running .text { color: var(--c-text-1); }
|
|
35
|
+
.STableCellIndicator.completed .text { color: var(--c-text-1); }
|
|
36
|
+
.STableCellIndicator.failed .text { color: var(--c-text-3); }
|
|
37
|
+
.STableCellIndicator.aborted .text { color: var(--c-text-3); }
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type TableCellPathSegment } from '../composables/Table'
|
|
3
|
+
import SLink from './SLink.vue'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
segments: TableCellPathSegment[]
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
function classes(item: TableCellPathSegment) {
|
|
10
|
+
return [
|
|
11
|
+
item.color ?? 'neutral',
|
|
12
|
+
{ link: !!item.link || !!item.onClick }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="STableCellPath">
|
|
19
|
+
<template v-for="segment, index in segments" :key="index">
|
|
20
|
+
<div v-if="index > 0" class="divider">/</div>
|
|
21
|
+
<SLink
|
|
22
|
+
class="text"
|
|
23
|
+
:class="classes(segment)"
|
|
24
|
+
:href="segment.link"
|
|
25
|
+
@click="segment.onClick"
|
|
26
|
+
>
|
|
27
|
+
{{ segment.text }}
|
|
28
|
+
</SLink>
|
|
29
|
+
</template>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped lang="postcss">
|
|
34
|
+
.STableCellPath {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 6px;
|
|
38
|
+
padding: 0 16px;
|
|
39
|
+
min-height: 40px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.divider {
|
|
43
|
+
color: var(--c-text-3);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.text {
|
|
47
|
+
line-height: 24px;
|
|
48
|
+
font-size: 14px;
|
|
49
|
+
transition: color 0.25s;
|
|
50
|
+
|
|
51
|
+
&.neutral { color: var(--c-text-1); }
|
|
52
|
+
&.soft { color: var(--c-text-2); }
|
|
53
|
+
&.mute { color: var(--c-text-3); }
|
|
54
|
+
&.info { color: var(--c-text-info-1); }
|
|
55
|
+
&.success { color: var(--c-text-success-1); }
|
|
56
|
+
&.warning { color: var(--c-text-warning-1); }
|
|
57
|
+
&.danger { color: var(--c-text-danger-1); }
|
|
58
|
+
|
|
59
|
+
&.link {
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&.link.neutral:hover { color: var(--c-text-info-1); }
|
|
64
|
+
&.link.soft:hover { color: var(--c-text-info-1); }
|
|
65
|
+
&.link.mute:hover { color: var(--c-text-info-1); }
|
|
66
|
+
&.link.info:hover { color: var(--c-text-info-2); }
|
|
67
|
+
&.link.success:hover { color: var(--c-text-success-2); }
|
|
68
|
+
&.link.warning:hover { color: var(--c-text-warning-2); }
|
|
69
|
+
&.link.danger:hover { color: var(--c-text-danger-2); }
|
|
70
|
+
}
|
|
71
|
+
</style>
|
package/lib/composables/Table.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Component, type MaybeRef, type MaybeRefOrGetter } from 'vue'
|
|
2
|
-
import { type Mode } from '../components/SButton.vue'
|
|
2
|
+
import { type Mode as ButtonMode } from '../components/SButton.vue'
|
|
3
|
+
import { type State as IndicatorState } from '../components/SIndicator.vue'
|
|
3
4
|
import { type Day } from '../support/Day'
|
|
4
5
|
import { type DropdownSection } from './Dropdown'
|
|
5
6
|
import { type Position } from './Tooltip'
|
|
@@ -55,10 +56,12 @@ export type TableColumnCellFn<V, R> = (value: V, record: R) => TableCell<V, R>
|
|
|
55
56
|
export type TableCell<V = any, R = any> =
|
|
56
57
|
| TableCellText<V, R>
|
|
57
58
|
| TableCellNumber<V, R>
|
|
59
|
+
| TableCellPath
|
|
58
60
|
| TableCellDay
|
|
59
61
|
| TableCellPill
|
|
60
62
|
| TableCellPills
|
|
61
63
|
| TableCellState
|
|
64
|
+
| TableCellIndicator
|
|
62
65
|
| TableCellAvatar<V, R>
|
|
63
66
|
| TableCellAvatars
|
|
64
67
|
| TableCellCustom
|
|
@@ -69,10 +72,12 @@ export type TableCell<V = any, R = any> =
|
|
|
69
72
|
export type TableCellType =
|
|
70
73
|
| 'text'
|
|
71
74
|
| 'number'
|
|
75
|
+
| 'path'
|
|
72
76
|
| 'day'
|
|
73
77
|
| 'pill'
|
|
74
78
|
| 'pills'
|
|
75
79
|
| 'state'
|
|
80
|
+
| 'indicator'
|
|
76
81
|
| 'avatar'
|
|
77
82
|
| 'avatars'
|
|
78
83
|
| 'custom'
|
|
@@ -116,6 +121,18 @@ export interface TableCellNumber<V = any, R = any> extends TableCellBase {
|
|
|
116
121
|
onClick?(value: V, record: R): void
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
export interface TableCellPath extends TableCellBase {
|
|
125
|
+
type: 'path'
|
|
126
|
+
segments: TableCellPathSegment[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TableCellPathSegment {
|
|
130
|
+
text: string
|
|
131
|
+
link?: string | null
|
|
132
|
+
color?: TableCellValueColor
|
|
133
|
+
onClick?(): void
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
export type TableCellValueColor = ColorModes | 'soft'
|
|
120
137
|
|
|
121
138
|
export interface TableCellDay extends TableCellBase {
|
|
@@ -187,17 +204,23 @@ export interface TableCellState extends TableCellBase {
|
|
|
187
204
|
mode?: ColorModes
|
|
188
205
|
}
|
|
189
206
|
|
|
207
|
+
export interface TableCellIndicator extends TableCellBase {
|
|
208
|
+
type: 'indicator'
|
|
209
|
+
state: IndicatorState
|
|
210
|
+
label?: string | null
|
|
211
|
+
}
|
|
212
|
+
|
|
190
213
|
export interface TableCellActions<R = any> extends TableCellBase {
|
|
191
214
|
type: 'actions'
|
|
192
215
|
actions: TableCellAction<R>[]
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
export interface TableCellAction<R = any> {
|
|
196
|
-
mode?:
|
|
219
|
+
mode?: ButtonMode
|
|
197
220
|
icon?: Component
|
|
198
|
-
iconMode?:
|
|
221
|
+
iconMode?: ButtonMode
|
|
199
222
|
label?: string
|
|
200
|
-
labelMode?:
|
|
223
|
+
labelMode?: ButtonMode
|
|
201
224
|
onClick(record: R): void
|
|
202
225
|
show?(record: R): boolean
|
|
203
226
|
}
|
|
@@ -210,9 +233,9 @@ export interface TableMenu {
|
|
|
210
233
|
|
|
211
234
|
export interface TableHeaderAction {
|
|
212
235
|
show?: boolean
|
|
213
|
-
mode?:
|
|
236
|
+
mode?: ButtonMode
|
|
214
237
|
label: string
|
|
215
|
-
labelMode?:
|
|
238
|
+
labelMode?: ButtonMode
|
|
216
239
|
onClick(): void
|
|
217
240
|
}
|
|
218
241
|
|
package/package.json
CHANGED