@aerogel/core 0.0.0-next.f8cdd39997c56dcd46e07c26af8a84d04d610fce → 0.0.0-next.f9394854509d71d644498ac087706a2f8f8eea1c

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 (115) 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 +1345 -231
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/histoire.config.ts +7 -0
  7. package/package.json +14 -5
  8. package/postcss.config.js +6 -0
  9. package/src/assets/histoire.css +3 -0
  10. package/src/bootstrap/bootstrap.test.ts +3 -3
  11. package/src/bootstrap/index.ts +32 -5
  12. package/src/bootstrap/options.ts +3 -0
  13. package/src/components/AGAppLayout.vue +7 -2
  14. package/src/components/AGAppOverlays.vue +5 -1
  15. package/src/components/AGAppSnackbars.vue +2 -2
  16. package/src/components/composition.ts +23 -0
  17. package/src/components/forms/AGCheckbox.vue +7 -1
  18. package/src/components/forms/AGForm.vue +9 -10
  19. package/src/components/forms/AGInput.vue +10 -6
  20. package/src/components/forms/AGSelect.story.vue +46 -0
  21. package/src/components/forms/AGSelect.vue +60 -0
  22. package/src/components/forms/index.ts +5 -6
  23. package/src/components/headless/forms/AGHeadlessButton.ts +3 -0
  24. package/src/components/headless/forms/AGHeadlessButton.vue +23 -12
  25. package/src/components/headless/forms/AGHeadlessInput.ts +30 -4
  26. package/src/components/headless/forms/AGHeadlessInput.vue +23 -7
  27. package/src/components/headless/forms/AGHeadlessInputDescription.vue +28 -0
  28. package/src/components/headless/forms/AGHeadlessInputInput.vue +44 -5
  29. package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
  30. package/src/components/headless/forms/AGHeadlessInputTextArea.vue +43 -0
  31. package/src/components/headless/forms/AGHeadlessSelect.ts +42 -0
  32. package/src/components/headless/forms/AGHeadlessSelect.vue +77 -0
  33. package/src/components/headless/forms/AGHeadlessSelectButton.vue +24 -0
  34. package/src/components/headless/forms/AGHeadlessSelectError.vue +26 -0
  35. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +24 -0
  36. package/src/components/headless/forms/AGHeadlessSelectOption.ts +4 -0
  37. package/src/components/headless/forms/AGHeadlessSelectOption.vue +39 -0
  38. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
  39. package/src/components/headless/forms/composition.ts +10 -0
  40. package/src/components/headless/forms/index.ts +13 -1
  41. package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
  42. package/src/components/headless/modals/AGHeadlessModal.vue +3 -5
  43. package/src/components/headless/modals/index.ts +4 -6
  44. package/src/components/headless/snackbars/index.ts +23 -8
  45. package/src/components/index.ts +3 -1
  46. package/src/components/interfaces.ts +24 -0
  47. package/src/components/{basic → lib}/AGErrorMessage.vue +2 -2
  48. package/src/components/{basic → lib}/AGMarkdown.vue +9 -4
  49. package/src/components/lib/AGMeasured.vue +16 -0
  50. package/src/components/lib/AGStartupCrash.vue +31 -0
  51. package/src/components/lib/index.ts +5 -0
  52. package/src/components/modals/AGAlertModal.ts +15 -0
  53. package/src/components/modals/AGAlertModal.vue +4 -15
  54. package/src/components/modals/AGConfirmModal.ts +33 -0
  55. package/src/components/modals/AGConfirmModal.vue +9 -13
  56. package/src/components/modals/AGErrorReportModal.ts +27 -1
  57. package/src/components/modals/AGErrorReportModal.vue +8 -16
  58. package/src/components/modals/AGErrorReportModalButtons.vue +4 -2
  59. package/src/components/modals/AGErrorReportModalTitle.vue +1 -1
  60. package/src/components/modals/AGLoadingModal.ts +23 -0
  61. package/src/components/modals/AGLoadingModal.vue +4 -8
  62. package/src/components/modals/AGModal.ts +1 -1
  63. package/src/components/modals/AGModal.vue +15 -12
  64. package/src/components/modals/AGModalTitle.vue +9 -0
  65. package/src/components/modals/AGPromptModal.ts +36 -0
  66. package/src/components/modals/AGPromptModal.vue +34 -0
  67. package/src/components/modals/index.ts +13 -17
  68. package/src/components/snackbars/AGSnackbar.vue +3 -9
  69. package/src/components/utils.ts +10 -0
  70. package/src/directives/index.ts +5 -1
  71. package/src/directives/measure.ts +40 -0
  72. package/src/errors/Errors.ts +26 -24
  73. package/src/errors/index.ts +10 -23
  74. package/src/errors/utils.ts +35 -0
  75. package/src/forms/Form.test.ts +28 -0
  76. package/src/forms/Form.ts +80 -14
  77. package/src/forms/index.ts +3 -1
  78. package/src/forms/utils.ts +34 -3
  79. package/src/forms/validation.ts +19 -0
  80. package/src/jobs/Job.ts +5 -0
  81. package/src/jobs/index.ts +7 -0
  82. package/src/lang/DefaultLangProvider.ts +43 -0
  83. package/src/lang/Lang.state.ts +11 -0
  84. package/src/lang/Lang.ts +44 -29
  85. package/src/main.histoire.ts +1 -0
  86. package/src/main.ts +3 -0
  87. package/src/plugins/Plugin.ts +1 -0
  88. package/src/plugins/index.ts +19 -0
  89. package/src/services/App.state.ts +20 -5
  90. package/src/services/App.ts +38 -3
  91. package/src/services/Cache.ts +43 -0
  92. package/src/services/Events.test.ts +39 -0
  93. package/src/services/Events.ts +100 -30
  94. package/src/services/Service.ts +59 -17
  95. package/src/services/index.ts +5 -2
  96. package/src/services/store.ts +8 -5
  97. package/src/testing/index.ts +25 -0
  98. package/src/testing/setup.ts +19 -0
  99. package/src/ui/UI.state.ts +7 -0
  100. package/src/ui/UI.ts +155 -22
  101. package/src/ui/index.ts +9 -3
  102. package/src/ui/utils.ts +16 -0
  103. package/src/utils/composition/events.ts +1 -0
  104. package/src/utils/index.ts +1 -0
  105. package/src/utils/tailwindcss.test.ts +26 -0
  106. package/src/utils/tailwindcss.ts +7 -0
  107. package/src/utils/vue.ts +23 -5
  108. package/tailwind.config.js +4 -0
  109. package/tsconfig.json +1 -1
  110. package/vite.config.ts +4 -1
  111. package/.eslintrc.js +0 -3
  112. package/dist/virtual.d.ts +0 -11
  113. package/src/components/basic/index.ts +0 -5
  114. package/src/types/virtual.d.ts +0 -11
  115. /package/src/components/{basic → lib}/AGLink.vue +0 -0
@@ -3,15 +3,18 @@ import type { App as VueApp } from 'vue';
3
3
  import { definePlugin } from '@/plugins';
4
4
 
5
5
  import App from './App';
6
+ import Cache from './Cache';
6
7
  import Events from './Events';
7
8
  import Service from './Service';
8
9
  import { getPiniaStore } from './store';
9
10
 
10
11
  export * from './App';
12
+ export * from './Cache';
11
13
  export * from './Events';
12
14
  export * from './Service';
15
+ export * from './store';
13
16
 
14
- export { App, Events, Service };
17
+ export { App, Cache, Events, Service };
15
18
 
16
19
  const defaultServices = {
17
20
  $app: App,
@@ -50,7 +53,7 @@ export default definePlugin({
50
53
  });
51
54
 
52
55
  declare module '@/bootstrap/options' {
53
- interface AerogelOptions {
56
+ export interface AerogelOptions {
54
57
  services?: Record<string, Service>;
55
58
  }
56
59
  }
@@ -1,16 +1,19 @@
1
+ import { tap } from '@noeldemartin/utils';
1
2
  import { createPinia, defineStore, setActivePinia } from 'pinia';
2
3
  import type { DefineStoreOptions, Pinia, StateTree, Store, _GettersTree } from 'pinia';
3
4
 
4
5
  let _store: Pinia | null = null;
5
6
 
6
7
  function initializePiniaStore(): Pinia {
7
- if (!_store) {
8
- _store = createPinia();
8
+ return _store ?? resetPiniaStore();
9
+ }
9
10
 
10
- setActivePinia(_store);
11
- }
11
+ export function resetPiniaStore(): Pinia {
12
+ return tap(createPinia(), (store) => {
13
+ _store = store;
12
14
 
13
- return _store;
15
+ setActivePinia(store);
16
+ });
14
17
  }
15
18
 
16
19
  export function getPiniaStore(): Pinia {
@@ -0,0 +1,25 @@
1
+ import type { GetClosureArgs } from '@noeldemartin/utils';
2
+
3
+ import Events from '@/services/Events';
4
+ import { definePlugin } from '@/plugins';
5
+
6
+ export interface AerogelTestingRuntime {
7
+ on: (typeof Events)['on'];
8
+ }
9
+
10
+ export default definePlugin({
11
+ async install() {
12
+ if (import.meta.env.MODE !== 'testing') {
13
+ return;
14
+ }
15
+
16
+ globalThis.testingRuntime = {
17
+ on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
18
+ };
19
+ },
20
+ });
21
+
22
+ declare global {
23
+ // eslint-disable-next-line no-var
24
+ var testingRuntime: AerogelTestingRuntime | undefined;
25
+ }
@@ -0,0 +1,19 @@
1
+ import { mock, tap } from '@noeldemartin/utils';
2
+ import { beforeEach, vi } from 'vitest';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ tap(globalThis, (global: any) => {
6
+ global.jest = vi;
7
+ global.navigator = { languages: ['en'] };
8
+ global.localStorage = mock<Storage>({
9
+ getItem: () => null,
10
+ setItem: () => null,
11
+ });
12
+ });
13
+
14
+ beforeEach(() => {
15
+ vi.stubGlobal('document', {
16
+ querySelector: () => null,
17
+ getElementById: () => null,
18
+ });
19
+ });
@@ -2,6 +2,8 @@ import type { Component } from 'vue';
2
2
 
3
3
  import { defineServiceState } from '@/services/Service';
4
4
 
5
+ import { Layouts, getCurrentLayout } from './utils';
6
+
5
7
  export interface Modal<T = unknown> {
6
8
  id: string;
7
9
  properties: Record<string, unknown>;
@@ -28,5 +30,10 @@ export default defineServiceState({
28
30
  initialState: {
29
31
  modals: [] as Modal[],
30
32
  snackbars: [] as Snackbar[],
33
+ layout: getCurrentLayout(),
34
+ },
35
+ computed: {
36
+ mobile: ({ layout }) => layout === Layouts.Mobile,
37
+ desktop: ({ layout }) => layout === Layouts.Desktop,
31
38
  },
32
39
  });
package/src/ui/UI.ts CHANGED
@@ -1,12 +1,15 @@
1
- import { facade, fail, uuid } from '@noeldemartin/utils';
1
+ import { after, facade, fail, 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
6
  import Events from '@/services/Events';
7
+ import type { Color } from '@/components/constants';
7
8
  import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
9
+ import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
8
10
 
9
11
  import Service from './UI.state';
12
+ import { MOBILE_BREAKPOINT, getCurrentLayout } from './utils';
10
13
  import type { Modal, ModalComponent, Snackbar } from './UI.state';
11
14
 
12
15
  interface ModalCallbacks<T = unknown> {
@@ -24,11 +27,31 @@ export const UIComponents = {
24
27
  ConfirmModal: 'confirm-modal',
25
28
  ErrorReportModal: 'error-report-modal',
26
29
  LoadingModal: 'loading-modal',
30
+ PromptModal: 'prompt-modal',
27
31
  Snackbar: 'snackbar',
32
+ StartupCrash: 'startup-crash',
28
33
  } as const;
29
34
 
30
35
  export type UIComponent = ObjectValues<typeof UIComponents>;
31
36
 
37
+ export interface ConfirmOptions {
38
+ acceptText?: string;
39
+ acceptColor?: Color;
40
+ cancelText?: string;
41
+ cancelColor?: Color;
42
+ }
43
+
44
+ export interface PromptOptions {
45
+ label?: string;
46
+ defaultValue?: string;
47
+ placeholder?: string;
48
+ acceptText?: string;
49
+ acceptColor?: Color;
50
+ cancelText?: string;
51
+ cancelColor?: Color;
52
+ trim?: boolean;
53
+ }
54
+
32
55
  export interface ShowSnackbarOptions {
33
56
  component?: Component;
34
57
  color?: SnackbarColor;
@@ -47,43 +70,117 @@ export class UIService extends Service {
47
70
  public alert(message: string): void;
48
71
  public alert(title: string, message: string): void;
49
72
  public alert(messageOrTitle: string, message?: string): void {
50
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
73
+ const getProperties = (): AGAlertModalProps => {
74
+ if (typeof message !== 'string') {
75
+ return { message: messageOrTitle };
76
+ }
51
77
 
52
- this.openModal(this.requireComponent(UIComponents.AlertModal), options);
78
+ return {
79
+ title: messageOrTitle,
80
+ message,
81
+ };
82
+ };
83
+
84
+ this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
53
85
  }
54
86
 
55
- public async confirm(message: string): Promise<boolean>;
56
- public async confirm(title: string, message: string): Promise<boolean>;
57
- public async confirm(messageOrTitle: string, message?: string): Promise<boolean> {
58
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
59
- const modal = await this.openModal<ModalComponent<{ message: string }, boolean>>(
87
+ public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
88
+ public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
89
+ public async confirm(
90
+ messageOrTitle: string,
91
+ messageOrOptions?: string | ConfirmOptions,
92
+ options?: ConfirmOptions,
93
+ ): Promise<boolean> {
94
+ const getProperties = (): AGConfirmModalProps => {
95
+ if (typeof messageOrOptions !== 'string') {
96
+ return {
97
+ message: messageOrTitle,
98
+ ...(messageOrOptions ?? {}),
99
+ };
100
+ }
101
+
102
+ return {
103
+ title: messageOrTitle,
104
+ message: messageOrOptions,
105
+ ...(options ?? {}),
106
+ };
107
+ };
108
+
109
+ const modal = await this.openModal<ModalComponent<AGConfirmModalProps, boolean>>(
60
110
  this.requireComponent(UIComponents.ConfirmModal),
61
- options,
111
+ getProperties(),
62
112
  );
63
113
  const result = await modal.beforeClose;
64
114
 
65
115
  return result ?? false;
66
116
  }
67
117
 
68
- public async loading<T>(operation: Promise<T>): Promise<T>;
69
- public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
70
- public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
71
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
118
+ public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
119
+ public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
120
+ public async prompt(
121
+ messageOrTitle: string,
122
+ messageOrOptions?: string | PromptOptions,
123
+ options?: PromptOptions,
124
+ ): Promise<string | null> {
125
+ const trim = options?.trim ?? true;
126
+ const getProperties = (): AGPromptModalProps => {
127
+ if (typeof messageOrOptions !== 'string') {
128
+ return {
129
+ message: messageOrTitle,
130
+ ...(messageOrOptions ?? {}),
131
+ };
132
+ }
133
+
134
+ return {
135
+ title: messageOrTitle,
136
+ message: messageOrOptions,
137
+ ...(options ?? {}),
138
+ };
139
+ };
140
+
141
+ const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
142
+ this.requireComponent(UIComponents.PromptModal),
143
+ getProperties(),
144
+ );
145
+ const rawResult = await modal.beforeClose;
146
+ const result = trim && typeof rawResult === 'string' ? rawResult?.trim() : rawResult;
147
+
148
+ return result ?? null;
149
+ }
150
+
151
+ public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
152
+ public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
153
+ public async loading<T>(
154
+ messageOrOperation: string | Promise<T> | (() => T),
155
+ operation?: Promise<T> | (() => T),
156
+ ): Promise<T> {
157
+ const getProperties = (): AGLoadingModalProps => {
158
+ if (typeof messageOrOperation !== 'string') {
159
+ return {};
160
+ }
161
+
162
+ return { message: messageOrOperation };
163
+ };
164
+
165
+ const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
72
166
 
73
- const message = typeof messageOrOperation === 'string' ? messageOrOperation : undefined;
74
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), { message });
75
- const result = await operation;
167
+ try {
168
+ operation = typeof messageOrOperation === 'string' ? (operation as () => T) : messageOrOperation;
169
+ operation = typeof operation === 'function' ? Promise.resolve(operation()) : operation;
76
170
 
77
- await this.closeModal(modal.id);
171
+ const [result] = await Promise.all([operation, after({ seconds: 1 })]);
78
172
 
79
- return result;
173
+ return result;
174
+ } finally {
175
+ await this.closeModal(modal.id);
176
+ }
80
177
  }
81
178
 
82
179
  public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
83
180
  const snackbar: Snackbar = {
84
181
  id: uuid(),
85
182
  properties: { message, ...options },
86
- component: options.component ?? markRaw(this.requireComponent(UIComponents.Snackbar)),
183
+ component: markRaw(options.component ?? this.requireComponent(UIComponents.Snackbar)),
87
184
  };
88
185
 
89
186
  this.setState('snackbars', this.snackbars.concat(snackbar));
@@ -138,6 +235,8 @@ export class UIService extends Service {
138
235
 
139
236
  protected async boot(): Promise<void> {
140
237
  this.watchModalEvents();
238
+ this.watchMountedEvent();
239
+ this.watchViewportBreakpoints();
141
240
  }
142
241
 
143
242
  private watchModalEvents(): void {
@@ -165,16 +264,50 @@ export class UIService extends Service {
165
264
  });
166
265
  }
167
266
 
267
+ private watchMountedEvent(): void {
268
+ Events.once('application-mounted', async () => {
269
+ if (!globalThis.document || !globalThis.getComputedStyle) {
270
+ return;
271
+ }
272
+
273
+ const splash = globalThis.document.getElementById('splash');
274
+
275
+ if (!splash) {
276
+ return;
277
+ }
278
+
279
+ if (globalThis.getComputedStyle(splash).opacity !== '0') {
280
+ splash.style.opacity = '0';
281
+
282
+ await after({ ms: 600 });
283
+ }
284
+
285
+ splash.remove();
286
+ });
287
+ }
288
+
289
+ private watchViewportBreakpoints(): void {
290
+ if (!globalThis.matchMedia) {
291
+ return;
292
+ }
293
+
294
+ const media = globalThis.matchMedia(`(min-width: ${MOBILE_BREAKPOINT}px)`);
295
+
296
+ media.addEventListener('change', () => this.setState({ layout: getCurrentLayout() }));
297
+ }
298
+
168
299
  }
169
300
 
170
- export default facade(new UIService());
301
+ export default facade(UIService);
171
302
 
172
303
  declare module '@/services/Events' {
173
304
  export interface EventsPayload {
174
- 'modal-will-close': { modal: Modal; result?: unknown };
175
- 'modal-closed': { modal: Modal; result?: unknown };
176
305
  'close-modal': { id: string; result?: unknown };
177
306
  'hide-modal': { id: string };
307
+ 'hide-overlays-backdrop': void;
308
+ 'modal-closed': { modal: Modal; result?: unknown };
309
+ 'modal-will-close': { modal: Modal; result?: unknown };
178
310
  'show-modal': { id: string };
311
+ 'show-overlays-backdrop': void;
179
312
  }
180
313
  }
package/src/ui/index.ts CHANGED
@@ -8,13 +8,17 @@ import AGAlertModal from '../components/modals/AGAlertModal.vue';
8
8
  import AGConfirmModal from '../components/modals/AGConfirmModal.vue';
9
9
  import AGErrorReportModal from '../components/modals/AGErrorReportModal.vue';
10
10
  import AGLoadingModal from '../components/modals/AGLoadingModal.vue';
11
+ import AGPromptModal from '../components/modals/AGPromptModal.vue';
11
12
  import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
13
+ import AGStartupCrash from '../components/lib/AGStartupCrash.vue';
12
14
  import type { UIComponent } from './UI';
13
15
 
14
- export { UI, UIComponents, UIComponent };
15
-
16
16
  const services = { $ui: UI };
17
17
 
18
+ export * from './UI';
19
+ export * from './utils';
20
+ export { default as UI } from './UI';
21
+
18
22
  export type UIServices = typeof services;
19
23
 
20
24
  export default definePlugin({
@@ -24,7 +28,9 @@ export default definePlugin({
24
28
  [UIComponents.ConfirmModal]: AGConfirmModal,
25
29
  [UIComponents.ErrorReportModal]: AGErrorReportModal,
26
30
  [UIComponents.LoadingModal]: AGLoadingModal,
31
+ [UIComponents.PromptModal]: AGPromptModal,
27
32
  [UIComponents.Snackbar]: AGSnackbar,
33
+ [UIComponents.StartupCrash]: AGStartupCrash,
28
34
  };
29
35
 
30
36
  Object.entries({
@@ -37,7 +43,7 @@ export default definePlugin({
37
43
  });
38
44
 
39
45
  declare module '@/bootstrap/options' {
40
- interface AerogelOptions {
46
+ export interface AerogelOptions {
41
47
  components?: Partial<Record<UIComponent, Component>>;
42
48
  }
43
49
  }
@@ -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
+ }
@@ -14,6 +14,7 @@ export function useEvent<Event extends EventWithPayload>(
14
14
  event: Event,
15
15
  listener: EventListener<EventsPayload[Event]>
16
16
  ): void;
17
+ export function useEvent<Payload>(event: string, listener: (payload: Payload) => unknown): void;
17
18
  export function useEvent<Event extends string>(event: UnknownEvent<Event>, listener: EventListener): void;
18
19
 
19
20
  export function useEvent(event: string, listener: EventListener): void {
@@ -1,4 +1,5 @@
1
1
  export * from './composition/events';
2
2
  export * from './composition/forms';
3
3
  export * from './composition/hooks';
4
+ export * from './tailwindcss';
4
5
  export * from './vue';
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { removeInteractiveClasses } from './tailwindcss';
4
+
5
+ describe('TailwindCSS utils', () => {
6
+
7
+ it('Removes interactive classes', () => {
8
+ const cases: [string, string][] = [
9
+ ['text-red hover:text-green', 'text-red'],
10
+ ['text-red hover:text-green text-lg', 'text-red text-lg'],
11
+ [
12
+ `
13
+ text-red text-lg
14
+ focus:text-yellow
15
+ hover:focus:text-black
16
+ `,
17
+ 'text-red text-lg',
18
+ ],
19
+ ];
20
+
21
+ cases.forEach(([original, expected]) => {
22
+ expect(removeInteractiveClasses(original)).toEqual(expected);
23
+ });
24
+ });
25
+
26
+ });
@@ -0,0 +1,7 @@
1
+ export function removeInteractiveClasses(classes: string): string {
2
+ return classes
3
+ .split(/\s+/)
4
+ .filter((className) => !/^(hover|focus|focus-visible):/.test(className))
5
+ .join(' ')
6
+ .trim();
7
+ }
package/src/utils/vue.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { fail } from '@noeldemartin/utils';
2
- import { inject, reactive, ref } from 'vue';
2
+ import { computed, inject, reactive, ref, watch } from 'vue';
3
3
  import type { Directive, InjectionKey, PropType, Ref, UnwrapNestedRefs } from 'vue';
4
4
 
5
5
  type BaseProp<T> = {
6
- type: PropType<T>;
6
+ type?: PropType<T>;
7
7
  validator?(value: unknown): boolean;
8
8
  };
9
9
 
@@ -30,6 +30,15 @@ export function componentRef<T>(): Ref<UnwrapNestedRefs<T> | undefined> {
30
30
  return ref<UnwrapNestedRefs<T>>();
31
31
  }
32
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;
40
+ }
41
+
33
42
  export function defineDirective(directive: Directive): Directive {
34
43
  return directive;
35
44
  }
@@ -64,13 +73,22 @@ export function injectOrFail<T>(key: InjectionKey<T> | string, errorMessage?: st
64
73
  return inject(key) ?? fail(errorMessage ?? `Could not resolve '${key}' injection key`);
65
74
  }
66
75
 
67
- export function mixedProp<T>(type: PropType<T>): OptionalProp<T | null> {
76
+ export function listenerProp<T extends Function = Function>(): OptionalProp<T | null> {
68
77
  return {
69
- type,
78
+ type: Function as PropType<T>,
70
79
  default: null,
71
80
  };
72
81
  }
73
82
 
83
+ export function mixedProp<T>(type?: PropType<T>): OptionalProp<T | null>;
84
+ export function mixedProp<T>(type: PropType<T>, defaultValue: T): OptionalProp<T>;
85
+ export function mixedProp<T>(type?: PropType<T>, defaultValue?: T): OptionalProp<T | null> {
86
+ return {
87
+ type,
88
+ default: defaultValue ?? null,
89
+ };
90
+ }
91
+
74
92
  export function numberProp(): OptionalProp<number | null>;
75
93
  export function numberProp(defaultValue: number): OptionalProp<number>;
76
94
  export function numberProp(defaultValue: number | null = null): OptionalProp<number | null> {
@@ -108,7 +126,7 @@ export function requiredEnumProp<Enum extends Record<string, unknown>>(
108
126
  };
109
127
  }
110
128
 
111
- export function requiredMixedProp<T>(type: PropType<T>): RequiredProp<T> {
129
+ export function requiredMixedProp<T>(type?: PropType<T>): RequiredProp<T> {
112
130
  return {
113
131
  type,
114
132
  required: true,
@@ -0,0 +1,4 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ['./src/**/*.{vue,ts}'],
4
+ };
package/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
- "types": ["unplugin-icons/types/vue3"],
4
+ "types": ["unplugin-icons/types/vue3", "@aerogel/vite/dist/virtual"],
5
5
  "baseUrl": ".",
6
6
  "paths": {
7
7
  "@/*": ["./src/*"]
package/vite.config.ts CHANGED
@@ -4,7 +4,10 @@ import { defineConfig } from 'vitest/config';
4
4
  import { resolve } from 'path';
5
5
 
6
6
  export default defineConfig({
7
- test: { clearMocks: true },
7
+ test: {
8
+ clearMocks: true,
9
+ setupFiles: ['./src/testing/setup.ts'],
10
+ },
8
11
  plugins: [Aerogel(), Icons()],
9
12
  resolve: {
10
13
  alias: {
package/.eslintrc.js DELETED
@@ -1,3 +0,0 @@
1
- module.exports = {
2
- extends: ['@noeldemartin/eslint-config-vue'],
3
- };
package/dist/virtual.d.ts DELETED
@@ -1,11 +0,0 @@
1
- declare module 'virtual:aerogel' {
2
- interface AerogelBuild {
3
- environment: 'production' | 'development' | 'testing';
4
- basePath?: string;
5
- sourceUrl?: string;
6
- }
7
-
8
- const build: AerogelBuild;
9
-
10
- export default build;
11
- }
@@ -1,5 +0,0 @@
1
- import AGErrorMessage from './AGErrorMessage.vue';
2
- import AGLink from './AGLink.vue';
3
- import AGMarkdown from './AGMarkdown.vue';
4
-
5
- export { AGErrorMessage, AGLink, AGMarkdown };
@@ -1,11 +0,0 @@
1
- declare module 'virtual:aerogel' {
2
- interface AerogelBuild {
3
- environment: 'production' | 'development' | 'testing';
4
- basePath?: string;
5
- sourceUrl?: string;
6
- }
7
-
8
- const build: AerogelBuild;
9
-
10
- export default build;
11
- }
File without changes