@falcondev-oss/nuxt-layers-base 0.24.0 → 0.26.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.
|
@@ -244,21 +244,21 @@ const columns = useTableColumns<typeof data>(
|
|
|
244
244
|
class="flex flex-col gap-4"
|
|
245
245
|
>
|
|
246
246
|
{{ form.data }}
|
|
247
|
-
<UField v-slot="{
|
|
248
|
-
<UInput class="w-full" v-bind="
|
|
247
|
+
<UField v-slot="{ bind }" :field="form.fields.text.$use()">
|
|
248
|
+
<UInput class="w-full" v-bind="bind" />
|
|
249
249
|
</UField>
|
|
250
250
|
<UField
|
|
251
|
-
v-slot="{
|
|
251
|
+
v-slot="{ bind }"
|
|
252
252
|
:field="
|
|
253
253
|
form.fields.dateIso.$use({
|
|
254
254
|
translate: dateValueIsoTranslator(),
|
|
255
255
|
})
|
|
256
256
|
"
|
|
257
257
|
>
|
|
258
|
-
<UInputDatePicker class="w-full" v-bind="
|
|
258
|
+
<UInputDatePicker class="w-full" v-bind="bind" />
|
|
259
259
|
</UField>
|
|
260
|
-
<UField v-slot="{
|
|
261
|
-
<UInputDurationMinutes class="w-full" v-bind="
|
|
260
|
+
<UField v-slot="{ bind }" :field="form.fields.duration.$use()">
|
|
261
|
+
<UInputDurationMinutes class="w-full" v-bind="bind" />
|
|
262
262
|
</UField>
|
|
263
263
|
</UForm>
|
|
264
264
|
</UCard>
|
|
@@ -6,15 +6,5 @@ export default defineNuxtConfig({
|
|
|
6
6
|
projectId: 'my-project',
|
|
7
7
|
},
|
|
8
8
|
},
|
|
9
|
-
typescript: {
|
|
10
|
-
tsConfig: {
|
|
11
|
-
vueCompilerOptions: {
|
|
12
|
-
strictTemplates: true,
|
|
13
|
-
strictVModel: false,
|
|
14
|
-
htmlAttributes: ['aria-*'],
|
|
15
|
-
dataAttributes: ['data-*'],
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
9
|
css: ['~/assets/test.css'],
|
|
20
10
|
})
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
<script setup lang="ts" generic="T
|
|
1
|
+
<script setup lang="ts" generic="T">
|
|
2
2
|
import type { FormField } from '@falcondev-oss/form-core'
|
|
3
3
|
import type { FormFieldProps, FormFieldSlots } from '@nuxt/ui'
|
|
4
4
|
import { useForwardProps } from 'reka-ui'
|
|
5
5
|
import * as R from 'remeda'
|
|
6
6
|
|
|
7
|
-
type
|
|
7
|
+
type InputProps<T> = {
|
|
8
8
|
'modelValue': T
|
|
9
9
|
'onUpdate:modelValue': (value: T) => void
|
|
10
10
|
'onBlur': () => void
|
|
11
11
|
'disabled': boolean
|
|
12
12
|
'loading': boolean
|
|
13
|
-
'modelModifiers':
|
|
13
|
+
'modelModifiers': { nullable: true }
|
|
14
14
|
'placeholder'?: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const props = defineProps<
|
|
18
18
|
FormFieldProps & {
|
|
19
19
|
field: FormField<T>
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
>()
|
|
22
|
+
|
|
22
23
|
const slots = defineSlots<
|
|
23
24
|
{
|
|
24
|
-
default: (slot: {
|
|
25
|
+
default: (slot: {
|
|
26
|
+
bind: InputProps<T>
|
|
27
|
+
model: WritableComputedRef<T>
|
|
28
|
+
field: FormField<T>
|
|
29
|
+
}) => any
|
|
25
30
|
} & Omit<FormFieldSlots, 'default'>
|
|
26
31
|
>()
|
|
27
32
|
|
|
@@ -52,7 +57,7 @@ const formFieldProps = computed<FormFieldProps>(() => {
|
|
|
52
57
|
}
|
|
53
58
|
})
|
|
54
59
|
|
|
55
|
-
const
|
|
60
|
+
const bind = computed(() => {
|
|
56
61
|
const field = forwardedProps.value.field
|
|
57
62
|
|
|
58
63
|
const placeholder = field.errors && field.errors.join('\n')
|
|
@@ -63,12 +68,21 @@ const inputProps = computed(() => {
|
|
|
63
68
|
'onBlur': () => field.handleBlur(),
|
|
64
69
|
'disabled': field.disabled,
|
|
65
70
|
'loading': field.isPending,
|
|
66
|
-
'modelModifiers': (props.nullable === true
|
|
67
|
-
? { nullable: true }
|
|
68
|
-
: undefined) as true extends Nullable ? { nullable: true } : undefined,
|
|
69
71
|
placeholder,
|
|
70
|
-
|
|
72
|
+
'modelModifiers': { nullable: true },
|
|
73
|
+
} satisfies InputProps<T>
|
|
71
74
|
})
|
|
75
|
+
|
|
76
|
+
const model = computed({
|
|
77
|
+
get() {
|
|
78
|
+
return forwardedProps.value.field.value
|
|
79
|
+
},
|
|
80
|
+
set(value: T) {
|
|
81
|
+
forwardedProps.value.field.handleChange(value)
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const model_ = { model }
|
|
72
86
|
</script>
|
|
73
87
|
|
|
74
88
|
<template>
|
|
@@ -101,7 +115,7 @@ const inputProps = computed(() => {
|
|
|
101
115
|
</span>
|
|
102
116
|
</template>
|
|
103
117
|
|
|
104
|
-
<slot v-bind="{
|
|
118
|
+
<slot v-bind="{ bind, model: model_.model, field: forwardedProps.field }">
|
|
105
119
|
<DevOnly>
|
|
106
120
|
<p class="font-black text-red-500">UField missing slot</p>
|
|
107
121
|
</DevOnly>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script setup lang="ts" generic="Multiple extends boolean = false">
|
|
2
|
+
import type { FileUploadEmits, FileUploadProps } from '@nuxt/ui'
|
|
3
|
+
import { useForwardPropsEmits } from 'reka-ui'
|
|
4
|
+
import { omit } from 'remeda'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<
|
|
7
|
+
FileUploadProps<Multiple> & {
|
|
8
|
+
/**
|
|
9
|
+
* Set to `false` to disable compression
|
|
10
|
+
*/
|
|
11
|
+
compression?:
|
|
12
|
+
| boolean
|
|
13
|
+
| {
|
|
14
|
+
/**
|
|
15
|
+
* @default 1920
|
|
16
|
+
*/
|
|
17
|
+
maxDimension?: number
|
|
18
|
+
/**
|
|
19
|
+
* @default 0.85
|
|
20
|
+
*/
|
|
21
|
+
quality?: number
|
|
22
|
+
/**
|
|
23
|
+
* @default 'image/webp'
|
|
24
|
+
*/
|
|
25
|
+
outputType?: string
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
>()
|
|
29
|
+
const emit = defineEmits<
|
|
30
|
+
FileUploadEmits & {
|
|
31
|
+
compressed: [
|
|
32
|
+
event: {
|
|
33
|
+
original: File
|
|
34
|
+
compressed: File
|
|
35
|
+
savedBytes: number
|
|
36
|
+
savedPercentage: number
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
>()
|
|
41
|
+
const forwarded = useForwardPropsEmits(props, emit)
|
|
42
|
+
|
|
43
|
+
type Files = Multiple extends true ? File[] : File
|
|
44
|
+
const model = defineModel<Files | null>()
|
|
45
|
+
|
|
46
|
+
const compressedFiles = new WeakMap<File, File>()
|
|
47
|
+
async function forwardFiles(files: File | File[] | null | undefined) {
|
|
48
|
+
if (!files) {
|
|
49
|
+
model.value = files
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const filesArray = Array.isArray(files) ? files : [files]
|
|
54
|
+
const compressed = await Promise.all(
|
|
55
|
+
filesArray.map(async (file) => {
|
|
56
|
+
if (!file.type.startsWith('image/')) return file
|
|
57
|
+
return await compressImage(file)
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
model.value = (forwarded.value.multiple ? compressed : compressed[0]!) as Files
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function compressImage(file: File) {
|
|
65
|
+
if (forwarded.value.compression === false) return file
|
|
66
|
+
if (compressedFiles.has(file)) return compressedFiles.get(file)!
|
|
67
|
+
|
|
68
|
+
const maxDimension =
|
|
69
|
+
typeof forwarded.value.compression === 'object'
|
|
70
|
+
? (forwarded.value.compression?.maxDimension ?? 1920)
|
|
71
|
+
: 1920
|
|
72
|
+
const quality =
|
|
73
|
+
typeof forwarded.value.compression === 'object'
|
|
74
|
+
? (forwarded.value.compression?.quality ?? 0.85)
|
|
75
|
+
: 0.85
|
|
76
|
+
const outputType =
|
|
77
|
+
typeof forwarded.value.compression === 'object'
|
|
78
|
+
? (forwarded.value.compression?.outputType ?? 'image/webp')
|
|
79
|
+
: 'image/webp'
|
|
80
|
+
|
|
81
|
+
const img = new Image()
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
img.addEventListener('load', resolve)
|
|
84
|
+
img.addEventListener('error', reject)
|
|
85
|
+
img.src = URL.createObjectURL(file)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const scale = Math.min(1, maxDimension / Math.max(img.width, img.height))
|
|
89
|
+
const canvas = document.createElement('canvas')
|
|
90
|
+
canvas.width = img.width * scale
|
|
91
|
+
canvas.height = img.height * scale
|
|
92
|
+
const ctx = canvas.getContext('2d')!
|
|
93
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
94
|
+
URL.revokeObjectURL(img.src)
|
|
95
|
+
|
|
96
|
+
return new Promise<File>((resolve, reject) => {
|
|
97
|
+
canvas.toBlob(
|
|
98
|
+
(blob) => {
|
|
99
|
+
if (!blob) return reject(new Error('Canvas is empty'))
|
|
100
|
+
|
|
101
|
+
const name = file.name.replace(/\.\w+$/, `.${outputType.split('/')[1]}`)
|
|
102
|
+
const compressedFile = new File([blob], name, { type: outputType })
|
|
103
|
+
|
|
104
|
+
compressedFiles.set(file, compressedFile)
|
|
105
|
+
compressedFiles.set(compressedFile, compressedFile) // required since we set model value to the compressed file
|
|
106
|
+
resolve(compressedFile)
|
|
107
|
+
|
|
108
|
+
emit('compressed', {
|
|
109
|
+
original: file,
|
|
110
|
+
compressed: compressedFile,
|
|
111
|
+
savedBytes: file.size - compressedFile.size,
|
|
112
|
+
savedPercentage: ((file.size - compressedFile.size) / file.size) * 100,
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
outputType,
|
|
116
|
+
quality,
|
|
117
|
+
)
|
|
118
|
+
canvas.remove()
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<UFileUpload
|
|
125
|
+
v-bind="omit(forwarded, ['compression'])"
|
|
126
|
+
@update:model-value="(files) => forwardFiles(files)"
|
|
127
|
+
/>
|
|
128
|
+
</template>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falcondev-oss/nuxt-layers-base",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"description": "Nuxt layer with lots of useful helpers and @nuxt/ui components",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:falcondev-oss/nuxt-layers",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"@iconify-json/lucide": "^1.2.94",
|
|
25
25
|
"@internationalized/date": "^3.11.0",
|
|
26
26
|
"@nuxt/icon": "^2.2.1",
|
|
27
|
-
"@nuxt/ui": "~4.5.0",
|
|
28
27
|
"@nuxtjs/color-mode": "^4.0.0",
|
|
29
28
|
"@tanstack/vue-query": "^5.92.9",
|
|
30
29
|
"@trpc/client": "^11.10.0",
|