@aerogel/core 0.0.0-next.824cf5311c4335d119158f507dad094ed4f4f0b6 → 0.0.0-next.8323c60b905020dcb3bd9d4b0bc8d9b6529e1082

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 (213) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +2311 -1612
  3. package/dist/aerogel-core.js +3722 -0
  4. package/dist/aerogel-core.js.map +1 -0
  5. package/package.json +32 -37
  6. package/src/bootstrap/bootstrap.test.ts +4 -8
  7. package/src/bootstrap/index.ts +23 -13
  8. package/src/bootstrap/options.ts +1 -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/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  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/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
  39. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  40. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  41. package/src/components/headless/HeadlessSelectOptions.vue +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} +34 -27
  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/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  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 +11 -5
  86. package/src/directives/measure.ts +41 -7
  87. package/src/errors/Errors.state.ts +1 -1
  88. package/src/errors/Errors.ts +49 -41
  89. package/src/errors/JobCancelledError.ts +3 -0
  90. package/src/errors/index.ts +16 -8
  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 +113 -0
  95. package/src/forms/FormController.ts +255 -0
  96. package/src/forms/index.ts +3 -2
  97. package/src/forms/utils.ts +76 -20
  98. package/src/forms/validation.ts +50 -0
  99. package/src/index.css +76 -0
  100. package/src/{main.ts → index.ts} +2 -0
  101. package/src/jobs/Job.ts +144 -2
  102. package/src/jobs/index.ts +4 -1
  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 +43 -28
  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 +36 -3
  114. package/src/services/App.ts +19 -3
  115. package/src/services/Cache.ts +43 -0
  116. package/src/services/Events.test.ts +8 -8
  117. package/src/services/Events.ts +26 -10
  118. package/src/services/Service.ts +135 -59
  119. package/src/services/Storage.ts +20 -0
  120. package/src/services/index.ts +18 -7
  121. package/src/services/utils.ts +18 -0
  122. package/src/testing/index.ts +30 -0
  123. package/src/testing/setup.ts +11 -0
  124. package/src/ui/UI.state.ts +14 -12
  125. package/src/ui/UI.ts +252 -120
  126. package/src/ui/index.ts +28 -28
  127. package/src/ui/utils.ts +16 -0
  128. package/src/utils/app.ts +7 -0
  129. package/src/utils/classes.ts +41 -0
  130. package/src/utils/composition/events.ts +4 -6
  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 +6 -1
  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 -132
  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/histoire.config.ts +0 -7
  146. package/noeldemartin.config.js +0 -5
  147. package/postcss.config.js +0 -6
  148. package/src/assets/histoire.css +0 -3
  149. package/src/components/AGAppLayout.vue +0 -16
  150. package/src/components/AGAppOverlays.vue +0 -41
  151. package/src/components/AGAppSnackbars.vue +0 -13
  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 -41
  155. package/src/components/forms/AGForm.vue +0 -26
  156. package/src/components/forms/AGInput.vue +0 -38
  157. package/src/components/forms/AGSelect.story.vue +0 -46
  158. package/src/components/forms/AGSelect.vue +0 -60
  159. package/src/components/forms/index.ts +0 -5
  160. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  161. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  162. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  163. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  164. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  165. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  166. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  167. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  168. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  169. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  170. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  171. package/src/components/headless/forms/index.ts +0 -14
  172. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  173. package/src/components/headless/modals/AGHeadlessModal.vue +0 -86
  174. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  175. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  176. package/src/components/headless/modals/index.ts +0 -4
  177. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  178. package/src/components/headless/snackbars/index.ts +0 -40
  179. package/src/components/lib/AGErrorMessage.vue +0 -16
  180. package/src/components/lib/AGLink.vue +0 -9
  181. package/src/components/lib/AGMarkdown.vue +0 -36
  182. package/src/components/lib/AGMeasured.vue +0 -15
  183. package/src/components/lib/index.ts +0 -5
  184. package/src/components/modals/AGAlertModal.ts +0 -15
  185. package/src/components/modals/AGAlertModal.vue +0 -14
  186. package/src/components/modals/AGConfirmModal.ts +0 -27
  187. package/src/components/modals/AGConfirmModal.vue +0 -26
  188. package/src/components/modals/AGErrorReportModal.ts +0 -46
  189. package/src/components/modals/AGErrorReportModal.vue +0 -54
  190. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  191. package/src/components/modals/AGLoadingModal.ts +0 -23
  192. package/src/components/modals/AGLoadingModal.vue +0 -15
  193. package/src/components/modals/AGModal.ts +0 -10
  194. package/src/components/modals/AGModal.vue +0 -39
  195. package/src/components/modals/AGModalContext.ts +0 -8
  196. package/src/components/modals/AGModalContext.vue +0 -22
  197. package/src/components/modals/AGModalTitle.vue +0 -9
  198. package/src/components/modals/AGPromptModal.ts +0 -30
  199. package/src/components/modals/AGPromptModal.vue +0 -34
  200. package/src/components/modals/index.ts +0 -17
  201. package/src/components/snackbars/AGSnackbar.vue +0 -36
  202. package/src/components/snackbars/index.ts +0 -3
  203. package/src/components/utils.ts +0 -10
  204. package/src/directives/initial-focus.ts +0 -11
  205. package/src/forms/Form.test.ts +0 -58
  206. package/src/forms/Form.ts +0 -185
  207. package/src/forms/composition.ts +0 -6
  208. package/src/main.histoire.ts +0 -1
  209. package/src/utils/tailwindcss.test.ts +0 -26
  210. package/src/utils/tailwindcss.ts +0 -7
  211. package/tailwind.config.js +0 -4
  212. package/tsconfig.json +0 -11
  213. package/vite.config.ts +0 -14
package/src/ui/UI.ts CHANGED
@@ -1,69 +1,115 @@
1
- import { after, 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';
8
- import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
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';
9
24
 
10
25
  import Service from './UI.state';
11
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
26
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
27
+ import type { UIModal, UIToast } from './UI.state';
12
28
 
13
29
  interface ModalCallbacks<T = unknown> {
14
30
  willClose(result: T | undefined): void;
15
- closed(result: T | undefined): void;
31
+ hasClosed(result: T | undefined): void;
16
32
  }
17
33
 
18
- type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
19
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
20
- ? TResult
21
- : never;
22
-
23
- export const UIComponents = {
24
- AlertModal: 'alert-modal',
25
- ConfirmModal: 'confirm-modal',
26
- ErrorReportModal: 'error-report-modal',
27
- LoadingModal: 'loading-modal',
28
- PromptModal: 'prompt-modal',
29
- Snackbar: 'snackbar',
30
- StartupCrash: 'startup-crash',
31
- } as const;
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
+ }
32
48
 
33
- export type UIComponent = ObjectValues<typeof UIComponents>;
49
+ export interface UIModalContext {
50
+ modal: UIModal;
51
+ childIndex?: number;
52
+ }
34
53
 
35
- export interface ConfirmOptions {
54
+ export type ConfirmOptions = AcceptRefs<{
36
55
  acceptText?: string;
56
+ acceptVariant?: ButtonVariant;
37
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;
38
73
  }
39
74
 
40
- export interface PromptOptions {
75
+ export type PromptOptions = AcceptRefs<{
41
76
  label?: string;
42
77
  defaultValue?: string;
43
78
  placeholder?: string;
44
79
  acceptText?: string;
80
+ acceptVariant?: ButtonVariant;
45
81
  cancelText?: string;
46
- }
82
+ cancelVariant?: ButtonVariant;
83
+ trim?: boolean;
84
+ }>;
47
85
 
48
- export interface ShowSnackbarOptions {
86
+ export interface ToastOptions {
49
87
  component?: Component;
50
- color?: SnackbarColor;
51
- actions?: SnackbarAction[];
88
+ variant?: ToastVariant;
89
+ actions?: ToastAction[];
52
90
  }
53
91
 
54
92
  export class UIService extends Service {
55
93
 
56
94
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
57
- 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
+ }
58
104
 
59
- public requireComponent(name: UIComponent): Component {
60
- 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!`);
61
107
  }
62
108
 
63
109
  public alert(message: string): void;
64
110
  public alert(title: string, message: string): void;
65
111
  public alert(messageOrTitle: string, message?: string): void {
66
- const getProperties = (): AGAlertModalProps => {
112
+ const getProperties = (): AlertModalProps => {
67
113
  if (typeof message !== 'string') {
68
114
  return { message: messageOrTitle };
69
115
  }
@@ -74,38 +120,66 @@ export class UIService extends Service {
74
120
  };
75
121
  };
76
122
 
77
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
123
+ this.modal(this.requireComponent('alert-modal'), getProperties());
78
124
  }
79
125
 
126
+ /* eslint-disable max-len */
80
127
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
81
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
+
82
133
  public async confirm(
83
134
  messageOrTitle: string,
84
- messageOrOptions?: string | ConfirmOptions,
85
- options?: ConfirmOptions,
86
- ): Promise<boolean> {
87
- const getProperties = (): AGConfirmModalProps => {
135
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
136
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
137
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
138
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
88
139
  if (typeof messageOrOptions !== 'string') {
89
140
  return {
90
- message: messageOrTitle,
91
141
  ...(messageOrOptions ?? {}),
142
+ message: messageOrTitle,
143
+ required: !!messageOrOptions?.required,
92
144
  };
93
145
  }
94
146
 
95
147
  return {
148
+ ...(options ?? {}),
96
149
  title: messageOrTitle,
97
150
  message: messageOrOptions,
98
- ...(options ?? {}),
151
+ required: !!options?.required,
99
152
  };
100
153
  };
101
154
 
102
- const modal = await this.openModal<ModalComponent<AGConfirmModalProps, boolean>>(
103
- this.requireComponent(UIComponents.ConfirmModal),
104
- getProperties(),
105
- );
106
- const result = await modal.beforeClose;
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
+ }
107
173
 
108
- return result ?? false;
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
+ }
178
+
179
+ return [false, checkboxes];
180
+ }
181
+
182
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
109
183
  }
110
184
 
111
185
  public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
@@ -115,47 +189,76 @@ export class UIService extends Service {
115
189
  messageOrOptions?: string | PromptOptions,
116
190
  options?: PromptOptions,
117
191
  ): Promise<string | null> {
118
- const getProperties = (): AGPromptModalProps => {
192
+ const trim = options?.trim ?? true;
193
+ const getProperties = (): PromptModalProps => {
119
194
  if (typeof messageOrOptions !== 'string') {
120
195
  return {
121
196
  message: messageOrTitle,
122
197
  ...(messageOrOptions ?? {}),
123
- };
198
+ } as PromptModalProps;
124
199
  }
125
200
 
126
201
  return {
127
202
  title: messageOrTitle,
128
203
  message: messageOrOptions,
129
204
  ...(options ?? {}),
130
- };
205
+ } as PromptModalProps;
131
206
  };
132
207
 
133
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
134
- this.requireComponent(UIComponents.PromptModal),
135
- getProperties(),
136
- );
137
- const result = await modal.beforeClose;
208
+ const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
209
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
138
210
 
139
211
  return result ?? null;
140
212
  }
141
213
 
142
- public async loading<T>(operation: Promise<T>): Promise<T>;
143
- public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
144
- public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
145
- const getProperties = (): AGLoadingModalProps => {
146
- if (typeof messageOrOperation !== 'string') {
147
- return {};
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) };
148
236
  }
149
237
 
150
- return { message: messageOrOperation };
238
+ const { delay, ...props } = operationOrMessageOrOptions;
239
+
240
+ return {
241
+ props,
242
+ delay: unref(delay),
243
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
244
+ };
151
245
  };
152
246
 
153
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
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);
154
257
 
155
258
  try {
156
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
259
+ const result = await operationPromise;
157
260
 
158
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
261
+ await after({ ms: 500 });
159
262
 
160
263
  return result;
161
264
  } finally {
@@ -163,43 +266,34 @@ export class UIService extends Service {
163
266
  }
164
267
  }
165
268
 
166
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
167
- const snackbar: Snackbar = {
269
+ public toast(message: string, options: ToastOptions = {}): void {
270
+ const { component, ...otherOptions } = options;
271
+ const toast: UIToast = {
168
272
  id: uuid(),
169
- properties: { message, ...options },
170
- component: options.component ?? markRaw(this.requireComponent(UIComponents.Snackbar)),
273
+ properties: { message, ...otherOptions },
274
+ component: markRaw(component ?? this.requireComponent('toast')),
171
275
  };
172
276
 
173
- this.setState('snackbars', this.snackbars.concat(snackbar));
174
-
175
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
277
+ this.setState('toasts', this.toasts.concat(toast));
176
278
  }
177
279
 
178
- public hideSnackbar(id: string): void {
179
- this.setState(
180
- 'snackbars',
181
- this.snackbars.filter((snackbar) => snackbar.id !== id),
182
- );
183
- }
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>>>;
184
285
 
185
- public registerComponent(name: UIComponent, component: Component): void {
186
- this.components[name] = component;
187
- }
188
-
189
- public async openModal<TModalComponent extends ModalComponent>(
190
- component: TModalComponent,
191
- properties?: ModalProperties<TModalComponent>,
192
- ): Promise<Modal<ModalResult<TModalComponent>>> {
286
+ public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
193
287
  const id = uuid();
194
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
195
- const modal: Modal<ModalResult<TModalComponent>> = {
288
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
289
+ const modal: UIModal<ModalResult<T>> = {
196
290
  id,
197
- properties: properties ?? {},
291
+ closing: false,
292
+ properties: props ?? {},
198
293
  component: markRaw(component),
199
294
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
200
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
295
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
201
296
  };
202
- const activeModal = this.modals.at(-1);
203
297
  const modals = this.modals.concat(modal);
204
298
 
205
299
  this.modalCallbacks[modal.id] = callbacks;
@@ -207,58 +301,88 @@ export class UIService extends Service {
207
301
  this.setState({ modals });
208
302
 
209
303
  await nextTick();
210
- await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
211
- await Promise.all([
212
- activeModal || Events.emit('show-overlays-backdrop'),
213
- Events.emit('show-modal', { id: modal.id }),
214
- ]);
215
304
 
216
305
  return modal;
217
306
  }
218
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
+
219
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
+
220
331
  await Events.emit('close-modal', { id, result });
221
332
  }
222
333
 
223
- 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> {
224
341
  this.watchModalEvents();
225
342
  this.watchMountedEvent();
343
+ this.watchViewportBreakpoints();
226
344
  }
227
345
 
228
- private watchModalEvents(): void {
229
- Events.on('modal-will-close', ({ modal, result }) => {
230
- this.modalCallbacks[modal.id]?.willClose?.(result);
346
+ private async removeModal(id: string, result?: unknown): Promise<void> {
347
+ this.setState(
348
+ 'modals',
349
+ this.modals.filter((m) => m.id !== id),
350
+ );
231
351
 
232
- if (this.modals.length === 1) {
233
- Events.emit('hide-overlays-backdrop');
234
- }
235
- });
352
+ this.modalCallbacks[id]?.hasClosed?.(result);
236
353
 
237
- Events.on('modal-closed', async ({ modal, result }) => {
238
- this.setState(
239
- 'modals',
240
- this.modals.filter((m) => m.id !== modal.id),
241
- );
354
+ delete this.modalCallbacks[id];
355
+ }
242
356
 
243
- this.modalCallbacks[modal.id]?.closed?.(result);
357
+ private watchModalEvents(): void {
358
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
359
+ const modal = this.modals.find((_modal) => id === _modal.id);
244
360
 
245
- delete this.modalCallbacks[modal.id];
361
+ if (modal) {
362
+ modal.closing = true;
363
+ }
246
364
 
247
- const activeModal = this.modals.at(-1);
365
+ this.modalCallbacks[id]?.willClose?.(result);
366
+ });
248
367
 
249
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
368
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
369
+ await this.removeModal(id, result);
250
370
  });
251
371
  }
252
372
 
253
373
  private watchMountedEvent(): void {
254
374
  Events.once('application-mounted', async () => {
255
- const splash = document.getElementById('splash');
375
+ if (!globalThis.document || !globalThis.getComputedStyle) {
376
+ return;
377
+ }
378
+
379
+ const splash = globalThis.document.getElementById('splash');
256
380
 
257
381
  if (!splash) {
258
382
  return;
259
383
  }
260
384
 
261
- if (window.getComputedStyle(splash).opacity !== '0') {
385
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
262
386
  splash.style.opacity = '0';
263
387
 
264
388
  await after({ ms: 600 });
@@ -268,16 +392,24 @@ export class UIService extends Service {
268
392
  });
269
393
  }
270
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
+
271
405
  }
272
406
 
273
407
  export default facade(UIService);
274
408
 
275
- declare module '@/services/Events' {
409
+ declare module '@aerogel/core/services/Events' {
276
410
  export interface EventsPayload {
277
- 'modal-will-close': { modal: Modal; result?: unknown };
278
- 'modal-closed': { modal: Modal; result?: unknown };
279
411
  'close-modal': { id: string; result?: unknown };
280
- 'hide-modal': { id: string };
281
- 'show-modal': { id: string };
412
+ 'modal-will-close': { modal: UIModal; result?: unknown };
413
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
282
414
  }
283
415
  }
package/src/ui/index.ts CHANGED
@@ -1,52 +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 AGPromptModal from '../components/modals/AGPromptModal.vue';
12
- import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
13
- import AGStartupCrash from '../components/lib/AGStartupCrash.vue';
14
- import type { UIComponent } from './UI';
15
-
16
15
  const services = { $ui: UI };
17
16
 
18
17
  export * from './UI';
18
+ export * from './utils';
19
19
  export { default as UI } from './UI';
20
20
 
21
21
  export type UIServices = typeof services;
22
22
 
23
23
  export default definePlugin({
24
24
  async install(app, options) {
25
- const defaultComponents = {
26
- [UIComponents.AlertModal]: AGAlertModal,
27
- [UIComponents.ConfirmModal]: AGConfirmModal,
28
- [UIComponents.ErrorReportModal]: AGErrorReportModal,
29
- [UIComponents.LoadingModal]: AGLoadingModal,
30
- [UIComponents.PromptModal]: AGPromptModal,
31
- [UIComponents.Snackbar]: AGSnackbar,
32
- [UIComponents.StartupCrash]: AGStartupCrash,
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,
33
34
  };
34
35
 
35
- Object.entries({
36
- ...defaultComponents,
37
- ...options.components,
38
- }).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
+ }
39
39
 
40
40
  await bootServices(app, services);
41
41
  },
42
42
  });
43
43
 
44
- declare module '@/bootstrap/options' {
44
+ declare module '@aerogel/core/bootstrap/options' {
45
45
  export interface AerogelOptions {
46
- components?: Partial<Record<UIComponent, Component>>;
46
+ components?: Partial<Partial<UIComponents>>;
47
47
  }
48
48
  }
49
49
 
50
- declare module '@/services' {
50
+ declare module '@aerogel/core/services' {
51
51
  export interface Services extends UIServices {}
52
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,7 @@
1
+ import Aerogel from 'virtual:aerogel';
2
+
3
+ import { stringToSlug } from '@noeldemartin/utils';
4
+
5
+ export function appNamespace(): string {
6
+ return Aerogel.namespace ?? stringToSlug(Aerogel.name);
7
+ }