@aerogel/core 0.0.0-next.f86b4b09f066c4aef21796a37dbc8417b7dce3cd → 0.0.0-next.f8c757d83e1e0d001a2836fa45aba318ec17b9b9

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 (196) hide show
  1. package/dist/aerogel-core.d.ts +1973 -1688
  2. package/dist/aerogel-core.js +3234 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +30 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -7
  6. package/src/bootstrap/index.ts +14 -15
  7. package/src/bootstrap/options.ts +1 -1
  8. package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -10
  11. package/src/components/AppToasts.vue +16 -0
  12. package/src/components/contracts/AlertModal.ts +4 -0
  13. package/src/components/contracts/Button.ts +16 -0
  14. package/src/components/contracts/ConfirmModal.ts +41 -0
  15. package/src/components/contracts/DropdownMenu.ts +18 -0
  16. package/src/components/contracts/ErrorReportModal.ts +29 -0
  17. package/src/components/contracts/Input.ts +26 -0
  18. package/src/components/contracts/LoadingModal.ts +18 -0
  19. package/src/components/contracts/Modal.ts +13 -0
  20. package/src/components/contracts/PromptModal.ts +30 -0
  21. package/src/components/contracts/Select.ts +44 -0
  22. package/src/components/contracts/Toast.ts +13 -0
  23. package/src/components/contracts/index.ts +9 -0
  24. package/src/components/contracts/shared.ts +9 -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} +16 -25
  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/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +18 -18
  33. package/src/components/headless/HeadlessModalContent.vue +24 -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 +113 -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 +37 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  44. package/src/components/headless/HeadlessToast.vue +18 -0
  45. package/src/components/headless/HeadlessToastAction.vue +13 -0
  46. package/src/components/headless/index.ts +19 -3
  47. package/src/components/index.ts +6 -11
  48. package/src/components/ui/AdvancedOptions.vue +18 -0
  49. package/src/components/ui/AlertModal.vue +13 -0
  50. package/src/components/ui/Button.vue +98 -0
  51. package/src/components/ui/Checkbox.vue +56 -0
  52. package/src/components/ui/ConfirmModal.vue +42 -0
  53. package/src/components/ui/DropdownMenu.vue +27 -0
  54. package/src/components/ui/DropdownMenuOption.vue +14 -0
  55. package/src/components/ui/DropdownMenuOptions.vue +27 -0
  56. package/src/components/ui/EditableContent.vue +82 -0
  57. package/src/components/ui/ErrorMessage.vue +15 -0
  58. package/src/components/ui/ErrorReportModal.vue +62 -0
  59. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  60. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  61. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  62. package/src/components/ui/Input.vue +56 -0
  63. package/src/components/ui/Link.vue +12 -0
  64. package/src/components/ui/LoadingModal.vue +32 -0
  65. package/src/components/ui/Markdown.vue +69 -0
  66. package/src/components/ui/Modal.vue +75 -0
  67. package/src/components/ui/ModalContext.vue +30 -0
  68. package/src/components/ui/ProgressBar.vue +50 -0
  69. package/src/components/ui/PromptModal.vue +35 -0
  70. package/src/components/ui/Select.vue +25 -0
  71. package/src/components/ui/SelectLabel.vue +17 -0
  72. package/src/components/ui/SelectOption.vue +29 -0
  73. package/src/components/ui/SelectOptions.vue +30 -0
  74. package/src/components/ui/SelectTrigger.vue +29 -0
  75. package/src/components/ui/SettingsModal.vue +15 -0
  76. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  77. package/src/components/ui/Toast.vue +42 -0
  78. package/src/components/ui/index.ts +30 -0
  79. package/src/directives/index.ts +9 -5
  80. package/src/directives/measure.ts +1 -1
  81. package/src/errors/Errors.state.ts +1 -1
  82. package/src/errors/Errors.ts +17 -18
  83. package/src/errors/JobCancelledError.ts +3 -0
  84. package/src/errors/index.ts +9 -6
  85. package/src/errors/utils.ts +1 -1
  86. package/src/forms/{Form.test.ts → FormController.test.ts} +5 -4
  87. package/src/forms/{Form.ts → FormController.ts} +22 -19
  88. package/src/forms/composition.ts +4 -4
  89. package/src/forms/index.ts +2 -2
  90. package/src/forms/utils.ts +2 -2
  91. package/src/index.css +46 -0
  92. package/src/jobs/Job.ts +144 -2
  93. package/src/jobs/index.ts +4 -1
  94. package/src/jobs/listeners.ts +3 -0
  95. package/src/jobs/status.ts +4 -0
  96. package/src/lang/DefaultLangProvider.ts +7 -4
  97. package/src/lang/Lang.state.ts +1 -1
  98. package/src/lang/Lang.ts +1 -1
  99. package/src/lang/index.ts +11 -6
  100. package/src/lang/settings/Language.vue +48 -0
  101. package/src/lang/settings/index.ts +10 -0
  102. package/src/plugins/Plugin.ts +1 -1
  103. package/src/plugins/index.ts +10 -7
  104. package/src/services/App.state.ts +21 -5
  105. package/src/services/App.ts +7 -4
  106. package/src/services/Cache.ts +1 -1
  107. package/src/services/Events.ts +15 -5
  108. package/src/services/Service.ts +116 -53
  109. package/src/services/Storage.ts +20 -0
  110. package/src/services/index.ts +14 -5
  111. package/src/services/utils.ts +18 -0
  112. package/src/testing/index.ts +4 -3
  113. package/src/testing/setup.ts +5 -13
  114. package/src/ui/UI.state.ts +12 -7
  115. package/src/ui/UI.ts +126 -84
  116. package/src/ui/index.ts +18 -18
  117. package/src/utils/classes.ts +49 -0
  118. package/src/utils/composition/events.ts +2 -2
  119. package/src/utils/composition/forms.ts +14 -4
  120. package/src/utils/composition/persistent.test.ts +33 -0
  121. package/src/utils/composition/persistent.ts +11 -0
  122. package/src/utils/composition/state.ts +11 -2
  123. package/src/utils/index.ts +4 -1
  124. package/src/utils/markdown.test.ts +50 -0
  125. package/src/utils/markdown.ts +19 -6
  126. package/src/utils/vue.ts +28 -136
  127. package/dist/aerogel-core.cjs.js +0 -2
  128. package/dist/aerogel-core.cjs.js.map +0 -1
  129. package/dist/aerogel-core.esm.js +0 -2
  130. package/dist/aerogel-core.esm.js.map +0 -1
  131. package/histoire.config.ts +0 -7
  132. package/noeldemartin.config.js +0 -5
  133. package/postcss.config.js +0 -6
  134. package/src/assets/histoire.css +0 -3
  135. package/src/components/AGAppSnackbars.vue +0 -13
  136. package/src/components/composition.ts +0 -23
  137. package/src/components/constants.ts +0 -8
  138. package/src/components/forms/AGButton.vue +0 -44
  139. package/src/components/forms/AGCheckbox.vue +0 -41
  140. package/src/components/forms/AGInput.vue +0 -40
  141. package/src/components/forms/AGSelect.story.vue +0 -46
  142. package/src/components/forms/AGSelect.vue +0 -60
  143. package/src/components/forms/index.ts +0 -5
  144. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  145. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  146. package/src/components/headless/forms/AGHeadlessInput.ts +0 -34
  147. package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
  148. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  149. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  150. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  151. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  152. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  153. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  154. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  155. package/src/components/headless/forms/composition.ts +0 -10
  156. package/src/components/headless/forms/index.ts +0 -18
  157. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  158. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  159. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  160. package/src/components/headless/modals/index.ts +0 -4
  161. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  162. package/src/components/headless/snackbars/index.ts +0 -40
  163. package/src/components/interfaces.ts +0 -24
  164. package/src/components/lib/AGErrorMessage.vue +0 -16
  165. package/src/components/lib/AGLink.vue +0 -9
  166. package/src/components/lib/AGMarkdown.vue +0 -41
  167. package/src/components/lib/AGMeasured.vue +0 -16
  168. package/src/components/lib/index.ts +0 -5
  169. package/src/components/modals/AGAlertModal.ts +0 -15
  170. package/src/components/modals/AGAlertModal.vue +0 -14
  171. package/src/components/modals/AGConfirmModal.ts +0 -35
  172. package/src/components/modals/AGConfirmModal.vue +0 -26
  173. package/src/components/modals/AGErrorReportModal.ts +0 -46
  174. package/src/components/modals/AGErrorReportModal.vue +0 -54
  175. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  176. package/src/components/modals/AGLoadingModal.ts +0 -23
  177. package/src/components/modals/AGLoadingModal.vue +0 -15
  178. package/src/components/modals/AGModal.ts +0 -10
  179. package/src/components/modals/AGModal.vue +0 -39
  180. package/src/components/modals/AGModalContext.ts +0 -8
  181. package/src/components/modals/AGModalContext.vue +0 -22
  182. package/src/components/modals/AGModalTitle.vue +0 -9
  183. package/src/components/modals/AGPromptModal.ts +0 -36
  184. package/src/components/modals/AGPromptModal.vue +0 -34
  185. package/src/components/modals/index.ts +0 -17
  186. package/src/components/snackbars/AGSnackbar.vue +0 -36
  187. package/src/components/snackbars/index.ts +0 -3
  188. package/src/components/utils.ts +0 -10
  189. package/src/directives/initial-focus.ts +0 -11
  190. package/src/main.histoire.ts +0 -1
  191. package/src/utils/tailwindcss.test.ts +0 -26
  192. package/src/utils/tailwindcss.ts +0 -7
  193. package/tailwind.config.js +0 -4
  194. package/tsconfig.json +0 -11
  195. package/vite.config.ts +0 -17
  196. /package/src/{main.ts → index.ts} +0 -0
@@ -1,10 +1,10 @@
1
1
  import type { Component } from 'vue';
2
2
 
3
- import { defineServiceState } from '@/services/Service';
3
+ 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;
@@ -12,14 +12,19 @@ export interface Modal<T = unknown> {
12
12
  afterClose: Promise<T | undefined>;
13
13
  }
14
14
 
15
+ export interface UIModalContext {
16
+ modal: UIModal;
17
+ childIndex?: number;
18
+ }
19
+
15
20
  export interface ModalComponent<
16
21
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- Properties extends Record<string, unknown> = Record<string, unknown>,
22
+ Properties extends object = object,
18
23
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
- Result = unknown
24
+ Result = unknown,
20
25
  > {}
21
26
 
22
- export interface Snackbar {
27
+ export interface UIToast {
23
28
  id: string;
24
29
  component: Component;
25
30
  properties: Record<string, unknown>;
@@ -28,8 +33,8 @@ export interface Snackbar {
28
33
  export default defineServiceState({
29
34
  name: 'ui',
30
35
  initialState: {
31
- modals: [] as Modal[],
32
- snackbars: [] as Snackbar[],
36
+ modals: [] as UIModal[],
37
+ toasts: [] as UIToast[],
33
38
  layout: getCurrentLayout(),
34
39
  },
35
40
  computed: {
package/src/ui/UI.ts CHANGED
@@ -1,17 +1,21 @@
1
- import { after, facade, fail, uuid } from '@noeldemartin/utils';
1
+ import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
2
2
  import { markRaw, nextTick } from 'vue';
3
3
  import type { Component } from 'vue';
4
4
  import type { ObjectValues } from '@noeldemartin/utils';
5
5
 
6
- import App from '@/services/App';
7
- import Events from '@/services/Events';
8
- import type { Color } from '@/components/constants';
9
- import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
10
- import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
6
+ import App from '@aerogel/core/services/App';
7
+ import Events from '@aerogel/core/services/Events';
8
+ import type { AcceptRefs } from '@aerogel/core/utils';
9
+ import type { AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
10
+ import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
11
+ import type { ConfirmModalCheckboxes, ConfirmModalProps } from '@aerogel/core/components/contracts/ConfirmModal';
12
+ import type { LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
13
+ import type { PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
14
+ import type { ToastAction, ToastVariant } from '@aerogel/core/components/contracts/Toast';
11
15
 
12
16
  import Service from './UI.state';
13
17
  import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
14
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
18
+ import type { ModalComponent, UIModal, UIToast } from './UI.state';
15
19
 
16
20
  interface ModalCallbacks<T = unknown> {
17
21
  willClose(result: T | undefined): void;
@@ -19,9 +23,8 @@ interface ModalCallbacks<T = unknown> {
19
23
  }
20
24
 
21
25
  type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
22
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
23
- ? TResult
24
- : never;
26
+ type ModalResult<TComponent> =
27
+ TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
25
28
 
26
29
  export const UIComponents = {
27
30
  AlertModal: 'alert-modal',
@@ -29,40 +32,47 @@ export const UIComponents = {
29
32
  ErrorReportModal: 'error-report-modal',
30
33
  LoadingModal: 'loading-modal',
31
34
  PromptModal: 'prompt-modal',
32
- Snackbar: 'snackbar',
35
+ Toast: 'toast',
33
36
  StartupCrash: 'startup-crash',
34
37
  } as const;
35
38
 
36
39
  export type UIComponent = ObjectValues<typeof UIComponents>;
37
40
 
38
- export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
39
-
40
- export interface ConfirmOptions {
41
+ export type ConfirmOptions = AcceptRefs<{
41
42
  acceptText?: string;
42
- acceptColor?: Color;
43
+ acceptVariant?: ButtonVariant;
43
44
  cancelText?: string;
44
- cancelColor?: Color;
45
- }
46
-
47
- export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
45
+ cancelVariant?: ButtonVariant;
46
+ actions?: Record<string, () => unknown>;
47
+ required?: boolean;
48
+ }>;
49
+
50
+ export type LoadingOptions = AcceptRefs<{
51
+ title?: string;
52
+ message?: string;
53
+ progress?: number;
54
+ }>;
55
+
56
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
57
+ extends ConfirmOptions {
48
58
  checkboxes?: T;
49
59
  }
50
60
 
51
- export interface PromptOptions {
61
+ export type PromptOptions = AcceptRefs<{
52
62
  label?: string;
53
63
  defaultValue?: string;
54
64
  placeholder?: string;
55
65
  acceptText?: string;
56
- acceptColor?: Color;
66
+ acceptVariant?: ButtonVariant;
57
67
  cancelText?: string;
58
- cancelColor?: Color;
68
+ cancelVariant?: ButtonVariant;
59
69
  trim?: boolean;
60
- }
70
+ }>;
61
71
 
62
- export interface ShowSnackbarOptions {
72
+ export interface ToastOptions {
63
73
  component?: Component;
64
- color?: SnackbarColor;
65
- actions?: SnackbarAction[];
74
+ variant?: ToastVariant;
75
+ actions?: ToastAction[];
66
76
  }
67
77
 
68
78
  export class UIService extends Service {
@@ -77,7 +87,7 @@ export class UIService extends Service {
77
87
  public alert(message: string): void;
78
88
  public alert(title: string, message: string): void;
79
89
  public alert(messageOrTitle: string, message?: string): void {
80
- const getProperties = (): AGAlertModalProps => {
90
+ const getProperties = (): AlertModalProps => {
81
91
  if (typeof message !== 'string') {
82
92
  return { message: messageOrTitle };
83
93
  }
@@ -88,14 +98,17 @@ export class UIService extends Service {
88
98
  };
89
99
  };
90
100
 
91
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
101
+ this.openModal<ModalComponent<AlertModalProps>>(
102
+ this.requireComponent(UIComponents.AlertModal),
103
+ getProperties(),
104
+ );
92
105
  }
93
106
 
94
107
  /* eslint-disable max-len */
95
108
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
96
109
  public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
97
- public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
98
- public async confirm<T extends ConfirmCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
110
+ public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
111
+ public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
99
112
  /* eslint-enable max-len */
100
113
 
101
114
  public async confirm(
@@ -103,27 +116,36 @@ export class UIService extends Service {
103
116
  messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
104
117
  options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
105
118
  ): Promise<boolean | [boolean, Record<string, boolean>]> {
106
- const getProperties = (): AGConfirmModalProps => {
119
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
107
120
  if (typeof messageOrOptions !== 'string') {
108
121
  return {
109
- message: messageOrTitle,
110
122
  ...(messageOrOptions ?? {}),
123
+ message: messageOrTitle,
124
+ required: !!messageOrOptions?.required,
111
125
  };
112
126
  }
113
127
 
114
128
  return {
129
+ ...(options ?? {}),
115
130
  title: messageOrTitle,
116
131
  message: messageOrOptions,
117
- ...(options ?? {}),
132
+ required: !!options?.required,
118
133
  };
119
134
  };
135
+
136
+ type ConfirmModalComponent = ModalComponent<
137
+ AcceptRefs<ConfirmModalProps>,
138
+ boolean | [boolean, Record<string, boolean>]
139
+ >;
140
+
120
141
  const properties = getProperties();
121
- const modal = await this.openModal<
122
- ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
123
- >(this.requireComponent(UIComponents.ConfirmModal), properties);
142
+ const modal = await this.openModal<ConfirmModalComponent>(
143
+ this.requireComponent(UIComponents.ConfirmModal),
144
+ properties,
145
+ );
124
146
  const result = await modal.beforeClose;
125
147
 
126
- const confirmed = typeof result === 'object' ? result[0] : result ?? false;
148
+ const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
127
149
  const checkboxes =
128
150
  typeof result === 'object'
129
151
  ? result[1]
@@ -140,7 +162,7 @@ export class UIService extends Service {
140
162
  continue;
141
163
  }
142
164
 
143
- if (confirmed && App.development) {
165
+ if (confirmed && isDevelopment()) {
144
166
  // eslint-disable-next-line no-console
145
167
  console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
146
168
  }
@@ -159,22 +181,22 @@ export class UIService extends Service {
159
181
  options?: PromptOptions,
160
182
  ): Promise<string | null> {
161
183
  const trim = options?.trim ?? true;
162
- const getProperties = (): AGPromptModalProps => {
184
+ const getProperties = (): PromptModalProps => {
163
185
  if (typeof messageOrOptions !== 'string') {
164
186
  return {
165
187
  message: messageOrTitle,
166
188
  ...(messageOrOptions ?? {}),
167
- };
189
+ } as PromptModalProps;
168
190
  }
169
191
 
170
192
  return {
171
193
  title: messageOrTitle,
172
194
  message: messageOrOptions,
173
195
  ...(options ?? {}),
174
- };
196
+ } as PromptModalProps;
175
197
  };
176
198
 
177
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
199
+ const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
178
200
  this.requireComponent(UIComponents.PromptModal),
179
201
  getProperties(),
180
202
  );
@@ -186,25 +208,37 @@ export class UIService extends Service {
186
208
 
187
209
  public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
188
210
  public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
211
+ public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
189
212
  public async loading<T>(
190
- messageOrOperation: string | Promise<T> | (() => T),
213
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
191
214
  operation?: Promise<T> | (() => T),
192
215
  ): Promise<T> {
193
- const getProperties = (): AGLoadingModalProps => {
194
- if (typeof messageOrOperation !== 'string') {
195
- return {};
216
+ const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
217
+ const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
218
+ if (typeof operationOrMessageOrOptions === 'string') {
219
+ return {
220
+ props: { message: operationOrMessageOrOptions },
221
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
222
+ };
196
223
  }
197
224
 
198
- return { message: messageOrOperation };
225
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
226
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
227
+ }
228
+
229
+ return {
230
+ props: operationOrMessageOrOptions,
231
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
232
+ };
199
233
  };
200
234
 
201
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
235
+ const { operationPromise, props } = processArgs();
236
+ const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
202
237
 
203
238
  try {
204
- operation = typeof messageOrOperation === 'string' ? (operation as () => T) : messageOrOperation;
205
- operation = typeof operation === 'function' ? Promise.resolve(operation()) : operation;
239
+ const result = await operationPromise;
206
240
 
207
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
241
+ await after({ ms: 500 });
208
242
 
209
243
  return result;
210
244
  } finally {
@@ -212,23 +246,15 @@ export class UIService extends Service {
212
246
  }
213
247
  }
214
248
 
215
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
216
- const snackbar: Snackbar = {
249
+ public toast(message: string, options: ToastOptions = {}): void {
250
+ const { component, ...otherOptions } = options;
251
+ const toast: UIToast = {
217
252
  id: uuid(),
218
- properties: { message, ...options },
219
- component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
253
+ properties: { message, ...otherOptions },
254
+ component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
220
255
  };
221
256
 
222
- this.setState('snackbars', this.snackbars.concat(snackbar));
223
-
224
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
225
- }
226
-
227
- public hideSnackbar(id: string): void {
228
- this.setState(
229
- 'snackbars',
230
- this.snackbars.filter((snackbar) => snackbar.id !== id),
231
- );
257
+ this.setState('toasts', this.toasts.concat(toast));
232
258
  }
233
259
 
234
260
  public registerComponent(name: UIComponent, component: Component): void {
@@ -238,10 +264,10 @@ export class UIService extends Service {
238
264
  public async openModal<TModalComponent extends ModalComponent>(
239
265
  component: TModalComponent,
240
266
  properties?: ModalProperties<TModalComponent>,
241
- ): Promise<Modal<ModalResult<TModalComponent>>> {
267
+ ): Promise<UIModal<ModalResult<TModalComponent>>> {
242
268
  const id = uuid();
243
269
  const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
244
- const modal: Modal<ModalResult<TModalComponent>> = {
270
+ const modal: UIModal<ModalResult<TModalComponent>> = {
245
271
  id,
246
272
  properties: properties ?? {},
247
273
  component: markRaw(component),
@@ -266,15 +292,42 @@ export class UIService extends Service {
266
292
  }
267
293
 
268
294
  public async closeModal(id: string, result?: unknown): Promise<void> {
295
+ if (!App.isMounted()) {
296
+ await this.removeModal(id, result);
297
+
298
+ return;
299
+ }
300
+
269
301
  await Events.emit('close-modal', { id, result });
270
302
  }
271
303
 
272
- protected async boot(): Promise<void> {
304
+ public async closeAllModals(): Promise<void> {
305
+ while (this.modals.length > 0) {
306
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
307
+ }
308
+ }
309
+
310
+ protected override async boot(): Promise<void> {
273
311
  this.watchModalEvents();
274
312
  this.watchMountedEvent();
275
313
  this.watchViewportBreakpoints();
276
314
  }
277
315
 
316
+ private async removeModal(id: string, result?: unknown): Promise<void> {
317
+ this.setState(
318
+ 'modals',
319
+ this.modals.filter((m) => m.id !== id),
320
+ );
321
+
322
+ this.modalCallbacks[id]?.closed?.(result);
323
+
324
+ delete this.modalCallbacks[id];
325
+
326
+ const activeModal = this.modals.at(-1);
327
+
328
+ await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
329
+ }
330
+
278
331
  private watchModalEvents(): void {
279
332
  Events.on('modal-will-close', ({ modal, result }) => {
280
333
  this.modalCallbacks[modal.id]?.willClose?.(result);
@@ -284,19 +337,8 @@ export class UIService extends Service {
284
337
  }
285
338
  });
286
339
 
287
- Events.on('modal-closed', async ({ modal, result }) => {
288
- this.setState(
289
- 'modals',
290
- this.modals.filter((m) => m.id !== modal.id),
291
- );
292
-
293
- this.modalCallbacks[modal.id]?.closed?.(result);
294
-
295
- delete this.modalCallbacks[modal.id];
296
-
297
- const activeModal = this.modals.at(-1);
298
-
299
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
340
+ Events.on('modal-closed', async ({ modal: { id }, result }) => {
341
+ await this.removeModal(id, result);
300
342
  });
301
343
  }
302
344
 
@@ -336,13 +378,13 @@ export class UIService extends Service {
336
378
 
337
379
  export default facade(UIService);
338
380
 
339
- declare module '@/services/Events' {
381
+ declare module '@aerogel/core/services/Events' {
340
382
  export interface EventsPayload {
341
383
  'close-modal': { id: string; result?: unknown };
342
384
  'hide-modal': { id: string };
343
385
  'hide-overlays-backdrop': void;
344
- 'modal-closed': { modal: Modal; result?: unknown };
345
- 'modal-will-close': { modal: Modal; result?: unknown };
386
+ 'modal-closed': { modal: UIModal; result?: unknown };
387
+ 'modal-will-close': { modal: UIModal; result?: unknown };
346
388
  'show-modal': { id: string };
347
389
  'show-overlays-backdrop': void;
348
390
  }
package/src/ui/index.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import type { Component } from 'vue';
2
2
 
3
- import { bootServices } from '@/services';
4
- import { definePlugin } from '@/plugins';
3
+ import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
4
+ import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
5
+ import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
6
+ import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
7
+ import PromptModal from '@aerogel/core/components/ui/PromptModal.vue';
8
+ import StartupCrash from '@aerogel/core/components/ui/StartupCrash.vue';
9
+ import Toast from '@aerogel/core/components/ui/Toast.vue';
10
+ import { bootServices } from '@aerogel/core/services';
11
+ import { definePlugin } from '@aerogel/core/plugins';
5
12
 
6
13
  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
14
  import type { UIComponent } from './UI';
15
15
 
16
16
  const services = { $ui: UI };
@@ -24,13 +24,13 @@ export type UIServices = typeof services;
24
24
  export default definePlugin({
25
25
  async install(app, options) {
26
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,
27
+ [UIComponents.AlertModal]: AlertModal,
28
+ [UIComponents.ConfirmModal]: ConfirmModal,
29
+ [UIComponents.ErrorReportModal]: ErrorReportModal,
30
+ [UIComponents.LoadingModal]: LoadingModal,
31
+ [UIComponents.PromptModal]: PromptModal,
32
+ [UIComponents.Toast]: Toast,
33
+ [UIComponents.StartupCrash]: StartupCrash,
34
34
  };
35
35
 
36
36
  Object.entries({
@@ -42,12 +42,12 @@ export default definePlugin({
42
42
  },
43
43
  });
44
44
 
45
- declare module '@/bootstrap/options' {
45
+ declare module '@aerogel/core/bootstrap/options' {
46
46
  export interface AerogelOptions {
47
47
  components?: Partial<Record<UIComponent, Component>>;
48
48
  }
49
49
  }
50
50
 
51
- declare module '@/services' {
51
+ declare module '@aerogel/core/services' {
52
52
  export interface Services extends UIServices {}
53
53
  }
@@ -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
+ }
@@ -1,13 +1,13 @@
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
9
  UnknownEvent,
10
- } from '@/services/Events';
10
+ } from '@aerogel/core/services/Events';
11
11
 
12
12
  export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
13
13
  export function useEvent<Event extends EventWithPayload>(
@@ -1,11 +1,21 @@
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 { FormController } from '@aerogel/core/forms';
6
+ import type { Nullable } from '@noeldemartin/utils';
4
7
 
5
- export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<unknown>] {
8
+ export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
9
+ const form = inject<FormController | null>('form', null);
10
+ const stop = form?.on('focus', (name) => input.name === name && listener());
11
+
12
+ onUnmounted(() => stop?.());
13
+ }
14
+
15
+ export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
6
16
  const attrs = useAttrs();
7
- const className = computed(() => attrs.class);
17
+ const classes = computed(() => attrs.class);
8
18
  const inputAttrs = computed(() => objectWithout(attrs, 'class'));
9
19
 
10
- return [inputAttrs, className];
20
+ return [inputAttrs, classes as ComputedRef<ClassValue>];
11
21
  }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { nextTick } from 'vue';
3
+ import { Storage } from '@noeldemartin/utils';
4
+
5
+ import { persistent } from './persistent';
6
+
7
+ describe('Vue persistent helper', () => {
8
+
9
+ it('serializes to localStorage', async () => {
10
+ // Arrange
11
+ const store = persistent<{ foo?: string }>('foobar', {});
12
+
13
+ // Act
14
+ store.foo = 'bar';
15
+
16
+ await nextTick();
17
+
18
+ // Assert
19
+ expect(Storage.get('foobar')).toEqual({ foo: 'bar' });
20
+ });
21
+
22
+ it('reads from localStorage', async () => {
23
+ // Arrange
24
+ Storage.set('foobar', { foo: 'bar' });
25
+
26
+ // Act
27
+ const store = persistent<{ foo?: string }>('foobar', {});
28
+
29
+ // Assert
30
+ expect(store.foo).toEqual('bar');
31
+ });
32
+
33
+ });
@@ -0,0 +1,11 @@
1
+ import { reactive, toRaw, watch } from 'vue';
2
+ import { Storage } from '@noeldemartin/utils';
3
+ import type { UnwrapNestedRefs } from 'vue';
4
+
5
+ export function persistent<T extends object>(name: string, defaults: T): UnwrapNestedRefs<T> {
6
+ const store = reactive<T>(Storage.get<T>(name) ?? defaults);
7
+
8
+ watch(store, () => Storage.set(name, toRaw(store)));
9
+
10
+ return store;
11
+ }
@@ -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>(