@aerogel/core 0.0.0-next.d34923f3b144e8f6720e6a9cdadb2cd4fb4ab289 → 0.0.0-next.d4f3ae130b52cace673d7c95681c19256ee1acb4

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