@aerogel/core 0.0.0-next.7035064d9ec6a82a936ee8dfcc4b58ed2e25a399 → 0.0.0-next.73a6df428477c011a1aebe0a932ebef0aa5e1b16

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