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

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