@aerogel/core 0.0.0-next.9a02fcd3bcf698211dd7a71d4c48257c96dd7832 → 0.0.0-next.9e0c0bbdcff5db68a1087ef53cbdc0f53299f6bb

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 +2529 -1597
  2. package/dist/aerogel-core.js +3265 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +30 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +25 -16
  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/HeadlessInputDescription.vue +27 -0
  27. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  28. package/src/components/headless/HeadlessInputInput.vue +75 -0
  29. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  30. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  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 +113 -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 -9
  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/ui/Form.vue +24 -0
  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 +11 -5
  79. package/src/directives/measure.ts +34 -6
  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 +17 -1
  85. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  86. package/src/forms/{Form.ts → FormController.ts} +85 -25
  87. package/src/forms/composition.ts +4 -4
  88. package/src/forms/index.ts +3 -1
  89. package/src/forms/utils.ts +36 -5
  90. package/src/forms/validation.ts +19 -0
  91. package/src/index.css +54 -0
  92. package/src/{main.ts → index.ts} +1 -0
  93. package/src/jobs/Job.ts +144 -2
  94. package/src/jobs/index.ts +4 -1
  95. package/src/jobs/listeners.ts +3 -0
  96. package/src/jobs/status.ts +4 -0
  97. package/src/lang/DefaultLangProvider.ts +46 -0
  98. package/src/lang/Lang.state.ts +11 -0
  99. package/src/lang/Lang.ts +48 -21
  100. package/src/lang/index.ts +12 -6
  101. package/src/lang/settings/Language.vue +48 -0
  102. package/src/lang/settings/index.ts +10 -0
  103. package/src/plugins/Plugin.ts +1 -1
  104. package/src/plugins/index.ts +10 -7
  105. package/src/services/App.state.ts +36 -3
  106. package/src/services/App.ts +19 -3
  107. package/src/services/Cache.ts +43 -0
  108. package/src/services/Events.ts +15 -5
  109. package/src/services/Service.ts +125 -54
  110. package/src/services/Storage.ts +20 -0
  111. package/src/services/index.ts +16 -5
  112. package/src/services/utils.ts +18 -0
  113. package/src/testing/index.ts +4 -3
  114. package/src/testing/setup.ts +11 -0
  115. package/src/ui/UI.state.ts +19 -7
  116. package/src/ui/UI.ts +188 -77
  117. package/src/ui/index.ts +19 -18
  118. package/src/ui/utils.ts +16 -0
  119. package/src/utils/classes.ts +49 -0
  120. package/src/utils/composition/events.ts +2 -2
  121. package/src/utils/composition/forms.ts +14 -4
  122. package/src/utils/composition/persistent.test.ts +33 -0
  123. package/src/utils/composition/persistent.ts +11 -0
  124. package/src/utils/composition/state.test.ts +47 -0
  125. package/src/utils/composition/state.ts +33 -0
  126. package/src/utils/index.ts +5 -1
  127. package/src/utils/markdown.test.ts +50 -0
  128. package/src/utils/markdown.ts +19 -6
  129. package/src/utils/types.ts +3 -0
  130. package/src/utils/vue.ts +28 -127
  131. package/dist/aerogel-core.cjs.js +0 -2
  132. package/dist/aerogel-core.cjs.js.map +0 -1
  133. package/dist/aerogel-core.esm.js +0 -2
  134. package/dist/aerogel-core.esm.js.map +0 -1
  135. package/histoire.config.ts +0 -7
  136. package/noeldemartin.config.js +0 -5
  137. package/postcss.config.js +0 -6
  138. package/src/assets/histoire.css +0 -3
  139. package/src/components/AGAppSnackbars.vue +0 -13
  140. package/src/components/constants.ts +0 -8
  141. package/src/components/forms/AGButton.vue +0 -44
  142. package/src/components/forms/AGCheckbox.vue +0 -41
  143. package/src/components/forms/AGForm.vue +0 -26
  144. package/src/components/forms/AGInput.vue +0 -38
  145. package/src/components/forms/AGSelect.story.vue +0 -46
  146. package/src/components/forms/AGSelect.vue +0 -60
  147. package/src/components/forms/index.ts +0 -5
  148. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  149. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  150. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  151. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -47
  152. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  153. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  154. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  155. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  156. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  157. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  158. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  159. package/src/components/headless/forms/index.ts +0 -14
  160. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  161. package/src/components/headless/modals/AGHeadlessModal.vue +0 -86
  162. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  163. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  164. package/src/components/headless/modals/index.ts +0 -4
  165. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  166. package/src/components/headless/snackbars/index.ts +0 -40
  167. package/src/components/lib/AGErrorMessage.vue +0 -16
  168. package/src/components/lib/AGLink.vue +0 -9
  169. package/src/components/lib/AGMarkdown.vue +0 -36
  170. package/src/components/lib/AGMeasured.vue +0 -15
  171. package/src/components/lib/index.ts +0 -5
  172. package/src/components/modals/AGAlertModal.ts +0 -15
  173. package/src/components/modals/AGAlertModal.vue +0 -14
  174. package/src/components/modals/AGConfirmModal.ts +0 -27
  175. package/src/components/modals/AGConfirmModal.vue +0 -26
  176. package/src/components/modals/AGErrorReportModal.ts +0 -46
  177. package/src/components/modals/AGErrorReportModal.vue +0 -54
  178. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  179. package/src/components/modals/AGLoadingModal.ts +0 -23
  180. package/src/components/modals/AGLoadingModal.vue +0 -15
  181. package/src/components/modals/AGModal.ts +0 -10
  182. package/src/components/modals/AGModal.vue +0 -39
  183. package/src/components/modals/AGModalContext.ts +0 -8
  184. package/src/components/modals/AGModalContext.vue +0 -22
  185. package/src/components/modals/AGModalTitle.vue +0 -9
  186. package/src/components/modals/AGPromptModal.ts +0 -30
  187. package/src/components/modals/AGPromptModal.vue +0 -34
  188. package/src/components/modals/index.ts +0 -17
  189. package/src/components/snackbars/AGSnackbar.vue +0 -36
  190. package/src/components/snackbars/index.ts +0 -3
  191. package/src/components/utils.ts +0 -10
  192. package/src/directives/initial-focus.ts +0 -11
  193. package/src/main.histoire.ts +0 -1
  194. package/src/utils/tailwindcss.test.ts +0 -26
  195. package/src/utils/tailwindcss.ts +0 -7
  196. package/tailwind.config.js +0 -4
  197. package/tsconfig.json +0 -11
  198. package/vite.config.ts +0 -14
package/src/ui/UI.ts CHANGED
@@ -1,14 +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 { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
8
- 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';
9
15
 
10
16
  import Service from './UI.state';
11
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
17
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
18
+ import type { ModalComponent, UIModal, UIToast } from './UI.state';
12
19
 
13
20
  interface ModalCallbacks<T = unknown> {
14
21
  willClose(result: T | undefined): void;
@@ -16,9 +23,8 @@ interface ModalCallbacks<T = unknown> {
16
23
  }
17
24
 
18
25
  type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
19
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
20
- ? TResult
21
- : never;
26
+ type ModalResult<TComponent> =
27
+ TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
22
28
 
23
29
  export const UIComponents = {
24
30
  AlertModal: 'alert-modal',
@@ -26,29 +32,48 @@ export const UIComponents = {
26
32
  ErrorReportModal: 'error-report-modal',
27
33
  LoadingModal: 'loading-modal',
28
34
  PromptModal: 'prompt-modal',
29
- Snackbar: 'snackbar',
35
+ Toast: 'toast',
30
36
  StartupCrash: 'startup-crash',
37
+ RouterLink: 'router-link',
31
38
  } as const;
32
39
 
33
40
  export type UIComponent = ObjectValues<typeof UIComponents>;
34
41
 
35
- export interface ConfirmOptions {
42
+ export type ConfirmOptions = AcceptRefs<{
36
43
  acceptText?: string;
44
+ acceptVariant?: ButtonVariant;
37
45
  cancelText?: string;
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;
38
60
  }
39
61
 
40
- export interface PromptOptions {
62
+ export type PromptOptions = AcceptRefs<{
41
63
  label?: string;
42
64
  defaultValue?: string;
43
65
  placeholder?: string;
44
66
  acceptText?: string;
67
+ acceptVariant?: ButtonVariant;
45
68
  cancelText?: string;
46
- }
69
+ cancelVariant?: ButtonVariant;
70
+ trim?: boolean;
71
+ }>;
47
72
 
48
- export interface ShowSnackbarOptions {
73
+ export interface ToastOptions {
49
74
  component?: Component;
50
- color?: SnackbarColor;
51
- actions?: SnackbarAction[];
75
+ variant?: ToastVariant;
76
+ actions?: ToastAction[];
52
77
  }
53
78
 
54
79
  export class UIService extends Service {
@@ -56,14 +81,18 @@ export class UIService extends Service {
56
81
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
57
82
  private components: Partial<Record<UIComponent, Component>> = {};
58
83
 
84
+ public resolveComponent(name: UIComponent): Component | null {
85
+ return this.components[name] ?? null;
86
+ }
87
+
59
88
  public requireComponent(name: UIComponent): Component {
60
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
89
+ return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
61
90
  }
62
91
 
63
92
  public alert(message: string): void;
64
93
  public alert(title: string, message: string): void;
65
94
  public alert(messageOrTitle: string, message?: string): void {
66
- const getProperties = (): AGAlertModalProps => {
95
+ const getProperties = (): AlertModalProps => {
67
96
  if (typeof message !== 'string') {
68
97
  return { message: messageOrTitle };
69
98
  }
@@ -74,38 +103,79 @@ export class UIService extends Service {
74
103
  };
75
104
  };
76
105
 
77
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
106
+ this.openModal<ModalComponent<AlertModalProps>>(
107
+ this.requireComponent(UIComponents.AlertModal),
108
+ getProperties(),
109
+ );
78
110
  }
79
111
 
112
+ /* eslint-disable max-len */
80
113
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
81
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
+
82
119
  public async confirm(
83
120
  messageOrTitle: string,
84
- messageOrOptions?: string | ConfirmOptions,
85
- options?: ConfirmOptions,
86
- ): Promise<boolean> {
87
- const getProperties = (): AGConfirmModalProps => {
121
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
122
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
123
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
124
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
88
125
  if (typeof messageOrOptions !== 'string') {
89
126
  return {
90
- message: messageOrTitle,
91
127
  ...(messageOrOptions ?? {}),
128
+ message: messageOrTitle,
129
+ required: !!messageOrOptions?.required,
92
130
  };
93
131
  }
94
132
 
95
133
  return {
134
+ ...(options ?? {}),
96
135
  title: messageOrTitle,
97
136
  message: messageOrOptions,
98
- ...(options ?? {}),
137
+ required: !!options?.required,
99
138
  };
100
139
  };
101
140
 
102
- 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>(
103
148
  this.requireComponent(UIComponents.ConfirmModal),
104
- getProperties(),
149
+ properties,
105
150
  );
106
151
  const result = await modal.beforeClose;
107
152
 
108
- 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;
109
179
  }
110
180
 
111
181
  public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
@@ -115,47 +185,65 @@ export class UIService extends Service {
115
185
  messageOrOptions?: string | PromptOptions,
116
186
  options?: PromptOptions,
117
187
  ): Promise<string | null> {
118
- const getProperties = (): AGPromptModalProps => {
188
+ const trim = options?.trim ?? true;
189
+ const getProperties = (): PromptModalProps => {
119
190
  if (typeof messageOrOptions !== 'string') {
120
191
  return {
121
192
  message: messageOrTitle,
122
193
  ...(messageOrOptions ?? {}),
123
- };
194
+ } as PromptModalProps;
124
195
  }
125
196
 
126
197
  return {
127
198
  title: messageOrTitle,
128
199
  message: messageOrOptions,
129
200
  ...(options ?? {}),
130
- };
201
+ } as PromptModalProps;
131
202
  };
132
203
 
133
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
204
+ const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
134
205
  this.requireComponent(UIComponents.PromptModal),
135
206
  getProperties(),
136
207
  );
137
- const result = await modal.beforeClose;
208
+ const rawResult = await modal.beforeClose;
209
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
138
210
 
139
211
  return result ?? null;
140
212
  }
141
213
 
142
- public async loading<T>(operation: Promise<T>): Promise<T>;
143
- public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
144
- public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
145
- const getProperties = (): AGLoadingModalProps => {
146
- if (typeof messageOrOperation !== 'string') {
147
- return {};
214
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
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>;
217
+ public async loading<T>(
218
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
219
+ operation?: Promise<T> | (() => T),
220
+ ): Promise<T> {
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
+ };
148
228
  }
149
229
 
150
- return { message: messageOrOperation };
230
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
231
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
232
+ }
233
+
234
+ return {
235
+ props: operationOrMessageOrOptions,
236
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
237
+ };
151
238
  };
152
239
 
153
- 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);
154
242
 
155
243
  try {
156
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
244
+ const result = await operationPromise;
157
245
 
158
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
246
+ await after({ ms: 500 });
159
247
 
160
248
  return result;
161
249
  } finally {
@@ -163,23 +251,15 @@ export class UIService extends Service {
163
251
  }
164
252
  }
165
253
 
166
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
167
- const snackbar: Snackbar = {
254
+ public toast(message: string, options: ToastOptions = {}): void {
255
+ const { component, ...otherOptions } = options;
256
+ const toast: UIToast = {
168
257
  id: uuid(),
169
- properties: { message, ...options },
170
- component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
258
+ properties: { message, ...otherOptions },
259
+ component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
171
260
  };
172
261
 
173
- this.setState('snackbars', this.snackbars.concat(snackbar));
174
-
175
- setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
176
- }
177
-
178
- public hideSnackbar(id: string): void {
179
- this.setState(
180
- 'snackbars',
181
- this.snackbars.filter((snackbar) => snackbar.id !== id),
182
- );
262
+ this.setState('toasts', this.toasts.concat(toast));
183
263
  }
184
264
 
185
265
  public registerComponent(name: UIComponent, component: Component): void {
@@ -189,10 +269,10 @@ export class UIService extends Service {
189
269
  public async openModal<TModalComponent extends ModalComponent>(
190
270
  component: TModalComponent,
191
271
  properties?: ModalProperties<TModalComponent>,
192
- ): Promise<Modal<ModalResult<TModalComponent>>> {
272
+ ): Promise<UIModal<ModalResult<TModalComponent>>> {
193
273
  const id = uuid();
194
274
  const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
195
- const modal: Modal<ModalResult<TModalComponent>> = {
275
+ const modal: UIModal<ModalResult<TModalComponent>> = {
196
276
  id,
197
277
  properties: properties ?? {},
198
278
  component: markRaw(component),
@@ -217,12 +297,40 @@ export class UIService extends Service {
217
297
  }
218
298
 
219
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
+
220
306
  await Events.emit('close-modal', { id, result });
221
307
  }
222
308
 
223
- 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> {
224
316
  this.watchModalEvents();
225
317
  this.watchMountedEvent();
318
+ this.watchViewportBreakpoints();
319
+ }
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 }));
226
334
  }
227
335
 
228
336
  private watchModalEvents(): void {
@@ -234,31 +342,24 @@ export class UIService extends Service {
234
342
  }
235
343
  });
236
344
 
237
- Events.on('modal-closed', async ({ modal, result }) => {
238
- this.setState(
239
- 'modals',
240
- this.modals.filter((m) => m.id !== modal.id),
241
- );
242
-
243
- this.modalCallbacks[modal.id]?.closed?.(result);
244
-
245
- delete this.modalCallbacks[modal.id];
246
-
247
- const activeModal = this.modals.at(-1);
248
-
249
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
345
+ Events.on('modal-closed', async ({ modal: { id }, result }) => {
346
+ await this.removeModal(id, result);
250
347
  });
251
348
  }
252
349
 
253
350
  private watchMountedEvent(): void {
254
351
  Events.once('application-mounted', async () => {
255
- const splash = document.getElementById('splash');
352
+ if (!globalThis.document || !globalThis.getComputedStyle) {
353
+ return;
354
+ }
355
+
356
+ const splash = globalThis.document.getElementById('splash');
256
357
 
257
358
  if (!splash) {
258
359
  return;
259
360
  }
260
361
 
261
- if (window.getComputedStyle(splash).opacity !== '0') {
362
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
262
363
  splash.style.opacity = '0';
263
364
 
264
365
  await after({ ms: 600 });
@@ -268,17 +369,27 @@ export class UIService extends Service {
268
369
  });
269
370
  }
270
371
 
372
+ private watchViewportBreakpoints(): void {
373
+ if (!globalThis.matchMedia) {
374
+ return;
375
+ }
376
+
377
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
378
+
379
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
380
+ }
381
+
271
382
  }
272
383
 
273
384
  export default facade(UIService);
274
385
 
275
- declare module '@/services/Events' {
386
+ declare module '@aerogel/core/services/Events' {
276
387
  export interface EventsPayload {
277
388
  'close-modal': { id: string; result?: unknown };
278
389
  'hide-modal': { id: string };
279
390
  'hide-overlays-backdrop': void;
280
- 'modal-closed': { modal: Modal; result?: unknown };
281
- 'modal-will-close': { modal: Modal; result?: unknown };
391
+ 'modal-closed': { modal: UIModal; result?: unknown };
392
+ 'modal-will-close': { modal: UIModal; result?: unknown };
282
393
  'show-modal': { id: string };
283
394
  'show-overlays-backdrop': void;
284
395
  }
package/src/ui/index.ts CHANGED
@@ -1,21 +1,22 @@
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 };
17
17
 
18
18
  export * from './UI';
19
+ export * from './utils';
19
20
  export { default as UI } from './UI';
20
21
 
21
22
  export type UIServices = typeof services;
@@ -23,13 +24,13 @@ export type UIServices = typeof services;
23
24
  export default definePlugin({
24
25
  async install(app, options) {
25
26
  const defaultComponents = {
26
- [UIComponents.AlertModal]: AGAlertModal,
27
- [UIComponents.ConfirmModal]: AGConfirmModal,
28
- [UIComponents.ErrorReportModal]: AGErrorReportModal,
29
- [UIComponents.LoadingModal]: AGLoadingModal,
30
- [UIComponents.PromptModal]: AGPromptModal,
31
- [UIComponents.Snackbar]: AGSnackbar,
32
- [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,
33
34
  };
34
35
 
35
36
  Object.entries({
@@ -41,12 +42,12 @@ export default definePlugin({
41
42
  },
42
43
  });
43
44
 
44
- declare module '@/bootstrap/options' {
45
+ declare module '@aerogel/core/bootstrap/options' {
45
46
  export interface AerogelOptions {
46
47
  components?: Partial<Record<UIComponent, Component>>;
47
48
  }
48
49
  }
49
50
 
50
- declare module '@/services' {
51
+ declare module '@aerogel/core/services' {
51
52
  export interface Services extends UIServices {}
52
53
  }
@@ -0,0 +1,16 @@
1
+ export const MOBILE_BREAKPOINT = 768;
2
+
3
+ export const Layouts = {
4
+ Mobile: 'mobile',
5
+ Desktop: 'desktop',
6
+ } as const;
7
+
8
+ export type Layout = (typeof Layouts)[keyof typeof Layouts];
9
+
10
+ export function getCurrentLayout(): Layout {
11
+ if (globalThis.innerWidth > MOBILE_BREAKPOINT) {
12
+ return Layouts.Desktop;
13
+ }
14
+
15
+ return Layouts.Mobile;
16
+ }
@@ -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
+ }