v0.0.47 공개 본문 스타일 가이드 기반 정의
Ordered list, 멀티라인/대체 인용구 문법을 추가하고 Prose 컴포넌트(리스트/인용/이미지/카드/임베드) 기본 스타일을 Thred 톤으로 통일했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,8 @@ const createBlock = (type = 'paragraph', text = '', level = null, id = '', optio
|
|||||||
url: options.url || '',
|
url: options.url || '',
|
||||||
alt: options.alt || '',
|
alt: options.alt || '',
|
||||||
title: options.title || '',
|
title: options.title || '',
|
||||||
|
variant: options.variant || '',
|
||||||
|
ordered: options.ordered || false,
|
||||||
width: options.width || 'regular',
|
width: options.width || 'regular',
|
||||||
images: options.images || []
|
images: options.images || []
|
||||||
})
|
})
|
||||||
@@ -89,6 +91,20 @@ const parseMarkdownBlocks = (markdown) => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trimmedLine === '>>>') {
|
||||||
|
const contentLines = []
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
while (index < lines.length && lines[index].trim() !== '<<<') {
|
||||||
|
contentLines.push(lines[index])
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(createBlock('quote', contentLines.join('\n').trim(), null, `block-${blocks.length}`, { variant: 'alt' }))
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (trimmedLine === ':::gallery') {
|
if (trimmedLine === ':::gallery') {
|
||||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||||
const images = []
|
const images = []
|
||||||
@@ -155,7 +171,7 @@ const parseMarkdownBlocks = (markdown) => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const headingMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/)
|
const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/)
|
||||||
|
|
||||||
if (headingMatch) {
|
if (headingMatch) {
|
||||||
blocks.push(createBlock('heading', headingMatch[2], headingMatch[1].length, `block-${blocks.length}`))
|
blocks.push(createBlock('heading', headingMatch[2], headingMatch[1].length, `block-${blocks.length}`))
|
||||||
@@ -164,8 +180,14 @@ const parseMarkdownBlocks = (markdown) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (trimmedLine.startsWith('> ')) {
|
if (trimmedLine.startsWith('> ')) {
|
||||||
blocks.push(createBlock('quote', trimmedLine.replace(/^>\s?/, ''), null, `block-${blocks.length}`))
|
const quoteLines = []
|
||||||
index += 1
|
|
||||||
|
while (index < lines.length && lines[index].trim().startsWith('>')) {
|
||||||
|
quoteLines.push(lines[index].trim().replace(/^>\s?/, ''))
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(createBlock('quote', quoteLines.join('\n').trim(), null, `block-${blocks.length}`))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +203,18 @@ const parseMarkdownBlocks = (markdown) => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/^\d+\.\s+/.test(trimmedLine)) {
|
||||||
|
const items = []
|
||||||
|
|
||||||
|
while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
|
||||||
|
items.push(lines[index].trim().replace(/^\d+\.\s+/, ''))
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(createBlock('list', items, null, `block-${blocks.length}`, { ordered: true }))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
blocks.push(createBlock('paragraph', trimmedLine, null, `block-${blocks.length}`))
|
blocks.push(createBlock('paragraph', trimmedLine, null, `block-${blocks.length}`))
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
@@ -236,10 +270,10 @@ const showNextImage = () => {
|
|||||||
<ProseHeading v-if="block.type === 'heading'" :level="block.level">
|
<ProseHeading v-if="block.type === 'heading'" :level="block.level">
|
||||||
{{ block.text }}
|
{{ block.text }}
|
||||||
</ProseHeading>
|
</ProseHeading>
|
||||||
<ProseBlockquote v-else-if="block.type === 'quote'">
|
<ProseBlockquote v-else-if="block.type === 'quote'" :variant="block.variant || 'default'">
|
||||||
{{ block.text }}
|
{{ block.text }}
|
||||||
</ProseBlockquote>
|
</ProseBlockquote>
|
||||||
<ProseList v-else-if="block.type === 'list'">
|
<ProseList v-else-if="block.type === 'list'" :ordered="block.ordered || false">
|
||||||
<li v-for="(item, itemIndex) in block.text" :key="`${block.id}-${itemIndex}`">
|
<li v-for="(item, itemIndex) in block.text" :key="`${block.id}-${itemIndex}`">
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</li>
|
</li>
|
||||||
@@ -258,7 +292,7 @@ const showNextImage = () => {
|
|||||||
<button
|
<button
|
||||||
v-for="(image, imageIndex) in block.images"
|
v-for="(image, imageIndex) in block.images"
|
||||||
:key="`${block.id}-${image.url}`"
|
:key="`${block.id}-${image.url}`"
|
||||||
class="content-markdown-renderer__gallery-button overflow-hidden rounded bg-surface"
|
class="content-markdown-renderer__gallery-button overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]"
|
||||||
type="button"
|
type="button"
|
||||||
@click="openLightbox(block.images, imageIndex)"
|
@click="openLightbox(block.images, imageIndex)"
|
||||||
>
|
>
|
||||||
@@ -270,7 +304,7 @@ const showNextImage = () => {
|
|||||||
class="content-markdown-renderer__code my-6 overflow-x-auto rounded bg-[#15171a] px-4 py-3 text-sm leading-6 text-white"
|
class="content-markdown-renderer__code my-6 overflow-x-auto rounded bg-[#15171a] px-4 py-3 text-sm leading-6 text-white"
|
||||||
><code>{{ block.text }}</code></pre>
|
><code>{{ block.text }}</code></pre>
|
||||||
<hr v-else-if="block.type === 'divider'" class="content-markdown-renderer__divider my-10 border-line">
|
<hr v-else-if="block.type === 'divider'" class="content-markdown-renderer__divider my-10 border-line">
|
||||||
<p v-else class="content-markdown-renderer__paragraph my-5 leading-8">
|
<p v-else class="content-markdown-renderer__paragraph my-5 text-[15px] leading-8 text-[var(--site-text)]">
|
||||||
{{ block.text }}
|
{{ block.text }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prose-audio my-8 border border-line bg-surface p-5">
|
<div class="prose-audio my-8 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] p-5">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'default'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<blockquote class="prose-blockquote my-8 border-l-4 border-ink bg-surface px-5 py-4 text-xl font-medium leading-8">
|
<blockquote
|
||||||
<slot />
|
class="prose-blockquote my-8 text-[15px] leading-8 text-[var(--site-text)]"
|
||||||
|
:class="variant === 'alt'
|
||||||
|
? 'rounded-[14px] border border-[var(--site-line)] bg-[var(--site-panel)] px-6 py-5 italic'
|
||||||
|
: 'rounded-[10px] border-l-2 border-[var(--site-text)] bg-[var(--site-panel)] px-5 py-4 font-medium'"
|
||||||
|
>
|
||||||
|
<span class="whitespace-pre-line">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ defineProps({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p class="prose-button my-8" :class="{ 'text-center': align === 'center' }">
|
<p class="prose-button my-8" :class="{ 'text-center': align === 'center' }">
|
||||||
<NuxtLink class="prose-button__link inline-flex rounded bg-ink px-5 py-3 text-sm font-semibold text-white hover:bg-muted" :to="href">
|
<NuxtLink
|
||||||
|
class="prose-button__link inline-flex items-center justify-center rounded-full bg-[var(--site-text)] px-5 py-2.5 text-sm font-semibold text-[var(--site-bg)] transition-opacity hover:opacity-80"
|
||||||
|
:to="href"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="prose-callout my-8 border border-line bg-surface p-5">
|
<aside class="prose-callout my-8 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] p-5 text-[15px] leading-8 text-[var(--site-text)]">
|
||||||
<slot />
|
<div class="whitespace-pre-line">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const youtubeEmbedUrl = computed(() => youtubeId.value ? `https://www.youtube.co
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="prose-embed my-8 overflow-hidden border border-line bg-paper">
|
<div class="prose-embed my-8 overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
||||||
<iframe
|
<iframe
|
||||||
v-if="youtubeEmbedUrl"
|
v-if="youtubeEmbedUrl"
|
||||||
class="prose-embed__frame aspect-video w-full"
|
class="prose-embed__frame aspect-video w-full"
|
||||||
@@ -46,7 +46,7 @@ const youtubeEmbedUrl = computed(() => youtubeId.value ? `https://www.youtube.co
|
|||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
class="prose-embed__link block p-5 text-sm font-semibold text-ink hover:opacity-70"
|
class="prose-embed__link block p-5 text-sm font-semibold text-[var(--site-text)] hover:opacity-70"
|
||||||
:href="url"
|
:href="url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prose-file my-8 border border-line bg-paper p-5">
|
<div class="prose-file my-8 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] p-5">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="prose-header-card my-8 bg-ink p-8 text-white" :class="`prose-header-card--${variant}`">
|
<header
|
||||||
|
class="prose-header-card my-8 overflow-hidden rounded-[14px] border border-[var(--site-line)] bg-[var(--site-panel)] p-8 text-[var(--site-text)]"
|
||||||
|
:class="`prose-header-card--${variant}`"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ const tagName = computed(() => `h${Math.min(Math.max(props.level, 1), 6)}`)
|
|||||||
<template>
|
<template>
|
||||||
<component
|
<component
|
||||||
:is="tagName"
|
:is="tagName"
|
||||||
class="prose-heading mt-10 font-semibold leading-tight tracking-normal first:mt-0"
|
class="prose-heading mt-12 font-semibold leading-[1.25] tracking-normal first:mt-0"
|
||||||
:class="{
|
:class="{
|
||||||
'text-5xl': level === 1,
|
'text-[clamp(1.35rem,1.25rem+0.35vw,1.6rem)] leading-[1.15]': level === 1,
|
||||||
'text-4xl': level === 2,
|
'text-[clamp(1.2rem,1.15rem+0.3vw,1.4rem)]': level === 2,
|
||||||
'text-3xl': level === 3,
|
'text-[clamp(1.1rem,1.05rem+0.25vw,1.25rem)]': level === 3,
|
||||||
'text-2xl': level === 4,
|
'text-[clamp(1.025rem,1rem+0.2vw,1.15rem)]': level === 4,
|
||||||
'text-xl': level === 5,
|
'text-[clamp(0.95rem,0.925rem+0.15vw,1.05rem)]': level === 5,
|
||||||
'text-lg': level === 6
|
'text-[clamp(0.9rem,0.875rem+0.1vw,1rem)]': level === 6
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ defineProps({
|
|||||||
<figure
|
<figure
|
||||||
class="prose-image my-8"
|
class="prose-image my-8"
|
||||||
:class="{
|
:class="{
|
||||||
'prose-image--wide lg:-mx-10': variant === 'wide',
|
'prose-image--wide lg:-mx-10 lg:max-w-none': variant === 'wide',
|
||||||
'prose-image--full lg:-mx-20': variant === 'full'
|
'prose-image--full lg:-mx-20 lg:max-w-none': variant === 'full'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<img class="prose-image__media w-full bg-surface object-cover" :src="src" :alt="alt">
|
<div class="overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
||||||
|
<img class="prose-image__media w-full object-cover" :src="src" :alt="alt">
|
||||||
|
</div>
|
||||||
<figcaption v-if="$slots.default" class="prose-image__caption mt-3 text-center text-sm text-muted">
|
<figcaption v-if="$slots.default" class="prose-image__caption mt-3 text-center text-sm text-muted">
|
||||||
<slot />
|
<slot />
|
||||||
</figcaption>
|
</figcaption>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<component
|
<component
|
||||||
:is="ordered ? 'ol' : 'ul'"
|
:is="ordered ? 'ol' : 'ul'"
|
||||||
class="prose-list my-6 space-y-2 pl-6"
|
class="prose-list my-6 space-y-2 pl-5 text-[15px] leading-8 text-[var(--site-text)] marker:text-[var(--site-muted)]"
|
||||||
:class="ordered ? 'list-decimal' : 'list-disc'"
|
:class="ordered ? 'list-decimal' : 'list-disc'"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prose-product my-8 border border-line bg-surface p-5">
|
<div class="prose-product my-8 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] p-5">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<details class="prose-toggle my-6 border border-line bg-paper p-5">
|
<details class="prose-toggle my-6 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] p-5">
|
||||||
<summary class="prose-toggle__summary cursor-pointer font-semibold">
|
<summary class="prose-toggle__summary cursor-pointer text-[15px] font-semibold leading-7 text-[var(--site-text)]">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</summary>
|
</summary>
|
||||||
<div class="prose-toggle__body mt-4 text-muted">
|
<div class="prose-toggle__body mt-4 whitespace-pre-line text-[15px] leading-8 text-[var(--site-muted)]">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prose-video my-8 aspect-video bg-ink text-white">
|
<div class="prose-video my-8 aspect-video overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
25
docs/spec.md
25
docs/spec.md
@@ -103,6 +103,29 @@ components/content/
|
|||||||
└── ProseEmbed.vue # Embeds (YouTube, Twitter)
|
└── ProseEmbed.vue # Embeds (YouTube, Twitter)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 공개 본문 스타일 가이드(Thred 기준)
|
||||||
|
|
||||||
|
- 리스트
|
||||||
|
- Unordered: `- 항목`
|
||||||
|
- Ordered: `1. 항목`
|
||||||
|
- 렌더링: `ProseList.vue` (마커 컬러, 간격, 줄높이 통일)
|
||||||
|
- 인용구
|
||||||
|
- 기본: `> 한 줄` 또는 `>` 연속 여러 줄(멀티라인)
|
||||||
|
- 대체 스타일(Alternative): `>>>`로 시작해 `<<<`로 끝나는 블록
|
||||||
|
- 렌더링: `ProseBlockquote.vue` (`variant=default|alt`)
|
||||||
|
- 이미지
|
||||||
|
- 기본: ``
|
||||||
|
- 와이드/풀: `{width=wide|full}`
|
||||||
|
- 렌더링: `ProseImage.vue` (라운드/보더/패널 배경)
|
||||||
|
- 이미지 갤러리
|
||||||
|
- `:::gallery` ~ `:::` fenced block 내부에 이미지 마크다운 행을 여러 개 작성
|
||||||
|
- 렌더링: `ContentMarkdownRenderer.vue` (그리드 + 라이트박스)
|
||||||
|
- 카드류
|
||||||
|
- Callout: `:::callout` ~ `:::`
|
||||||
|
- Toggle: `:::toggle 제목` ~ `:::`
|
||||||
|
- Embed: `:::embed` ~ `:::` (YouTube URL은 iframe, 그 외는 링크)
|
||||||
|
- 렌더링: `ProseCallout.vue`, `ProseToggle.vue`, `ProseEmbed.vue`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 데이터베이스 구조
|
## 데이터베이스 구조
|
||||||
@@ -458,6 +481,6 @@ APP_PORT=43118
|
|||||||
|
|
||||||
## 버전 관리
|
## 버전 관리
|
||||||
|
|
||||||
- 현재 버전: v0.0.46
|
- 현재 버전: v0.0.47
|
||||||
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
||||||
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v0.0.47
|
||||||
|
|
||||||
|
- 공개 본문 렌더러에서 Ordered list(숫자 목록) 문법을 지원.
|
||||||
|
- 공개 본문 인용구를 멀티라인 처리하고 대체(Alternative) 인용구 스타일 문법을 추가.
|
||||||
|
- 공개 본문 리스트/인용/이미지/갤러리/카드류 기본 스타일을 Thred 톤(보더/라운드/패널/타이포)으로 통일.
|
||||||
|
- 기술 명세에 공개 본문 스타일 가이드를 추가.
|
||||||
|
|
||||||
## v0.0.46
|
## v0.0.46
|
||||||
|
|
||||||
- 홈 Latest 피드 보기 방식 토글(4종) 드롭다운과 localStorage 저장 추가.
|
- 홈 Latest 피드 보기 방식 토글(4종) 드롭다운과 localStorage 저장 추가.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.46",
|
"version": "0.0.47",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user