@aerogel/core 0.0.0-next.bde642c4a8096c5fc3d5e676c2115da23f4bf1d8 → 0.0.0-next.c2e6acc000e97a1020c2e232678563c53884dd0e

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 (166) hide show
  1. package/dist/aerogel-core.d.ts +1396 -1645
  2. package/dist/aerogel-core.js +2968 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +27 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -7
  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 -5
  11. package/src/components/composition.ts +1 -1
  12. package/src/components/contracts/AlertModal.ts +4 -0
  13. package/src/components/contracts/Button.ts +15 -0
  14. package/src/components/contracts/ConfirmModal.ts +41 -0
  15. package/src/components/contracts/ErrorReportModal.ts +29 -0
  16. package/src/components/contracts/Input.ts +26 -0
  17. package/src/components/contracts/LoadingModal.ts +18 -0
  18. package/src/components/contracts/Modal.ts +9 -0
  19. package/src/components/contracts/PromptModal.ts +28 -0
  20. package/src/components/contracts/index.ts +7 -0
  21. package/src/components/contracts/shared.ts +9 -0
  22. package/src/components/forms/AGSelect.vue +11 -17
  23. package/src/components/forms/index.ts +0 -4
  24. package/src/components/headless/HeadlessButton.vue +45 -0
  25. package/src/components/headless/HeadlessInput.vue +59 -0
  26. package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +6 -7
  27. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  28. package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +11 -19
  29. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  30. package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +7 -9
  31. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +16 -18
  32. package/src/components/headless/HeadlessModalContent.vue +24 -0
  33. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  34. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  35. package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
  36. package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
  37. package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
  38. package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
  39. package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
  40. package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
  41. package/src/components/headless/forms/composition.ts +2 -2
  42. package/src/components/headless/forms/index.ts +2 -12
  43. package/src/components/headless/index.ts +12 -1
  44. package/src/components/headless/snackbars/index.ts +3 -3
  45. package/src/components/index.ts +5 -5
  46. package/src/components/lib/AGErrorMessage.vue +5 -5
  47. package/src/components/lib/AGMeasured.vue +3 -2
  48. package/src/components/lib/AGStartupCrash.vue +8 -8
  49. package/src/components/lib/index.ts +0 -2
  50. package/src/components/snackbars/AGSnackbar.vue +10 -8
  51. package/src/components/ui/AlertModal.vue +13 -0
  52. package/src/components/ui/Button.vue +58 -0
  53. package/src/components/ui/Checkbox.vue +49 -0
  54. package/src/components/ui/ConfirmModal.vue +42 -0
  55. package/src/components/ui/ErrorReportModal.vue +62 -0
  56. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
  57. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  58. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  59. package/src/components/ui/Input.vue +52 -0
  60. package/src/components/ui/Link.vue +12 -0
  61. package/src/components/ui/LoadingModal.vue +32 -0
  62. package/src/components/ui/Markdown.vue +62 -0
  63. package/src/components/ui/Modal.vue +55 -0
  64. package/src/components/ui/ModalContext.vue +30 -0
  65. package/src/components/ui/ProgressBar.vue +50 -0
  66. package/src/components/ui/PromptModal.vue +35 -0
  67. package/src/components/ui/index.ts +16 -0
  68. package/src/components/utils.ts +106 -9
  69. package/src/directives/index.ts +9 -5
  70. package/src/directives/measure.ts +25 -6
  71. package/src/errors/Errors.state.ts +1 -1
  72. package/src/errors/Errors.ts +14 -14
  73. package/src/errors/JobCancelledError.ts +3 -0
  74. package/src/errors/index.ts +9 -6
  75. package/src/errors/utils.ts +17 -1
  76. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  77. package/src/forms/{Form.ts → FormController.ts} +46 -25
  78. package/src/forms/composition.ts +4 -4
  79. package/src/forms/index.ts +3 -2
  80. package/src/forms/utils.ts +22 -6
  81. package/src/forms/validation.ts +19 -0
  82. package/src/index.css +8 -0
  83. package/src/jobs/Job.ts +144 -2
  84. package/src/jobs/index.ts +4 -1
  85. package/src/jobs/listeners.ts +3 -0
  86. package/src/jobs/status.ts +4 -0
  87. package/src/lang/DefaultLangProvider.ts +7 -4
  88. package/src/lang/Lang.state.ts +1 -1
  89. package/src/lang/Lang.ts +5 -1
  90. package/src/lang/index.ts +8 -6
  91. package/src/plugins/Plugin.ts +1 -1
  92. package/src/plugins/index.ts +10 -7
  93. package/src/services/App.state.ts +13 -4
  94. package/src/services/App.ts +8 -3
  95. package/src/services/Cache.ts +1 -1
  96. package/src/services/Events.ts +15 -5
  97. package/src/services/Service.ts +116 -53
  98. package/src/services/Storage.ts +20 -0
  99. package/src/services/index.ts +11 -5
  100. package/src/services/utils.ts +18 -0
  101. package/src/testing/index.ts +4 -3
  102. package/src/testing/setup.ts +5 -13
  103. package/src/ui/UI.state.ts +17 -5
  104. package/src/ui/UI.ts +168 -62
  105. package/src/ui/index.ts +17 -16
  106. package/src/ui/utils.ts +16 -0
  107. package/src/utils/composition/events.ts +2 -2
  108. package/src/utils/composition/forms.ts +4 -3
  109. package/src/utils/composition/persistent.test.ts +33 -0
  110. package/src/utils/composition/persistent.ts +11 -0
  111. package/src/utils/composition/state.test.ts +47 -0
  112. package/src/utils/composition/state.ts +24 -0
  113. package/src/utils/index.ts +2 -0
  114. package/src/utils/markdown.test.ts +50 -0
  115. package/src/utils/markdown.ts +19 -6
  116. package/src/utils/vdom.ts +31 -0
  117. package/src/utils/vue.ts +18 -13
  118. package/dist/aerogel-core.cjs.js +0 -2
  119. package/dist/aerogel-core.cjs.js.map +0 -1
  120. package/dist/aerogel-core.esm.js +0 -2
  121. package/dist/aerogel-core.esm.js.map +0 -1
  122. package/histoire.config.ts +0 -7
  123. package/noeldemartin.config.js +0 -5
  124. package/postcss.config.js +0 -6
  125. package/src/assets/histoire.css +0 -3
  126. package/src/components/forms/AGButton.vue +0 -44
  127. package/src/components/forms/AGCheckbox.vue +0 -41
  128. package/src/components/forms/AGInput.vue +0 -40
  129. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  130. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  131. package/src/components/headless/forms/AGHeadlessInput.ts +0 -33
  132. package/src/components/headless/forms/AGHeadlessInput.vue +0 -63
  133. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  134. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  135. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  136. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  137. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  138. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  139. package/src/components/headless/modals/index.ts +0 -4
  140. package/src/components/interfaces.ts +0 -24
  141. package/src/components/lib/AGLink.vue +0 -9
  142. package/src/components/lib/AGMarkdown.vue +0 -41
  143. package/src/components/modals/AGAlertModal.ts +0 -15
  144. package/src/components/modals/AGAlertModal.vue +0 -14
  145. package/src/components/modals/AGConfirmModal.ts +0 -33
  146. package/src/components/modals/AGConfirmModal.vue +0 -26
  147. package/src/components/modals/AGErrorReportModal.ts +0 -46
  148. package/src/components/modals/AGErrorReportModal.vue +0 -54
  149. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  150. package/src/components/modals/AGLoadingModal.ts +0 -23
  151. package/src/components/modals/AGLoadingModal.vue +0 -15
  152. package/src/components/modals/AGModal.ts +0 -10
  153. package/src/components/modals/AGModal.vue +0 -39
  154. package/src/components/modals/AGModalContext.ts +0 -8
  155. package/src/components/modals/AGModalContext.vue +0 -22
  156. package/src/components/modals/AGModalTitle.vue +0 -9
  157. package/src/components/modals/AGPromptModal.ts +0 -36
  158. package/src/components/modals/AGPromptModal.vue +0 -34
  159. package/src/components/modals/index.ts +0 -17
  160. package/src/directives/initial-focus.ts +0 -11
  161. package/src/main.histoire.ts +0 -1
  162. package/tailwind.config.js +0 -4
  163. package/tsconfig.json +0 -11
  164. package/vite.config.ts +0 -17
  165. /package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +0 -0
  166. /package/src/{main.ts → index.ts} +0 -0
package/src/ui/UI.ts CHANGED
@@ -1,15 +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 { SnackbarAction, SnackbarColor } from '@aerogel/core/components/headless/snackbars';
10
15
 
11
16
  import Service from './UI.state';
12
- import type { Modal, ModalComponent, Snackbar } from './UI.state';
17
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
18
+ import type { ModalComponent, Snackbar, UIModal } from './UI.state';
13
19
 
14
20
  interface ModalCallbacks<T = unknown> {
15
21
  willClose(result: T | undefined): void;
@@ -17,9 +23,8 @@ interface ModalCallbacks<T = unknown> {
17
23
  }
18
24
 
19
25
  type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
20
- type ModalResult<TComponent> = TComponent extends ModalComponent<Record<string, unknown>, infer TResult>
21
- ? TResult
22
- : never;
26
+ type ModalResult<TComponent> =
27
+ TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
23
28
 
24
29
  export const UIComponents = {
25
30
  AlertModal: 'alert-modal',
@@ -33,23 +38,36 @@ export const UIComponents = {
33
38
 
34
39
  export type UIComponent = ObjectValues<typeof UIComponents>;
35
40
 
36
- export interface ConfirmOptions {
41
+ export type ConfirmOptions = AcceptRefs<{
37
42
  acceptText?: string;
38
- acceptColor?: Color;
43
+ acceptVariant?: ButtonVariant;
39
44
  cancelText?: string;
40
- cancelColor?: Color;
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 {
58
+ checkboxes?: T;
41
59
  }
42
60
 
43
- export interface PromptOptions {
61
+ export type PromptOptions = AcceptRefs<{
44
62
  label?: string;
45
63
  defaultValue?: string;
46
64
  placeholder?: string;
47
65
  acceptText?: string;
48
- acceptColor?: Color;
66
+ acceptVariant?: ButtonVariant;
49
67
  cancelText?: string;
50
- cancelColor?: Color;
68
+ cancelVariant?: ButtonVariant;
51
69
  trim?: boolean;
52
- }
70
+ }>;
53
71
 
54
72
  export interface ShowSnackbarOptions {
55
73
  component?: Component;
@@ -69,7 +87,7 @@ export class UIService extends Service {
69
87
  public alert(message: string): void;
70
88
  public alert(title: string, message: string): void;
71
89
  public alert(messageOrTitle: string, message?: string): void {
72
- const getProperties = (): AGAlertModalProps => {
90
+ const getProperties = (): AlertModalProps => {
73
91
  if (typeof message !== 'string') {
74
92
  return { message: messageOrTitle };
75
93
  }
@@ -80,38 +98,79 @@ export class UIService extends Service {
80
98
  };
81
99
  };
82
100
 
83
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
101
+ this.openModal<ModalComponent<AlertModalProps>>(
102
+ this.requireComponent(UIComponents.AlertModal),
103
+ getProperties(),
104
+ );
84
105
  }
85
106
 
107
+ /* eslint-disable max-len */
86
108
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
87
109
  public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
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
112
+ /* eslint-enable max-len */
113
+
88
114
  public async confirm(
89
115
  messageOrTitle: string,
90
- messageOrOptions?: string | ConfirmOptions,
91
- options?: ConfirmOptions,
92
- ): Promise<boolean> {
93
- const getProperties = (): AGConfirmModalProps => {
116
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
117
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
118
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
119
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
94
120
  if (typeof messageOrOptions !== 'string') {
95
121
  return {
96
- message: messageOrTitle,
97
122
  ...(messageOrOptions ?? {}),
123
+ message: messageOrTitle,
124
+ required: !!messageOrOptions?.required,
98
125
  };
99
126
  }
100
127
 
101
128
  return {
129
+ ...(options ?? {}),
102
130
  title: messageOrTitle,
103
131
  message: messageOrOptions,
104
- ...(options ?? {}),
132
+ required: !!options?.required,
105
133
  };
106
134
  };
107
135
 
108
- const modal = await this.openModal<ModalComponent<AGConfirmModalProps, boolean>>(
136
+ type ConfirmModalComponent = ModalComponent<
137
+ AcceptRefs<ConfirmModalProps>,
138
+ boolean | [boolean, Record<string, boolean>]
139
+ >;
140
+
141
+ const properties = getProperties();
142
+ const modal = await this.openModal<ConfirmModalComponent>(
109
143
  this.requireComponent(UIComponents.ConfirmModal),
110
- getProperties(),
144
+ properties,
111
145
  );
112
146
  const result = await modal.beforeClose;
113
147
 
114
- return result ?? false;
148
+ const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
149
+ const checkboxes =
150
+ typeof result === 'object'
151
+ ? result[1]
152
+ : Object.entries(properties.checkboxes ?? {}).reduce(
153
+ (values, [checkbox, { default: defaultValue }]) => ({
154
+ [checkbox]: defaultValue ?? false,
155
+ ...values,
156
+ }),
157
+ {} as Record<string, boolean>,
158
+ );
159
+
160
+ for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
161
+ if (!checkbox.required || checkboxes[name]) {
162
+ continue;
163
+ }
164
+
165
+ if (confirmed && isDevelopment()) {
166
+ // eslint-disable-next-line no-console
167
+ console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
168
+ }
169
+
170
+ return [false, checkboxes];
171
+ }
172
+
173
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
115
174
  }
116
175
 
117
176
  public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
@@ -122,22 +181,22 @@ export class UIService extends Service {
122
181
  options?: PromptOptions,
123
182
  ): Promise<string | null> {
124
183
  const trim = options?.trim ?? true;
125
- const getProperties = (): AGPromptModalProps => {
184
+ const getProperties = (): PromptModalProps => {
126
185
  if (typeof messageOrOptions !== 'string') {
127
186
  return {
128
187
  message: messageOrTitle,
129
188
  ...(messageOrOptions ?? {}),
130
- };
189
+ } as PromptModalProps;
131
190
  }
132
191
 
133
192
  return {
134
193
  title: messageOrTitle,
135
194
  message: messageOrOptions,
136
195
  ...(options ?? {}),
137
- };
196
+ } as PromptModalProps;
138
197
  };
139
198
 
140
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
199
+ const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
141
200
  this.requireComponent(UIComponents.PromptModal),
142
201
  getProperties(),
143
202
  );
@@ -147,23 +206,39 @@ export class UIService extends Service {
147
206
  return result ?? null;
148
207
  }
149
208
 
150
- public async loading<T>(operation: Promise<T>): Promise<T>;
151
- public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
152
- public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
153
- const getProperties = (): AGLoadingModalProps => {
154
- if (typeof messageOrOperation !== 'string') {
155
- return {};
209
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
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>;
212
+ public async loading<T>(
213
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
214
+ operation?: Promise<T> | (() => T),
215
+ ): Promise<T> {
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
+ };
156
223
  }
157
224
 
158
- 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
+ };
159
233
  };
160
234
 
161
- 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);
162
237
 
163
238
  try {
164
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
239
+ const result = await operationPromise;
165
240
 
166
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
241
+ await after({ ms: 500 });
167
242
 
168
243
  return result;
169
244
  } finally {
@@ -197,10 +272,10 @@ export class UIService extends Service {
197
272
  public async openModal<TModalComponent extends ModalComponent>(
198
273
  component: TModalComponent,
199
274
  properties?: ModalProperties<TModalComponent>,
200
- ): Promise<Modal<ModalResult<TModalComponent>>> {
275
+ ): Promise<UIModal<ModalResult<TModalComponent>>> {
201
276
  const id = uuid();
202
277
  const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
203
- const modal: Modal<ModalResult<TModalComponent>> = {
278
+ const modal: UIModal<ModalResult<TModalComponent>> = {
204
279
  id,
205
280
  properties: properties ?? {},
206
281
  component: markRaw(component),
@@ -225,12 +300,40 @@ export class UIService extends Service {
225
300
  }
226
301
 
227
302
  public async closeModal(id: string, result?: unknown): Promise<void> {
303
+ if (!App.isMounted()) {
304
+ await this.removeModal(id, result);
305
+
306
+ return;
307
+ }
308
+
228
309
  await Events.emit('close-modal', { id, result });
229
310
  }
230
311
 
231
- protected async boot(): Promise<void> {
312
+ public async closeAllModals(): Promise<void> {
313
+ while (this.modals.length > 0) {
314
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
315
+ }
316
+ }
317
+
318
+ protected override async boot(): Promise<void> {
232
319
  this.watchModalEvents();
233
320
  this.watchMountedEvent();
321
+ this.watchViewportBreakpoints();
322
+ }
323
+
324
+ private async removeModal(id: string, result?: unknown): Promise<void> {
325
+ this.setState(
326
+ 'modals',
327
+ this.modals.filter((m) => m.id !== id),
328
+ );
329
+
330
+ this.modalCallbacks[id]?.closed?.(result);
331
+
332
+ delete this.modalCallbacks[id];
333
+
334
+ const activeModal = this.modals.at(-1);
335
+
336
+ await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
234
337
  }
235
338
 
236
339
  private watchModalEvents(): void {
@@ -242,31 +345,24 @@ export class UIService extends Service {
242
345
  }
243
346
  });
244
347
 
245
- Events.on('modal-closed', async ({ modal, result }) => {
246
- this.setState(
247
- 'modals',
248
- this.modals.filter((m) => m.id !== modal.id),
249
- );
250
-
251
- this.modalCallbacks[modal.id]?.closed?.(result);
252
-
253
- delete this.modalCallbacks[modal.id];
254
-
255
- const activeModal = this.modals.at(-1);
256
-
257
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
348
+ Events.on('modal-closed', async ({ modal: { id }, result }) => {
349
+ await this.removeModal(id, result);
258
350
  });
259
351
  }
260
352
 
261
353
  private watchMountedEvent(): void {
262
354
  Events.once('application-mounted', async () => {
263
- const splash = document.getElementById('splash');
355
+ if (!globalThis.document || !globalThis.getComputedStyle) {
356
+ return;
357
+ }
358
+
359
+ const splash = globalThis.document.getElementById('splash');
264
360
 
265
361
  if (!splash) {
266
362
  return;
267
363
  }
268
364
 
269
- if (window.getComputedStyle(splash).opacity !== '0') {
365
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
270
366
  splash.style.opacity = '0';
271
367
 
272
368
  await after({ ms: 600 });
@@ -276,17 +372,27 @@ export class UIService extends Service {
276
372
  });
277
373
  }
278
374
 
375
+ private watchViewportBreakpoints(): void {
376
+ if (!globalThis.matchMedia) {
377
+ return;
378
+ }
379
+
380
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
381
+
382
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
383
+ }
384
+
279
385
  }
280
386
 
281
387
  export default facade(UIService);
282
388
 
283
- declare module '@/services/Events' {
389
+ declare module '@aerogel/core/services/Events' {
284
390
  export interface EventsPayload {
285
391
  'close-modal': { id: string; result?: unknown };
286
392
  'hide-modal': { id: string };
287
393
  'hide-overlays-backdrop': void;
288
- 'modal-closed': { modal: Modal; result?: unknown };
289
- 'modal-will-close': { modal: Modal; result?: unknown };
394
+ 'modal-closed': { modal: UIModal; result?: unknown };
395
+ 'modal-will-close': { modal: UIModal; result?: unknown };
290
396
  'show-modal': { id: string };
291
397
  'show-overlays-backdrop': void;
292
398
  }
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 AGSnackbar from '@aerogel/core/components/snackbars/AGSnackbar.vue';
4
+ import AGStartupCrash from '@aerogel/core/components/lib/AGStartupCrash.vue';
5
+ import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
6
+ import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
7
+ import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
8
+ import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
9
+ import PromptModal from '@aerogel/core/components/ui/PromptModal.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,11 +24,11 @@ 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,
27
+ [UIComponents.AlertModal]: AlertModal,
28
+ [UIComponents.ConfirmModal]: ConfirmModal,
29
+ [UIComponents.ErrorReportModal]: ErrorReportModal,
30
+ [UIComponents.LoadingModal]: LoadingModal,
31
+ [UIComponents.PromptModal]: PromptModal,
31
32
  [UIComponents.Snackbar]: AGSnackbar,
32
33
  [UIComponents.StartupCrash]: AGStartupCrash,
33
34
  };
@@ -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
+ }
@@ -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,12 @@
1
1
  import { objectWithout } from '@noeldemartin/utils';
2
2
  import { computed, useAttrs } from 'vue';
3
+ import type { ClassValue } from 'clsx';
3
4
  import type { ComputedRef } from 'vue';
4
5
 
5
- export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<unknown>] {
6
+ export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
6
7
  const attrs = useAttrs();
7
- const className = computed(() => attrs.class);
8
+ const classes = computed(() => attrs.class);
8
9
  const inputAttrs = computed(() => objectWithout(attrs, 'class'));
9
10
 
10
- return [inputAttrs, className];
11
+ return [inputAttrs, classes as ComputedRef<ClassValue>];
11
12
  }
@@ -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
+ });
@@ -0,0 +1,24 @@
1
+ import { debounce } from '@noeldemartin/utils';
2
+ import { ref, watchEffect } from 'vue';
3
+ import type { ComputedGetter, ComputedRef } from '@vue/runtime-core';
4
+
5
+ export interface ComputedDebounceOptions<T> {
6
+ initial?: T;
7
+ delay?: number;
8
+ }
9
+
10
+ export function computedDebounce<T>(options: ComputedDebounceOptions<T>, getter: ComputedGetter<T>): ComputedRef<T>;
11
+ export function computedDebounce<T>(getter: ComputedGetter<T>): ComputedRef<T | null>;
12
+ export function computedDebounce<T>(
13
+ optionsOrGetter: ComputedGetter<T> | ComputedDebounceOptions<T>,
14
+ inputGetter?: ComputedGetter<T>,
15
+ ): ComputedRef<T> {
16
+ const inputOptions = inputGetter ? (optionsOrGetter as ComputedDebounceOptions<T>) : {};
17
+ const getter = inputGetter ?? (optionsOrGetter as ComputedGetter<T>);
18
+ const state = ref(inputOptions.initial ?? null);
19
+ const update = debounce((value) => (state.value = value), inputOptions.delay ?? 300);
20
+
21
+ watchEffect(() => update(getter()));
22
+
23
+ return state as unknown as ComputedRef<T>;
24
+ }
@@ -1,5 +1,7 @@
1
1
  export * from './composition/events';
2
2
  export * from './composition/forms';
3
3
  export * from './composition/hooks';
4
+ export * from './composition/persistent';
5
+ export * from './markdown';
4
6
  export * from './tailwindcss';
5
7
  export * from './vue';