@rizom/ui 0.2.0-alpha.36
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/README.md +26 -0
- package/package.json +50 -0
- package/src/Badge.tsx +14 -0
- package/src/Button.tsx +56 -0
- package/src/Divider.tsx +12 -0
- package/src/Footer.tsx +70 -0
- package/src/Header.tsx +38 -0
- package/src/ProductCard.tsx +170 -0
- package/src/ProductIllustration.tsx +689 -0
- package/src/Section.tsx +22 -0
- package/src/SideNav.tsx +22 -0
- package/src/cn.ts +35 -0
- package/src/frame.tsx +26 -0
- package/src/highlighted-text.tsx +45 -0
- package/src/index.ts +33 -0
- package/src/types.ts +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @rizom/ui
|
|
2
|
+
|
|
3
|
+
Shared Rizom UI primitives for app-owned Rizom site variants.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package holds the app-facing Rizom UI layer used by extracted or app-local Rizom sites.
|
|
8
|
+
It is intentionally narrower than `@brains/site-rizom` and excludes site/runtime composition concerns.
|
|
9
|
+
|
|
10
|
+
## Includes
|
|
11
|
+
|
|
12
|
+
- layout primitives such as `RizomFrame`, `Section`, `Header`, `Footer`, and `SideNav`
|
|
13
|
+
- content UI such as `Badge`, `Button`, `Divider`, and `ProductCard`
|
|
14
|
+
- shared text rendering helper `renderHighlightedText`
|
|
15
|
+
- lightweight shared presentational types
|
|
16
|
+
|
|
17
|
+
## Does not include
|
|
18
|
+
|
|
19
|
+
- Rizom site/runtime composition
|
|
20
|
+
- site-builder or `SiteInfo` contracts
|
|
21
|
+
- app-specific layout helpers
|
|
22
|
+
- `createRizomSite(...)`
|
|
23
|
+
|
|
24
|
+
## Consumer contract
|
|
25
|
+
|
|
26
|
+
Consumers should install `preact` alongside this package.
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rizom/ui",
|
|
3
|
+
"version": "0.2.0-alpha.36",
|
|
4
|
+
"description": "Shared Rizom UI primitives for app-owned Rizom site variants.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
18
|
+
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"clsx": "^2.1.0",
|
|
22
|
+
"tailwind-merge": "^2.2.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"preact": "^10.27.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@brains/eslint-config": "workspace:*",
|
|
29
|
+
"@brains/typescript-config": "workspace:*",
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"eslint": "^8.56.0",
|
|
32
|
+
"preact": "^10.27.2",
|
|
33
|
+
"typescript": "^5.3.3"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/rizom-ai/brains.git",
|
|
41
|
+
"directory": "shared/rizom-ui"
|
|
42
|
+
},
|
|
43
|
+
"license": "Apache-2.0",
|
|
44
|
+
"author": "Yeehaa <yeehaa@rizom.ai> (https://rizom.ai)",
|
|
45
|
+
"homepage": "https://github.com/rizom-ai/brains/tree/main/shared/rizom-ui#readme",
|
|
46
|
+
"bugs": "https://github.com/rizom-ai/brains/issues",
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.3.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/Badge.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JSX, ComponentChildren } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export interface BadgeProps {
|
|
5
|
+
children?: ComponentChildren;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BASE =
|
|
10
|
+
"inline-flex items-center px-5 py-2 border border-accent text-accent rounded-[20px] font-label text-label-md font-semibold tracking-[0.09375em] uppercase";
|
|
11
|
+
|
|
12
|
+
export const Badge = ({ children, className }: BadgeProps): JSX.Element => (
|
|
13
|
+
<span className={cn(BASE, className)}>{children}</span>
|
|
14
|
+
);
|
package/src/Button.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { JSX, ComponentChildren } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export type ButtonVariant = "primary" | "primary-strong" | "secondary";
|
|
5
|
+
export type ButtonSize = "md" | "lg";
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps {
|
|
8
|
+
href: string;
|
|
9
|
+
variant?: ButtonVariant;
|
|
10
|
+
size?: ButtonSize;
|
|
11
|
+
block?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
children?: ComponentChildren;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BASE =
|
|
17
|
+
"inline-flex items-center justify-center cursor-pointer border border-solid transition-all [gap:var(--rizom-btn-gap)] [border-radius:var(--rizom-btn-radius)] [font-family:var(--rizom-btn-font-family)] [font-style:var(--rizom-btn-font-style)] [letter-spacing:var(--rizom-btn-letter-spacing)] [text-transform:var(--rizom-btn-text-transform)]";
|
|
18
|
+
const VARIANT: Record<ButtonVariant, string> = {
|
|
19
|
+
primary:
|
|
20
|
+
"[font-weight:var(--rizom-btn-primary-font-weight)] [color:var(--rizom-btn-primary-color)] [background:var(--rizom-btn-primary-bg)] [border-color:var(--rizom-btn-primary-border-color)] [border-width:var(--rizom-btn-primary-border-width)] [box-shadow:var(--rizom-btn-primary-shadow)] hover:[color:var(--rizom-btn-primary-hover-color)] hover:[background:var(--rizom-btn-primary-hover-bg)] hover:[border-color:var(--rizom-btn-primary-hover-border-color)] hover:[border-width:var(--rizom-btn-primary-hover-border-width)] hover:[box-shadow:var(--rizom-btn-primary-hover-shadow)] hover:[transform:var(--rizom-btn-primary-hover-transform)]",
|
|
21
|
+
"primary-strong":
|
|
22
|
+
"duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)] [font-weight:var(--rizom-btn-primary-strong-font-weight)] [color:var(--rizom-btn-primary-strong-color)] [background:var(--rizom-btn-primary-strong-bg)] [border-color:var(--rizom-btn-primary-strong-border-color)] [border-width:var(--rizom-btn-primary-strong-border-width)] [box-shadow:var(--rizom-btn-primary-strong-shadow)] hover:[color:var(--rizom-btn-primary-strong-hover-color)] hover:[background:var(--rizom-btn-primary-strong-hover-bg)] hover:[border-color:var(--rizom-btn-primary-strong-hover-border-color)] hover:[border-width:var(--rizom-btn-primary-strong-hover-border-width)] hover:[box-shadow:var(--rizom-btn-primary-strong-hover-shadow)] hover:[transform:var(--rizom-btn-primary-strong-hover-transform)]",
|
|
23
|
+
secondary:
|
|
24
|
+
"[font-weight:var(--rizom-btn-secondary-font-weight)] [color:var(--rizom-btn-secondary-color)] [background:var(--rizom-btn-secondary-bg)] [border-color:var(--rizom-btn-secondary-border-color)] [border-width:var(--rizom-btn-secondary-border-width)] [box-shadow:var(--rizom-btn-secondary-shadow)] hover:[color:var(--rizom-btn-secondary-hover-color)] hover:[background:var(--rizom-btn-secondary-hover-bg)] hover:[border-color:var(--rizom-btn-secondary-hover-border-color)] hover:[border-width:var(--rizom-btn-secondary-hover-border-width)] hover:[box-shadow:var(--rizom-btn-secondary-hover-shadow)] hover:[transform:var(--rizom-btn-secondary-hover-transform)]",
|
|
25
|
+
};
|
|
26
|
+
const SIZE: Record<ButtonSize, string> = {
|
|
27
|
+
md: "text-base [padding:var(--rizom-btn-md-padding)]",
|
|
28
|
+
lg: "text-body-md md:text-body-lg [padding:var(--rizom-btn-lg-padding-mobile)] md:[padding:var(--rizom-btn-lg-padding-desktop)]",
|
|
29
|
+
};
|
|
30
|
+
const BLOCK = "w-full md:w-auto";
|
|
31
|
+
|
|
32
|
+
export const Button = ({
|
|
33
|
+
href,
|
|
34
|
+
variant = "primary",
|
|
35
|
+
size = "md",
|
|
36
|
+
block = false,
|
|
37
|
+
className,
|
|
38
|
+
children,
|
|
39
|
+
}: ButtonProps): JSX.Element => (
|
|
40
|
+
<a
|
|
41
|
+
href={href}
|
|
42
|
+
className={cn(
|
|
43
|
+
"rizom-btn",
|
|
44
|
+
`rizom-btn-${variant}`,
|
|
45
|
+
`rizom-btn-${size}`,
|
|
46
|
+
block && "rizom-btn-block",
|
|
47
|
+
BASE,
|
|
48
|
+
VARIANT[variant],
|
|
49
|
+
SIZE[size],
|
|
50
|
+
block && BLOCK,
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</a>
|
|
56
|
+
);
|
package/src/Divider.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { JSX } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
export interface DividerProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const BASE = "w-[60px] h-px bg-[var(--color-divider)] mx-auto";
|
|
9
|
+
|
|
10
|
+
export const Divider = ({ className }: DividerProps): JSX.Element => (
|
|
11
|
+
<div className={cn(BASE, className)} />
|
|
12
|
+
);
|
package/src/Footer.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { JSX } from "preact";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
import type { RizomBrandSuffix, RizomFooterTagline, RizomLink } from "./types";
|
|
4
|
+
import { GUTTER } from "./Section";
|
|
5
|
+
|
|
6
|
+
const LINK_CLS =
|
|
7
|
+
"text-label-md text-theme-light hover:text-theme transition-colors";
|
|
8
|
+
|
|
9
|
+
const TOGGLE_CLS =
|
|
10
|
+
"bg-transparent border border-theme-light rounded-md px-2.5 py-1.5 cursor-pointer text-theme-light text-label-md font-body transition-colors hover:text-theme hover:border-theme";
|
|
11
|
+
|
|
12
|
+
interface FooterProps {
|
|
13
|
+
brandSuffix: RizomBrandSuffix;
|
|
14
|
+
metaLabel: string;
|
|
15
|
+
tagline?: RizomFooterTagline;
|
|
16
|
+
links: RizomLink[];
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Footer = ({
|
|
21
|
+
brandSuffix,
|
|
22
|
+
metaLabel,
|
|
23
|
+
tagline,
|
|
24
|
+
links,
|
|
25
|
+
className,
|
|
26
|
+
}: FooterProps): JSX.Element => (
|
|
27
|
+
<footer
|
|
28
|
+
className={cn(
|
|
29
|
+
`flex flex-col gap-4 ${GUTTER} py-8 md:flex-row md:items-center md:justify-between md:gap-6 md:py-6 border-t border-theme-light text-center md:text-left`,
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<div className="flex flex-col items-center gap-1.5 md:items-start max-w-[560px]">
|
|
34
|
+
<div className="flex flex-col items-center gap-1.5 md:flex-row md:items-center md:gap-3">
|
|
35
|
+
<span className="font-nav text-[15px]">
|
|
36
|
+
<span className="font-bold">rizom</span>
|
|
37
|
+
<span className="font-bold text-accent">.</span>
|
|
38
|
+
<span className="text-theme-muted">{brandSuffix}</span>
|
|
39
|
+
</span>
|
|
40
|
+
<span className="text-label-md text-theme-light">{metaLabel}</span>
|
|
41
|
+
</div>
|
|
42
|
+
{tagline ? (
|
|
43
|
+
<p className="text-label-md leading-[1.6] text-theme-light">
|
|
44
|
+
{tagline.prefix ?? ""}
|
|
45
|
+
<a
|
|
46
|
+
href={tagline.link.href}
|
|
47
|
+
className="text-accent hover:opacity-75 transition-opacity"
|
|
48
|
+
>
|
|
49
|
+
{tagline.link.label}
|
|
50
|
+
</a>
|
|
51
|
+
{tagline.suffix ?? ""}
|
|
52
|
+
</p>
|
|
53
|
+
) : null}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex flex-wrap items-center justify-center gap-4 md:justify-end md:gap-6">
|
|
56
|
+
{links.map((link) => (
|
|
57
|
+
<a key={link.href + link.label} href={link.href} className={LINK_CLS}>
|
|
58
|
+
{link.label}
|
|
59
|
+
</a>
|
|
60
|
+
))}
|
|
61
|
+
<button
|
|
62
|
+
id="themeToggle"
|
|
63
|
+
aria-label="Toggle light mode"
|
|
64
|
+
className={TOGGLE_CLS}
|
|
65
|
+
>
|
|
66
|
+
☀ Light
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</footer>
|
|
70
|
+
);
|
package/src/Header.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { JSX } from "preact";
|
|
2
|
+
import type { RizomBrandSuffix, RizomLink } from "./types";
|
|
3
|
+
|
|
4
|
+
const LINK_CLS =
|
|
5
|
+
"hidden md:inline-block font-body text-[15px] text-theme-muted hover:text-theme transition-colors relative py-1 after:content-[''] after:absolute after:left-0 after:bottom-0 after:h-px after:w-0 after:bg-accent after:transition-all after:duration-300 hover:after:w-full";
|
|
6
|
+
|
|
7
|
+
interface HeaderProps {
|
|
8
|
+
brandSuffix: RizomBrandSuffix;
|
|
9
|
+
navLinks: RizomLink[];
|
|
10
|
+
primaryCta: RizomLink;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Header = ({
|
|
14
|
+
brandSuffix,
|
|
15
|
+
navLinks,
|
|
16
|
+
primaryCta,
|
|
17
|
+
}: HeaderProps): JSX.Element => (
|
|
18
|
+
<nav className="fixed top-0 left-0 right-0 z-[100] flex items-center justify-between bg-nav-fade px-6 py-4 backdrop-blur-[8px] md:px-10 md:py-5 xl:px-20">
|
|
19
|
+
<div className="flex items-center gap-0 font-nav text-[20px]">
|
|
20
|
+
<span className="font-bold text-theme">rizom</span>
|
|
21
|
+
<span className="font-bold text-accent">.</span>
|
|
22
|
+
<span className="text-theme-muted">{brandSuffix}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="flex items-center gap-3 md:gap-8">
|
|
25
|
+
{navLinks.map((link) => (
|
|
26
|
+
<a key={link.href} href={link.href} className={LINK_CLS}>
|
|
27
|
+
{link.label}
|
|
28
|
+
</a>
|
|
29
|
+
))}
|
|
30
|
+
<a
|
|
31
|
+
href={primaryCta.href}
|
|
32
|
+
className="font-body text-[13px] font-semibold text-theme border border-theme rounded-[8px] px-4 py-2 transition-colors hover:border-accent hover:text-accent md:px-6 md:py-2.5 md:text-[15px]"
|
|
33
|
+
>
|
|
34
|
+
{primaryCta.label}
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
</nav>
|
|
38
|
+
);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { ComponentChildren, JSX } from "preact";
|
|
2
|
+
import { ProductIllustration } from "./ProductIllustration";
|
|
3
|
+
import { Section } from "./Section";
|
|
4
|
+
import type { ProductCardContent, ProductVariant } from "./types";
|
|
5
|
+
|
|
6
|
+
const INNER_BASE =
|
|
7
|
+
"group relative overflow-hidden grid gap-8 md:gap-14 rounded-[20px] border px-6 py-8 md:px-12 md:py-11 transition-all duration-500 ease-[cubic-bezier(0.2,0.8,0.2,1)] hover:-translate-y-1 [background-image:var(--rizom-product-card-bg)] [border-color:var(--rizom-product-card-border)] hover:[border-color:var(--rizom-product-card-hover-border)] hover:[box-shadow:var(--rizom-product-card-hover-shadow)] before:content-[''] before:absolute before:left-0 before:right-0 before:top-0 before:h-[2px] before:opacity-70 hover:before:opacity-100 before:[background-image:var(--rizom-product-card-bar)] after:content-[''] after:absolute after:inset-0 after:pointer-events-none after:bg-[radial-gradient(circle_at_1px_1px,var(--color-card-grid-dot)_1px,transparent_0)] after:bg-[length:22px_22px] after:bg-[position:14px_14px] after:[mask-image:linear-gradient(180deg,#000_0%,#000_55%,transparent_100%)]";
|
|
8
|
+
const INNER_LAYOUT_CLASS: Record<ProductVariant, string> = {
|
|
9
|
+
rover: "md:[grid-template-columns:minmax(0,1fr)_minmax(0,1.05fr)]",
|
|
10
|
+
relay: "md:[grid-template-columns:minmax(0,1.05fr)_minmax(0,1fr)]",
|
|
11
|
+
ranger: "md:[grid-template-columns:minmax(0,1fr)_minmax(0,1.05fr)]",
|
|
12
|
+
};
|
|
13
|
+
const INNER_THEME_CLASS: Record<ProductVariant, string> = {
|
|
14
|
+
rover:
|
|
15
|
+
"[--rizom-product-card-bg:var(--color-card-rover-bg)] [--rizom-product-card-border:var(--color-card-rover-border)] [--rizom-product-card-hover-border:var(--color-card-rover-border-hover)] [--rizom-product-card-hover-shadow:0_30px_80px_-30px_var(--color-glow-rover)] [--rizom-product-card-bar:linear-gradient(90deg,transparent,var(--color-accent)_30%,var(--color-accent)_70%,transparent)]",
|
|
16
|
+
relay:
|
|
17
|
+
"[--rizom-product-card-bg:var(--color-card-relay-bg)] [--rizom-product-card-border:var(--color-card-relay-border)] [--rizom-product-card-hover-border:var(--color-card-relay-border-hover)] [--rizom-product-card-hover-shadow:0_30px_80px_-30px_var(--color-glow-relay)] [--rizom-product-card-bar:linear-gradient(90deg,transparent,var(--color-secondary)_30%,var(--color-secondary)_70%,transparent)]",
|
|
18
|
+
ranger:
|
|
19
|
+
"[--rizom-product-card-bg:var(--color-card-ranger-bg)] [--rizom-product-card-border:var(--color-card-ranger-border)] [--rizom-product-card-hover-border:var(--color-card-ranger-border-hover)] [--rizom-product-card-hover-shadow:0_30px_80px_-30px_var(--color-glow-ranger)] [--rizom-product-card-bar:linear-gradient(90deg,transparent,var(--palette-amber-light)_18%,var(--color-secondary)_82%,transparent)]",
|
|
20
|
+
};
|
|
21
|
+
const ILLUSTRATION_BASE =
|
|
22
|
+
"relative z-[1] order-first h-[220px] w-full overflow-hidden rounded-xl border md:h-[320px] [border-color:var(--color-card-illust-border)] [background-image:linear-gradient(var(--color-card-illust-grid)_1px,transparent_1px),linear-gradient(90deg,var(--color-card-illust-grid)_1px,transparent_1px),var(--color-card-illust-overlay)] [background-size:28px_28px,28px_28px,auto]";
|
|
23
|
+
|
|
24
|
+
const CORNER_BASE =
|
|
25
|
+
"pointer-events-none absolute h-[14px] w-[14px] opacity-85 before:content-[''] before:absolute before:left-0 before:top-0 before:h-[1.5px] before:w-full before:bg-current after:content-[''] after:absolute after:left-0 after:top-0 after:h-full after:w-[1.5px] after:bg-current";
|
|
26
|
+
const ILLUSTRATION_CORNER_BASE = `${CORNER_BASE} z-[2]`;
|
|
27
|
+
|
|
28
|
+
const CORNER_CLASS: Record<ProductVariant, string> = {
|
|
29
|
+
rover: "text-accent",
|
|
30
|
+
relay: "text-secondary",
|
|
31
|
+
ranger: "text-secondary",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const TAGLINE_ARROW_CLASS: Record<ProductVariant, string> = {
|
|
35
|
+
rover: "text-accent",
|
|
36
|
+
relay: "text-secondary",
|
|
37
|
+
ranger: "text-secondary",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DEFAULT_TAGLINES: Record<ProductVariant, string[]> = {
|
|
41
|
+
rover: ["Ingest", "Synthesize", "Publish"],
|
|
42
|
+
relay: ["Map", "Track", "Retain"],
|
|
43
|
+
ranger: ["Scan", "Score", "Assemble"],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const ProductCard = ({
|
|
47
|
+
variant,
|
|
48
|
+
label,
|
|
49
|
+
badge,
|
|
50
|
+
headline,
|
|
51
|
+
description,
|
|
52
|
+
tagline,
|
|
53
|
+
tags,
|
|
54
|
+
backgroundWatermark,
|
|
55
|
+
}: ProductCardContent & {
|
|
56
|
+
backgroundWatermark?: ComponentChildren;
|
|
57
|
+
}): JSX.Element => {
|
|
58
|
+
const amber = variant === "rover";
|
|
59
|
+
const isRelay = variant === "relay";
|
|
60
|
+
const accentText = amber ? "text-accent" : "text-secondary";
|
|
61
|
+
const badgeClasses = amber
|
|
62
|
+
? "border border-accent/45 text-accent bg-accent/10"
|
|
63
|
+
: "border border-secondary/45 text-secondary bg-secondary/10";
|
|
64
|
+
const tagClasses = amber ? "text-accent/90" : "text-secondary/90";
|
|
65
|
+
const taglineParts =
|
|
66
|
+
tagline && tagline.length > 0 ? tagline : DEFAULT_TAGLINES[variant];
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Section className="reveal py-9">
|
|
70
|
+
<div
|
|
71
|
+
className={`${INNER_BASE} ${INNER_LAYOUT_CLASS[variant]} ${INNER_THEME_CLASS[variant]}`}
|
|
72
|
+
>
|
|
73
|
+
<span
|
|
74
|
+
className={`${CORNER_BASE} ${CORNER_CLASS[variant]} left-3 top-3`}
|
|
75
|
+
/>
|
|
76
|
+
<span
|
|
77
|
+
className={`${CORNER_BASE} ${CORNER_CLASS[variant]} right-3 top-3 scale-x-[-1]`}
|
|
78
|
+
/>
|
|
79
|
+
<span
|
|
80
|
+
className={`${CORNER_BASE} ${CORNER_CLASS[variant]} bottom-3 left-3 scale-y-[-1]`}
|
|
81
|
+
/>
|
|
82
|
+
<span
|
|
83
|
+
className={`${CORNER_BASE} ${CORNER_CLASS[variant]} bottom-3 right-3 scale-[-1]`}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<div
|
|
87
|
+
className={`relative z-[1] min-w-0 pt-1 ${isRelay ? "md:order-2" : ""}`}
|
|
88
|
+
>
|
|
89
|
+
{backgroundWatermark ? (
|
|
90
|
+
<div className="pointer-events-none absolute left-1/2 top-1/2 z-0 w-[320px] -translate-x-1/2 -translate-y-1/2 md:w-[400px] lg:w-[460px] opacity-[0.18]">
|
|
91
|
+
{backgroundWatermark}
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
|
|
95
|
+
<div className="relative z-[1] flex flex-col gap-[18px]">
|
|
96
|
+
<div className="flex flex-wrap items-baseline gap-3.5 border-b border-dashed border-[var(--color-card-divider)] pb-3.5">
|
|
97
|
+
<span
|
|
98
|
+
className={`font-display text-[38px] font-bold leading-none tracking-[-1.2px] md:text-[52px] ${accentText}`}
|
|
99
|
+
>
|
|
100
|
+
{label}
|
|
101
|
+
</span>
|
|
102
|
+
<span
|
|
103
|
+
className={`ml-auto inline-flex items-center gap-1.5 rounded-[2px] px-2.5 py-[5px] font-mono text-[10px] font-semibold uppercase tracking-[0.2em] ${badgeClasses}`}
|
|
104
|
+
>
|
|
105
|
+
<span className="text-[9px] leading-none">▸</span>
|
|
106
|
+
{badge}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="flex flex-wrap items-center gap-2.5 font-mono text-[11.5px] uppercase tracking-[0.22em] text-theme-muted">
|
|
111
|
+
{taglineParts.map((part, index) => (
|
|
112
|
+
<span key={`${variant}-${part}`} className="contents">
|
|
113
|
+
{index > 0 ? (
|
|
114
|
+
<span
|
|
115
|
+
className={`opacity-55 ${TAGLINE_ARROW_CLASS[variant]}`}
|
|
116
|
+
>
|
|
117
|
+
→
|
|
118
|
+
</span>
|
|
119
|
+
) : null}
|
|
120
|
+
<span>{part}</span>
|
|
121
|
+
</span>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<h3 className="font-display text-[26px] font-bold leading-[1.18] tracking-[-0.6px] md:text-[36px]">
|
|
126
|
+
{headline}
|
|
127
|
+
</h3>
|
|
128
|
+
<p className="max-w-[54ch] text-body-md leading-[1.7] text-theme-muted">
|
|
129
|
+
{description}
|
|
130
|
+
</p>
|
|
131
|
+
|
|
132
|
+
<div className="mt-auto flex flex-wrap items-center gap-x-0 gap-y-1.5 border-t border-dashed border-[var(--color-card-divider)] pt-[18px]">
|
|
133
|
+
{tags.map((tag, index) => (
|
|
134
|
+
<span
|
|
135
|
+
key={tag}
|
|
136
|
+
className={`font-mono text-[10px] font-medium uppercase tracking-[0.16em] ${tagClasses}`}
|
|
137
|
+
>
|
|
138
|
+
{index > 0 ? (
|
|
139
|
+
<span className="pr-2.5 text-[var(--color-card-tag-separator)]">
|
|
140
|
+
/
|
|
141
|
+
</span>
|
|
142
|
+
) : null}
|
|
143
|
+
<span className="pr-2.5">{tag}</span>
|
|
144
|
+
</span>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div
|
|
151
|
+
className={`${ILLUSTRATION_BASE} ${isRelay ? "md:order-1" : "md:order-none"}`}
|
|
152
|
+
>
|
|
153
|
+
<span
|
|
154
|
+
className={`${ILLUSTRATION_CORNER_BASE} ${CORNER_CLASS[variant]} left-[10px] top-[10px]`}
|
|
155
|
+
/>
|
|
156
|
+
<span
|
|
157
|
+
className={`${ILLUSTRATION_CORNER_BASE} ${CORNER_CLASS[variant]} right-[10px] top-[10px] scale-x-[-1]`}
|
|
158
|
+
/>
|
|
159
|
+
<span
|
|
160
|
+
className={`${ILLUSTRATION_CORNER_BASE} ${CORNER_CLASS[variant]} bottom-[10px] left-[10px] scale-y-[-1]`}
|
|
161
|
+
/>
|
|
162
|
+
<span
|
|
163
|
+
className={`${ILLUSTRATION_CORNER_BASE} ${CORNER_CLASS[variant]} bottom-[10px] right-[10px] scale-[-1]`}
|
|
164
|
+
/>
|
|
165
|
+
<ProductIllustration variant={variant} />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</Section>
|
|
169
|
+
);
|
|
170
|
+
};
|