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

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 (206) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +2038 -1801
  3. package/dist/aerogel-core.js +3432 -0
  4. package/dist/aerogel-core.js.map +1 -0
  5. package/package.json +31 -37
  6. package/src/bootstrap/bootstrap.test.ts +4 -7
  7. package/src/bootstrap/index.ts +25 -16
  8. package/src/bootstrap/options.ts +1 -1
  9. package/src/components/AppLayout.vue +14 -0
  10. package/src/components/{AGAppModals.vue → AppModals.vue} +3 -4
  11. package/src/components/AppOverlays.vue +9 -0
  12. package/src/components/AppToasts.vue +16 -0
  13. package/src/components/contracts/AlertModal.ts +19 -0
  14. package/src/components/contracts/Button.ts +16 -0
  15. package/src/components/contracts/ConfirmModal.ts +48 -0
  16. package/src/components/contracts/DropdownMenu.ts +25 -0
  17. package/src/components/contracts/ErrorReportModal.ts +33 -0
  18. package/src/components/contracts/Input.ts +26 -0
  19. package/src/components/contracts/LoadingModal.ts +26 -0
  20. package/src/components/contracts/Modal.ts +21 -0
  21. package/src/components/contracts/PromptModal.ts +34 -0
  22. package/src/components/contracts/Select.ts +45 -0
  23. package/src/components/contracts/Toast.ts +15 -0
  24. package/src/components/contracts/index.ts +11 -0
  25. package/src/components/headless/HeadlessButton.vue +51 -0
  26. package/src/components/headless/HeadlessInput.vue +59 -0
  27. package/src/components/headless/{forms/AGHeadlessInputDescription.vue → HeadlessInputDescription.vue} +7 -8
  28. package/src/components/headless/{forms/AGHeadlessInputError.vue → HeadlessInputError.vue} +4 -8
  29. package/src/components/headless/{forms/AGHeadlessInputInput.vue → HeadlessInputInput.vue} +16 -25
  30. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  31. package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +10 -13
  32. package/src/components/headless/HeadlessModal.vue +57 -0
  33. package/src/components/headless/HeadlessModalContent.vue +30 -0
  34. package/src/components/headless/HeadlessModalDescription.vue +12 -0
  35. package/src/components/headless/HeadlessModalOverlay.vue +12 -0
  36. package/src/components/headless/HeadlessModalTitle.vue +12 -0
  37. package/src/components/headless/HeadlessSelect.vue +120 -0
  38. package/src/components/headless/{forms/AGHeadlessSelectError.vue → HeadlessSelectError.vue} +5 -6
  39. package/src/components/headless/HeadlessSelectLabel.vue +25 -0
  40. package/src/components/headless/HeadlessSelectOption.vue +34 -0
  41. package/src/components/headless/HeadlessSelectOptions.vue +42 -0
  42. package/src/components/headless/HeadlessSelectTrigger.vue +22 -0
  43. package/src/components/headless/HeadlessSelectValue.vue +18 -0
  44. package/src/components/headless/HeadlessToast.vue +18 -0
  45. package/src/components/headless/HeadlessToastAction.vue +13 -0
  46. package/src/components/headless/index.ts +19 -3
  47. package/src/components/index.ts +6 -11
  48. package/src/components/ui/AdvancedOptions.vue +18 -0
  49. package/src/components/ui/AlertModal.vue +17 -0
  50. package/src/components/ui/Button.vue +100 -0
  51. package/src/components/ui/Checkbox.vue +56 -0
  52. package/src/components/ui/ConfirmModal.vue +50 -0
  53. package/src/components/ui/DropdownMenu.vue +32 -0
  54. package/src/components/ui/DropdownMenuOption.vue +22 -0
  55. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  56. package/src/components/ui/EditableContent.vue +82 -0
  57. package/src/components/ui/ErrorLogs.vue +19 -0
  58. package/src/components/ui/ErrorLogsModal.vue +48 -0
  59. package/src/components/ui/ErrorMessage.vue +15 -0
  60. package/src/components/ui/ErrorReportModal.vue +73 -0
  61. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  62. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  63. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  64. package/src/components/ui/Input.vue +56 -0
  65. package/src/components/ui/Link.vue +12 -0
  66. package/src/components/ui/LoadingModal.vue +34 -0
  67. package/src/components/ui/Markdown.vue +97 -0
  68. package/src/components/ui/Modal.vue +123 -0
  69. package/src/components/ui/ModalContext.vue +31 -0
  70. package/src/components/ui/ProgressBar.vue +51 -0
  71. package/src/components/ui/PromptModal.vue +38 -0
  72. package/src/components/ui/Select.vue +27 -0
  73. package/src/components/ui/SelectLabel.vue +17 -0
  74. package/src/components/ui/SelectOption.vue +29 -0
  75. package/src/components/ui/SelectOptions.vue +35 -0
  76. package/src/components/ui/SelectTrigger.vue +29 -0
  77. package/src/components/ui/SettingsModal.vue +15 -0
  78. package/src/components/{lib/AGStartupCrash.vue → ui/StartupCrash.vue} +8 -8
  79. package/src/components/ui/Toast.vue +46 -0
  80. package/src/components/ui/index.ts +32 -0
  81. package/src/directives/index.ts +9 -5
  82. package/src/directives/measure.ts +12 -6
  83. package/src/errors/Errors.state.ts +1 -1
  84. package/src/errors/Errors.ts +29 -27
  85. package/src/errors/JobCancelledError.ts +3 -0
  86. package/src/errors/index.ts +9 -6
  87. package/src/errors/utils.ts +17 -1
  88. package/src/forms/{Form.test.ts → FormController.test.ts} +32 -8
  89. package/src/forms/{Form.ts → FormController.ts} +51 -40
  90. package/src/forms/index.ts +3 -3
  91. package/src/forms/utils.ts +40 -24
  92. package/src/forms/validation.ts +19 -0
  93. package/src/index.css +73 -0
  94. package/src/jobs/Job.ts +144 -2
  95. package/src/jobs/index.ts +4 -1
  96. package/src/jobs/listeners.ts +3 -0
  97. package/src/jobs/status.ts +4 -0
  98. package/src/lang/DefaultLangProvider.ts +7 -4
  99. package/src/lang/Lang.state.ts +1 -1
  100. package/src/lang/Lang.ts +5 -1
  101. package/src/lang/index.ts +12 -6
  102. package/src/lang/settings/Language.vue +48 -0
  103. package/src/lang/settings/index.ts +10 -0
  104. package/src/plugins/Plugin.ts +1 -1
  105. package/src/plugins/index.ts +10 -7
  106. package/src/services/App.state.ts +23 -4
  107. package/src/services/App.ts +16 -3
  108. package/src/services/Cache.ts +1 -1
  109. package/src/services/Events.test.ts +8 -8
  110. package/src/services/Events.ts +16 -12
  111. package/src/services/Service.ts +116 -53
  112. package/src/services/Storage.ts +20 -0
  113. package/src/services/index.ts +14 -5
  114. package/src/services/utils.ts +18 -0
  115. package/src/testing/index.ts +4 -3
  116. package/src/testing/setup.ts +5 -19
  117. package/src/ui/UI.state.ts +8 -13
  118. package/src/ui/UI.ts +222 -128
  119. package/src/ui/index.ts +27 -28
  120. package/src/ui/utils.ts +1 -1
  121. package/src/utils/classes.ts +41 -0
  122. package/src/utils/composition/events.ts +4 -6
  123. package/src/utils/composition/forms.ts +20 -4
  124. package/src/utils/composition/persistent.test.ts +33 -0
  125. package/src/utils/composition/persistent.ts +11 -0
  126. package/src/utils/composition/state.test.ts +47 -0
  127. package/src/utils/composition/state.ts +33 -0
  128. package/src/utils/index.ts +5 -1
  129. package/src/utils/markdown.test.ts +50 -0
  130. package/src/utils/markdown.ts +53 -6
  131. package/src/utils/types.ts +3 -0
  132. package/src/utils/vue.ts +38 -134
  133. package/dist/aerogel-core.cjs.js +0 -2
  134. package/dist/aerogel-core.cjs.js.map +0 -1
  135. package/dist/aerogel-core.esm.js +0 -2
  136. package/dist/aerogel-core.esm.js.map +0 -1
  137. package/histoire.config.ts +0 -7
  138. package/noeldemartin.config.js +0 -5
  139. package/postcss.config.js +0 -6
  140. package/src/assets/histoire.css +0 -3
  141. package/src/components/AGAppLayout.vue +0 -16
  142. package/src/components/AGAppOverlays.vue +0 -41
  143. package/src/components/AGAppSnackbars.vue +0 -13
  144. package/src/components/composition.ts +0 -23
  145. package/src/components/constants.ts +0 -8
  146. package/src/components/forms/AGButton.vue +0 -44
  147. package/src/components/forms/AGCheckbox.vue +0 -41
  148. package/src/components/forms/AGInput.vue +0 -40
  149. package/src/components/forms/AGSelect.story.vue +0 -46
  150. package/src/components/forms/AGSelect.vue +0 -60
  151. package/src/components/forms/index.ts +0 -5
  152. package/src/components/headless/forms/AGHeadlessButton.ts +0 -3
  153. package/src/components/headless/forms/AGHeadlessButton.vue +0 -62
  154. package/src/components/headless/forms/AGHeadlessInput.ts +0 -34
  155. package/src/components/headless/forms/AGHeadlessInput.vue +0 -70
  156. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  157. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  158. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  159. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  160. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  161. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  162. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  163. package/src/components/headless/forms/composition.ts +0 -10
  164. package/src/components/headless/forms/index.ts +0 -18
  165. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  166. package/src/components/headless/modals/AGHeadlessModal.vue +0 -86
  167. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  168. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  169. package/src/components/headless/modals/index.ts +0 -4
  170. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  171. package/src/components/headless/snackbars/index.ts +0 -40
  172. package/src/components/interfaces.ts +0 -24
  173. package/src/components/lib/AGErrorMessage.vue +0 -16
  174. package/src/components/lib/AGLink.vue +0 -9
  175. package/src/components/lib/AGMarkdown.vue +0 -41
  176. package/src/components/lib/AGMeasured.vue +0 -16
  177. package/src/components/lib/index.ts +0 -5
  178. package/src/components/modals/AGAlertModal.ts +0 -15
  179. package/src/components/modals/AGAlertModal.vue +0 -14
  180. package/src/components/modals/AGConfirmModal.ts +0 -33
  181. package/src/components/modals/AGConfirmModal.vue +0 -26
  182. package/src/components/modals/AGErrorReportModal.ts +0 -46
  183. package/src/components/modals/AGErrorReportModal.vue +0 -54
  184. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  185. package/src/components/modals/AGLoadingModal.ts +0 -23
  186. package/src/components/modals/AGLoadingModal.vue +0 -15
  187. package/src/components/modals/AGModal.ts +0 -10
  188. package/src/components/modals/AGModal.vue +0 -39
  189. package/src/components/modals/AGModalContext.ts +0 -8
  190. package/src/components/modals/AGModalContext.vue +0 -22
  191. package/src/components/modals/AGModalTitle.vue +0 -9
  192. package/src/components/modals/AGPromptModal.ts +0 -36
  193. package/src/components/modals/AGPromptModal.vue +0 -34
  194. package/src/components/modals/index.ts +0 -17
  195. package/src/components/snackbars/AGSnackbar.vue +0 -36
  196. package/src/components/snackbars/index.ts +0 -3
  197. package/src/components/utils.ts +0 -10
  198. package/src/directives/initial-focus.ts +0 -11
  199. package/src/forms/composition.ts +0 -6
  200. package/src/main.histoire.ts +0 -1
  201. package/src/utils/tailwindcss.test.ts +0 -26
  202. package/src/utils/tailwindcss.ts +0 -7
  203. package/tailwind.config.js +0 -4
  204. package/tsconfig.json +0 -11
  205. package/vite.config.ts +0 -17
  206. /package/src/{main.ts → index.ts} +0 -0
@@ -1,32 +1,32 @@
1
- import { MagicObject, arrayRemove, fail, toString } from '@noeldemartin/utils';
2
1
  import { computed, nextTick, reactive, readonly, ref } from 'vue';
3
- import type { ObjectValues } from '@noeldemartin/utils';
2
+ import { MagicObject, arrayRemove, fail, toString } from '@noeldemartin/utils';
4
3
  import type { ComputedRef, DeepReadonly, Ref, UnwrapNestedRefs } from 'vue';
5
4
 
6
- export const FormFieldTypes = {
7
- String: 'string',
8
- Number: 'number',
9
- Boolean: 'boolean',
10
- Object: 'object',
11
- Date: 'date',
12
- } as const;
5
+ import { validate } from './validation';
13
6
 
14
- export interface FormFieldDefinition<TType extends FormFieldType = FormFieldType, TRules extends string = string> {
7
+ export const __objectType: unique symbol = Symbol();
8
+
9
+ export interface FormFieldDefinition<
10
+ TType extends FormFieldType = FormFieldType,
11
+ TRules extends string = string,
12
+ TObjectType = object,
13
+ > {
15
14
  type: TType;
16
15
  trim?: boolean;
17
16
  default?: GetFormFieldValue<TType>;
18
17
  rules?: TRules;
18
+ [__objectType]?: TObjectType;
19
19
  }
20
20
 
21
- export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
22
- export type FormFieldType = ObjectValues<typeof FormFieldTypes>;
21
+ export type FormFieldType = 'string' | 'number' | 'boolean' | 'object' | 'date';
23
22
  export type FormFieldValue = GetFormFieldValue<FormFieldType>;
23
+ export type FormFieldDefinitions = Record<string, FormFieldDefinition>;
24
24
 
25
25
  export type FormData<T> = {
26
- -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules>
26
+ -readonly [k in keyof T]: T[k] extends FormFieldDefinition<infer TType, infer TRules, infer TObjectType>
27
27
  ? TRules extends 'required'
28
- ? GetFormFieldValue<TType>
29
- : GetFormFieldValue<TType> | null
28
+ ? GetFormFieldValue<TType, TObjectType>
29
+ : GetFormFieldValue<TType, TObjectType> | null
30
30
  : never;
31
31
  };
32
32
 
@@ -34,24 +34,26 @@ export type FormErrors<T> = {
34
34
  [k in keyof T]: string[] | null;
35
35
  };
36
36
 
37
- export type GetFormFieldValue<TType> = TType extends typeof FormFieldTypes.String
37
+ export type GetFormFieldValue<TType, TObjectType = object> = TType extends 'string'
38
38
  ? string
39
- : TType extends typeof FormFieldTypes.Number
40
- ? number
41
- : TType extends typeof FormFieldTypes.Boolean
42
- ? boolean
43
- : TType extends typeof FormFieldTypes.Object
44
- ? object
45
- : TType extends typeof FormFieldTypes.Date
46
- ? Date
47
- : never;
48
-
49
- const validForms: WeakMap<Form, ComputedRef<boolean>> = new WeakMap();
39
+ : TType extends 'number'
40
+ ? number
41
+ : TType extends 'boolean'
42
+ ? boolean
43
+ : TType extends 'object'
44
+ ? TObjectType extends object
45
+ ? TObjectType
46
+ : object
47
+ : TType extends 'date'
48
+ ? Date
49
+ : never;
50
+
51
+ const validForms: WeakMap<FormController, ComputedRef<boolean>> = new WeakMap();
50
52
 
51
53
  export type SubmitFormListener = () => unknown;
52
54
  export type FocusFormListener = (input: string) => unknown;
53
55
 
54
- export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
56
+ export default class FormController<Fields extends FormFieldDefinitions = FormFieldDefinitions> extends MagicObject {
55
57
 
56
58
  public errors: DeepReadonly<UnwrapNestedRefs<FormErrors<Fields>>>;
57
59
 
@@ -90,7 +92,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
90
92
  this._fields[field] ?? fail<FormFieldDefinition>(`Trying to set undefined '${toString(field)}' field`);
91
93
 
92
94
  this._data[field] =
93
- definition.type === FormFieldTypes.String && (definition.trim ?? true)
95
+ definition.type === 'string' && (definition.trim ?? true)
94
96
  ? (toString(value).trim() as FormData<Fields>[T])
95
97
  : value;
96
98
 
@@ -112,11 +114,14 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
112
114
  }
113
115
 
114
116
  public validate(): boolean {
115
- const errors = Object.entries(this._fields).reduce((formErrors, [name, definition]) => {
116
- formErrors[name] = this.getFieldErrors(name, definition);
117
+ const errors = Object.entries(this._fields).reduce(
118
+ (formErrors, [name, definition]) => {
119
+ formErrors[name] = this.getFieldErrors(name, definition);
117
120
 
118
- return formErrors;
119
- }, {} as Record<string, string[] | null>);
121
+ return formErrors;
122
+ },
123
+ {} as Record<string, string[] | null>,
124
+ );
120
125
 
121
126
  this.resetErrors(errors);
122
127
 
@@ -164,7 +169,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
164
169
  this._listeners['focus']?.forEach((listener) => listener(input));
165
170
  }
166
171
 
167
- protected __get(property: string): unknown {
172
+ protected override __get(property: string): unknown {
168
173
  if (!(property in this._fields)) {
169
174
  return super.__get(property);
170
175
  }
@@ -172,7 +177,7 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
172
177
  return this.getFieldValue(property);
173
178
  }
174
179
 
175
- protected __set(property: string, value: unknown): void {
180
+ protected override __set(property: string, value: unknown): void {
176
181
  if (!(property in this._fields)) {
177
182
  super.__set(property, value);
178
183
 
@@ -184,9 +189,15 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
184
189
 
185
190
  private getFieldErrors(name: keyof Fields, definition: FormFieldDefinition): string[] | null {
186
191
  const errors = [];
192
+ const value = this._data[name];
193
+ const rules = definition.rules?.split('|') ?? [];
194
+
195
+ for (const rule of rules) {
196
+ if (rule !== 'required' && (value === null || value === undefined)) {
197
+ continue;
198
+ }
187
199
 
188
- if (definition.rules?.includes('required') && !this._data[name]) {
189
- errors.push('required');
200
+ errors.push(...validate(value, rule));
190
201
  }
191
202
 
192
203
  return errors.length > 0 ? errors : null;
@@ -197,10 +208,10 @@ export default class Form<Fields extends FormFieldDefinitions = FormFieldDefinit
197
208
  return {} as FormData<Fields>;
198
209
  }
199
210
 
200
- const data = Object.entries(fields).reduce((formData, [name, definition]) => {
201
- formData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
211
+ const data = Object.entries(fields).reduce((initialData, [name, definition]) => {
212
+ initialData[name as keyof Fields] = (definition.default ?? null) as FormData<Fields>[keyof Fields];
202
213
 
203
- return formData;
214
+ return initialData;
204
215
  }, {} as FormData<Fields>);
205
216
 
206
217
  return reactive(data) as FormData<Fields>;
@@ -1,4 +1,4 @@
1
- export * from './Form';
2
- export * from './composition';
1
+ export * from './FormController';
3
2
  export * from './utils';
4
- export { default as Form } from './Form';
3
+ export * from './validation';
4
+ export { default as FormController } from './FormController';
@@ -1,68 +1,84 @@
1
- import { FormFieldTypes } from './Form';
2
- import type { FormFieldDefinition } from './Form';
1
+ import type { FormFieldDefinition } from './FormController';
3
2
 
4
- export function booleanInput(defaultValue?: boolean): FormFieldDefinition<typeof FormFieldTypes.Boolean> {
3
+ export function booleanInput(defaultValue?: boolean, options: { rules?: string } = {}): FormFieldDefinition<'boolean'> {
5
4
  return {
6
5
  default: defaultValue,
7
- type: FormFieldTypes.Boolean,
6
+ type: 'boolean',
7
+ rules: options.rules,
8
8
  };
9
9
  }
10
10
 
11
- export function dateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
11
+ export function dateInput(defaultValue?: Date, options: { rules?: string } = {}): FormFieldDefinition<'date'> {
12
12
  return {
13
13
  default: defaultValue,
14
- type: FormFieldTypes.Date,
14
+ type: 'date',
15
+ rules: options.rules,
15
16
  };
16
17
  }
17
18
 
18
- export function requiredBooleanInput(
19
- defaultValue?: boolean,
20
- ): FormFieldDefinition<typeof FormFieldTypes.Boolean, 'required'> {
19
+ export function requiredBooleanInput(defaultValue?: boolean): FormFieldDefinition<'boolean', 'required'> {
21
20
  return {
22
21
  default: defaultValue,
23
- type: FormFieldTypes.Boolean,
22
+ type: 'boolean',
24
23
  rules: 'required',
25
24
  };
26
25
  }
27
26
 
28
- export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<typeof FormFieldTypes.Date> {
27
+ export function requiredDateInput(defaultValue?: Date): FormFieldDefinition<'date', 'required'> {
29
28
  return {
30
29
  default: defaultValue,
31
- type: FormFieldTypes.Date,
30
+ type: 'date',
32
31
  rules: 'required',
33
32
  };
34
33
  }
35
34
 
36
- export function requiredNumberInput(
37
- defaultValue?: number,
38
- ): FormFieldDefinition<typeof FormFieldTypes.Number, 'required'> {
35
+ export function requiredNumberInput(defaultValue?: number): FormFieldDefinition<'number', 'required'> {
39
36
  return {
40
37
  default: defaultValue,
41
- type: FormFieldTypes.Number,
38
+ type: 'number',
42
39
  rules: 'required',
43
40
  };
44
41
  }
45
42
 
46
- export function requiredStringInput(
47
- defaultValue?: string,
48
- ): FormFieldDefinition<typeof FormFieldTypes.String, 'required'> {
43
+ export function requiredObjectInput<T extends object>(defaultValue?: T): FormFieldDefinition<'object', 'required', T> {
49
44
  return {
50
45
  default: defaultValue,
51
- type: FormFieldTypes.String,
46
+ type: 'object',
52
47
  rules: 'required',
53
48
  };
54
49
  }
55
50
 
56
- export function numberInput(defaultValue?: number): FormFieldDefinition<typeof FormFieldTypes.Number> {
51
+ export function requiredStringInput(defaultValue?: string): FormFieldDefinition<'string', 'required'> {
57
52
  return {
58
53
  default: defaultValue,
59
- type: FormFieldTypes.Number,
54
+ type: 'string',
55
+ rules: 'required',
56
+ };
57
+ }
58
+
59
+ export function numberInput(defaultValue?: number, options: { rules?: string } = {}): FormFieldDefinition<'number'> {
60
+ return {
61
+ default: defaultValue,
62
+ type: 'number',
63
+ rules: options.rules,
64
+ };
65
+ }
66
+
67
+ export function objectInput<T extends object>(
68
+ defaultValue?: T,
69
+ options: { rules?: string } = {},
70
+ ): FormFieldDefinition<'object', string, T> {
71
+ return {
72
+ default: defaultValue,
73
+ type: 'object',
74
+ rules: options.rules,
60
75
  };
61
76
  }
62
77
 
63
- export function stringInput(defaultValue?: string): FormFieldDefinition<typeof FormFieldTypes.String> {
78
+ export function stringInput(defaultValue?: string, options: { rules?: string } = {}): FormFieldDefinition<'string'> {
64
79
  return {
65
80
  default: defaultValue,
66
- type: FormFieldTypes.String,
81
+ type: 'string',
82
+ rules: options.rules,
67
83
  };
68
84
  }
@@ -0,0 +1,19 @@
1
+ import { arrayFrom } from '@noeldemartin/utils';
2
+
3
+ const builtInRules: Record<string, FormFieldValidator> = {
4
+ required: (value) => (value ? undefined : 'required'),
5
+ };
6
+
7
+ export type FormFieldValidator<T = unknown> = (value: T) => string | string[] | undefined;
8
+
9
+ export const validators: Record<string, FormFieldValidator> = { ...builtInRules };
10
+
11
+ export function defineFormValidationRule<T>(rule: string, validator: FormFieldValidator<T>): void {
12
+ validators[rule] = validator as FormFieldValidator;
13
+ }
14
+
15
+ export function validate(value: unknown, rule: string): string[] {
16
+ const errors = validators[rule]?.(value);
17
+
18
+ return errors ? arrayFrom(errors) : [];
19
+ }
package/src/index.css ADDED
@@ -0,0 +1,73 @@
1
+ @import 'tailwindcss';
2
+
3
+ @plugin '@tailwindcss/forms';
4
+ @plugin '@tailwindcss/typography';
5
+
6
+ @source './';
7
+
8
+ @theme {
9
+ --color-primary: oklch(0.205 0 0);
10
+ --color-primary-50: color-mix(in oklab, var(--color-primary-600) 5%, transparent);
11
+ --color-primary-100: color-mix(in oklab, var(--color-primary-600) 15%, transparent);
12
+ --color-primary-200: color-mix(in oklab, var(--color-primary-600) 30%, transparent);
13
+ --color-primary-300: color-mix(in oklab, var(--color-primary-600) 50%, transparent);
14
+ --color-primary-400: color-mix(in oklab, var(--color-primary-600) 65%, transparent);
15
+ --color-primary-500: color-mix(in oklab, var(--color-primary-600) 80%, transparent);
16
+ --color-primary-600: var(--color-primary);
17
+ --color-primary-700: color-mix(in oklab, var(--color-primary-600) 90%, black);
18
+ --color-primary-800: color-mix(in oklab, var(--color-primary-600) 80%, black);
19
+ --color-primary-900: color-mix(in oklab, var(--color-primary-600) 70%, black);
20
+ --color-primary-950: color-mix(in oklab, var(--color-primary-600) 50%, black);
21
+
22
+ --color-background: oklch(1 0 0);
23
+ --color-links: var(--color-primary);
24
+ }
25
+
26
+ .clickable {
27
+ position: relative;
28
+ }
29
+
30
+ .clickable::after {
31
+ --clickable-size: 44px;
32
+ --clickable-inset-by: min(0px, calc((100% - var(--clickable-size)) / 2));
33
+
34
+ content: '';
35
+ position: absolute;
36
+ top: var(--clickable-inset-by);
37
+ left: var(--clickable-inset-by);
38
+ right: var(--clickable-inset-by);
39
+ bottom: var(--clickable-inset-by);
40
+ }
41
+
42
+ input[type='number'].appearance-textfield {
43
+ appearance: textfield;
44
+ }
45
+
46
+ input[type='number'].appearance-textfield::-webkit-outer-spin-button,
47
+ input[type='number'].appearance-textfield::-webkit-inner-spin-button {
48
+ appearance: none;
49
+ }
50
+
51
+ button[data-markdown-action] {
52
+ color: var(--tw-prose-links);
53
+ text-decoration: underline;
54
+ font-weight: 500;
55
+ }
56
+
57
+ @keyframes fade-in {
58
+ 0% {
59
+ opacity: 0;
60
+ }
61
+ 100% {
62
+ opacity: 1;
63
+ }
64
+ }
65
+
66
+ @keyframes grow {
67
+ 0% {
68
+ scale: 0;
69
+ }
70
+ 100% {
71
+ opacity: 1;
72
+ }
73
+ }
package/src/jobs/Job.ts CHANGED
@@ -1,5 +1,147 @@
1
- export default abstract class Job {
1
+ import { ListenersManager, PromisedValue, round, tap, toError } from '@noeldemartin/utils';
2
+ import type { Listeners } from '@noeldemartin/utils';
2
3
 
3
- public abstract run(): Promise<void>;
4
+ import JobCancelledError from '@aerogel/core/errors/JobCancelledError';
5
+
6
+ import type { JobListener } from './listeners';
7
+ import type { JobStatus } from './status';
8
+
9
+ export default abstract class Job<
10
+ Listener extends JobListener = JobListener,
11
+ Status extends JobStatus = JobStatus,
12
+ SerializedStatus extends JobStatus = JobStatus,
13
+ > {
14
+
15
+ protected status: Status;
16
+ protected _listeners: ListenersManager<JobListener>;
17
+ protected _progress?: number;
18
+ protected _cancelled?: PromisedValue<void>;
19
+ protected _started: PromisedValue<void>;
20
+ protected _completed: PromisedValue<void>;
21
+
22
+ constructor() {
23
+ this.status = this.getInitialStatus();
24
+ this._listeners = new ListenersManager();
25
+ this._started = new PromisedValue();
26
+ this._completed = new PromisedValue();
27
+ }
28
+
29
+ public async start(): Promise<void> {
30
+ this.beforeStart();
31
+ this._started.resolve();
32
+
33
+ try {
34
+ await this.updateProgress();
35
+ await this.run();
36
+ await this.updateProgress();
37
+
38
+ this._completed.resolve();
39
+ } catch (error) {
40
+ if (error instanceof JobCancelledError) {
41
+ return;
42
+ }
43
+
44
+ throw tap(toError(error), (realError) => {
45
+ this._completed.reject(realError);
46
+ });
47
+ }
48
+ }
49
+
50
+ public async cancel(): Promise<void> {
51
+ this._cancelled = new PromisedValue();
52
+
53
+ await this._cancelled;
54
+ }
55
+
56
+ public serialize(): SerializedStatus {
57
+ return this.serializeStatus(this.status);
58
+ }
59
+
60
+ public get listeners(): Listeners<Listener> {
61
+ return this._listeners;
62
+ }
63
+
64
+ public get progress(): number {
65
+ return this._progress ?? 0;
66
+ }
67
+
68
+ public get cancelled(): boolean {
69
+ return !!this._cancelled?.isResolved();
70
+ }
71
+
72
+ public get started(): Promise<void> {
73
+ return this._started;
74
+ }
75
+
76
+ public get completed(): Promise<void> {
77
+ return this._completed;
78
+ }
79
+
80
+ protected abstract run(): Promise<void>;
81
+
82
+ protected getInitialStatus(): Status {
83
+ return { completed: false } as Status;
84
+ }
85
+
86
+ protected beforeStart(): void {
87
+ if (!this._started.isResolved()) {
88
+ return;
89
+ }
90
+
91
+ if (this._cancelled) {
92
+ delete this._progress;
93
+ delete this._cancelled;
94
+
95
+ return;
96
+ }
97
+
98
+ throw new Error('Job already started!');
99
+ }
100
+
101
+ protected assertNotCancelled(): void {
102
+ if (!this._cancelled) {
103
+ return;
104
+ }
105
+
106
+ this._cancelled.resolve();
107
+
108
+ throw new JobCancelledError();
109
+ }
110
+
111
+ protected calculateCurrentProgress(status?: JobStatus): number {
112
+ status ??= this.status;
113
+
114
+ if (status.completed) {
115
+ return 1;
116
+ }
117
+
118
+ if (!status.children) {
119
+ return 0;
120
+ }
121
+
122
+ return round(
123
+ status.children.reduce((total, child) => total + this.calculateCurrentProgress(child), 0) /
124
+ status.children.length,
125
+ 2,
126
+ );
127
+ }
128
+
129
+ protected async updateProgress(update?: (status: Status) => unknown): Promise<void> {
130
+ await update?.(this.status);
131
+
132
+ const progress = this.calculateCurrentProgress();
133
+
134
+ if (progress === this._progress) {
135
+ return;
136
+ }
137
+
138
+ this._progress = progress;
139
+
140
+ await this._listeners.emit('onUpdated', progress);
141
+ }
142
+
143
+ protected serializeStatus(status: Status): SerializedStatus {
144
+ return { ...status } as unknown as SerializedStatus;
145
+ }
4
146
 
5
147
  }
package/src/jobs/index.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import Job from './Job';
2
2
 
3
3
  export { Job };
4
+ export * from './Job';
5
+ export * from './listeners';
6
+ export * from './status';
4
7
 
5
8
  export async function dispatch(job: Job): Promise<void> {
6
- await job.run();
9
+ await job.start();
7
10
  }
@@ -0,0 +1,3 @@
1
+ export interface JobListener {
2
+ onUpdated?(progress: number): unknown;
3
+ }
@@ -0,0 +1,4 @@
1
+ export interface JobStatus {
2
+ completed: boolean;
3
+ children?: JobStatus[];
4
+ }
@@ -1,10 +1,13 @@
1
- import App from '@/services/App';
1
+ import { isDevelopment } from '@noeldemartin/utils';
2
2
 
3
3
  import type { LangProvider } from './Lang';
4
4
 
5
5
  export default class DefaultLangProvider implements LangProvider {
6
6
 
7
- constructor(private locale: string, private fallbackLocale: string) {}
7
+ constructor(
8
+ private locale: string,
9
+ private fallbackLocale: string,
10
+ ) {}
8
11
 
9
12
  public getLocale(): string {
10
13
  return this.locale;
@@ -28,14 +31,14 @@ export default class DefaultLangProvider implements LangProvider {
28
31
 
29
32
  public translate(key: string): string {
30
33
  // eslint-disable-next-line no-console
31
- App.development && console.warn('Lang provider is missing');
34
+ isDevelopment() && console.warn('Lang provider is missing');
32
35
 
33
36
  return key;
34
37
  }
35
38
 
36
39
  public translateWithDefault(_: string, defaultMessage: string): string {
37
40
  // eslint-disable-next-line no-console
38
- App.development && console.warn('Lang provider is missing');
41
+ isDevelopment() && console.warn('Lang provider is missing');
39
42
 
40
43
  return defaultMessage;
41
44
  }
@@ -1,4 +1,4 @@
1
- import { defineServiceState } from '@/services/Service';
1
+ import { defineServiceState } from '@aerogel/core/services/Service';
2
2
 
3
3
  export default defineServiceState({
4
4
  name: 'lang',
package/src/lang/Lang.ts CHANGED
@@ -52,7 +52,11 @@ export class LangService extends Service {
52
52
  return navigator.languages.find((locale) => locales.includes(locale)) ?? 'en';
53
53
  }
54
54
 
55
- protected async boot(): Promise<void> {
55
+ protected override async boot(): Promise<void> {
56
+ if (!globalThis.document) {
57
+ return;
58
+ }
59
+
56
60
  this.requireStore().$subscribe(
57
61
  async () => {
58
62
  await this.provider.setLocale(this.locale ?? this.getBrowserLocale());
package/src/lang/index.ts CHANGED
@@ -1,10 +1,14 @@
1
- import { bootServices } from '@/services';
2
- import { definePlugin } from '@/plugins';
1
+ import App from '@aerogel/core/services/App';
2
+ import { bootServices } from '@aerogel/core/services';
3
+ import { definePlugin } from '@aerogel/core/plugins';
3
4
 
4
- import Lang, { LangProvider } from './Lang';
5
+ import Lang from './Lang';
6
+ import settings from './settings';
7
+ import type { LangProvider } from './Lang';
5
8
  import { translate, translateWithDefault } from './utils';
6
9
 
7
- export { Lang, LangProvider, translate, translateWithDefault };
10
+ export { Lang, translate, translateWithDefault };
11
+ export type { LangProvider };
8
12
 
9
13
  const services = { $lang: Lang };
10
14
 
@@ -15,15 +19,17 @@ export default definePlugin({
15
19
  app.config.globalProperties.$t ??= translate;
16
20
  app.config.globalProperties.$td = translateWithDefault;
17
21
 
22
+ settings.forEach((setting) => App.addSetting(setting));
23
+
18
24
  await bootServices(app, services);
19
25
  },
20
26
  });
21
27
 
22
- declare module '@/services' {
28
+ declare module '@aerogel/core/services' {
23
29
  export interface Services extends LangServices {}
24
30
  }
25
31
 
26
- declare module '@vue/runtime-core' {
32
+ declare module 'vue' {
27
33
  interface ComponentCustomProperties {
28
34
  $td: typeof translateWithDefault;
29
35
  }