@roottale/cms-renderer-next 0.65.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/chunk-22MB5YER.js +122 -0
- package/dist/chunk-22MB5YER.js.map +1 -0
- package/dist/cms-public.css +376 -228
- package/dist/fonts/LICENSE +94 -0
- package/dist/fonts/PretendardVariable.woff2 +0 -0
- package/dist/image-lightbox.d.ts +10 -0
- package/dist/image-lightbox.js +8 -0
- package/dist/image-lightbox.js.map +1 -0
- package/dist/routes.js +13 -15
- package/dist/routes.js.map +1 -1
- package/dist/server.js +16 -10
- package/dist/server.js.map +1 -1
- package/package.json +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @roottale/cms-renderer-next
|
|
2
2
|
|
|
3
|
+
## 0.66.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- da23c9e: AVCD 본문 기준으로 기본 읽기 폭을 720px, 행간을 1.7로 맞추고 제목 위계와 문단 간격을 정리합니다. 가져온 HTML 내부 문단에도 24px 간격을 적용하며 모바일 제목 크기를 조정합니다. 기존 테마 변수 재정의는 유지합니다.
|
|
8
|
+
|
|
9
|
+
글머리표·번호는 li에 적용되는 CSS reset에서도 유지됩니다. 강조·기울임·인용·코드·이미지·표까지 AVCD의 서식 체계를 적용하며 독립 본문과 가져온 HTML에도 적용합니다. 번호 목록의 시작 번호도 보존합니다. 모바일 제목은 AVCD 실측값인 22/20/18px입니다.
|
|
10
|
+
|
|
11
|
+
Pretendard(SIL OFL)를 포함하고 헤더 높이를 고려한 sticky 목차 오프셋과 hover/focus 링크 밑줄을 적용합니다. 제목 내부 고정 크기 마크도 반응형 제목 크기를 따릅니다.
|
|
12
|
+
|
|
13
|
+
글 제목은 데스크톱 44px/700/1.3, 모바일 28px/600/1.3으로 통일하고 제목 아래 12px 간격을 적용합니다. 문단에 저장된 고정 글자 크기·행간도 모바일 본문과 소제목 크기를 따르게 합니다. 데스크톱의 문단 서식과 모바일 색상·강조는 유지합니다. 모바일 목차는 닫힌 높이를 52px로 줄입니다.
|
|
14
|
+
|
|
15
|
+
본문 이미지를 읽기 폭까지 크게 표시하고 클릭·Enter·Space로 확대할 수 있습니다. 확대 화면은 2배 확대, Esc·닫기, 포커스 복귀를 지원합니다. 독립 본문용 `image-lightbox` 진입점도 제공합니다.
|
|
16
|
+
|
|
17
|
+
- 1d2be30: 빈 분류 변경도 전체 분류 캐시와 레이아웃을 갱신합니다. 미디어 삭제의 CDN 정리·자동 재시도와 삭제 완료 응답 계약을 문서화합니다.
|
|
18
|
+
- Updated dependencies [a58c5d2]
|
|
19
|
+
- Updated dependencies [d5ef48e]
|
|
20
|
+
- Updated dependencies [716dc9b]
|
|
21
|
+
- Updated dependencies [bb7b516]
|
|
22
|
+
- @roottale/cms-core@0.66.0
|
|
23
|
+
- @roottale/cms-client@0.66.0
|
|
24
|
+
|
|
25
|
+
## 0.65.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- @roottale/cms-client@0.65.1
|
|
30
|
+
- @roottale/cms-core@0.65.1
|
|
31
|
+
|
|
3
32
|
## 0.65.0
|
|
4
33
|
|
|
5
34
|
### Minor Changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/image-lightbox.tsx
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function RootTaleImageLightbox({ children, imageSelector = "img" }) {
|
|
8
|
+
const containerRef = useRef(null);
|
|
9
|
+
const dialogRef = useRef(null);
|
|
10
|
+
const closeButtonRef = useRef(null);
|
|
11
|
+
const triggerRef = useRef(null);
|
|
12
|
+
const [selected, setSelected] = useState(null);
|
|
13
|
+
const [enlarged, setEnlarged] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const container = containerRef.current;
|
|
16
|
+
if (!container) return;
|
|
17
|
+
const cleanups = /* @__PURE__ */ new Map();
|
|
18
|
+
function enhance() {
|
|
19
|
+
if (!container) return;
|
|
20
|
+
for (const image of container.querySelectorAll(imageSelector)) {
|
|
21
|
+
if (image.tagName !== "IMG" || cleanups.has(image) || image.hasAttribute("data-no-zoom") || image.closest("a, button") || image.parentElement?.closest('[role="button"]')) continue;
|
|
22
|
+
const attributes = ["role", "tabindex", "aria-label", "aria-haspopup", "data-rt-image-zoom"];
|
|
23
|
+
const previous = attributes.map((name) => image.getAttribute(name));
|
|
24
|
+
image.setAttribute("role", "button");
|
|
25
|
+
image.setAttribute("tabindex", "0");
|
|
26
|
+
image.setAttribute("aria-label", image.alt ? `${image.alt} \uD655\uB300 \uBCF4\uAE30` : "\uC774\uBBF8\uC9C0 \uD655\uB300 \uBCF4\uAE30");
|
|
27
|
+
image.setAttribute("aria-haspopup", "dialog");
|
|
28
|
+
image.setAttribute("data-rt-image-zoom", "");
|
|
29
|
+
const open = () => {
|
|
30
|
+
triggerRef.current = image;
|
|
31
|
+
setEnlarged(false);
|
|
32
|
+
setSelected({
|
|
33
|
+
src: image.dataset.fullsizeSrc || image.src,
|
|
34
|
+
alt: image.alt,
|
|
35
|
+
zoomWidth: Math.max(image.naturalWidth, image.getBoundingClientRect().width) * 2
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
const keydown = (event) => {
|
|
39
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
open();
|
|
42
|
+
};
|
|
43
|
+
image.addEventListener("click", open);
|
|
44
|
+
image.addEventListener("keydown", keydown);
|
|
45
|
+
cleanups.set(image, () => {
|
|
46
|
+
image.removeEventListener("click", open);
|
|
47
|
+
image.removeEventListener("keydown", keydown);
|
|
48
|
+
attributes.forEach((name, index) => {
|
|
49
|
+
const value = previous[index];
|
|
50
|
+
if (value === null || value === void 0) image.removeAttribute(name);
|
|
51
|
+
else image.setAttribute(name, value);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
enhance();
|
|
57
|
+
const observer = new MutationObserver(enhance);
|
|
58
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
59
|
+
return () => {
|
|
60
|
+
observer.disconnect();
|
|
61
|
+
for (const cleanup of cleanups.values()) cleanup();
|
|
62
|
+
};
|
|
63
|
+
}, [children, imageSelector]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const dialog = dialogRef.current;
|
|
66
|
+
if (!selected || !dialog) return;
|
|
67
|
+
const overflow = document.body.style.overflow;
|
|
68
|
+
document.body.style.overflow = "hidden";
|
|
69
|
+
dialog.showModal();
|
|
70
|
+
closeButtonRef.current?.focus();
|
|
71
|
+
const closeOnBackdrop = (event) => {
|
|
72
|
+
const target = event.target;
|
|
73
|
+
if (target === dialog || target?.classList.contains("rt-cms-image-dialog__viewport")) setSelected(null);
|
|
74
|
+
};
|
|
75
|
+
dialog.addEventListener("click", closeOnBackdrop);
|
|
76
|
+
return () => {
|
|
77
|
+
dialog.removeEventListener("click", closeOnBackdrop);
|
|
78
|
+
if (dialog.open) dialog.close();
|
|
79
|
+
document.body.style.overflow = overflow;
|
|
80
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
81
|
+
};
|
|
82
|
+
}, [selected]);
|
|
83
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, "data-rt-image-gallery": "", style: { display: "contents" }, children: [
|
|
84
|
+
children,
|
|
85
|
+
selected && createPortal(
|
|
86
|
+
/* @__PURE__ */ jsxs(
|
|
87
|
+
"dialog",
|
|
88
|
+
{
|
|
89
|
+
ref: dialogRef,
|
|
90
|
+
className: "rt-cms-image-dialog",
|
|
91
|
+
"aria-label": "\uC774\uBBF8\uC9C0 \uD655\uB300 \uBCF4\uAE30",
|
|
92
|
+
onClose: () => setSelected(null),
|
|
93
|
+
onCancel: () => setSelected(null),
|
|
94
|
+
children: [
|
|
95
|
+
/* @__PURE__ */ jsxs("div", { className: "rt-cms-image-dialog__toolbar", children: [
|
|
96
|
+
/* @__PURE__ */ jsx("button", { type: "button", "aria-pressed": enlarged, onClick: () => setEnlarged(!enlarged), children: enlarged ? "\uD654\uBA74\uC5D0 \uB9DE\uCD94\uAE30" : "2\uBC30 \uD655\uB300" }),
|
|
97
|
+
/* @__PURE__ */ jsxs("button", { ref: closeButtonRef, type: "button", onClick: () => setSelected(null), "aria-label": "\uC774\uBBF8\uC9C0 \uD655\uB300 \uB2EB\uAE30", children: [
|
|
98
|
+
"\uB2EB\uAE30 ",
|
|
99
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xD7" })
|
|
100
|
+
] })
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsx("div", { className: "rt-cms-image-dialog__viewport", "data-enlarged": enlarged, children: /* @__PURE__ */ jsx(
|
|
103
|
+
"img",
|
|
104
|
+
{
|
|
105
|
+
src: selected.src,
|
|
106
|
+
alt: selected.alt,
|
|
107
|
+
referrerPolicy: "no-referrer",
|
|
108
|
+
style: enlarged ? { width: selected.zoomWidth } : void 0
|
|
109
|
+
}
|
|
110
|
+
) })
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
),
|
|
114
|
+
document.body
|
|
115
|
+
)
|
|
116
|
+
] });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
RootTaleImageLightbox
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=chunk-22MB5YER.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/image-lightbox.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\n\ntype ImageSource = { src: string; alt: string; zoomWidth: number };\n\n/** HTML·Tiptap 본문 모두에 적용한다. 이미 링크나 버튼인 이미지는 원래 동작을 유지한다. */\nexport function RootTaleImageLightbox({ children, imageSelector = 'img' }: {\n children: ReactNode;\n imageSelector?: string;\n}) {\n const containerRef = useRef<HTMLDivElement>(null);\n const dialogRef = useRef<HTMLDialogElement>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n const triggerRef = useRef<HTMLImageElement | null>(null);\n const [selected, setSelected] = useState<ImageSource | null>(null);\n const [enlarged, setEnlarged] = useState(false);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const cleanups = new Map<HTMLImageElement, () => void>();\n\n function enhance() {\n if (!container) return;\n for (const image of container.querySelectorAll<HTMLImageElement>(imageSelector)) {\n if (image.tagName !== 'IMG' || cleanups.has(image) || image.hasAttribute('data-no-zoom') ||\n image.closest('a, button') || image.parentElement?.closest('[role=\"button\"]')) continue;\n const attributes = ['role', 'tabindex', 'aria-label', 'aria-haspopup', 'data-rt-image-zoom'];\n const previous = attributes.map((name) => image.getAttribute(name));\n image.setAttribute('role', 'button');\n image.setAttribute('tabindex', '0');\n image.setAttribute('aria-label', image.alt ? `${image.alt} 확대 보기` : '이미지 확대 보기');\n image.setAttribute('aria-haspopup', 'dialog');\n image.setAttribute('data-rt-image-zoom', '');\n\n const open = () => {\n triggerRef.current = image;\n setEnlarged(false);\n setSelected({\n src: image.dataset.fullsizeSrc || image.src,\n alt: image.alt,\n zoomWidth: Math.max(image.naturalWidth, image.getBoundingClientRect().width) * 2,\n });\n };\n const keydown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' && event.key !== ' ') return;\n event.preventDefault();\n open();\n };\n image.addEventListener('click', open);\n image.addEventListener('keydown', keydown);\n cleanups.set(image, () => {\n image.removeEventListener('click', open);\n image.removeEventListener('keydown', keydown);\n attributes.forEach((name, index) => {\n const value = previous[index];\n if (value === null || value === undefined) image.removeAttribute(name);\n else image.setAttribute(name, value);\n });\n });\n }\n }\n\n enhance();\n const observer = new MutationObserver(enhance);\n observer.observe(container, { childList: true, subtree: true });\n return () => {\n observer.disconnect();\n for (const cleanup of cleanups.values()) cleanup();\n };\n }, [children, imageSelector]);\n\n useEffect(() => {\n const dialog = dialogRef.current;\n if (!selected || !dialog) return;\n const overflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n dialog.showModal();\n closeButtonRef.current?.focus();\n // 배경(dialog 자체·여백 영역) 클릭 닫기는 마우스 보조 동작이다. 키보드는 Esc(cancel)·닫기 버튼으로 같은 결과를 얻는다.\n const closeOnBackdrop = (event: MouseEvent) => {\n const target = event.target as HTMLElement | null;\n if (target === dialog || target?.classList.contains('rt-cms-image-dialog__viewport')) setSelected(null);\n };\n dialog.addEventListener('click', closeOnBackdrop);\n return () => {\n dialog.removeEventListener('click', closeOnBackdrop);\n if (dialog.open) dialog.close();\n document.body.style.overflow = overflow;\n triggerRef.current?.focus({ preventScroll: true });\n };\n }, [selected]);\n\n return (\n <div ref={containerRef} data-rt-image-gallery=\"\" style={{ display: 'contents' }}>\n {children}\n {selected && createPortal(\n <dialog ref={dialogRef} className=\"rt-cms-image-dialog\" aria-label=\"이미지 확대 보기\"\n onClose={() => setSelected(null)}\n onCancel={() => setSelected(null)}>\n <div className=\"rt-cms-image-dialog__toolbar\">\n <button type=\"button\" aria-pressed={enlarged} onClick={() => setEnlarged(!enlarged)}>\n {enlarged ? '화면에 맞추기' : '2배 확대'}\n </button>\n <button ref={closeButtonRef} type=\"button\" onClick={() => setSelected(null)} aria-label=\"이미지 확대 닫기\">\n 닫기 <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n <div className=\"rt-cms-image-dialog__viewport\" data-enlarged={enlarged}>\n <img src={selected.src} alt={selected.alt} referrerPolicy=\"no-referrer\"\n style={enlarged ? { width: selected.zoomWidth } : undefined} />\n </div>\n </dialog>, document.body,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|