@aerogel/core 0.0.0-next.a56c0f4966eb71571173f8502f3f36d357ceebc7 → 0.0.0-next.b18f4e0acd39431045c2f444c711303890143193

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 +593 -122
  4. package/dist/aerogel-core.esm.js +1 -1
  5. package/dist/aerogel-core.esm.js.map +1 -1
  6. package/package.json +4 -4
  7. package/src/bootstrap/bootstrap.test.ts +3 -3
  8. package/src/bootstrap/index.ts +13 -3
  9. package/src/bootstrap/options.ts +3 -0
  10. package/src/components/AGAppLayout.vue +3 -2
  11. package/src/components/AGAppOverlays.vue +5 -1
  12. package/src/components/forms/AGCheckbox.vue +7 -1
  13. package/src/components/forms/AGInput.vue +8 -6
  14. package/src/components/forms/AGSelect.story.vue +21 -3
  15. package/src/components/forms/AGSelect.vue +10 -3
  16. package/src/components/headless/forms/AGHeadlessInput.ts +21 -1
  17. package/src/components/headless/forms/AGHeadlessInput.vue +4 -1
  18. package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
  19. package/src/components/headless/forms/AGHeadlessSelect.ts +21 -22
  20. package/src/components/headless/forms/AGHeadlessSelect.vue +24 -24
  21. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +4 -1
  22. package/src/components/headless/forms/AGHeadlessSelectOption.vue +6 -6
  23. package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
  24. package/src/components/headless/modals/AGHeadlessModal.vue +3 -5
  25. package/src/components/headless/modals/index.ts +4 -6
  26. package/src/components/headless/snackbars/index.ts +23 -8
  27. package/src/components/lib/AGMeasured.vue +15 -0
  28. package/src/components/lib/index.ts +1 -0
  29. package/src/components/modals/AGAlertModal.ts +15 -0
  30. package/src/components/modals/AGAlertModal.vue +3 -14
  31. package/src/components/modals/AGConfirmModal.ts +27 -0
  32. package/src/components/modals/AGConfirmModal.vue +7 -11
  33. package/src/components/modals/AGErrorReportModal.ts +27 -1
  34. package/src/components/modals/AGErrorReportModal.vue +7 -15
  35. package/src/components/modals/AGErrorReportModalButtons.vue +4 -2
  36. package/src/components/modals/AGLoadingModal.ts +23 -0
  37. package/src/components/modals/AGLoadingModal.vue +3 -7
  38. package/src/components/modals/AGModal.ts +2 -2
  39. package/src/components/modals/AGModal.vue +14 -12
  40. package/src/components/modals/AGPromptModal.ts +30 -0
  41. package/src/components/modals/AGPromptModal.vue +34 -0
  42. package/src/components/modals/index.ts +13 -19
  43. package/src/components/snackbars/AGSnackbar.vue +2 -8
  44. package/src/components/utils.ts +10 -0
  45. package/src/directives/index.ts +3 -1
  46. package/src/directives/measure.ts +12 -0
  47. package/src/errors/Errors.ts +11 -6
  48. package/src/errors/index.ts +1 -1
  49. package/src/forms/Form.ts +1 -0
  50. package/src/services/App.state.ts +0 -1
  51. package/src/services/App.ts +20 -2
  52. package/src/services/Service.ts +22 -12
  53. package/src/services/index.ts +1 -1
  54. package/src/ui/UI.ts +92 -11
  55. package/src/ui/index.ts +8 -3
  56. package/src/utils/composition/events.ts +1 -0
  57. package/src/utils/index.ts +1 -0
  58. package/src/utils/tailwindcss.test.ts +26 -0
  59. package/src/utils/tailwindcss.ts +7 -0
  60. package/src/utils/vue.ts +10 -1
@@ -9,7 +9,6 @@ export default defineServiceState({
9
9
  plugins: {} as Record<string, Plugin>,
10
10
  environment: Aerogel.environment,
11
11
  sourceUrl: Aerogel.sourceUrl,
12
- isMounted: false,
13
12
  },
14
13
  computed: {
15
14
  development: (state) => state.environment === 'development',
@@ -1,4 +1,4 @@
1
- import { facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
1
+ import { PromisedValue, facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
2
2
 
3
3
  import Events from '@/services/Events';
4
4
  import type { Plugin } from '@/plugins';
@@ -7,6 +7,23 @@ import Service from './App.state';
7
7
 
8
8
  export class AppService extends Service {
9
9
 
10
+ public readonly ready = new PromisedValue<void>();
11
+ public readonly mounted = new PromisedValue<void>();
12
+
13
+ public isReady(): boolean {
14
+ return this.ready.isResolved();
15
+ }
16
+
17
+ public isMounted(): boolean {
18
+ return this.mounted.isResolved();
19
+ }
20
+
21
+ public async whenReady<T>(callback: () => T): Promise<T> {
22
+ const result = await this.ready.then(callback);
23
+
24
+ return result;
25
+ }
26
+
10
27
  public async reload(queryParameters?: Record<string, string | undefined>): Promise<void> {
11
28
  queryParameters && updateLocationQueryParameters(queryParameters);
12
29
 
@@ -21,7 +38,8 @@ export class AppService extends Service {
21
38
  }
22
39
 
23
40
  protected async boot(): Promise<void> {
24
- Events.once('application-mounted', () => this.setState({ isMounted: true }));
41
+ Events.once('application-ready', () => this.ready.resolve());
42
+ Events.once('application-mounted', () => this.mounted.resolve());
25
43
  }
26
44
 
27
45
  }
@@ -1,5 +1,6 @@
1
1
  import { MagicObject, PromisedValue, Storage, isEmpty, objectDeepClone, objectOnly } from '@noeldemartin/utils';
2
2
  import type { Constructor } from '@noeldemartin/utils';
3
+ import type { MaybeRef } from 'vue';
3
4
  import type { Store } from 'pinia';
4
5
 
5
6
  import ServiceBootError from '@/errors/ServiceBootError';
@@ -8,9 +9,12 @@ import { defineServiceStore } from '@/services/store';
8
9
  export type ServiceState = Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
9
10
  export type DefaultServiceState = any; // eslint-disable-line @typescript-eslint/no-explicit-any
10
11
  export type ServiceConstructor<T extends Service = Service> = Constructor<T> & typeof Service;
12
+ export type UnrefServiceState<State extends ServiceState> = {
13
+ [K in keyof State]: State[K] extends MaybeRef<infer T> ? T : State[K];
14
+ };
11
15
 
12
16
  export type ComputedStateDefinition<TState extends ServiceState, TComputedState extends ServiceState> = {
13
- [K in keyof TComputedState]: (state: TState) => TComputedState[K];
17
+ [K in keyof TComputedState]: (state: UnrefServiceState<TState>) => TComputedState[K];
14
18
  } & ThisType<{
15
19
  readonly [K in keyof TComputedState]: TComputedState[K];
16
20
  }>;
@@ -24,8 +28,10 @@ export function defineServiceState<
24
28
  persist?: (keyof State)[];
25
29
  computed?: ComputedStateDefinition<State, ComputedState>;
26
30
  serialize?: (state: Partial<State>) => Partial<State>;
27
- }): Constructor<State> & Constructor<ComputedState> & Constructor<Service<State, ComputedState, Partial<State>>> {
28
- return class extends Service<State, ComputedState> {
31
+ }): Constructor<UnrefServiceState<State>> &
32
+ Constructor<ComputedState> &
33
+ Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>> {
34
+ return class extends Service<UnrefServiceState<State>, ComputedState> {
29
35
 
30
36
  public static persist = (options.persist as string[]) ?? [];
31
37
 
@@ -37,21 +43,21 @@ export function defineServiceState<
37
43
  return options.name ?? null;
38
44
  }
39
45
 
40
- protected getInitialState(): State {
46
+ protected getInitialState(): UnrefServiceState<State> {
41
47
  return options.initialState;
42
48
  }
43
49
 
44
- protected getComputedStateDefinition(): ComputedStateDefinition<State, ComputedState> {
45
- return options.computed ?? ({} as ComputedStateDefinition<State, ComputedState>);
50
+ protected getComputedStateDefinition(): ComputedStateDefinition<UnrefServiceState<State>, ComputedState> {
51
+ return (options.computed ?? {}) as ComputedStateDefinition<UnrefServiceState<State>, ComputedState>;
46
52
  }
47
53
 
48
54
  protected serializePersistedState(state: Partial<State>): Partial<State> {
49
55
  return options.serialize?.(state) ?? state;
50
56
  }
51
57
 
52
- } as unknown as Constructor<State> &
58
+ } as unknown as Constructor<UnrefServiceState<State>> &
53
59
  Constructor<ComputedState> &
54
- Constructor<Service<State, ComputedState, Partial<State>>>;
60
+ Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>>;
55
61
  }
56
62
 
57
63
  export default class Service<
@@ -162,7 +168,11 @@ export default class Service<
162
168
  return;
163
169
  }
164
170
 
165
- const storage = Storage.require<ServiceStorage>(this._name);
171
+ const storage = Storage.get<ServiceStorage>(this._name);
172
+
173
+ if (!storage) {
174
+ return;
175
+ }
166
176
 
167
177
  Storage.set(this._name, {
168
178
  ...storage,
@@ -191,14 +201,14 @@ export default class Service<
191
201
  }
192
202
 
193
203
  protected async frameworkBoot(): Promise<void> {
194
- this.restorePersistedState();
204
+ this.initializePersistedState();
195
205
  }
196
206
 
197
207
  protected async boot(): Promise<void> {
198
- // Override.
208
+ // Placeholder for overrides, don't place any functionality here.
199
209
  }
200
210
 
201
- protected restorePersistedState(): void {
211
+ protected initializePersistedState(): void {
202
212
  // TODO fix this.static()
203
213
  const persist = (this.constructor as unknown as { persist: string[] }).persist;
204
214
 
@@ -50,7 +50,7 @@ export default definePlugin({
50
50
  });
51
51
 
52
52
  declare module '@/bootstrap/options' {
53
- interface AerogelOptions {
53
+ export interface AerogelOptions {
54
54
  services?: Record<string, Service>;
55
55
  }
56
56
  }
package/src/ui/UI.ts CHANGED
@@ -5,6 +5,7 @@ import type { ObjectValues } from '@noeldemartin/utils';
5
5
 
6
6
  import Events from '@/services/Events';
7
7
  import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
8
+ import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
8
9
 
9
10
  import Service from './UI.state';
10
11
  import type { Modal, ModalComponent, Snackbar } from './UI.state';
@@ -24,11 +25,26 @@ export const UIComponents = {
24
25
  ConfirmModal: 'confirm-modal',
25
26
  ErrorReportModal: 'error-report-modal',
26
27
  LoadingModal: 'loading-modal',
28
+ PromptModal: 'prompt-modal',
27
29
  Snackbar: 'snackbar',
30
+ StartupCrash: 'startup-crash',
28
31
  } as const;
29
32
 
30
33
  export type UIComponent = ObjectValues<typeof UIComponents>;
31
34
 
35
+ export interface ConfirmOptions {
36
+ acceptText?: string;
37
+ cancelText?: string;
38
+ }
39
+
40
+ export interface PromptOptions {
41
+ label?: string;
42
+ defaultValue?: string;
43
+ placeholder?: string;
44
+ acceptText?: string;
45
+ cancelText?: string;
46
+ }
47
+
32
48
  export interface ShowSnackbarOptions {
33
49
  component?: Component;
34
50
  color?: SnackbarColor;
@@ -47,33 +63,98 @@ export class UIService extends Service {
47
63
  public alert(message: string): void;
48
64
  public alert(title: string, message: string): void;
49
65
  public alert(messageOrTitle: string, message?: string): void {
50
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
66
+ const getProperties = (): AGAlertModalProps => {
67
+ if (typeof message !== 'string') {
68
+ return { message: messageOrTitle };
69
+ }
70
+
71
+ return {
72
+ title: messageOrTitle,
73
+ message,
74
+ };
75
+ };
51
76
 
52
- this.openModal(this.requireComponent(UIComponents.AlertModal), options);
77
+ this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
53
78
  }
54
79
 
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>>(
80
+ public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
81
+ public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
82
+ public async confirm(
83
+ messageOrTitle: string,
84
+ messageOrOptions?: string | ConfirmOptions,
85
+ options?: ConfirmOptions,
86
+ ): Promise<boolean> {
87
+ const getProperties = (): AGConfirmModalProps => {
88
+ if (typeof messageOrOptions !== 'string') {
89
+ return {
90
+ message: messageOrTitle,
91
+ ...(messageOrOptions ?? {}),
92
+ };
93
+ }
94
+
95
+ return {
96
+ title: messageOrTitle,
97
+ message: messageOrOptions,
98
+ ...(options ?? {}),
99
+ };
100
+ };
101
+
102
+ const modal = await this.openModal<ModalComponent<AGConfirmModalProps, boolean>>(
60
103
  this.requireComponent(UIComponents.ConfirmModal),
61
- options,
104
+ getProperties(),
62
105
  );
63
106
  const result = await modal.beforeClose;
64
107
 
65
108
  return result ?? false;
66
109
  }
67
110
 
111
+ public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
112
+ public async prompt(title: string, message: string, options?: PromptOptions): Promise<string | null>;
113
+ public async prompt(
114
+ messageOrTitle: string,
115
+ messageOrOptions?: string | PromptOptions,
116
+ options?: PromptOptions,
117
+ ): Promise<string | null> {
118
+ const getProperties = (): AGPromptModalProps => {
119
+ if (typeof messageOrOptions !== 'string') {
120
+ return {
121
+ message: messageOrTitle,
122
+ ...(messageOrOptions ?? {}),
123
+ };
124
+ }
125
+
126
+ return {
127
+ title: messageOrTitle,
128
+ message: messageOrOptions,
129
+ ...(options ?? {}),
130
+ };
131
+ };
132
+
133
+ const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
134
+ this.requireComponent(UIComponents.PromptModal),
135
+ getProperties(),
136
+ );
137
+ const result = await modal.beforeClose;
138
+
139
+ return result ?? null;
140
+ }
141
+
68
142
  public async loading<T>(operation: Promise<T>): Promise<T>;
69
143
  public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
70
144
  public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
71
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
145
+ const getProperties = (): AGLoadingModalProps => {
146
+ if (typeof messageOrOperation !== 'string') {
147
+ return {};
148
+ }
72
149
 
73
- const message = typeof messageOrOperation === 'string' ? messageOrOperation : undefined;
74
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), { message });
150
+ return { message: messageOrOperation };
151
+ };
152
+
153
+ const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
75
154
 
76
155
  try {
156
+ operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
157
+
77
158
  const [result] = await Promise.all([operation, after({ seconds: 1 })]);
78
159
 
79
160
  return result;
package/src/ui/index.ts CHANGED
@@ -8,13 +8,16 @@ 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 { default as UI } from './UI';
20
+
18
21
  export type UIServices = typeof services;
19
22
 
20
23
  export default definePlugin({
@@ -24,7 +27,9 @@ export default definePlugin({
24
27
  [UIComponents.ConfirmModal]: AGConfirmModal,
25
28
  [UIComponents.ErrorReportModal]: AGErrorReportModal,
26
29
  [UIComponents.LoadingModal]: AGLoadingModal,
30
+ [UIComponents.PromptModal]: AGPromptModal,
27
31
  [UIComponents.Snackbar]: AGSnackbar,
32
+ [UIComponents.StartupCrash]: AGStartupCrash,
28
33
  };
29
34
 
30
35
  Object.entries({
@@ -37,7 +42,7 @@ export default definePlugin({
37
42
  });
38
43
 
39
44
  declare module '@/bootstrap/options' {
40
- interface AerogelOptions {
45
+ export interface AerogelOptions {
41
46
  components?: Partial<Record<UIComponent, Component>>;
42
47
  }
43
48
  }
@@ -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,5 +1,5 @@
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> = {
@@ -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
  }