@mapomodule/form 2.0.0-beta.0 → 2.0.0-beta.10

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 (194) hide show
  1. package/README.md +2 -1
  2. package/dist/__tests__/__mocks__/nuxt-imports.d.ts +4 -0
  3. package/dist/__tests__/__mocks__/nuxt-imports.d.ts.map +1 -0
  4. package/dist/__tests__/camomillaRoundTrip.test.d.ts +2 -0
  5. package/dist/__tests__/camomillaRoundTrip.test.d.ts.map +1 -0
  6. package/dist/__tests__/registry.test.d.ts +2 -0
  7. package/dist/__tests__/registry.test.d.ts.map +1 -0
  8. package/dist/__tests__/useFormFromSchema.test.d.ts +2 -0
  9. package/dist/__tests__/useFormFromSchema.test.d.ts.map +1 -0
  10. package/dist/__tests__/useMapoForm.test.d.ts +2 -0
  11. package/dist/__tests__/useMapoForm.test.d.ts.map +1 -0
  12. package/dist/index.d.mts +9 -1
  13. package/dist/index.d.ts +15 -1
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.mjs +7 -34
  16. package/dist/module.d.mts +20 -2
  17. package/dist/module.d.ts +12 -2
  18. package/dist/module.d.ts.map +1 -0
  19. package/dist/module.json +1 -1
  20. package/dist/module.mjs +187 -13
  21. package/dist/runtime/components/MapoFocusPortal.vue +115 -0
  22. package/dist/runtime/components/MapoFocusPortal.vue.d.ts +4 -0
  23. package/dist/runtime/components/MapoFocusPortal.vue.d.ts.map +1 -0
  24. package/dist/runtime/components/MapoForm.vue +168 -0
  25. package/dist/runtime/components/MapoForm.vue.d.ts +47 -0
  26. package/dist/runtime/components/MapoForm.vue.d.ts.map +1 -0
  27. package/dist/runtime/components/MapoFormField.vue +269 -0
  28. package/dist/runtime/components/MapoFormField.vue.d.ts +46 -0
  29. package/dist/runtime/components/MapoFormField.vue.d.ts.map +1 -0
  30. package/dist/runtime/components/MapoFormFlatSection.vue +149 -0
  31. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts +15 -0
  32. package/dist/runtime/components/MapoFormFlatSection.vue.d.ts.map +1 -0
  33. package/dist/runtime/components/MapoFormGroup.vue +247 -0
  34. package/dist/runtime/components/MapoFormGroup.vue.d.ts +44 -0
  35. package/dist/runtime/components/MapoFormGroup.vue.d.ts.map +1 -0
  36. package/dist/runtime/components/MapoFormTabs.vue +110 -0
  37. package/dist/runtime/components/MapoFormTabs.vue.d.ts +15 -0
  38. package/dist/runtime/components/MapoFormTabs.vue.d.ts.map +1 -0
  39. package/dist/runtime/components/MapoUnknownField.vue +62 -0
  40. package/dist/runtime/components/MapoUnknownField.vue.d.ts +16 -0
  41. package/dist/runtime/components/MapoUnknownField.vue.d.ts.map +1 -0
  42. package/dist/runtime/components/fields/MapoDateField.vue +88 -0
  43. package/dist/runtime/components/fields/MapoDateField.vue.d.ts +17 -0
  44. package/dist/runtime/components/fields/MapoDateField.vue.d.ts.map +1 -0
  45. package/dist/runtime/components/fields/MapoDateTimeField.vue +159 -0
  46. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts +17 -0
  47. package/dist/runtime/components/fields/MapoDateTimeField.vue.d.ts.map +1 -0
  48. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +130 -0
  49. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
  50. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
  51. package/dist/runtime/components/fields/MapoFileField.vue +99 -0
  52. package/dist/runtime/components/fields/MapoFileField.vue.d.ts +16 -0
  53. package/dist/runtime/components/fields/MapoFileField.vue.d.ts.map +1 -0
  54. package/dist/runtime/components/fields/MapoFksField.vue +122 -0
  55. package/dist/runtime/components/fields/MapoFksField.vue.d.ts +16 -0
  56. package/dist/runtime/components/fields/MapoFksField.vue.d.ts.map +1 -0
  57. package/dist/runtime/components/fields/MapoMapField.vue +99 -0
  58. package/dist/runtime/components/fields/MapoMapField.vue.d.ts +22 -0
  59. package/dist/runtime/components/fields/MapoMapField.vue.d.ts.map +1 -0
  60. package/dist/runtime/components/fields/MapoMapFieldClient.vue +67 -0
  61. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts +20 -0
  62. package/dist/runtime/components/fields/MapoMapFieldClient.vue.d.ts.map +1 -0
  63. package/dist/runtime/components/fields/MapoMediaField.vue +91 -0
  64. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
  65. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
  66. package/dist/runtime/components/fields/MapoMediaM2mField.vue +110 -0
  67. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
  68. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
  69. package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
  70. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
  71. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
  72. package/dist/runtime/components/fields/MapoRepeater.vue +459 -0
  73. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts +16 -0
  74. package/dist/runtime/components/fields/MapoRepeater.vue.d.ts.map +1 -0
  75. package/dist/runtime/components/fields/MapoRepeaterItem.vue +346 -0
  76. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts +55 -0
  77. package/dist/runtime/components/fields/MapoRepeaterItem.vue.d.ts.map +1 -0
  78. package/dist/runtime/components/fields/MapoSeoPreview.vue +122 -0
  79. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts +22 -0
  80. package/dist/runtime/components/fields/MapoSeoPreview.vue.d.ts.map +1 -0
  81. package/dist/runtime/components/fields/MapoTimeField.vue +44 -0
  82. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts +17 -0
  83. package/dist/runtime/components/fields/MapoTimeField.vue.d.ts.map +1 -0
  84. package/dist/runtime/components/fields/MapoWygEditor.vue +252 -0
  85. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts +16 -0
  86. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -0
  87. package/dist/runtime/components/fields/nui/NuiCheckbox.vue +9 -0
  88. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts +4 -0
  89. package/dist/runtime/components/fields/nui/NuiCheckbox.vue.d.ts.map +1 -0
  90. package/dist/runtime/components/fields/nui/NuiInput.vue +9 -0
  91. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts +4 -0
  92. package/dist/runtime/components/fields/nui/NuiInput.vue.d.ts.map +1 -0
  93. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue +18 -0
  94. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts +4 -0
  95. package/dist/runtime/components/fields/nui/NuiSelectMenu.vue.d.ts.map +1 -0
  96. package/dist/runtime/components/fields/nui/NuiSlider.vue +9 -0
  97. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts +4 -0
  98. package/dist/runtime/components/fields/nui/NuiSlider.vue.d.ts.map +1 -0
  99. package/dist/runtime/components/fields/nui/NuiSwitch.vue +9 -0
  100. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts +4 -0
  101. package/dist/runtime/components/fields/nui/NuiSwitch.vue.d.ts.map +1 -0
  102. package/dist/runtime/components/fields/nui/NuiTextarea.vue +9 -0
  103. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts +4 -0
  104. package/dist/runtime/components/fields/nui/NuiTextarea.vue.d.ts.map +1 -0
  105. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts +13 -0
  106. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.d.ts.map +1 -0
  107. package/dist/runtime/components/fields/nui/useNuiFieldAttrs.js +19 -0
  108. package/dist/runtime/composables/defineFormField.d.ts +25 -0
  109. package/dist/runtime/composables/defineFormField.d.ts.map +1 -0
  110. package/dist/runtime/composables/defineFormField.js +27 -0
  111. package/dist/runtime/composables/useCurrentLang.d.ts +6 -0
  112. package/dist/runtime/composables/useCurrentLang.d.ts.map +1 -0
  113. package/dist/runtime/composables/useCurrentLang.js +9 -0
  114. package/dist/runtime/composables/useFocusMode.d.ts +40 -0
  115. package/dist/runtime/composables/useFocusMode.d.ts.map +1 -0
  116. package/dist/runtime/composables/useFocusMode.js +31 -0
  117. package/dist/runtime/composables/useFormDraft.d.ts +60 -0
  118. package/dist/runtime/composables/useFormDraft.d.ts.map +1 -0
  119. package/dist/runtime/composables/useFormDraft.js +52 -0
  120. package/dist/runtime/composables/useFormFromSchema.d.ts +21 -0
  121. package/dist/runtime/composables/useFormFromSchema.d.ts.map +1 -0
  122. package/dist/runtime/composables/useFormFromSchema.js +145 -0
  123. package/dist/runtime/composables/useMapoForm.d.ts +96 -0
  124. package/dist/runtime/composables/useMapoForm.d.ts.map +1 -0
  125. package/dist/runtime/composables/useMapoForm.js +325 -0
  126. package/dist/runtime/composables/useMediaManager.d.ts +17 -0
  127. package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
  128. package/dist/runtime/composables/useMediaManager.js +6 -0
  129. package/dist/runtime/composables/useProgressiveDisclosure.d.ts +44 -0
  130. package/dist/runtime/composables/useProgressiveDisclosure.d.ts.map +1 -0
  131. package/dist/runtime/composables/useProgressiveDisclosure.js +23 -0
  132. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts +24 -0
  133. package/dist/runtime/composables/useUnsavedChangesGuard.d.ts.map +1 -0
  134. package/dist/runtime/composables/useUnsavedChangesGuard.js +28 -0
  135. package/dist/runtime/devtools/DevtoolsFormsPage.vue +235 -0
  136. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts +4 -0
  137. package/dist/runtime/devtools/DevtoolsFormsPage.vue.d.ts.map +1 -0
  138. package/dist/runtime/plugins/form-registry.d.ts +18 -0
  139. package/dist/runtime/plugins/form-registry.d.ts.map +1 -0
  140. package/dist/runtime/plugins/form-registry.js +19 -0
  141. package/dist/runtime/registry/defaults.d.ts +3 -0
  142. package/dist/runtime/registry/defaults.d.ts.map +1 -0
  143. package/dist/runtime/registry/defaults.js +93 -0
  144. package/dist/runtime/types/descriptor.d.ts +373 -0
  145. package/dist/runtime/types/descriptor.d.ts.map +1 -0
  146. package/dist/runtime/types/descriptor.js +26 -0
  147. package/dist/runtime/types/index.d.ts +12 -0
  148. package/dist/runtime/types/index.d.ts.map +1 -0
  149. package/dist/runtime/types/index.js +6 -0
  150. package/dist/runtime/types/layout.d.ts +21 -0
  151. package/dist/runtime/types/layout.d.ts.map +1 -0
  152. package/dist/runtime/types/layout.js +0 -0
  153. package/dist/runtime/types/media.d.ts +43 -0
  154. package/dist/runtime/types/media.d.ts.map +1 -0
  155. package/dist/runtime/types/media.js +0 -0
  156. package/dist/runtime/types/registry.d.ts +62 -0
  157. package/dist/runtime/types/registry.d.ts.map +1 -0
  158. package/dist/runtime/types/registry.js +19 -0
  159. package/dist/runtime/utils/flattenFieldGroups.d.ts +50 -0
  160. package/dist/runtime/utils/flattenFieldGroups.d.ts.map +1 -0
  161. package/dist/runtime/utils/flattenFieldGroups.js +24 -0
  162. package/dist/types.d.mts +6 -10
  163. package/package.json +10 -8
  164. package/src/__tests__/article.schema.json +193 -0
  165. package/src/__tests__/camomillaRoundTrip.test.ts +73 -0
  166. package/src/__tests__/useFormFromSchema.test.ts +24 -0
  167. package/src/__tests__/useMapoForm.test.ts +5 -0
  168. package/src/index.ts +5 -0
  169. package/src/module.ts +34 -0
  170. package/src/runtime/components/MapoFocusPortal.vue +3 -1
  171. package/src/runtime/components/MapoFormTabs.vue +4 -1
  172. package/src/runtime/components/fields/MapoDateField.vue +3 -1
  173. package/src/runtime/components/fields/MapoDateTimeField.vue +3 -1
  174. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +153 -0
  175. package/src/runtime/components/fields/MapoFileField.vue +3 -1
  176. package/src/runtime/components/fields/MapoFksField.vue +7 -2
  177. package/src/runtime/components/fields/MapoMapField.vue +5 -2
  178. package/src/runtime/components/fields/MapoMediaField.vue +104 -0
  179. package/src/runtime/components/fields/MapoMediaM2mField.vue +130 -0
  180. package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
  181. package/src/runtime/components/fields/MapoRepeater.vue +33 -26
  182. package/src/runtime/components/fields/MapoRepeaterItem.vue +14 -8
  183. package/src/runtime/components/fields/MapoSeoPreview.vue +17 -13
  184. package/src/runtime/components/fields/MapoWygEditor.vue +47 -5
  185. package/src/runtime/composables/useFormFromSchema.ts +4 -1
  186. package/src/runtime/composables/useMapoForm.ts +4 -2
  187. package/src/runtime/composables/useMediaManager.ts +21 -0
  188. package/src/runtime/composables/useUnsavedChangesGuard.ts +3 -3
  189. package/src/runtime/registry/defaults.ts +9 -1
  190. package/src/runtime/types/descriptor.ts +22 -7
  191. package/src/runtime/types/index.ts +4 -0
  192. package/src/runtime/types/media.ts +44 -0
  193. package/dist/index.d.cts +0 -1
  194. package/dist/module.d.cts +0 -2
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, watch, watchEffect } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import type {
4
5
  AnyFieldDescriptor,
5
6
  FieldRegistry,
@@ -9,6 +10,8 @@ import { useMapoForm, injectMapoForm } from "../../composables/useMapoForm.js";
9
10
  import { useFocusMode } from "../../composables/useFocusMode.js";
10
11
  import MapoFormField from "../MapoFormField.vue";
11
12
 
13
+ const { t } = useI18n();
14
+
12
15
  const props = defineProps<{
13
16
  /** Stable identity-based UID assigned by the parent repeater. */
14
17
  uid?: string;
@@ -173,7 +176,10 @@ function enterFocusMode() {
173
176
  registry: props.registry,
174
177
  languages: props.languages,
175
178
  currentLang: props.currentLang,
176
- breadcrumb: [props.repeaterDescriptor?.label ?? "Repeater", label.value],
179
+ breadcrumb: [
180
+ props.repeaterDescriptor?.label ?? t("mapo.repeater.repeater"),
181
+ label.value,
182
+ ],
177
183
  });
178
184
  }
179
185
  </script>
@@ -246,7 +252,7 @@ function enterFocusMode() {
246
252
  color="error"
247
253
  icon="i-lucide-trash-2"
248
254
  :disabled="readonly"
249
- title="Delete"
255
+ :title="t('mapo.delete')"
250
256
  @click.stop="$emit('delete')"
251
257
  />
252
258
  <UIcon name="i-lucide-chevron-down" class="h-4 w-4 text-gray-400" />
@@ -307,7 +313,7 @@ function enterFocusMode() {
307
313
  :max="totalItems"
308
314
  size="xs"
309
315
  class="w-14 text-center"
310
- title="Position"
316
+ :title="t('mapo.repeater.position')"
311
317
  @change="
312
318
  $emit(
313
319
  'move-to',
@@ -329,7 +335,7 @@ function enterFocusMode() {
329
335
  variant="ghost"
330
336
  color="neutral"
331
337
  icon="i-lucide-maximize-2"
332
- title="Edit full screen"
338
+ :title="t('mapo.repeater.editFullScreen')"
333
339
  @click="enterFocusMode"
334
340
  />
335
341
 
@@ -339,7 +345,7 @@ function enterFocusMode() {
339
345
  color="neutral"
340
346
  icon="i-lucide-chevron-up"
341
347
  :disabled="readonly"
342
- title="Move up"
348
+ :title="t('mapo.repeater.moveUp')"
343
349
  @click="$emit('move-up')"
344
350
  />
345
351
  <UButton
@@ -348,7 +354,7 @@ function enterFocusMode() {
348
354
  color="neutral"
349
355
  icon="i-lucide-chevron-down"
350
356
  :disabled="readonly"
351
- title="Move down"
357
+ :title="t('mapo.repeater.moveDown')"
352
358
  @click="$emit('move-down')"
353
359
  />
354
360
 
@@ -359,7 +365,7 @@ function enterFocusMode() {
359
365
  color="neutral"
360
366
  icon="i-lucide-copy"
361
367
  :disabled="readonly"
362
- title="Duplicate"
368
+ :title="t('mapo.duplicate')"
363
369
  @click="$emit('duplicate')"
364
370
  />
365
371
 
@@ -369,7 +375,7 @@ function enterFocusMode() {
369
375
  color="error"
370
376
  icon="i-lucide-trash-2"
371
377
  :disabled="readonly"
372
- title="Delete"
378
+ :title="t('mapo.delete')"
373
379
  @click="$emit('delete')"
374
380
  />
375
381
 
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { useRequestURL } from "#app";
4
5
  import type { SeoDescriptor } from "../../types/index.js";
5
6
 
@@ -10,6 +11,8 @@ interface SeoValue {
10
11
  permalink?: string;
11
12
  }
12
13
 
14
+ const { t } = useI18n();
15
+
13
16
  const props = defineProps<{
14
17
  modelValue: unknown;
15
18
  descriptor: SeoDescriptor;
@@ -58,7 +61,9 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
58
61
  <div class="space-y-3">
59
62
  <div>
60
63
  <div class="mb-1 flex items-center justify-between">
61
- <label class="text-sm font-medium text-gray-700">URL</label>
64
+ <label class="text-sm font-medium text-gray-700">{{
65
+ t("mapo.url")
66
+ }}</label>
62
67
  </div>
63
68
  <UInput
64
69
  :model-value="value.permalink"
@@ -72,14 +77,16 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
72
77
 
73
78
  <div>
74
79
  <div class="mb-1 flex items-center justify-between">
75
- <label class="text-sm font-medium text-gray-700">SEO title</label>
80
+ <label class="text-sm font-medium text-gray-700">{{
81
+ t("mapo.seoPreview.seoTitle")
82
+ }}</label>
76
83
  <span class="text-xs" :class="titleColor">{{ titleLen }} / 60</span>
77
84
  </div>
78
85
  <UInput
79
86
  :model-value="value.title"
80
87
  :readonly="readonly"
81
88
  :disabled="disabled"
82
- placeholder="Page title..."
89
+ :placeholder="t('mapo.seoPreview.titlePlaceholder')"
83
90
  class="w-full"
84
91
  @update:model-value="update('title', $event as string)"
85
92
  />
@@ -87,9 +94,9 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
87
94
 
88
95
  <div>
89
96
  <div class="mb-1 flex items-center justify-between">
90
- <label class="text-sm font-medium text-gray-700"
91
- >Meta description</label
92
- >
97
+ <label class="text-sm font-medium text-gray-700">{{
98
+ t("mapo.seoPreview.metaDescription")
99
+ }}</label>
93
100
  <span class="text-xs" :class="descColor">{{ descLen }} / 155</span>
94
101
  </div>
95
102
  <UTextarea
@@ -97,7 +104,7 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
97
104
  :rows="3"
98
105
  :readonly="readonly"
99
106
  :disabled="disabled"
100
- placeholder="Page description..."
107
+ :placeholder="t('mapo.seoPreview.descriptionPlaceholder')"
101
108
  class="w-full"
102
109
  @update:model-value="update('description', $event as string)"
103
110
  />
@@ -107,20 +114,17 @@ const displayUrl = computed(() => value.value.permalink || requestUrl.host);
107
114
  <!-- SERP preview -->
108
115
  <div class="rounded border border-gray-200 bg-gray-50 p-4">
109
116
  <p class="mb-1 text-xs font-medium uppercase tracking-wide text-gray-400">
110
- Google preview
117
+ {{ t("mapo.seoPreview.googlePreview") }}
111
118
  </p>
112
119
  <div class="max-w-lg">
113
120
  <p class="truncate text-sm text-green-700">
114
121
  {{ displayUrl }}
115
122
  </p>
116
123
  <p class="truncate text-lg text-blue-700 underline">
117
- {{ value.title || "Page title" }}
124
+ {{ value.title || t("mapo.seoPreview.defaultTitle") }}
118
125
  </p>
119
126
  <p class="mt-1 line-clamp-2 text-sm text-gray-600">
120
- {{
121
- value.description ||
122
- "Page description that will appear in Google search results."
123
- }}
127
+ {{ value.description || t("mapo.seoPreview.defaultDescription") }}
124
128
  </p>
125
129
  </div>
126
130
  </div>
@@ -1,10 +1,12 @@
1
1
  <script setup lang="ts">
2
- import { watch, onBeforeUnmount } from "vue";
2
+ import { ref, watch, onBeforeUnmount, resolveComponent } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { useEditor, EditorContent } from "@tiptap/vue-3";
4
5
  import type { AnyExtension } from "@tiptap/vue-3";
5
6
  import StarterKit from "@tiptap/starter-kit";
6
7
  import Link from "@tiptap/extension-link";
7
8
  import Underline from "@tiptap/extension-underline";
9
+ import Image from "@tiptap/extension-image";
8
10
  import type { EditorDescriptor } from "../../types/index.js";
9
11
 
10
12
  const props = defineProps<{
@@ -17,6 +19,8 @@ const props = defineProps<{
17
19
 
18
20
  const emit = defineEmits<{ "update:modelValue": [value: string] }>();
19
21
 
22
+ const { t } = useI18n();
23
+
20
24
  const extraExtensions =
21
25
  (props.descriptor.attrs?.extensions as AnyExtension[] | undefined) ?? [];
22
26
 
@@ -85,6 +89,7 @@ const editor = useEditor({
85
89
  validate: (href: string) => isSafeUrl(href),
86
90
  }),
87
91
  Underline,
92
+ Image.configure({ inline: false, allowBase64: false }),
88
93
  ...extraExtensions,
89
94
  ] as AnyExtension[],
90
95
  onUpdate: ({ editor: e }) => {
@@ -112,6 +117,28 @@ watch(
112
117
  );
113
118
 
114
119
  onBeforeUnmount(() => editor.value?.destroy());
120
+
121
+ // ─── Image insertion ──────────────────────────────────────────────────────
122
+ // MapoMediaManagerDialog is globally registered by @mapomodule/uikit.
123
+ // resolveComponent() works at runtime because it is registered before this
124
+ // component renders. We avoid a direct import to break the form → uikit cycle.
125
+ const imagePickerOpen = ref(false);
126
+ const MediaManagerDialog = resolveComponent("MapoMediaManagerDialog");
127
+ const hasMediaManager = typeof MediaManagerDialog !== "string";
128
+
129
+ function onImageConfirm(
130
+ selection:
131
+ | { file: string; alt_text?: string }
132
+ | Array<{ file: string; alt_text?: string }>,
133
+ ) {
134
+ const media = Array.isArray(selection) ? selection[0] : selection;
135
+ if (!media || !editor.value) return;
136
+ editor.value
137
+ .chain()
138
+ .focus()
139
+ .setImage({ src: media.file, alt: media.alt_text ?? "" })
140
+ .run();
141
+ }
115
142
  </script>
116
143
 
117
144
  <template>
@@ -223,9 +250,13 @@ onBeforeUnmount(() => editor.value?.destroy());
223
250
 
224
251
  <USeparator orientation="vertical" class="mx-1 h-5" />
225
252
 
226
- <!-- Insert Image — enabled in Phase 6 when Media Manager is available -->
253
+ <!-- Insert Image via Media Manager (available when @mapomodule/uikit is installed) -->
227
254
  <UTooltip
228
- text="Insert image (requires Media Manager — Phase 6)"
255
+ :text="
256
+ hasMediaManager
257
+ ? t('mapo.wygEditor.insertImage')
258
+ : t('mapo.wygEditor.requiresUikit')
259
+ "
229
260
  :delay-open="300"
230
261
  >
231
262
  <UButton
@@ -233,12 +264,13 @@ onBeforeUnmount(() => editor.value?.destroy());
233
264
  variant="ghost"
234
265
  color="neutral"
235
266
  icon="i-lucide-image"
236
- disabled
267
+ :disabled="readonly || disabled || !hasMediaManager"
268
+ @click="imagePickerOpen = true"
237
269
  />
238
270
  </UTooltip>
239
271
  </div>
240
272
 
241
- <!-- Contenuto editor -->
273
+ <!-- Editor content -->
242
274
  <EditorContent
243
275
  :editor="editor"
244
276
  class="prose prose-sm max-w-none px-3 py-2 focus-within:outline-none"
@@ -253,4 +285,14 @@ onBeforeUnmount(() => editor.value?.destroy());
253
285
  {{ err }}
254
286
  </p>
255
287
  </div>
288
+
289
+ <!-- Image picker dialog — only rendered when MapoMediaManagerDialog is globally available -->
290
+ <component
291
+ :is="MediaManagerDialog"
292
+ v-if="hasMediaManager"
293
+ v-model="imagePickerOpen"
294
+ selection-mode="single"
295
+ mime="image/*"
296
+ @confirm="onImageConfirm"
297
+ />
256
298
  </template>
@@ -11,9 +11,10 @@ function mapType(schema: JSONSchema, key: string): FieldType {
11
11
  const t = Array.isArray(schema.type)
12
12
  ? schema.type.find((v) => v !== "null")
13
13
  : schema.type;
14
- const fmt = schema.format as string | undefined;
14
+ const fmt = schema.format;
15
15
 
16
16
  if (t === "string") {
17
+ if (fmt === "textarea") return "textarea";
17
18
  if (fmt === "date") return "date";
18
19
  if (fmt === "date-time") return "datetime";
19
20
  if (fmt === "time") return "time";
@@ -179,6 +180,8 @@ function propertyToDescriptor(
179
180
  type,
180
181
  label: schema.title ?? undefined,
181
182
  required: requiredKeys.has(key),
183
+ ...(schema.readOnly ? { readonly: true } : {}),
184
+ ...(schema.translatable ? { translatable: true } : {}),
182
185
  ...(visible ? { visible } : {}),
183
186
  ...(Object.keys(attrs).length ? { attrs } : {}),
184
187
  };
@@ -19,6 +19,7 @@ import type { AnyFieldDescriptor, FieldRegistry } from "../types/index.js";
19
19
  import { resolveFieldAccessor } from "../types/index.js";
20
20
  import { useCurrentLang } from "./useCurrentLang.js";
21
21
  import { useSnackStore } from "@mapomodule/store/runtime/stores/snack";
22
+ import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
22
23
  import { useAuthStore } from "@mapomodule/store/runtime/stores/auth";
23
24
 
24
25
  /** Options accepted by `useMapoForm()`. */
@@ -121,6 +122,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
121
122
 
122
123
  const currentLang = useCurrentLang(currentLangProp);
123
124
  const snack = useSnackStore();
125
+ const t = useMapoT();
124
126
  const authStore = useAuthStore();
125
127
  const backup = ref(safeClone(model.value)) as Ref<T>;
126
128
  const clientErrors = ref<Record<string, string>>({});
@@ -318,7 +320,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
318
320
  ): string | null {
319
321
  if (!descriptor.required) return null;
320
322
  if (val === null || val === undefined || val === "")
321
- return "This field is required.";
323
+ return t("mapo.form.required");
322
324
  return null;
323
325
  }
324
326
 
@@ -379,7 +381,7 @@ export function useMapoForm<T extends object>(options: UseMapoFormOptions<T>) {
379
381
  submitted.value = true;
380
382
  const { valid } = validateClient();
381
383
  if (!valid) {
382
- snack.show("Please fix the errors before saving.", "error");
384
+ snack.show(t("mapo.fixErrors"), "error");
383
385
  return;
384
386
  }
385
387
  isLoading.value = true;
@@ -0,0 +1,21 @@
1
+ import { resolveComponent } from "vue";
2
+
3
+ /**
4
+ * Resolves the Media Manager picker dialog at runtime.
5
+ *
6
+ * The media field types ship with `@mapomodule/form`, but the Media Manager
7
+ * itself lives in `@mapomodule/uikit`. Resolving the dialog by name instead of
8
+ * importing it keeps `form` free of a static dependency on `uikit` (which
9
+ * depends on `form`, so a static import would be a cycle) and lets `form` stay
10
+ * usable standalone — the fields then render an explicit notice instead of a
11
+ * broken picker.
12
+ *
13
+ * Must be called from `setup()`.
14
+ */
15
+ export function useMediaManager() {
16
+ const MediaDialog = resolveComponent("MapoMediaManagerDialog");
17
+ // resolveComponent() returns the name unchanged when nothing is registered.
18
+ const available = typeof MediaDialog !== "string";
19
+
20
+ return { MediaDialog, available };
21
+ }
@@ -1,5 +1,6 @@
1
1
  import { onBeforeUnmount, type Ref } from "vue";
2
- import { onBeforeRouteLeave } from "vue-router";
2
+ import { onBeforeRouteLeave } from "#app";
3
+ import { useMapoT } from "@mapomodule/i18n/runtime/composables/useMapoT";
3
4
 
4
5
  /** Optional configuration for the unsaved-changes navigation guard. */
5
6
  export interface UnsavedChangesGuardOptions {
@@ -27,8 +28,7 @@ export function useUnsavedChangesGuard(
27
28
  isDirty: Ref<boolean>,
28
29
  options: UnsavedChangesGuardOptions = {},
29
30
  ) {
30
- const message =
31
- options.message ?? "You have unsaved changes. Do you really want to leave?";
31
+ const message = options.message ?? useMapoT()("mapo.confirmLeaveChanges");
32
32
  const canUseWindow = typeof window !== "undefined";
33
33
 
34
34
  const isEnabled = (): boolean => {
@@ -31,7 +31,15 @@ export const defaultRegistry: FieldRegistry = {
31
31
  editor: () => import("../components/fields/MapoWygEditor.vue"),
32
32
  map: () => import("../components/fields/MapoMapField.vue"),
33
33
  repeater: () => import("../components/fields/MapoRepeater.vue"),
34
- // media, media-m2m, enhanced-media: added in Phase 6
34
+
35
+ // ─── Media pickers ────────────────────────────────────────────────────────
36
+ // The fields live here with every other field type; the Media Manager they
37
+ // drive ships with @mapomodule/uikit and is resolved at runtime
38
+ // (see useMediaManager). Without uikit they render an explicit notice.
39
+ media: () => import("../components/fields/MapoMediaField.vue"),
40
+ "media-m2m": () => import("../components/fields/MapoMediaM2mField.vue"),
41
+ "enhanced-media": () =>
42
+ import("../components/fields/MapoEnhancedMediaField.vue"),
35
43
  },
36
44
 
37
45
  attrs: {
@@ -344,15 +344,28 @@ export interface RepeaterDescriptor<
344
344
  };
345
345
  }
346
346
 
347
- /** Descriptor for media/file manager fields. */
347
+ /** Descriptor for a single-media picker field (`type: 'media'`). */
348
348
  export interface MediaDescriptor<
349
349
  T = Record<string, unknown>,
350
350
  > extends FieldBase<T> {
351
- type:
352
- | `${KnownFieldType.Media}`
353
- | `${KnownFieldType.MediaM2m}`
354
- | `${KnownFieldType.EnhancedMedia}`;
355
- attrs?: { mime?: string; multiple?: boolean };
351
+ type: `${KnownFieldType.Media}`;
352
+ attrs?: { mime?: string };
353
+ }
354
+
355
+ /** Descriptor for a multi-media picker field (`type: 'media-m2m'`). */
356
+ export interface MediaM2mDescriptor<
357
+ T = Record<string, unknown>,
358
+ > extends FieldBase<T> {
359
+ type: `${KnownFieldType.MediaM2m}`;
360
+ attrs?: { mime?: string; maxItems?: number };
361
+ }
362
+
363
+ /** Descriptor for a media picker with inline alt/caption metadata (`type: 'enhanced-media'`). */
364
+ export interface EnhancedMediaDescriptor<
365
+ T = Record<string, unknown>,
366
+ > extends FieldBase<T> {
367
+ type: `${KnownFieldType.EnhancedMedia}`;
368
+ attrs?: { mime?: string };
356
369
  }
357
370
 
358
371
  /**
@@ -395,7 +408,9 @@ export type FieldDescriptor<T = Record<string, unknown>> =
395
408
  | SeoDescriptor<T>
396
409
  | MapDescriptor<T>
397
410
  | RepeaterDescriptor<T>
398
- | MediaDescriptor<T>;
411
+ | MediaDescriptor<T>
412
+ | MediaM2mDescriptor<T>
413
+ | EnhancedMediaDescriptor<T>;
399
414
 
400
415
  /**
401
416
  * {@link FieldDescriptor} plus the {@link CustomDescriptor} escape hatch.
@@ -22,9 +22,13 @@ export type {
22
22
  MapDescriptor,
23
23
  RepeaterDescriptor,
24
24
  MediaDescriptor,
25
+ MediaM2mDescriptor,
26
+ EnhancedMediaDescriptor,
25
27
  CustomDescriptor,
26
28
  } from "./descriptor.js";
27
29
 
30
+ export type { MediaLink, MediaItem, EnhancedMediaValue } from "./media.js";
31
+
28
32
  export type {
29
33
  FieldComponentEntry,
30
34
  FieldRegistry,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Media **value** types — the shape that lands in the form model for the
3
+ * `media`, `media-m2m`, and `enhanced-media` field types.
4
+ *
5
+ * These live in `@mapomodule/form` (next to their descriptors) because they are
6
+ * a form contract, not a Media Manager implementation detail. `@mapomodule/uikit`
7
+ * re-exports them from `runtime/types/media.ts` alongside the store/adapter types
8
+ * so the Media Manager keeps a single import surface.
9
+ */
10
+
11
+ /** A model instance referencing a media (Camomilla returns these as `links`). */
12
+ export interface MediaLink {
13
+ model: string;
14
+ id: number;
15
+ name: string;
16
+ }
17
+
18
+ export interface MediaItem {
19
+ id: number;
20
+ file: string;
21
+ mime_type: string;
22
+ title: string;
23
+ alt_text: string;
24
+ description: string;
25
+ size: number;
26
+ created: string;
27
+ modified: string;
28
+ folder?: number | null;
29
+ name?: string;
30
+ /** Model instances that reference this media — shown in the editor info panel. */
31
+ links?: MediaLink[];
32
+ translations?: Record<
33
+ string,
34
+ { title?: string; alt_text?: string; description?: string }
35
+ >;
36
+ }
37
+
38
+ /** Value of an `enhanced-media` field: a media plus inline caption metadata. */
39
+ export interface EnhancedMediaValue {
40
+ media: MediaItem | null;
41
+ alt?: string;
42
+ caption?: string;
43
+ [key: string]: unknown;
44
+ }
package/dist/index.d.cts DELETED
@@ -1 +0,0 @@
1
- export * from "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/index.js";
package/dist/module.d.cts DELETED
@@ -1,2 +0,0 @@
1
- export * from "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/module.js";
2
- export { default } from "/Volumes/Development/Lotrek/Projects/mapo-new/packages/@mapomodule/form/src/module.js";