@bobbykim/manguito-cms-admin 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/dist/assets/{ChangePasswordView-B0wg3O_I.js → ChangePasswordView-BILytTnH.js} +1 -1
  2. package/dist/assets/{ComputedDisplay-C2FpwSB1.js → ComputedDisplay-Df_HvjzX.js} +1 -1
  3. package/dist/assets/ContentFormView-BsTU-zzP.js +1 -0
  4. package/dist/assets/{ContentListView-DM4xggDx.js → ContentListView-CWHYBV23.js} +1 -1
  5. package/dist/assets/{ContentSettingsView-B9UHNleT.js → ContentSettingsView-B9BHm-xg.js} +1 -1
  6. package/dist/assets/{LoginView-DPpdT3Ro.js → LoginView-8tF3SdN4.js} +1 -1
  7. package/dist/assets/MediaDetailView-HfRoV5W8.js +1 -0
  8. package/dist/assets/{MediaLibraryView-Dx9t8r8T.js → MediaLibraryView-COQmdQPB.js} +1 -1
  9. package/dist/assets/{TaxonomyFormView-CZlOjxOY.js → TaxonomyFormView-BibZ2qjc.js} +1 -1
  10. package/dist/assets/{TaxonomyListView-Dkga11a3.js → TaxonomyListView-DKnsY_ab.js} +1 -1
  11. package/dist/assets/{UserFormView-CaOcoKX5.js → UserFormView-DhH0m-Vq.js} +1 -1
  12. package/dist/assets/{UserListView-B6_Hdemt.js → UserListView-Dk3Jv2Z8.js} +1 -1
  13. package/dist/assets/{index-s-2X8XCa.js → index-C-iAN6wp.js} +2 -2
  14. package/dist/assets/{index-B7BOeskw.css → index-ivedPYqQ.css} +1 -1
  15. package/dist/assets/{useNotification-CEPVkHak.js → useNotification-kY57Myug.js} +1 -1
  16. package/dist/index.html +2 -2
  17. package/package.json +2 -2
  18. package/src/components/fields/__tests__/field-registry.test.ts +128 -0
  19. package/src/components/fields/field-registry.ts +116 -0
  20. package/src/views/content/ContentFormView.vue +2 -95
  21. package/src/views/media/MediaDetailView.vue +27 -5
  22. package/dist/assets/ContentFormView-D92O0ICh.js +0 -1
  23. package/dist/assets/MediaDetailView-OUzzhO1B.js +0 -1
@@ -0,0 +1,128 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { createPinia, setActivePinia } from 'pinia'
4
+ import type { ParsedField, ParsedParagraphType } from '@bobbykim/manguito-cms-core'
5
+ import { componentFor, fieldExtraProps, getParagraphForm, clearParagraphFormCache } from '../field-registry'
6
+ import { useSchemaStore } from '../../../stores/schema'
7
+ import ParagraphEmbed from '../ParagraphEmbed.vue'
8
+
9
+ // A paragraph type holding a paragraph field of its own — the one level of
10
+ // nesting ADR core/0005 permits. The nested ParagraphEmbed cannot render its
11
+ // fields without a `formComponent`, and nothing outside this factory supplies one.
12
+
13
+ function paragraphField(name: string, ref: string): ParsedField {
14
+ return {
15
+ name,
16
+ label: name,
17
+ field_type: 'paragraph',
18
+ required: false,
19
+ nullable: true,
20
+ order: 1,
21
+ validation: { required: false },
22
+ db_column: null,
23
+ ui_component: { component: 'paragraph-embed', ref, rel: 'one-to-many' },
24
+ }
25
+ }
26
+
27
+ function textField(name: string): ParsedField {
28
+ return {
29
+ name,
30
+ label: name,
31
+ field_type: 'text/plain',
32
+ required: false,
33
+ nullable: true,
34
+ order: 0,
35
+ validation: { required: false },
36
+ db_column: { column_name: name, column_type: 'varchar', nullable: true },
37
+ ui_component: { component: 'text-input' },
38
+ }
39
+ }
40
+
41
+ const INNER: ParsedParagraphType = {
42
+ schema_type: 'paragraph-type',
43
+ name: 'paragraph--link_item',
44
+ label: 'Link Item',
45
+ source_file: 't.json',
46
+ system_fields: [],
47
+ fields: [textField('link_item_url'), textField('link_item_text')],
48
+ db: { table_name: 'paragraph_link_item' },
49
+ }
50
+
51
+ const OUTER: ParsedParagraphType = {
52
+ schema_type: 'paragraph-type',
53
+ name: 'paragraph--card_image_link',
54
+ label: 'Card',
55
+ source_file: 't.json',
56
+ system_fields: [],
57
+ fields: [
58
+ textField('card_image_link_title'),
59
+ paragraphField('card_image_link_link', 'paragraph--link_item'),
60
+ ],
61
+ db: { table_name: 'paragraph_card_image_link' },
62
+ }
63
+
64
+ beforeEach(() => {
65
+ setActivePinia(createPinia())
66
+ clearParagraphFormCache()
67
+ const store = useSchemaStore()
68
+ store.paragraphTypes = {
69
+ 'paragraph--card_image_link': OUTER,
70
+ 'paragraph--link_item': INNER,
71
+ }
72
+ })
73
+
74
+ describe('componentFor', () => {
75
+ it('maps a paragraph field to ParagraphEmbed', () => {
76
+ expect(componentFor(paragraphField('x', 'paragraph--link_item'))).toBe(ParagraphEmbed)
77
+ })
78
+ })
79
+
80
+ describe('fieldExtraProps', () => {
81
+ it('supplies a formComponent for a paragraph field', () => {
82
+ const extra = fieldExtraProps(paragraphField('x', 'paragraph--link_item'))
83
+ expect(extra['formComponent']).toBeDefined()
84
+ })
85
+
86
+ it('supplies nothing for a non-paragraph field', () => {
87
+ expect(fieldExtraProps(textField('title'))).toEqual({})
88
+ })
89
+ })
90
+
91
+ describe('getParagraphForm — nested paragraphs', () => {
92
+ it('renders the nested paragraph field with a formComponent, not an empty block', () => {
93
+ const OuterForm = getParagraphForm('paragraph--card_image_link')
94
+
95
+ const wrapper = mount(OuterForm, {
96
+ props: {
97
+ modelValue: {
98
+ card_image_link_title: 'A card',
99
+ // One existing nested item — ParagraphEmbed only renders a body for
100
+ // items that exist.
101
+ card_image_link_link: [{ link_item_url: 'https://example.com', order: 0 }],
102
+ },
103
+ },
104
+ })
105
+
106
+ const nested = wrapper.findComponent(ParagraphEmbed)
107
+ expect(nested.exists()).toBe(true)
108
+
109
+ // The regression: without a formComponent the nested embed renders its item
110
+ // header and an empty body, so the block appears with no editable fields.
111
+ expect(nested.props('formComponent')).toBeTruthy()
112
+
113
+ // Proof it actually rendered the inner form: the nested type's own inputs.
114
+ const inputs = nested.findAll('input')
115
+ expect(inputs.length).toBeGreaterThan(0)
116
+ expect(wrapper.html()).toContain('link_item_url')
117
+ })
118
+
119
+ it('memoises the form per paragraph type', () => {
120
+ expect(getParagraphForm('paragraph--link_item')).toBe(getParagraphForm('paragraph--link_item'))
121
+ })
122
+
123
+ it('renders a placeholder when the paragraph type is unknown', () => {
124
+ const Form = getParagraphForm('paragraph--missing')
125
+ const wrapper = mount(Form, { props: { modelValue: {} } })
126
+ expect(wrapper.text()).toContain('Unknown paragraph type')
127
+ })
128
+ })
@@ -0,0 +1,116 @@
1
+ // Maps a parsed field to the component that edits it, and builds the paragraph
2
+ // sub-forms those components need.
3
+ //
4
+ // Lives apart from ContentFormView so the paragraph-form factory can be tested on
5
+ // its own: it is the piece that has to thread `formComponent` down through nested
6
+ // paragraphs, and that wiring is invisible from the outside until it breaks.
7
+ import { computed, defineComponent, h, markRaw } from 'vue'
8
+ import type { Component, PropType } from 'vue'
9
+ import type { ParsedField } from '@bobbykim/manguito-cms-core'
10
+ import { useSchemaStore } from '../../stores/schema'
11
+ import TextInput from './TextInput.vue'
12
+ import RichTextEditor from './RichTextEditor.vue'
13
+ import NumberInput from './NumberInput.vue'
14
+ import BooleanToggle from './BooleanToggle.vue'
15
+ import DatePicker from './DatePicker.vue'
16
+ import MediaUpload from './MediaUpload.vue'
17
+ import EnumSelect from './EnumSelect.vue'
18
+ import ReferenceSelect from './ReferenceSelect.vue'
19
+ import ParagraphEmbed from './ParagraphEmbed.vue'
20
+ import ComputedDisplay from './ComputedDisplay.vue'
21
+
22
+ export const FIELD_COMP: Record<string, Component> = {
23
+ 'text/plain': markRaw(TextInput),
24
+ 'text/rich': markRaw(RichTextEditor),
25
+ integer: markRaw(NumberInput),
26
+ float: markRaw(NumberInput),
27
+ boolean: markRaw(BooleanToggle),
28
+ date: markRaw(DatePicker),
29
+ image: markRaw(MediaUpload),
30
+ video: markRaw(MediaUpload),
31
+ file: markRaw(MediaUpload),
32
+ enum: markRaw(EnumSelect),
33
+ reference: markRaw(ReferenceSelect),
34
+ paragraph: markRaw(ParagraphEmbed),
35
+ programmatic: markRaw(ComputedDisplay),
36
+ }
37
+
38
+ export function componentFor(field: ParsedField): Component {
39
+ return FIELD_COMP[field.field_type] ?? FIELD_COMP['text/plain']!
40
+ }
41
+
42
+ // Props a field's component needs beyond the common set. ParagraphEmbed cannot
43
+ // render anything without a `formComponent`, so every paragraph field — top level
44
+ // or nested inside another paragraph — has to be given one here.
45
+ export function fieldExtraProps(field: ParsedField): Record<string, unknown> {
46
+ if (field.ui_component.component === 'paragraph-embed') {
47
+ return { formComponent: getParagraphForm(field.ui_component.ref) }
48
+ }
49
+ return {}
50
+ }
51
+
52
+ const paragraphFormCache = new Map<string, Component>()
53
+
54
+ /** Reset the memoised paragraph forms. Tests only — the app builds them once. */
55
+ export function clearParagraphFormCache(): void {
56
+ paragraphFormCache.clear()
57
+ }
58
+
59
+ // Builds a paragraph type's edit form as a render function, so no runtime template
60
+ // compiler is needed. Fields are read from the schema store at render time.
61
+ export function getParagraphForm(schemaName: string): Component {
62
+ const cached = paragraphFormCache.get(schemaName)
63
+ if (cached) return cached
64
+
65
+ const comp = markRaw(
66
+ defineComponent({
67
+ name: `ParagraphForm_${schemaName}`,
68
+ props: {
69
+ modelValue: {
70
+ type: Object as PropType<Record<string, unknown>>,
71
+ default: () => ({}),
72
+ },
73
+ disabled: { type: Boolean, default: false },
74
+ },
75
+ emits: ['update:modelValue'],
76
+ setup(props, { emit }) {
77
+ const store = useSchemaStore()
78
+ const schema = computed(() => store.paragraphTypes[schemaName])
79
+
80
+ function update(name: string, val: unknown) {
81
+ emit('update:modelValue', { ...props.modelValue, [name]: val })
82
+ }
83
+
84
+ return () => {
85
+ if (!schema.value) {
86
+ return h('div', { class: 'text-sm text-gray-400' }, 'Unknown paragraph type')
87
+ }
88
+ return h(
89
+ 'div',
90
+ { class: 'space-y-3' },
91
+ schema.value.fields.map((field) => {
92
+ const c = componentFor(field)
93
+ return h(c as Parameters<typeof h>[0], {
94
+ key: field.name,
95
+ field,
96
+ modelValue: (props.modelValue ?? {})[field.name],
97
+ disabled: props.disabled,
98
+ // A paragraph type may hold a paragraph field of its own (one
99
+ // level, per ADR core/0005). Without these the nested
100
+ // ParagraphEmbed has no formComponent and renders an item header
101
+ // with an empty body — the block adds but has no fields.
102
+ // getParagraphForm recurses here; the cache above and the parser's
103
+ // one-level cap keep that finite.
104
+ ...fieldExtraProps(field),
105
+ 'onUpdate:modelValue': (v: unknown) => update(field.name, v),
106
+ })
107
+ })
108
+ )
109
+ }
110
+ },
111
+ })
112
+ )
113
+
114
+ paragraphFormCache.set(schemaName, comp)
115
+ return comp
116
+ }
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed, onMounted, watch, defineComponent, h, markRaw } from 'vue'
3
- import type { Component, PropType } from 'vue'
2
+ import { ref, computed, onMounted, watch } from 'vue'
4
3
  import { useRoute, useRouter } from 'vue-router'
5
4
  import type { ParsedField } from '@bobbykim/manguito-cms-core'
6
5
  import { useApiClient } from '../../composables/useApiClient'
@@ -9,18 +8,9 @@ import { useFormValidation } from '../../composables/useFormValidation'
9
8
  import { useNotification } from '../../composables/useNotification'
10
9
  import { useSchemaStore } from '../../stores/schema'
11
10
  import { useContentStore } from '../../stores/content'
12
- import TextInput from '../../components/fields/TextInput.vue'
13
- import RichTextEditor from '../../components/fields/RichTextEditor.vue'
14
- import NumberInput from '../../components/fields/NumberInput.vue'
15
- import BooleanToggle from '../../components/fields/BooleanToggle.vue'
16
- import DatePicker from '../../components/fields/DatePicker.vue'
17
- import MediaUpload from '../../components/fields/MediaUpload.vue'
18
- import EnumSelect from '../../components/fields/EnumSelect.vue'
19
- import ReferenceSelect from '../../components/fields/ReferenceSelect.vue'
20
- import ParagraphEmbed from '../../components/fields/ParagraphEmbed.vue'
21
- import ComputedDisplay from '../../components/fields/ComputedDisplay.vue'
22
11
  import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
23
12
  import { useTabIndicator } from '../../composables/useTabIndicator'
13
+ import { componentFor, fieldExtraProps } from '../../components/fields/field-registry'
24
14
 
25
15
  const route = useRoute()
26
16
  const router = useRouter()
@@ -88,89 +78,6 @@ const slugChanged = computed(
88
78
  (form.value.slug as string | undefined) !== originalSlug.value
89
79
  )
90
80
 
91
- // ── Field component mapping ───────────────────────────────────────────────────
92
-
93
- const FIELD_COMP: Record<string, Component> = {
94
- 'text/plain': markRaw(TextInput),
95
- 'text/rich': markRaw(RichTextEditor),
96
- integer: markRaw(NumberInput),
97
- float: markRaw(NumberInput),
98
- boolean: markRaw(BooleanToggle),
99
- date: markRaw(DatePicker),
100
- image: markRaw(MediaUpload),
101
- video: markRaw(MediaUpload),
102
- file: markRaw(MediaUpload),
103
- enum: markRaw(EnumSelect),
104
- reference: markRaw(ReferenceSelect),
105
- paragraph: markRaw(ParagraphEmbed),
106
- programmatic: markRaw(ComputedDisplay),
107
- }
108
-
109
- function componentFor(field: ParsedField): Component {
110
- return FIELD_COMP[field.field_type] ?? FIELD_COMP['text/plain']!
111
- }
112
-
113
- // ── Paragraph form factory (render function — no runtime compiler needed) ─────
114
-
115
- const paragraphFormCache = new Map<string, Component>()
116
-
117
- function getParagraphForm(schemaName: string): Component {
118
- if (paragraphFormCache.has(schemaName)) {
119
- return paragraphFormCache.get(schemaName)!
120
- }
121
-
122
- const comp = markRaw(
123
- defineComponent({
124
- name: `ParagraphForm_${schemaName}`,
125
- props: {
126
- modelValue: {
127
- type: Object as PropType<Record<string, unknown>>,
128
- default: () => ({}),
129
- },
130
- disabled: { type: Boolean, default: false },
131
- },
132
- emits: ['update:modelValue'],
133
- setup(props, { emit }) {
134
- const store = useSchemaStore()
135
- const schema = computed(() => store.paragraphTypes[schemaName])
136
-
137
- function update(name: string, val: unknown) {
138
- emit('update:modelValue', { ...props.modelValue, [name]: val })
139
- }
140
-
141
- return () => {
142
- if (!schema.value) return h('div', { class: 'text-sm text-gray-400' }, 'Unknown paragraph type')
143
- return h(
144
- 'div',
145
- { class: 'space-y-3' },
146
- schema.value.fields.map(field => {
147
- const c = FIELD_COMP[field.field_type] ?? FIELD_COMP['text/plain']!
148
- return h(c as Parameters<typeof h>[0], {
149
- key: field.name,
150
- field,
151
- modelValue: (props.modelValue ?? {})[field.name],
152
- disabled: props.disabled,
153
- 'onUpdate:modelValue': (v: unknown) => update(field.name, v),
154
- })
155
- })
156
- )
157
- }
158
- },
159
- })
160
- )
161
-
162
- paragraphFormCache.set(schemaName, comp)
163
- return comp
164
- }
165
-
166
- // Extra props injected into paragraph fields.
167
- function fieldExtraProps(field: ParsedField): Record<string, unknown> {
168
- if (field.ui_component.component === 'paragraph-embed') {
169
- return { formComponent: getParagraphForm(field.ui_component.ref) }
170
- }
171
- return {}
172
- }
173
-
174
81
  // ── Form helpers ──────────────────────────────────────────────────────────────
175
82
 
176
83
  function defaultForField(field: ParsedField): unknown {
@@ -24,6 +24,8 @@ const savingAlt = ref(false)
24
24
  const deleting = ref(false)
25
25
  const showDeleteConfirm = ref(false)
26
26
 
27
+ const isPdf = computed(() => item.value?.mime_type === 'application/pdf')
28
+
27
29
  function formatSize(bytes: number): string {
28
30
  if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`
29
31
  if (bytes >= 1_000) return `${Math.round(bytes / 1_000)} KB`
@@ -123,12 +125,32 @@ const deleteTooltip = computed(() =>
123
125
  :alt="item.alt ?? ''"
124
126
  class="max-h-96 w-full object-contain"
125
127
  />
126
- <div
127
- v-else
128
- class="flex h-48 items-center justify-center text-6xl text-gray-300"
129
- aria-hidden="true"
128
+ <!-- Videos play inline; the CSP already allows media from storage hosts. -->
129
+ <video
130
+ v-else-if="item.type === 'video'"
131
+ :src="item.url"
132
+ controls
133
+ preload="metadata"
134
+ class="max-h-96 w-full bg-black object-contain"
130
135
  >
131
- {{ item.type === 'video' ? '▶' : '📄' }}
136
+ Your browser cannot play this video.
137
+ </video>
138
+ <!--
139
+ PDFs and other files are opened in a new tab rather than embedded: an
140
+ <iframe> to the storage host would need a frame-src CSP exception on
141
+ every admin route, and a link needs none.
142
+ -->
143
+ <div v-else class="flex h-48 flex-col items-center justify-center gap-3">
144
+ <div class="text-6xl text-gray-300" aria-hidden="true">📄</div>
145
+ <a
146
+ :href="item.url"
147
+ target="_blank"
148
+ rel="noopener noreferrer"
149
+ class="inline-flex items-center gap-1.5 rounded-md border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-sm font-medium text-indigo-700 hover:border-indigo-300 hover:bg-indigo-100"
150
+ >
151
+ {{ isPdf ? 'Open PDF in new tab' : 'Open file in new tab' }}
152
+ <span aria-hidden="true">↗</span>
153
+ </a>
132
154
  </div>
133
155
  </div>
134
156
 
@@ -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 ee,u as A}from"./index-s-2X8XCa.js";import{a as j,c as M,i as N,n as P,o as F,r as te,s as ne,t as I}from"./ComputedDisplay-C2FpwSB1.js";import{t as re}from"./useNotification-CEPVkHak.js";import{t as ie}from"./content--2XYr_UQ.js";import{t as ae}from"./ConfirmDialog-B7nxdDge.js";import{t as oe}from"./useTabIndicator-CvEsGBJh.js";var se={class:`flex h-full max-h-144 w-full max-w-3xl flex-col overflow-hidden rounded-lg bg-white shadow-xl`},L={class:`flex items-center justify-between border-b border-gray-200 px-5 py-4`},ce={key:0,class:`flex gap-1 border-b border-gray-200 px-5 pt-3`},le=[`onClick`],ue={key:1,class:`border-b border-gray-200 px-5 py-2 text-sm text-gray-500`},de={class:`font-medium text-gray-700`},fe={class:`flex-1 overflow-y-auto p-5`},pe={key:0,class:`flex items-center justify-center py-16`},me={key:1,class:`flex flex-col items-center justify-center py-16 text-gray-400`},R={key:2,class:`grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5`},z=[`aria-pressed`,`aria-label`,`onClick`],B=[`src`,`alt`],V={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`},H={class:`text-3xl`},he={class:`max-w-full truncate px-1 text-xs`},U={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`},W={key:2,class:`flex items-center justify-center gap-3 border-t border-gray-100 px-5 py-2`},G=[`disabled`],K={class:`text-xs text-gray-500`},ge=[`disabled`],q={class:`flex justify-end gap-3 border-t border-gray-200 px-5 py-4`},J=[`disabled`],Y=d({__name:`MediaSelectModal`,props:{acceptedType:{}},emits:[`select`,`close`],setup(e,{emit:r}){let a=e,o=r,d=ee(),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 A(){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,A()}),p(T,()=>{A()}),S(()=>{A()});function j(){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`,se,[c(`div`,L,[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`,ce,[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,le)),64))])):(n(),u(`div`,ue,[r[7]||=g(` Showing: `,-1),c(`span`,de,b(h.find(t=>t.value===e.acceptedType)?.label??e.acceptedType),1)])),c(`div`,fe,[w.value?(n(),u(`div`,pe,[...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`,me,[...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`,R,[(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,B)):(n(),u(`div`,V,[c(`span`,H,b(e.type===`video`?`▶`:`📄`),1),c(`span`,he,b(e.url.split(`/`).pop()),1)])),O.value?.id===e.id?(n(),u(`div`,U,` ✓ `)):y(``,!0)],10,z))),128))]))]),D.value>1?(n(),u(`div`,W,[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,G),c(`span`,K,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,ge)])):y(``,!0),c(`div`,q,[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:j},` Select `,8,J)])])])]))}}),X={class:`block text-[13px] font-semibold text-[#3D3D52]`},Z={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},_e={key:0,class:`mt-1 flex flex-col gap-2 rounded-md border border-gray-200 p-3`},Q={class:`flex items-center gap-3`},ve=[`src`,`alt`],ye={key:1,class:`flex h-16 w-16 items-center justify-center rounded bg-gray-100 text-2xl text-gray-400`,"aria-hidden":`true`},be={class:`min-w-0 flex-1`},xe={class:`truncate text-sm font-medium text-gray-800`},Se={class:`text-xs text-gray-500`},Ce={key:0},we=[`for`],Te=[`id`],Ee={key:1,class:`mt-1 rounded-md border border-gray-200 p-3`},De={class:`flex items-center gap-2`},Oe={class:`h-2 flex-1 overflow-hidden rounded-full bg-gray-100`},ke={class:`w-8 shrink-0 text-right text-xs text-gray-500`},Ae={key:2,class:`mt-1 rounded-md border border-gray-200 p-3`},je={class:`mb-2 truncate text-[13px] font-semibold text-[#3D3D52]`},Me=[`for`],Ne=[`id`],$={class:`mt-3 flex justify-end gap-2`},Pe=[`disabled`],Fe={key:3,class:`mt-1 flex gap-2`},Ie=[`accept`],Le=[`disabled`],Re=[`disabled`],ze={key:4,class:`mt-1 text-sm text-red-600`,role:`alert`},Be={key:5,class:`mt-1 text-sm text-red-600`,role:`alert`},Ve=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 ee(e){return e.split(`/`).pop()??e}function j(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 M(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 N(e){p.value=`uploading`,v.value=0,x.value=``;try{let t=w.value.trim(),n=s.presignedUploads?await M(e,t):await j(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 P(){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 F(e){let t=e.target.files?.[0];t&&(C.value=t,w.value=``,e.target.value=``,f.value?p.value=`alt-collect`:N(t))}function te(){C.value&&(f.value&&!w.value.trim()||N(C.value))}function ne(){C.value=null,w.value=``,p.value=`idle`,x.value=``}function I(){T.value=null,w.value=``,o(`update:modelValue`,null)}function re(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`,X,[g(b(e.field.label)+` `,1),e.field.required?(n(),u(`span`,Z,`*`)):y(``,!0)]),T.value?(n(),u(`div`,_e,[c(`div`,Q,[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,ve)):(n(),u(`div`,ye,b(T.value.type===`video`?`▶`:`📄`),1)),c(`div`,be,[c(`p`,xe,b(ee(T.value.url)),1),c(`p`,Se,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:I},` × `))]),T.value.type===`image`&&!e.disabled?(n(),u(`div`,Ce,[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,we),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:P},null,40,Te),[[A,w.value]])])):y(``,!0)])):p.value===`uploading`?(n(),u(`div`,Ee,[r[6]||=c(`p`,{class:`mb-1.5 text-xs text-gray-500`},`Uploading…`,-1),c(`div`,De,[c(`div`,Oe,[c(`div`,{class:`h-2 rounded-full bg-indigo-600 transition-all duration-150`,style:h({width:`${v.value}%`})},null,4)]),c(`span`,ke,b(v.value)+`% `,1)])])):p.value===`alt-collect`?(n(),u(`div`,Ae,[c(`p`,je,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,Me),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,Ne),[[A,w.value]]),c(`div`,$,[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:ne},` 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:te},` Upload `,8,Pe)])])):(n(),u(`div`,Fe,[c(`input`,{ref_key:`fileInputRef`,ref:D,type:`file`,class:`hidden`,accept:l.value,onChange:F},null,40,Ie),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,Le),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,Re)])),x.value?(n(),u(`p`,ze,b(x.value),1)):y(``,!0),e.error?(n(),u(`p`,Be,b(e.error),1)):y(``,!0),E.value?(n(),m(Y,{key:6,"accepted-type":d.value,onSelect:re,onClose:r[4]||=e=>E.value=!1},null,8,[`accepted-type`])):y(``,!0)]))}}),He={class:`block text-[13px] font-semibold text-[#3D3D52]`},Ue={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},We={key:0,class:`mt-1.5 flex flex-wrap gap-1.5`},Ge=[`disabled`,`aria-label`,`onClick`],Ke={key:1,class:`mt-1.5`},qe={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`},Je=[`disabled`],Ye={key:2,class:`mt-0.5 text-xs text-gray-500`},Xe={class:`relative mt-1`},Ze=[`id`,`value`,`disabled`,`placeholder`],Qe={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`},$e={key:0,class:`flex items-center gap-2 px-3 py-2 text-gray-400`},et={key:0,class:`px-3 py-2 text-gray-500`},tt=[`onClick`],nt={key:3,class:`mt-1 text-sm text-red-600`,role:`alert`},rt=d({__name:`ReferenceSelect`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:r}){let a=e,o=r,s=ee(),d=v(),p=_(()=>{let e=a.field.ui_component;return e.component===`typeahead-select`?e:null}),m=_(()=>p.value?.ref??``),h=_(()=>p.value?.rel??`one-to-one`),x=_(()=>a.field.validation.max_items??null),C=_(()=>h.value!==`one-to-one`),T=_(()=>!!d.contentTypes[m.value]),D=_(()=>(T.value?d.contentTypes[m.value]:d.taxonomyTypes[m.value])?.fields.find(e=>e.field_type===`text/plain`)?.name??`id`),O=_(()=>C.value?Array.isArray(a.modelValue)?a.modelValue.filter(e=>typeof e==`string`):[]:typeof a.modelValue==`string`?[a.modelValue]:[]),k=_(()=>x.value!==null&&O.value.length>=x.value),A=_(()=>O.value[0]??``),j=t({}),M=t(``),N=t(!1),P=t([]),F=t(!1);function te(){return T.value?`/content/${m.value}`:`/taxonomy/${m.value}`}function ne(e){let t=new URLSearchParams({[`filters[${D.value}][like]`]:e,per_page:`10`});return`${te()}?${t.toString()}`}function I(e){return String(e[D.value]??e.id??``)}let re=w(async e=>{if(e.length<2){P.value=[],F.value=!1;return}N.value=!0,F.value=!0;let t=await s.get(ne(e));N.value=!1,t.ok&&(P.value=t.data)},300);function ie(e){M.value=e.target.value,re(M.value)}function ae(){setTimeout(()=>{F.value=!1},150)}function oe(){M.value.length>=2&&(F.value=!0)}function se(e){let t=String(e.id);if(j.value[t]=I(e),M.value=``,P.value=[],F.value=!1,C.value){let e=[...O.value];e.includes(t)||e.push(t),o(`update:modelValue`,e)}else o(`update:modelValue`,t)}function L(e){C.value?o(`update:modelValue`,O.value.filter(t=>t!==e)):o(`update:modelValue`,null)}return S(async()=>{for(let e of O.value){if(j.value[e])continue;let t=await s.get(`${te()}/${e}`);t.ok?j.value[e]=I(t.data):j.value[e]=e}}),(t,r)=>(n(),u(`div`,null,[c(`label`,He,[g(b(e.field.label)+` `,1),e.field.required?(n(),u(`span`,Ue,`*`)):y(``,!0)]),C.value&&O.value.length?(n(),u(`div`,We,[(n(!0),u(f,null,i(O.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=>L(t)},[...r[2]||=[c(`span`,{"aria-hidden":`true`},`×`,-1)]],8,Ge)]))),128))])):!C.value&&O.value.length?(n(),u(`div`,Ke,[c(`span`,qe,[g(b(j.value[A.value]??A.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=>L(A.value)},[...r[3]||=[c(`span`,{"aria-hidden":`true`},`×`,-1)]],8,Je)])])):y(``,!0),x.value===null?y(``,!0):(n(),u(`p`,Ye,b(O.value.length)+` / `+b(x.value)+` selected `,1)),c(`div`,Xe,[c(`input`,{id:e.field.name,type:`text`,value:M.value,disabled:e.disabled||k.value,placeholder:k.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||k.value)&&`cursor-not-allowed bg-gray-50 opacity-60`]),autocomplete:`off`,onInput:ie,onFocus:oe,onFocusout:ae},null,42,Ze),F.value?(n(),u(`ul`,Qe,[N.value?(n(),u(`li`,$e,[...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`,et,` 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=>se(e)},b(I(e)),41,tt))),128))],64))])):y(``,!0)]),e.error?(n(),u(`p`,nt,b(e.error),1)):y(``,!0)]))}}),it={class:`block text-[13px] font-semibold text-[#3D3D52]`},at={key:0,class:`ml-0.5 text-red-500`,"aria-hidden":`true`},ot={class:`mt-1 space-y-2`},st=[`onDragstart`,`onDragover`,`onDrop`],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`,`onClick`],dt={class:`p-3`},ft={key:1,class:`mt-1 text-sm text-red-600`,role:`alert`},pt=d({__name:`ParagraphEmbed`,props:{field:{},modelValue:{},error:{},disabled:{type:Boolean},formComponent:{}},emits:[`update:modelValue`],setup(r,{emit:a}){let o=r,s=a,d=_(()=>Array.isArray(o.modelValue)?o.modelValue:[]),p=t(null),h=t(null);function v(){let e=[...d.value];e.push({order:e.length}),s(`update:modelValue`,e)}function x(e){let t=[...d.value];t.splice(e,1),s(`update:modelValue`,t.map((e,t)=>({...e,order:t})))}function S(e,t){let n=[...d.value];n[e]={...t,order:e},s(`update:modelValue`,n)}function C(e){p.value=e}function w(e){h.value=e}function T(e){if(p.value===null||p.value===e){p.value=null,h.value=null;return}let t=[...d.value],[n]=t.splice(p.value,1);n!==void 0&&(t.splice(e,0,n),s(`update:modelValue`,t.map((e,t)=>({...e,order:t}))),p.value=null,h.value=null)}function D(){p.value=null,h.value=null}return(t,a)=>(n(),u(`div`,null,[c(`label`,it,[g(b(r.field.label)+` `,1),r.field.required?(n(),u(`span`,at,`*`)):y(``,!0)]),c(`div`,ot,[(n(!0),u(f,null,i(d.value,(t,i)=>(n(),u(`div`,{key:i,draggable:`true`,class:l([`rounded-md border bg-white transition-colors`,h.value===i&&p.value!==i?`border-indigo-400 ring-2 ring-indigo-200`:`border-gray-200`,r.disabled&&`opacity-60`]),onDragstart:e=>C(i),onDragover:E(e=>w(i),[`prevent`]),onDrop:E(e=>T(i),[`prevent`]),onDragend:D},[c(`div`,ct,[a[0]||=c(`span`,{class:`cursor-grab select-none text-gray-400`,title:`Drag to reorder`,"aria-hidden":`true`},` ⋮⋮ `,-1),c(`span`,lt,` 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=>x(i)},` Remove `,8,ut))]),c(`div`,dt,[(n(),m(e(r.formComponent),{"model-value":t,disabled:r.disabled,"onUpdate:modelValue":e=>S(i,e)},null,8,[`model-value`,`disabled`,`onUpdate:modelValue`]))])],42,st))),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:v},[a[1]||=c(`span`,{"aria-hidden":`true`},`+`,-1),g(` Add `+b(r.field.label),1)])),r.error?(n(),u(`p`,ft,b(r.error),1)):y(``,!0)]))}}),mt={key:0,class:`flex items-center justify-center py-20`},ht={key:1,class:`py-12 text-center text-sm text-gray-500`},gt={key:2},_t={class:`mb-[22px] flex flex-wrap items-start justify-between gap-4`},vt={class:`mb-1.5 text-[11px] font-bold uppercase tracking-[.06em] text-faint`},yt={class:`text-[25px] font-bold tracking-tight text-ink`},bt={class:`flex shrink-0 flex-wrap items-center gap-[9px]`},xt=[`disabled`],St=[`disabled`],Ct=[`disabled`],wt=[`disabled`],Tt={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)]`},Et={class:`mt-2 flex flex-wrap items-center gap-[10px]`},Dt=[`disabled`],Ot={key:0,class:`mt-[9px] text-[12.5px] text-[#D9913C]`},kt={key:1,class:`mt-1 text-sm text-red-600`,role:`alert`},At=[`data-tab`,`onClick`],jt={class:`space-y-5`},Mt={key:2,class:`mt-4 text-sm text-red-600`,role:`alert`},Nt=d({__name:`ContentFormView`,setup(s){let w=k(),E=T(),O=ee(),{can:se}=D(),{errors:L,validate:ce,mergeServerErrors:le,clearErrors:ue,setError:de}=M(),fe=re(),pe=v(),me=ie(),R=_(()=>w.params.type),z=_(()=>w.params.id),B=_(()=>pe.contentTypes[R.value]),V=_(()=>B.value?.fields??[]),H=_(()=>B.value?.ui.tabs??[]),he=_(()=>B.value?.only_one===!0),U=_(()=>he.value?`singleton`:z.value?`edit`:`create`),W=t(null),G=t({}),K=t(!1),ge=t(``),q=t(!1),J=t(!0),Y=t(!1),X=t(``),Z=t(!1),_e=t(!1),Q=t(null),ve=t(0),ye=_(()=>H.value[ve.value]?.name??``),be=t(null),{left:xe,width:Se}=oe(be,ye),Ce=_(()=>{if(H.value.length===0)return V.value;let e=H.value[ve.value];return e?e.fields.map(e=>V.value.find(t=>t.name===e)).filter(e=>e!==void 0):V.value}),we=_(()=>U.value===`edit`&&K.value&&G.value.slug!==ge.value),Te={"text/plain":r(ne),"text/rich":r(F),integer:r(j),float:r(j),boolean:r(N),date:r(te),image:r(Ve),video:r(Ve),file:r(Ve),enum:r(P),reference:r(rt),paragraph:r(pt),programmatic:r(I)};function Ee(e){return Te[e.field_type]??Te[`text/plain`]}let De=new Map;function Oe(e){if(De.has(e))return De.get(e);let t=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(Te[e.field_type]??Te[`text/plain`],{key:e.name,field:e,modelValue:(t.modelValue??{})[e.name],disabled:t.disabled,"onUpdate:modelValue":t=>a(e.name,t)}))):x(`div`,{class:`text-sm text-gray-400`},`Unknown paragraph type`)}}));return De.set(e,t),t}function ke(e){return e.ui_component.component===`paragraph-embed`?{formComponent:Oe(e.ui_component.ref)}:{}}function Ae(e){switch(e.field_type){case`text/plain`:case`text/rich`:return``;case`integer`:case`float`:return null;case`boolean`:return!1;case`date`:return null;case`image`:case`video`:case`file`:return null;case`enum`:return``;case`reference`:return(e.ui_component.component===`typeahead-select`?e.ui_component.rel:`one-to-one`)===`one-to-one`?null:[];case`paragraph`:return[];default:return null}}function je(e){let t={};he.value||(t.slug=e?.slug??``);for(let n of V.value)n.field_type!==`programmatic`&&(t[n.name]=e?.[n.name]??Ae(n));G.value=t,K.value=e?.published===!0,e?.slug&&(ge.value=String(e.slug))}function Me(e,t){G.value[e]=t}async function Ne(e){let t=await O.get(`/content/${R.value}/${e}`);t.ok?(W.value=t.data,je(t.data)):X.value=t.error.message}S(async()=>{if(!B.value){J.value=!1;return}U.value===`edit`&&z.value||U.value===`singleton`&&z.value?await Ne(z.value):je(),J.value=!1}),p(z,async(e,t)=>{e!==t&&(J.value=!0,q.value=!1,ue(),e?await Ne(e):(W.value=null,je()),J.value=!1)});async function $(e){Y.value=!0,X.value=``;let t={...G.value};e!==void 0&&(t.published=e);let n;if(n=U.value===`create`?await O.post(`/content/${R.value}`,t):U.value===`singleton`?await O.put(`/content/${R.value}`,t):await O.patch(`/content/${R.value}/${z.value}`,t),Y.value=!1,!n.ok){let e=n.error;e.code===`PUBLISH_VALIDATION_ERROR`&&e.details?le(e.details):e.code===`SLUG_CONFLICT`?de(`slug`,`This slug is already taken.`):X.value=e.message;return}W.value=n.data,K.value=n.data.published===!0,ge.value=String(n.data.slug??``),q.value=!1,me.invalidate(R.value),fe.success(`Saved.`),U.value===`create`&&E.replace({name:`content-edit`,params:{type:R.value,id:String(n.data.id)}})}function Pe(){if(ce(V.value,G.value)){if(we.value){Q.value=()=>$(),Z.value=!0;return}$()}}function Fe(){if(ce(V.value,G.value,!0)){if(we.value){Q.value=()=>$(!0),Z.value=!0;return}$(!0)}}function Ie(){$(!1)}function Le(){Z.value=!1,Q.value&&=(Q.value(),null)}function Re(){Z.value=!1,Q.value=null}async function ze(){_e.value=!1,Y.value=!0;let e=await O.del(`/content/${R.value}/${z.value}`);if(Y.value=!1,!e.ok){X.value=e.error.message;return}me.invalidate(R.value),E.push({name:`content-list`,params:{type:R.value}})}let Be=_(()=>{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(W.value?.[e.name]??``):``)||(B.value?.label??``)});return(t,r)=>J.value?(n(),u(`div`,mt,[...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`,gt,[c(`div`,_t,[c(`div`,null,[c(`p`,vt,b(B.value.label),1),c(`h1`,yt,b(Be.value),1)]),c(`div`,bt,[o(se)(`content:delete`)&&U.value===`edit`?(n(),u(`button`,{key:0,type:`button`,disabled:Y.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=>_e.value=!0},` Delete `,8,xt)):y(``,!0),o(se)(`content:edit`)?(n(),u(f,{key:1},[K.value?(n(),u(`button`,{key:0,type:`button`,disabled:Y.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:Ie},` Unpublish `,8,St)):(n(),u(`button`,{key:1,type:`button`,disabled:Y.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:Fe},` Publish `,8,Ct)),c(`button`,{type:`button`,disabled:Y.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:Pe},b(Y.value?`Saving…`:`Save draft`),9,wt)],64)):y(``,!0)])]),he.value?y(``,!0):(n(),u(`div`,Tt,[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`,Et,[a(c(`input`,{id:`slug-field`,"onUpdate:modelValue":r[1]||=e=>G.value.slug=e,type:`text`,disabled:Y.value||K.value&&!q.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(L).slug?`border-red-300 focus:border-red-400 focus:ring-red-200`:`border-[#E4E3EE] bg-surface focus:border-indigo-400 focus:ring-indigo-200`,(Y.value||K.value&&!q.value)&&`cursor-not-allowed bg-gray-100 opacity-70`]),placeholder:`url-slug`},null,10,Dt),[[A,G.value.slug]]),K.value&&!q.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=>q.value=!0},` Edit slug `)):y(``,!0)]),K.value?(n(),u(`p`,Ot,` Changing this slug will break existing links. `)):y(``,!0),o(L).slug?(n(),u(`p`,kt,b(o(L).slug),1)):y(``,!0)])),H.value.length>1?(n(),u(`div`,{key:1,ref_key:`tabBarRef`,ref:be,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`,ve.value===t?`font-semibold text-indigo-600`:`font-medium text-faint hover:text-[#8A8A9E]`]),onClick:e=>ve.value=t},b(e.label),11,At))),128)),c(`div`,{class:`absolute -bottom-px h-0.5 rounded-full bg-indigo-600 transition-[left,width] duration-300`,style:h({left:`${o(xe)}px`,width:`${o(Se)}px`})},null,4)],512)):y(``,!0),c(`div`,jt,[(n(!0),u(f,null,i(Ce.value,t=>(n(),m(e(Ee(t)),C({key:t.name,field:t,"model-value":G.value[t.name],error:o(L)[t.name],disabled:Y.value},{ref_for:!0},ke(t),{"onUpdate:modelValue":e=>Me(t.name,e)}),null,16,[`field`,`model-value`,`error`,`disabled`,`onUpdate:modelValue`]))),128))]),X.value?(n(),u(`p`,Mt,b(X.value),1)):y(``,!0),Z.value?(n(),m(ae,{key:3,message:`This item is published. Changing its slug will break existing links. Continue?`,"on-confirm":Le,"on-cancel":Re})):y(``,!0),_e.value?(n(),m(ae,{key:4,message:`Are you sure you want to delete this item? This action cannot be undone.`,"on-confirm":ze,"on-cancel":()=>_e.value=!1},null,8,[`on-cancel`])):y(``,!0)])):(n(),u(`div`,ht,` Content type not found. `))}});export{Nt as default};
@@ -1 +0,0 @@
1
- import{B as e,E as t,P as n,W as r,c as i,d as a,g as o,l as s,s as c,u as l,ut as u,w as d}from"./schema-B-RKX_ep.js";import{c as f,n as p,s as m,t as h,u as g}from"./index-s-2X8XCa.js";import{t as _}from"./useNotification-CEPVkHak.js";import{t as v}from"./ConfirmDialog-B7nxdDge.js";import{t as y}from"./media-DvQfZD4o.js";var b={key:0,class:`flex items-center justify-center py-20`},ee={key:1,class:`py-12 text-center text-sm text-gray-500`},te={key:2,class:`mx-auto max-w-2xl space-y-6`},ne={class:`overflow-hidden rounded-2xl border border-card-border shadow-[0_1px_2px_rgba(24,24,48,0.04),0_8px_22px_rgba(24,24,48,0.04)] bg-white`},re=[`src`,`alt`],x={key:1,class:`flex h-48 items-center justify-center text-6xl text-gray-300`,"aria-hidden":`true`},S={class:`rounded-2xl border border-card-border shadow-[0_1px_2px_rgba(24,24,48,0.04),0_8px_22px_rgba(24,24,48,0.04)] bg-white p-5`},C={class:`grid grid-cols-2 gap-x-6 gap-y-3 text-sm`},w={class:`mt-0.5 font-medium text-gray-800`},T={class:`mt-0.5 font-medium text-gray-800`},E={key:0},D={class:`mt-0.5 font-medium text-gray-800`},O={key:1},k={class:`mt-0.5 font-medium text-gray-800`},A={class:`mt-0.5 font-medium text-gray-800`},j={class:`mt-0.5 font-medium text-gray-800`},M={class:`rounded-2xl border border-card-border shadow-[0_1px_2px_rgba(24,24,48,0.04),0_8px_22px_rgba(24,24,48,0.04)] bg-white p-5`},N=[`disabled`],P={class:`mt-3 flex justify-end`},F=[`disabled`],ie={key:0,class:`rounded-lg border border-red-200 bg-white p-5`},I=[`title`],L=[`disabled`],R={key:0,class:`mt-2 text-xs text-gray-400`},z=o({__name:`MediaDetailView`,setup(o){let z=m(),B=f(),V=h(),{can:H}=p(),U=_(),W=y(),G=c(()=>z.params.id),K=e(null),q=e(!0),J=e(``),Y=e(!1),X=e(!1),Z=e(!1);function ae(e){return e>=1e6?`${(e/1e6).toFixed(1)} MB`:e>=1e3?`${Math.round(e/1e3)} KB`:`${e} B`}function oe(e){return e?new Date(String(e)).toLocaleString(void 0,{year:`numeric`,month:`short`,day:`numeric`,hour:`2-digit`,minute:`2-digit`}):`—`}d(async()=>{let e=W.items.get(G.value);if(e){K.value=e,J.value=e.alt??``,q.value=!1;return}let t=await V.get(`/media/${G.value}`);q.value=!1,t.ok&&(K.value=t.data,J.value=t.data.alt??``,W.setItem(t.data))});async function se(){if(!K.value)return;Y.value=!0;let e=await V.patch(`/media/${K.value.id}`,{alt:J.value.trim()});Y.value=!1,e.ok?(K.value=e.data,W.setItem(e.data),U.success(`Alt text saved.`)):U.error(e.error.message)}async function ce(){if(!K.value)return;Z.value=!1,X.value=!0;let e=await V.del(`/media/${K.value.id}`);if(X.value=!1,!e.ok){U.error(e.error.message);return}W.removeItem(K.value.id),B.push({name:`media-library`})}let Q=c(()=>K.value!==null&&K.value.reference_count===0),$=c(()=>Q.value?void 0:`This file is referenced by ${K.value?.reference_count??0} item${(K.value?.reference_count??0)===1?``:`s`} and cannot be deleted.`);return(e,o)=>q.value?(t(),a(`div`,b,[...o[3]||=[i(`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)]])):K.value?(t(),a(`div`,te,[i(`button`,{type:`button`,class:`text-sm text-indigo-600 hover:text-indigo-800`,onClick:o[0]||=e=>r(B).back()},` ← Back to library `),i(`div`,ne,[K.value.type===`image`?(t(),a(`img`,{key:0,src:K.value.url,alt:K.value.alt??``,class:`max-h-96 w-full object-contain`},null,8,re)):(t(),a(`div`,x,u(K.value.type===`video`?`▶`:`📄`),1))]),i(`div`,S,[o[10]||=i(`h2`,{class:`mb-4 text-sm font-semibold uppercase tracking-wide text-gray-500`},`Metadata`,-1),i(`dl`,C,[i(`div`,null,[o[4]||=i(`dt`,{class:`text-gray-500`},`Type`,-1),i(`dd`,w,u(K.value.mime_type),1)]),i(`div`,null,[o[5]||=i(`dt`,{class:`text-gray-500`},`Size`,-1),i(`dd`,T,u(ae(K.value.file_size)),1)]),K.value.width&&K.value.height?(t(),a(`div`,E,[o[6]||=i(`dt`,{class:`text-gray-500`},`Dimensions`,-1),i(`dd`,D,u(K.value.width)+` × `+u(K.value.height)+`px`,1)])):l(``,!0),K.value.duration?(t(),a(`div`,O,[o[7]||=i(`dt`,{class:`text-gray-500`},`Duration`,-1),i(`dd`,k,u(K.value.duration)+`s`,1)])):l(``,!0),i(`div`,null,[o[8]||=i(`dt`,{class:`text-gray-500`},`Uploaded`,-1),i(`dd`,A,u(oe(K.value.created_at)),1)]),i(`div`,null,[o[9]||=i(`dt`,{class:`text-gray-500`},`References`,-1),i(`dd`,j,u(K.value.reference_count)+` content item`+u(K.value.reference_count===1?``:`s`),1)])])]),i(`div`,M,[o[11]||=i(`h2`,{class:`mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500`},`Alt text`,-1),n(i(`textarea`,{"onUpdate:modelValue":o[1]||=e=>J.value=e,rows:`3`,disabled:Y.value,placeholder:`Describe this file for screen readers and SEO`,class:`block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-200 disabled:cursor-not-allowed disabled:opacity-60`},null,8,N),[[g,J.value]]),i(`div`,P,[i(`button`,{type:`button`,disabled:Y.value,class:`rounded-[11px] bg-indigo-600 px-4 py-2.5 text-[13.5px] 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:se},u(Y.value?`Saving…`:`Save alt text`),9,F)])]),r(H)(`media:delete`)?(t(),a(`div`,ie,[o[12]||=i(`h2`,{class:`mb-1 text-sm font-semibold uppercase tracking-wide text-red-500`},`Danger zone`,-1),o[13]||=i(`p`,{class:`mb-3 text-sm text-gray-500`},` Permanently deletes this file from storage. This cannot be undone. `,-1),i(`span`,{title:$.value},[i(`button`,{type:`button`,disabled:!Q.value||X.value,class:`rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50`,onClick:o[2]||=e=>Z.value=!0},u(X.value?`Deleting…`:`Delete file`),9,L)],8,I),Q.value?l(``,!0):(t(),a(`p`,R,u($.value),1))])):l(``,!0),Z.value?(t(),s(v,{key:1,message:`Permanently delete this file? This action cannot be undone.`,"on-confirm":ce,"on-cancel":()=>Z.value=!1},null,8,[`on-cancel`])):l(``,!0)])):(t(),a(`div`,ee,` Media item not found. `))}});export{z as default};