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

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 (84) 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 +702 -132
  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 +4 -3
  11. package/src/bootstrap/index.ts +19 -3
  12. package/src/components/AGAppLayout.vue +7 -2
  13. package/src/components/AGAppOverlays.vue +5 -1
  14. package/src/components/AGAppSnackbars.vue +1 -1
  15. package/src/components/forms/AGSelect.story.vue +28 -0
  16. package/src/components/forms/AGSelect.vue +53 -0
  17. package/src/components/forms/index.ts +5 -6
  18. package/src/components/headless/forms/AGHeadlessButton.vue +4 -3
  19. package/src/components/headless/forms/AGHeadlessInput.ts +21 -1
  20. package/src/components/headless/forms/AGHeadlessInput.vue +8 -5
  21. package/src/components/headless/forms/AGHeadlessInputLabel.vue +8 -2
  22. package/src/components/headless/forms/AGHeadlessSelect.ts +39 -0
  23. package/src/components/headless/forms/AGHeadlessSelect.vue +76 -0
  24. package/src/components/headless/forms/AGHeadlessSelectButton.vue +24 -0
  25. package/src/components/headless/forms/AGHeadlessSelectError.vue +26 -0
  26. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +24 -0
  27. package/src/components/headless/forms/AGHeadlessSelectOption.ts +4 -0
  28. package/src/components/headless/forms/AGHeadlessSelectOption.vue +39 -0
  29. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +3 -0
  30. package/src/components/headless/forms/index.ts +9 -1
  31. package/src/components/headless/modals/AGHeadlessModal.ts +27 -0
  32. package/src/components/headless/modals/AGHeadlessModal.vue +3 -5
  33. package/src/components/headless/modals/index.ts +4 -6
  34. package/src/components/headless/snackbars/index.ts +23 -8
  35. package/src/components/index.ts +1 -1
  36. package/src/components/lib/AGErrorMessage.vue +16 -0
  37. package/src/components/lib/AGLink.vue +9 -0
  38. package/src/components/{basic → lib}/AGMarkdown.vue +6 -10
  39. package/src/components/lib/AGMeasured.vue +15 -0
  40. package/src/components/lib/AGStartupCrash.vue +31 -0
  41. package/src/components/lib/index.ts +5 -0
  42. package/src/components/modals/AGAlertModal.ts +15 -0
  43. package/src/components/modals/AGAlertModal.vue +4 -16
  44. package/src/components/modals/AGConfirmModal.ts +27 -0
  45. package/src/components/modals/AGConfirmModal.vue +7 -11
  46. package/src/components/modals/AGErrorReportModal.ts +27 -1
  47. package/src/components/modals/AGErrorReportModal.vue +8 -16
  48. package/src/components/modals/AGErrorReportModalButtons.vue +6 -1
  49. package/src/components/modals/AGErrorReportModalTitle.vue +2 -2
  50. package/src/components/modals/AGLoadingModal.ts +23 -0
  51. package/src/components/modals/AGLoadingModal.vue +4 -8
  52. package/src/components/modals/AGModal.ts +1 -1
  53. package/src/components/modals/AGModal.vue +15 -12
  54. package/src/components/modals/AGModalTitle.vue +9 -0
  55. package/src/components/modals/index.ts +5 -0
  56. package/src/components/snackbars/AGSnackbar.vue +4 -10
  57. package/src/components/utils.ts +10 -0
  58. package/src/directives/index.ts +3 -1
  59. package/src/directives/measure.ts +12 -0
  60. package/src/errors/Errors.ts +37 -10
  61. package/src/errors/index.ts +9 -13
  62. package/src/forms/Form.ts +14 -6
  63. package/src/lang/Lang.ts +1 -1
  64. package/src/main.histoire.ts +1 -0
  65. package/src/plugins/Plugin.ts +1 -0
  66. package/src/plugins/index.ts +19 -0
  67. package/src/services/App.state.ts +7 -5
  68. package/src/services/App.ts +15 -3
  69. package/src/services/Service.ts +13 -4
  70. package/src/services/index.ts +7 -4
  71. package/src/ui/UI.ts +77 -16
  72. package/src/ui/index.ts +6 -3
  73. package/src/utils/composition/events.ts +1 -0
  74. package/src/utils/index.ts +1 -0
  75. package/src/utils/markdown.ts +11 -2
  76. package/src/utils/tailwindcss.test.ts +26 -0
  77. package/src/utils/tailwindcss.ts +7 -0
  78. package/src/utils/vue.ts +13 -4
  79. package/tailwind.config.js +4 -0
  80. package/tsconfig.json +1 -1
  81. package/.eslintrc.js +0 -3
  82. package/dist/virtual.d.ts +0 -11
  83. package/src/components/basic/index.ts +0 -3
  84. package/src/types/virtual.d.ts +0 -11
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <AGHeadlessSnackbar class="flex flex-row items-center justify-center gap-3 p-4" :class="styleClasses">
3
- <AGMarkdown :text="message" raw />
3
+ <AGMarkdown :text="message" inline />
4
4
  <AGButton
5
5
  v-for="(action, i) of actions"
6
6
  :key="i"
@@ -15,16 +15,15 @@
15
15
  <script setup lang="ts">
16
16
  import { computed } from 'vue';
17
17
 
18
- import UI from '@/ui/UI';
19
18
  import { Colors } from '@/components/constants';
20
- import { useSnackbarProps } from '@/components/headless';
21
- import type { SnackbarAction } from '@/components/headless';
19
+ import { useSnackbar, useSnackbarProps } from '@/components/headless/snackbars';
22
20
 
23
21
  import AGButton from '../forms/AGButton.vue';
24
22
  import AGHeadlessSnackbar from '../headless/snackbars/AGHeadlessSnackbar.vue';
25
- import AGMarkdown from '../basic/AGMarkdown.vue';
23
+ import AGMarkdown from '../lib/AGMarkdown.vue';
26
24
 
27
25
  const props = defineProps(useSnackbarProps());
26
+ const { activate } = useSnackbar(props);
28
27
  const styleClasses = computed(() => {
29
28
  switch (props.color) {
30
29
  case Colors.Danger:
@@ -34,9 +33,4 @@ const styleClasses = computed(() => {
34
33
  return 'bg-gray-900 text-white';
35
34
  }
36
35
  });
37
-
38
- function activate(action: SnackbarAction): void {
39
- action.handler?.();
40
- action.dismiss && UI.hideSnackbar(props.id);
41
- }
42
36
  </script>
@@ -0,0 +1,10 @@
1
+ export function extractComponentProps<T extends Record<string, unknown>>(
2
+ values: Record<string, unknown>,
3
+ definitions: Record<string, unknown>,
4
+ ): T {
5
+ return Object.keys(definitions).reduce((extracted, prop) => {
6
+ extracted[prop] = values[prop];
7
+
8
+ return extracted;
9
+ }, {} as Record<string, unknown>) as T;
10
+ }
@@ -3,9 +3,11 @@ import type { Directive } from 'vue';
3
3
  import { definePlugin } from '@/plugins';
4
4
 
5
5
  import initialFocus from './initial-focus';
6
+ import measure from './measure';
6
7
 
7
8
  const builtInDirectives: Record<string, Directive> = {
8
9
  'initial-focus': initialFocus,
10
+ 'measure': measure,
9
11
  };
10
12
 
11
13
  export default definePlugin({
@@ -22,7 +24,7 @@ export default definePlugin({
22
24
  });
23
25
 
24
26
  declare module '@/bootstrap/options' {
25
- interface AerogelOptions {
27
+ export interface AerogelOptions {
26
28
  directives?: Record<string, Directive>;
27
29
  }
28
30
  }
@@ -0,0 +1,12 @@
1
+ import { defineDirective } from '@/utils/vue';
2
+
3
+ export default defineDirective({
4
+ mounted(element: HTMLElement, { value }: { value?: () => unknown }) {
5
+ const sizes = element.getBoundingClientRect();
6
+
7
+ element.style.setProperty('--width', `${sizes.width}px`);
8
+ element.style.setProperty('--height', `${sizes.height}px`);
9
+
10
+ value?.();
11
+ },
12
+ });
@@ -3,11 +3,13 @@ import { JSError, facade, isObject, objectWithoutEmpty, toString } from '@noelde
3
3
  import App from '@/services/App';
4
4
  import ServiceBootError from '@/errors/ServiceBootError';
5
5
  import UI, { UIComponents } from '@/ui/UI';
6
- import { translate, translateWithDefault } from '@/lang/utils';
6
+ import { translateWithDefault } from '@/lang/utils';
7
7
 
8
8
  import Service from './Errors.state';
9
9
  import { Colors } from '@/components/constants';
10
+ import type { AGErrorReportModalProps } from '@/components/modals/AGErrorReportModal';
10
11
  import type { ErrorReport, ErrorReportLog, ErrorSource } from './Errors.state';
12
+ import type { ModalComponent } from '@/ui/UI.state';
11
13
 
12
14
  export class ErrorsService extends Service {
13
15
 
@@ -23,7 +25,7 @@ export class ErrorsService extends Service {
23
25
  }
24
26
 
25
27
  public async inspect(error: ErrorSource | ErrorReport[]): Promise<void> {
26
- const reports = Array.isArray(error) ? error : [await this.createErrorReport(error)];
28
+ const reports = Array.isArray(error) ? (error as ErrorReport[]) : [await this.createErrorReport(error)];
27
29
 
28
30
  if (reports.length === 0) {
29
31
  UI.alert(translateWithDefault('errors.inspectEmpty', 'Nothing to inspect!'));
@@ -31,11 +33,13 @@ export class ErrorsService extends Service {
31
33
  return;
32
34
  }
33
35
 
34
- UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), { reports });
36
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(UI.requireComponent(UIComponents.ErrorReportModal), {
37
+ reports,
38
+ });
35
39
  }
36
40
 
37
41
  public async report(error: ErrorSource, message?: string): Promise<void> {
38
- if (App.isDevelopment || App.isTesting) {
42
+ if (App.development || App.testing) {
39
43
  this.logError(error);
40
44
  }
41
45
 
@@ -70,9 +74,10 @@ export class ErrorsService extends Service {
70
74
  text: translateWithDefault('errors.viewDetails', 'View details'),
71
75
  dismiss: true,
72
76
  handler: () =>
73
- UI.openModal(UI.requireComponent(UIComponents.ErrorReportModal), {
74
- reports: [report],
75
- }),
77
+ UI.openModal<ModalComponent<AGErrorReportModalProps>>(
78
+ UI.requireComponent(UIComponents.ErrorReportModal),
79
+ { reports: [report] },
80
+ ),
76
81
  },
77
82
  ],
78
83
  },
@@ -105,6 +110,22 @@ export class ErrorsService extends Service {
105
110
  });
106
111
  }
107
112
 
113
+ public getErrorMessage(error: ErrorSource): string {
114
+ if (typeof error === 'string') {
115
+ return error;
116
+ }
117
+
118
+ if (error instanceof Error || error instanceof JSError) {
119
+ return error.message;
120
+ }
121
+
122
+ if (isObject(error)) {
123
+ return toString(error['message'] ?? error['description'] ?? 'Unknown error object');
124
+ }
125
+
126
+ return translateWithDefault('errors.unknown', 'Unknown Error');
127
+ }
128
+
108
129
  private logError(error: unknown): void {
109
130
  // eslint-disable-next-line no-console
110
131
  console.error(error);
@@ -125,14 +146,20 @@ export class ErrorsService extends Service {
125
146
 
126
147
  if (isObject(error)) {
127
148
  return objectWithoutEmpty({
128
- title: toString(error['name'] ?? error['title'] ?? translate('errors.unknown')),
129
- description: toString(error['message'] ?? error['description'] ?? ''),
149
+ title: toString(
150
+ error['name'] ?? error['title'] ?? translateWithDefault('errors.unknown', 'Unknown Error'),
151
+ ),
152
+ description: toString(
153
+ error['message'] ??
154
+ error['description'] ??
155
+ translateWithDefault('errors.unknownDescription', 'Unknown error object'),
156
+ ),
130
157
  error,
131
158
  });
132
159
  }
133
160
 
134
161
  return {
135
- title: translate('errors.unknown'),
162
+ title: translateWithDefault('errors.unknown', 'Unknown Error'),
136
163
  error,
137
164
  };
138
165
  }
@@ -1,4 +1,4 @@
1
- import { tap } from '@noeldemartin/utils';
1
+ import type { App } from 'vue';
2
2
 
3
3
  import { bootServices } from '@/services';
4
4
  import { definePlugin } from '@/plugins';
@@ -25,14 +25,12 @@ const frameworkHandler: ErrorHandler = (error) => {
25
25
  return true;
26
26
  };
27
27
 
28
- function setUpErrorHandler(baseHandler: ErrorHandler = () => false): ErrorHandler {
29
- return tap(
30
- (error) => baseHandler(error) || frameworkHandler(error),
31
- (errorHandler) => {
32
- globalThis.onerror = (message, _, __, ___, error) => errorHandler(error ?? message);
33
- globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
34
- },
35
- );
28
+ function setUpErrorHandler(app: App, baseHandler: ErrorHandler = () => false): void {
29
+ const errorHandler: ErrorHandler = (error) => baseHandler(error) || frameworkHandler(error);
30
+
31
+ app.config.errorHandler = errorHandler;
32
+ globalThis.onerror = (event, _, __, ___, error) => errorHandler(error ?? event);
33
+ globalThis.onunhandledrejection = (event) => errorHandler(event.reason);
36
34
  }
37
35
 
38
36
  export type ErrorHandler = (error: ErrorSource) => boolean;
@@ -40,16 +38,14 @@ export type ErrorsServices = typeof services;
40
38
 
41
39
  export default definePlugin({
42
40
  async install(app, options) {
43
- const errorHandler = setUpErrorHandler(options.handleError);
44
-
45
- app.config.errorHandler = errorHandler;
41
+ setUpErrorHandler(app, options.handleError);
46
42
 
47
43
  await bootServices(app, services);
48
44
  },
49
45
  });
50
46
 
51
47
  declare module '@/bootstrap/options' {
52
- interface AerogelOptions {
48
+ export interface AerogelOptions {
53
49
  handleError?(error: ErrorSource): boolean;
54
50
  }
55
51
  }
package/src/forms/Form.ts CHANGED
@@ -7,6 +7,7 @@ export const FormFieldTypes = {
7
7
  String: 'string',
8
8
  Number: 'number',
9
9
  Boolean: 'boolean',
10
+ Object: 'object',
10
11
  } as const;
11
12
 
12
13
  export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
@@ -36,15 +37,18 @@ export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.Strin
36
37
  ? number
37
38
  : TType extends typeof FormFieldTypes.Boolean
38
39
  ? boolean
40
+ : TType extends typeof FormFieldTypes.Object
41
+ ? object
39
42
  : never;
40
43
 
44
+ const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
45
+
41
46
  export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
42
47
 
43
48
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
44
49
 
45
50
  private _fields: Fields;
46
51
  private _data: FormData<Fields>;
47
- private _valid: ComputedRef<boolean>;
48
52
  private _submitted: Ref<boolean>;
49
53
  private _errors: FormErrors<Fields>;
50
54
 
@@ -55,13 +59,17 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
55
59
  this._submitted = ref(false);
56
60
  this._data = this.getInitialData(fields);
57
61
  this._errors = this.getInitialErrors(fields);
58
- this._valid = computed(() => !Object.values(this._errors).some((error) => error !== null));
62
+
63
+ validForms.set(
64
+ this,
65
+ computed(() => !Object.values(this._errors).some((error) => error !== null)),
66
+ );
59
67
 
60
68
  this.errors = readonly(this._errors);
61
69
  }
62
70
 
63
71
  public get valid(): boolean {
64
- return this._valid.value;
72
+ return !!validForms.get(this)?.value;
65
73
  }
66
74
 
67
75
  public get submitted(): boolean {
@@ -92,11 +100,11 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
92
100
  return this.valid;
93
101
  }
94
102
 
95
- public reset(): void {
103
+ public reset(options: { keepData?: boolean; keepErrors?: boolean } = {}): void {
96
104
  this._submitted.value = false;
97
105
 
98
- this.resetData();
99
- this.resetErrors();
106
+ options.keepData || this.resetData();
107
+ options.keepErrors || this.resetErrors();
100
108
  }
101
109
 
102
110
  public submit(): boolean {
package/src/lang/Lang.ts CHANGED
@@ -17,7 +17,7 @@ export class LangService extends Service {
17
17
  this.provider = {
18
18
  translate: (key) => {
19
19
  // eslint-disable-next-line no-console
20
- App.isDevelopment && console.warn('Lang provider is missing');
20
+ App.development && console.warn('Lang provider is missing');
21
21
 
22
22
  return key;
23
23
  },
@@ -0,0 +1 @@
1
+ import './assets/histoire.css';
@@ -3,5 +3,6 @@ import type { App } from 'vue';
3
3
  import type { AerogelOptions } from '@/bootstrap/options';
4
4
 
5
5
  export interface Plugin {
6
+ name?: string;
6
7
  install(app: App, options: AerogelOptions): void | Promise<void>;
7
8
  }
@@ -1,3 +1,7 @@
1
+ import type { GetClosureArgs } from '@noeldemartin/utils';
2
+
3
+ import App from '@/services/App';
4
+
1
5
  import type { Plugin } from './Plugin';
2
6
 
3
7
  export * from './Plugin';
@@ -5,3 +9,18 @@ export * from './Plugin';
5
9
  export function definePlugin<T extends Plugin>(plugin: T): T {
6
10
  return plugin;
7
11
  }
12
+
13
+ export async function installPlugins(plugins: Plugin[], ...args: GetClosureArgs<Plugin['install']>): Promise<void> {
14
+ App.setState(
15
+ 'plugins',
16
+ plugins.reduce((pluginsMap, plugin) => {
17
+ if (plugin.name) {
18
+ pluginsMap[plugin.name] = plugin;
19
+ }
20
+
21
+ return pluginsMap;
22
+ }, {} as Record<string, Plugin>),
23
+ );
24
+
25
+ await Promise.all(plugins.map((plugin) => plugin.install(...args)) ?? []);
26
+ }
@@ -1,16 +1,18 @@
1
- import Build from 'virtual:aerogel';
1
+ import Aerogel from 'virtual:aerogel';
2
2
 
3
3
  import { defineServiceState } from '@/services/Service';
4
+ import type { Plugin } from '@/plugins/Plugin';
4
5
 
5
6
  export default defineServiceState({
6
7
  name: 'app',
7
8
  initialState: {
8
- environment: Build.environment,
9
- sourceUrl: Build.sourceUrl,
9
+ plugins: {} as Record<string, Plugin>,
10
+ environment: Aerogel.environment,
11
+ sourceUrl: Aerogel.sourceUrl,
10
12
  isMounted: false,
11
13
  },
12
14
  computed: {
13
- isDevelopment: (state) => state.environment === 'development',
14
- isTesting: (state) => state.environment === 'testing',
15
+ development: (state) => state.environment === 'development',
16
+ testing: (state) => state.environment === 'testing',
15
17
  },
16
18
  });
@@ -1,14 +1,26 @@
1
- import { facade } from '@noeldemartin/utils';
1
+ import { facade, forever, updateLocationQueryParameters } from '@noeldemartin/utils';
2
2
 
3
3
  import Events from '@/services/Events';
4
+ import type { Plugin } from '@/plugins';
4
5
 
5
6
  import Service from './App.state';
6
7
 
7
8
  export class AppService extends Service {
8
9
 
9
- protected async boot(): Promise<void> {
10
- await super.boot();
10
+ public async reload(queryParameters?: Record<string, string | undefined>): Promise<void> {
11
+ queryParameters && updateLocationQueryParameters(queryParameters);
12
+
13
+ location.reload();
14
+
15
+ // Stall until the reload happens
16
+ await forever();
17
+ }
11
18
 
19
+ public plugin<T extends Plugin = Plugin>(name: string): T | null {
20
+ return (this.plugins[name] as T) ?? null;
21
+ }
22
+
23
+ protected async boot(): Promise<void> {
12
24
  Events.once('application-mounted', () => this.setState({ isMounted: true }));
13
25
  }
14
26
 
@@ -93,7 +93,8 @@ export default class Service<
93
93
  const handleError = (error: unknown) => this._booted.reject(new ServiceBootError(this._name, error));
94
94
 
95
95
  try {
96
- this.boot()
96
+ this.frameworkBoot()
97
+ .then(() => this.boot())
97
98
  .then(() => this._booted.resolve())
98
99
  .catch(handleError);
99
100
  } catch (error) {
@@ -161,7 +162,11 @@ export default class Service<
161
162
  return;
162
163
  }
163
164
 
164
- const storage = Storage.require<ServiceStorage>(this._name);
165
+ const storage = Storage.get<ServiceStorage>(this._name);
166
+
167
+ if (!storage) {
168
+ return;
169
+ }
165
170
 
166
171
  Storage.set(this._name, {
167
172
  ...storage,
@@ -189,11 +194,15 @@ export default class Service<
189
194
  return state;
190
195
  }
191
196
 
197
+ protected async frameworkBoot(): Promise<void> {
198
+ this.initializePersistedState();
199
+ }
200
+
192
201
  protected async boot(): Promise<void> {
193
- this.restorePersistedState();
202
+ // Placeholder for overrides, don't place any functionality here.
194
203
  }
195
204
 
196
- protected restorePersistedState(): void {
205
+ protected initializePersistedState(): void {
197
206
  // TODO fix this.static()
198
207
  const persist = (this.constructor as unknown as { persist: string[] }).persist;
199
208
 
@@ -24,13 +24,16 @@ export interface Services extends DefaultServices {}
24
24
 
25
25
  export async function bootServices(app: VueApp, services: Record<string, Service>): Promise<void> {
26
26
  await Promise.all(
27
- Object.entries(services).map(async ([_, service]) => {
28
- // eslint-disable-next-line no-console
29
- await service.launch().catch((error) => console.error(error));
27
+ Object.entries(services).map(async ([name, service]) => {
28
+ await service
29
+ .launch()
30
+ .catch((error) => app.config.errorHandler?.(error, null, `Failed launching ${name}.`));
30
31
  }),
31
32
  );
32
33
 
33
34
  Object.assign(app.config.globalProperties, services);
35
+
36
+ App.development && Object.assign(window, services);
34
37
  }
35
38
 
36
39
  export default definePlugin({
@@ -47,7 +50,7 @@ export default definePlugin({
47
50
  });
48
51
 
49
52
  declare module '@/bootstrap/options' {
50
- interface AerogelOptions {
53
+ export interface AerogelOptions {
51
54
  services?: Record<string, Service>;
52
55
  }
53
56
  }
package/src/ui/UI.ts CHANGED
@@ -1,4 +1,4 @@
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';
@@ -8,6 +8,7 @@ import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackb
8
8
 
9
9
  import Service from './UI.state';
10
10
  import type { Modal, ModalComponent, Snackbar } from './UI.state';
11
+ import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps } from '@/components';
11
12
 
12
13
  interface ModalCallbacks<T = unknown> {
13
14
  willClose(result: T | undefined): void;
@@ -25,10 +26,16 @@ export const UIComponents = {
25
26
  ErrorReportModal: 'error-report-modal',
26
27
  LoadingModal: 'loading-modal',
27
28
  Snackbar: 'snackbar',
29
+ StartupCrash: 'startup-crash',
28
30
  } as const;
29
31
 
30
32
  export type UIComponent = ObjectValues<typeof UIComponents>;
31
33
 
34
+ export interface ConfirmOptions {
35
+ acceptText?: string;
36
+ cancelText?: string;
37
+ }
38
+
32
39
  export interface ShowSnackbarOptions {
33
40
  component?: Component;
34
41
  color?: SnackbarColor;
@@ -47,18 +54,45 @@ export class UIService extends Service {
47
54
  public alert(message: string): void;
48
55
  public alert(title: string, message: string): void;
49
56
  public alert(messageOrTitle: string, message?: string): void {
50
- const options = typeof message === 'string' ? { title: messageOrTitle, message } : { message: messageOrTitle };
57
+ const getProperties = (): AGAlertModalProps => {
58
+ if (typeof message !== 'string') {
59
+ return { message: messageOrTitle };
60
+ }
51
61
 
52
- this.openModal(this.requireComponent(UIComponents.AlertModal), options);
62
+ return {
63
+ title: messageOrTitle,
64
+ message,
65
+ };
66
+ };
67
+
68
+ this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
53
69
  }
54
70
 
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 };
71
+ public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
72
+ public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
73
+ public async confirm(
74
+ messageOrTitle: string,
75
+ messageOrOptions?: string | ConfirmOptions,
76
+ options?: ConfirmOptions,
77
+ ): Promise<boolean> {
78
+ const getProperties = (): AGConfirmModalProps => {
79
+ if (typeof messageOrOptions !== 'string') {
80
+ return {
81
+ message: messageOrTitle,
82
+ ...(messageOrOptions ?? {}),
83
+ };
84
+ }
85
+
86
+ return {
87
+ title: messageOrTitle,
88
+ message: messageOrOptions,
89
+ ...(options ?? {}),
90
+ };
91
+ };
92
+
59
93
  const modal = await this.openModal<ModalComponent<{ message: string }, boolean>>(
60
94
  this.requireComponent(UIComponents.ConfirmModal),
61
- options,
95
+ getProperties(),
62
96
  );
63
97
  const result = await modal.beforeClose;
64
98
 
@@ -68,15 +102,25 @@ export class UIService extends Service {
68
102
  public async loading<T>(operation: Promise<T>): Promise<T>;
69
103
  public async loading<T>(message: string, operation: Promise<T>): Promise<T>;
70
104
  public async loading<T>(messageOrOperation: string | Promise<T>, operation?: Promise<T>): Promise<T> {
71
- operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
105
+ const getProperties = (): AGLoadingModalProps => {
106
+ if (typeof messageOrOperation !== 'string') {
107
+ return {};
108
+ }
109
+
110
+ return { message: messageOrOperation };
111
+ };
72
112
 
73
- const message = typeof messageOrOperation === 'string' ? messageOrOperation : undefined;
74
- const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), { message });
75
- const result = await operation;
113
+ const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), getProperties());
76
114
 
77
- await this.closeModal(modal.id);
115
+ try {
116
+ operation = typeof messageOrOperation === 'string' ? (operation as Promise<T>) : messageOrOperation;
78
117
 
79
- return result;
118
+ const [result] = await Promise.all([operation, after({ seconds: 1 })]);
119
+
120
+ return result;
121
+ } finally {
122
+ await this.closeModal(modal.id);
123
+ }
80
124
  }
81
125
 
82
126
  public showSnackbar(message: string, options: ShowSnackbarOptions = {}): void {
@@ -137,9 +181,8 @@ export class UIService extends Service {
137
181
  }
138
182
 
139
183
  protected async boot(): Promise<void> {
140
- await super.boot();
141
-
142
184
  this.watchModalEvents();
185
+ this.watchMountedEvent();
143
186
  }
144
187
 
145
188
  private watchModalEvents(): void {
@@ -167,6 +210,24 @@ export class UIService extends Service {
167
210
  });
168
211
  }
169
212
 
213
+ private watchMountedEvent(): void {
214
+ Events.once('application-mounted', async () => {
215
+ const splash = document.getElementById('splash');
216
+
217
+ if (!splash) {
218
+ return;
219
+ }
220
+
221
+ if (window.getComputedStyle(splash).opacity !== '0') {
222
+ splash.style.opacity = '0';
223
+
224
+ await after({ ms: 600 });
225
+ }
226
+
227
+ splash.remove();
228
+ });
229
+ }
230
+
170
231
  }
171
232
 
172
233
  export default facade(new UIService());
package/src/ui/index.ts CHANGED
@@ -9,12 +9,14 @@ 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
11
  import AGSnackbar from '../components/snackbars/AGSnackbar.vue';
12
+ import AGStartupCrash from '../components/lib/AGStartupCrash.vue';
12
13
  import type { UIComponent } from './UI';
13
14
 
14
- export { UI, UIComponents, UIComponent };
15
-
16
15
  const services = { $ui: UI };
17
16
 
17
+ export * from './UI';
18
+ export { default as UI } from './UI';
19
+
18
20
  export type UIServices = typeof services;
19
21
 
20
22
  export default definePlugin({
@@ -25,6 +27,7 @@ export default definePlugin({
25
27
  [UIComponents.ErrorReportModal]: AGErrorReportModal,
26
28
  [UIComponents.LoadingModal]: AGLoadingModal,
27
29
  [UIComponents.Snackbar]: AGSnackbar,
30
+ [UIComponents.StartupCrash]: AGStartupCrash,
28
31
  };
29
32
 
30
33
  Object.entries({
@@ -37,7 +40,7 @@ export default definePlugin({
37
40
  });
38
41
 
39
42
  declare module '@/bootstrap/options' {
40
- interface AerogelOptions {
43
+ export interface AerogelOptions {
41
44
  components?: Partial<Record<UIComponent, Component>>;
42
45
  }
43
46
  }
@@ -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';