@aerogel/core 0.0.0-next.c4825c5cbe0fe3257e478c2a7ec8df27d5a72305 → 0.0.0-next.ce4783d09a83f492e439f8d4c39bc0b4998f4cbf

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 (201) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +1943 -1948
  3. package/dist/aerogel-core.js +3557 -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 +14 -15
  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 +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/{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/{modals/AGModalContext.vue → ui/ModalContext.vue} +8 -9
  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/index.ts +15 -8
  88. package/src/errors/settings/Debug.vue +39 -0
  89. package/src/errors/settings/index.ts +10 -0
  90. package/src/errors/utils.ts +1 -1
  91. package/src/forms/{Form.test.ts → FormController.test.ts} +32 -8
  92. package/src/forms/{Form.ts → FormController.ts} +42 -38
  93. package/src/forms/index.ts +2 -3
  94. package/src/forms/utils.ts +35 -35
  95. package/src/index.css +73 -0
  96. package/src/jobs/Job.ts +2 -2
  97. package/src/jobs/listeners.ts +1 -1
  98. package/src/lang/DefaultLangProvider.ts +7 -4
  99. package/src/lang/Lang.state.ts +1 -1
  100. package/src/lang/Lang.ts +1 -1
  101. package/src/lang/index.ts +12 -6
  102. package/src/lang/settings/Language.vue +48 -0
  103. package/src/lang/settings/index.ts +10 -0
  104. package/src/plugins/Plugin.ts +1 -1
  105. package/src/plugins/index.ts +10 -7
  106. package/src/services/App.state.ts +15 -4
  107. package/src/services/App.ts +12 -4
  108. package/src/services/Cache.ts +1 -1
  109. package/src/services/Events.test.ts +8 -8
  110. package/src/services/Events.ts +4 -10
  111. package/src/services/Service.ts +21 -21
  112. package/src/services/Storage.ts +3 -3
  113. package/src/services/index.ts +10 -6
  114. package/src/services/utils.ts +2 -2
  115. package/src/testing/index.ts +4 -3
  116. package/src/testing/setup.ts +3 -19
  117. package/src/ui/UI.state.ts +8 -13
  118. package/src/ui/UI.ts +124 -111
  119. package/src/ui/index.ts +27 -28
  120. package/src/utils/classes.ts +41 -0
  121. package/src/utils/composition/events.ts +4 -6
  122. package/src/utils/composition/forms.ts +20 -4
  123. package/src/utils/composition/state.ts +11 -2
  124. package/src/utils/index.ts +4 -1
  125. package/src/utils/markdown.ts +37 -5
  126. package/src/utils/types.ts +3 -0
  127. package/src/utils/vue.ts +31 -137
  128. package/dist/aerogel-core.cjs.js +0 -2
  129. package/dist/aerogel-core.cjs.js.map +0 -1
  130. package/dist/aerogel-core.esm.js +0 -2
  131. package/dist/aerogel-core.esm.js.map +0 -1
  132. package/histoire.config.ts +0 -7
  133. package/noeldemartin.config.js +0 -5
  134. package/postcss.config.js +0 -6
  135. package/src/assets/histoire.css +0 -3
  136. package/src/components/AGAppLayout.vue +0 -16
  137. package/src/components/AGAppOverlays.vue +0 -41
  138. package/src/components/AGAppSnackbars.vue +0 -13
  139. package/src/components/composition.ts +0 -23
  140. package/src/components/constants.ts +0 -8
  141. package/src/components/forms/AGButton.vue +0 -44
  142. package/src/components/forms/AGCheckbox.vue +0 -41
  143. package/src/components/forms/AGInput.vue +0 -40
  144. package/src/components/forms/AGSelect.story.vue +0 -46
  145. package/src/components/forms/AGSelect.vue +0 -60
  146. package/src/components/forms/index.ts +0 -5
  147. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  148. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  149. package/src/components/headless/forms/AGHeadlessInput.ts +0 -34
  150. package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
  151. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  152. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  153. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  154. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  155. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  156. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  157. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  158. package/src/components/headless/forms/composition.ts +0 -10
  159. package/src/components/headless/forms/index.ts +0 -18
  160. package/src/components/headless/modals/AGHeadlessModal.ts +0 -36
  161. package/src/components/headless/modals/AGHeadlessModal.vue +0 -92
  162. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -32
  163. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -23
  164. package/src/components/headless/modals/index.ts +0 -4
  165. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  166. package/src/components/headless/snackbars/index.ts +0 -40
  167. package/src/components/interfaces.ts +0 -24
  168. package/src/components/lib/AGErrorMessage.vue +0 -16
  169. package/src/components/lib/AGLink.vue +0 -9
  170. package/src/components/lib/AGMarkdown.vue +0 -54
  171. package/src/components/lib/AGMeasured.vue +0 -16
  172. package/src/components/lib/AGProgressBar.vue +0 -30
  173. package/src/components/lib/index.ts +0 -6
  174. package/src/components/modals/AGAlertModal.ts +0 -18
  175. package/src/components/modals/AGAlertModal.vue +0 -14
  176. package/src/components/modals/AGConfirmModal.ts +0 -42
  177. package/src/components/modals/AGConfirmModal.vue +0 -26
  178. package/src/components/modals/AGErrorReportModal.ts +0 -49
  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 -29
  182. package/src/components/modals/AGLoadingModal.vue +0 -15
  183. package/src/components/modals/AGModal.ts +0 -11
  184. package/src/components/modals/AGModal.vue +0 -39
  185. package/src/components/modals/AGModalContext.ts +0 -8
  186. package/src/components/modals/AGModalTitle.vue +0 -9
  187. package/src/components/modals/AGPromptModal.ts +0 -41
  188. package/src/components/modals/AGPromptModal.vue +0 -34
  189. package/src/components/modals/index.ts +0 -17
  190. package/src/components/snackbars/AGSnackbar.vue +0 -36
  191. package/src/components/snackbars/index.ts +0 -3
  192. package/src/components/utils.ts +0 -10
  193. package/src/directives/initial-focus.ts +0 -11
  194. package/src/forms/composition.ts +0 -6
  195. package/src/main.histoire.ts +0 -1
  196. package/src/utils/tailwindcss.test.ts +0 -26
  197. package/src/utils/tailwindcss.ts +0 -7
  198. package/tailwind.config.js +0 -4
  199. package/tsconfig.json +0 -11
  200. package/vite.config.ts +0 -17
  201. /package/src/{main.ts → index.ts} +0 -0
package/src/ui/UI.ts CHANGED
@@ -1,48 +1,61 @@
1
- import { after, facade, fail, required, 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 App from '@/services/App';
7
- import Events from '@/services/Events';
8
- import type { AcceptRefs } from '@/utils';
9
- import type { Color } from '@/components/constants';
10
- import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
11
- 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';
12
24
 
13
25
  import Service from './UI.state';
14
26
  import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
15
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
27
+ import type { UIModal, UIToast } from './UI.state';
16
28
 
17
29
  interface ModalCallbacks<T = unknown> {
18
30
  willClose(result: T | undefined): void;
19
- closed(result: T | undefined): void;
31
+ hasClosed(result: T | undefined): void;
20
32
  }
21
33
 
22
- type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
23
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
24
- ? TResult
25
- : never;
26
-
27
- export const UIComponents = {
28
- AlertModal: 'alert-modal',
29
- ConfirmModal: 'confirm-modal',
30
- ErrorReportModal: 'error-report-modal',
31
- LoadingModal: 'loading-modal',
32
- PromptModal: 'prompt-modal',
33
- Snackbar: 'snackbar',
34
- StartupCrash: 'startup-crash',
35
- } as const;
36
-
37
- export type UIComponent = ObjectValues<typeof UIComponents>;
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
+ }
38
48
 
39
- export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
49
+ export interface UIModalContext {
50
+ modal: UIModal;
51
+ childIndex?: number;
52
+ }
40
53
 
41
54
  export type ConfirmOptions = AcceptRefs<{
42
55
  acceptText?: string;
43
- acceptColor?: Color;
56
+ acceptVariant?: ButtonVariant;
44
57
  cancelText?: string;
45
- cancelColor?: Color;
58
+ cancelVariant?: ButtonVariant;
46
59
  actions?: Record<string, () => unknown>;
47
60
  required?: boolean;
48
61
  }>;
@@ -53,7 +66,8 @@ export type LoadingOptions = AcceptRefs<{
53
66
  progress?: number;
54
67
  }>;
55
68
 
56
- export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
69
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
70
+ extends ConfirmOptions {
57
71
  checkboxes?: T;
58
72
  }
59
73
 
@@ -62,31 +76,39 @@ export type PromptOptions = AcceptRefs<{
62
76
  defaultValue?: string;
63
77
  placeholder?: string;
64
78
  acceptText?: string;
65
- acceptColor?: Color;
79
+ acceptVariant?: ButtonVariant;
66
80
  cancelText?: string;
67
- cancelColor?: Color;
81
+ cancelVariant?: ButtonVariant;
68
82
  trim?: boolean;
69
83
  }>;
70
84
 
71
- export interface ShowSnackbarOptions {
85
+ export interface ToastOptions {
72
86
  component?: Component;
73
- color?: SnackbarColor;
74
- actions?: SnackbarAction[];
87
+ variant?: ToastVariant;
88
+ actions?: ToastAction[];
75
89
  }
76
90
 
77
91
  export class UIService extends Service {
78
92
 
79
93
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
80
- 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
+ }
81
99
 
82
- public requireComponent(name: UIComponent): Component {
83
- 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!`);
84
106
  }
85
107
 
86
108
  public alert(message: string): void;
87
109
  public alert(title: string, message: string): void;
88
110
  public alert(messageOrTitle: string, message?: string): void {
89
- const getProperties = (): AGAlertModalProps => {
111
+ const getProperties = (): AlertModalProps => {
90
112
  if (typeof message !== 'string') {
91
113
  return { message: messageOrTitle };
92
114
  }
@@ -97,14 +119,14 @@ export class UIService extends Service {
97
119
  };
98
120
  };
99
121
 
100
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
122
+ this.modal(this.requireComponent('alert-modal'), getProperties());
101
123
  }
102
124
 
103
125
  /* eslint-disable max-len */
104
126
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
105
127
  public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
106
- public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
107
- public async confirm<T extends ConfirmCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
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
108
130
  /* eslint-enable max-len */
109
131
 
110
132
  public async confirm(
@@ -112,7 +134,7 @@ export class UIService extends Service {
112
134
  messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
113
135
  options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
114
136
  ): Promise<boolean | [boolean, Record<string, boolean>]> {
115
- const getProperties = (): AGConfirmModalProps => {
137
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
116
138
  if (typeof messageOrOptions !== 'string') {
117
139
  return {
118
140
  ...(messageOrOptions ?? {}),
@@ -128,13 +150,10 @@ export class UIService extends Service {
128
150
  required: !!options?.required,
129
151
  };
130
152
  };
131
- const properties = getProperties();
132
- const modal = await this.openModal<
133
- ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
134
- >(this.requireComponent(UIComponents.ConfirmModal), properties);
135
- const result = await modal.beforeClose;
136
153
 
137
- const confirmed = typeof result === 'object' ? result[0] : result ?? false;
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);
138
157
  const checkboxes =
139
158
  typeof result === 'object'
140
159
  ? result[1]
@@ -151,7 +170,7 @@ export class UIService extends Service {
151
170
  continue;
152
171
  }
153
172
 
154
- if (confirmed && App.development) {
173
+ if (confirmed && isDevelopment()) {
155
174
  // eslint-disable-next-line no-console
156
175
  console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
157
176
  }
@@ -170,26 +189,22 @@ export class UIService extends Service {
170
189
  options?: PromptOptions,
171
190
  ): Promise<string | null> {
172
191
  const trim = options?.trim ?? true;
173
- const getProperties = (): AGPromptModalProps => {
192
+ const getProperties = (): PromptModalProps => {
174
193
  if (typeof messageOrOptions !== 'string') {
175
194
  return {
176
195
  message: messageOrTitle,
177
196
  ...(messageOrOptions ?? {}),
178
- } as AGPromptModalProps;
197
+ } as PromptModalProps;
179
198
  }
180
199
 
181
200
  return {
182
201
  title: messageOrTitle,
183
202
  message: messageOrOptions,
184
203
  ...(options ?? {}),
185
- } as AGPromptModalProps;
204
+ } as PromptModalProps;
186
205
  };
187
206
 
188
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
189
- this.requireComponent(UIComponents.PromptModal),
190
- getProperties(),
191
- );
192
- const rawResult = await modal.beforeClose;
207
+ const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
193
208
  const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
194
209
 
195
210
  return result ?? null;
@@ -203,7 +218,7 @@ export class UIService extends Service {
203
218
  operation?: Promise<T> | (() => T),
204
219
  ): Promise<T> {
205
220
  const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
206
- const processArgs = (): { operationPromise: Promise<T>; props?: AGLoadingModalProps } => {
221
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
207
222
  if (typeof operationOrMessageOrOptions === 'string') {
208
223
  return {
209
224
  props: { message: operationOrMessageOrOptions },
@@ -222,10 +237,12 @@ export class UIService extends Service {
222
237
  };
223
238
 
224
239
  const { operationPromise, props } = processArgs();
225
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
240
+ const modal = await this.modal(this.requireComponent('loading-modal'), props);
226
241
 
227
242
  try {
228
- const [result] = await Promise.all([operationPromise, after({ seconds: 1 })]);
243
+ const result = await operationPromise;
244
+
245
+ await after({ ms: 500 });
229
246
 
230
247
  return result;
231
248
  } finally {
@@ -233,43 +250,34 @@ export class UIService extends Service {
233
250
  }
234
251
  }
235
252
 
236
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
237
- const snackbar: Snackbar = {
253
+ public toast(message: string, options: ToastOptions = {}): void {
254
+ const { component, ...otherOptions } = options;
255
+ const toast: UIToast = {
238
256
  id: uuid(),
239
- properties: { message, ...options },
240
- component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
257
+ properties: { message, ...otherOptions },
258
+ component: markRaw(component ?? this.requireComponent('toast')),
241
259
  };
242
260
 
243
- this.setState('snackbars', this.snackbars.concat(snackbar));
244
-
245
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
261
+ this.setState('toasts', this.toasts.concat(toast));
246
262
  }
247
263
 
248
- public hideSnackbar(id: string): void {
249
- this.setState(
250
- 'snackbars',
251
- this.snackbars.filter((snackbar) => snackbar.id !== id),
252
- );
253
- }
254
-
255
- public registerComponent(name: UIComponent, component: Component): void {
256
- this.components[name] = component;
257
- }
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>>>;
258
269
 
259
- public async openModal<TModalComponent extends ModalComponent>(
260
- component: TModalComponent,
261
- properties?: ModalProperties<TModalComponent>,
262
- ): Promise<Modal<ModalResult<TModalComponent>>> {
270
+ public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
263
271
  const id = uuid();
264
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
265
- const modal: Modal<ModalResult<TModalComponent>> = {
272
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
273
+ const modal: UIModal<ModalResult<T>> = {
266
274
  id,
267
- properties: properties ?? {},
275
+ closing: false,
276
+ properties: props ?? {},
268
277
  component: markRaw(component),
269
278
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
270
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
279
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
271
280
  };
272
- const activeModal = this.modals.at(-1);
273
281
  const modals = this.modals.concat(modal);
274
282
 
275
283
  this.modalCallbacks[modal.id] = callbacks;
@@ -277,15 +285,26 @@ export class UIService extends Service {
277
285
  this.setState({ modals });
278
286
 
279
287
  await nextTick();
280
- await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
281
- await Promise.all([
282
- activeModal || Events.emit('show-overlays-backdrop'),
283
- Events.emit('show-modal', { id: modal.id }),
284
- ]);
285
288
 
286
289
  return modal;
287
290
  }
288
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
+
289
308
  public async closeModal(id: string, result?: unknown): Promise<void> {
290
309
  if (!App.isMounted()) {
291
310
  await this.removeModal(id, result);
@@ -302,7 +321,7 @@ export class UIService extends Service {
302
321
  }
303
322
  }
304
323
 
305
- protected async boot(): Promise<void> {
324
+ protected override async boot(): Promise<void> {
306
325
  this.watchModalEvents();
307
326
  this.watchMountedEvent();
308
327
  this.watchViewportBreakpoints();
@@ -314,25 +333,23 @@ export class UIService extends Service {
314
333
  this.modals.filter((m) => m.id !== id),
315
334
  );
316
335
 
317
- this.modalCallbacks[id]?.closed?.(result);
336
+ this.modalCallbacks[id]?.hasClosed?.(result);
318
337
 
319
338
  delete this.modalCallbacks[id];
320
-
321
- const activeModal = this.modals.at(-1);
322
-
323
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
324
339
  }
325
340
 
326
341
  private watchModalEvents(): void {
327
- Events.on('modal-will-close', ({ modal, result }) => {
328
- this.modalCallbacks[modal.id]?.willClose?.(result);
342
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
343
+ const modal = this.modals.find((_modal) => id === _modal.id);
329
344
 
330
- if (this.modals.length === 1) {
331
- Events.emit('hide-overlays-backdrop');
345
+ if (modal) {
346
+ modal.closing = true;
332
347
  }
348
+
349
+ this.modalCallbacks[id]?.willClose?.(result);
333
350
  });
334
351
 
335
- Events.on('modal-closed', async ({ modal: { id }, result }) => {
352
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
336
353
  await this.removeModal(id, result);
337
354
  });
338
355
  }
@@ -373,14 +390,10 @@ export class UIService extends Service {
373
390
 
374
391
  export default facade(UIService);
375
392
 
376
- declare module '@/services/Events' {
393
+ declare module '@aerogel/core/services/Events' {
377
394
  export interface EventsPayload {
378
395
  'close-modal': { id: string; result?: unknown };
379
- 'hide-modal': { id: string };
380
- 'hide-overlays-backdrop': void;
381
- 'modal-closed': { modal: Modal; result?: unknown };
382
- 'modal-will-close': { modal: Modal; result?: unknown };
383
- 'show-modal': { id: string };
384
- 'show-overlays-backdrop': void;
396
+ 'modal-will-close': { modal: UIModal; result?: unknown };
397
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
385
398
  }
386
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
  }
@@ -1,11 +1,27 @@
1
1
  import { objectWithout } from '@noeldemartin/utils';
2
- import { computed, useAttrs } from 'vue';
2
+ import { computed, inject, onUnmounted, useAttrs } from 'vue';
3
+ import type { ClassValue } from 'clsx';
3
4
  import type { ComputedRef } from 'vue';
5
+ import type { Nullable } from '@noeldemartin/utils';
4
6
 
5
- export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<unknown>] {
7
+ import FormController from '@aerogel/core/forms/FormController';
8
+ import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
9
+
10
+ export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
11
+ const form = inject<FormController | null>('form', null);
12
+ const stop = form?.on('focus', (name) => input.name === name && listener());
13
+
14
+ onUnmounted(() => stop?.());
15
+ }
16
+
17
+ export function useForm<const T extends FormFieldDefinitions>(fields: T): FormController<T> & FormData<T> {
18
+ return new FormController(fields) as FormController<T> & FormData<T>;
19
+ }
20
+
21
+ export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
6
22
  const attrs = useAttrs();
7
- const className = computed(() => attrs.class);
23
+ const classes = computed(() => attrs.class);
8
24
  const inputAttrs = computed(() => objectWithout(attrs, 'class'));
9
25
 
10
- return [inputAttrs, className];
26
+ return [inputAttrs, classes as ComputedRef<ClassValue>];
11
27
  }
@@ -1,12 +1,21 @@
1
1
  import { debounce } from '@noeldemartin/utils';
2
- import { ref, watchEffect } from 'vue';
3
- import type { ComputedGetter, ComputedRef } from '@vue/runtime-core';
2
+ import { computed, ref, watch, watchEffect } from 'vue';
3
+ import type { ComputedGetter, ComputedRef, Ref } from 'vue';
4
4
 
5
5
  export interface ComputedDebounceOptions<T> {
6
6
  initial?: T;
7
7
  delay?: number;
8
8
  }
9
9
 
10
+ export function computedAsync<T>(getter: () => Promise<T>): Ref<T | undefined> {
11
+ const result = ref<T>();
12
+ const asyncValue = computed(getter);
13
+
14
+ watch(asyncValue, async () => (result.value = await asyncValue.value), { immediate: true });
15
+
16
+ return result;
17
+ }
18
+
10
19
  export function computedDebounce<T>(options: ComputedDebounceOptions<T>, getter: ComputedGetter<T>): ComputedRef<T>;
11
20
  export function computedDebounce<T>(getter: ComputedGetter<T>): ComputedRef<T | null>;
12
21
  export function computedDebounce<T>(
@@ -1,6 +1,9 @@
1
+ export * from './classes';
1
2
  export * from './composition/events';
2
3
  export * from './composition/forms';
3
4
  export * from './composition/hooks';
4
5
  export * from './composition/persistent';
5
- export * from './tailwindcss';
6
+ export * from './composition/state';
7
+ export * from './markdown';
8
+ export * from './types';
6
9
  export * from './vue';