@aerogel/core 0.0.0-next.f1f5a990033d966dc0bb12d251110fbc9350dcc7 → 0.0.0-next.f7d6218c6788b7d93b1f58d848fc11cd0a351798

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