@aerogel/core 0.0.0-next.d34923f3b144e8f6720e6a9cdadb2cd4fb4ab289 → 0.0.0-next.d4f3ae130b52cace673d7c95681c19256ee1acb4
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.css +1 -0
- package/dist/aerogel-core.d.ts +2058 -1789
- package/dist/aerogel-core.js +3287 -0
- package/dist/aerogel-core.js.map +1 -0
- package/package.json +31 -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/AppLayout.vue +14 -0
- package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
- package/src/components/AppOverlays.vue +9 -0
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +19 -0
- package/src/components/contracts/Button.ts +16 -0
- package/src/components/contracts/ConfirmModal.ts +48 -0
- package/src/components/contracts/DropdownMenu.ts +20 -0
- package/src/components/contracts/ErrorReportModal.ts +32 -0
- package/src/components/contracts/Input.ts +26 -0
- package/src/components/contracts/LoadingModal.ts +26 -0
- package/src/components/contracts/Modal.ts +21 -0
- package/src/components/contracts/PromptModal.ts +34 -0
- package/src/components/contracts/Select.ts +45 -0
- package/src/components/contracts/Toast.ts +15 -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/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +7 -8
- package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
- package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +17 -25
- package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
- package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +11 -13
- package/src/components/headless/HeadlessModal.vue +57 -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 +120 -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 +42 -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 -10
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +17 -0
- package/src/components/ui/Button.vue +100 -0
- package/src/components/ui/Checkbox.vue +56 -0
- package/src/components/ui/ConfirmModal.vue +50 -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 +72 -0
- package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
- package/src/components/ui/ErrorReportModalTitle.vue +24 -0
- package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
- package/src/components/ui/Input.vue +56 -0
- package/src/components/ui/Link.vue +12 -0
- package/src/components/ui/LoadingModal.vue +34 -0
- package/src/components/ui/Markdown.vue +85 -0
- package/src/components/ui/Modal.vue +123 -0
- package/src/components/ui/ModalContext.vue +31 -0
- package/src/components/ui/ProgressBar.vue +51 -0
- package/src/components/ui/PromptModal.vue +38 -0
- package/src/components/ui/Select.vue +27 -0
- package/src/components/ui/SelectLabel.vue +17 -0
- package/src/components/ui/SelectOption.vue +29 -0
- package/src/components/ui/SelectOptions.vue +35 -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 +46 -0
- package/src/components/ui/index.ts +30 -0
- package/src/directives/index.ts +9 -5
- package/src/directives/measure.ts +33 -8
- package/src/errors/Errors.state.ts +1 -1
- package/src/errors/Errors.ts +14 -23
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/index.ts +9 -6
- package/src/errors/utils.ts +17 -1
- package/src/forms/FormController.test.ts +110 -0
- package/src/forms/{Form.ts → FormController.ts} +64 -42
- package/src/forms/index.ts +3 -3
- package/src/forms/utils.ts +40 -24
- package/src/forms/validation.ts +19 -0
- package/src/index.css +73 -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 +40 -17
- 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 +23 -4
- package/src/services/App.ts +16 -3
- package/src/services/Cache.ts +1 -1
- package/src/services/Events.test.ts +8 -8
- package/src/services/Events.ts +16 -12
- package/src/services/Service.ts +125 -54
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +14 -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 +14 -12
- package/src/ui/UI.ts +232 -126
- package/src/ui/index.ts +28 -28
- package/src/ui/utils.ts +16 -0
- package/src/utils/classes.ts +41 -0
- package/src/utils/composition/events.ts +4 -6
- package/src/utils/composition/forms.ts +20 -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 +38 -132
- 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/AGAppLayout.vue +0 -16
- package/src/components/AGAppOverlays.vue +0 -41
- 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/AGInput.vue +0 -40
- 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 -56
- package/src/components/headless/forms/AGHeadlessInput.ts +0 -33
- package/src/components/headless/forms/AGHeadlessInput.vue +0 -64
- 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/composition.ts +0 -10
- package/src/components/headless/forms/index.ts +0 -17
- 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/interfaces.ts +0 -9
- package/src/components/lib/AGErrorMessage.vue +0 -16
- package/src/components/lib/AGLink.vue +0 -9
- package/src/components/lib/AGMarkdown.vue +0 -41
- 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 -33
- 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 -36
- 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/forms/Form.test.ts +0 -58
- package/src/forms/composition.ts +0 -6
- 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/{main.ts → index.ts} +0 -0
package/src/ui/UI.ts
CHANGED
|
@@ -1,75 +1,114 @@
|
|
|
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
|
+
import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
|
|
3
4
|
import type { Component } from 'vue';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import type {
|
|
9
|
-
|
|
5
|
+
import type { ClosureArgs } from '@noeldemartin/utils';
|
|
6
|
+
|
|
7
|
+
import App from '@aerogel/core/services/App';
|
|
8
|
+
import Events from '@aerogel/core/services/Events';
|
|
9
|
+
import type {
|
|
10
|
+
ConfirmModalCheckboxes,
|
|
11
|
+
ConfirmModalExpose,
|
|
12
|
+
ConfirmModalProps,
|
|
13
|
+
} from '@aerogel/core/components/contracts/ConfirmModal';
|
|
14
|
+
import type {
|
|
15
|
+
ErrorReportModalExpose,
|
|
16
|
+
ErrorReportModalProps,
|
|
17
|
+
} from '@aerogel/core/components/contracts/ErrorReportModal';
|
|
18
|
+
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
19
|
+
import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
20
|
+
import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
|
|
21
|
+
import type { LoadingModalExpose, LoadingModalProps } from '@aerogel/core/components/contracts/LoadingModal';
|
|
22
|
+
import type { PromptModalExpose, PromptModalProps } from '@aerogel/core/components/contracts/PromptModal';
|
|
23
|
+
import type { ToastAction, ToastExpose, ToastProps, ToastVariant } from '@aerogel/core/components/contracts/Toast';
|
|
10
24
|
|
|
11
25
|
import Service from './UI.state';
|
|
12
|
-
import
|
|
26
|
+
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
27
|
+
import type { UIModal, UIToast } from './UI.state';
|
|
13
28
|
|
|
14
29
|
interface ModalCallbacks<T = unknown> {
|
|
15
30
|
willClose(result: T | undefined): void;
|
|
16
|
-
|
|
31
|
+
hasClosed(result: T | undefined): void;
|
|
17
32
|
}
|
|
18
33
|
|
|
19
|
-
type
|
|
20
|
-
type
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
34
|
+
export type ModalResult<T> = ModalExposeResult<ComponentExposed<T>>;
|
|
35
|
+
export type ModalExposeResult<T> = T extends { close(result?: infer Result): Promise<void> } ? Result : unknown;
|
|
36
|
+
export type UIComponent<Props = {}, Exposed = {}> = { new (...args: ClosureArgs): Exposed & { $props: Props } };
|
|
37
|
+
|
|
38
|
+
export interface UIComponents {
|
|
39
|
+
'alert-modal': UIComponent<AlertModalProps, AlertModalExpose>;
|
|
40
|
+
'confirm-modal': UIComponent<ConfirmModalProps, ConfirmModalExpose>;
|
|
41
|
+
'error-report-modal': UIComponent<ErrorReportModalProps, ErrorReportModalExpose>;
|
|
42
|
+
'loading-modal': UIComponent<LoadingModalProps, LoadingModalExpose>;
|
|
43
|
+
'prompt-modal': UIComponent<PromptModalProps, PromptModalExpose>;
|
|
44
|
+
'router-link': UIComponent;
|
|
45
|
+
'startup-crash': UIComponent;
|
|
46
|
+
toast: UIComponent<ToastProps, ToastExpose>;
|
|
47
|
+
}
|
|
33
48
|
|
|
34
|
-
export
|
|
49
|
+
export interface UIModalContext {
|
|
50
|
+
modal: UIModal;
|
|
51
|
+
childIndex?: number;
|
|
52
|
+
}
|
|
35
53
|
|
|
36
|
-
export
|
|
54
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
37
55
|
acceptText?: string;
|
|
38
|
-
|
|
56
|
+
acceptVariant?: ButtonVariant;
|
|
39
57
|
cancelText?: string;
|
|
40
|
-
|
|
58
|
+
cancelVariant?: ButtonVariant;
|
|
59
|
+
actions?: Record<string, () => unknown>;
|
|
60
|
+
required?: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
|
|
63
|
+
export type LoadingOptions = AcceptRefs<{
|
|
64
|
+
title?: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
progress?: number;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
70
|
+
extends ConfirmOptions {
|
|
71
|
+
checkboxes?: T;
|
|
41
72
|
}
|
|
42
73
|
|
|
43
|
-
export
|
|
74
|
+
export type PromptOptions = AcceptRefs<{
|
|
44
75
|
label?: string;
|
|
45
76
|
defaultValue?: string;
|
|
46
77
|
placeholder?: string;
|
|
47
78
|
acceptText?: string;
|
|
48
|
-
|
|
79
|
+
acceptVariant?: ButtonVariant;
|
|
49
80
|
cancelText?: string;
|
|
50
|
-
|
|
81
|
+
cancelVariant?: ButtonVariant;
|
|
51
82
|
trim?: boolean;
|
|
52
|
-
}
|
|
83
|
+
}>;
|
|
53
84
|
|
|
54
|
-
export interface
|
|
85
|
+
export interface ToastOptions {
|
|
55
86
|
component?: Component;
|
|
56
|
-
|
|
57
|
-
actions?:
|
|
87
|
+
variant?: ToastVariant;
|
|
88
|
+
actions?: ToastAction[];
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
export class UIService extends Service {
|
|
61
92
|
|
|
62
93
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
63
|
-
private components: Partial<
|
|
94
|
+
private components: Partial<UIComponents> = {};
|
|
64
95
|
|
|
65
|
-
public
|
|
66
|
-
|
|
96
|
+
public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
|
|
97
|
+
this.components[name] = component;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public resolveComponent<T extends keyof UIComponents>(name: T): UIComponents[T] | null {
|
|
101
|
+
return this.components[name] ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
|
|
105
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
67
106
|
}
|
|
68
107
|
|
|
69
108
|
public alert(message: string): void;
|
|
70
109
|
public alert(title: string, message: string): void;
|
|
71
110
|
public alert(messageOrTitle: string, message?: string): void {
|
|
72
|
-
const getProperties = ():
|
|
111
|
+
const getProperties = (): AlertModalProps => {
|
|
73
112
|
if (typeof message !== 'string') {
|
|
74
113
|
return { message: messageOrTitle };
|
|
75
114
|
}
|
|
@@ -80,38 +119,66 @@ export class UIService extends Service {
|
|
|
80
119
|
};
|
|
81
120
|
};
|
|
82
121
|
|
|
83
|
-
this.
|
|
122
|
+
this.modal(this.requireComponent('alert-modal'), getProperties());
|
|
84
123
|
}
|
|
85
124
|
|
|
125
|
+
/* eslint-disable max-len */
|
|
86
126
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
87
127
|
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
128
|
+
public async confirm<T extends ConfirmModalCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
129
|
+
public async confirm<T extends ConfirmModalCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
130
|
+
/* eslint-enable max-len */
|
|
131
|
+
|
|
88
132
|
public async confirm(
|
|
89
133
|
messageOrTitle: string,
|
|
90
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
91
|
-
options?: ConfirmOptions,
|
|
92
|
-
): Promise<boolean> {
|
|
93
|
-
const getProperties = ():
|
|
134
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
135
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
136
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
137
|
+
const getProperties = (): AcceptRefs<ConfirmModalProps> => {
|
|
94
138
|
if (typeof messageOrOptions !== 'string') {
|
|
95
139
|
return {
|
|
96
|
-
message: messageOrTitle,
|
|
97
140
|
...(messageOrOptions ?? {}),
|
|
141
|
+
message: messageOrTitle,
|
|
142
|
+
required: !!messageOrOptions?.required,
|
|
98
143
|
};
|
|
99
144
|
}
|
|
100
145
|
|
|
101
146
|
return {
|
|
147
|
+
...(options ?? {}),
|
|
102
148
|
title: messageOrTitle,
|
|
103
149
|
message: messageOrOptions,
|
|
104
|
-
|
|
150
|
+
required: !!options?.required,
|
|
105
151
|
};
|
|
106
152
|
};
|
|
107
153
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
154
|
+
const properties = getProperties();
|
|
155
|
+
const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
|
|
156
|
+
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
157
|
+
const checkboxes =
|
|
158
|
+
typeof result === 'object'
|
|
159
|
+
? result[1]
|
|
160
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
161
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
162
|
+
[checkbox]: defaultValue ?? false,
|
|
163
|
+
...values,
|
|
164
|
+
}),
|
|
165
|
+
{} as Record<string, boolean>,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
169
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (confirmed && isDevelopment()) {
|
|
174
|
+
// eslint-disable-next-line no-console
|
|
175
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [false, checkboxes];
|
|
179
|
+
}
|
|
113
180
|
|
|
114
|
-
return
|
|
181
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
115
182
|
}
|
|
116
183
|
|
|
117
184
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -122,48 +189,60 @@ export class UIService extends Service {
|
|
|
122
189
|
options?: PromptOptions,
|
|
123
190
|
): Promise<string | null> {
|
|
124
191
|
const trim = options?.trim ?? true;
|
|
125
|
-
const getProperties = ():
|
|
192
|
+
const getProperties = (): PromptModalProps => {
|
|
126
193
|
if (typeof messageOrOptions !== 'string') {
|
|
127
194
|
return {
|
|
128
195
|
message: messageOrTitle,
|
|
129
196
|
...(messageOrOptions ?? {}),
|
|
130
|
-
};
|
|
197
|
+
} as PromptModalProps;
|
|
131
198
|
}
|
|
132
199
|
|
|
133
200
|
return {
|
|
134
201
|
title: messageOrTitle,
|
|
135
202
|
message: messageOrOptions,
|
|
136
203
|
...(options ?? {}),
|
|
137
|
-
};
|
|
204
|
+
} as PromptModalProps;
|
|
138
205
|
};
|
|
139
206
|
|
|
140
|
-
const
|
|
141
|
-
this.requireComponent(UIComponents.PromptModal),
|
|
142
|
-
getProperties(),
|
|
143
|
-
);
|
|
144
|
-
const rawResult = await modal.beforeClose;
|
|
207
|
+
const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
|
|
145
208
|
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
146
209
|
|
|
147
210
|
return result ?? null;
|
|
148
211
|
}
|
|
149
212
|
|
|
150
|
-
public async loading<T>(operation: Promise<T>): Promise<T>;
|
|
151
|
-
public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
|
|
152
|
-
public async loading<T>(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
213
|
+
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
214
|
+
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
215
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
216
|
+
public async loading<T>(
|
|
217
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
218
|
+
operation?: Promise<T> | (() => T),
|
|
219
|
+
): Promise<T> {
|
|
220
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
221
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
|
|
222
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
223
|
+
return {
|
|
224
|
+
props: { message: operationOrMessageOrOptions },
|
|
225
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
230
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
156
231
|
}
|
|
157
232
|
|
|
158
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
props: operationOrMessageOrOptions,
|
|
235
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
236
|
+
};
|
|
159
237
|
};
|
|
160
238
|
|
|
161
|
-
const
|
|
239
|
+
const { operationPromise, props } = processArgs();
|
|
240
|
+
const modal = await this.modal(this.requireComponent('loading-modal'), props);
|
|
162
241
|
|
|
163
242
|
try {
|
|
164
|
-
|
|
243
|
+
const result = await operationPromise;
|
|
165
244
|
|
|
166
|
-
|
|
245
|
+
await after({ ms: 500 });
|
|
167
246
|
|
|
168
247
|
return result;
|
|
169
248
|
} finally {
|
|
@@ -171,43 +250,34 @@ export class UIService extends Service {
|
|
|
171
250
|
}
|
|
172
251
|
}
|
|
173
252
|
|
|
174
|
-
public
|
|
175
|
-
const
|
|
253
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
254
|
+
const { component, ...otherOptions } = options;
|
|
255
|
+
const toast: UIToast = {
|
|
176
256
|
id: uuid(),
|
|
177
|
-
properties: { message, ...
|
|
178
|
-
component: markRaw(
|
|
257
|
+
properties: { message, ...otherOptions },
|
|
258
|
+
component: markRaw(component ?? this.requireComponent('toast')),
|
|
179
259
|
};
|
|
180
260
|
|
|
181
|
-
this.setState('
|
|
182
|
-
|
|
183
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
261
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
184
262
|
}
|
|
185
263
|
|
|
186
|
-
public
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
public registerComponent(name: UIComponent, component: Component): void {
|
|
194
|
-
this.components[name] = component;
|
|
195
|
-
}
|
|
264
|
+
public modal<T extends Component>(
|
|
265
|
+
...args: {} extends ComponentProps<T>
|
|
266
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
267
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
268
|
+
): Promise<UIModal<ModalResult<T>>>;
|
|
196
269
|
|
|
197
|
-
public async
|
|
198
|
-
component: TModalComponent,
|
|
199
|
-
properties?: ModalProperties<TModalComponent>,
|
|
200
|
-
): Promise<Modal<ModalResult<TModalComponent>>> {
|
|
270
|
+
public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
|
|
201
271
|
const id = uuid();
|
|
202
|
-
const callbacks: Partial<ModalCallbacks<ModalResult<
|
|
203
|
-
const modal:
|
|
272
|
+
const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
|
|
273
|
+
const modal: UIModal<ModalResult<T>> = {
|
|
204
274
|
id,
|
|
205
|
-
|
|
275
|
+
closing: false,
|
|
276
|
+
properties: props ?? {},
|
|
206
277
|
component: markRaw(component),
|
|
207
278
|
beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
|
|
208
|
-
afterClose: new Promise((resolve) => (callbacks.
|
|
279
|
+
afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
|
|
209
280
|
};
|
|
210
|
-
const activeModal = this.modals.at(-1);
|
|
211
281
|
const modals = this.modals.concat(modal);
|
|
212
282
|
|
|
213
283
|
this.modalCallbacks[modal.id] = callbacks;
|
|
@@ -215,58 +285,88 @@ export class UIService extends Service {
|
|
|
215
285
|
this.setState({ modals });
|
|
216
286
|
|
|
217
287
|
await nextTick();
|
|
218
|
-
await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
|
|
219
|
-
await Promise.all([
|
|
220
|
-
activeModal || Events.emit('show-overlays-backdrop'),
|
|
221
|
-
Events.emit('show-modal', { id: modal.id }),
|
|
222
|
-
]);
|
|
223
288
|
|
|
224
289
|
return modal;
|
|
225
290
|
}
|
|
226
291
|
|
|
292
|
+
public modalForm<T extends Component>(
|
|
293
|
+
...args: {} extends ComponentProps<T>
|
|
294
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
295
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
296
|
+
): Promise<ModalResult<T> | undefined>;
|
|
297
|
+
|
|
298
|
+
public async modalForm<T extends Component>(
|
|
299
|
+
component: T,
|
|
300
|
+
props?: ComponentProps<T>,
|
|
301
|
+
): Promise<ModalResult<T> | undefined> {
|
|
302
|
+
const modal = await this.modal<T>(component, props as ComponentProps<T>);
|
|
303
|
+
const result = await modal.beforeClose;
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
227
308
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
309
|
+
if (!App.isMounted()) {
|
|
310
|
+
await this.removeModal(id, result);
|
|
311
|
+
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
228
315
|
await Events.emit('close-modal', { id, result });
|
|
229
316
|
}
|
|
230
317
|
|
|
231
|
-
|
|
318
|
+
public async closeAllModals(): Promise<void> {
|
|
319
|
+
while (this.modals.length > 0) {
|
|
320
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
protected override async boot(): Promise<void> {
|
|
232
325
|
this.watchModalEvents();
|
|
233
326
|
this.watchMountedEvent();
|
|
327
|
+
this.watchViewportBreakpoints();
|
|
234
328
|
}
|
|
235
329
|
|
|
236
|
-
private
|
|
237
|
-
|
|
238
|
-
|
|
330
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
331
|
+
this.setState(
|
|
332
|
+
'modals',
|
|
333
|
+
this.modals.filter((m) => m.id !== id),
|
|
334
|
+
);
|
|
239
335
|
|
|
240
|
-
|
|
241
|
-
Events.emit('hide-overlays-backdrop');
|
|
242
|
-
}
|
|
243
|
-
});
|
|
336
|
+
this.modalCallbacks[id]?.hasClosed?.(result);
|
|
244
337
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'modals',
|
|
248
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
249
|
-
);
|
|
338
|
+
delete this.modalCallbacks[id];
|
|
339
|
+
}
|
|
250
340
|
|
|
251
|
-
|
|
341
|
+
private watchModalEvents(): void {
|
|
342
|
+
Events.on('modal-will-close', ({ modal: { id }, result }) => {
|
|
343
|
+
const modal = this.modals.find((_modal) => id === _modal.id);
|
|
252
344
|
|
|
253
|
-
|
|
345
|
+
if (modal) {
|
|
346
|
+
modal.closing = true;
|
|
347
|
+
}
|
|
254
348
|
|
|
255
|
-
|
|
349
|
+
this.modalCallbacks[id]?.willClose?.(result);
|
|
350
|
+
});
|
|
256
351
|
|
|
257
|
-
|
|
352
|
+
Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
|
|
353
|
+
await this.removeModal(id, result);
|
|
258
354
|
});
|
|
259
355
|
}
|
|
260
356
|
|
|
261
357
|
private watchMountedEvent(): void {
|
|
262
358
|
Events.once('application-mounted', async () => {
|
|
263
|
-
|
|
359
|
+
if (!globalThis.document || !globalThis.getComputedStyle) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const splash = globalThis.document.getElementById('splash');
|
|
264
364
|
|
|
265
365
|
if (!splash) {
|
|
266
366
|
return;
|
|
267
367
|
}
|
|
268
368
|
|
|
269
|
-
if (
|
|
369
|
+
if (globalThis.getComputedStyle(splash).opacity !== '0') {
|
|
270
370
|
splash.style.opacity = '0';
|
|
271
371
|
|
|
272
372
|
await after({ ms: 600 });
|
|
@@ -276,18 +376,24 @@ export class UIService extends Service {
|
|
|
276
376
|
});
|
|
277
377
|
}
|
|
278
378
|
|
|
379
|
+
private watchViewportBreakpoints(): void {
|
|
380
|
+
if (!globalThis.matchMedia) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
|
|
385
|
+
|
|
386
|
+
media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
|
|
387
|
+
}
|
|
388
|
+
|
|
279
389
|
}
|
|
280
390
|
|
|
281
391
|
export default facade(UIService);
|
|
282
392
|
|
|
283
|
-
declare module '
|
|
393
|
+
declare module '@aerogel/core/services/Events' {
|
|
284
394
|
export interface EventsPayload {
|
|
285
395
|
'close-modal': { id: string; result?: unknown };
|
|
286
|
-
'
|
|
287
|
-
'
|
|
288
|
-
'modal-closed': { modal: Modal; result?: unknown };
|
|
289
|
-
'modal-will-close': { modal: Modal; result?: unknown };
|
|
290
|
-
'show-modal': { id: string };
|
|
291
|
-
'show-overlays-backdrop': void;
|
|
396
|
+
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
397
|
+
'modal-has-closed': { modal: UIModal; result?: unknown };
|
|
292
398
|
}
|
|
293
399
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
+
import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
|
|
2
|
+
import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
|
|
3
|
+
import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
|
|
4
|
+
import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
|
|
5
|
+
import PromptModal from '@aerogel/core/components/ui/PromptModal.vue';
|
|
6
|
+
import StartupCrash from '@aerogel/core/components/ui/StartupCrash.vue';
|
|
7
|
+
import Toast from '@aerogel/core/components/ui/Toast.vue';
|
|
8
|
+
import { bootServices } from '@aerogel/core/services';
|
|
9
|
+
import { definePlugin } from '@aerogel/core/plugins';
|
|
10
|
+
|
|
11
|
+
import UI from './UI';
|
|
12
|
+
import type { UIComponents } from './UI';
|
|
1
13
|
import type { Component } from 'vue';
|
|
2
14
|
|
|
3
|
-
import { bootServices } from '@/services';
|
|
4
|
-
import { definePlugin } from '@/plugins';
|
|
5
|
-
|
|
6
|
-
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
|
-
import type { UIComponent } from './UI';
|
|
15
|
-
|
|
16
15
|
const services = { $ui: UI };
|
|
17
16
|
|
|
18
17
|
export * from './UI';
|
|
18
|
+
export * from './utils';
|
|
19
19
|
export { default as UI } from './UI';
|
|
20
20
|
|
|
21
21
|
export type UIServices = typeof services;
|
|
22
22
|
|
|
23
23
|
export default definePlugin({
|
|
24
24
|
async install(app, options) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
const components: Partial<Record<keyof UIComponents, Component>> = {
|
|
26
|
+
'alert-modal': AlertModal,
|
|
27
|
+
'confirm-modal': ConfirmModal,
|
|
28
|
+
'error-report-modal': ErrorReportModal,
|
|
29
|
+
'loading-modal': LoadingModal,
|
|
30
|
+
'prompt-modal': PromptModal,
|
|
31
|
+
'startup-crash': StartupCrash,
|
|
32
|
+
'toast': Toast,
|
|
33
|
+
...options.components,
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
Object.entries({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}).forEach(([name, component]) => UI.registerComponent(name as UIComponent, component));
|
|
36
|
+
for (const [name, component] of Object.entries(components)) {
|
|
37
|
+
UI.registerComponent(name as keyof UIComponents, component as UIComponents[keyof UIComponents]);
|
|
38
|
+
}
|
|
39
39
|
|
|
40
40
|
await bootServices(app, services);
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
declare module '
|
|
44
|
+
declare module '@aerogel/core/bootstrap/options' {
|
|
45
45
|
export interface AerogelOptions {
|
|
46
|
-
components?: Partial<
|
|
46
|
+
components?: Partial<Partial<UIComponents>>;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
declare module '
|
|
50
|
+
declare module '@aerogel/core/services' {
|
|
51
51
|
export interface Services extends UIServices {}
|
|
52
52
|
}
|
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,41 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { 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 { PropType } 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 Variants<T extends Record<string, string | boolean>> = Required<{
|
|
12
|
+
[K in keyof T]: Exclude<T[K], undefined> extends string
|
|
13
|
+
? { [key in Exclude<T[K], undefined>]: string | null }
|
|
14
|
+
: { true: string | null; false: string | null };
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
export type ComponentPropDefinitions<T> = {
|
|
18
|
+
[K in keyof T]: {
|
|
19
|
+
type?: PropType<T[K]>;
|
|
20
|
+
default: T[K] | (() => T[K]) | null;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type PickComponentProps<TValues, TDefinitions> = {
|
|
25
|
+
[K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function variantClasses<T>(
|
|
29
|
+
value: { baseClasses?: string } & CVAProps<T>,
|
|
30
|
+
config: { baseClasses?: string } & CVAConfig<T>,
|
|
31
|
+
): string {
|
|
32
|
+
const { baseClasses: valueBaseClasses, ...values } = value;
|
|
33
|
+
const { baseClasses: configBaseClasses, ...configs } = config;
|
|
34
|
+
const variants = cva(configBaseClasses, configs as CVAConfig<T>);
|
|
35
|
+
|
|
36
|
+
return classes(variants(values as CVAProps<T>), unref(valueBaseClasses));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function classes(...inputs: ClassValue[]): string {
|
|
40
|
+
return twMerge(clsx(inputs));
|
|
41
|
+
}
|