@aerogel/core 0.0.0-next.9a02fcd3bcf698211dd7a71d4c48257c96dd7832 → 0.0.0-next.9aa7c279868edbedbcee075aef52212597d803fb

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.d.ts +2080 -1616
  2. package/dist/aerogel-core.js +3265 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +31 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +25 -16
  7. package/src/bootstrap/options.ts +1 -1
  8. package/src/components/AppLayout.vue +14 -0
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/AppOverlays.vue +9 -0
  11. package/src/components/AppToasts.vue +16 -0
  12. package/src/components/contracts/AlertModal.ts +8 -0
  13. package/src/components/contracts/Button.ts +16 -0
  14. package/src/components/contracts/ConfirmModal.ts +46 -0
  15. package/src/components/contracts/DropdownMenu.ts +20 -0
  16. package/src/components/contracts/ErrorReportModal.ts +32 -0
  17. package/src/components/contracts/Input.ts +26 -0
  18. package/src/components/contracts/LoadingModal.ts +26 -0
  19. package/src/components/contracts/Modal.ts +21 -0
  20. package/src/components/contracts/PromptModal.ts +34 -0
  21. package/src/components/contracts/Select.ts +45 -0
  22. package/src/components/contracts/Toast.ts +15 -0
  23. package/src/components/contracts/index.ts +11 -0
  24. package/src/components/headless/HeadlessButton.vue +51 -0
  25. package/src/components/headless/HeadlessInput.vue +59 -0
  26. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  27. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  28. package/src/components/headless/HeadlessInputInput.vue +75 -0
  29. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  30. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  31. package/src/components/headless/HeadlessModal.vue +57 -0
  32. package/src/components/headless/HeadlessModalContent.vue +30 -0
  33. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  34. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  35. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  36. package/src/components/headless/HeadlessSelect.vue +120 -0
  37. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
  38. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  39. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  40. package/src/components/headless/HeadlessSelectOptions.vue +42 -0
  41. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  42. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  43. package/src/components/headless/HeadlessToast.vue +18 -0
  44. package/src/components/headless/HeadlessToastAction.vue +13 -0
  45. package/src/components/headless/index.ts +19 -3
  46. package/src/components/index.ts +6 -9
  47. package/src/components/ui/AdvancedOptions.vue +18 -0
  48. package/src/components/ui/AlertModal.vue +14 -0
  49. package/src/components/ui/Button.vue +98 -0
  50. package/src/components/ui/Checkbox.vue +56 -0
  51. package/src/components/ui/ConfirmModal.vue +45 -0
  52. package/src/components/ui/DropdownMenu.vue +32 -0
  53. package/src/components/ui/DropdownMenuOption.vue +14 -0
  54. package/src/components/ui/DropdownMenuOptions.vue +27 -0
  55. package/src/components/ui/EditableContent.vue +82 -0
  56. package/src/components/ui/ErrorMessage.vue +15 -0
  57. package/src/components/ui/ErrorReportModal.vue +67 -0
  58. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  59. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  60. package/src/components/ui/Form.vue +24 -0
  61. package/src/components/ui/Input.vue +56 -0
  62. package/src/components/ui/Link.vue +12 -0
  63. package/src/components/ui/LoadingModal.vue +34 -0
  64. package/src/components/ui/Markdown.vue +69 -0
  65. package/src/components/ui/Modal.vue +122 -0
  66. package/src/components/ui/ModalContext.vue +31 -0
  67. package/src/components/ui/ProgressBar.vue +51 -0
  68. package/src/components/ui/PromptModal.vue +38 -0
  69. package/src/components/ui/Select.vue +27 -0
  70. package/src/components/ui/SelectLabel.vue +17 -0
  71. package/src/components/ui/SelectOption.vue +29 -0
  72. package/src/components/ui/SelectOptions.vue +35 -0
  73. package/src/components/ui/SelectTrigger.vue +29 -0
  74. package/src/components/ui/SettingsModal.vue +15 -0
  75. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  76. package/src/components/ui/Toast.vue +44 -0
  77. package/src/components/ui/index.ts +30 -0
  78. package/src/directives/index.ts +11 -5
  79. package/src/directives/measure.ts +34 -6
  80. package/src/errors/Errors.state.ts +1 -1
  81. package/src/errors/Errors.ts +14 -23
  82. package/src/errors/JobCancelledError.ts +3 -0
  83. package/src/errors/index.ts +9 -6
  84. package/src/errors/utils.ts +17 -1
  85. package/src/forms/FormController.test.ts +110 -0
  86. package/src/forms/FormController.ts +246 -0
  87. package/src/forms/index.ts +3 -2
  88. package/src/forms/utils.ts +51 -20
  89. package/src/forms/validation.ts +19 -0
  90. package/src/index.css +72 -0
  91. package/src/{main.ts → index.ts} +1 -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 +48 -21
  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 +36 -3
  105. package/src/services/App.ts +19 -3
  106. package/src/services/Cache.ts +43 -0
  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 +16 -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 +224 -120
  117. package/src/ui/index.ts +28 -28
  118. package/src/ui/utils.ts +16 -0
  119. package/src/utils/classes.ts +49 -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 +28 -127
  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/AGForm.vue +0 -26
  146. package/src/components/forms/AGInput.vue +0 -38
  147. package/src/components/forms/AGSelect.story.vue +0 -46
  148. package/src/components/forms/AGSelect.vue +0 -60
  149. package/src/components/forms/index.ts +0 -5
  150. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  151. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  152. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  153. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -47
  154. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  155. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  156. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  157. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  158. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  159. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  160. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  161. package/src/components/headless/forms/index.ts +0 -14
  162. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  163. package/src/components/headless/modals/AGHeadlessModal.vue +0 -86
  164. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  165. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  166. package/src/components/headless/modals/index.ts +0 -4
  167. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  168. package/src/components/headless/snackbars/index.ts +0 -40
  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 -36
  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 -27
  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 -30
  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/Form.ts +0 -185
  197. package/src/forms/composition.ts +0 -6
  198. package/src/main.histoire.ts +0 -1
  199. package/src/utils/tailwindcss.test.ts +0 -26
  200. package/src/utils/tailwindcss.ts +0 -7
  201. package/tailwind.config.js +0 -4
  202. package/tsconfig.json +0 -11
  203. 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> = {};
95
+
96
+ public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
97
+ this.components[name] = component;
98
+ }
58
99
 
59
- public requireComponent(name: UIComponent): Component {
60
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
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,68 @@ export class UIService extends Service {
74
119
  };
75
120
  };
76
121
 
77
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
122
+ this.openModal(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
- );
154
+ const properties = getProperties();
155
+ const modal = await this.openModal(this.requireComponent('confirm-modal'), properties);
106
156
  const result = await modal.beforeClose;
107
157
 
108
- return result ?? false;
158
+ const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
159
+ const checkboxes =
160
+ typeof result === 'object'
161
+ ? result[1]
162
+ : Object.entries(properties.checkboxes ?? {}).reduce(
163
+ (values, [checkbox, { default: defaultValue }]) => ({
164
+ [checkbox]: defaultValue ?? false,
165
+ ...values,
166
+ }),
167
+ {} as Record<string, boolean>,
168
+ );
169
+
170
+ for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
171
+ if (!checkbox.required || checkboxes[name]) {
172
+ continue;
173
+ }
174
+
175
+ if (confirmed && isDevelopment()) {
176
+ // eslint-disable-next-line no-console
177
+ console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
178
+ }
179
+
180
+ return [false, checkboxes];
181
+ }
182
+
183
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
109
184
  }
110
185
 
111
186
  public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
@@ -115,47 +190,62 @@ export class UIService extends Service {
115
190
  messageOrOptions?: string | PromptOptions,
116
191
  options?: PromptOptions,
117
192
  ): Promise<string | null> {
118
- const getProperties = (): AGPromptModalProps => {
193
+ const trim = options?.trim ?? true;
194
+ const getProperties = (): PromptModalProps => {
119
195
  if (typeof messageOrOptions !== 'string') {
120
196
  return {
121
197
  message: messageOrTitle,
122
198
  ...(messageOrOptions ?? {}),
123
- };
199
+ } as PromptModalProps;
124
200
  }
125
201
 
126
202
  return {
127
203
  title: messageOrTitle,
128
204
  message: messageOrOptions,
129
205
  ...(options ?? {}),
130
- };
206
+ } as PromptModalProps;
131
207
  };
132
208
 
133
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
134
- this.requireComponent(UIComponents.PromptModal),
135
- getProperties(),
136
- );
137
- const result = await modal.beforeClose;
209
+ const modal = await this.openModal(this.requireComponent('prompt-modal'), getProperties());
210
+ const rawResult = await modal.beforeClose;
211
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
138
212
 
139
213
  return result ?? null;
140
214
  }
141
215
 
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 {};
216
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
217
+ public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
218
+ public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
219
+ public async loading<T>(
220
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
221
+ operation?: Promise<T> | (() => T),
222
+ ): Promise<T> {
223
+ const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
224
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
225
+ if (typeof operationOrMessageOrOptions === 'string') {
226
+ return {
227
+ props: { message: operationOrMessageOrOptions },
228
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
229
+ };
148
230
  }
149
231
 
150
- return { message: messageOrOperation };
232
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
233
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
234
+ }
235
+
236
+ return {
237
+ props: operationOrMessageOrOptions,
238
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
239
+ };
151
240
  };
152
241
 
153
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
242
+ const { operationPromise, props } = processArgs();
243
+ const modal = await this.openModal(this.requireComponent('loading-modal'), props);
154
244
 
155
245
  try {
156
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
246
+ const result = await operationPromise;
157
247
 
158
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
248
+ await after({ ms: 500 });
159
249
 
160
250
  return result;
161
251
  } finally {
@@ -163,43 +253,37 @@ export class UIService extends Service {
163
253
  }
164
254
  }
165
255
 
166
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
167
- const snackbar: Snackbar = {
256
+ public toast(message: string, options: ToastOptions = {}): void {
257
+ const { component, ...otherOptions } = options;
258
+ const toast: UIToast = {
168
259
  id: uuid(),
169
- properties: { message, ...options },
170
- component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
260
+ properties: { message, ...otherOptions },
261
+ component: markRaw(component ?? this.requireComponent('toast')),
171
262
  };
172
263
 
173
- this.setState('snackbars', this.snackbars.concat(snackbar));
174
-
175
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
176
- }
177
-
178
- public hideSnackbar(id: string): void {
179
- this.setState(
180
- 'snackbars',
181
- this.snackbars.filter((snackbar) => snackbar.id !== id),
182
- );
264
+ this.setState('toasts', this.toasts.concat(toast));
183
265
  }
184
266
 
185
- public registerComponent(name: UIComponent, component: Component): void {
186
- this.components[name] = component;
187
- }
267
+ public openModal<T extends Component>(
268
+ ...args: {} extends ComponentProps<T>
269
+ ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
270
+ : [component: T, props: AcceptRefs<ComponentProps<T>>]
271
+ ): Promise<UIModal<ModalResult<T>>>;
188
272
 
189
- public async openModal<TModalComponent extends ModalComponent>(
190
- component: TModalComponent,
191
- properties?: ModalProperties<TModalComponent>,
192
- ): Promise<Modal<ModalResult<TModalComponent>>> {
273
+ public async openModal<T extends Component>(
274
+ component: T,
275
+ props?: ComponentProps<T>,
276
+ ): Promise<UIModal<ModalResult<T>>> {
193
277
  const id = uuid();
194
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
195
- const modal: Modal<ModalResult<TModalComponent>> = {
278
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
279
+ const modal: UIModal<ModalResult<T>> = {
196
280
  id,
197
- properties: properties ?? {},
281
+ closing: false,
282
+ properties: props ?? {},
198
283
  component: markRaw(component),
199
284
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
200
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
285
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
201
286
  };
202
- const activeModal = this.modals.at(-1);
203
287
  const modals = this.modals.concat(modal);
204
288
 
205
289
  this.modalCallbacks[modal.id] = callbacks;
@@ -207,58 +291,72 @@ export class UIService extends Service {
207
291
  this.setState({ modals });
208
292
 
209
293
  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
294
 
216
295
  return modal;
217
296
  }
218
297
 
219
298
  public async closeModal(id: string, result?: unknown): Promise<void> {
299
+ if (!App.isMounted()) {
300
+ await this.removeModal(id, result);
301
+
302
+ return;
303
+ }
304
+
220
305
  await Events.emit('close-modal', { id, result });
221
306
  }
222
307
 
223
- protected async boot(): Promise<void> {
308
+ public async closeAllModals(): Promise<void> {
309
+ while (this.modals.length > 0) {
310
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
311
+ }
312
+ }
313
+
314
+ protected override async boot(): Promise<void> {
224
315
  this.watchModalEvents();
225
316
  this.watchMountedEvent();
317
+ this.watchViewportBreakpoints();
226
318
  }
227
319
 
228
- private watchModalEvents(): void {
229
- Events.on('modal-will-close', ({ modal, result }) => {
230
- this.modalCallbacks[modal.id]?.willClose?.(result);
320
+ private async removeModal(id: string, result?: unknown): Promise<void> {
321
+ this.setState(
322
+ 'modals',
323
+ this.modals.filter((m) => m.id !== id),
324
+ );
231
325
 
232
- if (this.modals.length === 1) {
233
- Events.emit('hide-overlays-backdrop');
234
- }
235
- });
326
+ this.modalCallbacks[id]?.hasClosed?.(result);
236
327
 
237
- Events.on('modal-closed', async ({ modal, result }) => {
238
- this.setState(
239
- 'modals',
240
- this.modals.filter((m) => m.id !== modal.id),
241
- );
328
+ delete this.modalCallbacks[id];
329
+ }
242
330
 
243
- this.modalCallbacks[modal.id]?.closed?.(result);
331
+ private watchModalEvents(): void {
332
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
333
+ const modal = this.modals.find((_modal) => id === _modal.id);
244
334
 
245
- delete this.modalCallbacks[modal.id];
335
+ if (modal) {
336
+ modal.closing = true;
337
+ }
246
338
 
247
- const activeModal = this.modals.at(-1);
339
+ this.modalCallbacks[id]?.willClose?.(result);
340
+ });
248
341
 
249
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
342
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
343
+ await this.removeModal(id, result);
250
344
  });
251
345
  }
252
346
 
253
347
  private watchMountedEvent(): void {
254
348
  Events.once('application-mounted', async () => {
255
- const splash = document.getElementById('splash');
349
+ if (!globalThis.document || !globalThis.getComputedStyle) {
350
+ return;
351
+ }
352
+
353
+ const splash = globalThis.document.getElementById('splash');
256
354
 
257
355
  if (!splash) {
258
356
  return;
259
357
  }
260
358
 
261
- if (window.getComputedStyle(splash).opacity !== '0') {
359
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
262
360
  splash.style.opacity = '0';
263
361
 
264
362
  await after({ ms: 600 });
@@ -268,18 +366,24 @@ export class UIService extends Service {
268
366
  });
269
367
  }
270
368
 
369
+ private watchViewportBreakpoints(): void {
370
+ if (!globalThis.matchMedia) {
371
+ return;
372
+ }
373
+
374
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
375
+
376
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
377
+ }
378
+
271
379
  }
272
380
 
273
381
  export default facade(UIService);
274
382
 
275
- declare module '@/services/Events' {
383
+ declare module '@aerogel/core/services/Events' {
276
384
  export interface EventsPayload {
277
385
  'close-modal': { id: string; result?: unknown };
278
- 'hide-modal': { id: string };
279
- 'hide-overlays-backdrop': void;
280
- 'modal-closed': { modal: Modal; result?: unknown };
281
- 'modal-will-close': { modal: Modal; result?: unknown };
282
- 'show-modal': { id: string };
283
- 'show-overlays-backdrop': void;
386
+ 'modal-will-close': { modal: UIModal; result?: unknown };
387
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
284
388
  }
285
389
  }
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
+ }