@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 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
+ );
@@ -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
+ };