@mapomodule/form 2.0.0-beta.3 → 2.0.0-beta.5

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 (46) hide show
  1. package/README.md +2 -1
  2. package/dist/index.d.mts +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/module.d.ts.map +1 -1
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +17 -0
  8. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue +128 -0
  9. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts +16 -0
  10. package/dist/runtime/components/fields/MapoEnhancedMediaField.vue.d.ts.map +1 -0
  11. package/dist/runtime/components/fields/MapoMediaField.vue +89 -0
  12. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts +16 -0
  13. package/dist/runtime/components/fields/MapoMediaField.vue.d.ts.map +1 -0
  14. package/dist/runtime/components/fields/MapoMediaM2mField.vue +106 -0
  15. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts +16 -0
  16. package/dist/runtime/components/fields/MapoMediaM2mField.vue.d.ts.map +1 -0
  17. package/dist/runtime/components/fields/MapoMediaUnavailable.vue +15 -0
  18. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts +4 -0
  19. package/dist/runtime/components/fields/MapoMediaUnavailable.vue.d.ts.map +1 -0
  20. package/dist/runtime/components/fields/MapoWygEditor.vue +26 -5
  21. package/dist/runtime/components/fields/MapoWygEditor.vue.d.ts.map +1 -1
  22. package/dist/runtime/composables/useMediaManager.d.ts +17 -0
  23. package/dist/runtime/composables/useMediaManager.d.ts.map +1 -0
  24. package/dist/runtime/composables/useMediaManager.js +6 -0
  25. package/dist/runtime/registry/defaults.d.ts.map +1 -1
  26. package/dist/runtime/registry/defaults.js +8 -2
  27. package/dist/runtime/types/descriptor.d.ts +18 -4
  28. package/dist/runtime/types/descriptor.d.ts.map +1 -1
  29. package/dist/runtime/types/index.d.ts +2 -1
  30. package/dist/runtime/types/index.d.ts.map +1 -1
  31. package/dist/runtime/types/media.d.ts +43 -0
  32. package/dist/runtime/types/media.d.ts.map +1 -0
  33. package/dist/runtime/types/media.js +0 -0
  34. package/package.json +4 -3
  35. package/src/index.ts +5 -0
  36. package/src/module.ts +25 -0
  37. package/src/runtime/components/fields/MapoEnhancedMediaField.vue +150 -0
  38. package/src/runtime/components/fields/MapoMediaField.vue +101 -0
  39. package/src/runtime/components/fields/MapoMediaM2mField.vue +123 -0
  40. package/src/runtime/components/fields/MapoMediaUnavailable.vue +19 -0
  41. package/src/runtime/components/fields/MapoWygEditor.vue +40 -5
  42. package/src/runtime/composables/useMediaManager.ts +21 -0
  43. package/src/runtime/registry/defaults.ts +9 -1
  44. package/src/runtime/types/descriptor.ts +22 -7
  45. package/src/runtime/types/index.ts +4 -0
  46. package/src/runtime/types/media.ts +44 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@ Form engine for Mapo v2 — descriptor-driven, typesafe, headless-ready admin fo
5
5
  ## Features
6
6
 
7
7
  - **Descriptor-driven**: define fields as `FieldDescriptor[]` — layout, validation, i18n in one object
8
- - **All field types built-in**: text, select, date/time, FK/M2M autocomplete, WYSIWYG editor, SEO preview, map, repeater
8
+ - **All field types built-in**: text, select, date/time, FK/M2M autocomplete, WYSIWYG editor, SEO preview, map, repeater, media pickers
9
9
  - **Headless composable**: `useMapoForm()` for fully custom layouts; `<MapoForm>` for the default shell
10
10
  - **Registry auto-injection**: `<MapoForm>` and `<MapoDetail>` resolve `$mapoFormRegistry` automatically — no boilerplate
11
11
  - **Typed extension API**: `defineFormField(type, entry, opts)` to register custom fields with collision detection
@@ -16,6 +16,7 @@ Form engine for Mapo v2 — descriptor-driven, typesafe, headless-ready admin fo
16
16
  - **Consistent repeater items**: `MapoRepeater` drives each item through `useMapoForm`, so nested-path keys, `translatable`, accessors, `descriptor.is` overrides and **client validation** behave identically inside a repeater
17
17
  - **Draft persistence**: `draftKey` option on `useMapoForm()` auto-saves to localStorage every 2 s; `checkDraft()` / `draftBanner` offer restore/discard; clears on successful `submit()`
18
18
  - **Fail-soft**: unknown field types render a non-destructive placeholder, never crash the form
19
+ - **Media pickers**: `media`, `media-m2m` and `enhanced-media` field types are built in; they drive the Media Manager from [`@mapomodule/uikit`](../uikit/README.md), resolved at runtime — install it to enable them
19
20
 
20
21
  ## Install
21
22
 
package/dist/index.d.mts CHANGED
@@ -5,5 +5,5 @@ export { FocusTarget, useFocusMode } from '../dist/runtime/composables/useFocusM
5
5
  export { UnsavedChangesGuardOptions, useUnsavedChangesGuard } from '../dist/runtime/composables/useUnsavedChangesGuard.js';
6
6
  export { greaterThan, isEmpty, isNoneOf, isNotEmpty, isOneOf, lessThan, matches, matchesField, when, whenAny, whenNot } from '../dist/runtime/composables/useProgressiveDisclosure.js';
7
7
  export { FieldGroupDescriptor, flattenFieldGroups } from '../dist/runtime/utils/flattenFieldGroups.js';
8
- export { AnyFieldDescriptor, BooleanDescriptor, ColorDescriptor, CustomDescriptor, DateDescriptor, EditorDescriptor, FieldAccessor, FieldComponentEntry, FieldDescriptor, FieldRegistry, FieldType, FileDescriptor, FksDescriptor, KnownFieldType, MapDescriptor, MediaDescriptor, NumberDescriptor, PartialFieldRegistry, RepeaterDescriptor, SelectDescriptor, SeoDescriptor, TextDescriptor } from '../dist/runtime/types/index.js';
8
+ export { AnyFieldDescriptor, BooleanDescriptor, ColorDescriptor, CustomDescriptor, DateDescriptor, EditorDescriptor, EnhancedMediaDescriptor, EnhancedMediaValue, FieldAccessor, FieldComponentEntry, FieldDescriptor, FieldRegistry, FieldType, FileDescriptor, FksDescriptor, KnownFieldType, MapDescriptor, MediaDescriptor, MediaItem, MediaLink, MediaM2mDescriptor, NumberDescriptor, PartialFieldRegistry, RepeaterDescriptor, SelectDescriptor, SeoDescriptor, TextDescriptor } from '../dist/runtime/types/index.js';
9
9
  import '@nuxt/schema';
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ export { when, whenAny, whenNot, matchesField, isOneOf, isNoneOf, isNotEmpty, is
11
11
  export { flattenFieldGroups } from "./runtime/utils/flattenFieldGroups.js";
12
12
  export type { FieldGroupDescriptor } from "./runtime/utils/flattenFieldGroups.js";
13
13
  export { KnownFieldType } from "./runtime/types/index.js";
14
- export type { FieldType, FieldAccessor, FieldDescriptor, AnyFieldDescriptor, TextDescriptor, NumberDescriptor, BooleanDescriptor, SelectDescriptor, FksDescriptor, DateDescriptor, ColorDescriptor, FileDescriptor, EditorDescriptor, SeoDescriptor, MapDescriptor, RepeaterDescriptor, MediaDescriptor, CustomDescriptor, FieldComponentEntry, FieldRegistry, PartialFieldRegistry, } from "./runtime/types/index.js";
14
+ export type { FieldType, FieldAccessor, FieldDescriptor, AnyFieldDescriptor, TextDescriptor, NumberDescriptor, BooleanDescriptor, SelectDescriptor, FksDescriptor, DateDescriptor, ColorDescriptor, FileDescriptor, EditorDescriptor, SeoDescriptor, MapDescriptor, RepeaterDescriptor, MediaDescriptor, MediaM2mDescriptor, EnhancedMediaDescriptor, CustomDescriptor, MediaLink, MediaItem, EnhancedMediaValue, FieldComponentEntry, FieldRegistry, PartialFieldRegistry, } from "./runtime/types/index.js";
15
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAC/E,YAAY,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAC3F,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,YAAY,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iDAAiD,CAAC;AACzF,YAAY,EAAE,0BAA0B,EAAE,MAAM,iDAAiD,CAAC;AAElG,OAAO,EACL,IAAI,EACJ,OAAO,EACP,OAAO,EACP,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,UAAU,EACV,OAAO,EACP,WAAW,EACX,QAAQ,EACR,OAAO,GACR,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,YAAY,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,YAAY,EACV,SAAS,EACT,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,GACrB,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAC/E,YAAY,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAC3F,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AACrE,YAAY,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iDAAiD,CAAC;AACzF,YAAY,EAAE,0BAA0B,EAAE,MAAM,iDAAiD,CAAC;AAElG,OAAO,EACL,IAAI,EACJ,OAAO,EACP,OAAO,EACP,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,UAAU,EACV,OAAO,EACP,WAAW,EACX,QAAQ,EACR,OAAO,GACR,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAC3E,YAAY,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,YAAY,EACV,SAAS,EACT,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,GACrB,MAAM,0BAA0B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;;AAED,wBAkMyC"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;;AAED,wBA2NyC"}
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapomodule/form",
3
3
  "configKey": "mapoForm",
4
- "version": "2.0.0-beta.3",
4
+ "version": "2.0.0-beta.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -11,6 +11,11 @@ const module$1 = defineNuxtModule({
11
11
  },
12
12
  async setup(options, nuxt) {
13
13
  const resolver = createResolver(import.meta.url);
14
+ nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
15
+ nuxt.options.imports.transform ??= {};
16
+ (nuxt.options.imports.transform.include ??= []).push(
17
+ /@mapomodule[\\/]form[\\/]/
18
+ );
14
19
  nuxt.options.runtimeConfig.public.mapoForm = {
15
20
  groups: { expanded: options.groups?.expanded ?? true },
16
21
  debounce: options.debounce ?? 300,
@@ -69,6 +74,18 @@ export {};
69
74
  filePath: resolver.resolve(`./runtime/components/${name}.vue`)
70
75
  });
71
76
  }
77
+ const mediaFieldComponents = [
78
+ "MapoMediaField",
79
+ "MapoMediaM2mField",
80
+ "MapoEnhancedMediaField",
81
+ "MapoMediaUnavailable"
82
+ ];
83
+ for (const name of mediaFieldComponents) {
84
+ addComponent({
85
+ name,
86
+ filePath: resolver.resolve(`./runtime/components/fields/${name}.vue`)
87
+ });
88
+ }
72
89
  addImports([
73
90
  {
74
91
  name: "useMapoForm",
@@ -0,0 +1,128 @@
1
+ <script setup>
2
+ import { ref, computed } from "vue";
3
+ import { useMediaManager } from "../../composables/useMediaManager.js";
4
+ const props = defineProps({
5
+ modelValue: { type: null, required: true },
6
+ descriptor: { type: Object, required: true },
7
+ errors: { type: Array, required: false },
8
+ readonly: { type: Boolean, required: false },
9
+ disabled: { type: Boolean, required: false }
10
+ });
11
+ const emit = defineEmits(["update:modelValue"]);
12
+ const dialogOpen = ref(false);
13
+ const { MediaDialog, available } = useMediaManager();
14
+ const current = computed(
15
+ () => props.modelValue?.media ?? null
16
+ );
17
+ const altValue = computed({
18
+ get: () => props.modelValue?.alt ?? "",
19
+ set: (v) => emit("update:modelValue", {
20
+ ...props.modelValue ?? { media: null },
21
+ alt: v
22
+ })
23
+ });
24
+ const captionValue = computed({
25
+ get: () => props.modelValue?.caption ?? "",
26
+ set: (v) => emit("update:modelValue", {
27
+ ...props.modelValue ?? { media: null },
28
+ caption: v
29
+ })
30
+ });
31
+ const mime = computed(() => props.descriptor.attrs?.mime);
32
+ function onConfirm(selection) {
33
+ const media = Array.isArray(selection) ? selection[0] ?? null : selection;
34
+ emit("update:modelValue", {
35
+ ...props.modelValue ?? {},
36
+ media,
37
+ alt: props.modelValue?.alt ?? media?.alt_text ?? "",
38
+ caption: props.modelValue?.caption ?? ""
39
+ });
40
+ }
41
+ function clear() {
42
+ emit("update:modelValue", null);
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <div class="mapo-enhanced-media-field space-y-3">
48
+ <MapoMediaUnavailable v-if="!available" />
49
+
50
+ <template v-else>
51
+ <div class="flex gap-3">
52
+ <!-- Preview -->
53
+ <div class="shrink-0">
54
+ <div
55
+ class="flex size-24 cursor-pointer items-center justify-center overflow-hidden rounded-lg border-2 border-dashed transition-colors"
56
+ :class="
57
+ current ? 'border-default' : 'border-accented hover:border-primary'
58
+ "
59
+ @click="!readonly && !disabled && (dialogOpen = true)"
60
+ >
61
+ <MapoMediaPreview v-if="current" :media="current" size="md" />
62
+ <div v-else class="flex flex-col items-center gap-1 text-dimmed">
63
+ <UIcon name="i-lucide-image-plus" class="size-7" />
64
+ <span class="text-[10px]">Select</span>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <!-- Metadata fields -->
70
+ <div class="flex-1 space-y-2">
71
+ <div v-if="current" class="flex items-start justify-between gap-2">
72
+ <p class="truncate text-xs text-muted">
73
+ {{ current.title || current.file?.split("/").pop() }}
74
+ </p>
75
+ <div class="flex gap-1">
76
+ <UButton
77
+ v-if="!readonly && !disabled"
78
+ icon="i-lucide-refresh-cw"
79
+ size="xs"
80
+ variant="ghost"
81
+ color="neutral"
82
+ title="Change"
83
+ @click="dialogOpen = true"
84
+ />
85
+ <UButton
86
+ v-if="!readonly && !disabled"
87
+ icon="i-lucide-x"
88
+ size="xs"
89
+ variant="ghost"
90
+ color="neutral"
91
+ title="Remove"
92
+ @click="clear"
93
+ />
94
+ </div>
95
+ </div>
96
+
97
+ <UInput
98
+ v-model="altValue"
99
+ size="sm"
100
+ placeholder="Alt text"
101
+ :disabled="readonly || disabled || !current"
102
+ />
103
+ <UInput
104
+ v-model="captionValue"
105
+ size="sm"
106
+ placeholder="Caption"
107
+ :disabled="readonly || disabled || !current"
108
+ />
109
+ </div>
110
+ </div>
111
+
112
+ <!-- Dialog -->
113
+ <component
114
+ :is="MediaDialog"
115
+ v-model="dialogOpen"
116
+ selection-mode="single"
117
+ :mime="mime"
118
+ :selected="current"
119
+ @confirm="onConfirm"
120
+ />
121
+ </template>
122
+
123
+ <!-- Errors -->
124
+ <p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
125
+ {{ err }}
126
+ </p>
127
+ </div>
128
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { EnhancedMediaDescriptor, EnhancedMediaValue } from "../../types/index.js";
2
+ type __VLS_Props = {
3
+ modelValue: EnhancedMediaValue | null | undefined;
4
+ descriptor: EnhancedMediaDescriptor;
5
+ errors?: string[];
6
+ readonly?: boolean;
7
+ disabled?: boolean;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:modelValue": (value: EnhancedMediaValue | null) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:modelValue"?: ((value: EnhancedMediaValue | null) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
16
+ //# sourceMappingURL=MapoEnhancedMediaField.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoEnhancedMediaField.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoEnhancedMediaField.vue"],"names":[],"mappings":"AA0JA,OAAO,KAAK,EACV,uBAAuB,EACvB,kBAAkB,EAEnB,MAAM,sBAAsB,CAAC;AAG9B,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,UAAU,EAAE,uBAAuB,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA2UF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,89 @@
1
+ <script setup>
2
+ import { ref, computed } from "vue";
3
+ import { useMediaManager } from "../../composables/useMediaManager.js";
4
+ const props = defineProps({
5
+ modelValue: { type: null, required: true },
6
+ descriptor: { type: Object, required: true },
7
+ errors: { type: Array, required: false },
8
+ readonly: { type: Boolean, required: false },
9
+ disabled: { type: Boolean, required: false }
10
+ });
11
+ const emit = defineEmits(["update:modelValue"]);
12
+ const dialogOpen = ref(false);
13
+ const { MediaDialog, available } = useMediaManager();
14
+ const current = computed(() => {
15
+ if (!props.modelValue) return null;
16
+ if (typeof props.modelValue === "object") return props.modelValue;
17
+ return null;
18
+ });
19
+ const mime = computed(() => props.descriptor.attrs?.mime);
20
+ function onConfirm(selection) {
21
+ emit(
22
+ "update:modelValue",
23
+ Array.isArray(selection) ? selection[0] ?? null : selection
24
+ );
25
+ }
26
+ function clear() {
27
+ emit("update:modelValue", null);
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <div class="mapo-media-field space-y-2">
33
+ <MapoMediaUnavailable v-if="!available" />
34
+
35
+ <template v-else>
36
+ <!-- Selected media preview -->
37
+ <div
38
+ v-if="current"
39
+ class="flex items-center gap-3 rounded-lg border border-default p-2"
40
+ >
41
+ <MapoMediaPreview :media="current" size="sm" />
42
+ <div class="flex-1 min-w-0">
43
+ <p class="truncate text-xs font-medium">
44
+ {{ current.title || current.file?.split("/").pop() }}
45
+ </p>
46
+ <p class="text-xs text-dimmed">
47
+ {{ current.mime_type }}
48
+ </p>
49
+ </div>
50
+ <UButton
51
+ v-if="!readonly && !disabled"
52
+ icon="i-lucide-x"
53
+ size="xs"
54
+ variant="ghost"
55
+ color="neutral"
56
+ @click="clear"
57
+ />
58
+ </div>
59
+
60
+ <!-- Trigger button -->
61
+ <UButton
62
+ v-if="!readonly && !disabled"
63
+ :icon="current ? 'i-lucide-refresh-cw' : 'i-lucide-image-plus'"
64
+ size="sm"
65
+ variant="soft"
66
+ color="neutral"
67
+ :block="!current"
68
+ @click="dialogOpen = true"
69
+ >
70
+ {{ current ? "Change" : "Select media" }}
71
+ </UButton>
72
+
73
+ <!-- Dialog -->
74
+ <component
75
+ :is="MediaDialog"
76
+ v-model="dialogOpen"
77
+ selection-mode="single"
78
+ :mime="mime"
79
+ :selected="current"
80
+ @confirm="onConfirm"
81
+ />
82
+ </template>
83
+
84
+ <!-- Errors -->
85
+ <p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
86
+ {{ err }}
87
+ </p>
88
+ </div>
89
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { MediaDescriptor, MediaItem } from "../../types/index.js";
2
+ type __VLS_Props = {
3
+ modelValue: MediaItem | number | null | undefined;
4
+ descriptor: MediaDescriptor;
5
+ errors?: string[];
6
+ readonly?: boolean;
7
+ disabled?: boolean;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:modelValue": (value: MediaItem | null) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:modelValue"?: ((value: MediaItem | null) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
16
+ //# sourceMappingURL=MapoMediaField.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoMediaField.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoMediaField.vue"],"names":[],"mappings":"AAyGA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGvE,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,UAAU,EAAE,eAAe,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA6NF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,106 @@
1
+ <script setup>
2
+ import { ref, computed } from "vue";
3
+ import { useMediaManager } from "../../composables/useMediaManager.js";
4
+ const props = defineProps({
5
+ modelValue: { type: null, required: true },
6
+ descriptor: { type: Object, required: true },
7
+ errors: { type: Array, required: false },
8
+ readonly: { type: Boolean, required: false },
9
+ disabled: { type: Boolean, required: false }
10
+ });
11
+ const emit = defineEmits(["update:modelValue"]);
12
+ const dialogOpen = ref(false);
13
+ const { MediaDialog, available } = useMediaManager();
14
+ const current = computed(() => {
15
+ if (!props.modelValue) return [];
16
+ return props.modelValue.filter((v) => typeof v === "object");
17
+ });
18
+ const mime = computed(() => props.descriptor.attrs?.mime);
19
+ function onConfirm(selection) {
20
+ emit("update:modelValue", Array.isArray(selection) ? selection : [selection]);
21
+ }
22
+ function removeItem(id) {
23
+ emit(
24
+ "update:modelValue",
25
+ current.value.filter((m) => m.id !== id)
26
+ );
27
+ }
28
+ const dragIndex = ref(null);
29
+ function onDragStart(index) {
30
+ dragIndex.value = index;
31
+ }
32
+ function onDragOverItem(index) {
33
+ if (dragIndex.value === null || dragIndex.value === index) return;
34
+ const items = [...current.value];
35
+ const [moved] = items.splice(dragIndex.value, 1);
36
+ if (!moved) return;
37
+ items.splice(index, 0, moved);
38
+ dragIndex.value = index;
39
+ emit("update:modelValue", items);
40
+ }
41
+ function onDragEnd() {
42
+ dragIndex.value = null;
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <div class="mapo-media-m2m-field space-y-2">
48
+ <MapoMediaUnavailable v-if="!available" />
49
+
50
+ <template v-else>
51
+ <!-- Selected items grid -->
52
+ <div v-if="current.length > 0" class="flex flex-wrap gap-2">
53
+ <div
54
+ v-for="(media, i) in current"
55
+ :key="media.id"
56
+ class="group relative"
57
+ :class="{
58
+ 'opacity-50': dragIndex === i,
59
+ 'cursor-grab': !readonly && !disabled
60
+ }"
61
+ :draggable="!readonly && !disabled"
62
+ @dragstart="onDragStart(i)"
63
+ @dragover.prevent="onDragOverItem(i)"
64
+ @dragend="onDragEnd"
65
+ >
66
+ <MapoMediaPreview :media="media" size="sm" />
67
+ <button
68
+ v-if="!readonly && !disabled"
69
+ class="absolute -right-1.5 -top-1.5 hidden size-4 items-center justify-center rounded-full bg-error text-inverted group-hover:flex"
70
+ type="button"
71
+ @click="removeItem(media.id)"
72
+ >
73
+ <UIcon name="i-lucide-x" class="size-2.5" />
74
+ </button>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Add button -->
79
+ <UButton
80
+ v-if="!readonly && !disabled"
81
+ icon="i-lucide-plus"
82
+ size="sm"
83
+ variant="soft"
84
+ color="neutral"
85
+ @click="dialogOpen = true"
86
+ >
87
+ {{ current.length > 0 ? "Add media" : "Select media" }}
88
+ </UButton>
89
+
90
+ <!-- Dialog -->
91
+ <component
92
+ :is="MediaDialog"
93
+ v-model="dialogOpen"
94
+ selection-mode="multi"
95
+ :mime="mime"
96
+ :selected="current"
97
+ @confirm="onConfirm"
98
+ />
99
+ </template>
100
+
101
+ <!-- Errors -->
102
+ <p v-for="err in errors" :key="err" class="text-xs text-error" role="alert">
103
+ {{ err }}
104
+ </p>
105
+ </div>
106
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { MediaM2mDescriptor, MediaItem } from "../../types/index.js";
2
+ type __VLS_Props = {
3
+ modelValue: MediaItem[] | number[] | null | undefined;
4
+ descriptor: MediaM2mDescriptor;
5
+ errors?: string[];
6
+ readonly?: boolean;
7
+ disabled?: boolean;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ "update:modelValue": (value: MediaItem[]) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ "onUpdate:modelValue"?: ((value: MediaItem[]) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
16
+ //# sourceMappingURL=MapoMediaM2mField.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoMediaM2mField.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoMediaM2mField.vue"],"names":[],"mappings":"AA+HA,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAG1E,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,UAAU,EAAE,kBAAkB,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAwQF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,15 @@
1
+ <script setup>
2
+
3
+ </script>
4
+
5
+ <template>
6
+ <div
7
+ class="flex items-center gap-2 rounded-lg border border-dashed border-default px-3 py-2 text-xs text-muted"
8
+ >
9
+ <UIcon
10
+ name="i-lucide-triangle-alert"
11
+ class="size-4 shrink-0 text-warning"
12
+ />
13
+ <span>Media fields require <code>@mapomodule/uikit</code>.</span>
14
+ </div>
15
+ </template>
@@ -0,0 +1,4 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
4
+ //# sourceMappingURL=MapoMediaUnavailable.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MapoMediaUnavailable.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoMediaUnavailable.vue"],"names":[],"mappings":"AAuEA,QAAA,MAAM,YAAY,+QAChB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -1,9 +1,10 @@
1
1
  <script setup>
2
- import { watch, onBeforeUnmount } from "vue";
2
+ import { ref, watch, onBeforeUnmount, resolveComponent } from "vue";
3
3
  import { useEditor, EditorContent } from "@tiptap/vue-3";
4
4
  import StarterKit from "@tiptap/starter-kit";
5
5
  import Link from "@tiptap/extension-link";
6
6
  import Underline from "@tiptap/extension-underline";
7
+ import Image from "@tiptap/extension-image";
7
8
  const props = defineProps({
8
9
  modelValue: { type: null, required: true },
9
10
  descriptor: { type: Object, required: true },
@@ -61,6 +62,7 @@ const editor = useEditor({
61
62
  validate: (href) => isSafeUrl(href)
62
63
  }),
63
64
  Underline,
65
+ Image.configure({ inline: false, allowBase64: false }),
64
66
  ...extraExtensions
65
67
  ],
66
68
  onUpdate: ({ editor: e }) => {
@@ -83,6 +85,14 @@ watch(
83
85
  }
84
86
  );
85
87
  onBeforeUnmount(() => editor.value?.destroy());
88
+ const imagePickerOpen = ref(false);
89
+ const MediaManagerDialog = resolveComponent("MapoMediaManagerDialog");
90
+ const hasMediaManager = typeof MediaManagerDialog !== "string";
91
+ function onImageConfirm(selection) {
92
+ const media = Array.isArray(selection) ? selection[0] : selection;
93
+ if (!media || !editor.value) return;
94
+ editor.value.chain().focus().setImage({ src: media.file, alt: media.alt_text ?? "" }).run();
95
+ }
86
96
  </script>
87
97
 
88
98
  <template>
@@ -194,9 +204,9 @@ onBeforeUnmount(() => editor.value?.destroy());
194
204
 
195
205
  <USeparator orientation="vertical" class="mx-1 h-5" />
196
206
 
197
- <!-- Insert Image — enabled in Phase 6 when Media Manager is available -->
207
+ <!-- Insert Image via Media Manager (available when @mapomodule/uikit is installed) -->
198
208
  <UTooltip
199
- text="Insert image (requires Media Manager — Phase 6)"
209
+ :text="hasMediaManager ? 'Insert image' : 'Requires @mapomodule/uikit'"
200
210
  :delay-open="300"
201
211
  >
202
212
  <UButton
@@ -204,12 +214,13 @@ onBeforeUnmount(() => editor.value?.destroy());
204
214
  variant="ghost"
205
215
  color="neutral"
206
216
  icon="i-lucide-image"
207
- disabled
217
+ :disabled="readonly || disabled || !hasMediaManager"
218
+ @click="imagePickerOpen = true"
208
219
  />
209
220
  </UTooltip>
210
221
  </div>
211
222
 
212
- <!-- Contenuto editor -->
223
+ <!-- Editor content -->
213
224
  <EditorContent
214
225
  :editor="editor"
215
226
  class="prose prose-sm max-w-none px-3 py-2 focus-within:outline-none"
@@ -224,4 +235,14 @@ onBeforeUnmount(() => editor.value?.destroy());
224
235
  {{ err }}
225
236
  </p>
226
237
  </div>
238
+
239
+ <!-- Image picker dialog — only rendered when MapoMediaManagerDialog is globally available -->
240
+ <component
241
+ :is="MediaManagerDialog"
242
+ v-if="hasMediaManager"
243
+ v-model="imagePickerOpen"
244
+ selection-mode="single"
245
+ mime="image/*"
246
+ @confirm="onImageConfirm"
247
+ />
227
248
  </template>
@@ -1 +1 @@
1
- {"version":3,"file":"MapoWygEditor.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoWygEditor.vue"],"names":[],"mappings":"AAyQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAilBF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
1
+ {"version":3,"file":"MapoWygEditor.vue.d.ts","sourceRoot":"","sources":["../../../../src/runtime/components/fields/MapoWygEditor.vue"],"names":[],"mappings":"AA6SA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA+oBF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Resolves the Media Manager picker dialog at runtime.
3
+ *
4
+ * The media field types ship with `@mapomodule/form`, but the Media Manager
5
+ * itself lives in `@mapomodule/uikit`. Resolving the dialog by name instead of
6
+ * importing it keeps `form` free of a static dependency on `uikit` (which
7
+ * depends on `form`, so a static import would be a cycle) and lets `form` stay
8
+ * usable standalone — the fields then render an explicit notice instead of a
9
+ * broken picker.
10
+ *
11
+ * Must be called from `setup()`.
12
+ */
13
+ export declare function useMediaManager(): {
14
+ MediaDialog: string | import("vue").ConcreteComponent<{}, any, any, import("vue").ComputedOptions, import("vue").MethodOptions, {}, any>;
15
+ available: boolean;
16
+ };
17
+ //# sourceMappingURL=useMediaManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMediaManager.d.ts","sourceRoot":"","sources":["../../../src/runtime/composables/useMediaManager.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe;;;EAM9B"}
@@ -0,0 +1,6 @@
1
+ import { resolveComponent } from "vue";
2
+ export function useMediaManager() {
3
+ const MediaDialog = resolveComponent("MapoMediaManagerDialog");
4
+ const available = typeof MediaDialog !== "string";
5
+ return { MediaDialog, available };
6
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../../src/runtime/registry/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAMvD,eAAO,MAAM,eAAe,EAAE,aA0F7B,CAAC"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../../src/runtime/registry/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAMvD,eAAO,MAAM,eAAe,EAAE,aAkG7B,CAAC"}
@@ -22,8 +22,14 @@ export const defaultRegistry = {
22
22
  seo: () => import("../components/fields/MapoSeoPreview.vue"),
23
23
  editor: () => import("../components/fields/MapoWygEditor.vue"),
24
24
  map: () => import("../components/fields/MapoMapField.vue"),
25
- repeater: () => import("../components/fields/MapoRepeater.vue")
26
- // media, media-m2m, enhanced-media: added in Phase 6
25
+ repeater: () => import("../components/fields/MapoRepeater.vue"),
26
+ // ─── Media pickers ────────────────────────────────────────────────────────
27
+ // The fields live here with every other field type; the Media Manager they
28
+ // drive ships with @mapomodule/uikit and is resolved at runtime
29
+ // (see useMediaManager). Without uikit they render an explicit notice.
30
+ media: () => import("../components/fields/MapoMediaField.vue"),
31
+ "media-m2m": () => import("../components/fields/MapoMediaM2mField.vue"),
32
+ "enhanced-media": () => import("../components/fields/MapoEnhancedMediaField.vue")
27
33
  },
28
34
  attrs: {
29
35
  // Applied to all fields