@aerogel/core 0.0.0-next.e4c0d5bd2801fbe93545477ea515af53df69b522 → 0.0.0-next.eb6fcafb87cdccbc72933f616799ca3124d1eca8
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.d.ts +257 -83
- package/dist/aerogel-core.js +1607 -1230
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +2 -1
- package/src/components/contracts/AlertModal.ts +1 -1
- package/src/components/contracts/DropdownMenu.ts +2 -2
- package/src/components/contracts/Select.ts +2 -2
- 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/Button.vue +15 -0
- package/src/components/ui/Input.vue +2 -2
- package/src/components/ui/Modal.vue +12 -4
- package/src/components/ui/SelectLabel.vue +5 -1
- package/src/components/ui/Setting.vue +31 -0
- package/src/components/ui/StartupCrash.vue +51 -6
- package/src/components/ui/Switch.vue +11 -0
- package/src/components/ui/TextArea.vue +56 -0
- package/src/components/ui/index.ts +3 -0
- package/src/errors/Errors.state.ts +1 -0
- package/src/errors/Errors.ts +27 -6
- package/src/errors/index.ts +6 -2
- package/src/errors/settings/Debug.vue +14 -0
- package/src/errors/settings/index.ts +10 -0
- package/src/forms/FormController.test.ts +3 -0
- package/src/forms/FormController.ts +25 -16
- package/src/forms/utils.ts +25 -0
- package/src/forms/validation.ts +31 -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/Service.ts +11 -6
- package/src/services/index.ts +2 -2
- package/src/testing/index.ts +4 -0
- package/src/ui/UI.ts +20 -4
- package/src/utils/app.ts +7 -0
- package/src/utils/index.ts +1 -0
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.eb6fcafb87cdccbc72933f616799ca3124d1eca8",
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { computed } from 'vue';
|
|
2
2
|
|
|
3
|
-
import { translateWithDefault } from '@aerogel/core/lang';
|
|
3
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
4
4
|
import type { ModalExpose } from '@aerogel/core/components/contracts/Modal';
|
|
5
5
|
|
|
6
6
|
export interface AlertModalProps {
|
|
@@ -15,11 +15,11 @@ export type DropdownMenuOptionData = {
|
|
|
15
15
|
export interface DropdownMenuProps {
|
|
16
16
|
align?: DropdownMenuContentProps['align'];
|
|
17
17
|
side?: DropdownMenuContentProps['side'];
|
|
18
|
-
options?: Falsifiable<DropdownMenuOptionData>[];
|
|
18
|
+
options?: readonly Falsifiable<DropdownMenuOptionData>[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface DropdownMenuExpose {
|
|
22
22
|
align?: DropdownMenuContentProps['align'];
|
|
23
23
|
side?: DropdownMenuContentProps['side'];
|
|
24
|
-
options?: DropdownMenuOptionData[];
|
|
24
|
+
options?: readonly DropdownMenuOptionData[];
|
|
25
25
|
}
|
|
@@ -18,7 +18,7 @@ export interface HasSelectOptionLabel {
|
|
|
18
18
|
|
|
19
19
|
export interface SelectProps<T extends Nullable<FormFieldValue> = Nullable<FormFieldValue>> extends InputProps<T> {
|
|
20
20
|
as?: AsTag | Component;
|
|
21
|
-
options?: T[];
|
|
21
|
+
options?: readonly T[];
|
|
22
22
|
placeholder?: string;
|
|
23
23
|
renderOption?: (option: T) => string;
|
|
24
24
|
compareOptions?: (a: T, b: T) => boolean;
|
|
@@ -31,7 +31,7 @@ export interface SelectProps<T extends Nullable<FormFieldValue> = Nullable<FormF
|
|
|
31
31
|
export interface SelectEmits<T extends Nullable<FormFieldValue> = Nullable<FormFieldValue>> extends InputEmits<T> {}
|
|
32
32
|
|
|
33
33
|
export interface SelectExpose<T extends Nullable<FormFieldValue> = Nullable<FormFieldValue>> extends InputExpose<T> {
|
|
34
|
-
options: ComputedRef<Nullable<SelectOptionData[]>>;
|
|
34
|
+
options: ComputedRef<Nullable<readonly SelectOptionData[]>>;
|
|
35
35
|
selectedOption: ComputedRef<Nullable<SelectOptionData>>;
|
|
36
36
|
placeholder: ComputedRef<string>;
|
|
37
37
|
labelClass?: HTMLAttributes['class'];
|
|
@@ -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';
|
|
@@ -88,6 +88,21 @@ const renderedClasses = computed(() => variantClasses<Variants<Pick<ButtonProps,
|
|
|
88
88
|
disabled: false,
|
|
89
89
|
class: 'hover:underline',
|
|
90
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
|
+
},
|
|
91
106
|
],
|
|
92
107
|
defaultVariants: {
|
|
93
108
|
variant: 'default',
|
|
@@ -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
|
));
|
|
@@ -27,14 +27,22 @@
|
|
|
27
27
|
|
|
28
28
|
<HeadlessModalTitle
|
|
29
29
|
v-if="title"
|
|
30
|
-
class="px-4 pt-5
|
|
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">
|
|
@@ -7,11 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
9
|
import { computed } from 'vue';
|
|
10
|
+
import type { HTMLAttributes } from 'vue';
|
|
10
11
|
|
|
11
12
|
import HeadlessSelectLabel from '@aerogel/core/components/headless/HeadlessSelectLabel.vue';
|
|
12
13
|
import { classes, injectReactiveOrFail } from '@aerogel/core/utils';
|
|
13
14
|
import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
|
|
14
15
|
|
|
16
|
+
const { class: rootClasses } = defineProps<{ class?: HTMLAttributes['class'] }>();
|
|
17
|
+
|
|
15
18
|
const select = injectReactiveOrFail<SelectExpose>('select', '<SelectLabel> must be a child of a <Select>');
|
|
16
|
-
const renderedClasses = computed(() =>
|
|
19
|
+
const renderedClasses = computed(() =>
|
|
20
|
+
classes('block text-sm leading-6 font-medium text-gray-900', select.labelClass, rootClasses));
|
|
17
21
|
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-4 flex" :class="{ 'flex-col': layout === 'vertical' }">
|
|
3
|
+
<div class="flex-grow">
|
|
4
|
+
<h3 :id="titleId" class="text-base font-semibold">
|
|
5
|
+
{{ title }}
|
|
6
|
+
</h3>
|
|
7
|
+
<Markdown v-if="description" :text="description" class="mt-1 text-sm text-gray-500" />
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div :class="renderedRootClass">
|
|
11
|
+
<slot />
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed } from 'vue';
|
|
18
|
+
import type { HTMLAttributes } from 'vue';
|
|
19
|
+
|
|
20
|
+
import Markdown from '@aerogel/core/components/ui/Markdown.vue';
|
|
21
|
+
import { classes } from '@aerogel/core/utils';
|
|
22
|
+
|
|
23
|
+
const { layout = 'horizontal', class: rootClass } = defineProps<{
|
|
24
|
+
title: string;
|
|
25
|
+
titleId?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
class?: HTMLAttributes['class'];
|
|
28
|
+
layout?: 'vertical' | 'horizontal';
|
|
29
|
+
}>();
|
|
30
|
+
const renderedRootClass = computed(() => classes(rootClass, 'flex flex-col justify-center gap-1'));
|
|
31
|
+
</script>
|
|
@@ -1,31 +1,76 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="grid grow place-items-center">
|
|
3
|
-
<div class="flex flex-col items-center
|
|
4
|
-
<
|
|
5
|
-
|
|
3
|
+
<div class="flex flex-col items-center p-8">
|
|
4
|
+
<i-majesticons-exclamation class="size-20 text-red-600" />
|
|
5
|
+
<h1 class="mt-0 mb-0 text-center text-4xl font-medium text-red-600">
|
|
6
|
+
{{ $td('startupCrash.title', 'Oops, something went wrong!') }}
|
|
6
7
|
</h1>
|
|
7
8
|
<Markdown
|
|
8
9
|
:text="
|
|
9
10
|
$td(
|
|
10
11
|
'startupCrash.message',
|
|
11
|
-
'
|
|
12
|
+
'There was a problem starting the application, but here\'s some things you can do:'
|
|
12
13
|
)
|
|
13
14
|
"
|
|
14
15
|
class="mt-4 text-center"
|
|
15
16
|
/>
|
|
16
|
-
<div
|
|
17
|
+
<div
|
|
18
|
+
class="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2 [&_button]:justify-start sm:[&_button]:size-32 sm:[&_button]:flex-col sm:[&_button]:justify-center [&_svg]:size-6 sm:[&_svg]:size-8"
|
|
19
|
+
>
|
|
17
20
|
<Button variant="danger" @click="$app.reload()">
|
|
21
|
+
<IconRefresh />
|
|
18
22
|
{{ $td('startupCrash.reload', 'Try again') }}
|
|
19
23
|
</Button>
|
|
20
24
|
<Button variant="danger" @click="$errors.inspect($errors.startupErrors)">
|
|
25
|
+
<IconBug />
|
|
21
26
|
{{ $td('startupCrash.inspect', 'View error details') }}
|
|
22
27
|
</Button>
|
|
28
|
+
<Button variant="danger" @click="purgeDevice()">
|
|
29
|
+
<IconDelete />
|
|
30
|
+
{{ $td('startupCrash.purge', 'Purge device') }}
|
|
31
|
+
</Button>
|
|
32
|
+
<Button variant="danger" @click="$errors.debug = !$errors.debug">
|
|
33
|
+
<IconFrameInspect />
|
|
34
|
+
{{ $td('startupCrash.debug', 'Toggle debugging') }}
|
|
35
|
+
</Button>
|
|
23
36
|
</div>
|
|
24
37
|
</div>
|
|
25
38
|
</div>
|
|
26
39
|
</template>
|
|
27
40
|
|
|
28
41
|
<script setup lang="ts">
|
|
29
|
-
import
|
|
42
|
+
import IconBug from '~icons/material-symbols/bug-report';
|
|
43
|
+
import IconDelete from '~icons/material-symbols/delete-forever-rounded';
|
|
44
|
+
import IconFrameInspect from '~icons/material-symbols/frame-inspect';
|
|
45
|
+
import IconRefresh from '~icons/material-symbols/refresh-rounded';
|
|
46
|
+
|
|
47
|
+
import App from '@aerogel/core/services/App';
|
|
30
48
|
import Button from '@aerogel/core/components/ui/Button.vue';
|
|
49
|
+
import Markdown from '@aerogel/core/components/ui/Markdown.vue';
|
|
50
|
+
import Storage from '@aerogel/core/services/Storage';
|
|
51
|
+
import UI from '@aerogel/core/ui/UI';
|
|
52
|
+
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
53
|
+
|
|
54
|
+
async function purgeDevice() {
|
|
55
|
+
const confirmed = await UI.confirm(
|
|
56
|
+
translateWithDefault('startupCrash.purgeConfirmTitle', 'Delete everything?'),
|
|
57
|
+
translateWithDefault(
|
|
58
|
+
'startupCrash.purgeConfirmMessage',
|
|
59
|
+
'If the problem persists, one drastic solution may be to wipe the storage in this device ' +
|
|
60
|
+
'to start from scratch. However, keep in mind that **all the data that you haven\'t ' +
|
|
61
|
+
'synchronized will be deleted forever**.\n\nDo you still want to proceed?',
|
|
62
|
+
),
|
|
63
|
+
{
|
|
64
|
+
acceptVariant: 'danger',
|
|
65
|
+
acceptText: translateWithDefault('startupCrash.purgeConfirmAccept', 'Purge device'),
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!confirmed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await Storage.purge();
|
|
74
|
+
await App.reload();
|
|
75
|
+
}
|
|
31
76
|
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<HeadlessSwitch
|
|
3
|
+
class="flex flex-row items-center gap-1"
|
|
4
|
+
input-class="disabled:opacity-50 disabled:cursor-not-allowed data-[state=checked]:bg-primary-600 data-[state=unchecked]:bg-gray-200 relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focusdisabled:opacity-50 disabled:cursor-not-allowed :ring-2 focus:ring-primary-600 focus:ring-offset-2 focus:outline-hidden"
|
|
5
|
+
thumb-class="data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 pointer-events-none inline-block size-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
|
|
6
|
+
/>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import HeadlessSwitch from '@aerogel/core/components/headless/HeadlessSwitch.vue';
|
|
11
|
+
</script>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<HeadlessInput
|
|
3
|
+
ref="$inputRef"
|
|
4
|
+
:label="label"
|
|
5
|
+
:class="rootClasses"
|
|
6
|
+
v-bind="props"
|
|
7
|
+
@update:model-value="$emit('update:modelValue', $event)"
|
|
8
|
+
>
|
|
9
|
+
<HeadlessInputLabel class="block text-sm leading-6 font-medium text-gray-900" />
|
|
10
|
+
<div :class="renderedWrapperClasses">
|
|
11
|
+
<HeadlessInputTextArea v-bind="inputAttrs" :class="renderedInputClasses" />
|
|
12
|
+
<div v-if="$input?.errors" class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
|
13
|
+
<IconExclamationSolid class="size-5 text-red-500" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<HeadlessInputDescription class="mt-2 text-sm text-gray-600" />
|
|
17
|
+
<HeadlessInputError class="mt-2 text-sm text-red-600" />
|
|
18
|
+
</HeadlessInput>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import IconExclamationSolid from '~icons/zondicons/exclamation-solid';
|
|
23
|
+
|
|
24
|
+
import { computed, useTemplateRef } from 'vue';
|
|
25
|
+
import type { HTMLAttributes } from 'vue';
|
|
26
|
+
|
|
27
|
+
import HeadlessInput from '@aerogel/core/components/headless/HeadlessInput.vue';
|
|
28
|
+
import HeadlessInputLabel from '@aerogel/core/components/headless/HeadlessInputLabel.vue';
|
|
29
|
+
import HeadlessInputTextArea from '@aerogel/core/components/headless/HeadlessInputTextArea.vue';
|
|
30
|
+
import HeadlessInputDescription from '@aerogel/core/components/headless/HeadlessInputDescription.vue';
|
|
31
|
+
import HeadlessInputError from '@aerogel/core/components/headless/HeadlessInputError.vue';
|
|
32
|
+
import { classes } from '@aerogel/core/utils/classes';
|
|
33
|
+
import { useInputAttrs } from '@aerogel/core/utils/composition/forms';
|
|
34
|
+
import type { InputEmits, InputProps } from '@aerogel/core/components/contracts/Input';
|
|
35
|
+
|
|
36
|
+
defineOptions({ inheritAttrs: false });
|
|
37
|
+
defineEmits<InputEmits>();
|
|
38
|
+
const { label, inputClass, wrapperClass, ...props } = defineProps<
|
|
39
|
+
InputProps & { inputClass?: HTMLAttributes['class']; wrapperClass?: HTMLAttributes['class'] }
|
|
40
|
+
>();
|
|
41
|
+
const $input = useTemplateRef('$inputRef');
|
|
42
|
+
const [inputAttrs, rootClasses] = useInputAttrs();
|
|
43
|
+
const renderedWrapperClasses = computed(() =>
|
|
44
|
+
classes('relative rounded-md shadow-2xs', { 'mt-1': label }, wrapperClass));
|
|
45
|
+
const renderedInputClasses = computed(() =>
|
|
46
|
+
classes(
|
|
47
|
+
// eslint-disable-next-line vue/max-len
|
|
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
|
+
{
|
|
50
|
+
'focus:ring-primary-600': !$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
|
+
},
|
|
54
|
+
inputClass,
|
|
55
|
+
));
|
|
56
|
+
</script>
|
|
@@ -27,6 +27,9 @@ export { default as SelectLabel } from './SelectLabel.vue';
|
|
|
27
27
|
export { default as SelectOption } from './SelectOption.vue';
|
|
28
28
|
export { default as SelectOptions } from './SelectOptions.vue';
|
|
29
29
|
export { default as SelectTrigger } from './SelectTrigger.vue';
|
|
30
|
+
export { default as Setting } from './Setting.vue';
|
|
30
31
|
export { default as SettingsModal } from './SettingsModal.vue';
|
|
31
32
|
export { default as StartupCrash } from './StartupCrash.vue';
|
|
33
|
+
export { default as Switch } from './Switch.vue';
|
|
34
|
+
export { default as TextArea } from './TextArea.vue';
|
|
32
35
|
export { default as Toast } from './Toast.vue';
|
package/src/errors/Errors.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { JSError, facade, isDevelopment, isObject, isTesting, objectWithoutEmpty, toString } from '@noeldemartin/utils';
|
|
2
|
+
import { watchEffect } from 'vue';
|
|
3
|
+
import type Eruda from 'eruda';
|
|
2
4
|
|
|
3
5
|
import App from '@aerogel/core/services/App';
|
|
4
6
|
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
@@ -13,6 +15,7 @@ export class ErrorsService extends Service {
|
|
|
13
15
|
|
|
14
16
|
public forceReporting: boolean = false;
|
|
15
17
|
private enabled: boolean = true;
|
|
18
|
+
private eruda: typeof Eruda | null = null;
|
|
16
19
|
|
|
17
20
|
public enable(): void {
|
|
18
21
|
this.enabled = true;
|
|
@@ -91,6 +94,19 @@ export class ErrorsService extends Service {
|
|
|
91
94
|
this.setState({ logs: [log].concat(this.logs) });
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
public reportDevelopmentError(error: ErrorSource, message?: string): void {
|
|
98
|
+
if (!isDevelopment()) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (message) {
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.warn(message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.logError(error);
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
public see(report: ErrorReport): void {
|
|
95
111
|
this.setState({
|
|
96
112
|
logs: this.logs.map((log) => {
|
|
@@ -106,12 +122,17 @@ export class ErrorsService extends Service {
|
|
|
106
122
|
});
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
protected override async boot(): Promise<void> {
|
|
126
|
+
watchEffect(async () => {
|
|
127
|
+
if (!this.debug) {
|
|
128
|
+
this.eruda?.destroy();
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.eruda ??= (await import('eruda')).default;
|
|
134
|
+
|
|
135
|
+
this.eruda.init();
|
|
115
136
|
});
|
|
116
137
|
}
|
|
117
138
|
|
package/src/errors/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { App } from 'vue';
|
|
1
|
+
import type { App as AppInstance } from 'vue';
|
|
2
2
|
|
|
3
|
+
import App from '@aerogel/core/services/App';
|
|
3
4
|
import { bootServices } from '@aerogel/core/services';
|
|
4
5
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
5
6
|
|
|
6
7
|
import Errors from './Errors';
|
|
8
|
+
import settings from './settings';
|
|
7
9
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
8
10
|
|
|
9
11
|
export * from './utils';
|
|
@@ -19,7 +21,7 @@ const frameworkHandler: ErrorHandler = (error) => {
|
|
|
19
21
|
return true;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
function setUpErrorHandler(app:
|
|
24
|
+
function setUpErrorHandler(app: AppInstance, baseHandler: ErrorHandler = () => false): void {
|
|
23
25
|
const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
|
|
24
26
|
|
|
25
27
|
app.config.errorHandler = errorHandler;
|
|
@@ -34,6 +36,8 @@ export default definePlugin({
|
|
|
34
36
|
async install(app, options) {
|
|
35
37
|
setUpErrorHandler(app, options.handleError);
|
|
36
38
|
|
|
39
|
+
settings.forEach((setting) => App.addSetting(setting));
|
|
40
|
+
|
|
37
41
|
await bootServices(app, services);
|
|
38
42
|
},
|
|
39
43
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Setting
|
|
3
|
+
title-id="debug-setting"
|
|
4
|
+
:title="$td('settings.debug', 'Debugging')"
|
|
5
|
+
:description="$td('settings.debugDescription', 'Enable debugging with [Eruda](https://eruda.liriliri.io/).')"
|
|
6
|
+
>
|
|
7
|
+
<Switch v-model="$errors.debug" aria-labelledby="debug-setting" />
|
|
8
|
+
</Setting>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import Setting from '@aerogel/core/components/ui/Setting.vue';
|
|
13
|
+
import Switch from '@aerogel/core/components/ui/Switch.vue';
|
|
14
|
+
</script>
|
|
@@ -4,6 +4,7 @@ import type { Equals } from '@noeldemartin/utils';
|
|
|
4
4
|
import type { Expect } from '@noeldemartin/testing';
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
enumInput,
|
|
7
8
|
numberInput,
|
|
8
9
|
objectInput,
|
|
9
10
|
requiredObjectInput,
|
|
@@ -97,6 +98,7 @@ describe('FormController', () => {
|
|
|
97
98
|
two: requiredStringInput(),
|
|
98
99
|
three: objectInput(),
|
|
99
100
|
four: requiredObjectInput<{ foo: string; bar?: number }>(),
|
|
101
|
+
five: enumInput(['foo', 'bar']),
|
|
100
102
|
});
|
|
101
103
|
|
|
102
104
|
tt<
|
|
@@ -104,6 +106,7 @@ describe('FormController', () => {
|
|
|
104
106
|
| Expect<Equals<typeof form.two, string>>
|
|
105
107
|
| Expect<Equals<typeof form.three, object | null>>
|
|
106
108
|
| Expect<Equals<typeof form.four, { foo: string; bar?: number }>>
|
|
109
|
+
| Expect<Equals<typeof form.five, 'foo' | 'bar' | null>>
|
|
107
110
|
>();
|
|
108
111
|
});
|
|
109
112
|
|