@aerogel/core 0.0.0-next.fcfbfdc3428c34c4d1c0e781b61d244f13232fc9 → 0.1.0-next.c4b24f52d8b652bd5c14c2d12e1b38b779ab7682

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 (80) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +661 -919
  3. package/dist/aerogel-core.js +1983 -1440
  4. package/dist/aerogel-core.js.map +1 -1
  5. package/package.json +7 -5
  6. package/src/components/AppLayout.vue +1 -3
  7. package/src/components/AppOverlays.vue +0 -27
  8. package/src/components/contracts/AlertModal.ts +15 -0
  9. package/src/components/contracts/ConfirmModal.ts +12 -5
  10. package/src/components/contracts/DropdownMenu.ts +8 -3
  11. package/src/components/contracts/ErrorReportModal.ts +8 -4
  12. package/src/components/contracts/Input.ts +7 -7
  13. package/src/components/contracts/LoadingModal.ts +6 -2
  14. package/src/components/contracts/Modal.ts +4 -4
  15. package/src/components/contracts/PromptModal.ts +5 -1
  16. package/src/components/contracts/Select.ts +9 -8
  17. package/src/components/contracts/Toast.ts +4 -2
  18. package/src/components/headless/HeadlessButton.vue +2 -2
  19. package/src/components/headless/HeadlessInputInput.vue +16 -5
  20. package/src/components/headless/HeadlessModal.vue +8 -43
  21. package/src/components/headless/HeadlessModalContent.vue +2 -2
  22. package/src/components/headless/HeadlessSelect.vue +10 -8
  23. package/src/components/headless/HeadlessSelectOptions.vue +8 -3
  24. package/src/components/headless/HeadlessSwitch.vue +96 -0
  25. package/src/components/headless/index.ts +1 -0
  26. package/src/components/ui/AdvancedOptions.vue +1 -1
  27. package/src/components/ui/AlertModal.vue +7 -3
  28. package/src/components/ui/Button.vue +27 -10
  29. package/src/components/ui/ConfirmModal.vue +11 -3
  30. package/src/components/ui/DropdownMenuOption.vue +12 -4
  31. package/src/components/ui/DropdownMenuOptions.vue +18 -1
  32. package/src/components/ui/ErrorLogs.vue +19 -0
  33. package/src/components/ui/ErrorLogsModal.vue +48 -0
  34. package/src/components/ui/ErrorReportModal.vue +18 -7
  35. package/src/components/ui/Input.vue +2 -2
  36. package/src/components/ui/LoadingModal.vue +3 -1
  37. package/src/components/ui/Markdown.vue +29 -1
  38. package/src/components/ui/Modal.vue +61 -21
  39. package/src/components/ui/ModalContext.vue +2 -1
  40. package/src/components/ui/PromptModal.vue +5 -2
  41. package/src/components/ui/Select.vue +5 -3
  42. package/src/components/ui/SelectLabel.vue +5 -1
  43. package/src/components/ui/SelectOptions.vue +6 -1
  44. package/src/components/ui/SelectTrigger.vue +1 -1
  45. package/src/components/ui/Setting.vue +31 -0
  46. package/src/components/ui/StartupCrash.vue +51 -6
  47. package/src/components/ui/Switch.vue +11 -0
  48. package/src/components/ui/TextArea.vue +56 -0
  49. package/src/components/ui/Toast.vue +19 -15
  50. package/src/components/ui/index.ts +5 -0
  51. package/src/directives/measure.ts +11 -5
  52. package/src/errors/Errors.state.ts +1 -0
  53. package/src/errors/Errors.ts +45 -21
  54. package/src/errors/index.ts +6 -2
  55. package/src/errors/settings/Debug.vue +14 -0
  56. package/src/errors/settings/index.ts +10 -0
  57. package/src/forms/FormController.test.ts +35 -9
  58. package/src/forms/FormController.ts +34 -24
  59. package/src/forms/index.ts +0 -1
  60. package/src/forms/utils.ts +58 -33
  61. package/src/forms/validation.ts +31 -0
  62. package/src/index.css +34 -12
  63. package/src/lang/index.ts +1 -1
  64. package/src/lang/settings/Language.vue +1 -1
  65. package/src/services/Events.test.ts +8 -8
  66. package/src/services/Events.ts +2 -8
  67. package/src/services/Service.ts +11 -6
  68. package/src/services/index.ts +2 -2
  69. package/src/testing/index.ts +4 -0
  70. package/src/ui/UI.state.ts +3 -13
  71. package/src/ui/UI.ts +103 -84
  72. package/src/ui/index.ts +16 -17
  73. package/src/utils/app.ts +7 -0
  74. package/src/utils/classes.ts +9 -17
  75. package/src/utils/composition/events.ts +2 -4
  76. package/src/utils/composition/forms.ts +7 -1
  77. package/src/utils/index.ts +1 -0
  78. package/src/utils/markdown.ts +35 -1
  79. package/src/utils/vue.ts +6 -1
  80. package/src/forms/composition.ts +0 -6
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <div :class="rootClass">
3
+ <label v-if="label" :for="expose.id" :class="labelClass">
4
+ {{ label }}
5
+ </label>
6
+ <SwitchRoot
7
+ :id="expose.id"
8
+ :name
9
+ :model-value="expose.value.value"
10
+ v-bind="$attrs"
11
+ :class="inputClass"
12
+ @update:model-value="$emit('update:modelValue', $event)"
13
+ >
14
+ <SwitchThumb :class="thumbClass" />
15
+ </SwitchRoot>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts" generic="T extends boolean = boolean">
20
+ import { SwitchRoot, SwitchThumb } from 'reka-ui';
21
+ import { computed, inject, readonly, watchEffect } from 'vue';
22
+ import { uuid } from '@noeldemartin/utils';
23
+ import type { HTMLAttributes } from 'vue';
24
+
25
+ import type FormController from '@aerogel/core/forms/FormController';
26
+ import type { FormFieldValue } from '@aerogel/core/forms/FormController';
27
+ import type { InputEmits, InputExpose, InputProps } from '@aerogel/core/components/contracts/Input';
28
+
29
+ defineOptions({ inheritAttrs: false });
30
+
31
+ const {
32
+ name,
33
+ label,
34
+ description,
35
+ modelValue,
36
+ class: rootClass,
37
+ } = defineProps<
38
+ InputProps<T> & {
39
+ class?: HTMLAttributes['class'];
40
+ labelClass?: HTMLAttributes['class'];
41
+ inputClass?: HTMLAttributes['class'];
42
+ thumbClass?: HTMLAttributes['class'];
43
+ }
44
+ >();
45
+ const emit = defineEmits<InputEmits>();
46
+ const form = inject<FormController | null>('form', null);
47
+ const errors = computed(() => {
48
+ if (!form || !name) {
49
+ return null;
50
+ }
51
+
52
+ return form.errors[name] ?? null;
53
+ });
54
+
55
+ const expose = {
56
+ id: `switch-${uuid()}`,
57
+ name: computed(() => name),
58
+ label: computed(() => label),
59
+ description: computed(() => description),
60
+ value: computed(() => {
61
+ if (form && name) {
62
+ return form.getFieldValue(name) as boolean;
63
+ }
64
+
65
+ return modelValue;
66
+ }),
67
+ errors: readonly(errors),
68
+ required: computed(() => {
69
+ if (!name || !form) {
70
+ return;
71
+ }
72
+
73
+ return form.getFieldRules(name).includes('required');
74
+ }),
75
+ update(value) {
76
+ if (form && name) {
77
+ form.setFieldValue(name, value as FormFieldValue);
78
+
79
+ return;
80
+ }
81
+
82
+ emit('update:modelValue', value);
83
+ },
84
+ } satisfies InputExpose;
85
+
86
+ defineExpose(expose);
87
+
88
+ watchEffect(() => {
89
+ if (!description && !errors.value) {
90
+ return;
91
+ }
92
+
93
+ // eslint-disable-next-line no-console
94
+ console.warn('Errors and description not implemented in <HeadlessSwitch>');
95
+ });
96
+ </script>
@@ -16,4 +16,5 @@ export { default as HeadlessSelectOption } from './HeadlessSelectOption.vue';
16
16
  export { default as HeadlessSelectOptions } from './HeadlessSelectOptions.vue';
17
17
  export { default as HeadlessSelectTrigger } from './HeadlessSelectTrigger.vue';
18
18
  export { default as HeadlessSelectValue } from './HeadlessSelectValue.vue';
19
+ export { default as HeadlessSwitch } from './HeadlessSwitch.vue';
19
20
  export { default as HeadlessToast } from './HeadlessToast.vue';
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <details class="group">
3
3
  <summary
4
- class="-ml-2 flex w-[max-content] cursor-pointer items-center rounded-lg py-2 pr-3 pl-1 hover:bg-gray-100 focus-visible:outline focus-visible:outline-gray-700"
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
5
  >
6
6
  <IconCheveronRight class="size-6 transition-transform group-open:rotate-90" />
7
7
  <span>{{ $td('ui.advancedOptions', 'Advanced options') }}</span>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <Modal :title>
2
+ <Modal :title="renderedTitle" :title-hidden="titleHidden">
3
3
  <Markdown :text="message" />
4
4
  </Modal>
5
5
  </template>
@@ -7,7 +7,11 @@
7
7
  <script setup lang="ts">
8
8
  import Modal from '@aerogel/core/components/ui/Modal.vue';
9
9
  import Markdown from '@aerogel/core/components/ui/Markdown.vue';
10
- import type { AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
10
+ import { useAlertModal } from '@aerogel/core/components/contracts/AlertModal';
11
+ import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
11
12
 
12
- defineProps<AlertModalProps>();
13
+ const props = defineProps<AlertModalProps>();
14
+ const { renderedTitle, titleHidden } = useAlertModal(props);
15
+
16
+ defineExpose<AlertModalExpose>();
13
17
  </script>
@@ -5,8 +5,10 @@
5
5
  </template>
6
6
 
7
7
  <script setup lang="ts">
8
+ import { computed } from 'vue';
9
+
8
10
  import HeadlessButton from '@aerogel/core/components/headless/HeadlessButton.vue';
9
- import { computedVariantClasses } from '@aerogel/core/utils/classes';
11
+ import { variantClasses } from '@aerogel/core/utils/classes';
10
12
  import type { ButtonProps } from '@aerogel/core/components/contracts/Button';
11
13
  import type { Variants } from '@aerogel/core/utils/classes';
12
14
 
@@ -14,10 +16,10 @@ const { class: baseClasses, size, variant, disabled, ...props } = defineProps<Bu
14
16
 
15
17
  /* eslint-disable vue/max-len */
16
18
  // prettier-ignore
17
- const renderedClasses = computedVariantClasses<Variants<Pick<ButtonProps, 'size' | 'variant' | 'disabled'>>>(
19
+ const renderedClasses = computed(() => variantClasses<Variants<Pick<ButtonProps, 'size' | 'variant' | 'disabled'>>>(
18
20
  { baseClasses, variant, size, disabled },
19
21
  {
20
- baseClasses: 'flex items-center justify-center gap-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
22
+ baseClasses: 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
21
23
  variants: {
22
24
  variant: {
23
25
  default: 'bg-primary-600 text-white focus-visible:outline-primary-600',
@@ -25,12 +27,12 @@ const renderedClasses = computedVariantClasses<Variants<Pick<ButtonProps, 'size'
25
27
  danger: 'bg-red-600 text-white focus-visible:outline-red-600',
26
28
  ghost: 'bg-transparent',
27
29
  outline: 'bg-transparent text-primary-600 ring-primary-600',
28
- link: 'text-primary-600',
30
+ link: 'text-links',
29
31
  },
30
32
  size: {
31
- small: 'text-xs',
32
- default: 'text-sm',
33
- large: 'text-base',
33
+ small: 'text-xs min-h-6',
34
+ default: 'text-sm min-h-8',
35
+ large: 'text-base min-h-10',
34
36
  icon: 'rounded-full p-2.5',
35
37
  },
36
38
  disabled: {
@@ -41,7 +43,7 @@ const renderedClasses = computedVariantClasses<Variants<Pick<ButtonProps, 'size'
41
43
  compoundVariants: [
42
44
  {
43
45
  variant: ['default', 'secondary', 'danger', 'ghost', 'outline'],
44
- class: 'font-medium',
46
+ class: 'flex items-center justify-center gap-1 font-medium',
45
47
  },
46
48
  {
47
49
  variant: ['default', 'danger'],
@@ -79,13 +81,28 @@ const renderedClasses = computedVariantClasses<Variants<Pick<ButtonProps, 'size'
79
81
  {
80
82
  variant: 'danger',
81
83
  disabled: false,
82
- class: 'hover:bg-red-50',
84
+ class: 'hover:bg-red-500',
83
85
  },
84
86
  {
85
87
  variant: 'link',
86
88
  disabled: false,
87
89
  class: 'hover:underline',
88
90
  },
91
+ {
92
+ variant: 'link',
93
+ size: 'small',
94
+ class: 'leading-6',
95
+ },
96
+ {
97
+ variant: 'link',
98
+ size: 'default',
99
+ class: 'leading-8',
100
+ },
101
+ {
102
+ variant: 'link',
103
+ size: 'large',
104
+ class: 'leading-10',
105
+ },
89
106
  ],
90
107
  defaultVariants: {
91
108
  variant: 'default',
@@ -93,6 +110,6 @@ const renderedClasses = computedVariantClasses<Variants<Pick<ButtonProps, 'size'
93
110
  disabled: false,
94
111
  },
95
112
  },
96
- );
113
+ ));
97
114
  /* eslint-enable vue/max-len */
98
115
  </script>
@@ -1,5 +1,11 @@
1
1
  <template>
2
- <Modal v-slot="{ close }" :title persistent>
2
+ <!-- @vue-generic {import('@aerogel/core/ui/UI').ModalExposeResult<ConfirmModalExpose>} -->
3
+ <Modal
4
+ v-slot="{ close }"
5
+ :title="renderedTitle"
6
+ :title-hidden="titleHidden"
7
+ persistent
8
+ >
3
9
  <Form :form @submit="close([true, form.data()])">
4
10
  <Markdown :text="message" :actions />
5
11
 
@@ -35,8 +41,10 @@ import Markdown from '@aerogel/core/components/ui/Markdown.vue';
35
41
  import Button from '@aerogel/core/components/ui/Button.vue';
36
42
  import Modal from '@aerogel/core/components/ui/Modal.vue';
37
43
  import { useConfirmModal } from '@aerogel/core/components/contracts/ConfirmModal';
38
- import type { ConfirmModalProps } from '@aerogel/core/components/contracts/ConfirmModal';
44
+ import type { ConfirmModalExpose, ConfirmModalProps } from '@aerogel/core/components/contracts/ConfirmModal';
39
45
 
40
46
  const { cancelVariant = 'secondary', ...props } = defineProps<ConfirmModalProps>();
41
- const { form, renderedAcceptText, renderedCancelText } = useConfirmModal(props);
47
+ const { form, renderedTitle, titleHidden, renderedAcceptText, renderedCancelText } = useConfirmModal(props);
48
+
49
+ defineExpose<ConfirmModalExpose>();
42
50
  </script>
@@ -1,14 +1,22 @@
1
1
  <template>
2
- <DropdownMenuItem
3
- class="flex w-full items-center gap-2 rounded-lg px-2 py-2 text-sm text-gray-900 data-[highlighted]:bg-gray-100"
4
- @select="$emit('select')"
5
- >
2
+ <DropdownMenuItem :class="renderedClasses" v-bind="props" @select="$emit('select')">
6
3
  <slot />
7
4
  </DropdownMenuItem>
8
5
  </template>
9
6
 
10
7
  <script setup lang="ts">
8
+ import { classes } from '@aerogel/core/utils';
9
+ import { computed } from 'vue';
11
10
  import { DropdownMenuItem } from 'reka-ui';
11
+ import type { HTMLAttributes } from 'vue';
12
+ import type { PrimitiveProps } from 'reka-ui';
12
13
 
13
14
  defineEmits<{ select: [] }>();
15
+
16
+ const { class: rootClass, ...props } = defineProps<{ class?: HTMLAttributes['class'] } & PrimitiveProps>();
17
+ const renderedClasses = computed(() =>
18
+ classes(
19
+ 'flex w-full items-center gap-2 rounded-lg px-2 py-2 text-sm text-gray-900 data-[highlighted]:bg-gray-100',
20
+ rootClass,
21
+ ));
14
22
  </script>
@@ -5,7 +5,23 @@
5
5
  :side="dropdownMenu.side"
6
6
  >
7
7
  <slot>
8
- <DropdownMenuOption v-for="(option, key) in dropdownMenu.options" :key @select="option.click">
8
+ <DropdownMenuOption
9
+ v-for="(option, key) in dropdownMenu.options"
10
+ :key
11
+ :as="option.route || option.href ? HeadlessButton : undefined"
12
+ :class="option.class"
13
+ v-bind="
14
+ option.route || option.href
15
+ ? {
16
+ href: option.href,
17
+ route: option.route,
18
+ routeParams: option.routeParams,
19
+ routeQuery: option.routeQuery,
20
+ }
21
+ : {}
22
+ "
23
+ @select="option.click?.()"
24
+ >
9
25
  {{ option.label }}
10
26
  </DropdownMenuOption>
11
27
  </slot>
@@ -19,6 +35,7 @@ import { injectReactiveOrFail } from '@aerogel/core/utils';
19
35
  import type { DropdownMenuExpose } from '@aerogel/core/components/contracts/DropdownMenu';
20
36
 
21
37
  import DropdownMenuOption from './DropdownMenuOption.vue';
38
+ import HeadlessButton from '../headless/HeadlessButton.vue';
22
39
 
23
40
  const dropdownMenu = injectReactiveOrFail<DropdownMenuExpose>(
24
41
  'dropdown-menu',
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <Button
3
+ v-if="$errors.logs.length > 0"
4
+ size="icon"
5
+ variant="ghost"
6
+ :title="$td('errors.viewLogs', 'View error logs')"
7
+ :aria-label="$td('errors.viewLogs', 'View error logs')"
8
+ @click="$ui.modal(ErrorLogsModal)"
9
+ >
10
+ <IconWarning class="size-6 text-red-500" />
11
+ </Button>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import IconWarning from '~icons/ion/warning';
16
+
17
+ import Button from '@aerogel/core/components/ui/Button.vue';
18
+ import ErrorLogsModal from '@aerogel/core/components/ui/ErrorLogsModal.vue';
19
+ </script>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <Modal :title="$td('errors.report', 'Errors ({count})', { count: $errors.logs.length })">
3
+ <ol>
4
+ <li
5
+ v-for="(log, index) of $errors.logs"
6
+ :key="index"
7
+ class="mb-2 flex max-w-prose min-w-56 justify-between py-2 last:mb-0"
8
+ >
9
+ <div>
10
+ <h3 class="font-medium">
11
+ {{ log.report.title }}
12
+ </h3>
13
+ <time :datetime="log.date.toISOString()" class="text-xs text-gray-700">
14
+ {{ log.date.toLocaleTimeString() }}
15
+ </time>
16
+ <Markdown
17
+ class="text-sm text-gray-500"
18
+ :text="log.report.description ?? getErrorMessage(log.report)"
19
+ />
20
+ </div>
21
+ <Button
22
+ size="icon"
23
+ variant="ghost"
24
+ :aria-label="$td('errors.viewDetails', 'View details')"
25
+ :title="$td('errors.viewDetails', 'View details')"
26
+ class="self-center"
27
+ @click="
28
+ $errors.inspect(
29
+ log.report,
30
+ $errors.logs.map(({ report }) => report)
31
+ )
32
+ "
33
+ >
34
+ <IconViewShow class="size-4" aria-hidden="true" />
35
+ </Button>
36
+ </li>
37
+ </ol>
38
+ </Modal>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import IconViewShow from '~icons/zondicons/view-show';
43
+
44
+ import Button from '@aerogel/core/components/ui/Button.vue';
45
+ import Modal from '@aerogel/core/components/ui/Modal.vue';
46
+ import Markdown from '@aerogel/core/components/ui/Markdown.vue';
47
+ import { getErrorMessage } from '@aerogel/core/errors';
48
+ </script>
@@ -1,12 +1,18 @@
1
1
  <template>
2
- <Modal wrapper-class="p-0 sm:w-auto sm:min-w-lg sm:max-w-[80vw]">
2
+ <Modal
3
+ :title="$td('errors.report', 'Error report')"
4
+ title-hidden
5
+ close-hidden
6
+ class="p-0"
7
+ wrapper-class="sm:w-auto sm:min-w-lg sm:max-w-[80vw]"
8
+ >
3
9
  <div class="px-4 pt-5 pb-4">
4
10
  <h2 class="flex justify-between gap-4">
5
11
  <div class="flex items-center gap-2">
6
12
  <IconExclamationSolid class="size-5 text-red-600" />
7
13
  <ErrorReportModalTitle
8
14
  class="text-lg leading-6 font-semibold text-gray-900"
9
- :report="report"
15
+ :report="activeReport"
10
16
  :current-report="activeReportIndex + 1"
11
17
  :total-reports="reports.length"
12
18
  />
@@ -33,11 +39,11 @@
33
39
  </Button>
34
40
  </span>
35
41
  </div>
36
- <ErrorReportModalButtons :report class="gap-0.5" />
42
+ <ErrorReportModalButtons :report="activeReport" class="gap-0.5" />
37
43
  </h2>
38
- <Markdown v-if="report.description" :text="report.description" class="text-gray-600" />
44
+ <Markdown v-if="activeReport.description" :text="activeReport.description" class="text-gray-600" />
39
45
  </div>
40
- <div class="-mt-2 max-h-[80vh] overflow-auto bg-red-800/10">
46
+ <div class="-mt-2 max-h-[75vh] overflow-auto bg-red-800/10">
41
47
  <pre class="p-4 text-xs text-red-800" v-text="details" />
42
48
  </div>
43
49
  </Modal>
@@ -54,9 +60,14 @@ import ErrorReportModalButtons from '@aerogel/core/components/ui/ErrorReportModa
54
60
  import ErrorReportModalTitle from '@aerogel/core/components/ui/ErrorReportModalTitle.vue';
55
61
  import Modal from '@aerogel/core/components/ui/Modal.vue';
56
62
  import { useErrorReportModal } from '@aerogel/core/components/contracts/ErrorReportModal';
57
- import type { ErrorReportModalProps } from '@aerogel/core/components/contracts/ErrorReportModal';
63
+ import type {
64
+ ErrorReportModalExpose,
65
+ ErrorReportModalProps,
66
+ } from '@aerogel/core/components/contracts/ErrorReportModal';
58
67
 
59
68
  const props = defineProps<ErrorReportModalProps>();
60
69
 
61
- const { activeReportIndex, details, nextReportText, previousReportText, report } = useErrorReportModal(props);
70
+ const { activeReportIndex, details, nextReportText, previousReportText, activeReport } = useErrorReportModal(props);
71
+
72
+ defineExpose<ErrorReportModalExpose>();
62
73
  </script>
@@ -48,8 +48,8 @@ const renderedInputClasses = computed(() =>
48
48
  'block w-full rounded-md border-0 py-1.5 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6',
49
49
  {
50
50
  'focus:ring-primary-600': !$input.value?.errors,
51
- 'text-gray-900 shadow-2xs ring-gray-300 placeholder:text-gray-400': !$input.value?.errors,
52
- 'pr-10 text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500': $input.value?.errors,
51
+ 'text-gray-900 shadow-2xs ring-gray-900/10 placeholder:text-gray-400': !$input.value?.errors,
52
+ 'pr-10 text-red-900 ring-red-900/10 placeholder:text-red-300 focus:ring-red-500': $input.value?.errors,
53
53
  },
54
54
  inputClass,
55
55
  ));
@@ -25,8 +25,10 @@ import Markdown from '@aerogel/core/components/ui/Markdown.vue';
25
25
  import Modal from '@aerogel/core/components/ui/Modal.vue';
26
26
  import ProgressBar from '@aerogel/core/components/ui/ProgressBar.vue';
27
27
  import { useLoadingModal } from '@aerogel/core/components/contracts/LoadingModal';
28
- import type { LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
28
+ import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
29
29
 
30
30
  const props = defineProps<LoadingModalProps>();
31
31
  const { renderedTitle, renderedMessage, titleHidden, showProgress } = useLoadingModal(props);
32
+
33
+ defineExpose<LoadingModalExpose>();
32
34
  </script>
@@ -7,7 +7,7 @@ import { computed, h, useAttrs } from 'vue';
7
7
  import { isInstanceOf } from '@noeldemartin/utils';
8
8
  import type { VNode } from 'vue';
9
9
 
10
- import { renderMarkdown } from '@aerogel/core/utils/markdown';
10
+ import { getMarkdownRouter, renderMarkdown } from '@aerogel/core/utils/markdown';
11
11
  import { translate, translateWithDefault } from '@aerogel/core/lang';
12
12
  import { renderVNode } from '@aerogel/core/utils/vue';
13
13
 
@@ -65,5 +65,33 @@ async function onClick(event: Event) {
65
65
 
66
66
  return;
67
67
  }
68
+
69
+ if (isInstanceOf(target, HTMLAnchorElement) && target.dataset.markdownRoute) {
70
+ const router = getMarkdownRouter();
71
+
72
+ if (router) {
73
+ event.preventDefault();
74
+
75
+ router.visit(target.dataset.markdownRoute);
76
+ }
77
+
78
+ return;
79
+ }
68
80
  }
69
81
  </script>
82
+
83
+ <style scoped>
84
+ /* @apply .text-links .font-normal .no-underline .hover:underline; */
85
+ * :deep(a) {
86
+ --tw-font-weight: var(--font-weight-normal);
87
+ text-decoration-line: none;
88
+ color: var(--color-links);
89
+ font-weight: var(--font-weight-normal);
90
+ }
91
+
92
+ @media (hover: hover) {
93
+ * :deep(a:hover) {
94
+ text-decoration-line: underline;
95
+ }
96
+ }
97
+ </style>
@@ -1,29 +1,48 @@
1
1
  <template>
2
+ <!-- @vue-generic {T} -->
2
3
  <HeadlessModal
3
4
  v-slot="{ close }"
4
5
  v-bind="props"
5
6
  :ref="($modal) => forwardRef($modal as HeadlessModalInstance)"
6
7
  :persistent
7
8
  >
8
- <HeadlessModalOverlay class="fixed inset-0 bg-black/30" />
9
+ <HeadlessModalOverlay
10
+ class="fixed inset-0 animate-[fade-in_var(--tw-duration)_ease-in-out] transition-opacity duration-300 will-change-[opacity]"
11
+ :class="{
12
+ 'bg-black/30': context.childIndex === 1,
13
+ 'opacity-0': context.childIndex === 1 && context.modal.closing,
14
+ }"
15
+ />
9
16
  <HeadlessModalContent v-bind="contentProps" :class="renderedWrapperClass">
10
- <div v-if="!persistent && dismissable" class="absolute top-0 right-0 hidden pt-1.5 pr-1.5 sm:block">
11
- <Button variant="ghost" size="icon" @click="close()">
17
+ <div v-if="!persistent && !closeHidden" class="absolute top-0 right-0 hidden pt-3.5 pr-2.5 sm:block">
18
+ <button
19
+ type="button"
20
+ class="clickable z-10 rounded-full p-2.5 text-gray-400 hover:text-gray-500"
21
+ @click="close()"
22
+ >
12
23
  <span class="sr-only">{{ $td('ui.close', 'Close') }}</span>
13
- <IconClose class="size-3 text-gray-400" />
14
- </Button>
24
+ <IconClose class="size-4" />
25
+ </button>
15
26
  </div>
16
27
 
17
28
  <HeadlessModalTitle
18
29
  v-if="title"
19
- class="text-base font-semibold text-gray-900"
20
- :class="{ 'sr-only': titleHidden }"
30
+ class="px-4 pt-5 text-base font-semibold text-gray-900"
31
+ :class="{
32
+ 'sr-only': titleHidden,
33
+ 'pb-0': description && !descriptionHidden,
34
+ 'pb-2': !description || descriptionHidden,
35
+ }"
21
36
  >
22
37
  <Markdown :text="title" inline />
23
38
  </HeadlessModalTitle>
24
39
 
25
- <HeadlessModalDescription v-if="description" :class="{ 'sr-only': descriptionHidden }">
26
- <Markdown :text="description" class="mt-1 text-sm leading-6 text-gray-500" />
40
+ <HeadlessModalDescription
41
+ v-if="description"
42
+ class="px-4 pt-1 pb-2"
43
+ :class="{ 'sr-only': descriptionHidden }"
44
+ >
45
+ <Markdown :text="description" class="text-sm leading-6 text-gray-500" />
27
46
  </HeadlessModalDescription>
28
47
 
29
48
  <div :class="renderedContentClass">
@@ -33,59 +52,80 @@
33
52
  </HeadlessModal>
34
53
  </template>
35
54
 
36
- <script setup lang="ts">
55
+ <script setup lang="ts" generic="T = void">
37
56
  import IconClose from '~icons/zondicons/close';
38
57
 
58
+ import { after } from '@noeldemartin/utils';
39
59
  import { computed } from 'vue';
40
60
  import { useForwardExpose } from 'reka-ui';
41
- import type { HTMLAttributes, Ref } from 'vue';
61
+ import type { ComponentPublicInstance, HTMLAttributes, Ref } from 'vue';
42
62
  import type { Nullable } from '@noeldemartin/utils';
43
63
 
44
64
  import Markdown from '@aerogel/core/components/ui/Markdown.vue';
45
- import Button from '@aerogel/core/components/ui/Button.vue';
46
65
  import HeadlessModal from '@aerogel/core/components/headless/HeadlessModal.vue';
47
66
  import HeadlessModalContent from '@aerogel/core/components/headless/HeadlessModalContent.vue';
48
67
  import HeadlessModalDescription from '@aerogel/core/components/headless/HeadlessModalDescription.vue';
49
68
  import HeadlessModalOverlay from '@aerogel/core/components/headless/HeadlessModalOverlay.vue';
50
69
  import HeadlessModalTitle from '@aerogel/core/components/headless/HeadlessModalTitle.vue';
70
+ import UI from '@aerogel/core/ui/UI';
51
71
  import { classes } from '@aerogel/core/utils/classes';
72
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
73
+ import { useEvent } from '@aerogel/core/utils/composition/events';
74
+ import type { AcceptRefs } from '@aerogel/core/utils/vue';
52
75
  import type { ModalExpose, ModalProps, ModalSlots } from '@aerogel/core/components/contracts/Modal';
53
- import type { AcceptRefs } from '@aerogel/core/utils';
76
+ import type { UIModalContext } from '@aerogel/core/ui/UI';
54
77
 
55
- type HeadlessModalInstance = InstanceType<typeof HeadlessModal>;
78
+ type HeadlessModalInstance = ComponentPublicInstance & ModalExpose<T>;
56
79
 
57
80
  const {
58
81
  class: contentClass = '',
59
- dismissable = true,
60
82
  wrapperClass = '',
61
83
  title,
62
84
  titleHidden,
63
85
  description,
64
86
  persistent,
87
+ closeHidden,
65
88
  ...props
66
89
  } = defineProps<
67
90
  ModalProps & {
68
- dismissable?: boolean;
69
91
  wrapperClass?: HTMLAttributes['class'];
70
92
  class?: HTMLAttributes['class'];
93
+ closeHidden?: boolean;
71
94
  }
72
95
  >();
73
96
 
74
- defineExpose<AcceptRefs<ModalExpose>>({
97
+ defineSlots<ModalSlots<T>>();
98
+ defineExpose<AcceptRefs<ModalExpose<T>>>({
75
99
  close: async (result) => $modal.value?.close(result),
76
100
  $content: computed(() => $modal.value?.$content),
77
101
  });
78
102
 
79
103
  const { forwardRef, currentRef } = useForwardExpose<HeadlessModalInstance>();
80
104
  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);
81
107
  const contentProps = computed(() => (description ? {} : { 'aria-describedby': undefined }));
82
- const renderedContentClass = computed(() => classes({ 'mt-2': title && !titleHidden }, contentClass));
108
+ const renderedContentClass = computed(() =>
109
+ classes('max-h-[90vh] overflow-auto px-4 pb-4', { 'pt-4': !title || titleHidden }, contentClass));
83
110
  const renderedWrapperClass = computed(() =>
84
111
  classes(
85
- // eslint-disable-next-line vue/max-len
86
- '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',
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
+ },
87
120
  wrapperClass,
88
121
  ));
89
122
 
90
- defineSlots<ModalSlots>();
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
+ });
91
131
  </script>