memi-board 0.44.0 → 0.45.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.
- package/README.md +54 -3
- package/dist/ar-BCQdRw_S.js +622 -0
- package/dist/ar-BCQdRw_S.js.map +1 -0
- package/dist/composables/fetchPublicSeo.d.ts +4 -1
- package/dist/composables/useMemiBoardAutoTranslate.d.ts +33 -0
- package/dist/composables/useMemiBoardModeration.d.ts +5 -1
- package/dist/composables/useMemiBoardReports.d.ts +6 -1
- package/dist/composables/useMemiBoardSections.d.ts +2 -1
- package/dist/composables/useMemiBoardUsers.d.ts +4 -0
- package/dist/composables/useMemiBoardWritingAssistant.d.ts +9 -1
- package/dist/config.d.ts +5 -0
- package/dist/de-ddWQNaqs.js +622 -0
- package/dist/de-ddWQNaqs.js.map +1 -0
- package/dist/en-BVUBLkxc.js +648 -0
- package/dist/en-BVUBLkxc.js.map +1 -0
- package/dist/es-7JHjGVlM.js +622 -0
- package/dist/es-7JHjGVlM.js.map +1 -0
- package/dist/fr-C3NCMNoy.js +622 -0
- package/dist/fr-C3NCMNoy.js.map +1 -0
- package/dist/id-C8hCAMPJ.js +622 -0
- package/dist/id-C8hCAMPJ.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +1218 -1487
- package/dist/index.js.map +1 -1
- package/dist/ja-oTWri4hY.js +622 -0
- package/dist/ja-oTWri4hY.js.map +1 -0
- package/dist/module.mjs +55 -31
- package/dist/module.mjs.map +1 -1
- package/dist/pt-B9EH4scA.js +622 -0
- package/dist/pt-B9EH4scA.js.map +1 -0
- package/dist/runtime/components/Attachments.vue +4 -2
- package/dist/runtime/components/AuthorMenu.vue +14 -12
- package/dist/runtime/components/AvatarCropModal.vue +8 -5
- package/dist/runtime/components/CategorySettings.vue +65 -43
- package/dist/runtime/components/CommentFeed.vue +9 -7
- package/dist/runtime/components/CommentForm.vue +16 -10
- package/dist/runtime/components/CommentItem.vue +25 -26
- package/dist/runtime/components/CommentList.vue +7 -4
- package/dist/runtime/components/CommentThread.vue +7 -5
- package/dist/runtime/components/Detail.vue +20 -15
- package/dist/runtime/components/Editor.vue +145 -135
- package/dist/runtime/components/Footer.vue +14 -13
- package/dist/runtime/components/List.vue +31 -29
- package/dist/runtime/components/ListDefault.vue +4 -3
- package/dist/runtime/components/ListDense.vue +6 -5
- package/dist/runtime/components/ListImage.vue +4 -2
- package/dist/runtime/components/ListVideo.vue +3 -2
- package/dist/runtime/components/ListViewSwitch.vue +10 -6
- package/dist/runtime/components/MiniCard.vue +3 -2
- package/dist/runtime/components/MiniComments.vue +5 -4
- package/dist/runtime/components/MiniList.vue +3 -2
- package/dist/runtime/components/MiniPicker.vue +10 -9
- package/dist/runtime/components/MiniPost.vue +3 -2
- package/dist/runtime/components/MiniPosts.vue +5 -4
- package/dist/runtime/components/Profile.vue +12 -7
- package/dist/runtime/components/ProfileEditModal.vue +14 -13
- package/dist/runtime/components/ReportButton.vue +20 -10
- package/dist/runtime/components/ReportList.vue +11 -10
- package/dist/runtime/components/Section.vue +3 -2
- package/dist/runtime/components/Settings.vue +84 -62
- package/dist/runtime/components/ShareButton.vue +3 -1
- package/dist/runtime/components/SignIn.vue +14 -8
- package/dist/runtime/components/SwipeHint.vue +5 -3
- package/dist/runtime/components/UserCommentList.vue +8 -6
- package/dist/runtime/components/UserPostList.vue +6 -4
- package/dist/runtime/components/Users.vue +16 -10
- package/dist/runtime/composables/useMemiBoardSeo.ts +16 -10
- package/dist/slugify-6bG6GSO_.js +942 -0
- package/dist/slugify-6bG6GSO_.js.map +1 -0
- package/dist/storage.js +72 -71
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/utils/boardSeo.d.ts +5 -5
- package/dist/utils/compressImage.d.ts +2 -1
- package/dist/utils/formatDate.d.ts +6 -4
- package/dist/utils/imeSafeSubmit.d.ts +2 -0
- package/dist/utils/moderation-prompt.d.ts +18 -1
- package/dist/utils/moderation-strike.d.ts +2 -1
- package/dist/utils/section.d.ts +15 -2
- package/dist/zh-D5rDYV6E.js +622 -0
- package/dist/zh-D5rDYV6E.js.map +1 -0
- package/package.json +2 -3
- package/dist/slugify-Cm_pi2al.js +0 -124
- package/dist/slugify-Cm_pi2al.js.map +0 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 커스텀은 handlers.image(파일 업로드), YouTube 임베드 + DOM paste/drop.
|
|
8
8
|
*/
|
|
9
9
|
import { ref, computed, watch } from 'vue'
|
|
10
|
+
import { imeSafeSubmitClick, imeSafeSubmitPointerDown } from 'memi-board/runtime'
|
|
10
11
|
import Youtube from '@tiptap/extension-youtube'
|
|
11
12
|
import type { DropdownMenuItem, EditorSuggestionMenuItem } from '@nuxt/ui'
|
|
12
13
|
import { mapEditorItems } from '@nuxt/ui/utils/editor'
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
useMemiBoardAuth,
|
|
17
18
|
useMemiBoardPosts,
|
|
18
19
|
useMemiBoardModeration,
|
|
20
|
+
useMemiBoardI18n,
|
|
19
21
|
useMemiBoardSettings,
|
|
20
22
|
hasBodyImage,
|
|
21
23
|
hasBodyText,
|
|
@@ -38,10 +40,13 @@ const emit = defineEmits<{ saved: [id: string], cancel: [] }>()
|
|
|
38
40
|
const { user, isSignedIn, isAdmin, isWriteRestricted, restrictedMessage } = useMemiBoardAuth()
|
|
39
41
|
const resolvedBoardId = computed(() => props.boardId || props.fixedCategory || '')
|
|
40
42
|
const { createPostId, getPost, createPost, updatePost, resolveUniqueSlug } = useMemiBoardPosts(resolvedBoardId)
|
|
41
|
-
const { checkText } = useMemiBoardModeration()
|
|
42
43
|
const { getBoard, ensureSettings } = useMemiBoardSettings()
|
|
44
|
+
// 보드 설정 언어 → 호스트 언어 순.
|
|
45
|
+
const { t, dir, locale } = useMemiBoardI18n({ boardLocale: () => getBoard(resolvedBoardId.value)?.locale })
|
|
46
|
+
// provide 한 컴포넌트 자신은 inject 를 못 받으므로 AI 프롬프트 언어를 직접 넘긴다.
|
|
47
|
+
const { checkText } = useMemiBoardModeration({ locale })
|
|
43
48
|
const { uploadEditorImage } = useMemiBoardStorage()
|
|
44
|
-
const { assist } = useMemiBoardWritingAssistant()
|
|
49
|
+
const { assist } = useMemiBoardWritingAssistant({ locale })
|
|
45
50
|
|
|
46
51
|
const title = ref('')
|
|
47
52
|
/** UEditor content-type=html (shineb 와 동일) */
|
|
@@ -90,9 +95,9 @@ const aiPreviewBefore = ref('')
|
|
|
90
95
|
const aiPreviewPending = ref<AiPreviewPending | null>(null)
|
|
91
96
|
const aiPreviewLabels = computed(() => {
|
|
92
97
|
switch (aiPreviewPending.value?.action) {
|
|
93
|
-
case 'title': return { before: '
|
|
94
|
-
case 'continue': return { before: '
|
|
95
|
-
default: return { before: '
|
|
98
|
+
case 'title': return { before: t('editor.aiPreview.currentTitle'), after: t('editor.aiPreview.newTitle') }
|
|
99
|
+
case 'continue': return { before: t('editor.aiPreview.currentContent'), after: t('editor.aiPreview.appendedContent') }
|
|
100
|
+
default: return { before: t('editor.aiPreview.before'), after: t('editor.aiPreview.after') }
|
|
96
101
|
}
|
|
97
102
|
})
|
|
98
103
|
/** 관리자 전용 — 비속어 필터 건너뛰기 (테스트/공지 등 의도적 작성용) */
|
|
@@ -186,25 +191,25 @@ function appendCoverUrls(urls: string[]) {
|
|
|
186
191
|
}
|
|
187
192
|
coverSlots.value = next
|
|
188
193
|
if (urls.length && coverSlots.value.length >= COVER_IMAGE_MAX) {
|
|
189
|
-
imageUploadError.value =
|
|
194
|
+
imageUploadError.value = t('editor.errors.coverMax', { max: COVER_IMAGE_MAX })
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
function normalizeExternalImageUrl(value: string): string {
|
|
194
199
|
const raw = value.trim()
|
|
195
|
-
if (!raw) throw new Error('
|
|
200
|
+
if (!raw) throw new Error(t('editor.errors.imageUrlRequired'))
|
|
196
201
|
let url: URL
|
|
197
202
|
try {
|
|
198
203
|
url = new URL(raw)
|
|
199
204
|
}
|
|
200
205
|
catch {
|
|
201
|
-
throw new Error('
|
|
206
|
+
throw new Error(t('editor.errors.imageUrlInvalid'))
|
|
202
207
|
}
|
|
203
208
|
if (url.protocol !== 'https:') {
|
|
204
|
-
throw new Error('
|
|
209
|
+
throw new Error(t('editor.errors.imageUrlHttps'))
|
|
205
210
|
}
|
|
206
211
|
if (url.username || url.password) {
|
|
207
|
-
throw new Error('
|
|
212
|
+
throw new Error(t('editor.errors.imageUrlCredentials'))
|
|
208
213
|
}
|
|
209
214
|
return url.toString()
|
|
210
215
|
}
|
|
@@ -214,19 +219,19 @@ function verifyExternalImage(url: string): Promise<string> {
|
|
|
214
219
|
const image = new Image()
|
|
215
220
|
const timeout = window.setTimeout(() => {
|
|
216
221
|
image.src = ''
|
|
217
|
-
reject(new Error('
|
|
222
|
+
reject(new Error(t('editor.errors.imageTimeout')))
|
|
218
223
|
}, 10_000)
|
|
219
224
|
image.onload = () => {
|
|
220
225
|
window.clearTimeout(timeout)
|
|
221
226
|
if (!image.naturalWidth || !image.naturalHeight) {
|
|
222
|
-
reject(new Error('
|
|
227
|
+
reject(new Error(t('editor.errors.imageNotDisplayable')))
|
|
223
228
|
return
|
|
224
229
|
}
|
|
225
230
|
resolve(url)
|
|
226
231
|
}
|
|
227
232
|
image.onerror = () => {
|
|
228
233
|
window.clearTimeout(timeout)
|
|
229
|
-
reject(new Error('
|
|
234
|
+
reject(new Error(t('editor.errors.imageLoadFailed')))
|
|
230
235
|
}
|
|
231
236
|
image.referrerPolicy = 'no-referrer'
|
|
232
237
|
image.src = url
|
|
@@ -258,13 +263,13 @@ async function addExternalImage() {
|
|
|
258
263
|
}
|
|
259
264
|
else {
|
|
260
265
|
const editor = imageDialogEditor.value ?? resolveEditor()
|
|
261
|
-
if (!editor) throw new Error('
|
|
266
|
+
if (!editor) throw new Error(t('editor.errors.editorNotReady'))
|
|
262
267
|
editor.chain().focus().setImage({ src: url }).run()
|
|
263
268
|
}
|
|
264
269
|
imageDialogOpen.value = false
|
|
265
270
|
}
|
|
266
271
|
catch (cause) {
|
|
267
|
-
imageUploadError.value = cause instanceof Error ? cause.message : '
|
|
272
|
+
imageUploadError.value = cause instanceof Error ? cause.message : t('editor.errors.imageLinkFailed')
|
|
268
273
|
}
|
|
269
274
|
finally {
|
|
270
275
|
externalImageChecking.value = false
|
|
@@ -355,10 +360,10 @@ function onCoverItemDragEnd() {
|
|
|
355
360
|
|
|
356
361
|
async function doUploadImage(file: File): Promise<EditorImageEntry> {
|
|
357
362
|
if (!file.type.startsWith('image/')) {
|
|
358
|
-
throw new Error('
|
|
363
|
+
throw new Error(t('editor.errors.imageOnly'))
|
|
359
364
|
}
|
|
360
365
|
if (file.size > EDITOR_IMAGE_SOURCE_MAX_BYTES) {
|
|
361
|
-
throw new Error(
|
|
366
|
+
throw new Error(t('editor.errors.imageTooLarge', { mb: EDITOR_IMAGE_SOURCE_MAX_BYTES / 1024 / 1024 }))
|
|
362
367
|
}
|
|
363
368
|
const entry = await uploadEditorImage(file, imageNamespace())
|
|
364
369
|
uploadedEditorImages.value.push(entry)
|
|
@@ -378,7 +383,7 @@ async function uploadAndSetImage(editor: any, file: File) {
|
|
|
378
383
|
editor.chain().focus().setImage({ src: entry.originalUrl }).run()
|
|
379
384
|
}
|
|
380
385
|
catch (e) {
|
|
381
|
-
imageUploadError.value = (e as Error).message || '
|
|
386
|
+
imageUploadError.value = (e as Error).message || t('editor.errors.imageUploadFailed')
|
|
382
387
|
}
|
|
383
388
|
finally {
|
|
384
389
|
imageUploading.value = false
|
|
@@ -390,7 +395,7 @@ async function uploadCoverImages(files: File[]) {
|
|
|
390
395
|
if (!images.length) return
|
|
391
396
|
const room = COVER_IMAGE_MAX - coverSlots.value.length
|
|
392
397
|
if (room <= 0) {
|
|
393
|
-
imageUploadError.value =
|
|
398
|
+
imageUploadError.value = t('editor.errors.coverMax', { max: COVER_IMAGE_MAX })
|
|
394
399
|
return
|
|
395
400
|
}
|
|
396
401
|
const batch = images.slice(0, room)
|
|
@@ -404,11 +409,11 @@ async function uploadCoverImages(files: File[]) {
|
|
|
404
409
|
}
|
|
405
410
|
appendCoverUrls(urls)
|
|
406
411
|
if (images.length > batch.length) {
|
|
407
|
-
imageUploadError.value =
|
|
412
|
+
imageUploadError.value = t('editor.errors.coverMax', { max: COVER_IMAGE_MAX })
|
|
408
413
|
}
|
|
409
414
|
}
|
|
410
415
|
catch (e) {
|
|
411
|
-
imageUploadError.value = (e as Error).message || '
|
|
416
|
+
imageUploadError.value = (e as Error).message || t('editor.errors.imageUploadFailed')
|
|
412
417
|
}
|
|
413
418
|
finally {
|
|
414
419
|
imageUploading.value = false
|
|
@@ -487,7 +492,7 @@ function youtubeUrl(value: string | null | undefined): string | null {
|
|
|
487
492
|
}
|
|
488
493
|
|
|
489
494
|
function insertYoutube(editor: any, value?: string | null): boolean {
|
|
490
|
-
const src = youtubeUrl(value ?? window.prompt('
|
|
495
|
+
const src = youtubeUrl(value ?? window.prompt(t('editor.youtubePrompt')))
|
|
491
496
|
if (!src) return false
|
|
492
497
|
return editor.chain().focus().setYoutubeVideo({ src }).run()
|
|
493
498
|
}
|
|
@@ -532,31 +537,31 @@ const selectedNode = ref<{ node: { type?: string }, pos: number }>()
|
|
|
532
537
|
|
|
533
538
|
const suggestionItems = computed(() => {
|
|
534
539
|
const insertItems: EditorSuggestionMenuItem<typeof handlers.value>[] = [
|
|
535
|
-
{ kind: 'horizontalRule', label: '
|
|
540
|
+
{ kind: 'horizontalRule', label: t('editor.menu.horizontalRule'), icon: 'i-lucide-separator-horizontal' },
|
|
536
541
|
]
|
|
537
542
|
if (!isImageEditor.value) {
|
|
538
543
|
insertItems.unshift(
|
|
539
|
-
{ kind: 'image', label: '
|
|
544
|
+
{ kind: 'image', label: t('editor.menu.image'), icon: 'i-lucide-image' },
|
|
540
545
|
{ kind: 'youtube', label: 'YouTube', icon: 'i-lucide-youtube' },
|
|
541
546
|
)
|
|
542
547
|
}
|
|
543
548
|
return [
|
|
544
549
|
[
|
|
545
|
-
{ type: 'label' as const, label: '
|
|
546
|
-
{ kind: 'aiContinue' as const, label: '
|
|
550
|
+
{ type: 'label' as const, label: t('editor.menu.ai') },
|
|
551
|
+
{ kind: 'aiContinue' as const, label: t('editor.menu.aiContinue'), icon: 'i-lucide-sparkles' },
|
|
547
552
|
],
|
|
548
553
|
[
|
|
549
|
-
{ type: 'label' as const, label: '
|
|
550
|
-
{ kind: 'paragraph' as const, label: '
|
|
551
|
-
{ kind: 'heading' as const, level: 1 as const, label: '
|
|
552
|
-
{ kind: 'heading' as const, level: 2 as const, label: '
|
|
553
|
-
{ kind: 'heading' as const, level: 3 as const, label: '
|
|
554
|
-
{ kind: 'bulletList' as const, label: '
|
|
555
|
-
{ kind: 'orderedList' as const, label: '
|
|
556
|
-
{ kind: 'blockquote' as const, label: '
|
|
557
|
-
{ kind: 'codeBlock' as const, label: '
|
|
554
|
+
{ type: 'label' as const, label: t('editor.menu.style') },
|
|
555
|
+
{ kind: 'paragraph' as const, label: t('editor.menu.paragraph'), icon: 'i-lucide-type' },
|
|
556
|
+
{ kind: 'heading' as const, level: 1 as const, label: t('editor.menu.heading1'), icon: 'i-lucide-heading-1' },
|
|
557
|
+
{ kind: 'heading' as const, level: 2 as const, label: t('editor.menu.heading2'), icon: 'i-lucide-heading-2' },
|
|
558
|
+
{ kind: 'heading' as const, level: 3 as const, label: t('editor.menu.heading3'), icon: 'i-lucide-heading-3' },
|
|
559
|
+
{ kind: 'bulletList' as const, label: t('editor.menu.bulletList'), icon: 'i-lucide-list' },
|
|
560
|
+
{ kind: 'orderedList' as const, label: t('editor.menu.orderedList'), icon: 'i-lucide-list-ordered' },
|
|
561
|
+
{ kind: 'blockquote' as const, label: t('editor.menu.blockquote'), icon: 'i-lucide-text-quote' },
|
|
562
|
+
{ kind: 'codeBlock' as const, label: t('editor.menu.codeBlock'), icon: 'i-lucide-square-code' },
|
|
558
563
|
],
|
|
559
|
-
[{ type: 'label' as const, label: '
|
|
564
|
+
[{ type: 'label' as const, label: t('editor.menu.insert') }, ...insertItems],
|
|
560
565
|
]
|
|
561
566
|
})
|
|
562
567
|
|
|
@@ -564,97 +569,97 @@ const handleItems = (editor: any): DropdownMenuItem[][] => {
|
|
|
564
569
|
if (!selectedNode.value?.node?.type) return []
|
|
565
570
|
return mapEditorItems(editor, [
|
|
566
571
|
[
|
|
567
|
-
{ type: 'label', label: '
|
|
572
|
+
{ type: 'label', label: t('editor.blockMenu') },
|
|
568
573
|
{
|
|
569
|
-
label: '
|
|
574
|
+
label: t('editor.menu.turnInto'),
|
|
570
575
|
icon: 'i-lucide-repeat-2',
|
|
571
576
|
children: [
|
|
572
|
-
{ kind: 'paragraph', label: '
|
|
573
|
-
{ kind: 'heading', level: 1, label: '
|
|
574
|
-
{ kind: 'heading', level: 2, label: '
|
|
575
|
-
{ kind: 'heading', level: 3, label: '
|
|
576
|
-
{ kind: 'bulletList', label: '
|
|
577
|
-
{ kind: 'orderedList', label: '
|
|
578
|
-
{ kind: 'blockquote', label: '
|
|
579
|
-
{ kind: 'codeBlock', label: '
|
|
577
|
+
{ kind: 'paragraph', label: t('editor.menu.paragraph'), icon: 'i-lucide-type' },
|
|
578
|
+
{ kind: 'heading', level: 1, label: t('editor.menu.heading1'), icon: 'i-lucide-heading-1' },
|
|
579
|
+
{ kind: 'heading', level: 2, label: t('editor.menu.heading2'), icon: 'i-lucide-heading-2' },
|
|
580
|
+
{ kind: 'heading', level: 3, label: t('editor.menu.heading3'), icon: 'i-lucide-heading-3' },
|
|
581
|
+
{ kind: 'bulletList', label: t('editor.menu.bulletList'), icon: 'i-lucide-list' },
|
|
582
|
+
{ kind: 'orderedList', label: t('editor.menu.orderedList'), icon: 'i-lucide-list-ordered' },
|
|
583
|
+
{ kind: 'blockquote', label: t('editor.menu.blockquote'), icon: 'i-lucide-text-quote' },
|
|
584
|
+
{ kind: 'codeBlock', label: t('editor.menu.codeBlock'), icon: 'i-lucide-square-code' },
|
|
580
585
|
],
|
|
581
586
|
},
|
|
582
|
-
{ kind: 'clearFormatting', pos: selectedNode.value.pos, label: '
|
|
587
|
+
{ kind: 'clearFormatting', pos: selectedNode.value.pos, label: t('editor.menu.resetFormatting'), icon: 'i-lucide-rotate-ccw' },
|
|
583
588
|
],
|
|
584
589
|
[
|
|
585
|
-
{ kind: 'duplicate', pos: selectedNode.value.pos, label: '
|
|
586
|
-
{ kind: 'moveUp', pos: selectedNode.value.pos, label: '
|
|
587
|
-
{ kind: 'moveDown', pos: selectedNode.value.pos, label: '
|
|
590
|
+
{ kind: 'duplicate', pos: selectedNode.value.pos, label: t('editor.menu.duplicate'), icon: 'i-lucide-copy' },
|
|
591
|
+
{ kind: 'moveUp', pos: selectedNode.value.pos, label: t('editor.menu.moveUp'), icon: 'i-lucide-arrow-up' },
|
|
592
|
+
{ kind: 'moveDown', pos: selectedNode.value.pos, label: t('editor.menu.moveDown'), icon: 'i-lucide-arrow-down' },
|
|
588
593
|
],
|
|
589
|
-
[{ kind: 'delete', pos: selectedNode.value.pos, label: '
|
|
594
|
+
[{ kind: 'delete', pos: selectedNode.value.pos, label: t('editor.menu.delete'), icon: 'i-lucide-trash' }],
|
|
590
595
|
], handlers.value) as DropdownMenuItem[][]
|
|
591
596
|
}
|
|
592
597
|
|
|
593
598
|
const toolbarItems = computed(() => {
|
|
594
599
|
const media = isImageEditor.value
|
|
595
|
-
? [{ kind: 'link' as const, icon: 'i-lucide-link', tooltip: { text: '
|
|
600
|
+
? [{ kind: 'link' as const, icon: 'i-lucide-link', tooltip: { text: t('editor.toolbar.link') } }]
|
|
596
601
|
: [
|
|
597
|
-
{ kind: 'link' as const, icon: 'i-lucide-link', tooltip: { text: '
|
|
598
|
-
{ kind: 'image' as const, icon: 'i-lucide-image', tooltip: { text: '
|
|
602
|
+
{ kind: 'link' as const, icon: 'i-lucide-link', tooltip: { text: t('editor.toolbar.link') } },
|
|
603
|
+
{ kind: 'image' as const, icon: 'i-lucide-image', tooltip: { text: t('editor.toolbar.image') } },
|
|
599
604
|
{ kind: 'youtube' as const, icon: 'i-lucide-youtube', tooltip: { text: 'YouTube' } },
|
|
600
605
|
]
|
|
601
606
|
return [
|
|
602
607
|
[
|
|
603
608
|
{
|
|
604
609
|
icon: 'i-lucide-a-large-small',
|
|
605
|
-
tooltip: { text: '
|
|
610
|
+
tooltip: { text: t('editor.toolbar.format') },
|
|
606
611
|
content: { align: 'start' as const },
|
|
607
612
|
items: [
|
|
608
613
|
[
|
|
609
|
-
{ kind: 'mark' as const, mark: 'bold' as const, icon: 'i-lucide-bold', label: '
|
|
610
|
-
{ kind: 'mark' as const, mark: 'italic' as const, icon: 'i-lucide-italic', label: '
|
|
611
|
-
{ kind: 'mark' as const, mark: 'underline' as const, icon: 'i-lucide-underline', label: '
|
|
612
|
-
{ kind: 'mark' as const, mark: 'strike' as const, icon: 'i-lucide-strikethrough', label: '
|
|
614
|
+
{ kind: 'mark' as const, mark: 'bold' as const, icon: 'i-lucide-bold', label: t('editor.toolbar.bold') },
|
|
615
|
+
{ kind: 'mark' as const, mark: 'italic' as const, icon: 'i-lucide-italic', label: t('editor.toolbar.italic') },
|
|
616
|
+
{ kind: 'mark' as const, mark: 'underline' as const, icon: 'i-lucide-underline', label: t('editor.toolbar.underline') },
|
|
617
|
+
{ kind: 'mark' as const, mark: 'strike' as const, icon: 'i-lucide-strikethrough', label: t('editor.toolbar.strike') },
|
|
613
618
|
],
|
|
614
619
|
[
|
|
615
|
-
{ kind: 'heading' as const, level: 1 as const, icon: 'i-lucide-heading-1', label: '
|
|
616
|
-
{ kind: 'heading' as const, level: 2 as const, icon: 'i-lucide-heading-2', label: '
|
|
617
|
-
{ kind: 'heading' as const, level: 3 as const, icon: 'i-lucide-heading-3', label: '
|
|
618
|
-
{ kind: 'paragraph' as const, icon: 'i-lucide-text', label: '
|
|
619
|
-
{ kind: 'mark' as const, mark: 'code' as const, icon: 'i-lucide-code', label: '
|
|
620
|
+
{ kind: 'heading' as const, level: 1 as const, icon: 'i-lucide-heading-1', label: t('editor.toolbar.title') },
|
|
621
|
+
{ kind: 'heading' as const, level: 2 as const, icon: 'i-lucide-heading-2', label: t('editor.toolbar.heading') },
|
|
622
|
+
{ kind: 'heading' as const, level: 3 as const, icon: 'i-lucide-heading-3', label: t('editor.toolbar.subheading') },
|
|
623
|
+
{ kind: 'paragraph' as const, icon: 'i-lucide-text', label: t('editor.toolbar.body') },
|
|
624
|
+
{ kind: 'mark' as const, mark: 'code' as const, icon: 'i-lucide-code', label: t('editor.toolbar.mono') },
|
|
620
625
|
],
|
|
621
626
|
[
|
|
622
|
-
{ kind: 'bulletList' as const, icon: 'i-lucide-list', label: '
|
|
623
|
-
{ kind: 'orderedList' as const, icon: 'i-lucide-list-ordered', label: '
|
|
624
|
-
{ kind: 'blockquote' as const, icon: 'i-lucide-quote', label: '
|
|
625
|
-
{ kind: 'codeBlock' as const, icon: 'i-lucide-square-code', label: '
|
|
627
|
+
{ kind: 'bulletList' as const, icon: 'i-lucide-list', label: t('editor.toolbar.bulletList') },
|
|
628
|
+
{ kind: 'orderedList' as const, icon: 'i-lucide-list-ordered', label: t('editor.toolbar.orderedList') },
|
|
629
|
+
{ kind: 'blockquote' as const, icon: 'i-lucide-quote', label: t('editor.toolbar.blockquote') },
|
|
630
|
+
{ kind: 'codeBlock' as const, icon: 'i-lucide-square-code', label: t('editor.toolbar.codeBlock') },
|
|
626
631
|
],
|
|
627
632
|
[
|
|
628
|
-
{ kind: 'horizontalRule' as const, icon: 'i-lucide-minus', label: '
|
|
629
|
-
{ kind: 'clearFormatting' as const, icon: 'i-lucide-remove-formatting', label: '
|
|
633
|
+
{ kind: 'horizontalRule' as const, icon: 'i-lucide-minus', label: t('editor.toolbar.horizontalRule') },
|
|
634
|
+
{ kind: 'clearFormatting' as const, icon: 'i-lucide-remove-formatting', label: t('editor.toolbar.clearFormatting') },
|
|
630
635
|
],
|
|
631
636
|
],
|
|
632
637
|
},
|
|
633
638
|
],
|
|
634
639
|
media,
|
|
635
640
|
[
|
|
636
|
-
{ kind: 'undo' as const, icon: 'i-lucide-undo-2', tooltip: { text: '
|
|
637
|
-
{ kind: 'redo' as const, icon: 'i-lucide-redo-2', tooltip: { text: '
|
|
641
|
+
{ kind: 'undo' as const, icon: 'i-lucide-undo-2', tooltip: { text: t('editor.toolbar.undo') } },
|
|
642
|
+
{ kind: 'redo' as const, icon: 'i-lucide-redo-2', tooltip: { text: t('editor.toolbar.redo') } },
|
|
638
643
|
],
|
|
639
644
|
]
|
|
640
645
|
})
|
|
641
646
|
|
|
642
647
|
const aiMenuItems = computed(() => [
|
|
643
648
|
[
|
|
644
|
-
{ label: '
|
|
645
|
-
{ label: '
|
|
646
|
-
{ label: '
|
|
647
|
-
{ label: '
|
|
649
|
+
{ label: t('editor.ai.proofread'), icon: 'i-lucide-spell-check-2', onSelect: () => runWritingAssistant('proofread') },
|
|
650
|
+
{ label: t('editor.ai.polish'), icon: 'i-lucide-wand-sparkles', onSelect: () => runWritingAssistant('polish') },
|
|
651
|
+
{ label: t('editor.ai.continue'), icon: 'i-lucide-text-cursor-input', onSelect: () => runWritingAssistant('continue') },
|
|
652
|
+
{ label: t('editor.ai.summarize'), icon: 'i-lucide-scan-text', onSelect: () => runWritingAssistant('summarize') },
|
|
648
653
|
],
|
|
649
654
|
[
|
|
650
|
-
{ label: '
|
|
651
|
-
{ label: '
|
|
655
|
+
{ label: t('editor.ai.translateEn'), icon: 'i-lucide-languages', onSelect: () => runWritingAssistant('translate-en') },
|
|
656
|
+
{ label: t('editor.ai.translateKo'), icon: 'i-lucide-languages', onSelect: () => runWritingAssistant('translate-ko') },
|
|
652
657
|
],
|
|
653
658
|
[
|
|
654
|
-
{ label: '
|
|
659
|
+
{ label: t('editor.ai.title'), icon: 'i-lucide-heading-1', onSelect: () => runWritingAssistant('title') },
|
|
655
660
|
],
|
|
656
661
|
[
|
|
657
|
-
{ label: '
|
|
662
|
+
{ label: t('editor.ai.custom'), icon: 'i-lucide-pencil-line', onSelect: () => openCustomAssistantDialog() },
|
|
658
663
|
],
|
|
659
664
|
])
|
|
660
665
|
|
|
@@ -705,7 +710,7 @@ async function runWritingAssistant(action: WritingAssistantAction, customInstruc
|
|
|
705
710
|
}
|
|
706
711
|
catch (cause) {
|
|
707
712
|
console.warn('[memi-board] writing assistant failed', cause)
|
|
708
|
-
aiError.value = cause instanceof Error ? cause.message : '
|
|
713
|
+
aiError.value = cause instanceof Error ? cause.message : t('editor.ai.failed')
|
|
709
714
|
}
|
|
710
715
|
finally {
|
|
711
716
|
aiRunning.value = false
|
|
@@ -780,7 +785,7 @@ function onImageFormPaste(e: ClipboardEvent) {
|
|
|
780
785
|
imageUploadError.value = ''
|
|
781
786
|
}
|
|
782
787
|
else {
|
|
783
|
-
imageUploadError.value = '
|
|
788
|
+
imageUploadError.value = t('editor.errors.pasteAsFile')
|
|
784
789
|
}
|
|
785
790
|
}
|
|
786
791
|
}
|
|
@@ -872,7 +877,7 @@ async function loadPost(id: string) {
|
|
|
872
877
|
await ensureSettings().catch(() => {})
|
|
873
878
|
const post = await getPost(id)
|
|
874
879
|
if (!post) {
|
|
875
|
-
error.value = '
|
|
880
|
+
error.value = t('editor.errors.postNotFound')
|
|
876
881
|
title.value = ''
|
|
877
882
|
content.value = ''
|
|
878
883
|
coverSlots.value = []
|
|
@@ -901,8 +906,8 @@ async function loadPost(id: string) {
|
|
|
901
906
|
catch (e) {
|
|
902
907
|
const msg = (e as Error).message || String(e)
|
|
903
908
|
error.value = msg.includes('permission')
|
|
904
|
-
? '
|
|
905
|
-
:
|
|
909
|
+
? t('editor.errors.loadPermission')
|
|
910
|
+
: t('editor.errors.loadFailed', { message: msg })
|
|
906
911
|
}
|
|
907
912
|
finally {
|
|
908
913
|
loading.value = false
|
|
@@ -926,43 +931,44 @@ watch(
|
|
|
926
931
|
function friendlyWriteError(e: unknown): string {
|
|
927
932
|
const msg = e instanceof Error ? e.message : String(e)
|
|
928
933
|
if (msg.includes('permission-denied') || msg.includes('Permission denied')) {
|
|
929
|
-
return '
|
|
934
|
+
return t('editor.errors.editPermission')
|
|
930
935
|
}
|
|
931
936
|
return msg
|
|
932
937
|
}
|
|
933
938
|
|
|
934
939
|
async function handleSubmit() {
|
|
940
|
+
if (saving.value) return
|
|
935
941
|
error.value = ''
|
|
936
942
|
const imageBoard = isImageEditor.value
|
|
937
943
|
if (!imageBoard && !title.value.trim()) {
|
|
938
|
-
error.value = '
|
|
944
|
+
error.value = t('editor.errors.titleRequired')
|
|
939
945
|
return
|
|
940
946
|
}
|
|
941
947
|
// 이미지 보드: 갤러리 1장 이상 (TipTap 본문 이미지 불가)
|
|
942
948
|
if (imageBoard && coverSlots.value.length === 0) {
|
|
943
|
-
error.value = '
|
|
949
|
+
error.value = t('editor.errors.photoRequired')
|
|
944
950
|
return
|
|
945
951
|
}
|
|
946
952
|
if (!imageBoard && !hasBodyText(content.value) && !hasBodyImage(content.value, attachments.value)) {
|
|
947
|
-
error.value = '
|
|
953
|
+
error.value = t('editor.errors.bodyRequired')
|
|
948
954
|
return
|
|
949
955
|
}
|
|
950
956
|
if (!user.value) {
|
|
951
|
-
error.value = '
|
|
957
|
+
error.value = t('editor.errors.signInRequired')
|
|
952
958
|
return
|
|
953
959
|
}
|
|
954
960
|
if (!resolvedBoardId.value) {
|
|
955
|
-
error.value = '
|
|
961
|
+
error.value = t('editor.errors.boardMissing')
|
|
956
962
|
return
|
|
957
963
|
}
|
|
958
964
|
if (isWriteRestricted.value) {
|
|
959
965
|
error.value = restrictedMessage.value
|
|
960
|
-
|| '
|
|
966
|
+
|| t('editor.errors.writeRestricted')
|
|
961
967
|
return
|
|
962
968
|
}
|
|
963
969
|
|
|
964
970
|
saving.value = true
|
|
965
|
-
submitHint.value = '
|
|
971
|
+
submitHint.value = t('editor.hint.reviewing')
|
|
966
972
|
// 새 글 slug 중복 확인을 검열과 동시에 시작 — 검열에 걸리면 결과만 버린다.
|
|
967
973
|
const slugPromise = !props.postId && !imageBoard ? resolveUniqueSlug(title.value) : null
|
|
968
974
|
slugPromise?.catch(() => {})
|
|
@@ -971,11 +977,11 @@ async function handleSubmit() {
|
|
|
971
977
|
const moderationText = imageBoard ? plain : `${title.value}\n${plain}`
|
|
972
978
|
const moderation = await checkText(moderationText, { skipFilter: isAdmin.value && adminSkipModeration.value })
|
|
973
979
|
if (moderation.flagged) {
|
|
974
|
-
error.value = moderation.reason || '
|
|
980
|
+
error.value = moderation.reason || t('editor.errors.moderationBlocked')
|
|
975
981
|
return
|
|
976
982
|
}
|
|
977
983
|
|
|
978
|
-
submitHint.value = '
|
|
984
|
+
submitHint.value = t('editor.hint.saving')
|
|
979
985
|
const bodyContent = imageBoard && coverSlots.value.length
|
|
980
986
|
? withCoverImagesInContent(content.value, coverUrls())
|
|
981
987
|
: content.value
|
|
@@ -1016,6 +1022,7 @@ async function handleSubmit() {
|
|
|
1016
1022
|
<template>
|
|
1017
1023
|
<div
|
|
1018
1024
|
v-if="loading"
|
|
1025
|
+
:dir="dir"
|
|
1019
1026
|
class="flex flex-col gap-3"
|
|
1020
1027
|
>
|
|
1021
1028
|
<USkeleton class="h-10 w-full" />
|
|
@@ -1024,6 +1031,7 @@ async function handleSubmit() {
|
|
|
1024
1031
|
|
|
1025
1032
|
<form
|
|
1026
1033
|
v-else
|
|
1034
|
+
:dir="dir"
|
|
1027
1035
|
class="flex flex-col gap-4"
|
|
1028
1036
|
@submit.prevent="handleSubmit"
|
|
1029
1037
|
@paste.capture="onImageFormPaste"
|
|
@@ -1033,7 +1041,7 @@ async function handleSubmit() {
|
|
|
1033
1041
|
<UInput
|
|
1034
1042
|
v-if="!isImageEditor"
|
|
1035
1043
|
v-model="title"
|
|
1036
|
-
placeholder="
|
|
1044
|
+
:placeholder="t('editor.titlePlaceholder')"
|
|
1037
1045
|
size="lg"
|
|
1038
1046
|
required
|
|
1039
1047
|
@keydown.tab.exact.prevent="focusContentFromTitle"
|
|
@@ -1045,7 +1053,7 @@ async function handleSubmit() {
|
|
|
1045
1053
|
>
|
|
1046
1054
|
<div class="flex items-center justify-between gap-2">
|
|
1047
1055
|
<p class="text-xs text-muted">
|
|
1048
|
-
|
|
1056
|
+
{{ t('editor.cover.hint', { count: coverSlots.length, max: COVER_IMAGE_MAX }) }}
|
|
1049
1057
|
</p>
|
|
1050
1058
|
<div class="flex shrink-0 items-center gap-1">
|
|
1051
1059
|
<UButton
|
|
@@ -1053,7 +1061,7 @@ async function handleSubmit() {
|
|
|
1053
1061
|
size="xs"
|
|
1054
1062
|
color="neutral"
|
|
1055
1063
|
variant="ghost"
|
|
1056
|
-
label="
|
|
1064
|
+
:label="t('editor.cover.imageLink')"
|
|
1057
1065
|
icon="i-lucide-link"
|
|
1058
1066
|
:disabled="imageUploading || coverSlots.length >= COVER_IMAGE_MAX"
|
|
1059
1067
|
@click="openImageDialog()"
|
|
@@ -1064,7 +1072,7 @@ async function handleSubmit() {
|
|
|
1064
1072
|
size="xs"
|
|
1065
1073
|
color="neutral"
|
|
1066
1074
|
variant="ghost"
|
|
1067
|
-
label="
|
|
1075
|
+
:label="t('editor.cover.addPhoto')"
|
|
1068
1076
|
icon="i-lucide-plus"
|
|
1069
1077
|
:disabled="imageUploading || coverSlots.length >= COVER_IMAGE_MAX"
|
|
1070
1078
|
@click="pickCoverImages"
|
|
@@ -1102,7 +1110,7 @@ async function handleSubmit() {
|
|
|
1102
1110
|
>
|
|
1103
1111
|
<img
|
|
1104
1112
|
:src="slot.url"
|
|
1105
|
-
:alt="
|
|
1113
|
+
:alt="t('editor.cover.alt', { n: index + 1 })"
|
|
1106
1114
|
class="pointer-events-none size-full object-cover"
|
|
1107
1115
|
draggable="false"
|
|
1108
1116
|
>
|
|
@@ -1110,18 +1118,18 @@ async function handleSubmit() {
|
|
|
1110
1118
|
v-if="index === 0"
|
|
1111
1119
|
class="pointer-events-none absolute left-1 top-1 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white"
|
|
1112
1120
|
>
|
|
1113
|
-
|
|
1121
|
+
{{ t('editor.cover.representative') }}
|
|
1114
1122
|
</span>
|
|
1115
1123
|
<button
|
|
1116
1124
|
v-else
|
|
1117
1125
|
type="button"
|
|
1118
1126
|
class="absolute left-1 top-1 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white opacity-100 transition hover:bg-primary sm:opacity-0 sm:group-hover:opacity-100"
|
|
1119
1127
|
:disabled="imageUploading"
|
|
1120
|
-
:aria-label="
|
|
1128
|
+
:aria-label="t('editor.cover.setRepresentativeAria', { n: index + 1 })"
|
|
1121
1129
|
@click.stop="setRepresentativeCover(index)"
|
|
1122
1130
|
@dragstart.stop.prevent
|
|
1123
1131
|
>
|
|
1124
|
-
|
|
1132
|
+
{{ t('editor.cover.setRepresentative') }}
|
|
1125
1133
|
</button>
|
|
1126
1134
|
<span
|
|
1127
1135
|
class="pointer-events-none absolute bottom-1 left-1 rounded bg-black/50 px-1 py-0.5 text-[10px] text-white/90"
|
|
@@ -1133,7 +1141,7 @@ async function handleSubmit() {
|
|
|
1133
1141
|
type="button"
|
|
1134
1142
|
class="absolute right-1 top-1 flex size-7 items-center justify-center rounded-full bg-black/60 text-white opacity-100 transition sm:opacity-0 sm:group-hover:opacity-100"
|
|
1135
1143
|
:disabled="imageUploading"
|
|
1136
|
-
aria-label="
|
|
1144
|
+
:aria-label="t('editor.cover.remove')"
|
|
1137
1145
|
@click.stop="removeCoverAt(index)"
|
|
1138
1146
|
@dragstart.stop.prevent
|
|
1139
1147
|
>
|
|
@@ -1154,7 +1162,7 @@ async function handleSubmit() {
|
|
|
1154
1162
|
class="size-6"
|
|
1155
1163
|
:class="imageUploading ? 'animate-spin' : ''"
|
|
1156
1164
|
/>
|
|
1157
|
-
<span class="text-[11px] font-medium"
|
|
1165
|
+
<span class="text-[11px] font-medium">{{ t('editor.cover.add') }}</span>
|
|
1158
1166
|
</button>
|
|
1159
1167
|
</div>
|
|
1160
1168
|
|
|
@@ -1171,10 +1179,10 @@ async function handleSubmit() {
|
|
|
1171
1179
|
:class="imageUploading ? 'animate-spin' : ''"
|
|
1172
1180
|
/>
|
|
1173
1181
|
<span class="text-sm font-medium text-highlighted">
|
|
1174
|
-
{{ imageUploading ? '
|
|
1182
|
+
{{ imageUploading ? t('editor.cover.uploading') : t('editor.cover.dropOrClick') }}
|
|
1175
1183
|
</span>
|
|
1176
1184
|
<span class="text-xs text-muted">
|
|
1177
|
-
|
|
1185
|
+
{{ t('editor.cover.emptyHint') }}
|
|
1178
1186
|
</span>
|
|
1179
1187
|
</button>
|
|
1180
1188
|
</div>
|
|
@@ -1184,7 +1192,7 @@ async function handleSubmit() {
|
|
|
1184
1192
|
color="error"
|
|
1185
1193
|
variant="subtle"
|
|
1186
1194
|
icon="i-lucide-circle-alert"
|
|
1187
|
-
title="
|
|
1195
|
+
:title="t('editor.cover.uploadTitle')"
|
|
1188
1196
|
:description="imageUploadError"
|
|
1189
1197
|
/>
|
|
1190
1198
|
</div>
|
|
@@ -1200,7 +1208,7 @@ async function handleSubmit() {
|
|
|
1200
1208
|
color="error"
|
|
1201
1209
|
variant="subtle"
|
|
1202
1210
|
icon="i-lucide-circle-alert"
|
|
1203
|
-
title="
|
|
1211
|
+
:title="t('editor.uploadFailed')"
|
|
1204
1212
|
:description="imageUploadError"
|
|
1205
1213
|
class="rounded-none rounded-t-xl"
|
|
1206
1214
|
/>
|
|
@@ -1211,7 +1219,7 @@ async function handleSubmit() {
|
|
|
1211
1219
|
content-type="html"
|
|
1212
1220
|
:extensions="editorExtensions"
|
|
1213
1221
|
:handlers="handlers"
|
|
1214
|
-
:placeholder="isImageEditor ? '
|
|
1222
|
+
:placeholder="isImageEditor ? t('editor.imageContentPlaceholder') : t('editor.contentPlaceholder')"
|
|
1215
1223
|
:ui="{ content: 'min-h-64 p-4' }"
|
|
1216
1224
|
class="board-content w-full"
|
|
1217
1225
|
>
|
|
@@ -1232,7 +1240,7 @@ async function handleSubmit() {
|
|
|
1232
1240
|
class="m-2 shrink-0"
|
|
1233
1241
|
:loading="aiRunning"
|
|
1234
1242
|
:disabled="aiRunning"
|
|
1235
|
-
aria-label="
|
|
1243
|
+
:aria-label="t('editor.aiAssistant')"
|
|
1236
1244
|
/>
|
|
1237
1245
|
</UDropdownMenu>
|
|
1238
1246
|
</div>
|
|
@@ -1245,7 +1253,7 @@ async function handleSubmit() {
|
|
|
1245
1253
|
variant="ghost"
|
|
1246
1254
|
size="sm"
|
|
1247
1255
|
:class="ui.handle()"
|
|
1248
|
-
aria-label="
|
|
1256
|
+
:aria-label="t('editor.insertBlock')"
|
|
1249
1257
|
@click="(event: MouseEvent) => {
|
|
1250
1258
|
event.stopPropagation()
|
|
1251
1259
|
const selected = onClick()
|
|
@@ -1268,7 +1276,7 @@ async function handleSubmit() {
|
|
|
1268
1276
|
icon="i-lucide-grip-vertical"
|
|
1269
1277
|
:active="open"
|
|
1270
1278
|
:class="ui.handle()"
|
|
1271
|
-
aria-label="
|
|
1279
|
+
:aria-label="t('editor.blockMenu')"
|
|
1272
1280
|
/>
|
|
1273
1281
|
</UDropdownMenu>
|
|
1274
1282
|
</UEditorDragHandle>
|
|
@@ -1291,13 +1299,13 @@ async function handleSubmit() {
|
|
|
1291
1299
|
name="i-lucide-loader-circle"
|
|
1292
1300
|
class="size-3.5 animate-spin"
|
|
1293
1301
|
/>
|
|
1294
|
-
|
|
1302
|
+
{{ t('editor.imageOptimizing') }}
|
|
1295
1303
|
</p>
|
|
1296
1304
|
</div>
|
|
1297
1305
|
|
|
1298
1306
|
<UInputTags
|
|
1299
1307
|
v-model="tags"
|
|
1300
|
-
placeholder="
|
|
1308
|
+
:placeholder="t('editor.tagsPlaceholder')"
|
|
1301
1309
|
:delimiter="','"
|
|
1302
1310
|
add-on-paste
|
|
1303
1311
|
/>
|
|
@@ -1336,7 +1344,7 @@ async function handleSubmit() {
|
|
|
1336
1344
|
<USwitch
|
|
1337
1345
|
v-model="adminSkipModeration"
|
|
1338
1346
|
size="sm"
|
|
1339
|
-
label="
|
|
1347
|
+
:label="t('editor.skipModeration')"
|
|
1340
1348
|
/>
|
|
1341
1349
|
</div>
|
|
1342
1350
|
|
|
@@ -1345,13 +1353,15 @@ async function handleSubmit() {
|
|
|
1345
1353
|
type="submit"
|
|
1346
1354
|
:loading="saving"
|
|
1347
1355
|
:disabled="isWriteRestricted || imageUploading"
|
|
1348
|
-
:label="isEdit ? '
|
|
1356
|
+
:label="isEdit ? t('editor.submitEdit') : t('editor.submitNext')"
|
|
1357
|
+
@pointerdown="imeSafeSubmitPointerDown"
|
|
1358
|
+
@click="imeSafeSubmitClick"
|
|
1349
1359
|
/>
|
|
1350
1360
|
<UButton
|
|
1351
1361
|
type="button"
|
|
1352
1362
|
variant="ghost"
|
|
1353
1363
|
color="neutral"
|
|
1354
|
-
label="
|
|
1364
|
+
:label="t('common.action.cancel')"
|
|
1355
1365
|
@click="emit('cancel')"
|
|
1356
1366
|
/>
|
|
1357
1367
|
</div>
|
|
@@ -1359,7 +1369,7 @@ async function handleSubmit() {
|
|
|
1359
1369
|
|
|
1360
1370
|
<UModal
|
|
1361
1371
|
v-model:open="imageDialogOpen"
|
|
1362
|
-
title="
|
|
1372
|
+
:title="t('editor.imageDialog.title')"
|
|
1363
1373
|
:ui="{ content: 'sm:max-w-lg' }"
|
|
1364
1374
|
>
|
|
1365
1375
|
<template #body>
|
|
@@ -1369,7 +1379,7 @@ async function handleSubmit() {
|
|
|
1369
1379
|
color="neutral"
|
|
1370
1380
|
variant="outline"
|
|
1371
1381
|
icon="i-lucide-upload"
|
|
1372
|
-
label="
|
|
1382
|
+
:label="t('editor.imageDialog.chooseFile')"
|
|
1373
1383
|
block
|
|
1374
1384
|
:disabled="imageUploading"
|
|
1375
1385
|
@click="chooseImageFileFromDialog"
|
|
@@ -1377,13 +1387,13 @@ async function handleSubmit() {
|
|
|
1377
1387
|
|
|
1378
1388
|
<div class="flex items-center gap-3 text-xs text-muted">
|
|
1379
1389
|
<span class="h-px flex-1 bg-default" />
|
|
1380
|
-
|
|
1390
|
+
{{ t('editor.imageDialog.orLink') }}
|
|
1381
1391
|
<span class="h-px flex-1 bg-default" />
|
|
1382
1392
|
</div>
|
|
1383
1393
|
|
|
1384
1394
|
<UFormField
|
|
1385
|
-
label="
|
|
1386
|
-
help="
|
|
1395
|
+
:label="t('editor.imageDialog.urlLabel')"
|
|
1396
|
+
:help="t('editor.imageDialog.urlHelp')"
|
|
1387
1397
|
>
|
|
1388
1398
|
<UInput
|
|
1389
1399
|
v-model="externalImageUrl"
|
|
@@ -1402,7 +1412,7 @@ async function handleSubmit() {
|
|
|
1402
1412
|
>
|
|
1403
1413
|
<img
|
|
1404
1414
|
:src="externalImageCandidate"
|
|
1405
|
-
alt="
|
|
1415
|
+
:alt="t('editor.imageDialog.previewAlt')"
|
|
1406
1416
|
referrerpolicy="no-referrer"
|
|
1407
1417
|
class="max-h-64 max-w-full object-contain"
|
|
1408
1418
|
>
|
|
@@ -1427,7 +1437,7 @@ async function handleSubmit() {
|
|
|
1427
1437
|
:disabled="externalImageChecking"
|
|
1428
1438
|
@click="imageDialogOpen = false"
|
|
1429
1439
|
>
|
|
1430
|
-
|
|
1440
|
+
{{ t('common.action.cancel') }}
|
|
1431
1441
|
</UButton>
|
|
1432
1442
|
<UButton
|
|
1433
1443
|
type="button"
|
|
@@ -1436,7 +1446,7 @@ async function handleSubmit() {
|
|
|
1436
1446
|
:disabled="!externalImageCandidate"
|
|
1437
1447
|
@click="addExternalImage"
|
|
1438
1448
|
>
|
|
1439
|
-
|
|
1449
|
+
{{ t('editor.imageDialog.addLink') }}
|
|
1440
1450
|
</UButton>
|
|
1441
1451
|
</div>
|
|
1442
1452
|
</template>
|
|
@@ -1444,17 +1454,17 @@ async function handleSubmit() {
|
|
|
1444
1454
|
|
|
1445
1455
|
<UModal
|
|
1446
1456
|
v-model:open="aiCustomDialogOpen"
|
|
1447
|
-
title="
|
|
1457
|
+
:title="t('editor.customDialog.title')"
|
|
1448
1458
|
:ui="{ content: 'sm:max-w-lg' }"
|
|
1449
1459
|
>
|
|
1450
1460
|
<template #body>
|
|
1451
1461
|
<UFormField
|
|
1452
|
-
label="
|
|
1453
|
-
help="
|
|
1462
|
+
:label="t('editor.customDialog.label')"
|
|
1463
|
+
:help="t('editor.customDialog.help')"
|
|
1454
1464
|
>
|
|
1455
1465
|
<UTextarea
|
|
1456
1466
|
v-model="aiCustomInstruction"
|
|
1457
|
-
placeholder="
|
|
1467
|
+
:placeholder="t('editor.customDialog.placeholder')"
|
|
1458
1468
|
autocomplete="off"
|
|
1459
1469
|
class="w-full"
|
|
1460
1470
|
:rows="2"
|
|
@@ -1472,7 +1482,7 @@ async function handleSubmit() {
|
|
|
1472
1482
|
variant="ghost"
|
|
1473
1483
|
@click="aiCustomDialogOpen = false"
|
|
1474
1484
|
>
|
|
1475
|
-
|
|
1485
|
+
{{ t('common.action.cancel') }}
|
|
1476
1486
|
</UButton>
|
|
1477
1487
|
<UButton
|
|
1478
1488
|
type="button"
|
|
@@ -1480,7 +1490,7 @@ async function handleSubmit() {
|
|
|
1480
1490
|
:disabled="!aiCustomInstruction.trim()"
|
|
1481
1491
|
@click="submitCustomAssistant"
|
|
1482
1492
|
>
|
|
1483
|
-
|
|
1493
|
+
{{ t('editor.apply') }}
|
|
1484
1494
|
</UButton>
|
|
1485
1495
|
</div>
|
|
1486
1496
|
</template>
|
|
@@ -1488,7 +1498,7 @@ async function handleSubmit() {
|
|
|
1488
1498
|
|
|
1489
1499
|
<UModal
|
|
1490
1500
|
v-model:open="aiPreviewOpen"
|
|
1491
|
-
title="
|
|
1501
|
+
:title="t('editor.aiPreview.title')"
|
|
1492
1502
|
:ui="{ content: 'sm:max-w-4xl lg:max-w-6xl' }"
|
|
1493
1503
|
>
|
|
1494
1504
|
<template #body>
|
|
@@ -1530,14 +1540,14 @@ async function handleSubmit() {
|
|
|
1530
1540
|
variant="ghost"
|
|
1531
1541
|
@click="closeAiPreview"
|
|
1532
1542
|
>
|
|
1533
|
-
|
|
1543
|
+
{{ t('common.action.cancel') }}
|
|
1534
1544
|
</UButton>
|
|
1535
1545
|
<UButton
|
|
1536
1546
|
type="button"
|
|
1537
1547
|
icon="i-lucide-check"
|
|
1538
1548
|
@click="applyAiPreview"
|
|
1539
1549
|
>
|
|
1540
|
-
|
|
1550
|
+
{{ t('editor.apply') }}
|
|
1541
1551
|
</UButton>
|
|
1542
1552
|
</div>
|
|
1543
1553
|
</template>
|