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.
- package/CHANGELOG.md +108 -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/components/ui/input-otp/input-otp.test.tsx +14 -1
- 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 +4 -2
- 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 +349 -0
- package/templates/src/styles/xertica/tokens.css +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
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 HowItWorksStepItem {
|
|
30
|
+
number?: string | number;
|
|
31
|
+
icon?: React.ReactNode;
|
|
32
|
+
title: string;
|
|
33
|
+
description: string;
|
|
34
|
+
color?: BlockColor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface HowItWorksStepsProps {
|
|
38
|
+
items: HowItWorksStepItem[];
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Horizontal numbered process row — the horizontal counterpart to `EvidenceTimeline`.
|
|
44
|
+
*
|
|
45
|
+
* @description
|
|
46
|
+
* Renders a "how it works" style row of steps: each item gets a colored
|
|
47
|
+
* badge (icon or step number) above a title and description, laid out
|
|
48
|
+
* side by side in a responsive grid instead of `EvidenceTimeline`'s
|
|
49
|
+
* vertical connecting line.
|
|
50
|
+
*
|
|
51
|
+
* @ai-rules
|
|
52
|
+
* 1. Provide `icon` for a lucide-react icon in the badge, or `number` for a step label; if neither is set the 1-based index is used.
|
|
53
|
+
* 2. Each item's `color` maps to `colorTokens` — never hardcode colors.
|
|
54
|
+
* 3. There is no connecting line between steps (unlike `EvidenceTimeline`) — this avoids awkward line fragments when steps wrap to a new row on narrow screens.
|
|
55
|
+
* 4. This block has no outer card chrome; wrap it in a `<Card>` or section if chrome is needed.
|
|
56
|
+
*/
|
|
57
|
+
export function HowItWorksSteps({ items, className }: HowItWorksStepsProps) {
|
|
58
|
+
return (
|
|
59
|
+
<ol className={cn('grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4', className)}>
|
|
60
|
+
{items.map((item, index) => {
|
|
61
|
+
const { bg, icon: iconColor } = colorTokens[item.color ?? 'primary'];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<li key={index} className="flex flex-col gap-4">
|
|
65
|
+
<div
|
|
66
|
+
className={cn(
|
|
67
|
+
'flex size-10 shrink-0 items-center justify-center rounded-[var(--radius)] border border-current/20 md:size-12',
|
|
68
|
+
bg
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<span
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex items-center justify-center text-sm font-semibold [&>svg]:size-5',
|
|
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
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ImageWithFallback } from '../../figma/ImageWithFallback';
|
|
3
|
+
import { cn } from '../../shared/utils';
|
|
4
|
+
|
|
5
|
+
export interface LogoCloudLogo {
|
|
6
|
+
src: string;
|
|
7
|
+
alt: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LogoCloudProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
title?: string;
|
|
12
|
+
logos: LogoCloudLogo[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A "trusted by" row of partner or customer logos.
|
|
17
|
+
*
|
|
18
|
+
* @description
|
|
19
|
+
* Renders an optional centered, small uppercase muted title above a
|
|
20
|
+
* responsive, wrapping row of grayscale logos that colorize on hover.
|
|
21
|
+
*
|
|
22
|
+
* @ai-rules
|
|
23
|
+
* 1. Provide `logos` as an array of `{ src, alt }` — each is rendered with `ImageWithFallback`.
|
|
24
|
+
* 2. `title` is optional; omit it to render the logo row alone.
|
|
25
|
+
* 3. Logos are token-driven (grayscale/opacity via utilities) so they stay legible across all themes and dark mode.
|
|
26
|
+
*/
|
|
27
|
+
export function LogoCloud({ title, logos, className, ...props }: LogoCloudProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className={cn('w-full text-center', className)} {...props}>
|
|
30
|
+
{title && (
|
|
31
|
+
<p className="mb-6 text-xs font-medium uppercase tracking-wider text-muted-foreground sm:mb-8">
|
|
32
|
+
{title}
|
|
33
|
+
</p>
|
|
34
|
+
)}
|
|
35
|
+
<div className="flex flex-wrap items-center justify-center gap-8 md:gap-12">
|
|
36
|
+
{logos.map((logo, index) => (
|
|
37
|
+
<ImageWithFallback
|
|
38
|
+
key={`${logo.alt}-${index}`}
|
|
39
|
+
src={logo.src}
|
|
40
|
+
alt={logo.alt}
|
|
41
|
+
className="h-6 w-auto object-contain grayscale opacity-60 transition-all hover:opacity-100 hover:grayscale-0 md:h-8"
|
|
42
|
+
/>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../../shared/utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single link rendered inside a footer column or the legal links row.
|
|
6
|
+
*/
|
|
7
|
+
export interface MarketingFooterLink {
|
|
8
|
+
label: string;
|
|
9
|
+
href: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A labeled group of links rendered as one column of the footer.
|
|
14
|
+
*/
|
|
15
|
+
export interface MarketingFooterColumn {
|
|
16
|
+
heading: string;
|
|
17
|
+
links: MarketingFooterLink[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MarketingFooterProps {
|
|
21
|
+
/** Logo slot — pass a rendered logo element (e.g. `<XerticaLogo />`). */
|
|
22
|
+
logo: React.ReactNode;
|
|
23
|
+
/** Short description shown under the logo in the first column. */
|
|
24
|
+
tagline?: string;
|
|
25
|
+
/** Link columns rendered alongside the logo column. */
|
|
26
|
+
columns: MarketingFooterColumn[];
|
|
27
|
+
/** Small print links (Privacy, Terms, etc.) shown on the bottom row. */
|
|
28
|
+
legalLinks?: MarketingFooterLink[];
|
|
29
|
+
/** Copyright text shown on the bottom row. */
|
|
30
|
+
copyright?: string;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Maps logo column + link columns to a static grid-cols class so Tailwind's JIT scanner can find it (capped at 6). */
|
|
35
|
+
const gridColsClasses: Record<number, string> = {
|
|
36
|
+
2: 'md:grid-cols-2',
|
|
37
|
+
3: 'md:grid-cols-3',
|
|
38
|
+
4: 'md:grid-cols-4',
|
|
39
|
+
5: 'md:grid-cols-5',
|
|
40
|
+
6: 'md:grid-cols-6',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Multi-column site footer for public marketing pages.
|
|
45
|
+
*
|
|
46
|
+
* @description
|
|
47
|
+
* Pairs with `MarketingNavbar` to bookend a marketing page: a logo/tagline
|
|
48
|
+
* cell followed by an arbitrary number of link columns, plus a bottom bar
|
|
49
|
+
* with copyright and legal links.
|
|
50
|
+
*
|
|
51
|
+
* @ai-rules
|
|
52
|
+
* 1. Pass the logo as a rendered node via the `logo` prop — do not couple this file to any specific logo component.
|
|
53
|
+
* 2. Pass at most 5 `columns` — the logo cell plus columns are capped at a 6-column desktop grid.
|
|
54
|
+
* 3. `legalLinks` and `copyright` are optional; the bottom row still renders with just whichever is provided.
|
|
55
|
+
*/
|
|
56
|
+
export function MarketingFooter({
|
|
57
|
+
logo,
|
|
58
|
+
tagline,
|
|
59
|
+
columns,
|
|
60
|
+
legalLinks,
|
|
61
|
+
copyright,
|
|
62
|
+
className,
|
|
63
|
+
}: MarketingFooterProps) {
|
|
64
|
+
const resolvedCols = Math.min(Math.max(columns.length + 1, 2), 6);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<footer className={cn('border-t border-border bg-card', className)}>
|
|
68
|
+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
69
|
+
<div className={cn('grid grid-cols-2 gap-8 py-12', gridColsClasses[resolvedCols])}>
|
|
70
|
+
<div className="col-span-2 md:col-span-1">
|
|
71
|
+
<div className="flex items-center">{logo}</div>
|
|
72
|
+
{tagline && <p className="mt-3 max-w-xs text-sm text-muted-foreground">{tagline}</p>}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{columns.map(column => (
|
|
76
|
+
<div key={column.heading}>
|
|
77
|
+
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-foreground">{column.heading}</h3>
|
|
78
|
+
<ul className="space-y-2 text-sm text-muted-foreground">
|
|
79
|
+
{column.links.map(link => (
|
|
80
|
+
<li key={link.href}>
|
|
81
|
+
<a href={link.href} className="transition-colors hover:text-foreground">
|
|
82
|
+
{link.label}
|
|
83
|
+
</a>
|
|
84
|
+
</li>
|
|
85
|
+
))}
|
|
86
|
+
</ul>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex flex-col items-center justify-between gap-4 border-t border-border pt-6 pb-8 text-xs text-muted-foreground md:flex-row">
|
|
92
|
+
{copyright && <p>{copyright}</p>}
|
|
93
|
+
|
|
94
|
+
{legalLinks && legalLinks.length > 0 && (
|
|
95
|
+
<ul className="flex flex-wrap items-center gap-4">
|
|
96
|
+
{legalLinks.map(link => (
|
|
97
|
+
<li key={link.href}>
|
|
98
|
+
<a href={link.href} className="transition-colors hover:text-foreground">
|
|
99
|
+
{link.label}
|
|
100
|
+
</a>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</footer>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { Menu, X } from 'lucide-react';
|
|
6
|
+
import { Button } from '../../ui/button';
|
|
7
|
+
import { LanguageSelector } from '../../brand/language-selector';
|
|
8
|
+
import { cn } from '../../shared/utils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A single navigation link rendered in the navbar.
|
|
12
|
+
*/
|
|
13
|
+
export interface MarketingNavLink {
|
|
14
|
+
label: string;
|
|
15
|
+
href: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A call-to-action button rendered on the right side of the navbar.
|
|
20
|
+
*/
|
|
21
|
+
export interface MarketingNavbarCta {
|
|
22
|
+
label: string;
|
|
23
|
+
href?: string;
|
|
24
|
+
onClick?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MarketingNavbarProps {
|
|
28
|
+
/** Logo slot — pass a rendered logo element (e.g. `<XerticaLogo />`). */
|
|
29
|
+
logo: React.ReactNode;
|
|
30
|
+
/** Primary navigation links, shown inline on desktop and stacked on mobile. */
|
|
31
|
+
links: MarketingNavLink[];
|
|
32
|
+
/** Primary call-to-action, rendered as a solid `Button`. */
|
|
33
|
+
cta?: MarketingNavbarCta;
|
|
34
|
+
/** Secondary call-to-action, rendered as an outline `Button`. */
|
|
35
|
+
secondaryCta?: MarketingNavbarCta;
|
|
36
|
+
/** Shows the design system's language selector. Defaults to `true` — `LanguageSelector` auto-hides itself when only one language is configured. */
|
|
37
|
+
showLanguageSelector?: boolean;
|
|
38
|
+
/** Accessible label for the mobile menu trigger when closed. */
|
|
39
|
+
openMenuLabel?: string;
|
|
40
|
+
/** Accessible label for the mobile menu trigger when open. */
|
|
41
|
+
closeMenuLabel?: string;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sticky header for public marketing pages.
|
|
47
|
+
*
|
|
48
|
+
* @description
|
|
49
|
+
* Distinct from the app-shell `Header` (used inside the authenticated product layout):
|
|
50
|
+
* this navbar is for public-facing marketing sites, with a logo slot, inline nav links,
|
|
51
|
+
* and up to two CTA buttons. Collapses into a full-width dropdown panel on mobile.
|
|
52
|
+
*
|
|
53
|
+
* @ai-rules
|
|
54
|
+
* 1. Pass the logo as a rendered node via the `logo` prop — do not couple this file to any specific logo component.
|
|
55
|
+
* 2. Use `cta` for the primary action (solid button) and `secondaryCta` for a lower-emphasis action (outline button).
|
|
56
|
+
* 3. Each link/CTA needs either `href` (renders an `<a>`) or `onClick` — CTAs support both.
|
|
57
|
+
*/
|
|
58
|
+
export function MarketingNavbar({
|
|
59
|
+
logo,
|
|
60
|
+
links,
|
|
61
|
+
cta,
|
|
62
|
+
secondaryCta,
|
|
63
|
+
showLanguageSelector = true,
|
|
64
|
+
openMenuLabel = 'Open menu',
|
|
65
|
+
closeMenuLabel = 'Close menu',
|
|
66
|
+
className,
|
|
67
|
+
}: MarketingNavbarProps) {
|
|
68
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
69
|
+
|
|
70
|
+
const closeMenu = () => setIsOpen(false);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<header
|
|
74
|
+
className={cn(
|
|
75
|
+
'sticky top-0 z-50 w-full border-b border-border bg-card/80 backdrop-blur',
|
|
76
|
+
className
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<nav className="mx-auto flex h-16 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
|
|
80
|
+
{/* Logo */}
|
|
81
|
+
<div className="flex shrink-0 items-center">{logo}</div>
|
|
82
|
+
|
|
83
|
+
{/* Desktop nav links */}
|
|
84
|
+
<div className="hidden flex-1 items-center justify-center gap-8 md:flex">
|
|
85
|
+
{links.map(link => (
|
|
86
|
+
<a
|
|
87
|
+
key={link.href}
|
|
88
|
+
href={link.href}
|
|
89
|
+
className="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
90
|
+
>
|
|
91
|
+
{link.label}
|
|
92
|
+
</a>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Desktop CTAs */}
|
|
97
|
+
<div className="hidden shrink-0 items-center gap-3 md:flex">
|
|
98
|
+
{showLanguageSelector && <LanguageSelector variant="minimal" showIcon={false} />}
|
|
99
|
+
{secondaryCta && (
|
|
100
|
+
<Button
|
|
101
|
+
variant="outline"
|
|
102
|
+
asChild={!!secondaryCta.href}
|
|
103
|
+
onClick={secondaryCta.onClick}
|
|
104
|
+
>
|
|
105
|
+
{secondaryCta.href ? (
|
|
106
|
+
<a href={secondaryCta.href}>{secondaryCta.label}</a>
|
|
107
|
+
) : (
|
|
108
|
+
secondaryCta.label
|
|
109
|
+
)}
|
|
110
|
+
</Button>
|
|
111
|
+
)}
|
|
112
|
+
{cta && (
|
|
113
|
+
<Button variant="secondary" asChild={!!cta.href} onClick={cta.onClick}>
|
|
114
|
+
{cta.href ? <a href={cta.href}>{cta.label}</a> : cta.label}
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Mobile menu trigger */}
|
|
120
|
+
<Button
|
|
121
|
+
variant="ghost"
|
|
122
|
+
size="icon"
|
|
123
|
+
className="md:hidden"
|
|
124
|
+
onClick={() => setIsOpen(prev => !prev)}
|
|
125
|
+
aria-label={isOpen ? closeMenuLabel : openMenuLabel}
|
|
126
|
+
aria-expanded={isOpen}
|
|
127
|
+
>
|
|
128
|
+
{isOpen ? <X className="size-5" /> : <Menu className="size-5" />}
|
|
129
|
+
</Button>
|
|
130
|
+
</nav>
|
|
131
|
+
|
|
132
|
+
{/* Mobile dropdown panel */}
|
|
133
|
+
{isOpen && (
|
|
134
|
+
<div className="border-t border-border bg-card/95 backdrop-blur md:hidden">
|
|
135
|
+
<div className="flex flex-col gap-1 px-4 py-4 sm:px-6">
|
|
136
|
+
{links.map(link => (
|
|
137
|
+
<a
|
|
138
|
+
key={link.href}
|
|
139
|
+
href={link.href}
|
|
140
|
+
onClick={closeMenu}
|
|
141
|
+
className="rounded-[var(--radius)] px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
142
|
+
>
|
|
143
|
+
{link.label}
|
|
144
|
+
</a>
|
|
145
|
+
))}
|
|
146
|
+
|
|
147
|
+
{showLanguageSelector && (
|
|
148
|
+
<div className="mt-2 flex border-t border-border pt-4">
|
|
149
|
+
<LanguageSelector variant="minimal" showIcon={false} />
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{(secondaryCta || cta) && (
|
|
154
|
+
<div className="mt-2 flex flex-col gap-2 border-t border-border pt-4">
|
|
155
|
+
{secondaryCta && (
|
|
156
|
+
<Button
|
|
157
|
+
variant="outline"
|
|
158
|
+
className="w-full"
|
|
159
|
+
asChild={!!secondaryCta.href}
|
|
160
|
+
onClick={() => {
|
|
161
|
+
closeMenu();
|
|
162
|
+
secondaryCta.onClick?.();
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{secondaryCta.href ? (
|
|
166
|
+
<a href={secondaryCta.href}>{secondaryCta.label}</a>
|
|
167
|
+
) : (
|
|
168
|
+
secondaryCta.label
|
|
169
|
+
)}
|
|
170
|
+
</Button>
|
|
171
|
+
)}
|
|
172
|
+
{cta && (
|
|
173
|
+
<Button
|
|
174
|
+
variant="secondary"
|
|
175
|
+
className="w-full"
|
|
176
|
+
asChild={!!cta.href}
|
|
177
|
+
onClick={() => {
|
|
178
|
+
closeMenu();
|
|
179
|
+
cta.onClick?.();
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{cta.href ? <a href={cta.href}>{cta.label}</a> : cta.label}
|
|
183
|
+
</Button>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
</header>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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, { border: string; tint: string; icon: string }> = {
|
|
17
|
+
primary: { border: 'border-primary', tint: 'bg-primary/5', icon: 'text-primary' },
|
|
18
|
+
'chart-1': {
|
|
19
|
+
border: 'border-[var(--chart-1)]',
|
|
20
|
+
tint: 'bg-[var(--chart-1)]/5',
|
|
21
|
+
icon: 'text-[var(--chart-1)]',
|
|
22
|
+
},
|
|
23
|
+
'chart-2': {
|
|
24
|
+
border: 'border-[var(--chart-2)]',
|
|
25
|
+
tint: 'bg-[var(--chart-2)]/5',
|
|
26
|
+
icon: 'text-[var(--chart-2)]',
|
|
27
|
+
},
|
|
28
|
+
'chart-3': {
|
|
29
|
+
border: 'border-[var(--chart-3)]',
|
|
30
|
+
tint: 'bg-[var(--chart-3)]/5',
|
|
31
|
+
icon: 'text-[var(--chart-3)]',
|
|
32
|
+
},
|
|
33
|
+
'chart-4': {
|
|
34
|
+
border: 'border-[var(--chart-4)]',
|
|
35
|
+
tint: 'bg-[var(--chart-4)]/5',
|
|
36
|
+
icon: 'text-[var(--chart-4)]',
|
|
37
|
+
},
|
|
38
|
+
'chart-5': {
|
|
39
|
+
border: 'border-[var(--chart-5)]',
|
|
40
|
+
tint: 'bg-[var(--chart-5)]/5',
|
|
41
|
+
icon: 'text-[var(--chart-5)]',
|
|
42
|
+
},
|
|
43
|
+
success: { border: 'border-success', tint: 'bg-success/5', icon: 'text-success' },
|
|
44
|
+
info: { border: 'border-info', tint: 'bg-info/5', icon: 'text-info' },
|
|
45
|
+
warning: { border: 'border-warning', tint: 'bg-warning/5', icon: 'text-warning' },
|
|
46
|
+
destructive: { border: 'border-destructive', tint: 'bg-destructive/5', icon: 'text-destructive' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface PillBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
icon?: React.ReactNode;
|
|
52
|
+
color?: BlockColor;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Small outlined tag used as "eyebrow" text above marketing headlines.
|
|
57
|
+
*
|
|
58
|
+
* @description
|
|
59
|
+
* A compact, token-driven chip (e.g. "AI ADOPTION PLATFORM") that pairs an
|
|
60
|
+
* optional leading icon with short uppercase text, tinted via the shared
|
|
61
|
+
* color-chip palette so it reskins across all Xertica-ui themes.
|
|
62
|
+
*
|
|
63
|
+
* @ai-rules
|
|
64
|
+
* 1. Keep `children` short — this is an eyebrow label, not a paragraph.
|
|
65
|
+
* 2. Pass `icon` as a small lucide-react icon element; it is sized automatically.
|
|
66
|
+
* 3. Use `color` to match the section's accent; defaults to `primary`.
|
|
67
|
+
*/
|
|
68
|
+
export function PillBadge({
|
|
69
|
+
children,
|
|
70
|
+
icon,
|
|
71
|
+
color = 'primary',
|
|
72
|
+
className,
|
|
73
|
+
...props
|
|
74
|
+
}: PillBadgeProps) {
|
|
75
|
+
const { border, tint, icon: textColor } = colorTokens[color];
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
className={cn(
|
|
80
|
+
'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-wide',
|
|
81
|
+
border,
|
|
82
|
+
tint,
|
|
83
|
+
textColor,
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{icon && (
|
|
89
|
+
<span className="flex items-center [&>svg]:size-3.5" aria-hidden="true">
|
|
90
|
+
{icon}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
{children}
|
|
94
|
+
</span>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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, string> = {
|
|
17
|
+
primary: 'bg-primary',
|
|
18
|
+
'chart-1': 'bg-[var(--chart-1)]',
|
|
19
|
+
'chart-2': 'bg-[var(--chart-2)]',
|
|
20
|
+
'chart-3': 'bg-[var(--chart-3)]',
|
|
21
|
+
'chart-4': 'bg-[var(--chart-4)]',
|
|
22
|
+
'chart-5': 'bg-[var(--chart-5)]',
|
|
23
|
+
success: 'bg-success',
|
|
24
|
+
info: 'bg-info',
|
|
25
|
+
warning: 'bg-warning',
|
|
26
|
+
destructive: 'bg-destructive',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface SectionHeadingProps {
|
|
30
|
+
title: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Use 'sidebar' when this heading sits on a `bg-sidebar` surface (e.g. Methodology). */
|
|
33
|
+
tone?: 'default' | 'sidebar';
|
|
34
|
+
/** Optional small colored bar rendered under the title. Omit for no bar. */
|
|
35
|
+
accentColor?: BlockColor;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Plain section header used to introduce a landing-page section.
|
|
41
|
+
*
|
|
42
|
+
* @description
|
|
43
|
+
* Renders the section title (and optional description), with an optional
|
|
44
|
+
* small colored bar under the title.
|
|
45
|
+
*
|
|
46
|
+
* @ai-rules
|
|
47
|
+
* 1. Use `tone="sidebar"` when this heading renders on a `bg-sidebar` surface — the default tone's tokens are not guaranteed to contrast there.
|
|
48
|
+
* 2. `accentColor` is optional — omit it for a plain title with no bar; when set, it maps to `colorTokens` and never hardcodes a color.
|
|
49
|
+
*/
|
|
50
|
+
export function SectionHeading({
|
|
51
|
+
title,
|
|
52
|
+
description,
|
|
53
|
+
tone = 'default',
|
|
54
|
+
accentColor,
|
|
55
|
+
className,
|
|
56
|
+
}: SectionHeadingProps) {
|
|
57
|
+
const isSidebar = tone === 'sidebar';
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className={className}>
|
|
61
|
+
<h2 className={cn('text-2xl font-bold md:text-3xl', !isSidebar && 'text-foreground')}>
|
|
62
|
+
{title}
|
|
63
|
+
</h2>
|
|
64
|
+
{accentColor && (
|
|
65
|
+
<span
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
className={cn('mt-3 block h-1 w-10 rounded-full', colorTokens[accentColor])}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
{description && (
|
|
71
|
+
<p
|
|
72
|
+
className={cn(
|
|
73
|
+
'mt-6 max-w-2xl text-base',
|
|
74
|
+
isSidebar ? 'text-sidebar-foreground/70' : 'text-muted-foreground'
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{description}
|
|
78
|
+
</p>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|