xertica-ui 2.8.0 → 2.9.7

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 (286) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +1 -1
  3. package/bin/cli.ts +327 -87
  4. package/bin/language-config.ts +58 -18
  5. package/components/blocks/auth/AuthPageShell.tsx +63 -18
  6. package/components/blocks/auth/SocialLoginButtons.tsx +3 -3
  7. package/components/blocks/index.ts +1 -0
  8. package/components/blocks/marketing/CTASection.tsx +73 -0
  9. package/components/blocks/marketing/ComplianceBadgeRow.tsx +49 -0
  10. package/components/blocks/marketing/EvidenceTimeline.tsx +91 -0
  11. package/components/blocks/marketing/FeatureRow.tsx +78 -0
  12. package/components/blocks/marketing/Hero.tsx +125 -0
  13. package/components/blocks/marketing/HowItWorksSteps.tsx +90 -0
  14. package/components/blocks/marketing/LogoCloud.tsx +47 -0
  15. package/components/blocks/marketing/MarketingFooter.tsx +109 -0
  16. package/components/blocks/marketing/MarketingNavbar.tsx +192 -0
  17. package/components/blocks/marketing/PillBadge.tsx +96 -0
  18. package/components/blocks/marketing/SectionHeading.tsx +82 -0
  19. package/components/blocks/marketing/ServiceCardGrid.tsx +238 -0
  20. package/components/blocks/marketing/StatBand.tsx +99 -0
  21. package/components/blocks/marketing/TopAccentCards.tsx +91 -0
  22. package/components/blocks/marketing/index.ts +41 -0
  23. package/components/brand/index.ts +1 -0
  24. package/components/brand/isotype/Isotype.tsx +64 -0
  25. package/components/brand/isotype/IsotypeDiagonal.tsx +51 -0
  26. package/components/brand/isotype/IsotypeFrames.tsx +82 -0
  27. package/components/brand/isotype/IsotypeMini.tsx +65 -0
  28. package/components/brand/isotype/IsotypeTwist.tsx +68 -0
  29. package/components/brand/isotype/frame-variants.ts +20 -0
  30. package/components/brand/isotype/index.ts +9 -0
  31. package/components/brand/isotype/isotype-diagonal.mdx +52 -0
  32. package/components/brand/isotype/isotype-diagonal.stories.tsx +44 -0
  33. package/components/brand/isotype/isotype-diagonal.test.tsx +180 -0
  34. package/components/brand/isotype/isotype-frames.mdx +50 -0
  35. package/components/brand/isotype/isotype-frames.stories.tsx +63 -0
  36. package/components/brand/isotype/isotype-frames.test.tsx +92 -0
  37. package/components/brand/isotype/isotype-mini.mdx +61 -0
  38. package/components/brand/isotype/isotype-mini.stories.tsx +75 -0
  39. package/components/brand/isotype/isotype-mini.test.tsx +45 -0
  40. package/components/brand/isotype/isotype-twist.test.tsx +71 -0
  41. package/components/brand/isotype/isotype.mdx +61 -0
  42. package/components/brand/isotype/isotype.stories.tsx +74 -0
  43. package/components/brand/isotype/isotype.test.tsx +41 -0
  44. package/components/brand/isotype/mini-patterns.ts +50 -0
  45. package/components/brand/isotype/patterns.ts +83 -0
  46. package/components/brand/isotype/twist-variants.ts +20 -0
  47. package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +3 -6
  48. package/components/pages/index.ts +1 -0
  49. package/components/pages/landing-page/LandingPage.tsx +360 -0
  50. package/components/pages/landing-page/index.ts +1 -0
  51. package/components/pages/login-page/LoginPage.tsx +3 -6
  52. package/components/pages/reset-password-page/ResetPasswordPage.tsx +1 -4
  53. package/components/pages/verify-email-page/VerifyEmailPage.tsx +2 -5
  54. package/components/ui/button/button.test.tsx +1 -1
  55. package/components/ui/button/button.tsx +7 -6
  56. package/components/ui/card/card.test.tsx +12 -0
  57. package/components/ui/card/card.tsx +5 -3
  58. package/components/ui/input-otp/input-otp.test.tsx +14 -1
  59. package/contexts/BrandColorsContext.tsx +9 -5
  60. package/contexts/theme-data.ts +32 -5
  61. package/dist/AssistantChart-BU48lxL2.js +3383 -0
  62. package/dist/AssistantChart-CoN6ga6Z.js +3383 -0
  63. package/dist/AssistantChart-DIRYJ7WY.js +3383 -0
  64. package/dist/AssistantChart-DOnY1ZlD.cjs +3551 -0
  65. package/dist/AssistantChart-DjM3qnp5.cjs +3551 -0
  66. package/dist/AssistantChart-hxPyE1S9.cjs +3551 -0
  67. package/dist/AudioPlayer-CpihsbaD.js +1278 -0
  68. package/dist/AudioPlayer-CvGajzMC.cjs +1282 -0
  69. package/dist/AudioPlayer-CvHpFqDG.cjs +1282 -0
  70. package/dist/AudioPlayer-D4S7GQou.js +1278 -0
  71. package/dist/BrandColorsContext-BNKLUVcU.js +829 -0
  72. package/dist/BrandColorsContext-DNZGHItQ.cjs +836 -0
  73. package/dist/BrandColorsContext-Ug_TZ8yn.cjs +846 -0
  74. package/dist/BrandColorsContext-z0O2LlZz.js +839 -0
  75. package/dist/CodeBlock-BkaZf-R8.cjs +223 -0
  76. package/dist/CodeBlock-CKu8LgbP.cjs +223 -0
  77. package/dist/CodeBlock-DiLS5LLI.js +221 -0
  78. package/dist/CodeBlock-zdylBGdZ.js +221 -0
  79. package/dist/CustomTooltipContent-BAR5qCQg.js +33 -0
  80. package/dist/CustomTooltipContent-CBLHBxX8.cjs +54 -0
  81. package/dist/CustomTooltipContent-CjWm3jjV.js +33 -0
  82. package/dist/CustomTooltipContent-DTV9MN9E.cjs +54 -0
  83. package/dist/EvidenceTimeline-DrRk4yqb.cjs +560 -0
  84. package/dist/EvidenceTimeline-l4MaV4C7.js +546 -0
  85. package/dist/FeatureCard-CIbBHqeY.cjs +497 -0
  86. package/dist/FeatureCard-DtoKPSfW.cjs +497 -0
  87. package/dist/FeatureCard-P4iEpBmB.js +485 -0
  88. package/dist/FeatureCard-e3f8bnF0.js +485 -0
  89. package/dist/ImageWithFallback-C1ww9Hg3.cjs +122 -0
  90. package/dist/ImageWithFallback-CBrkAEo3.cjs +122 -0
  91. package/dist/ImageWithFallback-CGk5zPH-.js +98 -0
  92. package/dist/ImageWithFallback-CfnoFFSP.js +98 -0
  93. package/dist/Isotype--fRZWwS6.js +85 -0
  94. package/dist/Isotype-0dpLqetB.js +85 -0
  95. package/dist/Isotype-B8XLEXSB.cjs +92 -0
  96. package/dist/Isotype-ChKwN7e-.cjs +91 -0
  97. package/dist/IsotypeDiagonal-CuPYPP4f.js +84 -0
  98. package/dist/IsotypeDiagonal-eNcOXRAc.cjs +91 -0
  99. package/dist/IsotypeDiagonal-oiwpXZhE.js +104 -0
  100. package/dist/IsotypeDiagonal-xMFoFXkb.cjs +109 -0
  101. package/dist/IsotypeFrames-BcWtyAWp.cjs +83 -0
  102. package/dist/IsotypeFrames-BnvSJKiH.cjs +73 -0
  103. package/dist/IsotypeFrames-iDRTRuIu.js +79 -0
  104. package/dist/IsotypeFrames-qoXOvOJt.js +69 -0
  105. package/dist/IsotypeMini-CQtWVo3h.js +126 -0
  106. package/dist/IsotypeMini-Cu_sW5yN.cjs +134 -0
  107. package/dist/IsotypeTwist-BTdNL4iV.js +106 -0
  108. package/dist/IsotypeTwist-DvsUgZs_.cjs +115 -0
  109. package/dist/LanguageContext-B1aOXO-K.js +1987 -0
  110. package/dist/LanguageContext-BP0k7AzI.js +1993 -0
  111. package/dist/LanguageContext-CChcIHIG.cjs +1998 -0
  112. package/dist/LanguageContext-ubC7TSs_.cjs +1992 -0
  113. package/dist/LanguageSelector-BLFPXGo8.cjs +48 -0
  114. package/dist/LanguageSelector-CYRzWkRZ.cjs +48 -0
  115. package/dist/LanguageSelector-CyHjZgC4.cjs +48 -0
  116. package/dist/LanguageSelector-DE8lIySO.js +46 -0
  117. package/dist/LanguageSelector-DUr1hjBW.js +46 -0
  118. package/dist/LanguageSelector-DjbWIKWC.js +46 -0
  119. package/dist/LogoCloud-C_uMKVNg.cjs +513 -0
  120. package/dist/LogoCloud-DJsMPNPh.js +500 -0
  121. package/dist/MarketingFooter-B_ycEdTJ.cjs +727 -0
  122. package/dist/MarketingFooter-Bhx-2Vlr.cjs +727 -0
  123. package/dist/MarketingFooter-Br1MGK9e.js +699 -0
  124. package/dist/MarketingFooter-BwE1vmeX.cjs +700 -0
  125. package/dist/MarketingFooter-CsZ1_xL9.js +709 -0
  126. package/dist/MarketingFooter-DCwf78Q4.cjs +716 -0
  127. package/dist/MarketingFooter-KH2r-OOL.js +709 -0
  128. package/dist/MarketingFooter-nB7G7gQ5.js +682 -0
  129. package/dist/SectionHeading-DEeOKcAd.cjs +857 -0
  130. package/dist/SectionHeading-DMNObcwm.js +838 -0
  131. package/dist/ThemeToggle--CGJjQJv.cjs +219 -0
  132. package/dist/ThemeToggle-Bn_7Pmfe.js +215 -0
  133. package/dist/ThemeToggle-CoL8Ady7.js +215 -0
  134. package/dist/ThemeToggle-D2Wzm5bn.cjs +219 -0
  135. package/dist/VerifyEmailPage-90lK6pP4.js +3258 -0
  136. package/dist/VerifyEmailPage-BJX1Rter.cjs +3320 -0
  137. package/dist/VerifyEmailPage-BSWvOgry.js +3310 -0
  138. package/dist/VerifyEmailPage-CDqIkhx-.js +3224 -0
  139. package/dist/VerifyEmailPage-Crxje3ZB.js +3323 -0
  140. package/dist/VerifyEmailPage-D2ShbuEN.cjs +3333 -0
  141. package/dist/VerifyEmailPage-DEBZXQ7c.cjs +3310 -0
  142. package/dist/VerifyEmailPage-DGaX-jZv.cjs +3268 -0
  143. package/dist/VerifyEmailPage-ZtOPkgOA.cjs +3234 -0
  144. package/dist/VerifyEmailPage-s927GSiR.js +3300 -0
  145. package/dist/XerticaOrbe-BHex09xs.js +1925 -0
  146. package/dist/XerticaOrbe-BKW3LJEz.js +1925 -0
  147. package/dist/XerticaOrbe-Br2i99U_.cjs +1927 -0
  148. package/dist/XerticaOrbe-kjFmuyx-.cjs +1927 -0
  149. package/dist/XerticaProvider-Bi5lKC9N.js +44 -0
  150. package/dist/XerticaProvider-BrPMVhiv.js +44 -0
  151. package/dist/XerticaProvider-CnX5kt5W.cjs +46 -0
  152. package/dist/XerticaProvider-DEKNJTlN.js +44 -0
  153. package/dist/XerticaProvider-X_wavDYA.cjs +46 -0
  154. package/dist/XerticaProvider-b3n_oL8w.cjs +46 -0
  155. package/dist/alert-dialog--qdlmB5i.cjs +872 -0
  156. package/dist/alert-dialog-C48TOdYU.cjs +872 -0
  157. package/dist/alert-dialog-CEPpnpXt.js +819 -0
  158. package/dist/alert-dialog-NjflitDa.js +819 -0
  159. package/dist/assistant.cjs.js +2 -2
  160. package/dist/assistant.es.js +2 -2
  161. package/dist/avatar-CtEKgg7s.cjs +77 -0
  162. package/dist/avatar-D03MNQxs.js +54 -0
  163. package/dist/avatar-Df7KgSvs.js +54 -0
  164. package/dist/avatar-Gy0_Xu9i.cjs +77 -0
  165. package/dist/blocks.cjs.js +33 -18
  166. package/dist/blocks.es.js +2 -2
  167. package/dist/brand.cjs.js +110 -4
  168. package/dist/brand.es.js +98 -4
  169. package/dist/breadcrumb-BGO5PZPG.js +90 -0
  170. package/dist/breadcrumb-BvGAY4ne.cjs +98 -0
  171. package/dist/breadcrumb-CU-punra.js +90 -0
  172. package/dist/breadcrumb-DjV9wfX1.cjs +98 -0
  173. package/dist/button-B05czY_8.js +62 -0
  174. package/dist/button-B3SE1liJ.js +62 -0
  175. package/dist/button-BMDlWZR8.cjs +85 -0
  176. package/dist/button-DVhloDtr.cjs +85 -0
  177. package/dist/cli.js +280 -62
  178. package/dist/components/blocks/auth/AuthPageShell.d.ts +21 -5
  179. package/dist/components/blocks/index.d.ts +1 -0
  180. package/dist/components/blocks/marketing/CTASection.d.ts +28 -0
  181. package/dist/components/blocks/marketing/ComplianceBadgeRow.d.ts +23 -0
  182. package/dist/components/blocks/marketing/EvidenceTimeline.d.ts +26 -0
  183. package/dist/components/blocks/marketing/FeatureRow.d.ts +25 -0
  184. package/dist/components/blocks/marketing/Hero.d.ts +35 -0
  185. package/dist/components/blocks/marketing/HowItWorksSteps.d.ts +29 -0
  186. package/dist/components/blocks/marketing/LogoCloud.d.ts +22 -0
  187. package/dist/components/blocks/marketing/MarketingFooter.d.ts +42 -0
  188. package/dist/components/blocks/marketing/MarketingNavbar.d.ts +47 -0
  189. package/dist/components/blocks/marketing/PillBadge.d.ts +21 -0
  190. package/dist/components/blocks/marketing/SectionHeading.d.ts +23 -0
  191. package/dist/components/blocks/marketing/ServiceCardGrid.d.ts +65 -0
  192. package/dist/components/blocks/marketing/StatBand.d.ts +25 -0
  193. package/dist/components/blocks/marketing/TopAccentCards.d.ts +29 -0
  194. package/dist/components/blocks/marketing/index.d.ts +28 -0
  195. package/dist/components/brand/index.d.ts +1 -0
  196. package/dist/components/brand/isotype/Isotype.d.ts +27 -0
  197. package/dist/components/brand/isotype/IsotypeDiagonal.d.ts +19 -0
  198. package/dist/components/brand/isotype/IsotypeFrames.d.ts +21 -0
  199. package/dist/components/brand/isotype/IsotypeMini.d.ts +26 -0
  200. package/dist/components/brand/isotype/IsotypeTwist.d.ts +22 -0
  201. package/dist/components/brand/isotype/frame-variants.d.ts +11 -0
  202. package/dist/components/brand/isotype/index.d.ts +9 -0
  203. package/dist/components/brand/isotype/mini-patterns.d.ts +25 -0
  204. package/dist/components/brand/isotype/patterns.d.ts +42 -0
  205. package/dist/components/brand/isotype/twist-variants.d.ts +11 -0
  206. package/dist/components/pages/forgot-password-page/ForgotPasswordPage.d.ts +2 -2
  207. package/dist/components/pages/index.d.ts +1 -0
  208. package/dist/components/pages/landing-page/LandingPage.d.ts +18 -0
  209. package/dist/components/pages/landing-page/index.d.ts +1 -0
  210. package/dist/components/pages/login-page/LoginPage.d.ts +2 -2
  211. package/dist/components/pages/verify-email-page/VerifyEmailPage.d.ts +1 -1
  212. package/dist/components/ui/card/card.d.ts +3 -1
  213. package/dist/contexts/theme-data.d.ts +6 -0
  214. package/dist/dropdown-menu-ChQa9zW3.cjs +225 -0
  215. package/dist/dropdown-menu-DRvnsQAt.cjs +225 -0
  216. package/dist/dropdown-menu-DUk81i5Y.js +190 -0
  217. package/dist/dropdown-menu-NlUHhVmK.js +190 -0
  218. package/dist/hooks.cjs.js +2 -2
  219. package/dist/hooks.es.js +2 -2
  220. package/dist/index.cjs.js +57 -41
  221. package/dist/index.es.js +25 -25
  222. package/dist/input-Csp_Metb.cjs +91 -0
  223. package/dist/input-DVRwGVLx.js +65 -0
  224. package/dist/input-Dqyj7Hdw.cjs +91 -0
  225. package/dist/input-dOgrFQ-3.js +65 -0
  226. package/dist/layout.cjs.js +1 -1
  227. package/dist/layout.es.js +1 -1
  228. package/dist/media.cjs.js +1 -1
  229. package/dist/media.es.js +1 -1
  230. package/dist/pages.cjs.js +2 -1
  231. package/dist/pages.es.js +1 -1
  232. package/dist/rich-text-editor-7K7UjLJN.cjs +2968 -0
  233. package/dist/rich-text-editor-B3jLP2bm.cjs +2968 -0
  234. package/dist/rich-text-editor-BntMVQsB.js +2894 -0
  235. package/dist/rich-text-editor-Diz_onyB.js +2894 -0
  236. package/dist/rich-text-editor-RWvK4hd3.cjs +2968 -0
  237. package/dist/rich-text-editor-gauEkzPM.js +2894 -0
  238. package/dist/select-B6PygS38.cjs +161 -0
  239. package/dist/select-BD763J5Y.cjs +161 -0
  240. package/dist/select-QJDKZITN.js +130 -0
  241. package/dist/select-q7aDfuPn.js +130 -0
  242. package/dist/sidebar-Bkk1YAsD.cjs +1154 -0
  243. package/dist/sidebar-CuzYeOSz.cjs +1154 -0
  244. package/dist/sidebar-CwlW3LXE.js +1151 -0
  245. package/dist/sidebar-DNkf-Ug6.js +1151 -0
  246. package/dist/sidebar-DaS3fplw.cjs +1154 -0
  247. package/dist/sidebar-FDCY42oP.js +1151 -0
  248. package/dist/skeleton-BtBwJ39W.js +79 -0
  249. package/dist/skeleton-CJyg0eJ1.cjs +88 -0
  250. package/dist/skeleton-D8WaauK0.cjs +88 -0
  251. package/dist/skeleton-DBYtjaZW.cjs +88 -0
  252. package/dist/skeleton-DY_lSFWf.js +79 -0
  253. package/dist/skeleton-Df65Ox10.js +79 -0
  254. package/dist/slider-BKV_vvsh.cjs +78 -0
  255. package/dist/slider-B_lUjFNa.cjs +78 -0
  256. package/dist/slider-CtBpIaU4.js +56 -0
  257. package/dist/slider-KgWmSJbx.js +56 -0
  258. package/dist/tooltip-BPttuPxU.js +48 -0
  259. package/dist/tooltip-CIKRzQqE.cjs +72 -0
  260. package/dist/tooltip-CoDByjE_.cjs +72 -0
  261. package/dist/tooltip-_MAwsiOL.js +48 -0
  262. package/dist/ui.cjs.js +13 -13
  263. package/dist/ui.es.js +13 -13
  264. package/dist/xertica-assistant-B1xAl2Vn.js +2173 -0
  265. package/dist/xertica-assistant-BBUMaUXw.js +2173 -0
  266. package/dist/xertica-assistant-CWuBwfzt.cjs +2180 -0
  267. package/dist/xertica-assistant-D2hlwiEP.js +2173 -0
  268. package/dist/xertica-assistant-DN2J_sQQ.cjs +2180 -0
  269. package/dist/xertica-assistant-DdYS6WXT.cjs +2180 -0
  270. package/dist/xertica-ui.css +1 -1
  271. package/llms-compact.txt +1 -1
  272. package/llms.txt +1 -1
  273. package/package.json +4 -2
  274. package/styles/xertica/base.css +8 -0
  275. package/styles/xertica/tokens.css +1 -0
  276. package/templates/package.json +1 -1
  277. package/templates/src/app/components/AuthGuard.tsx +6 -0
  278. package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +1 -4
  279. package/templates/src/features/auth/ui/LoginContent.tsx +1 -4
  280. package/templates/src/features/auth/ui/ResetPasswordContent.tsx +1 -4
  281. package/templates/src/features/auth/ui/VerifyEmailContent.tsx +1 -4
  282. package/templates/src/locales/en/pages/landing.json +175 -0
  283. package/templates/src/locales/es/pages/landing.json +175 -0
  284. package/templates/src/locales/pt-BR/pages/landing.json +175 -0
  285. package/templates/src/pages/LandingPage.tsx +349 -0
  286. package/templates/src/styles/xertica/tokens.css +1 -0
@@ -0,0 +1,238 @@
1
+ import * as React from 'react';
2
+ import { Check } from 'lucide-react';
3
+ import {
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardDescription,
8
+ CardContent,
9
+ CardFooter,
10
+ } from '../../ui/card';
11
+ import { Button } from '../../ui/button';
12
+ import { cn } from '../../shared/utils';
13
+
14
+ export type BlockColor =
15
+ | 'primary'
16
+ | 'chart-1'
17
+ | 'chart-2'
18
+ | 'chart-3'
19
+ | 'chart-4'
20
+ | 'chart-5'
21
+ | 'success'
22
+ | 'info'
23
+ | 'warning'
24
+ | 'destructive';
25
+
26
+ const colorTokens: Record<BlockColor, { border: string; tint: string; icon: string }> = {
27
+ primary: { border: 'border-primary', tint: 'bg-primary/5', icon: 'text-primary' },
28
+ 'chart-1': {
29
+ border: 'border-[var(--chart-1)]',
30
+ tint: 'bg-[var(--chart-1)]/5',
31
+ icon: 'text-[var(--chart-1)]',
32
+ },
33
+ 'chart-2': {
34
+ border: 'border-[var(--chart-2)]',
35
+ tint: 'bg-[var(--chart-2)]/5',
36
+ icon: 'text-[var(--chart-2)]',
37
+ },
38
+ 'chart-3': {
39
+ border: 'border-[var(--chart-3)]',
40
+ tint: 'bg-[var(--chart-3)]/5',
41
+ icon: 'text-[var(--chart-3)]',
42
+ },
43
+ 'chart-4': {
44
+ border: 'border-[var(--chart-4)]',
45
+ tint: 'bg-[var(--chart-4)]/5',
46
+ icon: 'text-[var(--chart-4)]',
47
+ },
48
+ 'chart-5': {
49
+ border: 'border-[var(--chart-5)]',
50
+ tint: 'bg-[var(--chart-5)]/5',
51
+ icon: 'text-[var(--chart-5)]',
52
+ },
53
+ success: { border: 'border-success', tint: 'bg-success/5', icon: 'text-success' },
54
+ info: { border: 'border-info', tint: 'bg-info/5', icon: 'text-info' },
55
+ warning: { border: 'border-warning', tint: 'bg-warning/5', icon: 'text-warning' },
56
+ destructive: { border: 'border-destructive', tint: 'bg-destructive/5', icon: 'text-destructive' },
57
+ };
58
+
59
+ export interface ServiceCardItem {
60
+ /** Omit to render the card without an icon chip. */
61
+ icon?: React.ReactNode;
62
+ color?: BlockColor;
63
+ category?: string;
64
+ title: string;
65
+ description: string;
66
+ checklist?: string[];
67
+ bullets?: string[];
68
+ cta?: { label: string; href?: string; onClick?: () => void };
69
+ /** Card surface tone — only applies when the grid's `variant` is `"card"`. */
70
+ background?: 'default' | 'white';
71
+ className?: string;
72
+ }
73
+
74
+ export interface ServiceCardGridProps {
75
+ items: ServiceCardItem[];
76
+ variant?: 'card' | 'plain';
77
+ columns?: 2 | 3 | 4;
78
+ className?: string;
79
+ }
80
+
81
+ const columnClasses: Record<2 | 3 | 4, string> = {
82
+ 2: 'md:grid-cols-2',
83
+ 3: 'md:grid-cols-3',
84
+ 4: 'md:grid-cols-4',
85
+ };
86
+
87
+ /**
88
+ * Responsive grid of service/feature cards.
89
+ *
90
+ * @description
91
+ * Renders a list of `ServiceCardItem`s as either bordered `Card`s with an
92
+ * icon chip, checklist and CTA (`variant="card"`, e.g. a 3-up services
93
+ * section) or a borderless icon + title + description layout for compact
94
+ * 4-up feature grids (`variant="plain"`).
95
+ *
96
+ * @ai-rules
97
+ * 1. Use `variant="card"` for services that need a checklist/bullets/CTA; use `variant="plain"` for simple icon-grid feature lists.
98
+ * 2. `columns` sets the desktop column count (grid is always 1 column on mobile); it defaults to 3.
99
+ * 3. Each item's `color` maps to `colorTokens` — never hardcode chip colors.
100
+ * 4. `icon` is optional — omit it for an icon-less card (title + description, optionally with checklist/bullets/CTA).
101
+ * 5. Each item's `background` (`"default"` | `"white"`) only applies in `variant="card"`. Keep it uniform within a single grid — vary it across separate sections/blocks instead, e.g. by pairing a `bg-muted` wrapper section with `"default"` cards, rather than mixing tones inside one grid.
102
+ * 6. Pass `className: 'border'` on an item when its card sits directly on `bg-background` (the theme's `--card` and `--background` are identical in light mode, so without a border the card's own background is invisible there).
103
+ */
104
+ export function ServiceCardGrid({
105
+ items,
106
+ variant = 'card',
107
+ columns = 3,
108
+ className,
109
+ }: ServiceCardGridProps) {
110
+ return (
111
+ <div className={cn('grid grid-cols-1 gap-6 md:gap-8', columnClasses[columns], className)}>
112
+ {items.map((item, index) => (
113
+ <ServiceCard key={`${item.title}-${index}`} variant={variant} {...item} />
114
+ ))}
115
+ </div>
116
+ );
117
+ }
118
+
119
+ export interface ServiceCardProps extends ServiceCardItem {
120
+ variant?: 'card' | 'plain';
121
+ className?: string;
122
+ }
123
+
124
+ /**
125
+ * Single service/feature card, used standalone or via `ServiceCardGrid`.
126
+ *
127
+ * @description
128
+ * Pairs a token-tinted icon chip with a title and description. In
129
+ * `variant="card"` it renders inside a bordered `Card` with an optional
130
+ * category label, checklist, bullet list and footer CTA button. In
131
+ * `variant="plain"` it renders as a minimal chip + text stack with no
132
+ * chrome, checklist or CTA — for dense icon-grid layouts.
133
+ *
134
+ * @ai-rules
135
+ * 1. `checklist` and `bullets`/`cta` only render in `variant="card"`; they are ignored in `variant="plain"`.
136
+ * 2. Pass `icon` as a `lucide-react` element; it is sized automatically inside the chip. Omit it entirely for an icon-less card.
137
+ * 3. If `cta.href` is set the button renders as a link (`asChild`); otherwise it fires `cta.onClick`.
138
+ * 4. `background` is ignored in `variant="plain"` (no surface to color); it maps directly to the underlying `Card`'s `variant`.
139
+ */
140
+ export function ServiceCard({
141
+ icon,
142
+ color = 'primary',
143
+ category,
144
+ title,
145
+ description,
146
+ checklist,
147
+ bullets,
148
+ cta,
149
+ variant = 'card',
150
+ background = 'default',
151
+ className,
152
+ }: ServiceCardProps) {
153
+ const { border, tint, icon: iconColor } = colorTokens[color];
154
+
155
+ const iconChip = icon && (
156
+ <div className={cn('w-fit rounded-[var(--radius)] border p-3', border, tint)}>
157
+ <div className={cn('[&>svg]:size-6', iconColor)}>{icon}</div>
158
+ </div>
159
+ );
160
+
161
+ if (variant === 'plain') {
162
+ return (
163
+ <div className={cn('flex flex-col gap-3', className)}>
164
+ {iconChip}
165
+ <div>
166
+ {category && (
167
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
168
+ {category}
169
+ </p>
170
+ )}
171
+ <h3 className={cn('text-base font-semibold text-foreground', category && 'mt-1')}>
172
+ {title}
173
+ </h3>
174
+ </div>
175
+ <p className="text-sm leading-relaxed text-muted-foreground">{description}</p>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ return (
181
+ <Card variant={background} className={cn('flex h-full flex-col', className)}>
182
+ <CardHeader>
183
+ {iconChip}
184
+ <div>
185
+ {category && (
186
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
187
+ {category}
188
+ </p>
189
+ )}
190
+ <CardTitle className={category ? 'mt-1' : undefined}>{title}</CardTitle>
191
+ </div>
192
+ </CardHeader>
193
+
194
+ <CardContent className="flex-1 space-y-4">
195
+ <CardDescription className="leading-relaxed">{description}</CardDescription>
196
+
197
+ {checklist && checklist.length > 0 && (
198
+ <div className="divide-y divide-border">
199
+ {checklist.map(item => (
200
+ <div key={item} className="flex items-center gap-2.5 py-3">
201
+ <span
202
+ className={cn(
203
+ 'flex size-5 shrink-0 items-center justify-center rounded-[var(--radius)] border',
204
+ border,
205
+ tint,
206
+ iconColor
207
+ )}
208
+ >
209
+ <Check className="size-3.5" />
210
+ </span>
211
+ <span className="text-sm text-foreground">{item}</span>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ )}
216
+
217
+ {bullets && bullets.length > 0 && (
218
+ <ul className="space-y-1.5">
219
+ {bullets.map(bullet => (
220
+ <li key={bullet} className="flex items-start gap-2 text-sm text-muted-foreground">
221
+ <span className="mt-2 size-1.5 shrink-0 rounded-full bg-muted-foreground" />
222
+ <span>{bullet}</span>
223
+ </li>
224
+ ))}
225
+ </ul>
226
+ )}
227
+ </CardContent>
228
+
229
+ {cta && (
230
+ <CardFooter>
231
+ <Button variant="outline" className="w-full" onClick={cta.onClick} asChild={!!cta.href}>
232
+ {cta.href ? <a href={cta.href}>{cta.label}</a> : cta.label}
233
+ </Button>
234
+ </CardFooter>
235
+ )}
236
+ </Card>
237
+ );
238
+ }
@@ -0,0 +1,99 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../../shared/utils';
3
+
4
+ export type BlockColor =
5
+ | 'primary'
6
+ | 'chart-1'
7
+ | 'chart-2'
8
+ | 'chart-3'
9
+ | 'chart-4'
10
+ | 'chart-5'
11
+ | 'success'
12
+ | 'info'
13
+ | 'warning'
14
+ | 'destructive';
15
+
16
+ const colorTokens: Record<BlockColor, { bg: string; icon: string }> = {
17
+ primary: { bg: 'bg-primary/10', icon: 'text-primary' },
18
+ 'chart-1': { bg: 'bg-[var(--chart-1)]/15', icon: 'text-[var(--chart-1)]' },
19
+ 'chart-2': { bg: 'bg-[var(--chart-2)]/15', icon: 'text-[var(--chart-2)]' },
20
+ 'chart-3': { bg: 'bg-[var(--chart-3)]/15', icon: 'text-[var(--chart-3)]' },
21
+ 'chart-4': { bg: 'bg-[var(--chart-4)]/15', icon: 'text-[var(--chart-4)]' },
22
+ 'chart-5': { bg: 'bg-[var(--chart-5)]/15', icon: 'text-[var(--chart-5)]' },
23
+ success: { bg: 'bg-success/10', icon: 'text-success' },
24
+ info: { bg: 'bg-info/10', icon: 'text-info' },
25
+ warning: { bg: 'bg-warning/10', icon: 'text-warning' },
26
+ destructive: { bg: 'bg-destructive/10', icon: 'text-destructive' },
27
+ };
28
+
29
+ export interface StatBandStat {
30
+ value: string;
31
+ label: string;
32
+ color?: BlockColor;
33
+ }
34
+
35
+ export interface StatBandProps extends React.HTMLAttributes<HTMLDivElement> {
36
+ stats: StatBandStat[];
37
+ columns?: 2 | 3 | 4;
38
+ }
39
+
40
+ const columnClasses: Record<2 | 3 | 4, string> = {
41
+ 2: 'md:grid-cols-2',
42
+ 3: 'md:grid-cols-3',
43
+ 4: 'md:grid-cols-4',
44
+ };
45
+
46
+ /**
47
+ * Row of large highlighted stats with labels, no card chrome.
48
+ *
49
+ * @description
50
+ * Displays a set of headline numbers (e.g. "$5.5T", "3x", "9M+") side by side,
51
+ * each paired with a small muted label. Useful for social-proof or impact
52
+ * sections on marketing pages.
53
+ *
54
+ * @ai-rules
55
+ * 1. Pass 2-4 `stats`; `columns` defaults to `stats.length` capped at 4.
56
+ * 2. Each stat's `color` maps to a token from `colorTokens` — never hardcode colors.
57
+ * 3. This block has no background/border of its own; wrap it in a `<Card>` or section if chrome is needed.
58
+ */
59
+ export function StatBand({ stats, columns, className, ...props }: StatBandProps) {
60
+ const resolvedColumns = (columns ?? Math.min(stats.length, 4)) as 2 | 3 | 4;
61
+
62
+ return (
63
+ <div
64
+ className={cn(
65
+ 'grid grid-cols-2 gap-x-8 gap-y-10 md:gap-x-0',
66
+ columnClasses[resolvedColumns] ?? columnClasses[4],
67
+ className
68
+ )}
69
+ {...props}
70
+ >
71
+ {stats.map((stat, index) => {
72
+ const { icon: valueColor } = colorTokens[stat.color ?? 'primary'];
73
+ const isNewRow = index % resolvedColumns === 0;
74
+
75
+ return (
76
+ <div
77
+ key={index}
78
+ className={cn(
79
+ 'flex flex-col items-center text-center sm:items-start sm:text-left',
80
+ isNewRow ? 'md:pl-0' : 'md:border-l md:border-border md:pl-10'
81
+ )}
82
+ >
83
+ <span
84
+ className={cn(
85
+ 'text-3xl md:text-5xl font-bold tracking-tight tabular-nums',
86
+ valueColor
87
+ )}
88
+ >
89
+ {stat.value}
90
+ </span>
91
+ <span className="mt-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
92
+ {stat.label}
93
+ </span>
94
+ </div>
95
+ );
96
+ })}
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,91 @@
1
+ import * as React from 'react';
2
+ import { Card, CardHeader, CardTitle, CardContent } from '../../ui/card';
3
+ import { cn } from '../../shared/utils';
4
+
5
+ export type BlockColor =
6
+ | 'primary'
7
+ | 'chart-1'
8
+ | 'chart-2'
9
+ | 'chart-3'
10
+ | 'chart-4'
11
+ | 'chart-5'
12
+ | 'success'
13
+ | 'info'
14
+ | 'warning'
15
+ | 'destructive';
16
+
17
+ const colorTokens: Record<BlockColor, string> = {
18
+ primary: 'border-t-primary',
19
+ 'chart-1': 'border-t-[var(--chart-1)]',
20
+ 'chart-2': 'border-t-[var(--chart-2)]',
21
+ 'chart-3': 'border-t-[var(--chart-3)]',
22
+ 'chart-4': 'border-t-[var(--chart-4)]',
23
+ 'chart-5': 'border-t-[var(--chart-5)]',
24
+ success: 'border-t-success',
25
+ info: 'border-t-info',
26
+ warning: 'border-t-warning',
27
+ destructive: 'border-t-destructive',
28
+ };
29
+
30
+ export interface TopAccentCardItem {
31
+ title: string;
32
+ body: string;
33
+ bullets?: string[];
34
+ color?: BlockColor;
35
+ className?: string;
36
+ }
37
+
38
+ export interface TopAccentCardsProps {
39
+ items: TopAccentCardItem[];
40
+ className?: string;
41
+ }
42
+
43
+ /**
44
+ * Two-up framing cards with a colored top-border accent.
45
+ *
46
+ * @description
47
+ * A lightweight pairing block for "mission vs. problem" style framing copy:
48
+ * each card is a bordered `Card` whose top edge is thickened and tinted per
49
+ * `color`, with a title, body copy, and an optional plain bullet list. No
50
+ * icon chip and no CTA — use `ServiceCardGrid` when those are needed.
51
+ *
52
+ * @ai-rules
53
+ * 1. Each item's `color` maps to `colorTokens` and only tints the top border — never hardcode colors.
54
+ * 2. Keep the neutral `border-border` class before the accent `border-t-{color}` class in any className override — tailwind-merge drops the accent if the order is reversed.
55
+ * 3. Designed for 2 items; more will simply wrap onto a new row at the `sm:grid-cols-2` breakpoint.
56
+ * 4. `bullets` render as plain dot markers (no checkmark) — use `ServiceCardGrid`'s `checklist` instead when a checkmark/CTA is needed.
57
+ */
58
+ export function TopAccentCards({ items, className }: TopAccentCardsProps) {
59
+ return (
60
+ <div className={cn('grid grid-cols-1 gap-6 sm:grid-cols-2 md:gap-8', className)}>
61
+ {items.map((item, index) => (
62
+ <Card
63
+ key={`${item.title}-${index}`}
64
+ className={cn(
65
+ 'border border-t-4 border-border',
66
+ colorTokens[item.color ?? 'primary'],
67
+ item.className
68
+ )}
69
+ >
70
+ <CardHeader>
71
+ <CardTitle>{item.title}</CardTitle>
72
+ </CardHeader>
73
+ <CardContent className="space-y-4">
74
+ <p className="text-sm leading-relaxed text-muted-foreground">{item.body}</p>
75
+
76
+ {item.bullets && item.bullets.length > 0 && (
77
+ <ul className="space-y-1.5">
78
+ {item.bullets.map(bullet => (
79
+ <li key={bullet} className="flex items-start gap-2 text-sm text-muted-foreground">
80
+ <span className="mt-2 size-1.5 shrink-0 rounded-full bg-muted-foreground" />
81
+ <span>{bullet}</span>
82
+ </li>
83
+ ))}
84
+ </ul>
85
+ )}
86
+ </CardContent>
87
+ </Card>
88
+ ))}
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,41 @@
1
+ export { PillBadge } from './PillBadge';
2
+ export type { PillBadgeProps, BlockColor } from './PillBadge';
3
+
4
+ export { MarketingNavbar } from './MarketingNavbar';
5
+ export type { MarketingNavbarProps, MarketingNavLink, MarketingNavbarCta } from './MarketingNavbar';
6
+
7
+ export { Hero } from './Hero';
8
+ export type { HeroProps, HeroCta } from './Hero';
9
+
10
+ export { TopAccentCards } from './TopAccentCards';
11
+ export type { TopAccentCardsProps, TopAccentCardItem } from './TopAccentCards';
12
+
13
+ export { LogoCloud } from './LogoCloud';
14
+ export type { LogoCloudProps, LogoCloudLogo } from './LogoCloud';
15
+
16
+ export { StatBand } from './StatBand';
17
+ export type { StatBandProps, StatBandStat } from './StatBand';
18
+
19
+ export { FeatureRow } from './FeatureRow';
20
+ export type { FeatureRowProps } from './FeatureRow';
21
+
22
+ export { ServiceCardGrid, ServiceCard } from './ServiceCardGrid';
23
+ export type { ServiceCardGridProps, ServiceCardItem, ServiceCardProps } from './ServiceCardGrid';
24
+
25
+ export { EvidenceTimeline } from './EvidenceTimeline';
26
+ export type { EvidenceTimelineProps, EvidenceTimelineItem } from './EvidenceTimeline';
27
+
28
+ export { HowItWorksSteps } from './HowItWorksSteps';
29
+ export type { HowItWorksStepsProps, HowItWorksStepItem } from './HowItWorksSteps';
30
+
31
+ export { ComplianceBadgeRow } from './ComplianceBadgeRow';
32
+ export type { ComplianceBadgeRowProps, ComplianceBadge } from './ComplianceBadgeRow';
33
+
34
+ export { CTASection } from './CTASection';
35
+ export type { CTASectionProps, CTASectionAction } from './CTASection';
36
+
37
+ export { MarketingFooter } from './MarketingFooter';
38
+ export type { MarketingFooterProps, MarketingFooterLink, MarketingFooterColumn } from './MarketingFooter';
39
+
40
+ export { SectionHeading } from './SectionHeading';
41
+ export type { SectionHeadingProps } from './SectionHeading';
@@ -1,4 +1,5 @@
1
1
  export * from './xertica-provider';
2
+ export * from './isotype';
2
3
  export * from './xertica-logo';
3
4
  export * from './xertica-xlogo';
4
5
  export * from './xertica-orbe';
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../../shared/utils';
3
+ import {
4
+ getIsotypeVariant,
5
+ ISOTYPE_COLORS,
6
+ ISOTYPE_PATTERNS,
7
+ type IsotypePatternId,
8
+ } from './patterns';
9
+
10
+ export interface IsotypeProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ /** Which registered pattern to render. */
12
+ pattern?: IsotypePatternId;
13
+ /**
14
+ * Deterministic seed selecting one extra cell (beyond the pattern's own
15
+ * inherent blank cell) to also render as blank. Omit to render the pure
16
+ * base pattern with no randomization — the correct choice for canonical
17
+ * brand-mark placements (e.g. the landing page Hero).
18
+ */
19
+ variantSeed?: number;
20
+ /** Accessible label. Omit for purely decorative use. */
21
+ 'aria-label'?: string;
22
+ }
23
+
24
+ /**
25
+ * A 4x4 grid brand mark — the first in a growing series of Xertica
26
+ * "isotipos". Cells render flush against each other so the grid reads as a
27
+ * single composed image.
28
+ *
29
+ * @ai-rules
30
+ * 1. Colors are fixed brand hex values (see `patterns.ts`), not theme tokens — never make them theme-reactive.
31
+ * 2. Never add a `gap-*` utility or per-cell border between cells — it breaks the "single image" look.
32
+ * 3. Add new isotypes via `ISOTYPE_PATTERNS`/`IsotypePatternId` in `patterns.ts`; this component never needs to change.
33
+ * 4. Only pass `variantSeed` for decorative/repeated placements — canonical brand-mark spots (Hero, nav) should use the pure base pattern.
34
+ */
35
+ export function Isotype({
36
+ pattern = 'core',
37
+ variantSeed,
38
+ className,
39
+ 'aria-label': ariaLabel,
40
+ ...props
41
+ }: IsotypeProps) {
42
+ const cells =
43
+ variantSeed === undefined ? ISOTYPE_PATTERNS[pattern] : getIsotypeVariant(pattern, variantSeed);
44
+
45
+ return (
46
+ <div
47
+ role={ariaLabel ? 'img' : undefined}
48
+ aria-label={ariaLabel}
49
+ aria-hidden={ariaLabel ? undefined : true}
50
+ className={cn(
51
+ 'grid aspect-square grid-cols-4 grid-rows-4',
52
+ className
53
+ )}
54
+ {...props}
55
+ >
56
+ {cells.map((cell, index) => (
57
+ <div
58
+ key={index}
59
+ style={{ backgroundColor: cell === 'blank' ? undefined : ISOTYPE_COLORS[cell] }}
60
+ />
61
+ ))}
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,51 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../../shared/utils';
3
+ import { ISOTYPE_COLORS } from './patterns';
4
+
5
+ export interface IsotypeDiagonalProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ /** Accessible label. Omit for purely decorative use. */
7
+ 'aria-label'?: string;
8
+ }
9
+
10
+ const CELL = 25;
11
+ const SIZE = 100;
12
+
13
+ /**
14
+ * A single fixed diagonal-cut variation of the "core" grid isotype — the
15
+ * third mark in the Xertica "isotipos" series. Three color-pair boundaries
16
+ * (blue/green, magenta/red, and yellow's hypotenuse) all lie on the same
17
+ * outer-corner-to-outer-corner diagonal of the square.
18
+ *
19
+ * @ai-rules
20
+ * 1. Colors are fixed brand hex values (see `patterns.ts`), not theme tokens — never make them theme-reactive.
21
+ * 2. All 8 shapes' coordinates are interdependent — never move one vertex/rect without re-checking every seam it backs or is backed by.
22
+ * 3. This is one fixed composition, not a data-driven pattern — there is no variant/pattern prop and none should be added speculatively.
23
+ * 4. The blank polygon's `fill="none"` is required, not cosmetic — an SVG shape with no `fill` renders solid black, unlike a CSS `background-color` left unset.
24
+ * 5. `blue` and `red` are full-width backing rectangles, wider than the triangular shape they end up visually showing — `purple`/`green` paint over `blue`, and `magenta`/`brown` paint over `red`. A browser only renders a visible anti-aliasing seam where a shape's edge borders empty space; painting the diagonal-tip shapes directly over a same-side opaque rectangle means any imperfect edge coverage reveals more of that matching color instead of the page background, with no stroke or scale involved. Never shrink `blue`/`red` back down to their visible triangle, and never move `purple`/`green`/`magenta`/`brown` earlier than `blue`/`red` in paint order — the overlap and the order are what hide the seam.
25
+ */
26
+ export function IsotypeDiagonal({
27
+ className,
28
+ 'aria-label': ariaLabel,
29
+ ...props
30
+ }: IsotypeDiagonalProps) {
31
+ return (
32
+ <div
33
+ role={ariaLabel ? 'img' : undefined}
34
+ aria-label={ariaLabel}
35
+ aria-hidden={ariaLabel ? undefined : true}
36
+ className={cn('aspect-square', className)}
37
+ {...props}
38
+ >
39
+ <svg viewBox={`0 0 ${SIZE} ${SIZE}`} className="h-full w-full" aria-hidden="true">
40
+ <rect x={0} y={0} width={SIZE} height={CELL} fill={ISOTYPE_COLORS.blue} />
41
+ <rect x={0} y={0} width={CELL} height={CELL} fill={ISOTYPE_COLORS.purple} />
42
+ <polygon points="100,0 75,25 75,75 100,75" fill={ISOTYPE_COLORS.green} />
43
+ <rect x={0} y={SIZE - CELL} width={SIZE} height={CELL} fill={ISOTYPE_COLORS.red} />
44
+ <polygon points="0,25 25,25 25,75 0,100" fill={ISOTYPE_COLORS.magenta} />
45
+ <rect x={SIZE - CELL} y={SIZE - CELL} width={CELL} height={CELL} fill={ISOTYPE_COLORS.brown} />
46
+ <polygon points="75,25 75,75 25,75" fill={ISOTYPE_COLORS.yellow} />
47
+ <polygon points="25,25 75,25 25,75" fill="none" />
48
+ </svg>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,82 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../../shared/utils';
3
+ import { ISOTYPE_COLORS } from './patterns';
4
+ import { ISOTYPE_FRAME_VARIANTS, type IsotypeFrameVariantId } from './frame-variants';
5
+
6
+ export interface IsotypeFramesProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ /** Which registered color-pair variant to render. */
8
+ variant?: IsotypeFrameVariantId;
9
+ /** Accessible label. Omit for purely decorative use. */
10
+ 'aria-label'?: string;
11
+ }
12
+
13
+ const FRAME_SIZE = 78;
14
+ const FRAME_THICKNESS = 27;
15
+ /** CSS blend mode used to combine the two frames — the intersection color is whatever this mode produces, not a fixed value. */
16
+ const FRAME_BLEND_MODE: React.CSSProperties['mixBlendMode'] = 'multiply';
17
+ /** A sixth of the frame size — how far the front frame leans diagonally past the back frame. */
18
+ const FRAME_OFFSET = FRAME_SIZE / 6;
19
+ /** Painted first (behind); flush to the top-left corner. */
20
+ const BACK_FRAME = { x: 0, y: 0 };
21
+ /** Painted second (in front) — dominates the blend; shifted diagonally toward the bottom-right by FRAME_OFFSET. */
22
+ const FRONT_FRAME = { x: FRAME_OFFSET, y: FRAME_OFFSET };
23
+ /** Half the stroke width — the minimum symmetric margin so no painted stroke is clipped by the viewBox. */
24
+ const VIEWBOX_MIN = -FRAME_THICKNESS / 2;
25
+ const VIEWBOX_SIZE = FRAME_SIZE + FRAME_OFFSET + FRAME_THICKNESS;
26
+
27
+ /**
28
+ * Two overlapping hollow square frames — the second mark in the Xertica
29
+ * "isotipos" series. Both frames render fully opaque; their intersection
30
+ * blends into a third color via a CSS mix-blend-mode, not transparency.
31
+ *
32
+ * @ai-rules
33
+ * 1. Colors are fixed brand hex values (see `patterns.ts`), not theme tokens — never make them theme-reactive.
34
+ * 2. The intersection color is a rendering side-effect of `FRAME_BLEND_MODE`, not data — never hardcode a third color per variant.
35
+ * 3. Add new color pairs via `ISOTYPE_FRAME_VARIANTS` in `frame-variants.ts`; this component never needs to change.
36
+ * 4. Pair order is `[back, front]` — the front (second) color dominates the intersection blend.
37
+ * 5. The root `isolate` class scopes the blend to these two frames — removing it would let the front frame blend against whatever sits behind this component on the page.
38
+ */
39
+ export function IsotypeFrames({
40
+ variant = 'blue-magenta',
41
+ className,
42
+ 'aria-label': ariaLabel,
43
+ ...props
44
+ }: IsotypeFramesProps) {
45
+ const [backKey, frontKey] = ISOTYPE_FRAME_VARIANTS[variant];
46
+
47
+ return (
48
+ <div
49
+ role={ariaLabel ? 'img' : undefined}
50
+ aria-label={ariaLabel}
51
+ aria-hidden={ariaLabel ? undefined : true}
52
+ className={cn('isolate aspect-square', className)}
53
+ {...props}
54
+ >
55
+ <svg
56
+ viewBox={`${VIEWBOX_MIN} ${VIEWBOX_MIN} ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`}
57
+ className="h-full w-full"
58
+ aria-hidden="true"
59
+ >
60
+ <rect
61
+ x={BACK_FRAME.x}
62
+ y={BACK_FRAME.y}
63
+ width={FRAME_SIZE}
64
+ height={FRAME_SIZE}
65
+ fill="none"
66
+ stroke={ISOTYPE_COLORS[backKey]}
67
+ strokeWidth={FRAME_THICKNESS}
68
+ />
69
+ <rect
70
+ x={FRONT_FRAME.x}
71
+ y={FRONT_FRAME.y}
72
+ width={FRAME_SIZE}
73
+ height={FRAME_SIZE}
74
+ fill="none"
75
+ stroke={ISOTYPE_COLORS[frontKey]}
76
+ strokeWidth={FRAME_THICKNESS}
77
+ style={{ mixBlendMode: FRAME_BLEND_MODE }}
78
+ />
79
+ </svg>
80
+ </div>
81
+ );
82
+ }