@bobbykim/manguito-cms-admin 0.5.0 → 0.5.2

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 (33) hide show
  1. package/dist/assets/{ChangePasswordView-RhnQhTpj.js → ChangePasswordView-v3tSL_0C.js} +1 -1
  2. package/dist/assets/ContentFormView-DneaTAaq.js +1 -0
  3. package/dist/assets/{ContentListView-52Vdrqg6.js → ContentListView-i3MwjmNq.js} +1 -1
  4. package/dist/assets/{ContentSettingsView-vDV6xaxT.js → ContentSettingsView-4cpotfsZ.js} +1 -1
  5. package/dist/assets/{LoginView-y0TQTfvj.js → LoginView-BObIe0zr.js} +1 -1
  6. package/dist/assets/{MediaDetailView-CFGRX94J.js → MediaDetailView-BKTDY6Ne.js} +1 -1
  7. package/dist/assets/MediaLibraryView-DojOE-wL.js +1 -0
  8. package/dist/assets/MediaThumbnail-Ckiw3oQ9.js +1 -0
  9. package/dist/assets/TaxonomyFormView-Chd_LUly.js +1 -0
  10. package/dist/assets/{TaxonomyListView-COqfCnDR.js → TaxonomyListView-Cy3iI1oC.js} +1 -1
  11. package/dist/assets/{UserFormView-G2pzRNAl.js → UserFormView-B2m4OdJV.js} +1 -1
  12. package/dist/assets/{UserListView-BlP10YCt.js → UserListView-z6weJ8UF.js} +1 -1
  13. package/dist/assets/field-defaults-DLoPESiE.js +97 -0
  14. package/dist/assets/{index-ivedPYqQ.css → index-C4qBpxpk.css} +1 -1
  15. package/dist/assets/{index-GvXDeXi5.js → index-stKCwnA4.js} +2 -2
  16. package/dist/assets/{useNotification-DXsAIQaa.js → useNotification-BdRk9gQY.js} +1 -1
  17. package/dist/index.html +2 -2
  18. package/package.json +2 -2
  19. package/src/components/fields/MediaUpload.vue +5 -19
  20. package/src/components/shared/MediaSelectModal.vue +4 -21
  21. package/src/components/shared/MediaThumbnail.vue +108 -0
  22. package/src/components/shared/__tests__/MediaThumbnail.test.ts +118 -0
  23. package/src/utils/__tests__/file-kind.test.ts +102 -0
  24. package/src/utils/__tests__/taxonomy-form.test.ts +17 -18
  25. package/src/utils/file-kind.ts +83 -0
  26. package/src/utils/taxonomy-form.ts +7 -15
  27. package/src/views/media/MediaLibraryView.vue +7 -22
  28. package/src/views/taxonomy/TaxonomyFormView.vue +2 -27
  29. package/src/views/taxonomy/__tests__/TaxonomyFormView.test.ts +71 -0
  30. package/dist/assets/ContentFormView-Ch9JVUWn.js +0 -1
  31. package/dist/assets/MediaLibraryView-WCKhSv_6.js +0 -1
  32. package/dist/assets/TaxonomyFormView-B2__-WRX.js +0 -1
  33. package/dist/assets/field-defaults-BTA4TEHe.js +0 -97
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import MediaThumbnail from '../MediaThumbnail.vue'
4
+
5
+ const image = { type: 'image' as const, url: 'https://cdn.example.com/a.jpg', mime_type: 'image/jpeg', alt: 'A cat' }
6
+ const video = { type: 'video' as const, url: 'https://cdn.example.com/clip.mp4', mime_type: 'video/mp4' }
7
+ const pdf = { type: 'file' as const, url: 'https://cdn.example.com/report.pdf', mime_type: 'application/pdf' }
8
+
9
+ afterEach(() => {
10
+ vi.unstubAllGlobals()
11
+ })
12
+
13
+ describe('MediaThumbnail', () => {
14
+ it('renders an image as a lazy <img> with its alt text', () => {
15
+ // MUTATION: render every item through the video or file branch.
16
+ const img = mount(MediaThumbnail, { props: { item: image } }).get('img')
17
+ expect(img.attributes('src')).toBe(image.url)
18
+ expect(img.attributes('alt')).toBe('A cat')
19
+ expect(img.attributes('loading')).toBe('lazy')
20
+ })
21
+
22
+ it('renders a video as a muted, metadata-only <video> seeked to its first frame', () => {
23
+ // MUTATION: drop `#t=0.1` or use preload="auto". The frame is then either
24
+ // black (time 0 on some codecs) or the whole file downloads per thumbnail.
25
+ const v = mount(MediaThumbnail, { props: { item: video } }).get('video')
26
+ expect(v.attributes('src')).toBe(`${video.url}#t=0.1`)
27
+ expect(v.attributes('preload')).toBe('metadata')
28
+ expect((v.element as HTMLVideoElement).muted).toBe(true)
29
+ expect(v.attributes('controls')).toBeUndefined()
30
+ })
31
+
32
+ it('falls back to the video icon when the frame cannot load', async () => {
33
+ // MUTATION: ignore the video's error event. A broken file then shows an
34
+ // empty black box instead of an icon.
35
+ const w = mount(MediaThumbnail, { props: { item: video } })
36
+ await w.get('video').trigger('error')
37
+ expect(w.find('video').exists()).toBe(false)
38
+ expect(w.text()).toContain('VIDEO')
39
+ })
40
+
41
+ it('loads the video only once it scrolls into view', async () => {
42
+ // MUTATION: bind `src` immediately. A grid of many videos then fetches every
43
+ // file's metadata at once.
44
+ let fire: ((entries: Array<{ isIntersecting: boolean }>) => void) | undefined
45
+ vi.stubGlobal(
46
+ 'IntersectionObserver',
47
+ class {
48
+ constructor(cb: (entries: Array<{ isIntersecting: boolean }>) => void) {
49
+ fire = cb
50
+ }
51
+ observe() {}
52
+ disconnect() {}
53
+ }
54
+ )
55
+ const w = mount(MediaThumbnail, { props: { item: video } })
56
+ expect(w.get('video').attributes('src')).toBeUndefined()
57
+ fire!([{ isIntersecting: true }])
58
+ await w.vm.$nextTick()
59
+ expect(w.get('video').attributes('src')).toBe(`${video.url}#t=0.1`)
60
+ })
61
+
62
+ it('renders a file as a typed badge with its caption lines', () => {
63
+ // MUTATION: render the old generic glyph. The badge text and the caption
64
+ // lines disappear.
65
+ const w = mount(MediaThumbnail, { props: { item: pdf, label: 'report.pdf', detail: '1.2 MB' } })
66
+ expect(w.get('[data-testid="file-badge"]').text()).toBe('PDF')
67
+ expect(w.text()).toContain('report.pdf')
68
+ expect(w.text()).toContain('1.2 MB')
69
+ })
70
+
71
+ it('omits the caption lines when none are given', () => {
72
+ // MUTATION: always render the file name, which crowds the small field preview.
73
+ const w = mount(MediaThumbnail, { props: { item: pdf } })
74
+ expect(w.text().trim()).toBe('PDF')
75
+ })
76
+
77
+ it('shows a video caption over its frame when one is given', () => {
78
+ // MUTATION: render label/detail only in the file branch. Video tiles in the
79
+ // library then lose the name and size the old placeholder showed.
80
+ const w = mount(MediaThumbnail, { props: { item: video, label: 'clip.mp4', detail: '3.4 MB' } })
81
+ expect(w.find('video').exists()).toBe(true)
82
+ expect(w.text()).toContain('clip.mp4')
83
+ expect(w.text()).toContain('3.4 MB')
84
+ })
85
+
86
+ it('tries again when a failed video is replaced by another item', async () => {
87
+ // MUTATION: never reset `videoFailed`. In the field preview, which keeps
88
+ // the thumbnail mounted while the selection changes, every later video then
89
+ // shows the fallback icon.
90
+ const w = mount(MediaThumbnail, { props: { item: video } })
91
+ await w.get('video').trigger('error')
92
+ expect(w.find('video').exists()).toBe(false)
93
+ await w.setProps({ item: { ...video, url: 'https://cdn.example.com/other.mp4' } })
94
+ expect(w.get('video').attributes('src')).toBe('https://cdn.example.com/other.mp4#t=0.1')
95
+ })
96
+
97
+ it('starts watching for visibility when the item becomes a video after mount', async () => {
98
+ // MUTATION: set up the IntersectionObserver only in onMounted. A video
99
+ // selected after an image then never gets a src: a permanent black box.
100
+ let fire: ((entries: Array<{ isIntersecting: boolean }>) => void) | undefined
101
+ vi.stubGlobal(
102
+ 'IntersectionObserver',
103
+ class {
104
+ constructor(cb: (entries: Array<{ isIntersecting: boolean }>) => void) {
105
+ fire = cb
106
+ }
107
+ observe() {}
108
+ disconnect() {}
109
+ }
110
+ )
111
+ const w = mount(MediaThumbnail, { props: { item: image } })
112
+ await w.setProps({ item: video })
113
+ expect(fire).toBeDefined()
114
+ fire!([{ isIntersecting: true }])
115
+ await w.vm.$nextTick()
116
+ expect(w.get('video').attributes('src')).toBe(`${video.url}#t=0.1`)
117
+ })
118
+ })
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { fileKind, fileName } from '../file-kind'
3
+
4
+ describe('fileKind', () => {
5
+ it('labels PDFs', () => {
6
+ // MUTATION: drop the pdf rule. A PDF then falls back to its extension "PDF"
7
+ // only by accident of its URL; with an extensionless URL it reads "FILE".
8
+ expect(fileKind('application/pdf', 'https://cdn.example.com/abc')).toMatchObject({ label: 'PDF', tone: 'red' })
9
+ })
10
+
11
+ it('labels word-processing documents, whatever their exact MIME type', () => {
12
+ // MUTATION: match only the legacy "application/msword". Modern .docx files
13
+ // (the openxml type) and ODT/RTF would fall through to their extension.
14
+ for (const mime of [
15
+ 'application/msword',
16
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
17
+ 'application/vnd.oasis.opendocument.text',
18
+ 'application/rtf',
19
+ ]) {
20
+ expect(fileKind(mime, 'https://cdn.example.com/x').label).toBe('DOC')
21
+ }
22
+ })
23
+
24
+ it('labels spreadsheets, including CSV', () => {
25
+ // MUTATION: match only the "spreadsheet" substring. CSV ("text/csv") would
26
+ // then read as plain text.
27
+ for (const mime of [
28
+ 'application/vnd.ms-excel',
29
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
30
+ 'application/vnd.oasis.opendocument.spreadsheet',
31
+ 'text/csv',
32
+ ]) {
33
+ expect(fileKind(mime, 'https://cdn.example.com/x').label).toBe('XLS')
34
+ }
35
+ })
36
+
37
+ it('labels presentations and archives', () => {
38
+ // MUTATION: drop the presentation or archive rules.
39
+ expect(fileKind('application/vnd.openxmlformats-officedocument.presentationml.presentation', 'u').label).toBe('PPT')
40
+ expect(fileKind('application/vnd.ms-powerpoint', 'u').label).toBe('PPT')
41
+ expect(fileKind('application/zip', 'u').label).toBe('ZIP')
42
+ expect(fileKind('application/gzip', 'u').label).toBe('ZIP')
43
+ expect(fileKind('application/x-7z-compressed', 'u').label).toBe('ZIP')
44
+ })
45
+
46
+ it('labels plain text and Markdown as TXT, but not CSV', () => {
47
+ // MUTATION: test the text/* rule before the CSV rule. CSV then reads TXT.
48
+ expect(fileKind('text/plain', 'u').label).toBe('TXT')
49
+ expect(fileKind('text/markdown', 'u').label).toBe('TXT')
50
+ expect(fileKind('text/csv', 'u').label).not.toBe('TXT')
51
+ })
52
+
53
+ it('falls back to the upper-cased extension of an unknown type', () => {
54
+ // MUTATION: return "FILE" for every unknown type, losing the extension hint.
55
+ expect(fileKind('application/octet-stream', 'https://cdn.example.com/media/model.glb')).toMatchObject({
56
+ label: 'GLB',
57
+ tone: 'gray',
58
+ })
59
+ })
60
+
61
+ it('ignores the query string and fragment when reading the extension', () => {
62
+ // MUTATION: take the text after the last "." of the whole URL. A signed URL
63
+ // would then produce a label from its query string.
64
+ expect(fileKind('application/octet-stream', 'https://cdn.example.com/a/b.step?X-Amz-Signature=abc.def').label).toBe('STEP')
65
+ })
66
+
67
+ it('falls back to FILE when there is no usable extension', () => {
68
+ // MUTATION: return an empty label, which renders an empty badge.
69
+ expect(fileKind('application/octet-stream', 'https://cdn.example.com/media/blob').label).toBe('FILE')
70
+ expect(fileKind('application/octet-stream', 'https://cdn.example.com/media/archive.toolongext').label).toBe('FILE')
71
+ })
72
+
73
+ it('ignores MIME parameters such as a charset', () => {
74
+ // MUTATION: compare the raw stored mime_type. "text/csv; charset=utf-8"
75
+ // then misses the CSV rule and reads TXT, and a PDF with parameters reads
76
+ // as its extension or FILE.
77
+ expect(fileKind('text/csv; charset=utf-8', 'https://cdn.example.com/x').label).toBe('XLS')
78
+ expect(fileKind('Application/PDF; name="a.pdf"', 'https://cdn.example.com/x').label).toBe('PDF')
79
+ })
80
+
81
+ it('recognises the archive MIME types browsers actually report', () => {
82
+ // MUTATION: list only application/zip. Chrome on Windows reports .zip as
83
+ // application/x-zip-compressed, which then reads as FILE.
84
+ for (const mime of ['application/x-zip-compressed', 'application/x-gzip', 'application/x-rar-compressed', 'application/vnd.rar']) {
85
+ expect(fileKind(mime, 'https://cdn.example.com/x').label).toBe('ZIP')
86
+ }
87
+ })
88
+ })
89
+
90
+ describe('fileName', () => {
91
+ it('returns the last path segment without the query string or fragment', () => {
92
+ // MUTATION: `url.split('/').pop()`. A signed URL's caption then reads
93
+ // "report.pdf?X-Amz-Signature=…".
94
+ expect(fileName('https://cdn.example.com/a/report.pdf?X-Amz-Signature=abc')).toBe('report.pdf')
95
+ expect(fileName('https://cdn.example.com/a/clip.mp4#t=3')).toBe('clip.mp4')
96
+ })
97
+
98
+ it('falls back to the whole URL when there is no path segment', () => {
99
+ // MUTATION: return '' for a URL ending in "/". The caption would be blank.
100
+ expect(fileName('https://cdn.example.com/')).toBe('https://cdn.example.com/')
101
+ })
102
+ })
@@ -39,25 +39,24 @@ const fields: ParsedField[] = [
39
39
  ] as unknown as ParsedField[]
40
40
 
41
41
  describe('initialTaxonomyForm', () => {
42
- it('leaves out every field the taxonomy read does not return', () => {
43
- // MUTATION: put every non-programmatic field into the form (the old
44
- // initForm). The paragraph and many-to-many keys would then be sent as
45
- // null, which the api rejects with a 422 for a "many" field and which
46
- // erases a one-to-one paragraph.
47
- const form = initialTaxonomyForm(fields, { name: 'Tag', parent: 'p-1' })
48
- expect(form).not.toHaveProperty('blocks')
49
- expect(form).not.toHaveProperty('hero')
50
- expect(form).not.toHaveProperty('related')
51
- expect(form).not.toHaveProperty('computed')
52
- })
42
+ it('includes relation fields, from the source or their empty default', () => {
43
+ // MUTATION: keep leaving paragraph and many-to-many fields out (before
44
+ // #55). The taxonomy form then cannot show or edit them, although the
45
+ // edit read now returns them.
46
+ const form = initialTaxonomyForm(fields, {
47
+ name: 'Tag', parent: 'p-1', blocks: [{ title: 'b' }], hero: { title: 'h' }, related: ['t-2'],
48
+ })
49
+ expect(form).toMatchObject({ blocks: [{ title: 'b' }], hero: { title: 'h' }, related: ['t-2'] })
53
50
 
54
- it('keeps column-backed fields, from the source or their default', () => {
55
- // MUTATION: drop every reference field (filter on field_type instead of
56
- // storage). A one-to-one reference has a column and must stay editable.
57
- expect(initialTaxonomyForm(fields, { name: 'Tag', parent: 'p-1' })).toEqual({
58
- name: 'Tag',
59
- parent: 'p-1',
51
+ // Empty defaults follow cardinality: a list for "many", null for "one".
52
+ expect(initialTaxonomyForm(fields)).toEqual({
53
+ name: '', parent: null, blocks: [], hero: null, related: [],
60
54
  })
61
- expect(initialTaxonomyForm(fields)).toEqual({ name: '', parent: null })
55
+ })
56
+
57
+ it('leaves programmatic fields out: they are computed and never saved', () => {
58
+ // MUTATION: put programmatic fields into the form. Save would then send a
59
+ // computed value back to the api.
60
+ expect(initialTaxonomyForm(fields, { name: 'Tag', computed: 'x' })).not.toHaveProperty('computed')
62
61
  })
63
62
  })
@@ -0,0 +1,83 @@
1
+ // What a non-image, non-video media file looks like as a thumbnail: a short
2
+ // type label and a colour for its badge. Derived from the MIME type stored with
3
+ // the media item; the URL's extension is the fallback for types not listed.
4
+
5
+ export type FileTone = 'red' | 'blue' | 'green' | 'orange' | 'gray'
6
+
7
+ export type FileKind = { label: string; tone: FileTone }
8
+
9
+ // Tailwind background class for each badge tone.
10
+ export const TONE_CLASS: Record<FileTone, string> = {
11
+ red: 'bg-red-600',
12
+ blue: 'bg-blue-600',
13
+ green: 'bg-green-600',
14
+ orange: 'bg-orange-500',
15
+ gray: 'bg-gray-500',
16
+ }
17
+
18
+ // Checked in order; the first match wins. CSV sits before the generic text/*
19
+ // rule so it reads as a spreadsheet, not as plain text.
20
+ const RULES: Array<{ test: (mime: string) => boolean; kind: FileKind }> = [
21
+ { test: (m) => m === 'application/pdf', kind: { label: 'PDF', tone: 'red' } },
22
+ {
23
+ test: (m) =>
24
+ m === 'application/msword' ||
25
+ m === 'application/rtf' ||
26
+ m.includes('wordprocessingml') ||
27
+ m === 'application/vnd.oasis.opendocument.text',
28
+ kind: { label: 'DOC', tone: 'blue' },
29
+ },
30
+ {
31
+ test: (m) =>
32
+ m === 'application/vnd.ms-excel' ||
33
+ m === 'text/csv' ||
34
+ m.includes('spreadsheetml') ||
35
+ m === 'application/vnd.oasis.opendocument.spreadsheet',
36
+ kind: { label: 'XLS', tone: 'green' },
37
+ },
38
+ {
39
+ test: (m) =>
40
+ m === 'application/vnd.ms-powerpoint' ||
41
+ m.includes('presentationml') ||
42
+ m === 'application/vnd.oasis.opendocument.presentation',
43
+ kind: { label: 'PPT', tone: 'orange' },
44
+ },
45
+ {
46
+ test: (m) =>
47
+ m === 'application/zip' ||
48
+ m === 'application/x-zip-compressed' ||
49
+ m === 'application/gzip' ||
50
+ m === 'application/x-gzip' ||
51
+ m === 'application/x-tar' ||
52
+ m === 'application/x-7z-compressed' ||
53
+ m === 'application/x-rar-compressed' ||
54
+ m === 'application/vnd.rar',
55
+ kind: { label: 'ZIP', tone: 'gray' },
56
+ },
57
+ { test: (m) => m.startsWith('text/'), kind: { label: 'TXT', tone: 'gray' } },
58
+ ]
59
+
60
+ // The file name a media URL ends in, without its query string or fragment
61
+ // (signed storage URLs carry both). Falls back to the whole URL when the path
62
+ // has no last segment.
63
+ export function fileName(url: string): string {
64
+ const path = url.split(/[?#]/)[0] ?? ''
65
+ return path.split('/').pop() || url
66
+ }
67
+
68
+ // The URL's extension, upper-cased, or '' when it has none worth showing.
69
+ function extensionOf(url: string): string {
70
+ const name = fileName(url)
71
+ const dot = name.lastIndexOf('.')
72
+ if (dot <= 0) return ''
73
+ const ext = name.slice(dot + 1)
74
+ return /^[a-z0-9]{1,5}$/i.test(ext) ? ext.toUpperCase() : ''
75
+ }
76
+
77
+ export function fileKind(mimeType: string, url: string): FileKind {
78
+ // Stored MIME types can carry parameters ("text/csv; charset=utf-8").
79
+ const mime = (mimeType.split(';')[0] ?? '').trim().toLowerCase()
80
+ const rule = RULES.find((r) => r.test(mime))
81
+ if (rule) return rule.kind
82
+ return { label: extensionOf(url) || 'FILE', tone: 'gray' }
83
+ }
@@ -1,27 +1,19 @@
1
1
  import type { ParsedField } from '@bobbykim/manguito-cms-core'
2
2
  import { defaultForField } from './field-defaults'
3
3
 
4
- // Whether the taxonomy read (GET /admin/api/taxonomy/{type}/:id) returns the
5
- // field. It returns the item's own columns only: a paragraph field
6
- // (db_column null) lives on the paragraph table and a many-to-many reference
7
- // (db_column.junction) lives on its junction table, so neither comes back.
8
- function isReadByTaxonomy(field: ParsedField): boolean {
9
- if (field.field_type === 'programmatic') return false
10
- return field.db_column !== null && !field.db_column.junction
11
- }
12
-
13
- // The taxonomy form's starting state, which is also what Save sends. A field
14
- // the read does not return stays out of it: sending it as null would fail the
15
- // api's relation shape check for a "many" field and erase a one-to-one
16
- // paragraph, while an absent field is left untouched by the api.
17
- // Programmatic fields are computed at read time and never submitted.
4
+ // The taxonomy form's starting state, which is also what Save sends. Every
5
+ // field the taxonomy edit read returns is included, relation fields too: the
6
+ // read loads paragraph fields and many-to-many link ids (#55). A field with no
7
+ // stored value starts at its empty default (null for a 'one' relation, [] for
8
+ // a 'many' one). Programmatic fields are computed at read time and never
9
+ // submitted, so they stay out.
18
10
  export function initialTaxonomyForm(
19
11
  fields: ParsedField[],
20
12
  source?: Record<string, unknown>
21
13
  ): Record<string, unknown> {
22
14
  const form: Record<string, unknown> = {}
23
15
  for (const field of fields) {
24
- if (!isReadByTaxonomy(field)) continue
16
+ if (field.field_type === 'programmatic') continue
25
17
  form[field.name] = source?.[field.name] ?? defaultForField(field)
26
18
  }
27
19
  return form
@@ -10,6 +10,8 @@ import { useMediaStore } from '../../stores/media'
10
10
  import { useTabIndicator } from '../../composables/useTabIndicator'
11
11
  import Pagination from '../../components/shared/Pagination.vue'
12
12
  import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
13
+ import MediaThumbnail from '../../components/shared/MediaThumbnail.vue'
14
+ import { fileName } from '../../utils/file-kind'
13
15
 
14
16
  const router = useRouter()
15
17
  const api = useApiClient()
@@ -442,31 +444,14 @@ function cancelUpload() {
442
444
  <button
443
445
  type="button"
444
446
  class="h-full w-full overflow-hidden rounded-[14px] border border-card-border shadow-[0_1px_2px_rgba(24,24,48,0.04),0_8px_22px_rgba(24,24,48,0.05)] transition-all hover:-translate-y-[3px] hover:shadow-[0_12px_30px_rgba(24,24,48,0.12)] focus:outline-none focus:ring-2 focus:ring-indigo-500"
445
- :aria-label="`View ${item.alt ?? item.url.split('/').pop()}`"
447
+ :aria-label="`View ${item.alt ?? fileName(item.url)}`"
446
448
  @click="goToDetail(item.id)"
447
449
  >
448
- <!-- Image thumbnail -->
449
- <img
450
- v-if="item.type === 'image'"
451
- :src="item.url"
452
- :alt="item.alt ?? ''"
453
- class="h-full w-full object-cover"
454
- loading="lazy"
450
+ <MediaThumbnail
451
+ :item="item"
452
+ :label="fileName(item.url)"
453
+ :detail="formatSize(item.file_size)"
455
454
  />
456
-
457
- <!-- Video / file icon -->
458
- <div
459
- v-else
460
- class="flex h-full w-full flex-col items-center justify-center gap-1 bg-gray-50 p-1 text-gray-400"
461
- >
462
- <span class="text-3xl" aria-hidden="true">
463
- {{ item.type === 'video' ? '▶' : '📄' }}
464
- </span>
465
- <span class="max-w-full truncate text-center text-xs">
466
- {{ item.url.split('/').pop() }}
467
- </span>
468
- <span class="text-xs text-gray-300">{{ formatSize(item.file_size) }}</span>
469
- </div>
470
455
  </button>
471
456
  </div>
472
457
  </div>
@@ -1,8 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, onMounted, watch } from 'vue'
3
- import type { Component } from 'vue'
4
3
  import { useRoute, useRouter } from 'vue-router'
5
- import type { ParsedField } from '@bobbykim/manguito-cms-core'
6
4
  import { useApiClient } from '../../composables/useApiClient'
7
5
  import { usePermission } from '../../composables/usePermission'
8
6
  import { useFormValidation } from '../../composables/useFormValidation'
@@ -10,13 +8,7 @@ import { useNotification } from '../../composables/useNotification'
10
8
  import { useSchemaStore } from '../../stores/schema'
11
9
  import { useTaxonomyStore } from '../../stores/taxonomy'
12
10
  import { initialTaxonomyForm } from '../../utils/taxonomy-form'
13
- import TextInput from '../../components/fields/TextInput.vue'
14
- import RichTextEditor from '../../components/fields/RichTextEditor.vue'
15
- import NumberInput from '../../components/fields/NumberInput.vue'
16
- import BooleanToggle from '../../components/fields/BooleanToggle.vue'
17
- import DatePicker from '../../components/fields/DatePicker.vue'
18
- import EnumSelect from '../../components/fields/EnumSelect.vue'
19
- import ComputedDisplay from '../../components/fields/ComputedDisplay.vue'
11
+ import { componentFor, fieldExtraProps } from '../../components/fields/field-registry'
20
12
  import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
21
13
 
22
14
  const route = useRoute()
@@ -48,24 +40,6 @@ const saving = ref(false)
48
40
  const formError = ref('')
49
41
  const showDeleteConfirm = ref(false)
50
42
 
51
- // ── Field component mapping (flat — no paragraphs, media, or references in taxonomy) ──
52
-
53
- const FIELD_COMP = {
54
- 'text/plain': TextInput,
55
- 'text/rich': RichTextEditor,
56
- integer: NumberInput,
57
- float: NumberInput,
58
- boolean: BooleanToggle,
59
- date: DatePicker,
60
- enum: EnumSelect,
61
- programmatic: ComputedDisplay,
62
- } as const
63
-
64
- function componentFor(field: ParsedField): Component {
65
- const key = field.field_type as keyof typeof FIELD_COMP
66
- return FIELD_COMP[key] ?? TextInput
67
- }
68
-
69
43
  // ── Form helpers ──────────────────────────────────────────────────────────────
70
44
 
71
45
  function initForm(source?: Record<string, unknown>) {
@@ -241,6 +215,7 @@ const pageTitle = computed(() => {
241
215
  :model-value="form[field.name]"
242
216
  :error="errors[field.name]"
243
217
  :disabled="saving"
218
+ v-bind="fieldExtraProps(field)"
244
219
  @update:model-value="updateField(field.name, $event)"
245
220
  />
246
221
  </div>
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { mount, flushPromises } from '@vue/test-utils'
3
+ import { createPinia, setActivePinia } from 'pinia'
4
+ import type { ParsedField, ParsedTaxonomyType } from '@bobbykim/manguito-cms-core'
5
+ import { useSchemaStore } from '../../../stores/schema'
6
+ import TaxonomyFormView from '../TaxonomyFormView.vue'
7
+ import ParagraphEmbed from '../../../components/fields/ParagraphEmbed.vue'
8
+ import ReferenceSelect from '../../../components/fields/ReferenceSelect.vue'
9
+ import MediaUpload from '../../../components/fields/MediaUpload.vue'
10
+
11
+ // Create mode: no :id param, so the form renders from defaults with no fetch.
12
+ vi.mock('vue-router', () => ({
13
+ useRoute: () => ({ params: { type: 'taxonomy--tag' } }),
14
+ useRouter: () => ({ push: vi.fn() }),
15
+ }))
16
+
17
+ const base = { required: false, nullable: true, order: 0, validation: { required: false } }
18
+
19
+ const tagType = {
20
+ schema_type: 'taxonomy-type',
21
+ name: 'taxonomy--tag',
22
+ label: 'Tag',
23
+ source_file: '',
24
+ system_fields: [],
25
+ fields: [
26
+ { ...base, name: 'name', label: 'Name', field_type: 'text/plain',
27
+ db_column: { column_name: 'name', column_type: 'varchar', nullable: true }, ui_component: { component: 'text-input' } },
28
+ { ...base, name: 'icon', label: 'Icon', field_type: 'image',
29
+ validation: { required: false, allowed_mime_types: ['image/png'] },
30
+ db_column: { column_name: 'icon', column_type: 'uuid', nullable: true },
31
+ ui_component: { component: 'file-upload', accepted_mime_types: ['image/*'] } },
32
+ { ...base, name: 'parent', label: 'Parent', field_type: 'reference',
33
+ db_column: { column_name: 'parent', column_type: 'uuid', nullable: true },
34
+ ui_component: { component: 'typeahead-select', ref: 'taxonomy--tag', rel: 'one-to-one' } },
35
+ { ...base, name: 'related', label: 'Related', field_type: 'reference',
36
+ db_column: { column_name: '', column_type: 'uuid', nullable: true,
37
+ junction: { table_name: 'junction_taxonomy_tag_related', left_column: 'left_id', right_column: 'right_id', right_table: 'taxonomy_tag', order_column: false } },
38
+ ui_component: { component: 'typeahead-select', ref: 'taxonomy--tag', rel: 'many-to-many' } },
39
+ { ...base, name: 'blurb', label: 'Blurb', field_type: 'paragraph', db_column: null,
40
+ ui_component: { component: 'paragraph-embed', ref: 'paragraph--blurb', rel: 'one-to-one' } },
41
+ ] as unknown as ParsedField[],
42
+ db: { table_name: 'taxonomy_tag', junction_tables: [] },
43
+ } as unknown as ParsedTaxonomyType
44
+
45
+ let pinia: ReturnType<typeof createPinia>
46
+
47
+ beforeEach(() => {
48
+ pinia = createPinia()
49
+ setActivePinia(pinia)
50
+ useSchemaStore().setSchema(tagType)
51
+ })
52
+
53
+ describe('TaxonomyFormView', () => {
54
+ it('renders relation and media fields with their real editors, not a text box', async () => {
55
+ // MUTATION: keep the view's own flat component map (before #55). Every
56
+ // paragraph, reference and media field then falls back to TextInput.
57
+ const w = mount(TaxonomyFormView, { global: { plugins: [pinia] } })
58
+ await flushPromises()
59
+ expect(w.findComponent(MediaUpload).exists()).toBe(true)
60
+ expect(w.findAllComponents(ReferenceSelect)).toHaveLength(2)
61
+ expect(w.findComponent(ParagraphEmbed).exists()).toBe(true)
62
+ })
63
+
64
+ it('gives the paragraph editor its paragraph form', async () => {
65
+ // MUTATION: render with componentFor but drop `v-bind="fieldExtraProps(field)"`.
66
+ // ParagraphEmbed then has no formComponent and cannot render its item.
67
+ const w = mount(TaxonomyFormView, { global: { plugins: [pinia] } })
68
+ await flushPromises()
69
+ expect(w.findComponent(ParagraphEmbed).props('formComponent')).toBeTruthy()
70
+ })
71
+ })
@@ -1 +0,0 @@
1
- import{A as e,B as t,E as n,L as r,O as i,P as a,W as o,a as s,c,ct as l,d as u,g as d,i as f,j as p,l as m,lt as h,m as g,s as _,t as v,u as y,ut as b,v as x,w as S,x as C}from"./schema-B-RKX_ep.js";import{a as w,c as T,d as E,n as D,r as O,s as k,t as A,u as j}from"./index-GvXDeXi5.js";import{a as M,c as N,i as P,l as F,n as I,o as L,r as R,s as z,t as ee,u as te}from"./field-defaults-BTA4TEHe.js";import{t as ne}from"./useNotification-DXsAIQaa.js";import{t as re}from"./content--2XYr_UQ.js";import{t as ie}from"./ConfirmDialog-B7nxdDge.js";import{t as ae}from"./useTabIndicator-CvEsGBJh.js";var B={class:`flex h-full max-h-144 w-full max-w-3xl flex-col overflow-hidden rounded-lg bg-white shadow-xl`},V={class:`flex items-center justify-between border-b border-gray-200 px-5 py-4`},H={key:0,class:`flex gap-1 border-b border-gray-200 px-5 pt-3`},oe=[`onClick`],U={key:1,class:`border-b border-gray-200 px-5 py-2 text-sm text-gray-500`},se={class:`font-medium text-gray-700`},W={class:`flex-1 overflow-y-auto p-5`},G={key:0,class:`flex items-center justify-center py-16`},ce={key:1,class:`flex flex-col items-center justify-center py-16 text-gray-400`},K={key:2,class:`grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5`},q=[`aria-pressed`,`aria-label`,`onClick`],J=[`src`,`alt`],Y={key:1,class:`flex h-full w-full flex-col items-center justify-center gap-1 bg-gray-50 text-gray-400`,"aria-hidden":`true`},X={class:`text-3xl`},Z={class:`max-w-full truncate px-1 text-xs`},Q={key:2,class:`absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-indigo-600 text-xs text-white`,"aria-hidden":`true`},le={key:2,class:`flex items-center justify-center gap-3 border-t border-gray-100 px-5 py-2`},ue=[`disabled`],de={class:`text-xs text-gray-500`},fe=[`disabled`],pe={class:`flex justify-end gap-3 border-t border-gray-200 px-5 py-4`},me=[`disabled`],he=d({__name:`MediaSelectModal`,props:{acceptedType:{}},emits:[`select`,`close`],setup(e,{emit:r}){let a=e,o=r,d=A(),h=[{label:`Images`,value:`image`},{label:`Videos`,value:`video`},{label:`PDFs`,value:`file`}],v=t(a.acceptedType??`all`),x=_(()=>!a.acceptedType);p(()=>a.acceptedType,e=>{e&&(v.value=e)});let C=t([]),w=t(!1),T=t(1),D=t(1),O=t(null);function k(e,t){let n=new URLSearchParams({page:String(t),per_page:`20`});return e!==`all`&&n.set(`type`,e),`/media?${n.toString()}`}async function j(){w.value=!0;let e=await d.get(k(v.value,T.value));w.value=!1,e.ok&&(C.value=e.data,D.value=e.meta?.total_pages??1)}p(v,()=>{T.value=1,O.value=null,j()}),p(T,()=>{j()}),S(()=>{j()});function M(){O.value&&o(`select`,O.value)}return(t,r)=>(n(),m(s,{to:`body`},[c(`div`,{class:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,role:`dialog`,"aria-modal":`true`,"aria-label":`Select media`,onClick:r[5]||=E(e=>t.$emit(`close`),[`self`])},[c(`div`,B,[c(`div`,V,[r[6]||=c(`h2`,{class:`text-base font-semibold text-gray-900`},`Select Media`,-1),c(`button`,{type:`button`,class:`text-xl leading-none text-gray-400 hover:text-gray-600`,"aria-label":`Close`,onClick:r[0]||=e=>t.$emit(`close`)},` × `)]),x.value?(n(),u(`div`,H,[c(`button`,{type:`button`,class:l([`rounded-t-md px-3 py-1.5 text-sm font-medium transition-colors`,v.value===`all`?`border-b-2 border-indigo-600 text-indigo-600`:`text-gray-500 hover:text-gray-800`]),onClick:r[1]||=e=>v.value=`all`},` All `,2),(n(),u(f,null,i(h,e=>c(`button`,{key:e.value,type:`button`,class:l([`rounded-t-md px-3 py-1.5 text-sm font-medium transition-colors`,v.value===e.value?`border-b-2 border-indigo-600 text-indigo-600`:`text-gray-500 hover:text-gray-800`]),onClick:t=>v.value=e.value},b(e.label),11,oe)),64))])):(n(),u(`div`,U,[r[7]||=g(` Showing: `,-1),c(`span`,se,b(h.find(t=>t.value===e.acceptedType)?.label??e.acceptedType),1)])),c(`div`,W,[w.value?(n(),u(`div`,G,[...r[8]||=[c(`span`,{class:`inline-block h-6 w-6 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent`,"aria-hidden":`true`},null,-1)]])):C.value.length===0?(n(),u(`div`,ce,[...r[9]||=[c(`span`,{class:`text-4xl`,"aria-hidden":`true`},`🖼`,-1),c(`p`,{class:`mt-2 text-sm`},`No media found`,-1)]])):(n(),u(`div`,K,[(n(!0),u(f,null,i(C.value,e=>(n(),u(`button`,{key:e.id,type:`button`,class:l([`group relative aspect-square overflow-hidden rounded-md border-2 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1`,O.value?.id===e.id?`border-indigo-500`:`border-transparent hover:border-gray-300`]),"aria-pressed":O.value?.id===e.id,"aria-label":`Select ${e.alt??e.url.split(`/`).pop()}`,onClick:t=>O.value=e},[e.type===`image`?(n(),u(`img`,{key:0,src:e.url,alt:e.alt??``,class:`h-full w-full object-cover`,loading:`lazy`},null,8,J)):(n(),u(`div`,Y,[c(`span`,X,b(e.type===`video`?`▶`:`📄`),1),c(`span`,Z,b(e.url.split(`/`).pop()),1)])),O.value?.id===e.id?(n(),u(`div`,Q,` ✓ `)):y(``,!0)],10,q))),128))]))]),D.value>1?(n(),u(`div`,le,[c(`button`,{type:`button`,disabled:T.value===1,class:`rounded px-2 py-1 text-sm text-gray-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40`,onClick:r[2]||=e=>T.value--},` ← Prev `,8,ue),c(`span`,de,b(T.value)+` / `+b(D.value),1),c(`button`,{type:`button`,disabled:T.value===D.value,class:`rounded px-2 py-1 text-sm text-gray-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40`,onClick:r[3]||=e=>T.value++},` Next → `,8,fe)])):y(``,!0),c(`div`,pe,[c(`button`,{type:`button`,class:`rounded-md px-4 py-2 text-sm font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50`,onClick:r[4]||=e=>t.$emit(`close`)},` Cancel `),c(`button`,{type:`button`,disabled:!O.value,class:`rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50`,onClick:M},` Select `,8,me)])])])]))}}),ge={class:`block text-[13px] font-semibold text-[#3D3D52]`},_e={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},ve={key:0,class:`mt-1 flex flex-col gap-2 rounded-md border border-gray-200 p-3`},$={class:`flex items-center gap-3`},ye=[`src`,`alt`],be={key:1,class:`flex h-16 w-16 items-center justify-center rounded bg-gray-100 text-2xl text-gray-400`,"aria-hidden":`true`},xe={class:`min-w-0 flex-1`},Se={class:`truncate text-sm font-medium text-gray-800`},Ce={class:`text-xs text-gray-500`},we={key:0},Te=[`for`],Ee=[`id`],De={key:1,class:`mt-1 rounded-md border border-gray-200 p-3`},Oe={class:`flex items-center gap-2`},ke={class:`h-2 flex-1 overflow-hidden rounded-full bg-gray-100`},Ae={class:`w-8 shrink-0 text-right text-xs text-gray-500`},je={key:2,class:`mt-1 rounded-md border border-gray-200 p-3`},Me={class:`mb-2 truncate text-[13px] font-semibold text-[#3D3D52]`},Ne=[`for`],Pe=[`id`],Fe={class:`mt-3 flex justify-end gap-2`},Ie=[`disabled`],Le={key:3,class:`mt-1 flex gap-2`},Re=[`accept`],ze=[`disabled`],Be=[`disabled`],Ve={key:4,class:`mt-1 text-sm text-red-600`,role:`alert`},He={key:5,class:`mt-1 text-sm text-red-600`,role:`alert`},Ue=d({__name:`MediaUpload`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:r}){let i=e,o=r,s=O(),l=_(()=>{let e=i.field.ui_component;return e.component===`file-upload`?e.accepted_mime_types.join(`,`):``}),d=_(()=>{let e=i.field.field_type;return e===`image`||e===`video`||e===`file`?e:`file`}),f=_(()=>d.value!==`image`),p=t(`idle`),v=t(0),x=t(``),C=t(null),w=t(``),T=t(null),E=t(!1),D=t(null);function k(e){return e>=1e6?`${(e/1e6).toFixed(1)} MB`:e>=1e3?`${(e/1e3).toFixed(0)} KB`:`${e} B`}function A(e){return e.split(`/`).pop()??e}function M(e,t){return new Promise((n,r)=>{let i=new FormData;i.append(`file`,e),t&&i.append(`alt`,t);let a=new XMLHttpRequest;a.withCredentials=!0,a.open(`POST`,`/admin/api/media/${d.value}`),a.upload.onprogress=e=>{e.lengthComputable&&(v.value=Math.round(e.loaded/e.total*100))},a.onload=()=>{try{let e=JSON.parse(a.responseText);e.ok&&e.data?n(e.data):r(Error(e.error?.message??`Upload failed`))}catch{r(Error(`Invalid server response`))}},a.onerror=()=>r(Error(`Network error`)),a.send(i)})}async function N(e,t){let n=new URLSearchParams({type:d.value,filename:e.name,mime_type:e.type}),r=await(await fetch(`/admin/api/media/presigned-url?${n.toString()}`,{credentials:`include`})).json();if(!r.ok||!r.data)throw Error(r.error?.message??`Failed to get upload URL`);let{upload_url:i,media_id:a,method:o,fields:s}=r.data;await new Promise((t,n)=>{let r=new XMLHttpRequest;if(r.upload.onprogress=e=>{e.lengthComputable&&(v.value=Math.round(e.loaded/e.total*100))},r.onload=()=>{r.status>=200&&r.status<300?t():n(Error(`Storage upload failed (${r.status})`))},r.onerror=()=>n(Error(`Network error during upload`)),o===`POST`&&s){let t=new FormData;for(let[e,n]of Object.entries(s))t.append(e,n);t.append(`file`,e),r.open(`POST`,i),r.send(t)}else r.open(`PUT`,i),r.send(e)});let c=await(await fetch(`/admin/api/media/confirm/${a}`,{method:`POST`,credentials:`include`,headers:{"Content-Type":`application/json`},body:JSON.stringify({alt:t})})).json();if(!c.ok||!c.data)throw Error(c.error?.message??`Upload confirmation failed`);return c.data}async function P(e){p.value=`uploading`,v.value=0,x.value=``;try{let t=w.value.trim(),n=s.presignedUploads?await N(e,t):await M(e,t);T.value=n,w.value=n.alt??``,o(`update:modelValue`,n.id)}catch(e){x.value=e instanceof Error?e.message:`Upload failed`}finally{p.value=`idle`,C.value=null}}async function F(){let e=T.value;if(!e)return;let t=new XMLHttpRequest;t.withCredentials=!0,t.open(`PATCH`,`/admin/api/media/${e.id}`),t.setRequestHeader(`Content-Type`,`application/json`),t.onload=()=>{try{let e=JSON.parse(t.responseText);e.ok&&e.data&&(T.value=e.data)}catch{}},t.send(JSON.stringify({alt:w.value.trim()}))}function I(e){let t=e.target.files?.[0];t&&(C.value=t,w.value=``,e.target.value=``,f.value?p.value=`alt-collect`:P(t))}function L(){C.value&&(f.value&&!w.value.trim()||P(C.value))}function R(){C.value=null,w.value=``,p.value=`idle`,x.value=``}function z(){T.value=null,w.value=``,o(`update:modelValue`,null)}function ee(e){T.value=e,w.value=e.alt??``,E.value=!1,o(`update:modelValue`,e.id)}return S(async()=>{if(typeof i.modelValue!=`string`||!i.modelValue)return;let e=await fetch(`/admin/api/media/${i.modelValue}`,{credentials:`include`});if(!e.ok)return;let t=await e.json();t.ok&&t.data&&(T.value=t.data,w.value=t.data.alt??``)}),(t,r)=>(n(),u(`div`,null,[c(`label`,ge,[g(b(e.field.label)+` `,1),e.field.required?(n(),u(`span`,_e,`*`)):y(``,!0)]),T.value?(n(),u(`div`,ve,[c(`div`,$,[T.value.type===`image`?(n(),u(`img`,{key:0,src:T.value.url,alt:T.value.alt??``,class:`h-16 w-16 rounded object-cover`},null,8,ye)):(n(),u(`div`,be,b(T.value.type===`video`?`▶`:`📄`),1)),c(`div`,xe,[c(`p`,Se,b(A(T.value.url)),1),c(`p`,Ce,b(k(T.value.file_size)),1)]),e.disabled?y(``,!0):(n(),u(`button`,{key:2,type:`button`,class:`shrink-0 text-lg leading-none text-gray-400 hover:text-red-500`,"aria-label":`Remove selected media`,onClick:z},` × `))]),T.value.type===`image`&&!e.disabled?(n(),u(`div`,we,[c(`label`,{for:`${e.field.name}-alt`,class:`block text-xs text-gray-500`},[...r[5]||=[g(` Alt text `,-1),c(`span`,{class:`text-gray-400`},`(optional)`,-1)]],8,Te),a(c(`input`,{id:`${e.field.name}-alt`,"onUpdate:modelValue":r[0]||=e=>w.value=e,type:`text`,placeholder:`Describe this image`,class:`mt-0.5 block w-full rounded-md border border-gray-300 px-2 py-1 text-sm shadow-sm focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-200`,onBlur:F},null,40,Ee),[[j,w.value]])])):y(``,!0)])):p.value===`uploading`?(n(),u(`div`,De,[r[6]||=c(`p`,{class:`mb-1.5 text-xs text-gray-500`},`Uploading…`,-1),c(`div`,Oe,[c(`div`,ke,[c(`div`,{class:`h-2 rounded-full bg-indigo-600 transition-all duration-150`,style:h({width:`${v.value}%`})},null,4)]),c(`span`,Ae,b(v.value)+`% `,1)])])):p.value===`alt-collect`?(n(),u(`div`,je,[c(`p`,Me,b(C.value?.name),1),c(`label`,{for:`${e.field.name}-alt-pre`,class:`block text-xs text-gray-600`},[...r[7]||=[g(` Alt text `,-1),c(`span`,{class:`text-red-500`,"aria-hidden":`true`},`*`,-1)]],8,Ne),a(c(`input`,{id:`${e.field.name}-alt-pre`,"onUpdate:modelValue":r[1]||=e=>w.value=e,type:`text`,placeholder:`Describe this file`,class:`mt-0.5 block w-full rounded-md border border-gray-300 px-2 py-1 text-sm shadow-sm focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-200`},null,8,Pe),[[j,w.value]]),c(`div`,Fe,[c(`button`,{type:`button`,class:`rounded-md px-3 py-1.5 text-sm text-gray-600 ring-1 ring-inset ring-gray-300 hover:bg-gray-50`,onClick:R},` Cancel `),c(`button`,{type:`button`,disabled:!w.value.trim(),class:`rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50`,onClick:L},` Upload `,8,Ie)])])):(n(),u(`div`,Le,[c(`input`,{ref_key:`fileInputRef`,ref:D,type:`file`,class:`hidden`,accept:l.value,onChange:I},null,40,Re),c(`button`,{type:`button`,disabled:e.disabled,class:`rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50`,onClick:r[2]||=e=>D.value?.click()},` Upload `,8,ze),c(`button`,{type:`button`,disabled:e.disabled,class:`rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50`,onClick:r[3]||=e=>E.value=!0},` Select from library `,8,Be)])),x.value?(n(),u(`p`,Ve,b(x.value),1)):y(``,!0),e.error?(n(),u(`p`,He,b(e.error),1)):y(``,!0),E.value?(n(),m(he,{key:6,"accepted-type":d.value,onSelect:ee,onClose:r[4]||=e=>E.value=!1},null,8,[`accepted-type`])):y(``,!0)]))}}),We={class:`block text-[13px] font-semibold text-[#3D3D52]`},Ge={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},Ke={key:0,class:`mt-1.5 flex flex-wrap gap-1.5`},qe=[`disabled`,`aria-label`,`onClick`],Je={key:1,class:`mt-1.5`},Ye={class:`inline-flex items-center gap-1 rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-indigo-800`},Xe=[`disabled`],Ze={key:2,class:`mt-0.5 text-xs text-gray-500`},Qe={class:`relative mt-1`},$e=[`id`,`value`,`disabled`,`placeholder`],et={key:0,role:`listbox`,class:`absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-sm shadow-lg ring-1 ring-black/5 focus:outline-none`},tt={key:0,class:`flex items-center gap-2 px-3 py-2 text-gray-400`},nt={key:0,class:`px-3 py-2 text-gray-500`},rt=[`onClick`],it={key:3,class:`mt-1 text-sm text-red-600`,role:`alert`},at=d({__name:`ReferenceSelect`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:r}){let a=e,o=r,s=A(),d=v(),p=_(()=>{let e=a.field.ui_component;return e.component===`typeahead-select`?e:null}),m=_(()=>p.value?.ref??``),h=_(()=>a.field.validation.max_items??null),x=_(()=>R(a.field)===`many`),C=_(()=>!!d.contentTypes[m.value]),T=_(()=>(C.value?d.contentTypes[m.value]:d.taxonomyTypes[m.value])?.fields.find(e=>e.field_type===`text/plain`)?.name??`id`),D=_(()=>x.value?Array.isArray(a.modelValue)?a.modelValue.filter(e=>typeof e==`string`):[]:typeof a.modelValue==`string`?[a.modelValue]:[]),O=_(()=>h.value!==null&&D.value.length>=h.value),k=_(()=>D.value[0]??``),j=t({}),M=t(``),N=t(!1),P=t([]),F=t(!1);function I(){return C.value?`/content/${m.value}`:`/taxonomy/${m.value}`}function L(e){let t=new URLSearchParams({[`filters[${T.value}][like]`]:e,per_page:`10`});return`${I()}?${t.toString()}`}function z(e){return String(e[T.value]??e.id??``)}let ee=w(async e=>{if(e.length<2){P.value=[],F.value=!1;return}N.value=!0,F.value=!0;let t=await s.get(L(e));N.value=!1,t.ok&&(P.value=t.data)},300);function te(e){M.value=e.target.value,ee(M.value)}function ne(){setTimeout(()=>{F.value=!1},150)}function re(){M.value.length>=2&&(F.value=!0)}function ie(e){let t=String(e.id);if(j.value[t]=z(e),M.value=``,P.value=[],F.value=!1,x.value){let e=[...D.value];e.includes(t)||e.push(t),o(`update:modelValue`,e)}else o(`update:modelValue`,t)}function ae(e){x.value?o(`update:modelValue`,D.value.filter(t=>t!==e)):o(`update:modelValue`,null)}return S(async()=>{for(let e of D.value){if(j.value[e])continue;let t=await s.get(`${I()}/${e}`);t.ok?j.value[e]=z(t.data):j.value[e]=e}}),(t,r)=>(n(),u(`div`,null,[c(`label`,We,[g(b(e.field.label)+` `,1),e.field.required?(n(),u(`span`,Ge,`*`)):y(``,!0)]),x.value&&D.value.length?(n(),u(`div`,Ke,[(n(!0),u(f,null,i(D.value,t=>(n(),u(`span`,{key:t,class:`inline-flex items-center gap-1 rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-medium text-indigo-800`},[g(b(j.value[t]??t)+` `,1),c(`button`,{type:`button`,disabled:e.disabled,class:`ml-0.5 flex h-3.5 w-3.5 items-center justify-center rounded-full hover:bg-indigo-200 disabled:cursor-not-allowed`,"aria-label":`Remove ${j.value[t]??t}`,onClick:e=>ae(t)},[...r[2]||=[c(`span`,{"aria-hidden":`true`},`×`,-1)]],8,qe)]))),128))])):!x.value&&D.value.length?(n(),u(`div`,Je,[c(`span`,Ye,[g(b(j.value[k.value]??k.value)+` `,1),c(`button`,{type:`button`,disabled:e.disabled,class:`ml-0.5 flex h-3.5 w-3.5 items-center justify-center rounded-full hover:bg-indigo-200 disabled:cursor-not-allowed`,"aria-label":`Clear selection`,onClick:r[0]||=e=>ae(k.value)},[...r[3]||=[c(`span`,{"aria-hidden":`true`},`×`,-1)]],8,Xe)])])):y(``,!0),h.value===null?y(``,!0):(n(),u(`p`,Ze,b(D.value.length)+` / `+b(h.value)+` selected `,1)),c(`div`,Qe,[c(`input`,{id:e.field.name,type:`text`,value:M.value,disabled:e.disabled||O.value,placeholder:O.value?`Limit reached`:`Search ${e.field.label}…`,class:l([`block w-full rounded-[11px] border px-3.5 py-3 text-sm focus:outline-none focus:ring-[3px]`,e.error?`border-red-300 focus:border-red-400 focus:ring-red-200`:`border-[#E4E3EE] bg-[#FBFBFD] focus:border-indigo-400 focus:bg-white focus:ring-indigo-50`,(e.disabled||O.value)&&`cursor-not-allowed bg-gray-50 opacity-60`]),autocomplete:`off`,onInput:te,onFocus:re,onFocusout:ne},null,42,$e),F.value?(n(),u(`ul`,et,[N.value?(n(),u(`li`,tt,[...r[4]||=[c(`span`,{class:`inline-block h-3.5 w-3.5 animate-spin rounded-full border-2 border-indigo-500 border-t-transparent`,"aria-hidden":`true`},null,-1),g(` Searching… `,-1)]])):(n(),u(f,{key:1},[P.value.length===0?(n(),u(`li`,nt,` No results found for "`+b(M.value)+`" `,1)):y(``,!0),(n(!0),u(f,null,i(P.value,e=>(n(),u(`li`,{key:String(e.id),role:`option`,class:`cursor-pointer px-3 py-2 hover:bg-indigo-50`,onMousedown:r[1]||=E(()=>{},[`prevent`]),onClick:t=>ie(e)},b(z(e)),41,rt))),128))],64))])):y(``,!0)]),e.error?(n(),u(`p`,it,b(e.error),1)):y(``,!0)]))}}),ot={class:`block text-[13px] font-semibold text-[#3D3D52]`},st={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},ct={class:`flex items-center gap-2 border-b border-gray-100 px-3 py-2`},lt={class:`text-xs font-medium text-gray-500`},ut=[`aria-label`],dt={class:`p-3`},ft={class:`mt-1 space-y-2`},pt=[`onDragstart`,`onDragover`,`onDrop`],mt={class:`flex items-center gap-2 border-b border-gray-100 px-3 py-2`},ht={class:`text-xs font-medium text-gray-500`},gt=[`aria-label`,`onClick`],_t={class:`p-3`},vt={key:2,class:`mt-1 text-sm text-red-600`,role:`alert`},yt=d({__name:`ParagraphEmbed`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean},formComponent:{}},emits:[`update:modelValue`],setup(r,{emit:a}){let o=r,s=a,d=_(()=>R(o.field)===`one`),p=_(()=>{let e=o.modelValue;return typeof e==`object`&&e&&!Array.isArray(e)?e:null});function h(){s(`update:modelValue`,{})}function v(){s(`update:modelValue`,null)}function x(e){s(`update:modelValue`,e)}let S=_(()=>Array.isArray(o.modelValue)?o.modelValue:[]),C=t(null),w=t(null);function T(){let e=[...S.value];e.push({order:e.length}),s(`update:modelValue`,e)}function D(e){let t=[...S.value];t.splice(e,1),s(`update:modelValue`,t.map((e,t)=>({...e,order:t})))}function O(e,t){let n=[...S.value];n[e]={...t,order:e},s(`update:modelValue`,n)}function k(e){C.value=e}function A(e){w.value=e}function j(e){if(C.value===null||C.value===e){C.value=null,w.value=null;return}let t=[...S.value],[n]=t.splice(C.value,1);n!==void 0&&(t.splice(e,0,n),s(`update:modelValue`,t.map((e,t)=>({...e,order:t}))),C.value=null,w.value=null)}function M(){C.value=null,w.value=null}return(t,a)=>(n(),u(`div`,null,[c(`label`,ot,[g(b(r.field.label)+` `,1),r.field.required?(n(),u(`span`,st,`*`)):y(``,!0)]),d.value?(n(),u(f,{key:0},[p.value?(n(),u(`div`,{key:0,class:l([`mt-1 rounded-md border border-gray-200 bg-white`,r.disabled&&`opacity-60`])},[c(`div`,ct,[c(`span`,lt,b(r.field.label),1),r.disabled?y(``,!0):(n(),u(`button`,{key:0,type:`button`,class:`ml-auto text-xs text-red-500 hover:text-red-700`,"aria-label":`Remove ${r.field.label}`,onClick:v},` Remove `,8,ut))]),c(`div`,dt,[(n(),m(e(r.formComponent),{"model-value":p.value,disabled:r.disabled,"onUpdate:modelValue":a[0]||=e=>x(e)},null,8,[`model-value`,`disabled`]))])],2)):r.disabled?y(``,!0):(n(),u(`button`,{key:1,type:`button`,class:`mt-2 inline-flex items-center gap-1.5 rounded-md border border-dashed border-gray-300 px-3 py-2 text-sm text-gray-600 hover:border-indigo-400 hover:text-indigo-600`,onClick:h},[a[1]||=c(`span`,{"aria-hidden":`true`},`+`,-1),g(` Add `+b(r.field.label),1)]))],64)):(n(),u(f,{key:1},[c(`div`,ft,[(n(!0),u(f,null,i(S.value,(t,i)=>(n(),u(`div`,{key:i,draggable:`true`,class:l([`rounded-md border bg-white transition-colors`,w.value===i&&C.value!==i?`border-indigo-400 ring-2 ring-indigo-200`:`border-gray-200`,r.disabled&&`opacity-60`]),onDragstart:e=>k(i),onDragover:E(e=>A(i),[`prevent`]),onDrop:E(e=>j(i),[`prevent`]),onDragend:M},[c(`div`,mt,[a[2]||=c(`span`,{class:`cursor-grab select-none text-gray-400`,title:`Drag to reorder`,"aria-hidden":`true`},` ⋮⋮ `,-1),c(`span`,ht,` Item `+b(i+1),1),r.disabled?y(``,!0):(n(),u(`button`,{key:0,type:`button`,class:`ml-auto text-xs text-red-500 hover:text-red-700`,"aria-label":`Remove item ${i+1}`,onClick:e=>D(i)},` Remove `,8,gt))]),c(`div`,_t,[(n(),m(e(r.formComponent),{"model-value":t,disabled:r.disabled,"onUpdate:modelValue":e=>O(i,e)},null,8,[`model-value`,`disabled`,`onUpdate:modelValue`]))])],42,pt))),128))]),r.disabled?y(``,!0):(n(),u(`button`,{key:0,type:`button`,class:`mt-2 inline-flex items-center gap-1.5 rounded-md border border-dashed border-gray-300 px-3 py-2 text-sm text-gray-600 hover:border-indigo-400 hover:text-indigo-600`,onClick:T},[a[3]||=c(`span`,{"aria-hidden":`true`},`+`,-1),g(` Add `+b(r.field.label),1)]))],64)),r.error?(n(),u(`p`,vt,b(r.error),1)):y(``,!0)]))}}),bt={"text/plain":r(F),"text/rich":r(N),integer:r(z),float:r(z),boolean:r(L),date:r(M),image:r(Ue),video:r(Ue),file:r(Ue),enum:r(P),reference:r(at),paragraph:r(yt),programmatic:r(I)};function xt(e){return bt[e.field_type]??bt[`text/plain`]}function St(e){return e.ui_component.component===`paragraph-embed`?{formComponent:wt(e.ui_component.ref)}:{}}var Ct=new Map;function wt(e){let t=Ct.get(e);if(t)return t;let n=r(d({name:`ParagraphForm_${e}`,props:{modelValue:{type:Object,default:()=>({})},disabled:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(t,{emit:n}){let r=v(),i=_(()=>r.paragraphTypes[e]);function a(e,r){n(`update:modelValue`,{...t.modelValue,[e]:r})}return()=>i.value?x(`div`,{class:`space-y-3`},i.value.fields.map(e=>x(xt(e),{key:e.name,field:e,modelValue:(t.modelValue??{})[e.name],disabled:t.disabled,...St(e),"onUpdate:modelValue":t=>a(e.name,t)}))):x(`div`,{class:`text-sm text-gray-400`},`Unknown paragraph type`)}}));return Ct.set(e,n),n}var Tt={key:0,class:`flex items-center justify-center py-20`},Et={key:1,class:`py-12 text-center text-sm text-gray-500`},Dt={key:2},Ot={class:`mb-[22px] flex flex-wrap items-start justify-between gap-4`},kt={class:`mb-1.5 text-[11px] font-bold uppercase tracking-[.06em] text-faint`},At={class:`text-[25px] font-bold tracking-tight text-ink`},jt={class:`flex shrink-0 flex-wrap items-center gap-[9px]`},Mt=[`disabled`],Nt=[`disabled`],Pt=[`disabled`],Ft=[`disabled`],It={key:0,class:`mb-[18px] rounded-2xl border border-card-border bg-white p-5 shadow-[0_1px_2px_rgba(24,24,48,0.04),0_8px_22px_rgba(24,24,48,0.04)]`},Lt={class:`mt-2 flex flex-wrap items-center gap-[10px]`},Rt=[`disabled`],zt={key:0,class:`mt-[9px] text-[12.5px] text-[#D9913C]`},Bt={key:1,class:`mt-1 text-sm text-red-600`,role:`alert`},Vt=[`data-tab`,`onClick`],Ht={class:`space-y-5`},Ut={key:2,class:`mt-4 text-sm text-red-600`,role:`alert`},Wt=d({__name:`ContentFormView`,setup(r){let s=k(),d=T(),x=A(),{can:w}=D(),{errors:E,validate:O,mergeServerErrors:M,clearErrors:N,setError:P}=te(),F=ne(),I=v(),L=re(),R=_(()=>s.params.type),z=_(()=>s.params.id),B=_(()=>I.contentTypes[R.value]),V=_(()=>B.value?.fields??[]),H=_(()=>B.value?.ui.tabs??[]),oe=_(()=>B.value?.only_one===!0),U=_(()=>oe.value?`singleton`:z.value?`edit`:`create`),se=t(null),W=t({}),G=t(!1),ce=t(``),K=t(!1),q=t(!0),J=t(!1),Y=t(``),X=t(!1),Z=t(!1),Q=t(null),le=t(0),ue=_(()=>H.value[le.value]?.name??``),de=t(null),{left:fe,width:pe}=ae(de,ue),me=_(()=>{if(H.value.length===0)return V.value;let e=H.value[le.value];return e?e.fields.map(e=>V.value.find(t=>t.name===e)).filter(e=>e!==void 0):V.value}),he=_(()=>U.value===`edit`&&G.value&&W.value.slug!==ce.value);function ge(e){let t={};oe.value||(t.slug=e?.slug??``);for(let n of V.value)n.field_type!==`programmatic`&&(t[n.name]=e?.[n.name]??ee(n));W.value=t,G.value=e?.published===!0,e?.slug&&(ce.value=String(e.slug))}function _e(e,t){W.value[e]=t}async function ve(e){let t=await x.get(`/content/${R.value}/${e}`);t.ok?(se.value=t.data,ge(t.data)):Y.value=t.error.message}S(async()=>{if(!B.value){q.value=!1;return}U.value===`edit`&&z.value||U.value===`singleton`&&z.value?await ve(z.value):ge(),q.value=!1}),p(z,async(e,t)=>{e!==t&&(q.value=!0,K.value=!1,N(),e?await ve(e):(se.value=null,ge()),q.value=!1)});async function $(e){J.value=!0,Y.value=``;let t={...W.value};e!==void 0&&(t.published=e);let n;if(n=U.value===`create`?await x.post(`/content/${R.value}`,t):U.value===`singleton`?await x.put(`/content/${R.value}`,t):await x.patch(`/content/${R.value}/${z.value}`,t),J.value=!1,!n.ok){let e=n.error;e.code===`PUBLISH_VALIDATION_ERROR`&&e.details?M(e.details):e.code===`SLUG_CONFLICT`?P(`slug`,`This slug is already taken.`):Y.value=e.message;return}se.value=n.data,G.value=n.data.published===!0,ce.value=String(n.data.slug??``),K.value=!1,L.invalidate(R.value),F.success(`Saved.`),U.value===`create`&&d.replace({name:`content-edit`,params:{type:R.value,id:String(n.data.id)}})}function ye(){if(O(V.value,W.value)){if(he.value){Q.value=()=>$(),X.value=!0;return}$()}}function be(){if(O(V.value,W.value,!0)){if(he.value){Q.value=()=>$(!0),X.value=!0;return}$(!0)}}function xe(){$(!1)}function Se(){X.value=!1,Q.value&&=(Q.value(),null)}function Ce(){X.value=!1,Q.value=null}async function we(){Z.value=!1,J.value=!0;let e=await x.del(`/content/${R.value}/${z.value}`);if(J.value=!1,!e.ok){Y.value=e.error.message;return}L.invalidate(R.value),d.push({name:`content-list`,params:{type:R.value}})}let Te=_(()=>{if(U.value===`create`)return`New ${B.value?.label??``}`;if(U.value===`singleton`)return B.value?.label??``;let e=V.value.find(e=>e.field_type===`text/plain`);return(e?String(se.value?.[e.name]??``):``)||(B.value?.label??``)});return(t,r)=>q.value?(n(),u(`div`,Tt,[...r[3]||=[c(`span`,{class:`inline-block h-6 w-6 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent`,"aria-hidden":`true`},null,-1)]])):B.value?(n(),u(`div`,Dt,[c(`div`,Ot,[c(`div`,null,[c(`p`,kt,b(B.value.label),1),c(`h1`,At,b(Te.value),1)]),c(`div`,jt,[o(w)(`content:delete`)&&U.value===`edit`?(n(),u(`button`,{key:0,type:`button`,disabled:J.value,class:`inline-flex items-center gap-1.5 rounded-[10px] border border-[#F3D2D7] bg-white px-[15px] py-2.5 text-[13px] font-semibold text-[#E1495B] transition-colors hover:bg-[#FDF2F4] disabled:cursor-not-allowed disabled:opacity-50`,onClick:r[0]||=e=>Z.value=!0},` Delete `,8,Mt)):y(``,!0),o(w)(`content:edit`)?(n(),u(f,{key:1},[G.value?(n(),u(`button`,{key:0,type:`button`,disabled:J.value,class:`rounded-[10px] border border-[#E4E3EE] bg-white px-4 py-2.5 text-[13px] font-semibold text-[#3D3D52] transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50`,onClick:xe},` Unpublish `,8,Nt)):(n(),u(`button`,{key:1,type:`button`,disabled:J.value,class:`rounded-[10px] border border-[#E4E3EE] bg-white px-4 py-2.5 text-[13px] font-semibold text-[#3D3D52] transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50`,onClick:be},` Publish `,8,Pt)),c(`button`,{type:`button`,disabled:J.value,class:`inline-flex items-center gap-1.5 rounded-[10px] bg-indigo-600 px-4 py-2.5 text-[13px] font-semibold text-white shadow-[0_3px_10px_rgba(91,87,232,0.3)] transition-all hover:bg-indigo-700 hover:shadow-[0_6px_18px_rgba(91,87,232,0.4)] disabled:cursor-not-allowed disabled:opacity-50`,onClick:ye},b(J.value?`Saving…`:`Save draft`),9,Ft)],64)):y(``,!0)])]),oe.value?y(``,!0):(n(),u(`div`,It,[r[4]||=c(`label`,{for:`slug-field`,class:`block text-[13px] font-semibold text-[#3D3D52]`},[g(` Slug `),c(`span`,{class:`ml-0.5 text-red-500`,"aria-hidden":`true`},`*`)],-1),c(`div`,Lt,[a(c(`input`,{id:`slug-field`,"onUpdate:modelValue":r[1]||=e=>W.value.slug=e,type:`text`,disabled:J.value||G.value&&!K.value,class:l([`block min-w-[180px] flex-1 rounded-[10px] border px-[13px] py-[11px] font-mono text-sm focus:outline-none focus:ring-2`,o(E).slug?`border-red-300 focus:border-red-400 focus:ring-red-200`:`border-[#E4E3EE] bg-surface focus:border-indigo-400 focus:ring-indigo-200`,(J.value||G.value&&!K.value)&&`cursor-not-allowed bg-gray-100 opacity-70`]),placeholder:`url-slug`},null,10,Rt),[[j,W.value.slug]]),G.value&&!K.value?(n(),u(`button`,{key:0,type:`button`,class:`shrink-0 rounded-[10px] border border-[#E4E3EE] bg-white px-4 py-2.5 text-sm font-semibold text-[#3D3D52] transition-colors hover:bg-surface`,onClick:r[2]||=e=>K.value=!0},` Edit slug `)):y(``,!0)]),G.value?(n(),u(`p`,zt,` Changing this slug will break existing links. `)):y(``,!0),o(E).slug?(n(),u(`p`,Bt,b(o(E).slug),1)):y(``,!0)])),H.value.length>1?(n(),u(`div`,{key:1,ref_key:`tabBarRef`,ref:de,class:`relative mb-[22px] flex gap-0 overflow-x-auto border-b border-card-border`},[(n(!0),u(f,null,i(H.value,(e,t)=>(n(),u(`button`,{key:e.name,"data-tab":e.name,type:`button`,class:l([`mr-6 whitespace-nowrap bg-none px-0.5 py-[11px] text-sm transition-colors`,le.value===t?`font-semibold text-indigo-600`:`font-medium text-faint hover:text-[#8A8A9E]`]),onClick:e=>le.value=t},b(e.label),11,Vt))),128)),c(`div`,{class:`absolute -bottom-px h-0.5 rounded-full bg-indigo-600 transition-[left,width] duration-300`,style:h({left:`${o(fe)}px`,width:`${o(pe)}px`})},null,4)],512)):y(``,!0),c(`div`,Ht,[(n(!0),u(f,null,i(me.value,t=>(n(),m(e(o(xt)(t)),C({key:t.name,field:t,"model-value":W.value[t.name],error:o(E)[t.name],disabled:J.value},{ref_for:!0},o(St)(t),{"onUpdate:modelValue":e=>_e(t.name,e)}),null,16,[`field`,`model-value`,`error`,`disabled`,`onUpdate:modelValue`]))),128))]),Y.value?(n(),u(`p`,Ut,b(Y.value),1)):y(``,!0),X.value?(n(),m(ie,{key:3,message:`This item is published. Changing its slug will break existing links. Continue?`,"on-confirm":Se,"on-cancel":Ce})):y(``,!0),Z.value?(n(),m(ie,{key:4,message:`Are you sure you want to delete this item? This action cannot be undone.`,"on-confirm":we,"on-cancel":()=>Z.value=!1},null,8,[`on-cancel`])):y(``,!0)])):(n(),u(`div`,Et,` Content type not found. `))}});export{Wt as default};