@globalbrain/sefirot 4.9.0 → 4.11.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 +2 -1
- package/lib/components/SInputFileUpload.vue +141 -21
- package/lib/components/SInputFileUploadItem.vue +121 -20
- package/lib/composables/App.ts +22 -0
- package/lib/composables/Http.ts +17 -0
- package/lib/composables/Lang.ts +18 -6
- package/lib/composables/Theme.ts +25 -0
- package/lib/composables/Url.ts +12 -3
- package/package.json +1 -1
|
@@ -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>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HttpOptions } from '../http/Http'
|
|
2
|
+
import { useSetupHttp } from './Http'
|
|
3
|
+
import { type HasLang, useSetupLang } from './Lang'
|
|
4
|
+
import { type HasTheme, useSetupTheme } from './Theme'
|
|
5
|
+
|
|
6
|
+
export interface SetupAppUser extends HasLang, HasTheme {}
|
|
7
|
+
|
|
8
|
+
export interface SetupAppOptions {
|
|
9
|
+
http?: HttpOptions
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useSetupApp(): (user?: SetupAppUser | null, options?: SetupAppOptions) => void {
|
|
13
|
+
const setupLang = useSetupLang()
|
|
14
|
+
const setupTheme = useSetupTheme()
|
|
15
|
+
const setupHttp = useSetupHttp()
|
|
16
|
+
|
|
17
|
+
return (user, options) => {
|
|
18
|
+
setupLang(user)
|
|
19
|
+
setupTheme(user)
|
|
20
|
+
setupHttp(user, options?.http)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Lang, useBrowserLang } from 'sefirot/composables/Lang'
|
|
2
|
+
import { Http, type HttpOptions } from 'sefirot/http/Http'
|
|
3
|
+
|
|
4
|
+
export interface HasLang {
|
|
5
|
+
lang: Lang
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useSetupHttp(): (user?: HasLang | null, options?: HttpOptions) => void {
|
|
9
|
+
const browserLang = useBrowserLang()
|
|
10
|
+
|
|
11
|
+
return (user, options = {}) => {
|
|
12
|
+
Http.config({
|
|
13
|
+
lang: user?.lang ?? browserLang,
|
|
14
|
+
...options
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
package/lib/composables/Lang.ts
CHANGED
|
@@ -11,8 +11,20 @@ export interface TransMessages<T> {
|
|
|
11
11
|
ja: T
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface HasLang {
|
|
15
|
+
lang: Lang
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export const SefirotLangKey = 'sefirot-lang-key'
|
|
15
19
|
|
|
20
|
+
export function useSetupLang(): (user?: HasLang | null) => void {
|
|
21
|
+
const browserLang = useBrowserLang()
|
|
22
|
+
|
|
23
|
+
return (user) => {
|
|
24
|
+
provideLang(user?.lang ?? browserLang)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export function provideLang(lang: Lang) {
|
|
17
29
|
provide(SefirotLangKey, lang)
|
|
18
30
|
}
|
|
@@ -23,6 +35,12 @@ export function useLang(): Lang {
|
|
|
23
35
|
return inject(SefirotLangKey, 'en') || 'en'
|
|
24
36
|
}
|
|
25
37
|
|
|
38
|
+
export function useBrowserLang(): Lang {
|
|
39
|
+
const lang = navigator.language
|
|
40
|
+
|
|
41
|
+
return lang.split('-')[0] === 'ja' ? 'ja' : 'en'
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
export function useTrans<T>(messages: TransMessages<T>): Trans<T> {
|
|
27
45
|
const lang = useLang()
|
|
28
46
|
|
|
@@ -32,9 +50,3 @@ export function useTrans<T>(messages: TransMessages<T>): Trans<T> {
|
|
|
32
50
|
t
|
|
33
51
|
}
|
|
34
52
|
}
|
|
35
|
-
|
|
36
|
-
export function useBrowserLang(): Lang {
|
|
37
|
-
const lang = navigator.language
|
|
38
|
-
|
|
39
|
-
return lang.split('-')[0] === 'ja' ? 'ja' : 'en'
|
|
40
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useDark } from '@vueuse/core'
|
|
2
|
+
import { type WritableComputedRef, computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
export type Theme = 'light' | 'dark'
|
|
5
|
+
|
|
6
|
+
export interface HasTheme {
|
|
7
|
+
theme: Theme
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useSetupTheme(): (user?: HasTheme | null) => void {
|
|
11
|
+
const theme = useTheme()
|
|
12
|
+
|
|
13
|
+
return (user) => {
|
|
14
|
+
theme.value = user?.theme ?? 'light'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useTheme(): WritableComputedRef<Theme> {
|
|
19
|
+
const _isDark = useDark()
|
|
20
|
+
|
|
21
|
+
return computed({
|
|
22
|
+
get: () => _isDark.value ? 'dark' : 'light',
|
|
23
|
+
set: (v) => { _isDark.value = v === 'dark' }
|
|
24
|
+
})
|
|
25
|
+
}
|
package/lib/composables/Url.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import isEqual from 'lodash-es/isEqual'
|
|
2
|
-
import { type MaybeRef, nextTick, unref, watch } from 'vue'
|
|
2
|
+
import { type MaybeRef, computed, nextTick, unref, watch } from 'vue'
|
|
3
3
|
import { type LocationQuery, useRoute, useRouter } from 'vue-router'
|
|
4
4
|
|
|
5
5
|
export interface UseUrlQuerySyncOptions {
|
|
@@ -21,13 +21,22 @@ export function useUrlQuerySync(
|
|
|
21
21
|
const route = useRoute()
|
|
22
22
|
const router = useRouter()
|
|
23
23
|
|
|
24
|
+
const routeQuery = computed(() => ({
|
|
25
|
+
path: route.path,
|
|
26
|
+
query: route.query
|
|
27
|
+
}))
|
|
28
|
+
|
|
24
29
|
const flattenedDefaultState = flattenObject(unref(state))
|
|
25
30
|
|
|
26
31
|
let isSyncing = false
|
|
27
32
|
|
|
28
33
|
watch(
|
|
29
|
-
|
|
30
|
-
async () => {
|
|
34
|
+
routeQuery,
|
|
35
|
+
async (to, from) => {
|
|
36
|
+
if (from && from.path !== to.path) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
if (!isSyncing) {
|
|
32
41
|
isSyncing = true
|
|
33
42
|
await setState()
|
package/package.json
CHANGED