@aerogel/core 0.0.0-next.c8f032a868370824898e171969aec1bb6827688e → 0.0.0-next.ce4783d09a83f492e439f8d4c39bc0b4998f4cbf

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 (178) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +2387 -580
  3. package/dist/aerogel-core.js +3557 -0
  4. package/dist/aerogel-core.js.map +1 -0
  5. package/package.json +39 -34
  6. package/src/bootstrap/bootstrap.test.ts +7 -10
  7. package/src/bootstrap/index.ts +41 -9
  8. package/src/bootstrap/options.ts +4 -1
  9. package/src/components/AppLayout.vue +14 -0
  10. package/src/components/AppModals.vue +14 -0
  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 -2
  48. package/src/components/index.ts +6 -7
  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/ui/ErrorReportModalButtons.vue +118 -0
  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 +27 -6
  84. package/src/directives/measure.ts +46 -0
  85. package/src/errors/Errors.state.ts +31 -0
  86. package/src/errors/Errors.ts +187 -0
  87. package/src/errors/JobCancelledError.ts +3 -0
  88. package/src/errors/index.ts +53 -0
  89. package/src/errors/settings/Debug.vue +39 -0
  90. package/src/errors/settings/index.ts +10 -0
  91. package/src/errors/utils.ts +35 -0
  92. package/src/forms/FormController.test.ts +110 -0
  93. package/src/forms/FormController.ts +246 -0
  94. package/src/forms/index.ts +3 -2
  95. package/src/forms/utils.ts +62 -14
  96. package/src/forms/validation.ts +19 -0
  97. package/src/index.css +73 -0
  98. package/src/{main.ts → index.ts} +4 -2
  99. package/src/jobs/Job.ts +147 -0
  100. package/src/jobs/index.ts +10 -0
  101. package/src/jobs/listeners.ts +3 -0
  102. package/src/jobs/status.ts +4 -0
  103. package/src/lang/DefaultLangProvider.ts +46 -0
  104. package/src/lang/Lang.state.ts +11 -0
  105. package/src/lang/Lang.ts +45 -22
  106. package/src/lang/index.ts +14 -10
  107. package/src/lang/settings/Language.vue +48 -0
  108. package/src/lang/settings/index.ts +10 -0
  109. package/src/lang/utils.ts +4 -0
  110. package/src/plugins/Plugin.ts +2 -1
  111. package/src/plugins/index.ts +22 -0
  112. package/src/services/App.state.ts +43 -3
  113. package/src/services/App.ts +59 -3
  114. package/src/services/Cache.ts +43 -0
  115. package/src/services/Events.test.ts +39 -0
  116. package/src/services/Events.ts +110 -36
  117. package/src/services/Service.ts +245 -53
  118. package/src/services/Storage.ts +20 -0
  119. package/src/services/index.ts +38 -9
  120. package/src/services/store.ts +30 -0
  121. package/src/services/utils.ts +18 -0
  122. package/src/testing/index.ts +26 -0
  123. package/src/testing/setup.ts +11 -0
  124. package/src/ui/UI.state.ts +21 -9
  125. package/src/ui/UI.ts +327 -64
  126. package/src/ui/index.ts +33 -22
  127. package/src/ui/utils.ts +16 -0
  128. package/src/utils/classes.ts +41 -0
  129. package/src/utils/composition/events.ts +4 -5
  130. package/src/utils/composition/forms.ts +27 -0
  131. package/src/utils/composition/persistent.test.ts +33 -0
  132. package/src/utils/composition/persistent.ts +11 -0
  133. package/src/utils/composition/state.test.ts +47 -0
  134. package/src/utils/composition/state.ts +33 -0
  135. package/src/utils/index.ts +6 -0
  136. package/src/utils/markdown.test.ts +50 -0
  137. package/src/utils/markdown.ts +60 -4
  138. package/src/utils/types.ts +3 -0
  139. package/src/utils/vue.ts +38 -121
  140. package/.eslintrc.js +0 -3
  141. package/dist/aerogel-core.cjs.js +0 -2
  142. package/dist/aerogel-core.cjs.js.map +0 -1
  143. package/dist/aerogel-core.esm.js +0 -2
  144. package/dist/aerogel-core.esm.js.map +0 -1
  145. package/noeldemartin.config.js +0 -2
  146. package/src/components/AGAppLayout.vue +0 -11
  147. package/src/components/AGAppOverlays.vue +0 -39
  148. package/src/components/basic/AGMarkdown.vue +0 -35
  149. package/src/components/basic/index.ts +0 -3
  150. package/src/components/forms/AGButton.vue +0 -21
  151. package/src/components/forms/AGForm.vue +0 -26
  152. package/src/components/forms/AGInput.vue +0 -32
  153. package/src/components/forms/index.ts +0 -5
  154. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  155. package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
  156. package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
  157. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -33
  158. package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
  159. package/src/components/headless/forms/index.ts +0 -6
  160. package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
  161. package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
  162. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -24
  163. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  164. package/src/components/headless/modals/index.ts +0 -6
  165. package/src/components/modals/AGAlertModal.vue +0 -26
  166. package/src/components/modals/AGConfirmModal.vue +0 -30
  167. package/src/components/modals/AGModal.ts +0 -10
  168. package/src/components/modals/AGModal.vue +0 -18
  169. package/src/components/modals/AGModalContext.ts +0 -8
  170. package/src/components/modals/AGModalContext.vue +0 -22
  171. package/src/components/modals/index.ts +0 -7
  172. package/src/directives/initial-focus.ts +0 -11
  173. package/src/forms/Form.test.ts +0 -37
  174. package/src/forms/Form.ts +0 -166
  175. package/src/forms/composition.ts +0 -6
  176. package/src/globals.ts +0 -6
  177. package/tsconfig.json +0 -10
  178. package/vite.config.ts +0 -13
package/src/ui/UI.ts CHANGED
@@ -1,74 +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';
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';
7
24
 
8
25
  import Service from './UI.state';
9
- import type { Modal, ModalComponent } from './UI.state';
26
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
27
+ import type { UIModal, UIToast } from './UI.state';
10
28
 
11
29
  interface ModalCallbacks<T = unknown> {
12
30
  willClose(result: T | undefined): void;
13
- closed(result: T | undefined): void;
31
+ hasClosed(result: T | undefined): void;
14
32
  }
15
33
 
16
- type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
17
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
18
- ? TResult
19
- : 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
+ }
20
48
 
21
- export const UIComponents = {
22
- AlertModal: 'alert-modal',
23
- ConfirmModal: 'confirm-modal',
24
- } as const;
49
+ export interface UIModalContext {
50
+ modal: UIModal;
51
+ childIndex?: number;
52
+ }
25
53
 
26
- 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
+ }
73
+
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 {
86
+ component?: Component;
87
+ variant?: ToastVariant;
88
+ actions?: ToastAction[];
89
+ }
27
90
 
28
91
  export class UIService extends Service {
29
92
 
30
93
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
31
- 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
+ }
103
+
104
+ public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
105
+ return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
106
+ }
32
107
 
33
108
  public alert(message: string): void;
34
109
  public alert(title: string, message: string): void;
35
110
  public alert(messageOrTitle: string, message?: string): void {
36
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
111
+ const getProperties = (): AlertModalProps => {
112
+ if (typeof message !== 'string') {
113
+ return { message: messageOrTitle };
114
+ }
37
115
 
38
- this.openModal(this.requireComponent(UIComponents.AlertModal), options);
116
+ return {
117
+ title: messageOrTitle,
118
+ message,
119
+ };
120
+ };
121
+
122
+ this.modal(this.requireComponent('alert-modal'), getProperties());
39
123
  }
40
124
 
41
- public async confirm(message: string): Promise<boolean>;
42
- public async confirm(title: string, message: string): Promise<boolean>;
43
- public async confirm(messageOrTitle: string, message?: string): Promise<boolean> {
44
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
45
- const modal = await this.openModal<ModalComponent<{ message: string }, boolean>>(
46
- this.requireComponent(UIComponents.ConfirmModal),
47
- options,
48
- );
49
- 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
+ }
145
+
146
+ return {
147
+ ...(options ?? {}),
148
+ title: messageOrTitle,
149
+ message: messageOrOptions,
150
+ required: !!options?.required,
151
+ };
152
+ };
153
+
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
+ }
50
172
 
51
- return result ?? false;
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
+ }
177
+
178
+ return [false, checkboxes];
179
+ }
180
+
181
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
52
182
  }
53
183
 
54
- public registerComponent(name: UIComponent, component: Component): void {
55
- this.components[name] = component;
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;
205
+ };
206
+
207
+ const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
208
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
209
+
210
+ return result ?? null;
211
+ }
212
+
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
+ }
251
+ }
252
+
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));
56
262
  }
57
263
 
58
- public async openModal<TModalComponent extends ModalComponent>(
59
- component: TModalComponent,
60
- properties?: ModalProperties<TModalComponent>,
61
- ): 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>>> {
62
271
  const id = uuid();
63
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
64
- const modal: Modal<ModalResult<TModalComponent>> = {
272
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
273
+ const modal: UIModal<ModalResult<T>> = {
65
274
  id,
66
- properties: properties ?? {},
275
+ closing: false,
276
+ properties: props ?? {},
67
277
  component: markRaw(component),
68
278
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
69
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
279
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
70
280
  };
71
- const activeModal = this.modals.at(-1);
72
281
  const modals = this.modals.concat(modal);
73
282
 
74
283
  this.modalCallbacks[modal.id] = callbacks;
@@ -76,61 +285,115 @@ export class UIService extends Service {
76
285
  this.setState({ modals });
77
286
 
78
287
  await nextTick();
79
- await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
80
- await Promise.all([
81
- activeModal || Events.emit('show-overlays-backdrop'),
82
- Events.emit('show-modal', { id: modal.id }),
83
- ]);
84
288
 
85
289
  return modal;
86
290
  }
87
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
+
88
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
+
89
315
  await Events.emit('close-modal', { id, result });
90
316
  }
91
317
 
92
- protected async boot(): Promise<void> {
93
- await super.boot();
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
+ }
94
323
 
324
+ protected override async boot(): Promise<void> {
95
325
  this.watchModalEvents();
326
+ this.watchMountedEvent();
327
+ this.watchViewportBreakpoints();
96
328
  }
97
329
 
98
- private requireComponent(name: UIComponent): Component {
99
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
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];
100
339
  }
101
340
 
102
341
  private watchModalEvents(): void {
103
- Events.on('modal-will-close', ({ modal, result }) => {
104
- 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);
105
344
 
106
- if (this.modals.length === 1) {
107
- Events.emit('hide-overlays-backdrop');
345
+ if (modal) {
346
+ modal.closing = true;
108
347
  }
348
+
349
+ this.modalCallbacks[id]?.willClose?.(result);
109
350
  });
110
351
 
111
- Events.on('modal-closed', async ({ modal, result }) => {
112
- this.setState({ modals: this.modals.filter((m) => m.id !== modal.id) });
352
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
353
+ await this.removeModal(id, result);
354
+ });
355
+ }
113
356
 
114
- this.modalCallbacks[modal.id]?.closed?.(result);
357
+ private watchMountedEvent(): void {
358
+ Events.once('application-mounted', async () => {
359
+ if (!globalThis.document || !globalThis.getComputedStyle) {
360
+ return;
361
+ }
115
362
 
116
- delete this.modalCallbacks[modal.id];
363
+ const splash = globalThis.document.getElementById('splash');
117
364
 
118
- const activeModal = this.modals.at(-1);
365
+ if (!splash) {
366
+ return;
367
+ }
368
+
369
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
370
+ splash.style.opacity = '0';
371
+
372
+ await after({ ms: 600 });
373
+ }
119
374
 
120
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
375
+ splash.remove();
121
376
  });
122
377
  }
123
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
+
124
389
  }
125
390
 
126
- export default facade(new UIService());
391
+ export default facade(UIService);
127
392
 
128
- declare module '@/services/Events' {
393
+ declare module '@aerogel/core/services/Events' {
129
394
  export interface EventsPayload {
130
- 'modal-will-close': { modal: Modal; result?: unknown };
131
- 'modal-closed': { modal: Modal; result?: unknown };
132
395
  'close-modal': { id: string; result?: unknown };
133
- 'hide-modal': { id: string };
134
- 'show-modal': { id: string };
396
+ 'modal-will-close': { modal: UIModal; result?: unknown };
397
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
135
398
  }
136
399
  }
package/src/ui/index.ts CHANGED
@@ -1,41 +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 type { UIComponent } from './UI';
10
-
11
- export { UI, UIComponents, UIComponent };
12
-
13
15
  const services = { $ui: UI };
14
16
 
17
+ export * from './UI';
18
+ export * from './utils';
19
+ export { default as UI } from './UI';
20
+
15
21
  export type UIServices = typeof services;
16
22
 
17
23
  export default definePlugin({
18
24
  async install(app, options) {
19
- const defaultComponents = {
20
- [UIComponents.AlertModal]: AGAlertModal,
21
- [UIComponents.ConfirmModal]: AGConfirmModal,
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,
22
34
  };
23
35
 
24
- Object.entries({
25
- ...defaultComponents,
26
- ...options.components,
27
- }).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
+ }
28
39
 
29
40
  await bootServices(app, services);
30
41
  },
31
42
  });
32
43
 
33
- declare module '@/bootstrap/options' {
34
- interface AerogelOptions {
35
- components?: Partial<Record<UIComponent, Component>>;
44
+ declare module '@aerogel/core/bootstrap/options' {
45
+ export interface AerogelOptions {
46
+ components?: Partial<Partial<UIComponents>>;
36
47
  }
37
48
  }
38
49
 
39
- declare module '@/services' {
40
- interface Services extends UIServices {}
50
+ declare module '@aerogel/core/services' {
51
+ export interface Services extends UIServices {}
41
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
  }
@@ -0,0 +1,27 @@
1
+ import { objectWithout } from '@noeldemartin/utils';
2
+ import { computed, inject, onUnmounted, useAttrs } from 'vue';
3
+ import type { ClassValue } from 'clsx';
4
+ import type { ComputedRef } from 'vue';
5
+ import type { Nullable } from '@noeldemartin/utils';
6
+
7
+ import FormController from '@aerogel/core/forms/FormController';
8
+ import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
9
+
10
+ export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
11
+ const form = inject<FormController | null>('form', null);
12
+ const stop = form?.on('focus', (name) => input.name === name && listener());
13
+
14
+ onUnmounted(() => stop?.());
15
+ }
16
+
17
+ export function useForm<const T extends FormFieldDefinitions>(fields: T): FormController<T> & FormData<T> {
18
+ return new FormController(fields) as FormController<T> & FormData<T>;
19
+ }
20
+
21
+ export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
22
+ const attrs = useAttrs();
23
+ const classes = computed(() => attrs.class);
24
+ const inputAttrs = computed(() => objectWithout(attrs, 'class'));
25
+
26
+ return [inputAttrs, classes as ComputedRef<ClassValue>];
27
+ }