Files
sori.studio/components/content/ContentMarkdownCalloutEditor.vue
zenn 3fb8a40031 v1.2.9: 라이브 에디터·홈 피드·메인 커버 개선
라이브 모드 코드/콜아웃/토글 편집, 슬래시 명령, 홈 Latest List·Compact·Cards 보기,
사이트 설정 메인 화면 커버(720px) 및 HomeHero 반영.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 16:57:30 +09:00

258 lines
8.1 KiB
Vue

<script setup>
import {
buildCalloutOpenerLine,
CALLOUT_BACKGROUND_OPTIONS,
CALLOUT_EMOJI_OPTIONS
} from '../../lib/markdown-callout.js'
import ProseCallout from './ProseCallout.vue'
import ContentMarkdownEditableInline from './ContentMarkdownEditableInline.vue'
const props = defineProps({
/** 콜아웃 본문 */
modelValue: {
type: String,
default: ''
},
calloutEmojiEnabled: {
type: Boolean,
default: true
},
calloutEmoji: {
type: String,
default: '💡'
},
calloutBackground: {
type: String,
default: 'blue'
},
/** 본문 첫 줄 source-line(0-based) */
bodySourceLine: {
type: Number,
required: true
}
})
const emit = defineEmits(['commit'])
const settingsOpen = ref(false)
const emojiDraft = ref(props.calloutEmoji)
const emojiEnabled = ref(props.calloutEmojiEnabled)
const background = ref(props.calloutBackground)
/** @type {import('vue').ComputedRef<Array<{ value: string, label: string }>>} */
const backgroundOptions = computed(() => CALLOUT_BACKGROUND_OPTIONS.map((value) => ({
value,
label: value
})))
/** @type {import('vue').ComputedRef<string[]>} */
const emojiOptions = computed(() => CALLOUT_EMOJI_OPTIONS)
watch(() => props.calloutEmoji, (value) => {
emojiDraft.value = value
})
watch(() => props.calloutEmojiEnabled, (value) => {
emojiEnabled.value = value
})
watch(() => props.calloutBackground, (value) => {
background.value = value
})
/**
* 콜아웃 마크다운 줄을 반영한다.
* @param {string} body - 본문
* @returns {void}
*/
const commitCallout = (body) => {
const contentLines = String(body ?? '').replace(/\r/g, '').split('\n')
emit('commit', [
buildCalloutOpenerLine({
calloutEmojiEnabled: emojiEnabled.value,
calloutEmoji: emojiDraft.value,
calloutBackground: background.value
}),
...contentLines,
':::'
])
}
/**
* 본문 편집 반영
* @param {string} body - 본문
* @returns {void}
*/
const onBodyCommit = (body) => {
commitCallout(body)
}
/**
* 설정 모달을 연다.
* @returns {void}
*/
const openSettings = () => {
settingsOpen.value = true
}
/**
* 설정 모달을 닫는다.
* @returns {void}
*/
const closeSettings = () => {
settingsOpen.value = false
}
/**
* 이모지 표시를 토글한다.
* @returns {void}
*/
const toggleEmoji = () => {
emojiEnabled.value = !emojiEnabled.value
commitCallout(props.modelValue)
}
/**
* 배경색을 변경한다.
* @param {string} value - 배경 키
* @returns {void}
*/
const setBackground = (value) => {
background.value = value
commitCallout(props.modelValue)
}
/**
* 프리셋 이모지를 선택한다.
* @param {string} value - 이모지
* @returns {void}
*/
const setEmoji = (value) => {
emojiDraft.value = value
emojiEnabled.value = true
commitCallout(props.modelValue)
}
</script>
<template>
<div class="content-markdown-callout-editor relative">
<ProseCallout
:emoji-enabled="false"
:background="background"
>
<div class="content-markdown-callout-editor__inner flex items-start gap-2">
<button
class="content-markdown-callout-editor__emoji-btn inline-flex size-9 shrink-0 items-center justify-center rounded-md text-xl text-[var(--site-text)] transition-colors hover:bg-black/10"
type="button"
aria-label="콜아웃 아이콘·색상 설정"
@mousedown.prevent
@click.stop="openSettings"
>
<span v-if="emojiEnabled">{{ emojiDraft || '💡' }}</span>
<span v-else class="text-base text-[#8e9cac]">+</span>
</button>
<ContentMarkdownEditableInline
block-class="content-markdown-callout-editor__body min-w-0 flex-1 text-[15px] leading-8 text-[var(--site-text)]"
enter-mode="multiline"
:source-line="bodySourceLine"
:model-value="modelValue"
@commit="onBodyCommit"
/>
</div>
</ProseCallout>
<Teleport to="body">
<div
v-if="settingsOpen"
class="content-markdown-callout-editor__modal fixed inset-0 z-[80] grid place-items-center bg-black/35 px-4"
@click.self="closeSettings"
>
<section
class="content-markdown-callout-editor__panel w-full max-w-sm rounded-lg bg-white p-6 shadow-xl"
role="dialog"
aria-labelledby="callout-settings-title"
@click.stop
>
<header class="content-markdown-callout-editor__panel-header mb-4 flex items-center justify-between gap-3">
<h2 id="callout-settings-title" class="text-base font-semibold text-[#15171a]">
콜아웃 설정
</h2>
<button
class="content-markdown-callout-editor__close rounded px-2 py-1 text-sm text-[#6b7280] hover:bg-[#f1f3f4]"
type="button"
@click="closeSettings"
>
닫기
</button>
</header>
<div class="content-markdown-callout-editor__icon-toggle mb-4 flex items-center justify-between gap-3">
<span class="text-sm font-medium text-[#15171a]">아이콘</span>
<button
class="content-markdown-callout-editor__switch relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#8e9cac]"
:class="emojiEnabled ? 'bg-[#15171a]' : 'bg-[#d0d7de]'"
type="button"
role="switch"
:aria-checked="emojiEnabled"
@click="toggleEmoji"
>
<span
class="content-markdown-callout-editor__switch-knob pointer-events-none inline-block size-5 rounded-full bg-white shadow transition-transform duration-200 ease-out"
:class="emojiEnabled ? 'translate-x-[22px]' : 'translate-x-0.5'"
/>
<span class="sr-only">{{ emojiEnabled ? '아이콘 표시' : '아이콘 숨김' }}</span>
</button>
</div>
<div
v-if="emojiEnabled"
class="content-markdown-callout-editor__emoji-presets mb-4"
>
<ul class="flex flex-wrap gap-2">
<li
v-for="emoji in emojiOptions"
:key="`callout-emoji-${emoji}`"
>
<button
class="content-markdown-callout-editor__emoji-swatch inline-flex size-9 items-center justify-center rounded-full border-2 text-xl transition-transform"
:class="emojiDraft === emoji ? 'border-[#22c55e] scale-110 bg-[#f6f7f8]' : 'border-transparent hover:bg-[#f1f3f4]'"
type="button"
:aria-label="`이모지 ${emoji}`"
@click="setEmoji(emoji)"
>
{{ emoji }}
</button>
</li>
</ul>
</div>
<div class="content-markdown-callout-editor__colors">
<p class="mb-2 text-sm font-medium text-[#15171a]">배경색</p>
<ul class="flex flex-wrap gap-2">
<li v-for="option in backgroundOptions" :key="`callout-bg-${option.value}`">
<button
class="content-markdown-callout-editor__color-swatch size-8 rounded-full border-2 transition-transform"
:class="background === option.value ? 'border-[#22c55e] scale-110' : 'border-transparent'"
type="button"
:aria-label="option.label"
:style="{
background: option.value === 'gray' ? 'rgba(100,116,139,0.28)'
: option.value === 'blue' ? 'rgba(59,130,246,0.3)'
: option.value === 'green' ? 'rgba(34,197,94,0.3)'
: option.value === 'yellow' ? 'rgba(245,158,11,0.34)'
: option.value === 'red' ? 'rgba(239,68,68,0.3)'
: option.value === 'purple' ? 'rgba(168,85,247,0.3)'
: 'rgba(236,72,153,0.3)'
}"
@click="setBackground(option.value)"
/>
</li>
</ul>
</div>
</section>
</div>
</Teleport>
</div>
</template>