@aerogel/core 0.0.0-next.b656a964404fbde17d9cce7668722596098e47fd → 0.0.0-next.b9379d15fd4f40346d655134b49c9015ead9c536

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +649 -560
  3. package/dist/aerogel-core.js +1878 -1455
  4. package/dist/aerogel-core.js.map +1 -1
  5. package/package.json +6 -4
  6. package/src/components/AppLayout.vue +1 -3
  7. package/src/components/AppOverlays.vue +0 -27
  8. package/src/components/contracts/AlertModal.ts +15 -0
  9. package/src/components/contracts/ConfirmModal.ts +12 -5
  10. package/src/components/contracts/DropdownMenu.ts +17 -3
  11. package/src/components/contracts/ErrorReportModal.ts +8 -4
  12. package/src/components/contracts/Input.ts +7 -7
  13. package/src/components/contracts/LoadingModal.ts +12 -4
  14. package/src/components/contracts/Modal.ts +12 -4
  15. package/src/components/contracts/PromptModal.ts +8 -2
  16. package/src/components/contracts/Select.ts +21 -12
  17. package/src/components/contracts/Toast.ts +4 -2
  18. package/src/components/contracts/index.ts +3 -1
  19. package/src/components/headless/HeadlessButton.vue +2 -1
  20. package/src/components/headless/HeadlessInput.vue +3 -3
  21. package/src/components/headless/HeadlessInputInput.vue +5 -5
  22. package/src/components/headless/HeadlessInputTextArea.vue +3 -3
  23. package/src/components/headless/HeadlessModal.vue +22 -51
  24. package/src/components/headless/HeadlessModalContent.vue +11 -5
  25. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  26. package/src/components/headless/HeadlessSelect.vue +34 -18
  27. package/src/components/headless/HeadlessSelectOption.vue +1 -1
  28. package/src/components/headless/HeadlessSelectOptions.vue +16 -4
  29. package/src/components/headless/HeadlessSelectValue.vue +4 -1
  30. package/src/components/headless/HeadlessSwitch.vue +96 -0
  31. package/src/components/headless/index.ts +2 -0
  32. package/src/components/ui/AdvancedOptions.vue +1 -1
  33. package/src/components/ui/AlertModal.vue +7 -3
  34. package/src/components/ui/Button.vue +17 -15
  35. package/src/components/ui/Checkbox.vue +4 -4
  36. package/src/components/ui/ConfirmModal.vue +12 -4
  37. package/src/components/ui/DropdownMenu.vue +18 -19
  38. package/src/components/ui/DropdownMenuOption.vue +22 -0
  39. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  40. package/src/components/ui/EditableContent.vue +3 -3
  41. package/src/components/ui/ErrorLogs.vue +19 -0
  42. package/src/components/ui/ErrorLogsModal.vue +48 -0
  43. package/src/components/ui/ErrorReportModal.vue +18 -7
  44. package/src/components/ui/Input.vue +3 -3
  45. package/src/components/ui/LoadingModal.vue +6 -4
  46. package/src/components/ui/Markdown.vue +29 -1
  47. package/src/components/ui/Modal.vue +74 -21
  48. package/src/components/ui/ModalContext.vue +2 -1
  49. package/src/components/ui/ProgressBar.vue +8 -7
  50. package/src/components/ui/PromptModal.vue +8 -5
  51. package/src/components/ui/Select.vue +10 -4
  52. package/src/components/ui/SelectLabel.vue +13 -2
  53. package/src/components/ui/SelectOption.vue +29 -0
  54. package/src/components/ui/SelectOptions.vue +24 -20
  55. package/src/components/ui/SelectTrigger.vue +2 -2
  56. package/src/components/ui/Switch.vue +11 -0
  57. package/src/components/ui/Toast.vue +19 -15
  58. package/src/components/ui/index.ts +6 -0
  59. package/src/directives/measure.ts +11 -5
  60. package/src/errors/Errors.ts +18 -15
  61. package/src/errors/index.ts +6 -2
  62. package/src/errors/settings/Debug.vue +39 -0
  63. package/src/errors/settings/index.ts +10 -0
  64. package/src/forms/FormController.test.ts +32 -9
  65. package/src/forms/FormController.ts +23 -22
  66. package/src/forms/index.ts +0 -1
  67. package/src/forms/utils.ts +34 -34
  68. package/src/index.css +34 -7
  69. package/src/lang/index.ts +3 -2
  70. package/src/lang/settings/Language.vue +2 -2
  71. package/src/services/App.ts +6 -1
  72. package/src/services/Events.test.ts +8 -8
  73. package/src/services/Events.ts +2 -8
  74. package/src/services/index.ts +3 -3
  75. package/src/ui/UI.state.ts +3 -13
  76. package/src/ui/UI.ts +87 -79
  77. package/src/ui/index.ts +16 -17
  78. package/src/utils/classes.ts +9 -17
  79. package/src/utils/composition/events.ts +2 -4
  80. package/src/utils/composition/forms.ts +7 -1
  81. package/src/utils/index.ts +1 -0
  82. package/src/utils/markdown.ts +35 -1
  83. package/src/utils/types.ts +3 -0
  84. package/src/utils/vue.ts +6 -1
  85. package/src/components/contracts/shared.ts +0 -9
  86. package/src/forms/composition.ts +0 -6
@@ -10,12 +10,12 @@ describe('Events', () => {
10
10
  // Arrange
11
11
  let counter = 0;
12
12
 
13
- Events.on('trigger', () => counter++);
13
+ Events.on('application-mounted', () => counter++);
14
14
 
15
15
  // Act
16
- await Events.emit('trigger');
17
- await Events.emit('trigger');
18
- await Events.emit('trigger');
16
+ await Events.emit('application-mounted');
17
+ await Events.emit('application-mounted');
18
+ await Events.emit('application-mounted');
19
19
 
20
20
  // Assert
21
21
  expect(counter).toEqual(3);
@@ -25,12 +25,12 @@ describe('Events', () => {
25
25
  // Arrange
26
26
  const storage: string[] = [];
27
27
 
28
- Events.on('trigger', () => storage.push('second'));
29
- Events.on('trigger', { priority: EventListenerPriorities.Low }, () => storage.push('third'));
30
- Events.on('trigger', { priority: EventListenerPriorities.High }, () => storage.push('first'));
28
+ Events.on('application-mounted', () => storage.push('second'));
29
+ Events.on('application-mounted', { priority: EventListenerPriorities.Low }, () => storage.push('third'));
30
+ Events.on('application-mounted', { priority: EventListenerPriorities.High }, () => storage.push('first'));
31
31
 
32
32
  // Act
33
- await Events.emit('trigger');
33
+ await Events.emit('application-mounted');
34
34
 
35
35
  // Assert
36
36
  expect(storage).toEqual(['first', 'second', 'third']);
@@ -10,7 +10,6 @@ export type AerogelGlobalEvents = Partial<{ [Event in EventWithoutPayload]: () =
10
10
  Partial<{ [Event in EventWithPayload]: EventListener<EventsPayload[Event]> }>;
11
11
 
12
12
  export type EventListener<T = unknown> = (payload: T) => unknown;
13
- export type UnknownEvent<T> = T extends keyof EventsPayload ? never : T;
14
13
 
15
14
  export type EventWithoutPayload = {
16
15
  [K in keyof EventsPayload]: EventsPayload[K] extends void ? K : never;
@@ -34,12 +33,12 @@ export class EventsService extends Service {
34
33
 
35
34
  protected override async boot(): Promise<void> {
36
35
  Object.entries(globalThis.__aerogelEvents__ ?? {}).forEach(([event, listener]) =>
37
- this.on(event as string, listener as EventListener));
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ this.on(event as any, listener as EventListener));
38
38
  }
39
39
 
40
40
  public emit<Event extends EventWithoutPayload>(event: Event): Promise<void>;
41
41
  public emit<Event extends EventWithPayload>(event: Event, payload: EventsPayload[Event]): Promise<void>;
42
- public emit<Event extends string>(event: UnknownEvent<Event>, payload?: unknown): Promise<void>;
43
42
  public async emit(event: string, payload?: unknown): Promise<void> {
44
43
  const listeners = this.listeners[event] ?? { priorities: [], handlers: {} };
45
44
 
@@ -55,9 +54,6 @@ export class EventsService extends Service {
55
54
  public on<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
56
55
  public on<Event extends EventWithPayload>(event: Event, priority: EventListenerPriority, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
57
56
  public on<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
58
- public on<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
59
- public on<Event extends string>(event: UnknownEvent<Event>, priority: EventListenerPriority, listener: EventListener): () => void; // prettier-ignore
60
- public on<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
61
57
  /* eslint-enable max-len */
62
58
 
63
59
  public on(
@@ -83,8 +79,6 @@ export class EventsService extends Service {
83
79
  public once<Event extends EventWithoutPayload>(event: Event, options: Partial<EventListenerOptions>, listener: () => unknown): () => void; // prettier-ignore
84
80
  public once<Event extends EventWithPayload>(event: Event, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
85
81
  public once<Event extends EventWithPayload>(event: Event, options: Partial<EventListenerOptions>, listener: EventListener<EventsPayload[Event]>): () => void | void; // prettier-ignore
86
- public once<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): () => void;
87
- public once<Event extends string>(event: UnknownEvent<Event>, options: Partial<EventListenerOptions>, listener: EventListener): () => void; // prettier-ignore
88
82
  /* eslint-enable max-len */
89
83
 
90
84
  public once(
@@ -1,4 +1,4 @@
1
- import type { App as VueApp } from 'vue';
1
+ import type { App as AppInstance } from 'vue';
2
2
 
3
3
  import { definePlugin } from '@aerogel/core/plugins';
4
4
  import { isDevelopment, isTesting } from '@noeldemartin/utils';
@@ -30,7 +30,7 @@ export type DefaultServices = typeof defaultServices;
30
30
 
31
31
  export interface Services extends DefaultServices {}
32
32
 
33
- export async function bootServices(app: VueApp, services: Record<string, Service>): Promise<void> {
33
+ export async function bootServices(app: AppInstance, services: Record<string, Service>): Promise<void> {
34
34
  await Promise.all(
35
35
  Object.entries(services).map(async ([name, service]) => {
36
36
  await service
@@ -54,7 +54,7 @@ export default definePlugin({
54
54
  };
55
55
 
56
56
  app.use(getPiniaStore());
57
- App.settings.push(...(options.settings ?? []));
57
+ options.settings?.forEach((setting) => App.addSetting(setting));
58
58
 
59
59
  await bootServices(app, services);
60
60
  },
@@ -8,22 +8,11 @@ export interface UIModal<T = unknown> {
8
8
  id: string;
9
9
  properties: Record<string, unknown>;
10
10
  component: Component;
11
+ closing: boolean;
11
12
  beforeClose: Promise<T | undefined>;
12
13
  afterClose: Promise<T | undefined>;
13
14
  }
14
15
 
15
- export interface UIModalContext {
16
- modal: UIModal;
17
- childIndex?: number;
18
- }
19
-
20
- export interface ModalComponent<
21
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
22
- Properties extends object = object,
23
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
24
- Result = unknown,
25
- > {}
26
-
27
16
  export interface UIToast {
28
17
  id: string;
29
18
  component: Component;
@@ -38,7 +27,8 @@ export default defineServiceState({
38
27
  layout: getCurrentLayout(),
39
28
  },
40
29
  computed: {
41
- mobile: ({ layout }) => layout === Layouts.Mobile,
42
30
  desktop: ({ layout }) => layout === Layouts.Desktop,
31
+ mobile: ({ layout }) => layout === Layouts.Mobile,
32
+ openModals: ({ modals }) => modals.filter(({ closing }) => !closing),
43
33
  },
44
34
  });
package/src/ui/UI.ts CHANGED
@@ -1,42 +1,55 @@
1
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 { ObjectValues } from '@noeldemartin/utils';
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 { 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';
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 { ModalComponent, UIModal, UIToast } from './UI.state';
27
+ import type { UIModal, UIToast } from './UI.state';
19
28
 
20
29
  interface ModalCallbacks<T = unknown> {
21
30
  willClose(result: T | undefined): void;
22
- closed(result: T | undefined): void;
31
+ hasClosed(result: T | undefined): void;
23
32
  }
24
33
 
25
- type ModalProperties<TComponent> = TComponent extends ModalComponent<infer TProperties, unknown> ? TProperties : never;
26
- type ModalResult<TComponent> =
27
- TComponent extends ModalComponent<Record<string, unknown>, infer TResult> ? TResult : never;
28
-
29
- export const UIComponents = {
30
- AlertModal: 'alert-modal',
31
- ConfirmModal: 'confirm-modal',
32
- ErrorReportModal: 'error-report-modal',
33
- LoadingModal: 'loading-modal',
34
- PromptModal: 'prompt-modal',
35
- Toast: 'toast',
36
- StartupCrash: 'startup-crash',
37
- } as const;
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 type UIComponent = ObjectValues<typeof UIComponents>;
49
+ export interface UIModalContext {
50
+ modal: UIModal;
51
+ childIndex?: number;
52
+ }
40
53
 
41
54
  export type ConfirmOptions = AcceptRefs<{
42
55
  acceptText?: string;
@@ -78,10 +91,18 @@ export interface ToastOptions {
78
91
  export class UIService extends Service {
79
92
 
80
93
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
81
- private components: Partial<Record<UIComponent, Component>> = {};
94
+ private components: Partial<UIComponents> = {};
95
+
96
+ public registerComponent<T extends keyof UIComponents>(name: T, component: UIComponents[T]): void {
97
+ this.components[name] = component;
98
+ }
82
99
 
83
- public requireComponent(name: UIComponent): Component {
84
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
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!`);
85
106
  }
86
107
 
87
108
  public alert(message: string): void;
@@ -98,10 +119,7 @@ export class UIService extends Service {
98
119
  };
99
120
  };
100
121
 
101
- this.openModal<ModalComponent<AlertModalProps>>(
102
- this.requireComponent(UIComponents.AlertModal),
103
- getProperties(),
104
- );
122
+ this.modal(this.requireComponent('alert-modal'), getProperties());
105
123
  }
106
124
 
107
125
  /* eslint-disable max-len */
@@ -133,18 +151,8 @@ export class UIService extends Service {
133
151
  };
134
152
  };
135
153
 
136
- type ConfirmModalComponent = ModalComponent<
137
- AcceptRefs<ConfirmModalProps>,
138
- boolean | [boolean, Record<string, boolean>]
139
- >;
140
-
141
154
  const properties = getProperties();
142
- const modal = await this.openModal<ConfirmModalComponent>(
143
- this.requireComponent(UIComponents.ConfirmModal),
144
- properties,
145
- );
146
- const result = await modal.beforeClose;
147
-
155
+ const result = await this.modalForm(this.requireComponent('confirm-modal'), properties);
148
156
  const confirmed = typeof result === 'object' ? result[0] : (result ?? false);
149
157
  const checkboxes =
150
158
  typeof result === 'object'
@@ -196,11 +204,7 @@ export class UIService extends Service {
196
204
  } as PromptModalProps;
197
205
  };
198
206
 
199
- const modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
200
- this.requireComponent(UIComponents.PromptModal),
201
- getProperties(),
202
- );
203
- const rawResult = await modal.beforeClose;
207
+ const rawResult = await this.modalForm(this.requireComponent('prompt-modal'), getProperties());
204
208
  const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
205
209
 
206
210
  return result ?? null;
@@ -233,7 +237,7 @@ export class UIService extends Service {
233
237
  };
234
238
 
235
239
  const { operationPromise, props } = processArgs();
236
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
240
+ const modal = await this.modal(this.requireComponent('loading-modal'), props);
237
241
 
238
242
  try {
239
243
  const result = await operationPromise;
@@ -251,30 +255,29 @@ export class UIService extends Service {
251
255
  const toast: UIToast = {
252
256
  id: uuid(),
253
257
  properties: { message, ...otherOptions },
254
- component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
258
+ component: markRaw(component ?? this.requireComponent('toast')),
255
259
  };
256
260
 
257
261
  this.setState('toasts', this.toasts.concat(toast));
258
262
  }
259
263
 
260
- public registerComponent(name: UIComponent, component: Component): void {
261
- this.components[name] = component;
262
- }
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>>>;
263
269
 
264
- public async openModal<TModalComponent extends ModalComponent>(
265
- component: TModalComponent,
266
- properties?: ModalProperties<TModalComponent>,
267
- ): Promise<UIModal<ModalResult<TModalComponent>>> {
270
+ public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
268
271
  const id = uuid();
269
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
270
- const modal: UIModal<ModalResult<TModalComponent>> = {
272
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
273
+ const modal: UIModal<ModalResult<T>> = {
271
274
  id,
272
- properties: properties ?? {},
275
+ closing: false,
276
+ properties: props ?? {},
273
277
  component: markRaw(component),
274
278
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
275
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
279
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
276
280
  };
277
- const activeModal = this.modals.at(-1);
278
281
  const modals = this.modals.concat(modal);
279
282
 
280
283
  this.modalCallbacks[modal.id] = callbacks;
@@ -282,15 +285,26 @@ export class UIService extends Service {
282
285
  this.setState({ modals });
283
286
 
284
287
  await nextTick();
285
- await (activeModal && Events.emit('hide-modal', { id: activeModal.id }));
286
- await Promise.all([
287
- activeModal || Events.emit('show-overlays-backdrop'),
288
- Events.emit('show-modal', { id: modal.id }),
289
- ]);
290
288
 
291
289
  return modal;
292
290
  }
293
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
+
294
308
  public async closeModal(id: string, result?: unknown): Promise<void> {
295
309
  if (!App.isMounted()) {
296
310
  await this.removeModal(id, result);
@@ -319,25 +333,23 @@ export class UIService extends Service {
319
333
  this.modals.filter((m) => m.id !== id),
320
334
  );
321
335
 
322
- this.modalCallbacks[id]?.closed?.(result);
336
+ this.modalCallbacks[id]?.hasClosed?.(result);
323
337
 
324
338
  delete this.modalCallbacks[id];
325
-
326
- const activeModal = this.modals.at(-1);
327
-
328
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
329
339
  }
330
340
 
331
341
  private watchModalEvents(): void {
332
- Events.on('modal-will-close', ({ modal, result }) => {
333
- this.modalCallbacks[modal.id]?.willClose?.(result);
342
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
343
+ const modal = this.modals.find((_modal) => id === _modal.id);
334
344
 
335
- if (this.modals.length === 1) {
336
- Events.emit('hide-overlays-backdrop');
345
+ if (modal) {
346
+ modal.closing = true;
337
347
  }
348
+
349
+ this.modalCallbacks[id]?.willClose?.(result);
338
350
  });
339
351
 
340
- Events.on('modal-closed', async ({ modal: { id }, result }) => {
352
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
341
353
  await this.removeModal(id, result);
342
354
  });
343
355
  }
@@ -381,11 +393,7 @@ export default facade(UIService);
381
393
  declare module '@aerogel/core/services/Events' {
382
394
  export interface EventsPayload {
383
395
  'close-modal': { id: string; result?: unknown };
384
- 'hide-modal': { id: string };
385
- 'hide-overlays-backdrop': void;
386
- 'modal-closed': { modal: UIModal; result?: unknown };
387
396
  'modal-will-close': { modal: UIModal; result?: unknown };
388
- 'show-modal': { id: string };
389
- 'show-overlays-backdrop': void;
397
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
390
398
  }
391
399
  }
package/src/ui/index.ts CHANGED
@@ -1,5 +1,3 @@
1
- import type { Component } from 'vue';
2
-
3
1
  import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
4
2
  import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
5
3
  import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
@@ -10,8 +8,9 @@ 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, { UIComponents } from './UI';
14
- import type { UIComponent } from './UI';
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 defaultComponents = {
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,
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
- ...defaultComponents,
38
- ...options.components,
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<Record<UIComponent, Component>>;
46
+ components?: Partial<Partial<UIComponents>>;
48
47
  }
49
48
  }
50
49
 
@@ -1,14 +1,13 @@
1
1
  import clsx from 'clsx';
2
- import { computed, unref } from 'vue';
2
+ import { unref } from 'vue';
3
3
  import { cva } from 'class-variance-authority';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import type { ClassValue } from 'clsx';
6
- import type { ComputedRef, PropType, Ref } from 'vue';
6
+ import type { PropType } from 'vue';
7
7
  import type { GetClosureArgs, GetClosureResult } from '@noeldemartin/utils';
8
8
 
9
9
  export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
10
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
11
  export type Variants<T extends Record<string, string | boolean>> = Required<{
13
12
  [K in keyof T]: Exclude<T[K], undefined> extends string
14
13
  ? { [key in Exclude<T[K], undefined>]: string | null }
@@ -26,22 +25,15 @@ export type PickComponentProps<TValues, TDefinitions> = {
26
25
  [K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
27
26
  };
28
27
 
29
- export function computedVariantClasses<T>(
30
- value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
28
+ export function variantClasses<T>(
29
+ value: { baseClasses?: string } & CVAProps<T>,
31
30
  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);
31
+ ): string {
32
+ const { baseClasses: valueBaseClasses, ...values } = value;
33
+ const { baseClasses: configBaseClasses, ...configs } = config;
34
+ const variants = cva(configBaseClasses, configs as CVAConfig<T>);
39
35
 
40
- return extractedValues;
41
- }, {} as CVAProps<T>);
42
-
43
- return classes(variants(values), unref(valueBaseClasses));
44
- });
36
+ return classes(variants(values as CVAProps<T>), unref(valueBaseClasses));
45
37
  }
46
38
 
47
39
  export function classes(...inputs: ClassValue[]): string {
@@ -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
- const unsubscribe = Events.on(event, listener);
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
  }
@@ -2,9 +2,11 @@ import { objectWithout } from '@noeldemartin/utils';
2
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 { FormController } from '@aerogel/core/forms';
6
5
  import type { Nullable } from '@noeldemartin/utils';
7
6
 
7
+ import FormController from '@aerogel/core/forms/FormController';
8
+ import type { FormData, FormFieldDefinitions } from '@aerogel/core/forms/FormController';
9
+
8
10
  export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
9
11
  const form = inject<FormController | null>('form', null);
10
12
  const stop = form?.on('focus', (name) => input.name === name && listener());
@@ -12,6 +14,10 @@ export function onFormFocus(input: { name: Nullable<string> }, listener: () => u
12
14
  onUnmounted(() => stop?.());
13
15
  }
14
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
+ }
20
+
15
21
  export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
16
22
  const attrs = useAttrs();
17
23
  const classes = computed(() => attrs.class);
@@ -5,4 +5,5 @@ export * from './composition/hooks';
5
5
  export * from './composition/persistent';
6
6
  export * from './composition/state';
7
7
  export * from './markdown';
8
+ export * from './types';
8
9
  export * from './vue';
@@ -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
- return Renderer.prototype.link.apply(this, [link]).replace('<a', '<a target="_blank"');
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
  }
@@ -0,0 +1,3 @@
1
+ import type { Nullable } from '@noeldemartin/utils';
2
+
3
+ export type Falsifiable<T> = Nullable<T> | false;
package/src/utils/vue.ts CHANGED
@@ -12,7 +12,12 @@ function renderVNodeAttrs(node: VNode): string {
12
12
  }, '');
13
13
  }
14
14
 
15
- export function defineDirective(directive: Directive): Directive {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ export function defineDirective<TValue = any, TModifiers extends string = string>(
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ directive: Directive<any, TValue, TModifiers>,
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ ): Directive<any, TValue, TModifiers> {
16
21
  return directive;
17
22
  }
18
23