@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/{chunk-4LQPZDXZ.js → chunk-6MFHA555.js} +24 -6
  3. package/dist/chunk-6MFHA555.js.map +1 -0
  4. package/dist/chunk-FZK3EXYZ.js +103 -0
  5. package/dist/chunk-FZK3EXYZ.js.map +1 -0
  6. package/dist/{chunk-75LHSTKC.js → chunk-JXRHZGRJ.js} +72 -19
  7. package/dist/chunk-JXRHZGRJ.js.map +1 -0
  8. package/dist/chunk-LEHHSI6G.js +64 -0
  9. package/dist/chunk-LEHHSI6G.js.map +1 -0
  10. package/dist/chunk-OMXEU4LS.js +414 -0
  11. package/dist/chunk-OMXEU4LS.js.map +1 -0
  12. package/dist/{chunk-6PDZSFVZ.js → chunk-UM37DOIK.js} +44 -2
  13. package/dist/chunk-UM37DOIK.js.map +1 -0
  14. package/dist/chunk-YLSQAT6B.js +59 -0
  15. package/dist/chunk-YLSQAT6B.js.map +1 -0
  16. package/dist/chunk-ZL2NA3QT.js +313 -0
  17. package/dist/chunk-ZL2NA3QT.js.map +1 -0
  18. package/dist/concerns-client.d.ts +1 -1
  19. package/dist/contact-map-client.d.ts +27 -0
  20. package/dist/contact-map-client.js +8 -0
  21. package/dist/contact-map-client.js.map +1 -0
  22. package/dist/expertise-client.d.ts +7 -1
  23. package/dist/expertise-client.js +2 -1
  24. package/dist/hero-carousel-client.d.ts +18 -2
  25. package/dist/hero-carousel-client.js +2 -1
  26. package/dist/routes.js +6 -4
  27. package/dist/routes.js.map +1 -1
  28. package/dist/server.js +8 -5
  29. package/dist/{site-config-BHcVF993.d.ts → site-config-CeByrPJF.d.ts} +55 -4
  30. package/dist/spaces-client.d.ts +1 -1
  31. package/dist/team-expanding-stage-client.d.ts +35 -0
  32. package/dist/team-expanding-stage-client.js +8 -0
  33. package/dist/team-expanding-stage-client.js.map +1 -0
  34. package/dist/templates.css +457 -5
  35. package/dist/templates.d.ts +26 -3
  36. package/dist/templates.js +13 -6
  37. package/dist/templates.js.map +1 -1
  38. package/dist/ui-react-motion-client.d.ts +4 -1
  39. package/dist/ui-react-motion-client.js +3 -1
  40. package/dist/validate.js +1 -1
  41. package/package.json +7 -4
  42. package/dist/chunk-4LQPZDXZ.js.map +0 -1
  43. package/dist/chunk-6PDZSFVZ.js.map +0 -1
  44. package/dist/chunk-75LHSTKC.js.map +0 -1
  45. package/dist/chunk-F3G6RVIR.js +0 -220
  46. package/dist/chunk-F3G6RVIR.js.map +0 -1
  47. package/dist/chunk-WWS43XCT.js +0 -206
  48. package/dist/chunk-WWS43XCT.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @roottale/cms-renderer-next
2
2
 
3
+ ## 0.46.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 69f7d52: 콘텐츠 유형(Collection)에 **주소 없는 유형**(`basePath: ""`)을 지원합니다(ADR-0060). 강사·리뷰처럼 페이지 안에서 불러와 쓰는 콘텐츠는 이제 라우트 없이 운영할 수 있습니다 — `resolvePostUrl`/`resolvePostPath`는 이런 유형의 글에 항상 `null`을 반환하고, `validateCollections`는 basePath 형식·중복 검사를 건너뜁니다(여러 개 공존 허용). 외부 사이트는 posts API의 `collectionKey` 필터로 내용만 가져다 페이지 안에서 렌더하면 됩니다(`RootTaleBlogList`를 임베디드로 쓸 때는 자체 `postHref` 제공 권장). 소속 판정(`resolvePostCollection`)은 영향 없음 — key만으로 소속이 성립합니다. 기존 유형(basePath 있는 유형)은 동작 변화가 없습니다.
8
+
9
+ ### Patch Changes
10
+
11
+ - bae4a65: `package.json`에 `sideEffects`를 선언합니다(`["*.css"]` — CSS는 명시적 side-effect import 예외, 나머지는 side-effect 없음을 확인). 소스 전수 감사(등록·전역 상태·DOM API 호출 없음)를 거쳤습니다. 실측 결과 현재 Next.js 16.2.6 Turbopack 빌드에서는 이 선언만으로 소비 사이트 번들이 줄지 않습니다 — `SiteRenderer`가 17종 빌트인 섹션을 런타임 문자열로 디스패치하는 단일 함수라 도달성이 항상 전체이고, `templates` 서브패스가 전 섹션을 named export로 공개해 tsup 빌드 산출물이 미리 하나의 공유 청크로 병합되기 때문입니다. 소비 사이트 측 `optimizePackageImports`도 효과가 없음을 확인했습니다(사전 병합된 산출물이라 재작성 대상이 없음). 이번 선언은 향후 번들러 개선이나 dynamic import 재도입 시 전제조건을 미리 충족해 두는 무해한 위생 변경입니다. 동작·export 표면 변경 없음.
12
+ - Updated dependencies [69f7d52]
13
+ - @roottale/cms-core@0.46.0
14
+ - @roottale/cms-client@0.46.0
15
+
16
+ ## 0.45.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 21639a9: `kinetic/CountUpStat`(뷰포트 진입 시 목표값까지 rAF 카운트업, `prefers-reduced-motion` 즉시-표시)을 `./count-up-stat` 서브패스로 공개합니다(ADR-0086 P2 D3 — 구 룰 `docs/design/b2b-component-library.md` §2.2 "카운트업 모션 금지(정직성)"를 supersede). `cms-renderer-next` 의 trust 섹션(`stat-band`/`dashboard` variant)이 이 컴포넌트를 소비해 지표 숫자를 카운트업으로 렌더합니다 — 선행 숫자를 못 찾는 값(전화번호 등 자유 텍스트)은 기존 정적 렌더로 그대로 폴백합니다.
21
+ - 21639a9: hero `carousel` 배경 전환에 `transition: "slide" | "curtain"` prop을 추가합니다(clip-path 와이프 + 신 스윕, chambarun 이식 — ADR-0086 P2 D3). autoplay는 hover·focus-within·재생/일시정지 버튼·Page Visibility 4가지 사유로 정지합니다.
22
+
23
+ `team` `expanding-stage` variant를 실모션 client island(`TeamExpandingStageClient`)로 배선합니다 — hover/focus로 인물 패널이 확장되고, 접힌 상태에서도 자격·경력·CTA가 DOM에 그대로 남아 스크린리더가 읽을 수 있습니다.
24
+
25
+ contact 지도 블록에 `map.mapLoad: "lazy" | "click"`(기본 "lazy", additive)을 추가합니다 — `"click"`이면 실지도 iframe을 즉시 그리지 않고 정적 프리뷰(주소·지도 열기 버튼)만 먼저 렌더한 뒤 클릭 시에만 마운트합니다.
26
+
27
+ `expertise` `tab-stage`·hero carousel 탭 목록에 공용 `use-roving-tablist` 훅(WAI-ARIA APG tablist 키보드 계약 — Arrow/Home/End, 단일 tab stop)을 적용합니다.
28
+
29
+ - d9f671b: `SiteConfig`에 optional `schemaVersion` 필드와 `SITE_CONFIG_SCHEMA_VERSION` 상수·`resolveSiteConfigSchemaVersion` 헬퍼를 추가해 업종 템플릿 팩과 renderer 사이의 버전 계약을 표현합니다(ADR-0086 D2). 필드 미지정 시 버전 1로 해석해 기존 고객 `site.config.ts`는 그대로 동작합니다.
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies [23aa6ae]
34
+ - @roottale/cms-client@0.45.0
35
+ - @roottale/cms-core@0.45.0
36
+
3
37
  ## 0.44.0
4
38
 
5
39
  ### Minor Changes
@@ -1,13 +1,25 @@
1
1
  "use client";
2
2
 
3
+ import {
4
+ useRovingTablist
5
+ } from "./chunk-YLSQAT6B.js";
6
+
3
7
  // src/templates/expertise-client.tsx
4
8
  import { useState } from "react";
5
9
  import { jsx, jsxs } from "react/jsx-runtime";
6
10
  function ExpertiseTabStage({
7
11
  items
8
12
  }) {
9
- const [selectedId, setSelectedId] = useState(items[0]?.id ?? "");
10
- const selected = items.find((item) => item.id === selectedId) ?? items[0];
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ const selected = items[selectedIndex] ?? items[0];
15
+ const { getTabProps, onKeyDown } = useRovingTablist({
16
+ count: items.length,
17
+ selectedIndex,
18
+ onSelect: setSelectedIndex,
19
+ // 레이아웃은 항상 세로 1열(templates.css)이지만 좌우 화살표도 관대하게
20
+ // 허용 — 반응형 변형이나 향후 가로 배치 재사용에도 동일 훅이 그대로 맞는다.
21
+ orientation: "both"
22
+ });
11
23
  if (!selected) return null;
12
24
  return /* @__PURE__ */ jsxs("div", { className: "rt-tpl-expertise-stage", children: [
13
25
  /* @__PURE__ */ jsx(
@@ -16,8 +28,11 @@ function ExpertiseTabStage({
16
28
  className: "rt-tpl-expertise-tabs",
17
29
  role: "tablist",
18
30
  "aria-label": "\uC804\uBB38 \uBD84\uC57C \uC120\uD0DD",
19
- children: items.map((item) => {
20
- const active = item.id === selected.id;
31
+ "aria-orientation": "vertical",
32
+ onKeyDown,
33
+ children: items.map((item, index) => {
34
+ const active = index === selectedIndex;
35
+ const tabProps = getTabProps(index);
21
36
  return /* @__PURE__ */ jsxs(
22
37
  "button",
23
38
  {
@@ -28,7 +43,9 @@ function ExpertiseTabStage({
28
43
  "aria-controls": `rt-expertise-panel-${item.id}`,
29
44
  id: `rt-expertise-tab-${item.id}`,
30
45
  "data-active": active ? "true" : "false",
31
- onClick: () => setSelectedId(item.id),
46
+ tabIndex: tabProps.tabIndex,
47
+ ref: tabProps.ref,
48
+ onClick: () => setSelectedIndex(index),
32
49
  children: [
33
50
  /* @__PURE__ */ jsx("span", { className: "rt-tpl-expertise-tab-label", children: item.label }),
34
51
  /* @__PURE__ */ jsx("span", { className: "rt-tpl-expertise-tab-title", children: item.title })
@@ -46,6 +63,7 @@ function ExpertiseTabStage({
46
63
  role: "tabpanel",
47
64
  id: `rt-expertise-panel-${selected.id}`,
48
65
  "aria-labelledby": `rt-expertise-tab-${selected.id}`,
66
+ tabIndex: 0,
49
67
  children: [
50
68
  selected.imageSrc ? /* @__PURE__ */ jsx(
51
69
  "img",
@@ -72,4 +90,4 @@ function ExpertiseTabStage({
72
90
  export {
73
91
  ExpertiseTabStage
74
92
  };
75
- //# sourceMappingURL=chunk-4LQPZDXZ.js.map
93
+ //# sourceMappingURL=chunk-6MFHA555.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/templates/expertise-client.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\n\nimport type { ExpertiseSection } from \"./site-config.js\";\nimport { useRovingTablist } from \"./use-roving-tablist.js\";\n\ntype ExpertiseItem = ExpertiseSection[\"props\"][\"items\"][number];\n\n/**\n * `expertise` `tab-stage` variant — 좌측 탭 목록(항상 세로 1열, templates.css\n * `.rt-tpl-expertise-tabs`) + 우측 상세 패널. 키보드 계약은 WAI-ARIA APG\n * tablist 패턴(roving tabindex, 단일 tab stop) — `use-roving-tablist.ts` 공용\n * hook 사용(D4, 적대 리뷰 R1 실측 지적 대응).\n */\nexport function ExpertiseTabStage({\n items,\n}: {\n readonly items: readonly ExpertiseItem[];\n}) {\n const [selectedIndex, setSelectedIndex] = useState(0);\n const selected = items[selectedIndex] ?? items[0];\n\n const { getTabProps, onKeyDown } = useRovingTablist<HTMLButtonElement>({\n count: items.length,\n selectedIndex,\n onSelect: setSelectedIndex,\n // 레이아웃은 항상 세로 1열(templates.css)이지만 좌우 화살표도 관대하게\n // 허용 — 반응형 변형이나 향후 가로 배치 재사용에도 동일 훅이 그대로 맞는다.\n orientation: \"both\",\n });\n\n if (!selected) return null;\n\n return (\n <div className=\"rt-tpl-expertise-stage\">\n <div\n className=\"rt-tpl-expertise-tabs\"\n role=\"tablist\"\n aria-label=\"전문 분야 선택\"\n aria-orientation=\"vertical\"\n onKeyDown={onKeyDown}\n >\n {items.map((item, index) => {\n const active = index === selectedIndex;\n const tabProps = getTabProps(index);\n return (\n <button\n key={item.id}\n type=\"button\"\n role=\"tab\"\n className=\"rt-tpl-expertise-tab\"\n aria-selected={active}\n aria-controls={`rt-expertise-panel-${item.id}`}\n id={`rt-expertise-tab-${item.id}`}\n data-active={active ? \"true\" : \"false\"}\n tabIndex={tabProps.tabIndex}\n ref={tabProps.ref}\n onClick={() => setSelectedIndex(index)}\n >\n <span className=\"rt-tpl-expertise-tab-label\">{item.label}</span>\n <span className=\"rt-tpl-expertise-tab-title\">{item.title}</span>\n </button>\n );\n })}\n </div>\n <article\n className=\"rt-tpl-expertise-panel\"\n role=\"tabpanel\"\n id={`rt-expertise-panel-${selected.id}`}\n aria-labelledby={`rt-expertise-tab-${selected.id}`}\n tabIndex={0}\n >\n {selected.imageSrc ? (\n <img\n className=\"rt-tpl-expertise-panel-img\"\n src={selected.imageSrc}\n alt={selected.imageAlt ?? \"\"}\n loading=\"lazy\"\n />\n ) : null}\n <div className=\"rt-tpl-expertise-panel-body\">\n {selected.kicker ? (\n <p className=\"rt-tpl-expertise-kicker\">{selected.kicker}</p>\n ) : null}\n <h3 className=\"rt-tpl-expertise-title\">{selected.title}</h3>\n <p className=\"rt-tpl-expertise-body\">{selected.body}</p>\n {selected.tags ? (\n <ul className=\"rt-tpl-expertise-tags\" aria-label={`${selected.title} 태그`}>\n {selected.tags.map((tag) => (\n <li key={tag}>{tag}</li>\n ))}\n </ul>\n ) : null}\n {selected.href ? (\n <a className=\"rt-tpl-expertise-cta\" href={selected.href}>\n {selected.ctaLabel ?? \"자세히 보기\"}\n </a>\n ) : null}\n </div>\n </article>\n </div>\n );\n}\n"],"mappings":";;;;;AAEA,SAAS,gBAAgB;AA6Cb,SAaE,KAbF;AAhCL,SAAS,kBAAkB;AAAA,EAChC;AACF,GAEG;AACD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,CAAC;AACpD,QAAM,WAAW,MAAM,aAAa,KAAK,MAAM,CAAC;AAEhD,QAAM,EAAE,aAAa,UAAU,IAAI,iBAAoC;AAAA,IACrE,OAAO,MAAM;AAAA,IACb;AAAA,IACA,UAAU;AAAA;AAAA;AAAA,IAGV,aAAa;AAAA,EACf,CAAC;AAED,MAAI,CAAC,SAAU,QAAO;AAEtB,SACE,qBAAC,SAAI,WAAU,0BACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAW;AAAA,QACX,oBAAiB;AAAA,QACjB;AAAA,QAEC,gBAAM,IAAI,CAAC,MAAM,UAAU;AAC1B,gBAAM,SAAS,UAAU;AACzB,gBAAM,WAAW,YAAY,KAAK;AAClC,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,WAAU;AAAA,cACV,iBAAe;AAAA,cACf,iBAAe,sBAAsB,KAAK,EAAE;AAAA,cAC5C,IAAI,oBAAoB,KAAK,EAAE;AAAA,cAC/B,eAAa,SAAS,SAAS;AAAA,cAC/B,UAAU,SAAS;AAAA,cACnB,KAAK,SAAS;AAAA,cACd,SAAS,MAAM,iBAAiB,KAAK;AAAA,cAErC;AAAA,oCAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA,gBACzD,oBAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA;AAAA;AAAA,YAbpD,KAAK;AAAA,UAcZ;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAK;AAAA,QACL,IAAI,sBAAsB,SAAS,EAAE;AAAA,QACrC,mBAAiB,oBAAoB,SAAS,EAAE;AAAA,QAChD,UAAU;AAAA,QAET;AAAA,mBAAS,WACR;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,KAAK,SAAS;AAAA,cACd,KAAK,SAAS,YAAY;AAAA,cAC1B,SAAQ;AAAA;AAAA,UACV,IACE;AAAA,UACJ,qBAAC,SAAI,WAAU,+BACZ;AAAA,qBAAS,SACR,oBAAC,OAAE,WAAU,2BAA2B,mBAAS,QAAO,IACtD;AAAA,YACJ,oBAAC,QAAG,WAAU,0BAA0B,mBAAS,OAAM;AAAA,YACvD,oBAAC,OAAE,WAAU,yBAAyB,mBAAS,MAAK;AAAA,YACnD,SAAS,OACR,oBAAC,QAAG,WAAU,yBAAwB,cAAY,GAAG,SAAS,KAAK,iBAChE,mBAAS,KAAK,IAAI,CAAC,QAClB,oBAAC,QAAc,iBAAN,GAAU,CACpB,GACH,IACE;AAAA,YACH,SAAS,OACR,oBAAC,OAAE,WAAU,wBAAuB,MAAM,SAAS,MAChD,mBAAS,YAAY,mCACxB,IACE;AAAA,aACN;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ // src/templates/team-expanding-stage-client.tsx
4
+ import { useState } from "react";
5
+ import { useReducedMotion } from "motion/react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ function memberInitials(name) {
8
+ const parts = name.trim().split(/\s+/).filter(Boolean);
9
+ if (parts.length === 0) return "?";
10
+ const initials = parts.slice(0, 2).map((part) => part.at(0) ?? "").join("");
11
+ return initials || "?";
12
+ }
13
+ function TeamExpandingStageClient({
14
+ members
15
+ }) {
16
+ const [activeIndex, setActiveIndex] = useState(0);
17
+ const reducedMotion = useReducedMotion();
18
+ if (members.length === 0) return null;
19
+ const activate = (index) => setActiveIndex(index);
20
+ const onItemKeyDown = (event, index) => {
21
+ if ((event.key === "Enter" || event.key === " ") && index !== activeIndex) {
22
+ event.preventDefault();
23
+ activate(index);
24
+ }
25
+ };
26
+ return /* @__PURE__ */ jsx(
27
+ "ul",
28
+ {
29
+ className: "rt-tpl-team-expanding-stage",
30
+ "data-reduced-motion": reducedMotion ? "true" : "false",
31
+ children: members.map((member, index) => {
32
+ const active = index === activeIndex;
33
+ return /* @__PURE__ */ jsxs(
34
+ "li",
35
+ {
36
+ className: "rt-tpl-team-expanding-item",
37
+ "data-active": active ? "true" : "false",
38
+ tabIndex: 0,
39
+ onMouseEnter: () => activate(index),
40
+ onFocus: () => activate(index),
41
+ onClick: (event) => {
42
+ if (!active) event.preventDefault();
43
+ activate(index);
44
+ },
45
+ onKeyDown: (event) => onItemKeyDown(event, index),
46
+ children: [
47
+ /* @__PURE__ */ jsx("span", { className: "rt-tpl-team-expanding-index", "aria-hidden": "true", children: String(index + 1).padStart(2, "0") }),
48
+ member.photoSrc ? /* @__PURE__ */ jsx(
49
+ "img",
50
+ {
51
+ className: "rt-tpl-team-expanding-photo",
52
+ src: member.photoSrc,
53
+ alt: member.photoAlt ?? "",
54
+ loading: "lazy"
55
+ }
56
+ ) : /* @__PURE__ */ jsx(
57
+ "div",
58
+ {
59
+ className: "rt-tpl-team-expanding-photo rt-tpl-team-expanding-photo--initials",
60
+ "aria-hidden": "true",
61
+ children: memberInitials(member.name)
62
+ }
63
+ ),
64
+ /* @__PURE__ */ jsxs("div", { className: "rt-tpl-team-expanding-name", "aria-hidden": "true", children: [
65
+ /* @__PURE__ */ jsx("b", { children: member.name }),
66
+ /* @__PURE__ */ jsx("span", { children: member.title })
67
+ ] }),
68
+ /* @__PURE__ */ jsxs("div", { className: "rt-tpl-team-expanding-detail", children: [
69
+ /* @__PURE__ */ jsx("p", { className: "rt-tpl-team-expanding-role", children: member.title }),
70
+ /* @__PURE__ */ jsx("h3", { className: "rt-tpl-team-name", children: member.name }),
71
+ member.specialties ? /* @__PURE__ */ jsx(
72
+ "ul",
73
+ {
74
+ className: "rt-tpl-team-specialties",
75
+ "aria-label": `${member.name} \uC804\uBB38 \uBD84\uC57C`,
76
+ children: member.specialties.map((specialty) => /* @__PURE__ */ jsx("li", { children: specialty }, specialty))
77
+ }
78
+ ) : null,
79
+ member.credentials ? /* @__PURE__ */ jsx("ul", { className: "rt-tpl-team-credentials", children: member.credentials.map((credential) => /* @__PURE__ */ jsx("li", { children: credential }, credential)) }) : null,
80
+ member.bio ? /* @__PURE__ */ jsx("p", { className: "rt-tpl-team-bio", children: member.bio }) : null,
81
+ member.href ? /* @__PURE__ */ jsx(
82
+ "a",
83
+ {
84
+ className: "rt-tpl-team-cta",
85
+ href: member.href,
86
+ tabIndex: active ? 0 : -1,
87
+ children: member.ctaLabel ?? "\uD504\uB85C\uD544 \uBCF4\uAE30"
88
+ }
89
+ ) : null
90
+ ] })
91
+ ]
92
+ },
93
+ member.name
94
+ );
95
+ })
96
+ }
97
+ );
98
+ }
99
+
100
+ export {
101
+ TeamExpandingStageClient
102
+ };
103
+ //# sourceMappingURL=chunk-FZK3EXYZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/templates/team-expanding-stage-client.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, type KeyboardEvent, type ReactElement } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\n\nimport type { TeamSection } from \"./site-config.js\";\n\ntype TeamMember = TeamSection[\"props\"][\"members\"][number];\n\nfunction memberInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"?\";\n const initials = parts\n .slice(0, 2)\n .map((part) => part.at(0) ?? \"\")\n .join(\"\");\n return initials || \"?\";\n}\n\n/**\n * `team` `expanding-stage` variant 의 실모션 client island (D3 크래프트).\n * chambarun `.staff-stage`/`.staff-panel` 스펙(mockup/assets/site.js·site.css)\n * 이식 — hover/focus/tap 한 인물이 확장되며 자격 정보가 드러난다.\n *\n * 현재 `sections.tsx` `TemplateTeamExpandingStage` 는 이 컴포넌트를 아직\n * import 하지 않는다(정적 격자만 렌더) — `sections.tsx` 는 이 작업 소유 밖이라\n * 와이어링은 별도 보고 대상(README/보고 참조). 이 파일은 그 자리에 그대로\n * 끼워 넣을 수 있도록 `members` 하나만 받는 독립 컴포넌트로 설계했다.\n *\n * 접근성(계약, DESIGN.md \"Medical Staff Cards\" 원칙 확장):\n * - 각 패널은 `tabIndex=0` 컨테이너(hover/focus/click 시 확장) — chambarun\n * 원본과 동일하게 명시적 role 은 두지 않는다(내부에 실제 CTA 링크가 있어\n * `role=\"button\"` 을 얹으면 중첩 인터랙티브 요소가 된다).\n * - 자격정보·경력·CTA 는 접힌 상태에서도 DOM 에 그대로 존재한다(opacity 로만\n * 가림) — 스크린리더는 hover 없이 전체 내용을 읽을 수 있다(\"decorative hover\n * layers do not replace content\").\n * - CTA 링크는 비활성 패널일 때 `tabIndex=-1` — 보이지 않는 링크가 키보드\n * 포커스를 가로채지 않는다(chambarun 원본에는 없던, React 상태로만 가능한\n * 개선).\n * - `prefers-reduced-motion: reduce` → 전환 자체가 CSS 에서 제거된다\n * (templates.css, `@media (prefers-reduced-motion: no-preference)` 안에만\n * transition 선언 — ADR-0073 K3 C1 과 동일 관행). `data-reduced-motion`\n * 속성은 시각 회귀 테스트/QA 스크린샷 식별용.\n */\nexport function TeamExpandingStageClient({\n members,\n}: {\n readonly members: readonly TeamMember[];\n}): ReactElement | null {\n const [activeIndex, setActiveIndex] = useState(0);\n const reducedMotion = useReducedMotion();\n\n if (members.length === 0) return null;\n\n const activate = (index: number) => setActiveIndex(index);\n\n const onItemKeyDown = (event: KeyboardEvent<HTMLLIElement>, index: number) => {\n if ((event.key === \"Enter\" || event.key === \" \") && index !== activeIndex) {\n event.preventDefault();\n activate(index);\n }\n };\n\n return (\n <ul\n className=\"rt-tpl-team-expanding-stage\"\n data-reduced-motion={reducedMotion ? \"true\" : \"false\"}\n >\n {members.map((member, index) => {\n const active = index === activeIndex;\n return (\n <li\n key={member.name}\n className=\"rt-tpl-team-expanding-item\"\n data-active={active ? \"true\" : \"false\"}\n tabIndex={0}\n onMouseEnter={() => activate(index)}\n onFocus={() => activate(index)}\n onClick={(event) => {\n // 첫 탭(hover/focus 없이 바로 터치)은 확장만 — CTA 로 즉시\n // 이동하지 않는다. 이미 활성(=두 번째 탭)이면 기본 클릭 동작\n // (링크 없는 li 자체는 아무 것도 안 함, 내부 CTA 는 그대로 이동).\n if (!active) event.preventDefault();\n activate(index);\n }}\n onKeyDown={(event) => onItemKeyDown(event, index)}\n >\n <span className=\"rt-tpl-team-expanding-index\" aria-hidden=\"true\">\n {String(index + 1).padStart(2, \"0\")}\n </span>\n {member.photoSrc ? (\n <img\n className=\"rt-tpl-team-expanding-photo\"\n src={member.photoSrc}\n alt={member.photoAlt ?? \"\"}\n loading=\"lazy\"\n />\n ) : (\n <div\n className=\"rt-tpl-team-expanding-photo rt-tpl-team-expanding-photo--initials\"\n aria-hidden=\"true\"\n >\n {memberInitials(member.name)}\n </div>\n )}\n <div className=\"rt-tpl-team-expanding-name\" aria-hidden=\"true\">\n <b>{member.name}</b>\n <span>{member.title}</span>\n </div>\n <div className=\"rt-tpl-team-expanding-detail\">\n <p className=\"rt-tpl-team-expanding-role\">{member.title}</p>\n <h3 className=\"rt-tpl-team-name\">{member.name}</h3>\n {member.specialties ? (\n <ul\n className=\"rt-tpl-team-specialties\"\n aria-label={`${member.name} 전문 분야`}\n >\n {member.specialties.map((specialty) => (\n <li key={specialty}>{specialty}</li>\n ))}\n </ul>\n ) : null}\n {member.credentials ? (\n <ul className=\"rt-tpl-team-credentials\">\n {member.credentials.map((credential) => (\n <li key={credential}>{credential}</li>\n ))}\n </ul>\n ) : null}\n {member.bio ? <p className=\"rt-tpl-team-bio\">{member.bio}</p> : null}\n {member.href ? (\n <a\n className=\"rt-tpl-team-cta\"\n href={member.href}\n tabIndex={active ? 0 : -1}\n >\n {member.ctaLabel ?? \"프로필 보기\"}\n </a>\n ) : null}\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +1,15 @@
1
1
  import {
2
2
  BeforeAfterSlider
3
3
  } from "./chunk-U5AKACPN.js";
4
+ import {
5
+ ContactMapClickToLoad
6
+ } from "./chunk-LEHHSI6G.js";
4
7
  import {
5
8
  ExpertiseTabStage
6
- } from "./chunk-4LQPZDXZ.js";
9
+ } from "./chunk-6MFHA555.js";
10
+ import {
11
+ TeamExpandingStageClient
12
+ } from "./chunk-FZK3EXYZ.js";
7
13
  import {
8
14
  SpacesStage
9
15
  } from "./chunk-K62RR4UQ.js";
@@ -12,10 +18,11 @@ import {
12
18
  } from "./chunk-H46JUIHZ.js";
13
19
  import {
14
20
  AnimateOnScroll,
21
+ CountUpStat,
15
22
  Marquee,
16
23
  StaggerGroup,
17
24
  StaggerItem
18
- } from "./chunk-WWS43XCT.js";
25
+ } from "./chunk-ZL2NA3QT.js";
19
26
  import {
20
27
  collectCategories,
21
28
  collectCategoriesWithCounts,
@@ -25,10 +32,10 @@ import {
25
32
  } from "./chunk-FUA5H6JE.js";
26
33
  import {
27
34
  SectionSchema
28
- } from "./chunk-6PDZSFVZ.js";
35
+ } from "./chunk-UM37DOIK.js";
29
36
  import {
30
37
  HeroCarouselViewport
31
- } from "./chunk-F3G6RVIR.js";
38
+ } from "./chunk-OMXEU4LS.js";
32
39
 
33
40
  // src/server.tsx
34
41
  import {
@@ -307,6 +314,29 @@ function RootTaleLeadForm(props) {
307
314
 
308
315
  // src/templates/sections.tsx
309
316
  import { isAllowedEmbedSrc, isAllowedMapEmbedSrc } from "@roottale/cms-core";
317
+
318
+ // src/templates/metric-value.ts
319
+ var LEADING_NUMBER_PATTERN = /^(\d[\d,]*)(\.(\d+))?/;
320
+ function parseMetricValue(raw) {
321
+ const match = LEADING_NUMBER_PATTERN.exec(raw);
322
+ if (!match) {
323
+ return null;
324
+ }
325
+ const [matched, integerPart, , fractionPart] = match;
326
+ const integerLiteral = (integerPart ?? "").replace(/,/g, "");
327
+ const numericLiteral = fractionPart ? `${integerLiteral}.${fractionPart}` : integerLiteral;
328
+ const value = Number(numericLiteral);
329
+ if (!Number.isFinite(value)) {
330
+ return null;
331
+ }
332
+ return {
333
+ value,
334
+ decimals: fractionPart?.length ?? 0,
335
+ suffix: raw.slice(matched.length)
336
+ };
337
+ }
338
+
339
+ // src/templates/sections.tsx
310
340
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
311
341
  function sectionThemeStyle(theme) {
312
342
  if (!theme) return void 0;
@@ -373,6 +403,7 @@ function TemplateHeroCarousel(section) {
373
403
  slides,
374
404
  autoplay,
375
405
  intervalMs,
406
+ transition,
376
407
  proofMetrics,
377
408
  scrim
378
409
  } = section.props;
@@ -392,7 +423,8 @@ function TemplateHeroCarousel(section) {
392
423
  slides: safeSlides,
393
424
  autoplay: autoplay ?? true,
394
425
  intervalMs: intervalMs ?? 6e3,
395
- scrim
426
+ scrim,
427
+ transition
396
428
  }
397
429
  ) : null,
398
430
  /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-container rt-tpl-hero-carousel-overlay", children: [
@@ -1027,6 +1059,20 @@ function TemplateCtaBanner(section) {
1027
1059
  }
1028
1060
  );
1029
1061
  }
1062
+ function TrustMetricValue({ value }) {
1063
+ const parsed = parseMetricValue(value);
1064
+ if (!parsed) {
1065
+ return /* @__PURE__ */ jsx2(Fragment2, { children: value });
1066
+ }
1067
+ return /* @__PURE__ */ jsx2(
1068
+ CountUpStat,
1069
+ {
1070
+ value: parsed.value,
1071
+ decimals: parsed.decimals,
1072
+ suffix: parsed.suffix
1073
+ }
1074
+ );
1075
+ }
1030
1076
  function TrustCredentialIcon() {
1031
1077
  return /* @__PURE__ */ jsxs2(
1032
1078
  "svg",
@@ -1056,9 +1102,9 @@ function TemplateTrust(section) {
1056
1102
  children: /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-container rt-tpl-trust-inner", children: [
1057
1103
  heading ? /* @__PURE__ */ jsx2("h2", { className: "rt-tpl-heading", children: heading }) : null,
1058
1104
  section.variant === "stat-band" ? (
1059
- // 수치 밴드 진입 시 fade-up(C8). 카운트업은 미채택 — 이 seed 의
1060
- // disclaimer 계약("과장된 카운트업 모션 미사용")과 tabular-nums 결정성
1061
- // 유지를 위해 즉시-표시. reduce·SSR 시 <div> 그대로(마크업 불변).
1105
+ // 수치 밴드 진입 시 fade-up(C8) + 지표 숫자 카운트업(ADR-0086 D3로
1106
+ // 채택(구 룰 supersede, 2026-07-17)). reduce·SSR 시 <div> 그대로
1107
+ // (마크업 불변) + CountUpStat 자체도 mount 전엔 최종값 정적 표시.
1062
1108
  /* @__PURE__ */ jsx2(AnimateOnScroll, { as: "div", variant: "fade-up", className: "rt-tpl-trust-stat-band", children: (metrics ?? []).map((metric) => /* @__PURE__ */ jsxs2(
1063
1109
  "div",
1064
1110
  {
@@ -1066,7 +1112,7 @@ function TemplateTrust(section) {
1066
1112
  "data-variant": "band-item",
1067
1113
  children: [
1068
1114
  /* @__PURE__ */ jsxs2("span", { className: "rt-stat__value", children: [
1069
- metric.value,
1115
+ /* @__PURE__ */ jsx2(TrustMetricValue, { value: metric.value }),
1070
1116
  metric.unit ? /* @__PURE__ */ jsx2("span", { className: "rt-stat__unit", children: metric.unit }) : null
1071
1117
  ] }),
1072
1118
  /* @__PURE__ */ jsx2("span", { className: "rt-stat__label", children: metric.label }),
@@ -1192,7 +1238,7 @@ function TemplateTrust(section) {
1192
1238
  }
1193
1239
  ) : null,
1194
1240
  /* @__PURE__ */ jsxs2("span", { className: "rt-stat__value", children: [
1195
- metric.value,
1241
+ /* @__PURE__ */ jsx2(TrustMetricValue, { value: metric.value }),
1196
1242
  metric.unit ? /* @__PURE__ */ jsx2("span", { className: "rt-stat__unit", children: metric.unit }) : null
1197
1243
  ] }),
1198
1244
  /* @__PURE__ */ jsx2("span", { className: "rt-stat__label", children: metric.label }),
@@ -1497,11 +1543,7 @@ function TemplateTeamExpandingStage(section) {
1497
1543
  children: /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-container", children: [
1498
1544
  heading ? /* @__PURE__ */ jsx2("h2", { className: "rt-tpl-heading", children: heading }) : null,
1499
1545
  description ? /* @__PURE__ */ jsx2("p", { className: "rt-tpl-section-desc", children: description }) : null,
1500
- /* @__PURE__ */ jsx2("ul", { className: "rt-tpl-team-expanding-stage", children: members.map((member, index) => /* @__PURE__ */ jsx2("li", { className: "rt-tpl-team-expanding-item", children: /* @__PURE__ */ jsxs2("article", { className: "rt-tpl-team-card", children: [
1501
- /* @__PURE__ */ jsx2("span", { className: "rt-tpl-team-expanding-index", "aria-hidden": "true", children: String(index + 1).padStart(2, "0") }),
1502
- /* @__PURE__ */ jsx2(TeamMemberPhoto, { member }),
1503
- /* @__PURE__ */ jsx2(TeamMemberBody, { member })
1504
- ] }) }, member.name)) })
1546
+ /* @__PURE__ */ jsx2(TeamExpandingStageClient, { members })
1505
1547
  ] })
1506
1548
  }
1507
1549
  );
@@ -1561,6 +1603,7 @@ function ContactMapBlock({
1561
1603
  const liveMap = Boolean(
1562
1604
  embedUrl && (isAllowedMapEmbedSrc(embedUrl) || isAllowedEmbedSrc(embedUrl))
1563
1605
  );
1606
+ const clickToLoad = liveMap && embedUrl && (map?.mapLoad ?? "lazy") === "click";
1564
1607
  return /* @__PURE__ */ jsxs2("div", { className: "rt-tpl-contact-map-block", "data-provider": map?.provider, children: [
1565
1608
  map?.facadeImageSrc && !liveMap ? /* @__PURE__ */ jsxs2("figure", { className: "rt-tpl-contact-map-facade", children: [
1566
1609
  /* @__PURE__ */ jsx2(
@@ -1573,7 +1616,15 @@ function ContactMapBlock({
1573
1616
  ),
1574
1617
  externalUrl ? /* @__PURE__ */ jsx2("figcaption", { children: /* @__PURE__ */ jsx2("a", { href: externalUrl, target: "_blank", rel: "noopener noreferrer", children: "\uC9C0\uB3C4 \uD06C\uAC8C \uBCF4\uAE30" }) }) : null
1575
1618
  ] }) : null,
1576
- embedUrl ? /* @__PURE__ */ jsx2(ContactMap, { url: embedUrl }) : null
1619
+ clickToLoad ? /* @__PURE__ */ jsx2(
1620
+ ContactMapClickToLoad,
1621
+ {
1622
+ embedUrl,
1623
+ externalUrl,
1624
+ facadeImageSrc: map?.facadeImageSrc,
1625
+ facadeImageAlt: map?.facadeImageAlt
1626
+ }
1627
+ ) : embedUrl ? /* @__PURE__ */ jsx2(ContactMap, { url: embedUrl }) : null
1577
1628
  ] });
1578
1629
  }
1579
1630
  function ContactActions({
@@ -3051,7 +3102,7 @@ function archiveGroupTitle(group) {
3051
3102
  }
3052
3103
  function archivePostHref(collectionKey, slug, collections) {
3053
3104
  const collection = collections?.find((item) => item.key === collectionKey);
3054
- const basePath = collection?.basePath ?? defaultArchiveBasePath(collectionKey);
3105
+ const basePath = collection?.basePath || defaultArchiveBasePath(collectionKey);
3055
3106
  const trimmedBase = basePath.replace(/\/+$/, "");
3056
3107
  if (collection?.detail === false) return trimmedBase;
3057
3108
  return `${trimmedBase}/${slug}`;
@@ -3183,7 +3234,9 @@ function makeCollectionPostHref(collections) {
3183
3234
  const path = resolvePostPath(post, collections);
3184
3235
  if (path) return path;
3185
3236
  const collection = resolvePostCollection(post, collections);
3186
- if (collection && collection.detail === false) return collection.basePath;
3237
+ if (collection && collection.detail === false && collection.basePath) {
3238
+ return collection.basePath;
3239
+ }
3187
3240
  return defaultPostHref(post);
3188
3241
  };
3189
3242
  }
@@ -3473,4 +3526,4 @@ export {
3473
3526
  RootTalePage,
3474
3527
  RenderTiptap
3475
3528
  };
3476
- //# sourceMappingURL=chunk-75LHSTKC.js.map
3529
+ //# sourceMappingURL=chunk-JXRHZGRJ.js.map