@roottale/cms-renderer-next 0.41.0 → 0.41.2

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.
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ // src/templates/hero-carousel-client.tsx
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import useEmblaCarousel from "embla-carousel-react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ function usePrefersReducedMotion() {
8
+ const [reduced, setReduced] = useState(false);
9
+ useEffect(() => {
10
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
11
+ setReduced(query.matches);
12
+ const onChange = (event) => setReduced(event.matches);
13
+ query.addEventListener("change", onChange);
14
+ return () => query.removeEventListener("change", onChange);
15
+ }, []);
16
+ return reduced;
17
+ }
18
+ function useDocumentVisible() {
19
+ const [visible, setVisible] = useState(true);
20
+ useEffect(() => {
21
+ const onVisibility = () => setVisible(document.visibilityState === "visible");
22
+ onVisibility();
23
+ document.addEventListener("visibilitychange", onVisibility);
24
+ return () => document.removeEventListener("visibilitychange", onVisibility);
25
+ }, []);
26
+ return visible;
27
+ }
28
+ function HeroCarouselViewport({
29
+ slides,
30
+ autoplay,
31
+ intervalMs
32
+ }) {
33
+ const reducedMotion = usePrefersReducedMotion();
34
+ const tabVisible = useDocumentVisible();
35
+ const [emblaRef, emblaApi] = useEmblaCarousel({
36
+ loop: true,
37
+ duration: reducedMotion ? 1 : 20
38
+ });
39
+ const [selectedIndex, setSelectedIndex] = useState(0);
40
+ const rootRef = useRef(null);
41
+ const onSelect = useCallback(() => {
42
+ if (!emblaApi) return;
43
+ setSelectedIndex(emblaApi.selectedScrollSnap());
44
+ }, [emblaApi]);
45
+ useEffect(() => {
46
+ if (!emblaApi) return;
47
+ onSelect();
48
+ emblaApi.on("select", onSelect);
49
+ emblaApi.on("reInit", onSelect);
50
+ return () => {
51
+ emblaApi.off("select", onSelect);
52
+ emblaApi.off("reInit", onSelect);
53
+ };
54
+ }, [emblaApi, onSelect]);
55
+ const shouldAutoplay = autoplay && !reducedMotion && tabVisible;
56
+ useEffect(() => {
57
+ if (!emblaApi || !shouldAutoplay) return;
58
+ const id = window.setInterval(() => {
59
+ emblaApi.scrollNext();
60
+ }, intervalMs);
61
+ return () => window.clearInterval(id);
62
+ }, [emblaApi, shouldAutoplay, intervalMs]);
63
+ const scrollPrev = useCallback(() => {
64
+ emblaApi?.scrollPrev(reducedMotion);
65
+ }, [emblaApi, reducedMotion]);
66
+ const scrollNext = useCallback(() => {
67
+ emblaApi?.scrollNext(reducedMotion);
68
+ }, [emblaApi, reducedMotion]);
69
+ const scrollTo = useCallback(
70
+ (index) => {
71
+ emblaApi?.scrollTo(index, reducedMotion);
72
+ },
73
+ [emblaApi, reducedMotion]
74
+ );
75
+ const onKeyDown = useCallback(
76
+ (event) => {
77
+ if (event.key === "ArrowLeft") {
78
+ event.preventDefault();
79
+ scrollPrev();
80
+ } else if (event.key === "ArrowRight") {
81
+ event.preventDefault();
82
+ scrollNext();
83
+ }
84
+ },
85
+ [scrollPrev, scrollNext]
86
+ );
87
+ return /* @__PURE__ */ jsxs(
88
+ "div",
89
+ {
90
+ ref: rootRef,
91
+ className: "rt-tpl-hero-carousel",
92
+ role: "region",
93
+ "aria-roledescription": "carousel",
94
+ "aria-label": "\uBC30\uACBD \uC774\uBBF8\uC9C0 \uCE90\uB7EC\uC140",
95
+ tabIndex: 0,
96
+ onKeyDown,
97
+ "data-reduced-motion": reducedMotion ? "true" : "false",
98
+ children: [
99
+ /* @__PURE__ */ jsx("div", { className: "rt-tpl-hero-carousel-viewport", ref: emblaRef, children: /* @__PURE__ */ jsx("div", { className: "rt-tpl-hero-carousel-track", children: slides.map((slide, index) => /* @__PURE__ */ jsx(
100
+ "div",
101
+ {
102
+ className: "rt-tpl-hero-carousel-slide",
103
+ "aria-roledescription": "slide",
104
+ "aria-label": `Slide ${index + 1} of ${slides.length}`,
105
+ "aria-hidden": index === selectedIndex ? void 0 : true,
106
+ children: /* @__PURE__ */ jsx(
107
+ "img",
108
+ {
109
+ className: "rt-tpl-hero-carousel-img",
110
+ src: slide.image,
111
+ alt: slide.alt,
112
+ "data-active": index === selectedIndex ? "true" : "false",
113
+ loading: index === 0 ? "eager" : "lazy"
114
+ }
115
+ )
116
+ },
117
+ `${slide.image}-${index}`
118
+ )) }) }),
119
+ /* @__PURE__ */ jsx(
120
+ "button",
121
+ {
122
+ type: "button",
123
+ className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--prev",
124
+ "aria-label": "\uC774\uC804 \uC2AC\uB77C\uC774\uB4DC",
125
+ onClick: scrollPrev,
126
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M15 5 8 12l7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsx(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--next",
134
+ "aria-label": "\uB2E4\uC74C \uC2AC\uB77C\uC774\uB4DC",
135
+ onClick: scrollNext,
136
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "m9 5 7 7-7 7", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
137
+ }
138
+ ),
139
+ /* @__PURE__ */ jsxs("div", { className: "rt-tpl-hero-carousel-controls", children: [
140
+ /* @__PURE__ */ jsx("div", { className: "rt-tpl-hero-carousel-dots", role: "tablist", "aria-label": "\uC2AC\uB77C\uC774\uB4DC \uC120\uD0DD", children: slides.map((_slide, index) => /* @__PURE__ */ jsx(
141
+ "button",
142
+ {
143
+ type: "button",
144
+ role: "tab",
145
+ className: "rt-tpl-hero-carousel-dot",
146
+ "aria-selected": index === selectedIndex,
147
+ "aria-label": `${index + 1}\uBC88 \uC2AC\uB77C\uC774\uB4DC\uB85C \uC774\uB3D9`,
148
+ "data-active": index === selectedIndex ? "true" : "false",
149
+ onClick: () => scrollTo(index)
150
+ },
151
+ index
152
+ )) }),
153
+ /* @__PURE__ */ jsxs("p", { className: "rt-tpl-hero-carousel-counter", "aria-hidden": "true", children: [
154
+ /* @__PURE__ */ jsx("span", { className: "rt-tpl-hero-carousel-counter-current", children: selectedIndex + 1 }),
155
+ "/",
156
+ /* @__PURE__ */ jsx("span", { className: "rt-tpl-hero-carousel-counter-total", children: slides.length })
157
+ ] })
158
+ ] })
159
+ ]
160
+ }
161
+ );
162
+ }
163
+
164
+ export {
165
+ HeroCarouselViewport
166
+ };
167
+ //# sourceMappingURL=chunk-YUT4BRJY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/templates/hero-carousel-client.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, ds-rule-pack.md):\n * - `prefers-reduced-motion: reduce` → autoplay 강제 off + 전환은 embla\n * `jump=true`(무전환) + Ken Burns 스케일 없음.\n * - Page Visibility API → 탭 비활성 시 autoplay 일시정지.\n * - 좌/우 화살표 키보드 이동(포커스가 캐러셀 안에 있을 때).\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 useEmblaCarousel from \"embla-carousel-react\";\n\nexport interface HeroCarouselSlideInput {\n readonly image: string;\n readonly alt: string;\n readonly caption?: string;\n}\n\nexport interface HeroCarouselViewportProps {\n readonly slides: readonly HeroCarouselSlideInput[];\n readonly autoplay: boolean;\n readonly intervalMs: number;\n}\n\nfunction 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\nfunction 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 function HeroCarouselViewport({\n slides,\n autoplay,\n intervalMs,\n}: HeroCarouselViewportProps) {\n const reducedMotion = usePrefersReducedMotion();\n const tabVisible = useDocumentVisible();\n const [emblaRef, emblaApi] = useEmblaCarousel({\n loop: true,\n duration: reducedMotion ? 1 : 20,\n });\n const [selectedIndex, setSelectedIndex] = useState(0);\n const rootRef = useRef<HTMLDivElement | null>(null);\n\n const onSelect = useCallback(() => {\n if (!emblaApi) return;\n setSelectedIndex(emblaApi.selectedScrollSnap());\n }, [emblaApi]);\n\n useEffect(() => {\n if (!emblaApi) return;\n onSelect();\n emblaApi.on(\"select\", onSelect);\n emblaApi.on(\"reInit\", onSelect);\n return () => {\n emblaApi.off(\"select\", onSelect);\n emblaApi.off(\"reInit\", onSelect);\n };\n }, [emblaApi, onSelect]);\n\n const shouldAutoplay = autoplay && !reducedMotion && tabVisible;\n\n useEffect(() => {\n if (!emblaApi || !shouldAutoplay) return;\n const id = window.setInterval(() => {\n emblaApi.scrollNext();\n }, intervalMs);\n return () => window.clearInterval(id);\n }, [emblaApi, shouldAutoplay, intervalMs]);\n\n const scrollPrev = useCallback(() => {\n emblaApi?.scrollPrev(reducedMotion);\n }, [emblaApi, reducedMotion]);\n\n const scrollNext = useCallback(() => {\n emblaApi?.scrollNext(reducedMotion);\n }, [emblaApi, reducedMotion]);\n\n const scrollTo = useCallback(\n (index: number) => {\n emblaApi?.scrollTo(index, reducedMotion);\n },\n [emblaApi, reducedMotion],\n );\n\n const onKeyDown = useCallback(\n (event: React.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 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 >\n <div className=\"rt-tpl-hero-carousel-viewport\" ref={emblaRef}>\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 <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 <div className=\"rt-tpl-hero-carousel-controls\">\n <div className=\"rt-tpl-hero-carousel-dots\" role=\"tablist\" aria-label=\"슬라이드 선택\">\n {slides.map((_slide, index) => (\n <button\n key={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 ))}\n </div>\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 );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,15 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface HeroCarouselSlideInput {
4
+ readonly image: string;
5
+ readonly alt: string;
6
+ readonly caption?: string;
7
+ }
8
+ interface HeroCarouselViewportProps {
9
+ readonly slides: readonly HeroCarouselSlideInput[];
10
+ readonly autoplay: boolean;
11
+ readonly intervalMs: number;
12
+ }
13
+ declare function HeroCarouselViewport({ slides, autoplay, intervalMs, }: HeroCarouselViewportProps): react_jsx_runtime.JSX.Element;
14
+
15
+ export { type HeroCarouselSlideInput, HeroCarouselViewport, type HeroCarouselViewportProps };
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import {
3
+ HeroCarouselViewport
4
+ } from "./chunk-YUT4BRJY.js";
5
+ export {
6
+ HeroCarouselViewport
7
+ };
8
+ //# sourceMappingURL=hero-carousel-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/routes.js CHANGED
@@ -623,7 +623,7 @@ function createFleetInfoRoute(config = {}) {
623
623
  site: config.site ?? env.VERCEL_PROJECT_PRODUCTION_URL ?? null,
624
624
  framework: "next",
625
625
  cms_client: CMS_CLIENT_VERSION,
626
- cms_renderer: true ? "0.41.0" : "dev",
626
+ cms_renderer: true ? "0.41.2" : "dev",
627
627
  build_sha: env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? null
628
628
  },
629
629
  { headers: { "cache-control": "public, max-age=300" } }
package/dist/server.d.ts CHANGED
@@ -123,7 +123,8 @@ declare function RootTaleFloatingCta(props: RootTaleFloatingCtaProps): ReactElem
123
123
  * redirectPath prop:
124
124
  * - 외부 사이트가 폼 제출 후 돌아갈 path (API 가 allowlist 검증, ?ok=1 / ?err=*)
125
125
  *
126
- * 0 JS RSC. customer-facing className 전체 `.rt-cms-*` prefix, scoped via
126
+ * RSC form. Turnstile를 켠 경우 Cloudflare 위젯 script만 추가한다.
127
+ * customer-facing className 전체 `.rt-cms-*` prefix, scoped via
127
128
  * `[data-roottale-cms]` (cms-public.css). customer override 자유.
128
129
  */
129
130
 
@@ -149,6 +150,7 @@ interface RootTaleLeadFormProps {
149
150
  heading?: string;
150
151
  description?: string;
151
152
  submitLabel?: string;
153
+ turnstileSiteKey?: string;
152
154
  /** 추가 클래스 (customer 디자인 hook). */
153
155
  className?: string;
154
156
  }
package/dist/server.js CHANGED
@@ -17,14 +17,15 @@ import {
17
17
  renderBlocks,
18
18
  renderBoundBlocks,
19
19
  themeToCssVars
20
- } from "./chunk-6AFM2UR5.js";
20
+ } from "./chunk-5R6APJZ2.js";
21
21
  import {
22
22
  collectCategories,
23
23
  collectCategoriesWithCounts,
24
24
  defaultCategoryHref,
25
25
  filterPostsByCategory
26
26
  } from "./chunk-FUA5H6JE.js";
27
- import "./chunk-IOYBEH6E.js";
27
+ import "./chunk-QKZ4HRBB.js";
28
+ import "./chunk-YUT4BRJY.js";
28
29
  export {
29
30
  RenderTiptap,
30
31
  RootTaleBlogCategories,
@@ -489,3 +489,199 @@
489
489
  font-family: ui-monospace, monospace;
490
490
  font-size: 0.875rem;
491
491
  }
492
+
493
+ /* ── hero: carousel (ADR-0073 K3 C1) ─────────────────────────────────────────
494
+ * 별도 섹션에 둔다 — 위 legacy 5종/trust/contact CSS 는 트러스트 회귀 테스트가
495
+ * "@media 금지 + hex 리터럴 금지"를 강제하는 영역이라(`trust-contact.test.tsx`),
496
+ * `prefers-reduced-motion` 미디어쿼리가 필요한 이 variant 는 그 계약 밖에
497
+ * 둬야 두 계약이 충돌하지 않는다. 풀스크린은 컨테이너 쿼리 대신 `svh` 사용
498
+ * (이 리포에 아직 block-axis(`cqb`/`container-type: size`) 컨벤션이 없음 —
499
+ * S2 의 inline-axis(`cqi`) 컨벤션과는 다른 축이라 그대로 확장할 수 없다.
500
+ * ds-rule-pack.md 에 명시된 편차). */
501
+
502
+ [data-roottale-tpl="hero"][data-variant="carousel"] {
503
+ position: relative;
504
+ container-type: inline-size;
505
+ padding-block: 0;
506
+ overflow: hidden;
507
+ /* 고정 100svh — `min-block-size` 였다면 절대배치 아닌 자손(과거 컨트롤 바)이
508
+ * grid auto-row 를 밀어 첫 화면보다 커질 수 있다. 자손은 전부
509
+ * absolute/inset 로 이 고정 박스 안에서만 움직인다. */
510
+ block-size: 100svh;
511
+ display: grid;
512
+ }
513
+
514
+ [data-roottale-tpl="hero"][data-variant="carousel"] :where(.rt-tpl-hero-carousel),
515
+ [data-roottale-tpl="hero"][data-variant="carousel"] :where(.rt-tpl-hero-carousel-overlay) {
516
+ grid-area: 1 / 1;
517
+ }
518
+
519
+ [data-roottale-tpl="hero"][data-variant="carousel"] :where(.rt-tpl-hero-carousel) {
520
+ position: relative;
521
+ inline-size: 100%;
522
+ block-size: 100%;
523
+ overflow: hidden;
524
+ }
525
+
526
+ [data-roottale-tpl="hero"][data-variant="carousel"]
527
+ :where(.rt-tpl-hero-carousel-viewport) {
528
+ overflow: hidden;
529
+ inline-size: 100%;
530
+ block-size: 100%;
531
+ }
532
+
533
+ [data-roottale-tpl="hero"][data-variant="carousel"]
534
+ :where(.rt-tpl-hero-carousel-track) {
535
+ display: flex;
536
+ inline-size: 100%;
537
+ block-size: 100%;
538
+ }
539
+
540
+ [data-roottale-tpl="hero"][data-variant="carousel"]
541
+ :where(.rt-tpl-hero-carousel-slide) {
542
+ position: relative;
543
+ flex: 0 0 100%;
544
+ block-size: 100%;
545
+ overflow: hidden;
546
+ }
547
+
548
+ [data-roottale-tpl="hero"][data-variant="carousel"]
549
+ :where(.rt-tpl-hero-carousel-img) {
550
+ position: absolute;
551
+ inset: 0;
552
+ inline-size: 100%;
553
+ block-size: 100%;
554
+ object-fit: cover;
555
+ transform: scale(1);
556
+ }
557
+
558
+ /* Ken Burns — 활성 슬라이드만, reduced-motion 아닐 때만(M5). */
559
+ @media (prefers-reduced-motion: no-preference) {
560
+ [data-roottale-tpl="hero"][data-variant="carousel"]
561
+ :where(.rt-tpl-hero-carousel-img[data-active="true"]) {
562
+ animation: rt-hero-carousel-kenburns 8s ease-out forwards;
563
+ }
564
+ }
565
+
566
+ @keyframes rt-hero-carousel-kenburns {
567
+ from {
568
+ transform: scale(1);
569
+ }
570
+ to {
571
+ transform: scale(1.05);
572
+ }
573
+ }
574
+
575
+ /* 카피 오버레이 — 슬라이드와 무관하게 고정. 배경 위 대비 확보용 스크림. */
576
+ [data-roottale-tpl="hero"][data-variant="carousel"]
577
+ :where(.rt-tpl-hero-carousel-overlay) {
578
+ position: relative;
579
+ z-index: 1;
580
+ align-self: end;
581
+ inline-size: 100%;
582
+ padding-block-end: clamp(2.5rem, 8cqi, 6rem);
583
+ padding-block-start: clamp(6rem, 20cqi, 10rem);
584
+ background: linear-gradient(
585
+ to top,
586
+ color-mix(in srgb, var(--rt-color-background, #06162f) 78%, transparent) 0%,
587
+ transparent 100%
588
+ );
589
+ color: var(--rt-color-primary-foreground, #ffffff);
590
+ }
591
+
592
+ [data-roottale-tpl="hero"][data-variant="carousel"]
593
+ :where(.rt-tpl-hero-carousel-title) {
594
+ font-size: clamp(2.5rem, 9cqi, 5rem);
595
+ font-weight: 900;
596
+ color: inherit;
597
+ }
598
+
599
+ [data-roottale-tpl="hero"][data-variant="carousel"]
600
+ :where(.rt-tpl-hero-carousel-subtitle) {
601
+ color: color-mix(in srgb, var(--rt-color-primary-foreground, #ffffff) 82%, transparent);
602
+ max-inline-size: 40rem;
603
+ }
604
+
605
+ /* 화살표 */
606
+ [data-roottale-tpl="hero"][data-variant="carousel"]
607
+ :where(.rt-tpl-hero-carousel-arrow) {
608
+ position: absolute;
609
+ z-index: 2;
610
+ inset-block-start: 50%;
611
+ transform: translateY(-50%);
612
+ inline-size: 2.75rem;
613
+ block-size: 2.75rem;
614
+ display: inline-flex;
615
+ align-items: center;
616
+ justify-content: center;
617
+ border-radius: 999px;
618
+ border: 1px solid color-mix(in srgb, var(--rt-color-primary-foreground, #ffffff) 35%, transparent);
619
+ background: color-mix(in srgb, var(--rt-color-background, #06162f) 35%, transparent);
620
+ color: var(--rt-color-primary-foreground, #ffffff);
621
+ cursor: pointer;
622
+ }
623
+
624
+ [data-roottale-tpl="hero"][data-variant="carousel"]
625
+ :where(.rt-tpl-hero-carousel-arrow svg) {
626
+ inline-size: 1.25rem;
627
+ block-size: 1.25rem;
628
+ }
629
+
630
+ [data-roottale-tpl="hero"][data-variant="carousel"]
631
+ :where(.rt-tpl-hero-carousel-arrow--prev) {
632
+ inset-inline-start: clamp(1rem, 4cqi, 2.5rem);
633
+ }
634
+
635
+ [data-roottale-tpl="hero"][data-variant="carousel"]
636
+ :where(.rt-tpl-hero-carousel-arrow--next) {
637
+ inset-inline-end: clamp(1rem, 4cqi, 2.5rem);
638
+ }
639
+
640
+ /* 도트 + N/M 카운터 — absolute 로 뷰포트 바닥에 고정(일반 flow 에 두면 카피
641
+ * 오버레이 아래로 페이지 높이가 100svh 를 넘어버린다, VRT 로 실측). */
642
+ [data-roottale-tpl="hero"][data-variant="carousel"]
643
+ :where(.rt-tpl-hero-carousel-controls) {
644
+ position: absolute;
645
+ z-index: 2;
646
+ inset-inline: 0;
647
+ inset-block-end: clamp(1rem, 3cqi, 1.5rem);
648
+ display: flex;
649
+ align-items: center;
650
+ justify-content: space-between;
651
+ gap: 1rem;
652
+ max-inline-size: var(--rt-tpl-page-max, 72rem);
653
+ margin-inline: auto;
654
+ padding-inline: 1.25rem;
655
+ }
656
+
657
+ [data-roottale-tpl="hero"][data-variant="carousel"]
658
+ :where(.rt-tpl-hero-carousel-dots) {
659
+ display: flex;
660
+ align-items: center;
661
+ gap: 0.5rem;
662
+ }
663
+
664
+ [data-roottale-tpl="hero"][data-variant="carousel"]
665
+ :where(.rt-tpl-hero-carousel-dot) {
666
+ inline-size: 0.5rem;
667
+ block-size: 0.5rem;
668
+ border-radius: 999px;
669
+ border: 0;
670
+ padding: 0;
671
+ cursor: pointer;
672
+ background: color-mix(in srgb, var(--rt-color-primary-foreground, #ffffff) 40%, transparent);
673
+ }
674
+
675
+ [data-roottale-tpl="hero"][data-variant="carousel"]
676
+ :where(.rt-tpl-hero-carousel-dot[data-active="true"]) {
677
+ background: var(--rt-color-primary-foreground, #ffffff);
678
+ }
679
+
680
+ [data-roottale-tpl="hero"][data-variant="carousel"]
681
+ :where(.rt-tpl-hero-carousel-counter) {
682
+ margin: 0;
683
+ font-variant-numeric: tabular-nums;
684
+ color: var(--rt-color-primary-foreground, #ffffff);
685
+ font-size: 0.875rem;
686
+ letter-spacing: 0.04em;
687
+ }
@@ -64,12 +64,12 @@ declare const CtaSchema: z.ZodObject<{
64
64
  label: string;
65
65
  }>;
66
66
  type SectionCta = z.infer<typeof CtaSchema>;
67
- declare const HeroSectionSchema: z.ZodObject<{
67
+ declare const HeroSectionSchema: z.ZodEffects<z.ZodObject<{
68
68
  id: z.ZodOptional<z.ZodString>;
69
69
  theme: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
70
70
  } & {
71
71
  type: z.ZodLiteral<"hero">;
72
- variant: z.ZodDefault<z.ZodEnum<["split", "image", "minimal"]>>;
72
+ variant: z.ZodDefault<z.ZodEnum<["split", "image", "minimal", "carousel"]>>;
73
73
  props: z.ZodObject<{
74
74
  eyebrow: z.ZodOptional<z.ZodString>;
75
75
  /** `\n` 으로 줄바꿈. */
@@ -87,6 +87,26 @@ declare const HeroSectionSchema: z.ZodObject<{
87
87
  href: string;
88
88
  label: string;
89
89
  }>>;
90
+ /** `carousel` 전용 — 고정 카피 아래 배경만 전환되는 슬라이드. 2-6개. */
91
+ slides: z.ZodOptional<z.ZodArray<z.ZodObject<{
92
+ image: z.ZodString;
93
+ alt: z.ZodString;
94
+ caption: z.ZodOptional<z.ZodString>;
95
+ }, "strip", z.ZodTypeAny, {
96
+ image: string;
97
+ alt: string;
98
+ caption?: string | undefined;
99
+ }, {
100
+ image: string;
101
+ alt: string;
102
+ caption?: string | undefined;
103
+ }>, "many">>;
104
+ /** `carousel` 전용 — 부제(슬라이드와 무관한 고정 카피). */
105
+ subtitle: z.ZodOptional<z.ZodString>;
106
+ /** `carousel` 전용. 기본 true — `useReducedMotion` 시 렌더러가 강제 off. */
107
+ autoplay: z.ZodOptional<z.ZodBoolean>;
108
+ /** `carousel` 전용, ms. 기본 6000. */
109
+ intervalMs: z.ZodOptional<z.ZodNumber>;
90
110
  }, "strip", z.ZodTypeAny, {
91
111
  title: string;
92
112
  description?: string | undefined;
@@ -97,6 +117,14 @@ declare const HeroSectionSchema: z.ZodObject<{
97
117
  href: string;
98
118
  label: string;
99
119
  } | undefined;
120
+ slides?: {
121
+ image: string;
122
+ alt: string;
123
+ caption?: string | undefined;
124
+ }[] | undefined;
125
+ subtitle?: string | undefined;
126
+ autoplay?: boolean | undefined;
127
+ intervalMs?: number | undefined;
100
128
  }, {
101
129
  title: string;
102
130
  description?: string | undefined;
@@ -107,10 +135,66 @@ declare const HeroSectionSchema: z.ZodObject<{
107
135
  href: string;
108
136
  label: string;
109
137
  } | undefined;
138
+ slides?: {
139
+ image: string;
140
+ alt: string;
141
+ caption?: string | undefined;
142
+ }[] | undefined;
143
+ subtitle?: string | undefined;
144
+ autoplay?: boolean | undefined;
145
+ intervalMs?: number | undefined;
110
146
  }>;
111
147
  }, "strip", z.ZodTypeAny, {
112
148
  type: "hero";
113
- variant: "image" | "split" | "minimal";
149
+ variant: "image" | "split" | "minimal" | "carousel";
150
+ props: {
151
+ title: string;
152
+ description?: string | undefined;
153
+ eyebrow?: string | undefined;
154
+ imageSrc?: string | undefined;
155
+ imageAlt?: string | undefined;
156
+ cta?: {
157
+ href: string;
158
+ label: string;
159
+ } | undefined;
160
+ slides?: {
161
+ image: string;
162
+ alt: string;
163
+ caption?: string | undefined;
164
+ }[] | undefined;
165
+ subtitle?: string | undefined;
166
+ autoplay?: boolean | undefined;
167
+ intervalMs?: number | undefined;
168
+ };
169
+ theme?: Record<string, string> | undefined;
170
+ id?: string | undefined;
171
+ }, {
172
+ type: "hero";
173
+ props: {
174
+ title: string;
175
+ description?: string | undefined;
176
+ eyebrow?: string | undefined;
177
+ imageSrc?: string | undefined;
178
+ imageAlt?: string | undefined;
179
+ cta?: {
180
+ href: string;
181
+ label: string;
182
+ } | undefined;
183
+ slides?: {
184
+ image: string;
185
+ alt: string;
186
+ caption?: string | undefined;
187
+ }[] | undefined;
188
+ subtitle?: string | undefined;
189
+ autoplay?: boolean | undefined;
190
+ intervalMs?: number | undefined;
191
+ };
192
+ theme?: Record<string, string> | undefined;
193
+ id?: string | undefined;
194
+ variant?: "image" | "split" | "minimal" | "carousel" | undefined;
195
+ }>, {
196
+ type: "hero";
197
+ variant: "image" | "split" | "minimal" | "carousel";
114
198
  props: {
115
199
  title: string;
116
200
  description?: string | undefined;
@@ -121,6 +205,14 @@ declare const HeroSectionSchema: z.ZodObject<{
121
205
  href: string;
122
206
  label: string;
123
207
  } | undefined;
208
+ slides?: {
209
+ image: string;
210
+ alt: string;
211
+ caption?: string | undefined;
212
+ }[] | undefined;
213
+ subtitle?: string | undefined;
214
+ autoplay?: boolean | undefined;
215
+ intervalMs?: number | undefined;
124
216
  };
125
217
  theme?: Record<string, string> | undefined;
126
218
  id?: string | undefined;
@@ -136,10 +228,18 @@ declare const HeroSectionSchema: z.ZodObject<{
136
228
  href: string;
137
229
  label: string;
138
230
  } | undefined;
231
+ slides?: {
232
+ image: string;
233
+ alt: string;
234
+ caption?: string | undefined;
235
+ }[] | undefined;
236
+ subtitle?: string | undefined;
237
+ autoplay?: boolean | undefined;
238
+ intervalMs?: number | undefined;
139
239
  };
140
240
  theme?: Record<string, string> | undefined;
141
241
  id?: string | undefined;
142
- variant?: "image" | "split" | "minimal" | undefined;
242
+ variant?: "image" | "split" | "minimal" | "carousel" | undefined;
143
243
  }>;
144
244
  declare const RichTextSectionSchema: z.ZodObject<{
145
245
  id: z.ZodOptional<z.ZodString>;
@@ -1041,7 +1141,7 @@ declare function defineSiteConfig(input: SiteConfigInput): SiteConfig;
1041
1141
 
1042
1142
  /** 섹션 `theme`(--rt-* 변수 override)를 inline style 로. */
1043
1143
  declare function sectionThemeStyle(theme: Record<string, string> | undefined): CSSProperties | undefined;
1044
- /** 빌트인 `hero`. variant: split(기본) / image / minimal. */
1144
+ /** 빌트인 `hero`. variant: split(기본) / image / minimal / carousel. */
1045
1145
  declare function TemplateHero(section: HeroSection): ReactElement;
1046
1146
  /** 빌트인 `rich-text`. 문단은 `\n\n` 분리. */
1047
1147
  declare function TemplateRichText(section: RichTextSection): ReactElement;
package/dist/templates.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  TemplateTrust,
11
11
  sectionThemeStyle,
12
12
  themeToCssVars
13
- } from "./chunk-6AFM2UR5.js";
13
+ } from "./chunk-5R6APJZ2.js";
14
14
  import "./chunk-FUA5H6JE.js";
15
15
  import {
16
16
  BUILTIN_SECTION_SCHEMAS,
@@ -30,7 +30,8 @@ import {
30
30
  TrustSectionSchema,
31
31
  defineSiteConfig,
32
32
  parseSiteConfig
33
- } from "./chunk-IOYBEH6E.js";
33
+ } from "./chunk-QKZ4HRBB.js";
34
+ import "./chunk-YUT4BRJY.js";
34
35
 
35
36
  // src/templates/SiteRenderer.tsx
36
37
  import {