@aerogel/core 0.0.0-next.c2e6acc000e97a1020c2e232678563c53884dd0e → 0.0.0-next.c3236837f7f8fc319a4a56022accb32757b3db89
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 +1267 -599
- package/dist/aerogel-core.js +2560 -1859
- package/dist/aerogel-core.js.map +1 -1
- package/package.json +7 -2
- package/src/components/AppLayout.vue +1 -3
- package/src/components/AppModals.vue +1 -1
- package/src/components/AppOverlays.vue +2 -34
- package/src/components/AppToasts.vue +16 -0
- package/src/components/contracts/AlertModal.ts +15 -0
- package/src/components/contracts/Button.ts +1 -0
- package/src/components/contracts/ConfirmModal.ts +12 -5
- package/src/components/contracts/DropdownMenu.ts +25 -0
- package/src/components/contracts/ErrorReportModal.ts +8 -4
- package/src/components/contracts/Input.ts +9 -9
- package/src/components/contracts/LoadingModal.ts +12 -4
- package/src/components/contracts/Modal.ts +14 -2
- package/src/components/contracts/PromptModal.ts +8 -2
- package/src/components/contracts/Select.ts +45 -0
- package/src/components/contracts/Toast.ts +15 -0
- package/src/components/contracts/index.ts +5 -1
- package/src/components/headless/HeadlessButton.vue +9 -3
- package/src/components/headless/HeadlessInput.vue +3 -3
- package/src/components/headless/HeadlessInputDescription.vue +1 -1
- package/src/components/headless/HeadlessInputInput.vue +21 -10
- package/src/components/headless/HeadlessInputTextArea.vue +6 -6
- package/src/components/headless/HeadlessModal.vue +23 -50
- package/src/components/headless/HeadlessModalContent.vue +11 -5
- package/src/components/headless/HeadlessModalDescription.vue +12 -0
- package/src/components/headless/HeadlessModalOverlay.vue +2 -2
- package/src/components/headless/HeadlessModalTitle.vue +2 -2
- package/src/components/headless/HeadlessSelect.vue +120 -0
- package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +3 -4
- 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/HeadlessSwitch.vue +96 -0
- package/src/components/headless/HeadlessToast.vue +18 -0
- package/src/components/headless/HeadlessToastAction.vue +13 -0
- package/src/components/headless/index.ts +9 -3
- package/src/components/index.ts +4 -9
- package/src/components/ui/AdvancedOptions.vue +18 -0
- package/src/components/ui/AlertModal.vue +7 -3
- package/src/components/ui/Button.vue +74 -17
- package/src/components/ui/Checkbox.vue +21 -14
- package/src/components/ui/ConfirmModal.vue +14 -6
- package/src/components/ui/DropdownMenu.vue +32 -0
- package/src/components/ui/DropdownMenuOption.vue +22 -0
- package/src/components/ui/DropdownMenuOptions.vue +44 -0
- package/src/components/ui/EditableContent.vue +82 -0
- package/src/components/ui/ErrorLogs.vue +19 -0
- package/src/components/ui/ErrorLogsModal.vue +48 -0
- package/src/components/{lib/AGErrorMessage.vue → ui/ErrorMessage.vue} +2 -3
- package/src/components/ui/ErrorReportModal.vue +18 -7
- package/src/components/ui/ErrorReportModalButtons.vue +6 -8
- package/src/components/ui/ErrorReportModalTitle.vue +1 -1
- package/src/components/ui/Input.vue +11 -7
- package/src/components/ui/Link.vue +2 -2
- package/src/components/ui/LoadingModal.vue +8 -6
- package/src/components/ui/Markdown.vue +41 -6
- package/src/components/ui/Modal.vue +95 -19
- package/src/components/ui/ModalContext.vue +2 -1
- package/src/components/ui/ProgressBar.vue +9 -8
- package/src/components/ui/PromptModal.vue +11 -8
- package/src/components/ui/Select.vue +27 -0
- package/src/components/ui/SelectLabel.vue +21 -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/Setting.vue +31 -0
- package/src/components/ui/SettingsModal.vue +15 -0
- package/src/components/ui/Switch.vue +11 -0
- package/src/components/ui/TextArea.vue +56 -0
- package/src/components/ui/Toast.vue +46 -0
- package/src/components/ui/index.ts +19 -0
- package/src/directives/measure.ts +11 -5
- package/src/errors/Errors.ts +21 -19
- package/src/errors/index.ts +6 -2
- package/src/errors/settings/Debug.vue +32 -0
- package/src/errors/settings/index.ts +10 -0
- package/src/forms/FormController.test.ts +32 -9
- package/src/forms/FormController.ts +27 -22
- package/src/forms/index.ts +0 -1
- package/src/forms/utils.ts +34 -34
- package/src/index.css +70 -3
- package/src/lang/index.ts +5 -1
- package/src/lang/settings/Language.vue +48 -0
- package/src/lang/settings/index.ts +10 -0
- package/src/services/App.state.ts +11 -1
- package/src/services/App.ts +9 -1
- package/src/services/Events.test.ts +8 -8
- package/src/services/Events.ts +2 -8
- package/src/services/index.ts +5 -2
- package/src/testing/index.ts +4 -0
- package/src/ui/UI.state.ts +5 -15
- package/src/ui/UI.ts +115 -99
- package/src/ui/index.ts +18 -19
- package/src/utils/classes.ts +41 -0
- package/src/utils/composition/events.ts +2 -4
- package/src/utils/composition/forms.ts +16 -1
- package/src/utils/composition/state.ts +11 -2
- package/src/utils/index.ts +3 -1
- package/src/utils/markdown.ts +35 -1
- package/src/utils/types.ts +3 -0
- package/src/utils/vue.ts +28 -129
- package/src/components/AppSnackbars.vue +0 -13
- package/src/components/composition.ts +0 -23
- package/src/components/constants.ts +0 -8
- package/src/components/contracts/shared.ts +0 -9
- package/src/components/forms/AGSelect.story.vue +0 -46
- package/src/components/forms/AGSelect.vue +0 -54
- package/src/components/forms/index.ts +0 -1
- package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
- package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
- package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
- package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -31
- package/src/components/headless/forms/AGHeadlessSelectOptions.vue +0 -19
- package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +0 -25
- package/src/components/headless/forms/composition.ts +0 -10
- package/src/components/headless/forms/index.ts +0 -8
- package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
- package/src/components/headless/snackbars/index.ts +0 -40
- package/src/components/lib/AGMeasured.vue +0 -16
- package/src/components/lib/index.ts +0 -3
- package/src/components/snackbars/AGSnackbar.vue +0 -38
- package/src/components/snackbars/index.ts +0 -3
- package/src/components/utils.ts +0 -107
- package/src/forms/composition.ts +0 -6
- package/src/utils/tailwindcss.test.ts +0 -26
- package/src/utils/tailwindcss.ts +0 -7
- package/src/utils/vdom.ts +0 -31
- /package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +0 -0
package/src/ui/UI.ts
CHANGED
|
@@ -1,42 +1,55 @@
|
|
|
1
1
|
import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
|
|
2
|
-
import { markRaw, nextTick } from 'vue';
|
|
2
|
+
import { markRaw, nextTick, unref } from 'vue';
|
|
3
|
+
import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
|
|
3
4
|
import type { Component } from 'vue';
|
|
4
|
-
import type {
|
|
5
|
+
import type { ClosureArgs } from '@noeldemartin/utils';
|
|
5
6
|
|
|
6
7
|
import App from '@aerogel/core/services/App';
|
|
7
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';
|
|
8
18
|
import type { AcceptRefs } from '@aerogel/core/utils';
|
|
9
|
-
import type { AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
19
|
+
import type { AlertModalExpose, AlertModalProps } from '@aerogel/core/components/contracts/AlertModal';
|
|
10
20
|
import type { ButtonVariant } from '@aerogel/core/components/contracts/Button';
|
|
11
|
-
import type {
|
|
12
|
-
import type {
|
|
13
|
-
import type {
|
|
14
|
-
import type { SnackbarAction, SnackbarColor } from '@aerogel/core/components/headless/snackbars';
|
|
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';
|
|
15
24
|
|
|
16
25
|
import Service from './UI.state';
|
|
17
26
|
import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
|
|
18
|
-
import type {
|
|
27
|
+
import type { UIModal, UIToast } from './UI.state';
|
|
19
28
|
|
|
20
29
|
interface ModalCallbacks<T = unknown> {
|
|
21
30
|
willClose(result: T | undefined): void;
|
|
22
|
-
|
|
31
|
+
hasClosed(result: T | undefined): void;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|
|
38
48
|
|
|
39
|
-
export
|
|
49
|
+
export interface UIModalContext {
|
|
50
|
+
modal: UIModal;
|
|
51
|
+
childIndex?: number;
|
|
52
|
+
}
|
|
40
53
|
|
|
41
54
|
export type ConfirmOptions = AcceptRefs<{
|
|
42
55
|
acceptText?: string;
|
|
@@ -51,6 +64,7 @@ export type LoadingOptions = AcceptRefs<{
|
|
|
51
64
|
title?: string;
|
|
52
65
|
message?: string;
|
|
53
66
|
progress?: number;
|
|
67
|
+
delay?: number;
|
|
54
68
|
}>;
|
|
55
69
|
|
|
56
70
|
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
|
|
@@ -69,19 +83,27 @@ export type PromptOptions = AcceptRefs<{
|
|
|
69
83
|
trim?: boolean;
|
|
70
84
|
}>;
|
|
71
85
|
|
|
72
|
-
export interface
|
|
86
|
+
export interface ToastOptions {
|
|
73
87
|
component?: Component;
|
|
74
|
-
|
|
75
|
-
actions?:
|
|
88
|
+
variant?: ToastVariant;
|
|
89
|
+
actions?: ToastAction[];
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
export class UIService extends Service {
|
|
79
93
|
|
|
80
94
|
private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
|
|
81
|
-
private components: Partial<
|
|
95
|
+
private components: Partial<UIComponents> = {};
|
|
82
96
|
|
|
83
|
-
public
|
|
84
|
-
|
|
97
|
+
public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
|
|
98
|
+
this.components[name] = component;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public resolveComponent<T extends keyof UIComponents>(name: T): UIComponents[T] | null {
|
|
102
|
+
return this.components[name] ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public requireComponent<T extends keyof UIComponents>(name: T): UIComponents[T] {
|
|
106
|
+
return this.resolveComponent(name) ?? fail(`UI Component '${name}' is not defined!`);
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
public alert(message: string): void;
|
|
@@ -98,10 +120,7 @@ export class UIService extends Service {
|
|
|
98
120
|
};
|
|
99
121
|
};
|
|
100
122
|
|
|
101
|
-
this.
|
|
102
|
-
this.requireComponent(UIComponents.AlertModal),
|
|
103
|
-
getProperties(),
|
|
104
|
-
);
|
|
123
|
+
this.modal(this.requireComponent('alert-modal'), getProperties());
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
/* eslint-disable max-len */
|
|
@@ -133,18 +152,8 @@ export class UIService extends Service {
|
|
|
133
152
|
};
|
|
134
153
|
};
|
|
135
154
|
|
|
136
|
-
type ConfirmModalComponent = ModalComponent<
|
|
137
|
-
AcceptRefs<ConfirmModalProps>,
|
|
138
|
-
boolean | [boolean, Record<string, boolean>]
|
|
139
|
-
>;
|
|
140
|
-
|
|
141
155
|
const properties = getProperties();
|
|
142
|
-
const
|
|
143
|
-
this.requireComponent(UIComponents.ConfirmModal),
|
|
144
|
-
properties,
|
|
145
|
-
);
|
|
146
|
-
const result = await modal.beforeClose;
|
|
147
|
-
|
|
156
|
+
const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
|
|
148
157
|
const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
|
|
149
158
|
const checkboxes =
|
|
150
159
|
typeof result === 'object'
|
|
@@ -196,11 +205,7 @@ export class UIService extends Service {
|
|
|
196
205
|
} as PromptModalProps;
|
|
197
206
|
};
|
|
198
207
|
|
|
199
|
-
const
|
|
200
|
-
this.requireComponent(UIComponents.PromptModal),
|
|
201
|
-
getProperties(),
|
|
202
|
-
);
|
|
203
|
-
const rawResult = await modal.beforeClose;
|
|
208
|
+
const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
|
|
204
209
|
const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
|
|
205
210
|
|
|
206
211
|
return result ?? null;
|
|
@@ -214,7 +219,11 @@ export class UIService extends Service {
|
|
|
214
219
|
operation?: Promise<T> | (() => T),
|
|
215
220
|
): Promise<T> {
|
|
216
221
|
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
217
|
-
const processArgs = (): {
|
|
222
|
+
const processArgs = (): {
|
|
223
|
+
operationPromise: Promise<T>;
|
|
224
|
+
props?: AcceptRefs<LoadingModalProps>;
|
|
225
|
+
delay?: number;
|
|
226
|
+
} => {
|
|
218
227
|
if (typeof operationOrMessageOrOptions === 'string') {
|
|
219
228
|
return {
|
|
220
229
|
props: { message: operationOrMessageOrOptions },
|
|
@@ -226,14 +235,25 @@ export class UIService extends Service {
|
|
|
226
235
|
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
227
236
|
}
|
|
228
237
|
|
|
238
|
+
const { delay, ...props } = operationOrMessageOrOptions;
|
|
239
|
+
|
|
229
240
|
return {
|
|
230
|
-
props
|
|
241
|
+
props,
|
|
242
|
+
delay: unref(delay),
|
|
231
243
|
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
232
244
|
};
|
|
233
245
|
};
|
|
234
246
|
|
|
235
|
-
|
|
236
|
-
const
|
|
247
|
+
let delayed = false;
|
|
248
|
+
const { operationPromise, props, delay } = processArgs();
|
|
249
|
+
|
|
250
|
+
delay && (await Promise.race([after({ ms: delay }).then(() => (delayed = true)), operationPromise]));
|
|
251
|
+
|
|
252
|
+
if (delay && !delayed) {
|
|
253
|
+
return operationPromise;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const modal = await this.modal(this.requireComponent('loading-modal'), props);
|
|
237
257
|
|
|
238
258
|
try {
|
|
239
259
|
const result = await operationPromise;
|
|
@@ -246,43 +266,34 @@ export class UIService extends Service {
|
|
|
246
266
|
}
|
|
247
267
|
}
|
|
248
268
|
|
|
249
|
-
public
|
|
250
|
-
const
|
|
269
|
+
public toast(message: string, options: ToastOptions = {}): void {
|
|
270
|
+
const { component, ...otherOptions } = options;
|
|
271
|
+
const toast: UIToast = {
|
|
251
272
|
id: uuid(),
|
|
252
|
-
properties: { message, ...
|
|
253
|
-
component: markRaw(
|
|
273
|
+
properties: { message, ...otherOptions },
|
|
274
|
+
component: markRaw(component ?? this.requireComponent('toast')),
|
|
254
275
|
};
|
|
255
276
|
|
|
256
|
-
this.setState('
|
|
257
|
-
|
|
258
|
-
setTimeout(() => this.hideSnackbar(snackbar.id), 5000);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
public hideSnackbar(id: string): void {
|
|
262
|
-
this.setState(
|
|
263
|
-
'snackbars',
|
|
264
|
-
this.snackbars.filter((snackbar) => snackbar.id !== id),
|
|
265
|
-
);
|
|
277
|
+
this.setState('toasts', this.toasts.concat(toast));
|
|
266
278
|
}
|
|
267
279
|
|
|
268
|
-
public
|
|
269
|
-
|
|
270
|
-
|
|
280
|
+
public modal<T extends Component>(
|
|
281
|
+
...args: {} extends ComponentProps<T>
|
|
282
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
283
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
284
|
+
): Promise<UIModal<ModalResult<T>>>;
|
|
271
285
|
|
|
272
|
-
public async
|
|
273
|
-
component: TModalComponent,
|
|
274
|
-
properties?: ModalProperties<TModalComponent>,
|
|
275
|
-
): Promise<UIModal<ModalResult<TModalComponent>>> {
|
|
286
|
+
public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
|
|
276
287
|
const id = uuid();
|
|
277
|
-
const callbacks: Partial<ModalCallbacks<ModalResult<
|
|
278
|
-
const modal: UIModal<ModalResult<
|
|
288
|
+
const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
|
|
289
|
+
const modal: UIModal<ModalResult<T>> = {
|
|
279
290
|
id,
|
|
280
|
-
|
|
291
|
+
closing: false,
|
|
292
|
+
properties: props ?? {},
|
|
281
293
|
component: markRaw(component),
|
|
282
294
|
beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
|
|
283
|
-
afterClose: new Promise((resolve) => (callbacks.
|
|
295
|
+
afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
|
|
284
296
|
};
|
|
285
|
-
const activeModal = this.modals.at(-1);
|
|
286
297
|
const modals = this.modals.concat(modal);
|
|
287
298
|
|
|
288
299
|
this.modalCallbacks[modal.id] = callbacks;
|
|
@@ -290,15 +301,26 @@ export class UIService extends Service {
|
|
|
290
301
|
this.setState({ modals });
|
|
291
302
|
|
|
292
303
|
await nextTick();
|
|
293
|
-
await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
|
|
294
|
-
await Promise.all([
|
|
295
|
-
activeModal || Events.emit('show-overlays-backdrop'),
|
|
296
|
-
Events.emit('show-modal', { id: modal.id }),
|
|
297
|
-
]);
|
|
298
304
|
|
|
299
305
|
return modal;
|
|
300
306
|
}
|
|
301
307
|
|
|
308
|
+
public modalForm<T extends Component>(
|
|
309
|
+
...args: {} extends ComponentProps<T>
|
|
310
|
+
? [component: T, props?: AcceptRefs<ComponentProps<T>>]
|
|
311
|
+
: [component: T, props: AcceptRefs<ComponentProps<T>>]
|
|
312
|
+
): Promise<ModalResult<T> | undefined>;
|
|
313
|
+
|
|
314
|
+
public async modalForm<T extends Component>(
|
|
315
|
+
component: T,
|
|
316
|
+
props?: ComponentProps<T>,
|
|
317
|
+
): Promise<ModalResult<T> | undefined> {
|
|
318
|
+
const modal = await this.modal<T>(component, props as ComponentProps<T>);
|
|
319
|
+
const result = await modal.beforeClose;
|
|
320
|
+
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
302
324
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
303
325
|
if (!App.isMounted()) {
|
|
304
326
|
await this.removeModal(id, result);
|
|
@@ -327,25 +349,23 @@ export class UIService extends Service {
|
|
|
327
349
|
this.modals.filter((m) => m.id !== id),
|
|
328
350
|
);
|
|
329
351
|
|
|
330
|
-
this.modalCallbacks[id]?.
|
|
352
|
+
this.modalCallbacks[id]?.hasClosed?.(result);
|
|
331
353
|
|
|
332
354
|
delete this.modalCallbacks[id];
|
|
333
|
-
|
|
334
|
-
const activeModal = this.modals.at(-1);
|
|
335
|
-
|
|
336
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
337
355
|
}
|
|
338
356
|
|
|
339
357
|
private watchModalEvents(): void {
|
|
340
|
-
Events.on('modal-will-close', ({ modal, result }) => {
|
|
341
|
-
this.
|
|
358
|
+
Events.on('modal-will-close', ({ modal: { id }, result }) => {
|
|
359
|
+
const modal = this.modals.find((_modal) => id === _modal.id);
|
|
342
360
|
|
|
343
|
-
if (
|
|
344
|
-
|
|
361
|
+
if (modal) {
|
|
362
|
+
modal.closing = true;
|
|
345
363
|
}
|
|
364
|
+
|
|
365
|
+
this.modalCallbacks[id]?.willClose?.(result);
|
|
346
366
|
});
|
|
347
367
|
|
|
348
|
-
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
368
|
+
Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
|
|
349
369
|
await this.removeModal(id, result);
|
|
350
370
|
});
|
|
351
371
|
}
|
|
@@ -389,11 +409,7 @@ export default facade(UIService);
|
|
|
389
409
|
declare module '@aerogel/core/services/Events' {
|
|
390
410
|
export interface EventsPayload {
|
|
391
411
|
'close-modal': { id: string; result?: unknown };
|
|
392
|
-
'hide-modal': { id: string };
|
|
393
|
-
'hide-overlays-backdrop': void;
|
|
394
|
-
'modal-closed': { modal: UIModal; result?: unknown };
|
|
395
412
|
'modal-will-close': { modal: UIModal; result?: unknown };
|
|
396
|
-
'
|
|
397
|
-
'show-overlays-backdrop': void;
|
|
413
|
+
'modal-has-closed': { modal: UIModal; result?: unknown };
|
|
398
414
|
}
|
|
399
415
|
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import type { Component } from 'vue';
|
|
2
|
-
|
|
3
|
-
import AGSnackbar from '@aerogel/core/components/snackbars/AGSnackbar.vue';
|
|
4
|
-
import AGStartupCrash from '@aerogel/core/components/lib/AGStartupCrash.vue';
|
|
5
1
|
import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
|
|
6
2
|
import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
|
|
7
3
|
import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
|
|
8
4
|
import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
|
|
9
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';
|
|
10
8
|
import { bootServices } from '@aerogel/core/services';
|
|
11
9
|
import { definePlugin } from '@aerogel/core/plugins';
|
|
12
10
|
|
|
13
|
-
import UI
|
|
14
|
-
import type {
|
|
11
|
+
import UI from './UI';
|
|
12
|
+
import type { UIComponents } from './UI';
|
|
13
|
+
import type { Component } from 'vue';
|
|
15
14
|
|
|
16
15
|
const services = { $ui: UI };
|
|
17
16
|
|
|
@@ -23,20 +22,20 @@ export type UIServices = typeof services;
|
|
|
23
22
|
|
|
24
23
|
export default definePlugin({
|
|
25
24
|
async install(app, options) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
Object.entries({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}).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
|
+
}
|
|
40
39
|
|
|
41
40
|
await bootServices(app, services);
|
|
42
41
|
},
|
|
@@ -44,7 +43,7 @@ export default definePlugin({
|
|
|
44
43
|
|
|
45
44
|
declare module '@aerogel/core/bootstrap/options' {
|
|
46
45
|
export interface AerogelOptions {
|
|
47
|
-
components?: Partial<
|
|
46
|
+
components?: Partial<Partial<UIComponents>>;
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
|
|
@@ -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
|
+
}
|
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
EventWithPayload,
|
|
7
7
|
EventWithoutPayload,
|
|
8
8
|
EventsPayload,
|
|
9
|
-
UnknownEvent,
|
|
10
9
|
} from '@aerogel/core/services/Events';
|
|
11
10
|
|
|
12
11
|
export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
|
|
@@ -14,11 +13,10 @@ export function useEvent<Event extends EventWithPayload>(
|
|
|
14
13
|
event: Event,
|
|
15
14
|
listener: EventListener<EventsPayload[Event]>
|
|
16
15
|
): void;
|
|
17
|
-
export function useEvent<Payload>(event: string, listener: (payload: Payload) => unknown): void;
|
|
18
|
-
export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
|
|
19
16
|
|
|
20
17
|
export function useEvent(event: string, listener: EventListener): void {
|
|
21
|
-
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const unsubscribe = Events.on(event as any, listener);
|
|
22
20
|
|
|
23
21
|
onUnmounted(() => unsubscribe());
|
|
24
22
|
}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { objectWithout } from '@noeldemartin/utils';
|
|
2
|
-
import { computed, useAttrs } from 'vue';
|
|
2
|
+
import { computed, inject, onUnmounted, useAttrs } from 'vue';
|
|
3
3
|
import type { ClassValue } from 'clsx';
|
|
4
4
|
import type { ComputedRef } from 'vue';
|
|
5
|
+
import type { Nullable } from '@noeldemartin/utils';
|
|
6
|
+
|
|
7
|
+
import FormController from '@aerogel/core/forms/FormController';
|
|
8
|
+
import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
|
|
9
|
+
|
|
10
|
+
export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
|
|
11
|
+
const form = inject<FormController | null>('form', null);
|
|
12
|
+
const stop = form?.on('focus', (name) => input.name === name && listener());
|
|
13
|
+
|
|
14
|
+
onUnmounted(() => stop?.());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useForm<const T extends FormFieldDefinitions>(fields: T): FormController<T> & FormData<T> {
|
|
18
|
+
return new FormController(fields) as FormController<T> & FormData<T>;
|
|
19
|
+
}
|
|
5
20
|
|
|
6
21
|
export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
|
|
7
22
|
const attrs = useAttrs();
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { debounce } from '@noeldemartin/utils';
|
|
2
|
-
import { ref, watchEffect } from 'vue';
|
|
3
|
-
import type { ComputedGetter, ComputedRef } from '
|
|
2
|
+
import { computed, ref, watch, watchEffect } from 'vue';
|
|
3
|
+
import type { ComputedGetter, ComputedRef, Ref } from 'vue';
|
|
4
4
|
|
|
5
5
|
export interface ComputedDebounceOptions<T> {
|
|
6
6
|
initial?: T;
|
|
7
7
|
delay?: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function computedAsync<T>(getter: () => Promise<T>): Ref<T | undefined> {
|
|
11
|
+
const result = ref<T>();
|
|
12
|
+
const asyncValue = computed(getter);
|
|
13
|
+
|
|
14
|
+
watch(asyncValue, async () => (result.value = await asyncValue.value), { immediate: true });
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
export function computedDebounce<T>(options: ComputedDebounceOptions<T>, getter: ComputedGetter<T>): ComputedRef<T>;
|
|
11
20
|
export function computedDebounce<T>(getter: ComputedGetter<T>): ComputedRef<T | null>;
|
|
12
21
|
export function computedDebounce<T>(
|
package/src/utils/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
export * from './classes';
|
|
1
2
|
export * from './composition/events';
|
|
2
3
|
export * from './composition/forms';
|
|
3
4
|
export * from './composition/hooks';
|
|
4
5
|
export * from './composition/persistent';
|
|
6
|
+
export * from './composition/state';
|
|
5
7
|
export * from './markdown';
|
|
6
|
-
export * from './
|
|
8
|
+
export * from './types';
|
|
7
9
|
export * from './vue';
|
package/src/utils/markdown.ts
CHANGED
|
@@ -2,10 +2,18 @@ import DOMPurify from 'dompurify';
|
|
|
2
2
|
import { stringMatchAll, tap } from '@noeldemartin/utils';
|
|
3
3
|
import { Renderer, marked } from 'marked';
|
|
4
4
|
|
|
5
|
+
let router: MarkdownRouter | null = null;
|
|
6
|
+
|
|
5
7
|
function makeRenderer(): Renderer {
|
|
6
8
|
return tap(new Renderer(), (renderer) => {
|
|
7
9
|
renderer.link = function(link) {
|
|
8
|
-
|
|
10
|
+
const defaultLink = Renderer.prototype.link.apply(this, [link]);
|
|
11
|
+
|
|
12
|
+
if (!link.href.startsWith('#')) {
|
|
13
|
+
return defaultLink.replace('<a', '<a target="_blank"');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return defaultLink;
|
|
9
17
|
};
|
|
10
18
|
});
|
|
11
19
|
}
|
|
@@ -20,11 +28,37 @@ function renderActionLinks(html: string): string {
|
|
|
20
28
|
return html;
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
function renderRouteLinks(html: string): string {
|
|
32
|
+
const matches = stringMatchAll<3>(html, /<a[^>]*href="#route:([^"]+)"[^>]*>([^<]+)<\/a>/g);
|
|
33
|
+
|
|
34
|
+
for (const [link, route, text] of matches) {
|
|
35
|
+
const url = router?.resolve(route) ?? route;
|
|
36
|
+
|
|
37
|
+
html = html.replace(link, `<a data-markdown-route="${route}" href="${url}">${text}</a>`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return html;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MarkdownRouter {
|
|
44
|
+
resolve(route: string): string;
|
|
45
|
+
visit(route: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getMarkdownRouter(): MarkdownRouter | null {
|
|
49
|
+
return router;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setMarkdownRouter(markdownRouter: MarkdownRouter): void {
|
|
53
|
+
router = markdownRouter;
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
export function renderMarkdown(markdown: string): string {
|
|
24
57
|
let html = marked(markdown, { renderer: makeRenderer(), async: false });
|
|
25
58
|
|
|
26
59
|
html = safeHtml(html);
|
|
27
60
|
html = renderActionLinks(html);
|
|
61
|
+
html = renderRouteLinks(html);
|
|
28
62
|
|
|
29
63
|
return html;
|
|
30
64
|
}
|