Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/markdown-charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

- 서버 HTML과 최초 hydration에는 제목·단위·수치 표가 동일하게 렌더된다. 검색엔진과 스크립트를 사용할 수 없는 환경도 값을 읽을 수 있다.
- 기본 선택은 차트다. 화면 200px 근처에 도달했을 때 차트 모듈을 가져온다. 차트 로딩 중·실패 시 표를 유지하고 실패 시 재시도 버튼을 제공한다.
- 코드·다이어그램과 기존 benchmark 모듈의 다운로드 실패도 글 전체 오류로 전파하지 않는다. 원본 코드와 본문을 유지하고 해당 블록에 재시도 버튼을 표시한다.
- `표`·`차트` 전환은 shadcn ToggleGroup을 사용한다. 키보드로 조작 가능하며 툴팁을 사용하지 않아도 표에서 모든 수치와 조건을 확인할 수 있다.
- 막대 축은 0부터 시작한다. 계열 색은 라이트·다크 모드에 맞춰 바뀐다. 불완전 주차의 별표·사선 표시는 표와 차트 모두 유지한다.
- 초기 차트 애니메이션은 사용하지 않는다. 차트와 표는 좁은 본문 너비에 맞춰 표시하며 넓은 표만 자체 가로 스크롤을 허용한다.
Expand Down
20 changes: 17 additions & 3 deletions src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { lazy, Suspense, type ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState } from "react"
import { type ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState } from "react"
import { Copy, Check } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useT } from "@/i18n"
import { useTheme } from "@/hooks/useTheme"
import mermaid from "mermaid"
import { readMarkdownCode } from "@/lib/markdown-code"
import { createRetryableLoader } from "@/lib/async-loader"
import { useDeferredModule } from "@/hooks/useDeferredModule"

const BenchmarkChart = lazy(() => import("@/components/BenchmarkChart").then((module) => ({ default: module.BenchmarkChart })))
const loadBenchmark = createRetryableLoader(() => import("@/components/BenchmarkChart"))

function DeferredBenchmark({ code, children, ...props }: ComponentPropsWithoutRef<"pre"> & { code: string }) {
const { value: module, error, retry } = useDeferredModule(loadBenchmark)
const t = useT()
if (module) return <module.BenchmarkChart data={code} />
return <>
{error && <div className="not-prose flex flex-wrap items-center gap-3 text-sm text-muted-foreground" role="alert">
<p>{t.common.chartLoadError}</p><Button variant="outline" size="sm" onClick={retry}>{t.common.retry}</Button>
</div>}
<pre {...props}>{children}</pre>
</>
}

function getCssHex(varName: string): string {
const temp = document.createElement("div")
Expand Down Expand Up @@ -311,7 +325,7 @@ export function CodeBlock({ children, ...props }: ComponentPropsWithoutRef<"pre"
}

if (language === "benchmark") {
return <Suspense fallback={<pre {...props}>{children}</pre>}><BenchmarkChart data={code} /></Suspense>
return <DeferredBenchmark code={code} {...props}>{children}</DeferredBenchmark>
}

return <ShikiBlock code={code} language={language} preProps={props}>{children}</ShikiBlock>
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export interface Translations {
last30Days: string
tableOfContents: string
copyCode: string
codeLoadError: string
chartView: string
chartTable: string
chartViewLabel: string
Expand Down Expand Up @@ -444,6 +445,7 @@ export const ko: Translations = {
last30Days: "최근 30일",
tableOfContents: "목차",
copyCode: "코드 복사",
codeLoadError: "코드·다이어그램을 불러오지 못했습니다. 원본 내용을 표시합니다.",
chartView: "차트",
chartTable: "표",
chartViewLabel: "보기 방식",
Expand Down Expand Up @@ -680,6 +682,7 @@ export const en: Translations = {
last30Days: "Last 30 Days",
tableOfContents: "Table of Contents",
copyCode: "Copy code",
codeLoadError: "Could not load the code or diagram viewer. Showing the original content.",
chartView: "Chart",
chartTable: "Table",
chartViewLabel: "View",
Expand Down
40 changes: 14 additions & 26 deletions src/pages/PostPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { lazy, Suspense, type ComponentPropsWithoutRef, useEffect, useSyncExternalStore } from "react"
import { type ComponentPropsWithoutRef, useEffect } from "react"
import { useParams, Link } from "react-router-dom"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
Expand Down Expand Up @@ -50,10 +50,10 @@ import { StructuredData } from "@/components/StructuredData"
import { postStructuredData } from "@/lib/structured-data"
import { MarkdownChart } from "@/components/MarkdownChart"
import { readMarkdownCode } from "@/lib/markdown-code"
import { createRetryableLoader } from "@/lib/async-loader"
import { useDeferredModule } from "@/hooks/useDeferredModule"

const LazyCodeBlock = lazy(() =>
import("@/components/CodeBlock").then((module) => ({ default: module.CodeBlock }))
)
const loadCodeBlock = createRetryableLoader(() => import("@/components/CodeBlock"))

function localizeMarkdownLink(href: string, language: Language) {
if (/^\/(posts|tags|series|search|analytics|about)(\/|[?#]|$)/.test(href)) {
Expand All @@ -62,19 +62,6 @@ function localizeMarkdownLink(href: string, language: Language) {
return href
}

function subscribeHydration(callback: () => void) {
const timer = window.setTimeout(callback, 0)
return () => window.clearTimeout(timer)
}

function getClientSnapshot() {
return true
}

function getServerSnapshot() {
return false
}

function CodeBlockFallback({ children, ...props }: ComponentPropsWithoutRef<"pre">) {
return (
<pre
Expand All @@ -93,16 +80,17 @@ function MarkdownCodeBlock(props: ComponentPropsWithoutRef<"pre">) {
}

function HydratedCodeBlock(props: ComponentPropsWithoutRef<"pre">) {
const isClient = useSyncExternalStore(subscribeHydration, getClientSnapshot, getServerSnapshot)

if (!isClient) {
return <CodeBlockFallback {...props} />
}

const { value: module, error, retry } = useDeferredModule(loadCodeBlock)
const { t } = useLanguage()
if (module) return <module.CodeBlock {...props} />
return (
<Suspense fallback={<CodeBlockFallback {...props} />}>
<LazyCodeBlock {...props} />
</Suspense>
<>
{error && <div className="not-prose flex flex-wrap items-center gap-3 text-sm text-muted-foreground" role="alert">
<p>{t.components.codeLoadError}</p>
<Button variant="outline" size="sm" onClick={retry}>{t.common.retry}</Button>
</div>}
<CodeBlockFallback {...props} />
</>
)
}

Expand Down
Loading