@aerogel/core 0.0.0-next.aa6e27a9c197d1ee10c9fe018ee8c0fc6ff77767 → 0.0.0-next.abea39863ad0f7c484b8b1f365870e43e9bdd0ba

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 (97) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +683 -579
  3. package/dist/aerogel-core.js +2180 -1828
  4. package/dist/aerogel-core.js.map +1 -1
  5. package/package.json +7 -2
  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 +6 -6
  22. package/src/components/headless/HeadlessInputTextArea.vue +4 -4
  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 -19
  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/index.ts +2 -1
  33. package/src/components/ui/AdvancedOptions.vue +1 -1
  34. package/src/components/ui/AlertModal.vue +7 -3
  35. package/src/components/ui/Button.vue +33 -16
  36. package/src/components/ui/Checkbox.vue +5 -5
  37. package/src/components/ui/ConfirmModal.vue +12 -4
  38. package/src/components/ui/DropdownMenu.vue +18 -19
  39. package/src/components/ui/DropdownMenuOption.vue +22 -0
  40. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  41. package/src/components/ui/EditableContent.vue +4 -4
  42. package/src/components/ui/ErrorLogs.vue +19 -0
  43. package/src/components/ui/ErrorLogsModal.vue +48 -0
  44. package/src/components/ui/ErrorReportModal.vue +18 -7
  45. package/src/components/ui/Input.vue +4 -4
  46. package/src/components/ui/LoadingModal.vue +6 -4
  47. package/src/components/ui/Markdown.vue +31 -3
  48. package/src/components/ui/Modal.vue +83 -22
  49. package/src/components/ui/ModalContext.vue +2 -1
  50. package/src/components/ui/ProgressBar.vue +9 -8
  51. package/src/components/ui/PromptModal.vue +8 -5
  52. package/src/components/ui/Select.vue +10 -4
  53. package/src/components/ui/SelectLabel.vue +13 -2
  54. package/src/components/ui/SelectOption.vue +29 -0
  55. package/src/components/ui/SelectOptions.vue +24 -20
  56. package/src/components/ui/SelectTrigger.vue +3 -3
  57. package/src/components/ui/SettingsModal.vue +4 -40
  58. package/src/components/ui/Switch.vue +11 -0
  59. package/src/components/ui/Toast.vue +20 -16
  60. package/src/components/ui/index.ts +6 -0
  61. package/src/directives/measure.ts +11 -5
  62. package/src/errors/Errors.ts +18 -15
  63. package/src/errors/index.ts +6 -2
  64. package/src/errors/settings/Debug.vue +39 -0
  65. package/src/errors/settings/index.ts +10 -0
  66. package/src/forms/FormController.test.ts +32 -9
  67. package/src/forms/FormController.ts +23 -22
  68. package/src/forms/index.ts +0 -1
  69. package/src/forms/utils.ts +34 -34
  70. package/src/index.css +37 -5
  71. package/src/lang/index.ts +5 -1
  72. package/src/lang/settings/Language.vue +48 -0
  73. package/src/lang/settings/index.ts +10 -0
  74. package/src/services/App.state.ts +11 -1
  75. package/src/services/App.ts +9 -1
  76. package/src/services/Events.test.ts +8 -8
  77. package/src/services/Events.ts +2 -8
  78. package/src/services/index.ts +5 -2
  79. package/src/testing/index.ts +4 -0
  80. package/src/ui/UI.state.ts +3 -13
  81. package/src/ui/UI.ts +107 -83
  82. package/src/ui/index.ts +16 -17
  83. package/src/utils/classes.ts +41 -0
  84. package/src/utils/composition/events.ts +2 -4
  85. package/src/utils/composition/forms.ts +16 -1
  86. package/src/utils/composition/state.ts +11 -2
  87. package/src/utils/index.ts +3 -1
  88. package/src/utils/markdown.ts +35 -1
  89. package/src/utils/types.ts +3 -0
  90. package/src/utils/vue.ts +28 -125
  91. package/src/components/composition.ts +0 -23
  92. package/src/components/contracts/shared.ts +0 -9
  93. package/src/components/utils.ts +0 -107
  94. package/src/forms/composition.ts +0 -6
  95. package/src/utils/tailwindcss.test.ts +0 -26
  96. package/src/utils/tailwindcss.ts +0 -7
  97. package/src/utils/vdom.ts +0 -31
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 { 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;
@@ -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>
@@ -78,10 +92,18 @@ export interface ToastOptions {
78
92
  export class UIService extends Service {
79
93
 
80
94
  private modalCallbacks: Record<string, Partial<ModalCallbacks>> = {};
81
- private components: Partial<Record<UIComponent, Component>> = {};
95
+ private components: Partial<UIComponents> = {};
96
+
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
+ }
82
104
 
83
- public requireComponent(name: UIComponent): Component {
84
- return this.components[name] ?? fail(`UI Component '${name}' is not defined!`);
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.openModal<ModalComponent<AlertModalProps>>(
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 modal = await this.openModal<ConfirmModalComponent>(
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 modal = await this.openModal<ModalComponent<PromptModalProps, string | null>>(
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 = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
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: operationOrMessageOrOptions,
241
+ props,
242
+ delay: unref(delay),
231
243
  operationPromise: processOperation(operation as Promise<T> | (() => T)),
232
244
  };
233
245
  };
234
246
 
235
- const { operationPromise, props } = processArgs();
236
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
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;
@@ -251,30 +271,29 @@ export class UIService extends Service {
251
271
  const toast: UIToast = {
252
272
  id: uuid(),
253
273
  properties: { message, ...otherOptions },
254
- component: markRaw(component ?? this.requireComponent(UIComponents.Toast)),
274
+ component: markRaw(component ?? this.requireComponent('toast')),
255
275
  };
256
276
 
257
277
  this.setState('toasts', this.toasts.concat(toast));
258
278
  }
259
279
 
260
- public registerComponent(name: UIComponent, component: Component): void {
261
- this.components[name] = component;
262
- }
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>>>;
263
285
 
264
- public async openModal<TModalComponent extends ModalComponent>(
265
- component: TModalComponent,
266
- properties?: ModalProperties<TModalComponent>,
267
- ): Promise<UIModal<ModalResult<TModalComponent>>> {
286
+ public async modal<T extends Component>(component: T, props?: ComponentProps<T>): Promise<UIModal<ModalResult<T>>> {
268
287
  const id = uuid();
269
- const callbacks: Partial<ModalCallbacks<ModalResult<TModalComponent>>> = {};
270
- const modal: UIModal<ModalResult<TModalComponent>> = {
288
+ const callbacks: Partial<ModalCallbacks<ModalResult<T>>> = {};
289
+ const modal: UIModal<ModalResult<T>> = {
271
290
  id,
272
- properties: properties ?? {},
291
+ closing: false,
292
+ properties: props ?? {},
273
293
  component: markRaw(component),
274
294
  beforeClose: new Promise((resolve) => (callbacks.willClose = resolve)),
275
- afterClose: new Promise((resolve) => (callbacks.closed = resolve)),
295
+ afterClose: new Promise((resolve) => (callbacks.hasClosed = resolve)),
276
296
  };
277
- const activeModal = this.modals.at(-1);
278
297
  const modals = this.modals.concat(modal);
279
298
 
280
299
  this.modalCallbacks[modal.id] = callbacks;
@@ -282,15 +301,26 @@ export class UIService extends Service {
282
301
  this.setState({ modals });
283
302
 
284
303
  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
304
 
291
305
  return modal;
292
306
  }
293
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
+
294
324
  public async closeModal(id: string, result?: unknown): Promise<void> {
295
325
  if (!App.isMounted()) {
296
326
  await this.removeModal(id, result);
@@ -319,25 +349,23 @@ export class UIService extends Service {
319
349
  this.modals.filter((m) => m.id !== id),
320
350
  );
321
351
 
322
- this.modalCallbacks[id]?.closed?.(result);
352
+ this.modalCallbacks[id]?.hasClosed?.(result);
323
353
 
324
354
  delete this.modalCallbacks[id];
325
-
326
- const activeModal = this.modals.at(-1);
327
-
328
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
329
355
  }
330
356
 
331
357
  private watchModalEvents(): void {
332
- Events.on('modal-will-close', ({ modal, result }) => {
333
- this.modalCallbacks[modal.id]?.willClose?.(result);
358
+ Events.on('modal-will-close', ({ modal: { id }, result }) => {
359
+ const modal = this.modals.find((_modal) => id === _modal.id);
334
360
 
335
- if (this.modals.length === 1) {
336
- Events.emit('hide-overlays-backdrop');
361
+ if (modal) {
362
+ modal.closing = true;
337
363
  }
364
+
365
+ this.modalCallbacks[id]?.willClose?.(result);
338
366
  });
339
367
 
340
- Events.on('modal-closed', async ({ modal: { id }, result }) => {
368
+ Events.on('modal-has-closed', async ({ modal: { id }, result }) => {
341
369
  await this.removeModal(id, result);
342
370
  });
343
371
  }
@@ -381,11 +409,7 @@ export default facade(UIService);
381
409
  declare module '@aerogel/core/services/Events' {
382
410
  export interface EventsPayload {
383
411
  'close-modal': { id: string; result?: unknown };
384
- 'hide-modal': { id: string };
385
- 'hide-overlays-backdrop': void;
386
- 'modal-closed': { modal: UIModal; result?: unknown };
387
412
  'modal-will-close': { modal: UIModal; result?: unknown };
388
- 'show-modal': { id: string };
389
- 'show-overlays-backdrop': void;
413
+ 'modal-has-closed': { modal: UIModal; result?: unknown };
390
414
  }
391
415
  }
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
 
@@ -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
- 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
  }
@@ -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 '@vue/runtime-core';
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>(
@@ -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 './tailwindcss';
8
+ export * from './types';
7
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;