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

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 (166) hide show
  1. package/dist/aerogel-core.d.ts +1516 -1471
  2. package/dist/aerogel-core.js +2960 -0
  3. package/dist/aerogel-core.js.map +1 -0
  4. package/package.json +27 -37
  5. package/src/bootstrap/bootstrap.test.ts +4 -8
  6. package/src/bootstrap/index.ts +26 -16
  7. package/src/bootstrap/options.ts +1 -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 -5
  11. package/src/components/{AGAppSnackbars.vue → AppSnackbars.vue} +1 -1
  12. package/src/components/composition.ts +23 -0
  13. package/src/components/contracts/AlertModal.ts +4 -0
  14. package/src/components/contracts/Button.ts +15 -0
  15. package/src/components/contracts/ConfirmModal.ts +41 -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 +9 -0
  20. package/src/components/contracts/PromptModal.ts +28 -0
  21. package/src/components/contracts/index.ts +7 -0
  22. package/src/components/contracts/shared.ts +9 -0
  23. package/src/components/forms/AGSelect.vue +11 -17
  24. package/src/components/forms/index.ts +0 -4
  25. package/src/components/headless/HeadlessButton.vue +45 -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} +16 -18
  33. package/src/components/headless/HeadlessModalContent.vue +24 -0
  34. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  35. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  36. package/src/components/headless/forms/AGHeadlessSelect.ts +3 -3
  37. package/src/components/headless/forms/AGHeadlessSelect.vue +16 -16
  38. package/src/components/headless/forms/AGHeadlessSelectError.vue +2 -2
  39. package/src/components/headless/forms/AGHeadlessSelectOption.vue +10 -18
  40. package/src/components/headless/forms/AGHeadlessSelectOptions.vue +19 -0
  41. package/src/components/headless/forms/AGHeadlessSelectTrigger.vue +25 -0
  42. package/src/components/headless/forms/composition.ts +10 -0
  43. package/src/components/headless/forms/index.ts +3 -9
  44. package/src/components/headless/index.ts +12 -1
  45. package/src/components/headless/snackbars/index.ts +3 -3
  46. package/src/components/index.ts +6 -4
  47. package/src/components/lib/AGErrorMessage.vue +4 -4
  48. package/src/components/lib/AGMarkdown.vue +24 -6
  49. package/src/components/lib/AGMeasured.vue +3 -2
  50. package/src/components/lib/AGStartupCrash.vue +6 -6
  51. package/src/components/lib/index.ts +0 -1
  52. package/src/components/snackbars/AGSnackbar.vue +8 -6
  53. package/src/components/ui/AlertModal.vue +13 -0
  54. package/src/components/ui/Button.vue +58 -0
  55. package/src/components/ui/Checkbox.vue +49 -0
  56. package/src/components/ui/ConfirmModal.vue +42 -0
  57. package/src/components/ui/ErrorReportModal.vue +62 -0
  58. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +29 -20
  59. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  60. package/src/components/ui/Form.vue +24 -0
  61. package/src/components/ui/Input.vue +52 -0
  62. package/src/components/ui/Link.vue +12 -0
  63. package/src/components/ui/LoadingModal.vue +32 -0
  64. package/src/components/ui/Modal.vue +55 -0
  65. package/src/components/ui/ModalContext.vue +30 -0
  66. package/src/components/ui/ProgressBar.vue +50 -0
  67. package/src/components/ui/PromptModal.vue +35 -0
  68. package/src/components/ui/index.ts +15 -0
  69. package/src/components/utils.ts +106 -9
  70. package/src/directives/index.ts +11 -5
  71. package/src/directives/measure.ts +34 -6
  72. package/src/errors/Errors.state.ts +1 -1
  73. package/src/errors/Errors.ts +25 -28
  74. package/src/errors/JobCancelledError.ts +3 -0
  75. package/src/errors/index.ts +10 -16
  76. package/src/errors/utils.ts +35 -0
  77. package/src/forms/{Form.test.ts → FormController.test.ts} +33 -4
  78. package/src/forms/{Form.ts → FormController.ts} +85 -25
  79. package/src/forms/composition.ts +4 -4
  80. package/src/forms/index.ts +3 -1
  81. package/src/forms/utils.ts +36 -5
  82. package/src/forms/validation.ts +19 -0
  83. package/src/index.css +8 -0
  84. package/src/{main.ts → index.ts} +3 -0
  85. package/src/jobs/Job.ts +147 -0
  86. package/src/jobs/index.ts +10 -0
  87. package/src/jobs/listeners.ts +3 -0
  88. package/src/jobs/status.ts +4 -0
  89. package/src/lang/DefaultLangProvider.ts +46 -0
  90. package/src/lang/Lang.state.ts +11 -0
  91. package/src/lang/Lang.ts +44 -29
  92. package/src/lang/index.ts +8 -6
  93. package/src/plugins/Plugin.ts +1 -1
  94. package/src/plugins/index.ts +10 -7
  95. package/src/services/App.state.ts +27 -4
  96. package/src/services/App.ts +12 -4
  97. package/src/services/Cache.ts +43 -0
  98. package/src/services/Events.test.ts +39 -0
  99. package/src/services/Events.ts +112 -32
  100. package/src/services/Service.ts +150 -55
  101. package/src/services/Storage.ts +20 -0
  102. package/src/services/index.ts +14 -5
  103. package/src/services/store.ts +8 -5
  104. package/src/services/utils.ts +18 -0
  105. package/src/testing/index.ts +26 -0
  106. package/src/testing/setup.ts +11 -0
  107. package/src/ui/UI.state.ts +17 -5
  108. package/src/ui/UI.ts +176 -60
  109. package/src/ui/index.ts +17 -16
  110. package/src/ui/utils.ts +16 -0
  111. package/src/utils/composition/events.ts +2 -2
  112. package/src/utils/composition/forms.ts +4 -3
  113. package/src/utils/composition/persistent.test.ts +33 -0
  114. package/src/utils/composition/persistent.ts +11 -0
  115. package/src/utils/composition/state.test.ts +47 -0
  116. package/src/utils/composition/state.ts +24 -0
  117. package/src/utils/index.ts +2 -0
  118. package/src/utils/markdown.test.ts +50 -0
  119. package/src/utils/markdown.ts +19 -6
  120. package/src/utils/vue.ts +22 -15
  121. package/dist/aerogel-core.cjs.js +0 -2
  122. package/dist/aerogel-core.cjs.js.map +0 -1
  123. package/dist/aerogel-core.esm.js +0 -2
  124. package/dist/aerogel-core.esm.js.map +0 -1
  125. package/histoire.config.ts +0 -7
  126. package/noeldemartin.config.js +0 -5
  127. package/postcss.config.js +0 -6
  128. package/src/assets/histoire.css +0 -3
  129. package/src/components/forms/AGButton.vue +0 -44
  130. package/src/components/forms/AGCheckbox.vue +0 -41
  131. package/src/components/forms/AGForm.vue +0 -26
  132. package/src/components/forms/AGInput.vue +0 -38
  133. package/src/components/headless/forms/AGHeadlessButton.vue +0 -51
  134. package/src/components/headless/forms/AGHeadlessInput.ts +0 -28
  135. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  136. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -45
  137. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  138. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  139. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  140. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  141. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  142. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  143. package/src/components/headless/modals/index.ts +0 -4
  144. package/src/components/lib/AGLink.vue +0 -9
  145. package/src/components/modals/AGAlertModal.ts +0 -15
  146. package/src/components/modals/AGAlertModal.vue +0 -14
  147. package/src/components/modals/AGConfirmModal.ts +0 -27
  148. package/src/components/modals/AGConfirmModal.vue +0 -26
  149. package/src/components/modals/AGErrorReportModal.ts +0 -46
  150. package/src/components/modals/AGErrorReportModal.vue +0 -54
  151. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  152. package/src/components/modals/AGLoadingModal.ts +0 -23
  153. package/src/components/modals/AGLoadingModal.vue +0 -15
  154. package/src/components/modals/AGModal.ts +0 -10
  155. package/src/components/modals/AGModal.vue +0 -39
  156. package/src/components/modals/AGModalContext.ts +0 -8
  157. package/src/components/modals/AGModalContext.vue +0 -22
  158. package/src/components/modals/AGModalTitle.vue +0 -9
  159. package/src/components/modals/AGPromptModal.ts +0 -30
  160. package/src/components/modals/AGPromptModal.vue +0 -34
  161. package/src/components/modals/index.ts +0 -17
  162. package/src/directives/initial-focus.ts +0 -11
  163. package/src/main.histoire.ts +0 -1
  164. package/tailwind.config.js +0 -4
  165. package/tsconfig.json +0 -11
  166. package/vite.config.ts +0 -14
@@ -1,69 +1,114 @@
1
- import { MagicObject, PromisedValue, Storage, isEmpty, objectDeepClone, objectOnly } from '@noeldemartin/utils';
2
- import type { Constructor } from '@noeldemartin/utils';
3
- import type { MaybeRef } from 'vue';
1
+ import {
2
+ MagicObject,
3
+ PromisedValue,
4
+ Storage,
5
+ arrayFrom,
6
+ fail,
7
+ isEmpty,
8
+ objectDeepClone,
9
+ objectOnly,
10
+ } from '@noeldemartin/utils';
11
+ import type { Constructor, Nullable } from '@noeldemartin/utils';
4
12
  import type { Store } from 'pinia';
5
13
 
6
- import ServiceBootError from '@/errors/ServiceBootError';
7
- import { defineServiceStore } from '@/services/store';
14
+ import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
15
+ import { defineServiceStore } from '@aerogel/core/services/store';
16
+ import type { Unref } from '@aerogel/core/utils/vue';
8
17
 
9
18
  export type ServiceState = Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
10
19
  export type DefaultServiceState = any; // eslint-disable-line @typescript-eslint/no-explicit-any
11
20
  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
- };
15
21
 
16
22
  export type ComputedStateDefinition<TState extends ServiceState, TComputedState extends ServiceState> = {
17
- [K in keyof TComputedState]: (state: UnrefServiceState<TState>) => TComputedState[K];
23
+ [K in keyof TComputedState]: (state: Unref<TState>) => TComputedState[K];
18
24
  } & ThisType<{
19
25
  readonly [K in keyof TComputedState]: TComputedState[K];
20
26
  }>;
21
27
 
28
+ export type StateWatchers<TService extends Service, TState extends ServiceState> = {
29
+ [K in keyof TState]?: (this: TService, value: TState[K], oldValue: TState[K]) => unknown;
30
+ };
31
+
32
+ export type ServiceWithState<
33
+ State extends ServiceState = ServiceState,
34
+ ComputedState extends ServiceState = {},
35
+ ServiceStorage = Partial<State>,
36
+ > = Constructor<Unref<State>> &
37
+ Constructor<ComputedState> &
38
+ Constructor<Service<Unref<State>, ComputedState, Unref<ServiceStorage>>>;
39
+
22
40
  export function defineServiceState<
23
41
  State extends ServiceState = ServiceState,
24
- ComputedState extends ServiceState = {}
42
+ ComputedState extends ServiceState = {},
43
+ ServiceStorage = Partial<State>,
25
44
  >(options: {
26
45
  name: string;
27
- initialState: State;
46
+ initialState: State | (() => State);
28
47
  persist?: (keyof State)[];
48
+ watch?: StateWatchers<Service, State>;
29
49
  computed?: ComputedStateDefinition<State, ComputedState>;
30
- serialize?: (state: Partial<State>) => Partial<State>;
31
- }): Constructor<UnrefServiceState<State>> &
32
- Constructor<ComputedState> &
33
- Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>> {
34
- return class extends Service<UnrefServiceState<State>, ComputedState> {
50
+ serialize?: (state: Partial<State>) => ServiceStorage;
51
+ restore?: (state: ServiceStorage) => Partial<State>;
52
+ }): ServiceWithState<State, ComputedState, ServiceStorage> {
53
+ return class extends Service<Unref<State>, ComputedState, ServiceStorage> {
35
54
 
36
- public static persist = (options.persist as string[]) ?? [];
55
+ public static override persist = (options.persist as string[]) ?? [];
37
56
 
38
- protected usesStore(): boolean {
57
+ protected override usesStore(): boolean {
39
58
  return true;
40
59
  }
41
60
 
42
- protected getName(): string | null {
61
+ protected override getName(): string | null {
43
62
  return options.name ?? null;
44
63
  }
45
64
 
46
- protected getInitialState(): UnrefServiceState<State> {
47
- return options.initialState;
65
+ protected override getInitialState(): Unref<State> {
66
+ if (typeof options.initialState === 'function') {
67
+ return options.initialState();
68
+ }
69
+
70
+ return Object.entries(options.initialState).reduce((state, [key, value]) => {
71
+ try {
72
+ value = structuredClone(value);
73
+ } catch (error) {
74
+ // eslint-disable-next-line no-console
75
+ console.warn(
76
+ `Could not clone '${key}' state from ${this.getName()} service, ` +
77
+ 'this may cause problems if you\'re using multiple instances of the service ' +
78
+ '(for example, in unit tests).\n' +
79
+ 'To fix this problem, declare your initialState as a function instead.',
80
+ );
81
+ }
82
+
83
+ state[key as keyof State] = value;
84
+
85
+ return state;
86
+ }, {} as Unref<State>);
87
+ }
88
+
89
+ protected override getComputedStateDefinition(): ComputedStateDefinition<Unref<State>, ComputedState> {
90
+ return (options.computed ?? {}) as ComputedStateDefinition<Unref<State>, ComputedState>;
48
91
  }
49
92
 
50
- protected getComputedStateDefinition(): ComputedStateDefinition<UnrefServiceState<State>, ComputedState> {
51
- return (options.computed ?? {}) as ComputedStateDefinition<UnrefServiceState<State>, ComputedState>;
93
+ protected override getStateWatchers(): StateWatchers<Service, Unref<State>> {
94
+ return (options.watch ?? {}) as StateWatchers<Service, Unref<State>>;
52
95
  }
53
96
 
54
- protected serializePersistedState(state: Partial<State>): Partial<State> {
55
- return options.serialize?.(state) ?? state;
97
+ protected override serializePersistedState(state: Partial<State>): ServiceStorage {
98
+ return options.serialize?.(state) ?? (state as ServiceStorage);
99
+ }
100
+
101
+ protected override deserializePersistedState(state: ServiceStorage): Partial<State> {
102
+ return options.restore?.(state) ?? (state as Partial<State>);
56
103
  }
57
104
 
58
- } as unknown as Constructor<UnrefServiceState<State>> &
59
- Constructor<ComputedState> &
60
- Constructor<Service<UnrefServiceState<State>, ComputedState, Partial<UnrefServiceState<State>>>>;
105
+ } as unknown as ServiceWithState<State, ComputedState, ServiceStorage>;
61
106
  }
62
107
 
63
108
  export default class Service<
64
109
  State extends ServiceState = DefaultServiceState,
65
110
  ComputedState extends ServiceState = {},
66
- ServiceStorage extends Partial<State> = Partial<State>
111
+ ServiceStorage = Partial<State>,
67
112
  > extends MagicObject {
68
113
 
69
114
  public static persist: string[] = [];
@@ -71,7 +116,8 @@ export default class Service<
71
116
  protected _name: string;
72
117
  private _booted: PromisedValue<void>;
73
118
  private _computedStateKeys: Set<keyof State>;
74
- private _store?: Store | false;
119
+ private _watchers: StateWatchers<Service, State>;
120
+ private _store: Store<string, State, ComputedState, {}> | false;
75
121
 
76
122
  constructor() {
77
123
  super();
@@ -81,6 +127,7 @@ export default class Service<
81
127
  this._name = this.getName() ?? new.target.name;
82
128
  this._booted = new PromisedValue();
83
129
  this._computedStateKeys = new Set(Object.keys(getters));
130
+ this._watchers = this.getStateWatchers();
84
131
  this._store =
85
132
  this.usesStore() &&
86
133
  defineServiceStore(this._name, {
@@ -95,6 +142,12 @@ export default class Service<
95
142
  return this._booted;
96
143
  }
97
144
 
145
+ public override static<T extends typeof Service>(): T;
146
+ public override static<T extends typeof Service, K extends keyof T>(property: K): T[K];
147
+ public override static<T extends typeof Service, K extends keyof T>(property?: K): T | T[K] {
148
+ return super.static<T, K>(property as K);
149
+ }
150
+
98
151
  public launch(): Promise<void> {
99
152
  const handleError = (error: unknown) => this._booted.reject(new ServiceBootError(this._name, error));
100
153
 
@@ -110,6 +163,10 @@ export default class Service<
110
163
  return this._booted;
111
164
  }
112
165
 
166
+ public hasPersistedState(): boolean {
167
+ return Storage.has(this._name);
168
+ }
169
+
113
170
  public hasState<P extends keyof State>(property: P): boolean {
114
171
  if (!this._store) {
115
172
  return false;
@@ -125,10 +182,10 @@ export default class Service<
125
182
  const store = this._store as any;
126
183
 
127
184
  if (property) {
128
- return store ? store[property] : undefined;
185
+ return store ? store[property] : (undefined as State[P]);
129
186
  }
130
187
 
131
- return store ? store : {};
188
+ return store ? store : ({} as State);
132
189
  }
133
190
 
134
191
  public setState<P extends keyof State>(property: P, value: State[P]): void;
@@ -138,16 +195,31 @@ export default class Service<
138
195
  return;
139
196
  }
140
197
 
141
- const state = (
142
- typeof stateOrProperty === 'string' ? { [stateOrProperty]: value } : stateOrProperty
143
- ) as Partial<State>;
198
+ const update = typeof stateOrProperty === 'string' ? { [stateOrProperty]: value } : stateOrProperty;
199
+ const old = objectOnly(this._store.$state as State, Object.keys(update));
200
+
201
+ Object.assign(this._store.$state, update);
202
+ this.onStateUpdated(update as Partial<State>, old as Partial<State>);
203
+ }
204
+
205
+ public updatePersistedState<T extends keyof State>(key: T): void;
206
+ public updatePersistedState<T extends keyof State>(keys: T[]): void;
207
+ public updatePersistedState<T extends keyof State>(keyOrKeys: T | T[]): void {
208
+ if (!this._store) {
209
+ return;
210
+ }
211
+
212
+ const keys = arrayFrom(keyOrKeys) as Array<keyof State>;
213
+ const state = objectOnly(this._store.$state as State, keys);
144
214
 
145
- Object.assign(this._store.$state, state);
215
+ if (isEmpty(state)) {
216
+ return;
217
+ }
146
218
 
147
- this.onStateUpdated(state);
219
+ this.onPersistentStateUpdated(state);
148
220
  }
149
221
 
150
- protected __get(property: string): unknown {
222
+ protected override __get(property: string): unknown {
151
223
  if (this.hasState(property)) {
152
224
  return this.getState(property);
153
225
  }
@@ -155,19 +227,29 @@ export default class Service<
155
227
  return super.__get(property);
156
228
  }
157
229
 
158
- protected __set(property: string, value: unknown): void {
230
+ protected override __set(property: string, value: unknown): void {
159
231
  this.setState({ [property]: value } as Partial<State>);
160
232
  }
161
233
 
162
- protected onStateUpdated(state: Partial<State>): void {
163
- // TODO fix this.static()
164
- const persist = (this.constructor as unknown as { persist: string[] }).persist;
165
- const persisted = objectOnly(state, persist);
234
+ protected onStateUpdated(update: Partial<State>, old: Partial<State>): void {
235
+ const persisted = objectOnly(update, this.static('persist'));
166
236
 
167
- if (isEmpty(persisted)) {
168
- return;
237
+ if (!isEmpty(persisted)) {
238
+ this.onPersistentStateUpdated(persisted as Partial<State>);
169
239
  }
170
240
 
241
+ for (const property in update) {
242
+ const watcher = this._watchers[property] as Nullable<(value: unknown, oldValue: unknown) => unknown>;
243
+
244
+ if (!watcher || update[property] === old[property]) {
245
+ continue;
246
+ }
247
+
248
+ watcher.call(this, update[property], old[property]);
249
+ }
250
+ }
251
+
252
+ protected onPersistentStateUpdated(persisted: Partial<State>): void {
171
253
  const storage = Storage.get<ServiceStorage>(this._name);
172
254
 
173
255
  if (!storage) {
@@ -196,34 +278,47 @@ export default class Service<
196
278
  return {} as ComputedStateDefinition<State, ComputedState>;
197
279
  }
198
280
 
199
- protected serializePersistedState(state: Partial<State>): Partial<State> {
200
- return state;
281
+ protected getStateWatchers(): StateWatchers<Service, State> {
282
+ return {};
283
+ }
284
+
285
+ protected serializePersistedState(state: Partial<State>): ServiceStorage {
286
+ return state as ServiceStorage;
287
+ }
288
+
289
+ protected deserializePersistedState(state: ServiceStorage): Partial<State> {
290
+ return state as Partial<State>;
201
291
  }
202
292
 
203
293
  protected async frameworkBoot(): Promise<void> {
204
- this.initializePersistedState();
294
+ this.restorePersistedState();
205
295
  }
206
296
 
207
297
  protected async boot(): Promise<void> {
208
298
  // Placeholder for overrides, don't place any functionality here.
209
299
  }
210
300
 
211
- protected initializePersistedState(): void {
212
- // TODO fix this.static()
213
- const persist = (this.constructor as unknown as { persist: string[] }).persist;
214
-
215
- if (!this.usesStore() || isEmpty(persist)) {
301
+ protected restorePersistedState(): void {
302
+ if (!this.usesStore() || isEmpty(this.static('persist'))) {
216
303
  return;
217
304
  }
218
305
 
219
306
  if (Storage.has(this._name)) {
220
307
  const persisted = Storage.require<ServiceStorage>(this._name);
221
- this.setState(persisted);
308
+ this.setState(this.deserializePersistedState(persisted));
222
309
 
223
310
  return;
224
311
  }
225
312
 
226
- Storage.set(this._name, objectOnly(this.getState(), persist));
313
+ Storage.set(this._name, objectOnly(this.getState(), this.static('persist')));
314
+ }
315
+
316
+ protected requireStore(): Store<string, State, ComputedState, {}> {
317
+ if (!this._store) {
318
+ return fail(`Failed getting '${this._name}' store`);
319
+ }
320
+
321
+ return this._store;
227
322
  }
228
323
 
229
324
  }
@@ -0,0 +1,20 @@
1
+ import { facade } from '@noeldemartin/utils';
2
+
3
+ import Events from '@aerogel/core/services/Events';
4
+ import Service from '@aerogel/core/services/Service';
5
+
6
+ export class StorageService extends Service {
7
+
8
+ public async purge(): Promise<void> {
9
+ await Events.emit('purge-storage');
10
+ }
11
+
12
+ }
13
+
14
+ export default facade(StorageService);
15
+
16
+ declare module '@aerogel/core/services/Events' {
17
+ export interface EventsPayload {
18
+ 'purge-storage': void;
19
+ }
20
+ }
@@ -1,21 +1,28 @@
1
1
  import type { App as VueApp } from 'vue';
2
2
 
3
- import { definePlugin } from '@/plugins';
3
+ import { definePlugin } from '@aerogel/core/plugins';
4
+ import { isDevelopment, isTesting } from '@noeldemartin/utils';
4
5
 
5
6
  import App from './App';
7
+ import Cache from './Cache';
6
8
  import Events from './Events';
7
9
  import Service from './Service';
10
+ import Storage from './Storage';
8
11
  import { getPiniaStore } from './store';
9
12
 
10
13
  export * from './App';
14
+ export * from './Cache';
11
15
  export * from './Events';
12
16
  export * from './Service';
17
+ export * from './store';
18
+ export * from './utils';
13
19
 
14
- export { App, Events, Service };
20
+ export { App, Cache, Events, Storage, Service };
15
21
 
16
22
  const defaultServices = {
17
23
  $app: App,
18
24
  $events: Events,
25
+ $storage: Storage,
19
26
  };
20
27
 
21
28
  export type DefaultServices = typeof defaultServices;
@@ -33,7 +40,9 @@ export async function bootServices(app: VueApp, services: Record<string, Service
33
40
 
34
41
  Object.assign(app.config.globalProperties, services);
35
42
 
36
- App.development && Object.assign(window, services);
43
+ if (isDevelopment() || isTesting()) {
44
+ Object.assign(globalThis, services);
45
+ }
37
46
  }
38
47
 
39
48
  export default definePlugin({
@@ -49,12 +58,12 @@ export default definePlugin({
49
58
  },
50
59
  });
51
60
 
52
- declare module '@/bootstrap/options' {
61
+ declare module '@aerogel/core/bootstrap/options' {
53
62
  export interface AerogelOptions {
54
63
  services?: Record<string, Service>;
55
64
  }
56
65
  }
57
66
 
58
- declare module '@vue/runtime-core' {
67
+ declare module 'vue' {
59
68
  interface ComponentCustomProperties extends Services {}
60
69
  }
@@ -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,18 @@
1
+ import { objectOnly } from '@noeldemartin/utils';
2
+
3
+ export type Replace<
4
+ TOriginal extends Record<string, unknown>,
5
+ TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
6
+ > = {
7
+ [K in keyof TOriginal]: TReplacements extends Record<K, infer Replacement> ? Replacement : TOriginal[K];
8
+ };
9
+
10
+ export function replaceExisting<
11
+ TOriginal extends Record<string, unknown>,
12
+ TReplacements extends Partial<Record<keyof TOriginal, unknown>>,
13
+ >(original: TOriginal, replacements: TReplacements): Replace<TOriginal, TReplacements> {
14
+ return {
15
+ ...original,
16
+ ...objectOnly(replacements, Object.keys(original)),
17
+ } as Replace<TOriginal, TReplacements>;
18
+ }
@@ -0,0 +1,26 @@
1
+ import { isTesting } from '@noeldemartin/utils';
2
+ import type { GetClosureArgs } from '@noeldemartin/utils';
3
+
4
+ import Events from '@aerogel/core/services/Events';
5
+ import { definePlugin } from '@aerogel/core/plugins';
6
+
7
+ export interface AerogelTestingRuntime {
8
+ on: (typeof Events)['on'];
9
+ }
10
+
11
+ export default definePlugin({
12
+ async install() {
13
+ if (!isTesting()) {
14
+ return;
15
+ }
16
+
17
+ globalThis.testingRuntime = {
18
+ on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
19
+ };
20
+ },
21
+ });
22
+
23
+ declare global {
24
+ // eslint-disable-next-line no-var
25
+ var testingRuntime: AerogelTestingRuntime | undefined;
26
+ }
@@ -0,0 +1,11 @@
1
+ import { FakeLocalStorage } from '@noeldemartin/testing';
2
+ import { beforeEach, vi } from 'vitest';
3
+
4
+ vi.mock('dompurify', async () => {
5
+ return { default: { sanitize: (html: string) => html } };
6
+ });
7
+
8
+ beforeEach(() => {
9
+ FakeLocalStorage.reset();
10
+ FakeLocalStorage.patchGlobal();
11
+ });
@@ -1,8 +1,10 @@
1
1
  import type { Component } from 'vue';
2
2
 
3
- import { defineServiceState } from '@/services/Service';
3
+ import { defineServiceState } from '@aerogel/core/services/Service';
4
4
 
5
- export interface Modal<T = unknown> {
5
+ import { Layouts, getCurrentLayout } from './utils';
6
+
7
+ export interface UIModal<T = unknown> {
6
8
  id: string;
7
9
  properties: Record<string, unknown>;
8
10
  component: Component;
@@ -10,11 +12,16 @@ export interface Modal<T = unknown> {
10
12
  afterClose: Promise<T | undefined>;
11
13
  }
12
14
 
15
+ export interface UIModalContext {
16
+ modal: UIModal;
17
+ childIndex?: number;
18
+ }
19
+
13
20
  export interface ModalComponent<
14
21
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
- Properties extends Record<string, unknown> = Record<string, unknown>,
22
+ Properties extends object = object,
16
23
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
- Result = unknown
24
+ Result = unknown,
18
25
  > {}
19
26
 
20
27
  export interface Snackbar {
@@ -26,7 +33,12 @@ export interface Snackbar {
26
33
  export default defineServiceState({
27
34
  name: 'ui',
28
35
  initialState: {
29
- modals: [] as Modal[],
36
+ modals: [] as UIModal[],
30
37
  snackbars: [] as Snackbar[],
38
+ layout: getCurrentLayout(),
39
+ },
40
+ computed: {
41
+ mobile: ({ layout }) => layout === Layouts.Mobile,
42
+ desktop: ({ layout }) => layout === Layouts.Desktop,
31
43
  },
32
44
  });