@aerogel/core 0.1.1-next.bad775a386a94fedbee1d575ee1f4fb99d1f5bab → 0.1.1-next.bf5c51083c0817e96fa4f38273c32d388441f514

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 (40) hide show
  1. package/dist/aerogel-core.d.ts +364 -118
  2. package/dist/aerogel-core.js +1699 -1346
  3. package/dist/aerogel-core.js.map +1 -1
  4. package/package.json +3 -3
  5. package/src/bootstrap/index.ts +2 -1
  6. package/src/components/AppLayout.vue +1 -1
  7. package/src/components/contracts/Combobox.ts +5 -0
  8. package/src/components/contracts/Select.ts +98 -4
  9. package/src/components/contracts/index.ts +1 -0
  10. package/src/components/headless/HeadlessInputInput.vue +19 -5
  11. package/src/components/headless/HeadlessSelect.vue +10 -91
  12. package/src/components/headless/HeadlessSelectOption.vue +1 -5
  13. package/src/components/index.ts +1 -0
  14. package/src/components/ui/Combobox.vue +94 -0
  15. package/src/components/ui/ComboboxLabel.vue +29 -0
  16. package/src/components/ui/ComboboxOption.vue +46 -0
  17. package/src/components/ui/ComboboxOptions.vue +71 -0
  18. package/src/components/ui/ComboboxTrigger.vue +67 -0
  19. package/src/components/ui/Details.vue +16 -6
  20. package/src/components/ui/ProgressBar.vue +1 -1
  21. package/src/components/ui/Select.vue +2 -0
  22. package/src/components/ui/SelectTrigger.vue +13 -2
  23. package/src/components/ui/index.ts +5 -0
  24. package/src/components/vue/Provide.vue +11 -0
  25. package/src/components/vue/index.ts +1 -0
  26. package/src/errors/index.ts +5 -0
  27. package/src/forms/FormController.test.ts +4 -4
  28. package/src/forms/FormController.ts +19 -13
  29. package/src/forms/index.ts +11 -0
  30. package/src/forms/utils.ts +36 -17
  31. package/src/forms/validation.ts +5 -1
  32. package/src/index.css +1 -0
  33. package/src/jobs/Job.ts +1 -1
  34. package/src/plugins/index.ts +1 -3
  35. package/src/services/index.ts +2 -0
  36. package/src/testing/index.ts +1 -2
  37. package/src/ui/UI.ts +2 -1
  38. package/src/utils/classes.ts +2 -3
  39. package/src/utils/composition/reactiveSet.ts +10 -2
  40. package/src/utils/time.ts +6 -1
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <ComboboxAnchor class="relative">
3
+ <ComboboxInput
4
+ :id="select.id"
5
+ v-model="combobox.input"
6
+ :placeholder="select.placeholder"
7
+ :class="renderedRootClasses"
8
+ :display-value="select.renderOption"
9
+ :name="select.name"
10
+ @focus="$emit('focus')"
11
+ @blur="onBlur()"
12
+ @keydown.esc="$emit('blur')"
13
+ />
14
+ <div v-if="select?.errors" class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
15
+ <IconExclamationSolid class="size-5 text-red-500" />
16
+ </div>
17
+ </ComboboxAnchor>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import IconExclamationSolid from '~icons/zondicons/exclamation-solid';
22
+
23
+ import { ComboboxAnchor, ComboboxInput } from 'reka-ui';
24
+ import { computed, watch } from 'vue';
25
+
26
+ import { classes, injectReactiveOrFail } from '@aerogel/core/utils';
27
+ import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
28
+ import type { ComboboxContext } from '@aerogel/core/components/contracts/Combobox';
29
+
30
+ const emit = defineEmits<{ focus: []; change: []; blur: [] }>();
31
+ const select = injectReactiveOrFail<SelectExpose>('select', '<ComboboxTrigger> must be a child of a <Combobox>');
32
+ const combobox = injectReactiveOrFail<ComboboxContext>('combobox');
33
+ const renderedRootClasses = computed(() =>
34
+ classes(
35
+ // eslint-disable-next-line vue/max-len
36
+ 'block w-full rounded-md border-0 py-1.5 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6',
37
+ {
38
+ 'mt-1': select.label,
39
+ 'focus:ring-primary-600': !select.errors,
40
+ 'text-gray-900 shadow-2xs ring-gray-900/10 placeholder:text-gray-400': !select.errors,
41
+ 'pr-10 text-red-900 ring-red-900/10 placeholder:text-red-300 focus:ring-red-500': select.errors,
42
+ },
43
+ ));
44
+
45
+ function onBlur() {
46
+ const elements = Array.from(document.querySelectorAll(':hover'));
47
+
48
+ if (elements.some((element) => combobox.$group?.contains(element))) {
49
+ return;
50
+ }
51
+
52
+ emit('blur');
53
+ }
54
+
55
+ watch(
56
+ () => combobox.input,
57
+ () => {
58
+ if (combobox.preventChange) {
59
+ combobox.preventChange = false;
60
+
61
+ return;
62
+ }
63
+
64
+ emit('change');
65
+ },
66
+ );
67
+ </script>
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <details class="group">
3
- <summary
4
- class="-ml-2 flex w-[max-content] items-center rounded-lg py-2 pr-3 pl-1 hover:bg-gray-100 focus-visible:outline focus-visible:outline-gray-700"
5
- >
3
+ <summary :class="renderedSummaryClasses">
6
4
  <IconCheveronRight class="size-6 transition-transform group-open:rotate-90" />
7
- <span>{{ label }}</span>
5
+ <slot name="label">
6
+ <span>{{ label ?? $td('ui.details', 'Details') }}</span>
7
+ </slot>
8
8
  </summary>
9
9
 
10
10
  <div :class="renderedContentClasses">
@@ -14,10 +14,20 @@
14
14
  </template>
15
15
 
16
16
  <script setup lang="ts">
17
+ import IconCheveronRight from '~icons/zondicons/cheveron-right';
17
18
  import { classes } from '@aerogel/core/utils';
18
19
  import { type HTMLAttributes, computed } from 'vue';
19
- import IconCheveronRight from '~icons/zondicons/cheveron-right';
20
20
 
21
- const { label, contentClass } = defineProps<{ label: string; contentClass?: HTMLAttributes['class'] }>();
21
+ const {
22
+ label = undefined,
23
+ contentClass,
24
+ summaryClass,
25
+ } = defineProps<{ label?: string; contentClass?: HTMLAttributes['class']; summaryClass?: HTMLAttributes['class'] }>();
22
26
  const renderedContentClasses = computed(() => classes('pt-2 pl-4', contentClass));
27
+ const renderedSummaryClasses = computed(() =>
28
+ classes(
29
+ '-ml-2 flex w-[max-content] items-center rounded-lg py-2 pr-3 pl-1 max-w-full',
30
+ 'hover:bg-gray-100 focus-visible:outline focus-visible:outline-gray-700',
31
+ summaryClass,
32
+ ));
23
33
  </script>
@@ -36,7 +36,7 @@ const filledClasses = computed(() =>
36
36
  classes('size-full transition-transform duration-500 rounded-r-full ease-linear bg-primary-600', filledClass));
37
37
  const overflowClasses = computed(() =>
38
38
  classes(
39
- 'absolute inset-y-0 right-0 size-full rounded-r-full border-l-3 border-gray-200',
39
+ 'absolute inset-y-0 right-0 size-full rounded-r-full',
40
40
  'bg-primary-900 transition-[width] duration-500 ease-linear',
41
41
  overflowClass,
42
42
  ));
@@ -3,6 +3,7 @@
3
3
  <SelectLabel />
4
4
  <slot>
5
5
  <SelectTrigger />
6
+ <HeadlessSelectError class="mt-2 text-sm text-red-600" />
6
7
  <SelectOptions />
7
8
  </slot>
8
9
  </HeadlessSelect>
@@ -19,6 +20,7 @@ import type { FormFieldValue } from '@aerogel/core/forms';
19
20
  import SelectLabel from './SelectLabel.vue';
20
21
  import SelectOptions from './SelectOptions.vue';
21
22
  import SelectTrigger from './SelectTrigger.vue';
23
+ import HeadlessSelectError from '../headless/HeadlessSelectError.vue';
22
24
 
23
25
  defineProps<SelectProps<T>>();
24
26
  defineEmits<SelectEmits<T>>();
@@ -2,11 +2,15 @@
2
2
  <HeadlessSelectTrigger :class="renderedClasses">
3
3
  <HeadlessSelectValue class="col-start-1 row-start-1 truncate pr-6" />
4
4
  <IconCheveronDown class="col-start-1 row-start-1 size-5 self-center justify-self-end text-gray-500 sm:size-4" />
5
+ <div v-if="select?.errors" class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
6
+ <IconExclamationSolid class="size-5 text-red-500" />
7
+ </div>
5
8
  </HeadlessSelectTrigger>
6
9
  </template>
7
10
 
8
11
  <script setup lang="ts">
9
12
  import IconCheveronDown from '~icons/zondicons/cheveron-down';
13
+ import IconExclamationSolid from '~icons/zondicons/exclamation-solid';
10
14
 
11
15
  import { computed } from 'vue';
12
16
  import type { HTMLAttributes } from 'vue';
@@ -22,8 +26,15 @@ const select = injectReactiveOrFail<SelectExpose>('select', '<SelectTrigger> mus
22
26
  const renderedClasses = computed(() =>
23
27
  classes(
24
28
  // eslint-disable-next-line vue/max-len
25
- 'focus:outline-primary-600 data-[state=open]:outline-primary-600 grid w-full cursor-default grid-cols-1 rounded-md bg-white py-1.5 pr-2 pl-3 text-left text-gray-900 outline-1 -outline-offset-1 outline-gray-300 focus:outline-2 focus:-outline-offset-2 sm:text-sm/6',
26
- { 'mt-1': select.label },
29
+ 'relative grid w-full cursor-default grid-cols-1 rounded-md bg-white py-1.5 pr-2 pl-3 text-left text-gray-900 outline-1 -outline-offset-1',
30
+ 'focus:outline-2 focus:-outline-offset-2 sm:text-sm/6',
31
+ {
32
+ 'mt-1': select.label,
33
+ 'outline-gray-300 focus:outline-primary-600 data-[state=open]:outline-primary-600': !select.errors,
34
+ 'text-gray-900 shadow-2xs ring-gray-900/10 placeholder:text-gray-400': !select.errors,
35
+ 'outline-red-900/10 pr-10 text-red-900 placeholder:text-red-300': select.errors,
36
+ 'focus:outline-red-500 data-[state=open]:outline-red-500': select.errors,
37
+ },
27
38
  rootClasses,
28
39
  ));
29
40
  </script>
@@ -2,6 +2,11 @@ export { default as AdvancedOptions } from './AdvancedOptions.vue';
2
2
  export { default as AlertModal } from './AlertModal.vue';
3
3
  export { default as Button } from './Button.vue';
4
4
  export { default as Checkbox } from './Checkbox.vue';
5
+ export { default as Combobox } from './Combobox.vue';
6
+ export { default as ComboboxLabel } from './ComboboxLabel.vue';
7
+ export { default as ComboboxOption } from './ComboboxOption.vue';
8
+ export { default as ComboboxOptions } from './ComboboxOptions.vue';
9
+ export { default as ComboboxTrigger } from './ComboboxTrigger.vue';
5
10
  export { default as ConfirmModal } from './ConfirmModal.vue';
6
11
  export { default as Details } from './Details.vue';
7
12
  export { default as DropdownMenu } from './DropdownMenu.vue';
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <slot />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { provide } from 'vue';
7
+
8
+ const { name, value } = defineProps<{ name: string; value: unknown }>();
9
+
10
+ provide(name, value);
11
+ </script>
@@ -0,0 +1 @@
1
+ export { default as Provide } from './Provide.vue';
@@ -3,6 +3,7 @@ import type { App as AppInstance } from 'vue';
3
3
  import App from '@aerogel/core/services/App';
4
4
  import { bootServices } from '@aerogel/core/services';
5
5
  import { definePlugin } from '@aerogel/core/plugins';
6
+ import { getErrorMessage } from '@aerogel/core/errors/utils';
6
7
 
7
8
  import Errors from './Errors';
8
9
  import settings from './settings';
@@ -16,6 +17,10 @@ export type { ErrorSource, ErrorReport, ErrorReportLog };
16
17
 
17
18
  const services = { $errors: Errors };
18
19
  const frameworkHandler: ErrorHandler = (error) => {
20
+ if (getErrorMessage(error).includes('ResizeObserver loop completed with undelivered notifications.')) {
21
+ return true;
22
+ }
23
+
19
24
  Errors.report(error);
20
25
 
21
26
  return true;
@@ -30,7 +30,7 @@ describe('FormController', () => {
30
30
  const form = useForm({
31
31
  name: {
32
32
  type: 'string',
33
- rules: 'required',
33
+ rules: ['required'],
34
34
  },
35
35
  });
36
36
 
@@ -48,7 +48,7 @@ describe('FormController', () => {
48
48
  const form = useForm({
49
49
  name: {
50
50
  type: 'string',
51
- rules: 'required',
51
+ rules: ['required'],
52
52
  },
53
53
  });
54
54
 
@@ -69,11 +69,11 @@ describe('FormController', () => {
69
69
  const form = useForm({
70
70
  trimmed: {
71
71
  type: 'string',
72
- rules: 'required',
72
+ rules: ['required'],
73
73
  },
74
74
  untrimmed: {
75
75
  type: 'string',
76
- rules: 'required',
76
+ rules: ['required'],
77
77
  trim: false,
78
78
  },
79
79
  });
@@ -1,5 +1,5 @@
1
1
  import { computed, nextTick, reactive, readonly, ref } from 'vue';
2
- import { MagicObject, arrayRemove, fail, toString } from '@noeldemartin/utils';
2
+ import { MagicObject, arrayRemove, fail } from '@noeldemartin/utils';
3
3
  import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
4
4
 
5
5
  import { validate, validateType } from './validation';
@@ -14,7 +14,7 @@ export interface FormFieldDefinition<
14
14
  type: TType;
15
15
  trim?: boolean;
16
16
  default?: GetFormFieldValue<TType>;
17
- rules?: TRules;
17
+ rules?: TRules[];
18
18
  values?: readonly TValueType[];
19
19
  [__valueType]?: TValueType;
20
20
  }
@@ -91,17 +91,13 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
91
91
  }
92
92
 
93
93
  public setFieldValue<T extends keyof Fields>(field: T, value: FormData<Fields>[T]): void {
94
- const definition =
95
- this._fields[field] ?? fail<FormFieldDefinition>(`Trying to set undefined '${toString(field)}' field`);
94
+ this._data[field] = value;
96
95
 
97
- this._data[field] =
98
- definition.type === 'string' && (definition.trim ?? true)
99
- ? (toString(value).trim() as FormData<Fields>[T])
100
- : value;
101
-
102
- if (this._submitted.value) {
103
- this.validate();
96
+ if (!this._submitted.value) {
97
+ return;
104
98
  }
99
+
100
+ this.validate();
105
101
  }
106
102
 
107
103
  public getFieldValue<T extends keyof Fields>(field: T): GetFormFieldValue<Fields[T]['type']> {
@@ -109,7 +105,7 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
109
105
  }
110
106
 
111
107
  public getFieldRules<T extends keyof Fields>(field: T): string[] {
112
- return this._fields[field]?.rules?.split('|') ?? [];
108
+ return this._fields[field]?.rules ?? [];
113
109
  }
114
110
 
115
111
  public setFieldErrors<T extends keyof Fields>(field: T, errors: string[] | null): void {
@@ -149,6 +145,16 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
149
145
  public submit(): boolean {
150
146
  this._submitted.value = true;
151
147
 
148
+ for (const [field, value] of Object.entries(this._data)) {
149
+ const definition = this._fields[field] ?? fail<FormFieldDefinition>();
150
+
151
+ if (typeof value !== 'string' || !(definition.trim ?? true)) {
152
+ continue;
153
+ }
154
+
155
+ this._data[field as keyof Fields] = value.trim() as FormData<Fields>[string];
156
+ }
157
+
152
158
  const valid = this.validate();
153
159
 
154
160
  valid && this._listeners['submit']?.forEach((listener) => listener());
@@ -201,7 +207,7 @@ export default class FormController<Fields extends FormFieldDefinitions = FormFi
201
207
  private getFieldErrors(name: keyof Fields, definition: FormFieldDefinition): string[] | null {
202
208
  const errors = [];
203
209
  const value = this._data[name];
204
- const rules = definition.rules?.split('|') ?? [];
210
+ const rules = definition.rules ?? [];
205
211
 
206
212
  errors.push(...validateType(value, definition));
207
213
 
@@ -1,4 +1,15 @@
1
+ import { registerFormValidationRule } from '@aerogel/core/forms/validation';
2
+ import { definePlugin } from '@aerogel/core/plugins';
3
+
1
4
  export * from './FormController';
2
5
  export * from './utils';
3
6
  export * from './validation';
4
7
  export { default as FormController } from './FormController';
8
+
9
+ export default definePlugin({
10
+ async install(_, { formValidationRules }) {
11
+ for (const [rule, validator] of Object.entries(formValidationRules ?? {})) {
12
+ registerFormValidationRule(rule, validator);
13
+ }
14
+ },
15
+ });
@@ -1,6 +1,9 @@
1
1
  import type { FormFieldDefinition } from './FormController';
2
2
 
3
- export function booleanInput(defaultValue?: boolean, options: { rules?: string } = {}): FormFieldDefinition<'boolean'> {
3
+ export function booleanInput(
4
+ defaultValue?: boolean,
5
+ options: { rules?: string[] } = {},
6
+ ): FormFieldDefinition<'boolean'> {
4
7
  return {
5
8
  default: defaultValue,
6
9
  type: 'boolean',
@@ -8,7 +11,7 @@ export function booleanInput(defaultValue?: boolean, options: { rules?: string }
8
11
  };
9
12
  }
10
13
 
11
- export function dateInput(defaultValue?: Date, options: { rules?: string } = {}): FormFieldDefinition<'date'> {
14
+ export function dateInput(defaultValue?: Date, options: { rules?: string[] } = {}): FormFieldDefinition<'date'> {
12
15
  return {
13
16
  default: defaultValue,
14
17
  type: 'date',
@@ -19,7 +22,7 @@ export function dateInput(defaultValue?: Date, options: { rules?: string } = {})
19
22
  export function enumInput<const T extends string>(
20
23
  values: readonly T[],
21
24
  defaultValue?: T,
22
- options: { rules?: string } = {},
25
+ options: { rules?: string[] } = {},
23
26
  ): FormFieldDefinition<'enum', string, T> {
24
27
  return {
25
28
  default: defaultValue,
@@ -29,59 +32,75 @@ export function enumInput<const T extends string>(
29
32
  };
30
33
  }
31
34
 
32
- export function requiredBooleanInput(defaultValue?: boolean): FormFieldDefinition<'boolean', 'required'> {
35
+ export function requiredBooleanInput(
36
+ defaultValue?: boolean,
37
+ options: { rules?: string[] } = {},
38
+ ): FormFieldDefinition<'boolean', 'required'> {
33
39
  return {
34
40
  default: defaultValue,
35
41
  type: 'boolean',
36
- rules: 'required',
42
+ rules: ['required', ...((options.rules as 'required'[]) ?? [])],
37
43
  };
38
44
  }
39
45
 
40
- export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<'date', 'required'> {
46
+ export function requiredDateInput(
47
+ defaultValue?: Date,
48
+ options: { rules?: string[] } = {},
49
+ ): FormFieldDefinition<'date', 'required'> {
41
50
  return {
42
51
  default: defaultValue,
43
52
  type: 'date',
44
- rules: 'required',
53
+ rules: ['required', ...((options.rules as 'required'[]) ?? [])],
45
54
  };
46
55
  }
47
56
 
48
57
  export function requiredEnumInput<const T extends string>(
49
58
  values: readonly T[],
50
59
  defaultValue?: T,
60
+ options: { rules?: string[] } = {},
51
61
  ): FormFieldDefinition<'enum', 'required', T> {
52
62
  return {
53
63
  default: defaultValue,
54
64
  type: 'enum',
55
- rules: 'required',
65
+ rules: ['required', ...((options.rules as 'required'[]) ?? [])],
56
66
  values,
57
67
  };
58
68
  }
59
69
 
60
- export function requiredNumberInput(defaultValue?: number): FormFieldDefinition<'number', 'required'> {
70
+ export function requiredNumberInput(
71
+ defaultValue?: number,
72
+ options: { rules?: string[] } = {},
73
+ ): FormFieldDefinition<'number', 'required'> {
61
74
  return {
62
75
  default: defaultValue,
63
76
  type: 'number',
64
- rules: 'required',
77
+ rules: ['required', ...((options.rules as 'required'[]) ?? [])],
65
78
  };
66
79
  }
67
80
 
68
- export function requiredObjectInput<T extends object>(defaultValue?: T): FormFieldDefinition<'object', 'required', T> {
81
+ export function requiredObjectInput<T extends object>(
82
+ defaultValue?: T,
83
+ options: { rules?: string[] } = {},
84
+ ): FormFieldDefinition<'object', 'required', T> {
69
85
  return {
70
86
  default: defaultValue,
71
87
  type: 'object',
72
- rules: 'required',
88
+ rules: ['required', ...((options.rules as 'required'[]) ?? [])],
73
89
  };
74
90
  }
75
91
 
76
- export function requiredStringInput(defaultValue?: string): FormFieldDefinition<'string', 'required'> {
92
+ export function requiredStringInput(
93
+ defaultValue?: string,
94
+ options: { rules?: string[] } = {},
95
+ ): FormFieldDefinition<'string', 'required'> {
77
96
  return {
78
97
  default: defaultValue,
79
98
  type: 'string',
80
- rules: 'required',
99
+ rules: ['required', ...((options.rules as 'required'[]) ?? [])],
81
100
  };
82
101
  }
83
102
 
84
- export function numberInput(defaultValue?: number, options: { rules?: string } = {}): FormFieldDefinition<'number'> {
103
+ export function numberInput(defaultValue?: number, options: { rules?: string[] } = {}): FormFieldDefinition<'number'> {
85
104
  return {
86
105
  default: defaultValue,
87
106
  type: 'number',
@@ -91,7 +110,7 @@ export function numberInput(defaultValue?: number, options: { rules?: string } =
91
110
 
92
111
  export function objectInput<T extends object>(
93
112
  defaultValue?: T,
94
- options: { rules?: string } = {},
113
+ options: { rules?: string[] } = {},
95
114
  ): FormFieldDefinition<'object', string, T> {
96
115
  return {
97
116
  default: defaultValue,
@@ -100,7 +119,7 @@ export function objectInput<T extends object>(
100
119
  };
101
120
  }
102
121
 
103
- export function stringInput(defaultValue?: string, options: { rules?: string } = {}): FormFieldDefinition<'string'> {
122
+ export function stringInput(defaultValue?: string, options: { rules?: string[] } = {}): FormFieldDefinition<'string'> {
104
123
  return {
105
124
  default: defaultValue,
106
125
  type: 'string',
@@ -31,10 +31,14 @@ export type FormFieldValidator<T = unknown> = (value: T) => string | string[] |
31
31
 
32
32
  export const validators: Record<string, FormFieldValidator> = { ...builtInRules };
33
33
 
34
- export function defineFormValidationRule<T>(rule: string, validator: FormFieldValidator<T>): void {
34
+ export function registerFormValidationRule<T>(rule: string, validator: FormFieldValidator<T>): void {
35
35
  validators[rule] = validator as FormFieldValidator;
36
36
  }
37
37
 
38
+ export function defineFormValidationRules<T extends Record<string, FormFieldValidator>>(rules: T): T {
39
+ return rules;
40
+ }
41
+
38
42
  export function validateType(value: unknown, definition: FormFieldDefinition): string[] {
39
43
  if (isValidType(value, definition)) {
40
44
  return [];
package/src/index.css CHANGED
@@ -20,6 +20,7 @@
20
20
  --color-primary-950: color-mix(in oklab, var(--color-primary-600) 50%, black);
21
21
 
22
22
  --color-background: oklch(1 0 0);
23
+ --color-primary-text: var(--color-gray-900);
23
24
  --color-links: var(--color-primary);
24
25
 
25
26
  --breakpoint-content: var(--breakpoint-md);
package/src/jobs/Job.ts CHANGED
@@ -115,7 +115,7 @@ export default abstract class Job<
115
115
  return 1;
116
116
  }
117
117
 
118
- if (!status.children) {
118
+ if (!status.children || status.children.length === 0) {
119
119
  return 0;
120
120
  }
121
121
 
@@ -1,5 +1,3 @@
1
- import type { GetClosureArgs } from '@noeldemartin/utils';
2
-
3
1
  import App from '@aerogel/core/services/App';
4
2
 
5
3
  import type { Plugin } from './Plugin';
@@ -10,7 +8,7 @@ export function definePlugin<T extends Plugin>(plugin: T): T {
10
8
  return plugin;
11
9
  }
12
10
 
13
- export async function installPlugins(plugins: Plugin[], ...args: GetClosureArgs<Plugin['install']>): Promise<void> {
11
+ export async function installPlugins(plugins: Plugin[], ...args: Parameters<Plugin['install']>): Promise<void> {
14
12
  App.setState(
15
13
  'plugins',
16
14
  plugins.reduce(
@@ -10,6 +10,7 @@ import Service from './Service';
10
10
  import Storage from './Storage';
11
11
  import { getPiniaStore } from './store';
12
12
  import type { AppSetting } from './App.state';
13
+ import type { FormFieldValidator } from '@aerogel/core/forms';
13
14
 
14
15
  export * from './App';
15
16
  export * from './Cache';
@@ -68,6 +69,7 @@ declare module '@aerogel/core/bootstrap/options' {
68
69
  export interface AerogelOptions {
69
70
  services?: Record<string, Service>;
70
71
  settings?: AppSetting[];
72
+ formValidationRules?: Record<string, FormFieldValidator>;
71
73
  settingsFullscreenOnMobile?: boolean;
72
74
  }
73
75
  }
@@ -1,5 +1,4 @@
1
1
  import { isTesting } from '@noeldemartin/utils';
2
- import type { GetClosureArgs } from '@noeldemartin/utils';
3
2
 
4
3
  import Events from '@aerogel/core/services/Events';
5
4
  import { App } from '@aerogel/core/services';
@@ -18,7 +17,7 @@ export default definePlugin({
18
17
  }
19
18
 
20
19
  globalThis.testingRuntime = {
21
- on: ((...args: GetClosureArgs<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
20
+ on: ((...args: Parameters<(typeof Events)['on']>) => Events.on(...args)) as (typeof Events)['on'],
22
21
  service: (name) => App.service(name),
23
22
  };
24
23
  },
package/src/ui/UI.ts CHANGED
@@ -283,7 +283,8 @@ export class UIService extends Service {
283
283
  ): Promise<GetModalResponse<T>>;
284
284
 
285
285
  public modal<T extends Component>(component: T, componentProps?: GetModalProps<T>): Promise<GetModalResponse<T>> {
286
- return showModal(component, componentProps ?? {});
286
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
287
+ return showModal(component as any, componentProps ?? {}) as Promise<GetModalResponse<T>>;
287
288
  }
288
289
 
289
290
  public async closeAllModals(): Promise<void> {
@@ -4,10 +4,9 @@ import { cva } from 'class-variance-authority';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import type { ClassValue } from 'clsx';
6
6
  import type { PropType } from 'vue';
7
- import type { GetClosureArgs, GetClosureResult } from '@noeldemartin/utils';
8
7
 
9
- export type CVAConfig<T> = NonNullable<GetClosureArgs<typeof cva<T>>[1]>;
10
- export type CVAProps<T> = NonNullable<GetClosureArgs<GetClosureResult<typeof cva<T>>>[0]>;
8
+ export type CVAConfig<T> = NonNullable<Parameters<typeof cva<T>>[1]>;
9
+ export type CVAProps<T> = NonNullable<Parameters<ReturnType<typeof cva<T>>>[0]>;
11
10
  export type Variants<T extends Record<string, string | boolean>> = Required<{
12
11
  [K in keyof T]: Exclude<T[K], undefined> extends string
13
12
  ? { [key in Exclude<T[K], undefined>]: string | null }
@@ -2,10 +2,14 @@ import { fail } from '@noeldemartin/utils';
2
2
  import { customRef } from 'vue';
3
3
 
4
4
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
5
- export function reactiveSet<T>(initial?: T[] | Set<T>) {
5
+ export function reactiveSet<T>(initial?: T[] | Set<T>, options: { equals?: (a: T, b: T) => boolean } = {}) {
6
6
  let set: Set<T> = new Set(initial);
7
7
  let trigger: () => void;
8
8
  let track: () => void;
9
+ const equals = options?.equals;
10
+ const hasEqual = equals
11
+ ? (item: T) => ref.value.values().some((existingItem) => equals(item, existingItem))
12
+ : () => false;
9
13
  const ref = customRef((_track, _trigger) => {
10
14
  track = _track;
11
15
  trigger = _trigger;
@@ -25,11 +29,15 @@ export function reactiveSet<T>(initial?: T[] | Set<T>) {
25
29
  has(item: T) {
26
30
  track();
27
31
 
28
- return ref.value.has(item);
32
+ return ref.value.has(item) || hasEqual(item);
29
33
  },
30
34
  add(item: T) {
31
35
  trigger();
32
36
 
37
+ if (hasEqual(item)) {
38
+ return;
39
+ }
40
+
33
41
  ref.value.add(item);
34
42
  },
35
43
  delete(item: T) {
package/src/utils/time.ts CHANGED
@@ -1,2 +1,7 @@
1
+ import type { Nullable } from '@noeldemartin/utils';
2
+
1
3
  export const MINUTE_MILLISECONDS = 60000;
2
- export const LOCAL_TIMEZONE_OFFSET = -new Date().getTimezoneOffset() * -MINUTE_MILLISECONDS;
4
+
5
+ export function getLocalTimezoneOffset(date?: Nullable<Date>): number {
6
+ return -(date ?? new Date()).getTimezoneOffset() * -MINUTE_MILLISECONDS;
7
+ }