@aerogel/core 0.0.0-next.fcfbfdc3428c34c4d1c0e781b61d244f13232fc9 → 0.1.0-next.c4b24f52d8b652bd5c14c2d12e1b38b779ab7682
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aerogel-core.css +1 -0
- package/dist/aerogel-core.d.ts +661 -919
- package/dist/aerogel-core.js +1983 -1440
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +7 -5
- package/src/components/AppLayout.vue +1 -3
- package/src/components/AppOverlays.vue +0 -27
- package/src/components/contracts/AlertModal.ts +15 -0
- package/src/components/contracts/ConfirmModal.ts +12 -5
- package/src/components/contracts/DropdownMenu.ts +8 -3
- package/src/components/contracts/ErrorReportModal.ts +8 -4
- package/src/components/contracts/Input.ts +7 -7
- package/src/components/contracts/LoadingModal.ts +6 -2
- package/src/components/contracts/Modal.ts +4 -4
- package/src/components/contracts/PromptModal.ts +5 -1
- package/src/components/contracts/Select.ts +9 -8
- package/src/components/contracts/Toast.ts +4 -2
- package/src/components/headless/HeadlessButton.vue +2 -2
- package/src/components/headless/HeadlessInputInput.vue +16 -5
- package/src/components/headless/HeadlessModal.vue +8 -43
- package/src/components/headless/HeadlessModalContent.vue +2 -2
- package/src/components/headless/HeadlessSelect.vue +10 -8
- package/src/components/headless/HeadlessSelectOptions.vue +8 -3
- 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 +7 -3
- package/src/components/ui/Button.vue +27 -10
- package/src/components/ui/ConfirmModal.vue +11 -3
- 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 +18 -7
- package/src/components/ui/Input.vue +2 -2
- package/src/components/ui/LoadingModal.vue +3 -1
- package/src/components/ui/Markdown.vue +29 -1
- package/src/components/ui/Modal.vue +61 -21
- package/src/components/ui/ModalContext.vue +2 -1
- package/src/components/ui/PromptModal.vue +5 -2
- package/src/components/ui/Select.vue +5 -3
- package/src/components/ui/SelectLabel.vue +5 -1
- package/src/components/ui/SelectOptions.vue +6 -1
- package/src/components/ui/SelectTrigger.vue +1 -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/Toast.vue +19 -15
- package/src/components/ui/index.ts +5 -0
- package/src/directives/measure.ts +11 -5
- package/src/errors/Errors.state.ts +1 -0
- package/src/errors/Errors.ts +45 -21
- 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 +35 -9
- package/src/forms/FormController.ts +34 -24
- package/src/forms/index.ts +0 -1
- package/src/forms/utils.ts +58 -33
- package/src/forms/validation.ts +31 -0
- package/src/index.css +34 -12
- package/src/lang/index.ts +1 -1
- package/src/lang/settings/Language.vue +1 -1
- package/src/services/Events.test.ts +8 -8
- package/src/services/Events.ts +2 -8
- 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.state.ts +3 -13
- package/src/ui/UI.ts +103 -84
- package/src/ui/index.ts +16 -17
- package/src/utils/app.ts +7 -0
- package/src/utils/classes.ts +9 -17
- package/src/utils/composition/events.ts +2 -4
- package/src/utils/composition/forms.ts +7 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown.ts +35 -1
- package/src/utils/vue.ts +6 -1
- package/src/forms/composition.ts +0 -6
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
2
|
+
import { tt } from '@noeldemartin/testing';
|
|
3
|
+
import type { Equals } from '@noeldemartin/utils';
|
|
4
|
+
import type { Expect } from '@noeldemartin/testing';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
enumInput,
|
|
8
|
+
numberInput,
|
|
9
|
+
objectInput,
|
|
10
|
+
requiredObjectInput,
|
|
11
|
+
requiredStringInput,
|
|
12
|
+
stringInput,
|
|
13
|
+
} from '@aerogel/core/forms/utils';
|
|
14
|
+
import { useForm } from '@aerogel/core/utils/composition/forms';
|
|
7
15
|
|
|
8
16
|
describe('FormController', () => {
|
|
9
17
|
|
|
@@ -21,7 +29,7 @@ describe('FormController', () => {
|
|
|
21
29
|
// Arrange
|
|
22
30
|
const form = useForm({
|
|
23
31
|
name: {
|
|
24
|
-
type:
|
|
32
|
+
type: 'string',
|
|
25
33
|
rules: 'required',
|
|
26
34
|
},
|
|
27
35
|
});
|
|
@@ -39,7 +47,7 @@ describe('FormController', () => {
|
|
|
39
47
|
// Arrange
|
|
40
48
|
const form = useForm({
|
|
41
49
|
name: {
|
|
42
|
-
type:
|
|
50
|
+
type: 'string',
|
|
43
51
|
rules: 'required',
|
|
44
52
|
},
|
|
45
53
|
});
|
|
@@ -60,11 +68,11 @@ describe('FormController', () => {
|
|
|
60
68
|
// Arrange
|
|
61
69
|
const form = useForm({
|
|
62
70
|
trimmed: {
|
|
63
|
-
type:
|
|
71
|
+
type: 'string',
|
|
64
72
|
rules: 'required',
|
|
65
73
|
},
|
|
66
74
|
untrimmed: {
|
|
67
|
-
type:
|
|
75
|
+
type: 'string',
|
|
68
76
|
rules: 'required',
|
|
69
77
|
trim: false,
|
|
70
78
|
},
|
|
@@ -84,4 +92,22 @@ describe('FormController', () => {
|
|
|
84
92
|
expect(form.errors).toEqual({ trimmed: ['required'], untrimmed: null });
|
|
85
93
|
});
|
|
86
94
|
|
|
95
|
+
it('infers field types', () => {
|
|
96
|
+
const form = useForm({
|
|
97
|
+
one: stringInput(),
|
|
98
|
+
two: requiredStringInput(),
|
|
99
|
+
three: objectInput(),
|
|
100
|
+
four: requiredObjectInput<{ foo: string; bar?: number }>(),
|
|
101
|
+
five: enumInput(['foo', 'bar']),
|
|
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
|
+
| Expect<Equals<typeof form.five, 'foo' | 'bar' | null>>
|
|
110
|
+
>();
|
|
111
|
+
});
|
|
112
|
+
|
|
87
113
|
});
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
import { computed, nextTick, reactive, readonly, ref } from 'vue';
|
|
2
2
|
import { MagicObject, arrayRemove, fail, toString } from '@noeldemartin/utils';
|
|
3
|
-
import { validate } from './validation';
|
|
4
|
-
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
3
|
import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
String: 'string',
|
|
9
|
-
Number: 'number',
|
|
10
|
-
Boolean: 'boolean',
|
|
11
|
-
Object: 'object',
|
|
12
|
-
Date: 'date',
|
|
13
|
-
} as const;
|
|
5
|
+
import { validate, validateType } from './validation';
|
|
14
6
|
|
|
15
|
-
export
|
|
7
|
+
export const __valueType: unique symbol = Symbol();
|
|
8
|
+
|
|
9
|
+
export interface FormFieldDefinition<
|
|
10
|
+
TType extends FormFieldType = FormFieldType,
|
|
11
|
+
TRules extends string = string,
|
|
12
|
+
TValueType = unknown,
|
|
13
|
+
> {
|
|
16
14
|
type: TType;
|
|
17
15
|
trim?: boolean;
|
|
18
16
|
default?: GetFormFieldValue<TType>;
|
|
19
17
|
rules?: TRules;
|
|
18
|
+
values?: readonly TValueType[];
|
|
19
|
+
[__valueType]?: TValueType;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export type
|
|
23
|
-
export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
|
|
22
|
+
export type FormFieldType = 'string' | 'enum' | 'number' | 'boolean' | 'object' | 'date';
|
|
24
23
|
export type FormFieldValue = GetFormFieldValue<FormFieldType>;
|
|
24
|
+
export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
|
|
25
25
|
|
|
26
26
|
export type FormData<T> = {
|
|
27
|
-
-readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
|
|
27
|
+
-readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules, infer TValueType>
|
|
28
28
|
? TRules extends 'required'
|
|
29
|
-
? GetFormFieldValue<TType>
|
|
30
|
-
: GetFormFieldValue<TType> | null
|
|
29
|
+
? GetFormFieldValue<TType, TValueType>
|
|
30
|
+
: GetFormFieldValue<TType, TValueType> | null
|
|
31
31
|
: never;
|
|
32
32
|
};
|
|
33
33
|
|
|
@@ -35,17 +35,21 @@ export type FormErrors<T> = {
|
|
|
35
35
|
[k in keyof T]: string[] | null;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export type GetFormFieldValue<TType> = TType extends
|
|
38
|
+
export type GetFormFieldValue<TType, TValueType = unknown> = TType extends 'string'
|
|
39
39
|
? string
|
|
40
|
-
: TType extends
|
|
40
|
+
: TType extends 'number'
|
|
41
41
|
? number
|
|
42
|
-
: TType extends
|
|
42
|
+
: TType extends 'boolean'
|
|
43
43
|
? boolean
|
|
44
|
-
: TType extends
|
|
45
|
-
?
|
|
46
|
-
: TType extends
|
|
47
|
-
?
|
|
48
|
-
|
|
44
|
+
: TType extends 'enum'
|
|
45
|
+
? TValueType
|
|
46
|
+
: TType extends 'object'
|
|
47
|
+
? TValueType extends object
|
|
48
|
+
? TValueType
|
|
49
|
+
: object
|
|
50
|
+
: TType extends 'date'
|
|
51
|
+
? Date
|
|
52
|
+
: never;
|
|
49
53
|
|
|
50
54
|
const validForms: WeakMap<FormController, ComputedRef<boolean>> = new WeakMap();
|
|
51
55
|
|
|
@@ -91,7 +95,7 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
|
|
|
91
95
|
this._fields[field] ?? fail<FormFieldDefinition>(`Trying to set undefined '${toString(field)}' field`);
|
|
92
96
|
|
|
93
97
|
this._data[field] =
|
|
94
|
-
definition.type ===
|
|
98
|
+
definition.type === 'string' && (definition.trim ?? true)
|
|
95
99
|
? (toString(value).trim() as FormData<Fields>[T])
|
|
96
100
|
: value;
|
|
97
101
|
|
|
@@ -108,6 +112,10 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
|
|
|
108
112
|
return this._fields[field]?.rules?.split('|') ?? [];
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
public getFieldType<T extends keyof Fields>(field: T): FormFieldType | null {
|
|
116
|
+
return this._fields[field]?.type ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
public data(): FormData<Fields> {
|
|
112
120
|
return { ...this._data };
|
|
113
121
|
}
|
|
@@ -191,6 +199,8 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
|
|
|
191
199
|
const value = this._data[name];
|
|
192
200
|
const rules = definition.rules?.split('|') ?? [];
|
|
193
201
|
|
|
202
|
+
errors.push(...validateType(value, definition));
|
|
203
|
+
|
|
194
204
|
for (const rule of rules) {
|
|
195
205
|
if (rule !== 'required' && (value === null || value === undefined)) {
|
|
196
206
|
continue;
|
package/src/forms/index.ts
CHANGED
package/src/forms/utils.ts
CHANGED
|
@@ -1,84 +1,109 @@
|
|
|
1
|
-
import { FormFieldTypes } from './FormController';
|
|
2
1
|
import type { FormFieldDefinition } from './FormController';
|
|
3
2
|
|
|
4
|
-
export function booleanInput(
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export function booleanInput(defaultValue?: boolean, options: { rules?: string } = {}): FormFieldDefinition<'boolean'> {
|
|
4
|
+
return {
|
|
5
|
+
default: defaultValue,
|
|
6
|
+
type: 'boolean',
|
|
7
|
+
rules: options.rules,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function dateInput(defaultValue?: Date, options: { rules?: string } = {}): FormFieldDefinition<'date'> {
|
|
8
12
|
return {
|
|
9
13
|
default: defaultValue,
|
|
10
|
-
type:
|
|
14
|
+
type: 'date',
|
|
11
15
|
rules: options.rules,
|
|
12
16
|
};
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export function
|
|
16
|
-
|
|
19
|
+
export function enumInput<const T extends string>(
|
|
20
|
+
values: readonly T[],
|
|
21
|
+
defaultValue?: T,
|
|
17
22
|
options: { rules?: string } = {},
|
|
18
|
-
): FormFieldDefinition<
|
|
23
|
+
): FormFieldDefinition<'enum', string, T> {
|
|
19
24
|
return {
|
|
20
25
|
default: defaultValue,
|
|
21
|
-
type:
|
|
26
|
+
type: 'enum',
|
|
22
27
|
rules: options.rules,
|
|
28
|
+
values,
|
|
23
29
|
};
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
export function requiredBooleanInput(
|
|
27
|
-
defaultValue?: boolean,
|
|
28
|
-
): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
|
|
32
|
+
export function requiredBooleanInput(defaultValue?: boolean): FormFieldDefinition<'boolean', 'required'> {
|
|
29
33
|
return {
|
|
30
34
|
default: defaultValue,
|
|
31
|
-
type:
|
|
35
|
+
type: 'boolean',
|
|
32
36
|
rules: 'required',
|
|
33
37
|
};
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<
|
|
40
|
+
export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<'date', 'required'> {
|
|
37
41
|
return {
|
|
38
42
|
default: defaultValue,
|
|
39
|
-
type:
|
|
43
|
+
type: 'date',
|
|
40
44
|
rules: 'required',
|
|
41
45
|
};
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export function
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
export function requiredEnumInput<const T extends string>(
|
|
49
|
+
values: readonly T[],
|
|
50
|
+
defaultValue?: T,
|
|
51
|
+
): FormFieldDefinition<'enum', 'required', T> {
|
|
47
52
|
return {
|
|
48
53
|
default: defaultValue,
|
|
49
|
-
type:
|
|
54
|
+
type: 'enum',
|
|
50
55
|
rules: 'required',
|
|
56
|
+
values,
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
export function
|
|
55
|
-
defaultValue?: string,
|
|
56
|
-
): FormFieldDefinition<typeof FormFieldTypes.String, 'required'> {
|
|
60
|
+
export function requiredNumberInput(defaultValue?: number): FormFieldDefinition<'number', 'required'> {
|
|
57
61
|
return {
|
|
58
62
|
default: defaultValue,
|
|
59
|
-
type:
|
|
63
|
+
type: 'number',
|
|
60
64
|
rules: 'required',
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
export function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
export function requiredObjectInput<T extends object>(defaultValue?: T): FormFieldDefinition<'object', 'required', T> {
|
|
69
|
+
return {
|
|
70
|
+
default: defaultValue,
|
|
71
|
+
type: 'object',
|
|
72
|
+
rules: 'required',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function requiredStringInput(defaultValue?: string): FormFieldDefinition<'string', 'required'> {
|
|
77
|
+
return {
|
|
78
|
+
default: defaultValue,
|
|
79
|
+
type: 'string',
|
|
80
|
+
rules: 'required',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function numberInput(defaultValue?: number, options: { rules?: string } = {}): FormFieldDefinition<'number'> {
|
|
68
85
|
return {
|
|
69
86
|
default: defaultValue,
|
|
70
|
-
type:
|
|
87
|
+
type: 'number',
|
|
71
88
|
rules: options.rules,
|
|
72
89
|
};
|
|
73
90
|
}
|
|
74
91
|
|
|
75
|
-
export function
|
|
76
|
-
defaultValue?:
|
|
92
|
+
export function objectInput<T extends object>(
|
|
93
|
+
defaultValue?: T,
|
|
77
94
|
options: { rules?: string } = {},
|
|
78
|
-
): FormFieldDefinition<
|
|
95
|
+
): FormFieldDefinition<'object', string, T> {
|
|
96
|
+
return {
|
|
97
|
+
default: defaultValue,
|
|
98
|
+
type: 'object',
|
|
99
|
+
rules: options.rules,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function stringInput(defaultValue?: string, options: { rules?: string } = {}): FormFieldDefinition<'string'> {
|
|
79
104
|
return {
|
|
80
105
|
default: defaultValue,
|
|
81
|
-
type:
|
|
106
|
+
type: 'string',
|
|
82
107
|
rules: options.rules,
|
|
83
108
|
};
|
|
84
109
|
}
|
package/src/forms/validation.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { arrayFrom } from '@noeldemartin/utils';
|
|
2
2
|
|
|
3
|
+
import type { FormFieldDefinition } from '@aerogel/core/forms/FormController';
|
|
4
|
+
|
|
3
5
|
const builtInRules: Record<string, FormFieldValidator> = {
|
|
4
6
|
required: (value) => (value ? undefined : 'required'),
|
|
5
7
|
};
|
|
6
8
|
|
|
9
|
+
function isValidType(value: unknown, definition: FormFieldDefinition): boolean {
|
|
10
|
+
if (value === undefined || value === null) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
switch (definition.type) {
|
|
15
|
+
case 'string':
|
|
16
|
+
return typeof value === 'string';
|
|
17
|
+
case 'enum':
|
|
18
|
+
return !!definition.values?.includes(value);
|
|
19
|
+
case 'number':
|
|
20
|
+
return typeof value === 'number';
|
|
21
|
+
case 'boolean':
|
|
22
|
+
return typeof value === 'boolean';
|
|
23
|
+
case 'date':
|
|
24
|
+
return value instanceof Date;
|
|
25
|
+
case 'object':
|
|
26
|
+
return typeof value === 'object';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
export type FormFieldValidator<T = unknown> = (value: T) => string | string[] | undefined;
|
|
8
31
|
|
|
9
32
|
export const validators: Record<string, FormFieldValidator> = { ...builtInRules };
|
|
@@ -12,6 +35,14 @@ export function defineFormValidationRule<T>(rule: string, validator: FormFieldVa
|
|
|
12
35
|
validators[rule] = validator as FormFieldValidator;
|
|
13
36
|
}
|
|
14
37
|
|
|
38
|
+
export function validateType(value: unknown, definition: FormFieldDefinition): string[] {
|
|
39
|
+
if (isValidType(value, definition)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ['invalid_value'];
|
|
44
|
+
}
|
|
45
|
+
|
|
15
46
|
export function validate(value: unknown, rule: string): string[] {
|
|
16
47
|
const errors = validators[rule]?.(value);
|
|
17
48
|
|
package/src/index.css
CHANGED
|
@@ -6,20 +6,24 @@
|
|
|
6
6
|
@source './';
|
|
7
7
|
|
|
8
8
|
@theme {
|
|
9
|
-
--color-background: oklch(1 0 0);
|
|
10
|
-
|
|
11
9
|
--color-primary: oklch(0.205 0 0);
|
|
12
|
-
--color-primary-50: color-mix(
|
|
13
|
-
--color-primary-100: color-mix(
|
|
14
|
-
--color-primary-200: color-mix(
|
|
15
|
-
--color-primary-300: color-mix(
|
|
16
|
-
--color-primary-400: color-mix(
|
|
17
|
-
--color-primary-500: color-mix(
|
|
10
|
+
--color-primary-50: color-mix(in oklab, var(--color-primary-600) 5%, transparent);
|
|
11
|
+
--color-primary-100: color-mix(in oklab, var(--color-primary-600) 15%, transparent);
|
|
12
|
+
--color-primary-200: color-mix(in oklab, var(--color-primary-600) 30%, transparent);
|
|
13
|
+
--color-primary-300: color-mix(in oklab, var(--color-primary-600) 50%, transparent);
|
|
14
|
+
--color-primary-400: color-mix(in oklab, var(--color-primary-600) 65%, transparent);
|
|
15
|
+
--color-primary-500: color-mix(in oklab, var(--color-primary-600) 80%, transparent);
|
|
18
16
|
--color-primary-600: var(--color-primary);
|
|
19
|
-
--color-primary-700: color-mix(
|
|
20
|
-
--color-primary-800: color-mix(
|
|
21
|
-
--color-primary-900: color-mix(
|
|
22
|
-
--color-primary-950: color-mix(
|
|
17
|
+
--color-primary-700: color-mix(in oklab, var(--color-primary-600) 90%, black);
|
|
18
|
+
--color-primary-800: color-mix(in oklab, var(--color-primary-600) 80%, black);
|
|
19
|
+
--color-primary-900: color-mix(in oklab, var(--color-primary-600) 70%, black);
|
|
20
|
+
--color-primary-950: color-mix(in oklab, var(--color-primary-600) 50%, black);
|
|
21
|
+
|
|
22
|
+
--color-background: oklch(1 0 0);
|
|
23
|
+
--color-links: var(--color-primary);
|
|
24
|
+
|
|
25
|
+
--breakpoint-content: var(--breakpoint-md);
|
|
26
|
+
--spacing-edge: 1rem;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
.clickable {
|
|
@@ -52,3 +56,21 @@ button[data-markdown-action] {
|
|
|
52
56
|
text-decoration: underline;
|
|
53
57
|
font-weight: 500;
|
|
54
58
|
}
|
|
59
|
+
|
|
60
|
+
@keyframes fade-in {
|
|
61
|
+
0% {
|
|
62
|
+
opacity: 0;
|
|
63
|
+
}
|
|
64
|
+
100% {
|
|
65
|
+
opacity: 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@keyframes grow {
|
|
70
|
+
0% {
|
|
71
|
+
scale: 0;
|
|
72
|
+
}
|
|
73
|
+
100% {
|
|
74
|
+
opacity: 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/lang/index.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { definePlugin } from '@aerogel/core/plugins';
|
|
|
4
4
|
|
|
5
5
|
import Lang from './Lang';
|
|
6
6
|
import settings from './settings';
|
|
7
|
-
import type { LangProvider } from './Lang';
|
|
8
7
|
import { translate, translateWithDefault } from './utils';
|
|
8
|
+
import type { LangProvider } from './Lang';
|
|
9
9
|
|
|
10
10
|
export { Lang, translate, translateWithDefault };
|
|
11
11
|
export type { LangProvider };
|
|
@@ -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/Service.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { Constructor, Nullable } from '@noeldemartin/utils';
|
|
|
12
12
|
import type { Store } from 'pinia';
|
|
13
13
|
|
|
14
14
|
import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
|
|
15
|
+
import { appNamespace } from '@aerogel/core/utils/app';
|
|
15
16
|
import { defineServiceStore } from '@aerogel/core/services/store';
|
|
16
17
|
import type { Unref } from '@aerogel/core/utils/vue';
|
|
17
18
|
|
|
@@ -164,7 +165,7 @@ export default class Service<
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
public hasPersistedState(): boolean {
|
|
167
|
-
return Storage.has(this.
|
|
168
|
+
return Storage.has(this.storageKey);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
public hasState<P extends keyof State>(property: P): boolean {
|
|
@@ -231,6 +232,10 @@ export default class Service<
|
|
|
231
232
|
this.setState({ [property]: value } as Partial<State>);
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
protected get storageKey(): string {
|
|
236
|
+
return `${appNamespace()}:${this._name}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
234
239
|
protected onStateUpdated(update: Partial<State>, old: Partial<State>): void {
|
|
235
240
|
const persisted = objectOnly(update, this.static('persist'));
|
|
236
241
|
|
|
@@ -250,13 +255,13 @@ export default class Service<
|
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
protected onPersistentStateUpdated(persisted: Partial<State>): void {
|
|
253
|
-
const storage = Storage.get<ServiceStorage>(this.
|
|
258
|
+
const storage = Storage.get<ServiceStorage>(this.storageKey);
|
|
254
259
|
|
|
255
260
|
if (!storage) {
|
|
256
261
|
return;
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
Storage.set(this.
|
|
264
|
+
Storage.set(this.storageKey, {
|
|
260
265
|
...storage,
|
|
261
266
|
...this.serializePersistedState(objectDeepClone(persisted) as Partial<State>),
|
|
262
267
|
});
|
|
@@ -303,14 +308,14 @@ export default class Service<
|
|
|
303
308
|
return;
|
|
304
309
|
}
|
|
305
310
|
|
|
306
|
-
if (Storage.has(this.
|
|
307
|
-
const persisted = Storage.require<ServiceStorage>(this.
|
|
311
|
+
if (Storage.has(this.storageKey)) {
|
|
312
|
+
const persisted = Storage.require<ServiceStorage>(this.storageKey);
|
|
308
313
|
this.setState(this.deserializePersistedState(persisted));
|
|
309
314
|
|
|
310
315
|
return;
|
|
311
316
|
}
|
|
312
317
|
|
|
313
|
-
Storage.set(this.
|
|
318
|
+
Storage.set(this.storageKey, objectOnly(this.getState(), this.static('persist')));
|
|
314
319
|
}
|
|
315
320
|
|
|
316
321
|
protected requireStore(): Store<string, State, ComputedState, {}> {
|
package/src/services/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { App as
|
|
1
|
+
import type { App as AppInstance } from 'vue';
|
|
2
2
|
|
|
3
3
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
4
4
|
import { isDevelopment, isTesting } from '@noeldemartin/utils';
|
|
@@ -30,7 +30,7 @@ export type DefaultServices = typeof defaultServices;
|
|
|
30
30
|
|
|
31
31
|
export interface Services extends DefaultServices {}
|
|
32
32
|
|
|
33
|
-
export async function bootServices(app:
|
|
33
|
+
export async function bootServices(app: AppInstance, services: Record<string, Service>): Promise<void> {
|
|
34
34
|
await Promise.all(
|
|
35
35
|
Object.entries(services).map(async ([name, service]) => {
|
|
36
36
|
await service
|
package/src/testing/index.ts
CHANGED
|
@@ -2,10 +2,13 @@ import { isTesting } from '@noeldemartin/utils';
|
|
|
2
2
|
import type { GetClosureArgs } from '@noeldemartin/utils';
|
|
3
3
|
|
|
4
4
|
import Events from '@aerogel/core/services/Events';
|
|
5
|
+
import { App } from '@aerogel/core/services';
|
|
5
6
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
7
|
+
import type { Services } from '@aerogel/core/services';
|
|
6
8
|
|
|
7
9
|
export interface AerogelTestingRuntime {
|
|
8
10
|
on: (typeof Events)['on'];
|
|
11
|
+
service<T extends keyof Services>(name: T): Services[T] | null;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export default definePlugin({
|
|
@@ -16,6 +19,7 @@ export default definePlugin({
|
|
|
16
19
|
|
|
17
20
|
globalThis.testingRuntime = {
|
|
18
21
|
on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
|
|
22
|
+
service: (name) => App.service(name),
|
|
19
23
|
};
|
|
20
24
|
},
|
|
21
25
|
});
|