@aerogel/core 0.0.0-next.f1f5a990033d966dc0bb12d251110fbc9350dcc7 → 0.0.0-next.f8c757d83e1e0d001a2836fa45aba318ec17b9b9

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 (197) hide show
  1. package/dist/aerogel-core.d.ts +2144 -1268
  2. package/dist/aerogel-core.js +3234 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +30 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +27 -14
  7. package/src/bootstrap/options.ts +4 -1
  8. package/src/components/{AGAppLayout.vue → AppLayout.vue} +4 -4
  9. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  10. package/src/components/{AGAppOverlays.vue → AppOverlays.vue} +5 -10
  11. package/src/components/AppToasts.vue +16 -0
  12. package/src/components/contracts/AlertModal.ts +4 -0
  13. package/src/components/contracts/Button.ts +16 -0
  14. package/src/components/contracts/ConfirmModal.ts +41 -0
  15. package/src/components/contracts/DropdownMenu.ts +18 -0
  16. package/src/components/contracts/ErrorReportModal.ts +29 -0
  17. package/src/components/contracts/Input.ts +26 -0
  18. package/src/components/contracts/LoadingModal.ts +18 -0
  19. package/src/components/contracts/Modal.ts +13 -0
  20. package/src/components/contracts/PromptModal.ts +30 -0
  21. package/src/components/contracts/Select.ts +44 -0
  22. package/src/components/contracts/Toast.ts +13 -0
  23. package/src/components/contracts/index.ts +9 -0
  24. package/src/components/contracts/shared.ts +9 -0
  25. package/src/components/headless/HeadlessButton.vue +51 -0
  26. package/src/components/headless/HeadlessInput.vue +59 -0
  27. package/src/components/headless/HeadlessInputDescription.vue +27 -0
  28. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  29. package/src/components/headless/HeadlessInputInput.vue +75 -0
  30. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  31. package/src/components/headless/HeadlessInputTextArea.vue +40 -0
  32. package/src/components/headless/{modals/AGHeadlessModal.vue → HeadlessModal.vue} +18 -18
  33. package/src/components/headless/HeadlessModalContent.vue +24 -0
  34. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  35. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  36. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  37. package/src/components/headless/HeadlessSelect.vue +113 -0
  38. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
  39. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  40. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  41. package/src/components/headless/HeadlessSelectOptions.vue +37 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  44. package/src/components/headless/HeadlessToast.vue +18 -0
  45. package/src/components/headless/HeadlessToastAction.vue +13 -0
  46. package/src/components/headless/index.ts +19 -3
  47. package/src/components/index.ts +6 -9
  48. package/src/components/ui/AdvancedOptions.vue +18 -0
  49. package/src/components/ui/AlertModal.vue +13 -0
  50. package/src/components/ui/Button.vue +98 -0
  51. package/src/components/ui/Checkbox.vue +56 -0
  52. package/src/components/ui/ConfirmModal.vue +42 -0
  53. package/src/components/ui/DropdownMenu.vue +27 -0
  54. package/src/components/ui/DropdownMenuOption.vue +14 -0
  55. package/src/components/ui/DropdownMenuOptions.vue +27 -0
  56. package/src/components/ui/EditableContent.vue +82 -0
  57. package/src/components/ui/ErrorMessage.vue +15 -0
  58. package/src/components/ui/ErrorReportModal.vue +62 -0
  59. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  60. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  61. package/src/components/ui/Form.vue +24 -0
  62. package/src/components/ui/Input.vue +56 -0
  63. package/src/components/ui/Link.vue +12 -0
  64. package/src/components/ui/LoadingModal.vue +32 -0
  65. package/src/components/ui/Markdown.vue +69 -0
  66. package/src/components/ui/Modal.vue +75 -0
  67. package/src/components/ui/ModalContext.vue +30 -0
  68. package/src/components/ui/ProgressBar.vue +50 -0
  69. package/src/components/ui/PromptModal.vue +35 -0
  70. package/src/components/ui/Select.vue +25 -0
  71. package/src/components/ui/SelectLabel.vue +17 -0
  72. package/src/components/ui/SelectOption.vue +29 -0
  73. package/src/components/ui/SelectOptions.vue +30 -0
  74. package/src/components/ui/SelectTrigger.vue +29 -0
  75. package/src/components/ui/SettingsModal.vue +15 -0
  76. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  77. package/src/components/ui/Toast.vue +42 -0
  78. package/src/components/ui/index.ts +30 -0
  79. package/src/directives/index.ts +11 -5
  80. package/src/directives/measure.ts +34 -6
  81. package/src/errors/Errors.state.ts +1 -1
  82. package/src/errors/Errors.ts +29 -33
  83. package/src/errors/JobCancelledError.ts +3 -0
  84. package/src/errors/index.ts +10 -16
  85. package/src/errors/utils.ts +35 -0
  86. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  87. package/src/forms/{Form.ts → FormController.ts} +86 -25
  88. package/src/forms/composition.ts +4 -4
  89. package/src/forms/index.ts +3 -1
  90. package/src/forms/utils.ts +36 -5
  91. package/src/forms/validation.ts +19 -0
  92. package/src/index.css +46 -0
  93. package/src/{main.ts → index.ts} +3 -0
  94. package/src/jobs/Job.ts +147 -0
  95. package/src/jobs/index.ts +10 -0
  96. package/src/jobs/listeners.ts +3 -0
  97. package/src/jobs/status.ts +4 -0
  98. package/src/lang/DefaultLangProvider.ts +46 -0
  99. package/src/lang/Lang.state.ts +11 -0
  100. package/src/lang/Lang.ts +44 -29
  101. package/src/lang/index.ts +11 -6
  102. package/src/lang/settings/Language.vue +48 -0
  103. package/src/lang/settings/index.ts +10 -0
  104. package/src/plugins/Plugin.ts +1 -1
  105. package/src/plugins/index.ts +10 -7
  106. package/src/services/App.state.ts +37 -5
  107. package/src/services/App.ts +35 -6
  108. package/src/services/Cache.ts +43 -0
  109. package/src/services/Events.test.ts +39 -0
  110. package/src/services/Events.ts +112 -32
  111. package/src/services/Service.ts +150 -49
  112. package/src/services/Storage.ts +20 -0
  113. package/src/services/index.ts +17 -5
  114. package/src/services/store.ts +8 -5
  115. package/src/services/utils.ts +18 -0
  116. package/src/testing/index.ts +26 -0
  117. package/src/testing/setup.ts +11 -0
  118. package/src/ui/UI.state.ts +19 -7
  119. package/src/ui/UI.ts +218 -70
  120. package/src/ui/index.ts +19 -16
  121. package/src/ui/utils.ts +16 -0
  122. package/src/utils/classes.ts +49 -0
  123. package/src/utils/composition/events.ts +2 -2
  124. package/src/utils/composition/forms.ts +14 -4
  125. package/src/utils/composition/persistent.test.ts +33 -0
  126. package/src/utils/composition/persistent.ts +11 -0
  127. package/src/utils/composition/state.test.ts +47 -0
  128. package/src/utils/composition/state.ts +33 -0
  129. package/src/utils/index.ts +4 -1
  130. package/src/utils/markdown.test.ts +50 -0
  131. package/src/utils/markdown.ts +19 -6
  132. package/src/utils/vue.ts +28 -127
  133. package/dist/aerogel-core.cjs.js +0 -2
  134. package/dist/aerogel-core.cjs.js.map +0 -1
  135. package/dist/aerogel-core.esm.js +0 -2
  136. package/dist/aerogel-core.esm.js.map +0 -1
  137. package/histoire.config.ts +0 -7
  138. package/noeldemartin.config.js +0 -5
  139. package/postcss.config.js +0 -6
  140. package/src/assets/histoire.css +0 -3
  141. package/src/components/AGAppSnackbars.vue +0 -13
  142. package/src/components/constants.ts +0 -8
  143. package/src/components/forms/AGButton.vue +0 -44
  144. package/src/components/forms/AGCheckbox.vue +0 -35
  145. package/src/components/forms/AGForm.vue +0 -26
  146. package/src/components/forms/AGInput.vue +0 -36
  147. package/src/components/forms/AGSelect.story.vue +0 -28
  148. package/src/components/forms/AGSelect.vue +0 -53
  149. package/src/components/forms/index.ts +0 -5
  150. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  151. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  152. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  153. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  154. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -39
  155. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -76
  156. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  157. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  158. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  159. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  160. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  161. package/src/components/headless/forms/index.ts +0 -14
  162. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  163. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  164. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  165. package/src/components/headless/modals/index.ts +0 -4
  166. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  167. package/src/components/headless/snackbars/index.ts +0 -40
  168. package/src/components/lib/AGErrorMessage.vue +0 -16
  169. package/src/components/lib/AGLink.vue +0 -9
  170. package/src/components/lib/AGMarkdown.vue +0 -36
  171. package/src/components/lib/AGMeasured.vue +0 -15
  172. package/src/components/lib/index.ts +0 -5
  173. package/src/components/modals/AGAlertModal.ts +0 -15
  174. package/src/components/modals/AGAlertModal.vue +0 -14
  175. package/src/components/modals/AGConfirmModal.ts +0 -27
  176. package/src/components/modals/AGConfirmModal.vue +0 -26
  177. package/src/components/modals/AGErrorReportModal.ts +0 -46
  178. package/src/components/modals/AGErrorReportModal.vue +0 -54
  179. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  180. package/src/components/modals/AGLoadingModal.ts +0 -23
  181. package/src/components/modals/AGLoadingModal.vue +0 -15
  182. package/src/components/modals/AGModal.ts +0 -10
  183. package/src/components/modals/AGModal.vue +0 -39
  184. package/src/components/modals/AGModalContext.ts +0 -8
  185. package/src/components/modals/AGModalContext.vue +0 -22
  186. package/src/components/modals/AGModalTitle.vue +0 -9
  187. package/src/components/modals/index.ts +0 -26
  188. package/src/components/snackbars/AGSnackbar.vue +0 -36
  189. package/src/components/snackbars/index.ts +0 -3
  190. package/src/components/utils.ts +0 -10
  191. package/src/directives/initial-focus.ts +0 -11
  192. package/src/main.histoire.ts +0 -1
  193. package/src/utils/tailwindcss.test.ts +0 -26
  194. package/src/utils/tailwindcss.ts +0 -7
  195. package/tailwind.config.js +0 -4
  196. package/tsconfig.json +0 -11
  197. package/vite.config.ts +0 -14
package/src/ui/index.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  import type { Component } from 'vue';
2
2
 
3
- import { bootServices } from '@/services';
4
- import { definePlugin } from '@/plugins';
3
+ import AlertModal from '@aerogel/core/components/ui/AlertModal.vue';
4
+ import ConfirmModal from '@aerogel/core/components/ui/ConfirmModal.vue';
5
+ import ErrorReportModal from '@aerogel/core/components/ui/ErrorReportModal.vue';
6
+ import LoadingModal from '@aerogel/core/components/ui/LoadingModal.vue';
7
+ import PromptModal from '@aerogel/core/components/ui/PromptModal.vue';
8
+ import StartupCrash from '@aerogel/core/components/ui/StartupCrash.vue';
9
+ import Toast from '@aerogel/core/components/ui/Toast.vue';
10
+ import { bootServices } from '@aerogel/core/services';
11
+ import { definePlugin } from '@aerogel/core/plugins';
5
12
 
6
13
  import UI, { UIComponents } from './UI';
7
- import AGAlertModal from '../components/modals/AGAlertModal.vue';
8
- import AGConfirmModal from '../components/modals/AGConfirmModal.vue';
9
- import AGErrorReportModal from '../components/modals/AGErrorReportModal.vue';
10
- import AGLoadingModal from '../components/modals/AGLoadingModal.vue';
11
- import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
12
- import AGStartupCrash from '../components/lib/AGStartupCrash.vue';
13
14
  import type { UIComponent } from './UI';
14
15
 
15
16
  const services = { $ui: UI };
16
17
 
17
18
  export * from './UI';
19
+ export * from './utils';
18
20
  export { default as UI } from './UI';
19
21
 
20
22
  export type UIServices = typeof services;
@@ -22,12 +24,13 @@ export type UIServices = typeof services;
22
24
  export default definePlugin({
23
25
  async install(app, options) {
24
26
  const defaultComponents = {
25
- [UIComponents.AlertModal]: AGAlertModal,
26
- [UIComponents.ConfirmModal]: AGConfirmModal,
27
- [UIComponents.ErrorReportModal]: AGErrorReportModal,
28
- [UIComponents.LoadingModal]: AGLoadingModal,
29
- [UIComponents.Snackbar]: AGSnackbar,
30
- [UIComponents.StartupCrash]: AGStartupCrash,
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,
31
34
  };
32
35
 
33
36
  Object.entries({
@@ -39,12 +42,12 @@ export default definePlugin({
39
42
  },
40
43
  });
41
44
 
42
- declare module '@/bootstrap/options' {
45
+ declare module '@aerogel/core/bootstrap/options' {
43
46
  export interface AerogelOptions {
44
47
  components?: Partial<Record<UIComponent, Component>>;
45
48
  }
46
49
  }
47
50
 
48
- declare module '@/services' {
51
+ declare module '@aerogel/core/services' {
49
52
  export interface Services extends UIServices {}
50
53
  }
@@ -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,49 @@
1
+ import clsx from 'clsx';
2
+ import { computed, 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 { ComputedRef, PropType, Ref } 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 RefsObject<T> = { [K in keyof T]: Ref<T[K]> | T[K] };
12
+ export type Variants<T extends Record<string, string | boolean>> = Required<{
13
+ [K in keyof T]: Exclude<T[K], undefined> extends string
14
+ ? { [key in Exclude<T[K], undefined>]: string | null }
15
+ : { true: string | null; false: string | null };
16
+ }>;
17
+
18
+ export type ComponentPropDefinitions<T> = {
19
+ [K in keyof T]: {
20
+ type?: PropType<T[K]>;
21
+ default: T[K] | (() => T[K]) | null;
22
+ };
23
+ };
24
+
25
+ export type PickComponentProps<TValues, TDefinitions> = {
26
+ [K in keyof TValues]: K extends keyof TDefinitions ? TValues[K] : never;
27
+ };
28
+
29
+ export function computedVariantClasses<T>(
30
+ value: RefsObject<{ baseClasses?: string } & CVAProps<T>>,
31
+ 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);
39
+
40
+ return extractedValues;
41
+ }, {} as CVAProps<T>);
42
+
43
+ return classes(variants(values), unref(valueBaseClasses));
44
+ });
45
+ }
46
+
47
+ export function classes(...inputs: ClassValue[]): string {
48
+ return twMerge(clsx(inputs));
49
+ }
@@ -1,13 +1,13 @@
1
1
  import { onUnmounted } from 'vue';
2
2
 
3
- import Events from '@/services/Events';
3
+ import Events from '@aerogel/core/services/Events';
4
4
  import type {
5
5
  EventListener,
6
6
  EventWithPayload,
7
7
  EventWithoutPayload,
8
8
  EventsPayload,
9
9
  UnknownEvent,
10
- } from '@/services/Events';
10
+ } from '@aerogel/core/services/Events';
11
11
 
12
12
  export function useEvent<Event extends EventWithoutPayload>(event: Event, listener: () => unknown): void;
13
13
  export function useEvent<Event extends EventWithPayload>(
@@ -1,11 +1,21 @@
1
1
  import { objectWithout } from '@noeldemartin/utils';
2
- import { computed, useAttrs } from 'vue';
2
+ import { computed, inject, onUnmounted, useAttrs } from 'vue';
3
+ import type { ClassValue } from 'clsx';
3
4
  import type { ComputedRef } from 'vue';
5
+ import type { FormController } from '@aerogel/core/forms';
6
+ import type { Nullable } from '@noeldemartin/utils';
4
7
 
5
- export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<unknown>] {
8
+ export function onFormFocus(input: { name: Nullable<string> }, listener: () => unknown): void {
9
+ const form = inject<FormController | null>('form', null);
10
+ const stop = form?.on('focus', (name) => input.name === name && listener());
11
+
12
+ onUnmounted(() => stop?.());
13
+ }
14
+
15
+ export function useInputAttrs(): [ComputedRef<{}>, ComputedRef<ClassValue>] {
6
16
  const attrs = useAttrs();
7
- const className = computed(() => attrs.class);
17
+ const classes = computed(() => attrs.class);
8
18
  const inputAttrs = computed(() => objectWithout(attrs, 'class'));
9
19
 
10
- return [inputAttrs, className];
20
+ return [inputAttrs, classes as ComputedRef<ClassValue>];
11
21
  }
@@ -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,33 @@
1
+ import { debounce } from '@noeldemartin/utils';
2
+ import { computed, ref, watch, watchEffect } from 'vue';
3
+ import type { ComputedGetter, ComputedRef, Ref } from 'vue';
4
+
5
+ export interface ComputedDebounceOptions<T> {
6
+ initial?: T;
7
+ delay?: number;
8
+ }
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
+
19
+ export function computedDebounce<T>(options: ComputedDebounceOptions<T>, getter: ComputedGetter<T>): ComputedRef<T>;
20
+ export function computedDebounce<T>(getter: ComputedGetter<T>): ComputedRef<T | null>;
21
+ export function computedDebounce<T>(
22
+ optionsOrGetter: ComputedGetter<T> | ComputedDebounceOptions<T>,
23
+ inputGetter?: ComputedGetter<T>,
24
+ ): ComputedRef<T> {
25
+ const inputOptions = inputGetter ? (optionsOrGetter as ComputedDebounceOptions<T>) : {};
26
+ const getter = inputGetter ?? (optionsOrGetter as ComputedGetter<T>);
27
+ const state = ref(inputOptions.initial ?? null);
28
+ const update = debounce((value) => (state.value = value), inputOptions.delay ?? 300);
29
+
30
+ watchEffect(() => update(getter()));
31
+
32
+ return state as unknown as ComputedRef<T>;
33
+ }
@@ -1,5 +1,8 @@
1
+ export * from './classes';
1
2
  export * from './composition/events';
2
3
  export * from './composition/forms';
3
4
  export * from './composition/hooks';
4
- export * from './tailwindcss';
5
+ export * from './composition/persistent';
6
+ export * from './composition/state';
7
+ export * from './markdown';
5
8
  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,21 +1,34 @@
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 {
6
6
  return tap(new Renderer(), (renderer) => {
7
- renderer.link = function(href, title, text) {
8
- return Renderer.prototype.link.apply(this, [href, title, text]).replace('<a', '<a target="_blank"');
7
+ renderer.link = function(link) {
8
+ return Renderer.prototype.link.apply(this, [link]).replace('<a', '<a target="_blank"');
9
9
  };
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, { renderer: makeRenderer(), async: false });
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 {
18
- // TODO improve target="_blank" exception
19
- // See https://github.com/cure53/DOMPurify/issues/317
20
33
  return DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
21
34
  }
package/src/utils/vue.ts CHANGED
@@ -1,61 +1,21 @@
1
- import { fail } from '@noeldemartin/utils';
2
- import { computed, inject, reactive, ref, watch } from 'vue';
3
- import type { Directive, InjectionKey, PropType, Ref, UnwrapNestedRefs } from 'vue';
1
+ import { fail, toString } from '@noeldemartin/utils';
2
+ import { Comment, Static, Text, inject, reactive } from 'vue';
3
+ import type { Directive, InjectionKey, MaybeRef, Ref, UnwrapNestedRefs, VNode } from 'vue';
4
4
 
5
- type BaseProp<T> = {
6
- type?: PropType<T>;
7
- validator?(value: unknown): boolean;
8
- };
5
+ export type AcceptRefs<T> = { [K in keyof T]: T[K] | RefUnion<T[K]> };
6
+ export type RefUnion<T> = T extends infer R ? Ref<R> : never;
7
+ export type Unref<T> = { [K in keyof T]: T[K] extends MaybeRef<infer Value> ? Value : T[K] };
9
8
 
10
- type RequiredProp<T> = BaseProp<T> & { required: true };
11
- type OptionalProp<T> = BaseProp<T> & { default: T | (() => T) | null };
12
-
13
- export type ComponentProps = Record<string, unknown>;
14
-
15
- export function arrayProp<T>(defaultValue?: () => T[]): OptionalProp<T[]> {
16
- return {
17
- type: Array as PropType<T[]>,
18
- default: defaultValue ?? (() => []),
19
- };
20
- }
21
-
22
- export function booleanProp(defaultValue: boolean = false): OptionalProp<boolean> {
23
- return {
24
- type: Boolean,
25
- default: defaultValue,
26
- };
27
- }
28
-
29
- export function componentRef<T>(): Ref<UnwrapNestedRefs<T> | undefined> {
30
- return ref<UnwrapNestedRefs<T>>();
31
- }
32
-
33
- export function computedAsync<T>(getter: () => Promise<T>): Ref<T | undefined> {
34
- const result = ref<T>();
35
- const asyncValue = computed(getter);
36
-
37
- watch(asyncValue, async () => (result.value = await asyncValue.value), { immediate: true });
38
-
39
- return result;
9
+ function renderVNodeAttrs(node: VNode): string {
10
+ return Object.entries(node.props ?? {}).reduce((attrs, [name, value]) => {
11
+ return attrs + `${name}="${toString(value)}"`;
12
+ }, '');
40
13
  }
41
14
 
42
15
  export function defineDirective(directive: Directive): Directive {
43
16
  return directive;
44
17
  }
45
18
 
46
- export function enumProp<Enum extends Record<string, unknown>>(
47
- enumeration: Enum,
48
- defaultValue?: Enum[keyof Enum],
49
- ): OptionalProp<Enum[keyof Enum]> {
50
- const values = Object.values(enumeration) as Enum[keyof Enum][];
51
-
52
- return {
53
- type: String as unknown as PropType<Enum[keyof Enum]>,
54
- default: defaultValue ?? values[0] ?? null,
55
- validator: (value) => values.includes(value as Enum[keyof Enum]),
56
- };
57
- }
58
-
59
19
  export function injectReactive<T extends object>(key: InjectionKey<T> | string): UnwrapNestedRefs<T> | undefined {
60
20
  const value = inject(key);
61
21
 
@@ -66,90 +26,31 @@ export function injectReactiveOrFail<T extends object>(
66
26
  key: InjectionKey<T> | string,
67
27
  errorMessage?: string,
68
28
  ): UnwrapNestedRefs<T> {
69
- return injectReactive(key) ?? fail(errorMessage ?? `Could not resolve '${key}' injection key`);
29
+ return injectReactive(key) ?? fail(errorMessage ?? `Could not resolve '${toString(key)}' injection key`);
70
30
  }
71
31
 
72
32
  export function injectOrFail<T>(key: InjectionKey<T> | string, errorMessage?: string): T {
73
- return inject(key) ?? fail(errorMessage ?? `Could not resolve '${key}' injection key`);
74
- }
75
-
76
- export function mixedProp<T>(type?: PropType<T>): OptionalProp<T | null> {
77
- return {
78
- type,
79
- default: null,
80
- };
81
- }
82
-
83
- export function numberProp(): OptionalProp<number | null>;
84
- export function numberProp(defaultValue: number): OptionalProp<number>;
85
- export function numberProp(defaultValue: number | null = null): OptionalProp<number | null> {
86
- return {
87
- type: Number,
88
- default: defaultValue,
89
- };
90
- }
91
-
92
- export function objectProp<T = Object>(): OptionalProp<T | null>;
93
- export function objectProp<T>(defaultValue: () => T): OptionalProp<T>;
94
- export function objectProp<T = Object>(defaultValue: (() => T) | null = null): OptionalProp<T | null> {
95
- return {
96
- type: Object,
97
- default: defaultValue,
98
- };
99
- }
100
-
101
- export function requiredArrayProp<T>(): RequiredProp<T[]> {
102
- return {
103
- type: Array as PropType<T[]>,
104
- required: true,
105
- };
33
+ return inject(key) ?? fail(errorMessage ?? `Could not resolve '${toString(key)}' injection key`);
106
34
  }
107
35
 
108
- export function requiredEnumProp<Enum extends Record<string, unknown>>(
109
- enumeration: Enum,
110
- ): RequiredProp<Enum[keyof Enum]> {
111
- const values = Object.values(enumeration);
36
+ export function renderVNode(node: VNode | string): string {
37
+ if (typeof node === 'string') {
38
+ return node;
39
+ }
112
40
 
113
- return {
114
- type: String as unknown as PropType<Enum[keyof Enum]>,
115
- required: true,
116
- validator: (value) => values.includes(value),
117
- };
118
- }
119
-
120
- export function requiredMixedProp<T>(type?: PropType<T>): RequiredProp<T> {
121
- return {
122
- type,
123
- required: true,
124
- };
125
- }
41
+ if (node.type === Comment) {
42
+ return '';
43
+ }
126
44
 
127
- export function requiredNumberProp(): RequiredProp<number> {
128
- return {
129
- type: Number,
130
- required: true,
131
- };
132
- }
45
+ if (node.type === Text || node.type === Static) {
46
+ return node.children as string;
47
+ }
133
48
 
134
- export function requiredObjectProp<T = Object>(): RequiredProp<T> {
135
- return {
136
- type: Object,
137
- required: true,
138
- };
139
- }
140
-
141
- export function requiredStringProp(): RequiredProp<string> {
142
- return {
143
- type: String,
144
- required: true,
145
- };
146
- }
49
+ if (node.type === 'br') {
50
+ return '\n\n';
51
+ }
147
52
 
148
- export function stringProp(): OptionalProp<string | null>;
149
- export function stringProp(defaultValue: string): OptionalProp<string>;
150
- export function stringProp(defaultValue: string | null = null): OptionalProp<string | null> {
151
- return {
152
- type: String,
153
- default: defaultValue,
154
- };
53
+ return `<${node.type} ${renderVNodeAttrs(node)}>${Array.from(node.children as Array<VNode | string>)
54
+ .map(renderVNode)
55
+ .join('')}</${node.type}>`;
155
56
  }