@aerogel/core 0.0.0-next.f8cdd39997c56dcd46e07c26af8a84d04d610fce → 0.0.0-next.fb0f08b1df2e4aff5b34e23b9927b06b58484c98

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 (190) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +2262 -905
  3. package/dist/aerogel-core.js +3541 -0
  4. package/dist/aerogel-core.js.map +1 -0
  5. package/package.json +38 -34
  6. package/src/bootstrap/bootstrap.test.ts +7 -10
  7. package/src/bootstrap/index.ts +43 -14
  8. package/src/bootstrap/options.ts +4 -1
  9. package/src/components/AppLayout.vue +14 -0
  10. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  11. package/src/components/AppOverlays.vue +9 -0
  12. package/src/components/AppToasts.vue +16 -0
  13. package/src/components/contracts/AlertModal.ts +19 -0
  14. package/src/components/contracts/Button.ts +16 -0
  15. package/src/components/contracts/ConfirmModal.ts +48 -0
  16. package/src/components/contracts/DropdownMenu.ts +25 -0
  17. package/src/components/contracts/ErrorReportModal.ts +33 -0
  18. package/src/components/contracts/Input.ts +26 -0
  19. package/src/components/contracts/LoadingModal.ts +26 -0
  20. package/src/components/contracts/Modal.ts +21 -0
  21. package/src/components/contracts/PromptModal.ts +34 -0
  22. package/src/components/contracts/Select.ts +45 -0
  23. package/src/components/contracts/Toast.ts +15 -0
  24. package/src/components/contracts/index.ts +11 -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/HeadlessInputLabel.vue +18 -0
  31. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  32. package/src/components/headless/HeadlessModal.vue +57 -0
  33. package/src/components/headless/HeadlessModalContent.vue +30 -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 +120 -0
  38. package/src/components/headless/HeadlessSelectError.vue +25 -0
  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 +42 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  44. package/src/components/headless/HeadlessSwitch.vue +96 -0
  45. package/src/components/headless/HeadlessToast.vue +18 -0
  46. package/src/components/headless/HeadlessToastAction.vue +13 -0
  47. package/src/components/headless/index.ts +20 -3
  48. package/src/components/index.ts +6 -9
  49. package/src/components/ui/AdvancedOptions.vue +18 -0
  50. package/src/components/ui/AlertModal.vue +17 -0
  51. package/src/components/ui/Button.vue +100 -0
  52. package/src/components/ui/Checkbox.vue +56 -0
  53. package/src/components/ui/ConfirmModal.vue +50 -0
  54. package/src/components/ui/DropdownMenu.vue +32 -0
  55. package/src/components/ui/DropdownMenuOption.vue +22 -0
  56. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  57. package/src/components/ui/EditableContent.vue +82 -0
  58. package/src/components/ui/ErrorLogs.vue +19 -0
  59. package/src/components/ui/ErrorLogsModal.vue +48 -0
  60. package/src/components/ui/ErrorMessage.vue +15 -0
  61. package/src/components/ui/ErrorReportModal.vue +73 -0
  62. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +38 -29
  63. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  64. package/src/components/ui/Form.vue +24 -0
  65. package/src/components/ui/Input.vue +56 -0
  66. package/src/components/ui/Link.vue +12 -0
  67. package/src/components/ui/LoadingModal.vue +34 -0
  68. package/src/components/ui/Markdown.vue +97 -0
  69. package/src/components/ui/Modal.vue +123 -0
  70. package/src/components/ui/ModalContext.vue +31 -0
  71. package/src/components/ui/ProgressBar.vue +51 -0
  72. package/src/components/ui/PromptModal.vue +38 -0
  73. package/src/components/ui/Select.vue +27 -0
  74. package/src/components/ui/SelectLabel.vue +21 -0
  75. package/src/components/ui/SelectOption.vue +29 -0
  76. package/src/components/ui/SelectOptions.vue +35 -0
  77. package/src/components/ui/SelectTrigger.vue +29 -0
  78. package/src/components/ui/SettingsModal.vue +15 -0
  79. package/src/components/ui/StartupCrash.vue +31 -0
  80. package/src/components/ui/Switch.vue +11 -0
  81. package/src/components/ui/Toast.vue +46 -0
  82. package/src/components/ui/index.ts +33 -0
  83. package/src/directives/index.ts +13 -5
  84. package/src/directives/measure.ts +46 -0
  85. package/src/errors/Errors.state.ts +1 -1
  86. package/src/errors/Errors.ts +41 -37
  87. package/src/errors/JobCancelledError.ts +3 -0
  88. package/src/errors/index.ts +19 -29
  89. package/src/errors/utils.ts +35 -0
  90. package/src/forms/FormController.test.ts +110 -0
  91. package/src/forms/FormController.ts +246 -0
  92. package/src/forms/index.ts +3 -2
  93. package/src/forms/utils.ts +51 -20
  94. package/src/forms/validation.ts +19 -0
  95. package/src/index.css +73 -0
  96. package/src/{main.ts → index.ts} +3 -0
  97. package/src/jobs/Job.ts +147 -0
  98. package/src/jobs/index.ts +10 -0
  99. package/src/jobs/listeners.ts +3 -0
  100. package/src/jobs/status.ts +4 -0
  101. package/src/lang/DefaultLangProvider.ts +46 -0
  102. package/src/lang/Lang.state.ts +11 -0
  103. package/src/lang/Lang.ts +44 -29
  104. package/src/lang/index.ts +12 -6
  105. package/src/lang/settings/Debug.vue +39 -0
  106. package/src/lang/settings/Language.vue +48 -0
  107. package/src/lang/settings/index.ts +15 -0
  108. package/src/plugins/Plugin.ts +2 -1
  109. package/src/plugins/index.ts +22 -0
  110. package/src/services/App.state.ts +40 -6
  111. package/src/services/App.ts +53 -5
  112. package/src/services/Cache.ts +43 -0
  113. package/src/services/Events.test.ts +39 -0
  114. package/src/services/Events.ts +110 -36
  115. package/src/services/Service.ts +154 -49
  116. package/src/services/Storage.ts +20 -0
  117. package/src/services/index.ts +18 -6
  118. package/src/services/store.ts +8 -5
  119. package/src/services/utils.ts +18 -0
  120. package/src/testing/index.ts +26 -0
  121. package/src/testing/setup.ts +11 -0
  122. package/src/ui/UI.state.ts +14 -12
  123. package/src/ui/UI.ts +314 -95
  124. package/src/ui/index.ts +32 -27
  125. package/src/ui/utils.ts +16 -0
  126. package/src/utils/classes.ts +41 -0
  127. package/src/utils/composition/events.ts +4 -5
  128. package/src/utils/composition/forms.ts +20 -4
  129. package/src/utils/composition/persistent.test.ts +33 -0
  130. package/src/utils/composition/persistent.ts +11 -0
  131. package/src/utils/composition/state.test.ts +47 -0
  132. package/src/utils/composition/state.ts +33 -0
  133. package/src/utils/index.ts +5 -0
  134. package/src/utils/markdown.test.ts +50 -0
  135. package/src/utils/markdown.ts +53 -6
  136. package/src/utils/types.ts +3 -0
  137. package/src/utils/vue.ts +38 -123
  138. package/.eslintrc.js +0 -3
  139. package/dist/aerogel-core.cjs.js +0 -2
  140. package/dist/aerogel-core.cjs.js.map +0 -1
  141. package/dist/aerogel-core.esm.js +0 -2
  142. package/dist/aerogel-core.esm.js.map +0 -1
  143. package/dist/virtual.d.ts +0 -11
  144. package/noeldemartin.config.js +0 -5
  145. package/src/components/AGAppLayout.vue +0 -11
  146. package/src/components/AGAppOverlays.vue +0 -37
  147. package/src/components/AGAppSnackbars.vue +0 -13
  148. package/src/components/basic/AGErrorMessage.vue +0 -16
  149. package/src/components/basic/AGLink.vue +0 -9
  150. package/src/components/basic/AGMarkdown.vue +0 -36
  151. package/src/components/basic/index.ts +0 -5
  152. package/src/components/constants.ts +0 -8
  153. package/src/components/forms/AGButton.vue +0 -44
  154. package/src/components/forms/AGCheckbox.vue +0 -35
  155. package/src/components/forms/AGForm.vue +0 -26
  156. package/src/components/forms/AGInput.vue +0 -36
  157. package/src/components/forms/index.ts +0 -6
  158. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  159. package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
  160. package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
  161. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  162. package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
  163. package/src/components/headless/forms/index.ts +0 -6
  164. package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
  165. package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
  166. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  167. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  168. package/src/components/headless/modals/index.ts +0 -6
  169. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  170. package/src/components/headless/snackbars/index.ts +0 -25
  171. package/src/components/modals/AGAlertModal.vue +0 -25
  172. package/src/components/modals/AGConfirmModal.vue +0 -30
  173. package/src/components/modals/AGErrorReportModal.ts +0 -20
  174. package/src/components/modals/AGErrorReportModal.vue +0 -62
  175. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  176. package/src/components/modals/AGLoadingModal.vue +0 -19
  177. package/src/components/modals/AGModal.ts +0 -10
  178. package/src/components/modals/AGModal.vue +0 -36
  179. package/src/components/modals/AGModalContext.ts +0 -8
  180. package/src/components/modals/AGModalContext.vue +0 -22
  181. package/src/components/modals/index.ts +0 -21
  182. package/src/components/snackbars/AGSnackbar.vue +0 -42
  183. package/src/components/snackbars/index.ts +0 -3
  184. package/src/directives/initial-focus.ts +0 -11
  185. package/src/forms/Form.test.ts +0 -58
  186. package/src/forms/Form.ts +0 -176
  187. package/src/forms/composition.ts +0 -6
  188. package/src/types/virtual.d.ts +0 -11
  189. package/tsconfig.json +0 -11
  190. package/vite.config.ts +0 -14
package/src/ui/UI.ts CHANGED
@@ -1,121 +1,283 @@
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
+ import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
3
4
  import type { Component } from 'vue';
4
- import type { ObjectValues } from '@noeldemartin/utils';
5
-
6
- import Events from '@/services/Events';
7
- import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
5
+ import type { ClosureArgs } from '@noeldemartin/utils';
6
+
7
+ import App from '@aerogel/core/services/App';
8
+ import Events from '@aerogel/core/services/Events';
9
+ import type {
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';
8
24
 
9
25
  import Service from './UI.state';
10
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
26
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
27
+ import type { UIModal, UIToast } from './UI.state';
11
28
 
12
29
  interface ModalCallbacks<T = unknown> {
13
30
  willClose(result: T | undefined): void;
14
- closed(result: T | undefined): void;
31
+ hasClosed(result: T | undefined): void;
15
32
  }
16
33
 
17
- 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;
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
+ }
21
48
 
22
- export const UIComponents = {
23
- AlertModal: 'alert-modal',
24
- ConfirmModal: 'confirm-modal',
25
- ErrorReportModal: 'error-report-modal',
26
- LoadingModal: 'loading-modal',
27
- Snackbar: 'snackbar',
28
- } as const;
49
+ export interface UIModalContext {
50
+ modal: UIModal;
51
+ childIndex?: number;
52
+ }
29
53
 
30
- export type UIComponent = ObjectValues<typeof UIComponents>;
54
+ export type ConfirmOptions = AcceptRefs<{
55
+ acceptText?: string;
56
+ acceptVariant?: ButtonVariant;
57
+ cancelText?: string;
58
+ cancelVariant?: ButtonVariant;
59
+ actions?: Record<string, () => unknown>;
60
+ required?: boolean;
61
+ }>;
62
+
63
+ export type LoadingOptions = AcceptRefs<{
64
+ title?: string;
65
+ message?: string;
66
+ progress?: number;
67
+ }>;
68
+
69
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
70
+ extends ConfirmOptions {
71
+ checkboxes?: T;
72
+ }
31
73
 
32
- export interface ShowSnackbarOptions {
74
+ export type PromptOptions = AcceptRefs<{
75
+ label?: string;
76
+ defaultValue?: string;
77
+ placeholder?: string;
78
+ acceptText?: string;
79
+ acceptVariant?: ButtonVariant;
80
+ cancelText?: string;
81
+ cancelVariant?: ButtonVariant;
82
+ trim?: boolean;
83
+ }>;
84
+
85
+ export interface ToastOptions {
33
86
  component?: Component;
34
- color?: SnackbarColor;
35
- actions?: SnackbarAction[];
87
+ variant?: ToastVariant;
88
+ actions?: ToastAction[];
36
89
  }
37
90
 
38
91
  export class UIService extends Service {
39
92
 
40
93
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
41
- 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
+ }
42
103
 
43
- public requireComponent(name: UIComponent): Component {
44
- 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!`);
45
106
  }
46
107
 
47
108
  public alert(message: string): void;
48
109
  public alert(title: string, message: string): void;
49
110
  public alert(messageOrTitle: string, message?: string): void {
50
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
111
+ const getProperties = (): AlertModalProps => {
112
+ if (typeof message !== 'string') {
113
+ return { message: messageOrTitle };
114
+ }
115
+
116
+ return {
117
+ title: messageOrTitle,
118
+ message,
119
+ };
120
+ };
51
121
 
52
- this.openModal(this.requireComponent(UIComponents.AlertModal), options);
122
+ this.modal(this.requireComponent('alert-modal'), getProperties());
53
123
  }
54
124
 
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>>(
60
- this.requireComponent(UIComponents.ConfirmModal),
61
- options,
62
- );
63
- const result = await modal.beforeClose;
125
+ /* eslint-disable max-len */
126
+ public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
127
+ public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
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
130
+ /* eslint-enable max-len */
131
+
132
+ public async confirm(
133
+ messageOrTitle: string,
134
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
135
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
136
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
137
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
138
+ if (typeof messageOrOptions !== 'string') {
139
+ return {
140
+ ...(messageOrOptions ?? {}),
141
+ message: messageOrTitle,
142
+ required: !!messageOrOptions?.required,
143
+ };
144
+ }
64
145
 
65
- return result ?? false;
66
- }
146
+ return {
147
+ ...(options ?? {}),
148
+ title: messageOrTitle,
149
+ message: messageOrOptions,
150
+ required: !!options?.required,
151
+ };
152
+ };
67
153
 
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;
154
+ const properties = getProperties();
155
+ const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
156
+ const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
157
+ const checkboxes =
158
+ typeof result === 'object'
159
+ ? result[1]
160
+ : Object.entries(properties.checkboxes ?? {}).reduce(
161
+ (values, [checkbox, { default: defaultValue }]) => ({
162
+ [checkbox]: defaultValue ?? false,
163
+ ...values,
164
+ }),
165
+ {} as Record<string, boolean>,
166
+ );
167
+
168
+ for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
169
+ if (!checkbox.required || checkboxes[name]) {
170
+ continue;
171
+ }
72
172
 
73
- const message = typeof messageOrOperation === 'string' ? messageOrOperation : undefined;
74
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), { message });
75
- const result = await operation;
173
+ if (confirmed && isDevelopment()) {
174
+ // eslint-disable-next-line no-console
175
+ console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
176
+ }
76
177
 
77
- await this.closeModal(modal.id);
178
+ return [false, checkboxes];
179
+ }
78
180
 
79
- return result;
181
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
80
182
  }
81
183
 
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)),
184
+ public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
185
+ public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
186
+ public async prompt(
187
+ messageOrTitle: string,
188
+ messageOrOptions?: string | PromptOptions,
189
+ options?: PromptOptions,
190
+ ): Promise<string | null> {
191
+ const trim = options?.trim ?? true;
192
+ const getProperties = (): PromptModalProps => {
193
+ if (typeof messageOrOptions !== 'string') {
194
+ return {
195
+ message: messageOrTitle,
196
+ ...(messageOrOptions ?? {}),
197
+ } as PromptModalProps;
198
+ }
199
+
200
+ return {
201
+ title: messageOrTitle,
202
+ message: messageOrOptions,
203
+ ...(options ?? {}),
204
+ } as PromptModalProps;
87
205
  };
88
206
 
89
- this.setState('snackbars', this.snackbars.concat(snackbar));
207
+ const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
208
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
90
209
 
91
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
210
+ return result ?? null;
92
211
  }
93
212
 
94
- public hideSnackbar(id: string): void {
95
- this.setState(
96
- 'snackbars',
97
- this.snackbars.filter((snackbar) => snackbar.id !== id),
98
- );
213
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
214
+ public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
215
+ public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
216
+ public async loading<T>(
217
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
218
+ operation?: Promise<T> | (() => T),
219
+ ): Promise<T> {
220
+ const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
221
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
222
+ if (typeof operationOrMessageOrOptions === 'string') {
223
+ return {
224
+ props: { message: operationOrMessageOrOptions },
225
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
226
+ };
227
+ }
228
+
229
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
230
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
231
+ }
232
+
233
+ return {
234
+ props: operationOrMessageOrOptions,
235
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
236
+ };
237
+ };
238
+
239
+ const { operationPromise, props } = processArgs();
240
+ const modal = await this.modal(this.requireComponent('loading-modal'), props);
241
+
242
+ try {
243
+ const result = await operationPromise;
244
+
245
+ await after({ ms: 500 });
246
+
247
+ return result;
248
+ } finally {
249
+ await this.closeModal(modal.id);
250
+ }
99
251
  }
100
252
 
101
- public registerComponent(name: UIComponent, component: Component): void {
102
- this.components[name] = component;
253
+ public toast(message: string, options: ToastOptions = {}): void {
254
+ const { component, ...otherOptions } = options;
255
+ const toast: UIToast = {
256
+ id: uuid(),
257
+ properties: { message, ...otherOptions },
258
+ component: markRaw(component ?? this.requireComponent('toast')),
259
+ };
260
+
261
+ this.setState('toasts', this.toasts.concat(toast));
103
262
  }
104
263
 
105
- public async openModal<TModalComponent extends ModalComponent>(
106
- component: TModalComponent,
107
- properties?: ModalProperties<TModalComponent>,
108
- ): Promise<Modal<ModalResult<TModalComponent>>> {
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>>>;
269
+
270
+ public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
109
271
  const id = uuid();
110
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
111
- const modal: Modal<ModalResult<TModalComponent>> = {
272
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
273
+ const modal: UIModal<ModalResult<T>> = {
112
274
  id,
113
- properties: properties ?? {},
275
+ closing: false,
276
+ properties: props ?? {},
114
277
  component: markRaw(component),
115
278
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
116
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
279
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
117
280
  };
118
- const activeModal = this.modals.at(-1);
119
281
  const modals = this.modals.concat(modal);
120
282
 
121
283
  this.modalCallbacks[modal.id] = callbacks;
@@ -123,58 +285,115 @@ export class UIService extends Service {
123
285
  this.setState({ modals });
124
286
 
125
287
  await nextTick();
126
- await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
127
- await Promise.all([
128
- activeModal || Events.emit('show-overlays-backdrop'),
129
- Events.emit('show-modal', { id: modal.id }),
130
- ]);
131
288
 
132
289
  return modal;
133
290
  }
134
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
+
135
308
  public async closeModal(id: string, result?: unknown): Promise<void> {
309
+ if (!App.isMounted()) {
310
+ await this.removeModal(id, result);
311
+
312
+ return;
313
+ }
314
+
136
315
  await Events.emit('close-modal', { id, result });
137
316
  }
138
317
 
139
- protected async boot(): Promise<void> {
318
+ public async closeAllModals(): Promise<void> {
319
+ while (this.modals.length > 0) {
320
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
321
+ }
322
+ }
323
+
324
+ protected override async boot(): Promise<void> {
140
325
  this.watchModalEvents();
326
+ this.watchMountedEvent();
327
+ this.watchViewportBreakpoints();
328
+ }
329
+
330
+ private async removeModal(id: string, result?: unknown): Promise<void> {
331
+ this.setState(
332
+ 'modals',
333
+ this.modals.filter((m) => m.id !== id),
334
+ );
335
+
336
+ this.modalCallbacks[id]?.hasClosed?.(result);
337
+
338
+ delete this.modalCallbacks[id];
141
339
  }
142
340
 
143
341
  private watchModalEvents(): void {
144
- Events.on('modal-will-close', ({ modal, result }) => {
145
- 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);
146
344
 
147
- if (this.modals.length === 1) {
148
- Events.emit('hide-overlays-backdrop');
345
+ if (modal) {
346
+ modal.closing = true;
149
347
  }
348
+
349
+ this.modalCallbacks[id]?.willClose?.(result);
350
+ });
351
+
352
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
353
+ await this.removeModal(id, result);
150
354
  });
355
+ }
151
356
 
152
- Events.on('modal-closed', async ({ modal, result }) => {
153
- this.setState(
154
- 'modals',
155
- this.modals.filter((m) => m.id !== modal.id),
156
- );
357
+ private watchMountedEvent(): void {
358
+ Events.once('application-mounted', async () => {
359
+ if (!globalThis.document || !globalThis.getComputedStyle) {
360
+ return;
361
+ }
362
+
363
+ const splash = globalThis.document.getElementById('splash');
157
364
 
158
- this.modalCallbacks[modal.id]?.closed?.(result);
365
+ if (!splash) {
366
+ return;
367
+ }
159
368
 
160
- delete this.modalCallbacks[modal.id];
369
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
370
+ splash.style.opacity = '0';
161
371
 
162
- const activeModal = this.modals.at(-1);
372
+ await after({ ms: 600 });
373
+ }
163
374
 
164
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
375
+ splash.remove();
165
376
  });
166
377
  }
167
378
 
379
+ private watchViewportBreakpoints(): void {
380
+ if (!globalThis.matchMedia) {
381
+ return;
382
+ }
383
+
384
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
385
+
386
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
387
+ }
388
+
168
389
  }
169
390
 
170
- export default facade(new UIService());
391
+ export default facade(UIService);
171
392
 
172
- declare module '@/services/Events' {
393
+ declare module '@aerogel/core/services/Events' {
173
394
  export interface EventsPayload {
174
- 'modal-will-close': { modal: Modal; result?: unknown };
175
- 'modal-closed': { modal: Modal; result?: unknown };
176
395
  'close-modal': { id: string; result?: unknown };
177
- 'hide-modal': { id: string };
178
- 'show-modal': { id: string };
396
+ 'modal-will-close': { modal: UIModal; result?: unknown };
397
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
179
398
  }
180
399
  }
package/src/ui/index.ts CHANGED
@@ -1,47 +1,52 @@
1
+ import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
2
+ import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
3
+ import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
4
+ import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
5
+ import PromptModal from '@aerogel/core/components/ui/PromptModal.vue';
6
+ import StartupCrash from '@aerogel/core/components/ui/StartupCrash.vue';
7
+ import Toast from '@aerogel/core/components/ui/Toast.vue';
8
+ import { bootServices } from '@aerogel/core/services';
9
+ import { definePlugin } from '@aerogel/core/plugins';
10
+
11
+ import UI from './UI';
12
+ import type { UIComponents } from './UI';
1
13
  import type { Component } from 'vue';
2
14
 
3
- import { bootServices } from '@/services';
4
- import { definePlugin } from '@/plugins';
5
-
6
- 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
- import type { UIComponent } from './UI';
13
-
14
- export { UI, UIComponents, UIComponent };
15
-
16
15
  const services = { $ui: UI };
17
16
 
17
+ export * from './UI';
18
+ export * from './utils';
19
+ export { default as UI } from './UI';
20
+
18
21
  export type UIServices = typeof services;
19
22
 
20
23
  export default definePlugin({
21
24
  async install(app, options) {
22
- const defaultComponents = {
23
- [UIComponents.AlertModal]: AGAlertModal,
24
- [UIComponents.ConfirmModal]: AGConfirmModal,
25
- [UIComponents.ErrorReportModal]: AGErrorReportModal,
26
- [UIComponents.LoadingModal]: AGLoadingModal,
27
- [UIComponents.Snackbar]: AGSnackbar,
25
+ const components: Partial<Record<keyof UIComponents, Component>> = {
26
+ 'alert-modal': AlertModal,
27
+ 'confirm-modal': ConfirmModal,
28
+ 'error-report-modal': ErrorReportModal,
29
+ 'loading-modal': LoadingModal,
30
+ 'prompt-modal': PromptModal,
31
+ 'startup-crash': StartupCrash,
32
+ 'toast': Toast,
33
+ ...options.components,
28
34
  };
29
35
 
30
- Object.entries({
31
- ...defaultComponents,
32
- ...options.components,
33
- }).forEach(([name, component]) => UI.registerComponent(name as UIComponent, component));
36
+ for (const [name, component] of Object.entries(components)) {
37
+ UI.registerComponent(name as keyof UIComponents, component as UIComponents[keyof UIComponents]);
38
+ }
34
39
 
35
40
  await bootServices(app, services);
36
41
  },
37
42
  });
38
43
 
39
- declare module '@/bootstrap/options' {
40
- interface AerogelOptions {
41
- components?: Partial<Record<UIComponent, Component>>;
44
+ declare module '@aerogel/core/bootstrap/options' {
45
+ export interface AerogelOptions {
46
+ components?: Partial<Partial<UIComponents>>;
42
47
  }
43
48
  }
44
49
 
45
- declare module '@/services' {
50
+ declare module '@aerogel/core/services' {
46
51
  export interface Services extends UIServices {}
47
52
  }
@@ -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,41 @@
1
+ import clsx from 'clsx';
2
+ import { 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 { PropType } 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 Variants<T extends Record<string, string | boolean>> = Required<{
12
+ [K in keyof T]: Exclude<T[K], undefined> extends string
13
+ ? { [key in Exclude<T[K], undefined>]: string | null }
14
+ : { true: string | null; false: string | null };
15
+ }>;
16
+
17
+ export type ComponentPropDefinitions<T> = {
18
+ [K in keyof T]: {
19
+ type?: PropType<T[K]>;
20
+ default: T[K] | (() => T[K]) | null;
21
+ };
22
+ };
23
+
24
+ export type PickComponentProps<TValues, TDefinitions> = {
25
+ [K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
26
+ };
27
+
28
+ export function variantClasses<T>(
29
+ value: { baseClasses?: string } & CVAProps<T>,
30
+ config: { baseClasses?: string } & CVAConfig<T>,
31
+ ): string {
32
+ const { baseClasses: valueBaseClasses, ...values } = value;
33
+ const { baseClasses: configBaseClasses, ...configs } = config;
34
+ const variants = cva(configBaseClasses, configs as CVAConfig<T>);
35
+
36
+ return classes(variants(values as CVAProps<T>), unref(valueBaseClasses));
37
+ }
38
+
39
+ export function classes(...inputs: ClassValue[]): string {
40
+ return twMerge(clsx(inputs));
41
+ }
@@ -1,23 +1,22 @@
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
- UnknownEvent,
10
- } from '@/services/Events';
9
+ } from '@aerogel/core/services/Events';
11
10
 
12
11
  export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
13
12
  export function useEvent<Event extends EventWithPayload>(
14
13
  event: Event,
15
14
  listener: EventListener<EventsPayload[Event]>
16
15
  ): void;
17
- export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
18
16
 
19
17
  export function useEvent(event: string, listener: EventListener): void {
20
- const unsubscribe = Events.on(event, listener);
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const unsubscribe = Events.on(event as any, listener);
21
20
 
22
21
  onUnmounted(() => unsubscribe());
23
22
  }