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

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 (159) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +1309 -1587
  3. package/dist/aerogel-core.js +2637 -1991
  4. package/dist/aerogel-core.js.map +1 -1
  5. package/package.json +10 -2
  6. package/src/components/AppLayout.vue +14 -0
  7. package/src/components/{AGAppModals.vue → AppModals.vue} +2 -3
  8. package/src/components/AppOverlays.vue +9 -0
  9. package/src/components/AppToasts.vue +16 -0
  10. package/src/components/contracts/AlertModal.ts +19 -0
  11. package/src/components/contracts/Button.ts +16 -0
  12. package/src/components/contracts/ConfirmModal.ts +48 -0
  13. package/src/components/contracts/DropdownMenu.ts +25 -0
  14. package/src/components/contracts/ErrorReportModal.ts +33 -0
  15. package/src/components/contracts/Input.ts +26 -0
  16. package/src/components/contracts/LoadingModal.ts +26 -0
  17. package/src/components/contracts/Modal.ts +15 -10
  18. package/src/components/contracts/PromptModal.ts +34 -0
  19. package/src/components/contracts/Select.ts +45 -0
  20. package/src/components/contracts/Toast.ts +15 -0
  21. package/src/components/contracts/index.ts +10 -1
  22. package/src/components/headless/HeadlessButton.vue +51 -0
  23. package/src/components/headless/HeadlessInput.vue +59 -0
  24. package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +6 -7
  25. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +2 -6
  26. package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +16 -25
  27. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +2 -6
  28. package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +9 -12
  29. package/src/components/headless/HeadlessModal.vue +57 -0
  30. package/src/components/headless/HeadlessModalContent.vue +30 -0
  31. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  32. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  33. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  34. package/src/components/headless/HeadlessSelect.vue +120 -0
  35. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +3 -4
  36. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  37. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  38. package/src/components/headless/HeadlessSelectOptions.vue +42 -0
  39. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  40. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  41. package/src/components/headless/HeadlessToast.vue +18 -0
  42. package/src/components/headless/HeadlessToastAction.vue +13 -0
  43. package/src/components/headless/index.ts +19 -3
  44. package/src/components/index.ts +5 -10
  45. package/src/components/ui/AdvancedOptions.vue +18 -0
  46. package/src/components/ui/AlertModal.vue +17 -0
  47. package/src/components/ui/Button.vue +100 -0
  48. package/src/components/ui/Checkbox.vue +56 -0
  49. package/src/components/ui/ConfirmModal.vue +50 -0
  50. package/src/components/ui/DropdownMenu.vue +32 -0
  51. package/src/components/ui/DropdownMenuOption.vue +22 -0
  52. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  53. package/src/components/ui/EditableContent.vue +82 -0
  54. package/src/components/ui/ErrorLogs.vue +19 -0
  55. package/src/components/ui/ErrorLogsModal.vue +48 -0
  56. package/src/components/ui/ErrorMessage.vue +15 -0
  57. package/src/components/ui/ErrorReportModal.vue +73 -0
  58. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +28 -21
  59. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  60. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  61. package/src/components/ui/Input.vue +56 -0
  62. package/src/components/ui/Link.vue +12 -0
  63. package/src/components/ui/LoadingModal.vue +34 -0
  64. package/src/components/ui/Markdown.vue +85 -0
  65. package/src/components/ui/Modal.vue +123 -0
  66. package/src/components/{modals/AGModalContext.vue → ui/ModalContext.vue} +8 -9
  67. package/src/components/ui/ProgressBar.vue +51 -0
  68. package/src/components/ui/PromptModal.vue +38 -0
  69. package/src/components/ui/Select.vue +27 -0
  70. package/src/components/ui/SelectLabel.vue +17 -0
  71. package/src/components/ui/SelectOption.vue +29 -0
  72. package/src/components/ui/SelectOptions.vue +35 -0
  73. package/src/components/ui/SelectTrigger.vue +29 -0
  74. package/src/components/ui/SettingsModal.vue +15 -0
  75. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +7 -7
  76. package/src/components/ui/Toast.vue +46 -0
  77. package/src/components/ui/index.ts +32 -0
  78. package/src/directives/measure.ts +11 -5
  79. package/src/errors/Errors.ts +22 -20
  80. package/src/forms/{Form.test.ts → FormController.test.ts} +32 -9
  81. package/src/forms/{Form.ts → FormController.ts} +28 -27
  82. package/src/forms/index.ts +2 -3
  83. package/src/forms/utils.ts +35 -35
  84. package/src/index.css +73 -0
  85. package/src/lang/index.ts +4 -0
  86. package/src/lang/settings/Language.vue +48 -0
  87. package/src/lang/settings/index.ts +10 -0
  88. package/src/services/App.state.ts +11 -1
  89. package/src/services/App.ts +9 -1
  90. package/src/services/Events.test.ts +8 -8
  91. package/src/services/Events.ts +2 -8
  92. package/src/services/index.ts +3 -0
  93. package/src/ui/UI.state.ts +7 -12
  94. package/src/ui/UI.ts +115 -106
  95. package/src/ui/index.ts +23 -24
  96. package/src/utils/classes.ts +41 -0
  97. package/src/utils/composition/events.ts +2 -4
  98. package/src/utils/composition/forms.ts +20 -4
  99. package/src/utils/composition/state.ts +11 -2
  100. package/src/utils/index.ts +3 -1
  101. package/src/utils/types.ts +3 -0
  102. package/src/utils/vue.ts +28 -129
  103. package/src/components/AGAppLayout.vue +0 -16
  104. package/src/components/AGAppOverlays.vue +0 -41
  105. package/src/components/AGAppSnackbars.vue +0 -13
  106. package/src/components/composition.ts +0 -23
  107. package/src/components/constants.ts +0 -8
  108. package/src/components/contracts/shared.ts +0 -9
  109. package/src/components/forms/AGButton.vue +0 -44
  110. package/src/components/forms/AGCheckbox.vue +0 -42
  111. package/src/components/forms/AGInput.vue +0 -42
  112. package/src/components/forms/AGSelect.story.vue +0 -46
  113. package/src/components/forms/AGSelect.vue +0 -54
  114. package/src/components/forms/index.ts +0 -5
  115. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  116. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  117. package/src/components/headless/forms/AGHeadlessInput.ts +0 -41
  118. package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
  119. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  120. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  121. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  122. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -31
  123. package/src/components/headless/forms/AGHeadlessSelectOptions.vue +0 -19
  124. package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +0 -25
  125. package/src/components/headless/forms/composition.ts +0 -10
  126. package/src/components/headless/forms/index.ts +0 -17
  127. package/src/components/headless/modals/AGHeadlessModal.ts +0 -33
  128. package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
  129. package/src/components/headless/modals/AGHeadlessModalContent.vue +0 -25
  130. package/src/components/headless/modals/index.ts +0 -5
  131. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  132. package/src/components/headless/snackbars/index.ts +0 -40
  133. package/src/components/lib/AGErrorMessage.vue +0 -16
  134. package/src/components/lib/AGLink.vue +0 -9
  135. package/src/components/lib/AGMarkdown.vue +0 -54
  136. package/src/components/lib/AGMeasured.vue +0 -16
  137. package/src/components/lib/AGProgressBar.vue +0 -55
  138. package/src/components/lib/index.ts +0 -6
  139. package/src/components/modals/AGAlertModal.ts +0 -18
  140. package/src/components/modals/AGAlertModal.vue +0 -14
  141. package/src/components/modals/AGConfirmModal.ts +0 -42
  142. package/src/components/modals/AGConfirmModal.vue +0 -27
  143. package/src/components/modals/AGErrorReportModal.ts +0 -49
  144. package/src/components/modals/AGErrorReportModal.vue +0 -54
  145. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  146. package/src/components/modals/AGLoadingModal.ts +0 -29
  147. package/src/components/modals/AGLoadingModal.vue +0 -15
  148. package/src/components/modals/AGModal.vue +0 -40
  149. package/src/components/modals/AGModalContext.ts +0 -8
  150. package/src/components/modals/AGModalTitle.vue +0 -9
  151. package/src/components/modals/AGPromptModal.ts +0 -41
  152. package/src/components/modals/AGPromptModal.vue +0 -35
  153. package/src/components/modals/index.ts +0 -16
  154. package/src/components/snackbars/AGSnackbar.vue +0 -36
  155. package/src/components/snackbars/index.ts +0 -3
  156. package/src/components/utils.ts +0 -63
  157. package/src/forms/composition.ts +0 -6
  158. package/src/utils/tailwindcss.test.ts +0 -26
  159. package/src/utils/tailwindcss.ts +0 -7
@@ -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,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,22 +4,16 @@ 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
 
15
- export interface ModalComponent<
16
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- Properties extends Record<string, unknown> = Record<string, unknown>,
18
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
- Result = unknown,
20
- > {}
21
-
22
- export interface Snackbar {
16
+ export interface UIToast {
23
17
  id: string;
24
18
  component: Component;
25
19
  properties: Record<string, unknown>;
@@ -28,12 +22,13 @@ export interface Snackbar {
28
22
  export default defineServiceState({
29
23
  name: 'ui',
30
24
  initialState: {
31
- modals: [] as Modal[],
32
- snackbars: [] as Snackbar[],
25
+ modals: [] as UIModal[],
26
+ toasts: [] as UIToast[],
33
27
  layout: getCurrentLayout(),
34
28
  },
35
29
  computed: {
36
- mobile: ({ layout }) => layout === Layouts.Mobile,
37
30
  desktop: ({ layout }) => layout === Layouts.Desktop,
31
+ mobile: ({ layout }) => layout === Layouts.Mobile,
32
+ openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
38
33
  },
39
34
  });
package/src/ui/UI.ts CHANGED
@@ -1,52 +1,61 @@
1
1
  import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
2
2
  import { markRaw, nextTick } from 'vue';
3
+ import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
3
4
  import type { Component } from 'vue';
4
- import type { ObjectValues } from '@noeldemartin/utils';
5
+ import type { ClosureArgs } from '@noeldemartin/utils';
5
6
 
6
7
  import App from '@aerogel/core/services/App';
7
8
  import Events from '@aerogel/core/services/Events';
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
9
  import type {
12
- AGAlertModalProps,
13
- AGConfirmModalProps,
14
- AGLoadingModalProps,
15
- AGPromptModalProps,
16
- } from '@aerogel/core/components';
10
+ ConfirmModalCheckboxes,
11
+ ConfirmModalExpose,
12
+ ConfirmModalProps,
13
+ } from '@aerogel/core/components/contracts/ConfirmModal';
14
+ import type {
15
+ ErrorReportModalExpose,
16
+ ErrorReportModalProps,
17
+ } from '@aerogel/core/components/contracts/ErrorReportModal';
18
+ import type { AcceptRefs } from '@aerogel/core/utils';
19
+ import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
20
+ import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
21
+ import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
22
+ import type { PromptModalExpose, PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
23
+ import type { ToastAction, ToastExpose, ToastProps, ToastVariant } from '@aerogel/core/components/contracts/Toast';
17
24
 
18
25
  import Service from './UI.state';
19
26
  import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
20
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
27
+ import type { UIModal, UIToast } from './UI.state';
21
28
 
22
29
  interface ModalCallbacks<T = unknown> {
23
30
  willClose(result: T | undefined): void;
24
- closed(result: T | undefined): void;
31
+ hasClosed(result: T | undefined): void;
25
32
  }
26
33
 
27
- type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
28
- type ModalResult<TComponent> =
29
- TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
30
-
31
- export const UIComponents = {
32
- AlertModal: 'alert-modal',
33
- ConfirmModal: 'confirm-modal',
34
- ErrorReportModal: 'error-report-modal',
35
- LoadingModal: 'loading-modal',
36
- PromptModal: 'prompt-modal',
37
- Snackbar: 'snackbar',
38
- StartupCrash: 'startup-crash',
39
- } as const;
40
-
41
- export type UIComponent = ObjectValues<typeof UIComponents>;
34
+ export type ModalResult<T> = ModalExposeResult<ComponentExposed<T>>;
35
+ export type ModalExposeResult<T> = T extends { close(result?: infer Result): Promise<void> } ? Result : unknown;
36
+ export type UIComponent<Props = {}, Exposed = {}> = { new (...args: ClosureArgs): Exposed & { $props: Props } };
37
+
38
+ export interface UIComponents {
39
+ 'alert-modal': UIComponent<AlertModalProps, AlertModalExpose>;
40
+ 'confirm-modal': UIComponent<ConfirmModalProps, ConfirmModalExpose>;
41
+ 'error-report-modal': UIComponent<ErrorReportModalProps, ErrorReportModalExpose>;
42
+ 'loading-modal': UIComponent<LoadingModalProps, LoadingModalExpose>;
43
+ 'prompt-modal': UIComponent<PromptModalProps, PromptModalExpose>;
44
+ 'router-link': UIComponent;
45
+ 'startup-crash': UIComponent;
46
+ toast: UIComponent<ToastProps, ToastExpose>;
47
+ }
42
48
 
43
- export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
49
+ export interface UIModalContext {
50
+ modal: UIModal;
51
+ childIndex?: number;
52
+ }
44
53
 
45
54
  export type ConfirmOptions = AcceptRefs<{
46
55
  acceptText?: string;
47
- acceptColor?: Color;
56
+ acceptVariant?: ButtonVariant;
48
57
  cancelText?: string;
49
- cancelColor?: Color;
58
+ cancelVariant?: ButtonVariant;
50
59
  actions?: Record<string, () => unknown>;
51
60
  required?: boolean;
52
61
  }>;
@@ -57,7 +66,8 @@ export type LoadingOptions = AcceptRefs<{
57
66
  progress?: number;
58
67
  }>;
59
68
 
60
- export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
69
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
70
+ extends ConfirmOptions {
61
71
  checkboxes?: T;
62
72
  }
63
73
 
@@ -66,31 +76,39 @@ export type PromptOptions = AcceptRefs<{
66
76
  defaultValue?: string;
67
77
  placeholder?: string;
68
78
  acceptText?: string;
69
- acceptColor?: Color;
79
+ acceptVariant?: ButtonVariant;
70
80
  cancelText?: string;
71
- cancelColor?: Color;
81
+ cancelVariant?: ButtonVariant;
72
82
  trim?: boolean;
73
83
  }>;
74
84
 
75
- export interface ShowSnackbarOptions {
85
+ export interface ToastOptions {
76
86
  component?: Component;
77
- color?: SnackbarColor;
78
- actions?: SnackbarAction[];
87
+ variant?: ToastVariant;
88
+ actions?: ToastAction[];
79
89
  }
80
90
 
81
91
  export class UIService extends Service {
82
92
 
83
93
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
84
- private components: Partial<Record<UIComponent, Component>> = {};
94
+ private components: Partial<UIComponents> = {};
95
+
96
+ public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
97
+ this.components[name] = component;
98
+ }
99
+
100
+ public resolveComponent<T extends keyof UIComponents>(name: T): UIComponents[T] | null {
101
+ return this.components[name] ?? null;
102
+ }
85
103
 
86
- public requireComponent(name: UIComponent): Component {
87
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
104
+ public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
105
+ return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
88
106
  }
89
107
 
90
108
  public alert(message: string): void;
91
109
  public alert(title: string, message: string): void;
92
110
  public alert(messageOrTitle: string, message?: string): void {
93
- const getProperties = (): AGAlertModalProps => {
111
+ const getProperties = (): AlertModalProps => {
94
112
  if (typeof message !== 'string') {
95
113
  return { message: messageOrTitle };
96
114
  }
@@ -101,14 +119,14 @@ export class UIService extends Service {
101
119
  };
102
120
  };
103
121
 
104
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
122
+ this.modal(this.requireComponent('alert-modal'), getProperties());
105
123
  }
106
124
 
107
125
  /* eslint-disable max-len */
108
126
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
109
127
  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
128
+ public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
129
+ public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
112
130
  /* eslint-enable max-len */
113
131
 
114
132
  public async confirm(
@@ -116,7 +134,7 @@ export class UIService extends Service {
116
134
  messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
117
135
  options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
118
136
  ): Promise<boolean | [boolean, Record<string, boolean>]> {
119
- const getProperties = (): AGConfirmModalProps => {
137
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
120
138
  if (typeof messageOrOptions !== 'string') {
121
139
  return {
122
140
  ...(messageOrOptions ?? {}),
@@ -132,12 +150,9 @@ export class UIService extends Service {
132
150
  required: !!options?.required,
133
151
  };
134
152
  };
135
- const properties = getProperties();
136
- const modal = await this.openModal<
137
- ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
138
- >(this.requireComponent(UIComponents.ConfirmModal), properties);
139
- const result = await modal.beforeClose;
140
153
 
154
+ const properties = getProperties();
155
+ const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
141
156
  const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
142
157
  const checkboxes =
143
158
  typeof result === 'object'
@@ -174,26 +189,22 @@ export class UIService extends Service {
174
189
  options?: PromptOptions,
175
190
  ): Promise<string | null> {
176
191
  const trim = options?.trim ?? true;
177
- const getProperties = (): AGPromptModalProps => {
192
+ const getProperties = (): PromptModalProps => {
178
193
  if (typeof messageOrOptions !== 'string') {
179
194
  return {
180
195
  message: messageOrTitle,
181
196
  ...(messageOrOptions ?? {}),
182
- } as AGPromptModalProps;
197
+ } as PromptModalProps;
183
198
  }
184
199
 
185
200
  return {
186
201
  title: messageOrTitle,
187
202
  message: messageOrOptions,
188
203
  ...(options ?? {}),
189
- } as AGPromptModalProps;
204
+ } as PromptModalProps;
190
205
  };
191
206
 
192
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
193
- this.requireComponent(UIComponents.PromptModal),
194
- getProperties(),
195
- );
196
- const rawResult = await modal.beforeClose;
207
+ const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
197
208
  const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
198
209
 
199
210
  return result ?? null;
@@ -207,7 +218,7 @@ export class UIService extends Service {
207
218
  operation?: Promise<T> | (() => T),
208
219
  ): Promise<T> {
209
220
  const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
210
- const processArgs = (): { operationPromise: Promise<T>; props?: AGLoadingModalProps } => {
221
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
211
222
  if (typeof operationOrMessageOrOptions === 'string') {
212
223
  return {
213
224
  props: { message: operationOrMessageOrOptions },
@@ -226,10 +237,12 @@ export class UIService extends Service {
226
237
  };
227
238
 
228
239
  const { operationPromise, props } = processArgs();
229
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
240
+ const modal = await this.modal(this.requireComponent('loading-modal'), props);
230
241
 
231
242
  try {
232
- const [result] = await Promise.all([operationPromise, after({ seconds: 1 })]);
243
+ const result = await operationPromise;
244
+
245
+ await after({ ms: 500 });
233
246
 
234
247
  return result;
235
248
  } finally {
@@ -237,43 +250,34 @@ export class UIService extends Service {
237
250
  }
238
251
  }
239
252
 
240
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
241
- const snackbar: Snackbar = {
253
+ public toast(message: string, options: ToastOptions = {}): void {
254
+ const { component, ...otherOptions } = options;
255
+ const toast: UIToast = {
242
256
  id: uuid(),
243
- properties: { message, ...options },
244
- component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
257
+ properties: { message, ...otherOptions },
258
+ component: markRaw(component ?? this.requireComponent('toast')),
245
259
  };
246
260
 
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
- );
261
+ this.setState('toasts', this.toasts.concat(toast));
257
262
  }
258
263
 
259
- public registerComponent(name: UIComponent, component: Component): void {
260
- this.components[name] = component;
261
- }
264
+ public modal<T extends Component>(
265
+ ...args: {} extends ComponentProps<T>
266
+ ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
267
+ : [component: T, props: AcceptRefs<ComponentProps<T>>]
268
+ ): Promise<UIModal<ModalResult<T>>>;
262
269
 
263
- public async openModal<TModalComponent extends ModalComponent>(
264
- component: TModalComponent,
265
- properties?: ModalProperties<TModalComponent>,
266
- ): Promise<Modal<ModalResult<TModalComponent>>> {
270
+ public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
267
271
  const id = uuid();
268
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
269
- const modal: Modal<ModalResult<TModalComponent>> = {
272
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
273
+ const modal: UIModal<ModalResult<T>> = {
270
274
  id,
271
- properties: properties ?? {},
275
+ closing: false,
276
+ properties: props ?? {},
272
277
  component: markRaw(component),
273
278
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
274
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
279
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
275
280
  };
276
- const activeModal = this.modals.at(-1);
277
281
  const modals = this.modals.concat(modal);
278
282
 
279
283
  this.modalCallbacks[modal.id] = callbacks;
@@ -281,15 +285,26 @@ export class UIService extends Service {
281
285
  this.setState({ modals });
282
286
 
283
287
  await nextTick();
284
- await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
285
- await Promise.all([
286
- activeModal || Events.emit('show-overlays-backdrop'),
287
- Events.emit('show-modal', { id: modal.id }),
288
- ]);
289
288
 
290
289
  return modal;
291
290
  }
292
291
 
292
+ public modalForm<T extends Component>(
293
+ ...args: {} extends ComponentProps<T>
294
+ ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
295
+ : [component: T, props: AcceptRefs<ComponentProps<T>>]
296
+ ): Promise<ModalResult<T> | undefined>;
297
+
298
+ public async modalForm<T extends Component>(
299
+ component: T,
300
+ props?: ComponentProps<T>,
301
+ ): Promise<ModalResult<T> | undefined> {
302
+ const modal = await this.modal<T>(component, props as ComponentProps<T>);
303
+ const result = await modal.beforeClose;
304
+
305
+ return result;
306
+ }
307
+
293
308
  public async closeModal(id: string, result?: unknown): Promise<void> {
294
309
  if (!App.isMounted()) {
295
310
  await this.removeModal(id, result);
@@ -318,25 +333,23 @@ export class UIService extends Service {
318
333
  this.modals.filter((m) => m.id !== id),
319
334
  );
320
335
 
321
- this.modalCallbacks[id]?.closed?.(result);
336
+ this.modalCallbacks[id]?.hasClosed?.(result);
322
337
 
323
338
  delete this.modalCallbacks[id];
324
-
325
- const activeModal = this.modals.at(-1);
326
-
327
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
328
339
  }
329
340
 
330
341
  private watchModalEvents(): void {
331
- Events.on('modal-will-close', ({ modal, result }) => {
332
- this.modalCallbacks[modal.id]?.willClose?.(result);
342
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
343
+ const modal = this.modals.find((_modal) => id === _modal.id);
333
344
 
334
- if (this.modals.length === 1) {
335
- Events.emit('hide-overlays-backdrop');
345
+ if (modal) {
346
+ modal.closing = true;
336
347
  }
348
+
349
+ this.modalCallbacks[id]?.willClose?.(result);
337
350
  });
338
351
 
339
- Events.on('modal-closed', async ({ modal: { id }, result }) => {
352
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
340
353
  await this.removeModal(id, result);
341
354
  });
342
355
  }
@@ -380,11 +393,7 @@ export default facade(UIService);
380
393
  declare module '@aerogel/core/services/Events' {
381
394
  export interface EventsPayload {
382
395
  'close-modal': { id: string; result?: unknown };
383
- 'hide-modal': { id: string };
384
- 'hide-overlays-backdrop': void;
385
- 'modal-closed': { modal: Modal; result?: unknown };
386
- 'modal-will-close': { modal: Modal; result?: unknown };
387
- 'show-modal': { id: string };
388
- 'show-overlays-backdrop': void;
396
+ 'modal-will-close': { modal: UIModal; result?: unknown };
397
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
389
398
  }
390
399
  }