@aerogel/core 0.1.1-next.97f5cf5c97f16ab8a0ef63b82aaab049a0f9e15b → 0.1.1-next.99766defd90af84297715d34b226d66e90304026
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/dist/aerogel-core.d.ts +401 -112
- package/dist/aerogel-core.js +1838 -1420
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +2 -1
- package/src/bootstrap/index.ts +2 -1
- package/src/components/AppLayout.vue +1 -1
- package/src/components/contracts/Button.ts +1 -1
- package/src/components/contracts/Combobox.ts +5 -0
- package/src/components/contracts/Modal.ts +2 -0
- package/src/components/contracts/Select.ts +98 -4
- package/src/components/contracts/Toast.ts +1 -1
- package/src/components/contracts/index.ts +1 -0
- package/src/components/headless/HeadlessInputInput.vue +16 -5
- package/src/components/headless/HeadlessSelect.vue +10 -91
- package/src/components/headless/HeadlessSelectOption.vue +1 -5
- package/src/components/index.ts +1 -0
- package/src/components/ui/AdvancedOptions.vue +4 -13
- package/src/components/ui/Button.vue +1 -0
- package/src/components/ui/Combobox.vue +94 -0
- package/src/components/ui/ComboboxLabel.vue +29 -0
- package/src/components/ui/ComboboxOption.vue +46 -0
- package/src/components/ui/ComboboxOptions.vue +71 -0
- package/src/components/ui/ComboboxTrigger.vue +67 -0
- package/src/components/ui/Details.vue +33 -0
- package/src/components/ui/Input.vue +12 -4
- package/src/components/ui/LoadingModal.vue +1 -2
- package/src/components/ui/Modal.vue +30 -11
- package/src/components/ui/ProgressBar.vue +16 -2
- package/src/components/ui/Select.vue +2 -0
- package/src/components/ui/SelectTrigger.vue +13 -2
- package/src/components/ui/SettingsModal.vue +1 -1
- package/src/components/ui/Toast.vue +1 -0
- package/src/components/ui/index.ts +6 -0
- package/src/components/vue/Provide.vue +11 -0
- package/src/components/vue/index.ts +1 -0
- package/src/errors/Errors.ts +4 -0
- package/src/errors/index.ts +9 -0
- package/src/forms/FormController.test.ts +4 -4
- package/src/forms/FormController.ts +7 -3
- package/src/forms/index.ts +11 -0
- package/src/forms/utils.ts +36 -17
- package/src/forms/validation.ts +5 -1
- package/src/index.css +10 -0
- package/src/jobs/Job.ts +1 -1
- package/src/services/App.state.ts +1 -0
- package/src/services/App.ts +4 -0
- package/src/services/index.ts +7 -0
- package/src/utils/composition/reactiveSet.ts +10 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/time.ts +7 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ComboboxPortal>
|
|
3
|
+
<ComboboxContent
|
|
4
|
+
position="popper"
|
|
5
|
+
:align="select.align"
|
|
6
|
+
:side="select.side"
|
|
7
|
+
:side-offset="4"
|
|
8
|
+
:class="renderedClasses"
|
|
9
|
+
>
|
|
10
|
+
<ComboboxViewport>
|
|
11
|
+
<ComboboxEmpty class="group p-1 outline-none">
|
|
12
|
+
<div
|
|
13
|
+
class="relative flex max-w-[calc(100vw-2rem)] items-center gap-2 truncate rounded-md px-2 py-1 text-sm select-none *:truncate"
|
|
14
|
+
>
|
|
15
|
+
{{ $td('ui.comboboxEmpty', 'No options found') }}
|
|
16
|
+
</div>
|
|
17
|
+
</ComboboxEmpty>
|
|
18
|
+
|
|
19
|
+
<ComboboxGroup ref="$group">
|
|
20
|
+
<ComboboxOption
|
|
21
|
+
v-if="showInputOption"
|
|
22
|
+
:value="newInputValue?.(combobox.input) ?? (combobox.input as AcceptableValue)"
|
|
23
|
+
@select="$emit('select')"
|
|
24
|
+
>
|
|
25
|
+
{{ combobox.input }}
|
|
26
|
+
</ComboboxOption>
|
|
27
|
+
<ComboboxOption
|
|
28
|
+
v-for="option in filteredOptions"
|
|
29
|
+
:key="option.key"
|
|
30
|
+
:value="option.value"
|
|
31
|
+
@select="$emit('select')"
|
|
32
|
+
/>
|
|
33
|
+
</ComboboxGroup>
|
|
34
|
+
</ComboboxViewport>
|
|
35
|
+
</ComboboxContent>
|
|
36
|
+
</ComboboxPortal>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxPortal, ComboboxViewport, useFilter } from 'reka-ui';
|
|
41
|
+
import { computed, useTemplateRef, watch } from 'vue';
|
|
42
|
+
import type { Nullable } from '@noeldemartin/utils';
|
|
43
|
+
import type { AcceptableValue } from 'reka-ui';
|
|
44
|
+
|
|
45
|
+
import { classes, injectReactiveOrFail } from '@aerogel/core/utils';
|
|
46
|
+
import type { FormFieldValue } from '@aerogel/core/forms';
|
|
47
|
+
import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
|
|
48
|
+
import type { ComboboxContext } from '@aerogel/core/components/contracts/Combobox';
|
|
49
|
+
|
|
50
|
+
import ComboboxOption from './ComboboxOption.vue';
|
|
51
|
+
|
|
52
|
+
defineEmits<{ select: [] }>();
|
|
53
|
+
|
|
54
|
+
const { newInputValue } = defineProps<{ newInputValue?: (value: string) => Nullable<FormFieldValue> }>();
|
|
55
|
+
const { contains } = useFilter({ sensitivity: 'base' });
|
|
56
|
+
const select = injectReactiveOrFail<SelectExpose>('select', '<ComboboxOptions> must be a child of a <Combobox>');
|
|
57
|
+
const combobox = injectReactiveOrFail<ComboboxContext>('combobox');
|
|
58
|
+
const $group = useTemplateRef('$group');
|
|
59
|
+
const filteredOptions = computed(
|
|
60
|
+
() => select.options?.filter((option) => contains(option.label, combobox.input)) ?? [],
|
|
61
|
+
);
|
|
62
|
+
const showInputOption = computed(
|
|
63
|
+
() => combobox.input && !filteredOptions.value.some((option) => option.label === combobox.input),
|
|
64
|
+
);
|
|
65
|
+
const renderedClasses = classes(
|
|
66
|
+
'max-h-(--reka-combobox-content-available-height) min-w-(--reka-combobox-trigger-width)',
|
|
67
|
+
'z-50 overflow-auto rounded-lg bg-white text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
watch($group, () => (combobox.$group = $group.value?.$el ?? null));
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ComboboxAnchor class="relative">
|
|
3
|
+
<ComboboxInput
|
|
4
|
+
:id="select.id"
|
|
5
|
+
v-model="combobox.input"
|
|
6
|
+
:placeholder="select.placeholder"
|
|
7
|
+
:class="renderedRootClasses"
|
|
8
|
+
:display-value="select.renderOption"
|
|
9
|
+
:name="select.name"
|
|
10
|
+
@focus="$emit('focus')"
|
|
11
|
+
@blur="onBlur()"
|
|
12
|
+
@keydown.esc="$emit('blur')"
|
|
13
|
+
/>
|
|
14
|
+
<div v-if="select?.errors" class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
|
15
|
+
<IconExclamationSolid class="size-5 text-red-500" />
|
|
16
|
+
</div>
|
|
17
|
+
</ComboboxAnchor>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import IconExclamationSolid from '~icons/zondicons/exclamation-solid';
|
|
22
|
+
|
|
23
|
+
import { ComboboxAnchor, ComboboxInput } from 'reka-ui';
|
|
24
|
+
import { computed, watch } from 'vue';
|
|
25
|
+
|
|
26
|
+
import { classes, injectReactiveOrFail } from '@aerogel/core/utils';
|
|
27
|
+
import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
|
|
28
|
+
import type { ComboboxContext } from '@aerogel/core/components/contracts/Combobox';
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{ focus: []; change: []; blur: [] }>();
|
|
31
|
+
const select = injectReactiveOrFail<SelectExpose>('select', '<ComboboxTrigger> must be a child of a <Combobox>');
|
|
32
|
+
const combobox = injectReactiveOrFail<ComboboxContext>('combobox');
|
|
33
|
+
const renderedRootClasses = computed(() =>
|
|
34
|
+
classes(
|
|
35
|
+
// eslint-disable-next-line vue/max-len
|
|
36
|
+
'block w-full rounded-md border-0 py-1.5 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6',
|
|
37
|
+
{
|
|
38
|
+
'mt-1': select.label,
|
|
39
|
+
'focus:ring-primary-600': !select.errors,
|
|
40
|
+
'text-gray-900 shadow-2xs ring-gray-900/10 placeholder:text-gray-400': !select.errors,
|
|
41
|
+
'pr-10 text-red-900 ring-red-900/10 placeholder:text-red-300 focus:ring-red-500': select.errors,
|
|
42
|
+
},
|
|
43
|
+
));
|
|
44
|
+
|
|
45
|
+
function onBlur() {
|
|
46
|
+
const elements = Array.from(document.querySelectorAll(':hover'));
|
|
47
|
+
|
|
48
|
+
if (elements.some((element) => combobox.$group?.contains(element))) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
emit('blur');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
watch(
|
|
56
|
+
() => combobox.input,
|
|
57
|
+
() => {
|
|
58
|
+
if (combobox.preventChange) {
|
|
59
|
+
combobox.preventChange = false;
|
|
60
|
+
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
emit('change');
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<details class="group">
|
|
3
|
+
<summary :class="renderedSummaryClasses">
|
|
4
|
+
<IconCheveronRight class="size-6 transition-transform group-open:rotate-90" />
|
|
5
|
+
<slot name="label">
|
|
6
|
+
<span>{{ label ?? $td('ui.details', 'Details') }}</span>
|
|
7
|
+
</slot>
|
|
8
|
+
</summary>
|
|
9
|
+
|
|
10
|
+
<div :class="renderedContentClasses">
|
|
11
|
+
<slot />
|
|
12
|
+
</div>
|
|
13
|
+
</details>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import IconCheveronRight from '~icons/zondicons/cheveron-right';
|
|
18
|
+
import { classes } from '@aerogel/core/utils';
|
|
19
|
+
import { type HTMLAttributes, computed } from 'vue';
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
label = undefined,
|
|
23
|
+
contentClass,
|
|
24
|
+
summaryClass,
|
|
25
|
+
} = defineProps<{ label?: string; contentClass?: HTMLAttributes['class']; summaryClass?: HTMLAttributes['class'] }>();
|
|
26
|
+
const renderedContentClasses = computed(() => classes('pt-2 pl-4', contentClass));
|
|
27
|
+
const renderedSummaryClasses = computed(() =>
|
|
28
|
+
classes(
|
|
29
|
+
'-ml-2 flex w-[max-content] items-center rounded-lg py-2 pr-3 pl-1 max-w-full',
|
|
30
|
+
'hover:bg-gray-100 focus-visible:outline focus-visible:outline-gray-700',
|
|
31
|
+
summaryClass,
|
|
32
|
+
));
|
|
33
|
+
</script>
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<IconExclamationSolid class="size-5 text-red-500" />
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
16
|
-
<HeadlessInputDescription class="
|
|
17
|
-
<HeadlessInputError class="
|
|
16
|
+
<HeadlessInputDescription :class="renderedDescriptionClasses" />
|
|
17
|
+
<HeadlessInputError :class="renderedErrorClasses" />
|
|
18
18
|
</HeadlessInput>
|
|
19
19
|
</template>
|
|
20
20
|
|
|
@@ -35,13 +35,21 @@ import type { InputEmits, InputProps } from '@aerogel/core/components/contracts/
|
|
|
35
35
|
|
|
36
36
|
defineOptions({ inheritAttrs: false });
|
|
37
37
|
defineEmits<InputEmits>();
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
const { label, inputClass, wrapperClass, descriptionClass, errorClass, ...props } = defineProps<
|
|
40
|
+
InputProps & {
|
|
41
|
+
inputClass?: HTMLAttributes['class'];
|
|
42
|
+
wrapperClass?: HTMLAttributes['class'];
|
|
43
|
+
descriptionClass?: HTMLAttributes['class'];
|
|
44
|
+
errorClass?: HTMLAttributes['class'];
|
|
45
|
+
}
|
|
40
46
|
>();
|
|
41
47
|
const $input = useTemplateRef('$inputRef');
|
|
42
48
|
const [inputAttrs, rootClasses] = useInputAttrs();
|
|
43
49
|
const renderedWrapperClasses = computed(() =>
|
|
44
50
|
classes('relative rounded-md shadow-2xs', { 'mt-1': label }, wrapperClass));
|
|
51
|
+
const renderedDescriptionClasses = computed(() => classes('mt-2 text-sm text-gray-600', descriptionClass));
|
|
52
|
+
const renderedErrorClasses = computed(() => classes('mt-2 text-sm text-red-600', errorClass));
|
|
45
53
|
const renderedInputClasses = computed(() =>
|
|
46
54
|
classes(
|
|
47
55
|
// eslint-disable-next-line vue/max-len
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Modal
|
|
3
3
|
persistent
|
|
4
|
-
class="flex"
|
|
5
4
|
wrapper-class="w-auto"
|
|
6
5
|
:title="renderedTitle"
|
|
7
6
|
:title-hidden
|
|
8
|
-
:class="{ 'flex-col-reverse': showProgress, 'items-center justify-center gap-2': !showProgress }"
|
|
7
|
+
:class="{ 'flex-col-reverse': showProgress, 'flex-row items-center justify-center gap-2': !showProgress }"
|
|
9
8
|
>
|
|
10
9
|
<ProgressBar
|
|
11
10
|
v-if="showProgress"
|
|
@@ -12,10 +12,15 @@
|
|
|
12
12
|
'animate-[fade-in_var(--tw-duration)_ease-in-out]': !hasRenderedModals,
|
|
13
13
|
'bg-black/30': firstVisibleModal?.id === id || (!firstVisibleModal && modals[0]?.id === id),
|
|
14
14
|
'opacity-0': !firstVisibleModal,
|
|
15
|
+
hidden: renderFullscreen,
|
|
15
16
|
}"
|
|
16
17
|
/>
|
|
17
18
|
<HeadlessModalContent v-bind="contentProps" :class="renderedWrapperClass">
|
|
18
|
-
<div
|
|
19
|
+
<div
|
|
20
|
+
v-if="!persistent && !closeHidden"
|
|
21
|
+
class="absolute top-0 right-0 pt-3.5 pr-2.5"
|
|
22
|
+
:class="{ 'hidden sm:block': !renderFullscreen }"
|
|
23
|
+
>
|
|
19
24
|
<button
|
|
20
25
|
type="button"
|
|
21
26
|
class="clickable z-10 rounded-full p-2.5 text-gray-400 hover:text-gray-500"
|
|
@@ -67,6 +72,7 @@ import HeadlessModalContent from '@aerogel/core/components/headless/HeadlessModa
|
|
|
67
72
|
import HeadlessModalDescription from '@aerogel/core/components/headless/HeadlessModalDescription.vue';
|
|
68
73
|
import HeadlessModalOverlay from '@aerogel/core/components/headless/HeadlessModalOverlay.vue';
|
|
69
74
|
import HeadlessModalTitle from '@aerogel/core/components/headless/HeadlessModalTitle.vue';
|
|
75
|
+
import UI from '@aerogel/core/ui/UI';
|
|
70
76
|
import { classes } from '@aerogel/core/utils/classes';
|
|
71
77
|
import { reactiveSet } from '@aerogel/core/utils';
|
|
72
78
|
import { injectModal, modals, useModal } from '@aerogel/core/ui/modals';
|
|
@@ -88,6 +94,8 @@ const {
|
|
|
88
94
|
description,
|
|
89
95
|
persistent,
|
|
90
96
|
closeHidden,
|
|
97
|
+
fullscreen,
|
|
98
|
+
fullscreenOnMobile,
|
|
91
99
|
...props
|
|
92
100
|
} = defineProps<
|
|
93
101
|
ModalProps & {
|
|
@@ -113,18 +121,29 @@ const firstVisibleModal = computed(() => modals.value.find((modal) => modal.visi
|
|
|
113
121
|
const hasRenderedModals = computed(() => modals.value.some((modal) => renderedModals.has(modal)));
|
|
114
122
|
const contentProps = computed(() => (description ? {} : { 'aria-describedby': undefined }));
|
|
115
123
|
const renderedContentClass = computed(() =>
|
|
116
|
-
classes(
|
|
124
|
+
classes(
|
|
125
|
+
'overflow-auto px-4 pb-4 flex flex-col flex-1',
|
|
126
|
+
{ 'pt-4': !title || titleHidden, 'max-h-[90vh]': !renderFullscreen.value },
|
|
127
|
+
contentClass,
|
|
128
|
+
));
|
|
129
|
+
const renderFullscreen = computed(() => fullscreen || (fullscreenOnMobile && UI.mobile));
|
|
117
130
|
const renderedWrapperClass = computed(() =>
|
|
118
131
|
classes(
|
|
119
|
-
'isolate fixed
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
'isolate fixed z-50 flex flex-col overflow-hidden bg-white text-left duration-300',
|
|
133
|
+
renderFullscreen.value
|
|
134
|
+
? [
|
|
135
|
+
'inset-0 transition-[transform,translate] will-change-[transform,translate]',
|
|
136
|
+
renderedModals.has(modal.value) || 'animate-[slide-in_var(--tw-duration)_ease-in-out]',
|
|
137
|
+
inForeground.value ? 'translate-y-0' : 'translate-y-full',
|
|
138
|
+
]
|
|
139
|
+
: [
|
|
140
|
+
'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full',
|
|
141
|
+
'max-w-[calc(100%-2rem)] rounded-lg shadow-xl sm:max-w-lg',
|
|
142
|
+
'transition-[scale,opacity] will-change-[scale,opacity]',
|
|
143
|
+
renderedModals.has(modal.value) ||
|
|
144
|
+
'animate-[fade-in_var(--tw-duration)_ease-in-out,grow_var(--tw-duration)_ease-in-out]',
|
|
145
|
+
inForeground.value ? 'scale-100 opacity-100' : 'scale-50 opacity-0',
|
|
146
|
+
],
|
|
128
147
|
wrapperClass,
|
|
129
148
|
));
|
|
130
149
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
|
2
|
+
<div class="relative mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
|
3
3
|
<div :class="filledClasses" :style="`transform:translateX(-${(1 - renderedProgress) * 100}%)`" />
|
|
4
|
+
<div
|
|
5
|
+
v-if="overflowWidthPercentage"
|
|
6
|
+
:class="overflowClasses"
|
|
7
|
+
:style="{ width: `${overflowWidthPercentage}%` }"
|
|
8
|
+
/>
|
|
4
9
|
<span class="sr-only">
|
|
5
10
|
{{
|
|
6
11
|
$td('ui.progress', '{progress}% complete', {
|
|
@@ -18,8 +23,9 @@ import { classes } from '@aerogel/core/utils/classes';
|
|
|
18
23
|
import type Job from '@aerogel/core/jobs/Job';
|
|
19
24
|
import type { Falsifiable } from '@aerogel/core/utils';
|
|
20
25
|
|
|
21
|
-
const { filledClass, progress, job } = defineProps<{
|
|
26
|
+
const { filledClass, overflowClass, progress, job } = defineProps<{
|
|
22
27
|
filledClass?: string;
|
|
28
|
+
overflowClass?: string;
|
|
23
29
|
progress?: number;
|
|
24
30
|
job?: Falsifiable<Job>;
|
|
25
31
|
}>();
|
|
@@ -28,6 +34,12 @@ let cleanup: Falsifiable<Function>;
|
|
|
28
34
|
const jobProgress = ref(0);
|
|
29
35
|
const filledClasses = computed(() =>
|
|
30
36
|
classes('size-full transition-transform duration-500 rounded-r-full ease-linear bg-primary-600', filledClass));
|
|
37
|
+
const overflowClasses = computed(() =>
|
|
38
|
+
classes(
|
|
39
|
+
'absolute inset-y-0 right-0 size-full rounded-r-full',
|
|
40
|
+
'bg-primary-900 transition-[width] duration-500 ease-linear',
|
|
41
|
+
overflowClass,
|
|
42
|
+
));
|
|
31
43
|
const renderedProgress = computed(() => {
|
|
32
44
|
if (typeof progress === 'number') {
|
|
33
45
|
return progress;
|
|
@@ -35,6 +47,8 @@ const renderedProgress = computed(() => {
|
|
|
35
47
|
|
|
36
48
|
return jobProgress.value;
|
|
37
49
|
});
|
|
50
|
+
const overflowWidthPercentage = computed(() =>
|
|
51
|
+
renderedProgress.value > 1 ? 100 * ((renderedProgress.value - 1) / renderedProgress.value) : null);
|
|
38
52
|
|
|
39
53
|
watch(
|
|
40
54
|
() => job,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<SelectLabel />
|
|
4
4
|
<slot>
|
|
5
5
|
<SelectTrigger />
|
|
6
|
+
<HeadlessSelectError class="mt-2 text-sm text-red-600" />
|
|
6
7
|
<SelectOptions />
|
|
7
8
|
</slot>
|
|
8
9
|
</HeadlessSelect>
|
|
@@ -19,6 +20,7 @@ import type { FormFieldValue } from '@aerogel/core/forms';
|
|
|
19
20
|
import SelectLabel from './SelectLabel.vue';
|
|
20
21
|
import SelectOptions from './SelectOptions.vue';
|
|
21
22
|
import SelectTrigger from './SelectTrigger.vue';
|
|
23
|
+
import HeadlessSelectError from '../headless/HeadlessSelectError.vue';
|
|
22
24
|
|
|
23
25
|
defineProps<SelectProps<T>>();
|
|
24
26
|
defineEmits<SelectEmits<T>>();
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
<HeadlessSelectTrigger :class="renderedClasses">
|
|
3
3
|
<HeadlessSelectValue class="col-start-1 row-start-1 truncate pr-6" />
|
|
4
4
|
<IconCheveronDown class="col-start-1 row-start-1 size-5 self-center justify-self-end text-gray-500 sm:size-4" />
|
|
5
|
+
<div v-if="select?.errors" class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
|
6
|
+
<IconExclamationSolid class="size-5 text-red-500" />
|
|
7
|
+
</div>
|
|
5
8
|
</HeadlessSelectTrigger>
|
|
6
9
|
</template>
|
|
7
10
|
|
|
8
11
|
<script setup lang="ts">
|
|
9
12
|
import IconCheveronDown from '~icons/zondicons/cheveron-down';
|
|
13
|
+
import IconExclamationSolid from '~icons/zondicons/exclamation-solid';
|
|
10
14
|
|
|
11
15
|
import { computed } from 'vue';
|
|
12
16
|
import type { HTMLAttributes } from 'vue';
|
|
@@ -22,8 +26,15 @@ const select = injectReactiveOrFail<SelectExpose>('select', '<SelectTrigger> mus
|
|
|
22
26
|
const renderedClasses = computed(() =>
|
|
23
27
|
classes(
|
|
24
28
|
// eslint-disable-next-line vue/max-len
|
|
25
|
-
'
|
|
26
|
-
|
|
29
|
+
'relative grid w-full cursor-default grid-cols-1 rounded-md bg-white py-1.5 pr-2 pl-3 text-left text-gray-900 outline-1 -outline-offset-1',
|
|
30
|
+
'focus:outline-2 focus:-outline-offset-2 sm:text-sm/6',
|
|
31
|
+
{
|
|
32
|
+
'mt-1': select.label,
|
|
33
|
+
'outline-gray-300 focus:outline-primary-600 data-[state=open]:outline-primary-600': !select.errors,
|
|
34
|
+
'text-gray-900 shadow-2xs ring-gray-900/10 placeholder:text-gray-400': !select.errors,
|
|
35
|
+
'outline-red-900/10 pr-10 text-red-900 placeholder:text-red-300': select.errors,
|
|
36
|
+
'focus:outline-red-500 data-[state=open]:outline-red-500': select.errors,
|
|
37
|
+
},
|
|
27
38
|
rootClasses,
|
|
28
39
|
));
|
|
29
40
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Modal :title="$td('settings.title', 'Settings')">
|
|
2
|
+
<Modal :title="$td('settings.title', 'Settings')" :fullscreen-on-mobile="$app.settingsFullscreenOnMobile">
|
|
3
3
|
<component :is="setting.component" v-for="(setting, key) in settings" :key />
|
|
4
4
|
</Modal>
|
|
5
5
|
</template>
|
|
@@ -2,7 +2,13 @@ export { default as AdvancedOptions } from './AdvancedOptions.vue';
|
|
|
2
2
|
export { default as AlertModal } from './AlertModal.vue';
|
|
3
3
|
export { default as Button } from './Button.vue';
|
|
4
4
|
export { default as Checkbox } from './Checkbox.vue';
|
|
5
|
+
export { default as Combobox } from './Combobox.vue';
|
|
6
|
+
export { default as ComboboxLabel } from './ComboboxLabel.vue';
|
|
7
|
+
export { default as ComboboxOption } from './ComboboxOption.vue';
|
|
8
|
+
export { default as ComboboxOptions } from './ComboboxOptions.vue';
|
|
9
|
+
export { default as ComboboxTrigger } from './ComboboxTrigger.vue';
|
|
5
10
|
export { default as ConfirmModal } from './ConfirmModal.vue';
|
|
11
|
+
export { default as Details } from './Details.vue';
|
|
6
12
|
export { default as DropdownMenu } from './DropdownMenu.vue';
|
|
7
13
|
export { default as DropdownMenuOption } from './DropdownMenuOption.vue';
|
|
8
14
|
export { default as DropdownMenuOptions } from './DropdownMenuOptions.vue';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Provide } from './Provide.vue';
|
package/src/errors/Errors.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
2
2
|
import { watchEffect } from 'vue';
|
|
3
3
|
import type Eruda from 'eruda';
|
|
4
|
+
import type ErudaIndexedDB from 'eruda-indexeddb';
|
|
4
5
|
|
|
5
6
|
import App from '@aerogel/core/services/App';
|
|
6
7
|
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
@@ -16,6 +17,7 @@ export class ErrorsService extends Service {
|
|
|
16
17
|
public forceReporting: boolean = false;
|
|
17
18
|
private enabled: boolean = true;
|
|
18
19
|
private eruda: typeof Eruda | null = null;
|
|
20
|
+
private erudaPlugins: [typeof ErudaIndexedDB] | null = null;
|
|
19
21
|
|
|
20
22
|
public enable(): void {
|
|
21
23
|
this.enabled = true;
|
|
@@ -131,8 +133,10 @@ export class ErrorsService extends Service {
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
this.eruda ??= (await import('eruda')).default;
|
|
136
|
+
this.erudaPlugins ??= [(await import('eruda-indexeddb')).default];
|
|
134
137
|
|
|
135
138
|
this.eruda.init();
|
|
139
|
+
this.erudaPlugins.forEach((plugin) => this.eruda?.add(plugin));
|
|
136
140
|
});
|
|
137
141
|
}
|
|
138
142
|
|
package/src/errors/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { isTesting } from '@noeldemartin/utils';
|
|
1
2
|
import type { App as AppInstance } from 'vue';
|
|
2
3
|
|
|
3
4
|
import App from '@aerogel/core/services/App';
|
|
4
5
|
import { bootServices } from '@aerogel/core/services';
|
|
5
6
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
7
|
+
import { getErrorMessage } from '@aerogel/core/errors/utils';
|
|
6
8
|
|
|
7
9
|
import Errors from './Errors';
|
|
8
10
|
import settings from './settings';
|
|
@@ -16,6 +18,13 @@ export type { ErrorSource, ErrorReport, ErrorReportLog };
|
|
|
16
18
|
|
|
17
19
|
const services = { $errors: Errors };
|
|
18
20
|
const frameworkHandler: ErrorHandler = (error) => {
|
|
21
|
+
if (
|
|
22
|
+
isTesting('e2e') &&
|
|
23
|
+
getErrorMessage(error).includes('ResizeObserver loop completed with undelivered notifications.')
|
|
24
|
+
) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
Errors.report(error);
|
|
20
29
|
|
|
21
30
|
return true;
|
|
@@ -30,7 +30,7 @@ describe('FormController', () => {
|
|
|
30
30
|
const form = useForm({
|
|
31
31
|
name: {
|
|
32
32
|
type: 'string',
|
|
33
|
-
rules: 'required',
|
|
33
|
+
rules: ['required'],
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -48,7 +48,7 @@ describe('FormController', () => {
|
|
|
48
48
|
const form = useForm({
|
|
49
49
|
name: {
|
|
50
50
|
type: 'string',
|
|
51
|
-
rules: 'required',
|
|
51
|
+
rules: ['required'],
|
|
52
52
|
},
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -69,11 +69,11 @@ describe('FormController', () => {
|
|
|
69
69
|
const form = useForm({
|
|
70
70
|
trimmed: {
|
|
71
71
|
type: 'string',
|
|
72
|
-
rules: 'required',
|
|
72
|
+
rules: ['required'],
|
|
73
73
|
},
|
|
74
74
|
untrimmed: {
|
|
75
75
|
type: 'string',
|
|
76
|
-
rules: 'required',
|
|
76
|
+
rules: ['required'],
|
|
77
77
|
trim: false,
|
|
78
78
|
},
|
|
79
79
|
});
|
|
@@ -14,7 +14,7 @@ export interface FormFieldDefinition<
|
|
|
14
14
|
type: TType;
|
|
15
15
|
trim?: boolean;
|
|
16
16
|
default?: GetFormFieldValue<TType>;
|
|
17
|
-
rules?: TRules;
|
|
17
|
+
rules?: TRules[];
|
|
18
18
|
values?: readonly TValueType[];
|
|
19
19
|
[__valueType]?: TValueType;
|
|
20
20
|
}
|
|
@@ -109,7 +109,11 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
public getFieldRules<T extends keyof Fields>(field: T): string[] {
|
|
112
|
-
return this._fields[field]?.rules
|
|
112
|
+
return this._fields[field]?.rules ?? [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public setFieldErrors<T extends keyof Fields>(field: T, errors: string[] | null): void {
|
|
116
|
+
this._errors[field] = errors;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
public getFieldType<T extends keyof Fields>(field: T): FormFieldType | null {
|
|
@@ -197,7 +201,7 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
|
|
|
197
201
|
private getFieldErrors(name: keyof Fields, definition: FormFieldDefinition): string[] | null {
|
|
198
202
|
const errors = [];
|
|
199
203
|
const value = this._data[name];
|
|
200
|
-
const rules = definition.rules
|
|
204
|
+
const rules = definition.rules ?? [];
|
|
201
205
|
|
|
202
206
|
errors.push(...validateType(value, definition));
|
|
203
207
|
|
package/src/forms/index.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
import { registerFormValidationRule } from '@aerogel/core/forms/validation';
|
|
2
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
3
|
+
|
|
1
4
|
export * from './FormController';
|
|
2
5
|
export * from './utils';
|
|
3
6
|
export * from './validation';
|
|
4
7
|
export { default as FormController } from './FormController';
|
|
8
|
+
|
|
9
|
+
export default definePlugin({
|
|
10
|
+
async install(_, { formValidationRules }) {
|
|
11
|
+
for (const [rule, validator] of Object.entries(formValidationRules ?? {})) {
|
|
12
|
+
registerFormValidationRule(rule, validator);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
});
|