@aerogel/core 0.0.0-next.9a1c5ba39a454b316eba36ec7bdf579fed3d95d2 → 0.0.0-next.9e0c0bbdcff5db68a1087ef53cbdc0f53299f6bb

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 (148) hide show
  1. package/dist/aerogel-core.d.ts +1615 -1452
  2. package/dist/aerogel-core.js +2505 -2003
  3. package/dist/aerogel-core.js.map +1 -1
  4. package/package.json +10 -3
  5. package/src/components/{AGAppLayout.vue → AppLayout.vue} +3 -3
  6. package/src/components/{AGAppModals.vue → AppModals.vue} +2 -3
  7. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +4 -9
  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 +41 -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 +14 -9
  17. package/src/components/contracts/PromptModal.ts +30 -0
  18. package/src/components/contracts/Select.ts +44 -0
  19. package/src/components/contracts/Toast.ts +13 -0
  20. package/src/components/contracts/index.ts +10 -1
  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/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +47 -43
  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 +113 -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 +37 -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 +5 -10
  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} +28 -21
  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 +91 -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 +25 -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 +30 -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} +7 -7
  73. package/src/components/ui/Toast.vue +42 -0
  74. package/src/components/ui/index.ts +30 -0
  75. package/src/errors/Errors.ts +9 -10
  76. package/src/forms/{Form.test.ts → FormController.test.ts} +2 -2
  77. package/src/forms/{Form.ts → FormController.ts} +5 -5
  78. package/src/forms/composition.ts +4 -4
  79. package/src/forms/index.ts +2 -2
  80. package/src/forms/utils.ts +2 -2
  81. package/src/index.css +54 -0
  82. package/src/lang/index.ts +4 -0
  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/index.ts +3 -0
  88. package/src/ui/UI.state.ts +10 -5
  89. package/src/ui/UI.ts +59 -53
  90. package/src/ui/index.ts +14 -14
  91. package/src/utils/classes.ts +49 -0
  92. package/src/utils/composition/forms.ts +14 -4
  93. package/src/utils/composition/state.ts +11 -2
  94. package/src/utils/index.ts +3 -1
  95. package/src/utils/types.ts +3 -0
  96. package/src/utils/vue.ts +22 -128
  97. package/src/components/AGAppSnackbars.vue +0 -13
  98. package/src/components/composition.ts +0 -23
  99. package/src/components/constants.ts +0 -8
  100. package/src/components/contracts/shared.ts +0 -9
  101. package/src/components/forms/AGButton.vue +0 -44
  102. package/src/components/forms/AGCheckbox.vue +0 -42
  103. package/src/components/forms/AGInput.vue +0 -42
  104. package/src/components/forms/AGSelect.story.vue +0 -46
  105. package/src/components/forms/AGSelect.vue +0 -54
  106. package/src/components/forms/index.ts +0 -5
  107. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  108. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  109. package/src/components/headless/forms/AGHeadlessInput.ts +0 -41
  110. package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
  111. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  112. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  113. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  114. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -31
  115. package/src/components/headless/forms/AGHeadlessSelectOptions.vue +0 -19
  116. package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +0 -25
  117. package/src/components/headless/forms/composition.ts +0 -10
  118. package/src/components/headless/forms/index.ts +0 -17
  119. package/src/components/headless/modals/AGHeadlessModal.ts +0 -33
  120. package/src/components/headless/modals/AGHeadlessModalContent.vue +0 -25
  121. package/src/components/headless/modals/index.ts +0 -5
  122. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  123. package/src/components/headless/snackbars/index.ts +0 -40
  124. package/src/components/lib/AGErrorMessage.vue +0 -16
  125. package/src/components/lib/AGLink.vue +0 -9
  126. package/src/components/lib/AGMarkdown.vue +0 -54
  127. package/src/components/lib/AGMeasured.vue +0 -16
  128. package/src/components/lib/AGProgressBar.vue +0 -55
  129. package/src/components/lib/index.ts +0 -6
  130. package/src/components/modals/AGAlertModal.ts +0 -18
  131. package/src/components/modals/AGAlertModal.vue +0 -14
  132. package/src/components/modals/AGConfirmModal.ts +0 -42
  133. package/src/components/modals/AGConfirmModal.vue +0 -27
  134. package/src/components/modals/AGErrorReportModal.vue +0 -54
  135. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  136. package/src/components/modals/AGLoadingModal.ts +0 -29
  137. package/src/components/modals/AGLoadingModal.vue +0 -15
  138. package/src/components/modals/AGModal.vue +0 -40
  139. package/src/components/modals/AGModalContext.ts +0 -8
  140. package/src/components/modals/AGModalTitle.vue +0 -9
  141. package/src/components/modals/AGPromptModal.ts +0 -41
  142. package/src/components/modals/AGPromptModal.vue +0 -35
  143. package/src/components/modals/index.ts +0 -16
  144. package/src/components/snackbars/AGSnackbar.vue +0 -36
  145. package/src/components/snackbars/index.ts +0 -3
  146. package/src/components/utils.ts +0 -63
  147. package/src/utils/tailwindcss.test.ts +0 -26
  148. 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
  ),
@@ -3,9 +3,9 @@ import { describe, expect, expectTypeOf, it } from 'vitest';
3
3
  import { useForm } from '@aerogel/core/forms/composition';
4
4
  import { numberInput, requiredStringInput } from '@aerogel/core/forms/utils';
5
5
 
6
- import { FormFieldTypes } from './Form';
6
+ import { FormFieldTypes } from './FormController';
7
7
 
8
- describe('Form', () => {
8
+ describe('FormController', () => {
9
9
 
10
10
  it('defines magic fields', () => {
11
11
  const form = useForm({
@@ -47,12 +47,12 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
47
47
  ? Date
48
48
  : never;
49
49
 
50
- const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
50
+ const validForms: WeakMap<FormController, ComputedRef<boolean>> = new WeakMap();
51
51
 
52
52
  export type SubmitFormListener = () => unknown;
53
53
  export type FocusFormListener = (input: string) => unknown;
54
54
 
55
- export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
55
+ export default class FormController<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
56
56
 
57
57
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
58
58
 
@@ -207,10 +207,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
207
207
  return {} as FormData<Fields>;
208
208
  }
209
209
 
210
- const data = Object.entries(fields).reduce((formData, [name, definition]) => {
211
- formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
210
+ const data = Object.entries(fields).reduce((initialData, [name, definition]) => {
211
+ initialData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
212
212
 
213
- return formData;
213
+ return initialData;
214
214
  }, {} as FormData<Fields>);
215
215
 
216
216
  return reactive(data) as FormData<Fields>;
@@ -1,6 +1,6 @@
1
- import Form from '@aerogel/core/forms/Form';
2
- import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/Form';
1
+ import FormController from '@aerogel/core/forms/FormController';
2
+ import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
3
3
 
4
- export function useForm<const T extends FormFieldDefinitions>(fields: T): Form<T> & FormData<T> {
5
- return new Form(fields) as Form<T> & FormData<T>;
4
+ export function useForm<const T extends FormFieldDefinitions>(fields: T): FormController<T> & FormData<T> {
5
+ return new FormController(fields) as FormController<T> & FormData<T>;
6
6
  }
@@ -1,5 +1,5 @@
1
1
  export * from './composition';
2
- export * from './Form';
2
+ export * from './FormController';
3
3
  export * from './utils';
4
4
  export * from './validation';
5
- export { default as Form } from './Form';
5
+ 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,
package/src/index.css ADDED
@@ -0,0 +1,54 @@
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
+ }
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
  });
@@ -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
 
@@ -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,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
 
@@ -4,7 +4,7 @@ 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;
@@ -12,14 +12,19 @@ export interface Modal<T = unknown> {
12
12
  afterClose: Promise<T | undefined>;
13
13
  }
14
14
 
15
+ export interface UIModalContext {
16
+ modal: UIModal;
17
+ childIndex?: number;
18
+ }
19
+
15
20
  export interface ModalComponent<
16
21
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- Properties extends Record<string, unknown> = Record<string, unknown>,
22
+ Properties extends object = object,
18
23
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
24
  Result = unknown,
20
25
  > {}
21
26
 
22
- export interface Snackbar {
27
+ export interface UIToast {
23
28
  id: string;
24
29
  component: Component;
25
30
  properties: Record<string, unknown>;
@@ -28,8 +33,8 @@ export interface Snackbar {
28
33
  export default defineServiceState({
29
34
  name: 'ui',
30
35
  initialState: {
31
- modals: [] as Modal[],
32
- snackbars: [] as Snackbar[],
36
+ modals: [] as UIModal[],
37
+ toasts: [] as UIToast[],
33
38
  layout: getCurrentLayout(),
34
39
  },
35
40
  computed: {
package/src/ui/UI.ts CHANGED
@@ -6,18 +6,16 @@ import type { ObjectValues } from '@noeldemartin/utils';
6
6
  import App from '@aerogel/core/services/App';
7
7
  import Events from '@aerogel/core/services/Events';
8
8
  import type { AcceptRefs } from '@aerogel/core/utils';
9
- import type { Color } from '@aerogel/core/components/constants';
10
- import type { SnackbarAction, SnackbarColor } from '@aerogel/core/components/headless/snackbars';
11
- import type {
12
- AGAlertModalProps,
13
- AGConfirmModalProps,
14
- AGLoadingModalProps,
15
- AGPromptModalProps,
16
- } from '@aerogel/core/components';
9
+ import type { AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
10
+ import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
11
+ import type { ConfirmModalCheckboxes, ConfirmModalProps } from '@aerogel/core/components/contracts/ConfirmModal';
12
+ import type { LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
13
+ import type { PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
14
+ import type { ToastAction, ToastVariant } from '@aerogel/core/components/contracts/Toast';
17
15
 
18
16
  import Service from './UI.state';
19
17
  import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
20
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
18
+ import type { ModalComponent, UIModal, UIToast } from './UI.state';
21
19
 
22
20
  interface ModalCallbacks<T = unknown> {
23
21
  willClose(result: T | undefined): void;
@@ -34,19 +32,18 @@ export const UIComponents = {
34
32
  ErrorReportModal: 'error-report-modal',
35
33
  LoadingModal: 'loading-modal',
36
34
  PromptModal: 'prompt-modal',
37
- Snackbar: 'snackbar',
35
+ Toast: 'toast',
38
36
  StartupCrash: 'startup-crash',
37
+ RouterLink: 'router-link',
39
38
  } as const;
40
39
 
41
40
  export type UIComponent = ObjectValues<typeof UIComponents>;
42
41
 
43
- export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
44
-
45
42
  export type ConfirmOptions = AcceptRefs<{
46
43
  acceptText?: string;
47
- acceptColor?: Color;
44
+ acceptVariant?: ButtonVariant;
48
45
  cancelText?: string;
49
- cancelColor?: Color;
46
+ cancelVariant?: ButtonVariant;
50
47
  actions?: Record<string, () => unknown>;
51
48
  required?: boolean;
52
49
  }>;
@@ -57,7 +54,8 @@ export type LoadingOptions = AcceptRefs<{
57
54
  progress?: number;
58
55
  }>;
59
56
 
60
- export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
57
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
58
+ extends ConfirmOptions {
61
59
  checkboxes?: T;
62
60
  }
63
61
 
@@ -66,16 +64,16 @@ export type PromptOptions = AcceptRefs<{
66
64
  defaultValue?: string;
67
65
  placeholder?: string;
68
66
  acceptText?: string;
69
- acceptColor?: Color;
67
+ acceptVariant?: ButtonVariant;
70
68
  cancelText?: string;
71
- cancelColor?: Color;
69
+ cancelVariant?: ButtonVariant;
72
70
  trim?: boolean;
73
71
  }>;
74
72
 
75
- export interface ShowSnackbarOptions {
73
+ export interface ToastOptions {
76
74
  component?: Component;
77
- color?: SnackbarColor;
78
- actions?: SnackbarAction[];
75
+ variant?: ToastVariant;
76
+ actions?: ToastAction[];
79
77
  }
80
78
 
81
79
  export class UIService extends Service {
@@ -83,14 +81,18 @@ export class UIService extends Service {
83
81
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
84
82
  private components: Partial<Record<UIComponent, Component>> = {};
85
83
 
84
+ public resolveComponent(name: UIComponent): Component | null {
85
+ return this.components[name] ?? null;
86
+ }
87
+
86
88
  public requireComponent(name: UIComponent): Component {
87
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
89
+ return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
88
90
  }
89
91
 
90
92
  public alert(message: string): void;
91
93
  public alert(title: string, message: string): void;
92
94
  public alert(messageOrTitle: string, message?: string): void {
93
- const getProperties = (): AGAlertModalProps => {
95
+ const getProperties = (): AlertModalProps => {
94
96
  if (typeof message !== 'string') {
95
97
  return { message: messageOrTitle };
96
98
  }
@@ -101,14 +103,17 @@ export class UIService extends Service {
101
103
  };
102
104
  };
103
105
 
104
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
106
+ this.openModal<ModalComponent<AlertModalProps>>(
107
+ this.requireComponent(UIComponents.AlertModal),
108
+ getProperties(),
109
+ );
105
110
  }
106
111
 
107
112
  /* eslint-disable max-len */
108
113
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
109
114
  public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
110
- public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
111
- public async confirm<T extends ConfirmCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
115
+ public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
116
+ public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
112
117
  /* eslint-enable max-len */
113
118
 
114
119
  public async confirm(
@@ -116,7 +121,7 @@ export class UIService extends Service {
116
121
  messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
117
122
  options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
118
123
  ): Promise<boolean | [boolean, Record<string, boolean>]> {
119
- const getProperties = (): AGConfirmModalProps => {
124
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
120
125
  if (typeof messageOrOptions !== 'string') {
121
126
  return {
122
127
  ...(messageOrOptions ?? {}),
@@ -132,10 +137,17 @@ export class UIService extends Service {
132
137
  required: !!options?.required,
133
138
  };
134
139
  };
140
+
141
+ type ConfirmModalComponent = ModalComponent<
142
+ AcceptRefs<ConfirmModalProps>,
143
+ boolean | [boolean, Record<string, boolean>]
144
+ >;
145
+
135
146
  const properties = getProperties();
136
- const modal = await this.openModal<
137
- ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
138
- >(this.requireComponent(UIComponents.ConfirmModal), properties);
147
+ const modal = await this.openModal<ConfirmModalComponent>(
148
+ this.requireComponent(UIComponents.ConfirmModal),
149
+ properties,
150
+ );
139
151
  const result = await modal.beforeClose;
140
152
 
141
153
  const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
@@ -174,22 +186,22 @@ export class UIService extends Service {
174
186
  options?: PromptOptions,
175
187
  ): Promise<string | null> {
176
188
  const trim = options?.trim ?? true;
177
- const getProperties = (): AGPromptModalProps => {
189
+ const getProperties = (): PromptModalProps => {
178
190
  if (typeof messageOrOptions !== 'string') {
179
191
  return {
180
192
  message: messageOrTitle,
181
193
  ...(messageOrOptions ?? {}),
182
- } as AGPromptModalProps;
194
+ } as PromptModalProps;
183
195
  }
184
196
 
185
197
  return {
186
198
  title: messageOrTitle,
187
199
  message: messageOrOptions,
188
200
  ...(options ?? {}),
189
- } as AGPromptModalProps;
201
+ } as PromptModalProps;
190
202
  };
191
203
 
192
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
204
+ const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
193
205
  this.requireComponent(UIComponents.PromptModal),
194
206
  getProperties(),
195
207
  );
@@ -207,7 +219,7 @@ export class UIService extends Service {
207
219
  operation?: Promise<T> | (() => T),
208
220
  ): Promise<T> {
209
221
  const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
210
- const processArgs = (): { operationPromise: Promise<T>; props?: AGLoadingModalProps } => {
222
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
211
223
  if (typeof operationOrMessageOrOptions === 'string') {
212
224
  return {
213
225
  props: { message: operationOrMessageOrOptions },
@@ -229,7 +241,9 @@ export class UIService extends Service {
229
241
  const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
230
242
 
231
243
  try {
232
- const [result] = await Promise.all([operationPromise, after({ seconds: 1 })]);
244
+ const result = await operationPromise;
245
+
246
+ await after({ ms: 500 });
233
247
 
234
248
  return result;
235
249
  } finally {
@@ -237,23 +251,15 @@ export class UIService extends Service {
237
251
  }
238
252
  }
239
253
 
240
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
241
- const snackbar: Snackbar = {
254
+ public toast(message: string, options: ToastOptions = {}): void {
255
+ const { component, ...otherOptions } = options;
256
+ const toast: UIToast = {
242
257
  id: uuid(),
243
- properties: { message, ...options },
244
- component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
258
+ properties: { message, ...otherOptions },
259
+ component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
245
260
  };
246
261
 
247
- this.setState('snackbars', this.snackbars.concat(snackbar));
248
-
249
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
250
- }
251
-
252
- public hideSnackbar(id: string): void {
253
- this.setState(
254
- 'snackbars',
255
- this.snackbars.filter((snackbar) => snackbar.id !== id),
256
- );
262
+ this.setState('toasts', this.toasts.concat(toast));
257
263
  }
258
264
 
259
265
  public registerComponent(name: UIComponent, component: Component): void {
@@ -263,10 +269,10 @@ export class UIService extends Service {
263
269
  public async openModal<TModalComponent extends ModalComponent>(
264
270
  component: TModalComponent,
265
271
  properties?: ModalProperties<TModalComponent>,
266
- ): Promise<Modal<ModalResult<TModalComponent>>> {
272
+ ): Promise<UIModal<ModalResult<TModalComponent>>> {
267
273
  const id = uuid();
268
274
  const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
269
- const modal: Modal<ModalResult<TModalComponent>> = {
275
+ const modal: UIModal<ModalResult<TModalComponent>> = {
270
276
  id,
271
277
  properties: properties ?? {},
272
278
  component: markRaw(component),
@@ -382,8 +388,8 @@ declare module '@aerogel/core/services/Events' {
382
388
  'close-modal': { id: string; result?: unknown };
383
389
  'hide-modal': { id: string };
384
390
  'hide-overlays-backdrop': void;
385
- 'modal-closed': { modal: Modal; result?: unknown };
386
- 'modal-will-close': { modal: Modal; result?: unknown };
391
+ 'modal-closed': { modal: UIModal; result?: unknown };
392
+ 'modal-will-close': { modal: UIModal; result?: unknown };
387
393
  'show-modal': { id: string };
388
394
  'show-overlays-backdrop': void;
389
395
  }