@aerogel/core 0.0.0-next.b58141fee5d2fe7d25debdbca6b1d2bf1c13e48e → 0.0.0-next.b656a964404fbde17d9cce7668722596098e47fd

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 (177) hide show
  1. package/dist/aerogel-core.d.ts +2082 -813
  2. package/dist/aerogel-core.js +3119 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +36 -34
  5. package/src/bootstrap/bootstrap.test.ts +7 -10
  6. package/src/bootstrap/index.ts +43 -14
  7. package/src/bootstrap/options.ts +4 -1
  8. package/src/components/AppLayout.vue +16 -0
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -6
  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 +11 -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 +28 -0
  21. package/src/components/contracts/Select.ts +36 -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 +50 -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/HeadlessInputLabel.vue +18 -0
  31. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  32. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +17 -19
  33. package/src/components/headless/HeadlessModalContent.vue +24 -0
  34. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  35. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  36. package/src/components/headless/HeadlessSelect.vue +104 -0
  37. package/src/components/headless/HeadlessSelectError.vue +25 -0
  38. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  39. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  40. package/src/components/headless/HeadlessSelectOptions.vue +30 -0
  41. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  42. package/src/components/headless/HeadlessSelectValue.vue +15 -0
  43. package/src/components/headless/HeadlessToast.vue +18 -0
  44. package/src/components/headless/HeadlessToastAction.vue +13 -0
  45. package/src/components/headless/index.ts +18 -3
  46. package/src/components/index.ts +6 -9
  47. package/src/components/ui/AdvancedOptions.vue +18 -0
  48. package/src/components/ui/AlertModal.vue +13 -0
  49. package/src/components/ui/Button.vue +98 -0
  50. package/src/components/ui/Checkbox.vue +56 -0
  51. package/src/components/ui/ConfirmModal.vue +42 -0
  52. package/src/components/ui/DropdownMenu.vue +33 -0
  53. package/src/components/ui/EditableContent.vue +82 -0
  54. package/src/components/ui/ErrorMessage.vue +15 -0
  55. package/src/components/ui/ErrorReportModal.vue +62 -0
  56. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +38 -29
  57. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  58. package/src/components/ui/Form.vue +24 -0
  59. package/src/components/ui/Input.vue +56 -0
  60. package/src/components/ui/Link.vue +12 -0
  61. package/src/components/ui/LoadingModal.vue +32 -0
  62. package/src/components/ui/Markdown.vue +69 -0
  63. package/src/components/ui/Modal.vue +70 -0
  64. package/src/components/ui/ModalContext.vue +30 -0
  65. package/src/components/ui/ProgressBar.vue +50 -0
  66. package/src/components/ui/PromptModal.vue +35 -0
  67. package/src/components/ui/Select.vue +21 -0
  68. package/src/components/ui/SelectLabel.vue +10 -0
  69. package/src/components/ui/SelectOptions.vue +31 -0
  70. package/src/components/ui/SelectTrigger.vue +29 -0
  71. package/src/components/ui/SettingsModal.vue +15 -0
  72. package/src/components/ui/StartupCrash.vue +31 -0
  73. package/src/components/ui/Toast.vue +42 -0
  74. package/src/components/ui/index.ts +27 -0
  75. package/src/directives/index.ts +13 -5
  76. package/src/directives/measure.ts +40 -0
  77. package/src/errors/Errors.state.ts +1 -1
  78. package/src/errors/Errors.ts +35 -34
  79. package/src/errors/JobCancelledError.ts +3 -0
  80. package/src/errors/index.ts +19 -29
  81. package/src/errors/utils.ts +35 -0
  82. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  83. package/src/forms/{Form.ts → FormController.ts} +94 -25
  84. package/src/forms/composition.ts +4 -4
  85. package/src/forms/index.ts +3 -1
  86. package/src/forms/utils.ts +36 -5
  87. package/src/forms/validation.ts +19 -0
  88. package/src/index.css +46 -0
  89. package/src/{main.ts → index.ts} +3 -0
  90. package/src/jobs/Job.ts +147 -0
  91. package/src/jobs/index.ts +10 -0
  92. package/src/jobs/listeners.ts +3 -0
  93. package/src/jobs/status.ts +4 -0
  94. package/src/lang/DefaultLangProvider.ts +46 -0
  95. package/src/lang/Lang.state.ts +11 -0
  96. package/src/lang/Lang.ts +44 -29
  97. package/src/lang/index.ts +11 -6
  98. package/src/lang/settings/Language.vue +48 -0
  99. package/src/lang/settings/index.ts +10 -0
  100. package/src/plugins/Plugin.ts +2 -1
  101. package/src/plugins/index.ts +22 -0
  102. package/src/services/App.state.ts +40 -6
  103. package/src/services/App.ts +48 -5
  104. package/src/services/Cache.ts +43 -0
  105. package/src/services/Events.test.ts +39 -0
  106. package/src/services/Events.ts +112 -32
  107. package/src/services/Service.ts +154 -49
  108. package/src/services/Storage.ts +20 -0
  109. package/src/services/index.ts +18 -6
  110. package/src/services/store.ts +8 -5
  111. package/src/services/utils.ts +18 -0
  112. package/src/testing/index.ts +26 -0
  113. package/src/testing/setup.ts +11 -0
  114. package/src/ui/UI.state.ts +19 -7
  115. package/src/ui/UI.ts +268 -57
  116. package/src/ui/index.ts +23 -17
  117. package/src/ui/utils.ts +16 -0
  118. package/src/utils/classes.ts +49 -0
  119. package/src/utils/composition/events.ts +3 -2
  120. package/src/utils/composition/forms.ts +14 -4
  121. package/src/utils/composition/persistent.test.ts +33 -0
  122. package/src/utils/composition/persistent.ts +11 -0
  123. package/src/utils/composition/state.test.ts +47 -0
  124. package/src/utils/composition/state.ts +33 -0
  125. package/src/utils/index.ts +4 -0
  126. package/src/utils/markdown.test.ts +50 -0
  127. package/src/utils/markdown.ts +19 -6
  128. package/src/utils/vue.ts +28 -118
  129. package/.eslintrc.js +0 -3
  130. package/dist/aerogel-core.cjs.js +0 -2
  131. package/dist/aerogel-core.cjs.js.map +0 -1
  132. package/dist/aerogel-core.esm.js +0 -2
  133. package/dist/aerogel-core.esm.js.map +0 -1
  134. package/dist/virtual.d.ts +0 -11
  135. package/noeldemartin.config.js +0 -5
  136. package/src/components/AGAppLayout.vue +0 -11
  137. package/src/components/AGAppSnackbars.vue +0 -13
  138. package/src/components/basic/AGErrorMessage.vue +0 -16
  139. package/src/components/basic/AGLink.vue +0 -9
  140. package/src/components/basic/AGMarkdown.vue +0 -36
  141. package/src/components/basic/index.ts +0 -5
  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/index.ts +0 -6
  148. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  149. package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
  150. package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
  151. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  152. package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
  153. package/src/components/headless/forms/index.ts +0 -6
  154. package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
  155. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  156. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  157. package/src/components/headless/modals/index.ts +0 -6
  158. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  159. package/src/components/headless/snackbars/index.ts +0 -25
  160. package/src/components/modals/AGAlertModal.vue +0 -25
  161. package/src/components/modals/AGConfirmModal.vue +0 -30
  162. package/src/components/modals/AGErrorReportModal.ts +0 -20
  163. package/src/components/modals/AGErrorReportModal.vue +0 -62
  164. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  165. package/src/components/modals/AGLoadingModal.vue +0 -19
  166. package/src/components/modals/AGModal.ts +0 -10
  167. package/src/components/modals/AGModal.vue +0 -37
  168. package/src/components/modals/AGModalContext.ts +0 -8
  169. package/src/components/modals/AGModalContext.vue +0 -22
  170. package/src/components/modals/AGModalTitle.vue +0 -9
  171. package/src/components/modals/index.ts +0 -23
  172. package/src/components/snackbars/AGSnackbar.vue +0 -42
  173. package/src/components/snackbars/index.ts +0 -3
  174. package/src/directives/initial-focus.ts +0 -11
  175. package/src/types/virtual.d.ts +0 -11
  176. package/tsconfig.json +0 -11
  177. package/vite.config.ts +0 -14
@@ -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,13 +1,21 @@
1
- import { 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';
17
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
18
+ import type { ModalComponent, UIModal, UIToast } from './UI.state';
11
19
 
12
20
  interface ModalCallbacks<T = unknown> {
13
21
  willClose(result: T | undefined): void;
@@ -15,24 +23,56 @@ interface ModalCallbacks<T = unknown> {
15
23
  }
16
24
 
17
25
  type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
18
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
19
- ? TResult
20
- : never;
26
+ type ModalResult<TComponent> =
27
+ TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
21
28
 
22
29
  export const UIComponents = {
23
30
  AlertModal: 'alert-modal',
24
31
  ConfirmModal: 'confirm-modal',
25
32
  ErrorReportModal: 'error-report-modal',
26
33
  LoadingModal: 'loading-modal',
27
- Snackbar: 'snackbar',
34
+ PromptModal: 'prompt-modal',
35
+ Toast: 'toast',
36
+ StartupCrash: 'startup-crash',
28
37
  } as const;
29
38
 
30
39
  export type UIComponent = ObjectValues<typeof UIComponents>;
31
40
 
32
- export interface ShowSnackbarOptions {
41
+ export type ConfirmOptions = AcceptRefs<{
42
+ acceptText?: string;
43
+ acceptVariant?: ButtonVariant;
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;
59
+ }
60
+
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 {
33
73
  component?: Component;
34
- color?: SnackbarColor;
35
- actions?: SnackbarAction[];
74
+ variant?: ToastVariant;
75
+ actions?: ToastAction[];
36
76
  }
37
77
 
38
78
  export class UIService extends Service {
@@ -47,55 +87,174 @@ export class UIService extends Service {
47
87
  public alert(message: string): void;
48
88
  public alert(title: string, message: string): void;
49
89
  public alert(messageOrTitle: string, message?: string): void {
50
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
90
+ const getProperties = (): AlertModalProps => {
91
+ if (typeof message !== 'string') {
92
+ return { message: messageOrTitle };
93
+ }
51
94
 
52
- this.openModal(this.requireComponent(UIComponents.AlertModal), options);
95
+ return {
96
+ title: messageOrTitle,
97
+ message,
98
+ };
99
+ };
100
+
101
+ this.openModal<ModalComponent<AlertModalProps>>(
102
+ this.requireComponent(UIComponents.AlertModal),
103
+ getProperties(),
104
+ );
53
105
  }
54
106
 
55
- public async confirm(message: string): Promise<boolean>;
56
- public async confirm(title: string, message: string): Promise<boolean>;
57
- public async confirm(messageOrTitle: string, message?: string): Promise<boolean> {
58
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
59
- const modal = await this.openModal<ModalComponent<{ message: string }, boolean>>(
107
+ /* eslint-disable max-len */
108
+ public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
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
+
114
+ public async confirm(
115
+ messageOrTitle: string,
116
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
117
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
118
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
119
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
120
+ if (typeof messageOrOptions !== 'string') {
121
+ return {
122
+ ...(messageOrOptions ?? {}),
123
+ message: messageOrTitle,
124
+ required: !!messageOrOptions?.required,
125
+ };
126
+ }
127
+
128
+ return {
129
+ ...(options ?? {}),
130
+ title: messageOrTitle,
131
+ message: messageOrOptions,
132
+ required: !!options?.required,
133
+ };
134
+ };
135
+
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>(
60
143
  this.requireComponent(UIComponents.ConfirmModal),
61
- options,
144
+ properties,
62
145
  );
63
146
  const result = await modal.beforeClose;
64
147
 
65
- 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;
66
174
  }
67
175
 
68
- public async loading<T>(operation: Promise<T>): Promise<T>;
69
- public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
70
- public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
71
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
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
+ }
72
191
 
73
- const message = typeof messageOrOperation === 'string' ? messageOrOperation : undefined;
74
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), { message });
75
- const result = await operation;
192
+ return {
193
+ title: messageOrTitle,
194
+ message: messageOrOptions,
195
+ ...(options ?? {}),
196
+ } as PromptModalProps;
197
+ };
76
198
 
77
- await this.closeModal(modal.id);
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;
78
205
 
79
- return result;
206
+ return result ?? null;
80
207
  }
81
208
 
82
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
83
- const snackbar: Snackbar = {
84
- id: uuid(),
85
- properties: { message, ...options },
86
- component: options.component ?? markRaw(this.requireComponent(UIComponents.Snackbar)),
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) };
227
+ }
228
+
229
+ return {
230
+ props: operationOrMessageOrOptions,
231
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
232
+ };
87
233
  };
88
234
 
89
- this.setState('snackbars', this.snackbars.concat(snackbar));
235
+ const { operationPromise, props } = processArgs();
236
+ const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
237
+
238
+ try {
239
+ const result = await operationPromise;
90
240
 
91
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
241
+ await after({ ms: 500 });
242
+
243
+ return result;
244
+ } finally {
245
+ await this.closeModal(modal.id);
246
+ }
92
247
  }
93
248
 
94
- public hideSnackbar(id: string): void {
95
- this.setState(
96
- 'snackbars',
97
- this.snackbars.filter((snackbar) => snackbar.id !== id),
98
- );
249
+ public toast(message: string, options: ToastOptions = {}): void {
250
+ const { component, ...otherOptions } = options;
251
+ const toast: UIToast = {
252
+ id: uuid(),
253
+ properties: { message, ...otherOptions },
254
+ component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
255
+ };
256
+
257
+ this.setState('toasts', this.toasts.concat(toast));
99
258
  }
100
259
 
101
260
  public registerComponent(name: UIComponent, component: Component): void {
@@ -105,10 +264,10 @@ export class UIService extends Service {
105
264
  public async openModal<TModalComponent extends ModalComponent>(
106
265
  component: TModalComponent,
107
266
  properties?: ModalProperties<TModalComponent>,
108
- ): Promise<Modal<ModalResult<TModalComponent>>> {
267
+ ): Promise<UIModal<ModalResult<TModalComponent>>> {
109
268
  const id = uuid();
110
269
  const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
111
- const modal: Modal<ModalResult<TModalComponent>> = {
270
+ const modal: UIModal<ModalResult<TModalComponent>> = {
112
271
  id,
113
272
  properties: properties ?? {},
114
273
  component: markRaw(component),
@@ -133,11 +292,40 @@ export class UIService extends Service {
133
292
  }
134
293
 
135
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
+
136
301
  await Events.emit('close-modal', { id, result });
137
302
  }
138
303
 
139
- 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> {
140
311
  this.watchModalEvents();
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 }));
141
329
  }
142
330
 
143
331
  private watchModalEvents(): void {
@@ -149,32 +337,55 @@ export class UIService extends Service {
149
337
  }
150
338
  });
151
339
 
152
- Events.on('modal-closed', async ({ modal, result }) => {
153
- this.setState(
154
- 'modals',
155
- this.modals.filter((m) => m.id !== modal.id),
156
- );
340
+ Events.on('modal-closed', async ({ modal: { id }, result }) => {
341
+ await this.removeModal(id, result);
342
+ });
343
+ }
157
344
 
158
- this.modalCallbacks[modal.id]?.closed?.(result);
345
+ private watchMountedEvent(): void {
346
+ Events.once('application-mounted', async () => {
347
+ if (!globalThis.document || !globalThis.getComputedStyle) {
348
+ return;
349
+ }
350
+
351
+ const splash = globalThis.document.getElementById('splash');
159
352
 
160
- delete this.modalCallbacks[modal.id];
353
+ if (!splash) {
354
+ return;
355
+ }
161
356
 
162
- const activeModal = this.modals.at(-1);
357
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
358
+ splash.style.opacity = '0';
163
359
 
164
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
360
+ await after({ ms: 600 });
361
+ }
362
+
363
+ splash.remove();
165
364
  });
166
365
  }
167
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
+
168
377
  }
169
378
 
170
- export default facade(new UIService());
379
+ export default facade(UIService);
171
380
 
172
- declare module '@/services/Events' {
381
+ declare module '@aerogel/core/services/Events' {
173
382
  export interface EventsPayload {
174
- 'modal-will-close': { modal: Modal; result?: unknown };
175
- 'modal-closed': { modal: Modal; result?: unknown };
176
383
  'close-modal': { id: string; result?: unknown };
177
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 };
178
388
  'show-modal': { id: string };
389
+ 'show-overlays-backdrop': void;
179
390
  }
180
391
  }
package/src/ui/index.ts CHANGED
@@ -1,30 +1,36 @@
1
1
  import type { Component } from 'vue';
2
2
 
3
- import { bootServices } from '@/services';
4
- import { definePlugin } from '@/plugins';
3
+ import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
4
+ import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
5
+ import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
6
+ import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
7
+ import PromptModal from '@aerogel/core/components/ui/PromptModal.vue';
8
+ import StartupCrash from '@aerogel/core/components/ui/StartupCrash.vue';
9
+ import Toast from '@aerogel/core/components/ui/Toast.vue';
10
+ import { bootServices } from '@aerogel/core/services';
11
+ import { definePlugin } from '@aerogel/core/plugins';
5
12
 
6
13
  import UI, { UIComponents } from './UI';
7
- import AGAlertModal from '../components/modals/AGAlertModal.vue';
8
- import AGConfirmModal from '../components/modals/AGConfirmModal.vue';
9
- import AGErrorReportModal from '../components/modals/AGErrorReportModal.vue';
10
- import AGLoadingModal from '../components/modals/AGLoadingModal.vue';
11
- import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
12
14
  import type { UIComponent } from './UI';
13
15
 
14
- export { UI, UIComponents, UIComponent };
15
-
16
16
  const services = { $ui: UI };
17
17
 
18
+ export * from './UI';
19
+ export * from './utils';
20
+ export { default as UI } from './UI';
21
+
18
22
  export type UIServices = typeof services;
19
23
 
20
24
  export default definePlugin({
21
25
  async install(app, options) {
22
26
  const defaultComponents = {
23
- [UIComponents.AlertModal]: AGAlertModal,
24
- [UIComponents.ConfirmModal]: AGConfirmModal,
25
- [UIComponents.ErrorReportModal]: AGErrorReportModal,
26
- [UIComponents.LoadingModal]: AGLoadingModal,
27
- [UIComponents.Snackbar]: AGSnackbar,
27
+ [UIComponents.AlertModal]: AlertModal,
28
+ [UIComponents.ConfirmModal]: ConfirmModal,
29
+ [UIComponents.ErrorReportModal]: ErrorReportModal,
30
+ [UIComponents.LoadingModal]: LoadingModal,
31
+ [UIComponents.PromptModal]: PromptModal,
32
+ [UIComponents.Toast]: Toast,
33
+ [UIComponents.StartupCrash]: StartupCrash,
28
34
  };
29
35
 
30
36
  Object.entries({
@@ -36,12 +42,12 @@ export default definePlugin({
36
42
  },
37
43
  });
38
44
 
39
- declare module '@/bootstrap/options' {
40
- interface AerogelOptions {
45
+ declare module '@aerogel/core/bootstrap/options' {
46
+ export interface AerogelOptions {
41
47
  components?: Partial<Record<UIComponent, Component>>;
42
48
  }
43
49
  }
44
50
 
45
- declare module '@/services' {
51
+ declare module '@aerogel/core/services' {
46
52
  export interface Services extends UIServices {}
47
53
  }
@@ -0,0 +1,16 @@
1
+ export const MOBILE_BREAKPOINT = 768;
2
+
3
+ export const Layouts = {
4
+ Mobile: 'mobile',
5
+ Desktop: 'desktop',
6
+ } as const;
7
+
8
+ export type Layout = (typeof Layouts)[keyof typeof Layouts];
9
+
10
+ export function getCurrentLayout(): Layout {
11
+ if (globalThis.innerWidth > MOBILE_BREAKPOINT) {
12
+ return Layouts.Desktop;
13
+ }
14
+
15
+ return Layouts.Mobile;
16
+ }
@@ -0,0 +1,49 @@
1
+ import clsx from 'clsx';
2
+ import { computed, unref } from 'vue';
3
+ import { cva } from 'class-variance-authority';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import type { ClassValue } from 'clsx';
6
+ import type { ComputedRef, PropType, Ref } from 'vue';
7
+ import type { GetClosureArgs, GetClosureResult } from '@noeldemartin/utils';
8
+
9
+ export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
10
+ export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
11
+ export type RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
12
+ export type Variants<T extends Record<string, string | boolean>> = Required<{
13
+ [K in keyof T]: Exclude<T[K], undefined> extends string
14
+ ? { [key in Exclude<T[K], undefined>]: string | null }
15
+ : { true: string | null; false: string | null };
16
+ }>;
17
+
18
+ export type ComponentPropDefinitions<T> = {
19
+ [K in keyof T]: {
20
+ type?: PropType<T[K]>;
21
+ default: T[K] | (() => T[K]) | null;
22
+ };
23
+ };
24
+
25
+ export type PickComponentProps<TValues, TDefinitions> = {
26
+ [K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
27
+ };
28
+
29
+ export function computedVariantClasses<T>(
30
+ value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
31
+ config: { baseClasses?: string } & CVAConfig<T>,
32
+ ): ComputedRef<string> {
33
+ return computed(() => {
34
+ const { baseClasses: valueBaseClasses, ...valueRefs } = value;
35
+ const { baseClasses: configBaseClasses, ...configs } = config;
36
+ const variants = cva(configBaseClasses, configs as CVAConfig<T>);
37
+ const values = Object.entries(valueRefs).reduce((extractedValues, [name, valueRef]) => {
38
+ extractedValues[name as keyof CVAProps<T>] = unref(valueRef);
39
+
40
+ return extractedValues;
41
+ }, {} as CVAProps<T>);
42
+
43
+ return classes(variants(values), unref(valueBaseClasses));
44
+ });
45
+ }
46
+
47
+ export function classes(...inputs: ClassValue[]): string {
48
+ return twMerge(clsx(inputs));
49
+ }
@@ -1,19 +1,20 @@
1
1
  import { onUnmounted } from 'vue';
2
2
 
3
- import Events from '@/services/Events';
3
+ import Events from '@aerogel/core/services/Events';
4
4
  import type {
5
5
  EventListener,
6
6
  EventWithPayload,
7
7
  EventWithoutPayload,
8
8
  EventsPayload,
9
9
  UnknownEvent,
10
- } from '@/services/Events';
10
+ } from '@aerogel/core/services/Events';
11
11
 
12
12
  export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
13
13
  export function useEvent<Event extends EventWithPayload>(
14
14
  event: Event,
15
15
  listener: EventListener<EventsPayload[Event]>
16
16
  ): void;
17
+ export function useEvent<Payload>(event: string, listener: (payload: Payload) => unknown): void;
17
18
  export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
18
19
 
19
20
  export function useEvent(event: string, listener: EventListener): void {