인라인 텍스트 색상 패널 추가

This commit is contained in:
2026-06-16 16:54:01 +09:00
parent 0bb07b5e03
commit 6c4064d8b5
16 changed files with 581 additions and 126 deletions

View File

@@ -1540,4 +1540,48 @@ defineExpose({ focusEditor, readEditorValue })
font-size: 0.9em;
color: #fff;
}
.content-markdown-editable-inline :deep([data-inline-color='gray']) {
color: var(--site-muted);
}
.content-markdown-editable-inline :deep([data-inline-color='blue']) {
color: #0055ff;
}
.content-markdown-editable-inline :deep([data-inline-color='green']) {
color: #16ae68;
}
.content-markdown-editable-inline :deep([data-inline-color='yellow']) {
color: #a16207;
}
.content-markdown-editable-inline :deep([data-inline-color='red']) {
color: #e60000;
}
.content-markdown-editable-inline :deep([data-inline-color='purple']) {
color: #8800ff;
}
:global(html[data-theme='dark']) .content-markdown-editable-inline :deep([data-inline-color='blue']) {
color: #7aa2ff;
}
:global(html[data-theme='dark']) .content-markdown-editable-inline :deep([data-inline-color='green']) {
color: #5ee0a0;
}
:global(html[data-theme='dark']) .content-markdown-editable-inline :deep([data-inline-color='yellow']) {
color: #facc15;
}
:global(html[data-theme='dark']) .content-markdown-editable-inline :deep([data-inline-color='red']) {
color: #ff6b6b;
}
:global(html[data-theme='dark']) .content-markdown-editable-inline :deep([data-inline-color='purple']) {
color: #c084fc;
}
</style>

View File

@@ -0,0 +1,103 @@
<script setup>
import { parseInlineSegments } from '../../lib/markdown-inline.js'
const props = defineProps({
/** 렌더링할 인라인 마크다운 텍스트 */
text: {
type: String,
default: ''
},
/** 링크에 적용할 클래스 */
linkClass: {
type: String,
default: 'text-[var(--site-accent)] underline underline-offset-4'
},
/** 줄바꿈을 br로 렌더링할지 여부 */
multiline: {
type: Boolean,
default: false
}
})
const segmentLines = computed(() => {
const source = String(props.text || '')
const lines = props.multiline ? source.split('\n') : [source]
return lines.map(parseInlineSegments)
})
/**
* 인라인 색상 클래스명을 반환한다.
* @param {string} color - 색상 이름
* @returns {string} 클래스명
*/
const getInlineColorClass = (color) => `content-markdown-inline-segments__color--${String(color || '').trim().toLowerCase()}`
</script>
<template>
<template v-for="(lineSegments, lineIndex) in segmentLines" :key="`inline-line-${lineIndex}`">
<br v-if="lineIndex > 0">
<template v-for="(segment, segmentIndex) in lineSegments" :key="`inline-segment-${lineIndex}-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-inline-segments__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" :class="linkClass" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<span
v-else-if="segment.type === 'color'"
class="content-markdown-inline-segments__color"
:class="getInlineColorClass(segment.color)"
:data-inline-color="segment.color"
>
<ContentMarkdownInlineSegments :text="segment.text" :link-class="linkClass" />
</span>
<template v-else>{{ segment.text }}</template>
</template>
</template>
</template>
<style scoped>
.content-markdown-inline-segments__color--gray {
color: var(--site-muted);
}
.content-markdown-inline-segments__color--blue {
color: #0055ff;
}
.content-markdown-inline-segments__color--green {
color: #16ae68;
}
.content-markdown-inline-segments__color--yellow {
color: #a16207;
}
.content-markdown-inline-segments__color--red {
color: #e60000;
}
.content-markdown-inline-segments__color--purple {
color: #8800ff;
}
:global(html[data-theme='dark']) .content-markdown-inline-segments__color--blue {
color: #7aa2ff;
}
:global(html[data-theme='dark']) .content-markdown-inline-segments__color--green {
color: #5ee0a0;
}
:global(html[data-theme='dark']) .content-markdown-inline-segments__color--yellow {
color: #facc15;
}
:global(html[data-theme='dark']) .content-markdown-inline-segments__color--red {
color: #ff6b6b;
}
:global(html[data-theme='dark']) .content-markdown-inline-segments__color--purple {
color: #c084fc;
}
</style>

View File

@@ -5,7 +5,7 @@ import {
isImageUrl,
parseImageMarkdownLine
} from '../../lib/markdown-image.js'
import { parseInlineSegments, readEditableTextFromElement, setEditableCaretOffset } from '../../lib/markdown-inline.js'
import { readEditableTextFromElement, setEditableCaretOffset } from '../../lib/markdown-inline.js'
import {
applyLiveSelectionDelete,
collapseLiveSelection,
@@ -2482,15 +2482,6 @@ const onMergeWithPreviousLine = (lineIndex, payload) => {
emit('merge-with-previous-line', { lineIndex, mergedLine })
}
/**
* 줄바꿈이 포함된 인라인 마크다운을 줄 단위 세그먼트로 변환한다.
* @param {string} value - 원본 문자열
* @returns {Array<Array<{ type: string, text: string, href?: string }>>} 줄별 인라인 세그먼트
*/
const parseInlineSegmentLines = (value) => {
return String(value || '').split('\n').map(parseInlineSegments)
}
/**
* 라이트박스를 연다
* @param {Array<Object>} images - 이미지 목록
@@ -3057,15 +3048,7 @@ onBeforeUnmount(() => {
@merge-with-previous="onMergeWithPreviousLine(block.meta.startLine, $event)"
/>
<ProseHeading v-else-if="block.type === 'heading'" :id="headingIdsByBlock[block.id]" :level="block.level">
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-heading-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
<ContentMarkdownInlineSegments :text="block.text" />
</ProseHeading>
<ProseBlockquote
v-else-if="block.type === 'quote' && interactive && block.variant !== 'alt'"
@@ -3097,15 +3080,7 @@ onBeforeUnmount(() => {
:variant="block.variant || 'default'"
:background="block.quoteBackground"
>
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-quote-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
<ContentMarkdownInlineSegments :text="block.text" />
</ProseBlockquote>
<ProseList v-else-if="block.type === 'list' && interactive" :ordered="block.ordered || false">
<li
@@ -3156,15 +3131,7 @@ onBeforeUnmount(() => {
aria-hidden="true"
/>
<span class="min-w-0 flex-1">
<template v-for="(segment, segmentIndex) in parseInlineSegments(item)" :key="`${block.id}-${itemIndex}-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
<ContentMarkdownInlineSegments :text="item" />
</span>
</li>
</ProseList>
@@ -3244,15 +3211,7 @@ onBeforeUnmount(() => {
:title="block.title"
:background="block.calloutBackground"
>
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-callout-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
<ContentMarkdownInlineSegments :text="block.text" link-class="underline underline-offset-4" />
</ProseCallout>
<ContentMarkdownToggleEditor
v-else-if="block.type === 'toggle' && interactive"
@@ -3267,15 +3226,7 @@ onBeforeUnmount(() => {
@delete-line="onDeleteLine"
/>
<ProseToggle v-else-if="block.type === 'toggle'" :title="block.title || '더 보기'" :default-open="block.defaultOpen" animated>
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-toggle-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
<ContentMarkdownInlineSegments :text="block.text" />
</ProseToggle>
<ProseBookmark
v-else-if="block.type === 'bookmark' && block.meta.url"
@@ -3466,15 +3417,7 @@ onBeforeUnmount(() => {
class="border border-line bg-[color-mix(in_srgb,var(--site-text)_6%,transparent)] px-3 py-2 font-semibold"
:class="getTableCellAlignClass(block.meta.alignments?.[headerIndex])"
>
<template v-for="(segment, segmentIndex) in parseInlineSegments(header)" :key="`${block.id}-table-head-${headerIndex}-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
<ContentMarkdownInlineSegments :text="header" />
</th>
</tr>
</thead>
@@ -3486,15 +3429,7 @@ onBeforeUnmount(() => {
class="border border-line px-3 py-2 align-top"
:class="getTableCellAlignClass(block.meta.alignments?.[cellIndex])"
>
<template v-for="(segment, segmentIndex) in parseInlineSegments(row[cellIndex] || '')" :key="`${block.id}-table-cell-${rowIndex}-${cellIndex}-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
<ContentMarkdownInlineSegments :text="row[cellIndex] || ''" />
</td>
</tr>
</tbody>
@@ -3543,18 +3478,7 @@ onBeforeUnmount(() => {
@slash-apply="emit('slash-apply', $event)"
/>
<p v-else-if="block.type === 'paragraph'" class="content-markdown-renderer__paragraph mb-2.5 text-base text-[var(--site-text)] last:mb-0">
<template v-for="(lineSegments, lineIndex) in parseInlineSegmentLines(block.text)" :key="`${block.id}-paragraph-line-${lineIndex}`">
<br v-if="lineIndex > 0">
<template v-for="(segment, segmentIndex) in lineSegments" :key="`${block.id}-paragraph-${lineIndex}-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
</template>
<ContentMarkdownInlineSegments :text="block.text" multiline />
</p>
<SiteAdSlot