@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.
Files changed (166) hide show
  1. package/dist/aerogel-core.d.ts +1516 -1471
  2. package/dist/aerogel-core.js +2960 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +27 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +26 -16
  7. package/src/bootstrap/options.ts +1 -1
  8. package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -5
  11. package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +1 -1
  12. package/src/components/composition.ts +23 -0
  13. package/src/components/contracts/AlertModal.ts +4 -0
  14. package/src/components/contracts/Button.ts +15 -0
  15. package/src/components/contracts/ConfirmModal.ts +41 -0
  16. package/src/components/contracts/ErrorReportModal.ts +29 -0
  17. package/src/components/contracts/Input.ts +26 -0
  18. package/src/components/contracts/LoadingModal.ts +18 -0
  19. package/src/components/contracts/Modal.ts +9 -0
  20. package/src/components/contracts/PromptModal.ts +28 -0
  21. package/src/components/contracts/index.ts +7 -0
  22. package/src/components/contracts/shared.ts +9 -0
  23. package/src/components/forms/AGSelect.vue +11 -17
  24. package/src/components/forms/index.ts +0 -4
  25. package/src/components/headless/HeadlessButton.vue +45 -0
  26. package/src/components/headless/HeadlessInput.vue +59 -0
  27. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  28. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  29. package/src/components/headless/HeadlessInputInput.vue +75 -0
  30. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  31. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  32. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +16 -18
  33. package/src/components/headless/HeadlessModalContent.vue +24 -0
  34. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  35. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  36. package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
  37. package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
  38. package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
  39. package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
  40. package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
  41. package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
  42. package/src/components/headless/forms/composition.ts +10 -0
  43. package/src/components/headless/forms/index.ts +3 -9
  44. package/src/components/headless/index.ts +12 -1
  45. package/src/components/headless/snackbars/index.ts +3 -3
  46. package/src/components/index.ts +6 -4
  47. package/src/components/lib/AGErrorMessage.vue +4 -4
  48. package/src/components/lib/AGMarkdown.vue +24 -6
  49. package/src/components/lib/AGMeasured.vue +3 -2
  50. package/src/components/lib/AGStartupCrash.vue +6 -6
  51. package/src/components/lib/index.ts +0 -1
  52. package/src/components/snackbars/AGSnackbar.vue +8 -6
  53. package/src/components/ui/AlertModal.vue +13 -0
  54. package/src/components/ui/Button.vue +58 -0
  55. package/src/components/ui/Checkbox.vue +49 -0
  56. package/src/components/ui/ConfirmModal.vue +42 -0
  57. package/src/components/ui/ErrorReportModal.vue +62 -0
  58. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
  59. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  60. package/src/components/ui/Form.vue +24 -0
  61. package/src/components/ui/Input.vue +52 -0
  62. package/src/components/ui/Link.vue +12 -0
  63. package/src/components/ui/LoadingModal.vue +32 -0
  64. package/src/components/ui/Modal.vue +55 -0
  65. package/src/components/ui/ModalContext.vue +30 -0
  66. package/src/components/ui/ProgressBar.vue +50 -0
  67. package/src/components/ui/PromptModal.vue +35 -0
  68. package/src/components/ui/index.ts +15 -0
  69. package/src/components/utils.ts +106 -9
  70. package/src/directives/index.ts +11 -5
  71. package/src/directives/measure.ts +34 -6
  72. package/src/errors/Errors.state.ts +1 -1
  73. package/src/errors/Errors.ts +25 -28
  74. package/src/errors/JobCancelledError.ts +3 -0
  75. package/src/errors/index.ts +10 -16
  76. package/src/errors/utils.ts +35 -0
  77. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  78. package/src/forms/{Form.ts → FormController.ts} +85 -25
  79. package/src/forms/composition.ts +4 -4
  80. package/src/forms/index.ts +3 -1
  81. package/src/forms/utils.ts +36 -5
  82. package/src/forms/validation.ts +19 -0
  83. package/src/index.css +8 -0
  84. package/src/{main.ts → index.ts} +3 -0
  85. package/src/jobs/Job.ts +147 -0
  86. package/src/jobs/index.ts +10 -0
  87. package/src/jobs/listeners.ts +3 -0
  88. package/src/jobs/status.ts +4 -0
  89. package/src/lang/DefaultLangProvider.ts +46 -0
  90. package/src/lang/Lang.state.ts +11 -0
  91. package/src/lang/Lang.ts +44 -29
  92. package/src/lang/index.ts +8 -6
  93. package/src/plugins/Plugin.ts +1 -1
  94. package/src/plugins/index.ts +10 -7
  95. package/src/services/App.state.ts +27 -4
  96. package/src/services/App.ts +12 -4
  97. package/src/services/Cache.ts +43 -0
  98. package/src/services/Events.test.ts +39 -0
  99. package/src/services/Events.ts +112 -32
  100. package/src/services/Service.ts +150 -55
  101. package/src/services/Storage.ts +20 -0
  102. package/src/services/index.ts +14 -5
  103. package/src/services/store.ts +8 -5
  104. package/src/services/utils.ts +18 -0
  105. package/src/testing/index.ts +26 -0
  106. package/src/testing/setup.ts +11 -0
  107. package/src/ui/UI.state.ts +17 -5
  108. package/src/ui/UI.ts +176 -60
  109. package/src/ui/index.ts +17 -16
  110. package/src/ui/utils.ts +16 -0
  111. package/src/utils/composition/events.ts +2 -2
  112. package/src/utils/composition/forms.ts +4 -3
  113. package/src/utils/composition/persistent.test.ts +33 -0
  114. package/src/utils/composition/persistent.ts +11 -0
  115. package/src/utils/composition/state.test.ts +47 -0
  116. package/src/utils/composition/state.ts +24 -0
  117. package/src/utils/index.ts +2 -0
  118. package/src/utils/markdown.test.ts +50 -0
  119. package/src/utils/markdown.ts +19 -6
  120. package/src/utils/vue.ts +22 -15
  121. package/dist/aerogel-core.cjs.js +0 -2
  122. package/dist/aerogel-core.cjs.js.map +0 -1
  123. package/dist/aerogel-core.esm.js +0 -2
  124. package/dist/aerogel-core.esm.js.map +0 -1
  125. package/histoire.config.ts +0 -7
  126. package/noeldemartin.config.js +0 -5
  127. package/postcss.config.js +0 -6
  128. package/src/assets/histoire.css +0 -3
  129. package/src/components/forms/AGButton.vue +0 -44
  130. package/src/components/forms/AGCheckbox.vue +0 -41
  131. package/src/components/forms/AGForm.vue +0 -26
  132. package/src/components/forms/AGInput.vue +0 -38
  133. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  134. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  135. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  136. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  137. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  138. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  139. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  140. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  141. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  142. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  143. package/src/components/headless/modals/index.ts +0 -4
  144. package/src/components/lib/AGLink.vue +0 -9
  145. package/src/components/modals/AGAlertModal.ts +0 -15
  146. package/src/components/modals/AGAlertModal.vue +0 -14
  147. package/src/components/modals/AGConfirmModal.ts +0 -27
  148. package/src/components/modals/AGConfirmModal.vue +0 -26
  149. package/src/components/modals/AGErrorReportModal.ts +0 -46
  150. package/src/components/modals/AGErrorReportModal.vue +0 -54
  151. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  152. package/src/components/modals/AGLoadingModal.ts +0 -23
  153. package/src/components/modals/AGLoadingModal.vue +0 -15
  154. package/src/components/modals/AGModal.ts +0 -10
  155. package/src/components/modals/AGModal.vue +0 -39
  156. package/src/components/modals/AGModalContext.ts +0 -8
  157. package/src/components/modals/AGModalContext.vue +0 -22
  158. package/src/components/modals/AGModalTitle.vue +0 -9
  159. package/src/components/modals/AGPromptModal.ts +0 -30
  160. package/src/components/modals/AGPromptModal.vue +0 -34
  161. package/src/components/modals/index.ts +0 -17
  162. package/src/directives/initial-focus.ts +0 -11
  163. package/src/main.histoire.ts +0 -1
  164. package/tailwind.config.js +0 -4
  165. package/tsconfig.json +0 -11
  166. 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';
@@ -1,10 +1,107 @@
1
- export function extractComponentProps<T extends Record<string, unknown>>(
2
- values: Record<string, unknown>,
3
- definitions: Record<string, unknown>,
4
- ): T {
5
- return Object.keys(definitions).reduce((extracted, prop) => {
6
- extracted[prop] = values[prop];
7
-
8
- return extracted;
9
- }, {} as Record<string, unknown>) as T;
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
  }
@@ -1,15 +1,15 @@
1
1
  import type { Directive } from 'vue';
2
2
 
3
- import { definePlugin } from '@/plugins';
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
- 'initial-focus': initialFocus,
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 '@/bootstrap/options' {
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 '@/utils/vue';
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 }: { value?: () => unknown }) {
5
- const sizes = element.getBoundingClientRect();
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
- element.style.setProperty('--width', `${sizes.width}px`);
8
- element.style.setProperty('--height', `${sizes.height}px`);
22
+ if (modifiers.css) {
23
+ element.style.setProperty('--width', `${sizes.width}px`);
24
+ element.style.setProperty('--height', `${sizes.height}px`);
25
+ }
9
26
 
10
- value?.();
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
  });
@@ -1,6 +1,6 @@
1
1
  import type { JSError } from '@noeldemartin/utils';
2
2
 
3
- import { defineServiceState } from '@/services';
3
+ import { defineServiceState } from '@aerogel/core/services';
4
4
 
5
5
  export type ErrorSource = string | Error | JSError | unknown;
6
6
 
@@ -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 '@/services/App';
4
- import ServiceBootError from '@/errors/ServiceBootError';
5
- import UI, { UIComponents } from '@/ui/UI';
6
- import { translateWithDefault } from '@/lang/utils';
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<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
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
- if (App.development || App.testing) {
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<AGErrorReportModalProps>>(
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(new ErrorsService());
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
+ }
@@ -0,0 +1,3 @@
1
+ import { JSError } from '@noeldemartin/utils';
2
+
3
+ export default class JobCancelledError extends JSError {}
@@ -1,25 +1,19 @@
1
1
  import type { App } from 'vue';
2
2
 
3
- import { bootServices } from '@/services';
4
- import { definePlugin } from '@/plugins';
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 { Errors, ErrorSource, ErrorReport, ErrorReportLog };
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 '@/bootstrap/options' {
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 '@/services' {
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 './composition';
4
- import { FormFieldTypes } from '@/main';
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
- describe('Form', () => {
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
  });