xertica-ui 2.8.0 → 2.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/README.md +1 -1
- package/bin/cli.ts +327 -87
- package/bin/language-config.ts +58 -18
- package/components/blocks/auth/AuthPageShell.tsx +63 -18
- package/components/blocks/auth/SocialLoginButtons.tsx +3 -3
- package/components/blocks/index.ts +1 -0
- package/components/blocks/marketing/CTASection.tsx +73 -0
- package/components/blocks/marketing/ComplianceBadgeRow.tsx +49 -0
- package/components/blocks/marketing/EvidenceTimeline.tsx +91 -0
- package/components/blocks/marketing/FeatureRow.tsx +78 -0
- package/components/blocks/marketing/Hero.tsx +125 -0
- package/components/blocks/marketing/HowItWorksSteps.tsx +90 -0
- package/components/blocks/marketing/LogoCloud.tsx +47 -0
- package/components/blocks/marketing/MarketingFooter.tsx +109 -0
- package/components/blocks/marketing/MarketingNavbar.tsx +192 -0
- package/components/blocks/marketing/PillBadge.tsx +96 -0
- package/components/blocks/marketing/SectionHeading.tsx +82 -0
- package/components/blocks/marketing/ServiceCardGrid.tsx +238 -0
- package/components/blocks/marketing/StatBand.tsx +99 -0
- package/components/blocks/marketing/TopAccentCards.tsx +91 -0
- package/components/blocks/marketing/index.ts +41 -0
- package/components/brand/index.ts +1 -0
- package/components/brand/isotype/Isotype.tsx +64 -0
- package/components/brand/isotype/IsotypeDiagonal.tsx +51 -0
- package/components/brand/isotype/IsotypeFrames.tsx +82 -0
- package/components/brand/isotype/IsotypeMini.tsx +65 -0
- package/components/brand/isotype/IsotypeTwist.tsx +68 -0
- package/components/brand/isotype/frame-variants.ts +20 -0
- package/components/brand/isotype/index.ts +9 -0
- package/components/brand/isotype/isotype-diagonal.mdx +52 -0
- package/components/brand/isotype/isotype-diagonal.stories.tsx +44 -0
- package/components/brand/isotype/isotype-diagonal.test.tsx +180 -0
- package/components/brand/isotype/isotype-frames.mdx +50 -0
- package/components/brand/isotype/isotype-frames.stories.tsx +63 -0
- package/components/brand/isotype/isotype-frames.test.tsx +92 -0
- package/components/brand/isotype/isotype-mini.mdx +61 -0
- package/components/brand/isotype/isotype-mini.stories.tsx +75 -0
- package/components/brand/isotype/isotype-mini.test.tsx +45 -0
- package/components/brand/isotype/isotype-twist.test.tsx +71 -0
- package/components/brand/isotype/isotype.mdx +61 -0
- package/components/brand/isotype/isotype.stories.tsx +74 -0
- package/components/brand/isotype/isotype.test.tsx +41 -0
- package/components/brand/isotype/mini-patterns.ts +50 -0
- package/components/brand/isotype/patterns.ts +83 -0
- package/components/brand/isotype/twist-variants.ts +20 -0
- package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +3 -6
- package/components/pages/index.ts +1 -0
- package/components/pages/landing-page/LandingPage.tsx +360 -0
- package/components/pages/landing-page/index.ts +1 -0
- package/components/pages/login-page/LoginPage.tsx +3 -6
- package/components/pages/reset-password-page/ResetPasswordPage.tsx +1 -4
- package/components/pages/verify-email-page/VerifyEmailPage.tsx +2 -5
- package/components/ui/button/button.test.tsx +1 -1
- package/components/ui/button/button.tsx +7 -6
- package/components/ui/card/card.test.tsx +12 -0
- package/components/ui/card/card.tsx +5 -3
- package/contexts/BrandColorsContext.tsx +9 -5
- package/contexts/theme-data.ts +32 -5
- package/dist/AssistantChart-BU48lxL2.js +3383 -0
- package/dist/AssistantChart-CoN6ga6Z.js +3383 -0
- package/dist/AssistantChart-DIRYJ7WY.js +3383 -0
- package/dist/AssistantChart-DOnY1ZlD.cjs +3551 -0
- package/dist/AssistantChart-DjM3qnp5.cjs +3551 -0
- package/dist/AssistantChart-hxPyE1S9.cjs +3551 -0
- package/dist/AudioPlayer-CpihsbaD.js +1278 -0
- package/dist/AudioPlayer-CvGajzMC.cjs +1282 -0
- package/dist/AudioPlayer-CvHpFqDG.cjs +1282 -0
- package/dist/AudioPlayer-D4S7GQou.js +1278 -0
- package/dist/BrandColorsContext-BNKLUVcU.js +829 -0
- package/dist/BrandColorsContext-DNZGHItQ.cjs +836 -0
- package/dist/BrandColorsContext-Ug_TZ8yn.cjs +846 -0
- package/dist/BrandColorsContext-z0O2LlZz.js +839 -0
- package/dist/CodeBlock-BkaZf-R8.cjs +223 -0
- package/dist/CodeBlock-CKu8LgbP.cjs +223 -0
- package/dist/CodeBlock-DiLS5LLI.js +221 -0
- package/dist/CodeBlock-zdylBGdZ.js +221 -0
- package/dist/CustomTooltipContent-BAR5qCQg.js +33 -0
- package/dist/CustomTooltipContent-CBLHBxX8.cjs +54 -0
- package/dist/CustomTooltipContent-CjWm3jjV.js +33 -0
- package/dist/CustomTooltipContent-DTV9MN9E.cjs +54 -0
- package/dist/EvidenceTimeline-DrRk4yqb.cjs +560 -0
- package/dist/EvidenceTimeline-l4MaV4C7.js +546 -0
- package/dist/FeatureCard-CIbBHqeY.cjs +497 -0
- package/dist/FeatureCard-DtoKPSfW.cjs +497 -0
- package/dist/FeatureCard-P4iEpBmB.js +485 -0
- package/dist/FeatureCard-e3f8bnF0.js +485 -0
- package/dist/ImageWithFallback-C1ww9Hg3.cjs +122 -0
- package/dist/ImageWithFallback-CBrkAEo3.cjs +122 -0
- package/dist/ImageWithFallback-CGk5zPH-.js +98 -0
- package/dist/ImageWithFallback-CfnoFFSP.js +98 -0
- package/dist/Isotype--fRZWwS6.js +85 -0
- package/dist/Isotype-0dpLqetB.js +85 -0
- package/dist/Isotype-B8XLEXSB.cjs +92 -0
- package/dist/Isotype-ChKwN7e-.cjs +91 -0
- package/dist/IsotypeDiagonal-CuPYPP4f.js +84 -0
- package/dist/IsotypeDiagonal-eNcOXRAc.cjs +91 -0
- package/dist/IsotypeDiagonal-oiwpXZhE.js +104 -0
- package/dist/IsotypeDiagonal-xMFoFXkb.cjs +109 -0
- package/dist/IsotypeFrames-BcWtyAWp.cjs +83 -0
- package/dist/IsotypeFrames-BnvSJKiH.cjs +73 -0
- package/dist/IsotypeFrames-iDRTRuIu.js +79 -0
- package/dist/IsotypeFrames-qoXOvOJt.js +69 -0
- package/dist/IsotypeMini-CQtWVo3h.js +126 -0
- package/dist/IsotypeMini-Cu_sW5yN.cjs +134 -0
- package/dist/IsotypeTwist-BTdNL4iV.js +106 -0
- package/dist/IsotypeTwist-DvsUgZs_.cjs +115 -0
- package/dist/LanguageContext-B1aOXO-K.js +1987 -0
- package/dist/LanguageContext-BP0k7AzI.js +1993 -0
- package/dist/LanguageContext-CChcIHIG.cjs +1998 -0
- package/dist/LanguageContext-ubC7TSs_.cjs +1992 -0
- package/dist/LanguageSelector-BLFPXGo8.cjs +48 -0
- package/dist/LanguageSelector-CYRzWkRZ.cjs +48 -0
- package/dist/LanguageSelector-CyHjZgC4.cjs +48 -0
- package/dist/LanguageSelector-DE8lIySO.js +46 -0
- package/dist/LanguageSelector-DUr1hjBW.js +46 -0
- package/dist/LanguageSelector-DjbWIKWC.js +46 -0
- package/dist/LogoCloud-C_uMKVNg.cjs +513 -0
- package/dist/LogoCloud-DJsMPNPh.js +500 -0
- package/dist/MarketingFooter-B_ycEdTJ.cjs +727 -0
- package/dist/MarketingFooter-Bhx-2Vlr.cjs +727 -0
- package/dist/MarketingFooter-Br1MGK9e.js +699 -0
- package/dist/MarketingFooter-BwE1vmeX.cjs +700 -0
- package/dist/MarketingFooter-CsZ1_xL9.js +709 -0
- package/dist/MarketingFooter-DCwf78Q4.cjs +716 -0
- package/dist/MarketingFooter-KH2r-OOL.js +709 -0
- package/dist/MarketingFooter-nB7G7gQ5.js +682 -0
- package/dist/SectionHeading-DEeOKcAd.cjs +857 -0
- package/dist/SectionHeading-DMNObcwm.js +838 -0
- package/dist/ThemeToggle--CGJjQJv.cjs +219 -0
- package/dist/ThemeToggle-Bn_7Pmfe.js +215 -0
- package/dist/ThemeToggle-CoL8Ady7.js +215 -0
- package/dist/ThemeToggle-D2Wzm5bn.cjs +219 -0
- package/dist/VerifyEmailPage-90lK6pP4.js +3258 -0
- package/dist/VerifyEmailPage-BJX1Rter.cjs +3320 -0
- package/dist/VerifyEmailPage-BSWvOgry.js +3310 -0
- package/dist/VerifyEmailPage-CDqIkhx-.js +3224 -0
- package/dist/VerifyEmailPage-Crxje3ZB.js +3323 -0
- package/dist/VerifyEmailPage-D2ShbuEN.cjs +3333 -0
- package/dist/VerifyEmailPage-DEBZXQ7c.cjs +3310 -0
- package/dist/VerifyEmailPage-DGaX-jZv.cjs +3268 -0
- package/dist/VerifyEmailPage-ZtOPkgOA.cjs +3234 -0
- package/dist/VerifyEmailPage-s927GSiR.js +3300 -0
- package/dist/XerticaOrbe-BHex09xs.js +1925 -0
- package/dist/XerticaOrbe-BKW3LJEz.js +1925 -0
- package/dist/XerticaOrbe-Br2i99U_.cjs +1927 -0
- package/dist/XerticaOrbe-kjFmuyx-.cjs +1927 -0
- package/dist/XerticaProvider-Bi5lKC9N.js +44 -0
- package/dist/XerticaProvider-BrPMVhiv.js +44 -0
- package/dist/XerticaProvider-CnX5kt5W.cjs +46 -0
- package/dist/XerticaProvider-DEKNJTlN.js +44 -0
- package/dist/XerticaProvider-X_wavDYA.cjs +46 -0
- package/dist/XerticaProvider-b3n_oL8w.cjs +46 -0
- package/dist/alert-dialog--qdlmB5i.cjs +872 -0
- package/dist/alert-dialog-C48TOdYU.cjs +872 -0
- package/dist/alert-dialog-CEPpnpXt.js +819 -0
- package/dist/alert-dialog-NjflitDa.js +819 -0
- package/dist/assistant.cjs.js +2 -2
- package/dist/assistant.es.js +2 -2
- package/dist/avatar-CtEKgg7s.cjs +77 -0
- package/dist/avatar-D03MNQxs.js +54 -0
- package/dist/avatar-Df7KgSvs.js +54 -0
- package/dist/avatar-Gy0_Xu9i.cjs +77 -0
- package/dist/blocks.cjs.js +33 -18
- package/dist/blocks.es.js +2 -2
- package/dist/brand.cjs.js +110 -4
- package/dist/brand.es.js +98 -4
- package/dist/breadcrumb-BGO5PZPG.js +90 -0
- package/dist/breadcrumb-BvGAY4ne.cjs +98 -0
- package/dist/breadcrumb-CU-punra.js +90 -0
- package/dist/breadcrumb-DjV9wfX1.cjs +98 -0
- package/dist/button-B05czY_8.js +62 -0
- package/dist/button-B3SE1liJ.js +62 -0
- package/dist/button-BMDlWZR8.cjs +85 -0
- package/dist/button-DVhloDtr.cjs +85 -0
- package/dist/cli.js +280 -62
- package/dist/components/blocks/auth/AuthPageShell.d.ts +21 -5
- package/dist/components/blocks/index.d.ts +1 -0
- package/dist/components/blocks/marketing/CTASection.d.ts +28 -0
- package/dist/components/blocks/marketing/ComplianceBadgeRow.d.ts +23 -0
- package/dist/components/blocks/marketing/EvidenceTimeline.d.ts +26 -0
- package/dist/components/blocks/marketing/FeatureRow.d.ts +25 -0
- package/dist/components/blocks/marketing/Hero.d.ts +35 -0
- package/dist/components/blocks/marketing/HowItWorksSteps.d.ts +29 -0
- package/dist/components/blocks/marketing/LogoCloud.d.ts +22 -0
- package/dist/components/blocks/marketing/MarketingFooter.d.ts +42 -0
- package/dist/components/blocks/marketing/MarketingNavbar.d.ts +47 -0
- package/dist/components/blocks/marketing/PillBadge.d.ts +21 -0
- package/dist/components/blocks/marketing/SectionHeading.d.ts +23 -0
- package/dist/components/blocks/marketing/ServiceCardGrid.d.ts +65 -0
- package/dist/components/blocks/marketing/StatBand.d.ts +25 -0
- package/dist/components/blocks/marketing/TopAccentCards.d.ts +29 -0
- package/dist/components/blocks/marketing/index.d.ts +28 -0
- package/dist/components/brand/index.d.ts +1 -0
- package/dist/components/brand/isotype/Isotype.d.ts +27 -0
- package/dist/components/brand/isotype/IsotypeDiagonal.d.ts +19 -0
- package/dist/components/brand/isotype/IsotypeFrames.d.ts +21 -0
- package/dist/components/brand/isotype/IsotypeMini.d.ts +26 -0
- package/dist/components/brand/isotype/IsotypeTwist.d.ts +22 -0
- package/dist/components/brand/isotype/frame-variants.d.ts +11 -0
- package/dist/components/brand/isotype/index.d.ts +9 -0
- package/dist/components/brand/isotype/mini-patterns.d.ts +25 -0
- package/dist/components/brand/isotype/patterns.d.ts +42 -0
- package/dist/components/brand/isotype/twist-variants.d.ts +11 -0
- package/dist/components/pages/forgot-password-page/ForgotPasswordPage.d.ts +2 -2
- package/dist/components/pages/index.d.ts +1 -0
- package/dist/components/pages/landing-page/LandingPage.d.ts +18 -0
- package/dist/components/pages/landing-page/index.d.ts +1 -0
- package/dist/components/pages/login-page/LoginPage.d.ts +2 -2
- package/dist/components/pages/verify-email-page/VerifyEmailPage.d.ts +1 -1
- package/dist/components/ui/card/card.d.ts +3 -1
- package/dist/contexts/theme-data.d.ts +6 -0
- package/dist/dropdown-menu-ChQa9zW3.cjs +225 -0
- package/dist/dropdown-menu-DRvnsQAt.cjs +225 -0
- package/dist/dropdown-menu-DUk81i5Y.js +190 -0
- package/dist/dropdown-menu-NlUHhVmK.js +190 -0
- package/dist/hooks.cjs.js +2 -2
- package/dist/hooks.es.js +2 -2
- package/dist/index.cjs.js +57 -41
- package/dist/index.es.js +25 -25
- package/dist/input-Csp_Metb.cjs +91 -0
- package/dist/input-DVRwGVLx.js +65 -0
- package/dist/input-Dqyj7Hdw.cjs +91 -0
- package/dist/input-dOgrFQ-3.js +65 -0
- package/dist/layout.cjs.js +1 -1
- package/dist/layout.es.js +1 -1
- package/dist/media.cjs.js +1 -1
- package/dist/media.es.js +1 -1
- package/dist/pages.cjs.js +2 -1
- package/dist/pages.es.js +1 -1
- package/dist/rich-text-editor-7K7UjLJN.cjs +2968 -0
- package/dist/rich-text-editor-B3jLP2bm.cjs +2968 -0
- package/dist/rich-text-editor-BntMVQsB.js +2894 -0
- package/dist/rich-text-editor-Diz_onyB.js +2894 -0
- package/dist/rich-text-editor-RWvK4hd3.cjs +2968 -0
- package/dist/rich-text-editor-gauEkzPM.js +2894 -0
- package/dist/select-B6PygS38.cjs +161 -0
- package/dist/select-BD763J5Y.cjs +161 -0
- package/dist/select-QJDKZITN.js +130 -0
- package/dist/select-q7aDfuPn.js +130 -0
- package/dist/sidebar-Bkk1YAsD.cjs +1154 -0
- package/dist/sidebar-CuzYeOSz.cjs +1154 -0
- package/dist/sidebar-CwlW3LXE.js +1151 -0
- package/dist/sidebar-DNkf-Ug6.js +1151 -0
- package/dist/sidebar-DaS3fplw.cjs +1154 -0
- package/dist/sidebar-FDCY42oP.js +1151 -0
- package/dist/skeleton-BtBwJ39W.js +79 -0
- package/dist/skeleton-CJyg0eJ1.cjs +88 -0
- package/dist/skeleton-D8WaauK0.cjs +88 -0
- package/dist/skeleton-DBYtjaZW.cjs +88 -0
- package/dist/skeleton-DY_lSFWf.js +79 -0
- package/dist/skeleton-Df65Ox10.js +79 -0
- package/dist/slider-BKV_vvsh.cjs +78 -0
- package/dist/slider-B_lUjFNa.cjs +78 -0
- package/dist/slider-CtBpIaU4.js +56 -0
- package/dist/slider-KgWmSJbx.js +56 -0
- package/dist/tooltip-BPttuPxU.js +48 -0
- package/dist/tooltip-CIKRzQqE.cjs +72 -0
- package/dist/tooltip-CoDByjE_.cjs +72 -0
- package/dist/tooltip-_MAwsiOL.js +48 -0
- package/dist/ui.cjs.js +13 -13
- package/dist/ui.es.js +13 -13
- package/dist/xertica-assistant-B1xAl2Vn.js +2173 -0
- package/dist/xertica-assistant-BBUMaUXw.js +2173 -0
- package/dist/xertica-assistant-CWuBwfzt.cjs +2180 -0
- package/dist/xertica-assistant-D2hlwiEP.js +2173 -0
- package/dist/xertica-assistant-DN2J_sQQ.cjs +2180 -0
- package/dist/xertica-assistant-DdYS6WXT.cjs +2180 -0
- package/dist/xertica-ui.css +1 -1
- package/llms-compact.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/styles/xertica/base.css +8 -0
- package/styles/xertica/tokens.css +1 -0
- package/templates/package.json +1 -1
- package/templates/src/app/components/AuthGuard.tsx +6 -0
- package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +1 -4
- package/templates/src/features/auth/ui/LoginContent.tsx +1 -4
- package/templates/src/features/auth/ui/ResetPasswordContent.tsx +1 -4
- package/templates/src/features/auth/ui/VerifyEmailContent.tsx +1 -4
- package/templates/src/locales/en/pages/landing.json +175 -0
- package/templates/src/locales/es/pages/landing.json +175 -0
- package/templates/src/locales/pt-BR/pages/landing.json +175 -0
- package/templates/src/pages/LandingPage.tsx +3 -0
- package/templates/src/styles/xertica/tokens.css +1 -0
package/bin/language-config.ts
CHANGED
|
@@ -299,24 +299,7 @@ export function generateAppTsx(selectedCodes: string[], disableDarkMode: boolean
|
|
|
299
299
|
const selectedLangs = SUPPORTED_LANGUAGES.filter(l => selectedCodes.includes(l.code));
|
|
300
300
|
const isMonolingual = selectedLangs.length === 1;
|
|
301
301
|
const disableDarkModeProp = disableDarkMode ? `\n disableDarkMode={true}` : '';
|
|
302
|
-
const
|
|
303
|
-
selectedLangs.length === SUPPORTED_LANGUAGES.length &&
|
|
304
|
-
selectedLangs.every(l => DEFAULT_SELECTION.includes(l.code));
|
|
305
|
-
|
|
306
|
-
// Decide whether and how to render the `availableLanguages` prop on
|
|
307
|
-
// XerticaProvider. When the user selected all 3 defaults, we can omit the
|
|
308
|
-
// prop (the library default matches). Otherwise we emit the explicit set.
|
|
309
|
-
const languagesArrayLiteral = selectedLangs
|
|
310
|
-
.map(
|
|
311
|
-
l => ` { code: '${l.code}', label: '${l.label}', shortLabel: '${l.shortLabel}' },`
|
|
312
|
-
)
|
|
313
|
-
.join('\n');
|
|
314
|
-
|
|
315
|
-
const availableLanguagesProp = isAllDefaults
|
|
316
|
-
? ''
|
|
317
|
-
: `\n availableLanguages={[
|
|
318
|
-
${languagesArrayLiteral}
|
|
319
|
-
]}`;
|
|
302
|
+
const availableLanguagesProp = buildAvailableLanguagesProp(selectedCodes, ' ');
|
|
320
303
|
|
|
321
304
|
const monolingualBanner = isMonolingual
|
|
322
305
|
? `\n// This project is configured for a single language — the LanguageSelector
|
|
@@ -376,8 +359,65 @@ export default function App() {
|
|
|
376
359
|
`;
|
|
377
360
|
}
|
|
378
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Generate the App.tsx source for a minimal "web page" project — renders the
|
|
364
|
+
* marketing `LandingPage` directly under `XerticaProvider`, with none of the
|
|
365
|
+
* router/query-client/auth machinery the full system scaffold needs (the
|
|
366
|
+
* landing page itself has no dependency on any of it).
|
|
367
|
+
*/
|
|
368
|
+
export function generateLandingAppTsx(
|
|
369
|
+
selectedCodes: string[],
|
|
370
|
+
disableDarkMode: boolean = false
|
|
371
|
+
): string {
|
|
372
|
+
const disableDarkModeProp = disableDarkMode ? `\n disableDarkMode={true}` : '';
|
|
373
|
+
const availableLanguagesProp = buildAvailableLanguagesProp(selectedCodes, ' ');
|
|
374
|
+
|
|
375
|
+
return `import React from 'react';
|
|
376
|
+
import { XerticaProvider } from 'xertica-ui/brand';
|
|
377
|
+
import { LandingPage } from 'xertica-ui/pages';
|
|
378
|
+
|
|
379
|
+
export default function App() {
|
|
380
|
+
return (
|
|
381
|
+
<XerticaProvider
|
|
382
|
+
useCustomTokens={true}${availableLanguagesProp}${disableDarkModeProp}
|
|
383
|
+
>
|
|
384
|
+
<LandingPage />
|
|
385
|
+
</XerticaProvider>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
|
|
379
391
|
// ── Internal ─────────────────────────────────────────────────────────────────
|
|
380
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Build the `availableLanguages` prop source for `<XerticaProvider>`. When the
|
|
395
|
+
* selection matches the library's default 3 languages, the prop is omitted
|
|
396
|
+
* entirely (the library default already matches). `indent` is the whitespace
|
|
397
|
+
* prefix the prop line itself should have — array items are indented one
|
|
398
|
+
* level deeper.
|
|
399
|
+
*/
|
|
400
|
+
function buildAvailableLanguagesProp(selectedCodes: string[], indent: string): string {
|
|
401
|
+
const selectedLangs = SUPPORTED_LANGUAGES.filter(l => selectedCodes.includes(l.code));
|
|
402
|
+
const isAllDefaults =
|
|
403
|
+
selectedLangs.length === SUPPORTED_LANGUAGES.length &&
|
|
404
|
+
selectedLangs.every(l => DEFAULT_SELECTION.includes(l.code));
|
|
405
|
+
|
|
406
|
+
if (isAllDefaults) return '';
|
|
407
|
+
|
|
408
|
+
const itemIndent = `${indent} `;
|
|
409
|
+
const languagesArrayLiteral = selectedLangs
|
|
410
|
+
.map(
|
|
411
|
+
l =>
|
|
412
|
+
`${itemIndent}{ code: '${l.code}', label: '${l.label}', shortLabel: '${l.shortLabel}' },`
|
|
413
|
+
)
|
|
414
|
+
.join('\n');
|
|
415
|
+
|
|
416
|
+
return `\n${indent}availableLanguages={[
|
|
417
|
+
${languagesArrayLiteral}
|
|
418
|
+
${indent}]}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
381
421
|
/** Convert a BCP-47 code to a safe JS identifier (e.g. 'pt-BR' → 'ptBR'). */
|
|
382
422
|
function toJsIdent(code: string): string {
|
|
383
423
|
return code.replace(/[^a-zA-Z0-9]/g, '');
|
|
@@ -1,35 +1,80 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ImageWithFallback } from '../../figma/ImageWithFallback';
|
|
3
3
|
import { LanguageSelector } from '../../brand/language-selector';
|
|
4
|
+
import { IsotypeTwist } from '../../brand/isotype';
|
|
5
|
+
import { cn } from '../../shared/utils';
|
|
4
6
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export type AuthPageShellProps =
|
|
8
|
+
| {
|
|
9
|
+
/** Hero photo on the left (default). Requires `imageSrc`/`imageAlt`. */
|
|
10
|
+
leftPanel?: 'image';
|
|
11
|
+
imageSrc: string;
|
|
12
|
+
imageAlt: string;
|
|
13
|
+
/**
|
|
14
|
+
* Background classes for the hero image overlay gradient. Defaults to the
|
|
15
|
+
* shared `--gradient-diagonal` theme token; pass a Tailwind gradient to
|
|
16
|
+
* override it for a specific auth page without affecting the others.
|
|
17
|
+
*/
|
|
18
|
+
gradientClassName?: string;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
/**
|
|
23
|
+
* Brand isotype mark on a solid `bg-muted` panel — no photo, no
|
|
24
|
+
* gradient. Pairs with a lighter `bg-card` form panel on the right,
|
|
25
|
+
* the inverse of the `image` variant's `bg-muted` form panel.
|
|
26
|
+
*/
|
|
27
|
+
leftPanel: 'isotype';
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
};
|
|
10
30
|
|
|
11
31
|
/**
|
|
12
32
|
* Shared two-column shell used by every auth page (Login, ForgotPassword,
|
|
13
|
-
* ResetPassword, VerifyEmail): hero image
|
|
14
|
-
* with a language selector on the right.
|
|
33
|
+
* ResetPassword, VerifyEmail): a left panel (hero image or brand isotype) and
|
|
34
|
+
* a centered form panel with a language selector on the right.
|
|
15
35
|
*/
|
|
16
|
-
export function AuthPageShell(
|
|
36
|
+
export function AuthPageShell(props: AuthPageShellProps) {
|
|
37
|
+
const { children } = props;
|
|
38
|
+
|
|
17
39
|
return (
|
|
18
40
|
<div className="h-screen w-full flex overflow-y-auto">
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
{props.leftPanel === 'isotype' ? (
|
|
42
|
+
<div className="hidden lg:flex lg:flex-1 items-center justify-center bg-muted">
|
|
43
|
+
<IsotypeTwist variant="magenta-blue" className="w-full max-w-sm" />
|
|
44
|
+
</div>
|
|
45
|
+
) : (
|
|
46
|
+
<div className="hidden lg:flex lg:flex-1 relative overflow-hidden">
|
|
47
|
+
<ImageWithFallback
|
|
48
|
+
src={props.imageSrc}
|
|
49
|
+
alt={props.imageAlt}
|
|
50
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
51
|
+
/>
|
|
52
|
+
<div
|
|
53
|
+
className={cn(
|
|
54
|
+
'absolute inset-0 opacity-80',
|
|
55
|
+
props.gradientClassName ?? 'bg-[image:var(--gradient-diagonal)]'
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
27
60
|
|
|
28
|
-
<div
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
'flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8 lg:flex-none lg:w-1/2 relative',
|
|
64
|
+
props.leftPanel === 'isotype' ? 'bg-card' : 'bg-muted'
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
29
67
|
<div className="absolute top-4 right-4 z-20">
|
|
30
68
|
<LanguageSelector variant="minimal" showIcon={false} />
|
|
31
69
|
</div>
|
|
32
|
-
|
|
70
|
+
{props.leftPanel !== 'isotype' && (
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
'absolute inset-0 lg:hidden opacity-10 dark:opacity-5',
|
|
74
|
+
props.gradientClassName ?? 'bg-[image:var(--gradient-diagonal)]'
|
|
75
|
+
)}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
33
78
|
<div className="w-full max-w-sm space-y-6 relative z-10">{children}</div>
|
|
34
79
|
</div>
|
|
35
80
|
</div>
|
|
@@ -37,7 +37,7 @@ export function SocialLoginButtons({
|
|
|
37
37
|
<Button
|
|
38
38
|
type="button"
|
|
39
39
|
variant="outline"
|
|
40
|
-
className="w-full justify-center"
|
|
40
|
+
className="w-full justify-center shadow-xs"
|
|
41
41
|
onClick={() => onSocialLogin('Google')}
|
|
42
42
|
>
|
|
43
43
|
<svg className="w-5 h-5 mr-2" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -67,7 +67,7 @@ export function SocialLoginButtons({
|
|
|
67
67
|
<Button
|
|
68
68
|
type="button"
|
|
69
69
|
variant="outline"
|
|
70
|
-
className="w-full justify-center"
|
|
70
|
+
className="w-full justify-center shadow-xs"
|
|
71
71
|
onClick={() => onSocialLogin('MT Login')}
|
|
72
72
|
>
|
|
73
73
|
<Lock className="w-5 h-5 mr-2 text-[var(--chart-4)]" />
|
|
@@ -79,7 +79,7 @@ export function SocialLoginButtons({
|
|
|
79
79
|
<Button
|
|
80
80
|
type="button"
|
|
81
81
|
variant="outline"
|
|
82
|
-
className="w-full justify-center font-normal"
|
|
82
|
+
className="w-full justify-center font-normal shadow-xs"
|
|
83
83
|
onClick={() => onSocialLogin('gov.br')}
|
|
84
84
|
>
|
|
85
85
|
<span>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button } from '../../ui/button';
|
|
3
|
+
import { cn } from '../../shared/utils';
|
|
4
|
+
|
|
5
|
+
export interface CTASectionAction {
|
|
6
|
+
label: string;
|
|
7
|
+
href?: string;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CTASectionProps extends React.HTMLAttributes<HTMLElement> {
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
cta: CTASectionAction;
|
|
15
|
+
secondaryCta?: CTASectionAction;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Full-width dark band prompting a primary conversion action.
|
|
20
|
+
*
|
|
21
|
+
* @description
|
|
22
|
+
* A high-impact closing section for marketing pages: a bold left-aligned
|
|
23
|
+
* headline, optional supporting copy, and up to two CTA buttons on a flat
|
|
24
|
+
* `bg-sidebar` surface — the same token-driven dark surface used elsewhere
|
|
25
|
+
* on this page (e.g. the methodology section) — so it stays legible across
|
|
26
|
+
* all Xertica-ui themes without per-theme contrast tuning or a gradient.
|
|
27
|
+
*
|
|
28
|
+
* @ai-rules
|
|
29
|
+
* 1. The background is a flat `bg-sidebar` surface with a `border-sidebar-border` hairline — never reintroduce a gradient or hardcode colors here.
|
|
30
|
+
* 2. Both buttons use their natural variants (`default` for `cta`, `outline` for `secondaryCta`) with no color overrides — they already contrast correctly against `bg-sidebar`; do not force-style them with one-off colors.
|
|
31
|
+
* 3. Each `cta`/`secondaryCta` needs either `href` (renders an `<a>`) or `onClick`.
|
|
32
|
+
*/
|
|
33
|
+
export function CTASection({
|
|
34
|
+
title,
|
|
35
|
+
description,
|
|
36
|
+
cta,
|
|
37
|
+
secondaryCta,
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: CTASectionProps) {
|
|
41
|
+
return (
|
|
42
|
+
<section
|
|
43
|
+
className={cn(
|
|
44
|
+
'w-full rounded-[var(--radius-card)] border border-sidebar-border bg-sidebar px-6 py-16 md:py-20',
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<div className="flex flex-col gap-8 md:flex-row md:items-center md:justify-between">
|
|
50
|
+
<div className="max-w-xl">
|
|
51
|
+
<h2 className="text-2xl font-bold text-sidebar-foreground md:text-4xl">{title}</h2>
|
|
52
|
+
{description && <p className="mt-6 text-sidebar-foreground/70">{description}</p>}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="flex flex-col gap-3 sm:flex-row md:shrink-0">
|
|
56
|
+
<Button asChild={!!cta.href} onClick={cta.onClick}>
|
|
57
|
+
{cta.href ? <a href={cta.href}>{cta.label}</a> : cta.label}
|
|
58
|
+
</Button>
|
|
59
|
+
|
|
60
|
+
{secondaryCta && (
|
|
61
|
+
<Button variant="outline" asChild={!!secondaryCta.href} onClick={secondaryCta.onClick}>
|
|
62
|
+
{secondaryCta.href ? (
|
|
63
|
+
<a href={secondaryCta.href}>{secondaryCta.label}</a>
|
|
64
|
+
) : (
|
|
65
|
+
secondaryCta.label
|
|
66
|
+
)}
|
|
67
|
+
</Button>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../shared/utils';
|
|
3
|
+
|
|
4
|
+
export interface ComplianceBadge {
|
|
5
|
+
icon: React.ReactNode;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ComplianceBadgeRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
badges: ComplianceBadge[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A row of certification/compliance badges (e.g. SOC 2, GDPR, WCAG 2.1 AA).
|
|
16
|
+
*
|
|
17
|
+
* @description
|
|
18
|
+
* Renders a left-aligned grid where each badge pairs a token-radius outlined
|
|
19
|
+
* icon chip with a label and an optional short description, built entirely
|
|
20
|
+
* from theme tokens so it reskins across all Xertica-ui themes and dark mode.
|
|
21
|
+
*
|
|
22
|
+
* @ai-rules
|
|
23
|
+
* 1. Provide `badges` as `{ icon, label, description? }` — `icon` should be a small lucide-react element.
|
|
24
|
+
* 2. Keep `label` and `description` short — badges sit in a `grid-cols-2 sm:grid-cols-4` layout, so each column has limited width.
|
|
25
|
+
* 3. Do not hand-roll badge styling elsewhere — this component owns the icon-chip/label/description layout.
|
|
26
|
+
*/
|
|
27
|
+
export function ComplianceBadgeRow({ badges, className, ...props }: ComplianceBadgeRowProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn('grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-4', className)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{badges.map((badge, index) => (
|
|
34
|
+
<div
|
|
35
|
+
key={`${badge.label}-${index}`}
|
|
36
|
+
className="flex flex-col items-start gap-3 text-left"
|
|
37
|
+
>
|
|
38
|
+
<div className="flex items-center justify-center rounded-[var(--radius)] border border-border bg-background p-3 [&>svg]:size-6">
|
|
39
|
+
{badge.icon}
|
|
40
|
+
</div>
|
|
41
|
+
<p className="text-sm font-medium text-foreground">{badge.label}</p>
|
|
42
|
+
{badge.description && (
|
|
43
|
+
<p className="text-xs text-muted-foreground">{badge.description}</p>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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 EvidenceTimelineItem {
|
|
30
|
+
number?: string | number;
|
|
31
|
+
icon?: React.ReactNode;
|
|
32
|
+
title: string;
|
|
33
|
+
description: string;
|
|
34
|
+
color?: BlockColor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EvidenceTimelineProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
38
|
+
items: EvidenceTimelineItem[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Vertical numbered timeline with a connecting line and colored step dots.
|
|
43
|
+
*
|
|
44
|
+
* @description
|
|
45
|
+
* Renders a "how our methodology works" style list: each item gets a
|
|
46
|
+
* colored dot (icon or step number) on a shared vertical line, with a
|
|
47
|
+
* title and description to its right.
|
|
48
|
+
*
|
|
49
|
+
* @ai-rules
|
|
50
|
+
* 1. Provide `icon` for a lucide-react icon in the dot, or `number` for a step label; if neither is set the 1-based index is used.
|
|
51
|
+
* 2. Each item's `color` maps to `colorTokens` — never hardcode colors.
|
|
52
|
+
* 3. This block has no outer card chrome; wrap it in a `<Card>` or section if chrome is needed.
|
|
53
|
+
*/
|
|
54
|
+
export function EvidenceTimeline({ items, className, ...props }: EvidenceTimelineProps) {
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn('relative pl-12', className)} {...props}>
|
|
57
|
+
<div aria-hidden="true" className="absolute left-4 top-4 bottom-4 w-px bg-border" />
|
|
58
|
+
|
|
59
|
+
<ol className="relative">
|
|
60
|
+
{items.map((item, index) => {
|
|
61
|
+
const { bg, icon: iconColor } = colorTokens[item.color ?? 'primary'];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<li key={index} className="relative flex items-start gap-4 pb-10 last:pb-0">
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
'absolute -left-12 top-0 flex size-8 shrink-0 items-center justify-center rounded-[var(--radius)] z-10 border border-current/20',
|
|
68
|
+
bg
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<span
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex items-center justify-center text-sm font-semibold [&>svg]:size-4',
|
|
74
|
+
iconColor
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{item.icon ?? (item.number ?? index + 1)}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="min-w-0">
|
|
82
|
+
<p className="font-semibold">{item.title}</p>
|
|
83
|
+
<p className="mt-2 text-sm opacity-70">{item.description}</p>
|
|
84
|
+
</div>
|
|
85
|
+
</li>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</ol>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { CheckCircle2 } from 'lucide-react';
|
|
3
|
+
import { cn } from '../../shared/utils';
|
|
4
|
+
|
|
5
|
+
export interface FeatureRowProps extends React.HTMLAttributes<HTMLElement> {
|
|
6
|
+
eyebrow?: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
bullets?: string[];
|
|
10
|
+
visual: React.ReactNode;
|
|
11
|
+
reverse?: boolean;
|
|
12
|
+
tone?: 'default' | 'muted' | 'card';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Alternating text + visual marketing section.
|
|
17
|
+
*
|
|
18
|
+
* @description
|
|
19
|
+
* A two-column "how it works" / definition row: a text block (eyebrow,
|
|
20
|
+
* title, description, optional checklist) paired with a consumer-supplied
|
|
21
|
+
* visual (image, illustration, or custom node). Stacks on mobile and
|
|
22
|
+
* alternates sides on desktop via `reverse`.
|
|
23
|
+
*
|
|
24
|
+
* @ai-rules
|
|
25
|
+
* 1. Chain multiple `FeatureRow`s and alternate `reverse` to build a "how it works" narrative.
|
|
26
|
+
* 2. Pass the visual as `<ImageWithFallback />` or any custom `ReactNode`; this component never renders an image itself.
|
|
27
|
+
* 3. Use `tone="muted"` on every other row to create a subtle section break instead of adding manual dividers; use `tone="card"` when a theme needs a stronger, genuinely lighter break (some themes set `--muted` equal to `--background`).
|
|
28
|
+
*/
|
|
29
|
+
export function FeatureRow({
|
|
30
|
+
eyebrow,
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
bullets,
|
|
34
|
+
visual,
|
|
35
|
+
reverse = false,
|
|
36
|
+
tone = 'default',
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}: FeatureRowProps) {
|
|
40
|
+
return (
|
|
41
|
+
<section
|
|
42
|
+
className={cn(
|
|
43
|
+
'py-16 md:py-24',
|
|
44
|
+
tone === 'muted' ? 'bg-muted' : tone === 'card' ? 'bg-card' : 'bg-background',
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<div className="mx-auto grid max-w-6xl gap-12 px-4 sm:px-6 lg:grid-cols-2 lg:items-center lg:px-8">
|
|
50
|
+
<div className={cn(reverse ? 'lg:order-last' : 'lg:order-first')}>
|
|
51
|
+
{eyebrow && (
|
|
52
|
+
<p className="text-sm font-semibold uppercase tracking-wide text-primary">
|
|
53
|
+
{eyebrow}
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
<h2 className={cn('text-2xl font-bold text-foreground md:text-3xl', eyebrow && 'mt-2')}>
|
|
57
|
+
{title}
|
|
58
|
+
</h2>
|
|
59
|
+
{description && (
|
|
60
|
+
<p className="mt-6 text-muted-foreground leading-relaxed">{description}</p>
|
|
61
|
+
)}
|
|
62
|
+
{bullets && bullets.length > 0 && (
|
|
63
|
+
<ul className="mt-4 space-y-2">
|
|
64
|
+
{bullets.map(bullet => (
|
|
65
|
+
<li key={bullet} className="flex items-start gap-2">
|
|
66
|
+
<CheckCircle2 className="size-5 shrink-0 text-primary" />
|
|
67
|
+
<span className="text-foreground">{bullet}</span>
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className={cn(reverse ? 'lg:order-first' : 'lg:order-last')}>{visual}</div>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button } from '../../ui/button';
|
|
3
|
+
import { PillBadge } from './PillBadge';
|
|
4
|
+
import { cn } from '../../shared/utils';
|
|
5
|
+
|
|
6
|
+
export interface HeroCta {
|
|
7
|
+
label: string;
|
|
8
|
+
href?: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HeroProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
|
|
13
|
+
eyebrow?: { label: string; icon?: React.ReactNode };
|
|
14
|
+
title: React.ReactNode;
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
primaryCta?: HeroCta;
|
|
17
|
+
secondaryCta?: HeroCta;
|
|
18
|
+
visual?: React.ReactNode;
|
|
19
|
+
align?: 'left' | 'center';
|
|
20
|
+
decorative?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function HeroCtaButton({
|
|
24
|
+
cta,
|
|
25
|
+
variant,
|
|
26
|
+
}: {
|
|
27
|
+
cta: HeroCta;
|
|
28
|
+
variant: React.ComponentProps<typeof Button>['variant'];
|
|
29
|
+
}) {
|
|
30
|
+
if (cta.href) {
|
|
31
|
+
return (
|
|
32
|
+
<Button asChild variant={variant} size="lg">
|
|
33
|
+
<a href={cta.href} onClick={cta.onClick}>
|
|
34
|
+
{cta.label}
|
|
35
|
+
</a>
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Button variant={variant} size="lg" onClick={cta.onClick}>
|
|
42
|
+
{cta.label}
|
|
43
|
+
</Button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Full-width, above-the-fold marketing hero section.
|
|
49
|
+
*
|
|
50
|
+
* @description
|
|
51
|
+
* The lead block for landing and marketing pages: an optional `PillBadge`
|
|
52
|
+
* eyebrow, a very large headline, supporting copy, and up to two `<Button>`
|
|
53
|
+
* CTAs. Automatically switches from a single column to a two-column layout
|
|
54
|
+
* once a `visual` (screenshot, illustration, collage) is supplied.
|
|
55
|
+
*
|
|
56
|
+
* @ai-rules
|
|
57
|
+
* 1. Pass `visual` for the two-column desktop layout; omit it for a single-column hero.
|
|
58
|
+
* 2. `primaryCta` renders as `default` (primary/brand) and `secondaryCta` as `secondary`, both via `<Button>` (`size="lg"`) — never hand-roll buttons.
|
|
59
|
+
* 3. Set `decorative` to add a token-based gradient blob backdrop; it is purely visual and never shifts layout.
|
|
60
|
+
* 4. `align="center"` centers the eyebrow/heading/copy/CTAs — pair it with omitting `visual` for a classic centered hero.
|
|
61
|
+
*/
|
|
62
|
+
export function Hero({
|
|
63
|
+
eyebrow,
|
|
64
|
+
title,
|
|
65
|
+
description,
|
|
66
|
+
primaryCta,
|
|
67
|
+
secondaryCta,
|
|
68
|
+
visual,
|
|
69
|
+
align = 'left',
|
|
70
|
+
decorative = false,
|
|
71
|
+
className,
|
|
72
|
+
...props
|
|
73
|
+
}: HeroProps) {
|
|
74
|
+
const isCentered = align === 'center';
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<section className={cn('relative overflow-hidden py-16 md:py-24', className)} {...props}>
|
|
78
|
+
{decorative && (
|
|
79
|
+
<div
|
|
80
|
+
className="pointer-events-none absolute inset-0 z-0 overflow-hidden"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
>
|
|
83
|
+
<div className="absolute left-1/2 top-1/2 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[linear-gradient(135deg,var(--gradient-start),var(--gradient-end))] opacity-20 blur-3xl md:h-[28rem] md:w-[28rem] lg:h-[36rem] lg:w-[36rem]" />
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
<div className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
88
|
+
<div
|
|
89
|
+
className={cn('grid items-center gap-12', visual ? 'lg:grid-cols-2' : 'mx-auto max-w-3xl')}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
className={cn(
|
|
93
|
+
'flex flex-col gap-6',
|
|
94
|
+
isCentered ? 'items-center text-center' : 'items-start text-left'
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{eyebrow && <PillBadge icon={eyebrow.icon}>{eyebrow.label}</PillBadge>}
|
|
98
|
+
|
|
99
|
+
<h1 className="text-4xl font-bold tracking-tight text-foreground md:text-5xl lg:text-6xl">
|
|
100
|
+
{title}
|
|
101
|
+
</h1>
|
|
102
|
+
|
|
103
|
+
{description && (
|
|
104
|
+
<p className="max-w-2xl text-lg text-muted-foreground">{description}</p>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{(primaryCta || secondaryCta) && (
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
'flex flex-col gap-3 pt-2 sm:flex-row',
|
|
111
|
+
isCentered && 'sm:justify-center'
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{primaryCta && <HeroCtaButton cta={primaryCta} variant="default" />}
|
|
115
|
+
{secondaryCta && <HeroCtaButton cta={secondaryCta} variant="secondary" />}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{visual && <div className="relative">{visual}</div>}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
);
|
|
125
|
+
}
|