Keep the template picker in the create flow, make catalog cards real links, and stop showing a fake community when review has no draft.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-10 15:22:46 -06:00
co-authored by Cursor
parent 3f9a8395be
commit 90db213413
31 changed files with 412 additions and 126 deletions
+3 -1
View File
@@ -35,6 +35,7 @@ const RuleContainer = memo<RuleProps>(
backgroundColor = "bg-[var(--color-community-teal-100)]",
className = "",
onClick,
href,
expanded = false,
size: sizeProp,
categories,
@@ -95,8 +96,9 @@ const RuleContainer = memo<RuleProps>(
icon={icon}
backgroundColor={backgroundColor}
className={className}
href={hasBottomLinks ? undefined : href}
onClick={hasBottomLinks ? undefined : handleClick}
onKeyDown={hasBottomLinks ? undefined : handleKeyDown}
onKeyDown={hasBottomLinks || href ? undefined : handleKeyDown}
expanded={expanded}
size={size}
categories={categories}
+6
View File
@@ -50,6 +50,11 @@ export interface RuleProps {
backgroundColor?: string;
className?: string;
onClick?: () => void;
/**
* When set, the card is a real link (focusable, Enter, open in a new tab)
* instead of a click-only `role="button"` surface.
*/
href?: string;
expanded?: boolean;
size?: RuleSizeValue;
categories?: Category[];
@@ -93,6 +98,7 @@ export interface RuleViewProps {
backgroundColor: string;
className: string;
onClick?: () => void;
href?: string;
onKeyDown?: (_event: React.KeyboardEvent<HTMLDivElement>) => void;
expanded: boolean;
size: RuleSizeValue;
+34 -10
View File
@@ -1,6 +1,7 @@
"use client";
import Image from "next/image";
import NextLink from "next/link";
import MultiSelect from "../../controls/MultiSelect";
import InlineTextButton from "../../buttons/InlineTextButton";
import NavigationLink from "../../navigation/Link";
@@ -19,6 +20,7 @@ export function RuleView({
backgroundColor,
className,
onClick,
href,
onKeyDown,
expanded,
size,
@@ -264,16 +266,11 @@ export function RuleView({
);
}
return (
<div
className={`${backgroundColor} ${cardPadding} ${cardGap} ${borderRadiusClass} shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] ${interactiveCard ? "hover:shadow-[0px_0px_64px_0px_rgba(0,0,0,0.15)] transition-shadow duration-200" : ""} flex flex-col items-start justify-center relative ${cardWidth || "w-full"} ${className || ""}`}
tabIndex={interactiveCard ? 0 : undefined}
role={interactiveCard ? "button" : "article"}
aria-label={ariaLabel}
aria-expanded={interactiveCard ? expanded : undefined}
onClick={interactiveCard ? onClick : undefined}
onKeyDown={interactiveCard ? onKeyDown : undefined}
>
const cardClassName = `${backgroundColor} ${cardPadding} ${cardGap} ${borderRadiusClass} shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] ${interactiveCard ? "hover:shadow-[0px_0px_64px_0px_rgba(0,0,0,0.15)] transition-shadow duration-200" : ""} flex flex-col items-start justify-center relative ${cardWidth || "w-full"} ${href ? "no-underline text-inherit" : ""} ${className || ""}`;
const isLinkCard = Boolean(href) && interactiveCard;
const cardBody = (
<>
{/* Figma: Header = `border-b` row, `gap-px`, icon `pl-1 pr-2 py-2` + `border-l` on title. */}
<div
className="
@@ -483,6 +480,33 @@ export function RuleView({
</div>
)
)}
</>
);
if (isLinkCard && href) {
return (
<NextLink
href={href}
className={cardClassName}
aria-label={ariaLabel}
onClick={onClick}
>
{cardBody}
</NextLink>
);
}
return (
<div
className={cardClassName}
tabIndex={interactiveCard ? 0 : undefined}
role={interactiveCard ? "button" : "article"}
aria-label={ariaLabel}
aria-expanded={interactiveCard ? expanded : undefined}
onClick={interactiveCard ? onClick : undefined}
onKeyDown={interactiveCard ? onKeyDown : undefined}
>
{cardBody}
</div>
);
}
@@ -9,7 +9,13 @@ import type { GovernanceTemplateCatalogEntry } from "../../../../lib/templates/g
export interface GovernanceTemplateGridProps {
entries: GovernanceTemplateCatalogEntry[];
onTemplateClick: (_slug: string) => void;
/**
* Real navigation target per card. Cards render as anchors so they are
* keyboard-focusable and work in a new tab.
*/
hrefForTemplate: (_slug: string) => string;
/** Optional side effects on activate (analytics, draft reset). */
onTemplateClick?: (_slug: string) => void;
/**
* When true, use project **`md`** (640px) for a 2-column grid (e.g. `/use-cases`).
* Default keeps the template shell break at **768px**.
@@ -19,6 +25,7 @@ export interface GovernanceTemplateGridProps {
export function GovernanceTemplateGrid({
entries,
hrefForTemplate,
onTemplateClick,
twoColumnsFromMd = false,
}: GovernanceTemplateGridProps) {
@@ -105,9 +112,14 @@ export function GovernanceTemplateGrid({
/>
}
backgroundColor={card.backgroundColor}
onClick={() => {
onTemplateClick(card.slug);
}}
href={hrefForTemplate(card.slug)}
onClick={
onTemplateClick
? () => {
onTemplateClick(card.slug);
}
: undefined
}
/>
))}
</div>
@@ -5,10 +5,10 @@
*/
import { memo, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "../../../contexts/MessagesContext";
import { logger } from "../../../../lib/logger";
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import { buildTemplateReviewHref } from "../../../(app)/create/utils/flowSteps";
import {
fetchTemplates,
isTemplatesFetchAborted,
@@ -34,7 +34,6 @@ declare global {
const RuleStackContainer = memo<RuleStackProps>(
({ className = "", initialGridEntries, translationNamespace, twoColumnsFromMd }) => {
const router = useRouter();
const namespace = translationNamespace ?? "pages.home.ruleStack";
const t = useTranslation(namespace);
const [gridEntries, setGridEntries] = useState<TemplateGridCardEntry[] | null>(
@@ -101,12 +100,12 @@ const RuleStackContainer = memo<RuleStackProps>(
// `signedIn: true` because this surface has no session in hand; DELETE is
// best-effort and the sentinel blocks stale-draft hydration.
prepareFreshCreateFlowEntrySync({ signedIn: true });
router.push(`/create/review-template/${encodeURIComponent(slug)}`);
};
return (
<RuleStackView
className={className}
hrefForTemplate={(slug) => buildTemplateReviewHref(slug)}
onTemplateClick={handleTemplateClick}
gridEntries={gridEntries}
sectionTitle={t("title")}
@@ -21,6 +21,7 @@ export interface RuleStackProps {
export interface RuleStackViewProps {
className: string;
onTemplateClick: (_slug: string) => void;
hrefForTemplate: (_slug: string) => string;
/** `null` while loading curated templates from the API. */
gridEntries: TemplateGridCardEntry[] | null;
sectionTitle: string;
@@ -10,6 +10,7 @@ import type { RuleStackViewProps } from "./RuleStack.types";
export function RuleStackView({
className,
onTemplateClick,
hrefForTemplate,
gridEntries,
sectionTitle,
sectionSubtitle,
@@ -46,6 +47,7 @@ export function RuleStackView({
) : (
<GovernanceTemplateGrid
entries={gridEntries}
hrefForTemplate={hrefForTemplate}
onTemplateClick={onTemplateClick}
twoColumnsFromMd={twoColumnsFromMd}
/>