@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.
- package/dist/aerogel-core.css +1 -1
- package/dist/aerogel-core.d.ts +250 -57
- package/dist/aerogel-core.js +1530 -1134
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +2 -1
- package/src/components/contracts/AlertModal.ts +11 -0
- package/src/components/contracts/ConfirmModal.ts +3 -1
- package/src/components/contracts/DropdownMenu.ts +6 -1
- package/src/components/contracts/ErrorReportModal.ts +5 -4
- package/src/components/headless/HeadlessInputInput.vue +16 -5
- package/src/components/headless/HeadlessSwitch.vue +96 -0
- package/src/components/headless/index.ts +1 -0
- package/src/components/ui/AdvancedOptions.vue +1 -1
- package/src/components/ui/AlertModal.vue +5 -2
- package/src/components/ui/Button.vue +23 -6
- package/src/components/ui/ConfirmModal.vue +7 -2
- package/src/components/ui/DropdownMenuOption.vue +12 -4
- package/src/components/ui/DropdownMenuOptions.vue +18 -1
- package/src/components/ui/ErrorLogs.vue +19 -0
- package/src/components/ui/ErrorLogsModal.vue +48 -0
- package/src/components/ui/ErrorReportModal.vue +12 -6
- package/src/components/ui/Input.vue +2 -2
- package/src/components/ui/Markdown.vue +13 -1
- package/src/components/ui/Modal.vue +18 -9
- package/src/components/ui/SelectLabel.vue +5 -1
- package/src/components/ui/Setting.vue +31 -0
- package/src/components/ui/Switch.vue +11 -0
- package/src/components/ui/TextArea.vue +56 -0
- package/src/components/ui/Toast.vue +16 -14
- package/src/components/ui/index.ts +5 -0
- package/src/directives/measure.ts +11 -5
- package/src/errors/Errors.ts +17 -6
- package/src/errors/index.ts +6 -2
- package/src/errors/settings/Debug.vue +32 -0
- package/src/errors/settings/index.ts +10 -0
- package/src/forms/FormController.ts +4 -0
- package/src/index.css +3 -0
- package/src/lang/index.ts +1 -1
- package/src/lang/settings/Language.vue +1 -1
- package/src/services/index.ts +2 -2
- package/src/testing/index.ts +4 -0
- package/src/ui/UI.ts +20 -4
- package/src/utils/classes.ts +9 -17
- package/src/utils/markdown.ts +35 -1
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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]
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
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="
|
|
44
|
+
<Markdown v-if="activeReport.description" :text="activeReport.description" class="text-gray-600" />
|
|
39
45
|
</div>
|
|
40
|
-
<div class="-mt-2 max-h-[
|
|
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,
|
|
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-
|
|
52
|
-
'pr-10 text-red-900 ring-red-
|
|
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 &&
|
|
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="{
|
|
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
|
|
37
|
-
|
|
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(() =>
|
|
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
|
|
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
|
{
|