@aerogel/core 0.0.0-next.f9394854509d71d644498ac087706a2f8f8eea1c → 0.0.0-next.fb0f08b1df2e4aff5b34e23b9927b06b58484c98

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