@aerogel/core 0.0.0-next.f8cdd39997c56dcd46e07c26af8a84d04d610fce → 0.0.0-next.fcfbfdc3428c34c4d1c0e781b61d244f13232fc9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aerogel-core.d.ts +2645 -884
- package/dist/aerogel-core.js +3266 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +36 -34
- package/src/bootstrap/bootstrap.test.ts +7 -10
- package/src/bootstrap/index.ts +43 -14
- package/src/bootstrap/options.ts +4 -1
- package/src/components/AppLayout.vue +16 -0
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -6
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +4 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +41 -0
- package/src/components/contracts/DropdownMenu.ts +20 -0
- package/src/components/contracts/ErrorReportModal.ts +29 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +22 -0
- package/src/components/contracts/Modal.ts +21 -0
- package/src/components/contracts/PromptModal.ts +30 -0
- package/src/components/contracts/Select.ts +44 -0
- package/src/components/contracts/Toast.ts +13 -0
- package/src/components/contracts/index.ts +11 -0
- package/src/components/headless/HeadlessButton.vue +51 -0
- package/src/components/headless/HeadlessInput.vue +59 -0
- package/src/components/headless/HeadlessInputDescription.vue +27 -0
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/HeadlessInputInput.vue +75 -0
- package/src/components/headless/HeadlessInputLabel.vue +18 -0
- package/src/components/headless/HeadlessInputTextArea.vue +40 -0
- package/src/components/headless/HeadlessModal.vue +92 -0
- package/src/components/headless/HeadlessModalContent.vue +30 -0
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessModalOverlay.vue +12 -0
- package/src/components/headless/HeadlessModalTitle.vue +12 -0
- package/src/components/headless/HeadlessSelect.vue +118 -0
- package/src/components/headless/HeadlessSelectError.vue +25 -0
- package/src/components/headless/HeadlessSelectLabel.vue +25 -0
- package/src/components/headless/HeadlessSelectOption.vue +34 -0
- package/src/components/headless/HeadlessSelectOptions.vue +37 -0
- package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
- package/src/components/headless/HeadlessSelectValue.vue +18 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +19 -3
- package/src/components/index.ts +6 -9
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +13 -0
- package/src/components/ui/Button.vue +98 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +42 -0
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +14 -0
- package/src/components/ui/DropdownMenuOptions.vue +27 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorMessage.vue +15 -0
- package/src/components/ui/ErrorReportModal.vue +62 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +38 -29
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/ui/Form.vue +24 -0
- package/src/components/ui/Input.vue +56 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +32 -0
- package/src/components/ui/Markdown.vue +69 -0
- package/src/components/ui/Modal.vue +91 -0
- package/src/components/ui/ModalContext.vue +30 -0
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +35 -0
- package/src/components/ui/Select.vue +25 -0
- package/src/components/ui/SelectLabel.vue +17 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +30 -0
- package/src/components/ui/SelectTrigger.vue +29 -0
- package/src/components/ui/SettingsModal.vue +15 -0
- package/src/components/ui/StartupCrash.vue +31 -0
- package/src/components/ui/Toast.vue +42 -0
- package/src/components/ui/index.ts +30 -0
- package/src/directives/index.ts +13 -5
- package/src/directives/measure.ts +40 -0
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +35 -34
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +19 -29
- package/src/errors/utils.ts +35 -0
- package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
- package/src/forms/FormController.ts +245 -0
- package/src/forms/composition.ts +4 -4
- package/src/forms/index.ts +3 -1
- package/src/forms/utils.ts +36 -5
- package/src/forms/validation.ts +19 -0
- package/src/index.css +54 -0
- package/src/{main.ts → index.ts} +3 -0
- package/src/jobs/Job.ts +147 -0
- package/src/jobs/index.ts +10 -0
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/lang/DefaultLangProvider.ts +46 -0
- package/src/lang/Lang.state.ts +11 -0
- package/src/lang/Lang.ts +44 -29
- package/src/lang/index.ts +12 -6
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/plugins/Plugin.ts +2 -1
- package/src/plugins/index.ts +22 -0
- package/src/services/App.state.ts +40 -6
- package/src/services/App.ts +53 -5
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.test.ts +39 -0
- package/src/services/Events.ts +112 -32
- package/src/services/Service.ts +154 -49
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +18 -6
- package/src/services/store.ts +8 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +19 -7
- package/src/ui/UI.ts +274 -58
- package/src/ui/index.ts +23 -17
- package/src/ui/utils.ts +16 -0
- package/src/utils/classes.ts +49 -0
- package/src/utils/composition/events.ts +3 -2
- package/src/utils/composition/forms.ts +14 -4
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +33 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +19 -6
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +28 -118
- package/.eslintrc.js +0 -3
- package/dist/aerogel-core.cjs.js +0 -2
- package/dist/aerogel-core.cjs.js.map +0 -1
- package/dist/aerogel-core.esm.js +0 -2
- package/dist/aerogel-core.esm.js.map +0 -1
- package/dist/virtual.d.ts +0 -11
- package/noeldemartin.config.js +0 -5
- package/src/components/AGAppLayout.vue +0 -11
- package/src/components/AGAppSnackbars.vue +0 -13
- package/src/components/basic/AGErrorMessage.vue +0 -16
- package/src/components/basic/AGLink.vue +0 -9
- package/src/components/basic/AGMarkdown.vue +0 -36
- package/src/components/basic/index.ts +0 -5
- package/src/components/constants.ts +0 -8
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -35
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -36
- package/src/components/forms/index.ts +0 -6
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -8
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -54
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
- package/src/components/headless/forms/AGHeadlessInputLabel.vue +0 -16
- package/src/components/headless/forms/index.ts +0 -6
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -7
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -88
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
- package/src/components/headless/modals/index.ts +0 -6
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
- package/src/components/headless/snackbars/index.ts +0 -25
- package/src/components/modals/AGAlertModal.vue +0 -25
- package/src/components/modals/AGConfirmModal.vue +0 -30
- package/src/components/modals/AGErrorReportModal.ts +0 -20
- package/src/components/modals/AGErrorReportModal.vue +0 -62
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.vue +0 -19
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -36
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/index.ts +0 -21
- package/src/components/snackbars/AGSnackbar.vue +0 -42
- package/src/components/snackbars/index.ts +0 -3
- package/src/directives/initial-focus.ts +0 -11
- package/src/forms/Form.ts +0 -176
- package/src/types/virtual.d.ts +0 -11
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -14
package/src/ui/UI.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import { 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
|
|
7
|
-
import
|
|
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';
|
|
8
15
|
|
|
9
16
|
import Service from './UI.state';
|
|
10
|
-
import
|
|
17
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
18
|
+
import type { ModalComponent, UIModal, UIToast } from './UI.state';
|
|
11
19
|
|
|
12
20
|
interface ModalCallbacks<T = unknown> {
|
|
13
21
|
willClose(result: T | undefined): void;
|
|
@@ -15,24 +23,57 @@ interface ModalCallbacks<T = unknown> {
|
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
|
|
18
|
-
type ModalResult<TComponent> =
|
|
19
|
-
? TResult
|
|
20
|
-
: never;
|
|
26
|
+
type ModalResult<TComponent> =
|
|
27
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
21
28
|
|
|
22
29
|
export const UIComponents = {
|
|
23
30
|
AlertModal: 'alert-modal',
|
|
24
31
|
ConfirmModal: 'confirm-modal',
|
|
25
32
|
ErrorReportModal: 'error-report-modal',
|
|
26
33
|
LoadingModal: 'loading-modal',
|
|
27
|
-
|
|
34
|
+
PromptModal: 'prompt-modal',
|
|
35
|
+
Toast: 'toast',
|
|
36
|
+
StartupCrash: 'startup-crash',
|
|
37
|
+
RouterLink: 'router-link',
|
|
28
38
|
} as const;
|
|
29
39
|
|
|
30
40
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
31
41
|
|
|
32
|
-
export
|
|
42
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
43
|
+
acceptText?: string;
|
|
44
|
+
acceptVariant?: ButtonVariant;
|
|
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;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type PromptOptions = AcceptRefs<{
|
|
63
|
+
label?: string;
|
|
64
|
+
defaultValue?: string;
|
|
65
|
+
placeholder?: string;
|
|
66
|
+
acceptText?: string;
|
|
67
|
+
acceptVariant?: ButtonVariant;
|
|
68
|
+
cancelText?: string;
|
|
69
|
+
cancelVariant?: ButtonVariant;
|
|
70
|
+
trim?: boolean;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
export interface ToastOptions {
|
|
33
74
|
component?: Component;
|
|
34
|
-
|
|
35
|
-
actions?:
|
|
75
|
+
variant?: ToastVariant;
|
|
76
|
+
actions?: ToastAction[];
|
|
36
77
|
}
|
|
37
78
|
|
|
38
79
|
export class UIService extends Service {
|
|
@@ -40,62 +81,185 @@ export class UIService extends Service {
|
|
|
40
81
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
41
82
|
private components: Partial<Record<UIComponent, Component>> = {};
|
|
42
83
|
|
|
84
|
+
public resolveComponent(name: UIComponent): Component | null {
|
|
85
|
+
return this.components[name] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
43
88
|
public requireComponent(name: UIComponent): Component {
|
|
44
|
-
return this.
|
|
89
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
45
90
|
}
|
|
46
91
|
|
|
47
92
|
public alert(message: string): void;
|
|
48
93
|
public alert(title: string, message: string): void;
|
|
49
94
|
public alert(messageOrTitle: string, message?: string): void {
|
|
50
|
-
const
|
|
95
|
+
const getProperties = (): AlertModalProps => {
|
|
96
|
+
if (typeof message !== 'string') {
|
|
97
|
+
return { message: messageOrTitle };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
title: messageOrTitle,
|
|
102
|
+
message,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
51
105
|
|
|
52
|
-
this.openModal(
|
|
106
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
107
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
108
|
+
getProperties(),
|
|
109
|
+
);
|
|
53
110
|
}
|
|
54
111
|
|
|
55
|
-
|
|
56
|
-
public async confirm(
|
|
57
|
-
public async confirm(
|
|
58
|
-
|
|
59
|
-
|
|
112
|
+
/* eslint-disable max-len */
|
|
113
|
+
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
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
|
+
|
|
119
|
+
public async confirm(
|
|
120
|
+
messageOrTitle: string,
|
|
121
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
122
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
123
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
124
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
125
|
+
if (typeof messageOrOptions !== 'string') {
|
|
126
|
+
return {
|
|
127
|
+
...(messageOrOptions ?? {}),
|
|
128
|
+
message: messageOrTitle,
|
|
129
|
+
required: !!messageOrOptions?.required,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...(options ?? {}),
|
|
135
|
+
title: messageOrTitle,
|
|
136
|
+
message: messageOrOptions,
|
|
137
|
+
required: !!options?.required,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
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>(
|
|
60
148
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
61
|
-
|
|
149
|
+
properties,
|
|
62
150
|
);
|
|
63
151
|
const result = await modal.beforeClose;
|
|
64
152
|
|
|
65
|
-
|
|
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;
|
|
66
179
|
}
|
|
67
180
|
|
|
68
|
-
public async
|
|
69
|
-
public async
|
|
70
|
-
public async
|
|
71
|
-
|
|
181
|
+
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
182
|
+
public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
|
|
183
|
+
public async prompt(
|
|
184
|
+
messageOrTitle: string,
|
|
185
|
+
messageOrOptions?: string | PromptOptions,
|
|
186
|
+
options?: PromptOptions,
|
|
187
|
+
): Promise<string | null> {
|
|
188
|
+
const trim = options?.trim ?? true;
|
|
189
|
+
const getProperties = (): PromptModalProps => {
|
|
190
|
+
if (typeof messageOrOptions !== 'string') {
|
|
191
|
+
return {
|
|
192
|
+
message: messageOrTitle,
|
|
193
|
+
...(messageOrOptions ?? {}),
|
|
194
|
+
} as PromptModalProps;
|
|
195
|
+
}
|
|
72
196
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
197
|
+
return {
|
|
198
|
+
title: messageOrTitle,
|
|
199
|
+
message: messageOrOptions,
|
|
200
|
+
...(options ?? {}),
|
|
201
|
+
} as PromptModalProps;
|
|
202
|
+
};
|
|
76
203
|
|
|
77
|
-
await this.
|
|
204
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
205
|
+
this.requireComponent(UIComponents.PromptModal),
|
|
206
|
+
getProperties(),
|
|
207
|
+
);
|
|
208
|
+
const rawResult = await modal.beforeClose;
|
|
209
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
78
210
|
|
|
79
|
-
return result;
|
|
211
|
+
return result ?? null;
|
|
80
212
|
}
|
|
81
213
|
|
|
82
|
-
public
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
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
|
+
};
|
|
87
238
|
};
|
|
88
239
|
|
|
89
|
-
|
|
240
|
+
const { operationPromise, props } = processArgs();
|
|
241
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
90
242
|
|
|
91
|
-
|
|
243
|
+
try {
|
|
244
|
+
const result = await operationPromise;
|
|
245
|
+
|
|
246
|
+
await after({ ms: 500 });
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
} finally {
|
|
250
|
+
await this.closeModal(modal.id);
|
|
251
|
+
}
|
|
92
252
|
}
|
|
93
253
|
|
|
94
|
-
public
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
254
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
255
|
+
const { component, ...otherOptions } = options;
|
|
256
|
+
const toast: UIToast = {
|
|
257
|
+
id: uuid(),
|
|
258
|
+
properties: { message, ...otherOptions },
|
|
259
|
+
component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
99
263
|
}
|
|
100
264
|
|
|
101
265
|
public registerComponent(name: UIComponent, component: Component): void {
|
|
@@ -105,10 +269,10 @@ export class UIService extends Service {
|
|
|
105
269
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
106
270
|
component: TModalComponent,
|
|
107
271
|
properties?: ModalProperties<TModalComponent>,
|
|
108
|
-
): Promise<
|
|
272
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
109
273
|
const id = uuid();
|
|
110
274
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
111
|
-
const modal:
|
|
275
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
112
276
|
id,
|
|
113
277
|
properties: properties ?? {},
|
|
114
278
|
component: markRaw(component),
|
|
@@ -133,11 +297,40 @@ export class UIService extends Service {
|
|
|
133
297
|
}
|
|
134
298
|
|
|
135
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
|
+
|
|
136
306
|
await Events.emit('close-modal', { id, result });
|
|
137
307
|
}
|
|
138
308
|
|
|
139
|
-
|
|
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> {
|
|
140
316
|
this.watchModalEvents();
|
|
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 }));
|
|
141
334
|
}
|
|
142
335
|
|
|
143
336
|
private watchModalEvents(): void {
|
|
@@ -149,32 +342,55 @@ export class UIService extends Service {
|
|
|
149
342
|
}
|
|
150
343
|
});
|
|
151
344
|
|
|
152
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
153
|
-
this.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
345
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
346
|
+
await this.removeModal(id, result);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private watchMountedEvent(): void {
|
|
351
|
+
Events.once('application-mounted', async () => {
|
|
352
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const splash = globalThis.document.getElementById('splash');
|
|
157
357
|
|
|
158
|
-
|
|
358
|
+
if (!splash) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
159
361
|
|
|
160
|
-
|
|
362
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
363
|
+
splash.style.opacity = '0';
|
|
161
364
|
|
|
162
|
-
|
|
365
|
+
await after({ ms: 600 });
|
|
366
|
+
}
|
|
163
367
|
|
|
164
|
-
|
|
368
|
+
splash.remove();
|
|
165
369
|
});
|
|
166
370
|
}
|
|
167
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
|
+
|
|
168
382
|
}
|
|
169
383
|
|
|
170
|
-
export default facade(
|
|
384
|
+
export default facade(UIService);
|
|
171
385
|
|
|
172
|
-
declare module '
|
|
386
|
+
declare module '@aerogel/core/services/Events' {
|
|
173
387
|
export interface EventsPayload {
|
|
174
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
175
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
176
388
|
'close-modal': { id: string; result?: unknown };
|
|
177
389
|
'hide-modal': { id: string };
|
|
390
|
+
'hide-overlays-backdrop': void;
|
|
391
|
+
'modal-closed': { modal: UIModal; result?: unknown };
|
|
392
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
178
393
|
'show-modal': { id: string };
|
|
394
|
+
'show-overlays-backdrop': void;
|
|
179
395
|
}
|
|
180
396
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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 AGSnackbar from '../components/snackbars/AGSnackbar.vue';
|
|
12
14
|
import type { UIComponent } from './UI';
|
|
13
15
|
|
|
14
|
-
export { UI, UIComponents, UIComponent };
|
|
15
|
-
|
|
16
16
|
const services = { $ui: UI };
|
|
17
17
|
|
|
18
|
+
export * from './UI';
|
|
19
|
+
export * from './utils';
|
|
20
|
+
export { default as UI } from './UI';
|
|
21
|
+
|
|
18
22
|
export type UIServices = typeof services;
|
|
19
23
|
|
|
20
24
|
export default definePlugin({
|
|
21
25
|
async install(app, options) {
|
|
22
26
|
const defaultComponents = {
|
|
23
|
-
[UIComponents.AlertModal]:
|
|
24
|
-
[UIComponents.ConfirmModal]:
|
|
25
|
-
[UIComponents.ErrorReportModal]:
|
|
26
|
-
[UIComponents.LoadingModal]:
|
|
27
|
-
[UIComponents.
|
|
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,
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
Object.entries({
|
|
@@ -36,12 +42,12 @@ export default definePlugin({
|
|
|
36
42
|
},
|
|
37
43
|
});
|
|
38
44
|
|
|
39
|
-
declare module '
|
|
40
|
-
interface AerogelOptions {
|
|
45
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
46
|
+
export interface AerogelOptions {
|
|
41
47
|
components?: Partial<Record<UIComponent, Component>>;
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
declare module '
|
|
51
|
+
declare module '@aerogel/core/services' {
|
|
46
52
|
export interface Services extends UIServices {}
|
|
47
53
|
}
|
package/src/ui/utils.ts
ADDED
|
@@ -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,19 +1,20 @@
|
|
|
1
1
|
import { onUnmounted } from 'vue';
|
|
2
2
|
|
|
3
|
-
import Events from '
|
|
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 '
|
|
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>(
|
|
14
14
|
event: Event,
|
|
15
15
|
listener: EventListener<EventsPayload[Event]>
|
|
16
16
|
): void;
|
|
17
|
+
export function useEvent<Payload>(event: string, listener: (payload: Payload) => unknown): void;
|
|
17
18
|
export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
|
|
18
19
|
|
|
19
20
|
export function useEvent(event: string, listener: EventListener): void {
|
|
@@ -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
|
|
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
|
|
17
|
+
const classes = computed(() => attrs.class);
|
|
8
18
|
const inputAttrs = computed(() => objectWithout(attrs, 'class'));
|
|
9
19
|
|
|
10
|
-
return [inputAttrs,
|
|
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
|
+
}
|