@roottale/cms-renderer-next 0.43.0 → 0.45.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 +29 -0
  2. package/README.md +4 -0
  3. package/dist/{chunk-FKDCPFYV.js → chunk-45KYMMDY.js} +156 -21
  4. package/dist/chunk-45KYMMDY.js.map +1 -0
  5. package/dist/{chunk-4LQPZDXZ.js → chunk-6MFHA555.js} +24 -6
  6. package/dist/chunk-6MFHA555.js.map +1 -0
  7. package/dist/chunk-FZK3EXYZ.js +103 -0
  8. package/dist/chunk-FZK3EXYZ.js.map +1 -0
  9. package/dist/{chunk-NUUTTSGT.js → chunk-GUV5WHD5.js} +37 -3
  10. package/dist/chunk-GUV5WHD5.js.map +1 -0
  11. package/dist/chunk-LEHHSI6G.js +64 -0
  12. package/dist/chunk-LEHHSI6G.js.map +1 -0
  13. package/dist/chunk-OMXEU4LS.js +414 -0
  14. package/dist/chunk-OMXEU4LS.js.map +1 -0
  15. package/dist/chunk-YLSQAT6B.js +59 -0
  16. package/dist/chunk-YLSQAT6B.js.map +1 -0
  17. package/dist/{chunk-WWS43XCT.js → chunk-Z3ZSCPG4.js} +82 -20
  18. package/dist/chunk-Z3ZSCPG4.js.map +1 -0
  19. package/dist/concerns-client.d.ts +1 -1
  20. package/dist/contact-map-client.d.ts +27 -0
  21. package/dist/contact-map-client.js +8 -0
  22. package/dist/contact-map-client.js.map +1 -0
  23. package/dist/expertise-client.d.ts +7 -1
  24. package/dist/expertise-client.js +2 -1
  25. package/dist/hero-carousel-client.d.ts +18 -2
  26. package/dist/hero-carousel-client.js +2 -1
  27. package/dist/routes.js +1 -1
  28. package/dist/server.d.ts +5 -0
  29. package/dist/server.js +8 -5
  30. package/dist/{site-config-DgYGGwu1.d.ts → site-config-kqurIH-k.d.ts} +42 -4
  31. package/dist/spaces-client.d.ts +1 -1
  32. package/dist/team-expanding-stage-client.d.ts +35 -0
  33. package/dist/team-expanding-stage-client.js +8 -0
  34. package/dist/team-expanding-stage-client.js.map +1 -0
  35. package/dist/templates.css +607 -5
  36. package/dist/templates.d.ts +30 -4
  37. package/dist/templates.js +13 -6
  38. package/dist/templates.js.map +1 -1
  39. package/dist/ui-react-motion-client.d.ts +1 -0
  40. package/dist/ui-react-motion-client.js +3 -1
  41. package/dist/validate.js +1 -1
  42. package/package.json +4 -4
  43. package/dist/chunk-4LQPZDXZ.js.map +0 -1
  44. package/dist/chunk-F3G6RVIR.js +0 -220
  45. package/dist/chunk-F3G6RVIR.js.map +0 -1
  46. package/dist/chunk-FKDCPFYV.js.map +0 -1
  47. package/dist/chunk-NUUTTSGT.js.map +0 -1
  48. package/dist/chunk-WWS43XCT.js.map +0 -1
@@ -1,11 +1,34 @@
1
- import { C as ConcernsSection, E as ExpertiseSection, S as SpacesSection, B as BeforeAfterSection, a as ContactSection, b as CtaBannerSection, F as FaqSection, c as FooterSection, H as HeroSection, d as HoursSection, P as PricingSection, e as ProcessSection, R as RichTextSection, f as ServicesSection, T as TeamSection, g as TestimonialsSection, h as TrustSection, i as SiteConfig, j as SiteConfigInput } from './site-config-DgYGGwu1.js';
2
- export { k as BUILTIN_SECTION_SCHEMAS, l as BUILTIN_SECTION_TYPES, m as BeforeAfterSectionSchema, n as BlogListSection, o as BlogListSectionSchema, p as ConcernsSectionSchema, q as ContactSectionSchema, r as CtaBannerSectionSchema, s as CustomSection, t as CustomSectionSchema, u as ExpertiseSectionSchema, v as FaqSectionSchema, w as FooterSectionSchema, x as HeroSectionSchema, y as HoursSectionSchema, L as LeadFormSection, z as LeadFormSectionSchema, N as NavItem, A as NavItemSchema, D as PricingSectionSchema, G as ProcessSectionSchema, I as RichTextSectionSchema, J as SECTION_VARIANTS, K as Section, M as SectionCta, O as SectionInput, Q as SectionSchema, U as ServicesSectionSchema, V as SiteConfigSchema, W as SpacesSectionSchema, X as TeamSectionSchema, Y as TestimonialsSectionSchema, Z as TrustSectionSchema, _ as defineSiteConfig, $ as parseSiteConfig } from './site-config-DgYGGwu1.js';
1
+ import { C as ConcernsSection, E as ExpertiseSection, S as SpacesSection, B as BeforeAfterSection, a as ContactSection, b as CtaBannerSection, F as FaqSection, c as FooterSection, H as HeroSection, d as HoursSection, P as PricingSection, e as ProcessSection, R as RichTextSection, f as ServicesSection, T as TeamSection, g as TestimonialsSection, h as TrustSection, i as SiteConfig, j as SiteConfigInput } from './site-config-kqurIH-k.js';
2
+ export { k as BUILTIN_SECTION_SCHEMAS, l as BUILTIN_SECTION_TYPES, m as BeforeAfterSectionSchema, n as BlogListSection, o as BlogListSectionSchema, p as ConcernsSectionSchema, q as ContactSectionSchema, r as CtaBannerSectionSchema, s as CustomSection, t as CustomSectionSchema, u as ExpertiseSectionSchema, v as FaqSectionSchema, w as FooterSectionSchema, x as HeroSectionSchema, y as HoursSectionSchema, L as LeadFormSection, z as LeadFormSectionSchema, N as NavItem, A as NavItemSchema, D as PricingSectionSchema, G as ProcessSectionSchema, I as RichTextSectionSchema, J as SECTION_VARIANTS, K as Section, M as SectionCta, O as SectionInput, Q as SectionSchema, U as ServicesSectionSchema, V as SiteConfigSchema, W as SpacesSectionSchema, X as TeamSectionSchema, Y as TestimonialsSectionSchema, Z as TrustSectionSchema, _ as defineSiteConfig, $ as parseSiteConfig } from './site-config-kqurIH-k.js';
3
3
  export { R as RootTaleCssVars, t as themeToCssVars } from './theme-css-vars-CD3jcEkp.js';
4
4
  import { ReactElement, CSSProperties, ReactNode } from 'react';
5
5
  import 'zod';
6
6
  import '@roottale/cms-client/server';
7
7
  import '@roottale/ui-react/theme';
8
8
 
9
+ /**
10
+ * `SiteConfig` 계약 버전 (ADR-0086 D2 — 팩 ↔ renderer 호환성 계약).
11
+ *
12
+ * `site-config.ts` 는 이미 SIZE_OK 상한 근처(canonical SectionSchema
13
+ * registry)라 버전 해석 로직은 별도 모듈에 두고 `site-config.ts` 에서
14
+ * re-export 한다.
15
+ *
16
+ * 팩 manifest(`packages/assembly/src/pack-manifest.ts`)의
17
+ * `siteConfigVersion` 필드가 이 버전과 비교 대상 — 팩이 요구하는 버전이
18
+ * renderer 가 실제로 해석하는 버전보다 높으면 조립 전 fail-fast 해야 한다.
19
+ */
20
+ /** 현재 `SiteConfig` 스키마 버전. 계약 필드 추가 등 breaking 변경 시에만 올린다. */
21
+ declare const SITE_CONFIG_SCHEMA_VERSION = 1;
22
+ /**
23
+ * `config.schemaVersion` 해석 헬퍼 — 필드 미지정(기존 고객
24
+ * `site.config.ts`, `schemaVersion` 도입 이전 작성분)은 버전 1 로 해석한다
25
+ * (하위 호환, ADR-0086 D2). 현재 버전 상수가 아니라 리터럴 1 — 후일
26
+ * `SITE_CONFIG_SCHEMA_VERSION` 이 올라도 미지정 config 는 계속 1 이다.
27
+ */
28
+ declare function resolveSiteConfigSchemaVersion(config: {
29
+ schemaVersion?: number;
30
+ }): number;
31
+
9
32
  declare function TemplateExpertise(section: ExpertiseSection): ReactElement;
10
33
  declare function TemplateSpaces(section: SpacesSection): ReactElement;
11
34
  declare function TemplateConcerns(section: ConcernsSection): ReactElement;
@@ -37,7 +60,10 @@ declare function TemplateFaq(section: FaqSection): ReactElement;
37
60
  /** 빌트인 `cta-banner`. variant: default(기본) / photo-overlay. */
38
61
  declare function TemplateCtaBanner(section: CtaBannerSection): ReactElement;
39
62
  declare function TemplateTrust(section: TrustSection): ReactElement;
40
- /** 빌트인 `testimonials`(C11). variant: editorial-quote(기본) / quote-rail. */
63
+ /**
64
+ * 빌트인 `testimonials`(C11 + W8-7). variant: editorial-quote(기본) /
65
+ * quote-rail / star-compact.
66
+ */
41
67
  declare function TemplateTestimonials(section: TestimonialsSection): ReactElement;
42
68
  declare function TemplateTeam(section: TeamSection): ReactElement;
43
69
  declare function TemplateContact(section: ContactSection): ReactElement;
@@ -103,4 +129,4 @@ interface SiteRendererProps {
103
129
  */
104
130
  declare function SiteRenderer(props: SiteRendererProps): ReactElement;
105
131
 
106
- export { BeforeAfterSection, ConcernsSection, ContactSection, CtaBannerSection, ExpertiseSection, FaqSection, FooterSection, HeroSection, HoursSection, PricingSection, ProcessSection, RichTextSection, type SectionComponent, ServicesSection, SiteConfig, SiteConfigInput, SiteRenderer, type SiteRendererProps, SpacesSection, TeamSection, TemplateBeforeAfter, TemplateConcerns, TemplateContact, TemplateCtaBanner, TemplateExpertise, TemplateFaq, TemplateFooter, TemplateHero, TemplateHours, TemplatePricing, TemplateProcess, TemplateRichText, TemplateServices, TemplateSpaces, TemplateTeam, TemplateTestimonials, TemplateTrust, TestimonialsSection, TrustSection, sectionThemeStyle };
132
+ export { BeforeAfterSection, ConcernsSection, ContactSection, CtaBannerSection, ExpertiseSection, FaqSection, FooterSection, HeroSection, HoursSection, PricingSection, ProcessSection, RichTextSection, SITE_CONFIG_SCHEMA_VERSION, type SectionComponent, ServicesSection, SiteConfig, SiteConfigInput, SiteRenderer, type SiteRendererProps, SpacesSection, TeamSection, TemplateBeforeAfter, TemplateConcerns, TemplateContact, TemplateCtaBanner, TemplateExpertise, TemplateFaq, TemplateFooter, TemplateHero, TemplateHours, TemplatePricing, TemplateProcess, TemplateRichText, TemplateServices, TemplateSpaces, TemplateTeam, TemplateTestimonials, TemplateTrust, TestimonialsSection, TrustSection, resolveSiteConfigSchemaVersion, sectionThemeStyle };
package/dist/templates.js CHANGED
@@ -20,12 +20,14 @@ import {
20
20
  TemplateTrust,
21
21
  sectionThemeStyle,
22
22
  themeToCssVars
23
- } from "./chunk-FKDCPFYV.js";
23
+ } from "./chunk-45KYMMDY.js";
24
24
  import "./chunk-U5AKACPN.js";
25
- import "./chunk-4LQPZDXZ.js";
25
+ import "./chunk-LEHHSI6G.js";
26
+ import "./chunk-6MFHA555.js";
27
+ import "./chunk-FZK3EXYZ.js";
26
28
  import "./chunk-K62RR4UQ.js";
27
29
  import "./chunk-H46JUIHZ.js";
28
- import "./chunk-WWS43XCT.js";
30
+ import "./chunk-Z3ZSCPG4.js";
29
31
  import "./chunk-FUA5H6JE.js";
30
32
  import {
31
33
  BUILTIN_SECTION_SCHEMAS,
@@ -47,6 +49,7 @@ import {
47
49
  ProcessSectionSchema,
48
50
  RichTextSectionSchema,
49
51
  SECTION_VARIANTS,
52
+ SITE_CONFIG_SCHEMA_VERSION,
50
53
  SectionSchema,
51
54
  ServicesSectionSchema,
52
55
  SiteConfigSchema,
@@ -55,9 +58,11 @@ import {
55
58
  TestimonialsSectionSchema,
56
59
  TrustSectionSchema,
57
60
  defineSiteConfig,
58
- parseSiteConfig
59
- } from "./chunk-NUUTTSGT.js";
60
- import "./chunk-F3G6RVIR.js";
61
+ parseSiteConfig,
62
+ resolveSiteConfigSchemaVersion
63
+ } from "./chunk-GUV5WHD5.js";
64
+ import "./chunk-OMXEU4LS.js";
65
+ import "./chunk-YLSQAT6B.js";
61
66
 
62
67
  // src/templates/SiteRenderer.tsx
63
68
  import {
@@ -243,6 +248,7 @@ export {
243
248
  ProcessSectionSchema,
244
249
  RichTextSectionSchema,
245
250
  SECTION_VARIANTS,
251
+ SITE_CONFIG_SCHEMA_VERSION,
246
252
  SectionSchema,
247
253
  ServicesSectionSchema,
248
254
  SiteConfigSchema,
@@ -270,6 +276,7 @@ export {
270
276
  TrustSectionSchema,
271
277
  defineSiteConfig,
272
278
  parseSiteConfig,
279
+ resolveSiteConfigSchemaVersion,
273
280
  sectionThemeStyle,
274
281
  themeToCssVars
275
282
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/templates/SiteRenderer.tsx"],"sourcesContent":["/**\n * `SiteRenderer` — `SiteConfig.pages[path]` 섹션 배열을 registry 로 렌더.\n *\n * ADR-0040 P2 + P0.5 RFC §①:\n * - 빌트인 registry + 클라 주입 `customSections` 머지. **빌트인이 우선** —\n * 클라가 빌트인 type 을 가로채 계약이 갈라지는 것 방지 (고유 섹션은 고유\n * type 으로).\n * - **render-prop 금지**: 임의 JSX escape hatch 는 customSections registry 로만.\n * - **admin preview parity**: admin draft/preview 도 동일 `SiteRenderer` + 동일\n * 빌트인 registry 를 탄다. customSections 는 클라 repo 자산 — preview 에선\n * 미주입 → `unknownSection: \"placeholder\"` 로 skeleton 표시.\n * - cms-bound 섹션(blog-list/lead-form)은 `RootTaleBlogList`/`RootTaleLeadForm`\n * 위임 (단방향: templates → renderer 코어).\n */\nimport {\n Fragment,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n} from \"react\";\n\nimport { RootTaleLeadForm } from \"../LeadForm.js\";\nimport { RootTaleBlogList } from \"../server.js\";\nimport {\n TemplateConcerns,\n TemplateExpertise,\n TemplateSpaces,\n} from \"./professional-sections.js\";\nimport {\n TemplateBeforeAfter,\n TemplateContact,\n TemplateCtaBanner,\n TemplateFaq,\n TemplateFooter,\n TemplateHero,\n TemplateHours,\n TemplatePricing,\n TemplateProcess,\n TemplateRichText,\n TemplateServices,\n TemplateTeam,\n TemplateTestimonials,\n TemplateTrust,\n sectionThemeStyle,\n} from \"./sections.js\";\nimport {\n type BeforeAfterSection,\n type BlogListSection,\n type ConcernsSection,\n type ContactSection,\n type CtaBannerSection,\n type ExpertiseSection,\n type FaqSection,\n type FooterSection,\n type HeroSection,\n type HoursSection,\n type LeadFormSection,\n type PricingSection,\n type ProcessSection,\n type RichTextSection,\n type Section,\n type ServicesSection,\n type SpacesSection,\n type SiteConfig,\n type SiteConfigInput,\n type TeamSection,\n type TestimonialsSection,\n type TrustSection,\n parseSiteConfig,\n} from \"./site-config.js\";\n\n/** customSections registry 컴포넌트 계약 (RFC §①). */\nexport type SectionComponent = (p: {\n id?: string;\n props: Record<string, unknown>;\n variant?: string;\n theme?: Record<string, string>;\n}) => ReactNode;\n\nexport interface SiteRendererProps {\n /** `defineSiteConfig()` 결과 또는 raw config (내부에서 검증). */\n config: SiteConfig | SiteConfigInput;\n /** 현재 페이지 경로 — `config.pages` 의 key (\"/\", \"/services\", …). */\n path: string;\n /** cms-bound 섹션(blog-list/lead-form 의 theme fetch) 위임용. 서버 전용. */\n apiKey: string;\n /** self-host 등 API base override (`ROOTTALE_API_BASE`). */\n baseUrl?: string;\n /** 클라 repo 고유 섹션 registry (escape hatch). 빌트인 type 은 가로챌 수 없음. */\n customSections?: Record<string, SectionComponent>;\n /**\n * registry 에 없는 type 처리. 기본 \"warn-dev\" = dev 에서 경고 박스, prod 에서\n * 생략. admin preview 는 \"placeholder\" (customSections 미주입 환경).\n */\n unknownSection?: \"warn-dev\" | \"placeholder\" | \"ignore\";\n}\n\nfunction isDev(): boolean {\n // dts 빌드는 node types 없이 돈다 — process 는 globalThis 경유로 안전 접근.\n const env =\n (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n return env.NODE_ENV !== \"production\";\n}\n\nfunction UnknownSection({\n type,\n mode,\n}: {\n type: string;\n mode: \"warn-dev\" | \"placeholder\" | \"ignore\";\n}): ReactElement | null {\n if (mode === \"ignore\") return null;\n if (mode === \"placeholder\") {\n return (\n <section\n data-roottale-tpl=\"placeholder\"\n className=\"rt-tpl-section rt-tpl-placeholder\"\n aria-hidden\n >\n <div className=\"rt-tpl-container\">\n <div className=\"rt-tpl-placeholder-box\">{type}</div>\n </div>\n </section>\n );\n }\n if (!isDev()) return null;\n const style: CSSProperties = {\n padding: \"1rem\",\n border: \"1px dashed #c00\",\n color: \"#c00\",\n };\n return (\n <div style={style}>\n 알 수 없는 섹션 type: <code>{type}</code> — SiteRenderer 의 customSections\n 에 등록하세요.\n </div>\n );\n}\n\nfunction renderBuiltin(\n section: Section,\n ctx: { apiKey: string; baseUrl?: string },\n): ReactNode | undefined {\n switch (section.type) {\n case \"hero\":\n return <TemplateHero {...(section as HeroSection)} />;\n case \"rich-text\":\n return <TemplateRichText {...(section as RichTextSection)} />;\n case \"services\":\n return <TemplateServices {...(section as ServicesSection)} />;\n case \"faq\":\n return <TemplateFaq {...(section as FaqSection)} />;\n case \"footer\":\n return <TemplateFooter {...(section as FooterSection)} />;\n case \"cta-banner\":\n return <TemplateCtaBanner {...(section as CtaBannerSection)} />;\n case \"blog-list\": {\n const s = section as BlogListSection;\n const base = s.props.blogBasePath ?? \"/blog\";\n return (\n <section\n data-roottale-tpl=\"blog-list\"\n data-roottale-cms=\"list\"\n id={s.id}\n className=\"rt-tpl-section rt-tpl-blog-list\"\n style={sectionThemeStyle(s.theme)}\n >\n <div className=\"rt-tpl-container\">\n {s.props.heading ? (\n <h2 className=\"rt-tpl-heading\">{s.props.heading}</h2>\n ) : null}\n <RootTaleBlogList\n apiKey={ctx.apiKey}\n baseUrl={ctx.baseUrl}\n limit={s.props.limit}\n emptyMessage={s.props.emptyMessage}\n postHref={(post) => `${base}/${post.slug}`}\n />\n </div>\n </section>\n );\n }\n case \"lead-form\": {\n const s = section as LeadFormSection;\n return (\n <section\n data-roottale-tpl=\"lead-form\"\n data-roottale-cms=\"lead-form\"\n id={s.id}\n className=\"rt-tpl-section rt-tpl-lead-form\"\n style={sectionThemeStyle(s.theme)}\n >\n <div className=\"rt-tpl-container\">\n <RootTaleLeadForm\n action={s.props.action}\n vertical={s.props.vertical}\n heading={s.props.heading}\n description={s.props.description}\n submitLabel={s.props.submitLabel}\n />\n </div>\n </section>\n );\n }\n case \"trust\":\n return <TemplateTrust {...(section as TrustSection)} />;\n case \"contact\":\n return <TemplateContact {...(section as ContactSection)} />;\n case \"hours\":\n return <TemplateHours {...(section as HoursSection)} />;\n case \"pricing\":\n return <TemplatePricing {...(section as PricingSection)} />;\n case \"process\":\n return <TemplateProcess {...(section as ProcessSection)} />;\n case \"before-after\":\n return <TemplateBeforeAfter {...(section as BeforeAfterSection)} />;\n case \"testimonials\":\n return <TemplateTestimonials {...(section as TestimonialsSection)} />;\n case \"expertise\":\n return <TemplateExpertise {...(section as ExpertiseSection)} />;\n case \"spaces\":\n return <TemplateSpaces {...(section as SpacesSection)} />;\n case \"concerns\":\n return <TemplateConcerns {...(section as ConcernsSection)} />;\n case \"team\":\n return <TemplateTeam {...(section as TeamSection)} />;\n default:\n return undefined;\n }\n}\n\n/**\n * 한 페이지의 섹션 배열을 순서대로 렌더.\n *\n * ```tsx\n * // app/services/page.tsx\n * import config from \"@/site.config\";\n * import { SiteRenderer } from \"@roottale/cms-renderer-next/templates\";\n *\n * export default function ServicesPage() {\n * return (\n * <SiteRenderer config={config} path=\"/services\"\n * apiKey={process.env.ROOTTALE_API_KEY!} />\n * );\n * }\n * ```\n */\nexport function SiteRenderer(props: SiteRendererProps): ReactElement {\n const {\n path,\n apiKey,\n baseUrl,\n customSections = {},\n unknownSection = \"warn-dev\",\n } = props;\n const config = parseSiteConfig(props.config);\n const sections = config.pages[path] ?? [];\n return (\n <>\n {sections.length === 0 ? (\n <UnknownSection\n type={`pages[\"${path}\"] (섹션 0개)`}\n mode={unknownSection}\n />\n ) : null}\n {sections.map((section, i) => {\n const key = section.id ?? `${section.type}-${i}`;\n const builtin = renderBuiltin(section, { apiKey, baseUrl });\n if (builtin !== undefined) {\n return <Fragment key={key}>{builtin}</Fragment>;\n }\n const Custom = customSections[section.type];\n if (Custom) {\n return (\n <Fragment key={key}>\n {Custom({\n id: section.id,\n props: section.props as Record<string, unknown>,\n variant: section.variant,\n theme: section.theme,\n })}\n </Fragment>\n );\n }\n return (\n <UnknownSection key={key} type={section.type} mode={unknownSection} />\n );\n })}\n </>\n );\n}\n"],"mappings":"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","names":["Fragment"]}
1
+ {"version":3,"sources":["../src/templates/SiteRenderer.tsx"],"sourcesContent":["/**\n * `SiteRenderer` — `SiteConfig.pages[path]` 섹션 배열을 registry 로 렌더.\n *\n * ADR-0040 P2 + P0.5 RFC §①:\n * - 빌트인 registry + 클라 주입 `customSections` 머지. **빌트인이 우선** —\n * 클라가 빌트인 type 을 가로채 계약이 갈라지는 것 방지 (고유 섹션은 고유\n * type 으로).\n * - **render-prop 금지**: 임의 JSX escape hatch 는 customSections registry 로만.\n * - **admin preview parity**: admin draft/preview 도 동일 `SiteRenderer` + 동일\n * 빌트인 registry 를 탄다. customSections 는 클라 repo 자산 — preview 에선\n * 미주입 → `unknownSection: \"placeholder\"` 로 skeleton 표시.\n * - cms-bound 섹션(blog-list/lead-form)은 `RootTaleBlogList`/`RootTaleLeadForm`\n * 위임 (단방향: templates → renderer 코어).\n */\nimport {\n Fragment,\n type CSSProperties,\n type ReactElement,\n type ReactNode,\n} from \"react\";\n\nimport { RootTaleLeadForm } from \"../LeadForm.js\";\nimport { RootTaleBlogList } from \"../server.js\";\nimport {\n TemplateConcerns,\n TemplateExpertise,\n TemplateSpaces,\n} from \"./professional-sections.js\";\nimport {\n TemplateBeforeAfter,\n TemplateContact,\n TemplateCtaBanner,\n TemplateFaq,\n TemplateFooter,\n TemplateHero,\n TemplateHours,\n TemplatePricing,\n TemplateProcess,\n TemplateRichText,\n TemplateServices,\n TemplateTeam,\n TemplateTestimonials,\n TemplateTrust,\n sectionThemeStyle,\n} from \"./sections.js\";\nimport {\n type BeforeAfterSection,\n type BlogListSection,\n type ConcernsSection,\n type ContactSection,\n type CtaBannerSection,\n type ExpertiseSection,\n type FaqSection,\n type FooterSection,\n type HeroSection,\n type HoursSection,\n type LeadFormSection,\n type PricingSection,\n type ProcessSection,\n type RichTextSection,\n type Section,\n type ServicesSection,\n type SpacesSection,\n type SiteConfig,\n type SiteConfigInput,\n type TeamSection,\n type TestimonialsSection,\n type TrustSection,\n parseSiteConfig,\n} from \"./site-config.js\";\n\n/** customSections registry 컴포넌트 계약 (RFC §①). */\nexport type SectionComponent = (p: {\n id?: string;\n props: Record<string, unknown>;\n variant?: string;\n theme?: Record<string, string>;\n}) => ReactNode;\n\nexport interface SiteRendererProps {\n /** `defineSiteConfig()` 결과 또는 raw config (내부에서 검증). */\n config: SiteConfig | SiteConfigInput;\n /** 현재 페이지 경로 — `config.pages` 의 key (\"/\", \"/services\", …). */\n path: string;\n /** cms-bound 섹션(blog-list/lead-form 의 theme fetch) 위임용. 서버 전용. */\n apiKey: string;\n /** self-host 등 API base override (`ROOTTALE_API_BASE`). */\n baseUrl?: string;\n /** 클라 repo 고유 섹션 registry (escape hatch). 빌트인 type 은 가로챌 수 없음. */\n customSections?: Record<string, SectionComponent>;\n /**\n * registry 에 없는 type 처리. 기본 \"warn-dev\" = dev 에서 경고 박스, prod 에서\n * 생략. admin preview 는 \"placeholder\" (customSections 미주입 환경).\n */\n unknownSection?: \"warn-dev\" | \"placeholder\" | \"ignore\";\n}\n\nfunction isDev(): boolean {\n // dts 빌드는 node types 없이 돈다 — process 는 globalThis 경유로 안전 접근.\n const env =\n (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env ?? {};\n return env.NODE_ENV !== \"production\";\n}\n\nfunction UnknownSection({\n type,\n mode,\n}: {\n type: string;\n mode: \"warn-dev\" | \"placeholder\" | \"ignore\";\n}): ReactElement | null {\n if (mode === \"ignore\") return null;\n if (mode === \"placeholder\") {\n return (\n <section\n data-roottale-tpl=\"placeholder\"\n className=\"rt-tpl-section rt-tpl-placeholder\"\n aria-hidden\n >\n <div className=\"rt-tpl-container\">\n <div className=\"rt-tpl-placeholder-box\">{type}</div>\n </div>\n </section>\n );\n }\n if (!isDev()) return null;\n const style: CSSProperties = {\n padding: \"1rem\",\n border: \"1px dashed #c00\",\n color: \"#c00\",\n };\n return (\n <div style={style}>\n 알 수 없는 섹션 type: <code>{type}</code> — SiteRenderer 의 customSections\n 에 등록하세요.\n </div>\n );\n}\n\nfunction renderBuiltin(\n section: Section,\n ctx: { apiKey: string; baseUrl?: string },\n): ReactNode | undefined {\n switch (section.type) {\n case \"hero\":\n return <TemplateHero {...(section as HeroSection)} />;\n case \"rich-text\":\n return <TemplateRichText {...(section as RichTextSection)} />;\n case \"services\":\n return <TemplateServices {...(section as ServicesSection)} />;\n case \"faq\":\n return <TemplateFaq {...(section as FaqSection)} />;\n case \"footer\":\n return <TemplateFooter {...(section as FooterSection)} />;\n case \"cta-banner\":\n return <TemplateCtaBanner {...(section as CtaBannerSection)} />;\n case \"blog-list\": {\n const s = section as BlogListSection;\n const base = s.props.blogBasePath ?? \"/blog\";\n return (\n <section\n data-roottale-tpl=\"blog-list\"\n data-roottale-cms=\"list\"\n id={s.id}\n className=\"rt-tpl-section rt-tpl-blog-list\"\n style={sectionThemeStyle(s.theme)}\n >\n <div className=\"rt-tpl-container\">\n {s.props.heading ? (\n <h2 className=\"rt-tpl-heading\">{s.props.heading}</h2>\n ) : null}\n <RootTaleBlogList\n apiKey={ctx.apiKey}\n baseUrl={ctx.baseUrl}\n limit={s.props.limit}\n emptyMessage={s.props.emptyMessage}\n postHref={(post) => `${base}/${post.slug}`}\n />\n </div>\n </section>\n );\n }\n case \"lead-form\": {\n const s = section as LeadFormSection;\n return (\n <section\n data-roottale-tpl=\"lead-form\"\n data-roottale-cms=\"lead-form\"\n id={s.id}\n className=\"rt-tpl-section rt-tpl-lead-form\"\n style={sectionThemeStyle(s.theme)}\n >\n <div className=\"rt-tpl-container\">\n <RootTaleLeadForm\n action={s.props.action}\n vertical={s.props.vertical}\n heading={s.props.heading}\n description={s.props.description}\n submitLabel={s.props.submitLabel}\n />\n </div>\n </section>\n );\n }\n case \"trust\":\n return <TemplateTrust {...(section as TrustSection)} />;\n case \"contact\":\n return <TemplateContact {...(section as ContactSection)} />;\n case \"hours\":\n return <TemplateHours {...(section as HoursSection)} />;\n case \"pricing\":\n return <TemplatePricing {...(section as PricingSection)} />;\n case \"process\":\n return <TemplateProcess {...(section as ProcessSection)} />;\n case \"before-after\":\n return <TemplateBeforeAfter {...(section as BeforeAfterSection)} />;\n case \"testimonials\":\n return <TemplateTestimonials {...(section as TestimonialsSection)} />;\n case \"expertise\":\n return <TemplateExpertise {...(section as ExpertiseSection)} />;\n case \"spaces\":\n return <TemplateSpaces {...(section as SpacesSection)} />;\n case \"concerns\":\n return <TemplateConcerns {...(section as ConcernsSection)} />;\n case \"team\":\n return <TemplateTeam {...(section as TeamSection)} />;\n default:\n return undefined;\n }\n}\n\n/**\n * 한 페이지의 섹션 배열을 순서대로 렌더.\n *\n * ```tsx\n * // app/services/page.tsx\n * import config from \"@/site.config\";\n * import { SiteRenderer } from \"@roottale/cms-renderer-next/templates\";\n *\n * export default function ServicesPage() {\n * return (\n * <SiteRenderer config={config} path=\"/services\"\n * apiKey={process.env.ROOTTALE_API_KEY!} />\n * );\n * }\n * ```\n */\nexport function SiteRenderer(props: SiteRendererProps): ReactElement {\n const {\n path,\n apiKey,\n baseUrl,\n customSections = {},\n unknownSection = \"warn-dev\",\n } = props;\n const config = parseSiteConfig(props.config);\n const sections = config.pages[path] ?? [];\n return (\n <>\n {sections.length === 0 ? (\n <UnknownSection\n type={`pages[\"${path}\"] (섹션 0개)`}\n mode={unknownSection}\n />\n ) : null}\n {sections.map((section, i) => {\n const key = section.id ?? `${section.type}-${i}`;\n const builtin = renderBuiltin(section, { apiKey, baseUrl });\n if (builtin !== undefined) {\n return <Fragment key={key}>{builtin}</Fragment>;\n }\n const Custom = customSections[section.type];\n if (Custom) {\n return (\n <Fragment key={key}>\n {Custom({\n id: section.id,\n props: section.props as Record<string, unknown>,\n variant: section.variant,\n theme: section.theme,\n })}\n </Fragment>\n );\n }\n return (\n <UnknownSection key={key} type={section.type} mode={unknownSection} />\n );\n })}\n </>\n );\n}\n"],"mappings":"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","names":["Fragment"]}
@@ -1,5 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, CSSProperties } from 'react';
3
+ export { CountUpStat } from '@roottale/ui-react/count-up-stat';
3
4
 
4
5
  /**
5
6
  * 공유 모션 variants — C5(생성물 손 복사 금지)의 정신을 따라 모든 `scroll`/`motion`
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
  import {
3
3
  AnimateOnScroll,
4
+ CountUpStat,
4
5
  Marquee,
5
6
  StaggerGroup,
6
7
  StaggerItem
7
- } from "./chunk-WWS43XCT.js";
8
+ } from "./chunk-Z3ZSCPG4.js";
8
9
  export {
9
10
  AnimateOnScroll,
11
+ CountUpStat,
10
12
  Marquee,
11
13
  StaggerGroup,
12
14
  StaggerItem
package/dist/validate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SectionSchema
3
- } from "./chunk-NUUTTSGT.js";
3
+ } from "./chunk-GUV5WHD5.js";
4
4
  import {
5
5
  runReadabilityChecks,
6
6
  runSeoChecks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-renderer-next",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "type": "module",
5
5
  "description": "RootTale CMS public-render React/Next.js Server Components. SSR-only RSC components (RootTaleBlogList / RootTaleBlogPost / RootTaleLeadForm) for external customer sites.",
6
6
  "main": "./dist/index.js",
@@ -54,8 +54,8 @@
54
54
  "embla-carousel-react": "^8.6.0",
55
55
  "motion": "^12.24.12",
56
56
  "zod": "^3.23.8",
57
- "@roottale/cms-client": "^0.43.0",
58
- "@roottale/cms-core": "^0.43.0"
57
+ "@roottale/cms-client": "^0.45.0",
58
+ "@roottale/cms-core": "^0.45.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "react": "^19.0.0"
@@ -72,7 +72,7 @@
72
72
  "@roottale/audit-medical": "^0.0.1",
73
73
  "@roottale/audit-tax": "^0.0.1",
74
74
  "@roottale/cms-i18n": "^0.2.0",
75
- "@roottale/ui-react": "^0.3.0"
75
+ "@roottale/ui-react": "^0.4.0"
76
76
  },
77
77
  "publishConfig": {
78
78
  "access": "public"
@@ -1 +0,0 @@
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\";\n\ntype ExpertiseItem = ExpertiseSection[\"props\"][\"items\"][number];\n\nexport function ExpertiseTabStage({\n items,\n}: {\n readonly items: readonly ExpertiseItem[];\n}) {\n const [selectedId, setSelectedId] = useState(items[0]?.id ?? \"\");\n const selected = items.find((item) => item.id === selectedId) ?? items[0];\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 >\n {items.map((item) => {\n const active = item.id === selected.id;\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 onClick={() => setSelectedId(item.id)}\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 >\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;AA0Bb,SAWE,KAXF;AApBL,SAAS,kBAAkB;AAAA,EAChC;AACF,GAEG;AACD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE;AAC/D,QAAM,WAAW,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,UAAU,KAAK,MAAM,CAAC;AAExE,MAAI,CAAC,SAAU,QAAO;AAEtB,SACE,qBAAC,SAAI,WAAU,0BACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAW;AAAA,QAEV,gBAAM,IAAI,CAAC,SAAS;AACnB,gBAAM,SAAS,KAAK,OAAO,SAAS;AACpC,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,SAAS,MAAM,cAAc,KAAK,EAAE;AAAA,cAEpC;AAAA,oCAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA,gBACzD,oBAAC,UAAK,WAAU,8BAA8B,eAAK,OAAM;AAAA;AAAA;AAAA,YAXpD,KAAK;AAAA,UAYZ;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,QAE/C;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":[]}
@@ -1,220 +0,0 @@
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 { Fragment, 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
- scrim
33
- }) {
34
- const reducedMotion = usePrefersReducedMotion();
35
- const tabVisible = useDocumentVisible();
36
- const [emblaRef, emblaApi] = useEmblaCarousel({
37
- loop: true,
38
- duration: reducedMotion ? 1 : 20
39
- });
40
- const [selectedIndex, setSelectedIndex] = useState(0);
41
- const rootRef = useRef(null);
42
- const onSelect = useCallback(() => {
43
- if (!emblaApi) return;
44
- setSelectedIndex(emblaApi.selectedScrollSnap());
45
- }, [emblaApi]);
46
- useEffect(() => {
47
- if (!emblaApi) return;
48
- onSelect();
49
- emblaApi.on("select", onSelect);
50
- emblaApi.on("reInit", onSelect);
51
- return () => {
52
- emblaApi.off("select", onSelect);
53
- emblaApi.off("reInit", onSelect);
54
- };
55
- }, [emblaApi, onSelect]);
56
- const shouldAutoplay = autoplay && !reducedMotion && tabVisible;
57
- useEffect(() => {
58
- if (!emblaApi || !shouldAutoplay) return;
59
- const id = window.setInterval(() => {
60
- emblaApi.scrollNext();
61
- }, intervalMs);
62
- return () => window.clearInterval(id);
63
- }, [emblaApi, shouldAutoplay, intervalMs]);
64
- const scrollPrev = useCallback(() => {
65
- emblaApi?.scrollPrev(reducedMotion);
66
- }, [emblaApi, reducedMotion]);
67
- const scrollNext = useCallback(() => {
68
- emblaApi?.scrollNext(reducedMotion);
69
- }, [emblaApi, reducedMotion]);
70
- const scrollTo = useCallback(
71
- (index) => {
72
- emblaApi?.scrollTo(index, reducedMotion);
73
- },
74
- [emblaApi, reducedMotion]
75
- );
76
- const onKeyDown = useCallback(
77
- (event) => {
78
- if (event.key === "ArrowLeft") {
79
- event.preventDefault();
80
- scrollPrev();
81
- } else if (event.key === "ArrowRight") {
82
- event.preventDefault();
83
- scrollNext();
84
- }
85
- },
86
- [scrollPrev, scrollNext]
87
- );
88
- const selectedSlide = slides[selectedIndex];
89
- const hasSlidePanel = Boolean(
90
- selectedSlide && (selectedSlide.eyebrow || selectedSlide.title || selectedSlide.subtitle || selectedSlide.description || selectedSlide.primaryCta || selectedSlide.secondaryCta || selectedSlide.metrics && selectedSlide.metrics.length > 0)
91
- );
92
- const hasTabLabels = slides.some((slide) => slide.tabLabel || slide.tabMeta);
93
- return /* @__PURE__ */ jsxs(
94
- "div",
95
- {
96
- ref: rootRef,
97
- className: "rt-tpl-hero-carousel",
98
- role: "region",
99
- "aria-roledescription": "carousel",
100
- "aria-label": "\uBC30\uACBD \uC774\uBBF8\uC9C0 \uCE90\uB7EC\uC140",
101
- tabIndex: 0,
102
- onKeyDown,
103
- "data-reduced-motion": reducedMotion ? "true" : "false",
104
- "data-scrim": scrim ?? "soft",
105
- children: [
106
- /* @__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(
107
- "div",
108
- {
109
- className: "rt-tpl-hero-carousel-slide",
110
- "aria-roledescription": "slide",
111
- "aria-label": `Slide ${index + 1} of ${slides.length}`,
112
- "aria-hidden": index === selectedIndex ? void 0 : true,
113
- children: /* @__PURE__ */ jsx(
114
- "img",
115
- {
116
- className: "rt-tpl-hero-carousel-img",
117
- src: slide.image,
118
- alt: slide.alt,
119
- "data-active": index === selectedIndex ? "true" : "false",
120
- loading: index === 0 ? "eager" : "lazy"
121
- }
122
- )
123
- },
124
- `${slide.image}-${index}`
125
- )) }) }),
126
- /* @__PURE__ */ jsx(
127
- "button",
128
- {
129
- type: "button",
130
- className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--prev",
131
- "aria-label": "\uC774\uC804 \uC2AC\uB77C\uC774\uB4DC",
132
- onClick: scrollPrev,
133
- 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" }) })
134
- }
135
- ),
136
- /* @__PURE__ */ jsx(
137
- "button",
138
- {
139
- type: "button",
140
- className: "rt-tpl-hero-carousel-arrow rt-tpl-hero-carousel-arrow--next",
141
- "aria-label": "\uB2E4\uC74C \uC2AC\uB77C\uC774\uB4DC",
142
- onClick: scrollNext,
143
- 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" }) })
144
- }
145
- ),
146
- hasSlidePanel && selectedSlide ? /* @__PURE__ */ jsxs("div", { className: "rt-tpl-hero-carousel-slide-panel", children: [
147
- selectedSlide.eyebrow ? /* @__PURE__ */ jsx("p", { className: "rt-tpl-hero-carousel-slide-eyebrow", children: selectedSlide.eyebrow }) : null,
148
- selectedSlide.title ? /* @__PURE__ */ jsx("p", { className: "rt-tpl-hero-carousel-slide-title", children: selectedSlide.title }) : null,
149
- selectedSlide.subtitle ? /* @__PURE__ */ jsx("p", { className: "rt-tpl-hero-carousel-slide-subtitle", children: selectedSlide.subtitle }) : null,
150
- selectedSlide.description ? /* @__PURE__ */ jsx("p", { className: "rt-tpl-hero-carousel-slide-desc", children: selectedSlide.description }) : null,
151
- selectedSlide.metrics && selectedSlide.metrics.length > 0 ? /* @__PURE__ */ jsx("dl", { className: "rt-tpl-hero-carousel-slide-metrics", children: selectedSlide.metrics.map((metric) => /* @__PURE__ */ jsxs("div", { children: [
152
- /* @__PURE__ */ jsx("dt", { children: metric.label }),
153
- /* @__PURE__ */ jsxs("dd", { children: [
154
- metric.value,
155
- metric.unit ? /* @__PURE__ */ jsx("span", { children: metric.unit }) : null
156
- ] }),
157
- metric.source ? /* @__PURE__ */ jsx("dd", { children: metric.source }) : null
158
- ] }, `${metric.value}-${metric.label}`)) }) : null,
159
- selectedSlide.primaryCta || selectedSlide.secondaryCta ? /* @__PURE__ */ jsxs("div", { className: "rt-tpl-hero-carousel-slide-actions", children: [
160
- selectedSlide.primaryCta ? /* @__PURE__ */ jsx(
161
- "a",
162
- {
163
- className: "rt-tpl-btn rt-tpl-btn--primary",
164
- href: selectedSlide.primaryCta.href,
165
- "data-track": "cta_hero",
166
- children: selectedSlide.primaryCta.label
167
- }
168
- ) : null,
169
- selectedSlide.secondaryCta ? /* @__PURE__ */ jsx(
170
- "a",
171
- {
172
- className: "rt-tpl-btn rt-tpl-btn--secondary",
173
- href: selectedSlide.secondaryCta.href,
174
- "data-track": "cta_hero",
175
- children: selectedSlide.secondaryCta.label
176
- }
177
- ) : null
178
- ] }) : null
179
- ] }) : null,
180
- /* @__PURE__ */ jsxs("div", { className: "rt-tpl-hero-carousel-controls", children: [
181
- /* @__PURE__ */ jsx(
182
- "div",
183
- {
184
- className: hasTabLabels ? "rt-tpl-hero-carousel-dots rt-tpl-hero-carousel-dots--tabs" : "rt-tpl-hero-carousel-dots",
185
- role: "tablist",
186
- "aria-label": "\uC2AC\uB77C\uC774\uB4DC \uC120\uD0DD",
187
- children: slides.map((_slide, index) => /* @__PURE__ */ jsx(
188
- "button",
189
- {
190
- type: "button",
191
- role: "tab",
192
- className: "rt-tpl-hero-carousel-dot",
193
- "aria-selected": index === selectedIndex,
194
- "aria-label": `${index + 1}\uBC88 \uC2AC\uB77C\uC774\uB4DC\uB85C \uC774\uB3D9`,
195
- "data-active": index === selectedIndex ? "true" : "false",
196
- onClick: () => scrollTo(index),
197
- children: hasTabLabels ? /* @__PURE__ */ jsxs(Fragment, { children: [
198
- /* @__PURE__ */ jsx("span", { children: _slide.tabLabel ?? `${index + 1}` }),
199
- _slide.tabMeta ? /* @__PURE__ */ jsx("small", { children: _slide.tabMeta }) : null
200
- ] }) : null
201
- },
202
- index
203
- ))
204
- }
205
- ),
206
- /* @__PURE__ */ jsxs("p", { className: "rt-tpl-hero-carousel-counter", "aria-hidden": "true", children: [
207
- /* @__PURE__ */ jsx("span", { className: "rt-tpl-hero-carousel-counter-current", children: selectedIndex + 1 }),
208
- "/",
209
- /* @__PURE__ */ jsx("span", { className: "rt-tpl-hero-carousel-counter-total", children: slides.length })
210
- ] })
211
- ] })
212
- ]
213
- }
214
- );
215
- }
216
-
217
- export {
218
- HeroCarouselViewport
219
- };
220
- //# sourceMappingURL=chunk-F3G6RVIR.js.map
@@ -1 +0,0 @@
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 readonly eyebrow?: string;\n readonly title?: string;\n readonly subtitle?: string;\n readonly description?: string;\n readonly primaryCta?: { readonly label: string; readonly href: string };\n readonly secondaryCta?: { readonly label: string; readonly href: string };\n readonly tabLabel?: string;\n readonly tabMeta?: string;\n readonly metrics?: readonly {\n readonly value: string;\n readonly unit?: string;\n readonly label: string;\n readonly source?: string;\n }[];\n}\n\nexport interface HeroCarouselViewportProps {\n readonly slides: readonly HeroCarouselSlideInput[];\n readonly autoplay: boolean;\n readonly intervalMs: number;\n readonly scrim?: \"none\" | \"soft\" | \"strong\";\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 scrim,\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 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\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 >\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 {hasSlidePanel && selectedSlide ? (\n <div className=\"rt-tpl-hero-carousel-slide-panel\">\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 >\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 {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 <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":";AAuBA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AACzD,OAAO,sBAAsB;AA2Jf,SAoHE,UApHF,KA4DI,YA5DJ;AA9Hd,SAAS,0BAAmC;AAC1C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,YAAU,MAAM;AACd,UAAM,QAAQ,OAAO,WAAW,kCAAkC;AAClE,eAAW,MAAM,OAAO;AACxB,UAAM,WAAW,CAAC,UAA+B,WAAW,MAAM,OAAO;AACzE,UAAM,iBAAiB,UAAU,QAAQ;AACzC,WAAO,MAAM,MAAM,oBAAoB,UAAU,QAAQ;AAAA,EAC3D,GAAG,CAAC,CAAC;AACL,SAAO;AACT;AAEA,SAAS,qBAA8B;AACrC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,YAAU,MAAM;AACd,UAAM,eAAe,MAAM,WAAW,SAAS,oBAAoB,SAAS;AAC5E,iBAAa;AACb,aAAS,iBAAiB,oBAAoB,YAAY;AAC1D,WAAO,MAAM,SAAS,oBAAoB,oBAAoB,YAAY;AAAA,EAC5E,GAAG,CAAC,CAAC;AACL,SAAO;AACT;AAEO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA8B;AAC5B,QAAM,gBAAgB,wBAAwB;AAC9C,QAAM,aAAa,mBAAmB;AACtC,QAAM,CAAC,UAAU,QAAQ,IAAI,iBAAiB;AAAA,IAC5C,MAAM;AAAA,IACN,UAAU,gBAAgB,IAAI;AAAA,EAChC,CAAC;AACD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,CAAC;AACpD,QAAM,UAAU,OAA8B,IAAI;AAElD,QAAM,WAAW,YAAY,MAAM;AACjC,QAAI,CAAC,SAAU;AACf,qBAAiB,SAAS,mBAAmB,CAAC;AAAA,EAChD,GAAG,CAAC,QAAQ,CAAC;AAEb,YAAU,MAAM;AACd,QAAI,CAAC,SAAU;AACf,aAAS;AACT,aAAS,GAAG,UAAU,QAAQ;AAC9B,aAAS,GAAG,UAAU,QAAQ;AAC9B,WAAO,MAAM;AACX,eAAS,IAAI,UAAU,QAAQ;AAC/B,eAAS,IAAI,UAAU,QAAQ;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,UAAU,QAAQ,CAAC;AAEvB,QAAM,iBAAiB,YAAY,CAAC,iBAAiB;AAErD,YAAU,MAAM;AACd,QAAI,CAAC,YAAY,CAAC,eAAgB;AAClC,UAAM,KAAK,OAAO,YAAY,MAAM;AAClC,eAAS,WAAW;AAAA,IACtB,GAAG,UAAU;AACb,WAAO,MAAM,OAAO,cAAc,EAAE;AAAA,EACtC,GAAG,CAAC,UAAU,gBAAgB,UAAU,CAAC;AAEzC,QAAM,aAAa,YAAY,MAAM;AACnC,cAAU,WAAW,aAAa;AAAA,EACpC,GAAG,CAAC,UAAU,aAAa,CAAC;AAE5B,QAAM,aAAa,YAAY,MAAM;AACnC,cAAU,WAAW,aAAa;AAAA,EACpC,GAAG,CAAC,UAAU,aAAa,CAAC;AAE5B,QAAM,WAAW;AAAA,IACf,CAAC,UAAkB;AACjB,gBAAU,SAAS,OAAO,aAAa;AAAA,IACzC;AAAA,IACA,CAAC,UAAU,aAAa;AAAA,EAC1B;AAEA,QAAM,YAAY;AAAA,IAChB,CAAC,UAA+C;AAC9C,UAAI,MAAM,QAAQ,aAAa;AAC7B,cAAM,eAAe;AACrB,mBAAW;AAAA,MACb,WAAW,MAAM,QAAQ,cAAc;AACrC,cAAM,eAAe;AACrB,mBAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,CAAC,YAAY,UAAU;AAAA,EACzB;AACA,QAAM,gBAAgB,OAAO,aAAa;AAC1C,QAAM,gBAAgB;AAAA,IACpB,kBACG,cAAc,WACb,cAAc,SACd,cAAc,YACd,cAAc,eACd,cAAc,cACd,cAAc,gBACb,cAAc,WAAW,cAAc,QAAQ,SAAS;AAAA,EAC/D;AACA,QAAM,eAAe,OAAO,KAAK,CAAC,UAAU,MAAM,YAAY,MAAM,OAAO;AAE3E,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAU;AAAA,MACV,MAAK;AAAA,MACL,wBAAqB;AAAA,MACrB,cAAW;AAAA,MACX,UAAU;AAAA,MACV;AAAA,MACA,uBAAqB,gBAAgB,SAAS;AAAA,MAC9C,cAAY,SAAS;AAAA,MAErB;AAAA,4BAAC,SAAI,WAAU,iCAAgC,KAAK,UAClD,8BAAC,SAAI,WAAU,8BACZ,iBAAO,IAAI,CAAC,OAAO,UAClB;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YACV,wBAAqB;AAAA,YACrB,cAAY,SAAS,QAAQ,CAAC,OAAO,OAAO,MAAM;AAAA,YAClD,eAAa,UAAU,gBAAgB,SAAY;AAAA,YAEnD;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,KAAK,MAAM;AAAA,gBACX,KAAK,MAAM;AAAA,gBACX,eAAa,UAAU,gBAAgB,SAAS;AAAA,gBAChD,SAAS,UAAU,IAAI,UAAU;AAAA;AAAA,YACnC;AAAA;AAAA,UAZK,GAAG,MAAM,KAAK,IAAI,KAAK;AAAA,QAa9B,CACD,GACH,GACF;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,cAAW;AAAA,YACX,SAAS;AAAA,YAET,8BAAC,SAAI,SAAQ,aAAY,eAAY,QAAO,WAAU,SACpD,8BAAC,UAAK,GAAE,kBAAiB,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,GAC1H;AAAA;AAAA,QACF;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,cAAW;AAAA,YACX,SAAS;AAAA,YAET,8BAAC,SAAI,SAAQ,aAAY,eAAY,QAAO,WAAU,SACpD,8BAAC,UAAK,GAAE,gBAAe,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,GACxH;AAAA;AAAA,QACF;AAAA,QAEC,iBAAiB,gBAChB,qBAAC,SAAI,WAAU,oCACZ;AAAA,wBAAc,UACb,oBAAC,OAAE,WAAU,sCACV,wBAAc,SACjB,IACE;AAAA,UACH,cAAc,QACb,oBAAC,OAAE,WAAU,oCACV,wBAAc,OACjB,IACE;AAAA,UACH,cAAc,WACb,oBAAC,OAAE,WAAU,uCACV,wBAAc,UACjB,IACE;AAAA,UACH,cAAc,cACb,oBAAC,OAAE,WAAU,mCACV,wBAAc,aACjB,IACE;AAAA,UACH,cAAc,WAAW,cAAc,QAAQ,SAAS,IACvD,oBAAC,QAAG,WAAU,sCACX,wBAAc,QAAQ,IAAI,CAAC,WAC1B,qBAAC,SACC;AAAA,gCAAC,QAAI,iBAAO,OAAM;AAAA,YAClB,qBAAC,QACE;AAAA,qBAAO;AAAA,cACP,OAAO,OAAO,oBAAC,UAAM,iBAAO,MAAK,IAAU;AAAA,eAC9C;AAAA,YACC,OAAO,SAAS,oBAAC,QAAI,iBAAO,QAAO,IAAQ;AAAA,eANpC,GAAG,OAAO,KAAK,IAAI,OAAO,KAAK,EAOzC,CACD,GACH,IACE;AAAA,UACH,cAAc,cAAc,cAAc,eACzC,qBAAC,SAAI,WAAU,sCACZ;AAAA,0BAAc,aACb;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,MAAM,cAAc,WAAW;AAAA,gBAC/B,cAAW;AAAA,gBAEV,wBAAc,WAAW;AAAA;AAAA,YAC5B,IACE;AAAA,YACH,cAAc,eACb;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,MAAM,cAAc,aAAa;AAAA,gBACjC,cAAW;AAAA,gBAEV,wBAAc,aAAa;AAAA;AAAA,YAC9B,IACE;AAAA,aACN,IACE;AAAA,WACN,IACE;AAAA,QAEJ,qBAAC,SAAI,WAAU,iCACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WACE,eACI,8DACA;AAAA,cAEN,MAAK;AAAA,cACL,cAAW;AAAA,cAEV,iBAAO,IAAI,CAAC,QAAQ,UACnB;AAAA,gBAAC;AAAA;AAAA,kBAEC,MAAK;AAAA,kBACL,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,iBAAe,UAAU;AAAA,kBACzB,cAAY,GAAG,QAAQ,CAAC;AAAA,kBACxB,eAAa,UAAU,gBAAgB,SAAS;AAAA,kBAChD,SAAS,MAAM,SAAS,KAAK;AAAA,kBAE5B,yBACC,iCACE;AAAA,wCAAC,UAAM,iBAAO,YAAY,GAAG,QAAQ,CAAC,IAAG;AAAA,oBACxC,OAAO,UAAU,oBAAC,WAAO,iBAAO,SAAQ,IAAW;AAAA,qBACtD,IACE;AAAA;AAAA,gBAdC;AAAA,cAeP,CACD;AAAA;AAAA,UACH;AAAA,UACA,qBAAC,OAAE,WAAU,gCAA+B,eAAY,QACtD;AAAA,gCAAC,UAAK,WAAU,wCACb,0BAAgB,GACnB;AAAA,YAAO;AAAA,YAEP,oBAAC,UAAK,WAAU,sCAAsC,iBAAO,QAAO;AAAA,aACtE;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}