@roottale/cms-renderer-next 0.44.0 → 0.46.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 +34 -0
- package/dist/{chunk-4LQPZDXZ.js → chunk-6MFHA555.js} +24 -6
- package/dist/chunk-6MFHA555.js.map +1 -0
- package/dist/chunk-FZK3EXYZ.js +103 -0
- package/dist/chunk-FZK3EXYZ.js.map +1 -0
- package/dist/{chunk-75LHSTKC.js → chunk-JXRHZGRJ.js} +72 -19
- package/dist/chunk-JXRHZGRJ.js.map +1 -0
- package/dist/chunk-LEHHSI6G.js +64 -0
- package/dist/chunk-LEHHSI6G.js.map +1 -0
- package/dist/chunk-OMXEU4LS.js +414 -0
- package/dist/chunk-OMXEU4LS.js.map +1 -0
- package/dist/{chunk-6PDZSFVZ.js → chunk-UM37DOIK.js} +44 -2
- package/dist/chunk-UM37DOIK.js.map +1 -0
- package/dist/chunk-YLSQAT6B.js +59 -0
- package/dist/chunk-YLSQAT6B.js.map +1 -0
- package/dist/chunk-ZL2NA3QT.js +313 -0
- package/dist/chunk-ZL2NA3QT.js.map +1 -0
- package/dist/concerns-client.d.ts +1 -1
- package/dist/contact-map-client.d.ts +27 -0
- package/dist/contact-map-client.js +8 -0
- package/dist/contact-map-client.js.map +1 -0
- package/dist/expertise-client.d.ts +7 -1
- package/dist/expertise-client.js +2 -1
- package/dist/hero-carousel-client.d.ts +18 -2
- package/dist/hero-carousel-client.js +2 -1
- package/dist/routes.js +6 -4
- package/dist/routes.js.map +1 -1
- package/dist/server.js +8 -5
- package/dist/{site-config-BHcVF993.d.ts → site-config-CeByrPJF.d.ts} +55 -4
- package/dist/spaces-client.d.ts +1 -1
- package/dist/team-expanding-stage-client.d.ts +35 -0
- package/dist/team-expanding-stage-client.js +8 -0
- package/dist/team-expanding-stage-client.js.map +1 -0
- package/dist/templates.css +457 -5
- package/dist/templates.d.ts +26 -3
- package/dist/templates.js +13 -6
- package/dist/templates.js.map +1 -1
- package/dist/ui-react-motion-client.d.ts +4 -1
- package/dist/ui-react-motion-client.js +3 -1
- package/dist/validate.js +1 -1
- package/package.json +7 -4
- package/dist/chunk-4LQPZDXZ.js.map +0 -1
- package/dist/chunk-6PDZSFVZ.js.map +0 -1
- package/dist/chunk-75LHSTKC.js.map +0 -1
- package/dist/chunk-F3G6RVIR.js +0 -220
- package/dist/chunk-F3G6RVIR.js.map +0 -1
- package/dist/chunk-WWS43XCT.js +0 -206
- package/dist/chunk-WWS43XCT.js.map +0 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/templates/contact-map-client.tsx
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
function ContactMapClickToLoad({
|
|
7
|
+
embedUrl,
|
|
8
|
+
externalUrl,
|
|
9
|
+
facadeImageSrc,
|
|
10
|
+
facadeImageAlt
|
|
11
|
+
}) {
|
|
12
|
+
const [loaded, setLoaded] = useState(false);
|
|
13
|
+
if (loaded) {
|
|
14
|
+
return /* @__PURE__ */ jsx("div", { className: "rt-tpl-contact-map", children: /* @__PURE__ */ jsx(
|
|
15
|
+
"iframe",
|
|
16
|
+
{
|
|
17
|
+
src: embedUrl,
|
|
18
|
+
title: "\uC624\uC2DC\uB294\uAE38 \uC9C0\uB3C4",
|
|
19
|
+
loading: "lazy",
|
|
20
|
+
referrerPolicy: "no-referrer-when-downgrade"
|
|
21
|
+
}
|
|
22
|
+
) });
|
|
23
|
+
}
|
|
24
|
+
return (
|
|
25
|
+
// `rt-tpl-contact-map` 클래스를 함께 붙여 aspect-ratio/border 등 variant별
|
|
26
|
+
// 박스 규칙(channel-cards/visit-band 오버라이드 포함, templates.css)을
|
|
27
|
+
// 그대로 물려받는다 — 클릭 후 iframe 전환 시 레이아웃 시프트가 없다.
|
|
28
|
+
/* @__PURE__ */ jsxs("div", { className: "rt-tpl-contact-map rt-tpl-contact-map-click", children: [
|
|
29
|
+
facadeImageSrc ? /* @__PURE__ */ jsx(
|
|
30
|
+
"img",
|
|
31
|
+
{
|
|
32
|
+
className: "rt-tpl-contact-map-click-bg",
|
|
33
|
+
src: facadeImageSrc,
|
|
34
|
+
alt: facadeImageAlt ?? "",
|
|
35
|
+
loading: "lazy"
|
|
36
|
+
}
|
|
37
|
+
) : null,
|
|
38
|
+
/* @__PURE__ */ jsx(
|
|
39
|
+
"button",
|
|
40
|
+
{
|
|
41
|
+
type: "button",
|
|
42
|
+
className: "rt-tpl-contact-map-click-btn",
|
|
43
|
+
onClick: () => setLoaded(true),
|
|
44
|
+
children: "\uC9C0\uB3C4 \uC5F4\uAE30"
|
|
45
|
+
}
|
|
46
|
+
),
|
|
47
|
+
externalUrl ? /* @__PURE__ */ jsx(
|
|
48
|
+
"a",
|
|
49
|
+
{
|
|
50
|
+
className: "rt-tpl-contact-map-click-external",
|
|
51
|
+
href: externalUrl,
|
|
52
|
+
target: "_blank",
|
|
53
|
+
rel: "noopener noreferrer",
|
|
54
|
+
children: "\uC0C8 \uCC3D\uC5D0\uC11C \uD06C\uAC8C \uBCF4\uAE30"
|
|
55
|
+
}
|
|
56
|
+
) : null
|
|
57
|
+
] })
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
ContactMapClickToLoad
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=chunk-LEHHSI6G.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/templates/contact-map-client.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `contact` 지도 블록의 \"클릭-투-로드\" 모드(ADR-0086 P2 D3) — chambarun\n * mockup `.map-facade`/`site.js` \"지도: 클릭-투-로드\" 이식\n * (`clients/chambarun/mockup/assets/site.js` \"지도: 클릭-투-로드\" 블록).\n *\n * 원본은 DOM 을 직접 갈아끼우는 vanilla 스크립트(`replaceWith`)였다 — 여기서는\n * React state 로 같은 효과(정적 프리뷰 → 클릭 시 1회 iframe 마운트)를 낸다.\n * `ContactMapBlock`(sections.tsx, RSC)이 embedUrl 이 allowlist 를 통과했을\n * 때만 이 컴포넌트를 렌더하므로, 이 컴포넌트 자신은 allowlist 재검증을 하지\n * 않는다(호출자 계약).\n *\n * 빌드: tsup 독립 entry(`contact-map-client`, tsup.config.ts) — \"use client\"\n * 경계가 `templates.js`(서버 전용)로 새지 않게 격리(hero-carousel-client 류와\n * 동일 계약).\n */\nimport { useState, type ReactElement } from \"react\";\n\nexport interface ContactMapClickToLoadProps {\n readonly embedUrl: string;\n readonly externalUrl: string | undefined;\n readonly facadeImageSrc: string | undefined;\n readonly facadeImageAlt: string | undefined;\n}\n\nexport function ContactMapClickToLoad({\n embedUrl,\n externalUrl,\n facadeImageSrc,\n facadeImageAlt,\n}: ContactMapClickToLoadProps): ReactElement {\n const [loaded, setLoaded] = useState(false);\n\n if (loaded) {\n return (\n <div className=\"rt-tpl-contact-map\">\n <iframe\n src={embedUrl}\n title=\"오시는길 지도\"\n loading=\"lazy\"\n referrerPolicy=\"no-referrer-when-downgrade\"\n />\n </div>\n );\n }\n\n return (\n // `rt-tpl-contact-map` 클래스를 함께 붙여 aspect-ratio/border 등 variant별\n // 박스 규칙(channel-cards/visit-band 오버라이드 포함, templates.css)을\n // 그대로 물려받는다 — 클릭 후 iframe 전환 시 레이아웃 시프트가 없다.\n <div className=\"rt-tpl-contact-map rt-tpl-contact-map-click\">\n {facadeImageSrc ? (\n <img\n className=\"rt-tpl-contact-map-click-bg\"\n src={facadeImageSrc}\n alt={facadeImageAlt ?? \"\"}\n loading=\"lazy\"\n />\n ) : null}\n <button\n type=\"button\"\n className=\"rt-tpl-contact-map-click-btn\"\n onClick={() => setLoaded(true)}\n >\n 지도 열기\n </button>\n {externalUrl ? (\n <a\n className=\"rt-tpl-contact-map-click-external\"\n href={externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n 새 창에서 크게 보기\n </a>\n ) : null}\n </div>\n );\n}\n"],"mappings":";AAiBA,SAAS,gBAAmC;AAoBpC,cAcJ,YAdI;AAXD,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6C;AAC3C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAE1C,MAAI,QAAQ;AACV,WACE,oBAAC,SAAI,WAAU,sBACb;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,OAAM;AAAA,QACN,SAAQ;AAAA,QACR,gBAAe;AAAA;AAAA,IACjB,GACF;AAAA,EAEJ;AAEA;AAAA;AAAA;AAAA;AAAA,IAIE,qBAAC,SAAI,WAAU,+CACZ;AAAA,uBACC;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,KAAK;AAAA,UACL,KAAK,kBAAkB;AAAA,UACvB,SAAQ;AAAA;AAAA,MACV,IACE;AAAA,MACJ;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UACV,SAAS,MAAM,UAAU,IAAI;AAAA,UAC9B;AAAA;AAAA,MAED;AAAA,MACC,cACC;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,MAAM;AAAA,UACN,QAAO;AAAA,UACP,KAAI;AAAA,UACL;AAAA;AAAA,MAED,IACE;AAAA,OACN;AAAA;AAEJ;","names":[]}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useRovingTablist
|
|
5
|
+
} from "./chunk-YLSQAT6B.js";
|
|
6
|
+
|
|
7
|
+
// src/templates/hero-carousel-client.tsx
|
|
8
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
|
|
9
|
+
import useEmblaCarousel from "embla-carousel-react";
|
|
10
|
+
|
|
11
|
+
// src/templates/hero-carousel-curtain.tsx
|
|
12
|
+
import { useCallback, useRef, useState } from "react";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
var LEAVING_CLEAR_MS = 980;
|
|
15
|
+
function useCurtainCarousel(slideCount, reducedMotion) {
|
|
16
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
17
|
+
const [leavingIndex, setLeavingIndex] = useState(null);
|
|
18
|
+
const selectedRef = useRef(0);
|
|
19
|
+
const runRef = useRef(0);
|
|
20
|
+
const clearTimerRef = useRef(void 0);
|
|
21
|
+
const goTo = useCallback(
|
|
22
|
+
(nextRaw) => {
|
|
23
|
+
if (slideCount < 1) return;
|
|
24
|
+
const nextIndex = (nextRaw % slideCount + slideCount) % slideCount;
|
|
25
|
+
const prevIndex = selectedRef.current;
|
|
26
|
+
if (nextIndex === prevIndex) return;
|
|
27
|
+
selectedRef.current = nextIndex;
|
|
28
|
+
setSelectedIndex(nextIndex);
|
|
29
|
+
clearTimeout(clearTimerRef.current);
|
|
30
|
+
if (reducedMotion) {
|
|
31
|
+
setLeavingIndex(null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
setLeavingIndex(prevIndex);
|
|
35
|
+
runRef.current += 1;
|
|
36
|
+
const runId = runRef.current;
|
|
37
|
+
clearTimerRef.current = setTimeout(() => {
|
|
38
|
+
if (runId === runRef.current) setLeavingIndex(null);
|
|
39
|
+
}, LEAVING_CLEAR_MS);
|
|
40
|
+
},
|
|
41
|
+
[slideCount, reducedMotion]
|
|
42
|
+
);
|
|
43
|
+
const next = useCallback(() => goTo(selectedRef.current + 1), [goTo]);
|
|
44
|
+
const prev = useCallback(() => goTo(selectedRef.current - 1), [goTo]);
|
|
45
|
+
return { selectedIndex, leavingIndex, goTo, next, prev };
|
|
46
|
+
}
|
|
47
|
+
function CurtainTrack({
|
|
48
|
+
slides,
|
|
49
|
+
selectedIndex,
|
|
50
|
+
leavingIndex
|
|
51
|
+
}) {
|
|
52
|
+
return /* @__PURE__ */ jsx("div", { className: "rt-tpl-hero-carousel-viewport", children: /* @__PURE__ */ jsx("div", { className: "rt-tpl-hero-carousel-track", children: slides.map((slide, index) => {
|
|
53
|
+
const active = index === selectedIndex;
|
|
54
|
+
const leaving = index === leavingIndex;
|
|
55
|
+
return /* @__PURE__ */ jsxs(
|
|
56
|
+
"div",
|
|
57
|
+
{
|
|
58
|
+
className: "rt-tpl-hero-carousel-slide",
|
|
59
|
+
"aria-roledescription": "slide",
|
|
60
|
+
"aria-label": `Slide ${index + 1} of ${slides.length}`,
|
|
61
|
+
"aria-hidden": active ? void 0 : true,
|
|
62
|
+
"data-active": active ? "true" : "false",
|
|
63
|
+
"data-leaving": leaving ? "true" : "false",
|
|
64
|
+
children: [
|
|
65
|
+
/* @__PURE__ */ jsx(
|
|
66
|
+
"img",
|
|
67
|
+
{
|
|
68
|
+
className: "rt-tpl-hero-carousel-img",
|
|
69
|
+
src: slide.image,
|
|
70
|
+
alt: slide.alt,
|
|
71
|
+
"data-active": active ? "true" : "false",
|
|
72
|
+
loading: index === 0 ? "eager" : "lazy"
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
active ? /* @__PURE__ */ jsx("span", { className: "rt-tpl-hero-carousel-sheen", "aria-hidden": "true" }) : null
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
`${slide.image}-${index}`
|
|
79
|
+
);
|
|
80
|
+
}) }) });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/templates/hero-carousel-hooks.ts
|
|
84
|
+
import { useCallback as useCallback2, useState as useState2 } from "react";
|
|
85
|
+
import { useEffect } from "react";
|
|
86
|
+
function usePrefersReducedMotion() {
|
|
87
|
+
const [reduced, setReduced] = useState2(false);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
90
|
+
setReduced(query.matches);
|
|
91
|
+
const onChange = (event) => setReduced(event.matches);
|
|
92
|
+
query.addEventListener("change", onChange);
|
|
93
|
+
return () => query.removeEventListener("change", onChange);
|
|
94
|
+
}, []);
|
|
95
|
+
return reduced;
|
|
96
|
+
}
|
|
97
|
+
function useDocumentVisible() {
|
|
98
|
+
const [visible, setVisible] = useState2(true);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const onVisibility = () => setVisible(document.visibilityState === "visible");
|
|
101
|
+
onVisibility();
|
|
102
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
103
|
+
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
104
|
+
}, []);
|
|
105
|
+
return visible;
|
|
106
|
+
}
|
|
107
|
+
function useAutoplayGate(params) {
|
|
108
|
+
const { autoplay, reducedMotion, rootRef } = params;
|
|
109
|
+
const tabVisible = useDocumentVisible();
|
|
110
|
+
const [hovered, setHovered] = useState2(false);
|
|
111
|
+
const [focusedWithin, setFocusedWithin] = useState2(false);
|
|
112
|
+
const [userPaused, setUserPaused] = useState2(false);
|
|
113
|
+
const onBlur = useCallback2(
|
|
114
|
+
(event) => {
|
|
115
|
+
const next = event.relatedTarget;
|
|
116
|
+
if (!next || !rootRef.current?.contains(next)) {
|
|
117
|
+
setFocusedWithin(false);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[rootRef]
|
|
121
|
+
);
|
|
122
|
+
const shouldAutoplay = autoplay && !reducedMotion && tabVisible && !userPaused && !hovered && !focusedWithin;
|
|
123
|
+
return {
|
|
124
|
+
shouldAutoplay,
|
|
125
|
+
userPaused,
|
|
126
|
+
togglePaused: () => setUserPaused((value) => !value),
|
|
127
|
+
rootHandlers: {
|
|
128
|
+
onMouseEnter: () => setHovered(true),
|
|
129
|
+
onMouseLeave: () => setHovered(false),
|
|
130
|
+
onFocus: () => setFocusedWithin(true),
|
|
131
|
+
onBlur
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/templates/hero-carousel-slide-track.tsx
|
|
137
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
138
|
+
function SlideTrack({
|
|
139
|
+
viewportRef,
|
|
140
|
+
slides,
|
|
141
|
+
selectedIndex
|
|
142
|
+
}) {
|
|
143
|
+
return /* @__PURE__ */ jsx2("div", { className: "rt-tpl-hero-carousel-viewport", ref: viewportRef, children: /* @__PURE__ */ jsx2("div", { className: "rt-tpl-hero-carousel-track", children: slides.map((slide, index) => /* @__PURE__ */ jsx2(
|
|
144
|
+
"div",
|
|
145
|
+
{
|
|
146
|
+
className: "rt-tpl-hero-carousel-slide",
|
|
147
|
+
"aria-roledescription": "slide",
|
|
148
|
+
"aria-label": `Slide ${index + 1} of ${slides.length}`,
|
|
149
|
+
"aria-hidden": index === selectedIndex ? void 0 : true,
|
|
150
|
+
children: /* @__PURE__ */ jsx2(
|
|
151
|
+
"img",
|
|
152
|
+
{
|
|
153
|
+
className: "rt-tpl-hero-carousel-img",
|
|
154
|
+
src: slide.image,
|
|
155
|
+
alt: slide.alt,
|
|
156
|
+
"data-active": index === selectedIndex ? "true" : "false",
|
|
157
|
+
loading: index === 0 ? "eager" : "lazy"
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
`${slide.image}-${index}`
|
|
162
|
+
)) }) });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/templates/hero-carousel-client.tsx
|
|
166
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
167
|
+
function PauseIcon() {
|
|
168
|
+
return /* @__PURE__ */ jsxs2("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: [
|
|
169
|
+
/* @__PURE__ */ jsx3("rect", { x: "6", y: "5", width: "4", height: "14", rx: "1", fill: "currentColor" }),
|
|
170
|
+
/* @__PURE__ */ jsx3("rect", { x: "14", y: "5", width: "4", height: "14", rx: "1", fill: "currentColor" })
|
|
171
|
+
] });
|
|
172
|
+
}
|
|
173
|
+
function PlayIcon() {
|
|
174
|
+
return /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: "M8 5v14l11-7z", fill: "currentColor" }) });
|
|
175
|
+
}
|
|
176
|
+
function HeroCarouselViewport({
|
|
177
|
+
slides,
|
|
178
|
+
autoplay,
|
|
179
|
+
intervalMs,
|
|
180
|
+
scrim,
|
|
181
|
+
transition = "slide"
|
|
182
|
+
}) {
|
|
183
|
+
const isCurtain = transition === "curtain";
|
|
184
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
185
|
+
const rootRef = useRef2(null);
|
|
186
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
187
|
+
loop: true,
|
|
188
|
+
duration: reducedMotion ? 1 : 20
|
|
189
|
+
});
|
|
190
|
+
const [emblaSelectedIndex, setEmblaSelectedIndex] = useState3(0);
|
|
191
|
+
const curtain = useCurtainCarousel(slides.length, reducedMotion);
|
|
192
|
+
const onEmblaSelect = useCallback3(() => {
|
|
193
|
+
if (!emblaApi) return;
|
|
194
|
+
setEmblaSelectedIndex(emblaApi.selectedScrollSnap());
|
|
195
|
+
}, [emblaApi]);
|
|
196
|
+
useEffect2(() => {
|
|
197
|
+
if (!emblaApi) return;
|
|
198
|
+
onEmblaSelect();
|
|
199
|
+
emblaApi.on("select", onEmblaSelect);
|
|
200
|
+
emblaApi.on("reInit", onEmblaSelect);
|
|
201
|
+
return () => {
|
|
202
|
+
emblaApi.off("select", onEmblaSelect);
|
|
203
|
+
emblaApi.off("reInit", onEmblaSelect);
|
|
204
|
+
};
|
|
205
|
+
}, [emblaApi, onEmblaSelect]);
|
|
206
|
+
const selectedIndex = isCurtain ? curtain.selectedIndex : emblaSelectedIndex;
|
|
207
|
+
const autoplayGate = useAutoplayGate({ autoplay, reducedMotion, rootRef });
|
|
208
|
+
useEffect2(() => {
|
|
209
|
+
if (!autoplayGate.shouldAutoplay) return;
|
|
210
|
+
if (!isCurtain && !emblaApi) return;
|
|
211
|
+
const id = window.setInterval(() => {
|
|
212
|
+
if (isCurtain) curtain.next();
|
|
213
|
+
else emblaApi?.scrollNext();
|
|
214
|
+
}, intervalMs);
|
|
215
|
+
return () => window.clearInterval(id);
|
|
216
|
+
}, [autoplayGate.shouldAutoplay, intervalMs, isCurtain, curtain.next, emblaApi]);
|
|
217
|
+
const scrollPrev = useCallback3(() => {
|
|
218
|
+
if (isCurtain) {
|
|
219
|
+
curtain.prev();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
emblaApi?.scrollPrev(reducedMotion);
|
|
223
|
+
}, [isCurtain, curtain.prev, emblaApi, reducedMotion]);
|
|
224
|
+
const scrollNext = useCallback3(() => {
|
|
225
|
+
if (isCurtain) {
|
|
226
|
+
curtain.next();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
emblaApi?.scrollNext(reducedMotion);
|
|
230
|
+
}, [isCurtain, curtain.next, emblaApi, reducedMotion]);
|
|
231
|
+
const scrollTo = useCallback3(
|
|
232
|
+
(index) => {
|
|
233
|
+
if (isCurtain) {
|
|
234
|
+
curtain.goTo(index);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
emblaApi?.scrollTo(index, reducedMotion);
|
|
238
|
+
},
|
|
239
|
+
[isCurtain, curtain.goTo, emblaApi, reducedMotion]
|
|
240
|
+
);
|
|
241
|
+
const onKeyDown = useCallback3(
|
|
242
|
+
(event) => {
|
|
243
|
+
if (event.key === "ArrowLeft") {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
scrollPrev();
|
|
246
|
+
} else if (event.key === "ArrowRight") {
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
scrollNext();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
[scrollPrev, scrollNext]
|
|
252
|
+
);
|
|
253
|
+
const selectedSlide = slides[selectedIndex];
|
|
254
|
+
const hasSlidePanel = Boolean(
|
|
255
|
+
selectedSlide && (selectedSlide.eyebrow || selectedSlide.title || selectedSlide.subtitle || selectedSlide.description || selectedSlide.primaryCta || selectedSlide.secondaryCta || selectedSlide.metrics && selectedSlide.metrics.length > 0)
|
|
256
|
+
);
|
|
257
|
+
const hasTabLabels = slides.some((slide) => slide.tabLabel || slide.tabMeta);
|
|
258
|
+
const showPlayPause = autoplay && !reducedMotion;
|
|
259
|
+
const { getTabProps, onKeyDown: onTablistKeyDown } = useRovingTablist({
|
|
260
|
+
count: slides.length,
|
|
261
|
+
selectedIndex,
|
|
262
|
+
onSelect: scrollTo,
|
|
263
|
+
orientation: "horizontal"
|
|
264
|
+
});
|
|
265
|
+
return /* @__PURE__ */ jsxs2(
|
|
266
|
+
"div",
|
|
267
|
+
{
|
|
268
|
+
ref: rootRef,
|
|
269
|
+
className: "rt-tpl-hero-carousel",
|
|
270
|
+
role: "region",
|
|
271
|
+
"aria-roledescription": "carousel",
|
|
272
|
+
"aria-label": "\uBC30\uACBD \uC774\uBBF8\uC9C0 \uCE90\uB7EC\uC140",
|
|
273
|
+
tabIndex: 0,
|
|
274
|
+
onKeyDown,
|
|
275
|
+
"data-reduced-motion": reducedMotion ? "true" : "false",
|
|
276
|
+
"data-scrim": scrim ?? "soft",
|
|
277
|
+
"data-transition": transition,
|
|
278
|
+
onMouseEnter: autoplayGate.rootHandlers.onMouseEnter,
|
|
279
|
+
onMouseLeave: autoplayGate.rootHandlers.onMouseLeave,
|
|
280
|
+
onFocus: autoplayGate.rootHandlers.onFocus,
|
|
281
|
+
onBlur: autoplayGate.rootHandlers.onBlur,
|
|
282
|
+
children: [
|
|
283
|
+
isCurtain ? /* @__PURE__ */ jsx3(
|
|
284
|
+
CurtainTrack,
|
|
285
|
+
{
|
|
286
|
+
slides,
|
|
287
|
+
selectedIndex: curtain.selectedIndex,
|
|
288
|
+
leavingIndex: curtain.leavingIndex
|
|
289
|
+
}
|
|
290
|
+
) : /* @__PURE__ */ jsx3(
|
|
291
|
+
SlideTrack,
|
|
292
|
+
{
|
|
293
|
+
viewportRef: emblaRef,
|
|
294
|
+
slides,
|
|
295
|
+
selectedIndex: emblaSelectedIndex
|
|
296
|
+
}
|
|
297
|
+
),
|
|
298
|
+
/* @__PURE__ */ jsx3(
|
|
299
|
+
"button",
|
|
300
|
+
{
|
|
301
|
+
type: "button",
|
|
302
|
+
className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--prev",
|
|
303
|
+
"aria-label": "\uC774\uC804 \uC2AC\uB77C\uC774\uB4DC",
|
|
304
|
+
onClick: scrollPrev,
|
|
305
|
+
children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: "M15 5 8 12l7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
|
|
306
|
+
}
|
|
307
|
+
),
|
|
308
|
+
/* @__PURE__ */ jsx3(
|
|
309
|
+
"button",
|
|
310
|
+
{
|
|
311
|
+
type: "button",
|
|
312
|
+
className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--next",
|
|
313
|
+
"aria-label": "\uB2E4\uC74C \uC2AC\uB77C\uC774\uB4DC",
|
|
314
|
+
onClick: scrollNext,
|
|
315
|
+
children: /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx3("path", { d: "m9 5 7 7-7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
|
|
316
|
+
}
|
|
317
|
+
),
|
|
318
|
+
hasSlidePanel && selectedSlide ? /* @__PURE__ */ jsxs2(
|
|
319
|
+
"div",
|
|
320
|
+
{
|
|
321
|
+
className: "rt-tpl-hero-carousel-slide-panel",
|
|
322
|
+
children: [
|
|
323
|
+
selectedSlide.eyebrow ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-eyebrow", children: selectedSlide.eyebrow }) : null,
|
|
324
|
+
selectedSlide.title ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-title", children: selectedSlide.title }) : null,
|
|
325
|
+
selectedSlide.subtitle ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-subtitle", children: selectedSlide.subtitle }) : null,
|
|
326
|
+
selectedSlide.description ? /* @__PURE__ */ jsx3("p", { className: "rt-tpl-hero-carousel-slide-desc", children: selectedSlide.description }) : null,
|
|
327
|
+
selectedSlide.metrics && selectedSlide.metrics.length > 0 ? /* @__PURE__ */ jsx3("dl", { className: "rt-tpl-hero-carousel-slide-metrics", children: selectedSlide.metrics.map((metric) => /* @__PURE__ */ jsxs2("div", { children: [
|
|
328
|
+
/* @__PURE__ */ jsx3("dt", { children: metric.label }),
|
|
329
|
+
/* @__PURE__ */ jsxs2("dd", { children: [
|
|
330
|
+
metric.value,
|
|
331
|
+
metric.unit ? /* @__PURE__ */ jsx3("span", { children: metric.unit }) : null
|
|
332
|
+
] }),
|
|
333
|
+
metric.source ? /* @__PURE__ */ jsx3("dd", { children: metric.source }) : null
|
|
334
|
+
] }, `${metric.value}-${metric.label}`)) }) : null,
|
|
335
|
+
selectedSlide.primaryCta || selectedSlide.secondaryCta ? /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-slide-actions", children: [
|
|
336
|
+
selectedSlide.primaryCta ? /* @__PURE__ */ jsx3(
|
|
337
|
+
"a",
|
|
338
|
+
{
|
|
339
|
+
className: "rt-tpl-btn rt-tpl-btn--primary",
|
|
340
|
+
href: selectedSlide.primaryCta.href,
|
|
341
|
+
"data-track": "cta_hero",
|
|
342
|
+
children: selectedSlide.primaryCta.label
|
|
343
|
+
}
|
|
344
|
+
) : null,
|
|
345
|
+
selectedSlide.secondaryCta ? /* @__PURE__ */ jsx3(
|
|
346
|
+
"a",
|
|
347
|
+
{
|
|
348
|
+
className: "rt-tpl-btn rt-tpl-btn--secondary",
|
|
349
|
+
href: selectedSlide.secondaryCta.href,
|
|
350
|
+
"data-track": "cta_hero",
|
|
351
|
+
children: selectedSlide.secondaryCta.label
|
|
352
|
+
}
|
|
353
|
+
) : null
|
|
354
|
+
] }) : null
|
|
355
|
+
]
|
|
356
|
+
},
|
|
357
|
+
isCurtain ? `panel-${selectedIndex}` : "panel"
|
|
358
|
+
) : null,
|
|
359
|
+
/* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-controls", children: [
|
|
360
|
+
/* @__PURE__ */ jsx3(
|
|
361
|
+
"div",
|
|
362
|
+
{
|
|
363
|
+
className: hasTabLabels ? "rt-tpl-hero-carousel-dots rt-tpl-hero-carousel-dots--tabs" : "rt-tpl-hero-carousel-dots",
|
|
364
|
+
role: "tablist",
|
|
365
|
+
"aria-label": "\uC2AC\uB77C\uC774\uB4DC \uC120\uD0DD",
|
|
366
|
+
onKeyDown: onTablistKeyDown,
|
|
367
|
+
children: slides.map((_slide, index) => /* @__PURE__ */ jsx3(
|
|
368
|
+
"button",
|
|
369
|
+
{
|
|
370
|
+
...getTabProps(index),
|
|
371
|
+
type: "button",
|
|
372
|
+
role: "tab",
|
|
373
|
+
className: "rt-tpl-hero-carousel-dot",
|
|
374
|
+
"aria-selected": index === selectedIndex,
|
|
375
|
+
"aria-label": `${index + 1}\uBC88 \uC2AC\uB77C\uC774\uB4DC\uB85C \uC774\uB3D9`,
|
|
376
|
+
"data-active": index === selectedIndex ? "true" : "false",
|
|
377
|
+
onClick: () => scrollTo(index),
|
|
378
|
+
children: hasTabLabels ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
379
|
+
/* @__PURE__ */ jsx3("span", { children: _slide.tabLabel ?? `${index + 1}` }),
|
|
380
|
+
_slide.tabMeta ? /* @__PURE__ */ jsx3("small", { children: _slide.tabMeta }) : null
|
|
381
|
+
] }) : null
|
|
382
|
+
},
|
|
383
|
+
index
|
|
384
|
+
))
|
|
385
|
+
}
|
|
386
|
+
),
|
|
387
|
+
/* @__PURE__ */ jsxs2("div", { className: "rt-tpl-hero-carousel-controls-right", children: [
|
|
388
|
+
showPlayPause ? /* @__PURE__ */ jsx3(
|
|
389
|
+
"button",
|
|
390
|
+
{
|
|
391
|
+
type: "button",
|
|
392
|
+
className: "rt-tpl-hero-carousel-playpause",
|
|
393
|
+
"aria-pressed": autoplayGate.userPaused,
|
|
394
|
+
"aria-label": autoplayGate.userPaused ? "\uC790\uB3D9 \uC804\uD658 \uC7AC\uC0DD" : "\uC790\uB3D9 \uC804\uD658 \uC77C\uC2DC\uC815\uC9C0",
|
|
395
|
+
onClick: autoplayGate.togglePaused,
|
|
396
|
+
children: autoplayGate.userPaused ? /* @__PURE__ */ jsx3(PlayIcon, {}) : /* @__PURE__ */ jsx3(PauseIcon, {})
|
|
397
|
+
}
|
|
398
|
+
) : null,
|
|
399
|
+
/* @__PURE__ */ jsxs2("p", { className: "rt-tpl-hero-carousel-counter", "aria-hidden": "true", children: [
|
|
400
|
+
/* @__PURE__ */ jsx3("span", { className: "rt-tpl-hero-carousel-counter-current", children: selectedIndex + 1 }),
|
|
401
|
+
"/",
|
|
402
|
+
/* @__PURE__ */ jsx3("span", { className: "rt-tpl-hero-carousel-counter-total", children: slides.length })
|
|
403
|
+
] })
|
|
404
|
+
] })
|
|
405
|
+
] })
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export {
|
|
412
|
+
HeroCarouselViewport
|
|
413
|
+
};
|
|
414
|
+
//# sourceMappingURL=chunk-OMXEU4LS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/templates/hero-carousel-client.tsx","../src/templates/hero-carousel-curtain.tsx","../src/templates/hero-carousel-hooks.ts","../src/templates/hero-carousel-slide-track.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `hero` `carousel` variant — 배경 사진 전환 메커니즘만 담당하는 client\n * island (M6 서버 우선: 인터랙션 최소 단위 격리). 카피 오버레이(h1/subtitle/\n * eyebrow/CTA)는 `TemplateHero`(RSC, sections.tsx)가 서버에서 그린다 — 이\n * 컴포넌트는 배경 슬라이드 + 도트 + 화살표 + \"N/M\" 카운터만 렌더한다.\n *\n * 빌드 계약: 본 파일은 tsup 의 독립 entry(`hero-carousel-client`)로 번들되고\n * `esbuild-plugin-preserve-directives` 가 \"use client\" 지시어를 해당 청크\n * 최상단에 보존한다 — `templates.js`(서버 전용, 나머지 4개 hero variant +\n * rich-text/services/faq/cta-banner/trust/contact) 로 병합되지 않는다\n * (tsup.config.ts 참고).\n *\n * 모션 규율(M5 + ADR-0086 P2 D3/D4, ds-rule-pack.md):\n * - `transition`: \"slide\"(기본, embla 가로 스크롤) | \"curtain\"(clip-path 와이프\n * — chambarun 이식, `hero-carousel-curtain.tsx`). variant 는 그대로\n * \"carousel\" 하나 — \"variant=구조, motion=인상\" 규칙(신규 variant 금지).\n * - `prefers-reduced-motion: reduce` → autoplay 강제 off + 전환은 즉시(embla\n * `jump=true` 무전환 / curtain 은 leaving 단계 생략) + Ken Burns·신 스윕·카피\n * 스태거 없음.\n * - autoplay 정지 사유 4종(D4): hover, focus-within, pause/재생 버튼, Page\n * Visibility API(탭 비활성). 합성은 `hero-carousel-hooks.ts`\n * `useAutoplayGate`.\n * - 좌/우 화살표 키보드 이동(포커스가 캐러셀 안에 있을 때), 도트는\n * `role=\"tablist\"`/`role=\"tab\"` + roving tabindex(방향키/Home/End, D4) —\n * 공용 `useRovingTablist`(`expertise-client.tsx` 와 동일 계약 재사용).\n * - WAI-ARIA carousel 패턴: 컨테이너 `role=\"region\"` +\n * `aria-roledescription=\"carousel\"`, 슬라이드별 `aria-roledescription=\"slide\"`\n * + `aria-label=\"Slide N of M\"`.\n */\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactElement } from \"react\";\nimport useEmblaCarousel from \"embla-carousel-react\";\n\nimport { useCurtainCarousel, CurtainTrack } from \"./hero-carousel-curtain.js\";\nimport { useAutoplayGate, usePrefersReducedMotion } from \"./hero-carousel-hooks.js\";\nimport { SlideTrack } from \"./hero-carousel-slide-track.js\";\nimport type { HeroCarouselViewportProps } from \"./hero-carousel-types.js\";\nimport { useRovingTablist } from \"./use-roving-tablist.js\";\n\nexport type {\n HeroCarouselSlideInput,\n HeroCarouselTransition,\n HeroCarouselViewportProps,\n} from \"./hero-carousel-types.js\";\n\nfunction PauseIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" />\n </svg>\n );\n}\n\nfunction PlayIcon(): ReactElement {\n return (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M8 5v14l11-7z\" fill=\"currentColor\" />\n </svg>\n );\n}\n\nexport function HeroCarouselViewport({\n slides,\n autoplay,\n intervalMs,\n scrim,\n transition = \"slide\",\n}: HeroCarouselViewportProps) {\n const isCurtain = transition === \"curtain\";\n const reducedMotion = usePrefersReducedMotion();\n const rootRef = useRef<HTMLDivElement | null>(null);\n\n const [emblaRef, emblaApi] = useEmblaCarousel({\n loop: true,\n duration: reducedMotion ? 1 : 20,\n });\n const [emblaSelectedIndex, setEmblaSelectedIndex] = useState(0);\n const curtain = useCurtainCarousel(slides.length, reducedMotion);\n\n const onEmblaSelect = useCallback(() => {\n if (!emblaApi) return;\n setEmblaSelectedIndex(emblaApi.selectedScrollSnap());\n }, [emblaApi]);\n\n useEffect(() => {\n if (!emblaApi) return;\n onEmblaSelect();\n emblaApi.on(\"select\", onEmblaSelect);\n emblaApi.on(\"reInit\", onEmblaSelect);\n return () => {\n emblaApi.off(\"select\", onEmblaSelect);\n emblaApi.off(\"reInit\", onEmblaSelect);\n };\n }, [emblaApi, onEmblaSelect]);\n\n const selectedIndex = isCurtain ? curtain.selectedIndex : emblaSelectedIndex;\n\n const autoplayGate = useAutoplayGate({ autoplay, reducedMotion, rootRef });\n\n useEffect(() => {\n if (!autoplayGate.shouldAutoplay) return;\n if (!isCurtain && !emblaApi) return;\n const id = window.setInterval(() => {\n if (isCurtain) curtain.next();\n else emblaApi?.scrollNext();\n }, intervalMs);\n return () => window.clearInterval(id);\n }, [autoplayGate.shouldAutoplay, intervalMs, isCurtain, curtain.next, emblaApi]);\n\n const scrollPrev = useCallback(() => {\n if (isCurtain) {\n curtain.prev();\n return;\n }\n emblaApi?.scrollPrev(reducedMotion);\n }, [isCurtain, curtain.prev, emblaApi, reducedMotion]);\n\n const scrollNext = useCallback(() => {\n if (isCurtain) {\n curtain.next();\n return;\n }\n emblaApi?.scrollNext(reducedMotion);\n }, [isCurtain, curtain.next, emblaApi, reducedMotion]);\n\n const scrollTo = useCallback(\n (index: number) => {\n if (isCurtain) {\n curtain.goTo(index);\n return;\n }\n emblaApi?.scrollTo(index, reducedMotion);\n },\n [isCurtain, curtain.goTo, emblaApi, reducedMotion],\n );\n\n const onKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n scrollPrev();\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault();\n scrollNext();\n }\n },\n [scrollPrev, scrollNext],\n );\n\n const selectedSlide = slides[selectedIndex];\n const hasSlidePanel = Boolean(\n selectedSlide &&\n (selectedSlide.eyebrow ||\n selectedSlide.title ||\n selectedSlide.subtitle ||\n selectedSlide.description ||\n selectedSlide.primaryCta ||\n selectedSlide.secondaryCta ||\n (selectedSlide.metrics && selectedSlide.metrics.length > 0)),\n );\n const hasTabLabels = slides.some((slide) => slide.tabLabel || slide.tabMeta);\n const showPlayPause = autoplay && !reducedMotion;\n\n const { getTabProps, onKeyDown: onTablistKeyDown } = useRovingTablist<HTMLButtonElement>({\n count: slides.length,\n selectedIndex,\n onSelect: scrollTo,\n orientation: \"horizontal\",\n });\n\n return (\n <div\n ref={rootRef}\n className=\"rt-tpl-hero-carousel\"\n role=\"region\"\n aria-roledescription=\"carousel\"\n aria-label=\"배경 이미지 캐러셀\"\n tabIndex={0}\n onKeyDown={onKeyDown}\n data-reduced-motion={reducedMotion ? \"true\" : \"false\"}\n data-scrim={scrim ?? \"soft\"}\n data-transition={transition}\n onMouseEnter={autoplayGate.rootHandlers.onMouseEnter}\n onMouseLeave={autoplayGate.rootHandlers.onMouseLeave}\n onFocus={autoplayGate.rootHandlers.onFocus}\n onBlur={autoplayGate.rootHandlers.onBlur}\n >\n {isCurtain ? (\n <CurtainTrack\n slides={slides}\n selectedIndex={curtain.selectedIndex}\n leavingIndex={curtain.leavingIndex}\n />\n ) : (\n <SlideTrack\n viewportRef={emblaRef}\n slides={slides}\n selectedIndex={emblaSelectedIndex}\n />\n )}\n\n <button\n type=\"button\"\n className=\"rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--prev\"\n aria-label=\"이전 슬라이드\"\n onClick={scrollPrev}\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M15 5 8 12l7 7\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n <button\n type=\"button\"\n className=\"rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--next\"\n aria-label=\"다음 슬라이드\"\n onClick={scrollNext}\n >\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"m9 5 7 7-7 7\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </button>\n\n {hasSlidePanel && selectedSlide ? (\n <div\n key={isCurtain ? `panel-${selectedIndex}` : \"panel\"}\n className=\"rt-tpl-hero-carousel-slide-panel\"\n >\n {selectedSlide.eyebrow ? (\n <p className=\"rt-tpl-hero-carousel-slide-eyebrow\">\n {selectedSlide.eyebrow}\n </p>\n ) : null}\n {selectedSlide.title ? (\n <p className=\"rt-tpl-hero-carousel-slide-title\">\n {selectedSlide.title}\n </p>\n ) : null}\n {selectedSlide.subtitle ? (\n <p className=\"rt-tpl-hero-carousel-slide-subtitle\">\n {selectedSlide.subtitle}\n </p>\n ) : null}\n {selectedSlide.description ? (\n <p className=\"rt-tpl-hero-carousel-slide-desc\">\n {selectedSlide.description}\n </p>\n ) : null}\n {selectedSlide.metrics && selectedSlide.metrics.length > 0 ? (\n <dl className=\"rt-tpl-hero-carousel-slide-metrics\">\n {selectedSlide.metrics.map((metric) => (\n <div key={`${metric.value}-${metric.label}`}>\n <dt>{metric.label}</dt>\n <dd>\n {metric.value}\n {metric.unit ? <span>{metric.unit}</span> : null}\n </dd>\n {metric.source ? <dd>{metric.source}</dd> : null}\n </div>\n ))}\n </dl>\n ) : null}\n {selectedSlide.primaryCta || selectedSlide.secondaryCta ? (\n <div className=\"rt-tpl-hero-carousel-slide-actions\">\n {selectedSlide.primaryCta ? (\n <a\n className=\"rt-tpl-btn rt-tpl-btn--primary\"\n href={selectedSlide.primaryCta.href}\n data-track=\"cta_hero\"\n >\n {selectedSlide.primaryCta.label}\n </a>\n ) : null}\n {selectedSlide.secondaryCta ? (\n <a\n className=\"rt-tpl-btn rt-tpl-btn--secondary\"\n href={selectedSlide.secondaryCta.href}\n data-track=\"cta_hero\"\n >\n {selectedSlide.secondaryCta.label}\n </a>\n ) : null}\n </div>\n ) : null}\n </div>\n ) : null}\n\n <div className=\"rt-tpl-hero-carousel-controls\">\n <div\n className={\n hasTabLabels\n ? \"rt-tpl-hero-carousel-dots rt-tpl-hero-carousel-dots--tabs\"\n : \"rt-tpl-hero-carousel-dots\"\n }\n role=\"tablist\"\n aria-label=\"슬라이드 선택\"\n onKeyDown={onTablistKeyDown}\n >\n {slides.map((_slide, index) => (\n <button\n key={index}\n {...getTabProps(index)}\n type=\"button\"\n role=\"tab\"\n className=\"rt-tpl-hero-carousel-dot\"\n aria-selected={index === selectedIndex}\n aria-label={`${index + 1}번 슬라이드로 이동`}\n data-active={index === selectedIndex ? \"true\" : \"false\"}\n onClick={() => scrollTo(index)}\n >\n {hasTabLabels ? (\n <>\n <span>{_slide.tabLabel ?? `${index + 1}`}</span>\n {_slide.tabMeta ? <small>{_slide.tabMeta}</small> : null}\n </>\n ) : null}\n </button>\n ))}\n </div>\n <div className=\"rt-tpl-hero-carousel-controls-right\">\n {showPlayPause ? (\n <button\n type=\"button\"\n className=\"rt-tpl-hero-carousel-playpause\"\n aria-pressed={autoplayGate.userPaused}\n aria-label={\n autoplayGate.userPaused ? \"자동 전환 재생\" : \"자동 전환 일시정지\"\n }\n onClick={autoplayGate.togglePaused}\n >\n {autoplayGate.userPaused ? <PlayIcon /> : <PauseIcon />}\n </button>\n ) : null}\n <p className=\"rt-tpl-hero-carousel-counter\" aria-hidden=\"true\">\n <span className=\"rt-tpl-hero-carousel-counter-current\">\n {selectedIndex + 1}\n </span>\n /\n <span className=\"rt-tpl-hero-carousel-counter-total\">{slides.length}</span>\n </p>\n </div>\n </div>\n </div>\n );\n}\n","\"use client\";\n\n/**\n * `curtain` transition — chambarun 이식(ADR-0086 P2 D3).\n * 모션 스펙 원본: chambarun mockup `assets/site.css` `@keyframes curtainReveal`\n * (clip-path 와이프) + `.curtain-sheen`(신 스윕) + `.hero-copy>*` 스태거, 엔진은\n * `assets/site.js` 36-85행 `show(n)`(is-active/is-leaving 상태 머신, 980ms 뒤\n * is-leaving 정리). 타이밍·이징·클립 경로는 원본 값을 그대로 유지하고 색은\n * 하드코딩 헥스 대신 `--rt-*` 토큰으로 치환했다(CSS, `templates.css` 참고).\n *\n * embla 를 쓰지 않는다 — embla 는 가로 스크롤 트랙 물리 이동이 전제라\n * clip-path 와이프(절대배치 스택)와 구조가 다르다. `hero-carousel-client.tsx`\n * 400줄 상한 분할.\n */\nimport { useCallback, useRef, useState } from \"react\";\nimport type { ReactElement } from \"react\";\n\nimport type { HeroCarouselSlideInput } from \"./hero-carousel-types.js\";\n\n/** chambarun site.js — is-leaving 클래스 정리 타이밍(커튼 리빌 .92s + 여유). */\nconst LEAVING_CLEAR_MS = 980;\n\nexport interface CurtainCarouselController {\n readonly selectedIndex: number;\n readonly leavingIndex: number | null;\n readonly goTo: (index: number) => void;\n readonly next: () => void;\n readonly prev: () => void;\n}\n\n/**\n * `curtain` 전용 인덱스 상태 머신 — chambarun `show(n)` 이식. `reducedMotion`\n * 이면 leaving 단계를 만들지 않는다(D4 — 커튼 대신 즉시 전환).\n */\nexport function useCurtainCarousel(\n slideCount: number,\n reducedMotion: boolean,\n): CurtainCarouselController {\n const [selectedIndex, setSelectedIndex] = useState(0);\n const [leavingIndex, setLeavingIndex] = useState<number | null>(null);\n const selectedRef = useRef(0);\n const runRef = useRef(0);\n const clearTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const goTo = useCallback(\n (nextRaw: number) => {\n if (slideCount < 1) return;\n const nextIndex = ((nextRaw % slideCount) + slideCount) % slideCount;\n const prevIndex = selectedRef.current;\n if (nextIndex === prevIndex) return;\n selectedRef.current = nextIndex;\n setSelectedIndex(nextIndex);\n clearTimeout(clearTimerRef.current);\n if (reducedMotion) {\n setLeavingIndex(null);\n return;\n }\n setLeavingIndex(prevIndex);\n runRef.current += 1;\n const runId = runRef.current;\n clearTimerRef.current = setTimeout(() => {\n if (runId === runRef.current) setLeavingIndex(null);\n }, LEAVING_CLEAR_MS);\n },\n [slideCount, reducedMotion],\n );\n\n const next = useCallback(() => goTo(selectedRef.current + 1), [goTo]);\n const prev = useCallback(() => goTo(selectedRef.current - 1), [goTo]);\n\n return { selectedIndex, leavingIndex, goTo, next, prev };\n}\n\nexport interface CurtainTrackProps {\n readonly slides: readonly HeroCarouselSlideInput[];\n readonly selectedIndex: number;\n readonly leavingIndex: number | null;\n}\n\n/**\n * 절대배치 스택 트랙(chambarun `.slide{position:absolute;inset:0}`) — 각\n * 슬라이드가 `data-active`/`data-leaving` 로 clip-path 와이프·오버랩을\n * 제어한다(CSS `templates.css` `[data-transition=\"curtain\"]` 스코프). 활성\n * 슬라이드에만 신 스윕 엘리먼트를 렌더(비활성 슬라이드는 DOM 비용 절감).\n */\nexport function CurtainTrack({\n slides,\n selectedIndex,\n leavingIndex,\n}: CurtainTrackProps): ReactElement {\n return (\n <div className=\"rt-tpl-hero-carousel-viewport\">\n <div className=\"rt-tpl-hero-carousel-track\">\n {slides.map((slide, index) => {\n const active = index === selectedIndex;\n const leaving = index === leavingIndex;\n return (\n <div\n key={`${slide.image}-${index}`}\n className=\"rt-tpl-hero-carousel-slide\"\n aria-roledescription=\"slide\"\n aria-label={`Slide ${index + 1} of ${slides.length}`}\n aria-hidden={active ? undefined : true}\n data-active={active ? \"true\" : \"false\"}\n data-leaving={leaving ? \"true\" : \"false\"}\n >\n <img\n className=\"rt-tpl-hero-carousel-img\"\n src={slide.image}\n alt={slide.alt}\n data-active={active ? \"true\" : \"false\"}\n loading={index === 0 ? \"eager\" : \"lazy\"}\n />\n {active ? (\n <span className=\"rt-tpl-hero-carousel-sheen\" aria-hidden=\"true\" />\n ) : null}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","\"use client\";\n\n/**\n * `hero` `carousel` 공유 훅 — reduced-motion·document visibility·autoplay\n * 정지 사유 합성(D4 접근성 계약, ADR-0086 P2). `hero-carousel-client.tsx` 400줄\n * 상한 분할.\n */\nimport { useCallback, useState } from \"react\";\nimport type { FocusEvent, RefObject } from \"react\";\nimport { useEffect } from \"react\";\n\nexport function usePrefersReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false);\n useEffect(() => {\n const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n setReduced(query.matches);\n const onChange = (event: MediaQueryListEvent) => setReduced(event.matches);\n query.addEventListener(\"change\", onChange);\n return () => query.removeEventListener(\"change\", onChange);\n }, []);\n return reduced;\n}\n\nexport function useDocumentVisible(): boolean {\n const [visible, setVisible] = useState(true);\n useEffect(() => {\n const onVisibility = () => setVisible(document.visibilityState === \"visible\");\n onVisibility();\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => document.removeEventListener(\"visibilitychange\", onVisibility);\n }, []);\n return visible;\n}\n\nexport interface AutoplayGate {\n /** 실제 autoplay 인터벌을 걸어도 되는지 — 모든 정지 사유의 합성 결과. */\n readonly shouldAutoplay: boolean;\n /** pause/재생 버튼이 반영하는 사용자 의도(수동 일시정지). */\n readonly userPaused: boolean;\n readonly togglePaused: () => void;\n /** 루트 엘리먼트에 그대로 스프레드 — hover/focus-within 일시정지. */\n readonly rootHandlers: {\n readonly onMouseEnter: () => void;\n readonly onMouseLeave: () => void;\n readonly onFocus: () => void;\n readonly onBlur: (event: FocusEvent<HTMLElement>) => void;\n };\n}\n\n/**\n * autoplay pause 사유 4종(D4): hover, focus-within, 사용자 버튼, 문서 비가시.\n * `reducedMotion` 은 autoplay 자체를 아예 시작하지 않는 별도 계약이라 이 훅\n * 밖(`autoplay && !reducedMotion`)에서 이미 걸러지지만, 재확인 차원에서 인자로\n * 받아 합성식에도 포함한다.\n */\nexport function useAutoplayGate(params: {\n readonly autoplay: boolean;\n readonly reducedMotion: boolean;\n readonly rootRef: RefObject<HTMLElement | null>;\n}): AutoplayGate {\n const { autoplay, reducedMotion, rootRef } = params;\n const tabVisible = useDocumentVisible();\n const [hovered, setHovered] = useState(false);\n const [focusedWithin, setFocusedWithin] = useState(false);\n const [userPaused, setUserPaused] = useState(false);\n\n const onBlur = useCallback(\n (event: FocusEvent<HTMLElement>) => {\n const next = event.relatedTarget as Node | null;\n if (!next || !rootRef.current?.contains(next)) {\n setFocusedWithin(false);\n }\n },\n [rootRef],\n );\n\n const shouldAutoplay =\n autoplay &&\n !reducedMotion &&\n tabVisible &&\n !userPaused &&\n !hovered &&\n !focusedWithin;\n\n return {\n shouldAutoplay,\n userPaused,\n togglePaused: () => setUserPaused((value) => !value),\n rootHandlers: {\n onMouseEnter: () => setHovered(true),\n onMouseLeave: () => setHovered(false),\n onFocus: () => setFocusedWithin(true),\n onBlur,\n },\n };\n}\n","\"use client\";\n\n/**\n * `slide` transition(기본값) 트랙 — 기존 embla 가로 스크롤 배경 전환, 회귀 없음.\n * `hero-carousel-client.tsx` 400줄 상한 분할(`curtain` transition 은\n * `hero-carousel-curtain.tsx`).\n */\nimport type { ReactElement } from \"react\";\nimport type { EmblaViewportRefType } from \"embla-carousel-react\";\n\nimport type { HeroCarouselSlideInput } from \"./hero-carousel-types.js\";\n\nexport interface SlideTrackProps {\n readonly viewportRef: EmblaViewportRefType;\n readonly slides: readonly HeroCarouselSlideInput[];\n readonly selectedIndex: number;\n}\n\nexport function SlideTrack({\n viewportRef,\n slides,\n selectedIndex,\n}: SlideTrackProps): ReactElement {\n return (\n <div className=\"rt-tpl-hero-carousel-viewport\" ref={viewportRef}>\n <div className=\"rt-tpl-hero-carousel-track\">\n {slides.map((slide, index) => (\n <div\n key={`${slide.image}-${index}`}\n className=\"rt-tpl-hero-carousel-slide\"\n aria-roledescription=\"slide\"\n aria-label={`Slide ${index + 1} of ${slides.length}`}\n aria-hidden={index === selectedIndex ? undefined : true}\n >\n <img\n className=\"rt-tpl-hero-carousel-img\"\n src={slide.image}\n alt={slide.alt}\n data-active={index === selectedIndex ? \"true\" : \"false\"}\n loading={index === 0 ? \"eager\" : \"lazy\"}\n />\n </div>\n ))}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","useCallback","useState","jsx","jsx","jsxs","useRef","useState","useCallback","useEffect"]}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// src/templates/site-config-version.ts
|
|
2
|
+
var SITE_CONFIG_SCHEMA_VERSION = 1;
|
|
3
|
+
var LEGACY_SITE_CONFIG_SCHEMA_VERSION = 1;
|
|
4
|
+
function resolveSiteConfigSchemaVersion(config) {
|
|
5
|
+
return config.schemaVersion ?? LEGACY_SITE_CONFIG_SCHEMA_VERSION;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
// src/templates/site-config.ts
|
|
2
9
|
import { z } from "zod";
|
|
3
10
|
var JsonValue = z.lazy(
|
|
@@ -105,6 +112,13 @@ var HeroSectionSchema = SectionBase.extend({
|
|
|
105
112
|
autoplay: z.boolean().optional(),
|
|
106
113
|
/** `carousel` 전용, ms. 기본 6000. */
|
|
107
114
|
intervalMs: z.number().min(3e3).max(1e4).optional(),
|
|
115
|
+
/**
|
|
116
|
+
* `carousel` 전용 배경 전환 모션(ADR-0086 P2 D3). 기본 "slide"(기존 embla
|
|
117
|
+
* 가로 스크롤). "curtain" = clip-path 와이프 + 신 스윕(chambarun 이식).
|
|
118
|
+
* variant 는 그대로 "carousel" — 신규 variant 아님("variant=구조,
|
|
119
|
+
* motion=인상" 규칙).
|
|
120
|
+
*/
|
|
121
|
+
transition: z.enum(["slide", "curtain"]).optional(),
|
|
108
122
|
proofMetrics: HeroProofMetricsSchema.optional(),
|
|
109
123
|
scrim: z.enum(["none", "soft", "strong"]).optional()
|
|
110
124
|
})
|
|
@@ -487,7 +501,15 @@ var ContactMapSchema = z.object({
|
|
|
487
501
|
embedUrl: z.string().optional(),
|
|
488
502
|
externalUrl: z.string().optional(),
|
|
489
503
|
lat: z.number().optional(),
|
|
490
|
-
lng: z.number().optional()
|
|
504
|
+
lng: z.number().optional(),
|
|
505
|
+
/**
|
|
506
|
+
* 지도 로드 모드(ADR-0086 P2 D3, chambarun `.map-facade` 클릭-투-로드 이식).
|
|
507
|
+
* 기본 "lazy"(기존 동작 그대로 — 실지도면 즉시 iframe `loading="lazy"`).
|
|
508
|
+
* "click" 은 정적 프리뷰(주소·지도 열기 버튼)만 먼저 그리고, 클릭 시에만
|
|
509
|
+
* iframe 을 마운트한다(외부 지도 스크립트/네트워크 요청을 사용자 의도
|
|
510
|
+
* 전까지 미루는 성능·프라이버시 절충 — additive, 기존 사이트는 영향 없음).
|
|
511
|
+
*/
|
|
512
|
+
mapLoad: z.enum(["lazy", "click"]).optional()
|
|
491
513
|
});
|
|
492
514
|
var ContactActionSchema = z.object({
|
|
493
515
|
label: z.string(),
|
|
@@ -859,6 +881,13 @@ var CollectionSchema = z.object({
|
|
|
859
881
|
icon: z.string().optional()
|
|
860
882
|
});
|
|
861
883
|
var SiteConfigSchema = z.object({
|
|
884
|
+
/**
|
|
885
|
+
* `SiteConfig` 계약 버전 (ADR-0086 D2) — 팩 manifest 의 `siteConfigVersion`
|
|
886
|
+
* 과 비교해 팩↔renderer 호환성을 표현한다. optional — 하위 호환(기존 고객
|
|
887
|
+
* `site.config.ts` 에는 이 필드가 없다). 미지정은 버전 1 로 해석한다
|
|
888
|
+
* (`resolveSiteConfigSchemaVersion`).
|
|
889
|
+
*/
|
|
890
|
+
schemaVersion: z.number().int().positive().optional(),
|
|
862
891
|
site: z.object({
|
|
863
892
|
/** canonical origin. 생략 시 소비측이 `SITE_URL` env 등으로 해석. */
|
|
864
893
|
url: z.string().url().optional(),
|
|
@@ -875,6 +904,17 @@ var SiteConfigSchema = z.object({
|
|
|
875
904
|
collections: z.array(CollectionSchema).default([]),
|
|
876
905
|
/** 사이트 전역 토큰 override (`--rt-*` CSS 변수 → 값). 비우면 admin /design SSoT. */
|
|
877
906
|
theme: z.record(z.string()).default({}),
|
|
907
|
+
/**
|
|
908
|
+
* 테마 방향(Theme Directions, `@roottale/tokens` 프리셋 id — 예: "d1-editorial").
|
|
909
|
+
* root layout 이 `<html data-rt-direction="…">` 로 부여해
|
|
910
|
+
* `@roottale/tokens/theme-directions.css` 의 `[data-rt-direction="…"]` 규칙을
|
|
911
|
+
* 활성화한다(roottale-docs `docs/design/theme-directions.md`). 값은 문자열로만
|
|
912
|
+
* 검증한다 — `@roottale/cms-renderer-next` 는 `@roottale/tokens` 를 직접
|
|
913
|
+
* 의존하지 않으므로 유효 id 집합은 `@roottale/tokens/theme-directions` 의
|
|
914
|
+
* `ThemeDirectionId` 를 참조해 `site.config.ts` 작성 시점에 타입으로 좁힌다.
|
|
915
|
+
* 미지정 시 속성 자체가 안 붙어 완전히 무변화(기존 동작).
|
|
916
|
+
*/
|
|
917
|
+
direction: z.string().optional(),
|
|
878
918
|
nav: z.array(NavItemSchema).default([]),
|
|
879
919
|
/** path("/", "/services", …) → 섹션 배열. */
|
|
880
920
|
pages: z.record(z.array(SectionSchema))
|
|
@@ -887,6 +927,8 @@ function defineSiteConfig(input) {
|
|
|
887
927
|
}
|
|
888
928
|
|
|
889
929
|
export {
|
|
930
|
+
SITE_CONFIG_SCHEMA_VERSION,
|
|
931
|
+
resolveSiteConfigSchemaVersion,
|
|
890
932
|
NavItemSchema,
|
|
891
933
|
SECTION_VARIANTS,
|
|
892
934
|
HeroSectionSchema,
|
|
@@ -916,4 +958,4 @@ export {
|
|
|
916
958
|
parseSiteConfig,
|
|
917
959
|
defineSiteConfig
|
|
918
960
|
};
|
|
919
|
-
//# sourceMappingURL=chunk-
|
|
961
|
+
//# sourceMappingURL=chunk-UM37DOIK.js.map
|