라이브 모드 코드/콜아웃/토글 편집, 슬래시 명령, 홈 Latest List·Compact·Cards 보기, 사이트 설정 메인 화면 커버(720px) 및 HomeHero 반영. Co-authored-by: Cursor <cursoragent@cursor.com>
258 lines
8.1 KiB
Vue
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>
|