@aerogel/core 0.0.0-next.8ae083000611b11799d37033e9a5250d0d07c324 → 0.0.0-next.8bd66d5f5e264650120ea3cc37519f2409c6cc39

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 (45) hide show
  1. package/dist/aerogel-core.css +1 -1
  2. package/dist/aerogel-core.d.ts +250 -57
  3. package/dist/aerogel-core.js +1530 -1134
  4. package/dist/aerogel-core.js.map +1 -1
  5. package/package.json +2 -1
  6. package/src/components/contracts/AlertModal.ts +11 -0
  7. package/src/components/contracts/ConfirmModal.ts +3 -1
  8. package/src/components/contracts/DropdownMenu.ts +6 -1
  9. package/src/components/contracts/ErrorReportModal.ts +5 -4
  10. package/src/components/headless/HeadlessInputInput.vue +16 -5
  11. package/src/components/headless/HeadlessSwitch.vue +96 -0
  12. package/src/components/headless/index.ts +1 -0
  13. package/src/components/ui/AdvancedOptions.vue +1 -1
  14. package/src/components/ui/AlertModal.vue +5 -2
  15. package/src/components/ui/Button.vue +23 -6
  16. package/src/components/ui/ConfirmModal.vue +7 -2
  17. package/src/components/ui/DropdownMenuOption.vue +12 -4
  18. package/src/components/ui/DropdownMenuOptions.vue +18 -1
  19. package/src/components/ui/ErrorLogs.vue +19 -0
  20. package/src/components/ui/ErrorLogsModal.vue +48 -0
  21. package/src/components/ui/ErrorReportModal.vue +12 -6
  22. package/src/components/ui/Input.vue +2 -2
  23. package/src/components/ui/Markdown.vue +13 -1
  24. package/src/components/ui/Modal.vue +18 -9
  25. package/src/components/ui/SelectLabel.vue +5 -1
  26. package/src/components/ui/Setting.vue +31 -0
  27. package/src/components/ui/Switch.vue +11 -0
  28. package/src/components/ui/TextArea.vue +56 -0
  29. package/src/components/ui/Toast.vue +16 -14
  30. package/src/components/ui/index.ts +5 -0
  31. package/src/directives/measure.ts +11 -5
  32. package/src/errors/Errors.ts +17 -6
  33. package/src/errors/index.ts +6 -2
  34. package/src/errors/settings/Debug.vue +32 -0
  35. package/src/errors/settings/index.ts +10 -0
  36. package/src/forms/FormController.ts +4 -0
  37. package/src/index.css +3 -0
  38. package/src/lang/index.ts +1 -1
  39. package/src/lang/settings/Language.vue +1 -1
  40. package/src/services/index.ts +2 -2
  41. package/src/testing/index.ts +4 -0
  42. package/src/ui/UI.ts +20 -4
  43. package/src/utils/classes.ts +9 -17
  44. package/src/utils/markdown.ts +35 -1
  45. package/src/utils/vue.ts +6 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerogel/core",
3
- "version": "0.0.0-next.8ae083000611b11799d37033e9a5250d0d07c324",
3
+ "version": "0.0.0-next.8bd66d5f5e264650120ea3cc37519f2409c6cc39",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -33,6 +33,7 @@
33
33
  "class-variance-authority": "^0.7.1",
34
34
  "clsx": "^2.1.1",
35
35
  "dompurify": "^3.2.4",
36
+ "eruda": "^3.4.1",
36
37
  "marked": "^15.0.7",
37
38
  "pinia": "^2.1.6",
38
39
  "reka-ui": "^2.2.0",
@@ -1,3 +1,6 @@
1
+ import { computed } from 'vue';
2
+
3
+ import { translateWithDefault } from '@aerogel/core/lang';
1
4
  import type { ModalExpose } from '@aerogel/core/components/contracts/Modal';
2
5
 
3
6
  export interface AlertModalProps {
@@ -6,3 +9,11 @@ export interface AlertModalProps {
6
9
  }
7
10
 
8
11
  export interface AlertModalExpose extends ModalExpose<void> {}
12
+
13
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
14
+ export function useAlertModal(props: AlertModalProps) {
15
+ const renderedTitle = computed(() => props.title ?? translateWithDefault('ui.alert', 'Alert'));
16
+ const titleHidden = computed(() => !props.title);
17
+
18
+ return { renderedTitle, titleHidden };
19
+ }
@@ -39,8 +39,10 @@ export function useConfirmModal(props: ConfirmModalProps) {
39
39
  ),
40
40
  );
41
41
 
42
+ const renderedTitle = computed(() => props.title ?? translateWithDefault('ui.confirm', 'Confirm'));
43
+ const titleHidden = computed(() => !props.title);
42
44
  const renderedAcceptText = computed(() => props.acceptText ?? translateWithDefault('ui.accept', 'Ok'));
43
45
  const renderedCancelText = computed(() => props.cancelText ?? translateWithDefault('ui.cancel', 'Cancel'));
44
46
 
45
- return { form, renderedAcceptText, renderedCancelText };
47
+ return { form, renderedTitle, titleHidden, renderedAcceptText, renderedCancelText };
46
48
  }
@@ -4,7 +4,12 @@ import type { Falsifiable } from '@aerogel/core/utils/types';
4
4
 
5
5
  export type DropdownMenuOptionData = {
6
6
  label: string;
7
- click: () => unknown;
7
+ href?: string;
8
+ route?: string;
9
+ routeParams?: object;
10
+ routeQuery?: object;
11
+ click?: () => unknown;
12
+ class?: string;
8
13
  };
9
14
 
10
15
  export interface DropdownMenuProps {
@@ -5,6 +5,7 @@ import type { ErrorReport } from '@aerogel/core/errors';
5
5
  import type { ModalExpose } from '@aerogel/core/components/contracts/Modal';
6
6
 
7
7
  export interface ErrorReportModalProps {
8
+ report: ErrorReport;
8
9
  reports: ErrorReport[];
9
10
  }
10
11
 
@@ -12,11 +13,11 @@ export interface ErrorReportModalExpose extends ModalExpose {}
12
13
 
13
14
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
14
15
  export function useErrorReportModal(props: ErrorReportModalProps) {
15
- const activeReportIndex = ref(0);
16
- const report = computed(() => props.reports[activeReportIndex.value] as ErrorReport);
16
+ const activeReportIndex = ref(props.reports.includes(props.report) ? props.reports.indexOf(props.report) : 0);
17
+ const activeReport = computed(() => props.reports[activeReportIndex.value] as ErrorReport);
17
18
  const details = computed(
18
19
  () =>
19
- report.value.details?.trim() ||
20
+ activeReport.value.details?.trim() ||
20
21
  translateWithDefault('errors.detailsEmpty', 'This error is missing a stacktrace.'),
21
22
  );
22
23
  const previousReportText = translateWithDefault('errors.previousReport', 'Show previous report');
@@ -27,6 +28,6 @@ export function useErrorReportModal(props: ErrorReportModalProps) {
27
28
  details,
28
29
  nextReportText,
29
30
  previousReportText,
30
- report,
31
+ activeReport,
31
32
  };
32
33
  }
@@ -3,8 +3,8 @@
3
3
  :id="input.id"
4
4
  ref="$inputRef"
5
5
  :name
6
- :type
7
6
  :checked
7
+ :type="renderedType"
8
8
  :required="input.required ?? undefined"
9
9
  :aria-invalid="input.errors ? 'true' : 'false'"
10
10
  :aria-describedby="
@@ -15,18 +15,29 @@
15
15
  </template>
16
16
 
17
17
  <script setup lang="ts">
18
- import { computed, useTemplateRef, watchEffect } from 'vue';
18
+ 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 type FormController from '@aerogel/core/forms/FormController';
22
23
  import type { FormFieldValue } from '@aerogel/core/forms/FormController';
23
24
  import type { InputExpose } from '@aerogel/core/components/contracts/Input';
24
25
 
25
- const { type = 'text' } = defineProps<{ type?: string }>();
26
+ const { type } = defineProps<{ type?: string }>();
26
27
  const $input = useTemplateRef('$inputRef');
27
28
  const input = injectReactiveOrFail<InputExpose>('input', '<HeadlessInputInput> must be a child of a <HeadlessInput>');
29
+ const form = inject<FormController | null>('form', null);
28
30
  const name = computed(() => input.name ?? undefined);
29
31
  const value = computed(() => input.value);
32
+ const renderedType = computed(() => {
33
+ if (type) {
34
+ return type;
35
+ }
36
+
37
+ const fieldType = (name.value && form?.getFieldType(name.value)) ?? '';
38
+
39
+ return ['text', 'email', 'number', 'tel', 'url'].includes(fieldType) ? fieldType : 'text';
40
+ });
30
41
  const checked = computed(() => {
31
42
  if (type !== 'checkbox') {
32
43
  return;
@@ -64,8 +75,8 @@ watchEffect(() => {
64
75
  return;
65
76
  }
66
77
 
67
- if (type === 'date') {
68
- $input.value.valueAsDate = value.value as Date;
78
+ if (type === 'date' && value.value instanceof Date) {
79
+ $input.value.valueAsDate = value.value;
69
80
 
70
81
  return;
71
82
  }
@@ -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,8 +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 { useAlertModal } from '@aerogel/core/components/contracts/AlertModal';
10
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
+
13
16
  defineExpose<AlertModalExpose>();
14
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,7 +16,7 @@ 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
22
  baseClasses: 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
@@ -28,9 +30,9 @@ const renderedClasses = computedVariantClasses<Variants<Pick<ButtonProps, 'size'
28
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: {
@@ -86,6 +88,21 @@ const renderedClasses = computedVariantClasses<Variants<Pick<ButtonProps, 'size'
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,6 +1,11 @@
1
1
  <template>
2
2
  <!-- @vue-generic {import('@aerogel/core/ui/UI').ModalExposeResult<ConfirmModalExpose>} -->
3
- <Modal v-slot="{ close }" :title persistent>
3
+ <Modal
4
+ v-slot="{ close }"
5
+ :title="renderedTitle"
6
+ :title-hidden="titleHidden"
7
+ persistent
8
+ >
4
9
  <Form :form @submit="close([true, form.data()])">
5
10
  <Markdown :text="message" :actions />
6
11
 
@@ -39,7 +44,7 @@ import { useConfirmModal } from '@aerogel/core/components/contracts/ConfirmModal
39
44
  import type { ConfirmModalExpose, ConfirmModalProps } from '@aerogel/core/components/contracts/ConfirmModal';
40
45
 
41
46
  const { cancelVariant = 'secondary', ...props } = defineProps<ConfirmModalProps>();
42
- const { form, renderedAcceptText, renderedCancelText } = useConfirmModal(props);
47
+ const { form, renderedTitle, titleHidden, renderedAcceptText, renderedCancelText } = useConfirmModal(props);
43
48
 
44
49
  defineExpose<ConfirmModalExpose>();
45
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>
@@ -61,7 +67,7 @@ import type {
61
67
 
62
68
  const props = defineProps<ErrorReportModalProps>();
63
69
 
64
- const { activeReportIndex, details, nextReportText, previousReportText, report } = useErrorReportModal(props);
70
+ const { activeReportIndex, details, nextReportText, previousReportText, activeReport } = useErrorReportModal(props);
65
71
 
66
72
  defineExpose<ErrorReportModalExpose>();
67
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
  ));
@@ -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,6 +65,18 @@ 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>
70
82
 
@@ -14,7 +14,7 @@
14
14
  }"
15
15
  />
16
16
  <HeadlessModalContent v-bind="contentProps" :class="renderedWrapperClass">
17
- <div v-if="!persistent && dismissable" class="absolute top-0 right-0 hidden pt-3.5 pr-2.5 sm:block">
17
+ <div v-if="!persistent && !closeHidden" class="absolute top-0 right-0 hidden pt-3.5 pr-2.5 sm:block">
18
18
  <button
19
19
  type="button"
20
20
  class="clickable z-10 rounded-full p-2.5 text-gray-400 hover:text-gray-500"
@@ -27,14 +27,22 @@
27
27
 
28
28
  <HeadlessModalTitle
29
29
  v-if="title"
30
- class="text-base font-semibold text-gray-900"
31
- :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
+ }"
32
36
  >
33
37
  <Markdown :text="title" inline />
34
38
  </HeadlessModalTitle>
35
39
 
36
- <HeadlessModalDescription v-if="description" :class="{ 'sr-only': descriptionHidden }">
37
- <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" />
38
46
  </HeadlessModalDescription>
39
47
 
40
48
  <div :class="renderedContentClass">
@@ -71,18 +79,18 @@ type HeadlessModalInstance = ComponentPublicInstance & ModalExpose<T>;
71
79
 
72
80
  const {
73
81
  class: contentClass = '',
74
- dismissable = true,
75
82
  wrapperClass = '',
76
83
  title,
77
84
  titleHidden,
78
85
  description,
79
86
  persistent,
87
+ closeHidden,
80
88
  ...props
81
89
  } = defineProps<
82
90
  ModalProps & {
83
- dismissable?: boolean;
84
91
  wrapperClass?: HTMLAttributes['class'];
85
92
  class?: HTMLAttributes['class'];
93
+ closeHidden?: boolean;
86
94
  }
87
95
  >();
88
96
 
@@ -97,11 +105,12 @@ const $modal = currentRef as Ref<Nullable<HeadlessModalInstance>>;
97
105
  const context = injectReactiveOrFail<UIModalContext>('modal');
98
106
  const inForeground = computed(() => !context.modal.closing && context.childIndex === UI.openModals.length);
99
107
  const contentProps = computed(() => (description ? {} : { 'aria-describedby': undefined }));
100
- 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));
101
110
  const renderedWrapperClass = computed(() =>
102
111
  classes(
103
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',
104
- 'overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl sm:max-w-lg',
113
+ 'overflow-hidden rounded-lg bg-white text-left shadow-xl sm:max-w-lg',
105
114
  'animate-[fade-in_var(--tw-duration)_ease-in-out,grow_var(--tw-duration)_ease-in-out]',
106
115
  'transition-[scale,opacity] will-change-[scale,opacity] duration-300',
107
116
  {