@aerogel/core 0.1.1-next.b25730fd2850ebabef064973972dabd342d92769 → 0.1.1-next.f40b19c2bd8a3c0f252f1811ed740ce17bb6f699

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 (36) hide show
  1. package/dist/aerogel-core.d.ts +222 -261
  2. package/dist/aerogel-core.js +1371 -1359
  3. package/dist/aerogel-core.js.map +1 -1
  4. package/package.json +3 -2
  5. package/src/components/AppOverlays.vue +3 -2
  6. package/src/components/contracts/AlertModal.ts +1 -1
  7. package/src/components/contracts/Button.ts +1 -1
  8. package/src/components/contracts/ConfirmModal.ts +5 -2
  9. package/src/components/contracts/Modal.ts +8 -3
  10. package/src/components/contracts/PromptModal.ts +6 -2
  11. package/src/components/contracts/Toast.ts +1 -1
  12. package/src/components/headless/HeadlessInputInput.vue +13 -5
  13. package/src/components/headless/HeadlessModal.vue +6 -34
  14. package/src/components/headless/HeadlessModalContent.vue +5 -12
  15. package/src/components/index.ts +0 -1
  16. package/src/components/ui/AdvancedOptions.vue +4 -13
  17. package/src/components/ui/Button.vue +1 -0
  18. package/src/components/ui/ConfirmModal.vue +7 -2
  19. package/src/components/ui/Details.vue +20 -0
  20. package/src/components/ui/LoadingModal.vue +1 -2
  21. package/src/components/ui/Modal.vue +53 -33
  22. package/src/components/ui/PromptModal.vue +7 -2
  23. package/src/components/ui/Toast.vue +1 -0
  24. package/src/components/ui/index.ts +1 -1
  25. package/src/forms/FormController.ts +4 -0
  26. package/src/index.css +9 -0
  27. package/src/ui/UI.state.ts +0 -11
  28. package/src/ui/UI.ts +42 -125
  29. package/src/ui/index.ts +1 -0
  30. package/src/ui/modals.ts +36 -0
  31. package/src/utils/composition/reactiveSet.test.ts +32 -0
  32. package/src/utils/composition/reactiveSet.ts +53 -0
  33. package/src/utils/index.ts +2 -0
  34. package/src/utils/time.ts +2 -0
  35. package/src/components/AppModals.vue +0 -14
  36. package/src/components/ui/ModalContext.vue +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerogel/core",
3
- "version": "0.1.1-next.b25730fd2850ebabef064973972dabd342d92769",
3
+ "version": "0.1.1-next.f40b19c2bd8a3c0f252f1811ed740ce17bb6f699",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -29,7 +29,8 @@
29
29
  "vue": "^3.5.0"
30
30
  },
31
31
  "dependencies": {
32
- "@noeldemartin/utils": "^0.7.1",
32
+ "@noeldemartin/utils": "0.7.1-next.5f7bc66cad4aaa2dce4a2314e99d562ff3ab993b",
33
+ "@noeldemartin/vue-modals": "0.0.0-next.124c6a1c5e8a2cef4ec43d6a01de0fc450f155b1",
33
34
  "class-variance-authority": "^0.7.1",
34
35
  "clsx": "^2.1.1",
35
36
  "dompurify": "^3.2.4",
@@ -1,9 +1,10 @@
1
1
  <template>
2
- <AppModals />
2
+ <ModalsPortal nested />
3
3
  <AppToasts />
4
4
  </template>
5
5
 
6
6
  <script setup lang="ts">
7
- import AppModals from './AppModals.vue';
7
+ import { ModalsPortal } from '@aerogel/core/ui/modals';
8
+
8
9
  import AppToasts from './AppToasts.vue';
9
10
  </script>
@@ -8,7 +8,7 @@ export interface AlertModalProps {
8
8
  message: string;
9
9
  }
10
10
 
11
- export interface AlertModalExpose extends ModalExpose<void> {}
11
+ export interface AlertModalExpose extends ModalExpose {}
12
12
 
13
13
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
14
14
  export function useAlertModal(props: AlertModalProps) {
@@ -1,7 +1,7 @@
1
1
  import type { PrimitiveProps } from 'reka-ui';
2
2
  import type { HTMLAttributes } from 'vue';
3
3
 
4
- export type ButtonVariant = 'default' | 'secondary' | 'danger' | 'ghost' | 'outline' | 'link';
4
+ export type ButtonVariant = 'default' | 'secondary' | 'danger' | 'warning' | 'ghost' | 'outline' | 'link';
5
5
  export type ButtonSize = 'default' | 'small' | 'large' | 'icon';
6
6
  export interface ButtonProps extends PrimitiveProps {
7
7
  class?: HTMLAttributes['class'];
@@ -4,10 +4,11 @@ import { translateWithDefault } from '@aerogel/core/lang';
4
4
  import { useForm } from '@aerogel/core/utils/composition/forms';
5
5
  import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
6
6
  import type { FormFieldDefinition } from '@aerogel/core/forms/FormController';
7
- import type { ModalExpose } from '@aerogel/core/components/contracts/Modal';
8
7
  import type { Nullable } from '@noeldemartin/utils';
8
+ import type { ModalEmits, ModalExpose } from '@aerogel/core/components/contracts/Modal';
9
9
 
10
10
  export type ConfirmModalCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
11
+ export type ConfirmModalResult = boolean | [boolean, Record<string, Nullable<boolean>>];
11
12
 
12
13
  export interface ConfirmModalProps {
13
14
  title?: string;
@@ -21,7 +22,9 @@ export interface ConfirmModalProps {
21
22
  required?: boolean;
22
23
  }
23
24
 
24
- export interface ConfirmModalExpose extends ModalExpose<boolean | [boolean, Record<string, Nullable<boolean>>]> {}
25
+ export interface ConfirmModalExpose extends ModalExpose {}
26
+
27
+ export interface ConfirmModalEmits extends ModalEmits<ConfirmModalResult> {}
25
28
 
26
29
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
27
30
  export function useConfirmModal(props: ConfirmModalProps) {
@@ -5,17 +5,22 @@ export type ModalContentInstance = Nullable<InstanceType<typeof DialogContent>>;
5
5
 
6
6
  export interface ModalProps {
7
7
  persistent?: boolean;
8
+ fullscreen?: boolean;
9
+ fullscreenMobile?: boolean;
8
10
  title?: string;
9
11
  titleHidden?: boolean;
10
12
  description?: string;
11
13
  descriptionHidden?: boolean;
12
14
  }
13
15
 
14
- export interface ModalSlots<Result = void> {
16
+ export interface ModalSlots<Result = never> {
15
17
  default(props: { close(result?: Result): Promise<void> }): unknown;
16
18
  }
17
19
 
18
- export interface ModalExpose<Result = void> {
19
- close(result?: Result): Promise<void>;
20
+ export interface ModalExpose {
20
21
  $content: ModalContentInstance;
21
22
  }
23
+
24
+ export interface ModalEmits<Result = never> {
25
+ (event: 'close', payload: Result): void;
26
+ }
@@ -4,7 +4,9 @@ import { useForm } from '@aerogel/core/utils/composition/forms';
4
4
  import { requiredStringInput } from '@aerogel/core/forms/utils';
5
5
  import { translateWithDefault } from '@aerogel/core/lang';
6
6
  import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
7
- import type { ModalExpose } from '@aerogel/core/components/contracts/Modal';
7
+ import type { ModalEmits, ModalExpose } from '@aerogel/core/components/contracts/Modal';
8
+
9
+ export type PromptModalResult = string;
8
10
 
9
11
  export interface PromptModalProps {
10
12
  title?: string;
@@ -18,7 +20,9 @@ export interface PromptModalProps {
18
20
  cancelVariant?: ButtonVariant;
19
21
  }
20
22
 
21
- export interface PromptModalExpose extends ModalExpose<string> {}
23
+ export interface PromptModalExpose extends ModalExpose {}
24
+
25
+ export interface PromptModalEmits extends ModalEmits<PromptModalResult> {}
22
26
 
23
27
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
24
28
  export function usePromptModal(props: PromptModalProps) {
@@ -1,4 +1,4 @@
1
- export type ToastVariant = 'secondary' | 'danger';
1
+ export type ToastVariant = 'secondary' | 'warning' | 'danger';
2
2
 
3
3
  export interface ToastAction {
4
4
  label: string;
@@ -19,6 +19,7 @@ import { computed, inject, useTemplateRef, watchEffect } from 'vue';
19
19
 
20
20
  import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
21
21
  import { onFormFocus } from '@aerogel/core/utils/composition/forms';
22
+ import { LOCAL_TIMEZONE_OFFSET } from '@aerogel/core/utils';
22
23
  import type FormController from '@aerogel/core/forms/FormController';
23
24
  import type { FormFieldValue } from '@aerogel/core/forms/FormController';
24
25
  import type { InputExpose } from '@aerogel/core/components/contracts/Input';
@@ -39,7 +40,7 @@ const renderedType = computed(() => {
39
40
  return ['text', 'email', 'number', 'tel', 'url'].includes(fieldType) ? fieldType : 'text';
40
41
  });
41
42
  const checked = computed(() => {
42
- if (type !== 'checkbox') {
43
+ if (renderedType.value !== 'checkbox') {
43
44
  return;
44
45
  }
45
46
 
@@ -59,11 +60,15 @@ function getValue(): FormFieldValue | null {
59
60
  return null;
60
61
  }
61
62
 
62
- switch (type) {
63
+ switch (renderedType.value) {
63
64
  case 'checkbox':
64
65
  return $input.value.checked;
65
66
  case 'date':
66
- return $input.value.valueAsDate;
67
+ case 'time':
68
+ case 'datetime-local':
69
+ return new Date(Math.round($input.value.valueAsNumber / 60000) * 60000 + LOCAL_TIMEZONE_OFFSET);
70
+ case 'number':
71
+ return $input.value.valueAsNumber;
67
72
  default:
68
73
  return $input.value.value;
69
74
  }
@@ -75,8 +80,11 @@ watchEffect(() => {
75
80
  return;
76
81
  }
77
82
 
78
- if (type === 'date' && value.value instanceof Date) {
79
- $input.value.valueAsDate = value.value;
83
+ if (['date', 'time', 'datetime-local'].includes(renderedType.value) && value.value instanceof Date) {
84
+ const roundedValue = Math.round(value.value.getTime() / 60000) * 60000;
85
+
86
+ $input.value.valueAsNumber = roundedValue - LOCAL_TIMEZONE_OFFSET;
87
+ input.update(new Date(roundedValue));
80
88
 
81
89
  return;
82
90
  }
@@ -1,57 +1,29 @@
1
1
  <template>
2
- <DialogRoot :ref="forwardRef" open @update:open="persistent || close()">
2
+ <DialogRoot :ref="forwardRef" open @update:open="persistent || $event || close()">
3
3
  <DialogPortal>
4
- <slot :close="close" />
4
+ <slot :close />
5
5
  </DialogPortal>
6
6
  </DialogRoot>
7
7
  </template>
8
8
 
9
9
  <script setup lang="ts" generic="T = void">
10
- import { provide, ref } from 'vue';
11
10
  import { DialogPortal, DialogRoot, useForwardExpose } from 'reka-ui';
11
+ import { provide, ref } from 'vue';
12
12
  import type { DialogContent } from 'reka-ui';
13
13
  import type { Nullable } from '@noeldemartin/utils';
14
14
 
15
- import Events from '@aerogel/core/services/Events';
16
- import { useEvent } from '@aerogel/core/utils/composition/events';
17
- import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
15
+ import { useModal } from '@aerogel/core/ui/modals';
18
16
  import type { AcceptRefs } from '@aerogel/core/utils/vue';
19
- import type { UIModalContext } from '@aerogel/core/ui/UI';
20
17
  import type { ModalExpose, ModalProps, ModalSlots } from '@aerogel/core/components/contracts/Modal';
21
18
 
22
19
  const $content = ref<Nullable<InstanceType<typeof DialogContent>>>(null);
23
- const { modal } = injectReactiveOrFail<UIModalContext>(
24
- 'modal',
25
- 'could not obtain modal reference from <HeadlessModal>, ' +
26
- 'did you render this component manually? Show it using $ui.modal() instead',
27
- );
20
+ const { close } = useModal<T>();
28
21
 
29
22
  defineProps<ModalProps>();
30
23
  defineSlots<ModalSlots<T>>();
31
- defineExpose<AcceptRefs<ModalExpose<T>>>({ close, $content });
24
+ defineExpose<AcceptRefs<ModalExpose>>({ $content });
32
25
 
33
26
  const { forwardRef } = useForwardExpose();
34
- const closed = ref(false);
35
27
 
36
28
  provide('$modalContentRef', $content);
37
-
38
- useEvent('close-modal', async ({ id, result }) => {
39
- if (id !== modal.id) {
40
- return;
41
- }
42
-
43
- await close(result);
44
- });
45
-
46
- async function close(result?: unknown) {
47
- if (closed.value) {
48
- return;
49
- }
50
-
51
- await Events.emit('modal-will-close', { modal, result });
52
-
53
- closed.value = true;
54
-
55
- await Events.emit('modal-has-closed', { modal, result });
56
- }
57
29
  </script>
@@ -2,29 +2,22 @@
2
2
  <DialogContent ref="$contentRef">
3
3
  <slot />
4
4
 
5
- <ModalContext v-if="childModal" :child-index="childIndex + 1" :modal="childModal" />
5
+ <ModalComponent :is="child" v-if="child" />
6
6
  </DialogContent>
7
7
  </template>
8
8
 
9
9
  <script setup lang="ts">
10
- import { computed, useTemplateRef, watchEffect } from 'vue';
10
+ import { useTemplateRef, watchEffect } from 'vue';
11
11
  import { DialogContent } from 'reka-ui';
12
12
  import type { Ref } from 'vue';
13
13
 
14
- import ModalContext from '@aerogel/core/components/ui/ModalContext.vue';
15
- import UI from '@aerogel/core/ui/UI';
16
- import { injectOrFail, injectReactiveOrFail } from '@aerogel/core/utils/vue';
17
- import type { UIModalContext } from '@aerogel/core/ui/UI';
14
+ import { ModalComponent, useModal } from '@aerogel/core/ui/modals';
15
+ import { injectOrFail } from '@aerogel/core/utils/vue';
18
16
  import type { ModalContentInstance } from '@aerogel/core/components/contracts/Modal';
19
17
 
20
- const { childIndex = 0 } = injectReactiveOrFail<UIModalContext>(
21
- 'modal',
22
- 'could not obtain modal reference from <HeadlessModalContent>, ' +
23
- 'did you render this component manually? Show it using $ui.modal() instead',
24
- );
18
+ const { child } = useModal();
25
19
  const $modalContentRef = injectOrFail<Ref<ModalContentInstance>>('$modalContentRef');
26
20
  const $content = useTemplateRef('$contentRef');
27
- const childModal = computed(() => UI.modals[childIndex] ?? null);
28
21
 
29
22
  watchEffect(() => ($modalContentRef.value = $content.value));
30
23
  </script>
@@ -1,5 +1,4 @@
1
1
  export { default as AppLayout } from './AppLayout.vue';
2
- export { default as AppModals } from './AppModals.vue';
3
2
  export { default as AppOverlays } from './AppOverlays.vue';
4
3
  export { default as AppToasts } from './AppToasts.vue';
5
4
 
@@ -1,18 +1,9 @@
1
1
  <template>
2
- <details class="group">
3
- <summary
4
- class="-ml-2 flex w-[max-content] items-center rounded-lg py-2 pr-3 pl-1 hover:bg-gray-100 focus-visible:outline focus-visible:outline-gray-700"
5
- >
6
- <IconCheveronRight class="size-6 transition-transform group-open:rotate-90" />
7
- <span>{{ $td('ui.advancedOptions', 'Advanced options') }}</span>
8
- </summary>
9
-
10
- <div class="pt-2 pl-4">
11
- <slot />
12
- </div>
13
- </details>
2
+ <Details :label="$td('ui.advancedOptions', 'Advanced options')">
3
+ <slot />
4
+ </Details>
14
5
  </template>
15
6
 
16
7
  <script setup lang="ts">
17
- import IconCheveronRight from '~icons/zondicons/cheveron-right';
8
+ import Details from './Details.vue';
18
9
  </script>
@@ -25,6 +25,7 @@ const renderedClasses = computed(() => variantClasses<Variants<Pick<ButtonProps,
25
25
  default: 'bg-primary-600 text-white focus-visible:outline-primary-600',
26
26
  secondary: 'bg-background text-gray-900 ring-gray-300',
27
27
  danger: 'bg-red-600 text-white focus-visible:outline-red-600',
28
+ warning: 'bg-yellow-600 text-white focus-visible:outline-yellow-600',
28
29
  ghost: 'bg-transparent',
29
30
  outline: 'bg-transparent text-primary-600 ring-primary-600',
30
31
  link: 'text-links',
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <!-- @vue-generic {import('@aerogel/core/ui/UI').ModalExposeResult<ConfirmModalExpose>} -->
2
+ <!-- @vue-generic {import('@aerogel/core/components/contracts/ConfirmModal').ConfirmModalResult} -->
3
3
  <Modal
4
4
  v-slot="{ close }"
5
5
  :title="renderedTitle"
@@ -41,10 +41,15 @@ import Markdown from '@aerogel/core/components/ui/Markdown.vue';
41
41
  import Button from '@aerogel/core/components/ui/Button.vue';
42
42
  import Modal from '@aerogel/core/components/ui/Modal.vue';
43
43
  import { useConfirmModal } from '@aerogel/core/components/contracts/ConfirmModal';
44
- import type { ConfirmModalExpose, ConfirmModalProps } from '@aerogel/core/components/contracts/ConfirmModal';
44
+ import type {
45
+ ConfirmModalEmits,
46
+ ConfirmModalExpose,
47
+ ConfirmModalProps,
48
+ } from '@aerogel/core/components/contracts/ConfirmModal';
45
49
 
46
50
  const { cancelVariant = 'secondary', ...props } = defineProps<ConfirmModalProps>();
47
51
  const { form, renderedTitle, titleHidden, renderedAcceptText, renderedCancelText } = useConfirmModal(props);
48
52
 
53
+ defineEmits<ConfirmModalEmits>();
49
54
  defineExpose<ConfirmModalExpose>();
50
55
  </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <details class="group">
3
+ <summary
4
+ class="-ml-2 flex w-[max-content] items-center rounded-lg py-2 pr-3 pl-1 hover:bg-gray-100 focus-visible:outline focus-visible:outline-gray-700"
5
+ >
6
+ <IconCheveronRight class="size-6 transition-transform group-open:rotate-90" />
7
+ <span>{{ label }}</span>
8
+ </summary>
9
+
10
+ <div class="pt-2 pl-4">
11
+ <slot />
12
+ </div>
13
+ </details>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import IconCheveronRight from '~icons/zondicons/cheveron-right';
18
+
19
+ defineProps<{ label: string }>();
20
+ </script>
@@ -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"
@@ -7,14 +7,20 @@
7
7
  :persistent
8
8
  >
9
9
  <HeadlessModalOverlay
10
- class="fixed inset-0 animate-[fade-in_var(--tw-duration)_ease-in-out] transition-opacity duration-300 will-change-[opacity]"
10
+ class="fixed inset-0 transition-opacity duration-300 will-change-[opacity]"
11
11
  :class="{
12
- 'bg-black/30': context.childIndex === 1,
13
- 'opacity-0': context.childIndex === 1 && context.modal.closing,
12
+ 'animate-[fade-in_var(--tw-duration)_ease-in-out]': !hasRenderedModals,
13
+ 'bg-black/30': firstVisibleModal?.id === id || (!firstVisibleModal && modals[0]?.id === id),
14
+ 'opacity-0': !firstVisibleModal,
15
+ hidden: renderFullscreen,
14
16
  }"
15
17
  />
16
18
  <HeadlessModalContent v-bind="contentProps" :class="renderedWrapperClass">
17
- <div v-if="!persistent && !closeHidden" class="absolute top-0 right-0 hidden pt-3.5 pr-2.5 sm:block">
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
+ >
18
24
  <button
19
25
  type="button"
20
26
  class="clickable z-10 rounded-full p-2.5 text-gray-400 hover:text-gray-500"
@@ -52,14 +58,13 @@
52
58
  </HeadlessModal>
53
59
  </template>
54
60
 
55
- <script setup lang="ts" generic="T = void">
61
+ <script lang="ts">
56
62
  import IconClose from '~icons/zondicons/close';
57
63
 
58
- import { after } from '@noeldemartin/utils';
59
- import { computed } from 'vue';
60
64
  import { useForwardExpose } from 'reka-ui';
65
+ import { computed, onMounted } from 'vue';
61
66
  import type { ComponentPublicInstance, HTMLAttributes, Ref } from 'vue';
62
- import type { Nullable } from '@noeldemartin/utils';
67
+ import { type Nullable, after } from '@noeldemartin/utils';
63
68
 
64
69
  import Markdown from '@aerogel/core/components/ui/Markdown.vue';
65
70
  import HeadlessModal from '@aerogel/core/components/headless/HeadlessModal.vue';
@@ -69,13 +74,17 @@ import HeadlessModalOverlay from '@aerogel/core/components/headless/HeadlessModa
69
74
  import HeadlessModalTitle from '@aerogel/core/components/headless/HeadlessModalTitle.vue';
70
75
  import UI from '@aerogel/core/ui/UI';
71
76
  import { classes } from '@aerogel/core/utils/classes';
72
- import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
73
- import { useEvent } from '@aerogel/core/utils/composition/events';
77
+ import { reactiveSet } from '@aerogel/core/utils';
78
+ import { injectModal, modals, useModal } from '@aerogel/core/ui/modals';
79
+ import type { ModalController } from '@aerogel/core/ui/modals';
74
80
  import type { AcceptRefs } from '@aerogel/core/utils/vue';
75
81
  import type { ModalExpose, ModalProps, ModalSlots } from '@aerogel/core/components/contracts/Modal';
76
- import type { UIModalContext } from '@aerogel/core/ui/UI';
77
82
 
78
- type HeadlessModalInstance = ComponentPublicInstance & ModalExpose<T>;
83
+ const renderedModals = reactiveSet<ModalController>();
84
+ </script>
85
+
86
+ <script setup lang="ts" generic="T = void">
87
+ type HeadlessModalInstance = ComponentPublicInstance & ModalExpose;
79
88
 
80
89
  const {
81
90
  class: contentClass = '',
@@ -85,6 +94,8 @@ const {
85
94
  description,
86
95
  persistent,
87
96
  closeHidden,
97
+ fullscreen,
98
+ fullscreenMobile,
88
99
  ...props
89
100
  } = defineProps<
90
101
  ModalProps & {
@@ -95,37 +106,46 @@ const {
95
106
  >();
96
107
 
97
108
  defineSlots<ModalSlots<T>>();
98
- defineExpose<AcceptRefs<ModalExpose<T>>>({
99
- close: async (result) => $modal.value?.close(result),
109
+ defineExpose<AcceptRefs<ModalExpose>>({
100
110
  $content: computed(() => $modal.value?.$content),
101
111
  });
102
112
 
103
113
  const { forwardRef, currentRef } = useForwardExpose<HeadlessModalInstance>();
114
+ const { id, visible } = useModal();
104
115
  const $modal = currentRef as Ref<Nullable<HeadlessModalInstance>>;
105
- const context = injectReactiveOrFail<UIModalContext>('modal');
106
- const inForeground = computed(() => !context.modal.closing && context.childIndex === UI.openModals.length);
116
+ const modal = injectModal();
117
+ const inForeground = computed(
118
+ () => visible.value && modals.value.toReversed().find((modal) => modal.visible.value)?.id === id.value,
119
+ );
120
+ const firstVisibleModal = computed(() => modals.value.find((modal) => modal.visible.value));
121
+ const hasRenderedModals = computed(() => modals.value.some((modal) => renderedModals.has(modal)));
107
122
  const contentProps = computed(() => (description ? {} : { 'aria-describedby': undefined }));
108
123
  const renderedContentClass = computed(() =>
109
- classes('max-h-[90vh] overflow-auto px-4 pb-4', { 'pt-4': !title || titleHidden }, contentClass));
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 || (fullscreenMobile && UI.mobile));
110
130
  const renderedWrapperClass = computed(() =>
111
131
  classes(
112
- 'isolate fixed top-1/2 left-1/2 z-50 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2',
113
- 'overflow-hidden rounded-lg bg-white text-left shadow-xl sm:max-w-lg',
114
- 'animate-[fade-in_var(--tw-duration)_ease-in-out,grow_var(--tw-duration)_ease-in-out]',
115
- 'transition-[scale,opacity] will-change-[scale,opacity] duration-300',
116
- {
117
- 'scale-50 opacity-0': !inForeground.value,
118
- 'scale-100 opacity-100': inForeground.value,
119
- },
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
+ ],
120
147
  wrapperClass,
121
148
  ));
122
149
 
123
- useEvent('modal-will-close', async ({ modal: { id } }) => {
124
- if (id !== context.modal.id) {
125
- return;
126
- }
127
-
128
- // Wait for transitions to finish
129
- await after({ ms: 300 });
130
- });
150
+ onMounted(() => after(500).then(() => renderedModals.add(modal.value)));
131
151
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <!-- @vue-generic {import('@aerogel/core/ui/UI').ModalExposeResult<PromptModalExpose>} -->
2
+ <!-- @vue-generic {import('@aerogel/core/components/contracts/PromptModal').PromptModalResult} -->
3
3
  <Modal v-slot="{ close }" :title="renderedTitle" persistent>
4
4
  <Form :form @submit="close(form.draft)">
5
5
  <Markdown v-if="renderedMessage" :text="renderedMessage" />
@@ -29,10 +29,15 @@ import Form from '@aerogel/core/components/ui/Form.vue';
29
29
  import Input from '@aerogel/core/components/ui/Input.vue';
30
30
  import Modal from '@aerogel/core/components/ui/Modal.vue';
31
31
  import { usePromptModal } from '@aerogel/core/components/contracts/PromptModal';
32
- import type { PromptModalExpose, PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
32
+ import type {
33
+ PromptModalEmits,
34
+ PromptModalExpose,
35
+ PromptModalProps,
36
+ } from '@aerogel/core/components/contracts/PromptModal';
33
37
 
34
38
  const { cancelVariant = 'secondary', ...props } = defineProps<PromptModalProps>();
35
39
  const { form, renderedTitle, renderedMessage, renderedAcceptText, renderedCancelText } = usePromptModal(props);
36
40
 
41
+ defineEmits<PromptModalEmits>();
37
42
  defineExpose<PromptModalExpose>();
38
43
  </script>
@@ -34,6 +34,7 @@ const renderedClasses = computed(() =>
34
34
  variant: {
35
35
  secondary: 'bg-gray-900 text-white ring-black',
36
36
  danger: 'bg-red-50 text-red-900 ring-red-100',
37
+ warning: 'bg-yellow-50 text-yellow-900 ring-yellow-100',
37
38
  },
38
39
  },
39
40
  defaultVariants: {
@@ -3,6 +3,7 @@ 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
5
  export { default as ConfirmModal } from './ConfirmModal.vue';
6
+ export { default as Details } from './Details.vue';
6
7
  export { default as DropdownMenu } from './DropdownMenu.vue';
7
8
  export { default as DropdownMenuOption } from './DropdownMenuOption.vue';
8
9
  export { default as DropdownMenuOptions } from './DropdownMenuOptions.vue';
@@ -19,7 +20,6 @@ export { default as Link } from './Link.vue';
19
20
  export { default as LoadingModal } from './LoadingModal.vue';
20
21
  export { default as Markdown } from './Markdown.vue';
21
22
  export { default as Modal } from './Modal.vue';
22
- export { default as ModalContext } from './ModalContext.vue';
23
23
  export { default as ProgressBar } from './ProgressBar.vue';
24
24
  export { default as PromptModal } from './PromptModal.vue';
25
25
  export { default as Select } from './Select.vue';
@@ -112,6 +112,10 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
112
112
  return this._fields[field]?.rules?.split('|') ?? [];
113
113
  }
114
114
 
115
+ public setFieldErrors<T extends keyof Fields>(field: T, errors: string[] | null): void {
116
+ this._errors[field] = errors;
117
+ }
118
+
115
119
  public getFieldType<T extends keyof Fields>(field: T): FormFieldType | null {
116
120
  return this._fields[field]?.type ?? null;
117
121
  }
package/src/index.css CHANGED
@@ -66,6 +66,15 @@ button[data-markdown-action] {
66
66
  }
67
67
  }
68
68
 
69
+ @keyframes slide-in {
70
+ 0% {
71
+ transform: translateY(100%);
72
+ }
73
+ 100% {
74
+ transform: translateY(0);
75
+ }
76
+ }
77
+
69
78
  @keyframes grow {
70
79
  0% {
71
80
  scale: 0;
@@ -4,15 +4,6 @@ import { defineServiceState } from '@aerogel/core/services/Service';
4
4
 
5
5
  import { Layouts, getCurrentLayout } from './utils';
6
6
 
7
- export interface UIModal<T = unknown> {
8
- id: string;
9
- properties: Record<string, unknown>;
10
- component: Component;
11
- closing: boolean;
12
- beforeClose: Promise<T | undefined>;
13
- afterClose: Promise<T | undefined>;
14
- }
15
-
16
7
  export interface UIToast {
17
8
  id: string;
18
9
  component: Component;
@@ -22,13 +13,11 @@ export interface UIToast {
22
13
  export default defineServiceState({
23
14
  name: 'ui',
24
15
  initialState: {
25
- modals: [] as UIModal[],
26
16
  toasts: [] as UIToast[],
27
17
  layout: getCurrentLayout(),
28
18
  },
29
19
  computed: {
30
20
  desktop: ({ layout }) => layout === Layouts.Desktop,
31
21
  mobile: ({ layout }) => layout === Layouts.Mobile,
32
- openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
33
22
  },
34
23
  });