@aerogel/core 0.0.0-next.eed7a057cf5b844cd9f7fc6bda2d8df49fcd6736 → 0.0.0-next.f0368a3107664018eb88652e719d2cb7d24b82e7
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 +1699 -2701
- package/dist/aerogel-core.js +2544 -2037
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +14 -6
- package/src/components/{AGAppLayout.vue → AppLayout.vue} +3 -3
- package/src/components/{AGAppModals.vue → AppModals.vue} +2 -3
- package/src/components/AppOverlays.vue +9 -0
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +4 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +42 -0
- package/src/components/contracts/DropdownMenu.ts +20 -0
- package/src/components/{modals/AGErrorReportModal.ts → contracts/ErrorReportModal.ts} +3 -23
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +22 -0
- package/src/components/contracts/Modal.ts +21 -0
- package/src/components/contracts/PromptModal.ts +31 -0
- package/src/components/contracts/Select.ts +45 -0
- package/src/components/contracts/Toast.ts +13 -0
- package/src/components/contracts/index.ts +11 -0
- package/src/components/headless/HeadlessButton.vue +51 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +6 -7
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +2 -6
- package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +16 -25
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +2 -6
- package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +9 -12
- package/src/components/headless/HeadlessModal.vue +57 -0
- package/src/components/headless/HeadlessModalContent.vue +30 -0
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +120 -0
- package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +3 -4
- package/src/components/headless/HeadlessSelectLabel.vue +25 -0
- package/src/components/headless/HeadlessSelectOption.vue +34 -0
- package/src/components/headless/HeadlessSelectOptions.vue +42 -0
- package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
- package/src/components/headless/HeadlessSelectValue.vue +18 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +19 -3
- package/src/components/index.ts +6 -11
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +98 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +14 -0
- package/src/components/ui/DropdownMenuOptions.vue +27 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +62 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -22
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
- package/src/components/ui/Input.vue +56 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +32 -0
- package/src/components/ui/Markdown.vue +69 -0
- package/src/components/ui/Modal.vue +121 -0
- package/src/components/{modals/AGModalContext.vue → ui/ModalContext.vue} +7 -9
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/Select.vue +27 -0
- package/src/components/ui/SelectLabel.vue +17 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +35 -0
- package/src/components/ui/SelectTrigger.vue +29 -0
- package/src/components/ui/SettingsModal.vue +15 -0
- package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
- package/src/components/ui/Toast.vue +42 -0
- package/src/components/ui/index.ts +30 -0
- package/src/directives/index.ts +6 -0
- package/src/errors/Errors.ts +9 -10
- package/src/forms/{Form.test.ts → FormController.test.ts} +29 -4
- package/src/forms/{Form.ts → FormController.ts} +20 -11
- package/src/forms/index.ts +2 -3
- package/src/forms/utils.ts +23 -2
- package/src/index.css +72 -0
- package/src/lang/index.ts +5 -1
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/services/App.state.ts +11 -1
- package/src/services/App.ts +9 -1
- package/src/services/Events.test.ts +8 -8
- package/src/services/Events.ts +2 -8
- package/src/services/index.ts +4 -1
- package/src/ui/UI.state.ts +13 -6
- package/src/ui/UI.ts +70 -75
- package/src/ui/index.ts +14 -14
- package/src/utils/classes.ts +49 -0
- package/src/utils/composition/events.ts +2 -4
- package/src/utils/composition/forms.ts +20 -4
- package/src/utils/composition/state.ts +11 -2
- package/src/utils/index.ts +3 -1
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +22 -133
- package/src/components/AGAppOverlays.vue +0 -41
- package/src/components/AGAppSnackbars.vue +0 -13
- package/src/components/composition.ts +0 -23
- package/src/components/constants.ts +0 -8
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -42
- package/src/components/forms/AGInput.vue +0 -42
- package/src/components/forms/AGSelect.story.vue +0 -46
- package/src/components/forms/AGSelect.vue +0 -60
- package/src/components/forms/index.ts +0 -5
- package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -41
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
- package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
- package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- package/src/components/headless/forms/composition.ts +0 -10
- package/src/components/headless/forms/index.ts +0 -18
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -36
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -92
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -32
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -23
- package/src/components/headless/modals/index.ts +0 -4
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
- package/src/components/headless/snackbars/index.ts +0 -40
- package/src/components/interfaces.ts +0 -24
- package/src/components/lib/AGErrorMessage.vue +0 -16
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/lib/AGMarkdown.vue +0 -54
- package/src/components/lib/AGMeasured.vue +0 -16
- package/src/components/lib/AGProgressBar.vue +0 -55
- package/src/components/lib/index.ts +0 -6
- package/src/components/modals/AGAlertModal.ts +0 -18
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -42
- package/src/components/modals/AGConfirmModal.vue +0 -26
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -29
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -11
- package/src/components/modals/AGModal.vue +0 -42
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/AGPromptModal.ts +0 -41
- package/src/components/modals/AGPromptModal.vue +0 -34
- package/src/components/modals/index.ts +0 -17
- package/src/components/snackbars/AGSnackbar.vue +0 -36
- package/src/components/snackbars/index.ts +0 -3
- package/src/components/utils.ts +0 -13
- package/src/forms/composition.ts +0 -6
- package/src/utils/tailwindcss.test.ts +0 -26
- package/src/utils/tailwindcss.ts +0 -7
package/src/errors/Errors.ts
CHANGED
|
@@ -4,13 +4,12 @@ import App from '@aerogel/core/services/App';
|
|
|
4
4
|
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
5
5
|
import UI, { UIComponents } from '@aerogel/core/ui/UI';
|
|
6
6
|
import { translateWithDefault } from '@aerogel/core/lang/utils';
|
|
7
|
+
import { Events } from '@aerogel/core/services';
|
|
8
|
+
import type { ErrorReportModalProps } from '@aerogel/core/components/contracts/ErrorReportModal';
|
|
9
|
+
import type { ModalComponent } from '@aerogel/core/ui/UI.state';
|
|
7
10
|
|
|
8
11
|
import Service from './Errors.state';
|
|
9
|
-
import { Colors } from '@aerogel/core/components/constants';
|
|
10
|
-
import { Events } from '@aerogel/core/services';
|
|
11
|
-
import type { AGErrorReportModalProps } from '@aerogel/core/components/modals/AGErrorReportModal';
|
|
12
12
|
import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
|
|
13
|
-
import type { ModalComponent } from '@aerogel/core/ui/UI.state';
|
|
14
13
|
|
|
15
14
|
export class ErrorsService extends Service {
|
|
16
15
|
|
|
@@ -34,7 +33,7 @@ export class ErrorsService extends Service {
|
|
|
34
33
|
return;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
UI.openModal<ModalComponent<
|
|
36
|
+
UI.openModal<ModalComponent<ErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
|
|
38
37
|
reports,
|
|
39
38
|
});
|
|
40
39
|
}
|
|
@@ -71,17 +70,17 @@ export class ErrorsService extends Service {
|
|
|
71
70
|
date: new Date(),
|
|
72
71
|
};
|
|
73
72
|
|
|
74
|
-
UI.
|
|
73
|
+
UI.toast(
|
|
75
74
|
message ??
|
|
76
75
|
translateWithDefault('errors.notice', 'Something went wrong, but it\'s not your fault. Try again!'),
|
|
77
76
|
{
|
|
78
|
-
|
|
77
|
+
variant: 'danger',
|
|
79
78
|
actions: [
|
|
80
79
|
{
|
|
81
|
-
|
|
80
|
+
label: translateWithDefault('errors.viewDetails', 'View details'),
|
|
82
81
|
dismiss: true,
|
|
83
|
-
|
|
84
|
-
UI.openModal<ModalComponent<
|
|
82
|
+
click: () =>
|
|
83
|
+
UI.openModal<ModalComponent<ErrorReportModalProps>>(
|
|
85
84
|
UI.requireComponent(UIComponents.ErrorReportModal),
|
|
86
85
|
{ reports: [report] },
|
|
87
86
|
),
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import { tt } from '@noeldemartin/testing';
|
|
3
|
+
import type { Equals } from '@noeldemartin/utils';
|
|
4
|
+
import type { Expect } from '@noeldemartin/testing';
|
|
2
5
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
6
|
+
import {
|
|
7
|
+
numberInput,
|
|
8
|
+
objectInput,
|
|
9
|
+
requiredObjectInput,
|
|
10
|
+
requiredStringInput,
|
|
11
|
+
stringInput,
|
|
12
|
+
} from '@aerogel/core/forms/utils';
|
|
13
|
+
import { useForm } from '@aerogel/core/utils/composition/forms';
|
|
5
14
|
|
|
6
|
-
import { FormFieldTypes } from './
|
|
15
|
+
import { FormFieldTypes } from './FormController';
|
|
7
16
|
|
|
8
|
-
describe('
|
|
17
|
+
describe('FormController', () => {
|
|
9
18
|
|
|
10
19
|
it('defines magic fields', () => {
|
|
11
20
|
const form = useForm({
|
|
@@ -84,4 +93,20 @@ describe('Form', () => {
|
|
|
84
93
|
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
85
94
|
});
|
|
86
95
|
|
|
96
|
+
it('infers field types', () => {
|
|
97
|
+
const form = useForm({
|
|
98
|
+
one: stringInput(),
|
|
99
|
+
two: requiredStringInput(),
|
|
100
|
+
three: objectInput(),
|
|
101
|
+
four: requiredObjectInput<{ foo: string; bar?: number }>(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
tt<
|
|
105
|
+
| Expect<Equals<typeof form.one, string | null>>
|
|
106
|
+
| Expect<Equals<typeof form.two, string>>
|
|
107
|
+
| Expect<Equals<typeof form.three, object | null>>
|
|
108
|
+
| Expect<Equals<typeof form.four, { foo: string; bar?: number }>>
|
|
109
|
+
>();
|
|
110
|
+
});
|
|
111
|
+
|
|
87
112
|
});
|
|
@@ -12,11 +12,18 @@ export const FormFieldTypes = {
|
|
|
12
12
|
Date: 'date',
|
|
13
13
|
} as const;
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export const __objectType: unique symbol = Symbol();
|
|
16
|
+
|
|
17
|
+
export interface FormFieldDefinition<
|
|
18
|
+
TType extends FormFieldType = FormFieldType,
|
|
19
|
+
TRules extends string = string,
|
|
20
|
+
TObjectType = object,
|
|
21
|
+
> {
|
|
16
22
|
type: TType;
|
|
17
23
|
trim?: boolean;
|
|
18
24
|
default?: GetFormFieldValue<TType>;
|
|
19
25
|
rules?: TRules;
|
|
26
|
+
[__objectType]?: TObjectType;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
|
|
@@ -24,10 +31,10 @@ export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
|
|
|
24
31
|
export type FormFieldValue = GetFormFieldValue<FormFieldType>;
|
|
25
32
|
|
|
26
33
|
export type FormData<T> = {
|
|
27
|
-
-readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
|
|
34
|
+
-readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules, infer TObjectType>
|
|
28
35
|
? TRules extends 'required'
|
|
29
|
-
? GetFormFieldValue<TType>
|
|
30
|
-
: GetFormFieldValue<TType> | null
|
|
36
|
+
? GetFormFieldValue<TType, TObjectType>
|
|
37
|
+
: GetFormFieldValue<TType, TObjectType> | null
|
|
31
38
|
: never;
|
|
32
39
|
};
|
|
33
40
|
|
|
@@ -35,24 +42,26 @@ export type FormErrors<T> = {
|
|
|
35
42
|
[k in keyof T]: string[] | null;
|
|
36
43
|
};
|
|
37
44
|
|
|
38
|
-
export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.String
|
|
45
|
+
export type GetFormFieldValue<TType, TObjectType = object> = TType extends typeof FormFieldTypes.String
|
|
39
46
|
? string
|
|
40
47
|
: TType extends typeof FormFieldTypes.Number
|
|
41
48
|
? number
|
|
42
49
|
: TType extends typeof FormFieldTypes.Boolean
|
|
43
50
|
? boolean
|
|
44
51
|
: TType extends typeof FormFieldTypes.Object
|
|
45
|
-
? object
|
|
52
|
+
? TObjectType extends object
|
|
53
|
+
? TObjectType
|
|
54
|
+
: object
|
|
46
55
|
: TType extends typeof FormFieldTypes.Date
|
|
47
56
|
? Date
|
|
48
57
|
: never;
|
|
49
58
|
|
|
50
|
-
const validForms: WeakMap<
|
|
59
|
+
const validForms: WeakMap<FormController, ComputedRef<boolean>> = new WeakMap();
|
|
51
60
|
|
|
52
61
|
export type SubmitFormListener = () => unknown;
|
|
53
62
|
export type FocusFormListener = (input: string) => unknown;
|
|
54
63
|
|
|
55
|
-
export default class
|
|
64
|
+
export default class FormController<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
|
|
56
65
|
|
|
57
66
|
public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
|
|
58
67
|
|
|
@@ -207,10 +216,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
|
|
|
207
216
|
return {} as FormData<Fields>;
|
|
208
217
|
}
|
|
209
218
|
|
|
210
|
-
const data = Object.entries(fields).reduce((
|
|
211
|
-
|
|
219
|
+
const data = Object.entries(fields).reduce((initialData, [name, definition]) => {
|
|
220
|
+
initialData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
|
|
212
221
|
|
|
213
|
-
return
|
|
222
|
+
return initialData;
|
|
214
223
|
}, {} as FormData<Fields>);
|
|
215
224
|
|
|
216
225
|
return reactive(data) as FormData<Fields>;
|
package/src/forms/index.ts
CHANGED
package/src/forms/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FormFieldTypes } from './
|
|
2
|
-
import type { FormFieldDefinition } from './
|
|
1
|
+
import { FormFieldTypes } from './FormController';
|
|
2
|
+
import type { FormFieldDefinition } from './FormController';
|
|
3
3
|
|
|
4
4
|
export function booleanInput(
|
|
5
5
|
defaultValue?: boolean,
|
|
@@ -51,6 +51,16 @@ export function requiredNumberInput(
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export function requiredObjectInput<T extends object>(
|
|
55
|
+
defaultValue?: T,
|
|
56
|
+
): FormFieldDefinition<typeof FormFieldTypes.Object, 'required', T> {
|
|
57
|
+
return {
|
|
58
|
+
default: defaultValue,
|
|
59
|
+
type: FormFieldTypes.Object,
|
|
60
|
+
rules: 'required',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export function requiredStringInput(
|
|
55
65
|
defaultValue?: string,
|
|
56
66
|
): FormFieldDefinition<typeof FormFieldTypes.String, 'required'> {
|
|
@@ -72,6 +82,17 @@ export function numberInput(
|
|
|
72
82
|
};
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
export function objectInput<T extends object>(
|
|
86
|
+
defaultValue?: T,
|
|
87
|
+
options: { rules?: string } = {},
|
|
88
|
+
): FormFieldDefinition<typeof FormFieldTypes.Object, string, T> {
|
|
89
|
+
return {
|
|
90
|
+
default: defaultValue,
|
|
91
|
+
type: FormFieldTypes.Object,
|
|
92
|
+
rules: options.rules,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
export function stringInput(
|
|
76
97
|
defaultValue?: string,
|
|
77
98
|
options: { rules?: string } = {},
|
package/src/index.css
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
@plugin '@tailwindcss/forms';
|
|
4
|
+
@plugin '@tailwindcss/typography';
|
|
5
|
+
|
|
6
|
+
@source './';
|
|
7
|
+
|
|
8
|
+
@theme {
|
|
9
|
+
--color-background: oklch(1 0 0);
|
|
10
|
+
|
|
11
|
+
--color-primary: oklch(0.205 0 0);
|
|
12
|
+
--color-primary-50: color-mix(in oklab, var(--color-primary-600) 5%, transparent);
|
|
13
|
+
--color-primary-100: color-mix(in oklab, var(--color-primary-600) 15%, transparent);
|
|
14
|
+
--color-primary-200: color-mix(in oklab, var(--color-primary-600) 30%, transparent);
|
|
15
|
+
--color-primary-300: color-mix(in oklab, var(--color-primary-600) 50%, transparent);
|
|
16
|
+
--color-primary-400: color-mix(in oklab, var(--color-primary-600) 65%, transparent);
|
|
17
|
+
--color-primary-500: color-mix(in oklab, var(--color-primary-600) 80%, transparent);
|
|
18
|
+
--color-primary-600: var(--color-primary);
|
|
19
|
+
--color-primary-700: color-mix(in oklab, var(--color-primary-600) 90%, black);
|
|
20
|
+
--color-primary-800: color-mix(in oklab, var(--color-primary-600) 80%, black);
|
|
21
|
+
--color-primary-900: color-mix(in oklab, var(--color-primary-600) 70%, black);
|
|
22
|
+
--color-primary-950: color-mix(in oklab, var(--color-primary-600) 50%, black);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.clickable {
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.clickable::after {
|
|
30
|
+
--clickable-size: 44px;
|
|
31
|
+
--clickable-inset-by: min(0px, calc((100% - var(--clickable-size)) / 2));
|
|
32
|
+
|
|
33
|
+
content: '';
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: var(--clickable-inset-by);
|
|
36
|
+
left: var(--clickable-inset-by);
|
|
37
|
+
right: var(--clickable-inset-by);
|
|
38
|
+
bottom: var(--clickable-inset-by);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
input[type='number'].appearance-textfield {
|
|
42
|
+
appearance: textfield;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
input[type='number'].appearance-textfield::-webkit-outer-spin-button,
|
|
46
|
+
input[type='number'].appearance-textfield::-webkit-inner-spin-button {
|
|
47
|
+
appearance: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
button[data-markdown-action] {
|
|
51
|
+
color: var(--tw-prose-links);
|
|
52
|
+
text-decoration: underline;
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@keyframes fade-in {
|
|
57
|
+
0% {
|
|
58
|
+
opacity: 0;
|
|
59
|
+
}
|
|
60
|
+
100% {
|
|
61
|
+
opacity: 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes grow {
|
|
66
|
+
0% {
|
|
67
|
+
scale: 0;
|
|
68
|
+
}
|
|
69
|
+
100% {
|
|
70
|
+
opacity: 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/lang/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import App from '@aerogel/core/services/App';
|
|
1
2
|
import { bootServices } from '@aerogel/core/services';
|
|
2
3
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
3
4
|
|
|
4
5
|
import Lang from './Lang';
|
|
6
|
+
import settings from './settings';
|
|
5
7
|
import type { LangProvider } from './Lang';
|
|
6
8
|
import { translate, translateWithDefault } from './utils';
|
|
7
9
|
|
|
@@ -17,6 +19,8 @@ export default definePlugin({
|
|
|
17
19
|
app.config.globalProperties.$t ??= translate;
|
|
18
20
|
app.config.globalProperties.$td = translateWithDefault;
|
|
19
21
|
|
|
22
|
+
settings.forEach((setting) => App.addSetting(setting));
|
|
23
|
+
|
|
20
24
|
await bootServices(app, services);
|
|
21
25
|
},
|
|
22
26
|
});
|
|
@@ -25,7 +29,7 @@ declare module '@aerogel/core/services' {
|
|
|
25
29
|
export interface Services extends LangServices {}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
declare module '
|
|
32
|
+
declare module 'vue' {
|
|
29
33
|
interface ComponentCustomProperties {
|
|
30
34
|
$td: typeof translateWithDefault;
|
|
31
35
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Select
|
|
3
|
+
v-model="$lang.locale"
|
|
4
|
+
class="flex flex-col items-start md:flex-row"
|
|
5
|
+
as="div"
|
|
6
|
+
:options
|
|
7
|
+
:render-option="renderLocale"
|
|
8
|
+
>
|
|
9
|
+
<div class="grow">
|
|
10
|
+
<SelectLabel>
|
|
11
|
+
{{ $td('settings.locale', 'Language') }}
|
|
12
|
+
</SelectLabel>
|
|
13
|
+
<Markdown
|
|
14
|
+
lang-key="settings.localeDescription"
|
|
15
|
+
lang-default="Choose the application's language."
|
|
16
|
+
class="mt-1 text-sm text-gray-500"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
<Button variant="ghost" :as="SelectTrigger" class="grid w-auto outline-none" />
|
|
20
|
+
<SelectOptions />
|
|
21
|
+
</Select>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import Aerogel from 'virtual:aerogel';
|
|
26
|
+
|
|
27
|
+
import { computed } from 'vue';
|
|
28
|
+
|
|
29
|
+
import Markdown from '@aerogel/core/components/ui/Markdown.vue';
|
|
30
|
+
import Button from '@aerogel/core/components/ui/Button.vue';
|
|
31
|
+
import Select from '@aerogel/core/components/ui/Select.vue';
|
|
32
|
+
import SelectLabel from '@aerogel/core/components/ui/SelectLabel.vue';
|
|
33
|
+
import SelectTrigger from '@aerogel/core/components/ui/SelectTrigger.vue';
|
|
34
|
+
import SelectOptions from '@aerogel/core/components/ui/SelectOptions.vue';
|
|
35
|
+
import { Lang, translateWithDefault } from '@aerogel/core/lang';
|
|
36
|
+
|
|
37
|
+
const browserLocale = Lang.getBrowserLocale();
|
|
38
|
+
const options = computed(() => [null, ...Lang.locales]);
|
|
39
|
+
|
|
40
|
+
function renderLocale(locale: string | null): string {
|
|
41
|
+
return (
|
|
42
|
+
(locale && Aerogel.locales[locale]) ??
|
|
43
|
+
translateWithDefault('settings.localeDefault', '{locale} (default)', {
|
|
44
|
+
locale: Aerogel.locales[browserLocale] ?? browserLocale,
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import Aerogel from 'virtual:aerogel';
|
|
2
2
|
|
|
3
3
|
import { getEnv } from '@noeldemartin/utils';
|
|
4
|
-
import type { App } from 'vue';
|
|
4
|
+
import type { App, Component } from 'vue';
|
|
5
5
|
|
|
6
6
|
import { defineServiceState } from '@aerogel/core/services/Service';
|
|
7
7
|
import type { Plugin } from '@aerogel/core/plugins/Plugin';
|
|
8
8
|
|
|
9
|
+
export interface AppSetting {
|
|
10
|
+
component: Component;
|
|
11
|
+
priority: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function defineSettings<T extends AppSetting[]>(settings: T): T {
|
|
15
|
+
return settings;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
export default defineServiceState({
|
|
10
19
|
name: 'app',
|
|
11
20
|
initialState: {
|
|
@@ -14,6 +23,7 @@ export default defineServiceState({
|
|
|
14
23
|
environment: getEnv() ?? 'development',
|
|
15
24
|
version: Aerogel.version,
|
|
16
25
|
sourceUrl: Aerogel.sourceUrl,
|
|
26
|
+
settings: [] as AppSetting[],
|
|
17
27
|
},
|
|
18
28
|
computed: {
|
|
19
29
|
development: (state) => state.environment === 'development',
|
package/src/services/App.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import Aerogel from 'virtual:aerogel';
|
|
2
2
|
|
|
3
3
|
import { PromisedValue, facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
|
|
4
|
+
import { markRaw } from 'vue';
|
|
4
5
|
|
|
5
6
|
import Events from '@aerogel/core/services/Events';
|
|
6
7
|
import type { Plugin } from '@aerogel/core/plugins';
|
|
7
|
-
import type { Services } from '@aerogel/core/services';
|
|
8
|
+
import type { AppSetting, Services } from '@aerogel/core/services';
|
|
8
9
|
|
|
9
10
|
import Service from './App.state';
|
|
10
11
|
|
|
12
|
+
export { defineSettings } from './App.state';
|
|
13
|
+
export type { AppSetting } from './App.state';
|
|
14
|
+
|
|
11
15
|
export class AppService extends Service {
|
|
12
16
|
|
|
13
17
|
public readonly name = Aerogel.name;
|
|
@@ -22,6 +26,10 @@ export class AppService extends Service {
|
|
|
22
26
|
return this.mounted.isResolved();
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
public addSetting(setting: AppSetting): void {
|
|
30
|
+
this.settings.push(markRaw(setting));
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
public async whenReady<T>(callback: () => T): Promise<T> {
|
|
26
34
|
const result = await this.ready.then(callback);
|
|
27
35
|
|
|
@@ -10,12 +10,12 @@ describe('Events', () => {
|
|
|
10
10
|
// Arrange
|
|
11
11
|
let counter = 0;
|
|
12
12
|
|
|
13
|
-
Events.on('
|
|
13
|
+
Events.on('application-mounted', () => counter++);
|
|
14
14
|
|
|
15
15
|
// Act
|
|
16
|
-
await Events.emit('
|
|
17
|
-
await Events.emit('
|
|
18
|
-
await Events.emit('
|
|
16
|
+
await Events.emit('application-mounted');
|
|
17
|
+
await Events.emit('application-mounted');
|
|
18
|
+
await Events.emit('application-mounted');
|
|
19
19
|
|
|
20
20
|
// Assert
|
|
21
21
|
expect(counter).toEqual(3);
|
|
@@ -25,12 +25,12 @@ describe('Events', () => {
|
|
|
25
25
|
// Arrange
|
|
26
26
|
const storage: string[] = [];
|
|
27
27
|
|
|
28
|
-
Events.on('
|
|
29
|
-
Events.on('
|
|
30
|
-
Events.on('
|
|
28
|
+
Events.on('application-mounted', () => storage.push('second'));
|
|
29
|
+
Events.on('application-mounted', { priority: EventListenerPriorities.Low }, () => storage.push('third'));
|
|
30
|
+
Events.on('application-mounted', { priority: EventListenerPriorities.High }, () => storage.push('first'));
|
|
31
31
|
|
|
32
32
|
// Act
|
|
33
|
-
await Events.emit('
|
|
33
|
+
await Events.emit('application-mounted');
|
|
34
34
|
|
|
35
35
|
// Assert
|
|
36
36
|
expect(storage).toEqual(['first', 'second', 'third']);
|
package/src/services/Events.ts
CHANGED
|
@@ -10,7 +10,6 @@ export type AerogelGlobalEvents = Partial<{ [Event in EventWithoutPayload]: () =
|
|
|
10
10
|
Partial<{ [Event in EventWithPayload]: EventListener<EventsPayload[Event]> }>;
|
|
11
11
|
|
|
12
12
|
export type EventListener<T = unknown> = (payload: T) => unknown;
|
|
13
|
-
export type UnknownEvent<T> = T extends keyof EventsPayload ? never : T;
|
|
14
13
|
|
|
15
14
|
export type EventWithoutPayload = {
|
|
16
15
|
[K in keyof EventsPayload]: EventsPayload[K] extends void ? K : never;
|
|
@@ -34,12 +33,12 @@ export class EventsService extends Service {
|
|
|
34
33
|
|
|
35
34
|
protected override async boot(): Promise<void> {
|
|
36
35
|
Object.entries(globalThis.__aerogelEvents__ ?? {}).forEach(([event, listener]) =>
|
|
37
|
-
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
this.on(event as any, listener as EventListener));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
public emit<Event extends EventWithoutPayload>(event: Event): Promise<void>;
|
|
41
41
|
public emit<Event extends EventWithPayload>(event: Event, payload: EventsPayload[Event]): Promise<void>;
|
|
42
|
-
public emit<Event extends string>(event: UnknownEvent<Event>, payload?: unknown): Promise<void>;
|
|
43
42
|
public async emit(event: string, payload?: unknown): Promise<void> {
|
|
44
43
|
const listeners = this.listeners[event] ?? { priorities: [], handlers: {} };
|
|
45
44
|
|
|
@@ -55,9 +54,6 @@ export class EventsService extends Service {
|
|
|
55
54
|
public on<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
56
55
|
public on<Event extends EventWithPayload>(event: Event, priority: EventListenerPriority, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
57
56
|
public on<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
58
|
-
public on<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
|
|
59
|
-
public on<Event extends string>(event: UnknownEvent<Event>, priority: EventListenerPriority, listener: EventListener): () => void; // prettier-ignore
|
|
60
|
-
public on<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
|
|
61
57
|
/* eslint-enable max-len */
|
|
62
58
|
|
|
63
59
|
public on(
|
|
@@ -83,8 +79,6 @@ export class EventsService extends Service {
|
|
|
83
79
|
public once<Event extends EventWithoutPayload>(event: Event, options: Partial<EventListenerOptions>, listener: () => unknown): () => void; // prettier-ignore
|
|
84
80
|
public once<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
85
81
|
public once<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
|
|
86
|
-
public once<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
|
|
87
|
-
public once<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
|
|
88
82
|
/* eslint-enable max-len */
|
|
89
83
|
|
|
90
84
|
public once(
|
package/src/services/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import Events from './Events';
|
|
|
9
9
|
import Service from './Service';
|
|
10
10
|
import Storage from './Storage';
|
|
11
11
|
import { getPiniaStore } from './store';
|
|
12
|
+
import type { AppSetting } from './App.state';
|
|
12
13
|
|
|
13
14
|
export * from './App';
|
|
14
15
|
export * from './Cache';
|
|
@@ -53,6 +54,7 @@ export default definePlugin({
|
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
app.use(getPiniaStore());
|
|
57
|
+
options.settings?.forEach((setting) => App.addSetting(setting));
|
|
56
58
|
|
|
57
59
|
await bootServices(app, services);
|
|
58
60
|
},
|
|
@@ -61,9 +63,10 @@ export default definePlugin({
|
|
|
61
63
|
declare module '@aerogel/core/bootstrap/options' {
|
|
62
64
|
export interface AerogelOptions {
|
|
63
65
|
services?: Record<string, Service>;
|
|
66
|
+
settings?: AppSetting[];
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
declare module '
|
|
70
|
+
declare module 'vue' {
|
|
68
71
|
interface ComponentCustomProperties extends Services {}
|
|
69
72
|
}
|
package/src/ui/UI.state.ts
CHANGED
|
@@ -4,22 +4,28 @@ import { defineServiceState } from '@aerogel/core/services/Service';
|
|
|
4
4
|
|
|
5
5
|
import { Layouts, getCurrentLayout } from './utils';
|
|
6
6
|
|
|
7
|
-
export interface
|
|
7
|
+
export interface UIModal<T = unknown> {
|
|
8
8
|
id: string;
|
|
9
9
|
properties: Record<string, unknown>;
|
|
10
10
|
component: Component;
|
|
11
|
+
closing: boolean;
|
|
11
12
|
beforeClose: Promise<T | undefined>;
|
|
12
13
|
afterClose: Promise<T | undefined>;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
export interface UIModalContext {
|
|
17
|
+
modal: UIModal;
|
|
18
|
+
childIndex?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export interface ModalComponent<
|
|
16
22
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
17
|
-
Properties extends
|
|
23
|
+
Properties extends object = object,
|
|
18
24
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
19
25
|
Result = unknown,
|
|
20
26
|
> {}
|
|
21
27
|
|
|
22
|
-
export interface
|
|
28
|
+
export interface UIToast {
|
|
23
29
|
id: string;
|
|
24
30
|
component: Component;
|
|
25
31
|
properties: Record<string, unknown>;
|
|
@@ -28,12 +34,13 @@ export interface Snackbar {
|
|
|
28
34
|
export default defineServiceState({
|
|
29
35
|
name: 'ui',
|
|
30
36
|
initialState: {
|
|
31
|
-
modals: [] as
|
|
32
|
-
|
|
37
|
+
modals: [] as UIModal[],
|
|
38
|
+
toasts: [] as UIToast[],
|
|
33
39
|
layout: getCurrentLayout(),
|
|
34
40
|
},
|
|
35
41
|
computed: {
|
|
36
|
-
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
37
42
|
desktop: ({ layout }) => layout === Layouts.Desktop,
|
|
43
|
+
mobile: ({ layout }) => layout === Layouts.Mobile,
|
|
44
|
+
openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
|
|
38
45
|
},
|
|
39
46
|
});
|