@aerogel/core 0.0.0-next.88c59e62f64db70aedfbc4c31b5bbc287be44483 → 0.0.0-next.8bd66d5f5e264650120ea3cc37519f2409c6cc39

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 (200) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +2376 -990
  3. package/dist/aerogel-core.js +3669 -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 +41 -15
  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 +86 -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 +115 -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 +131 -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/Setting.vue +31 -0
  79. package/src/components/ui/SettingsModal.vue +15 -0
  80. package/src/components/ui/StartupCrash.vue +31 -0
  81. package/src/components/ui/Switch.vue +11 -0
  82. package/src/components/ui/TextArea.vue +56 -0
  83. package/src/components/ui/Toast.vue +46 -0
  84. package/src/components/ui/index.ts +35 -0
  85. package/src/directives/index.ts +13 -5
  86. package/src/directives/measure.ts +46 -0
  87. package/src/errors/Errors.state.ts +1 -1
  88. package/src/errors/Errors.ts +41 -37
  89. package/src/errors/JobCancelledError.ts +3 -0
  90. package/src/errors/index.ts +22 -28
  91. package/src/errors/settings/Debug.vue +32 -0
  92. package/src/errors/settings/index.ts +10 -0
  93. package/src/errors/utils.ts +35 -0
  94. package/src/forms/FormController.test.ts +110 -0
  95. package/src/forms/FormController.ts +250 -0
  96. package/src/forms/index.ts +3 -2
  97. package/src/forms/utils.ts +51 -20
  98. package/src/forms/validation.ts +19 -0
  99. package/src/index.css +76 -0
  100. package/src/{main.ts → index.ts} +3 -0
  101. package/src/jobs/Job.ts +147 -0
  102. package/src/jobs/index.ts +10 -0
  103. package/src/jobs/listeners.ts +3 -0
  104. package/src/jobs/status.ts +4 -0
  105. package/src/lang/DefaultLangProvider.ts +46 -0
  106. package/src/lang/Lang.state.ts +11 -0
  107. package/src/lang/Lang.ts +44 -29
  108. package/src/lang/index.ts +12 -6
  109. package/src/lang/settings/Language.vue +48 -0
  110. package/src/lang/settings/index.ts +10 -0
  111. package/src/plugins/Plugin.ts +1 -1
  112. package/src/plugins/index.ts +10 -7
  113. package/src/services/App.state.ts +39 -7
  114. package/src/services/App.ts +49 -6
  115. package/src/services/Cache.ts +43 -0
  116. package/src/services/Events.test.ts +39 -0
  117. package/src/services/Events.ts +110 -36
  118. package/src/services/Service.ts +154 -49
  119. package/src/services/Storage.ts +20 -0
  120. package/src/services/index.ts +20 -8
  121. package/src/services/store.ts +8 -5
  122. package/src/services/utils.ts +18 -0
  123. package/src/testing/index.ts +30 -0
  124. package/src/testing/setup.ts +11 -0
  125. package/src/ui/UI.state.ts +14 -12
  126. package/src/ui/UI.ts +331 -96
  127. package/src/ui/index.ts +32 -27
  128. package/src/ui/utils.ts +16 -0
  129. package/src/utils/classes.ts +41 -0
  130. package/src/utils/composition/events.ts +4 -5
  131. package/src/utils/composition/forms.ts +20 -4
  132. package/src/utils/composition/persistent.test.ts +33 -0
  133. package/src/utils/composition/persistent.ts +11 -0
  134. package/src/utils/composition/state.test.ts +47 -0
  135. package/src/utils/composition/state.ts +33 -0
  136. package/src/utils/index.ts +5 -0
  137. package/src/utils/markdown.test.ts +50 -0
  138. package/src/utils/markdown.ts +53 -6
  139. package/src/utils/types.ts +3 -0
  140. package/src/utils/vue.ts +38 -123
  141. package/.eslintrc.js +0 -3
  142. package/dist/aerogel-core.cjs.js +0 -2
  143. package/dist/aerogel-core.cjs.js.map +0 -1
  144. package/dist/aerogel-core.esm.js +0 -2
  145. package/dist/aerogel-core.esm.js.map +0 -1
  146. package/dist/virtual.d.ts +0 -11
  147. package/noeldemartin.config.js +0 -5
  148. package/src/components/AGAppLayout.vue +0 -11
  149. package/src/components/AGAppOverlays.vue +0 -37
  150. package/src/components/AGAppSnackbars.vue +0 -13
  151. package/src/components/basic/AGErrorMessage.vue +0 -16
  152. package/src/components/basic/AGLink.vue +0 -9
  153. package/src/components/basic/AGMarkdown.vue +0 -36
  154. package/src/components/basic/index.ts +0 -5
  155. package/src/components/constants.ts +0 -8
  156. package/src/components/forms/AGButton.vue +0 -44
  157. package/src/components/forms/AGCheckbox.vue +0 -35
  158. package/src/components/forms/AGForm.vue +0 -26
  159. package/src/components/forms/AGInput.vue +0 -36
  160. package/src/components/forms/index.ts +0 -4
  161. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  162. package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
  163. package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
  164. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  165. package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
  166. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -31
  167. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -45
  168. package/src/components/headless/forms/AGHeadlessSelectButton.ts +0 -3
  169. package/src/components/headless/forms/AGHeadlessSelectLabel.ts +0 -3
  170. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -8
  171. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  172. package/src/components/headless/forms/index.ts +0 -12
  173. package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
  174. package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
  175. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  176. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  177. package/src/components/headless/modals/index.ts +0 -6
  178. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  179. package/src/components/headless/snackbars/index.ts +0 -25
  180. package/src/components/modals/AGAlertModal.vue +0 -25
  181. package/src/components/modals/AGConfirmModal.vue +0 -30
  182. package/src/components/modals/AGErrorReportModal.ts +0 -20
  183. package/src/components/modals/AGErrorReportModal.vue +0 -62
  184. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  185. package/src/components/modals/AGLoadingModal.vue +0 -19
  186. package/src/components/modals/AGModal.ts +0 -10
  187. package/src/components/modals/AGModal.vue +0 -37
  188. package/src/components/modals/AGModalContext.ts +0 -8
  189. package/src/components/modals/AGModalContext.vue +0 -22
  190. package/src/components/modals/AGModalTitle.vue +0 -9
  191. package/src/components/modals/index.ts +0 -23
  192. package/src/components/snackbars/AGSnackbar.vue +0 -42
  193. package/src/components/snackbars/index.ts +0 -3
  194. package/src/directives/initial-focus.ts +0 -11
  195. package/src/forms/Form.test.ts +0 -58
  196. package/src/forms/Form.ts +0 -179
  197. package/src/forms/composition.ts +0 -6
  198. package/src/types/virtual.d.ts +0 -11
  199. package/tsconfig.json +0 -11
  200. package/vite.config.ts +0 -14
package/src/ui/UI.ts CHANGED
@@ -1,121 +1,299 @@
1
- import { facade, fail, uuid } from '@noeldemartin/utils';
2
- import { markRaw, nextTick } from 'vue';
1
+ import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
2
+ import { markRaw, nextTick, unref } 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
+ delay?: number;
68
+ }>;
69
+
70
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
71
+ extends ConfirmOptions {
72
+ checkboxes?: T;
73
+ }
31
74
 
32
- export interface ShowSnackbarOptions {
75
+ export type PromptOptions = AcceptRefs<{
76
+ label?: string;
77
+ defaultValue?: string;
78
+ placeholder?: string;
79
+ acceptText?: string;
80
+ acceptVariant?: ButtonVariant;
81
+ cancelText?: string;
82
+ cancelVariant?: ButtonVariant;
83
+ trim?: boolean;
84
+ }>;
85
+
86
+ export interface ToastOptions {
33
87
  component?: Component;
34
- color?: SnackbarColor;
35
- actions?: SnackbarAction[];
88
+ variant?: ToastVariant;
89
+ actions?: ToastAction[];
36
90
  }
37
91
 
38
92
  export class UIService extends Service {
39
93
 
40
94
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
41
- private components: Partial<Record<UIComponent, Component>> = {};
95
+ private components: Partial<UIComponents> = {};
96
+
97
+ public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
98
+ this.components[name] = component;
99
+ }
100
+
101
+ public resolveComponent<T extends keyof UIComponents>(name: T): UIComponents[T] | null {
102
+ return this.components[name] ?? null;
103
+ }
42
104
 
43
- public requireComponent(name: UIComponent): Component {
44
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
105
+ public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
106
+ return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
45
107
  }
46
108
 
47
109
  public alert(message: string): void;
48
110
  public alert(title: string, message: string): void;
49
111
  public alert(messageOrTitle: string, message?: string): void {
50
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
112
+ const getProperties = (): AlertModalProps => {
113
+ if (typeof message !== 'string') {
114
+ return { message: messageOrTitle };
115
+ }
116
+
117
+ return {
118
+ title: messageOrTitle,
119
+ message,
120
+ };
121
+ };
51
122
 
52
- this.openModal(this.requireComponent(UIComponents.AlertModal), options);
123
+ this.modal(this.requireComponent('alert-modal'), getProperties());
53
124
  }
54
125
 
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;
126
+ /* eslint-disable max-len */
127
+ public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
128
+ public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
129
+ public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
130
+ public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
131
+ /* eslint-enable max-len */
132
+
133
+ public async confirm(
134
+ messageOrTitle: string,
135
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
136
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
137
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
138
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
139
+ if (typeof messageOrOptions !== 'string') {
140
+ return {
141
+ ...(messageOrOptions ?? {}),
142
+ message: messageOrTitle,
143
+ required: !!messageOrOptions?.required,
144
+ };
145
+ }
64
146
 
65
- return result ?? false;
66
- }
147
+ return {
148
+ ...(options ?? {}),
149
+ title: messageOrTitle,
150
+ message: messageOrOptions,
151
+ required: !!options?.required,
152
+ };
153
+ };
67
154
 
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;
155
+ const properties = getProperties();
156
+ const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
157
+ const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
158
+ const checkboxes =
159
+ typeof result === 'object'
160
+ ? result[1]
161
+ : Object.entries(properties.checkboxes ?? {}).reduce(
162
+ (values, [checkbox, { default: defaultValue }]) => ({
163
+ [checkbox]: defaultValue ?? false,
164
+ ...values,
165
+ }),
166
+ {} as Record<string, boolean>,
167
+ );
168
+
169
+ for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
170
+ if (!checkbox.required || checkboxes[name]) {
171
+ continue;
172
+ }
72
173
 
73
- const message = typeof messageOrOperation === 'string' ? messageOrOperation : undefined;
74
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), { message });
75
- const result = await operation;
174
+ if (confirmed && isDevelopment()) {
175
+ // eslint-disable-next-line no-console
176
+ console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
177
+ }
76
178
 
77
- await this.closeModal(modal.id);
179
+ return [false, checkboxes];
180
+ }
78
181
 
79
- return result;
182
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
80
183
  }
81
184
 
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)),
185
+ public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
186
+ public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
187
+ public async prompt(
188
+ messageOrTitle: string,
189
+ messageOrOptions?: string | PromptOptions,
190
+ options?: PromptOptions,
191
+ ): Promise<string | null> {
192
+ const trim = options?.trim ?? true;
193
+ const getProperties = (): PromptModalProps => {
194
+ if (typeof messageOrOptions !== 'string') {
195
+ return {
196
+ message: messageOrTitle,
197
+ ...(messageOrOptions ?? {}),
198
+ } as PromptModalProps;
199
+ }
200
+
201
+ return {
202
+ title: messageOrTitle,
203
+ message: messageOrOptions,
204
+ ...(options ?? {}),
205
+ } as PromptModalProps;
87
206
  };
88
207
 
89
- this.setState('snackbars', this.snackbars.concat(snackbar));
208
+ const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
209
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
90
210
 
91
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
211
+ return result ?? null;
92
212
  }
93
213
 
94
- public hideSnackbar(id: string): void {
95
- this.setState(
96
- 'snackbars',
97
- this.snackbars.filter((snackbar) => snackbar.id !== id),
98
- );
214
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
215
+ public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
216
+ public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
217
+ public async loading<T>(
218
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
219
+ operation?: Promise<T> | (() => T),
220
+ ): Promise<T> {
221
+ const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
222
+ const processArgs = (): {
223
+ operationPromise: Promise<T>;
224
+ props?: AcceptRefs<LoadingModalProps>;
225
+ delay?: number;
226
+ } => {
227
+ if (typeof operationOrMessageOrOptions === 'string') {
228
+ return {
229
+ props: { message: operationOrMessageOrOptions },
230
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
231
+ };
232
+ }
233
+
234
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
235
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
236
+ }
237
+
238
+ const { delay, ...props } = operationOrMessageOrOptions;
239
+
240
+ return {
241
+ props,
242
+ delay: unref(delay),
243
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
244
+ };
245
+ };
246
+
247
+ let delayed = false;
248
+ const { operationPromise, props, delay } = processArgs();
249
+
250
+ delay && (await Promise.race([after({ ms: delay }).then(() => (delayed = true)), operationPromise]));
251
+
252
+ if (delay && !delayed) {
253
+ return operationPromise;
254
+ }
255
+
256
+ const modal = await this.modal(this.requireComponent('loading-modal'), props);
257
+
258
+ try {
259
+ const result = await operationPromise;
260
+
261
+ await after({ ms: 500 });
262
+
263
+ return result;
264
+ } finally {
265
+ await this.closeModal(modal.id);
266
+ }
99
267
  }
100
268
 
101
- public registerComponent(name: UIComponent, component: Component): void {
102
- this.components[name] = component;
269
+ public toast(message: string, options: ToastOptions = {}): void {
270
+ const { component, ...otherOptions } = options;
271
+ const toast: UIToast = {
272
+ id: uuid(),
273
+ properties: { message, ...otherOptions },
274
+ component: markRaw(component ?? this.requireComponent('toast')),
275
+ };
276
+
277
+ this.setState('toasts', this.toasts.concat(toast));
103
278
  }
104
279
 
105
- public async openModal<TModalComponent extends ModalComponent>(
106
- component: TModalComponent,
107
- properties?: ModalProperties<TModalComponent>,
108
- ): Promise<Modal<ModalResult<TModalComponent>>> {
280
+ public modal<T extends Component>(
281
+ ...args: {} extends ComponentProps<T>
282
+ ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
283
+ : [component: T, props: AcceptRefs<ComponentProps<T>>]
284
+ ): Promise<UIModal<ModalResult<T>>>;
285
+
286
+ public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
109
287
  const id = uuid();
110
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
111
- const modal: Modal<ModalResult<TModalComponent>> = {
288
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
289
+ const modal: UIModal<ModalResult<T>> = {
112
290
  id,
113
- properties: properties ?? {},
291
+ closing: false,
292
+ properties: props ?? {},
114
293
  component: markRaw(component),
115
294
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
116
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
295
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
117
296
  };
118
- const activeModal = this.modals.at(-1);
119
297
  const modals = this.modals.concat(modal);
120
298
 
121
299
  this.modalCallbacks[modal.id] = callbacks;
@@ -123,58 +301,115 @@ export class UIService extends Service {
123
301
  this.setState({ modals });
124
302
 
125
303
  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
304
 
132
305
  return modal;
133
306
  }
134
307
 
308
+ public modalForm<T extends Component>(
309
+ ...args: {} extends ComponentProps<T>
310
+ ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
311
+ : [component: T, props: AcceptRefs<ComponentProps<T>>]
312
+ ): Promise<ModalResult<T> | undefined>;
313
+
314
+ public async modalForm<T extends Component>(
315
+ component: T,
316
+ props?: ComponentProps<T>,
317
+ ): Promise<ModalResult<T> | undefined> {
318
+ const modal = await this.modal<T>(component, props as ComponentProps<T>);
319
+ const result = await modal.beforeClose;
320
+
321
+ return result;
322
+ }
323
+
135
324
  public async closeModal(id: string, result?: unknown): Promise<void> {
325
+ if (!App.isMounted()) {
326
+ await this.removeModal(id, result);
327
+
328
+ return;
329
+ }
330
+
136
331
  await Events.emit('close-modal', { id, result });
137
332
  }
138
333
 
139
- protected async boot(): Promise<void> {
334
+ public async closeAllModals(): Promise<void> {
335
+ while (this.modals.length > 0) {
336
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
337
+ }
338
+ }
339
+
340
+ protected override async boot(): Promise<void> {
140
341
  this.watchModalEvents();
342
+ this.watchMountedEvent();
343
+ this.watchViewportBreakpoints();
344
+ }
345
+
346
+ private async removeModal(id: string, result?: unknown): Promise<void> {
347
+ this.setState(
348
+ 'modals',
349
+ this.modals.filter((m) => m.id !== id),
350
+ );
351
+
352
+ this.modalCallbacks[id]?.hasClosed?.(result);
353
+
354
+ delete this.modalCallbacks[id];
141
355
  }
142
356
 
143
357
  private watchModalEvents(): void {
144
- Events.on('modal-will-close', ({ modal, result }) => {
145
- this.modalCallbacks[modal.id]?.willClose?.(result);
358
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
359
+ const modal = this.modals.find((_modal) => id === _modal.id);
146
360
 
147
- if (this.modals.length === 1) {
148
- Events.emit('hide-overlays-backdrop');
361
+ if (modal) {
362
+ modal.closing = true;
149
363
  }
364
+
365
+ this.modalCallbacks[id]?.willClose?.(result);
150
366
  });
151
367
 
152
- Events.on('modal-closed', async ({ modal, result }) => {
153
- this.setState(
154
- 'modals',
155
- this.modals.filter((m) => m.id !== modal.id),
156
- );
368
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
369
+ await this.removeModal(id, result);
370
+ });
371
+ }
157
372
 
158
- this.modalCallbacks[modal.id]?.closed?.(result);
373
+ private watchMountedEvent(): void {
374
+ Events.once('application-mounted', async () => {
375
+ if (!globalThis.document || !globalThis.getComputedStyle) {
376
+ return;
377
+ }
159
378
 
160
- delete this.modalCallbacks[modal.id];
379
+ const splash = globalThis.document.getElementById('splash');
161
380
 
162
- const activeModal = this.modals.at(-1);
381
+ if (!splash) {
382
+ return;
383
+ }
384
+
385
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
386
+ splash.style.opacity = '0';
387
+
388
+ await after({ ms: 600 });
389
+ }
163
390
 
164
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
391
+ splash.remove();
165
392
  });
166
393
  }
167
394
 
395
+ private watchViewportBreakpoints(): void {
396
+ if (!globalThis.matchMedia) {
397
+ return;
398
+ }
399
+
400
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
401
+
402
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
403
+ }
404
+
168
405
  }
169
406
 
170
- export default facade(new UIService());
407
+ export default facade(UIService);
171
408
 
172
- declare module '@/services/Events' {
409
+ declare module '@aerogel/core/services/Events' {
173
410
  export interface EventsPayload {
174
- 'modal-will-close': { modal: Modal; result?: unknown };
175
- 'modal-closed': { modal: Modal; result?: unknown };
176
411
  'close-modal': { id: string; result?: unknown };
177
- 'hide-modal': { id: string };
178
- 'show-modal': { id: string };
412
+ 'modal-will-close': { modal: UIModal; result?: unknown };
413
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
179
414
  }
180
415
  }
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
+ }