@aerogel/core 0.0.0-next.f1f5a990033d966dc0bb12d251110fbc9350dcc7 → 0.0.0-next.f8c757d83e1e0d001a2836fa45aba318ec17b9b9

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 (197) hide show
  1. package/dist/aerogel-core.d.ts +2144 -1268
  2. package/dist/aerogel-core.js +3234 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +30 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +27 -14
  7. package/src/bootstrap/options.ts +4 -1
  8. package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -10
  11. package/src/components/AppToasts.vue +16 -0
  12. package/src/components/contracts/AlertModal.ts +4 -0
  13. package/src/components/contracts/Button.ts +16 -0
  14. package/src/components/contracts/ConfirmModal.ts +41 -0
  15. package/src/components/contracts/DropdownMenu.ts +18 -0
  16. package/src/components/contracts/ErrorReportModal.ts +29 -0
  17. package/src/components/contracts/Input.ts +26 -0
  18. package/src/components/contracts/LoadingModal.ts +18 -0
  19. package/src/components/contracts/Modal.ts +13 -0
  20. package/src/components/contracts/PromptModal.ts +30 -0
  21. package/src/components/contracts/Select.ts +44 -0
  22. package/src/components/contracts/Toast.ts +13 -0
  23. package/src/components/contracts/index.ts +9 -0
  24. package/src/components/contracts/shared.ts +9 -0
  25. package/src/components/headless/HeadlessButton.vue +51 -0
  26. package/src/components/headless/HeadlessInput.vue +59 -0
  27. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  28. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  29. package/src/components/headless/HeadlessInputInput.vue +75 -0
  30. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  31. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  32. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +18 -18
  33. package/src/components/headless/HeadlessModalContent.vue +24 -0
  34. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  35. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  36. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  37. package/src/components/headless/HeadlessSelect.vue +113 -0
  38. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
  39. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  40. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  41. package/src/components/headless/HeadlessSelectOptions.vue +37 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  44. package/src/components/headless/HeadlessToast.vue +18 -0
  45. package/src/components/headless/HeadlessToastAction.vue +13 -0
  46. package/src/components/headless/index.ts +19 -3
  47. package/src/components/index.ts +6 -9
  48. package/src/components/ui/AdvancedOptions.vue +18 -0
  49. package/src/components/ui/AlertModal.vue +13 -0
  50. package/src/components/ui/Button.vue +98 -0
  51. package/src/components/ui/Checkbox.vue +56 -0
  52. package/src/components/ui/ConfirmModal.vue +42 -0
  53. package/src/components/ui/DropdownMenu.vue +27 -0
  54. package/src/components/ui/DropdownMenuOption.vue +14 -0
  55. package/src/components/ui/DropdownMenuOptions.vue +27 -0
  56. package/src/components/ui/EditableContent.vue +82 -0
  57. package/src/components/ui/ErrorMessage.vue +15 -0
  58. package/src/components/ui/ErrorReportModal.vue +62 -0
  59. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  60. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  61. package/src/components/ui/Form.vue +24 -0
  62. package/src/components/ui/Input.vue +56 -0
  63. package/src/components/ui/Link.vue +12 -0
  64. package/src/components/ui/LoadingModal.vue +32 -0
  65. package/src/components/ui/Markdown.vue +69 -0
  66. package/src/components/ui/Modal.vue +75 -0
  67. package/src/components/ui/ModalContext.vue +30 -0
  68. package/src/components/ui/ProgressBar.vue +50 -0
  69. package/src/components/ui/PromptModal.vue +35 -0
  70. package/src/components/ui/Select.vue +25 -0
  71. package/src/components/ui/SelectLabel.vue +17 -0
  72. package/src/components/ui/SelectOption.vue +29 -0
  73. package/src/components/ui/SelectOptions.vue +30 -0
  74. package/src/components/ui/SelectTrigger.vue +29 -0
  75. package/src/components/ui/SettingsModal.vue +15 -0
  76. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  77. package/src/components/ui/Toast.vue +42 -0
  78. package/src/components/ui/index.ts +30 -0
  79. package/src/directives/index.ts +11 -5
  80. package/src/directives/measure.ts +34 -6
  81. package/src/errors/Errors.state.ts +1 -1
  82. package/src/errors/Errors.ts +29 -33
  83. package/src/errors/JobCancelledError.ts +3 -0
  84. package/src/errors/index.ts +10 -16
  85. package/src/errors/utils.ts +35 -0
  86. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  87. package/src/forms/{Form.ts → FormController.ts} +86 -25
  88. package/src/forms/composition.ts +4 -4
  89. package/src/forms/index.ts +3 -1
  90. package/src/forms/utils.ts +36 -5
  91. package/src/forms/validation.ts +19 -0
  92. package/src/index.css +46 -0
  93. package/src/{main.ts → index.ts} +3 -0
  94. package/src/jobs/Job.ts +147 -0
  95. package/src/jobs/index.ts +10 -0
  96. package/src/jobs/listeners.ts +3 -0
  97. package/src/jobs/status.ts +4 -0
  98. package/src/lang/DefaultLangProvider.ts +46 -0
  99. package/src/lang/Lang.state.ts +11 -0
  100. package/src/lang/Lang.ts +44 -29
  101. package/src/lang/index.ts +11 -6
  102. package/src/lang/settings/Language.vue +48 -0
  103. package/src/lang/settings/index.ts +10 -0
  104. package/src/plugins/Plugin.ts +1 -1
  105. package/src/plugins/index.ts +10 -7
  106. package/src/services/App.state.ts +37 -5
  107. package/src/services/App.ts +35 -6
  108. package/src/services/Cache.ts +43 -0
  109. package/src/services/Events.test.ts +39 -0
  110. package/src/services/Events.ts +112 -32
  111. package/src/services/Service.ts +150 -49
  112. package/src/services/Storage.ts +20 -0
  113. package/src/services/index.ts +17 -5
  114. package/src/services/store.ts +8 -5
  115. package/src/services/utils.ts +18 -0
  116. package/src/testing/index.ts +26 -0
  117. package/src/testing/setup.ts +11 -0
  118. package/src/ui/UI.state.ts +19 -7
  119. package/src/ui/UI.ts +218 -70
  120. package/src/ui/index.ts +19 -16
  121. package/src/ui/utils.ts +16 -0
  122. package/src/utils/classes.ts +49 -0
  123. package/src/utils/composition/events.ts +2 -2
  124. package/src/utils/composition/forms.ts +14 -4
  125. package/src/utils/composition/persistent.test.ts +33 -0
  126. package/src/utils/composition/persistent.ts +11 -0
  127. package/src/utils/composition/state.test.ts +47 -0
  128. package/src/utils/composition/state.ts +33 -0
  129. package/src/utils/index.ts +4 -1
  130. package/src/utils/markdown.test.ts +50 -0
  131. package/src/utils/markdown.ts +19 -6
  132. package/src/utils/vue.ts +28 -127
  133. package/dist/aerogel-core.cjs.js +0 -2
  134. package/dist/aerogel-core.cjs.js.map +0 -1
  135. package/dist/aerogel-core.esm.js +0 -2
  136. package/dist/aerogel-core.esm.js.map +0 -1
  137. package/histoire.config.ts +0 -7
  138. package/noeldemartin.config.js +0 -5
  139. package/postcss.config.js +0 -6
  140. package/src/assets/histoire.css +0 -3
  141. package/src/components/AGAppSnackbars.vue +0 -13
  142. package/src/components/constants.ts +0 -8
  143. package/src/components/forms/AGButton.vue +0 -44
  144. package/src/components/forms/AGCheckbox.vue +0 -35
  145. package/src/components/forms/AGForm.vue +0 -26
  146. package/src/components/forms/AGInput.vue +0 -36
  147. package/src/components/forms/AGSelect.story.vue +0 -28
  148. package/src/components/forms/AGSelect.vue +0 -53
  149. package/src/components/forms/index.ts +0 -5
  150. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  151. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  152. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  153. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  154. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -39
  155. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -76
  156. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  157. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  158. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  159. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  160. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  161. package/src/components/headless/forms/index.ts +0 -14
  162. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  163. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  164. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  165. package/src/components/headless/modals/index.ts +0 -4
  166. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  167. package/src/components/headless/snackbars/index.ts +0 -40
  168. package/src/components/lib/AGErrorMessage.vue +0 -16
  169. package/src/components/lib/AGLink.vue +0 -9
  170. package/src/components/lib/AGMarkdown.vue +0 -36
  171. package/src/components/lib/AGMeasured.vue +0 -15
  172. package/src/components/lib/index.ts +0 -5
  173. package/src/components/modals/AGAlertModal.ts +0 -15
  174. package/src/components/modals/AGAlertModal.vue +0 -14
  175. package/src/components/modals/AGConfirmModal.ts +0 -27
  176. package/src/components/modals/AGConfirmModal.vue +0 -26
  177. package/src/components/modals/AGErrorReportModal.ts +0 -46
  178. package/src/components/modals/AGErrorReportModal.vue +0 -54
  179. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  180. package/src/components/modals/AGLoadingModal.ts +0 -23
  181. package/src/components/modals/AGLoadingModal.vue +0 -15
  182. package/src/components/modals/AGModal.ts +0 -10
  183. package/src/components/modals/AGModal.vue +0 -39
  184. package/src/components/modals/AGModalContext.ts +0 -8
  185. package/src/components/modals/AGModalContext.vue +0 -22
  186. package/src/components/modals/AGModalTitle.vue +0 -9
  187. package/src/components/modals/index.ts +0 -26
  188. package/src/components/snackbars/AGSnackbar.vue +0 -36
  189. package/src/components/snackbars/index.ts +0 -3
  190. package/src/components/utils.ts +0 -10
  191. package/src/directives/initial-focus.ts +0 -11
  192. package/src/main.histoire.ts +0 -1
  193. package/src/utils/tailwindcss.test.ts +0 -26
  194. package/src/utils/tailwindcss.ts +0 -7
  195. package/tailwind.config.js +0 -4
  196. package/tsconfig.json +0 -11
  197. package/vite.config.ts +0 -14
@@ -1,21 +1,29 @@
1
1
  import type { App as VueApp } from 'vue';
2
2
 
3
- import { definePlugin } from '@/plugins';
3
+ import { definePlugin } from '@aerogel/core/plugins';
4
+ import { isDevelopment, isTesting } from '@noeldemartin/utils';
4
5
 
5
6
  import App from './App';
7
+ import Cache from './Cache';
6
8
  import Events from './Events';
7
9
  import Service from './Service';
10
+ import Storage from './Storage';
8
11
  import { getPiniaStore } from './store';
12
+ import type { AppSetting } from './App.state';
9
13
 
10
14
  export * from './App';
15
+ export * from './Cache';
11
16
  export * from './Events';
12
17
  export * from './Service';
18
+ export * from './store';
19
+ export * from './utils';
13
20
 
14
- export { App, Events, Service };
21
+ export { App, Cache, Events, Storage, Service };
15
22
 
16
23
  const defaultServices = {
17
24
  $app: App,
18
25
  $events: Events,
26
+ $storage: Storage,
19
27
  };
20
28
 
21
29
  export type DefaultServices = typeof defaultServices;
@@ -33,7 +41,9 @@ export async function bootServices(app: VueApp, services: Record<string, Service
33
41
 
34
42
  Object.assign(app.config.globalProperties, services);
35
43
 
36
- App.development && Object.assign(window, services);
44
+ if (isDevelopment() || isTesting()) {
45
+ Object.assign(globalThis, services);
46
+ }
37
47
  }
38
48
 
39
49
  export default definePlugin({
@@ -44,17 +54,19 @@ export default definePlugin({
44
54
  };
45
55
 
46
56
  app.use(getPiniaStore());
57
+ App.settings.push(...(options.settings ?? []));
47
58
 
48
59
  await bootServices(app, services);
49
60
  },
50
61
  });
51
62
 
52
- declare module '@/bootstrap/options' {
63
+ declare module '@aerogel/core/bootstrap/options' {
53
64
  export interface AerogelOptions {
54
65
  services?: Record<string, Service>;
66
+ settings?: AppSetting[];
55
67
  }
56
68
  }
57
69
 
58
- declare module '@vue/runtime-core' {
70
+ declare module 'vue' {
59
71
  interface ComponentCustomProperties extends Services {}
60
72
  }
@@ -1,16 +1,19 @@
1
+ import { tap } from '@noeldemartin/utils';
1
2
  import { createPinia, defineStore, setActivePinia } from 'pinia';
2
3
  import type { DefineStoreOptions, Pinia, StateTree, Store, _GettersTree } from 'pinia';
3
4
 
4
5
  let _store: Pinia | null = null;
5
6
 
6
7
  function initializePiniaStore(): Pinia {
7
- if (!_store) {
8
- _store = createPinia();
8
+ return _store ?? resetPiniaStore();
9
+ }
9
10
 
10
- setActivePinia(_store);
11
- }
11
+ export function resetPiniaStore(): Pinia {
12
+ return tap(createPinia(), (store) => {
13
+ _store = store;
12
14
 
13
- return _store;
15
+ setActivePinia(store);
16
+ });
14
17
  }
15
18
 
16
19
  export function getPiniaStore(): Pinia {
@@ -0,0 +1,18 @@
1
+ import { objectOnly } from '@noeldemartin/utils';
2
+
3
+ export type Replace<
4
+ TOriginal extends Record<string, unknown>,
5
+ TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
6
+ > = {
7
+ [K in keyof TOriginal]: TReplacements extends Record<K, infer Replacement> ? Replacement : TOriginal[K];
8
+ };
9
+
10
+ export function replaceExisting<
11
+ TOriginal extends Record<string, unknown>,
12
+ TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
13
+ >(original: TOriginal, replacements: TReplacements): Replace<TOriginal, TReplacements> {
14
+ return {
15
+ ...original,
16
+ ...objectOnly(replacements, Object.keys(original)),
17
+ } as Replace<TOriginal, TReplacements>;
18
+ }
@@ -0,0 +1,26 @@
1
+ import { isTesting } from '@noeldemartin/utils';
2
+ import type { GetClosureArgs } from '@noeldemartin/utils';
3
+
4
+ import Events from '@aerogel/core/services/Events';
5
+ import { definePlugin } from '@aerogel/core/plugins';
6
+
7
+ export interface AerogelTestingRuntime {
8
+ on: (typeof Events)['on'];
9
+ }
10
+
11
+ export default definePlugin({
12
+ async install() {
13
+ if (!isTesting()) {
14
+ return;
15
+ }
16
+
17
+ globalThis.testingRuntime = {
18
+ on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
19
+ };
20
+ },
21
+ });
22
+
23
+ declare global {
24
+ // eslint-disable-next-line no-var
25
+ var testingRuntime: AerogelTestingRuntime | undefined;
26
+ }
@@ -0,0 +1,11 @@
1
+ import { FakeLocalStorage } from '@noeldemartin/testing';
2
+ import { beforeEach, vi } from 'vitest';
3
+
4
+ vi.mock('dompurify', async () => {
5
+ return { default: { sanitize: (html: string) => html } };
6
+ });
7
+
8
+ beforeEach(() => {
9
+ FakeLocalStorage.reset();
10
+ FakeLocalStorage.patchGlobal();
11
+ });
@@ -1,8 +1,10 @@
1
1
  import type { Component } from 'vue';
2
2
 
3
- import { defineServiceState } from '@/services/Service';
3
+ import { defineServiceState } from '@aerogel/core/services/Service';
4
4
 
5
- export interface Modal<T = unknown> {
5
+ import { Layouts, getCurrentLayout } from './utils';
6
+
7
+ export interface UIModal<T = unknown> {
6
8
  id: string;
7
9
  properties: Record<string, unknown>;
8
10
  component: Component;
@@ -10,14 +12,19 @@ export interface Modal<T = unknown> {
10
12
  afterClose: Promise<T | undefined>;
11
13
  }
12
14
 
15
+ export interface UIModalContext {
16
+ modal: UIModal;
17
+ childIndex?: number;
18
+ }
19
+
13
20
  export interface ModalComponent<
14
21
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
- Properties extends Record<string, unknown> = Record<string, unknown>,
22
+ Properties extends object = object,
16
23
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- Result = unknown
24
+ Result = unknown,
18
25
  > {}
19
26
 
20
- export interface Snackbar {
27
+ export interface UIToast {
21
28
  id: string;
22
29
  component: Component;
23
30
  properties: Record<string, unknown>;
@@ -26,7 +33,12 @@ export interface Snackbar {
26
33
  export default defineServiceState({
27
34
  name: 'ui',
28
35
  initialState: {
29
- modals: [] as Modal[],
30
- snackbars: [] as Snackbar[],
36
+ modals: [] as UIModal[],
37
+ toasts: [] as UIToast[],
38
+ layout: getCurrentLayout(),
39
+ },
40
+ computed: {
41
+ mobile: ({ layout }) => layout === Layouts.Mobile,
42
+ desktop: ({ layout }) => layout === Layouts.Desktop,
31
43
  },
32
44
  });
package/src/ui/UI.ts CHANGED
@@ -1,14 +1,21 @@
1
- import { after, facade, fail, uuid } from '@noeldemartin/utils';
1
+ import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
2
2
  import { markRaw, nextTick } from 'vue';
3
3
  import type { Component } from 'vue';
4
4
  import type { ObjectValues } from '@noeldemartin/utils';
5
5
 
6
- import Events from '@/services/Events';
7
- import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
6
+ import App from '@aerogel/core/services/App';
7
+ import Events from '@aerogel/core/services/Events';
8
+ import type { AcceptRefs } from '@aerogel/core/utils';
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';
8
15
 
9
16
  import Service from './UI.state';
10
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
11
- import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps } from '@/components';
17
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
18
+ import type { ModalComponent, UIModal, UIToast } from './UI.state';
12
19
 
13
20
  interface ModalCallbacks<T = unknown> {
14
21
  willClose(result: T | undefined): void;
@@ -16,30 +23,56 @@ interface ModalCallbacks<T = unknown> {
16
23
  }
17
24
 
18
25
  type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
19
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
20
- ? TResult
21
- : never;
26
+ type ModalResult<TComponent> =
27
+ TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
22
28
 
23
29
  export const UIComponents = {
24
30
  AlertModal: 'alert-modal',
25
31
  ConfirmModal: 'confirm-modal',
26
32
  ErrorReportModal: 'error-report-modal',
27
33
  LoadingModal: 'loading-modal',
28
- Snackbar: 'snackbar',
34
+ PromptModal: 'prompt-modal',
35
+ Toast: 'toast',
29
36
  StartupCrash: 'startup-crash',
30
37
  } as const;
31
38
 
32
39
  export type UIComponent = ObjectValues<typeof UIComponents>;
33
40
 
34
- export interface ConfirmOptions {
41
+ export type ConfirmOptions = AcceptRefs<{
35
42
  acceptText?: string;
43
+ acceptVariant?: ButtonVariant;
36
44
  cancelText?: string;
45
+ cancelVariant?: ButtonVariant;
46
+ actions?: Record<string, () => unknown>;
47
+ required?: boolean;
48
+ }>;
49
+
50
+ export type LoadingOptions = AcceptRefs<{
51
+ title?: string;
52
+ message?: string;
53
+ progress?: number;
54
+ }>;
55
+
56
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
57
+ extends ConfirmOptions {
58
+ checkboxes?: T;
37
59
  }
38
60
 
39
- export interface ShowSnackbarOptions {
61
+ export type PromptOptions = AcceptRefs<{
62
+ label?: string;
63
+ defaultValue?: string;
64
+ placeholder?: string;
65
+ acceptText?: string;
66
+ acceptVariant?: ButtonVariant;
67
+ cancelText?: string;
68
+ cancelVariant?: ButtonVariant;
69
+ trim?: boolean;
70
+ }>;
71
+
72
+ export interface ToastOptions {
40
73
  component?: Component;
41
- color?: SnackbarColor;
42
- actions?: SnackbarAction[];
74
+ variant?: ToastVariant;
75
+ actions?: ToastAction[];
43
76
  }
44
77
 
45
78
  export class UIService extends Service {
@@ -54,7 +87,7 @@ export class UIService extends Service {
54
87
  public alert(message: string): void;
55
88
  public alert(title: string, message: string): void;
56
89
  public alert(messageOrTitle: string, message?: string): void {
57
- const getProperties = (): AGAlertModalProps => {
90
+ const getProperties = (): AlertModalProps => {
58
91
  if (typeof message !== 'string') {
59
92
  return { message: messageOrTitle };
60
93
  }
@@ -65,57 +98,147 @@ export class UIService extends Service {
65
98
  };
66
99
  };
67
100
 
68
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
101
+ this.openModal<ModalComponent<AlertModalProps>>(
102
+ this.requireComponent(UIComponents.AlertModal),
103
+ getProperties(),
104
+ );
69
105
  }
70
106
 
107
+ /* eslint-disable max-len */
71
108
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
72
109
  public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
110
+ public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
111
+ public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
112
+ /* eslint-enable max-len */
113
+
73
114
  public async confirm(
74
115
  messageOrTitle: string,
75
- messageOrOptions?: string | ConfirmOptions,
76
- options?: ConfirmOptions,
77
- ): Promise<boolean> {
78
- const getProperties = (): AGConfirmModalProps => {
116
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
117
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
118
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
119
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
79
120
  if (typeof messageOrOptions !== 'string') {
80
121
  return {
81
- message: messageOrTitle,
82
122
  ...(messageOrOptions ?? {}),
123
+ message: messageOrTitle,
124
+ required: !!messageOrOptions?.required,
83
125
  };
84
126
  }
85
127
 
86
128
  return {
129
+ ...(options ?? {}),
87
130
  title: messageOrTitle,
88
131
  message: messageOrOptions,
89
- ...(options ?? {}),
132
+ required: !!options?.required,
90
133
  };
91
134
  };
92
135
 
93
- const modal = await this.openModal<ModalComponent<{ message: string }, boolean>>(
136
+ type ConfirmModalComponent = ModalComponent<
137
+ AcceptRefs<ConfirmModalProps>,
138
+ boolean | [boolean, Record<string, boolean>]
139
+ >;
140
+
141
+ const properties = getProperties();
142
+ const modal = await this.openModal<ConfirmModalComponent>(
94
143
  this.requireComponent(UIComponents.ConfirmModal),
95
- getProperties(),
144
+ properties,
96
145
  );
97
146
  const result = await modal.beforeClose;
98
147
 
99
- return result ?? false;
148
+ const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
149
+ const checkboxes =
150
+ typeof result === 'object'
151
+ ? result[1]
152
+ : Object.entries(properties.checkboxes ?? {}).reduce(
153
+ (values, [checkbox, { default: defaultValue }]) => ({
154
+ [checkbox]: defaultValue ?? false,
155
+ ...values,
156
+ }),
157
+ {} as Record<string, boolean>,
158
+ );
159
+
160
+ for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
161
+ if (!checkbox.required || checkboxes[name]) {
162
+ continue;
163
+ }
164
+
165
+ if (confirmed && isDevelopment()) {
166
+ // eslint-disable-next-line no-console
167
+ console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
168
+ }
169
+
170
+ return [false, checkboxes];
171
+ }
172
+
173
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
100
174
  }
101
175
 
102
- public async loading<T>(operation: Promise<T>): Promise<T>;
103
- public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
104
- public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
105
- const getProperties = (): AGLoadingModalProps => {
106
- if (typeof messageOrOperation !== 'string') {
107
- return {};
176
+ public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
177
+ public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
178
+ public async prompt(
179
+ messageOrTitle: string,
180
+ messageOrOptions?: string | PromptOptions,
181
+ options?: PromptOptions,
182
+ ): Promise<string | null> {
183
+ const trim = options?.trim ?? true;
184
+ const getProperties = (): PromptModalProps => {
185
+ if (typeof messageOrOptions !== 'string') {
186
+ return {
187
+ message: messageOrTitle,
188
+ ...(messageOrOptions ?? {}),
189
+ } as PromptModalProps;
190
+ }
191
+
192
+ return {
193
+ title: messageOrTitle,
194
+ message: messageOrOptions,
195
+ ...(options ?? {}),
196
+ } as PromptModalProps;
197
+ };
198
+
199
+ const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
200
+ this.requireComponent(UIComponents.PromptModal),
201
+ getProperties(),
202
+ );
203
+ const rawResult = await modal.beforeClose;
204
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
205
+
206
+ return result ?? null;
207
+ }
208
+
209
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
210
+ public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
211
+ public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
212
+ public async loading<T>(
213
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
214
+ operation?: Promise<T> | (() => T),
215
+ ): Promise<T> {
216
+ const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
217
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
218
+ if (typeof operationOrMessageOrOptions === 'string') {
219
+ return {
220
+ props: { message: operationOrMessageOrOptions },
221
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
222
+ };
223
+ }
224
+
225
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
226
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
108
227
  }
109
228
 
110
- return { message: messageOrOperation };
229
+ return {
230
+ props: operationOrMessageOrOptions,
231
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
232
+ };
111
233
  };
112
234
 
113
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
235
+ const { operationPromise, props } = processArgs();
236
+ const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
114
237
 
115
238
  try {
116
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
239
+ const result = await operationPromise;
117
240
 
118
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
241
+ await after({ ms: 500 });
119
242
 
120
243
  return result;
121
244
  } finally {
@@ -123,23 +246,15 @@ export class UIService extends Service {
123
246
  }
124
247
  }
125
248
 
126
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
127
- const snackbar: Snackbar = {
249
+ public toast(message: string, options: ToastOptions = {}): void {
250
+ const { component, ...otherOptions } = options;
251
+ const toast: UIToast = {
128
252
  id: uuid(),
129
- properties: { message, ...options },
130
- component: options.component ?? markRaw(this.requireComponent(UIComponents.Snackbar)),
253
+ properties: { message, ...otherOptions },
254
+ component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
131
255
  };
132
256
 
133
- this.setState('snackbars', this.snackbars.concat(snackbar));
134
-
135
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
136
- }
137
-
138
- public hideSnackbar(id: string): void {
139
- this.setState(
140
- 'snackbars',
141
- this.snackbars.filter((snackbar) => snackbar.id !== id),
142
- );
257
+ this.setState('toasts', this.toasts.concat(toast));
143
258
  }
144
259
 
145
260
  public registerComponent(name: UIComponent, component: Component): void {
@@ -149,10 +264,10 @@ export class UIService extends Service {
149
264
  public async openModal<TModalComponent extends ModalComponent>(
150
265
  component: TModalComponent,
151
266
  properties?: ModalProperties<TModalComponent>,
152
- ): Promise<Modal<ModalResult<TModalComponent>>> {
267
+ ): Promise<UIModal<ModalResult<TModalComponent>>> {
153
268
  const id = uuid();
154
269
  const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
155
- const modal: Modal<ModalResult<TModalComponent>> = {
270
+ const modal: UIModal<ModalResult<TModalComponent>> = {
156
271
  id,
157
272
  properties: properties ?? {},
158
273
  component: markRaw(component),
@@ -177,12 +292,40 @@ export class UIService extends Service {
177
292
  }
178
293
 
179
294
  public async closeModal(id: string, result?: unknown): Promise<void> {
295
+ if (!App.isMounted()) {
296
+ await this.removeModal(id, result);
297
+
298
+ return;
299
+ }
300
+
180
301
  await Events.emit('close-modal', { id, result });
181
302
  }
182
303
 
183
- protected async boot(): Promise<void> {
304
+ public async closeAllModals(): Promise<void> {
305
+ while (this.modals.length > 0) {
306
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
307
+ }
308
+ }
309
+
310
+ protected override async boot(): Promise<void> {
184
311
  this.watchModalEvents();
185
312
  this.watchMountedEvent();
313
+ this.watchViewportBreakpoints();
314
+ }
315
+
316
+ private async removeModal(id: string, result?: unknown): Promise<void> {
317
+ this.setState(
318
+ 'modals',
319
+ this.modals.filter((m) => m.id !== id),
320
+ );
321
+
322
+ this.modalCallbacks[id]?.closed?.(result);
323
+
324
+ delete this.modalCallbacks[id];
325
+
326
+ const activeModal = this.modals.at(-1);
327
+
328
+ await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
186
329
  }
187
330
 
188
331
  private watchModalEvents(): void {
@@ -194,31 +337,24 @@ export class UIService extends Service {
194
337
  }
195
338
  });
196
339
 
197
- Events.on('modal-closed', async ({ modal, result }) => {
198
- this.setState(
199
- 'modals',
200
- this.modals.filter((m) => m.id !== modal.id),
201
- );
202
-
203
- this.modalCallbacks[modal.id]?.closed?.(result);
204
-
205
- delete this.modalCallbacks[modal.id];
206
-
207
- const activeModal = this.modals.at(-1);
208
-
209
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
340
+ Events.on('modal-closed', async ({ modal: { id }, result }) => {
341
+ await this.removeModal(id, result);
210
342
  });
211
343
  }
212
344
 
213
345
  private watchMountedEvent(): void {
214
346
  Events.once('application-mounted', async () => {
215
- const splash = document.getElementById('splash');
347
+ if (!globalThis.document || !globalThis.getComputedStyle) {
348
+ return;
349
+ }
350
+
351
+ const splash = globalThis.document.getElementById('splash');
216
352
 
217
353
  if (!splash) {
218
354
  return;
219
355
  }
220
356
 
221
- if (window.getComputedStyle(splash).opacity !== '0') {
357
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
222
358
  splash.style.opacity = '0';
223
359
 
224
360
  await after({ ms: 600 });
@@ -228,16 +364,28 @@ export class UIService extends Service {
228
364
  });
229
365
  }
230
366
 
367
+ private watchViewportBreakpoints(): void {
368
+ if (!globalThis.matchMedia) {
369
+ return;
370
+ }
371
+
372
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
373
+
374
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
375
+ }
376
+
231
377
  }
232
378
 
233
- export default facade(new UIService());
379
+ export default facade(UIService);
234
380
 
235
- declare module '@/services/Events' {
381
+ declare module '@aerogel/core/services/Events' {
236
382
  export interface EventsPayload {
237
- 'modal-will-close': { modal: Modal; result?: unknown };
238
- 'modal-closed': { modal: Modal; result?: unknown };
239
383
  'close-modal': { id: string; result?: unknown };
240
384
  'hide-modal': { id: string };
385
+ 'hide-overlays-backdrop': void;
386
+ 'modal-closed': { modal: UIModal; result?: unknown };
387
+ 'modal-will-close': { modal: UIModal; result?: unknown };
241
388
  'show-modal': { id: string };
389
+ 'show-overlays-backdrop': void;
242
390
  }
243
391
  }