@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.
Files changed (157) hide show
  1. package/dist/aerogel-core.d.ts +1699 -2701
  2. package/dist/aerogel-core.js +2544 -2037
  3. package/dist/aerogel-core.js.map +1 -1
  4. package/package.json +14 -6
  5. package/src/components/{AGAppLayout.vue → AppLayout.vue} +3 -3
  6. package/src/components/{AGAppModals.vue → AppModals.vue} +2 -3
  7. package/src/components/AppOverlays.vue +9 -0
  8. package/src/components/AppToasts.vue +16 -0
  9. package/src/components/contracts/AlertModal.ts +4 -0
  10. package/src/components/contracts/Button.ts +16 -0
  11. package/src/components/contracts/ConfirmModal.ts +42 -0
  12. package/src/components/contracts/DropdownMenu.ts +20 -0
  13. package/src/components/{modals/AGErrorReportModal.ts → contracts/ErrorReportModal.ts} +3 -23
  14. package/src/components/contracts/Input.ts +26 -0
  15. package/src/components/contracts/LoadingModal.ts +22 -0
  16. package/src/components/contracts/Modal.ts +21 -0
  17. package/src/components/contracts/PromptModal.ts +31 -0
  18. package/src/components/contracts/Select.ts +45 -0
  19. package/src/components/contracts/Toast.ts +13 -0
  20. package/src/components/contracts/index.ts +11 -0
  21. package/src/components/headless/HeadlessButton.vue +51 -0
  22. package/src/components/headless/HeadlessInput.vue +59 -0
  23. package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +6 -7
  24. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +2 -6
  25. package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +16 -25
  26. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +2 -6
  27. package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +9 -12
  28. package/src/components/headless/HeadlessModal.vue +57 -0
  29. package/src/components/headless/HeadlessModalContent.vue +30 -0
  30. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  31. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  32. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  33. package/src/components/headless/HeadlessSelect.vue +120 -0
  34. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +3 -4
  35. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  36. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  37. package/src/components/headless/HeadlessSelectOptions.vue +42 -0
  38. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  39. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  40. package/src/components/headless/HeadlessToast.vue +18 -0
  41. package/src/components/headless/HeadlessToastAction.vue +13 -0
  42. package/src/components/headless/index.ts +19 -3
  43. package/src/components/index.ts +6 -11
  44. package/src/components/ui/AdvancedOptions.vue +18 -0
  45. package/src/components/ui/AlertModal.vue +13 -0
  46. package/src/components/ui/Button.vue +98 -0
  47. package/src/components/ui/Checkbox.vue +56 -0
  48. package/src/components/ui/ConfirmModal.vue +42 -0
  49. package/src/components/ui/DropdownMenu.vue +32 -0
  50. package/src/components/ui/DropdownMenuOption.vue +14 -0
  51. package/src/components/ui/DropdownMenuOptions.vue +27 -0
  52. package/src/components/ui/EditableContent.vue +82 -0
  53. package/src/components/ui/ErrorMessage.vue +15 -0
  54. package/src/components/ui/ErrorReportModal.vue +62 -0
  55. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -22
  56. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  57. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  58. package/src/components/ui/Input.vue +56 -0
  59. package/src/components/ui/Link.vue +12 -0
  60. package/src/components/ui/LoadingModal.vue +32 -0
  61. package/src/components/ui/Markdown.vue +69 -0
  62. package/src/components/ui/Modal.vue +121 -0
  63. package/src/components/{modals/AGModalContext.vue → ui/ModalContext.vue} +7 -9
  64. package/src/components/ui/ProgressBar.vue +51 -0
  65. package/src/components/ui/PromptModal.vue +35 -0
  66. package/src/components/ui/Select.vue +27 -0
  67. package/src/components/ui/SelectLabel.vue +17 -0
  68. package/src/components/ui/SelectOption.vue +29 -0
  69. package/src/components/ui/SelectOptions.vue +35 -0
  70. package/src/components/ui/SelectTrigger.vue +29 -0
  71. package/src/components/ui/SettingsModal.vue +15 -0
  72. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  73. package/src/components/ui/Toast.vue +42 -0
  74. package/src/components/ui/index.ts +30 -0
  75. package/src/directives/index.ts +6 -0
  76. package/src/errors/Errors.ts +9 -10
  77. package/src/forms/{Form.test.ts → FormController.test.ts} +29 -4
  78. package/src/forms/{Form.ts → FormController.ts} +20 -11
  79. package/src/forms/index.ts +2 -3
  80. package/src/forms/utils.ts +23 -2
  81. package/src/index.css +72 -0
  82. package/src/lang/index.ts +5 -1
  83. package/src/lang/settings/Language.vue +48 -0
  84. package/src/lang/settings/index.ts +10 -0
  85. package/src/services/App.state.ts +11 -1
  86. package/src/services/App.ts +9 -1
  87. package/src/services/Events.test.ts +8 -8
  88. package/src/services/Events.ts +2 -8
  89. package/src/services/index.ts +4 -1
  90. package/src/ui/UI.state.ts +13 -6
  91. package/src/ui/UI.ts +70 -75
  92. package/src/ui/index.ts +14 -14
  93. package/src/utils/classes.ts +49 -0
  94. package/src/utils/composition/events.ts +2 -4
  95. package/src/utils/composition/forms.ts +20 -4
  96. package/src/utils/composition/state.ts +11 -2
  97. package/src/utils/index.ts +3 -1
  98. package/src/utils/types.ts +3 -0
  99. package/src/utils/vue.ts +22 -133
  100. package/src/components/AGAppOverlays.vue +0 -41
  101. package/src/components/AGAppSnackbars.vue +0 -13
  102. package/src/components/composition.ts +0 -23
  103. package/src/components/constants.ts +0 -8
  104. package/src/components/forms/AGButton.vue +0 -44
  105. package/src/components/forms/AGCheckbox.vue +0 -42
  106. package/src/components/forms/AGInput.vue +0 -42
  107. package/src/components/forms/AGSelect.story.vue +0 -46
  108. package/src/components/forms/AGSelect.vue +0 -60
  109. package/src/components/forms/index.ts +0 -5
  110. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  111. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  112. package/src/components/headless/forms/AGHeadlessInput.ts +0 -41
  113. package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
  114. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  115. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  116. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  117. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  118. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  119. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  120. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  121. package/src/components/headless/forms/composition.ts +0 -10
  122. package/src/components/headless/forms/index.ts +0 -18
  123. package/src/components/headless/modals/AGHeadlessModal.ts +0 -36
  124. package/src/components/headless/modals/AGHeadlessModal.vue +0 -92
  125. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -32
  126. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -23
  127. package/src/components/headless/modals/index.ts +0 -4
  128. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  129. package/src/components/headless/snackbars/index.ts +0 -40
  130. package/src/components/interfaces.ts +0 -24
  131. package/src/components/lib/AGErrorMessage.vue +0 -16
  132. package/src/components/lib/AGLink.vue +0 -9
  133. package/src/components/lib/AGMarkdown.vue +0 -54
  134. package/src/components/lib/AGMeasured.vue +0 -16
  135. package/src/components/lib/AGProgressBar.vue +0 -55
  136. package/src/components/lib/index.ts +0 -6
  137. package/src/components/modals/AGAlertModal.ts +0 -18
  138. package/src/components/modals/AGAlertModal.vue +0 -14
  139. package/src/components/modals/AGConfirmModal.ts +0 -42
  140. package/src/components/modals/AGConfirmModal.vue +0 -26
  141. package/src/components/modals/AGErrorReportModal.vue +0 -54
  142. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  143. package/src/components/modals/AGLoadingModal.ts +0 -29
  144. package/src/components/modals/AGLoadingModal.vue +0 -15
  145. package/src/components/modals/AGModal.ts +0 -11
  146. package/src/components/modals/AGModal.vue +0 -42
  147. package/src/components/modals/AGModalContext.ts +0 -8
  148. package/src/components/modals/AGModalTitle.vue +0 -9
  149. package/src/components/modals/AGPromptModal.ts +0 -41
  150. package/src/components/modals/AGPromptModal.vue +0 -34
  151. package/src/components/modals/index.ts +0 -17
  152. package/src/components/snackbars/AGSnackbar.vue +0 -36
  153. package/src/components/snackbars/index.ts +0 -3
  154. package/src/components/utils.ts +0 -13
  155. package/src/forms/composition.ts +0 -6
  156. package/src/utils/tailwindcss.test.ts +0 -26
  157. package/src/utils/tailwindcss.ts +0 -7
@@ -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<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
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.showSnackbar(
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
- color: Colors.Danger,
77
+ variant: 'danger',
79
78
  actions: [
80
79
  {
81
- text: translateWithDefault('errors.viewDetails', 'View details'),
80
+ label: translateWithDefault('errors.viewDetails', 'View details'),
82
81
  dismiss: true,
83
- handler: () =>
84
- UI.openModal<ModalComponent<AGErrorReportModalProps>>(
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 { useForm } from '@aerogel/core/forms/composition';
4
- import { numberInput, requiredStringInput } from '@aerogel/core/forms/utils';
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 './Form';
15
+ import { FormFieldTypes } from './FormController';
7
16
 
8
- describe('Form', () => {
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 interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
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<Form, ComputedRef<boolean>> = new 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 Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
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((formData, [name, definition]) => {
211
- formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
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 formData;
222
+ return initialData;
214
223
  }, {} as FormData<Fields>);
215
224
 
216
225
  return reactive(data) as FormData<Fields>;
@@ -1,5 +1,4 @@
1
- export * from './composition';
2
- export * from './Form';
1
+ export * from './FormController';
3
2
  export * from './utils';
4
3
  export * from './validation';
5
- export { default as Form } from './Form';
4
+ export { default as FormController } from './FormController';
@@ -1,5 +1,5 @@
1
- import { FormFieldTypes } from './Form';
2
- import type { FormFieldDefinition } from './Form';
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 '@vue/runtime-core' {
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>
@@ -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(
@@ -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 '@vue/runtime-core' {
70
+ declare module 'vue' {
68
71
  interface ComponentCustomProperties extends Services {}
69
72
  }
@@ -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 Modal<T = unknown> {
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 Record<string, unknown> = Record<string, unknown>,
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 Snackbar {
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 Modal[],
32
- snackbars: [] as Snackbar[],
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
  });