@aerogel/core 0.0.0-next.fd1bd21aea7a9ab8c4eab69a5f5776db5de8bf35 → 0.1.0

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