@aerogel/core 0.0.0-next.fd1bd21aea7a9ab8c4eab69a5f5776db5de8bf35 → 0.1.0

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 (213) hide show
  1. package/dist/aerogel-core.css +1 -0
  2. package/dist/aerogel-core.d.ts +2263 -1698
  3. package/dist/aerogel-core.js +3809 -0
  4. package/dist/aerogel-core.js.map +1 -0
  5. package/package.json +32 -37
  6. package/src/bootstrap/bootstrap.test.ts +4 -8
  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/HeadlessInputInput.vue +86 -0
  30. package/src/components/headless/{forms/AGHeadlessInputLabel.vue → HeadlessInputLabel.vue} +3 -7
  31. package/src/components/headless/{forms/AGHeadlessInputTextArea.vue → HeadlessInputTextArea.vue} +11 -11
  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/HeadlessSwitch.vue +96 -0
  45. package/src/components/headless/HeadlessToast.vue +18 -0
  46. package/src/components/headless/HeadlessToastAction.vue +13 -0
  47. package/src/components/headless/index.ts +20 -3
  48. package/src/components/index.ts +6 -9
  49. package/src/components/ui/AdvancedOptions.vue +18 -0
  50. package/src/components/ui/AlertModal.vue +17 -0
  51. package/src/components/ui/Button.vue +115 -0
  52. package/src/components/ui/Checkbox.vue +56 -0
  53. package/src/components/ui/ConfirmModal.vue +50 -0
  54. package/src/components/ui/DropdownMenu.vue +32 -0
  55. package/src/components/ui/DropdownMenuOption.vue +22 -0
  56. package/src/components/ui/DropdownMenuOptions.vue +44 -0
  57. package/src/components/ui/EditableContent.vue +82 -0
  58. package/src/components/ui/ErrorLogs.vue +19 -0
  59. package/src/components/ui/ErrorLogsModal.vue +48 -0
  60. package/src/components/ui/ErrorMessage.vue +15 -0
  61. package/src/components/ui/ErrorReportModal.vue +73 -0
  62. package/src/components/{modals/AGErrorReportModalButtons.vue → ui/ErrorReportModalButtons.vue} +34 -27
  63. package/src/components/ui/ErrorReportModalTitle.vue +24 -0
  64. package/src/components/{forms/AGForm.vue → ui/Form.vue} +4 -5
  65. package/src/components/ui/Input.vue +56 -0
  66. package/src/components/ui/Link.vue +12 -0
  67. package/src/components/ui/LoadingModal.vue +34 -0
  68. package/src/components/ui/Markdown.vue +97 -0
  69. package/src/components/ui/Modal.vue +131 -0
  70. package/src/components/ui/ModalContext.vue +31 -0
  71. package/src/components/ui/ProgressBar.vue +51 -0
  72. package/src/components/ui/PromptModal.vue +38 -0
  73. package/src/components/ui/Select.vue +27 -0
  74. package/src/components/ui/SelectLabel.vue +21 -0
  75. package/src/components/ui/SelectOption.vue +29 -0
  76. package/src/components/ui/SelectOptions.vue +35 -0
  77. package/src/components/ui/SelectTrigger.vue +29 -0
  78. package/src/components/ui/Setting.vue +31 -0
  79. package/src/components/ui/SettingsModal.vue +15 -0
  80. package/src/components/ui/StartupCrash.vue +76 -0
  81. package/src/components/ui/Switch.vue +11 -0
  82. package/src/components/ui/TextArea.vue +56 -0
  83. package/src/components/ui/Toast.vue +46 -0
  84. package/src/components/ui/index.ts +35 -0
  85. package/src/directives/index.ts +9 -5
  86. package/src/directives/measure.ts +33 -8
  87. package/src/errors/Errors.state.ts +2 -1
  88. package/src/errors/Errors.ts +56 -33
  89. package/src/errors/JobCancelledError.ts +3 -0
  90. package/src/errors/index.ts +15 -8
  91. package/src/errors/settings/Debug.vue +14 -0
  92. package/src/errors/settings/index.ts +10 -0
  93. package/src/errors/utils.ts +17 -1
  94. package/src/forms/FormController.test.ts +113 -0
  95. package/src/forms/{Form.ts → FormController.ts} +73 -42
  96. package/src/forms/index.ts +3 -3
  97. package/src/forms/utils.ts +65 -24
  98. package/src/forms/validation.ts +50 -0
  99. package/src/index.css +76 -0
  100. package/src/jobs/Job.ts +144 -2
  101. package/src/jobs/index.ts +4 -1
  102. package/src/jobs/listeners.ts +3 -0
  103. package/src/jobs/status.ts +4 -0
  104. package/src/lang/DefaultLangProvider.ts +46 -0
  105. package/src/lang/Lang.state.ts +11 -0
  106. package/src/lang/Lang.ts +48 -21
  107. package/src/lang/index.ts +12 -6
  108. package/src/lang/settings/Language.vue +48 -0
  109. package/src/lang/settings/index.ts +10 -0
  110. package/src/plugins/Plugin.ts +1 -1
  111. package/src/plugins/index.ts +10 -7
  112. package/src/services/App.state.ts +36 -3
  113. package/src/services/App.ts +19 -3
  114. package/src/services/Cache.ts +1 -1
  115. package/src/services/Events.test.ts +8 -8
  116. package/src/services/Events.ts +16 -12
  117. package/src/services/Service.ts +135 -59
  118. package/src/services/Storage.ts +20 -0
  119. package/src/services/index.ts +16 -7
  120. package/src/services/utils.ts +18 -0
  121. package/src/testing/index.ts +8 -3
  122. package/src/testing/setup.ts +11 -0
  123. package/src/ui/UI.state.ts +14 -12
  124. package/src/ui/UI.ts +250 -123
  125. package/src/ui/index.ts +28 -28
  126. package/src/ui/utils.ts +16 -0
  127. package/src/utils/app.ts +7 -0
  128. package/src/utils/classes.ts +41 -0
  129. package/src/utils/composition/events.ts +4 -6
  130. package/src/utils/composition/forms.ts +20 -4
  131. package/src/utils/composition/persistent.test.ts +33 -0
  132. package/src/utils/composition/persistent.ts +11 -0
  133. package/src/utils/composition/state.test.ts +47 -0
  134. package/src/utils/composition/state.ts +33 -0
  135. package/src/utils/index.ts +6 -1
  136. package/src/utils/markdown.test.ts +50 -0
  137. package/src/utils/markdown.ts +53 -6
  138. package/src/utils/types.ts +3 -0
  139. package/src/utils/vue.ts +38 -132
  140. package/dist/aerogel-core.cjs.js +0 -2
  141. package/dist/aerogel-core.cjs.js.map +0 -1
  142. package/dist/aerogel-core.esm.js +0 -2
  143. package/dist/aerogel-core.esm.js.map +0 -1
  144. package/histoire.config.ts +0 -7
  145. package/noeldemartin.config.js +0 -5
  146. package/postcss.config.js +0 -6
  147. package/src/assets/histoire.css +0 -3
  148. package/src/components/AGAppLayout.vue +0 -16
  149. package/src/components/AGAppOverlays.vue +0 -41
  150. package/src/components/AGAppSnackbars.vue +0 -13
  151. package/src/components/constants.ts +0 -8
  152. package/src/components/forms/AGButton.vue +0 -44
  153. package/src/components/forms/AGCheckbox.vue +0 -41
  154. package/src/components/forms/AGInput.vue +0 -40
  155. package/src/components/forms/AGSelect.story.vue +0 -46
  156. package/src/components/forms/AGSelect.vue +0 -60
  157. package/src/components/forms/index.ts +0 -5
  158. package/src/components/headless/forms/AGHeadlessButton.vue +0 -56
  159. package/src/components/headless/forms/AGHeadlessInput.ts +0 -32
  160. package/src/components/headless/forms/AGHeadlessInput.vue +0 -57
  161. package/src/components/headless/forms/AGHeadlessInputInput.vue +0 -81
  162. package/src/components/headless/forms/AGHeadlessSelect.ts +0 -42
  163. package/src/components/headless/forms/AGHeadlessSelect.vue +0 -77
  164. package/src/components/headless/forms/AGHeadlessSelectButton.vue +0 -24
  165. package/src/components/headless/forms/AGHeadlessSelectLabel.vue +0 -24
  166. package/src/components/headless/forms/AGHeadlessSelectOption.ts +0 -4
  167. package/src/components/headless/forms/AGHeadlessSelectOption.vue +0 -39
  168. package/src/components/headless/forms/AGHeadlessSelectOptions.ts +0 -3
  169. package/src/components/headless/forms/composition.ts +0 -10
  170. package/src/components/headless/forms/index.ts +0 -17
  171. package/src/components/headless/modals/AGHeadlessModal.ts +0 -34
  172. package/src/components/headless/modals/AGHeadlessModal.vue +0 -86
  173. package/src/components/headless/modals/AGHeadlessModalPanel.vue +0 -28
  174. package/src/components/headless/modals/AGHeadlessModalTitle.vue +0 -13
  175. package/src/components/headless/modals/index.ts +0 -4
  176. package/src/components/headless/snackbars/AGHeadlessSnackbar.vue +0 -10
  177. package/src/components/headless/snackbars/index.ts +0 -40
  178. package/src/components/lib/AGErrorMessage.vue +0 -16
  179. package/src/components/lib/AGLink.vue +0 -9
  180. package/src/components/lib/AGMarkdown.vue +0 -41
  181. package/src/components/lib/AGMeasured.vue +0 -15
  182. package/src/components/lib/AGStartupCrash.vue +0 -31
  183. package/src/components/lib/index.ts +0 -5
  184. package/src/components/modals/AGAlertModal.ts +0 -15
  185. package/src/components/modals/AGAlertModal.vue +0 -14
  186. package/src/components/modals/AGConfirmModal.ts +0 -27
  187. package/src/components/modals/AGConfirmModal.vue +0 -26
  188. package/src/components/modals/AGErrorReportModal.ts +0 -46
  189. package/src/components/modals/AGErrorReportModal.vue +0 -54
  190. package/src/components/modals/AGErrorReportModalTitle.vue +0 -25
  191. package/src/components/modals/AGLoadingModal.ts +0 -23
  192. package/src/components/modals/AGLoadingModal.vue +0 -15
  193. package/src/components/modals/AGModal.ts +0 -10
  194. package/src/components/modals/AGModal.vue +0 -39
  195. package/src/components/modals/AGModalContext.ts +0 -8
  196. package/src/components/modals/AGModalContext.vue +0 -22
  197. package/src/components/modals/AGModalTitle.vue +0 -9
  198. package/src/components/modals/AGPromptModal.ts +0 -30
  199. package/src/components/modals/AGPromptModal.vue +0 -34
  200. package/src/components/modals/index.ts +0 -17
  201. package/src/components/snackbars/AGSnackbar.vue +0 -36
  202. package/src/components/snackbars/index.ts +0 -3
  203. package/src/components/utils.ts +0 -10
  204. package/src/directives/initial-focus.ts +0 -11
  205. package/src/forms/Form.test.ts +0 -58
  206. package/src/forms/composition.ts +0 -6
  207. package/src/main.histoire.ts +0 -1
  208. package/src/utils/tailwindcss.test.ts +0 -26
  209. package/src/utils/tailwindcss.ts +0 -7
  210. package/tailwind.config.js +0 -4
  211. package/tsconfig.json +0 -11
  212. package/vite.config.ts +0 -14
  213. /package/src/{main.ts → index.ts} +0 -0
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <component :is="as">
3
+ <slot />
4
+ </component>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { computed, inject, provide, readonly } from 'vue';
9
+ import { uuid } from '@noeldemartin/utils';
10
+
11
+ import type FormController from '@aerogel/core/forms/FormController';
12
+ import type { FormFieldValue } from '@aerogel/core/forms/FormController';
13
+ import type { InputEmits, InputExpose, InputProps } from '@aerogel/core/components/contracts/Input';
14
+
15
+ const { as = 'div', name, label, description, modelValue } = defineProps<InputProps & { as?: string }>();
16
+ const emit = defineEmits<InputEmits>();
17
+ const form = inject<FormController | null>('form', null);
18
+ const errors = computed(() => {
19
+ if (!form || !name) {
20
+ return null;
21
+ }
22
+
23
+ return form.errors[name] ?? null;
24
+ });
25
+
26
+ const expose = {
27
+ id: `input-${uuid()}`,
28
+ name: computed(() => name),
29
+ label: computed(() => label),
30
+ description: computed(() => description),
31
+ value: computed(() => {
32
+ if (form && name) {
33
+ return form.getFieldValue(name);
34
+ }
35
+
36
+ return modelValue;
37
+ }),
38
+ errors: readonly(errors),
39
+ required: computed(() => {
40
+ if (!name || !form) {
41
+ return;
42
+ }
43
+
44
+ return form.getFieldRules(name).includes('required');
45
+ }),
46
+ update(value) {
47
+ if (form && name) {
48
+ form.setFieldValue(name, value as FormFieldValue);
49
+
50
+ return;
51
+ }
52
+
53
+ emit('update:modelValue', value);
54
+ },
55
+ } satisfies InputExpose;
56
+
57
+ provide('input', expose);
58
+ defineExpose(expose);
59
+ </script>
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <slot :id="`${input.id}-description`">
3
- <AGMarkdown
3
+ <Markdown
4
4
  v-if="show"
5
5
  v-bind="$attrs"
6
6
  :id="`${input.id}-description`"
7
- :text="text"
7
+ :text
8
8
  />
9
9
  </slot>
10
10
  </template>
@@ -12,16 +12,15 @@
12
12
  <script setup lang="ts">
13
13
  import { computed } from 'vue';
14
14
 
15
- import { injectReactiveOrFail } from '@/utils/vue';
16
-
17
- import AGMarkdown from '../../lib/AGMarkdown.vue';
18
- import type { IAGHeadlessInput } from './AGHeadlessInput';
15
+ import Markdown from '@aerogel/core/components/ui/Markdown.vue';
16
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
17
+ import type { InputExpose } from '@aerogel/core/components/contracts/Input';
19
18
 
20
19
  defineOptions({ inheritAttrs: false });
21
20
 
22
- const input = injectReactiveOrFail<IAGHeadlessInput>(
21
+ const input = injectReactiveOrFail<InputExpose>(
23
22
  'input',
24
- '<AGHeadlessInputDescription> must be a child of a <AGHeadlessInput>',
23
+ '<HeadlessInputDescription> must be a child of a <HeadlessInput>',
25
24
  );
26
25
  const text = computed(() => (typeof input.description === 'string' ? input.description : ''));
27
26
  const show = computed(() => !!input.description);
@@ -7,15 +7,11 @@
7
7
  <script setup lang="ts">
8
8
  import { computed } from 'vue';
9
9
 
10
- import { injectReactiveOrFail } from '@/utils/vue';
11
- import { translateWithDefault } from '@/lang/utils';
10
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
11
+ import { translateWithDefault } from '@aerogel/core/lang/utils';
12
+ import type { InputExpose } from '@aerogel/core/components/contracts/Input';
12
13
 
13
- import type { IAGHeadlessInput } from './AGHeadlessInput';
14
-
15
- const input = injectReactiveOrFail<IAGHeadlessInput>(
16
- 'input',
17
- '<AGHeadlessInputError> must be a child of a <AGHeadlessInput>',
18
- );
14
+ const input = injectReactiveOrFail<InputExpose>('input', '<HeadlessInputError> must be a child of a <HeadlessInput>');
19
15
  const errorMessage = computed(() => {
20
16
  if (!input.errors) {
21
17
  return null;
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <input
3
+ :id="input.id"
4
+ ref="$inputRef"
5
+ :name
6
+ :checked
7
+ :type="renderedType"
8
+ :required="input.required ?? undefined"
9
+ :aria-invalid="input.errors ? 'true' : 'false'"
10
+ :aria-describedby="
11
+ input.errors ? `${input.id}-error` : input.description ? `${input.id}-description` : undefined
12
+ "
13
+ @input="update"
14
+ >
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { computed, inject, useTemplateRef, watchEffect } from 'vue';
19
+
20
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
21
+ import { onFormFocus } from '@aerogel/core/utils/composition/forms';
22
+ import type FormController from '@aerogel/core/forms/FormController';
23
+ import type { FormFieldValue } from '@aerogel/core/forms/FormController';
24
+ import type { InputExpose } from '@aerogel/core/components/contracts/Input';
25
+
26
+ const { type } = defineProps<{ type?: string }>();
27
+ const $input = useTemplateRef('$inputRef');
28
+ const input = injectReactiveOrFail<InputExpose>('input', '<HeadlessInputInput> must be a child of a <HeadlessInput>');
29
+ const form = inject<FormController | null>('form', null);
30
+ const name = computed(() => input.name ?? undefined);
31
+ const value = computed(() => input.value);
32
+ const renderedType = computed(() => {
33
+ if (type) {
34
+ return type;
35
+ }
36
+
37
+ const fieldType = (name.value && form?.getFieldType(name.value)) ?? '';
38
+
39
+ return ['text', 'email', 'number', 'tel', 'url'].includes(fieldType) ? fieldType : 'text';
40
+ });
41
+ const checked = computed(() => {
42
+ if (type !== 'checkbox') {
43
+ return;
44
+ }
45
+
46
+ return !!value.value;
47
+ });
48
+
49
+ function update() {
50
+ if (!$input.value) {
51
+ return;
52
+ }
53
+
54
+ input.update(getValue());
55
+ }
56
+
57
+ function getValue(): FormFieldValue | null {
58
+ if (!$input.value) {
59
+ return null;
60
+ }
61
+
62
+ switch (type) {
63
+ case 'checkbox':
64
+ return $input.value.checked;
65
+ case 'date':
66
+ return $input.value.valueAsDate;
67
+ default:
68
+ return $input.value.value;
69
+ }
70
+ }
71
+
72
+ onFormFocus(input, () => $input.value?.focus());
73
+ watchEffect(() => {
74
+ if (!$input.value) {
75
+ return;
76
+ }
77
+
78
+ if (type === 'date' && value.value instanceof Date) {
79
+ $input.value.valueAsDate = value.value;
80
+
81
+ return;
82
+ }
83
+
84
+ $input.value.value = (value.value as string) ?? null;
85
+ });
86
+ </script>
@@ -9,14 +9,10 @@
9
9
  <script setup lang="ts">
10
10
  import { computed, useSlots } from 'vue';
11
11
 
12
- import { injectReactiveOrFail } from '@/utils/vue';
12
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
13
+ import type { InputExpose } from '@aerogel/core/components/contracts/Input';
13
14
 
14
- import type { IAGHeadlessInput } from './AGHeadlessInput';
15
-
16
- const input = injectReactiveOrFail<IAGHeadlessInput>(
17
- 'input',
18
- '<AGHeadlessInputLabel> must be a child of a <AGHeadlessInput>',
19
- );
15
+ const input = injectReactiveOrFail<InputExpose>('input', '<HeadlessInputLabel> must be a child of a <HeadlessInput>');
20
16
  const slots = useSlots();
21
17
  const show = computed(() => !!(input.label || slots.default));
22
18
  </script>
@@ -1,9 +1,10 @@
1
1
  <template>
2
2
  <textarea
3
3
  :id="input.id"
4
- ref="$textArea"
5
- :name="name"
6
- :value="value"
4
+ ref="$textAreaRef"
5
+ :name
6
+ :value
7
+ :required="input.required ?? undefined"
7
8
  :aria-invalid="input.errors ? 'true' : 'false'"
8
9
  :aria-describedby="
9
10
  input.errors ? `${input.id}-error` : input.description ? `${input.id}-description` : undefined
@@ -13,17 +14,16 @@
13
14
  </template>
14
15
 
15
16
  <script setup lang="ts">
16
- import { computed, ref } from 'vue';
17
+ import { computed, useTemplateRef } from 'vue';
17
18
 
18
- import { injectReactiveOrFail } from '@/utils';
19
- import type { IAGHeadlessInput } from '@/components/headless/forms/AGHeadlessInput';
19
+ import { onFormFocus } from '@aerogel/core/utils/composition/forms';
20
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
21
+ import type { InputExpose } from '@aerogel/core/components/contracts/Input';
20
22
 
21
- import { onFormFocus } from './composition';
22
-
23
- const $textArea = ref<HTMLTextAreaElement>();
24
- const input = injectReactiveOrFail<IAGHeadlessInput>(
23
+ const $textArea = useTemplateRef('$textAreaRef');
24
+ const input = injectReactiveOrFail<InputExpose>(
25
25
  'input',
26
- '<AGHeadlessInputTextArea> must be a child of a <AGHeadlessInput>',
26
+ '<HeadlessInputTextArea> must be a child of a <HeadlessInput>',
27
27
  );
28
28
  const name = computed(() => input.name ?? undefined);
29
29
  const value = computed(() => input.value as string);
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <DialogRoot :ref="forwardRef" open @update:open="persistent || close()">
3
+ <DialogPortal>
4
+ <slot :close="close" />
5
+ </DialogPortal>
6
+ </DialogRoot>
7
+ </template>
8
+
9
+ <script setup lang="ts" generic="T = void">
10
+ import { provide, ref } from 'vue';
11
+ import { DialogPortal, DialogRoot, useForwardExpose } from 'reka-ui';
12
+ import type { DialogContent } from 'reka-ui';
13
+ import type { Nullable } from '@noeldemartin/utils';
14
+
15
+ import Events from '@aerogel/core/services/Events';
16
+ import { useEvent } from '@aerogel/core/utils/composition/events';
17
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
18
+ import type { AcceptRefs } from '@aerogel/core/utils/vue';
19
+ import type { UIModalContext } from '@aerogel/core/ui/UI';
20
+ import type { ModalExpose, ModalProps, ModalSlots } from '@aerogel/core/components/contracts/Modal';
21
+
22
+ const $content = ref<Nullable<InstanceType<typeof DialogContent>>>(null);
23
+ const { modal } = injectReactiveOrFail<UIModalContext>(
24
+ 'modal',
25
+ 'could not obtain modal reference from <HeadlessModal>, ' +
26
+ 'did you render this component manually? Show it using $ui.modal() instead',
27
+ );
28
+
29
+ defineProps<ModalProps>();
30
+ defineSlots<ModalSlots<T>>();
31
+ defineExpose<AcceptRefs<ModalExpose<T>>>({ close, $content });
32
+
33
+ const { forwardRef } = useForwardExpose();
34
+ const closed = ref(false);
35
+
36
+ provide('$modalContentRef', $content);
37
+
38
+ useEvent('close-modal', async ({ id, result }) => {
39
+ if (id !== modal.id) {
40
+ return;
41
+ }
42
+
43
+ await close(result);
44
+ });
45
+
46
+ async function close(result?: unknown) {
47
+ if (closed.value) {
48
+ return;
49
+ }
50
+
51
+ await Events.emit('modal-will-close', { modal, result });
52
+
53
+ closed.value = true;
54
+
55
+ await Events.emit('modal-has-closed', { modal, result });
56
+ }
57
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <DialogContent ref="$contentRef">
3
+ <slot />
4
+
5
+ <ModalContext v-if="childModal" :child-index="childIndex + 1" :modal="childModal" />
6
+ </DialogContent>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { computed, useTemplateRef, watchEffect } from 'vue';
11
+ import { DialogContent } from 'reka-ui';
12
+ import type { Ref } from 'vue';
13
+
14
+ import ModalContext from '@aerogel/core/components/ui/ModalContext.vue';
15
+ import UI from '@aerogel/core/ui/UI';
16
+ import { injectOrFail, injectReactiveOrFail } from '@aerogel/core/utils/vue';
17
+ import type { UIModalContext } from '@aerogel/core/ui/UI';
18
+ import type { ModalContentInstance } from '@aerogel/core/components/contracts/Modal';
19
+
20
+ const { childIndex = 0 } = injectReactiveOrFail<UIModalContext>(
21
+ 'modal',
22
+ 'could not obtain modal reference from <HeadlessModalContent>, ' +
23
+ 'did you render this component manually? Show it using $ui.modal() instead',
24
+ );
25
+ const $modalContentRef = injectOrFail<Ref<ModalContentInstance>>('$modalContentRef');
26
+ const $content = useTemplateRef('$contentRef');
27
+ const childModal = computed(() => UI.modals[childIndex] ?? null);
28
+
29
+ watchEffect(() => ($modalContentRef.value = $content.value));
30
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <DialogDescription v-bind="$props">
3
+ <slot />
4
+ </DialogDescription>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { DialogDescription } from 'reka-ui';
9
+ import type { DialogDescriptionProps } from 'reka-ui';
10
+
11
+ defineProps<DialogDescriptionProps>();
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <DialogOverlay v-bind="$props">
3
+ <slot />
4
+ </DialogOverlay>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { DialogOverlay } from 'reka-ui';
9
+ import type { DialogOverlayProps } from 'reka-ui';
10
+
11
+ defineProps<DialogOverlayProps>();
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <DialogTitle v-bind="$props">
3
+ <slot />
4
+ </DialogTitle>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { DialogTitle } from 'reka-ui';
9
+ import type { DialogTitleProps } from 'reka-ui';
10
+
11
+ defineProps<DialogTitleProps>();
12
+ </script>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <SelectRoot
3
+ v-slot="{ open }"
4
+ :model-value="acceptableValue"
5
+ :by="compareOptions as Closure<[AcceptableValue, AcceptableValue], boolean>"
6
+ @update:model-value="update($event)"
7
+ >
8
+ <component :is="as" v-bind="$attrs">
9
+ <slot :model-value :open>
10
+ <HeadlessSelectTrigger />
11
+ <HeadlessSelectOptions />
12
+ </slot>
13
+ </component>
14
+ </SelectRoot>
15
+ </template>
16
+
17
+ <script setup lang="ts" generic="T extends Nullable<FormFieldValue>">
18
+ import { computed, inject, provide, readonly } from 'vue';
19
+ import { value as evaluate, toString, uuid } from '@noeldemartin/utils';
20
+ import { SelectRoot } from 'reka-ui';
21
+ import type { AcceptableValue } from 'reka-ui';
22
+ import type { Closure, Nullable } from '@noeldemartin/utils';
23
+
24
+ import { translateWithDefault } from '@aerogel/core/lang';
25
+ import { hasSelectOptionLabel } from '@aerogel/core/components/contracts/Select';
26
+ import type FormController from '@aerogel/core/forms/FormController';
27
+ import type { SelectEmits, SelectExpose, SelectProps } from '@aerogel/core/components/contracts/Select';
28
+ import type { FormFieldValue } from '@aerogel/core/forms/FormController';
29
+
30
+ import HeadlessSelectTrigger from './HeadlessSelectTrigger.vue';
31
+ import HeadlessSelectOptions from './HeadlessSelectOptions.vue';
32
+
33
+ defineOptions({ inheritAttrs: false });
34
+
35
+ const {
36
+ name,
37
+ as = 'div',
38
+ label,
39
+ options,
40
+ labelClass,
41
+ optionsClass,
42
+ renderOption,
43
+ compareOptions = (a, b) => a === b,
44
+ description,
45
+ placeholder,
46
+ modelValue,
47
+ align,
48
+ side,
49
+ } = defineProps<SelectProps<T>>();
50
+ const emit = defineEmits<SelectEmits<T>>();
51
+ const form = inject<FormController | null>('form', null);
52
+ const computedValue = computed(() => {
53
+ if (form && name) {
54
+ return form.getFieldValue(name) as T;
55
+ }
56
+
57
+ return modelValue as T;
58
+ });
59
+ const acceptableValue = computed(() => computedValue.value as AcceptableValue);
60
+ const errors = computed(() => {
61
+ if (!form || !name) {
62
+ return null;
63
+ }
64
+
65
+ return form.errors[name] ?? null;
66
+ });
67
+ const computedOptions = computed(() => {
68
+ if (!options) {
69
+ return null;
70
+ }
71
+
72
+ return options.map((option) => ({
73
+ key: uuid(),
74
+ label: renderOption
75
+ ? renderOption(option)
76
+ : hasSelectOptionLabel(option)
77
+ ? evaluate(option.label as string)
78
+ : toString(option),
79
+ value: option as AcceptableValue,
80
+ }));
81
+ });
82
+ const expose = {
83
+ labelClass,
84
+ optionsClass,
85
+ align,
86
+ side,
87
+ value: computedValue,
88
+ id: `select-${uuid()}`,
89
+ name: computed(() => name),
90
+ label: computed(() => label),
91
+ description: computed(() => description),
92
+ placeholder: computed(() => placeholder ?? translateWithDefault('ui.select', 'Select an option')),
93
+ options: computedOptions,
94
+ selectedOption: computed(() => computedOptions.value?.find((option) => option.value === modelValue)),
95
+ errors: readonly(errors),
96
+ required: computed(() => {
97
+ if (!name || !form) {
98
+ return;
99
+ }
100
+
101
+ return form.getFieldRules(name).includes('required');
102
+ }),
103
+ update(value) {
104
+ if (form && name) {
105
+ form.setFieldValue(name, value as FormFieldValue);
106
+
107
+ return;
108
+ }
109
+
110
+ emit('update:modelValue', value);
111
+ },
112
+ } satisfies SelectExpose<T>;
113
+
114
+ function update(value: AcceptableValue) {
115
+ expose.update(value as T);
116
+ }
117
+
118
+ provide('select', expose);
119
+ defineExpose(expose);
120
+ </script>
@@ -7,14 +7,13 @@
7
7
  <script setup lang="ts">
8
8
  import { computed } from 'vue';
9
9
 
10
- import { injectReactiveOrFail } from '@/utils/vue';
11
- import { translateWithDefault } from '@/lang/utils';
10
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
11
+ import { translateWithDefault } from '@aerogel/core/lang/utils';
12
+ import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
12
13
 
13
- import type { IAGHeadlessSelect } from './AGHeadlessSelect';
14
-
15
- const select = injectReactiveOrFail<IAGHeadlessSelect>(
14
+ const select = injectReactiveOrFail<SelectExpose>(
16
15
  'select',
17
- '<AGHeadlessSelectError> must be a child of a <AGHeadlessSelect>',
16
+ '<HeadlessSelectError> must be a child of a <HeadlessSelect>',
18
17
  );
19
18
  const errorMessage = computed(() => {
20
19
  if (!select.errors) {
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <Label v-if="show" :for="select.id" v-bind="$props">
3
+ <slot>
4
+ {{ select.label }}
5
+ </slot>
6
+ </Label>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { computed, useSlots } from 'vue';
11
+ import { Label } from 'reka-ui';
12
+ import type { LabelProps } from 'reka-ui';
13
+
14
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
15
+ import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
16
+
17
+ defineProps<Omit<LabelProps, 'for'>>();
18
+
19
+ const select = injectReactiveOrFail<SelectExpose>(
20
+ 'select',
21
+ '<HeadlessSelectLabel> must be a child of a <HeadlessSelect>',
22
+ );
23
+ const slots = useSlots();
24
+ const show = computed(() => !!(select.label || slots.default));
25
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <SelectItem v-bind="$props">
3
+ <SelectItemText>
4
+ <slot>
5
+ {{ renderedLabel }}
6
+ </slot>
7
+ </SelectItemText>
8
+ </SelectItem>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { computed } from 'vue';
13
+ import { SelectItem, SelectItemText } from 'reka-ui';
14
+ import { toString } from '@noeldemartin/utils';
15
+ import type { SelectItemProps } from 'reka-ui';
16
+
17
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
18
+ import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
19
+
20
+ const { value } = defineProps<SelectItemProps>();
21
+ const select = injectReactiveOrFail<SelectExpose>(
22
+ 'select',
23
+ '<HeadlessSelectOption> must be a child of a <HeadlessSelect>',
24
+ );
25
+ const renderedLabel = computed(() => {
26
+ const itemOption = select.options?.find((option) => option.value === value);
27
+
28
+ if (itemOption) {
29
+ return itemOption.label;
30
+ }
31
+
32
+ return toString(value);
33
+ });
34
+ </script>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <SelectPortal>
3
+ <SelectContent
4
+ position="popper"
5
+ :class="renderedClasses"
6
+ :align="select.align"
7
+ :side="select.side"
8
+ :side-offset="4"
9
+ >
10
+ <SelectViewport :class="innerClass">
11
+ <slot>
12
+ <HeadlessSelectOption
13
+ v-for="option in select.options ?? []"
14
+ :key="option.key"
15
+ :value="option.value"
16
+ />
17
+ </slot>
18
+ </SelectViewport>
19
+ </SelectContent>
20
+ </SelectPortal>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import { SelectContent, SelectPortal, SelectViewport } from 'reka-ui';
25
+ import { computed } from 'vue';
26
+ import type { HTMLAttributes } from 'vue';
27
+
28
+ import { injectReactiveOrFail } from '@aerogel/core/utils/vue';
29
+ import { classes } from '@aerogel/core/utils/classes';
30
+ import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
31
+
32
+ import HeadlessSelectOption from './HeadlessSelectOption.vue';
33
+
34
+ const { class: rootClass } = defineProps<{ class?: HTMLAttributes['class']; innerClass?: HTMLAttributes['class'] }>();
35
+
36
+ const select = injectReactiveOrFail<SelectExpose>(
37
+ 'select',
38
+ '<HeadlessSelectOptions> must be a child of a <HeadlessSelect>',
39
+ );
40
+ const renderedClasses = computed(() =>
41
+ classes('min-w-(--reka-select-trigger-width) max-h-(--reka-select-content-available-height)', rootClass));
42
+ </script>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <SelectTrigger :id="select.id">
3
+ <slot>
4
+ <HeadlessSelectValue :placeholder="select.placeholder" />
5
+ <SelectIcon />
6
+ </slot>
7
+ </SelectTrigger>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { SelectIcon, SelectTrigger } from 'reka-ui';
12
+
13
+ import { injectReactiveOrFail } from '@aerogel/core/utils';
14
+ import type { SelectExpose } from '@aerogel/core/components/contracts/Select';
15
+
16
+ import HeadlessSelectValue from './HeadlessSelectValue.vue';
17
+
18
+ const select = injectReactiveOrFail<SelectExpose>(
19
+ 'select',
20
+ '<HeadlessSelectTrigger> must be a child of a <HeadlessSelect>',
21
+ );
22
+ </script>