@aerogel/core 0.0.0-next.824cf5311c4335d119158f507dad094ed4f4f0b6 → 0.0.0-next.8ae083000611b11799d37033e9a5250d0d07c324

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