nuxt-unified-ui 0.1.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/README.md +19 -0
- package/app/app.config.js +49 -0
- package/app/assets/css/main.css +24 -0
- package/app/components/un-form.vue +67 -0
- package/app/components/un-spinner.vue +6 -0
- package/app/components/un-typography.vue +93 -0
- package/app/composables/use-form.ts +30 -0
- package/app/dialogs/choice-picker-dialog.vue +94 -0
- package/app/dialogs/form-picker-dialog.vue +101 -0
- package/app/elements/form-element-checkbox.vue +22 -0
- package/app/elements/form-element-date.vue +70 -0
- package/app/elements/form-element-select.vue +22 -0
- package/app/elements/form-element-series.vue +153 -0
- package/app/elements/form-element-text.vue +22 -0
- package/app/utils/format-date.ts +36 -0
- package/app/utils/launch-choice-picker-dialog.ts +19 -0
- package/app/utils/launch-dialog.ts +11 -0
- package/app/utils/launch-form-picker-dialog.ts +21 -0
- package/app/utils/launch-toast.ts +26 -0
- package/app/utils/path-relative.ts +6 -0
- package/modules/radash.ts +23 -0
- package/nuxt.config.js +26 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Unified Nuxt UI
|
|
2
|
+
|
|
3
|
+
A reuseable Nuxt layer which integrates Nuxt UI and some other useful libraries into your Nuxt application.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Make sure to install the dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun i
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Development Server
|
|
14
|
+
|
|
15
|
+
Start the development server on http://localhost:8080
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun dev
|
|
19
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export default defineAppConfig({
|
|
4
|
+
ui: {
|
|
5
|
+
|
|
6
|
+
badge: {
|
|
7
|
+
defaultVariants: {
|
|
8
|
+
color: 'neutral',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
button: {
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
color: 'neutral',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
calendar: {
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
color: 'neutral',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
card: {
|
|
25
|
+
slots: {
|
|
26
|
+
body: 'p-3 sm:p-3',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
input: {
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
color: 'neutral',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
select: {
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
color: 'neutral',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
tabs: {
|
|
43
|
+
defaultVariants: {
|
|
44
|
+
color: 'neutral',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@import "tailwindcss" source("../../..");
|
|
2
|
+
@import "@nuxt/ui";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/* localization */
|
|
6
|
+
|
|
7
|
+
@utility rtl {
|
|
8
|
+
direction: rtl;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@utility ltr {
|
|
12
|
+
direction: ltr;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/* fixes */
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
@apply overflow-y-auto! ps-0!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
table thead tr th {
|
|
23
|
+
@apply text-start whitespace-nowrap;
|
|
24
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
target: Object,
|
|
7
|
+
fields: Array,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/* elements */
|
|
12
|
+
|
|
13
|
+
import FormElementText from '../elements/form-element-text.vue';
|
|
14
|
+
import FormElementSelect from '../elements/form-element-select.vue';
|
|
15
|
+
import FormElementSeries from '../elements/form-element-series.vue';
|
|
16
|
+
import FormElementCheckbox from '../elements/form-element-checkbox.vue';
|
|
17
|
+
import FormElementDate from '../elements/form-element-date.vue';
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const elementsMap = {
|
|
21
|
+
'text': FormElementText,
|
|
22
|
+
'select': FormElementSelect,
|
|
23
|
+
'series': FormElementSeries,
|
|
24
|
+
'date': FormElementDate,
|
|
25
|
+
'checkbox': FormElementCheckbox,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/* v-if */
|
|
30
|
+
|
|
31
|
+
import { matches } from 'unified-mongo-filter';
|
|
32
|
+
|
|
33
|
+
const filteredFields = computed(() => {
|
|
34
|
+
return props.fields?.filter(it =>
|
|
35
|
+
!it.vIf || matches(it.vIf, props.target)
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<div class="grid grid-cols-12 gap-3">
|
|
44
|
+
<div
|
|
45
|
+
v-for="field of filteredFields" :key="field.key"
|
|
46
|
+
:class="{
|
|
47
|
+
'col-span-12': field.width === 12 || !field.width,
|
|
48
|
+
'col-span-11': field.width === 11,
|
|
49
|
+
'col-span-10': field.width === 10,
|
|
50
|
+
'col-span-9': field.width === 9,
|
|
51
|
+
'col-span-8': field.width === 8,
|
|
52
|
+
'col-span-7': field.width === 7,
|
|
53
|
+
'col-span-6': field.width === 6,
|
|
54
|
+
'col-span-5': field.width === 5,
|
|
55
|
+
'col-span-4': field.width === 4,
|
|
56
|
+
'col-span-3': field.width === 3,
|
|
57
|
+
'col-span-2': field.width === 2,
|
|
58
|
+
'col-span-1': field.width === 1,
|
|
59
|
+
}">
|
|
60
|
+
<component
|
|
61
|
+
:is="elementsMap[field.identifier]"
|
|
62
|
+
:field="field"
|
|
63
|
+
v-model="props.target[field.key]"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
icon: {
|
|
7
|
+
type: String,
|
|
8
|
+
},
|
|
9
|
+
iconClasses: {
|
|
10
|
+
type: String,
|
|
11
|
+
},
|
|
12
|
+
title: {
|
|
13
|
+
type: String,
|
|
14
|
+
},
|
|
15
|
+
titleClasses: {
|
|
16
|
+
type: String,
|
|
17
|
+
},
|
|
18
|
+
subtitle: {
|
|
19
|
+
type: String,
|
|
20
|
+
},
|
|
21
|
+
subtitleClasses: {
|
|
22
|
+
type: String,
|
|
23
|
+
},
|
|
24
|
+
text: {
|
|
25
|
+
type: String,
|
|
26
|
+
},
|
|
27
|
+
textClasses: {
|
|
28
|
+
type: String,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const slots = useSlots();
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/* flags */
|
|
36
|
+
|
|
37
|
+
const shouldShow = computed(() => {
|
|
38
|
+
return props.icon || props.title || props.subtitle || props.text || slots.append;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div v-if="shouldShow">
|
|
46
|
+
|
|
47
|
+
<div v-if="props.title || props.subtitle || props.icon || slots.append" class="flex items-start">
|
|
48
|
+
<div v-if="props.title || props.icon" class="flex items-center" style="gap: 0.5em;">
|
|
49
|
+
<u-icon
|
|
50
|
+
v-if="props.icon"
|
|
51
|
+
:name="props.icon"
|
|
52
|
+
:class="props.iconClasses"
|
|
53
|
+
style="width: 1.3em; height: 1.3em; flex-shrink: 0; flex-grow: 0;"
|
|
54
|
+
/>
|
|
55
|
+
<h1
|
|
56
|
+
v-if="props.title"
|
|
57
|
+
class="font-medium"
|
|
58
|
+
:class="props.titleClasses"
|
|
59
|
+
style="font-size: 1.3em;">
|
|
60
|
+
{{ props.title }}
|
|
61
|
+
</h1>
|
|
62
|
+
</div>
|
|
63
|
+
<template v-if="$slots.append">
|
|
64
|
+
<div class="mt-1 ms-auto">
|
|
65
|
+
<slot name="append" />
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<h2
|
|
71
|
+
v-if="props.subtitle"
|
|
72
|
+
class="font-light"
|
|
73
|
+
:class="props.subtitleClasses"
|
|
74
|
+
style="font-size: 0.9em;"
|
|
75
|
+
:style="{
|
|
76
|
+
marginInlineStart: props.icon ? '2em' : '0',
|
|
77
|
+
}">
|
|
78
|
+
{{ props.subtitle }}
|
|
79
|
+
</h2>
|
|
80
|
+
|
|
81
|
+
<p
|
|
82
|
+
v-if="props.text"
|
|
83
|
+
:class="[
|
|
84
|
+
{
|
|
85
|
+
'mt-2': props.title || props.subtitle,
|
|
86
|
+
},
|
|
87
|
+
props.textClasses
|
|
88
|
+
]">
|
|
89
|
+
{{ props.text }}
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { UnForm } from '#components';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
interface IOptions {
|
|
5
|
+
target?: MaybeRefOrGetter<any>;
|
|
6
|
+
fields: MaybeRefOrGetter<any[]>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useForm(options: IOptions) {
|
|
10
|
+
|
|
11
|
+
const formData: Ref<any> = toRef(options.target || {});
|
|
12
|
+
const formFields = computed(() => toValue(options.fields) || []);
|
|
13
|
+
|
|
14
|
+
const FormTag = defineComponent({
|
|
15
|
+
setup() {
|
|
16
|
+
return () => {
|
|
17
|
+
return h(UnForm, {
|
|
18
|
+
target: toValue(formData),
|
|
19
|
+
fields: toValue(formFields),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
form: formData,
|
|
27
|
+
formTag: FormTag,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
icon: {
|
|
7
|
+
type: String,
|
|
8
|
+
},
|
|
9
|
+
title: {
|
|
10
|
+
type: String,
|
|
11
|
+
},
|
|
12
|
+
subtitle: {
|
|
13
|
+
type: String,
|
|
14
|
+
},
|
|
15
|
+
text: {
|
|
16
|
+
type: String,
|
|
17
|
+
},
|
|
18
|
+
startButtons: {
|
|
19
|
+
type: Array,
|
|
20
|
+
default: () => [
|
|
21
|
+
{
|
|
22
|
+
label: 'Submit',
|
|
23
|
+
value: true,
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
endButtons: {
|
|
28
|
+
type: Array,
|
|
29
|
+
default: () => [
|
|
30
|
+
{
|
|
31
|
+
variant: 'ghost',
|
|
32
|
+
label: 'Cancel',
|
|
33
|
+
value: false,
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits([
|
|
40
|
+
'close',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/* actions */
|
|
45
|
+
|
|
46
|
+
async function handleButtonClick(button) {
|
|
47
|
+
|
|
48
|
+
if (button.onClick) {
|
|
49
|
+
await button.onClick(button.value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
emit('close', button.value);
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<u-modal @update:open="!$event && emit('close')">
|
|
61
|
+
<template #content>
|
|
62
|
+
<u-card>
|
|
63
|
+
|
|
64
|
+
<un-typography
|
|
65
|
+
:icon="props.icon"
|
|
66
|
+
:title="props.title"
|
|
67
|
+
:subtitle="props.subtitle"
|
|
68
|
+
:text="props.text"
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<div class="flex items-end gap-2 mt-4">
|
|
72
|
+
|
|
73
|
+
<u-button
|
|
74
|
+
v-for="button of props.startButtons" :key="button.value || button.label || button.icon"
|
|
75
|
+
v-bind="radOmit(button, [ 'value', 'onClick' ])"
|
|
76
|
+
loading-auto
|
|
77
|
+
@click="handleButtonClick(button)"
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<div class="grow" />
|
|
81
|
+
|
|
82
|
+
<u-button
|
|
83
|
+
v-for="button of props.endButtons" :key="button.value || button.label || button.icon"
|
|
84
|
+
v-bind="radOmit(button, [ 'value', 'onClick' ])"
|
|
85
|
+
loading-auto
|
|
86
|
+
@click="handleButtonClick(button)"
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
</u-card>
|
|
92
|
+
</template>
|
|
93
|
+
</u-modal>
|
|
94
|
+
</template>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
icon: {
|
|
7
|
+
type: String,
|
|
8
|
+
},
|
|
9
|
+
title: {
|
|
10
|
+
type: String,
|
|
11
|
+
},
|
|
12
|
+
subtitle: {
|
|
13
|
+
type: String,
|
|
14
|
+
},
|
|
15
|
+
text: {
|
|
16
|
+
type: String,
|
|
17
|
+
},
|
|
18
|
+
fields: {
|
|
19
|
+
type: Array,
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
initialForm: {
|
|
23
|
+
type: Object,
|
|
24
|
+
},
|
|
25
|
+
submitButton: {
|
|
26
|
+
type: Object,
|
|
27
|
+
},
|
|
28
|
+
cancelButton: {
|
|
29
|
+
type: Object,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits([
|
|
34
|
+
'close',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/* form */
|
|
39
|
+
|
|
40
|
+
const { form, formTag } = useForm({
|
|
41
|
+
target: !props.initialForm ? undefined : JSON.parse(JSON.stringify(props.initialForm)),
|
|
42
|
+
fields: () => props.fields,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/* actions */
|
|
47
|
+
|
|
48
|
+
async function handleSubmit() {
|
|
49
|
+
|
|
50
|
+
if (props.submitButton?.onClick) {
|
|
51
|
+
await props.submitButton?.onClick(form.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
emit('close', form.value);
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<u-modal @update:open="!$event && emit('close')">
|
|
63
|
+
<template #content>
|
|
64
|
+
<u-card>
|
|
65
|
+
|
|
66
|
+
<un-typography
|
|
67
|
+
:icon="props.icon"
|
|
68
|
+
:title="props.title"
|
|
69
|
+
:subtitle="props.subtitle"
|
|
70
|
+
:text="props.text"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<form-tag
|
|
74
|
+
class="mt-4"
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<div class="flex items-end gap-2 mt-4">
|
|
78
|
+
|
|
79
|
+
<u-button
|
|
80
|
+
label="Submit"
|
|
81
|
+
v-bind="radOmit(props.submitButton, [ 'onClick' ])"
|
|
82
|
+
loading-auto
|
|
83
|
+
@click="handleSubmit()"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<div class="grow" />
|
|
87
|
+
|
|
88
|
+
<u-button
|
|
89
|
+
variant="ghost"
|
|
90
|
+
label="Cancel"
|
|
91
|
+
v-bind="radOmit(props.cancelButton, [ 'onClick' ])"
|
|
92
|
+
loading-auto
|
|
93
|
+
@click="emit('close')"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
</u-card>
|
|
99
|
+
</template>
|
|
100
|
+
</u-modal>
|
|
101
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
field: Object,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const modelValue = defineModel();
|
|
10
|
+
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<u-form-field v-bind="radPick(props.field, [ 'fieldLabel' ])">
|
|
16
|
+
<u-checkbox
|
|
17
|
+
class="w-full"
|
|
18
|
+
v-bind="radOmit(props.field, [ 'key', 'identifier', 'fieldLabel' ])"
|
|
19
|
+
v-model="modelValue"
|
|
20
|
+
/>
|
|
21
|
+
</u-form-field>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
field: Object,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const modelValue = defineModel();
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/* dates */
|
|
13
|
+
|
|
14
|
+
import { CalendarDate } from '@internationalized/date';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const internalModel = computed({
|
|
18
|
+
get: () => {
|
|
19
|
+
|
|
20
|
+
if (!modelValue.value) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const date = new Date(modelValue.value);
|
|
25
|
+
|
|
26
|
+
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
|
27
|
+
|
|
28
|
+
},
|
|
29
|
+
set: v => {
|
|
30
|
+
modelValue.value = parseDate(String(v), 'YYYY-MM-DD');
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const inputTitle = computed(() => {
|
|
36
|
+
|
|
37
|
+
if (!modelValue.value) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return formatDate(modelValue.value, 'YYYY-MM-DD');
|
|
42
|
+
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<u-popover>
|
|
50
|
+
|
|
51
|
+
<u-form-field v-bind="radPick(props.field, [ 'label' ])">
|
|
52
|
+
<u-input
|
|
53
|
+
class="w-full"
|
|
54
|
+
v-bind="radOmit(props.field, [ 'key', 'identifier', 'label' ])"
|
|
55
|
+
readonly
|
|
56
|
+
:model-value="inputTitle"
|
|
57
|
+
/>
|
|
58
|
+
</u-form-field>
|
|
59
|
+
|
|
60
|
+
<template #content>
|
|
61
|
+
|
|
62
|
+
<u-calendar
|
|
63
|
+
class="ltr p-2 [&_th]:text-center!"
|
|
64
|
+
v-model="internalModel"
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
</u-popover>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
field: Object,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const modelValue = defineModel();
|
|
10
|
+
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<u-form-field v-bind="radPick(props.field, [ 'label' ])">
|
|
16
|
+
<u-select
|
|
17
|
+
class="w-full"
|
|
18
|
+
v-bind="radOmit(props.field, [ 'key', 'identifier', 'label' ])"
|
|
19
|
+
v-model="modelValue"
|
|
20
|
+
/>
|
|
21
|
+
</u-form-field>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
field: Object,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const modelValue = defineModel();
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/* actions */
|
|
13
|
+
|
|
14
|
+
function handleAddItem() {
|
|
15
|
+
modelValue.value = [
|
|
16
|
+
...(Array.isArray(modelValue.value) ? modelValue.value : []),
|
|
17
|
+
JSON.parse(JSON.stringify( props.field.itemBase ?? {} )),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleDuplicateItem(index) {
|
|
22
|
+
modelValue.value = [
|
|
23
|
+
...(modelValue.value.slice(0, index + 1)),
|
|
24
|
+
{
|
|
25
|
+
...modelValue.value[index],
|
|
26
|
+
_id: undefined,
|
|
27
|
+
},
|
|
28
|
+
...(modelValue.value.slice(index + 1)),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleDeleteItem(index) {
|
|
33
|
+
modelValue.value = modelValue.value.filter((_, i) => i !== index);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleMoveItem(index, direction) {
|
|
37
|
+
|
|
38
|
+
const items = [ ...modelValue.value ];
|
|
39
|
+
const poppedItem = items.splice(index, 1)[0];
|
|
40
|
+
|
|
41
|
+
items.splice(index + direction, 0, poppedItem);
|
|
42
|
+
|
|
43
|
+
modelValue.value = items;
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div class="border border-default rounded">
|
|
52
|
+
|
|
53
|
+
<label class="text-sm flex items-center px-2 py-1 border-b border-default">
|
|
54
|
+
|
|
55
|
+
<span>
|
|
56
|
+
{{ props.field.label }}
|
|
57
|
+
</span>
|
|
58
|
+
|
|
59
|
+
<span class="text-xs ms-1">
|
|
60
|
+
({{ (modelValue?.length > 0) ? (`${modelValue.length} Items`) : ('None') }})
|
|
61
|
+
</span>
|
|
62
|
+
|
|
63
|
+
<u-button
|
|
64
|
+
variant="soft"
|
|
65
|
+
size="xs"
|
|
66
|
+
icon="i-lucide-plus"
|
|
67
|
+
label="New Item"
|
|
68
|
+
class="ms-3"
|
|
69
|
+
@click="handleAddItem()"
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
</label>
|
|
73
|
+
|
|
74
|
+
<template v-if="!modelValue || !(modelValue.length > 0)">
|
|
75
|
+
<p class="text-xs text-center py-6">
|
|
76
|
+
No items added yet. Click on "New Item" to add one.
|
|
77
|
+
</p>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
v-else
|
|
82
|
+
class="p-2 grid gap-2 bg-stone-100"
|
|
83
|
+
:class="{
|
|
84
|
+
'grid-cols-1': props.field.seriesColumns === 1 || !props.field.seriesColumns,
|
|
85
|
+
'grid-cols-2': props.field.seriesColumns === 2,
|
|
86
|
+
'grid-cols-3': props.field.seriesColumns === 3,
|
|
87
|
+
'grid-cols-4': props.field.seriesColumns === 4,
|
|
88
|
+
'grid-cols-5': props.field.seriesColumns === 5,
|
|
89
|
+
'grid-cols-6': props.field.seriesColumns === 6,
|
|
90
|
+
}">
|
|
91
|
+
<div
|
|
92
|
+
v-for="(item, index) of modelValue" :key="index"
|
|
93
|
+
class="relative group">
|
|
94
|
+
|
|
95
|
+
<un-form
|
|
96
|
+
:target="item"
|
|
97
|
+
:fields="props.field.itemFields"
|
|
98
|
+
class="p-2 bg-default border border-default rounded"
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<div
|
|
102
|
+
class="
|
|
103
|
+
absolute top-2 end-2
|
|
104
|
+
hidden
|
|
105
|
+
group-hover:flex group-hover:gap-1
|
|
106
|
+
">
|
|
107
|
+
|
|
108
|
+
<u-tooltip text="Duplicate">
|
|
109
|
+
<u-button
|
|
110
|
+
variant="soft"
|
|
111
|
+
icon="i-lucide-copy"
|
|
112
|
+
size="xs"
|
|
113
|
+
@click="handleDuplicateItem(index)"
|
|
114
|
+
/>
|
|
115
|
+
</u-tooltip>
|
|
116
|
+
|
|
117
|
+
<u-tooltip text="Move Back">
|
|
118
|
+
<u-button
|
|
119
|
+
v-if="index > 0"
|
|
120
|
+
variant="soft"
|
|
121
|
+
icon="i-lucide-chevron-left"
|
|
122
|
+
size="xs"
|
|
123
|
+
@click="handleMoveItem(index, -1)"
|
|
124
|
+
/>
|
|
125
|
+
</u-tooltip>
|
|
126
|
+
|
|
127
|
+
<u-tooltip text="Move Forward">
|
|
128
|
+
<u-button
|
|
129
|
+
v-if="index < modelValue.length - 1"
|
|
130
|
+
variant="soft"
|
|
131
|
+
icon="i-lucide-chevron-right"
|
|
132
|
+
size="xs"
|
|
133
|
+
@click="handleMoveItem(index, 1)"
|
|
134
|
+
/>
|
|
135
|
+
</u-tooltip>
|
|
136
|
+
|
|
137
|
+
<u-tooltip text="Delete">
|
|
138
|
+
<u-button
|
|
139
|
+
variant="soft"
|
|
140
|
+
color="error"
|
|
141
|
+
icon="i-lucide-trash"
|
|
142
|
+
size="xs"
|
|
143
|
+
@click="handleDeleteItem(index)"
|
|
144
|
+
/>
|
|
145
|
+
</u-tooltip>
|
|
146
|
+
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
|
|
3
|
+
/* interface */
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
field: Object,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const modelValue = defineModel();
|
|
10
|
+
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<u-form-field v-bind="radPick(props.field, [ 'label' ])">
|
|
16
|
+
<u-input
|
|
17
|
+
class="w-full"
|
|
18
|
+
v-bind="radOmit(props.field, [ 'key', 'identifier', 'label' ])"
|
|
19
|
+
v-model="modelValue"
|
|
20
|
+
/>
|
|
21
|
+
</u-form-field>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { format as tempoFormat, parse } from '@formkit/tempo';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export function formatDate(timestamp: number | string, format = 'YYYY/MM/DD HH:mm', locale = 'en-US', timestampFormat?: string) {
|
|
5
|
+
|
|
6
|
+
if (!timestampFormat) {
|
|
7
|
+
try {
|
|
8
|
+
return tempoFormat(new Date(Number(timestamp)), format, locale);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
try {
|
|
16
|
+
return tempoFormat(
|
|
17
|
+
parse(String(timestamp), timestampFormat, locale),
|
|
18
|
+
format,
|
|
19
|
+
locale,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseDate(date: string, format: string, locale = 'en-US') {
|
|
30
|
+
try {
|
|
31
|
+
return parse(date, format, locale).valueOf();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ButtonProps } from '@nuxt/ui';
|
|
2
|
+
import ChoicePickerDialog from '../dialogs/choice-picker-dialog.vue';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
interface IOptions {
|
|
6
|
+
icon?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
startButtons?: ( ButtonProps & { value?: string } )[];
|
|
11
|
+
endButtons?: ( ButtonProps & { value?: string } )[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function launchChoicePickerDialog(options: IOptions) {
|
|
15
|
+
return launchDialog({
|
|
16
|
+
component: ChoicePickerDialog,
|
|
17
|
+
props: options,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ButtonProps } from '@nuxt/ui';
|
|
2
|
+
import FormPickerDialog from '../dialogs/form-picker-dialog.vue';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
interface IOptions {
|
|
6
|
+
icon?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
fields: any[];
|
|
11
|
+
initialForm?: any;
|
|
12
|
+
submitButton?: ButtonProps;
|
|
13
|
+
cancelButton?: ButtonProps;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function launchFormPickerDialog(options: IOptions) {
|
|
17
|
+
return launchDialog({
|
|
18
|
+
component: FormPickerDialog,
|
|
19
|
+
props: options,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
type IToast = Parameters<ReturnType<typeof useToast>['add']>['0'];
|
|
4
|
+
type ITypedToast = Omit<IToast, 'icon' | 'color'>;
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function toast(options: IToast) {
|
|
8
|
+
useToast().add(options);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function toastSuccess(options: ITypedToast) {
|
|
13
|
+
toast({
|
|
14
|
+
icon: 'lucide:check',
|
|
15
|
+
color: 'success',
|
|
16
|
+
...options,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function toastError(options: ITypedToast) {
|
|
21
|
+
toast({
|
|
22
|
+
icon: 'lucide:circle-alert',
|
|
23
|
+
color: 'error',
|
|
24
|
+
...options,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineNuxtModule, addImports } from "@nuxt/kit";
|
|
2
|
+
import * as radash from "radash";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export default defineNuxtModule({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "nuxt-radash",
|
|
8
|
+
},
|
|
9
|
+
setup() {
|
|
10
|
+
for (const name of Object.keys(radash)) {
|
|
11
|
+
|
|
12
|
+
const prefix = 'rad';
|
|
13
|
+
const as = `${prefix}${radash.pascal(name)}`;
|
|
14
|
+
|
|
15
|
+
addImports({
|
|
16
|
+
name,
|
|
17
|
+
as,
|
|
18
|
+
from: "radash",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
});
|
package/nuxt.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { pathRelativeToBase } from './app/utils/path-relative';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export default defineNuxtConfig({
|
|
5
|
+
|
|
6
|
+
compatibilityDate: 'latest',
|
|
7
|
+
devtools: { enabled: false },
|
|
8
|
+
|
|
9
|
+
experimental: {
|
|
10
|
+
typedPages: true,
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
modules: [
|
|
14
|
+
'@vueuse/nuxt',
|
|
15
|
+
'@nuxt/ui',
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
css: [
|
|
19
|
+
pathRelativeToBase(import.meta.url, './app/assets/css/main.css'),
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
ui: {
|
|
23
|
+
colorMode: false,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-unified-ui",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"main": "./nuxt.config.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"nuxt.config.js",
|
|
8
|
+
"package.json",
|
|
9
|
+
"app",
|
|
10
|
+
"layers",
|
|
11
|
+
"modules"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@formkit/tempo": "0.1.2",
|
|
15
|
+
"@iconify-json/lucide": "1.2.72",
|
|
16
|
+
"@nuxt/kit": "4.2.1",
|
|
17
|
+
"@nuxt/ui": "4.1.0",
|
|
18
|
+
"@vueuse/core": "14.0.0",
|
|
19
|
+
"@vueuse/nuxt": "14.0.0",
|
|
20
|
+
"radash": "12.1.1",
|
|
21
|
+
"unified-mongo-filter": "0.4.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"nuxt": ">4.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|