@aerogel/core 0.1.0 → 0.1.1-next.01bc3a1d850a91b650f8cade6c0dd8e44109e0d8

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 (33) hide show
  1. package/dist/aerogel-core.d.ts +163 -228
  2. package/dist/aerogel-core.js +1501 -1524
  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 +6 -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 +5 -3
  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/Button.vue +1 -0
  17. package/src/components/ui/ConfirmModal.vue +7 -2
  18. package/src/components/ui/Form.vue +1 -1
  19. package/src/components/ui/Modal.vue +26 -25
  20. package/src/components/ui/PromptModal.vue +7 -2
  21. package/src/components/ui/Toast.vue +1 -0
  22. package/src/components/ui/index.ts +0 -1
  23. package/src/directives/index.ts +10 -8
  24. package/src/directives/safe-html.ts +10 -0
  25. package/src/ui/UI.state.ts +0 -11
  26. package/src/ui/UI.ts +42 -125
  27. package/src/ui/index.ts +1 -0
  28. package/src/ui/modals.ts +36 -0
  29. package/src/utils/composition/reactiveSet.test.ts +32 -0
  30. package/src/utils/composition/reactiveSet.ts +53 -0
  31. package/src/utils/index.ts +1 -0
  32. package/src/components/AppModals.vue +0 -14
  33. 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.0",
3
+ "version": "0.1.1-next.01bc3a1d850a91b650f8cade6c0dd8e44109e0d8",
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) {
@@ -11,11 +11,14 @@ export interface ModalProps {
11
11
  descriptionHidden?: boolean;
12
12
  }
13
13
 
14
- export interface ModalSlots<Result = void> {
14
+ export interface ModalSlots<Result = never> {
15
15
  default(props: { close(result?: Result): Promise<void> }): unknown;
16
16
  }
17
17
 
18
- export interface ModalExpose<Result = void> {
19
- close(result?: Result): Promise<void>;
18
+ export interface ModalExpose {
20
19
  $content: ModalContentInstance;
21
20
  }
21
+
22
+ export interface ModalEmits<Result = never> {
23
+ (event: 'close', payload: Result): void;
24
+ }
@@ -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;
@@ -39,7 +39,7 @@ const renderedType = computed(() => {
39
39
  return ['text', 'email', 'number', 'tel', 'url'].includes(fieldType) ? fieldType : 'text';
40
40
  });
41
41
  const checked = computed(() => {
42
- if (type !== 'checkbox') {
42
+ if (renderedType.value !== 'checkbox') {
43
43
  return;
44
44
  }
45
45
 
@@ -59,11 +59,13 @@ function getValue(): FormFieldValue | null {
59
59
  return null;
60
60
  }
61
61
 
62
- switch (type) {
62
+ switch (renderedType.value) {
63
63
  case 'checkbox':
64
64
  return $input.value.checked;
65
65
  case 'date':
66
66
  return $input.value.valueAsDate;
67
+ case 'number':
68
+ return $input.value.valueAsNumber;
67
69
  default:
68
70
  return $input.value.value;
69
71
  }
@@ -75,7 +77,7 @@ watchEffect(() => {
75
77
  return;
76
78
  }
77
79
 
78
- if (type === 'date' && value.value instanceof Date) {
80
+ if (renderedType.value === 'date' && value.value instanceof Date) {
79
81
  $input.value.valueAsDate = value.value;
80
82
 
81
83
  return;
@@ -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
 
@@ -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>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <form @submit.prevent="form?.submit()">
2
+ <form @submit.prevent="form ? form.submit() : $emit('submit')">
3
3
  <slot />
4
4
  </form>
5
5
  </template>
@@ -7,10 +7,11 @@
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,
14
15
  }"
15
16
  />
16
17
  <HeadlessModalContent v-bind="contentProps" :class="renderedWrapperClass">
@@ -52,14 +53,13 @@
52
53
  </HeadlessModal>
53
54
  </template>
54
55
 
55
- <script setup lang="ts" generic="T = void">
56
+ <script lang="ts">
56
57
  import IconClose from '~icons/zondicons/close';
57
58
 
58
- import { after } from '@noeldemartin/utils';
59
- import { computed } from 'vue';
60
59
  import { useForwardExpose } from 'reka-ui';
60
+ import { computed, onMounted } from 'vue';
61
61
  import type { ComponentPublicInstance, HTMLAttributes, Ref } from 'vue';
62
- import type { Nullable } from '@noeldemartin/utils';
62
+ import { type Nullable, after } from '@noeldemartin/utils';
63
63
 
64
64
  import Markdown from '@aerogel/core/components/ui/Markdown.vue';
65
65
  import HeadlessModal from '@aerogel/core/components/headless/HeadlessModal.vue';
@@ -67,15 +67,18 @@ import HeadlessModalContent from '@aerogel/core/components/headless/HeadlessModa
67
67
  import HeadlessModalDescription from '@aerogel/core/components/headless/HeadlessModalDescription.vue';
68
68
  import HeadlessModalOverlay from '@aerogel/core/components/headless/HeadlessModalOverlay.vue';
69
69
  import HeadlessModalTitle from '@aerogel/core/components/headless/HeadlessModalTitle.vue';
70
- import UI from '@aerogel/core/ui/UI';
71
70
  import { classes } from '@aerogel/core/utils/classes';
72
- import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
73
- import { useEvent } from '@aerogel/core/utils/composition/events';
71
+ import { reactiveSet } from '@aerogel/core/utils';
72
+ import { injectModal, modals, useModal } from '@aerogel/core/ui/modals';
73
+ import type { ModalController } from '@aerogel/core/ui/modals';
74
74
  import type { AcceptRefs } from '@aerogel/core/utils/vue';
75
75
  import type { ModalExpose, ModalProps, ModalSlots } from '@aerogel/core/components/contracts/Modal';
76
- import type { UIModalContext } from '@aerogel/core/ui/UI';
77
76
 
78
- type HeadlessModalInstance = ComponentPublicInstance & ModalExpose<T>;
77
+ const renderedModals = reactiveSet<ModalController>();
78
+ </script>
79
+
80
+ <script setup lang="ts" generic="T = void">
81
+ type HeadlessModalInstance = ComponentPublicInstance & ModalExpose;
79
82
 
80
83
  const {
81
84
  class: contentClass = '',
@@ -95,15 +98,19 @@ const {
95
98
  >();
96
99
 
97
100
  defineSlots<ModalSlots<T>>();
98
- defineExpose<AcceptRefs<ModalExpose<T>>>({
99
- close: async (result) => $modal.value?.close(result),
101
+ defineExpose<AcceptRefs<ModalExpose>>({
100
102
  $content: computed(() => $modal.value?.$content),
101
103
  });
102
104
 
103
105
  const { forwardRef, currentRef } = useForwardExpose<HeadlessModalInstance>();
106
+ const { id, visible } = useModal();
104
107
  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);
108
+ const modal = injectModal();
109
+ const inForeground = computed(
110
+ () => visible.value && modals.value.toReversed().find((modal) => modal.visible.value)?.id === id.value,
111
+ );
112
+ const firstVisibleModal = computed(() => modals.value.find((modal) => modal.visible.value));
113
+ const hasRenderedModals = computed(() => modals.value.some((modal) => renderedModals.has(modal)));
107
114
  const contentProps = computed(() => (description ? {} : { 'aria-describedby': undefined }));
108
115
  const renderedContentClass = computed(() =>
109
116
  classes('max-h-[90vh] overflow-auto px-4 pb-4', { 'pt-4': !title || titleHidden }, contentClass));
@@ -111,7 +118,8 @@ const renderedWrapperClass = computed(() =>
111
118
  classes(
112
119
  '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
120
  '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]',
121
+ renderedModals.has(modal.value) ||
122
+ 'animate-[fade-in_var(--tw-duration)_ease-in-out,grow_var(--tw-duration)_ease-in-out]',
115
123
  'transition-[scale,opacity] will-change-[scale,opacity] duration-300',
116
124
  {
117
125
  'scale-50 opacity-0': !inForeground.value,
@@ -120,12 +128,5 @@ const renderedWrapperClass = computed(() =>
120
128
  wrapperClass,
121
129
  ));
122
130
 
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
- });
131
+ onMounted(() => after(500).then(() => renderedModals.add(modal.value)));
131
132
  </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: {
@@ -19,7 +19,6 @@ export { default as Link } from './Link.vue';
19
19
  export { default as LoadingModal } from './LoadingModal.vue';
20
20
  export { default as Markdown } from './Markdown.vue';
21
21
  export { default as Modal } from './Modal.vue';
22
- export { default as ModalContext } from './ModalContext.vue';
23
22
  export { default as ProgressBar } from './ProgressBar.vue';
24
23
  export { default as PromptModal } from './PromptModal.vue';
25
24
  export { default as Select } from './Select.vue';
@@ -3,22 +3,26 @@ import type { Directive } from 'vue';
3
3
  import { definePlugin } from '@aerogel/core/plugins';
4
4
 
5
5
  import measure from './measure';
6
+ import safeHtml from './safe-html';
6
7
 
7
- const builtInDirectives: Record<string, Directive> = {
8
- measure: measure,
9
- };
8
+ export const aerogelDirectives = {
9
+ 'measure': measure,
10
+ 'safe-html': safeHtml,
11
+ } as const satisfies Record<string, Directive>;
12
+
13
+ export type AerogelDirectives = typeof aerogelDirectives;
10
14
 
11
15
  export * from './measure';
12
16
 
13
17
  export default definePlugin({
14
18
  install(app, options) {
15
19
  const directives = {
16
- ...builtInDirectives,
20
+ ...aerogelDirectives,
17
21
  ...options.directives,
18
22
  };
19
23
 
20
24
  for (const [name, directive] of Object.entries(directives)) {
21
- app.directive(name, directive);
25
+ app.directive(name, directive as Directive);
22
26
  }
23
27
  },
24
28
  });
@@ -30,7 +34,5 @@ declare module '@aerogel/core/bootstrap/options' {
30
34
  }
31
35
 
32
36
  declare module 'vue' {
33
- interface ComponentCustomDirectives {
34
- measure: Directive<string, string>;
35
- }
37
+ interface ComponentCustomDirectives extends AerogelDirectives {}
36
38
  }
@@ -0,0 +1,10 @@
1
+ import { safeHtml } from '@aerogel/core/utils';
2
+ import { defineDirective } from '@aerogel/core/utils/vue';
3
+
4
+ export type SafeHTMLDirectiveValue = string;
5
+
6
+ export default defineDirective<SafeHTMLDirectiveValue>({
7
+ mounted(element: HTMLElement, { value }) {
8
+ element.innerHTML = safeHtml(value);
9
+ },
10
+ });
@@ -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
  });