@aerogel/core 0.0.0-next.e4c0d5bd2801fbe93545477ea515af53df69b522 → 0.0.0-next.eb6fcafb87cdccbc72933f616799ca3124d1eca8

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 (37) hide show
  1. package/dist/aerogel-core.d.ts +257 -83
  2. package/dist/aerogel-core.js +1607 -1230
  3. package/dist/aerogel-core.js.map +1 -1
  4. package/package.json +2 -1
  5. package/src/components/contracts/AlertModal.ts +1 -1
  6. package/src/components/contracts/DropdownMenu.ts +2 -2
  7. package/src/components/contracts/Select.ts +2 -2
  8. package/src/components/headless/HeadlessInputInput.vue +16 -5
  9. package/src/components/headless/HeadlessSwitch.vue +96 -0
  10. package/src/components/headless/index.ts +1 -0
  11. package/src/components/ui/Button.vue +15 -0
  12. package/src/components/ui/Input.vue +2 -2
  13. package/src/components/ui/Modal.vue +12 -4
  14. package/src/components/ui/SelectLabel.vue +5 -1
  15. package/src/components/ui/Setting.vue +31 -0
  16. package/src/components/ui/StartupCrash.vue +51 -6
  17. package/src/components/ui/Switch.vue +11 -0
  18. package/src/components/ui/TextArea.vue +56 -0
  19. package/src/components/ui/index.ts +3 -0
  20. package/src/errors/Errors.state.ts +1 -0
  21. package/src/errors/Errors.ts +27 -6
  22. package/src/errors/index.ts +6 -2
  23. package/src/errors/settings/Debug.vue +14 -0
  24. package/src/errors/settings/index.ts +10 -0
  25. package/src/forms/FormController.test.ts +3 -0
  26. package/src/forms/FormController.ts +25 -16
  27. package/src/forms/utils.ts +25 -0
  28. package/src/forms/validation.ts +31 -0
  29. package/src/index.css +3 -0
  30. package/src/lang/index.ts +1 -1
  31. package/src/lang/settings/Language.vue +1 -1
  32. package/src/services/Service.ts +11 -6
  33. package/src/services/index.ts +2 -2
  34. package/src/testing/index.ts +4 -0
  35. package/src/ui/UI.ts +20 -4
  36. package/src/utils/app.ts +7 -0
  37. package/src/utils/index.ts +1 -0
@@ -2,31 +2,32 @@ import { computed, nextTick, reactive, readonly, ref } from 'vue';
2
2
  import { MagicObject, arrayRemove, fail, toString } from '@noeldemartin/utils';
3
3
  import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
4
4
 
5
- import { validate } from './validation';
5
+ import { validate, validateType } from './validation';
6
6
 
7
- export const __objectType: unique symbol = Symbol();
7
+ export const __valueType: unique symbol = Symbol();
8
8
 
9
9
  export interface FormFieldDefinition<
10
10
  TType extends FormFieldType = FormFieldType,
11
11
  TRules extends string = string,
12
- TObjectType = object,
12
+ TValueType = unknown,
13
13
  > {
14
14
  type: TType;
15
15
  trim?: boolean;
16
16
  default?: GetFormFieldValue<TType>;
17
17
  rules?: TRules;
18
- [__objectType]?: TObjectType;
18
+ values?: readonly TValueType[];
19
+ [__valueType]?: TValueType;
19
20
  }
20
21
 
21
- export type FormFieldType = 'string' | 'number' | 'boolean' | 'object' | 'date';
22
+ export type FormFieldType = 'string' | 'enum' | 'number' | 'boolean' | 'object' | 'date';
22
23
  export type FormFieldValue = GetFormFieldValue<FormFieldType>;
23
24
  export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
24
25
 
25
26
  export type FormData<T> = {
26
- -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules, infer TObjectType>
27
+ -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules, infer TValueType>
27
28
  ? TRules extends 'required'
28
- ? GetFormFieldValue<TType, TObjectType>
29
- : GetFormFieldValue<TType, TObjectType> | null
29
+ ? GetFormFieldValue<TType, TValueType>
30
+ : GetFormFieldValue<TType, TValueType> | null
30
31
  : never;
31
32
  };
32
33
 
@@ -34,19 +35,21 @@ export type FormErrors<T> = {
34
35
  [k in keyof T]: string[] | null;
35
36
  };
36
37
 
37
- export type GetFormFieldValue<TType, TObjectType = object> = TType extends 'string'
38
+ export type GetFormFieldValue<TType, TValueType = unknown> = TType extends 'string'
38
39
  ? string
39
40
  : TType extends 'number'
40
41
  ? number
41
42
  : TType extends 'boolean'
42
43
  ? boolean
43
- : TType extends 'object'
44
- ? TObjectType extends object
45
- ? TObjectType
46
- : object
47
- : TType extends 'date'
48
- ? Date
49
- : never;
44
+ : TType extends 'enum'
45
+ ? TValueType
46
+ : TType extends 'object'
47
+ ? TValueType extends object
48
+ ? TValueType
49
+ : object
50
+ : TType extends 'date'
51
+ ? Date
52
+ : never;
50
53
 
51
54
  const validForms: WeakMap<FormController, ComputedRef<boolean>> = new WeakMap();
52
55
 
@@ -109,6 +112,10 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
109
112
  return this._fields[field]?.rules?.split('|') ?? [];
110
113
  }
111
114
 
115
+ public getFieldType<T extends keyof Fields>(field: T): FormFieldType | null {
116
+ return this._fields[field]?.type ?? null;
117
+ }
118
+
112
119
  public data(): FormData<Fields> {
113
120
  return { ...this._data };
114
121
  }
@@ -192,6 +199,8 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
192
199
  const value = this._data[name];
193
200
  const rules = definition.rules?.split('|') ?? [];
194
201
 
202
+ errors.push(...validateType(value, definition));
203
+
195
204
  for (const rule of rules) {
196
205
  if (rule !== 'required' && (value === null || value === undefined)) {
197
206
  continue;
@@ -16,6 +16,19 @@ export function dateInput(defaultValue?: Date, options: { rules?: string } = {})
16
16
  };
17
17
  }
18
18
 
19
+ export function enumInput<const T extends string>(
20
+ values: readonly T[],
21
+ defaultValue?: T,
22
+ options: { rules?: string } = {},
23
+ ): FormFieldDefinition<'enum', string, T> {
24
+ return {
25
+ default: defaultValue,
26
+ type: 'enum',
27
+ rules: options.rules,
28
+ values,
29
+ };
30
+ }
31
+
19
32
  export function requiredBooleanInput(defaultValue?: boolean): FormFieldDefinition<'boolean', 'required'> {
20
33
  return {
21
34
  default: defaultValue,
@@ -32,6 +45,18 @@ export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<'dat
32
45
  };
33
46
  }
34
47
 
48
+ export function requiredEnumInput<const T extends string>(
49
+ values: readonly T[],
50
+ defaultValue?: T,
51
+ ): FormFieldDefinition<'enum', 'required', T> {
52
+ return {
53
+ default: defaultValue,
54
+ type: 'enum',
55
+ rules: 'required',
56
+ values,
57
+ };
58
+ }
59
+
35
60
  export function requiredNumberInput(defaultValue?: number): FormFieldDefinition<'number', 'required'> {
36
61
  return {
37
62
  default: defaultValue,
@@ -1,9 +1,32 @@
1
1
  import { arrayFrom } from '@noeldemartin/utils';
2
2
 
3
+ import type { FormFieldDefinition } from '@aerogel/core/forms/FormController';
4
+
3
5
  const builtInRules: Record<string, FormFieldValidator> = {
4
6
  required: (value) => (value ? undefined : 'required'),
5
7
  };
6
8
 
9
+ function isValidType(value: unknown, definition: FormFieldDefinition): boolean {
10
+ if (value === undefined || value === null) {
11
+ return true;
12
+ }
13
+
14
+ switch (definition.type) {
15
+ case 'string':
16
+ return typeof value === 'string';
17
+ case 'enum':
18
+ return !!definition.values?.includes(value);
19
+ case 'number':
20
+ return typeof value === 'number';
21
+ case 'boolean':
22
+ return typeof value === 'boolean';
23
+ case 'date':
24
+ return value instanceof Date;
25
+ case 'object':
26
+ return typeof value === 'object';
27
+ }
28
+ }
29
+
7
30
  export type FormFieldValidator<T = unknown> = (value: T) => string | string[] | undefined;
8
31
 
9
32
  export const validators: Record<string, FormFieldValidator> = { ...builtInRules };
@@ -12,6 +35,14 @@ export function defineFormValidationRule<T>(rule: string, validator: FormFieldVa
12
35
  validators[rule] = validator as FormFieldValidator;
13
36
  }
14
37
 
38
+ export function validateType(value: unknown, definition: FormFieldDefinition): string[] {
39
+ if (isValidType(value, definition)) {
40
+ return [];
41
+ }
42
+
43
+ return ['invalid_value'];
44
+ }
45
+
15
46
  export function validate(value: unknown, rule: string): string[] {
16
47
  const errors = validators[rule]?.(value);
17
48
 
package/src/index.css CHANGED
@@ -21,6 +21,9 @@
21
21
 
22
22
  --color-background: oklch(1 0 0);
23
23
  --color-links: var(--color-primary);
24
+
25
+ --breakpoint-content: var(--breakpoint-md);
26
+ --spacing-edge: 1rem;
24
27
  }
25
28
 
26
29
  .clickable {
package/src/lang/index.ts CHANGED
@@ -4,8 +4,8 @@ import { definePlugin } from '@aerogel/core/plugins';
4
4
 
5
5
  import Lang from './Lang';
6
6
  import settings from './settings';
7
- import type { LangProvider } from './Lang';
8
7
  import { translate, translateWithDefault } from './utils';
8
+ import type { LangProvider } from './Lang';
9
9
 
10
10
  export { Lang, translate, translateWithDefault };
11
11
  export type { LangProvider };
@@ -7,7 +7,7 @@
7
7
  :render-option="renderLocale"
8
8
  >
9
9
  <div class="grow">
10
- <SelectLabel>
10
+ <SelectLabel class="text-base font-semibold">
11
11
  {{ $td('settings.locale', 'Language') }}
12
12
  </SelectLabel>
13
13
  <Markdown
@@ -12,6 +12,7 @@ import type { Constructor, Nullable } from '@noeldemartin/utils';
12
12
  import type { Store } from 'pinia';
13
13
 
14
14
  import ServiceBootError from '@aerogel/core/errors/ServiceBootError';
15
+ import { appNamespace } from '@aerogel/core/utils/app';
15
16
  import { defineServiceStore } from '@aerogel/core/services/store';
16
17
  import type { Unref } from '@aerogel/core/utils/vue';
17
18
 
@@ -164,7 +165,7 @@ export default class Service<
164
165
  }
165
166
 
166
167
  public hasPersistedState(): boolean {
167
- return Storage.has(this._name);
168
+ return Storage.has(this.storageKey);
168
169
  }
169
170
 
170
171
  public hasState<P extends keyof State>(property: P): boolean {
@@ -231,6 +232,10 @@ export default class Service<
231
232
  this.setState({ [property]: value } as Partial<State>);
232
233
  }
233
234
 
235
+ protected get storageKey(): string {
236
+ return `${appNamespace()}:${this._name}`;
237
+ }
238
+
234
239
  protected onStateUpdated(update: Partial<State>, old: Partial<State>): void {
235
240
  const persisted = objectOnly(update, this.static('persist'));
236
241
 
@@ -250,13 +255,13 @@ export default class Service<
250
255
  }
251
256
 
252
257
  protected onPersistentStateUpdated(persisted: Partial<State>): void {
253
- const storage = Storage.get<ServiceStorage>(this._name);
258
+ const storage = Storage.get<ServiceStorage>(this.storageKey);
254
259
 
255
260
  if (!storage) {
256
261
  return;
257
262
  }
258
263
 
259
- Storage.set(this._name, {
264
+ Storage.set(this.storageKey, {
260
265
  ...storage,
261
266
  ...this.serializePersistedState(objectDeepClone(persisted) as Partial<State>),
262
267
  });
@@ -303,14 +308,14 @@ export default class Service<
303
308
  return;
304
309
  }
305
310
 
306
- if (Storage.has(this._name)) {
307
- const persisted = Storage.require<ServiceStorage>(this._name);
311
+ if (Storage.has(this.storageKey)) {
312
+ const persisted = Storage.require<ServiceStorage>(this.storageKey);
308
313
  this.setState(this.deserializePersistedState(persisted));
309
314
 
310
315
  return;
311
316
  }
312
317
 
313
- Storage.set(this._name, objectOnly(this.getState(), this.static('persist')));
318
+ Storage.set(this.storageKey, objectOnly(this.getState(), this.static('persist')));
314
319
  }
315
320
 
316
321
  protected requireStore(): Store<string, State, ComputedState, {}> {
@@ -1,4 +1,4 @@
1
- import type { App as VueApp } from 'vue';
1
+ import type { App as AppInstance } from 'vue';
2
2
 
3
3
  import { definePlugin } from '@aerogel/core/plugins';
4
4
  import { isDevelopment, isTesting } from '@noeldemartin/utils';
@@ -30,7 +30,7 @@ export type DefaultServices = typeof defaultServices;
30
30
 
31
31
  export interface Services extends DefaultServices {}
32
32
 
33
- export async function bootServices(app: VueApp, services: Record<string, Service>): Promise<void> {
33
+ export async function bootServices(app: AppInstance, services: Record<string, Service>): Promise<void> {
34
34
  await Promise.all(
35
35
  Object.entries(services).map(async ([name, service]) => {
36
36
  await service
@@ -2,10 +2,13 @@ import { isTesting } from '@noeldemartin/utils';
2
2
  import type { GetClosureArgs } from '@noeldemartin/utils';
3
3
 
4
4
  import Events from '@aerogel/core/services/Events';
5
+ import { App } from '@aerogel/core/services';
5
6
  import { definePlugin } from '@aerogel/core/plugins';
7
+ import type { Services } from '@aerogel/core/services';
6
8
 
7
9
  export interface AerogelTestingRuntime {
8
10
  on: (typeof Events)['on'];
11
+ service<T extends keyof Services>(name: T): Services[T] | null;
9
12
  }
10
13
 
11
14
  export default definePlugin({
@@ -16,6 +19,7 @@ export default definePlugin({
16
19
 
17
20
  globalThis.testingRuntime = {
18
21
  on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
22
+ service: (name) => App.service(name),
19
23
  };
20
24
  },
21
25
  });
package/src/ui/UI.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { after, facade, fail, isDevelopment, required, uuid } from '@noeldemartin/utils';
2
- import { markRaw, nextTick } from 'vue';
2
+ import { markRaw, nextTick, unref } from 'vue';
3
3
  import type { ComponentExposed, ComponentProps } from 'vue-component-type-helpers';
4
4
  import type { Component } from 'vue';
5
5
  import type { ClosureArgs } from '@noeldemartin/utils';
@@ -64,6 +64,7 @@ export type LoadingOptions = AcceptRefs<{
64
64
  title?: string;
65
65
  message?: string;
66
66
  progress?: number;
67
+ delay?: number;
67
68
  }>;
68
69
 
69
70
  export interface ConfirmOptionsWithCheckboxes<T extends ConfirmModalCheckboxes = ConfirmModalCheckboxes>
@@ -218,7 +219,11 @@ export class UIService extends Service {
218
219
  operation?: Promise<T> | (() => T),
219
220
  ): Promise<T> {
220
221
  const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
221
- const processArgs = (): { operationPromise: Promise<T>; props?: AcceptRefs<LoadingModalProps> } => {
222
+ const processArgs = (): {
223
+ operationPromise: Promise<T>;
224
+ props?: AcceptRefs<LoadingModalProps>;
225
+ delay?: number;
226
+ } => {
222
227
  if (typeof operationOrMessageOrOptions === 'string') {
223
228
  return {
224
229
  props: { message: operationOrMessageOrOptions },
@@ -230,13 +235,24 @@ export class UIService extends Service {
230
235
  return { operationPromise: processOperation(operationOrMessageOrOptions) };
231
236
  }
232
237
 
238
+ const { delay, ...props } = operationOrMessageOrOptions;
239
+
233
240
  return {
234
- props: operationOrMessageOrOptions,
241
+ props,
242
+ delay: unref(delay),
235
243
  operationPromise: processOperation(operation as Promise<T> | (() => T)),
236
244
  };
237
245
  };
238
246
 
239
- const { operationPromise, props } = processArgs();
247
+ let delayed = false;
248
+ const { operationPromise, props, delay } = processArgs();
249
+
250
+ delay && (await Promise.race([after({ ms: delay }).then(() => (delayed = true)), operationPromise]));
251
+
252
+ if (delay && !delayed) {
253
+ return operationPromise;
254
+ }
255
+
240
256
  const modal = await this.modal(this.requireComponent('loading-modal'), props);
241
257
 
242
258
  try {
@@ -0,0 +1,7 @@
1
+ import Aerogel from 'virtual:aerogel';
2
+
3
+ import { stringToSlug } from '@noeldemartin/utils';
4
+
5
+ export function appNamespace(): string {
6
+ return Aerogel.namespace ?? stringToSlug(Aerogel.name);
7
+ }
@@ -1,3 +1,4 @@
1
+ export * from './app';
1
2
  export * from './classes';
2
3
  export * from './composition/events';
3
4
  export * from './composition/forms';