@mapomodule/form 2.0.0-beta.4 → 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 +12 -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 +3 -2
  35. package/src/index.ts +5 -0
  36. package/src/module.ts +15 -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
@@ -1,10 +1,11 @@
1
1
  <script setup lang="ts">
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 type { AnyExtension } from "@tiptap/vue-3";
5
5
  import StarterKit from "@tiptap/starter-kit";
6
6
  import Link from "@tiptap/extension-link";
7
7
  import Underline from "@tiptap/extension-underline";
8
+ import Image from "@tiptap/extension-image";
8
9
  import type { EditorDescriptor } from "../../types/index.js";
9
10
 
10
11
  const props = defineProps<{
@@ -85,6 +86,7 @@ const editor = useEditor({
85
86
  validate: (href: string) => isSafeUrl(href),
86
87
  }),
87
88
  Underline,
89
+ Image.configure({ inline: false, allowBase64: false }),
88
90
  ...extraExtensions,
89
91
  ] as AnyExtension[],
90
92
  onUpdate: ({ editor: e }) => {
@@ -112,6 +114,28 @@ watch(
112
114
  );
113
115
 
114
116
  onBeforeUnmount(() => editor.value?.destroy());
117
+
118
+ // ─── Image insertion ──────────────────────────────────────────────────────
119
+ // MapoMediaManagerDialog is globally registered by @mapomodule/uikit.
120
+ // resolveComponent() works at runtime because it is registered before this
121
+ // component renders. We avoid a direct import to break the form → uikit cycle.
122
+ const imagePickerOpen = ref(false);
123
+ const MediaManagerDialog = resolveComponent("MapoMediaManagerDialog");
124
+ const hasMediaManager = typeof MediaManagerDialog !== "string";
125
+
126
+ function onImageConfirm(
127
+ selection:
128
+ | { file: string; alt_text?: string }
129
+ | Array<{ file: string; alt_text?: string }>,
130
+ ) {
131
+ const media = Array.isArray(selection) ? selection[0] : selection;
132
+ if (!media || !editor.value) return;
133
+ editor.value
134
+ .chain()
135
+ .focus()
136
+ .setImage({ src: media.file, alt: media.alt_text ?? "" })
137
+ .run();
138
+ }
115
139
  </script>
116
140
 
117
141
  <template>
@@ -223,9 +247,9 @@ onBeforeUnmount(() => editor.value?.destroy());
223
247
 
224
248
  <USeparator orientation="vertical" class="mx-1 h-5" />
225
249
 
226
- <!-- Insert Image — enabled in Phase 6 when Media Manager is available -->
250
+ <!-- Insert Image via Media Manager (available when @mapomodule/uikit is installed) -->
227
251
  <UTooltip
228
- text="Insert image (requires Media Manager — Phase 6)"
252
+ :text="hasMediaManager ? 'Insert image' : 'Requires @mapomodule/uikit'"
229
253
  :delay-open="300"
230
254
  >
231
255
  <UButton
@@ -233,12 +257,13 @@ onBeforeUnmount(() => editor.value?.destroy());
233
257
  variant="ghost"
234
258
  color="neutral"
235
259
  icon="i-lucide-image"
236
- disabled
260
+ :disabled="readonly || disabled || !hasMediaManager"
261
+ @click="imagePickerOpen = true"
237
262
  />
238
263
  </UTooltip>
239
264
  </div>
240
265
 
241
- <!-- Contenuto editor -->
266
+ <!-- Editor content -->
242
267
  <EditorContent
243
268
  :editor="editor"
244
269
  class="prose prose-sm max-w-none px-3 py-2 focus-within:outline-none"
@@ -253,4 +278,14 @@ onBeforeUnmount(() => editor.value?.destroy());
253
278
  {{ err }}
254
279
  </p>
255
280
  </div>
281
+
282
+ <!-- Image picker dialog — only rendered when MapoMediaManagerDialog is globally available -->
283
+ <component
284
+ :is="MediaManagerDialog"
285
+ v-if="hasMediaManager"
286
+ v-model="imagePickerOpen"
287
+ selection-mode="single"
288
+ mime="image/*"
289
+ @confirm="onImageConfirm"
290
+ />
256
291
  </template>
@@ -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
+ }
@@ -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
+ }