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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/aerogel-core.d.ts +1283 -1588
  2. package/dist/aerogel-core.js +2496 -1994
  3. package/dist/aerogel-core.js.map +1 -1
  4. package/package.json +10 -2
  5. package/src/components/AppLayout.vue +14 -0
  6. package/src/components/{AGAppModals.vue → AppModals.vue} +2 -3
  7. package/src/components/AppOverlays.vue +9 -0
  8. package/src/components/AppToasts.vue +16 -0
  9. package/src/components/contracts/AlertModal.ts +8 -0
  10. package/src/components/contracts/Button.ts +16 -0
  11. package/src/components/contracts/ConfirmModal.ts +46 -0
  12. package/src/components/contracts/DropdownMenu.ts +20 -0
  13. package/src/components/{modals/AGErrorReportModal.ts → contracts/ErrorReportModal.ts} +5 -22
  14. package/src/components/contracts/Input.ts +26 -0
  15. package/src/components/contracts/LoadingModal.ts +26 -0
  16. package/src/components/contracts/Modal.ts +15 -10
  17. package/src/components/contracts/PromptModal.ts +34 -0
  18. package/src/components/contracts/Select.ts +45 -0
  19. package/src/components/contracts/Toast.ts +15 -0
  20. package/src/components/contracts/index.ts +10 -1
  21. package/src/components/headless/HeadlessButton.vue +51 -0
  22. package/src/components/headless/HeadlessInput.vue +59 -0
  23. package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +6 -7
  24. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +2 -6
  25. package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +16 -25
  26. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +2 -6
  27. package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +9 -12
  28. package/src/components/headless/HeadlessModal.vue +57 -0
  29. package/src/components/headless/HeadlessModalContent.vue +30 -0
  30. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  31. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  32. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  33. package/src/components/headless/HeadlessSelect.vue +120 -0
  34. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +3 -4
  35. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  36. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  37. package/src/components/headless/HeadlessSelectOptions.vue +42 -0
  38. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  39. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  40. package/src/components/headless/HeadlessToast.vue +18 -0
  41. package/src/components/headless/HeadlessToastAction.vue +13 -0
  42. package/src/components/headless/index.ts +19 -3
  43. package/src/components/index.ts +5 -10
  44. package/src/components/ui/AdvancedOptions.vue +18 -0
  45. package/src/components/ui/AlertModal.vue +14 -0
  46. package/src/components/ui/Button.vue +98 -0
  47. package/src/components/ui/Checkbox.vue +56 -0
  48. package/src/components/ui/ConfirmModal.vue +45 -0
  49. package/src/components/ui/DropdownMenu.vue +32 -0
  50. package/src/components/ui/DropdownMenuOption.vue +14 -0
  51. package/src/components/ui/DropdownMenuOptions.vue +27 -0
  52. package/src/components/ui/EditableContent.vue +82 -0
  53. package/src/components/ui/ErrorMessage.vue +15 -0
  54. package/src/components/ui/ErrorReportModal.vue +67 -0
  55. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +28 -21
  56. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  57. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  58. package/src/components/ui/Input.vue +56 -0
  59. package/src/components/ui/Link.vue +12 -0
  60. package/src/components/ui/LoadingModal.vue +34 -0
  61. package/src/components/ui/Markdown.vue +69 -0
  62. package/src/components/ui/Modal.vue +122 -0
  63. package/src/components/{modals/AGModalContext.vue → ui/ModalContext.vue} +8 -9
  64. package/src/components/ui/ProgressBar.vue +51 -0
  65. package/src/components/ui/PromptModal.vue +38 -0
  66. package/src/components/ui/Select.vue +27 -0
  67. package/src/components/ui/SelectLabel.vue +17 -0
  68. package/src/components/ui/SelectOption.vue +29 -0
  69. package/src/components/ui/SelectOptions.vue +35 -0
  70. package/src/components/ui/SelectTrigger.vue +29 -0
  71. package/src/components/ui/SettingsModal.vue +15 -0
  72. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +7 -7
  73. package/src/components/ui/Toast.vue +44 -0
  74. package/src/components/ui/index.ts +30 -0
  75. package/src/errors/Errors.ts +7 -16
  76. package/src/forms/{Form.test.ts → FormController.test.ts} +32 -9
  77. package/src/forms/{Form.ts → FormController.ts} +28 -27
  78. package/src/forms/index.ts +2 -3
  79. package/src/forms/utils.ts +35 -35
  80. package/src/index.css +72 -0
  81. package/src/lang/index.ts +4 -0
  82. package/src/lang/settings/Language.vue +48 -0
  83. package/src/lang/settings/index.ts +10 -0
  84. package/src/services/App.state.ts +11 -1
  85. package/src/services/App.ts +9 -1
  86. package/src/services/Events.test.ts +8 -8
  87. package/src/services/Events.ts +2 -8
  88. package/src/services/index.ts +3 -0
  89. package/src/ui/UI.state.ts +7 -12
  90. package/src/ui/UI.ts +102 -103
  91. package/src/ui/index.ts +23 -24
  92. package/src/utils/classes.ts +49 -0
  93. package/src/utils/composition/events.ts +2 -4
  94. package/src/utils/composition/forms.ts +20 -4
  95. package/src/utils/composition/state.ts +11 -2
  96. package/src/utils/index.ts +3 -1
  97. package/src/utils/types.ts +3 -0
  98. package/src/utils/vue.ts +22 -128
  99. package/src/components/AGAppLayout.vue +0 -16
  100. package/src/components/AGAppOverlays.vue +0 -41
  101. package/src/components/AGAppSnackbars.vue +0 -13
  102. package/src/components/composition.ts +0 -23
  103. package/src/components/constants.ts +0 -8
  104. package/src/components/contracts/shared.ts +0 -9
  105. package/src/components/forms/AGButton.vue +0 -44
  106. package/src/components/forms/AGCheckbox.vue +0 -42
  107. package/src/components/forms/AGInput.vue +0 -42
  108. package/src/components/forms/AGSelect.story.vue +0 -46
  109. package/src/components/forms/AGSelect.vue +0 -54
  110. package/src/components/forms/index.ts +0 -5
  111. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  112. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  113. package/src/components/headless/forms/AGHeadlessInput.ts +0 -41
  114. package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
  115. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  116. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  117. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  118. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -31
  119. package/src/components/headless/forms/AGHeadlessSelectOptions.vue +0 -19
  120. package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +0 -25
  121. package/src/components/headless/forms/composition.ts +0 -10
  122. package/src/components/headless/forms/index.ts +0 -17
  123. package/src/components/headless/modals/AGHeadlessModal.ts +0 -33
  124. package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
  125. package/src/components/headless/modals/AGHeadlessModalContent.vue +0 -25
  126. package/src/components/headless/modals/index.ts +0 -5
  127. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  128. package/src/components/headless/snackbars/index.ts +0 -40
  129. package/src/components/lib/AGErrorMessage.vue +0 -16
  130. package/src/components/lib/AGLink.vue +0 -9
  131. package/src/components/lib/AGMarkdown.vue +0 -54
  132. package/src/components/lib/AGMeasured.vue +0 -16
  133. package/src/components/lib/AGProgressBar.vue +0 -55
  134. package/src/components/lib/index.ts +0 -6
  135. package/src/components/modals/AGAlertModal.ts +0 -18
  136. package/src/components/modals/AGAlertModal.vue +0 -14
  137. package/src/components/modals/AGConfirmModal.ts +0 -42
  138. package/src/components/modals/AGConfirmModal.vue +0 -27
  139. package/src/components/modals/AGErrorReportModal.vue +0 -54
  140. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  141. package/src/components/modals/AGLoadingModal.ts +0 -29
  142. package/src/components/modals/AGLoadingModal.vue +0 -15
  143. package/src/components/modals/AGModal.vue +0 -40
  144. package/src/components/modals/AGModalContext.ts +0 -8
  145. package/src/components/modals/AGModalTitle.vue +0 -9
  146. package/src/components/modals/AGPromptModal.ts +0 -41
  147. package/src/components/modals/AGPromptModal.vue +0 -35
  148. package/src/components/modals/index.ts +0 -16
  149. package/src/components/snackbars/AGSnackbar.vue +0 -36
  150. package/src/components/snackbars/index.ts +0 -3
  151. package/src/components/utils.ts +0 -63
  152. package/src/forms/composition.ts +0 -6
  153. package/src/utils/tailwindcss.test.ts +0 -26
  154. package/src/utils/tailwindcss.ts +0 -7
@@ -9,6 +9,7 @@ import Events from './Events';
9
9
  import Service from './Service';
10
10
  import Storage from './Storage';
11
11
  import { getPiniaStore } from './store';
12
+ import type { AppSetting } from './App.state';
12
13
 
13
14
  export * from './App';
14
15
  export * from './Cache';
@@ -53,6 +54,7 @@ export default definePlugin({
53
54
  };
54
55
 
55
56
  app.use(getPiniaStore());
57
+ options.settings?.forEach((setting) => App.addSetting(setting));
56
58
 
57
59
  await bootServices(app, services);
58
60
  },
@@ -61,6 +63,7 @@ export default definePlugin({
61
63
  declare module '@aerogel/core/bootstrap/options' {
62
64
  export interface AerogelOptions {
63
65
  services?: Record<string, Service>;
66
+ settings?: AppSetting[];
64
67
  }
65
68
  }
66
69
 
@@ -4,22 +4,16 @@ import { defineServiceState } from '@aerogel/core/services/Service';
4
4
 
5
5
  import { Layouts, getCurrentLayout } from './utils';
6
6
 
7
- export interface Modal<T = unknown> {
7
+ export interface UIModal<T = unknown> {
8
8
  id: string;
9
9
  properties: Record<string, unknown>;
10
10
  component: Component;
11
+ closing: boolean;
11
12
  beforeClose: Promise<T | undefined>;
12
13
  afterClose: Promise<T | undefined>;
13
14
  }
14
15
 
15
- export interface ModalComponent<
16
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- Properties extends Record<string, unknown> = Record<string, unknown>,
18
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
- Result = unknown,
20
- > {}
21
-
22
- export interface Snackbar {
16
+ export interface UIToast {
23
17
  id: string;
24
18
  component: Component;
25
19
  properties: Record<string, unknown>;
@@ -28,12 +22,13 @@ export interface Snackbar {
28
22
  export default defineServiceState({
29
23
  name: 'ui',
30
24
  initialState: {
31
- modals: [] as Modal[],
32
- snackbars: [] as Snackbar[],
25
+ modals: [] as UIModal[],
26
+ toasts: [] as UIToast[],
33
27
  layout: getCurrentLayout(),
34
28
  },
35
29
  computed: {
36
- mobile: ({ layout }) => layout === Layouts.Mobile,
37
30
  desktop: ({ layout }) => layout === Layouts.Desktop,
31
+ mobile: ({ layout }) => layout === Layouts.Mobile,
32
+ openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
38
33
  },
39
34
  });
package/src/ui/UI.ts CHANGED
@@ -1,52 +1,61 @@
1
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
+ import type { ClosureArgs } from '@noeldemartin/utils';
5
6
 
6
7
  import App from '@aerogel/core/services/App';
7
8
  import Events from '@aerogel/core/services/Events';
8
- import type { AcceptRefs } from '@aerogel/core/utils';
9
- import type { Color } from '@aerogel/core/components/constants';
10
- import type { SnackbarAction, SnackbarColor } from '@aerogel/core/components/headless/snackbars';
11
9
  import type {
12
- AGAlertModalProps,
13
- AGConfirmModalProps,
14
- AGLoadingModalProps,
15
- AGPromptModalProps,
16
- } from '@aerogel/core/components';
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';
17
24
 
18
25
  import Service from './UI.state';
19
26
  import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
20
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
27
+ import type { UIModal, UIToast } from './UI.state';
21
28
 
22
29
  interface ModalCallbacks<T = unknown> {
23
30
  willClose(result: T | undefined): void;
24
- closed(result: T | undefined): void;
31
+ hasClosed(result: T | undefined): void;
25
32
  }
26
33
 
27
- type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
28
- type ModalResult<TComponent> =
29
- TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
30
-
31
- export const UIComponents = {
32
- AlertModal: 'alert-modal',
33
- ConfirmModal: 'confirm-modal',
34
- ErrorReportModal: 'error-report-modal',
35
- LoadingModal: 'loading-modal',
36
- PromptModal: 'prompt-modal',
37
- Snackbar: 'snackbar',
38
- StartupCrash: 'startup-crash',
39
- } as const;
40
-
41
- 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
+ }
42
48
 
43
- export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
49
+ export interface UIModalContext {
50
+ modal: UIModal;
51
+ childIndex?: number;
52
+ }
44
53
 
45
54
  export type ConfirmOptions = AcceptRefs<{
46
55
  acceptText?: string;
47
- acceptColor?: Color;
56
+ acceptVariant?: ButtonVariant;
48
57
  cancelText?: string;
49
- cancelColor?: Color;
58
+ cancelVariant?: ButtonVariant;
50
59
  actions?: Record<string, () => unknown>;
51
60
  required?: boolean;
52
61
  }>;
@@ -57,7 +66,8 @@ export type LoadingOptions = AcceptRefs<{
57
66
  progress?: number;
58
67
  }>;
59
68
 
60
- export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
69
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
70
+ extends ConfirmOptions {
61
71
  checkboxes?: T;
62
72
  }
63
73
 
@@ -66,31 +76,39 @@ export type PromptOptions = AcceptRefs<{
66
76
  defaultValue?: string;
67
77
  placeholder?: string;
68
78
  acceptText?: string;
69
- acceptColor?: Color;
79
+ acceptVariant?: ButtonVariant;
70
80
  cancelText?: string;
71
- cancelColor?: Color;
81
+ cancelVariant?: ButtonVariant;
72
82
  trim?: boolean;
73
83
  }>;
74
84
 
75
- export interface ShowSnackbarOptions {
85
+ export interface ToastOptions {
76
86
  component?: Component;
77
- color?: SnackbarColor;
78
- actions?: SnackbarAction[];
87
+ variant?: ToastVariant;
88
+ actions?: ToastAction[];
79
89
  }
80
90
 
81
91
  export class UIService extends Service {
82
92
 
83
93
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
84
- private components: Partial<Record<UIComponent, Component>> = {};
94
+ private components: Partial<UIComponents> = {};
85
95
 
86
- public requireComponent(name: UIComponent): Component {
87
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
96
+ public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
97
+ this.components[name] = component;
98
+ }
99
+
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!`);
88
106
  }
89
107
 
90
108
  public alert(message: string): void;
91
109
  public alert(title: string, message: string): void;
92
110
  public alert(messageOrTitle: string, message?: string): void {
93
- const getProperties = (): AGAlertModalProps => {
111
+ const getProperties = (): AlertModalProps => {
94
112
  if (typeof message !== 'string') {
95
113
  return { message: messageOrTitle };
96
114
  }
@@ -101,14 +119,14 @@ export class UIService extends Service {
101
119
  };
102
120
  };
103
121
 
104
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
122
+ this.openModal(this.requireComponent('alert-modal'), getProperties());
105
123
  }
106
124
 
107
125
  /* eslint-disable max-len */
108
126
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
109
127
  public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
110
- public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
111
- 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
112
130
  /* eslint-enable max-len */
113
131
 
114
132
  public async confirm(
@@ -116,7 +134,7 @@ export class UIService extends Service {
116
134
  messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
117
135
  options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
118
136
  ): Promise<boolean | [boolean, Record<string, boolean>]> {
119
- const getProperties = (): AGConfirmModalProps => {
137
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
120
138
  if (typeof messageOrOptions !== 'string') {
121
139
  return {
122
140
  ...(messageOrOptions ?? {}),
@@ -132,10 +150,9 @@ export class UIService extends Service {
132
150
  required: !!options?.required,
133
151
  };
134
152
  };
153
+
135
154
  const properties = getProperties();
136
- const modal = await this.openModal<
137
- ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
138
- >(this.requireComponent(UIComponents.ConfirmModal), properties);
155
+ const modal = await this.openModal(this.requireComponent('confirm-modal'), properties);
139
156
  const result = await modal.beforeClose;
140
157
 
141
158
  const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
@@ -174,25 +191,22 @@ export class UIService extends Service {
174
191
  options?: PromptOptions,
175
192
  ): Promise<string | null> {
176
193
  const trim = options?.trim ?? true;
177
- const getProperties = (): AGPromptModalProps => {
194
+ const getProperties = (): PromptModalProps => {
178
195
  if (typeof messageOrOptions !== 'string') {
179
196
  return {
180
197
  message: messageOrTitle,
181
198
  ...(messageOrOptions ?? {}),
182
- } as AGPromptModalProps;
199
+ } as PromptModalProps;
183
200
  }
184
201
 
185
202
  return {
186
203
  title: messageOrTitle,
187
204
  message: messageOrOptions,
188
205
  ...(options ?? {}),
189
- } as AGPromptModalProps;
206
+ } as PromptModalProps;
190
207
  };
191
208
 
192
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
193
- this.requireComponent(UIComponents.PromptModal),
194
- getProperties(),
195
- );
209
+ const modal = await this.openModal(this.requireComponent('prompt-modal'), getProperties());
196
210
  const rawResult = await modal.beforeClose;
197
211
  const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
198
212
 
@@ -207,7 +221,7 @@ export class UIService extends Service {
207
221
  operation?: Promise<T> | (() => T),
208
222
  ): Promise<T> {
209
223
  const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
210
- const processArgs = (): { operationPromise: Promise<T>; props?: AGLoadingModalProps } => {
224
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
211
225
  if (typeof operationOrMessageOrOptions === 'string') {
212
226
  return {
213
227
  props: { message: operationOrMessageOrOptions },
@@ -226,10 +240,12 @@ export class UIService extends Service {
226
240
  };
227
241
 
228
242
  const { operationPromise, props } = processArgs();
229
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
243
+ const modal = await this.openModal(this.requireComponent('loading-modal'), props);
230
244
 
231
245
  try {
232
- const [result] = await Promise.all([operationPromise, after({ seconds: 1 })]);
246
+ const result = await operationPromise;
247
+
248
+ await after({ ms: 500 });
233
249
 
234
250
  return result;
235
251
  } finally {
@@ -237,43 +253,37 @@ export class UIService extends Service {
237
253
  }
238
254
  }
239
255
 
240
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
241
- const snackbar: Snackbar = {
256
+ public toast(message: string, options: ToastOptions = {}): void {
257
+ const { component, ...otherOptions } = options;
258
+ const toast: UIToast = {
242
259
  id: uuid(),
243
- properties: { message, ...options },
244
- component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
260
+ properties: { message, ...otherOptions },
261
+ component: markRaw(component ?? this.requireComponent('toast')),
245
262
  };
246
263
 
247
- this.setState('snackbars', this.snackbars.concat(snackbar));
248
-
249
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
250
- }
251
-
252
- public hideSnackbar(id: string): void {
253
- this.setState(
254
- 'snackbars',
255
- this.snackbars.filter((snackbar) => snackbar.id !== id),
256
- );
264
+ this.setState('toasts', this.toasts.concat(toast));
257
265
  }
258
266
 
259
- public registerComponent(name: UIComponent, component: Component): void {
260
- this.components[name] = component;
261
- }
267
+ public openModal<T extends Component>(
268
+ ...args: {} extends ComponentProps<T>
269
+ ? [component: T, props?: AcceptRefs<ComponentProps<T>>]
270
+ : [component: T, props: AcceptRefs<ComponentProps<T>>]
271
+ ): Promise<UIModal<ModalResult<T>>>;
262
272
 
263
- public async openModal<TModalComponent extends ModalComponent>(
264
- component: TModalComponent,
265
- properties?: ModalProperties<TModalComponent>,
266
- ): Promise<Modal<ModalResult<TModalComponent>>> {
273
+ public async openModal<T extends Component>(
274
+ component: T,
275
+ props?: ComponentProps<T>,
276
+ ): Promise<UIModal<ModalResult<T>>> {
267
277
  const id = uuid();
268
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
269
- const modal: Modal<ModalResult<TModalComponent>> = {
278
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
279
+ const modal: UIModal<ModalResult<T>> = {
270
280
  id,
271
- properties: properties ?? {},
281
+ closing: false,
282
+ properties: props ?? {},
272
283
  component: markRaw(component),
273
284
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
274
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
285
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
275
286
  };
276
- const activeModal = this.modals.at(-1);
277
287
  const modals = this.modals.concat(modal);
278
288
 
279
289
  this.modalCallbacks[modal.id] = callbacks;
@@ -281,11 +291,6 @@ export class UIService extends Service {
281
291
  this.setState({ modals });
282
292
 
283
293
  await nextTick();
284
- await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
285
- await Promise.all([
286
- activeModal || Events.emit('show-overlays-backdrop'),
287
- Events.emit('show-modal', { id: modal.id }),
288
- ]);
289
294
 
290
295
  return modal;
291
296
  }
@@ -318,25 +323,23 @@ export class UIService extends Service {
318
323
  this.modals.filter((m) => m.id !== id),
319
324
  );
320
325
 
321
- this.modalCallbacks[id]?.closed?.(result);
326
+ this.modalCallbacks[id]?.hasClosed?.(result);
322
327
 
323
328
  delete this.modalCallbacks[id];
324
-
325
- const activeModal = this.modals.at(-1);
326
-
327
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
328
329
  }
329
330
 
330
331
  private watchModalEvents(): void {
331
- Events.on('modal-will-close', ({ modal, result }) => {
332
- this.modalCallbacks[modal.id]?.willClose?.(result);
332
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
333
+ const modal = this.modals.find((_modal) => id === _modal.id);
333
334
 
334
- if (this.modals.length === 1) {
335
- Events.emit('hide-overlays-backdrop');
335
+ if (modal) {
336
+ modal.closing = true;
336
337
  }
338
+
339
+ this.modalCallbacks[id]?.willClose?.(result);
337
340
  });
338
341
 
339
- Events.on('modal-closed', async ({ modal: { id }, result }) => {
342
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
340
343
  await this.removeModal(id, result);
341
344
  });
342
345
  }
@@ -380,11 +383,7 @@ export default facade(UIService);
380
383
  declare module '@aerogel/core/services/Events' {
381
384
  export interface EventsPayload {
382
385
  'close-modal': { id: string; result?: unknown };
383
- 'hide-modal': { id: string };
384
- 'hide-overlays-backdrop': void;
385
- 'modal-closed': { modal: Modal; result?: unknown };
386
- 'modal-will-close': { modal: Modal; result?: unknown };
387
- 'show-modal': { id: string };
388
- 'show-overlays-backdrop': void;
386
+ 'modal-will-close': { modal: UIModal; result?: unknown };
387
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
389
388
  }
390
389
  }
package/src/ui/index.ts CHANGED
@@ -1,17 +1,16 @@
1
- import type { Component } from 'vue';
2
-
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';
3
8
  import { bootServices } from '@aerogel/core/services';
4
9
  import { definePlugin } from '@aerogel/core/plugins';
5
10
 
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';
11
+ import UI from './UI';
12
+ import type { UIComponents } from './UI';
13
+ import type { Component } from 'vue';
15
14
 
16
15
  const services = { $ui: UI };
17
16
 
@@ -23,20 +22,20 @@ 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
  },
@@ -44,7 +43,7 @@ export default definePlugin({
44
43
 
45
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
 
@@ -0,0 +1,49 @@
1
+ import clsx from 'clsx';
2
+ import { computed, unref } from 'vue';
3
+ import { cva } from 'class-variance-authority';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import type { ClassValue } from 'clsx';
6
+ import type { ComputedRef, PropType, Ref } from 'vue';
7
+ import type { GetClosureArgs, GetClosureResult } from '@noeldemartin/utils';
8
+
9
+ export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
10
+ export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
11
+ export type RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
12
+ export type Variants<T extends Record<string, string | boolean>> = Required<{
13
+ [K in keyof T]: Exclude<T[K], undefined> extends string
14
+ ? { [key in Exclude<T[K], undefined>]: string | null }
15
+ : { true: string | null; false: string | null };
16
+ }>;
17
+
18
+ export type ComponentPropDefinitions<T> = {
19
+ [K in keyof T]: {
20
+ type?: PropType<T[K]>;
21
+ default: T[K] | (() => T[K]) | null;
22
+ };
23
+ };
24
+
25
+ export type PickComponentProps<TValues, TDefinitions> = {
26
+ [K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
27
+ };
28
+
29
+ export function computedVariantClasses<T>(
30
+ value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
31
+ config: { baseClasses?: string } & CVAConfig<T>,
32
+ ): ComputedRef<string> {
33
+ return computed(() => {
34
+ const { baseClasses: valueBaseClasses, ...valueRefs } = value;
35
+ const { baseClasses: configBaseClasses, ...configs } = config;
36
+ const variants = cva(configBaseClasses, configs as CVAConfig<T>);
37
+ const values = Object.entries(valueRefs).reduce((extractedValues, [name, valueRef]) => {
38
+ extractedValues[name as keyof CVAProps<T>] = unref(valueRef);
39
+
40
+ return extractedValues;
41
+ }, {} as CVAProps<T>);
42
+
43
+ return classes(variants(values), unref(valueBaseClasses));
44
+ });
45
+ }
46
+
47
+ export function classes(...inputs: ClassValue[]): string {
48
+ return twMerge(clsx(inputs));
49
+ }
@@ -6,7 +6,6 @@ import type {
6
6
  EventWithPayload,
7
7
  EventWithoutPayload,
8
8
  EventsPayload,
9
- UnknownEvent,
10
9
  } from '@aerogel/core/services/Events';
11
10
 
12
11
  export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
@@ -14,11 +13,10 @@ 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,7 +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';
6
+ export * from './composition/state';
5
7
  export * from './markdown';
6
- export * from './tailwindcss';
8
+ export * from './types';
7
9
  export * from './vue';
@@ -0,0 +1,3 @@
1
+ import type { Nullable } from '@noeldemartin/utils';
2
+
3
+ export type Falsifiable<T> = Nullable<T> | false;