@aerogel/core 0.0.0-next.a68f133e2c9a1ae9ba84b4e2e42df909289e5fba → 0.0.0-next.aa6e27a9c197d1ee10c9fe018ee8c0fc6ff77767

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 (191) hide show
  1. package/dist/aerogel-core.d.ts +1948 -1460
  2. package/dist/aerogel-core.js +3223 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +27 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +26 -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/composition.ts +23 -0
  13. package/src/components/contracts/AlertModal.ts +4 -0
  14. package/src/components/contracts/Button.ts +16 -0
  15. package/src/components/contracts/ConfirmModal.ts +41 -0
  16. package/src/components/contracts/DropdownMenu.ts +11 -0
  17. package/src/components/contracts/ErrorReportModal.ts +29 -0
  18. package/src/components/contracts/Input.ts +26 -0
  19. package/src/components/contracts/LoadingModal.ts +18 -0
  20. package/src/components/contracts/Modal.ts +13 -0
  21. package/src/components/contracts/PromptModal.ts +28 -0
  22. package/src/components/contracts/Select.ts +36 -0
  23. package/src/components/contracts/Toast.ts +13 -0
  24. package/src/components/contracts/index.ts +9 -0
  25. package/src/components/contracts/shared.ts +9 -0
  26. package/src/components/headless/HeadlessButton.vue +50 -0
  27. package/src/components/headless/HeadlessInput.vue +59 -0
  28. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  29. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  30. package/src/components/headless/HeadlessInputInput.vue +75 -0
  31. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  32. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  33. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +17 -17
  34. package/src/components/headless/HeadlessModalContent.vue +24 -0
  35. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  36. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  37. package/src/components/headless/HeadlessSelect.vue +105 -0
  38. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
  39. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  40. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  41. package/src/components/headless/HeadlessSelectOptions.vue +30 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +15 -0
  44. package/src/components/headless/HeadlessToast.vue +18 -0
  45. package/src/components/headless/HeadlessToastAction.vue +13 -0
  46. package/src/components/headless/index.ts +18 -3
  47. package/src/components/index.ts +5 -9
  48. package/src/components/ui/AdvancedOptions.vue +18 -0
  49. package/src/components/ui/AlertModal.vue +13 -0
  50. package/src/components/ui/Button.vue +98 -0
  51. package/src/components/ui/Checkbox.vue +56 -0
  52. package/src/components/ui/ConfirmModal.vue +42 -0
  53. package/src/components/ui/DropdownMenu.vue +33 -0
  54. package/src/components/ui/EditableContent.vue +82 -0
  55. package/src/components/ui/ErrorMessage.vue +15 -0
  56. package/src/components/ui/ErrorReportModal.vue +62 -0
  57. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  58. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  59. package/src/components/ui/Form.vue +24 -0
  60. package/src/components/ui/Input.vue +56 -0
  61. package/src/components/ui/Link.vue +12 -0
  62. package/src/components/ui/LoadingModal.vue +32 -0
  63. package/src/components/ui/Markdown.vue +69 -0
  64. package/src/components/ui/Modal.vue +70 -0
  65. package/src/components/ui/ModalContext.vue +30 -0
  66. package/src/components/ui/ProgressBar.vue +50 -0
  67. package/src/components/ui/PromptModal.vue +35 -0
  68. package/src/components/ui/Select.vue +21 -0
  69. package/src/components/ui/SelectLabel.vue +10 -0
  70. package/src/components/ui/SelectOptions.vue +31 -0
  71. package/src/components/ui/SelectTrigger.vue +29 -0
  72. package/src/components/ui/SettingsModal.vue +51 -0
  73. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  74. package/src/components/ui/Toast.vue +42 -0
  75. package/src/components/ui/index.ts +27 -0
  76. package/src/components/utils.ts +106 -9
  77. package/src/directives/index.ts +11 -5
  78. package/src/directives/measure.ts +34 -6
  79. package/src/errors/Errors.state.ts +1 -1
  80. package/src/errors/Errors.ts +24 -32
  81. package/src/errors/JobCancelledError.ts +3 -0
  82. package/src/errors/index.ts +10 -6
  83. package/src/errors/utils.ts +35 -0
  84. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  85. package/src/forms/{Form.ts → FormController.ts} +85 -25
  86. package/src/forms/composition.ts +4 -4
  87. package/src/forms/index.ts +3 -1
  88. package/src/forms/utils.ts +36 -5
  89. package/src/forms/validation.ts +19 -0
  90. package/src/index.css +41 -0
  91. package/src/{main.ts → index.ts} +3 -0
  92. package/src/jobs/Job.ts +147 -0
  93. package/src/jobs/index.ts +10 -0
  94. package/src/jobs/listeners.ts +3 -0
  95. package/src/jobs/status.ts +4 -0
  96. package/src/lang/DefaultLangProvider.ts +46 -0
  97. package/src/lang/Lang.state.ts +11 -0
  98. package/src/lang/Lang.ts +43 -28
  99. package/src/lang/index.ts +8 -6
  100. package/src/plugins/Plugin.ts +1 -1
  101. package/src/plugins/index.ts +10 -7
  102. package/src/services/App.state.ts +26 -3
  103. package/src/services/App.ts +11 -3
  104. package/src/services/Cache.ts +43 -0
  105. package/src/services/Events.test.ts +39 -0
  106. package/src/services/Events.ts +111 -31
  107. package/src/services/Service.ts +129 -54
  108. package/src/services/Storage.ts +20 -0
  109. package/src/services/index.ts +13 -5
  110. package/src/services/utils.ts +18 -0
  111. package/src/testing/index.ts +26 -0
  112. package/src/testing/setup.ts +11 -0
  113. package/src/ui/UI.state.ts +19 -7
  114. package/src/ui/UI.ts +184 -76
  115. package/src/ui/index.ts +19 -18
  116. package/src/ui/utils.ts +16 -0
  117. package/src/utils/composition/events.ts +2 -2
  118. package/src/utils/composition/forms.ts +4 -3
  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 +24 -0
  123. package/src/utils/index.ts +2 -0
  124. package/src/utils/markdown.test.ts +50 -0
  125. package/src/utils/markdown.ts +19 -6
  126. package/src/utils/vdom.ts +31 -0
  127. package/src/utils/vue.ts +22 -19
  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/constants.ts +0 -8
  138. package/src/components/forms/AGButton.vue +0 -44
  139. package/src/components/forms/AGCheckbox.vue +0 -41
  140. package/src/components/forms/AGForm.vue +0 -26
  141. package/src/components/forms/AGInput.vue +0 -38
  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.vue +0 -51
  146. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  147. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  148. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  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/index.ts +0 -14
  157. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  158. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  159. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  160. package/src/components/headless/modals/index.ts +0 -4
  161. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  162. package/src/components/headless/snackbars/index.ts +0 -40
  163. package/src/components/lib/AGErrorMessage.vue +0 -16
  164. package/src/components/lib/AGLink.vue +0 -9
  165. package/src/components/lib/AGMarkdown.vue +0 -36
  166. package/src/components/lib/AGMeasured.vue +0 -15
  167. package/src/components/lib/index.ts +0 -5
  168. package/src/components/modals/AGAlertModal.ts +0 -15
  169. package/src/components/modals/AGAlertModal.vue +0 -14
  170. package/src/components/modals/AGConfirmModal.ts +0 -27
  171. package/src/components/modals/AGConfirmModal.vue +0 -26
  172. package/src/components/modals/AGErrorReportModal.ts +0 -46
  173. package/src/components/modals/AGErrorReportModal.vue +0 -54
  174. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  175. package/src/components/modals/AGLoadingModal.ts +0 -23
  176. package/src/components/modals/AGLoadingModal.vue +0 -15
  177. package/src/components/modals/AGModal.ts +0 -10
  178. package/src/components/modals/AGModal.vue +0 -39
  179. package/src/components/modals/AGModalContext.ts +0 -8
  180. package/src/components/modals/AGModalContext.vue +0 -22
  181. package/src/components/modals/AGModalTitle.vue +0 -9
  182. package/src/components/modals/AGPromptModal.ts +0 -30
  183. package/src/components/modals/AGPromptModal.vue +0 -34
  184. package/src/components/modals/index.ts +0 -17
  185. package/src/components/snackbars/AGSnackbar.vue +0 -36
  186. package/src/components/snackbars/index.ts +0 -3
  187. package/src/directives/initial-focus.ts +0 -11
  188. package/src/main.histoire.ts +0 -1
  189. package/tailwind.config.js +0 -4
  190. package/tsconfig.json +0 -11
  191. package/vite.config.ts +0 -14
@@ -0,0 +1,11 @@
1
+ import { FakeLocalStorage } from '@noeldemartin/testing';
2
+ import { beforeEach, vi } from 'vitest';
3
+
4
+ vi.mock('dompurify', async () => {
5
+ return { default: { sanitize: (html: string) => html } };
6
+ });
7
+
8
+ beforeEach(() => {
9
+ FakeLocalStorage.reset();
10
+ FakeLocalStorage.patchGlobal();
11
+ });
@@ -1,8 +1,10 @@
1
1
  import type { Component } from 'vue';
2
2
 
3
- import { defineServiceState } from '@/services/Service';
3
+ import { defineServiceState } from '@aerogel/core/services/Service';
4
4
 
5
- export interface Modal<T = unknown> {
5
+ import { Layouts, getCurrentLayout } from './utils';
6
+
7
+ export interface UIModal<T = unknown> {
6
8
  id: string;
7
9
  properties: Record<string, unknown>;
8
10
  component: Component;
@@ -10,14 +12,19 @@ export interface Modal<T = unknown> {
10
12
  afterClose: Promise<T | undefined>;
11
13
  }
12
14
 
15
+ export interface UIModalContext {
16
+ modal: UIModal;
17
+ childIndex?: number;
18
+ }
19
+
13
20
  export interface ModalComponent<
14
21
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
- Properties extends Record<string, unknown> = Record<string, unknown>,
22
+ Properties extends object = object,
16
23
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- Result = unknown
24
+ Result = unknown,
18
25
  > {}
19
26
 
20
- export interface Snackbar {
27
+ export interface UIToast {
21
28
  id: string;
22
29
  component: Component;
23
30
  properties: Record<string, unknown>;
@@ -26,7 +33,12 @@ export interface Snackbar {
26
33
  export default defineServiceState({
27
34
  name: 'ui',
28
35
  initialState: {
29
- modals: [] as Modal[],
30
- snackbars: [] as Snackbar[],
36
+ modals: [] as UIModal[],
37
+ toasts: [] as UIToast[],
38
+ layout: getCurrentLayout(),
39
+ },
40
+ computed: {
41
+ mobile: ({ layout }) => layout === Layouts.Mobile,
42
+ desktop: ({ layout }) => layout === Layouts.Desktop,
31
43
  },
32
44
  });
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,47 @@ 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',
31
37
  } as const;
32
38
 
33
39
  export type UIComponent = ObjectValues<typeof UIComponents>;
34
40
 
35
- export interface ConfirmOptions {
41
+ export type ConfirmOptions = AcceptRefs<{
36
42
  acceptText?: string;
43
+ acceptVariant?: ButtonVariant;
37
44
  cancelText?: string;
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;
38
59
  }
39
60
 
40
- export interface PromptOptions {
61
+ export type PromptOptions = AcceptRefs<{
41
62
  label?: string;
42
63
  defaultValue?: string;
43
64
  placeholder?: string;
44
65
  acceptText?: string;
66
+ acceptVariant?: ButtonVariant;
45
67
  cancelText?: string;
46
- }
68
+ cancelVariant?: ButtonVariant;
69
+ trim?: boolean;
70
+ }>;
47
71
 
48
- export interface ShowSnackbarOptions {
72
+ export interface ToastOptions {
49
73
  component?: Component;
50
- color?: SnackbarColor;
51
- actions?: SnackbarAction[];
74
+ variant?: ToastVariant;
75
+ actions?: ToastAction[];
52
76
  }
53
77
 
54
78
  export class UIService extends Service {
@@ -63,7 +87,7 @@ export class UIService extends Service {
63
87
  public alert(message: string): void;
64
88
  public alert(title: string, message: string): void;
65
89
  public alert(messageOrTitle: string, message?: string): void {
66
- const getProperties = (): AGAlertModalProps => {
90
+ const getProperties = (): AlertModalProps => {
67
91
  if (typeof message !== 'string') {
68
92
  return { message: messageOrTitle };
69
93
  }
@@ -74,38 +98,79 @@ export class UIService extends Service {
74
98
  };
75
99
  };
76
100
 
77
- this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
101
+ this.openModal<ModalComponent<AlertModalProps>>(
102
+ this.requireComponent(UIComponents.AlertModal),
103
+ getProperties(),
104
+ );
78
105
  }
79
106
 
107
+ /* eslint-disable max-len */
80
108
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
81
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
+
82
114
  public async confirm(
83
115
  messageOrTitle: string,
84
- messageOrOptions?: string | ConfirmOptions,
85
- options?: ConfirmOptions,
86
- ): Promise<boolean> {
87
- const getProperties = (): AGConfirmModalProps => {
116
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
117
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
118
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
119
+ const getProperties = (): AcceptRefs<ConfirmModalProps> => {
88
120
  if (typeof messageOrOptions !== 'string') {
89
121
  return {
90
- message: messageOrTitle,
91
122
  ...(messageOrOptions ?? {}),
123
+ message: messageOrTitle,
124
+ required: !!messageOrOptions?.required,
92
125
  };
93
126
  }
94
127
 
95
128
  return {
129
+ ...(options ?? {}),
96
130
  title: messageOrTitle,
97
131
  message: messageOrOptions,
98
- ...(options ?? {}),
132
+ required: !!options?.required,
99
133
  };
100
134
  };
101
135
 
102
- 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>(
103
143
  this.requireComponent(UIComponents.ConfirmModal),
104
- getProperties(),
144
+ properties,
105
145
  );
106
146
  const result = await modal.beforeClose;
107
147
 
108
- 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;
109
174
  }
110
175
 
111
176
  public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
@@ -115,47 +180,65 @@ export class UIService extends Service {
115
180
  messageOrOptions?: string | PromptOptions,
116
181
  options?: PromptOptions,
117
182
  ): Promise<string | null> {
118
- const getProperties = (): AGPromptModalProps => {
183
+ const trim = options?.trim ?? true;
184
+ const getProperties = (): PromptModalProps => {
119
185
  if (typeof messageOrOptions !== 'string') {
120
186
  return {
121
187
  message: messageOrTitle,
122
188
  ...(messageOrOptions ?? {}),
123
- };
189
+ } as PromptModalProps;
124
190
  }
125
191
 
126
192
  return {
127
193
  title: messageOrTitle,
128
194
  message: messageOrOptions,
129
195
  ...(options ?? {}),
130
- };
196
+ } as PromptModalProps;
131
197
  };
132
198
 
133
- const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
199
+ const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
134
200
  this.requireComponent(UIComponents.PromptModal),
135
201
  getProperties(),
136
202
  );
137
- const result = await modal.beforeClose;
203
+ const rawResult = await modal.beforeClose;
204
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
138
205
 
139
206
  return result ?? null;
140
207
  }
141
208
 
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 {};
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
+ };
223
+ }
224
+
225
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
226
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
148
227
  }
149
228
 
150
- return { message: messageOrOperation };
229
+ return {
230
+ props: operationOrMessageOrOptions,
231
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
232
+ };
151
233
  };
152
234
 
153
- 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);
154
237
 
155
238
  try {
156
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
239
+ const result = await operationPromise;
157
240
 
158
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
241
+ await after({ ms: 500 });
159
242
 
160
243
  return result;
161
244
  } finally {
@@ -163,23 +246,15 @@ export class UIService extends Service {
163
246
  }
164
247
  }
165
248
 
166
- public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
167
- const snackbar: Snackbar = {
249
+ public toast(message: string, options: ToastOptions = {}): void {
250
+ const { component, ...otherOptions } = options;
251
+ const toast: UIToast = {
168
252
  id: uuid(),
169
- properties: { message, ...options },
170
- component: options.component ?? markRaw(this.requireComponent(UIComponents.Snackbar)),
253
+ properties: { message, ...otherOptions },
254
+ component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
171
255
  };
172
256
 
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
- );
257
+ this.setState('toasts', this.toasts.concat(toast));
183
258
  }
184
259
 
185
260
  public registerComponent(name: UIComponent, component: Component): void {
@@ -189,10 +264,10 @@ export class UIService extends Service {
189
264
  public async openModal<TModalComponent extends ModalComponent>(
190
265
  component: TModalComponent,
191
266
  properties?: ModalProperties<TModalComponent>,
192
- ): Promise<Modal<ModalResult<TModalComponent>>> {
267
+ ): Promise<UIModal<ModalResult<TModalComponent>>> {
193
268
  const id = uuid();
194
269
  const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
195
- const modal: Modal<ModalResult<TModalComponent>> = {
270
+ const modal: UIModal<ModalResult<TModalComponent>> = {
196
271
  id,
197
272
  properties: properties ?? {},
198
273
  component: markRaw(component),
@@ -217,12 +292,40 @@ export class UIService extends Service {
217
292
  }
218
293
 
219
294
  public async closeModal(id: string, result?: unknown): Promise<void> {
295
+ if (!App.isMounted()) {
296
+ await this.removeModal(id, result);
297
+
298
+ return;
299
+ }
300
+
220
301
  await Events.emit('close-modal', { id, result });
221
302
  }
222
303
 
223
- protected async boot(): Promise<void> {
304
+ public async closeAllModals(): Promise<void> {
305
+ while (this.modals.length > 0) {
306
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
307
+ }
308
+ }
309
+
310
+ protected override async boot(): Promise<void> {
224
311
  this.watchModalEvents();
225
312
  this.watchMountedEvent();
313
+ this.watchViewportBreakpoints();
314
+ }
315
+
316
+ private async removeModal(id: string, result?: unknown): Promise<void> {
317
+ this.setState(
318
+ 'modals',
319
+ this.modals.filter((m) => m.id !== id),
320
+ );
321
+
322
+ this.modalCallbacks[id]?.closed?.(result);
323
+
324
+ delete this.modalCallbacks[id];
325
+
326
+ const activeModal = this.modals.at(-1);
327
+
328
+ await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
226
329
  }
227
330
 
228
331
  private watchModalEvents(): void {
@@ -234,31 +337,24 @@ export class UIService extends Service {
234
337
  }
235
338
  });
236
339
 
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 }));
340
+ Events.on('modal-closed', async ({ modal: { id }, result }) => {
341
+ await this.removeModal(id, result);
250
342
  });
251
343
  }
252
344
 
253
345
  private watchMountedEvent(): void {
254
346
  Events.once('application-mounted', async () => {
255
- const splash = document.getElementById('splash');
347
+ if (!globalThis.document || !globalThis.getComputedStyle) {
348
+ return;
349
+ }
350
+
351
+ const splash = globalThis.document.getElementById('splash');
256
352
 
257
353
  if (!splash) {
258
354
  return;
259
355
  }
260
356
 
261
- if (window.getComputedStyle(splash).opacity !== '0') {
357
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
262
358
  splash.style.opacity = '0';
263
359
 
264
360
  await after({ ms: 600 });
@@ -268,16 +364,28 @@ export class UIService extends Service {
268
364
  });
269
365
  }
270
366
 
367
+ private watchViewportBreakpoints(): void {
368
+ if (!globalThis.matchMedia) {
369
+ return;
370
+ }
371
+
372
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
373
+
374
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
375
+ }
376
+
271
377
  }
272
378
 
273
379
  export default facade(UIService);
274
380
 
275
- declare module '@/services/Events' {
381
+ declare module '@aerogel/core/services/Events' {
276
382
  export interface EventsPayload {
277
- 'modal-will-close': { modal: Modal; result?: unknown };
278
- 'modal-closed': { modal: Modal; result?: unknown };
279
383
  'close-modal': { id: string; result?: unknown };
280
384
  'hide-modal': { id: string };
385
+ 'hide-overlays-backdrop': void;
386
+ 'modal-closed': { modal: UIModal; result?: unknown };
387
+ 'modal-will-close': { modal: UIModal; result?: unknown };
281
388
  'show-modal': { id: string };
389
+ 'show-overlays-backdrop': void;
282
390
  }
283
391
  }
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
+ }
@@ -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
+ }