@aerogel/core 0.0.0-next.9a02fcd3bcf698211dd7a71d4c48257c96dd7832 → 0.0.0-next.9e0c0bbdcff5db68a1087ef53cbdc0f53299f6bb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aerogel-core.d.ts +2529 -1597
- package/dist/aerogel-core.js +3265 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +30 -37
- package/src/bootstrap/bootstrap.test.ts +4 -8
- package/src/bootstrap/index.ts +25 -16
- package/src/bootstrap/options.ts +1 -1
- package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -10
- 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/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- 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 +113 -0
- package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
- 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} +34 -27
- 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/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
- package/src/components/ui/Toast.vue +42 -0
- package/src/components/ui/index.ts +30 -0
- package/src/directives/index.ts +11 -5
- package/src/directives/measure.ts +34 -6
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +17 -18
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +9 -6
- package/src/errors/utils.ts +17 -1
- package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
- package/src/forms/{Form.ts → FormController.ts} +85 -25
- 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} +1 -0
- package/src/jobs/Job.ts +144 -2
- package/src/jobs/index.ts +4 -1
- 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 +48 -21
- 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 +1 -1
- package/src/plugins/index.ts +10 -7
- package/src/services/App.state.ts +36 -3
- package/src/services/App.ts +19 -3
- package/src/services/Cache.ts +43 -0
- package/src/services/Events.ts +15 -5
- package/src/services/Service.ts +125 -54
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +16 -5
- package/src/services/utils.ts +18 -0
- package/src/testing/index.ts +4 -3
- package/src/testing/setup.ts +11 -0
- package/src/ui/UI.state.ts +19 -7
- package/src/ui/UI.ts +188 -77
- package/src/ui/index.ts +19 -18
- package/src/ui/utils.ts +16 -0
- package/src/utils/classes.ts +49 -0
- package/src/utils/composition/events.ts +2 -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 -1
- 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 -127
- 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/histoire.config.ts +0 -7
- package/noeldemartin.config.js +0 -5
- package/postcss.config.js +0 -6
- package/src/assets/histoire.css +0 -3
- package/src/components/AGAppSnackbars.vue +0 -13
- package/src/components/constants.ts +0 -8
- package/src/components/forms/AGButton.vue +0 -44
- package/src/components/forms/AGCheckbox.vue +0 -41
- package/src/components/forms/AGForm.vue +0 -26
- package/src/components/forms/AGInput.vue +0 -38
- package/src/components/forms/AGSelect.story.vue +0 -46
- package/src/components/forms/AGSelect.vue +0 -60
- package/src/components/forms/index.ts +0 -5
- package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
- package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -47
- package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
- package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
- package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
- package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
- package/src/components/headless/forms/index.ts +0 -14
- package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
- package/src/components/headless/modals/AGHeadlessModal.vue +0 -86
- 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 -4
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
- package/src/components/headless/snackbars/index.ts +0 -40
- package/src/components/lib/AGErrorMessage.vue +0 -16
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/lib/AGMarkdown.vue +0 -36
- package/src/components/lib/AGMeasured.vue +0 -15
- package/src/components/lib/index.ts +0 -5
- package/src/components/modals/AGAlertModal.ts +0 -15
- package/src/components/modals/AGAlertModal.vue +0 -14
- package/src/components/modals/AGConfirmModal.ts +0 -27
- package/src/components/modals/AGConfirmModal.vue +0 -26
- package/src/components/modals/AGErrorReportModal.ts +0 -46
- package/src/components/modals/AGErrorReportModal.vue +0 -54
- package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
- package/src/components/modals/AGLoadingModal.ts +0 -23
- package/src/components/modals/AGLoadingModal.vue +0 -15
- package/src/components/modals/AGModal.ts +0 -10
- package/src/components/modals/AGModal.vue +0 -39
- package/src/components/modals/AGModalContext.ts +0 -8
- package/src/components/modals/AGModalContext.vue +0 -22
- package/src/components/modals/AGModalTitle.vue +0 -9
- package/src/components/modals/AGPromptModal.ts +0 -30
- package/src/components/modals/AGPromptModal.vue +0 -34
- package/src/components/modals/index.ts +0 -17
- package/src/components/snackbars/AGSnackbar.vue +0 -36
- package/src/components/snackbars/index.ts +0 -3
- package/src/components/utils.ts +0 -10
- package/src/directives/initial-focus.ts +0 -11
- package/src/main.histoire.ts +0 -1
- package/src/utils/tailwindcss.test.ts +0 -26
- package/src/utils/tailwindcss.ts +0 -7
- package/tailwind.config.js +0 -4
- package/tsconfig.json +0 -11
- package/vite.config.ts +0 -14
package/src/ui/UI.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { after, facade, fail, uuid } from '@noeldemartin/utils';
|
|
1
|
+
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
3
|
import type { Component } from 'vue';
|
|
4
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import type {
|
|
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
|
|
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> =
|
|
20
|
-
? TResult
|
|
21
|
-
: never;
|
|
26
|
+
type ModalResult<TComponent> =
|
|
27
|
+
TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
|
|
22
28
|
|
|
23
29
|
export const UIComponents = {
|
|
24
30
|
AlertModal: 'alert-modal',
|
|
@@ -26,29 +32,48 @@ export const UIComponents = {
|
|
|
26
32
|
ErrorReportModal: 'error-report-modal',
|
|
27
33
|
LoadingModal: 'loading-modal',
|
|
28
34
|
PromptModal: 'prompt-modal',
|
|
29
|
-
|
|
35
|
+
Toast: 'toast',
|
|
30
36
|
StartupCrash: 'startup-crash',
|
|
37
|
+
RouterLink: 'router-link',
|
|
31
38
|
} as const;
|
|
32
39
|
|
|
33
40
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
34
41
|
|
|
35
|
-
export
|
|
42
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
36
43
|
acceptText?: string;
|
|
44
|
+
acceptVariant?: ButtonVariant;
|
|
37
45
|
cancelText?: string;
|
|
46
|
+
cancelVariant?: ButtonVariant;
|
|
47
|
+
actions?: Record<string, () => unknown>;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
export type LoadingOptions = AcceptRefs<{
|
|
52
|
+
title?: string;
|
|
53
|
+
message?: string;
|
|
54
|
+
progress?: number;
|
|
55
|
+
}>;
|
|
56
|
+
|
|
57
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
58
|
+
extends ConfirmOptions {
|
|
59
|
+
checkboxes?: T;
|
|
38
60
|
}
|
|
39
61
|
|
|
40
|
-
export
|
|
62
|
+
export type PromptOptions = AcceptRefs<{
|
|
41
63
|
label?: string;
|
|
42
64
|
defaultValue?: string;
|
|
43
65
|
placeholder?: string;
|
|
44
66
|
acceptText?: string;
|
|
67
|
+
acceptVariant?: ButtonVariant;
|
|
45
68
|
cancelText?: string;
|
|
46
|
-
|
|
69
|
+
cancelVariant?: ButtonVariant;
|
|
70
|
+
trim?: boolean;
|
|
71
|
+
}>;
|
|
47
72
|
|
|
48
|
-
export interface
|
|
73
|
+
export interface ToastOptions {
|
|
49
74
|
component?: Component;
|
|
50
|
-
|
|
51
|
-
actions?:
|
|
75
|
+
variant?: ToastVariant;
|
|
76
|
+
actions?: ToastAction[];
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
export class UIService extends Service {
|
|
@@ -56,14 +81,18 @@ export class UIService extends Service {
|
|
|
56
81
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
57
82
|
private components: Partial<Record<UIComponent, Component>> = {};
|
|
58
83
|
|
|
84
|
+
public resolveComponent(name: UIComponent): Component | null {
|
|
85
|
+
return this.components[name] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
public requireComponent(name: UIComponent): Component {
|
|
60
|
-
return this.
|
|
89
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
61
90
|
}
|
|
62
91
|
|
|
63
92
|
public alert(message: string): void;
|
|
64
93
|
public alert(title: string, message: string): void;
|
|
65
94
|
public alert(messageOrTitle: string, message?: string): void {
|
|
66
|
-
const getProperties = ():
|
|
95
|
+
const getProperties = (): AlertModalProps => {
|
|
67
96
|
if (typeof message !== 'string') {
|
|
68
97
|
return { message: messageOrTitle };
|
|
69
98
|
}
|
|
@@ -74,38 +103,79 @@ export class UIService extends Service {
|
|
|
74
103
|
};
|
|
75
104
|
};
|
|
76
105
|
|
|
77
|
-
this.openModal(
|
|
106
|
+
this.openModal<ModalComponent<AlertModalProps>>(
|
|
107
|
+
this.requireComponent(UIComponents.AlertModal),
|
|
108
|
+
getProperties(),
|
|
109
|
+
);
|
|
78
110
|
}
|
|
79
111
|
|
|
112
|
+
/* eslint-disable max-len */
|
|
80
113
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
81
114
|
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
115
|
+
public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
116
|
+
public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
117
|
+
/* eslint-enable max-len */
|
|
118
|
+
|
|
82
119
|
public async confirm(
|
|
83
120
|
messageOrTitle: string,
|
|
84
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
85
|
-
options?: ConfirmOptions,
|
|
86
|
-
): Promise<boolean> {
|
|
87
|
-
const getProperties = ():
|
|
121
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
122
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
123
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
124
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
88
125
|
if (typeof messageOrOptions !== 'string') {
|
|
89
126
|
return {
|
|
90
|
-
message: messageOrTitle,
|
|
91
127
|
...(messageOrOptions ?? {}),
|
|
128
|
+
message: messageOrTitle,
|
|
129
|
+
required: !!messageOrOptions?.required,
|
|
92
130
|
};
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
return {
|
|
134
|
+
...(options ?? {}),
|
|
96
135
|
title: messageOrTitle,
|
|
97
136
|
message: messageOrOptions,
|
|
98
|
-
|
|
137
|
+
required: !!options?.required,
|
|
99
138
|
};
|
|
100
139
|
};
|
|
101
140
|
|
|
102
|
-
|
|
141
|
+
type ConfirmModalComponent = ModalComponent<
|
|
142
|
+
AcceptRefs<ConfirmModalProps>,
|
|
143
|
+
boolean | [boolean, Record<string, boolean>]
|
|
144
|
+
>;
|
|
145
|
+
|
|
146
|
+
const properties = getProperties();
|
|
147
|
+
const modal = await this.openModal<ConfirmModalComponent>(
|
|
103
148
|
this.requireComponent(UIComponents.ConfirmModal),
|
|
104
|
-
|
|
149
|
+
properties,
|
|
105
150
|
);
|
|
106
151
|
const result = await modal.beforeClose;
|
|
107
152
|
|
|
108
|
-
|
|
153
|
+
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
154
|
+
const checkboxes =
|
|
155
|
+
typeof result === 'object'
|
|
156
|
+
? result[1]
|
|
157
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
158
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
159
|
+
[checkbox]: defaultValue ?? false,
|
|
160
|
+
...values,
|
|
161
|
+
}),
|
|
162
|
+
{} as Record<string, boolean>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
166
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (confirmed && isDevelopment()) {
|
|
171
|
+
// eslint-disable-next-line no-console
|
|
172
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [false, checkboxes];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
109
179
|
}
|
|
110
180
|
|
|
111
181
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -115,47 +185,65 @@ export class UIService extends Service {
|
|
|
115
185
|
messageOrOptions?: string | PromptOptions,
|
|
116
186
|
options?: PromptOptions,
|
|
117
187
|
): Promise<string | null> {
|
|
118
|
-
const
|
|
188
|
+
const trim = options?.trim ?? true;
|
|
189
|
+
const getProperties = (): PromptModalProps => {
|
|
119
190
|
if (typeof messageOrOptions !== 'string') {
|
|
120
191
|
return {
|
|
121
192
|
message: messageOrTitle,
|
|
122
193
|
...(messageOrOptions ?? {}),
|
|
123
|
-
};
|
|
194
|
+
} as PromptModalProps;
|
|
124
195
|
}
|
|
125
196
|
|
|
126
197
|
return {
|
|
127
198
|
title: messageOrTitle,
|
|
128
199
|
message: messageOrOptions,
|
|
129
200
|
...(options ?? {}),
|
|
130
|
-
};
|
|
201
|
+
} as PromptModalProps;
|
|
131
202
|
};
|
|
132
203
|
|
|
133
|
-
const modal = await this.openModal<ModalComponent<
|
|
204
|
+
const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
|
|
134
205
|
this.requireComponent(UIComponents.PromptModal),
|
|
135
206
|
getProperties(),
|
|
136
207
|
);
|
|
137
|
-
const
|
|
208
|
+
const rawResult = await modal.beforeClose;
|
|
209
|
+
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
138
210
|
|
|
139
211
|
return result ?? null;
|
|
140
212
|
}
|
|
141
213
|
|
|
142
|
-
public async loading<T>(operation: Promise<T>): Promise<T>;
|
|
143
|
-
public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
|
|
144
|
-
public async loading<T>(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
214
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
215
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
216
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
217
|
+
public async loading<T>(
|
|
218
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
219
|
+
operation?: Promise<T> | (() => T),
|
|
220
|
+
): Promise<T> {
|
|
221
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
222
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
|
|
223
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
224
|
+
return {
|
|
225
|
+
props: { message: operationOrMessageOrOptions },
|
|
226
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
227
|
+
};
|
|
148
228
|
}
|
|
149
229
|
|
|
150
|
-
|
|
230
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
231
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
props: operationOrMessageOrOptions,
|
|
236
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
237
|
+
};
|
|
151
238
|
};
|
|
152
239
|
|
|
153
|
-
const
|
|
240
|
+
const { operationPromise, props } = processArgs();
|
|
241
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
154
242
|
|
|
155
243
|
try {
|
|
156
|
-
|
|
244
|
+
const result = await operationPromise;
|
|
157
245
|
|
|
158
|
-
|
|
246
|
+
await after({ ms: 500 });
|
|
159
247
|
|
|
160
248
|
return result;
|
|
161
249
|
} finally {
|
|
@@ -163,23 +251,15 @@ export class UIService extends Service {
|
|
|
163
251
|
}
|
|
164
252
|
}
|
|
165
253
|
|
|
166
|
-
public
|
|
167
|
-
const
|
|
254
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
255
|
+
const { component, ...otherOptions } = options;
|
|
256
|
+
const toast: UIToast = {
|
|
168
257
|
id: uuid(),
|
|
169
|
-
properties: { message, ...
|
|
170
|
-
component: markRaw(
|
|
258
|
+
properties: { message, ...otherOptions },
|
|
259
|
+
component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
|
|
171
260
|
};
|
|
172
261
|
|
|
173
|
-
this.setState('
|
|
174
|
-
|
|
175
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
public hideSnackbar(id: string): void {
|
|
179
|
-
this.setState(
|
|
180
|
-
'snackbars',
|
|
181
|
-
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
182
|
-
);
|
|
262
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
183
263
|
}
|
|
184
264
|
|
|
185
265
|
public registerComponent(name: UIComponent, component: Component): void {
|
|
@@ -189,10 +269,10 @@ export class UIService extends Service {
|
|
|
189
269
|
public async openModal<TModalComponent extends ModalComponent>(
|
|
190
270
|
component: TModalComponent,
|
|
191
271
|
properties?: ModalProperties<TModalComponent>,
|
|
192
|
-
): Promise<
|
|
272
|
+
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
193
273
|
const id = uuid();
|
|
194
274
|
const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
|
|
195
|
-
const modal:
|
|
275
|
+
const modal: UIModal<ModalResult<TModalComponent>> = {
|
|
196
276
|
id,
|
|
197
277
|
properties: properties ?? {},
|
|
198
278
|
component: markRaw(component),
|
|
@@ -217,12 +297,40 @@ export class UIService extends Service {
|
|
|
217
297
|
}
|
|
218
298
|
|
|
219
299
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
300
|
+
if (!App.isMounted()) {
|
|
301
|
+
await this.removeModal(id, result);
|
|
302
|
+
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
220
306
|
await Events.emit('close-modal', { id, result });
|
|
221
307
|
}
|
|
222
308
|
|
|
223
|
-
|
|
309
|
+
public async closeAllModals(): Promise<void> {
|
|
310
|
+
while (this.modals.length > 0) {
|
|
311
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
protected override async boot(): Promise<void> {
|
|
224
316
|
this.watchModalEvents();
|
|
225
317
|
this.watchMountedEvent();
|
|
318
|
+
this.watchViewportBreakpoints();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
322
|
+
this.setState(
|
|
323
|
+
'modals',
|
|
324
|
+
this.modals.filter((m) => m.id !== id),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
this.modalCallbacks[id]?.closed?.(result);
|
|
328
|
+
|
|
329
|
+
delete this.modalCallbacks[id];
|
|
330
|
+
|
|
331
|
+
const activeModal = this.modals.at(-1);
|
|
332
|
+
|
|
333
|
+
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
226
334
|
}
|
|
227
335
|
|
|
228
336
|
private watchModalEvents(): void {
|
|
@@ -234,31 +342,24 @@ export class UIService extends Service {
|
|
|
234
342
|
}
|
|
235
343
|
});
|
|
236
344
|
|
|
237
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
238
|
-
this.
|
|
239
|
-
'modals',
|
|
240
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
244
|
-
|
|
245
|
-
delete this.modalCallbacks[modal.id];
|
|
246
|
-
|
|
247
|
-
const activeModal = this.modals.at(-1);
|
|
248
|
-
|
|
249
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
345
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
346
|
+
await this.removeModal(id, result);
|
|
250
347
|
});
|
|
251
348
|
}
|
|
252
349
|
|
|
253
350
|
private watchMountedEvent(): void {
|
|
254
351
|
Events.once('application-mounted', async () => {
|
|
255
|
-
|
|
352
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const splash = globalThis.document.getElementById('splash');
|
|
256
357
|
|
|
257
358
|
if (!splash) {
|
|
258
359
|
return;
|
|
259
360
|
}
|
|
260
361
|
|
|
261
|
-
if (
|
|
362
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
262
363
|
splash.style.opacity = '0';
|
|
263
364
|
|
|
264
365
|
await after({ ms: 600 });
|
|
@@ -268,17 +369,27 @@ export class UIService extends Service {
|
|
|
268
369
|
});
|
|
269
370
|
}
|
|
270
371
|
|
|
372
|
+
private watchViewportBreakpoints(): void {
|
|
373
|
+
if (!globalThis.matchMedia) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
378
|
+
|
|
379
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
380
|
+
}
|
|
381
|
+
|
|
271
382
|
}
|
|
272
383
|
|
|
273
384
|
export default facade(UIService);
|
|
274
385
|
|
|
275
|
-
declare module '
|
|
386
|
+
declare module '@aerogel/core/services/Events' {
|
|
276
387
|
export interface EventsPayload {
|
|
277
388
|
'close-modal': { id: string; result?: unknown };
|
|
278
389
|
'hide-modal': { id: string };
|
|
279
390
|
'hide-overlays-backdrop': void;
|
|
280
|
-
'modal-closed': { modal:
|
|
281
|
-
'modal-will-close': { modal:
|
|
391
|
+
'modal-closed': { modal: UIModal; result?: unknown };
|
|
392
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
282
393
|
'show-modal': { id: string };
|
|
283
394
|
'show-overlays-backdrop': void;
|
|
284
395
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import type { Component } from 'vue';
|
|
2
2
|
|
|
3
|
-
import
|
|
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 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]:
|
|
27
|
-
[UIComponents.ConfirmModal]:
|
|
28
|
-
[UIComponents.ErrorReportModal]:
|
|
29
|
-
[UIComponents.LoadingModal]:
|
|
30
|
-
[UIComponents.PromptModal]:
|
|
31
|
-
[UIComponents.
|
|
32
|
-
[UIComponents.StartupCrash]:
|
|
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 '
|
|
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 '
|
|
51
|
+
declare module '@aerogel/core/services' {
|
|
51
52
|
export interface Services extends UIServices {}
|
|
52
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,13 +1,13 @@
|
|
|
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>(
|
|
@@ -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
|
+
}
|