Skip to content

Landing page v5#452

Open
ghesp wants to merge 13 commits intomainfrom
landing-page-v5
Open

Landing page v5#452
ghesp wants to merge 13 commits intomainfrom
landing-page-v5

Conversation

@ghesp
Copy link
Copy Markdown

@ghesp ghesp commented Apr 22, 2026

Layout update

@docs-page
Copy link
Copy Markdown

docs-page Bot commented Apr 22, 2026

To view this pull requests documentation preview, visit the following URL:

docs.page/invertase/docs.page~452

Documentation is deployed and generated using docs.page.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs-page Ready Ready Preview Apr 22, 2026 11:01am

Request Review

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant updates to the design system, including the addition of new design token structures, UI best practices, and a comprehensive overhaul of the marketing homepage layout. The changes include new components for the homepage, updated styling for existing components, and improved accessibility and responsiveness. I have identified a bug in the rehype-inline-badges plugin where a comparison operator was used instead of an assignment, and a responsiveness issue in the TrustedBy component. Additionally, there are two new components, FeatureCell and FeaturesScrollStrip, that appear to be unused and should be removed if they are not intended for future use.

function visitor(node: NodeWithChildren) {
function visitor(node: HastNode) {
if (!isElementWithVisited(node)) return;
node.visited === "true";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This line uses a strict equality operator (===) instead of an assignment operator (=). This prevents the visited flag from being set, which likely breaks the logic in containsBadge (line 41) intended to prevent redundant processing of the same node.

Suggested change
node.visited === "true";
node.visited = "true";

className={cn(
"mt-8 grid w-full list-none sm:mt-10",
"px-20",
"grid-cols-1 gap-2 sm:grid-cols-2 sm:gap-3 lg:grid-cols-4",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The px-20 padding on this list applies to all screen sizes. On narrow mobile devices, this significantly restricts the available space for project cards, leading to aggressive truncation of project names. Consider using a smaller padding for mobile and increasing it at larger breakpoints.

Suggested change
"grid-cols-1 gap-2 sm:grid-cols-2 sm:gap-3 lg:grid-cols-4",
"px-4 sm:px-20",

Comment on lines +1 to +169
"use client";

import type { ComponentProps } from "react";
import { useEffect, useState } from "react";

import { cn } from "~/utils";

function delay(ms: number) {
return new Promise<void>((resolve) => {
setTimeout(resolve, ms);
});
}

export function FeatureCell({
icon,
title,
description,
className,
tabIndex = 0,
...rest
}: {
icon: React.ReactNode;
title: string;
description: string;
} & ComponentProps<"div">) {
const [active, setActive] = useState(false);
const [titleShown, setTitleShown] = useState("");
const [descShown, setDescShown] = useState("");
/** `null` until client reads hover / motion media (avoids clearing touch layout on first paint). */
const [typewriterOff, setTypewriterOff] = useState<boolean | null>(null);

useEffect(() => {
const mqHover = window.matchMedia("(hover: none)");
const mqMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const sync = () => {
setTypewriterOff(mqHover.matches || mqMotion.matches);
};
sync();
mqHover.addEventListener("change", sync);
mqMotion.addEventListener("change", sync);
return () => {
mqHover.removeEventListener("change", sync);
mqMotion.removeEventListener("change", sync);
};
}, []);

useEffect(() => {
if (typewriterOff === null) return;

if (typewriterOff) {
setTitleShown(title);
setDescShown(description);
return;
}

if (!active) {
setTitleShown("");
setDescShown("");
return;
}

let cancelled = false;

(async () => {
setTitleShown("");
setDescShown("");
for (let i = 1; i <= title.length; i++) {
if (cancelled) return;
await delay(18);
if (cancelled) return;
setTitleShown(title.slice(0, i));
}
for (let i = 1; i <= description.length; i++) {
if (cancelled) return;
await delay(11);
if (cancelled) return;
setDescShown(description.slice(0, i));
}
})();

return () => {
cancelled = true;
};
}, [active, typewriterOff, title, description]);

const typingTitle = titleShown.length < title.length;
const typingDesc =
titleShown.length >= title.length && descShown.length < description.length;
const showCaret =
active && typewriterOff === false && (typingTitle || typingDesc);

return (
// biome-ignore lint/a11y/useSemanticElements: Marketing tile contains headings and overlay content; not representable as a single button or fieldset.
<div
{...rest}
role="group"
tabIndex={tabIndex}
aria-label={`${title}. ${description}`}
className={cn(
"feature-cell group relative z-10 flex aspect-square w-full min-w-0 flex-col items-center justify-center overflow-visible rounded-md border border-border",
"bg-background bg-gradient-to-b from-[#F1F2F3]/20 to-[#F8F8F8]/20 dark:from-[#2D2F39]/20 dark:to-[#16171D]/20",
"text-center",
"transition-[transform,box-shadow,background-color,background-image,border-color] duration-300 ease-out will-change-transform",
"hover:z-50 hover:scale-[1.18] hover:border-border hover:shadow-lg focus-within:z-50 focus-within:scale-[1.18] focus-within:border-border focus-within:shadow-lg",
"dark:hover:bg-[hsl(var(--color-design-black))] dark:focus-within:bg-[hsl(var(--color-design-black))]",
"motion-reduce:transform-none motion-reduce:hover:shadow-none",
"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
className,
)}
onMouseEnter={() => setActive(true)}
onMouseLeave={() => setActive(false)}
onFocus={() => setActive(true)}
onBlur={() => setActive(false)}
>
<div className="feature-cell-content flex h-full w-full flex-col items-center justify-center p-4">
<div className="feature-cell-icon-wrap origin-center shrink-0 opacity-100 transition-opacity duration-300 ease-out group-hover:opacity-0 group-focus-within:opacity-0">
{icon}
</div>
</div>

<div
aria-hidden
className={cn(
"feature-cell-copy-overlay pointer-events-none absolute inset-0 z-[1] flex flex-col items-center justify-center rounded-md p-4",
"bg-background bg-gradient-to-b from-[#F1F2F3]/20 to-[#F8F8F8]/20 dark:from-[#2D2F39]/20 dark:to-[#16171D]/20",
"dark:group-hover:bg-[hsl(var(--color-design-black))] dark:group-focus-within:bg-[hsl(var(--color-design-black))]",
"opacity-0 transition-opacity duration-300 ease-out",
"group-hover:pointer-events-auto group-hover:opacity-100",
"group-focus-within:pointer-events-auto group-focus-within:opacity-100",
)}
>
<div className="feature-cell-copy-inner flex w-full flex-col items-center justify-center gap-1.5 overflow-visible px-0.5 py-1 text-center">
<h3 className="relative w-full overflow-visible text-center font-heading text-sm font-medium leading-snug sm:text-base">
<span className="invisible block w-full select-none" aria-hidden>
{title}
</span>
<span className="absolute left-0 top-0 block w-full text-foreground">
<span className="inline text-center">
{titleShown}
{showCaret && typingTitle ? (
<span
aria-hidden
className="ml-px inline-block h-[1.05em] w-[2px] translate-y-[0.08em] animate-pulse bg-foreground align-baseline"
/>
) : null}
</span>
</span>
</h3>
<p className="relative w-full overflow-visible text-center text-xs font-light leading-relaxed sm:text-[0.8125rem]">
<span className="invisible block w-full select-none" aria-hidden>
{description}
</span>
<span className="absolute left-0 top-0 block w-full text-muted-foreground">
<span className="inline text-center">
{descShown}
{showCaret && typingDesc ? (
<span
aria-hidden
className="ml-px inline-block h-[1.05em] w-[2px] translate-y-[0.08em] animate-pulse bg-muted-foreground align-baseline"
/>
) : null}
</span>
</span>
</p>
</div>
</div>
</div>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component appears to be unused in the current pull request. The Features section in website/src/layouts/homepage/Features.tsx uses a different implementation. If this is a leftover from a previous layout iteration, it should be removed to avoid maintaining dead code.

Comment on lines +1 to +137
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

import { useIsomorphicLayoutEffect } from "~/lib/use-isomorphic-layout-effect";
import { cn } from "~/lib/utils";

type ThumbState = { widthPct: number; leftPct: number };

const MIN_THUMB_PCT = 10;

export function FeaturesScrollStrip({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
const scrollRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
const [thumb, setThumb] = useState<ThumbState>({
widthPct: 100,
leftPct: 0,
});
const [overflow, setOverflow] = useState(false);

const updateThumb = useCallback(() => {
const el = scrollRef.current;
if (!el) return;
const { scrollLeft, scrollWidth, clientWidth } = el;
const maxScroll = scrollWidth - clientWidth;
const hasOverflow = maxScroll > 2;
setOverflow(hasOverflow);
if (!hasOverflow) {
setThumb({ widthPct: 100, leftPct: 0 });
return;
}
const rawWidthPct = (clientWidth / scrollWidth) * 100;
const widthPct = Math.max(rawWidthPct, MIN_THUMB_PCT);
const maxLeft = 100 - widthPct;
const leftPct = maxScroll > 0 ? (scrollLeft / maxScroll) * maxLeft : 0;
setThumb({ widthPct, leftPct });
}, []);

useIsomorphicLayoutEffect(() => {
updateThumb();
}, [updateThumb]);

useEffect(() => {
const el = scrollRef.current;
if (!el) return;
updateThumb();
el.addEventListener("scroll", updateThumb, { passive: true });
const ro = new ResizeObserver(updateThumb);
ro.observe(el);
return () => {
el.removeEventListener("scroll", updateThumb);
ro.disconnect();
};
}, [updateThumb]);

const onTrackPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (e.button !== 0) return;
const scroll = scrollRef.current;
const track = trackRef.current;
if (!scroll || !track) return;
if ((e.target as HTMLElement).dataset.thumb === "true") return;
const rect = track.getBoundingClientRect();
const x = e.clientX - rect.left;
const maxScroll = scroll.scrollWidth - scroll.clientWidth;
const ratio = rect.width > 0 ? Math.max(0, Math.min(1, x / rect.width)) : 0;
scroll.scrollLeft = ratio * maxScroll;
};

const onThumbPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
const scroll = scrollRef.current;
const track = trackRef.current;
if (!scroll || !track) return;
const startX = e.clientX;
const startScroll = scroll.scrollLeft;
const trackW = track.getBoundingClientRect().width;
const maxScroll = scroll.scrollWidth - scroll.clientWidth;

const onMove = (ev: PointerEvent) => {
const dx = ev.clientX - startX;
scroll.scrollLeft =
trackW > 0 ? startScroll + (dx / trackW) * maxScroll : startScroll;
};
const onUp = () => {
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", onUp);
};
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
};

return (
<div className={cn("relative w-full", className)}>
<div
ref={scrollRef}
className={cn(
/* Horizontal inset so end cards aren’t flush with the strip edges; outer wrapper size unchanged */
"marketing-features-scroll-native-hidden w-full py-0 px-4 sm:px-6",
/* Space below cards; extra when scrollbar track is shown */
overflow ? "pb-5" : "pb-4",
)}
>
{children}
</div>
{overflow ? (
<div
className="pointer-events-none absolute bottom-0 left-0 right-0 z-10"
aria-hidden
>
<div
ref={trackRef}
role="presentation"
className="pointer-events-auto relative h-1 w-full cursor-pointer rounded-none bg-muted"
onPointerDown={onTrackPointerDown}
>
<div
data-thumb="true"
className="absolute top-0 h-full cursor-grab rounded-none bg-primary active:cursor-grabbing"
style={{
width: `${thumb.widthPct}%`,
left: `${thumb.leftPct}%`,
}}
onPointerDown={onThumbPointerDown}
/>
</div>
</div>
) : null}
</div>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component appears to be unused in the current pull request. Consider removing it to avoid maintaining dead code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants