@aerogel/core 0.0.0-next.b18f4e0acd39431045c2f444c711303890143193 → 0.0.0-next.b3caf219a503ce9b8c65ef1463132c9507f56c0a
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 +1516 -1471
- package/dist/aerogel-core.js +2960 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +27 -37
- package/src/bootstrap/bootstrap.test.ts +4 -8
- package/src/bootstrap/index.ts +26 -16
- package/src/bootstrap/options.ts +1 -1
- package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -5
- package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +1 -1
- package/src/components/composition.ts +23 -0
- package/src/components/contracts/AlertModal.ts +4 -0
- package/src/components/contracts/Button.ts +15 -0
- package/src/components/contracts/ConfirmModal.ts +41 -0
- package/src/components/contracts/ErrorReportModal.ts +29 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +18 -0
- package/src/components/contracts/Modal.ts +9 -0
- package/src/components/contracts/PromptModal.ts +28 -0
- package/src/components/contracts/index.ts +7 -0
- package/src/components/contracts/shared.ts +9 -0
- package/src/components/forms/AGSelect.vue +11 -17
- package/src/components/forms/index.ts +0 -4
- package/src/components/headless/HeadlessButton.vue +45 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/HeadlessInputDescription.vue +27 -0
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/HeadlessInputInput.vue +75 -0
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +16 -18
- package/src/components/headless/HeadlessModalContent.vue +24 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
- package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
- package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
- package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
- package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
- package/src/components/headless/forms/composition.ts +10 -0
- package/src/components/headless/forms/index.ts +3 -9
- package/src/components/headless/index.ts +12 -1
- package/src/components/headless/snackbars/index.ts +3 -3
- package/src/components/index.ts +6 -4
- package/src/components/lib/AGErrorMessage.vue +4 -4
- package/src/components/lib/AGMarkdown.vue +24 -6
- package/src/components/lib/AGMeasured.vue +3 -2
- package/src/components/lib/AGStartupCrash.vue +6 -6
- package/src/components/lib/index.ts +0 -1
- package/src/components/snackbars/AGSnackbar.vue +8 -6
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +58 -0
- package/src/components/ui/Checkbox.vue +49 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/ErrorReportModal.vue +62 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/ui/Form.vue +24 -0
- package/src/components/ui/Input.vue +52 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +32 -0
- package/src/components/ui/Modal.vue +55 -0
- package/src/components/ui/ModalContext.vue +30 -0
- package/src/components/ui/ProgressBar.vue +50 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/index.ts +15 -0
- package/src/components/utils.ts +106 -9
- package/src/directives/index.ts +11 -5
- package/src/directives/measure.ts +34 -6
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +25 -28
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +10 -16
- package/src/errors/utils.ts +35 -0
- package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
- package/src/forms/{Form.ts → FormController.ts} +85 -25
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +36 -5
- package/src/forms/validation.ts +19 -0
- package/src/index.css +8 -0
- package/src/{main.ts → index.ts} +3 -0
- package/src/jobs/Job.ts +147 -0
- package/src/jobs/index.ts +10 -0
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +46 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +44 -29
- package/src/lang/index.ts +8 -6
- package/src/plugins/Plugin.ts +1 -1
- package/src/plugins/index.ts +10 -7
- package/src/services/App.state.ts +27 -4
- package/src/services/App.ts +12 -4
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +112 -32
- package/src/services/Service.ts +150 -55
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +14 -5
- package/src/services/store.ts +8 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +17 -5
- package/src/ui/UI.ts +176 -60
- package/src/ui/index.ts +17 -16
- package/src/ui/utils.ts +16 -0
- package/src/utils/composition/events.ts +2 -2
- package/src/utils/composition/forms.ts +4 -3
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/vue.ts +22 -15
- package/dist/aerogel-core.cjs.js +0 -2
- package/dist/aerogel-core.cjs.js.map +0 -1
- package/dist/aerogel-core.esm.js +0 -2
- package/dist/aerogel-core.esm.js.map +0 -1
- package/histoire.config.ts +0 -7
- package/noeldemartin.config.js +0 -5
- package/postcss.config.js +0 -6
- package/src/assets/histoire.css +0 -3
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -41
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -38
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -4
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/modals/AGAlertModal.ts +0 -15
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -27
- package/src/components/modals/AGConfirmModal.vue +0 -26
- package/src/components/modals/AGErrorReportModal.ts +0 -46
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -23
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -39
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/AGPromptModal.ts +0 -30
- package/src/components/modals/AGPromptModal.vue +0 -34
- package/src/components/modals/index.ts +0 -17
- package/src/directives/initial-focus.ts +0 -11
- package/src/main.histoire.ts +0 -1
- package/tailwind.config.js +0 -4
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -14
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<HeadlessModal v-slot="{ close }" :persistent="persistent" v-bind="props">
|
|
3
|
+
<HeadlessModalOverlay class="fixed inset-0 bg-gray-500/75" />
|
|
4
|
+
|
|
5
|
+
<HeadlessModalContent :class="renderedWrapperClass">
|
|
6
|
+
<div v-if="!persistent" class="absolute top-0 right-0 hidden pt-1.5 pr-1.5 sm:block">
|
|
7
|
+
<Button variant="ghost" size="icon" @click="close()">
|
|
8
|
+
<span class="sr-only">{{ $td('ui.close', 'Close') }}</span>
|
|
9
|
+
<IconClose class="size-3 text-gray-400" />
|
|
10
|
+
</Button>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<HeadlessModalTitle v-if="title" class="text-base font-semibold text-gray-900">
|
|
14
|
+
<AGMarkdown :text="title" inline />
|
|
15
|
+
</HeadlessModalTitle>
|
|
16
|
+
|
|
17
|
+
<div :class="renderedContentClass">
|
|
18
|
+
<slot :close="close" />
|
|
19
|
+
</div>
|
|
20
|
+
</HeadlessModalContent>
|
|
21
|
+
</HeadlessModal>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import IconClose from '~icons/zondicons/close';
|
|
26
|
+
|
|
27
|
+
import { computed } from 'vue';
|
|
28
|
+
import type { HTMLAttributes } from 'vue';
|
|
29
|
+
|
|
30
|
+
import AGMarkdown from '@aerogel/core/components/lib/AGMarkdown.vue';
|
|
31
|
+
import HeadlessModal from '@aerogel/core/components/headless/HeadlessModal.vue';
|
|
32
|
+
import HeadlessModalContent from '@aerogel/core/components/headless/HeadlessModalContent.vue';
|
|
33
|
+
import HeadlessModalOverlay from '@aerogel/core/components/headless/HeadlessModalOverlay.vue';
|
|
34
|
+
import HeadlessModalTitle from '@aerogel/core/components/headless/HeadlessModalTitle.vue';
|
|
35
|
+
import { classes } from '@aerogel/core/components/utils';
|
|
36
|
+
import type { ModalProps, ModalSlots } from '@aerogel/core/components/contracts/Modal';
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
class: contentClass = '',
|
|
40
|
+
wrapperClass = '',
|
|
41
|
+
title,
|
|
42
|
+
persistent,
|
|
43
|
+
...props
|
|
44
|
+
} = defineProps<ModalProps & { wrapperClass?: HTMLAttributes['class']; class?: HTMLAttributes['class'] }>();
|
|
45
|
+
|
|
46
|
+
defineSlots<ModalSlots>();
|
|
47
|
+
|
|
48
|
+
const renderedContentClass = computed(() => classes({ 'mt-2': title }, contentClass));
|
|
49
|
+
const renderedWrapperClass = computed(() =>
|
|
50
|
+
classes(
|
|
51
|
+
// eslint-disable-next-line vue/max-len
|
|
52
|
+
'fixed top-1/2 left-1/2 z-50 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl sm:max-w-lg',
|
|
53
|
+
wrapperClass,
|
|
54
|
+
));
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="modal.component" v-bind="modalProperties" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { computed, provide, toRef, unref } from 'vue';
|
|
7
|
+
|
|
8
|
+
import type { UIModal, UIModalContext } from '@aerogel/core/ui/UI.state';
|
|
9
|
+
import type { AcceptRefs } from '@aerogel/core/utils/vue';
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
modal: UIModal;
|
|
13
|
+
childIndex?: number;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const modalProperties = computed(() => {
|
|
17
|
+
const properties = {} as typeof props.modal.properties;
|
|
18
|
+
|
|
19
|
+
for (const property in props.modal.properties) {
|
|
20
|
+
properties[property] = unref(props.modal.properties[property]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return properties;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
provide<AcceptRefs<UIModalContext>>('modal', {
|
|
27
|
+
modal: toRef(props, 'modal'),
|
|
28
|
+
childIndex: toRef(props, 'childIndex'),
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
|
3
|
+
<div :class="filledClasses" :style="`transform:translateX(-${(1 - renderedProgress) * 100}%)`" />
|
|
4
|
+
<span class="sr-only">
|
|
5
|
+
{{
|
|
6
|
+
$td('ui.progress', '{progress}% complete', {
|
|
7
|
+
progress: renderedProgress * 100,
|
|
8
|
+
})
|
|
9
|
+
}}
|
|
10
|
+
</span>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { computed, onUnmounted, ref, watch } from 'vue';
|
|
16
|
+
|
|
17
|
+
import { classes } from '@aerogel/core/components/utils';
|
|
18
|
+
import type Job from '@aerogel/core/jobs/Job';
|
|
19
|
+
|
|
20
|
+
const { filledClass, progress, job } = defineProps<{
|
|
21
|
+
filledClass?: string;
|
|
22
|
+
progress?: number;
|
|
23
|
+
job?: Job;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
let cleanup: Function | undefined;
|
|
27
|
+
const jobProgress = ref(0);
|
|
28
|
+
const filledClasses = computed(() =>
|
|
29
|
+
classes('size-full transition-transform duration-500 rounded-r-full ease-linear bg-primary', filledClass));
|
|
30
|
+
const renderedProgress = computed(() => {
|
|
31
|
+
if (typeof progress === 'number') {
|
|
32
|
+
return progress;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return jobProgress.value;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
watch(
|
|
39
|
+
() => job,
|
|
40
|
+
() => {
|
|
41
|
+
cleanup?.();
|
|
42
|
+
|
|
43
|
+
jobProgress.value = job?.progress ?? 0;
|
|
44
|
+
cleanup = job?.listeners.add({ onUpdated: (value) => (jobProgress.value = value) });
|
|
45
|
+
},
|
|
46
|
+
{ immediate: true },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
onUnmounted(() => cleanup?.());
|
|
50
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Modal v-slot="{ close }" :title="title" persistent>
|
|
3
|
+
<Form :form="form" @submit="close(form.draft)">
|
|
4
|
+
<AGMarkdown :text="message" />
|
|
5
|
+
<Input
|
|
6
|
+
name="draft"
|
|
7
|
+
class="mt-2"
|
|
8
|
+
:placeholder="placeholder"
|
|
9
|
+
:label="label"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<div class="mt-4 flex flex-row-reverse gap-2">
|
|
13
|
+
<Button :variant="acceptVariant" submit>
|
|
14
|
+
{{ renderedAcceptText }}
|
|
15
|
+
</Button>
|
|
16
|
+
<Button :variant="cancelVariant" @click="close(false)">
|
|
17
|
+
{{ renderedCancelText }}
|
|
18
|
+
</Button>
|
|
19
|
+
</div>
|
|
20
|
+
</Form>
|
|
21
|
+
</Modal>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import AGMarkdown from '@aerogel/core/components/lib/AGMarkdown.vue';
|
|
26
|
+
import Button from '@aerogel/core/components/ui/Button.vue';
|
|
27
|
+
import Form from '@aerogel/core/components/ui/Form.vue';
|
|
28
|
+
import Input from '@aerogel/core/components/ui/Input.vue';
|
|
29
|
+
import Modal from '@aerogel/core/components/ui/Modal.vue';
|
|
30
|
+
import { usePromptModal } from '@aerogel/core/components/contracts/PromptModal';
|
|
31
|
+
import type { PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
|
|
32
|
+
|
|
33
|
+
const { cancelVariant = 'secondary', ...props } = defineProps<PromptModalProps>();
|
|
34
|
+
const { form, renderedAcceptText, renderedCancelText } = usePromptModal(props);
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default as AlertModal } from './AlertModal.vue';
|
|
2
|
+
export { default as Button } from './Button.vue';
|
|
3
|
+
export { default as Checkbox } from './Checkbox.vue';
|
|
4
|
+
export { default as ConfirmModal } from './ConfirmModal.vue';
|
|
5
|
+
export { default as ErrorReportModal } from './ErrorReportModal.vue';
|
|
6
|
+
export { default as ErrorReportModalButtons } from './ErrorReportModalButtons.vue';
|
|
7
|
+
export { default as ErrorReportModalTitle } from './ErrorReportModalTitle.vue';
|
|
8
|
+
export { default as Form } from './Form.vue';
|
|
9
|
+
export { default as Input } from './Input.vue';
|
|
10
|
+
export { default as Link } from './Link.vue';
|
|
11
|
+
export { default as LoadingModal } from './LoadingModal.vue';
|
|
12
|
+
export { default as Modal } from './Modal.vue';
|
|
13
|
+
export { default as ModalContext } from './ModalContext.vue';
|
|
14
|
+
export { default as ProgressBar } from './ProgressBar.vue';
|
|
15
|
+
export { default as PromptModal } from './PromptModal.vue';
|
package/src/components/utils.ts
CHANGED
|
@@ -1,10 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { computed, customRef, inject, onUnmounted, unref } from 'vue';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { isObject } from '@noeldemartin/utils';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import type { ClassValue } from 'clsx';
|
|
7
|
+
import type { ComputedRef, ExtractPropTypes, PropType, Ref, UnwrapNestedRefs } from 'vue';
|
|
8
|
+
import type { GetClosureArgs, GetClosureResult, Nullable } from '@noeldemartin/utils';
|
|
9
|
+
|
|
10
|
+
import type { HasElement } from '@aerogel/core/components/contracts/shared';
|
|
11
|
+
import type { FormController } from '@aerogel/core/forms';
|
|
12
|
+
|
|
13
|
+
export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
|
|
14
|
+
export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
|
|
15
|
+
export type RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
|
|
16
|
+
export type Variants<T extends Record<string, string>> = Required<{
|
|
17
|
+
[K in keyof T]: {
|
|
18
|
+
[key in T[K]]: string;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
export type ComponentPropDefinitions<T> = {
|
|
23
|
+
[K in keyof T]: {
|
|
24
|
+
type?: PropType<T[K]>;
|
|
25
|
+
default: T[K] | (() => T[K]) | null;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PickComponentProps<TValues, TDefinitions> = {
|
|
30
|
+
[K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function computedVariantClasses<T>(
|
|
34
|
+
value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
|
|
35
|
+
config: { baseClasses?: string } & CVAConfig<T>,
|
|
36
|
+
): ComputedRef<string> {
|
|
37
|
+
return computed(() => {
|
|
38
|
+
const { baseClasses: valueBaseClasses, ...valueRefs } = value;
|
|
39
|
+
const { baseClasses: configBaseClasses, ...configs } = config;
|
|
40
|
+
const variants = cva(configBaseClasses, configs as CVAConfig<T>);
|
|
41
|
+
const values = Object.entries(valueRefs).reduce((extractedValues, [name, valueRef]) => {
|
|
42
|
+
extractedValues[name as keyof CVAProps<T>] = unref(valueRef);
|
|
43
|
+
|
|
44
|
+
return extractedValues;
|
|
45
|
+
}, {} as CVAProps<T>);
|
|
46
|
+
|
|
47
|
+
return classes(variants(values), unref(valueBaseClasses));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function classes(...inputs: ClassValue[]): string {
|
|
52
|
+
return twMerge(clsx(inputs));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function elementRef(): Ref<HTMLElement | undefined> {
|
|
56
|
+
return customRef((track, trigger) => {
|
|
57
|
+
let value: HTMLElement | undefined = undefined;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
get() {
|
|
61
|
+
track();
|
|
62
|
+
|
|
63
|
+
return value;
|
|
64
|
+
},
|
|
65
|
+
set(newValue) {
|
|
66
|
+
value = getElement(newValue);
|
|
67
|
+
|
|
68
|
+
trigger();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function extractComponentProps<TDefinitions extends {}, TValues extends ExtractPropTypes<TDefinitions>>(
|
|
75
|
+
values: TValues,
|
|
76
|
+
definitions: TDefinitions,
|
|
77
|
+
): PickComponentProps<TValues, TDefinitions> {
|
|
78
|
+
return Object.keys(definitions).reduce(
|
|
79
|
+
(extracted, prop) => {
|
|
80
|
+
extracted[prop] = values[prop as keyof TValues];
|
|
81
|
+
|
|
82
|
+
return extracted;
|
|
83
|
+
},
|
|
84
|
+
{} as Record<string, unknown>,
|
|
85
|
+
) as PickComponentProps<TValues, TDefinitions>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getElement(value: unknown): HTMLElement | undefined {
|
|
89
|
+
if (value instanceof HTMLElement) {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasElement(value)) {
|
|
94
|
+
return value.$el;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function hasElement(value: unknown): value is UnwrapNestedRefs<HasElement> {
|
|
99
|
+
return isObject(value) && '$el' in value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
|
|
103
|
+
const form = inject<FormController | null>('form', null);
|
|
104
|
+
const stop = form?.on('focus', (name) => input.name === name && listener());
|
|
105
|
+
|
|
106
|
+
onUnmounted(() => stop?.());
|
|
10
107
|
}
|
package/src/directives/index.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { Directive } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { definePlugin } from '
|
|
3
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
4
|
|
|
5
|
-
import initialFocus from './initial-focus';
|
|
6
5
|
import measure from './measure';
|
|
7
6
|
|
|
8
7
|
const builtInDirectives: Record<string, Directive> = {
|
|
9
|
-
|
|
10
|
-
'measure': measure,
|
|
8
|
+
measure: measure,
|
|
11
9
|
};
|
|
12
10
|
|
|
11
|
+
export * from './measure';
|
|
12
|
+
|
|
13
13
|
export default definePlugin({
|
|
14
14
|
install(app, options) {
|
|
15
15
|
const directives = {
|
|
@@ -23,8 +23,14 @@ export default definePlugin({
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
declare module '
|
|
26
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
27
27
|
export interface AerogelOptions {
|
|
28
28
|
directives?: Record<string, Directive>;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
declare module 'vue' {
|
|
33
|
+
interface ComponentCustomDirectives {
|
|
34
|
+
measure: Directive<string, string>;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,12 +1,40 @@
|
|
|
1
|
-
import { defineDirective } from '
|
|
1
|
+
import { defineDirective } from '@aerogel/core/utils/vue';
|
|
2
|
+
import { tap } from '@noeldemartin/utils';
|
|
3
|
+
|
|
4
|
+
const resizeObservers: WeakMap<HTMLElement, ResizeObserver> = new WeakMap();
|
|
5
|
+
|
|
6
|
+
export interface ElementSize {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type MeasureDirectiveListener = (size: ElementSize) => unknown;
|
|
2
12
|
|
|
3
13
|
export default defineDirective({
|
|
4
|
-
mounted(element: HTMLElement, { value }
|
|
5
|
-
|
|
14
|
+
mounted(element: HTMLElement, { value }) {
|
|
15
|
+
// TODO replace with argument when typed properly
|
|
16
|
+
const modifiers = { css: true, watch: true };
|
|
17
|
+
|
|
18
|
+
const listener = typeof value === 'function' ? (value as MeasureDirectiveListener) : null;
|
|
19
|
+
const update = () => {
|
|
20
|
+
const sizes = element.getBoundingClientRect();
|
|
6
21
|
|
|
7
|
-
|
|
8
|
-
|
|
22
|
+
if (modifiers.css) {
|
|
23
|
+
element.style.setProperty('--width', `${sizes.width}px`);
|
|
24
|
+
element.style.setProperty('--height', `${sizes.height}px`);
|
|
25
|
+
}
|
|
9
26
|
|
|
10
|
-
|
|
27
|
+
listener?.({ width: sizes.width, height: sizes.height });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (modifiers.watch) {
|
|
31
|
+
resizeObservers.set(element, tap(new ResizeObserver(update)).observe(element));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
update();
|
|
35
|
+
},
|
|
36
|
+
unmounted(element) {
|
|
37
|
+
resizeObservers.get(element)?.unobserve(element);
|
|
38
|
+
resizeObservers.delete(element);
|
|
11
39
|
},
|
|
12
40
|
});
|
package/src/errors/Errors.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
1
|
+
import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
|
-
import App from '
|
|
4
|
-
import ServiceBootError from '
|
|
5
|
-
import UI, { UIComponents } from '
|
|
6
|
-
import { translateWithDefault } from '
|
|
3
|
+
import App from '@aerogel/core/services/App';
|
|
4
|
+
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
5
|
+
import UI, { UIComponents } from '@aerogel/core/ui/UI';
|
|
6
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
7
|
+
import { Colors } from '@aerogel/core/components/constants';
|
|
8
|
+
import { Events } from '@aerogel/core/services';
|
|
9
|
+
import type { ErrorReportModalProps } from '@aerogel/core/components/contracts/ErrorReportModal';
|
|
10
|
+
import type { ModalComponent } from '@aerogel/core/ui/UI.state';
|
|
7
11
|
|
|
8
12
|
import Service from './Errors.state';
|
|
9
|
-
import { Colors } from '@/components/constants';
|
|
10
|
-
import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
|
|
11
13
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
12
|
-
import type { ModalComponent } from '@/ui/UI.state';
|
|
13
14
|
|
|
14
15
|
export class ErrorsService extends Service {
|
|
15
16
|
|
|
@@ -33,13 +34,19 @@ export class ErrorsService extends Service {
|
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
UI.openModal<ModalComponent<
|
|
37
|
+
UI.openModal<ModalComponent<ErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
37
38
|
reports,
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
public async report(error: ErrorSource, message?: string): Promise<void> {
|
|
42
|
-
|
|
43
|
+
await Events.emit('error', { error, message });
|
|
44
|
+
|
|
45
|
+
if (isTesting('unit')) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isDevelopment()) {
|
|
43
50
|
this.logError(error);
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -74,7 +81,7 @@ export class ErrorsService extends Service {
|
|
|
74
81
|
text: translateWithDefault('errors.viewDetails', 'View details'),
|
|
75
82
|
dismiss: true,
|
|
76
83
|
handler: () =>
|
|
77
|
-
UI.openModal<ModalComponent<
|
|
84
|
+
UI.openModal<ModalComponent<ErrorReportModalProps>>(
|
|
78
85
|
UI.requireComponent(UIComponents.ErrorReportModal),
|
|
79
86
|
{ reports: [report] },
|
|
80
87
|
),
|
|
@@ -110,22 +117,6 @@ export class ErrorsService extends Service {
|
|
|
110
117
|
});
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
public getErrorMessage(error: ErrorSource): string {
|
|
114
|
-
if (typeof error === 'string') {
|
|
115
|
-
return error;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (error instanceof Error || error instanceof JSError) {
|
|
119
|
-
return error.message;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (isObject(error)) {
|
|
123
|
-
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
120
|
private logError(error: unknown): void {
|
|
130
121
|
// eslint-disable-next-line no-console
|
|
131
122
|
console.error(error);
|
|
@@ -185,4 +176,10 @@ export class ErrorsService extends Service {
|
|
|
185
176
|
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
export default facade(
|
|
179
|
+
export default facade(ErrorsService);
|
|
180
|
+
|
|
181
|
+
declare module '@aerogel/core/services/Events' {
|
|
182
|
+
export interface EventsPayload {
|
|
183
|
+
error: { error: ErrorSource; message?: string };
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import type { App } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { bootServices } from '
|
|
4
|
-
import { definePlugin } from '
|
|
3
|
+
import { bootServices } from '@aerogel/core/services';
|
|
4
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
5
5
|
|
|
6
6
|
import Errors from './Errors';
|
|
7
|
-
import { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
7
|
+
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export * from './utils';
|
|
10
|
+
export { Errors };
|
|
11
|
+
export { default as JobCancelledError } from './JobCancelledError';
|
|
12
|
+
export { default as ServiceBootError } from './ServiceBootError';
|
|
13
|
+
export type { ErrorSource, ErrorReport, ErrorReportLog };
|
|
10
14
|
|
|
11
15
|
const services = { $errors: Errors };
|
|
12
16
|
const frameworkHandler: ErrorHandler = (error) => {
|
|
13
|
-
if (!Errors.instance) {
|
|
14
|
-
// eslint-disable-next-line no-console
|
|
15
|
-
console.warn('Errors service hasn\'t been initialized properly!');
|
|
16
|
-
|
|
17
|
-
// eslint-disable-next-line no-console
|
|
18
|
-
console.error(error);
|
|
19
|
-
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
17
|
Errors.report(error);
|
|
24
18
|
|
|
25
19
|
return true;
|
|
@@ -44,12 +38,12 @@ export default definePlugin({
|
|
|
44
38
|
},
|
|
45
39
|
});
|
|
46
40
|
|
|
47
|
-
declare module '
|
|
41
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
48
42
|
export interface AerogelOptions {
|
|
49
43
|
handleError?(error: ErrorSource): boolean;
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
declare module '
|
|
47
|
+
declare module '@aerogel/core/services' {
|
|
54
48
|
export interface Services extends ErrorsServices {}
|
|
55
49
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { JSError, isObject, toString } from '@noeldemartin/utils';
|
|
2
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
3
|
+
import type { ErrorSource } from './Errors.state';
|
|
4
|
+
|
|
5
|
+
const handlers: ErrorHandler[] = [];
|
|
6
|
+
|
|
7
|
+
export type ErrorHandler = (error: ErrorSource) => string | undefined;
|
|
8
|
+
|
|
9
|
+
export function registerErrorHandler(handler: ErrorHandler): void {
|
|
10
|
+
handlers.push(handler);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getErrorMessage(error: ErrorSource): string {
|
|
14
|
+
for (const handler of handlers) {
|
|
15
|
+
const result = handler(error);
|
|
16
|
+
|
|
17
|
+
if (result) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof error === 'string') {
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (error instanceof Error || error instanceof JSError) {
|
|
27
|
+
return error.message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isObject(error)) {
|
|
31
|
+
return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return translateWithDefault('errors.unknown', 'Unknown Error');
|
|
35
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { useForm } from '
|
|
4
|
-
import {
|
|
5
|
-
import { numberInput, requiredStringInput } from '@/forms/utils';
|
|
3
|
+
import { useForm } from '@aerogel/core/forms/composition';
|
|
4
|
+
import { numberInput, requiredStringInput } from '@aerogel/core/forms/utils';
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
import { FormFieldTypes } from './FormController';
|
|
7
|
+
|
|
8
|
+
describe('FormController', () => {
|
|
8
9
|
|
|
9
10
|
it('defines magic fields', () => {
|
|
10
11
|
const form = useForm({
|
|
@@ -55,4 +56,32 @@ describe('Form', () => {
|
|
|
55
56
|
expect(form.name).toBeNull();
|
|
56
57
|
});
|
|
57
58
|
|
|
59
|
+
it('trims values', () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const form = useForm({
|
|
62
|
+
trimmed: {
|
|
63
|
+
type: FormFieldTypes.String,
|
|
64
|
+
rules: 'required',
|
|
65
|
+
},
|
|
66
|
+
untrimmed: {
|
|
67
|
+
type: FormFieldTypes.String,
|
|
68
|
+
rules: 'required',
|
|
69
|
+
trim: false,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
form.trimmed = ' ';
|
|
75
|
+
form.untrimmed = ' ';
|
|
76
|
+
|
|
77
|
+
form.submit();
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
expect(form.valid).toBe(false);
|
|
81
|
+
expect(form.submitted).toBe(true);
|
|
82
|
+
expect(form.trimmed).toEqual('');
|
|
83
|
+
expect(form.untrimmed).toEqual(' ');
|
|
84
|
+
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
85
|
+
});
|
|
86
|
+
|
|
58
87
|
});
|