@hraness/design-kit 0.31.1
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/ARTICLE_COPY.md +208 -0
- package/HERO_FIELDS.md +55 -0
- package/ICONS.md +56 -0
- package/LANTERN_MATERIAL.md +60 -0
- package/LICENSE +21 -0
- package/MARKETING_COPY.md +49 -0
- package/MARKETING_PRESET.md +108 -0
- package/PALETTES.md +90 -0
- package/PAPER_THEME.md +128 -0
- package/README.md +745 -0
- package/STATUS_PAGES.md +67 -0
- package/STYLE.md +245 -0
- package/WRITING.md +72 -0
- package/dist/browser/index.js +2345 -0
- package/dist/chunk-44djxk16.js +6872 -0
- package/dist/chunk-5gtx3pza.js +9 -0
- package/dist/chunk-77391vmq.js +454 -0
- package/dist/chunk-7b1586eb.js +1209 -0
- package/dist/chunk-8834fh4n.js +6 -0
- package/dist/chunk-9t8xyqte.js +870 -0
- package/dist/chunk-cejpzyfh.js +446 -0
- package/dist/chunk-eh71jz57.js +429 -0
- package/dist/chunk-evb02bc1.js +516 -0
- package/dist/chunk-h4k7yv6x.js +368 -0
- package/dist/chunk-he8eznb1.js +760 -0
- package/dist/chunk-mr1vdcjq.js +404 -0
- package/dist/chunk-wzvdn8ey.js +116 -0
- package/dist/fonts/nebula-sans/social-fonts.generated.js +53 -0
- package/dist/icons.js +48 -0
- package/dist/index.js +237 -0
- package/dist/launch.js +41 -0
- package/dist/mockups/client.js +422 -0
- package/dist/mockups/index.js +1850 -0
- package/dist/portfolio.js +1549 -0
- package/dist/provider-marks.js +19 -0
- package/dist/react/charts.js +11 -0
- package/dist/react/hero-backdrop.js +8 -0
- package/dist/react/index.js +7252 -0
- package/dist/react/platform-install.js +10 -0
- package/dist/react/server.js +137 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +5392 -0
- package/dist/syntax-highlighting.js +15 -0
- package/dist/testing.js +250 -0
- package/package.json +377 -0
- package/portfolio-inventory.json +31 -0
- package/scripts/check-lantern-material-snapshot.d.mts +26 -0
- package/scripts/check-lantern-material-snapshot.mjs +126 -0
- package/scripts/check-marketing-snapshot.d.mts +12 -0
- package/scripts/check-marketing-snapshot.mjs +58 -0
- package/scripts/lantern-material-snapshot.ts +101 -0
- package/scripts/marketing-textures.ts +48 -0
- package/scripts/paper-theme-snapshot.ts +97 -0
- package/scripts/product-marketing-snapshot.ts +149 -0
- package/src/appearance-menu.css +266 -0
- package/src/appearance.ts +32 -0
- package/src/article-html.ts +269 -0
- package/src/article.ts +686 -0
- package/src/browser/appearance-menu.ts +604 -0
- package/src/browser/artifact-share.ts +164 -0
- package/src/browser/design-palette.ts +197 -0
- package/src/browser/foil.ts +124 -0
- package/src/browser/hero-light.ts +11 -0
- package/src/browser/index.ts +29 -0
- package/src/browser/status-field.ts +432 -0
- package/src/browser/status-page.ts +100 -0
- package/src/browser/sticky-offset.ts +82 -0
- package/src/browser/theme-color-sync.ts +213 -0
- package/src/charts.css +7 -0
- package/src/compiler-components.css +34 -0
- package/src/compiler-foundation.css +18 -0
- package/src/compiler-palettes.css +7 -0
- package/src/compiler-tokens.css +342 -0
- package/src/components.css +3 -0
- package/src/design-gallery.css +431 -0
- package/src/effects.css +14 -0
- package/src/fonts/geist-mono/GeistMono[wght].woff2 +0 -0
- package/src/fonts/geist-mono/OFL.txt +93 -0
- package/src/fonts/geist-mono/PROVENANCE.md +12 -0
- package/src/fonts/instrument-serif/OFL.txt +93 -0
- package/src/fonts/instrument-serif/UPSTREAM.md +14 -0
- package/src/fonts/instrument-serif/instrument-serif-latin-400.woff2 +0 -0
- package/src/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/fonts/nebula-sans/NebulaSans-Black.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BlackItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BoldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BookItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Light.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-LightItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Medium.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-MediumItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Semibold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-SemiboldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/fonts/nebula-sans/social-fonts.generated.ts +3873 -0
- package/src/fonts.css +106 -0
- package/src/icons/act60/act60.svg +5 -0
- package/src/icons/act60/application.svg +9 -0
- package/src/icons/act60/chapter-compare.svg +3 -0
- package/src/icons/act60/decree-residence.svg +6 -0
- package/src/icons/act60/estimate.svg +7 -0
- package/src/icons/act60/export-services.svg +4 -0
- package/src/icons/act60/investor.svg +13 -0
- package/src/icons/act60/presence.svg +7 -0
- package/src/icons/aicharts/aicharts.svg +6 -0
- package/src/icons/aicharts/cost-compare.svg +4 -0
- package/src/icons/aicharts/task-audio.svg +7 -0
- package/src/icons/aicharts/task-coding.svg +6 -0
- package/src/icons/aicharts/task-images.svg +3 -0
- package/src/icons/aicharts/task-reasoning.svg +9 -0
- package/src/icons/aicharts/task-research.svg +5 -0
- package/src/icons/aicharts/task-video.svg +4 -0
- package/src/icons/manifest.json +1371 -0
- package/src/icons/platonik/conversation.svg +3 -0
- package/src/icons/platonik/creature.svg +7 -0
- package/src/icons/platonik/economy.svg +4 -0
- package/src/icons/platonik/keep-light.svg +6 -0
- package/src/icons/platonik/platonik.svg +7 -0
- package/src/icons/platonik/spark-route.svg +15 -0
- package/src/icons/receipts/act60-retry.receipt.json +888 -0
- package/src/icons/receipts/act60.receipt.json +1366 -0
- package/src/icons/receipts/aicharts.receipt.json +1546 -0
- package/src/icons/receipts/platonik.receipt.json +1011 -0
- package/src/icons/receipts/roughday.receipt.json +770 -0
- package/src/icons/receipts/shared.receipt.json +1296 -0
- package/src/icons/receipts/slopcamera.receipt.json +1495 -0
- package/src/icons/receipts/soundfish-retry.receipt.json +460 -0
- package/src/icons/receipts/soundfish.receipt.json +1709 -0
- package/src/icons/receipts/sponge.receipt.json +1743 -0
- package/src/icons/receipts/stripe-history-retry.receipt.json +145 -0
- package/src/icons/receipts/stripe-history.receipt.json +1202 -0
- package/src/icons/receipts/wordcell.receipt.json +1924 -0
- package/src/icons/roughday/finance.svg +7 -0
- package/src/icons/roughday/roughday.svg +6 -0
- package/src/icons/roughday/technology.svg +4 -0
- package/src/icons/roughday/world.svg +4 -0
- package/src/icons/sets/act60.json +63 -0
- package/src/icons/sets/aicharts.json +63 -0
- package/src/icons/sets/platonik.json +39 -0
- package/src/icons/sets/roughday.json +25 -0
- package/src/icons/sets/shared.json +12 -0
- package/src/icons/sets/slopcamera.json +47 -0
- package/src/icons/sets/soundfish.json +43 -0
- package/src/icons/sets/sponge.json +51 -0
- package/src/icons/sets/stripe-history.json +39 -0
- package/src/icons/sets/wordcell.json +51 -0
- package/src/icons/shared/agent-skill.svg +4 -0
- package/src/icons/shared/cli.svg +5 -0
- package/src/icons/shared/privacy.svg +3 -0
- package/src/icons/shared/research.svg +7 -0
- package/src/icons/shared/sdk.svg +3 -0
- package/src/icons/slopcamera/diagram.svg +3 -0
- package/src/icons/slopcamera/direct-scene.svg +5 -0
- package/src/icons/slopcamera/edit-video.svg +17 -0
- package/src/icons/slopcamera/mcp.svg +3 -0
- package/src/icons/slopcamera/native-world.svg +3 -0
- package/src/icons/slopcamera/slopcamera.svg +11 -0
- package/src/icons/soundfish/agent-ui.svg +7 -0
- package/src/icons/soundfish/collaborate.svg +5 -0
- package/src/icons/soundfish/compose.svg +3 -0
- package/src/icons/soundfish/hear.svg +6 -0
- package/src/icons/soundfish/human-ui.svg +6 -0
- package/src/icons/soundfish/share.svg +3 -0
- package/src/icons/soundfish/soundfish.svg +6 -0
- package/src/icons/sponge/api.svg +3 -0
- package/src/icons/sponge/documents.svg +3 -0
- package/src/icons/sponge/library.svg +3 -0
- package/src/icons/sponge/one-question.svg +5 -0
- package/src/icons/sponge/people-direct.svg +5 -0
- package/src/icons/sponge/read.svg +3 -0
- package/src/icons/sponge/save.svg +4 -0
- package/src/icons/sponge/sponge.svg +7 -0
- package/src/icons/stripe-history/company-history.svg +3 -0
- package/src/icons/stripe-history/evidence.svg +4 -0
- package/src/icons/stripe-history/independence.svg +7 -0
- package/src/icons/stripe-history/publications.svg +11 -0
- package/src/icons/stripe-history/sources.svg +6 -0
- package/src/icons/stripe-history/stripe-history.svg +3 -0
- package/src/icons/wordcell/backlinks.svg +6 -0
- package/src/icons/wordcell/capture.svg +6 -0
- package/src/icons/wordcell/git-provenance.svg +4 -0
- package/src/icons/wordcell/kb.svg +4 -0
- package/src/icons/wordcell/markdown.svg +4 -0
- package/src/icons/wordcell/scopes.svg +3 -0
- package/src/icons/wordcell/search.svg +7 -0
- package/src/icons.generated.ts +734 -0
- package/src/icons.ts +51 -0
- package/src/index.ts +275 -0
- package/src/lantern-material.css +213 -0
- package/src/launch.ts +624 -0
- package/src/marketing-assets/UPSTREAM.md +18 -0
- package/src/marketing-assets/cells.svg +1 -0
- package/src/marketing-assets/grain.svg +1 -0
- package/src/marketing-forced-colors.css +12 -0
- package/src/mockups/client.tsx +415 -0
- package/src/mockups/core.tsx +318 -0
- package/src/mockups/frames.tsx +368 -0
- package/src/mockups/index.ts +14 -0
- package/src/mockups/surfaces.tsx +681 -0
- package/src/mockups.css +2607 -0
- package/src/palette-appearance.ts +45 -0
- package/src/palette-bridge.css +124 -0
- package/src/palette-color.ts +41 -0
- package/src/palette-system.css +721 -0
- package/src/palette-themes.ts +23 -0
- package/src/palette-tokens.stylex.ts +519 -0
- package/src/palettes.css +3 -0
- package/src/palettes.ts +161 -0
- package/src/paper-theme.css +244 -0
- package/src/plain-publication.css +1241 -0
- package/src/plain-site.css +397 -0
- package/src/platforms.ts +151 -0
- package/src/portfolio.generated.json +1595 -0
- package/src/portfolio.generated.ts +1596 -0
- package/src/portfolio.ts +226 -0
- package/src/product-marketing-foundation.css +433 -0
- package/src/product-marketing-preset.css +261 -0
- package/src/product-marketing.css +2186 -0
- package/src/provider-marks.generated.ts +112 -0
- package/src/provider-marks.ts +143 -0
- package/src/react/animated-rail-stage.stylex.ts +15 -0
- package/src/react/animated-rail-stage.tsx +51 -0
- package/src/react/app-shell.stylex.ts +104 -0
- package/src/react/app-shell.tsx +121 -0
- package/src/react/article.tsx +602 -0
- package/src/react/aurora-dots-background.tsx +34 -0
- package/src/react/charts.stylex.ts +563 -0
- package/src/react/charts.tsx +610 -0
- package/src/react/chat.stylex.ts +34 -0
- package/src/react/chat.tsx +155 -0
- package/src/react/design-gallery.tsx +943 -0
- package/src/react/design-palette.stylex.ts +124 -0
- package/src/react/design-palette.tsx +196 -0
- package/src/react/design-theme-context.tsx +41 -0
- package/src/react/effects.stylex.ts +325 -0
- package/src/react/fader.stylex.ts +71 -0
- package/src/react/fader.tsx +151 -0
- package/src/react/foil-card-math.ts +100 -0
- package/src/react/foil-card-surface.stylex.ts +326 -0
- package/src/react/foil-card-surface.tsx +742 -0
- package/src/react/foil-mark.tsx +35 -0
- package/src/react/foil.stylex.ts +163 -0
- package/src/react/haptics.ts +228 -0
- package/src/react/hero-backdrop.tsx +22 -0
- package/src/react/index.ts +38 -0
- package/src/react/keyboard-shortcuts.ts +228 -0
- package/src/react/lantern-material-gallery.tsx +166 -0
- package/src/react/lantern-material.stylex.ts +39 -0
- package/src/react/launch-beats.tsx +73 -0
- package/src/react/navigation-rail.stylex.ts +130 -0
- package/src/react/navigation-rail.tsx +228 -0
- package/src/react/particle-halo.tsx +115 -0
- package/src/react/phaser-dots.tsx +501 -0
- package/src/react/platform-icons.tsx +94 -0
- package/src/react/platform-install.stylex.ts +385 -0
- package/src/react/platform-install.tsx +389 -0
- package/src/react/playback-transport.stylex.ts +14 -0
- package/src/react/playback-transport.tsx +112 -0
- package/src/react/procedural-backdrop.tsx +192 -0
- package/src/react/procedural-recipe.ts +323 -0
- package/src/react/product-marketing.stylex.ts +4469 -0
- package/src/react/product-marketing.tsx +1611 -0
- package/src/react/production-data-preview-notice.stylex.ts +40 -0
- package/src/react/production-data-preview-notice.tsx +34 -0
- package/src/react/provider-mark.stylex.ts +132 -0
- package/src/react/provider-mark.tsx +120 -0
- package/src/react/relative-time.tsx +117 -0
- package/src/react/route-state.stylex.ts +39 -0
- package/src/react/route-state.tsx +350 -0
- package/src/react/server.ts +11 -0
- package/src/react/social-kit-panel.tsx +103 -0
- package/src/react/sticky-offset.tsx +33 -0
- package/src/react/surfaces.stylex.ts +179 -0
- package/src/react/surfaces.tsx +330 -0
- package/src/react/syntax-code.tsx +33 -0
- package/src/react/theme-color-sync.ts +1 -0
- package/src/react/theme-resolution.ts +11 -0
- package/src/react/theme.stylex.ts +276 -0
- package/src/react/theme.tsx +402 -0
- package/src/reading.css +72 -0
- package/src/relative-time.ts +185 -0
- package/src/reset.css +2 -0
- package/src/site-shell.css +16 -0
- package/src/status-page-html.ts +63 -0
- package/src/status-page.css +276 -0
- package/src/status-page.ts +323 -0
- package/src/styles.css +21 -0
- package/src/syntax-code-lexers.ts +303 -0
- package/src/syntax-highlighting.css +78 -0
- package/src/syntax-highlighting.ts +477 -0
- package/src/syntax-tokens.ts +33 -0
- package/src/testing.ts +307 -0
- package/src/tokens.css +3 -0
- package/src/typography.css +86 -0
- package/vendor/evilcharts/LICENSE +21 -0
- package/vendor/evilcharts/UPSTREAM.md +14 -0
- package/vendor/platform-marks/LICENSE +30 -0
- package/vendor/platform-marks/UPSTREAM.md +15 -0
- package/vendor/platform-marks/apple.svg +1 -0
- package/vendor/platform-marks/linux.svg +1 -0
- package/vendor/provider-marks/CRUSH-LICENSE.md +134 -0
- package/vendor/provider-marks/LICENSE +21 -0
- package/vendor/provider-marks/UPSTREAM.md +27 -0
- package/vendor/provider-marks/aider.svg +3 -0
- package/vendor/provider-marks/crush-heartbit.svg +8 -0
- package/vendor/provider-marks/lobehub/alibabacloud-color.svg +1 -0
- package/vendor/provider-marks/lobehub/alibabacloud.svg +1 -0
- package/vendor/provider-marks/lobehub/amp-color.svg +1 -0
- package/vendor/provider-marks/lobehub/amp.svg +1 -0
- package/vendor/provider-marks/lobehub/anthropic.svg +1 -0
- package/vendor/provider-marks/lobehub/claude-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claude.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode.svg +1 -0
- package/vendor/provider-marks/lobehub/codex-color.svg +1 -0
- package/vendor/provider-marks/lobehub/codex.svg +1 -0
- package/vendor/provider-marks/lobehub/cursor.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek-color.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek.svg +1 -0
- package/vendor/provider-marks/lobehub/devin-color.svg +1 -0
- package/vendor/provider-marks/lobehub/devin.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini-color.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli-color.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli.svg +1 -0
- package/vendor/provider-marks/lobehub/goose.svg +1 -0
- package/vendor/provider-marks/lobehub/meta-color.svg +1 -0
- package/vendor/provider-marks/lobehub/meta.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral-color.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral.svg +1 -0
- package/vendor/provider-marks/lobehub/moonshot.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia-color.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia.svg +1 -0
- package/vendor/provider-marks/lobehub/openai.svg +1 -0
- package/vendor/provider-marks/lobehub/opencode.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity-color.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen-color.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen.svg +1 -0
- package/vendor/provider-marks/lobehub/xai.svg +1 -0
- package/vendor/provider-marks/lobehub/zai.svg +1 -0
|
@@ -0,0 +1,1611 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { FoilMark } from "./foil-mark.js";
|
|
3
|
+
import { SyntaxCode } from "./syntax-code.js";
|
|
4
|
+
import { marketingClassName as classNames, marketingColumnClassName, marketingFactCellVariant } from "./product-marketing.stylex.js";
|
|
5
|
+
import type { MarketingColumnCount } from "./product-marketing.stylex.js";
|
|
6
|
+
|
|
7
|
+
export type { MarketingColumnCount } from "./product-marketing.stylex.js";
|
|
8
|
+
|
|
9
|
+
export type MarketingHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
10
|
+
|
|
11
|
+
const MARKETING_HEADING_TAGS = {
|
|
12
|
+
1: "h1",
|
|
13
|
+
2: "h2",
|
|
14
|
+
3: "h3",
|
|
15
|
+
4: "h4",
|
|
16
|
+
5: "h5",
|
|
17
|
+
6: "h6",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type MarketingTone = "paper" | "accent";
|
|
21
|
+
export type MarketingPreset = "editorial" | "minimal";
|
|
22
|
+
/**
|
|
23
|
+
* Accepted `data-hraness-pattern` values. Patterns are retired: `cells`,
|
|
24
|
+
* `weave`, `contour`, and `mesh` stay valid for compatibility but render
|
|
25
|
+
* exactly like `none`.
|
|
26
|
+
*/
|
|
27
|
+
export const marketingPatterns = ["cells", "weave", "contour", "mesh", "none"] as const;
|
|
28
|
+
/** @deprecated Patterns are retired; every value renders as `none`. */
|
|
29
|
+
export type MarketingPattern = (typeof marketingPatterns)[number];
|
|
30
|
+
|
|
31
|
+
function assertMarketingPattern(pattern: MarketingPattern | undefined): void {
|
|
32
|
+
if (pattern !== undefined && !marketingPatterns.includes(pattern)) throw new RangeError("Unknown marketing pattern.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MarketingAction {
|
|
36
|
+
readonly emphasis?: "primary" | "secondary";
|
|
37
|
+
readonly href: string;
|
|
38
|
+
readonly label: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MarketingFact {
|
|
42
|
+
readonly detail?: string;
|
|
43
|
+
readonly label: string;
|
|
44
|
+
readonly value: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MarketingStep {
|
|
48
|
+
readonly code?: string;
|
|
49
|
+
readonly detail?: string;
|
|
50
|
+
readonly label: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MarketingLink {
|
|
54
|
+
readonly current?: boolean;
|
|
55
|
+
readonly href: string;
|
|
56
|
+
readonly label: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Heading({
|
|
60
|
+
children,
|
|
61
|
+
className,
|
|
62
|
+
id,
|
|
63
|
+
level,
|
|
64
|
+
}: Readonly<{
|
|
65
|
+
children: ReactNode;
|
|
66
|
+
className?: string;
|
|
67
|
+
id?: string;
|
|
68
|
+
level: MarketingHeadingLevel;
|
|
69
|
+
}>) {
|
|
70
|
+
const properties = { children, className, id };
|
|
71
|
+
const HeadingTag = MARKETING_HEADING_TAGS[level];
|
|
72
|
+
return <HeadingTag {...properties} />;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function childHeadingLevel(level: MarketingHeadingLevel): MarketingHeadingLevel {
|
|
76
|
+
return Math.min(level + 1, 6) as MarketingHeadingLevel;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function MarketingActions({
|
|
80
|
+
actions,
|
|
81
|
+
className,
|
|
82
|
+
tone,
|
|
83
|
+
context,
|
|
84
|
+
}: Readonly<{ actions: readonly MarketingAction[]; className: string; tone: MarketingTone; context: "hero" | "cta" }>) {
|
|
85
|
+
if (actions.length === 0) return null;
|
|
86
|
+
return (
|
|
87
|
+
<div className={className}>
|
|
88
|
+
{actions.map((action, index) => {
|
|
89
|
+
const emphasis = action.emphasis ?? (index === 0 ? "primary" : "secondary");
|
|
90
|
+
return (
|
|
91
|
+
<a
|
|
92
|
+
className={classNames("hraness-marketing-action", undefined, tone === "accent"
|
|
93
|
+
? `${context}-${emphasis}`
|
|
94
|
+
: emphasis === "primary" ? "primary" : "default")}
|
|
95
|
+
data-emphasis={emphasis}
|
|
96
|
+
data-foil={emphasis === "primary" ? "" : undefined}
|
|
97
|
+
href={action.href}
|
|
98
|
+
key={`${action.href}-${action.label}`}
|
|
99
|
+
>
|
|
100
|
+
{action.label}
|
|
101
|
+
</a>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Page root. Binds the marketing tokens, the text face, and the horizontal
|
|
110
|
+
* gutter for every direct child role. Products set `--hraness-site-accent`
|
|
111
|
+
* on this element or on a parent.
|
|
112
|
+
*/
|
|
113
|
+
export function MarketingPage({
|
|
114
|
+
children,
|
|
115
|
+
className,
|
|
116
|
+
id,
|
|
117
|
+
preset,
|
|
118
|
+
pattern,
|
|
119
|
+
}: Readonly<{
|
|
120
|
+
children: ReactNode;
|
|
121
|
+
className?: string;
|
|
122
|
+
id?: string;
|
|
123
|
+
/** Opt in to the separately imported product-marketing-preset.css contract. */
|
|
124
|
+
preset?: MarketingPreset;
|
|
125
|
+
/**
|
|
126
|
+
* @deprecated Patterns are retired. Every value, including the default,
|
|
127
|
+
* paints the flat palette background; the attribute is still emitted for
|
|
128
|
+
* compatibility with existing selectors.
|
|
129
|
+
*/
|
|
130
|
+
pattern?: MarketingPattern;
|
|
131
|
+
}>) {
|
|
132
|
+
if (preset !== undefined && preset !== "editorial" && preset !== "minimal") throw new RangeError("Unknown marketing preset.");
|
|
133
|
+
assertMarketingPattern(pattern);
|
|
134
|
+
return (
|
|
135
|
+
<div className={classNames("hraness-marketing-page", className)} data-hraness-marketing="page" data-hraness-marketing-preset={preset} data-hraness-pattern={pattern} id={id}>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Opening container for the hero. It paints the flat palette background: the
|
|
143
|
+
* retired grain, cell, and pattern textures no longer render. It never creates
|
|
144
|
+
* an overlay or a fixed-position containing block.
|
|
145
|
+
*/
|
|
146
|
+
export function MarketingField({ children, className, pattern }: Readonly<{
|
|
147
|
+
children: ReactNode;
|
|
148
|
+
className?: string;
|
|
149
|
+
/** @deprecated Patterns are retired; every value renders as `none`. */
|
|
150
|
+
pattern?: MarketingPattern;
|
|
151
|
+
}>) {
|
|
152
|
+
assertMarketingPattern(pattern);
|
|
153
|
+
return <div className={["hraness-marketing-field", className].filter(Boolean).join(" ")} data-hraness-marketing="field" data-hraness-pattern={pattern}>{children}</div>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type MarketingMainClearance = "scroll" | "pad";
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Main landmark that inherits --hraness-sticky-offset from a preceding sticky
|
|
160
|
+
* header. Hash and skip targets use the offset as scroll-margin. Pass
|
|
161
|
+
* clearance="pad" when the header is out of flow.
|
|
162
|
+
*/
|
|
163
|
+
export function MarketingMain({
|
|
164
|
+
children,
|
|
165
|
+
className,
|
|
166
|
+
clearance = "scroll",
|
|
167
|
+
id = "main-content",
|
|
168
|
+
}: Readonly<{
|
|
169
|
+
children: ReactNode;
|
|
170
|
+
className?: string;
|
|
171
|
+
clearance?: MarketingMainClearance;
|
|
172
|
+
id?: string;
|
|
173
|
+
}>) {
|
|
174
|
+
if (clearance !== "scroll" && clearance !== "pad") throw new RangeError("Marketing main clearance must be scroll or pad.");
|
|
175
|
+
return (
|
|
176
|
+
<main
|
|
177
|
+
className={classNames("hraness-marketing-main", className, clearance === "pad" ? "pad" : "default")}
|
|
178
|
+
data-hraness-clearance={clearance === "pad" ? "pad" : undefined}
|
|
179
|
+
data-hraness-marketing="main"
|
|
180
|
+
id={id}
|
|
181
|
+
>
|
|
182
|
+
{children}
|
|
183
|
+
</main>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface MarketingCardItem {
|
|
188
|
+
readonly art?: ReactNode;
|
|
189
|
+
readonly href?: string;
|
|
190
|
+
readonly meta?: ReactNode;
|
|
191
|
+
readonly title: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Stretching product card row. Direct children share the tallest item's
|
|
196
|
+
* height. Meta sits in a reserved two-line block; titles wrap without
|
|
197
|
+
* clamping. Per-card art wells clip overflow so a logo surface cannot
|
|
198
|
+
* paint through the gutter.
|
|
199
|
+
*/
|
|
200
|
+
export function MarketingCardRow({
|
|
201
|
+
ariaLabel,
|
|
202
|
+
cards,
|
|
203
|
+
children,
|
|
204
|
+
className,
|
|
205
|
+
}: Readonly<{
|
|
206
|
+
ariaLabel?: string;
|
|
207
|
+
cards?: readonly MarketingCardItem[];
|
|
208
|
+
children?: ReactNode;
|
|
209
|
+
className?: string;
|
|
210
|
+
}>) {
|
|
211
|
+
return (
|
|
212
|
+
<div
|
|
213
|
+
aria-label={ariaLabel}
|
|
214
|
+
className={classNames("hraness-marketing-card-row", className)}
|
|
215
|
+
data-hraness-marketing="card-row"
|
|
216
|
+
>
|
|
217
|
+
{cards?.map((card) => (
|
|
218
|
+
<MarketingCard
|
|
219
|
+
key={card.title}
|
|
220
|
+
title={card.title}
|
|
221
|
+
{...(card.art === undefined ? {} : { art: card.art })}
|
|
222
|
+
{...(card.href === undefined ? {} : { href: card.href })}
|
|
223
|
+
{...(card.meta === undefined ? {} : { meta: card.meta })}
|
|
224
|
+
/>
|
|
225
|
+
))}
|
|
226
|
+
{children}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Clipped media well. Keep logo and background paint inside the card. */
|
|
232
|
+
export function MarketingCardArt({
|
|
233
|
+
children,
|
|
234
|
+
className,
|
|
235
|
+
}: Readonly<{
|
|
236
|
+
children?: ReactNode;
|
|
237
|
+
className?: string;
|
|
238
|
+
}>) {
|
|
239
|
+
return (
|
|
240
|
+
<div className={classNames("hraness-marketing-card__art", className)} data-hraness-marketing="card-art">
|
|
241
|
+
{children}
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isPresentNode(value: ReactNode): boolean {
|
|
247
|
+
return value !== undefined && value !== false && value !== null && value !== "";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function MarketingCard({
|
|
251
|
+
art,
|
|
252
|
+
children,
|
|
253
|
+
className,
|
|
254
|
+
href,
|
|
255
|
+
meta,
|
|
256
|
+
title,
|
|
257
|
+
}: Readonly<{
|
|
258
|
+
art?: ReactNode;
|
|
259
|
+
children?: ReactNode;
|
|
260
|
+
className?: string;
|
|
261
|
+
href?: string;
|
|
262
|
+
meta?: ReactNode;
|
|
263
|
+
title: string;
|
|
264
|
+
}>) {
|
|
265
|
+
const body = (
|
|
266
|
+
<>
|
|
267
|
+
{isPresentNode(art) ? <MarketingCardArt>{art}</MarketingCardArt> : null}
|
|
268
|
+
<h3 className={classNames("hraness-marketing-card__title")}>{title}</h3>
|
|
269
|
+
{meta === undefined || meta === "" ? null : <p className={classNames("hraness-marketing-card__meta")}>{meta}</p>}
|
|
270
|
+
{isPresentNode(children) ? <div className={classNames("hraness-marketing-card__body")}>{children}</div> : null}
|
|
271
|
+
</>
|
|
272
|
+
);
|
|
273
|
+
if (href === undefined) {
|
|
274
|
+
return (
|
|
275
|
+
<article className={classNames("hraness-marketing-card", className)} data-hraness-marketing="card">
|
|
276
|
+
{body}
|
|
277
|
+
</article>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return (
|
|
281
|
+
<a className={classNames("hraness-marketing-card", className)} data-hraness-marketing="card" href={href}>
|
|
282
|
+
{body}
|
|
283
|
+
</a>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function MarketingSiteHeader({
|
|
288
|
+
action,
|
|
289
|
+
ariaLabel = "Site",
|
|
290
|
+
brand,
|
|
291
|
+
brandHref = "/",
|
|
292
|
+
brandLabel,
|
|
293
|
+
brandMark,
|
|
294
|
+
className,
|
|
295
|
+
links,
|
|
296
|
+
trailing,
|
|
297
|
+
sticky = true,
|
|
298
|
+
}: Readonly<{
|
|
299
|
+
action?: MarketingAction;
|
|
300
|
+
ariaLabel?: string;
|
|
301
|
+
brand: ReactNode;
|
|
302
|
+
brandHref?: string;
|
|
303
|
+
brandLabel?: string;
|
|
304
|
+
/** Transparent product mark; the original image remains the fallback. */
|
|
305
|
+
brandMark?: string;
|
|
306
|
+
className?: string;
|
|
307
|
+
links: readonly MarketingLink[];
|
|
308
|
+
trailing?: ReactNode;
|
|
309
|
+
/** Keep embedded previews in document flow; product headers stick by default. */
|
|
310
|
+
sticky?: boolean;
|
|
311
|
+
}>) {
|
|
312
|
+
const brandProperties = brandLabel === undefined ? {} : { "aria-label": brandLabel };
|
|
313
|
+
return (
|
|
314
|
+
<header
|
|
315
|
+
className={classNames("hraness-marketing-header", className, sticky ? "default" : "static")}
|
|
316
|
+
data-hraness-marketing="header"
|
|
317
|
+
data-position={sticky ? "sticky" : "static"}
|
|
318
|
+
>
|
|
319
|
+
<div className={classNames("hraness-marketing-header__inner")}>
|
|
320
|
+
<a className={classNames("hraness-marketing-header__brand")} data-foil="" href={brandHref} {...brandProperties}>
|
|
321
|
+
{brandMark === undefined ? null : <FoilMark src={brandMark} />}
|
|
322
|
+
{brand}
|
|
323
|
+
</a>
|
|
324
|
+
<nav aria-label={ariaLabel} className={classNames("hraness-marketing-header__nav")}>
|
|
325
|
+
{links.map((link) => (
|
|
326
|
+
<a
|
|
327
|
+
aria-current={link.current === true ? "page" : undefined}
|
|
328
|
+
className={classNames("hraness-marketing-header__link", undefined, link.current === true ? "current" : "default")}
|
|
329
|
+
href={link.href}
|
|
330
|
+
key={`${link.href}-${link.label}`}
|
|
331
|
+
>
|
|
332
|
+
{link.label}
|
|
333
|
+
</a>
|
|
334
|
+
))}
|
|
335
|
+
</nav>
|
|
336
|
+
{action === undefined && trailing === undefined
|
|
337
|
+
? null
|
|
338
|
+
: (
|
|
339
|
+
<div className={classNames("hraness-marketing-header__actions")}>
|
|
340
|
+
{action === undefined
|
|
341
|
+
? null
|
|
342
|
+
: (
|
|
343
|
+
<a
|
|
344
|
+
className={classNames("hraness-marketing-action", undefined, `header-${action.emphasis ?? "primary"}`)}
|
|
345
|
+
data-emphasis={action.emphasis ?? "primary"}
|
|
346
|
+
data-foil={(action.emphasis ?? "primary") === "primary" ? "" : undefined}
|
|
347
|
+
href={action.href}
|
|
348
|
+
>
|
|
349
|
+
{action.label}
|
|
350
|
+
</a>
|
|
351
|
+
)}
|
|
352
|
+
{trailing}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</header>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* In-flow site footer. The brand lockup is always the product mark followed by
|
|
362
|
+
* its name; optional children carry a product note and optional links a quiet
|
|
363
|
+
* footer navigation. It rests at the end of document flow; when the shared
|
|
364
|
+
* network footer follows it directly, the marketing grammar joins the pair
|
|
365
|
+
* into one band.
|
|
366
|
+
*/
|
|
367
|
+
export function MarketingSiteFooter({
|
|
368
|
+
ariaLabel = "Site",
|
|
369
|
+
brand,
|
|
370
|
+
brandHref = "/",
|
|
371
|
+
brandLabel,
|
|
372
|
+
brandMark,
|
|
373
|
+
children,
|
|
374
|
+
className,
|
|
375
|
+
links = [],
|
|
376
|
+
linksLabel = "Footer navigation",
|
|
377
|
+
name,
|
|
378
|
+
}: Readonly<{
|
|
379
|
+
/** Landmark label; keep it distinct from the network footer on the same page. */
|
|
380
|
+
ariaLabel?: string;
|
|
381
|
+
/** The product mark, conventionally one inline SVG; rendered before `name`. */
|
|
382
|
+
brand: ReactNode;
|
|
383
|
+
brandHref?: string;
|
|
384
|
+
brandLabel?: string;
|
|
385
|
+
/**
|
|
386
|
+
* Transparent product mark. When set, the lockup renders the shared foil
|
|
387
|
+
* icon-plus-text treatment like the site header; `brand` remains the
|
|
388
|
+
* forced-color and no-mask fallback for that mark.
|
|
389
|
+
*/
|
|
390
|
+
brandMark?: string;
|
|
391
|
+
/** Product-owned note content between the lockup and the navigation. */
|
|
392
|
+
children?: ReactNode;
|
|
393
|
+
className?: string;
|
|
394
|
+
links?: readonly MarketingLink[];
|
|
395
|
+
linksLabel?: string;
|
|
396
|
+
/** The product name rendered inside the home link after `brand`. */
|
|
397
|
+
name: string;
|
|
398
|
+
}>) {
|
|
399
|
+
const brandProperties = brandLabel === undefined ? {} : { "aria-label": brandLabel };
|
|
400
|
+
const foilBrand = brandMark !== undefined;
|
|
401
|
+
return (
|
|
402
|
+
<footer aria-label={ariaLabel} className={classNames("hraness-marketing-footer", className)} data-hraness-marketing="footer">
|
|
403
|
+
<div className={classNames("hraness-marketing-footer__inner")}>
|
|
404
|
+
<a
|
|
405
|
+
className={classNames("hraness-marketing-footer__brand", undefined, foilBrand ? "foil" : "default")}
|
|
406
|
+
data-foil={foilBrand ? "" : undefined}
|
|
407
|
+
href={brandHref}
|
|
408
|
+
{...brandProperties}
|
|
409
|
+
>
|
|
410
|
+
{foilBrand ? <FoilMark fallback={brand} size={18} src={brandMark} /> : brand}
|
|
411
|
+
<span className={classNames("hraness-marketing-footer__name")}>{name}</span>
|
|
412
|
+
</a>
|
|
413
|
+
{children}
|
|
414
|
+
{links.length === 0
|
|
415
|
+
? null
|
|
416
|
+
: (
|
|
417
|
+
<nav aria-label={linksLabel} className={classNames("hraness-marketing-footer__nav")}>
|
|
418
|
+
{links.map((link) => (
|
|
419
|
+
<a
|
|
420
|
+
aria-current={link.current === true ? "page" : undefined}
|
|
421
|
+
className={classNames("hraness-marketing-footer__link", undefined, link.current === true ? "current" : "default")}
|
|
422
|
+
href={link.href}
|
|
423
|
+
key={`${link.href}-${link.label}`}
|
|
424
|
+
>
|
|
425
|
+
{link.label}
|
|
426
|
+
</a>
|
|
427
|
+
))}
|
|
428
|
+
</nav>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
</footer>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function MarketingFlow({
|
|
436
|
+
ariaLabel,
|
|
437
|
+
className,
|
|
438
|
+
steps,
|
|
439
|
+
}: Readonly<{
|
|
440
|
+
ariaLabel: string;
|
|
441
|
+
className?: string;
|
|
442
|
+
steps: readonly MarketingStep[];
|
|
443
|
+
}>) {
|
|
444
|
+
return (
|
|
445
|
+
<ol
|
|
446
|
+
aria-label={ariaLabel}
|
|
447
|
+
className={classNames("hraness-marketing-flow", className)}
|
|
448
|
+
data-hraness-marketing="flow"
|
|
449
|
+
>
|
|
450
|
+
{steps.map((step, index) => (
|
|
451
|
+
<li className={classNames("hraness-marketing-flow__step", undefined, index === 0 ? "first" : "default")} key={`${String(index)}-${step.label}`}>
|
|
452
|
+
<span aria-hidden="true" className={classNames("hraness-marketing-flow__number")}>
|
|
453
|
+
{String(index + 1).padStart(2, "0")}
|
|
454
|
+
</span>
|
|
455
|
+
<div className={classNames("hraness-marketing-flow__body")}>
|
|
456
|
+
<strong className={classNames("hraness-marketing-flow__label")}>{step.label}</strong>
|
|
457
|
+
{step.code === undefined
|
|
458
|
+
? null
|
|
459
|
+
: <SyntaxCode className={classNames("hraness-marketing-flow__code")} code={step.code} styles="classes" />}
|
|
460
|
+
{step.detail === undefined
|
|
461
|
+
? null
|
|
462
|
+
: <p className={classNames("hraness-marketing-flow__detail")}>{step.detail}</p>}
|
|
463
|
+
</div>
|
|
464
|
+
</li>
|
|
465
|
+
))}
|
|
466
|
+
</ol>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function MarketingFacts({
|
|
471
|
+
className,
|
|
472
|
+
columns,
|
|
473
|
+
facts,
|
|
474
|
+
}: Readonly<{
|
|
475
|
+
className?: string;
|
|
476
|
+
/** Static desktop columns without inline styles; omission uses the collection length. Mobile stays two columns. */
|
|
477
|
+
columns?: MarketingColumnCount;
|
|
478
|
+
facts: readonly MarketingFact[];
|
|
479
|
+
}>) {
|
|
480
|
+
const rootClassName = marketingColumnClassName("hraness-marketing-facts", className, columns);
|
|
481
|
+
if (facts.length === 0) return null;
|
|
482
|
+
return (
|
|
483
|
+
<dl
|
|
484
|
+
className={rootClassName}
|
|
485
|
+
data-hraness-marketing="facts"
|
|
486
|
+
style={columns === undefined ? { "--hraness-marketing-fact-columns": String(facts.length) } as Record<string, string> : undefined}
|
|
487
|
+
>
|
|
488
|
+
{facts.map((fact, index) => (
|
|
489
|
+
<div className={classNames("hraness-marketing-facts__item", undefined, marketingFactCellVariant(index))} key={`${fact.label}-${fact.value}`}>
|
|
490
|
+
<dt className={classNames("hraness-marketing-facts__label")}>{fact.label}</dt>
|
|
491
|
+
<dd className={classNames("hraness-marketing-facts__body")}>
|
|
492
|
+
<strong className={classNames("hraness-marketing-facts__value")}>{fact.value}</strong>
|
|
493
|
+
{fact.detail === undefined ? null : <span className={classNames("hraness-marketing-facts__detail")}>{fact.detail}</span>}
|
|
494
|
+
</dd>
|
|
495
|
+
</div>
|
|
496
|
+
))}
|
|
497
|
+
</dl>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export interface ProductHeroProps {
|
|
502
|
+
readonly actions?: readonly MarketingAction[];
|
|
503
|
+
readonly align?: "center" | "start";
|
|
504
|
+
/**
|
|
505
|
+
* @deprecated Hero backdrops are retired. The hero renders no decorative
|
|
506
|
+
* artwork, light field, or blur, whatever this holds. Show real product
|
|
507
|
+
* proof in `frame` instead.
|
|
508
|
+
*/
|
|
509
|
+
readonly backdrop?: ReactNode | false;
|
|
510
|
+
readonly boundary?: string;
|
|
511
|
+
readonly className?: string;
|
|
512
|
+
/** A concrete request a reader could make, shown under the summary. */
|
|
513
|
+
readonly example?: string;
|
|
514
|
+
readonly eyebrow?: string;
|
|
515
|
+
readonly facts?: readonly MarketingFact[];
|
|
516
|
+
/** Static fact columns without inline styles; omission uses the facts length. Mobile stays two columns. */
|
|
517
|
+
readonly factsColumns?: MarketingColumnCount;
|
|
518
|
+
/** A product frame or other proof rendered full width below the copy. */
|
|
519
|
+
readonly frame?: ReactNode;
|
|
520
|
+
readonly heading: string;
|
|
521
|
+
readonly headingId: string;
|
|
522
|
+
readonly headingLevel?: MarketingHeadingLevel;
|
|
523
|
+
readonly name: string;
|
|
524
|
+
/** Product-owned notice after the copy boundary, without an added wrapper. */
|
|
525
|
+
readonly notice?: ReactNode;
|
|
526
|
+
readonly proof?: Readonly<{
|
|
527
|
+
readonly content: ReactNode;
|
|
528
|
+
readonly heading: string;
|
|
529
|
+
readonly kicker?: string;
|
|
530
|
+
}>;
|
|
531
|
+
readonly summary: string;
|
|
532
|
+
readonly tone?: MarketingTone;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function ProductHero({
|
|
536
|
+
actions = [],
|
|
537
|
+
align = "center",
|
|
538
|
+
boundary,
|
|
539
|
+
className,
|
|
540
|
+
example,
|
|
541
|
+
eyebrow,
|
|
542
|
+
facts = [],
|
|
543
|
+
factsColumns,
|
|
544
|
+
frame,
|
|
545
|
+
heading,
|
|
546
|
+
headingId,
|
|
547
|
+
headingLevel = 1,
|
|
548
|
+
name,
|
|
549
|
+
notice,
|
|
550
|
+
proof,
|
|
551
|
+
summary,
|
|
552
|
+
tone = "paper",
|
|
553
|
+
}: Readonly<ProductHeroProps>) {
|
|
554
|
+
return (
|
|
555
|
+
<header
|
|
556
|
+
aria-labelledby={headingId}
|
|
557
|
+
className={classNames("hraness-marketing-hero", className, tone === "accent" ? "accent" : "default")}
|
|
558
|
+
data-align={align}
|
|
559
|
+
data-hraness-marketing="hero"
|
|
560
|
+
data-tone={tone}
|
|
561
|
+
>
|
|
562
|
+
<div className={classNames("hraness-marketing-hero__copy", undefined, align === "start" ? "start" : "default")}>
|
|
563
|
+
{eyebrow === undefined || eyebrow === "" ? null : <p className={classNames("hraness-marketing-hero__eyebrow", undefined, tone === "accent" ? "accent" : "default")}>{eyebrow}</p>}
|
|
564
|
+
<p className={classNames("hraness-marketing-hero__name")}>{name}</p>
|
|
565
|
+
<Heading className={classNames("hraness-marketing-hero__heading")} id={headingId} level={headingLevel}>
|
|
566
|
+
{heading}
|
|
567
|
+
</Heading>
|
|
568
|
+
<p className={classNames("hraness-marketing-hero__summary")}>{summary}</p>
|
|
569
|
+
{example === undefined
|
|
570
|
+
? null
|
|
571
|
+
: <p className={classNames("hraness-marketing-hero__example")}>{example}</p>}
|
|
572
|
+
<MarketingActions actions={actions} className={classNames("hraness-marketing-hero__actions", undefined, align === "start" ? "start" : "default")} context="hero" tone={tone} />
|
|
573
|
+
{boundary === undefined
|
|
574
|
+
? null
|
|
575
|
+
: <p className={classNames("hraness-marketing-hero__boundary")}>{boundary}</p>}
|
|
576
|
+
{notice}
|
|
577
|
+
</div>
|
|
578
|
+
{frame === undefined
|
|
579
|
+
? null
|
|
580
|
+
: <div className={classNames("hraness-marketing-hero__frame")}>{frame}</div>}
|
|
581
|
+
{proof === undefined
|
|
582
|
+
? null
|
|
583
|
+
: (
|
|
584
|
+
<aside className={classNames("hraness-marketing-proof")} aria-labelledby={`${headingId}-proof`}>
|
|
585
|
+
{proof.kicker === undefined
|
|
586
|
+
? null
|
|
587
|
+
: <p className={classNames("hraness-marketing-proof__kicker")}>{proof.kicker}</p>}
|
|
588
|
+
<Heading
|
|
589
|
+
className={classNames("hraness-marketing-proof__heading")}
|
|
590
|
+
id={`${headingId}-proof`}
|
|
591
|
+
level={childHeadingLevel(headingLevel)}
|
|
592
|
+
>
|
|
593
|
+
{proof.heading}
|
|
594
|
+
</Heading>
|
|
595
|
+
{proof.content}
|
|
596
|
+
</aside>
|
|
597
|
+
)}
|
|
598
|
+
<MarketingFacts facts={facts} {...(factsColumns === undefined ? {} : { columns: factsColumns })} />
|
|
599
|
+
</header>
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export interface MarketingPillar {
|
|
604
|
+
readonly label: string;
|
|
605
|
+
readonly summary: string;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function MarketingPillars({
|
|
609
|
+
ariaLabel,
|
|
610
|
+
className,
|
|
611
|
+
columns,
|
|
612
|
+
pillars,
|
|
613
|
+
}: Readonly<{
|
|
614
|
+
ariaLabel: string;
|
|
615
|
+
className?: string;
|
|
616
|
+
/** Static desktop columns without inline styles; omission uses the collection length. Mobile stays one column. */
|
|
617
|
+
columns?: MarketingColumnCount;
|
|
618
|
+
pillars: readonly MarketingPillar[];
|
|
619
|
+
}>) {
|
|
620
|
+
const rootClassName = marketingColumnClassName("hraness-marketing-pillars", className, columns);
|
|
621
|
+
if (pillars.length === 0) return null;
|
|
622
|
+
return (
|
|
623
|
+
<dl
|
|
624
|
+
aria-label={ariaLabel}
|
|
625
|
+
className={rootClassName}
|
|
626
|
+
data-hraness-marketing="pillars"
|
|
627
|
+
style={columns === undefined ? { "--hraness-marketing-pillar-columns": String(pillars.length) } as Record<string, string> : undefined}
|
|
628
|
+
>
|
|
629
|
+
{pillars.map((pillar, index) => (
|
|
630
|
+
<div className={classNames("hraness-marketing-pillars__item", undefined, index === 0 ? "default" : "later")} key={pillar.label}>
|
|
631
|
+
<dt className={classNames("hraness-marketing-pillars__label")}>{pillar.label}</dt>
|
|
632
|
+
<dd className={classNames("hraness-marketing-pillars__summary")}>{pillar.summary}</dd>
|
|
633
|
+
</div>
|
|
634
|
+
))}
|
|
635
|
+
</dl>
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function MarketingInstallPanel({
|
|
640
|
+
children,
|
|
641
|
+
className,
|
|
642
|
+
eyebrow,
|
|
643
|
+
heading,
|
|
644
|
+
headingId,
|
|
645
|
+
headingLevel = 2,
|
|
646
|
+
id,
|
|
647
|
+
note,
|
|
648
|
+
}: Readonly<{
|
|
649
|
+
children: ReactNode;
|
|
650
|
+
className?: string;
|
|
651
|
+
eyebrow?: string;
|
|
652
|
+
heading: string;
|
|
653
|
+
headingId: string;
|
|
654
|
+
headingLevel?: MarketingHeadingLevel;
|
|
655
|
+
id?: string;
|
|
656
|
+
/** Product-owned supporting copy directly after the heading. */
|
|
657
|
+
note?: ReactNode;
|
|
658
|
+
}>) {
|
|
659
|
+
return (
|
|
660
|
+
<section
|
|
661
|
+
aria-labelledby={headingId}
|
|
662
|
+
className={classNames("hraness-marketing-install", className)}
|
|
663
|
+
data-hraness-marketing="install"
|
|
664
|
+
id={id}
|
|
665
|
+
>
|
|
666
|
+
<div className={classNames("hraness-marketing-install__heading-group")}>
|
|
667
|
+
{eyebrow === undefined || eyebrow === "" ? null : <p className={classNames("hraness-marketing-install__eyebrow")}>{eyebrow}</p>}
|
|
668
|
+
<Heading className={classNames("hraness-marketing-install__heading")} id={headingId} level={headingLevel}>
|
|
669
|
+
{heading}
|
|
670
|
+
</Heading>
|
|
671
|
+
{note}
|
|
672
|
+
</div>
|
|
673
|
+
<div className={classNames("hraness-marketing-install__commands")}>{children}</div>
|
|
674
|
+
</section>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export type MarketingProofFrameChrome = "window" | "browser" | "terminal";
|
|
679
|
+
|
|
680
|
+
/** Reads a frame URL for display: host and path, no scheme, no query or fragment. */
|
|
681
|
+
export function marketingProofFrameAddress(url: string): string {
|
|
682
|
+
let parsed: URL;
|
|
683
|
+
try {
|
|
684
|
+
parsed = new URL(url);
|
|
685
|
+
} catch {
|
|
686
|
+
throw new RangeError(`Proof frame url must be an absolute URL; received ${JSON.stringify(url)}.`);
|
|
687
|
+
}
|
|
688
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new RangeError("Proof frame url must use http or https.");
|
|
689
|
+
const path = parsed.pathname === "/" ? "" : parsed.pathname;
|
|
690
|
+
return `${parsed.host}${path}`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* A captioned frame around a screenshot, recording, or code-built mockup.
|
|
695
|
+
* `chrome` draws a window title bar, a browser address bar, or a terminal
|
|
696
|
+
* title bar above the content. A `title` without `chrome` keeps the window
|
|
697
|
+
* bar. The browser bar shows `url` as host and path.
|
|
698
|
+
*/
|
|
699
|
+
export function MarketingProofFrame({
|
|
700
|
+
caption,
|
|
701
|
+
children,
|
|
702
|
+
chrome,
|
|
703
|
+
className,
|
|
704
|
+
credit,
|
|
705
|
+
title,
|
|
706
|
+
url,
|
|
707
|
+
}: Readonly<{
|
|
708
|
+
caption?: string;
|
|
709
|
+
children: ReactNode;
|
|
710
|
+
/** The title bar drawn above the content. Defaults to `window` when `title` is given, otherwise none. */
|
|
711
|
+
chrome?: MarketingProofFrameChrome;
|
|
712
|
+
className?: string;
|
|
713
|
+
credit?: string;
|
|
714
|
+
/** Window or terminal title; for the browser bar, the address shown when `url` is omitted. */
|
|
715
|
+
title?: string;
|
|
716
|
+
/** The page address shown in the browser bar. */
|
|
717
|
+
url?: string;
|
|
718
|
+
}>) {
|
|
719
|
+
if (chrome !== undefined && chrome !== "window" && chrome !== "browser" && chrome !== "terminal") {
|
|
720
|
+
throw new RangeError(`Unknown proof frame chrome: ${String(chrome)}.`);
|
|
721
|
+
}
|
|
722
|
+
if (url !== undefined && chrome !== "browser") throw new RangeError("Proof frame url needs chrome=\"browser\".");
|
|
723
|
+
const bar = chrome ?? (title === undefined ? undefined : "window");
|
|
724
|
+
const label = bar === "browser" ? (url === undefined ? title : marketingProofFrameAddress(url)) : title;
|
|
725
|
+
if (bar !== undefined && (label === undefined || label.trim() === "")) {
|
|
726
|
+
throw new RangeError(`Proof frame chrome=${bar} needs ${bar === "browser" ? "a url or title" : "a title"}.`);
|
|
727
|
+
}
|
|
728
|
+
return (
|
|
729
|
+
<figure
|
|
730
|
+
className={classNames("hraness-marketing-proof-frame", className)}
|
|
731
|
+
data-chrome={bar}
|
|
732
|
+
data-hraness-marketing="proof-frame"
|
|
733
|
+
>
|
|
734
|
+
{bar === undefined
|
|
735
|
+
? null
|
|
736
|
+
: (
|
|
737
|
+
<div aria-hidden="true" className={classNames("hraness-marketing-proof-frame__chrome")}>
|
|
738
|
+
<span className={classNames("hraness-marketing-proof-frame__lights")}>
|
|
739
|
+
<span className={classNames("hraness-marketing-proof-frame__light")} />
|
|
740
|
+
<span className={classNames("hraness-marketing-proof-frame__light")} />
|
|
741
|
+
<span className={classNames("hraness-marketing-proof-frame__light")} />
|
|
742
|
+
</span>
|
|
743
|
+
{bar === "browser"
|
|
744
|
+
? <span className={classNames("hraness-marketing-proof-frame__address")}>{label}</span>
|
|
745
|
+
: <span className={classNames("hraness-marketing-proof-frame__title", undefined, bar === "terminal" ? "terminal" : "default")}>{label}</span>}
|
|
746
|
+
</div>
|
|
747
|
+
)}
|
|
748
|
+
<div className={classNames("hraness-marketing-proof-frame__content")}>{children}</div>
|
|
749
|
+
{caption === undefined && credit === undefined
|
|
750
|
+
? null
|
|
751
|
+
: (
|
|
752
|
+
<figcaption className={classNames("hraness-marketing-proof-frame__caption")}>
|
|
753
|
+
{caption === undefined ? null : <span>{caption}</span>}
|
|
754
|
+
{credit === undefined ? null : <small className={classNames("hraness-marketing-proof-frame__credit")}>{credit}</small>}
|
|
755
|
+
</figcaption>
|
|
756
|
+
)}
|
|
757
|
+
</figure>
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export interface MarketingDataTableColumn {
|
|
762
|
+
readonly label: string;
|
|
763
|
+
/** Right-aligns the column and keeps values on one line. */
|
|
764
|
+
readonly numeric?: boolean;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export interface MarketingDataTableCell {
|
|
768
|
+
readonly content: ReactNode;
|
|
769
|
+
/** Marks a notable outcome: `positive` accents the value, `negative` mutes it. */
|
|
770
|
+
readonly tone?: "positive" | "negative";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function dataTableCell(cell: ReactNode | MarketingDataTableCell): {
|
|
774
|
+
content: ReactNode;
|
|
775
|
+
tone?: "positive" | "negative";
|
|
776
|
+
} {
|
|
777
|
+
if (cell !== null && typeof cell === "object" && !Array.isArray(cell) && "content" in cell) {
|
|
778
|
+
return cell as MarketingDataTableCell;
|
|
779
|
+
}
|
|
780
|
+
return { content: cell };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* A dated, captioned comparison table. Renders a ruled figure whose scrollable
|
|
785
|
+
* region never escapes the measure; the first column is each row's heading and
|
|
786
|
+
* `note` keeps the measurement's scope honest underneath.
|
|
787
|
+
*/
|
|
788
|
+
export function MarketingDataTable({
|
|
789
|
+
caption,
|
|
790
|
+
className,
|
|
791
|
+
columns,
|
|
792
|
+
meta,
|
|
793
|
+
note,
|
|
794
|
+
rows,
|
|
795
|
+
}: Readonly<{
|
|
796
|
+
/** Headline naming the measurement. */
|
|
797
|
+
caption: ReactNode;
|
|
798
|
+
className?: string;
|
|
799
|
+
columns: readonly MarketingDataTableColumn[];
|
|
800
|
+
/** Date, source, or sample size — rendered muted beside the caption. */
|
|
801
|
+
meta?: ReactNode;
|
|
802
|
+
/** Caveat rendered under the table; keep the measurement's scope honest. */
|
|
803
|
+
note?: ReactNode;
|
|
804
|
+
rows: readonly ReadonlyArray<ReactNode | MarketingDataTableCell>[];
|
|
805
|
+
}>) {
|
|
806
|
+
if (columns.length === 0) throw new RangeError("Marketing data table needs at least one column.");
|
|
807
|
+
for (const row of rows) {
|
|
808
|
+
if (row.length !== columns.length) throw new RangeError("Marketing data table rows must match the column count.");
|
|
809
|
+
}
|
|
810
|
+
return (
|
|
811
|
+
<figure
|
|
812
|
+
className={classNames("hraness-marketing-data-table", className)}
|
|
813
|
+
data-hraness-marketing="data-table"
|
|
814
|
+
>
|
|
815
|
+
<figcaption className={classNames("hraness-marketing-data-table__head")}>
|
|
816
|
+
<span className={classNames("hraness-marketing-data-table__title")}>{caption}</span>
|
|
817
|
+
{meta === undefined ? null : <span className={classNames("hraness-marketing-data-table__meta")}>{meta}</span>}
|
|
818
|
+
</figcaption>
|
|
819
|
+
<div className={classNames("hraness-marketing-data-table__scroll")}>
|
|
820
|
+
<table className={classNames("hraness-marketing-data-table__table")}>
|
|
821
|
+
<thead>
|
|
822
|
+
<tr>
|
|
823
|
+
{columns.map((column, index) => (
|
|
824
|
+
<th
|
|
825
|
+
className={classNames("hraness-marketing-data-table__heading")}
|
|
826
|
+
data-numeric={column.numeric === true ? "" : undefined}
|
|
827
|
+
key={index}
|
|
828
|
+
scope="col"
|
|
829
|
+
>
|
|
830
|
+
{column.label}
|
|
831
|
+
</th>
|
|
832
|
+
))}
|
|
833
|
+
</tr>
|
|
834
|
+
</thead>
|
|
835
|
+
<tbody>
|
|
836
|
+
{rows.map((row, rowIndex) => (
|
|
837
|
+
<tr key={rowIndex}>
|
|
838
|
+
{row.map((cell, cellIndex) => {
|
|
839
|
+
const { content, tone } = dataTableCell(cell);
|
|
840
|
+
if (cellIndex === 0) {
|
|
841
|
+
return (
|
|
842
|
+
<th
|
|
843
|
+
className={classNames("hraness-marketing-data-table__row-heading")}
|
|
844
|
+
data-numeric={columns[0]?.numeric === true ? "" : undefined}
|
|
845
|
+
key={cellIndex}
|
|
846
|
+
scope="row"
|
|
847
|
+
>
|
|
848
|
+
{content}
|
|
849
|
+
</th>
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return (
|
|
853
|
+
<td
|
|
854
|
+
className={classNames("hraness-marketing-data-table__cell")}
|
|
855
|
+
data-numeric={columns[cellIndex]?.numeric === true ? "" : undefined}
|
|
856
|
+
data-tone={tone}
|
|
857
|
+
key={cellIndex}
|
|
858
|
+
>
|
|
859
|
+
{content}
|
|
860
|
+
</td>
|
|
861
|
+
);
|
|
862
|
+
})}
|
|
863
|
+
</tr>
|
|
864
|
+
))}
|
|
865
|
+
</tbody>
|
|
866
|
+
</table>
|
|
867
|
+
</div>
|
|
868
|
+
{note === undefined ? null : <p className={classNames("hraness-marketing-data-table__note")}>{note}</p>}
|
|
869
|
+
</figure>
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* A mono code block with shared syntax highlighting. Wraps `SyntaxCode` in the
|
|
875
|
+
* `pre` the marketing grammar styles and scrolls inside the measure when a
|
|
876
|
+
* line overflows.
|
|
877
|
+
*/
|
|
878
|
+
export function MarketingCodeBlock({
|
|
879
|
+
className,
|
|
880
|
+
code,
|
|
881
|
+
language,
|
|
882
|
+
}: Readonly<{
|
|
883
|
+
className?: string;
|
|
884
|
+
code: string;
|
|
885
|
+
/** A language/fence hint; omitted hints use conservative automatic selection. */
|
|
886
|
+
language?: string;
|
|
887
|
+
}>) {
|
|
888
|
+
return (
|
|
889
|
+
<pre className={classNames("hraness-marketing-code", className)} data-hraness-marketing="code">
|
|
890
|
+
<SyntaxCode code={code} {...(language === undefined ? {} : { language })} styles="classes" />
|
|
891
|
+
</pre>
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** The section's native paragraph label, reusable by product-owned prose. */
|
|
896
|
+
export function MarketingSectionLabel({
|
|
897
|
+
children,
|
|
898
|
+
className,
|
|
899
|
+
size = "default",
|
|
900
|
+
}: Readonly<{
|
|
901
|
+
children: string;
|
|
902
|
+
className?: string;
|
|
903
|
+
size?: "default" | "body";
|
|
904
|
+
}>) {
|
|
905
|
+
if (size !== "default" && size !== "body") throw new RangeError("Marketing label size must be default or body.");
|
|
906
|
+
return <p className={classNames("hraness-marketing-section__label", className, size)}
|
|
907
|
+
data-size={size === "body" ? "body" : undefined}>{children}</p>;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export function MarketingSection({
|
|
911
|
+
children,
|
|
912
|
+
className,
|
|
913
|
+
heading,
|
|
914
|
+
headingId,
|
|
915
|
+
headingLevel = 2,
|
|
916
|
+
id,
|
|
917
|
+
label,
|
|
918
|
+
layout = "stack",
|
|
919
|
+
summary,
|
|
920
|
+
}: Readonly<{
|
|
921
|
+
children: ReactNode;
|
|
922
|
+
className?: string;
|
|
923
|
+
heading: string;
|
|
924
|
+
headingId: string;
|
|
925
|
+
headingLevel?: MarketingHeadingLevel;
|
|
926
|
+
id?: string;
|
|
927
|
+
label?: string;
|
|
928
|
+
layout?: "split" | "split-reverse" | "stack";
|
|
929
|
+
summary?: string;
|
|
930
|
+
}>) {
|
|
931
|
+
return (
|
|
932
|
+
<section
|
|
933
|
+
aria-labelledby={headingId}
|
|
934
|
+
className={classNames("hraness-marketing-section", className, layout === "stack" ? "default" : "split")}
|
|
935
|
+
data-hraness-marketing="section"
|
|
936
|
+
data-layout={layout}
|
|
937
|
+
id={id}
|
|
938
|
+
>
|
|
939
|
+
<div className={classNames("hraness-marketing-section__heading-group", undefined, layout === "stack" ? "default" : layout === "split" ? "split" : "reverse")}>
|
|
940
|
+
{label === undefined || label === "" ? null : <MarketingSectionLabel>{label}</MarketingSectionLabel>}
|
|
941
|
+
<Heading className={classNames("hraness-marketing-section__heading")} id={headingId} level={headingLevel}>
|
|
942
|
+
{heading}
|
|
943
|
+
</Heading>
|
|
944
|
+
{summary === undefined
|
|
945
|
+
? null
|
|
946
|
+
: <p className={classNames("hraness-marketing-section__summary")}>{summary}</p>}
|
|
947
|
+
</div>
|
|
948
|
+
<div className={classNames("hraness-marketing-section__body")}>{children}</div>
|
|
949
|
+
</section>
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
type MarketingCollectionPrefix = "interfaces" | "pricing" | "primitives" | "questions" | "quotes" | "related" | "trust";
|
|
954
|
+
|
|
955
|
+
interface MarketingCollectionHeaderProps {
|
|
956
|
+
readonly heading: string;
|
|
957
|
+
readonly headingId: string;
|
|
958
|
+
readonly headingLevel: MarketingHeadingLevel;
|
|
959
|
+
readonly label: string | undefined;
|
|
960
|
+
readonly prefix: MarketingCollectionPrefix;
|
|
961
|
+
readonly summary: string | undefined;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function MarketingCollectionHeader({
|
|
965
|
+
heading,
|
|
966
|
+
headingId,
|
|
967
|
+
headingLevel,
|
|
968
|
+
label,
|
|
969
|
+
prefix,
|
|
970
|
+
summary,
|
|
971
|
+
}: Readonly<MarketingCollectionHeaderProps>) {
|
|
972
|
+
return (
|
|
973
|
+
<header className={classNames(`hraness-marketing-${prefix}__header`)}>
|
|
974
|
+
{label === undefined || label === "" ? null : <p className={classNames(`hraness-marketing-${prefix}__label`)}>{label}</p>}
|
|
975
|
+
<Heading className={classNames(`hraness-marketing-${prefix}__heading`)} id={headingId} level={headingLevel}>
|
|
976
|
+
{heading}
|
|
977
|
+
</Heading>
|
|
978
|
+
{summary === undefined ? null : <p className={classNames(`hraness-marketing-${prefix}__summary`)}>{summary}</p>}
|
|
979
|
+
</header>
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export interface MarketingPrimitive {
|
|
984
|
+
readonly example?: ReactNode;
|
|
985
|
+
readonly label: string;
|
|
986
|
+
readonly summary: string;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export function MarketingPrimitives({
|
|
990
|
+
className,
|
|
991
|
+
heading,
|
|
992
|
+
headingId,
|
|
993
|
+
headingLevel = 2,
|
|
994
|
+
id,
|
|
995
|
+
items,
|
|
996
|
+
label,
|
|
997
|
+
summary,
|
|
998
|
+
}: Readonly<{
|
|
999
|
+
className?: string;
|
|
1000
|
+
heading: string;
|
|
1001
|
+
headingId: string;
|
|
1002
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1003
|
+
id?: string;
|
|
1004
|
+
items: readonly MarketingPrimitive[];
|
|
1005
|
+
label?: string;
|
|
1006
|
+
summary?: string;
|
|
1007
|
+
}>) {
|
|
1008
|
+
return (
|
|
1009
|
+
<section
|
|
1010
|
+
aria-labelledby={headingId}
|
|
1011
|
+
className={classNames("hraness-marketing-primitives", className)}
|
|
1012
|
+
data-hraness-marketing="primitives"
|
|
1013
|
+
id={id}
|
|
1014
|
+
>
|
|
1015
|
+
<MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="primitives" />
|
|
1016
|
+
<ol className={classNames("hraness-marketing-primitives__list")}>
|
|
1017
|
+
{items.map((item, index) => (
|
|
1018
|
+
<li className={classNames("hraness-marketing-primitive")} key={item.label}>
|
|
1019
|
+
<span aria-hidden="true" className={classNames("hraness-marketing-primitive__number")}>
|
|
1020
|
+
{String(index + 1).padStart(2, "0")}
|
|
1021
|
+
</span>
|
|
1022
|
+
<Heading className={classNames("hraness-marketing-primitive__heading")} level={childHeadingLevel(headingLevel)}>
|
|
1023
|
+
{item.label}
|
|
1024
|
+
</Heading>
|
|
1025
|
+
<p className={classNames("hraness-marketing-primitive__summary")}>{item.summary}</p>
|
|
1026
|
+
{item.example}
|
|
1027
|
+
</li>
|
|
1028
|
+
))}
|
|
1029
|
+
</ol>
|
|
1030
|
+
</section>
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export type MarketingNoticeTone = "info" | "success" | "error";
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* One status or alert line inside the page column, such as a confirmation
|
|
1038
|
+
* after a redirect. Errors are announced as alerts; other tones as status.
|
|
1039
|
+
*/
|
|
1040
|
+
export function MarketingNotice({
|
|
1041
|
+
children,
|
|
1042
|
+
className,
|
|
1043
|
+
tone = "info",
|
|
1044
|
+
}: Readonly<{
|
|
1045
|
+
children: ReactNode;
|
|
1046
|
+
className?: string;
|
|
1047
|
+
tone?: MarketingNoticeTone;
|
|
1048
|
+
}>) {
|
|
1049
|
+
if (tone !== "info" && tone !== "success" && tone !== "error") throw new RangeError("Marketing notice tone must be info, success, or error.");
|
|
1050
|
+
return (
|
|
1051
|
+
<p
|
|
1052
|
+
className={classNames("hraness-marketing-notice", className, tone === "info" ? "default" : tone)}
|
|
1053
|
+
data-hraness-marketing="notice"
|
|
1054
|
+
data-tone={tone}
|
|
1055
|
+
role={tone === "error" ? "alert" : "status"}
|
|
1056
|
+
>
|
|
1057
|
+
{children}
|
|
1058
|
+
</p>
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export interface MarketingStat {
|
|
1063
|
+
readonly detail?: string;
|
|
1064
|
+
readonly label: string;
|
|
1065
|
+
readonly value: string;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export function MarketingStatStrip({
|
|
1069
|
+
ariaLabel,
|
|
1070
|
+
className,
|
|
1071
|
+
columns,
|
|
1072
|
+
source,
|
|
1073
|
+
stats,
|
|
1074
|
+
}: Readonly<{
|
|
1075
|
+
ariaLabel: string;
|
|
1076
|
+
className?: string;
|
|
1077
|
+
/** Static desktop columns without inline styles; omission uses the collection length. Mobile stays two columns. */
|
|
1078
|
+
columns?: MarketingColumnCount;
|
|
1079
|
+
/** Where the numbers come from and when they were checked. */
|
|
1080
|
+
source?: ReactNode;
|
|
1081
|
+
stats: readonly MarketingStat[];
|
|
1082
|
+
}>) {
|
|
1083
|
+
const listClassName = marketingColumnClassName("hraness-marketing-stats__list", undefined, columns);
|
|
1084
|
+
if (stats.length === 0) return null;
|
|
1085
|
+
return (
|
|
1086
|
+
<section
|
|
1087
|
+
aria-label={ariaLabel}
|
|
1088
|
+
className={classNames("hraness-marketing-stats", className)}
|
|
1089
|
+
data-hraness-marketing="stats"
|
|
1090
|
+
>
|
|
1091
|
+
<dl
|
|
1092
|
+
className={listClassName}
|
|
1093
|
+
style={columns === undefined ? { "--hraness-marketing-fact-columns": String(stats.length) } as Record<string, string> : undefined}
|
|
1094
|
+
>
|
|
1095
|
+
{stats.map((stat, index) => (
|
|
1096
|
+
<div className={classNames("hraness-marketing-facts__item", undefined, marketingFactCellVariant(index))} key={`${stat.label}-${stat.value}`}>
|
|
1097
|
+
<dt className={classNames("hraness-marketing-facts__label")}>{stat.label}</dt>
|
|
1098
|
+
<dd className={classNames("hraness-marketing-facts__body")}>
|
|
1099
|
+
<strong className={classNames("hraness-marketing-stats__value")}>{stat.value}</strong>
|
|
1100
|
+
{stat.detail === undefined ? null : <span className={classNames("hraness-marketing-facts__detail")}>{stat.detail}</span>}
|
|
1101
|
+
</dd>
|
|
1102
|
+
</div>
|
|
1103
|
+
))}
|
|
1104
|
+
</dl>
|
|
1105
|
+
{source === undefined ? null : <p className={classNames("hraness-marketing-stats__source")}>{source}</p>}
|
|
1106
|
+
</section>
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
export interface MarketingInterface {
|
|
1111
|
+
readonly example?: ReactNode;
|
|
1112
|
+
readonly label: string;
|
|
1113
|
+
readonly summary: string;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function MarketingInterfaceGrid({
|
|
1117
|
+
className,
|
|
1118
|
+
heading,
|
|
1119
|
+
headingId,
|
|
1120
|
+
headingLevel = 2,
|
|
1121
|
+
id,
|
|
1122
|
+
interfaces,
|
|
1123
|
+
label,
|
|
1124
|
+
summary,
|
|
1125
|
+
}: Readonly<{
|
|
1126
|
+
className?: string;
|
|
1127
|
+
heading: string;
|
|
1128
|
+
headingId: string;
|
|
1129
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1130
|
+
id?: string;
|
|
1131
|
+
interfaces: readonly MarketingInterface[];
|
|
1132
|
+
label?: string;
|
|
1133
|
+
summary?: string;
|
|
1134
|
+
}>) {
|
|
1135
|
+
return (
|
|
1136
|
+
<section
|
|
1137
|
+
aria-labelledby={headingId}
|
|
1138
|
+
className={classNames("hraness-marketing-interfaces", className)}
|
|
1139
|
+
data-hraness-marketing="interfaces"
|
|
1140
|
+
id={id}
|
|
1141
|
+
>
|
|
1142
|
+
<MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="interfaces" />
|
|
1143
|
+
<div className={classNames("hraness-marketing-interface-grid")}>
|
|
1144
|
+
{interfaces.map((entry) => (
|
|
1145
|
+
<article className={classNames("hraness-marketing-interface")} key={entry.label}>
|
|
1146
|
+
<Heading
|
|
1147
|
+
className={classNames("hraness-marketing-interface__heading")}
|
|
1148
|
+
level={childHeadingLevel(headingLevel)}
|
|
1149
|
+
>
|
|
1150
|
+
{entry.label}
|
|
1151
|
+
</Heading>
|
|
1152
|
+
<p className={classNames("hraness-marketing-interface__summary")}>{entry.summary}</p>
|
|
1153
|
+
{entry.example}
|
|
1154
|
+
</article>
|
|
1155
|
+
))}
|
|
1156
|
+
</div>
|
|
1157
|
+
</section>
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export interface MarketingTrustItem {
|
|
1162
|
+
readonly detail: string;
|
|
1163
|
+
readonly label: string;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
export function MarketingTrustBoundary({
|
|
1167
|
+
className,
|
|
1168
|
+
heading,
|
|
1169
|
+
headingId,
|
|
1170
|
+
headingLevel = 2,
|
|
1171
|
+
id,
|
|
1172
|
+
items,
|
|
1173
|
+
label,
|
|
1174
|
+
summary,
|
|
1175
|
+
}: Readonly<{
|
|
1176
|
+
className?: string;
|
|
1177
|
+
heading: string;
|
|
1178
|
+
headingId: string;
|
|
1179
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1180
|
+
id?: string;
|
|
1181
|
+
items: readonly MarketingTrustItem[];
|
|
1182
|
+
label?: string;
|
|
1183
|
+
summary?: string;
|
|
1184
|
+
}>) {
|
|
1185
|
+
return (
|
|
1186
|
+
<section
|
|
1187
|
+
aria-labelledby={headingId}
|
|
1188
|
+
className={classNames("hraness-marketing-trust", className)}
|
|
1189
|
+
data-hraness-marketing="trust"
|
|
1190
|
+
id={id}
|
|
1191
|
+
>
|
|
1192
|
+
<MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="trust" />
|
|
1193
|
+
<dl className={classNames("hraness-marketing-trust-grid")}>
|
|
1194
|
+
{items.map((item) => (
|
|
1195
|
+
<div className={classNames("hraness-marketing-trust-item")} key={item.label}>
|
|
1196
|
+
<dt className={classNames("hraness-marketing-trust-item__label")}>{item.label}</dt>
|
|
1197
|
+
<dd className={classNames("hraness-marketing-trust-item__detail")}>{item.detail}</dd>
|
|
1198
|
+
</div>
|
|
1199
|
+
))}
|
|
1200
|
+
</dl>
|
|
1201
|
+
</section>
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export interface MarketingQuote {
|
|
1206
|
+
/** Optional profile link for the attribution. */
|
|
1207
|
+
readonly href?: string;
|
|
1208
|
+
readonly name: string;
|
|
1209
|
+
readonly quote: string;
|
|
1210
|
+
/** Handle, title, or affiliation shown after the name. */
|
|
1211
|
+
readonly role?: string;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Attributed quotes. Only real, permissioned quotes belong here; render
|
|
1216
|
+
* nothing rather than fill the grid with placeholders.
|
|
1217
|
+
*/
|
|
1218
|
+
export function MarketingQuoteGrid({
|
|
1219
|
+
className,
|
|
1220
|
+
heading,
|
|
1221
|
+
headingId,
|
|
1222
|
+
headingLevel = 2,
|
|
1223
|
+
id,
|
|
1224
|
+
label,
|
|
1225
|
+
quotes,
|
|
1226
|
+
summary,
|
|
1227
|
+
}: Readonly<{
|
|
1228
|
+
className?: string;
|
|
1229
|
+
heading: string;
|
|
1230
|
+
headingId: string;
|
|
1231
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1232
|
+
id?: string;
|
|
1233
|
+
label?: string;
|
|
1234
|
+
quotes: readonly MarketingQuote[];
|
|
1235
|
+
summary?: string;
|
|
1236
|
+
}>) {
|
|
1237
|
+
if (quotes.length === 0) return null;
|
|
1238
|
+
return (
|
|
1239
|
+
<section
|
|
1240
|
+
aria-labelledby={headingId}
|
|
1241
|
+
className={classNames("hraness-marketing-quotes", className)}
|
|
1242
|
+
data-hraness-marketing="quotes"
|
|
1243
|
+
id={id}
|
|
1244
|
+
>
|
|
1245
|
+
<MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="quotes" />
|
|
1246
|
+
<ul className={classNames("hraness-marketing-quote-grid")}>
|
|
1247
|
+
{quotes.map((entry) => (
|
|
1248
|
+
<li key={`${entry.name}-${entry.quote.slice(0, 24)}`}>
|
|
1249
|
+
<figure className={classNames("hraness-marketing-quote")}>
|
|
1250
|
+
<blockquote className={classNames("hraness-marketing-quote__body")}>
|
|
1251
|
+
<p className={classNames("hraness-marketing-quote__text")}>{entry.quote}</p>
|
|
1252
|
+
</blockquote>
|
|
1253
|
+
<figcaption className={classNames("hraness-marketing-quote__attribution")}>
|
|
1254
|
+
<strong className={classNames("hraness-marketing-quote__name")}>{entry.name}</strong>
|
|
1255
|
+
{entry.role === undefined
|
|
1256
|
+
? null
|
|
1257
|
+
: entry.href === undefined
|
|
1258
|
+
? <span>{entry.role}</span>
|
|
1259
|
+
: <a className={classNames("hraness-marketing-quote__link")} href={entry.href}>{entry.role}</a>}
|
|
1260
|
+
</figcaption>
|
|
1261
|
+
</figure>
|
|
1262
|
+
</li>
|
|
1263
|
+
))}
|
|
1264
|
+
</ul>
|
|
1265
|
+
</section>
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export interface MarketingPlan {
|
|
1270
|
+
readonly action?: MarketingAction;
|
|
1271
|
+
readonly emphasis?: "primary" | "secondary";
|
|
1272
|
+
readonly features: readonly string[];
|
|
1273
|
+
readonly name: string;
|
|
1274
|
+
readonly note?: string;
|
|
1275
|
+
/** Billing period or qualifier shown after the price, such as "per year". */
|
|
1276
|
+
readonly period?: string;
|
|
1277
|
+
readonly price: string;
|
|
1278
|
+
readonly summary?: string;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
export function MarketingPricing({
|
|
1282
|
+
className,
|
|
1283
|
+
heading,
|
|
1284
|
+
headingId,
|
|
1285
|
+
headingLevel = 2,
|
|
1286
|
+
id,
|
|
1287
|
+
label,
|
|
1288
|
+
plans,
|
|
1289
|
+
summary,
|
|
1290
|
+
}: Readonly<{
|
|
1291
|
+
className?: string;
|
|
1292
|
+
heading: string;
|
|
1293
|
+
headingId: string;
|
|
1294
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1295
|
+
id?: string;
|
|
1296
|
+
label?: string;
|
|
1297
|
+
plans: readonly MarketingPlan[];
|
|
1298
|
+
summary?: string;
|
|
1299
|
+
}>) {
|
|
1300
|
+
return (
|
|
1301
|
+
<section
|
|
1302
|
+
aria-labelledby={headingId}
|
|
1303
|
+
className={classNames("hraness-marketing-pricing", className)}
|
|
1304
|
+
data-hraness-marketing="pricing"
|
|
1305
|
+
id={id}
|
|
1306
|
+
>
|
|
1307
|
+
<MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="pricing" />
|
|
1308
|
+
<ul className={classNames("hraness-marketing-plan-grid")}>
|
|
1309
|
+
{plans.map((plan) => (
|
|
1310
|
+
<li className={classNames("hraness-marketing-plan", undefined, plan.emphasis === "primary" ? "primary" : "default")} data-emphasis={plan.emphasis ?? "secondary"} key={plan.name}>
|
|
1311
|
+
<Heading className={classNames("hraness-marketing-plan__name")} level={childHeadingLevel(headingLevel)}>
|
|
1312
|
+
{plan.name}
|
|
1313
|
+
</Heading>
|
|
1314
|
+
<p className={classNames("hraness-marketing-plan__price")}>
|
|
1315
|
+
<strong className={classNames("hraness-marketing-plan__value")}>{plan.price}</strong>
|
|
1316
|
+
{plan.period === undefined ? null : <span className={classNames("hraness-marketing-plan__period")}>{plan.period}</span>}
|
|
1317
|
+
</p>
|
|
1318
|
+
{plan.summary === undefined ? null : <p className={classNames("hraness-marketing-plan__summary")}>{plan.summary}</p>}
|
|
1319
|
+
{plan.features.length === 0
|
|
1320
|
+
? null
|
|
1321
|
+
: (
|
|
1322
|
+
<ul className={classNames("hraness-marketing-plan__features")}>
|
|
1323
|
+
{plan.features.map((feature) => <li className={classNames("hraness-marketing-plan__feature")} key={feature}>{feature}</li>)}
|
|
1324
|
+
</ul>
|
|
1325
|
+
)}
|
|
1326
|
+
{plan.action === undefined
|
|
1327
|
+
? null
|
|
1328
|
+
: (
|
|
1329
|
+
<a
|
|
1330
|
+
className={classNames("hraness-marketing-action", undefined, `plan-${plan.action.emphasis ?? plan.emphasis ?? "secondary"}`)}
|
|
1331
|
+
data-emphasis={plan.action.emphasis ?? plan.emphasis ?? "secondary"}
|
|
1332
|
+
data-foil={(plan.action.emphasis ?? plan.emphasis ?? "secondary") === "primary" ? "" : undefined}
|
|
1333
|
+
href={plan.action.href}
|
|
1334
|
+
>
|
|
1335
|
+
{plan.action.label}
|
|
1336
|
+
</a>
|
|
1337
|
+
)}
|
|
1338
|
+
{plan.note === undefined ? null : <p className={classNames("hraness-marketing-plan__note")}>{plan.note}</p>}
|
|
1339
|
+
</li>
|
|
1340
|
+
))}
|
|
1341
|
+
</ul>
|
|
1342
|
+
</section>
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
export interface MarketingQuestion {
|
|
1347
|
+
readonly answer: ReactNode;
|
|
1348
|
+
readonly question: string;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
export function MarketingQuestionList({
|
|
1352
|
+
className,
|
|
1353
|
+
heading,
|
|
1354
|
+
headingId,
|
|
1355
|
+
headingLevel = 2,
|
|
1356
|
+
id,
|
|
1357
|
+
label,
|
|
1358
|
+
questions,
|
|
1359
|
+
summary,
|
|
1360
|
+
}: Readonly<{
|
|
1361
|
+
className?: string;
|
|
1362
|
+
heading: string;
|
|
1363
|
+
headingId: string;
|
|
1364
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1365
|
+
id?: string;
|
|
1366
|
+
label?: string;
|
|
1367
|
+
questions: readonly MarketingQuestion[];
|
|
1368
|
+
summary?: string;
|
|
1369
|
+
}>) {
|
|
1370
|
+
return (
|
|
1371
|
+
<section
|
|
1372
|
+
aria-labelledby={headingId}
|
|
1373
|
+
className={classNames("hraness-marketing-questions", className)}
|
|
1374
|
+
data-hraness-marketing="questions"
|
|
1375
|
+
id={id}
|
|
1376
|
+
>
|
|
1377
|
+
<MarketingCollectionHeader {...{ heading, headingId, headingLevel, label, summary }} prefix="questions" />
|
|
1378
|
+
<div className={classNames("hraness-marketing-question-list")}>
|
|
1379
|
+
{questions.map((question, index) => (
|
|
1380
|
+
<details className={classNames("hraness-marketing-question", undefined, index === questions.length - 1 ? "last" : "default")} key={question.question}>
|
|
1381
|
+
<summary className={classNames("hraness-marketing-question__summary")}>{question.question}</summary>
|
|
1382
|
+
<div className={classNames("hraness-marketing-question__answer")}>{question.answer}</div>
|
|
1383
|
+
</details>
|
|
1384
|
+
))}
|
|
1385
|
+
</div>
|
|
1386
|
+
</section>
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* The person behind the product, in plain words. `children` is the bio as one
|
|
1392
|
+
* or more paragraphs.
|
|
1393
|
+
*/
|
|
1394
|
+
export function MarketingMaker({
|
|
1395
|
+
children,
|
|
1396
|
+
className,
|
|
1397
|
+
heading,
|
|
1398
|
+
headingId,
|
|
1399
|
+
headingLevel = 2,
|
|
1400
|
+
id,
|
|
1401
|
+
label,
|
|
1402
|
+
linkClassName,
|
|
1403
|
+
links = [],
|
|
1404
|
+
portrait,
|
|
1405
|
+
}: Readonly<{
|
|
1406
|
+
children: ReactNode;
|
|
1407
|
+
className?: string;
|
|
1408
|
+
heading: string;
|
|
1409
|
+
headingId: string;
|
|
1410
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1411
|
+
id?: string;
|
|
1412
|
+
label?: string;
|
|
1413
|
+
/** Added only to listed links, never to caller-owned biography content. */
|
|
1414
|
+
linkClassName?: string;
|
|
1415
|
+
links?: readonly MarketingLink[];
|
|
1416
|
+
portrait?: ReactNode;
|
|
1417
|
+
}>) {
|
|
1418
|
+
return (
|
|
1419
|
+
<section
|
|
1420
|
+
aria-labelledby={headingId}
|
|
1421
|
+
className={classNames("hraness-marketing-maker", className)}
|
|
1422
|
+
data-hraness-marketing="maker"
|
|
1423
|
+
id={id}
|
|
1424
|
+
>
|
|
1425
|
+
<header className={classNames("hraness-marketing-maker__header")}>
|
|
1426
|
+
{portrait === undefined ? null : <div className={classNames("hraness-marketing-maker__portrait")}>{portrait}</div>}
|
|
1427
|
+
{label === undefined || label === "" ? null : <p className={classNames("hraness-marketing-maker__label")}>{label}</p>}
|
|
1428
|
+
<Heading className={classNames("hraness-marketing-maker__heading")} id={headingId} level={headingLevel}>
|
|
1429
|
+
{heading}
|
|
1430
|
+
</Heading>
|
|
1431
|
+
</header>
|
|
1432
|
+
<div className={classNames("hraness-marketing-maker__body")}>
|
|
1433
|
+
{children}
|
|
1434
|
+
{links.length === 0
|
|
1435
|
+
? null
|
|
1436
|
+
: (
|
|
1437
|
+
<ul className={classNames("hraness-marketing-maker__links")}>
|
|
1438
|
+
{links.map((link) => (
|
|
1439
|
+
<li key={`${link.href}-${link.label}`}>
|
|
1440
|
+
<a className={linkClassName} href={link.href}>{link.label}</a>
|
|
1441
|
+
</li>
|
|
1442
|
+
))}
|
|
1443
|
+
</ul>
|
|
1444
|
+
)}
|
|
1445
|
+
</div>
|
|
1446
|
+
</section>
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
export interface MarketingRelatedProduct {
|
|
1451
|
+
/** Custom artwork in place of `mark`, drawn in the same 44px slot. */
|
|
1452
|
+
readonly art?: ReactNode;
|
|
1453
|
+
readonly href: string;
|
|
1454
|
+
/** Transparent product mark, such as a portfolio item's `mark`; drawn as a foil mark before the name. */
|
|
1455
|
+
readonly mark?: string;
|
|
1456
|
+
readonly name: string;
|
|
1457
|
+
/**
|
|
1458
|
+
* @deprecated Cards show `role` only. Kept so existing item lists still
|
|
1459
|
+
* type-check; the relation sentence is not rendered.
|
|
1460
|
+
*/
|
|
1461
|
+
readonly relationship?: ReactNode;
|
|
1462
|
+
/** The product's one-line description, shown under its name. */
|
|
1463
|
+
readonly role: string;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export interface MarketingRelatedGroup {
|
|
1467
|
+
/** Heading naming this tier of siblings, rendered one level below the section heading. */
|
|
1468
|
+
readonly heading: string;
|
|
1469
|
+
readonly headingId: string;
|
|
1470
|
+
readonly items: readonly MarketingRelatedProduct[];
|
|
1471
|
+
readonly summary?: string;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function MarketingRelatedCards({
|
|
1475
|
+
items,
|
|
1476
|
+
level,
|
|
1477
|
+
}: Readonly<{
|
|
1478
|
+
items: readonly MarketingRelatedProduct[];
|
|
1479
|
+
level: MarketingHeadingLevel;
|
|
1480
|
+
}>) {
|
|
1481
|
+
return (
|
|
1482
|
+
<>
|
|
1483
|
+
{items.map((item) => (
|
|
1484
|
+
<a className={classNames("hraness-marketing-related__card")} data-foil="" data-hraness-marketing="card" href={item.href} key={item.name}>
|
|
1485
|
+
{isPresentNode(item.art) || (item.mark !== undefined && item.mark !== "")
|
|
1486
|
+
? (
|
|
1487
|
+
<span aria-hidden="true" className={classNames("hraness-marketing-related__card-mark")}>
|
|
1488
|
+
{isPresentNode(item.art) ? item.art : <FoilMark size={44} src={item.mark ?? ""} />}
|
|
1489
|
+
</span>
|
|
1490
|
+
)
|
|
1491
|
+
: null}
|
|
1492
|
+
<span className={classNames("hraness-marketing-related__card-text")}>
|
|
1493
|
+
<Heading className={classNames("hraness-marketing-related__card-name")} level={level}>{item.name}</Heading>
|
|
1494
|
+
<span className={classNames("hraness-marketing-related__card-role")}>{item.role}</span>
|
|
1495
|
+
</span>
|
|
1496
|
+
</a>
|
|
1497
|
+
))}
|
|
1498
|
+
</>
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
type MarketingRelatedBody =
|
|
1503
|
+
| Readonly<{ groups: readonly MarketingRelatedGroup[]; items?: undefined }>
|
|
1504
|
+
| Readonly<{ groups?: undefined; items: readonly MarketingRelatedProduct[] }>;
|
|
1505
|
+
|
|
1506
|
+
/** Linked cards for sibling products: each shows the product's mark, name, and one-line description. */
|
|
1507
|
+
export function MarketingRelated({
|
|
1508
|
+
className,
|
|
1509
|
+
heading,
|
|
1510
|
+
headingId,
|
|
1511
|
+
headingLevel = 2,
|
|
1512
|
+
id,
|
|
1513
|
+
items,
|
|
1514
|
+
groups,
|
|
1515
|
+
label,
|
|
1516
|
+
summary,
|
|
1517
|
+
}: Readonly<{
|
|
1518
|
+
className?: string;
|
|
1519
|
+
heading: string;
|
|
1520
|
+
headingId: string;
|
|
1521
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1522
|
+
id?: string;
|
|
1523
|
+
label?: string;
|
|
1524
|
+
summary?: string;
|
|
1525
|
+
}> &
|
|
1526
|
+
MarketingRelatedBody) {
|
|
1527
|
+
return (
|
|
1528
|
+
<section
|
|
1529
|
+
aria-labelledby={headingId}
|
|
1530
|
+
className={classNames("hraness-marketing-related", className)}
|
|
1531
|
+
data-hraness-marketing="related"
|
|
1532
|
+
id={id}
|
|
1533
|
+
>
|
|
1534
|
+
<MarketingCollectionHeader
|
|
1535
|
+
heading={heading}
|
|
1536
|
+
headingId={headingId}
|
|
1537
|
+
headingLevel={headingLevel}
|
|
1538
|
+
label={label}
|
|
1539
|
+
prefix="related"
|
|
1540
|
+
summary={summary}
|
|
1541
|
+
/>
|
|
1542
|
+
{groups === undefined
|
|
1543
|
+
? (
|
|
1544
|
+
<MarketingCardRow>
|
|
1545
|
+
<MarketingRelatedCards items={items} level={childHeadingLevel(headingLevel)} />
|
|
1546
|
+
</MarketingCardRow>
|
|
1547
|
+
)
|
|
1548
|
+
: groups.map((group) => (
|
|
1549
|
+
<div className={classNames("hraness-marketing-related__group")} key={group.headingId}>
|
|
1550
|
+
<div className={classNames("hraness-marketing-related__group-header")}>
|
|
1551
|
+
<Heading
|
|
1552
|
+
className={classNames("hraness-marketing-related__group-heading")}
|
|
1553
|
+
id={group.headingId}
|
|
1554
|
+
level={childHeadingLevel(headingLevel)}
|
|
1555
|
+
>
|
|
1556
|
+
{group.heading}
|
|
1557
|
+
</Heading>
|
|
1558
|
+
{group.summary === undefined
|
|
1559
|
+
? null
|
|
1560
|
+
: <p className={classNames("hraness-marketing-related__group-summary")}>{group.summary}</p>}
|
|
1561
|
+
</div>
|
|
1562
|
+
<MarketingCardRow ariaLabel={group.heading}>
|
|
1563
|
+
<MarketingRelatedCards items={group.items} level={childHeadingLevel(childHeadingLevel(headingLevel))} />
|
|
1564
|
+
</MarketingCardRow>
|
|
1565
|
+
</div>
|
|
1566
|
+
))}
|
|
1567
|
+
</section>
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
export function MarketingCallToAction({
|
|
1572
|
+
actions,
|
|
1573
|
+
className,
|
|
1574
|
+
eyebrow,
|
|
1575
|
+
footnote,
|
|
1576
|
+
heading,
|
|
1577
|
+
headingId,
|
|
1578
|
+
headingLevel = 2,
|
|
1579
|
+
id,
|
|
1580
|
+
summary,
|
|
1581
|
+
tone = "paper",
|
|
1582
|
+
}: Readonly<{
|
|
1583
|
+
actions: readonly MarketingAction[];
|
|
1584
|
+
className?: string;
|
|
1585
|
+
eyebrow?: string;
|
|
1586
|
+
footnote?: string;
|
|
1587
|
+
heading: string;
|
|
1588
|
+
headingId: string;
|
|
1589
|
+
headingLevel?: MarketingHeadingLevel;
|
|
1590
|
+
id?: string;
|
|
1591
|
+
summary?: string;
|
|
1592
|
+
tone?: MarketingTone;
|
|
1593
|
+
}>) {
|
|
1594
|
+
return (
|
|
1595
|
+
<section
|
|
1596
|
+
aria-labelledby={headingId}
|
|
1597
|
+
className={classNames("hraness-marketing-cta", className, tone === "accent" ? "accent" : "default")}
|
|
1598
|
+
data-hraness-marketing="cta"
|
|
1599
|
+
data-tone={tone}
|
|
1600
|
+
id={id}
|
|
1601
|
+
>
|
|
1602
|
+
{eyebrow === undefined || eyebrow === "" ? null : <p className={classNames("hraness-marketing-cta__eyebrow")}>{eyebrow}</p>}
|
|
1603
|
+
<Heading className={classNames("hraness-marketing-cta__heading")} id={headingId} level={headingLevel}>
|
|
1604
|
+
{heading}
|
|
1605
|
+
</Heading>
|
|
1606
|
+
{summary === undefined ? null : <p className={classNames("hraness-marketing-cta__summary")}>{summary}</p>}
|
|
1607
|
+
<MarketingActions actions={actions} className={classNames("hraness-marketing-cta__actions")} context="cta" tone={tone} />
|
|
1608
|
+
{footnote === undefined ? null : <p className={classNames("hraness-marketing-cta__footnote")}>{footnote}</p>}
|
|
1609
|
+
</section>
|
|
1610
|
+
);
|
|
1611
|
+
}
|