@aerogel/core 0.0.0-next.bde642c4a8096c5fc3d5e676c2115da23f4bf1d8 → 0.0.0-next.c4825c5cbe0fe3257e478c2a7ec8df27d5a72305

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 (60) hide show
  1. package/dist/aerogel-core.cjs.js +1 -1
  2. package/dist/aerogel-core.cjs.js.map +1 -1
  3. package/dist/aerogel-core.d.ts +371 -69
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/package.json +3 -3
  7. package/src/bootstrap/index.ts +12 -2
  8. package/src/components/headless/forms/AGHeadlessInput.ts +1 -0
  9. package/src/components/headless/forms/AGHeadlessInput.vue +7 -0
  10. package/src/components/headless/forms/AGHeadlessInputInput.vue +1 -0
  11. package/src/components/headless/forms/AGHeadlessInputTextArea.vue +1 -0
  12. package/src/components/headless/modals/AGHeadlessModal.ts +3 -1
  13. package/src/components/headless/modals/AGHeadlessModal.vue +10 -4
  14. package/src/components/headless/modals/AGHeadlessModalPanel.vue +10 -6
  15. package/src/components/headless/modals/AGHeadlessModalTitle.vue +14 -4
  16. package/src/components/lib/AGMarkdown.vue +14 -1
  17. package/src/components/lib/AGMeasured.vue +1 -0
  18. package/src/components/lib/AGProgressBar.vue +30 -0
  19. package/src/components/lib/index.ts +1 -0
  20. package/src/components/modals/AGAlertModal.ts +5 -2
  21. package/src/components/modals/AGConfirmModal.ts +14 -5
  22. package/src/components/modals/AGConfirmModal.vue +2 -2
  23. package/src/components/modals/AGErrorReportModal.ts +5 -2
  24. package/src/components/modals/AGLoadingModal.ts +10 -4
  25. package/src/components/modals/AGModal.ts +1 -0
  26. package/src/components/modals/AGModalContext.vue +14 -4
  27. package/src/components/modals/AGPromptModal.ts +9 -4
  28. package/src/directives/measure.ts +24 -5
  29. package/src/errors/JobCancelledError.ts +3 -0
  30. package/src/errors/utils.ts +16 -0
  31. package/src/forms/Form.test.ts +28 -0
  32. package/src/forms/Form.ts +24 -6
  33. package/src/forms/index.ts +2 -1
  34. package/src/forms/utils.ts +20 -4
  35. package/src/forms/validation.ts +19 -0
  36. package/src/jobs/Job.ts +144 -2
  37. package/src/jobs/index.ts +4 -1
  38. package/src/jobs/listeners.ts +3 -0
  39. package/src/jobs/status.ts +4 -0
  40. package/src/lang/Lang.ts +4 -0
  41. package/src/services/App.state.ts +9 -1
  42. package/src/services/App.ts +5 -0
  43. package/src/services/Events.ts +13 -3
  44. package/src/services/Service.ts +107 -44
  45. package/src/services/Storage.ts +20 -0
  46. package/src/services/index.ts +7 -2
  47. package/src/services/utils.ts +18 -0
  48. package/src/testing/setup.ts +11 -3
  49. package/src/ui/UI.state.ts +7 -0
  50. package/src/ui/UI.ts +136 -43
  51. package/src/ui/index.ts +1 -0
  52. package/src/ui/utils.ts +16 -0
  53. package/src/utils/composition/persistent.test.ts +33 -0
  54. package/src/utils/composition/persistent.ts +11 -0
  55. package/src/utils/composition/state.test.ts +47 -0
  56. package/src/utils/composition/state.ts +24 -0
  57. package/src/utils/index.ts +1 -0
  58. package/src/utils/markdown.test.ts +50 -0
  59. package/src/utils/markdown.ts +17 -2
  60. package/src/utils/vue.ts +11 -1
package/src/ui/UI.ts CHANGED
@@ -1,14 +1,17 @@
1
- import { after, facade, fail, uuid } from '@noeldemartin/utils';
1
+ import { after, facade, fail, required, uuid } from '@noeldemartin/utils';
2
2
  import { markRaw, nextTick } from 'vue';
3
3
  import type { Component } from 'vue';
4
4
  import type { ObjectValues } from '@noeldemartin/utils';
5
5
 
6
+ import App from '@/services/App';
6
7
  import Events from '@/services/Events';
8
+ import type { AcceptRefs } from '@/utils';
7
9
  import type { Color } from '@/components/constants';
8
10
  import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
9
11
  import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
10
12
 
11
13
  import Service from './UI.state';
14
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
12
15
  import type { Modal, ModalComponent, Snackbar } from './UI.state';
13
16
 
14
17
  interface ModalCallbacks<T = unknown> {
@@ -33,14 +36,28 @@ export const UIComponents = {
33
36
 
34
37
  export type UIComponent = ObjectValues<typeof UIComponents>;
35
38
 
36
- export interface ConfirmOptions {
39
+ export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
40
+
41
+ export type ConfirmOptions = AcceptRefs<{
37
42
  acceptText?: string;
38
43
  acceptColor?: Color;
39
44
  cancelText?: string;
40
45
  cancelColor?: Color;
46
+ actions?: Record<string, () => unknown>;
47
+ required?: boolean;
48
+ }>;
49
+
50
+ export type LoadingOptions = AcceptRefs<{
51
+ title?: string;
52
+ message?: string;
53
+ progress?: number;
54
+ }>;
55
+
56
+ export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
57
+ checkboxes?: T;
41
58
  }
42
59
 
43
- export interface PromptOptions {
60
+ export type PromptOptions = AcceptRefs<{
44
61
  label?: string;
45
62
  defaultValue?: string;
46
63
  placeholder?: string;
@@ -49,7 +66,7 @@ export interface PromptOptions {
49
66
  cancelText?: string;
50
67
  cancelColor?: Color;
51
68
  trim?: boolean;
52
- }
69
+ }>;
53
70
 
54
71
  export interface ShowSnackbarOptions {
55
72
  component?: Component;
@@ -83,35 +100,66 @@ export class UIService extends Service {
83
100
  this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
84
101
  }
85
102
 
103
+ /* eslint-disable max-len */
86
104
  public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
87
105
  public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
106
+ public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
107
+ public async confirm<T extends ConfirmCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
108
+ /* eslint-enable max-len */
109
+
88
110
  public async confirm(
89
111
  messageOrTitle: string,
90
- messageOrOptions?: string | ConfirmOptions,
91
- options?: ConfirmOptions,
92
- ): Promise<boolean> {
112
+ messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
113
+ options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
114
+ ): Promise<boolean | [boolean, Record<string, boolean>]> {
93
115
  const getProperties = (): AGConfirmModalProps => {
94
116
  if (typeof messageOrOptions !== 'string') {
95
117
  return {
96
- message: messageOrTitle,
97
118
  ...(messageOrOptions ?? {}),
119
+ message: messageOrTitle,
120
+ required: !!messageOrOptions?.required,
98
121
  };
99
122
  }
100
123
 
101
124
  return {
125
+ ...(options ?? {}),
102
126
  title: messageOrTitle,
103
127
  message: messageOrOptions,
104
- ...(options ?? {}),
128
+ required: !!options?.required,
105
129
  };
106
130
  };
107
-
108
- const modal = await this.openModal<ModalComponent<AGConfirmModalProps, boolean>>(
109
- this.requireComponent(UIComponents.ConfirmModal),
110
- getProperties(),
111
- );
131
+ const properties = getProperties();
132
+ const modal = await this.openModal<
133
+ ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
134
+ >(this.requireComponent(UIComponents.ConfirmModal), properties);
112
135
  const result = await modal.beforeClose;
113
136
 
114
- return result ?? false;
137
+ const confirmed = typeof result === 'object' ? result[0] : result ?? false;
138
+ const checkboxes =
139
+ typeof result === 'object'
140
+ ? result[1]
141
+ : Object.entries(properties.checkboxes ?? {}).reduce(
142
+ (values, [checkbox, { default: defaultValue }]) => ({
143
+ [checkbox]: defaultValue ?? false,
144
+ ...values,
145
+ }),
146
+ {} as Record<string, boolean>,
147
+ );
148
+
149
+ for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
150
+ if (!checkbox.required || checkboxes[name]) {
151
+ continue;
152
+ }
153
+
154
+ if (confirmed && App.development) {
155
+ // eslint-disable-next-line no-console
156
+ console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
157
+ }
158
+
159
+ return [false, checkboxes];
160
+ }
161
+
162
+ return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
115
163
  }
116
164
 
117
165
  public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
@@ -127,14 +175,14 @@ export class UIService extends Service {
127
175
  return {
128
176
  message: messageOrTitle,
129
177
  ...(messageOrOptions ?? {}),
130
- };
178
+ } as AGPromptModalProps;
131
179
  }
132
180
 
133
181
  return {
134
182
  title: messageOrTitle,
135
183
  message: messageOrOptions,
136
184
  ...(options ?? {}),
137
- };
185
+ } as AGPromptModalProps;
138
186
  };
139
187
 
140
188
  const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
@@ -147,23 +195,37 @@ export class UIService extends Service {
147
195
  return result ?? null;
148
196
  }
149
197
 
150
- public async loading<T>(operation: Promise<T>): Promise<T>;
151
- public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
152
- public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
153
- const getProperties = (): AGLoadingModalProps => {
154
- if (typeof messageOrOperation !== 'string') {
155
- return {};
198
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
199
+ public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
200
+ public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
201
+ public async loading<T>(
202
+ operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
203
+ operation?: Promise<T> | (() => T),
204
+ ): Promise<T> {
205
+ const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
206
+ const processArgs = (): { operationPromise: Promise<T>; props?: AGLoadingModalProps } => {
207
+ if (typeof operationOrMessageOrOptions === 'string') {
208
+ return {
209
+ props: { message: operationOrMessageOrOptions },
210
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
211
+ };
156
212
  }
157
213
 
158
- return { message: messageOrOperation };
214
+ if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
215
+ return { operationPromise: processOperation(operationOrMessageOrOptions) };
216
+ }
217
+
218
+ return {
219
+ props: operationOrMessageOrOptions,
220
+ operationPromise: processOperation(operation as Promise<T> | (() => T)),
221
+ };
159
222
  };
160
223
 
161
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
224
+ const { operationPromise, props } = processArgs();
225
+ const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
162
226
 
163
227
  try {
164
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
165
-
166
- const [result] = await Promise.all([operation, after({ seconds: 1 })]);
228
+ const [result] = await Promise.all([operationPromise, after({ seconds: 1 })]);
167
229
 
168
230
  return result;
169
231
  } finally {
@@ -225,12 +287,40 @@ export class UIService extends Service {
225
287
  }
226
288
 
227
289
  public async closeModal(id: string, result?: unknown): Promise<void> {
290
+ if (!App.isMounted()) {
291
+ await this.removeModal(id, result);
292
+
293
+ return;
294
+ }
295
+
228
296
  await Events.emit('close-modal', { id, result });
229
297
  }
230
298
 
299
+ public async closeAllModals(): Promise<void> {
300
+ while (this.modals.length > 0) {
301
+ await this.closeModal(required(this.modals[this.modals.length - 1]).id);
302
+ }
303
+ }
304
+
231
305
  protected async boot(): Promise<void> {
232
306
  this.watchModalEvents();
233
307
  this.watchMountedEvent();
308
+ this.watchViewportBreakpoints();
309
+ }
310
+
311
+ private async removeModal(id: string, result?: unknown): Promise<void> {
312
+ this.setState(
313
+ 'modals',
314
+ this.modals.filter((m) => m.id !== id),
315
+ );
316
+
317
+ this.modalCallbacks[id]?.closed?.(result);
318
+
319
+ delete this.modalCallbacks[id];
320
+
321
+ const activeModal = this.modals.at(-1);
322
+
323
+ await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
234
324
  }
235
325
 
236
326
  private watchModalEvents(): void {
@@ -242,31 +332,24 @@ export class UIService extends Service {
242
332
  }
243
333
  });
244
334
 
245
- Events.on('modal-closed', async ({ modal, result }) => {
246
- this.setState(
247
- 'modals',
248
- this.modals.filter((m) => m.id !== modal.id),
249
- );
250
-
251
- this.modalCallbacks[modal.id]?.closed?.(result);
252
-
253
- delete this.modalCallbacks[modal.id];
254
-
255
- const activeModal = this.modals.at(-1);
256
-
257
- await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
335
+ Events.on('modal-closed', async ({ modal: { id }, result }) => {
336
+ await this.removeModal(id, result);
258
337
  });
259
338
  }
260
339
 
261
340
  private watchMountedEvent(): void {
262
341
  Events.once('application-mounted', async () => {
263
- const splash = document.getElementById('splash');
342
+ if (!globalThis.document || !globalThis.getComputedStyle) {
343
+ return;
344
+ }
345
+
346
+ const splash = globalThis.document.getElementById('splash');
264
347
 
265
348
  if (!splash) {
266
349
  return;
267
350
  }
268
351
 
269
- if (window.getComputedStyle(splash).opacity !== '0') {
352
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
270
353
  splash.style.opacity = '0';
271
354
 
272
355
  await after({ ms: 600 });
@@ -276,6 +359,16 @@ export class UIService extends Service {
276
359
  });
277
360
  }
278
361
 
362
+ private watchViewportBreakpoints(): void {
363
+ if (!globalThis.matchMedia) {
364
+ return;
365
+ }
366
+
367
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
368
+
369
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
370
+ }
371
+
279
372
  }
280
373
 
281
374
  export default facade(UIService);
package/src/ui/index.ts CHANGED
@@ -16,6 +16,7 @@ import type { UIComponent } from './UI';
16
16
  const services = { $ui: UI };
17
17
 
18
18
  export * from './UI';
19
+ export * from './utils';
19
20
  export { default as UI } from './UI';
20
21
 
21
22
  export type UIServices = typeof services;
@@ -0,0 +1,16 @@
1
+ export const MOBILE_BREAKPOINT = 768;
2
+
3
+ export const Layouts = {
4
+ Mobile: 'mobile',
5
+ Desktop: 'desktop',
6
+ } as const;
7
+
8
+ export type Layout = (typeof Layouts)[keyof typeof Layouts];
9
+
10
+ export function getCurrentLayout(): Layout {
11
+ if (globalThis.innerWidth > MOBILE_BREAKPOINT) {
12
+ return Layouts.Desktop;
13
+ }
14
+
15
+ return Layouts.Mobile;
16
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { nextTick } from 'vue';
3
+ import { Storage } from '@noeldemartin/utils';
4
+
5
+ import { persistent } from './persistent';
6
+
7
+ describe('Vue persistent helper', () => {
8
+
9
+ it('serializes to localStorage', async () => {
10
+ // Arrange
11
+ const store = persistent<{ foo?: string }>('foobar', {});
12
+
13
+ // Act
14
+ store.foo = 'bar';
15
+
16
+ await nextTick();
17
+
18
+ // Assert
19
+ expect(Storage.get('foobar')).toEqual({ foo: 'bar' });
20
+ });
21
+
22
+ it('reads from localStorage', async () => {
23
+ // Arrange
24
+ Storage.set('foobar', { foo: 'bar' });
25
+
26
+ // Act
27
+ const store = persistent<{ foo?: string }>('foobar', {});
28
+
29
+ // Assert
30
+ expect(store.foo).toEqual('bar');
31
+ });
32
+
33
+ });
@@ -0,0 +1,11 @@
1
+ import { reactive, toRaw, watch } from 'vue';
2
+ import { Storage } from '@noeldemartin/utils';
3
+ import type { UnwrapNestedRefs } from 'vue';
4
+
5
+ export function persistent<T extends object>(name: string, defaults: T): UnwrapNestedRefs<T> {
6
+ const store = reactive<T>(Storage.get<T>(name) ?? defaults);
7
+
8
+ watch(store, () => Storage.set(name, toRaw(store)));
9
+
10
+ return store;
11
+ }
@@ -0,0 +1,47 @@
1
+ import { after } from '@noeldemartin/utils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { ref } from 'vue';
4
+
5
+ import { computedDebounce } from './state';
6
+
7
+ describe('Vue state helpers', () => {
8
+
9
+ it('computes debounced state', async () => {
10
+ // Initial
11
+ const state = ref(0);
12
+ const value = computedDebounce({ delay: 90 }, () => state.value);
13
+
14
+ expect(value.value).toBe(null);
15
+
16
+ await after({ ms: 100 });
17
+
18
+ expect(value.value).toBe(0);
19
+
20
+ // Update
21
+ state.value = 42;
22
+
23
+ expect(value.value).toBe(0);
24
+
25
+ await after({ ms: 100 });
26
+
27
+ expect(value.value).toBe(42);
28
+
29
+ // Debounced Update
30
+ state.value = 23;
31
+
32
+ expect(value.value).toBe(42);
33
+
34
+ await after({ ms: 50 });
35
+
36
+ state.value = 32;
37
+
38
+ await after({ ms: 50 });
39
+
40
+ expect(value.value).toBe(42);
41
+
42
+ await after({ ms: 100 });
43
+
44
+ expect(value.value).toBe(32);
45
+ });
46
+
47
+ });
@@ -0,0 +1,24 @@
1
+ import { debounce } from '@noeldemartin/utils';
2
+ import { ref, watchEffect } from 'vue';
3
+ import type { ComputedGetter, ComputedRef } from '@vue/runtime-core';
4
+
5
+ export interface ComputedDebounceOptions<T> {
6
+ initial?: T;
7
+ delay?: number;
8
+ }
9
+
10
+ export function computedDebounce<T>(options: ComputedDebounceOptions<T>, getter: ComputedGetter<T>): ComputedRef<T>;
11
+ export function computedDebounce<T>(getter: ComputedGetter<T>): ComputedRef<T | null>;
12
+ export function computedDebounce<T>(
13
+ optionsOrGetter: ComputedGetter<T> | ComputedDebounceOptions<T>,
14
+ inputGetter?: ComputedGetter<T>,
15
+ ): ComputedRef<T> {
16
+ const inputOptions = inputGetter ? (optionsOrGetter as ComputedDebounceOptions<T>) : {};
17
+ const getter = inputGetter ?? (optionsOrGetter as ComputedGetter<T>);
18
+ const state = ref(inputOptions.initial ?? null);
19
+ const update = debounce((value) => (state.value = value), inputOptions.delay ?? 300);
20
+
21
+ watchEffect(() => update(getter()));
22
+
23
+ return state as unknown as ComputedRef<T>;
24
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './composition/events';
2
2
  export * from './composition/forms';
3
3
  export * from './composition/hooks';
4
+ export * from './composition/persistent';
4
5
  export * from './tailwindcss';
5
6
  export * from './vue';
@@ -0,0 +1,50 @@
1
+ /* eslint-disable max-len */
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { renderMarkdown } from './markdown';
5
+
6
+ describe('Markdown utils', () => {
7
+
8
+ it('renders basic markdown', () => {
9
+ // Arrange
10
+ const expectedHTML = `
11
+ <h1>Title</h1>
12
+ <p>body with <a target="_blank" href="https://example.com">link</a></p>
13
+ <ul>
14
+ <li>One</li>
15
+ <li>Two</li>
16
+ <li>Three</li>
17
+ </ul>
18
+ `;
19
+
20
+ // Act
21
+ const html = renderMarkdown(
22
+ ['# Title', 'body with [link](https://example.com)', '- One', '- Two', '- Three'].join('\n'),
23
+ );
24
+
25
+ // Assert
26
+ expect(normalizeHTML(html)).toMatch(new RegExp(normalizeHTML(expectedHTML)));
27
+ });
28
+
29
+ it('renders button links', () => {
30
+ // Arrange
31
+ const expectedHTML = `
32
+ <p><button type="button" data-markdown-action="do-something">link</button></p>
33
+ `;
34
+
35
+ // Act
36
+ const html = renderMarkdown('[link](#action:do-something)');
37
+
38
+ // Assert
39
+ expect(normalizeHTML(html)).toMatch(new RegExp(normalizeHTML(expectedHTML)));
40
+ });
41
+
42
+ });
43
+
44
+ function normalizeHTML(html: string): string {
45
+ return html
46
+ .split('\n')
47
+ .map((line) => line.trim())
48
+ .join('\n')
49
+ .trim();
50
+ }
@@ -1,5 +1,5 @@
1
- import { tap } from '@noeldemartin/utils';
2
1
  import DOMPurify from 'dompurify';
2
+ import { stringMatchAll, tap } from '@noeldemartin/utils';
3
3
  import { Renderer, marked } from 'marked';
4
4
 
5
5
  function makeRenderer(): Renderer {
@@ -10,8 +10,23 @@ function makeRenderer(): Renderer {
10
10
  });
11
11
  }
12
12
 
13
+ function renderActionLinks(html: string): string {
14
+ const matches = stringMatchAll<3>(html, /<a[^>]*href="#action:([^"]+)"[^>]*>([^<]+)<\/a>/g);
15
+
16
+ for (const [link, action, text] of matches) {
17
+ html = html.replace(link, `<button type="button" data-markdown-action="${action}">${text}</button>`);
18
+ }
19
+
20
+ return html;
21
+ }
22
+
13
23
  export function renderMarkdown(markdown: string): string {
14
- return safeHtml(marked(markdown, { mangle: false, headerIds: false, renderer: makeRenderer() }));
24
+ let html = marked(markdown, { mangle: false, headerIds: false, renderer: makeRenderer() });
25
+
26
+ html = safeHtml(html);
27
+ html = renderActionLinks(html);
28
+
29
+ return html;
15
30
  }
16
31
 
17
32
  export function safeHtml(html: string): string {
package/src/utils/vue.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fail } from '@noeldemartin/utils';
2
2
  import { computed, inject, reactive, ref, watch } from 'vue';
3
- import type { Directive, InjectionKey, PropType, Ref, UnwrapNestedRefs } from 'vue';
3
+ import type { Directive, InjectionKey, MaybeRef, PropType, Ref, UnwrapNestedRefs } from 'vue';
4
4
 
5
5
  type BaseProp<T> = {
6
6
  type?: PropType<T>;
@@ -10,7 +10,10 @@ type BaseProp<T> = {
10
10
  type RequiredProp<T> = BaseProp<T> & { required: true };
11
11
  type OptionalProp<T> = BaseProp<T> & { default: T | (() => T) | null };
12
12
 
13
+ export type AcceptRefs<T> = { [K in keyof T]: T[K] | RefUnion<T[K]> };
13
14
  export type ComponentProps = Record<string, unknown>;
15
+ export type RefUnion<T> = T extends infer R ? Ref<R> : never;
16
+ export type Unref<T> = { [K in keyof T]: T[K] extends MaybeRef<infer Value> ? Value : T[K] };
14
17
 
15
18
  export function arrayProp<T>(defaultValue?: () => T[]): OptionalProp<T[]> {
16
19
  return {
@@ -73,6 +76,13 @@ export function injectOrFail<T>(key: InjectionKey<T> | string, errorMessage?: st
73
76
  return inject(key) ?? fail(errorMessage ?? `Could not resolve '${key}' injection key`);
74
77
  }
75
78
 
79
+ export function listenerProp<T extends Function = Function>(): OptionalProp<T | null> {
80
+ return {
81
+ type: Function as PropType<T>,
82
+ default: null,
83
+ };
84
+ }
85
+
76
86
  export function mixedProp<T>(type?: PropType<T>): OptionalProp<T | null>;
77
87
  export function mixedProp<T>(type: PropType<T>, defaultValue: T): OptionalProp<T>;
78
88
  export function mixedProp<T>(type?: PropType<T>, defaultValue?: T): OptionalProp<T | null> {