@aerogel/core 0.0.0-next.c33ad773d3eb977461630ff22012d99eeedf46cb → 0.0.0-next.c38a10cd5e81c5f04469ef79e59a53e6bfd3df55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +895 -814
  3. package/dist/aerogel-core.js +2412 -1829
  4. package/dist/aerogel-core.js.map +1 -1
  5. package/package.json +7 -2
  6. package/src/components/AppLayout.vue +1 -3
  7. package/src/components/AppOverlays.vue +0 -27
  8. package/src/components/contracts/AlertModal.ts +15 -0
  9. package/src/components/contracts/ConfirmModal.ts +12 -5
  10. package/src/components/contracts/DropdownMenu.ts +17 -3
  11. package/src/components/contracts/ErrorReportModal.ts +8 -4
  12. package/src/components/contracts/Input.ts +7 -7
  13. package/src/components/contracts/LoadingModal.ts +12 -4
  14. package/src/components/contracts/Modal.ts +12 -4
  15. package/src/components/contracts/PromptModal.ts +8 -2
  16. package/src/components/contracts/Select.ts +22 -10
  17. package/src/components/contracts/Toast.ts +4 -2
  18. package/src/components/contracts/index.ts +3 -1
  19. package/src/components/headless/HeadlessButton.vue +2 -1
  20. package/src/components/headless/HeadlessInput.vue +3 -3
  21. package/src/components/headless/HeadlessInputInput.vue +19 -8
  22. package/src/components/headless/HeadlessInputTextArea.vue +4 -4
  23. package/src/components/headless/HeadlessModal.vue +22 -51
  24. package/src/components/headless/HeadlessModalContent.vue +11 -5
  25. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  26. package/src/components/headless/HeadlessSelect.vue +50 -22
  27. package/src/components/headless/HeadlessSelectOption.vue +1 -1
  28. package/src/components/headless/HeadlessSelectOptions.vue +16 -4
  29. package/src/components/headless/HeadlessSelectValue.vue +4 -1
  30. package/src/components/headless/HeadlessSwitch.vue +96 -0
  31. package/src/components/headless/index.ts +2 -0
  32. package/src/components/index.ts +2 -1
  33. package/src/components/ui/AdvancedOptions.vue +1 -1
  34. package/src/components/ui/AlertModal.vue +7 -3
  35. package/src/components/ui/Button.vue +33 -16
  36. package/src/components/ui/Checkbox.vue +5 -5
  37. package/src/components/ui/ConfirmModal.vue +12 -4
  38. package/src/components/ui/DropdownMenu.vue +18 -19
  39. package/src/components/ui/DropdownMenuOption.vue +22 -0
  40. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  41. package/src/components/ui/EditableContent.vue +4 -4
  42. package/src/components/ui/ErrorLogs.vue +19 -0
  43. package/src/components/ui/ErrorLogsModal.vue +48 -0
  44. package/src/components/ui/ErrorReportModal.vue +18 -7
  45. package/src/components/ui/Input.vue +6 -6
  46. package/src/components/ui/LoadingModal.vue +6 -4
  47. package/src/components/ui/Markdown.vue +31 -3
  48. package/src/components/ui/Modal.vue +83 -22
  49. package/src/components/ui/ModalContext.vue +2 -1
  50. package/src/components/ui/ProgressBar.vue +9 -8
  51. package/src/components/ui/PromptModal.vue +8 -5
  52. package/src/components/ui/Select.vue +15 -41
  53. package/src/components/ui/SelectLabel.vue +21 -0
  54. package/src/components/ui/SelectOption.vue +29 -0
  55. package/src/components/ui/SelectOptions.vue +35 -0
  56. package/src/components/ui/SelectTrigger.vue +29 -0
  57. package/src/components/ui/Setting.vue +31 -0
  58. package/src/components/ui/SettingsModal.vue +15 -0
  59. package/src/components/ui/Switch.vue +11 -0
  60. package/src/components/ui/TextArea.vue +56 -0
  61. package/src/components/ui/Toast.vue +20 -16
  62. package/src/components/ui/index.ts +13 -1
  63. package/src/directives/measure.ts +11 -5
  64. package/src/errors/Errors.ts +18 -15
  65. package/src/errors/index.ts +6 -2
  66. package/src/errors/settings/Debug.vue +32 -0
  67. package/src/errors/settings/index.ts +10 -0
  68. package/src/forms/FormController.test.ts +35 -9
  69. package/src/forms/FormController.ts +34 -24
  70. package/src/forms/index.ts +0 -1
  71. package/src/forms/utils.ts +58 -33
  72. package/src/forms/validation.ts +31 -0
  73. package/src/index.css +40 -5
  74. package/src/lang/index.ts +5 -1
  75. package/src/lang/settings/Language.vue +48 -0
  76. package/src/lang/settings/index.ts +10 -0
  77. package/src/services/App.state.ts +11 -1
  78. package/src/services/App.ts +9 -1
  79. package/src/services/Events.test.ts +8 -8
  80. package/src/services/Events.ts +2 -8
  81. package/src/services/index.ts +5 -2
  82. package/src/testing/index.ts +4 -0
  83. package/src/ui/UI.state.ts +3 -13
  84. package/src/ui/UI.ts +107 -83
  85. package/src/ui/index.ts +16 -17
  86. package/src/utils/classes.ts +41 -0
  87. package/src/utils/composition/events.ts +2 -4
  88. package/src/utils/composition/forms.ts +16 -1
  89. package/src/utils/composition/state.ts +11 -2
  90. package/src/utils/index.ts +3 -1
  91. package/src/utils/markdown.ts +35 -1
  92. package/src/utils/types.ts +3 -0
  93. package/src/utils/vue.ts +28 -125
  94. package/src/components/composition.ts +0 -23
  95. package/src/components/contracts/shared.ts +0 -9
  96. package/src/components/utils.ts +0 -107
  97. package/src/forms/composition.ts +0 -6
  98. package/src/utils/tailwindcss.test.ts +0 -26
  99. package/src/utils/tailwindcss.ts +0 -7
  100. package/src/utils/vdom.ts +0 -31
@@ -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
- export const FormFieldTypes = {
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 interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
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?: TValueType[];
19
+ [__valueType]?: TValueType;
20
20
  }
21
21
 
22
- export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
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 typeof FormFieldTypes.String
38
+ export type GetFormFieldValue<TType, TValueType = unknown> = TType extends 'string'
39
39
  ? string
40
- : TType extends typeof FormFieldTypes.Number
40
+ : TType extends 'number'
41
41
  ? number
42
- : TType extends typeof FormFieldTypes.Boolean
42
+ : TType extends 'boolean'
43
43
  ? boolean
44
- : TType extends typeof FormFieldTypes.Object
45
- ? object
46
- : TType extends typeof FormFieldTypes.Date
47
- ? Date
48
- : never;
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 === FormFieldTypes.String && (definition.trim ?? true)
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;
@@ -1,4 +1,3 @@
1
- export * from './composition';
2
1
  export * from './FormController';
3
2
  export * from './utils';
4
3
  export * from './validation';
@@ -1,84 +1,109 @@
1
- import { FormFieldTypes } from './FormController';
2
1
  import type { FormFieldDefinition } from './FormController';
3
2
 
4
- export function booleanInput(
5
- defaultValue?: boolean,
6
- options: { rules?: string } = {},
7
- ): FormFieldDefinition<typeof FormFieldTypes.Boolean> {
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: FormFieldTypes.Boolean,
14
+ type: 'date',
11
15
  rules: options.rules,
12
16
  };
13
17
  }
14
18
 
15
- export function dateInput(
16
- defaultValue?: Date,
19
+ export function enumInput<const T extends string>(
20
+ values: T[],
21
+ defaultValue?: T,
17
22
  options: { rules?: string } = {},
18
- ): FormFieldDefinition<typeof FormFieldTypes.Date> {
23
+ ): FormFieldDefinition<'enum', string, T> {
19
24
  return {
20
25
  default: defaultValue,
21
- type: FormFieldTypes.Date,
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: FormFieldTypes.Boolean,
35
+ type: 'boolean',
32
36
  rules: 'required',
33
37
  };
34
38
  }
35
39
 
36
- export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
40
+ export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<'date', 'required'> {
37
41
  return {
38
42
  default: defaultValue,
39
- type: FormFieldTypes.Date,
43
+ type: 'date',
40
44
  rules: 'required',
41
45
  };
42
46
  }
43
47
 
44
- export function requiredNumberInput(
45
- defaultValue?: number,
46
- ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
48
+ export function requiredEnumInput<T extends string>(
49
+ values: T[],
50
+ defaultValue?: T,
51
+ ): FormFieldDefinition<'enum', 'required', T> {
47
52
  return {
48
53
  default: defaultValue,
49
- type: FormFieldTypes.Number,
54
+ type: 'enum',
50
55
  rules: 'required',
56
+ values,
51
57
  };
52
58
  }
53
59
 
54
- export function requiredStringInput(
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: FormFieldTypes.String,
63
+ type: 'number',
60
64
  rules: 'required',
61
65
  };
62
66
  }
63
67
 
64
- export function numberInput(
65
- defaultValue?: number,
66
- options: { rules?: string } = {},
67
- ): FormFieldDefinition<typeof FormFieldTypes.Number> {
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: FormFieldTypes.Number,
87
+ type: 'number',
71
88
  rules: options.rules,
72
89
  };
73
90
  }
74
91
 
75
- export function stringInput(
76
- defaultValue?: string,
92
+ export function objectInput<T extends object>(
93
+ defaultValue?: T,
77
94
  options: { rules?: string } = {},
78
- ): FormFieldDefinition<typeof FormFieldTypes.String> {
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: FormFieldTypes.String,
106
+ type: 'string',
82
107
  rules: options.rules,
83
108
  };
84
109
  }
@@ -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
@@ -1,12 +1,29 @@
1
+ @import 'tailwindcss';
2
+
3
+ @plugin '@tailwindcss/forms';
4
+ @plugin '@tailwindcss/typography';
5
+
1
6
  @source './';
2
- @source '../../plugin-local-first/src';
3
- @source '../../plugin-solid/src';
4
7
 
5
8
  @theme {
6
- --color-background: oklch(1 0 0);
7
9
  --color-primary: oklch(0.205 0 0);
8
- --color-danger: oklch(0.577 0.245 27.325);
9
- --color-accent: oklch(0.97 0 0);
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);
16
+ --color-primary-600: var(--color-primary);
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;
10
27
  }
11
28
 
12
29
  .clickable {
@@ -39,3 +56,21 @@ button[data-markdown-action] {
39
56
  text-decoration: underline;
40
57
  font-weight: 500;
41
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
@@ -1,9 +1,11 @@
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';
5
- import type { LangProvider } from './Lang';
6
+ import settings from './settings';
6
7
  import { translate, translateWithDefault } from './utils';
8
+ import type { LangProvider } from './Lang';
7
9
 
8
10
  export { Lang, translate, translateWithDefault };
9
11
  export type { LangProvider };
@@ -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
  });
@@ -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 class="text-base font-semibold">
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>
@@ -0,0 +1,10 @@
1
+ import { defineSettings } from '@aerogel/core/services';
2
+
3
+ import Language from './Language.vue';
4
+
5
+ export default defineSettings([
6
+ {
7
+ priority: 100,
8
+ component: Language,
9
+ },
10
+ ]);
@@ -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',
@@ -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('trigger', () => counter++);
13
+ Events.on('application-mounted', () => counter++);
14
14
 
15
15
  // Act
16
- await Events.emit('trigger');
17
- await Events.emit('trigger');
18
- await Events.emit('trigger');
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('trigger', () => storage.push('second'));
29
- Events.on('trigger', { priority: EventListenerPriorities.Low }, () => storage.push('third'));
30
- Events.on('trigger', { priority: EventListenerPriorities.High }, () => storage.push('first'));
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('trigger');
33
+ await Events.emit('application-mounted');
34
34
 
35
35
  // Assert
36
36
  expect(storage).toEqual(['first', 'second', 'third']);
@@ -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
- this.on(event as string, listener as EventListener));
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(
@@ -1,4 +1,4 @@
1
- import type { App as VueApp } from 'vue';
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';
@@ -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';
@@ -29,7 +30,7 @@ export type DefaultServices = typeof defaultServices;
29
30
 
30
31
  export interface Services extends DefaultServices {}
31
32
 
32
- export async function bootServices(app: VueApp, services: Record<string, Service>): Promise<void> {
33
+ export async function bootServices(app: AppInstance, services: Record<string, Service>): Promise<void> {
33
34
  await Promise.all(
34
35
  Object.entries(services).map(async ([name, service]) => {
35
36
  await service
@@ -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,6 +63,7 @@ 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
 
@@ -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
  });