Component cleanup

This commit is contained in:
adilallo
2026-04-29 07:20:16 -06:00
parent 252848eba9
commit e6127f1a3f
267 changed files with 2087 additions and 2196 deletions
+3 -2
View File
@@ -34,8 +34,9 @@ export type ChipPaletteValue = (typeof CHIP_PALETTE_OPTIONS)[number];
distinct Figma node. distinct Figma node.
- For create-flow screens, node ids come from `CREATE_FLOW_SCREEN_REGISTRY` - For create-flow screens, node ids come from `CREATE_FLOW_SCREEN_REGISTRY`
in `app/(app)/create/utils/createFlowScreenRegistry.ts`. For everything else, in `app/(app)/create/utils/createFlowScreenRegistry.ts`. For everything else,
pull the node id from the Figma file directly. Use `TODO(figma)` as a prefer `Figma: "<Path>" (<node-id>)` from the file. If the node id is not
placeholder rather than omitting the docstring entirely. wired yet, use `Figma: "<Path>"` plus a short note (e.g. *canonical code under
\`controls/\`*) rather than omitting the docstring.
```typescript ```typescript
/** /**
+41 -6
View File
@@ -51,13 +51,48 @@ export { default } from "./<Name>.container";
export type { <Name>Props } from "./<Name>.types"; export type { <Name>Props } from "./<Name>.types";
``` ```
## Single-file pattern (exception) ## Small presentational packages (buttons)
`app/components/buttons/*.tsx` and other trivially-presentational components `app/components/buttons/<Name>/` holds **`index.tsx`** plus **`<Name>.tsx`**
can stay as a single file when they have **no derived state and only a (the **`Button`**, **`InlineTextButton`** packages today). Promote to the full
handful of props** (e.g. `Button.tsx`, `InlineTextButton.tsx`). If you find container/view/types split when state or logic outgrows a single module (like **`controls/TextInput`**).
yourself adding state, side effects, or enum logic, promote it to the split
pattern. ## `cards/` packages
Prefer the **container / view / types** layout for **`Selection/`**, **`CardStack/`**, **`Rule/`**,
**`Icon/`**, **`Mini/`**, **`TemplateReviewCard/`**. **`Step/`** keeps a single
**`<Name>.tsx`** next to **`index.tsx`** until complexity justifies a split.
## `modals/` packages
Use the same **container / view / types** split where those files exist (**`Alert`**, **`Create`**, **`Dialog`**, **`Login`**, **`Tooltip`**, **`ModalHeader`**, **`ModalFooter`**).
## `navigation/` packages
Use the **container / view / types** split + per-package **`index.tsx`** for **`Top/`**, **`CreateFlowTopNav/`**, **`CreateFlowFooter/`**, **`NavigationItem/`**, **`Link/`**, **`MenuItem/`**. **`TopWithPathname.tsx`** lives inside **`Top/`** as the pathname + session wrapper.
**Root-level** **`Menu.tsx`**, **`Footer.tsx`**, **`ConditionalNavigation.tsx`**, and **`ConditionalNavigationClient.tsx`** sit beside those folders—no bucket-level barrel. Figma **Navigation / Menu** maps to **`Menu`** + **`MenuItem`** (see **`docs/figma-component-registry.md`**, Navigation conventions) and **`routes.mdc`** for shell behavior.
## `progress/` packages
Use the **container / view / types** split + **`index.tsx`** for **`Stepper/`** and **`ProportionBar/`** (same shape as **`controls/`**). See **`docs/figma-component-registry.md`** — **Progress conventions** for Figma **Progress** vs **Control / Proportion**.
## `sections/` packages
Section-level compositions are **mixed**: many folders use **`container` / `view` / `types`** (**`FeatureGrid/`**, **`QuoteBlock/`**, …), while **`ContentBanner.tsx`** and **`SectionNumber.tsx`** are **single modules** at the bucket root. Prefer the **split** for **new** composites; see **`docs/figma-component-registry.md`** — **Sections conventions**. **`SectionHeader/`** lives under **`type/`** (Figma Type / SectionHeader). Published rule typography body **`CommunityRule/`** lives under **`type/`** (see **Type conventions**).
## `type/` packages
**`type/`** is mostly **`container` / `view` / `types`** + **`index.tsx`** (**`HeaderLockup/`**, **`ContentLockup/`**, **`NumberedList/`**, **`InputLabel/`**). **`SectionHeader/`** is a small presentational package (**`SectionHeader.tsx`** + **`index.tsx`**) for the Figma Type SectionHeader lockup. **`CommunityRule/`**, **`Section/`**, and **`TextBlock/`** are **view + types** packages (Community Rule document tree). See **`docs/figma-component-registry.md`** — **Type conventions**.
## No package-level barrels
Do **not** add **`app/components/<bucket>/index.tsx`** that re-exports every
sibling under that bucket (there is no `buttons/index.tsx` or `asset/index.tsx`).
Import **`…/buttons/Button`**, **`…/asset/icon`**, **`…/asset/Logo`**, etc.—same
mental model everywhere.
**Per-component** **`index.tsx`** entrypoints (**`Logo/index.tsx`**, **`controls/TextInput/index.tsx`**, …) stay as documented above—aggregating an entire **`buttons/`** or **`asset/`** tier in one file does not.
## Wrapper / group components ## Wrapper / group components
+1 -1
View File
@@ -29,7 +29,7 @@ Reach for these before writing new markup:
| Toggle-chip row + inline "+ Add" input | `app/(app)/create/components/ApplicableScopeField` | | Toggle-chip row + inline "+ Add" input | `app/(app)/create/components/ApplicableScopeField` |
| `[ value +]` numeric stepper (± label) | `app/components/controls/Incrementer` / `IncrementerBlock` | | `[ value +]` numeric stepper (± label) | `app/components/controls/Incrementer` / `IncrementerBlock` |
| Mid-paragraph "expand / see all" link button | `app/components/buttons/InlineTextButton` | | Mid-paragraph "expand / see all" link button | `app/components/buttons/InlineTextButton` |
| Help-icon + label above a control | `app/components/utility/InputLabel` (`helpIcon` prop) | | Help-icon + label above a control | `app/components/type/InputLabel` (`helpIcon` prop) |
| Toggle chip (dim-but-clickable) | `Chip` with `state="Disabled" disabled={false}` | | Toggle chip (dim-but-clickable) | `Chip` with `state="Disabled" disabled={false}` |
| Card-click → structured creation modal | `Create` with `backdropVariant="blurredYellow"` | | Card-click → structured creation modal | `Create` with `backdropVariant="blurredYellow"` |
+4 -4
View File
@@ -14,10 +14,10 @@ the file tree without affecting URLs.
| Group | URL surface | Audience | Chrome | | Group | URL surface | Audience | Chrome |
|---|---|---|---| |---|---|---|---|
| `app/(marketing)/` | `/`, `/learn`, `/blog`, `/templates`, future public pages | Public, indexable | TopNav (via root) + marketing `<Footer />` | | `app/(marketing)/` | `/`, `/learn`, `/blog`, `/templates`, future public pages | Public, indexable | `Top` (via root) + marketing `<Footer />` |
| `app/(app)/` | `/create/*`, `/login`, `/profile`, future signed-in surfaces | Authenticated product | TopNav (via root) — no footer except **`/profile`** (see `profile/layout.tsx`) | | `app/(app)/` | `/create/*`, `/login`, `/profile`, future signed-in surfaces | Authenticated product | `Top` (via root) — no footer except **`/profile`** (see `profile/layout.tsx`) |
| `app/(admin)/` | `/monitor`, future ops dashboards | Operators | TopNav (via root) — no footer | | `app/(admin)/` | `/monitor`, future ops dashboards | Operators | `Top` (via root) — no footer |
| `app/(dev)/` | `/components-preview`, future dev previews | Local dev (NODE_ENV gated) | TopNav (via root) — no footer | | `app/(dev)/` | `/components-preview`, future dev previews | Local dev (NODE_ENV gated) | `Top` (via root) — no footer |
| `app/api/` | API routes | n/a | n/a | | `app/api/` | API routes | n/a | n/a |
Route folders **must not** sit loose at the top level of `app/`. If a new Route folders **must not** sit loose at the top level of `app/`. If a new
+5
View File
@@ -41,6 +41,8 @@ priors — they reflect deliberate decisions.
(`app/components/Foo``tests/components/Foo.test.tsx`). (`app/components/Foo``tests/components/Foo.test.tsx`).
5. **Routes live inside groups**`(marketing)`, `(app)`, `(admin)`, 5. **Routes live inside groups**`(marketing)`, `(app)`, `(admin)`,
`(dev)`. Don't drop a new route folder loose at the top of `app/`. `(dev)`. Don't drop a new route folder loose at the top of `app/`.
**Admin-only widgets** may live in **`app/(admin)/<route>/_components/`**
when only that route uses them (e.g. **`WebVitalsDashboard`** on **`/monitor`**).
6. **No new pathname-sniffing chrome.** Compose chrome via group/nested 6. **No new pathname-sniffing chrome.** Compose chrome via group/nested
layouts, not `usePathname()` checks. (`ConditionalNavigation` is the layouts, not `usePathname()` checks. (`ConditionalNavigation` is the
sole tolerated exception — it carries SSR session state.) sole tolerated exception — it carries SSR session state.)
@@ -83,3 +85,6 @@ For changes under `prisma/`: `npm run migrate:smoke` (see
- [docs/README.md](docs/README.md) — index of user-facing docs. - [docs/README.md](docs/README.md) — index of user-facing docs.
- [docs/create-flow.md](docs/create-flow.md) — wizard URL/persistence canon - [docs/create-flow.md](docs/create-flow.md) — wizard URL/persistence canon
(read alongside `create-flow.mdc`). (read alongside `create-flow.mdc`).
- [docs/figma-component-registry.md](docs/figma-component-registry.md) —
Figma ↔ component bucket map after refactors (Type, Sections, admin
`_components/`, etc.).
+3 -1
View File
@@ -35,7 +35,9 @@ Backend setup (Postgres, Prisma, magic-link auth) is documented in
## Project layout ## Project layout
```text ```text
app/ Next.js app router (routes, components, hooks, contexts) app/ Next.js app router: route groups (marketing), (app), (admin), (dev);
shared components under app/components/; optional _components/
colocated with a route (e.g. (admin)/monitor/_components/)
lib/ Shared library code (i18n, validation, utilities) lib/ Shared library code (i18n, validation, utilities)
messages/en/ Localized UI copy (see docs/guides/i18n-translation-workflow.md) messages/en/ Localized UI copy (see docs/guides/i18n-translation-workflow.md)
prisma/ Database schema, migrations, seed prisma/ Database schema, migrations, seed
+3 -3
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import WebVitalsDashboard from "../../components/sections/WebVitalsDashboard"; import WebVitalsDashboard from "./_components/WebVitalsDashboard";
import TopNav from "../../components/navigation/TopNav"; import Top from "../../components/navigation/Top";
import Footer from "../../components/navigation/Footer"; import Footer from "../../components/navigation/Footer";
import { useMessages } from "../../contexts/MessagesContext"; import { useMessages } from "../../contexts/MessagesContext";
@@ -11,7 +11,7 @@ export default function MonitorPageContent() {
return ( return (
<div className="min-h-screen bg-[var(--color-surface-default-primary)]"> <div className="min-h-screen bg-[var(--color-surface-default-primary)]">
<TopNav folderTop={false} /> <Top folderTop={false} />
<main className="container mx-auto px-[var(--spacing-scale-024)] py-[var(--spacing-scale-032)]"> <main className="container mx-auto px-[var(--spacing-scale-024)] py-[var(--spacing-scale-032)]">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
@@ -1,8 +1,8 @@
"use client"; "use client";
import { memo, useEffect, useState } from "react"; import { memo, useEffect, useState } from "react";
import { useMessages } from "../../../contexts/MessagesContext"; import { useMessages } from "../../../../contexts/MessagesContext";
import { logger } from "../../../../lib/logger"; import { logger } from "../../../../../lib/logger";
import WebVitalsDashboardView from "./WebVitalsDashboard.view"; import WebVitalsDashboardView from "./WebVitalsDashboard.view";
import type { Metrics, Vitals, VitalData } from "./WebVitalsDashboard.types"; import type { Metrics, Vitals, VitalData } from "./WebVitalsDashboard.types";
@@ -78,7 +78,6 @@ const WebVitalsDashboardContainer = memo(() => {
fetchVitals(); fetchVitals();
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// web-vitals v4+ exposes onLCP / onCLS / … — legacy getLCP was removed.
import("web-vitals").then( import("web-vitals").then(
({ onCLS, onFID, onFCP, onLCP, onTTFB }) => { ({ onCLS, onFID, onFCP, onLCP, onTTFB }) => {
onLCP((metric) => { onLCP((metric) => {
@@ -1,4 +1,4 @@
import type messages from "../../../../messages/en/index"; import type messages from "../../../../../messages/en/index";
export interface VitalData { export interface VitalData {
value: number; value: number;
+2 -2
View File
@@ -13,14 +13,14 @@ import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
import { useCreateFlowExit } from "./hooks/useCreateFlowExit"; import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
import { useCreateFlowFinalize } from "./hooks/useCreateFlowFinalize"; import { useCreateFlowFinalize } from "./hooks/useCreateFlowFinalize";
import { useTemplateReviewActions } from "./hooks/useTemplateReviewActions"; import { useTemplateReviewActions } from "./hooks/useTemplateReviewActions";
import CreateFlowTopNav from "../../components/utility/CreateFlowTopNav"; import CreateFlowFooter from "../../components/navigation/CreateFlowFooter";
import CreateFlowTopNav from "../../components/navigation/CreateFlowTopNav";
import { getNextStep, getStepIndex } from "./utils/flowSteps"; import { getNextStep, getStepIndex } from "./utils/flowSteps";
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress"; import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
import { import {
createFlowStepUsesCenteredTextLayout, createFlowStepUsesCenteredTextLayout,
createFlowStepUsesCardLayout, createFlowStepUsesCardLayout,
} from "./utils/createFlowScreenRegistry"; } from "./utils/createFlowScreenRegistry";
import CreateFlowFooter from "../../components/utility/CreateFlowFooter";
import Button from "../../components/buttons/Button"; import Button from "../../components/buttons/Button";
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail"; import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
import { import {
@@ -9,7 +9,7 @@
import { memo, useState } from "react"; import { memo, useState } from "react";
import Chip from "../../../components/controls/Chip"; import Chip from "../../../components/controls/Chip";
import InputLabel from "../../../components/utility/InputLabel"; import InputLabel from "../../../components/type/InputLabel";
export interface ApplicableScopeFieldProps { export interface ApplicableScopeFieldProps {
/** Label rendered above the capsule row. */ /** Label rendered above the capsule row. */
@@ -8,8 +8,8 @@ import {
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS, CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "./createFlowLayoutTokens"; } from "./createFlowLayoutTokens";
/** Shared `RuleCard` / template card chrome: width + radius; padding comes from `RuleCard` (L+expanded = 24px). */ /** Shared `Rule` / template card chrome: width + radius; padding comes from `Rule` (L+expanded = 24px). */
export const CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS = export const CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS =
"w-full min-w-0 rounded-[12px] md:rounded-[24px] md:max-w-[640px]"; "w-full min-w-0 rounded-[12px] md:rounded-[24px] md:max-w-[640px]";
type CreateFlowLockupCardStepShellProps = { type CreateFlowLockupCardStepShellProps = {
@@ -8,7 +8,7 @@
import { memo, useId } from "react"; import { memo, useId } from "react";
import TextArea from "../../../components/controls/TextArea"; import TextArea from "../../../components/controls/TextArea";
import InputLabel from "../../../components/utility/InputLabel"; import InputLabel from "../../../components/type/InputLabel";
export interface ModalTextAreaFieldProps { export interface ModalTextAreaFieldProps {
/** Label rendered above the text area. */ /** Label rendered above the text area. */
@@ -11,7 +11,7 @@ import {
import messages from "../../../../../messages/en/index"; import messages from "../../../../../messages/en/index";
import Alert from "../../../../components/modals/Alert"; import Alert from "../../../../components/modals/Alert";
import { import {
CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS, CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS,
CreateFlowLockupCardStepShell, CreateFlowLockupCardStepShell,
} from "../../components/CreateFlowLockupCardStepShell"; } from "../../components/CreateFlowLockupCardStepShell";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell"; import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
@@ -118,7 +118,7 @@ export default function ReviewTemplatePage({ params }: PageProps) {
> >
<TemplateReviewCard <TemplateReviewCard
template={template} template={template}
ruleCardClassName={CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS} ruleCardClassName={CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS}
size={mdUp ? "L" : "M"} size={mdUp ? "L" : "M"}
/> />
</CreateFlowLockupCardStepShell> </CreateFlowLockupCardStepShell>
@@ -24,7 +24,7 @@ import {
useFacetRecommendations, useFacetRecommendations,
} from "../../hooks/useFacetRecommendations"; } from "../../hooks/useFacetRecommendations";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import CardStack from "../../../../components/utility/CardStack"; import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create"; import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton"; import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell"; import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
@@ -22,7 +22,7 @@ import {
useFacetRecommendations, useFacetRecommendations,
} from "../../hooks/useFacetRecommendations"; } from "../../hooks/useFacetRecommendations";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import CardStack from "../../../../components/utility/CardStack"; import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create"; import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton"; import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell"; import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
@@ -23,7 +23,7 @@ import {
useFacetRecommendations, useFacetRecommendations,
} from "../../hooks/useFacetRecommendations"; } from "../../hooks/useFacetRecommendations";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import CardStack from "../../../../components/utility/CardStack"; import CardStack from "../../../../components/cards/CardStack";
import Create from "../../../../components/modals/Create"; import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton"; import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell"; import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
@@ -2,8 +2,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import CommunityRuleDocument from "../../../../components/sections/CommunityRuleDocument"; import CommunityRule from "../../../../components/type/CommunityRule";
import type { CommunityRuleDocumentSection } from "../../../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types"; import type { CommunityRuleSection } from "../../../../components/type/CommunityRule/CommunityRule.types";
import Alert from "../../../../components/modals/Alert"; import Alert from "../../../../components/modals/Alert";
import { useMessages } from "../../../../contexts/MessagesContext"; import { useMessages } from "../../../../contexts/MessagesContext";
import { fetchPublishedRuleDetail } from "../../../../../lib/create/api"; import { fetchPublishedRuleDetail } from "../../../../../lib/create/api";
@@ -24,7 +24,7 @@ function initialCompletedUi(
): { ): {
headerTitle: string; headerTitle: string;
headerDescription: string | undefined; headerDescription: string | undefined;
documentSections: CommunityRuleDocumentSection[]; documentSections: CommunityRuleSection[];
} { } {
if (ruleIdFromUrl) { if (ruleIdFromUrl) {
return { return {
@@ -80,7 +80,7 @@ export function CompletedScreen() {
string | undefined string | undefined
>(initial.headerDescription); >(initial.headerDescription);
const [documentSections, setDocumentSections] = const [documentSections, setDocumentSections] =
useState<CommunityRuleDocumentSection[]>(initial.documentSections); useState<CommunityRuleSection[]>(initial.documentSections);
useEffect(() => { useEffect(() => {
if (!ruleIdParam) return; if (!ruleIdParam) return;
@@ -170,7 +170,7 @@ export function CompletedScreen() {
aria-hidden aria-hidden
/> />
<div className="w-full min-w-0 py-0 md:pb-8"> <div className="w-full min-w-0 py-0 md:pb-8">
<CommunityRuleDocument <CommunityRule
sections={documentSections} sections={documentSections}
useCardStyle={!mdUp} useCardStyle={!mdUp}
className={mdUp ? "min-w-0" : "w-full min-w-0 p-4"} className={mdUp ? "min-w-0" : "w-full min-w-0 p-4"}
@@ -2,7 +2,7 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import RuleCard from "../../../../components/cards/RuleCard"; import Rule from "../../../../components/cards/Rule";
import { useTranslation } from "../../../../contexts/MessagesContext"; import { useTranslation } from "../../../../contexts/MessagesContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlow } from "../../context/CreateFlowContext";
@@ -12,6 +12,10 @@ import {
CREATE_FLOW_MD_UP_GRID_CELL_CLASS, CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS, CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "../../components/createFlowLayoutTokens"; } from "../../components/createFlowLayoutTokens";
import {
getAssetPath,
vectorMarkPath,
} from "../../../../../lib/assetUtils";
/** /**
* Targets for a `pendingTemplateAction` redirect. Customize resumes the * Targets for a `pendingTemplateAction` redirect. Customize resumes the
@@ -90,13 +94,13 @@ export function CommunityReviewScreen() {
/> />
</div> </div>
<div className={CREATE_FLOW_MD_UP_GRID_CELL_CLASS}> <div className={CREATE_FLOW_MD_UP_GRID_CELL_CLASS}>
<RuleCard <Rule
title={cardTitle} title={cardTitle}
description={cardDescription} description={cardDescription}
size={lgUp ? "L" : "M"} size={lgUp ? "L" : "M"}
expanded={false} expanded={false}
backgroundColor="bg-[var(--color-teal-teal50,#c9fef9)]" backgroundColor="bg-[var(--color-teal-teal50,#c9fef9)]"
logoUrl="/assets/Vector_MutualAid.svg" logoUrl={getAssetPath(vectorMarkPath("mutual-aid"))}
logoAlt={cardTitle} logoAlt={cardTitle}
className="rounded-[24px]" className="rounded-[24px]"
/> />
@@ -1,14 +1,13 @@
"use client"; "use client";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import RuleCard from "../../../../components/cards/RuleCard"; import Rule, { type Category } from "../../../../components/cards/Rule";
import type { Category } from "../../../../components/cards/RuleCard/RuleCard.types";
import { TemplateChipDetailModal } from "../../../../components/cards/TemplateReviewCard/TemplateChipDetailModal"; import { TemplateChipDetailModal } from "../../../../components/cards/TemplateReviewCard/TemplateChipDetailModal";
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext"; import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { import {
CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS, CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS,
CreateFlowLockupCardStepShell, CreateFlowLockupCardStepShell,
} from "../../components/CreateFlowLockupCardStepShell"; } from "../../components/CreateFlowLockupCardStepShell";
import { import {
@@ -22,12 +21,16 @@ import {
type FinalReviewChipEditPatch, type FinalReviewChipEditPatch,
type FinalReviewChipEditTarget, type FinalReviewChipEditTarget,
} from "../../components/FinalReviewChipEditModal"; } from "../../components/FinalReviewChipEditModal";
import {
getAssetPath,
vectorMarkPath,
} from "../../../../../lib/assetUtils";
/** /**
* `finalReview.json.categories` ships a demo ordering + localized names * `finalReview.json.categories` ships a demo ordering + localized names
* (Values / Communication / Membership / Decision-making / Conflict * (Values / Communication / Membership / Decision-making / Conflict
* management). We reuse that ordering for the state-derived rows so the * management). We reuse that ordering for the state-derived rows so the
* RuleCard layout stays stable across customize / use-without-changes / * Rule layout stays stable across customize / use-without-changes /
* plain-custom flows, and fall back to the demo chips when state resolves * plain-custom flows, and fall back to the demo chips when state resolves
* to nothing selected. * to nothing selected.
*/ */
@@ -183,16 +186,16 @@ export function FinalReviewScreen() {
lockupTitle={t("title")} lockupTitle={t("title")}
lockupDescription={t("description")} lockupDescription={t("description")}
> >
<RuleCard <Rule
title={ruleCardTitle} title={ruleCardTitle}
description={ruleCardDescription} description={ruleCardDescription}
size={mdUp ? "L" : "M"} size={mdUp ? "L" : "M"}
expanded={true} expanded={true}
backgroundColor="bg-[#c9fef9]" backgroundColor="bg-[#c9fef9]"
logoUrl="/assets/Vector_MutualAid.svg" logoUrl={getAssetPath(vectorMarkPath("mutual-aid"))}
logoAlt={ruleCardTitle} logoAlt={ruleCardTitle}
categories={finalReviewCategories} categories={finalReviewCategories}
className={CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS} className={CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS}
onClick={() => {}} onClick={() => {}}
/> />
<FinalReviewChipEditModal <FinalReviewChipEditModal
@@ -17,12 +17,13 @@
*/ */
import { useState, useCallback, useMemo } from "react"; import { useState, useCallback, useMemo } from "react";
import DecisionMakingSidebar from "../../../../components/utility/DecisionMakingSidebar"; import CardStack from "../../../../components/cards/CardStack";
import CardStack from "../../../../components/utility/CardStack"; import HeaderLockup from "../../../../components/type/HeaderLockup";
import Create from "../../../../components/modals/Create"; import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton"; import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import type { InfoMessageBoxItem } from "../../../../components/utility/InfoMessageBox/InfoMessageBox.types"; import InfoMessageBox from "../../../../components/controls/InfoMessageBox";
import type { CardStackItem } from "../../../../components/utility/CardStack/CardStack.types"; import type { InfoMessageBoxItem } from "../../../../components/controls/InfoMessageBox/InfoMessageBox.types";
import type { CardStackItem } from "../../../../components/cards/CardStack/CardStack.types";
import { useMessages } from "../../../../contexts/MessagesContext"; import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp"; import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
@@ -204,16 +205,20 @@ export function DecisionApproachesScreen() {
contentTopBelowMd="space-800" contentTopBelowMd="space-800"
lgVerticalAlign="start" lgVerticalAlign="start"
header={ header={
<DecisionMakingSidebar <div className="flex w-full min-w-0 flex-col gap-3">
title={da.sidebar.title} <HeaderLockup
description={sidebarDescription} title={da.sidebar.title}
messageBoxTitle={da.messageBox.title} description={sidebarDescription}
messageBoxItems={messageBoxItems} size={mdUp ? "L" : "M"}
messageBoxCheckedIds={messageBoxCheckedIds} justification={mdUp ? "left" : "center"}
onMessageBoxCheckboxChange={handleMessageBoxCheckboxChange} />
size={mdUp ? "L" : "M"} <InfoMessageBox
justification={mdUp ? "left" : "center"} title={da.messageBox.title}
/> items={messageBoxItems}
checkedIds={messageBoxCheckedIds}
onCheckboxChange={handleMessageBoxCheckboxChange}
/>
</div>
} }
> >
<div className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0"> <div className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0">
@@ -2,11 +2,11 @@
import { useId, useMemo } from "react"; import { useId, useMemo } from "react";
import Button from "../../../components/buttons/Button"; import Button from "../../../components/buttons/Button";
import RuleCard from "../../../components/cards/RuleCard"; import Rule from "../../../components/cards/Rule";
import TextInput from "../../../components/controls/TextInput"; import TextInput from "../../../components/controls/TextInput";
import List from "../../../components/layout/List"; import List from "../../../components/layout/List";
import type { ListItem, ListSize } from "../../../components/layout/List"; import type { ListItem, ListSize } from "../../../components/layout/List";
import Icon from "../../../components/asset/Icon"; import Icon from "../../../components/asset/icon";
import Dialog from "../../../components/modals/Dialog"; import Dialog from "../../../components/modals/Dialog";
import Alert from "../../../components/modals/Alert"; import Alert from "../../../components/modals/Alert";
import HeaderLockup from "../../../components/type/HeaderLockup"; import HeaderLockup from "../../../components/type/HeaderLockup";
@@ -82,10 +82,10 @@ const profileSectionHeadingClass =
"font-bricolage text-base font-bold leading-[22px] text-[var(--color-content-default-primary)] md:font-inter md:text-xl md:font-bold md:leading-7 xl:font-bricolage-grotesque xl:font-bold xl:text-[28px] xl:leading-9"; "font-bricolage text-base font-bold leading-[22px] text-[var(--color-content-default-primary)] md:font-inter md:text-xl md:font-bold md:leading-7 xl:font-bricolage-grotesque xl:font-bold xl:text-[28px] xl:leading-9";
/** /**
* Sticky `top` for page content below the product {@link TopNav} (standard variant). * Sticky `top` for page content below the product {@link Top} (standard variant).
* Must match `TopNav.view.tsx`: nav `h` 40px → `lg` 84px → `xl` 88px, plus `header` `border-b` (+1px). * Must match `Top.view.tsx`: nav `h` 40px → `lg` 84px → `xl` 88px, plus `header` `border-b` (+1px).
*/ */
const stickyBelowTopNavTopClass = const stickyBelowTopTopClass =
"top-[41px] lg:top-[85px] xl:top-[89px]"; "top-[41px] lg:top-[85px] xl:top-[89px]";
export type ProfilePageSignedOutViewProps = { export type ProfilePageSignedOutViewProps = {
@@ -111,7 +111,7 @@ export function ProfilePageSignedOutView({
<header <header
className={ className={
profileLgUp profileLgUp
? `sticky z-10 bg-[var(--color-surface-default-primary)] ${stickyBelowTopNavTopClass}` ? `sticky z-10 bg-[var(--color-surface-default-primary)] ${stickyBelowTopTopClass}`
: `flex flex-col gap-1 py-3 md:sticky md:top-[41px] md:z-10 md:bg-[var(--color-surface-default-primary)]` : `flex flex-col gap-1 py-3 md:sticky md:top-[41px] md:z-10 md:bg-[var(--color-surface-default-primary)]`
} }
> >
@@ -304,7 +304,7 @@ export function ProfilePageView({
</h2> </h2>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{showDraftCard && draft?.hasDraft ? ( {showDraftCard && draft?.hasDraft ? (
<RuleCard <Rule
title={(() => { title={(() => {
const raw = draft.state.title; const raw = draft.state.title;
const s = typeof raw === "string" ? raw.trim() : ""; const s = typeof raw === "string" ? raw.trim() : "";
@@ -337,7 +337,7 @@ export function ProfilePageView({
/> />
) : null} ) : null}
{rules.map((rule) => ( {rules.map((rule) => (
<RuleCard <Rule
key={rule.id} key={rule.id}
title={rule.title} title={rule.title}
description={rule.summary ?? undefined} description={rule.summary ?? undefined}
+19 -17
View File
@@ -14,15 +14,12 @@ const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
ssr: true, ssr: true,
}); });
const NumberedCards = dynamic( const CardSteps = dynamic(() => import("../components/sections/CardSteps"), {
() => import("../components/sections/NumberedCards"), loading: () => (
{ <section className="py-[var(--spacing-scale-032)] min-h-[300px]" />
loading: () => ( ),
<section className="py-[var(--spacing-scale-032)] min-h-[300px]" /> ssr: true,
), });
ssr: true,
},
);
const FeatureGrid = dynamic( const FeatureGrid = dynamic(
() => import("../components/sections/FeatureGrid"), () => import("../components/sections/FeatureGrid"),
@@ -54,22 +51,27 @@ export default function Page() {
ctaHref: t("pages.home.heroBanner.ctaHref"), ctaHref: t("pages.home.heroBanner.ctaHref"),
}; };
const numberedCardsData = { const cardStepsData = {
title: t("pages.home.numberedCards.title"), title: t("pages.home.cardSteps.title"),
subtitle: t("pages.home.numberedCards.subtitle"), subtitle: t("pages.home.cardSteps.subtitle"),
cards: [ headingDesktopLines: [
t("pages.home.cardSteps.headingDesktopLine1"),
t("pages.home.cardSteps.headingDesktopLine2"),
t("pages.home.cardSteps.headingDesktopLine3"),
] as const,
steps: [
{ {
text: t("pages.home.numberedCards.cards.card1.text"), text: t("pages.home.cardSteps.cards.card1.text"),
iconShape: "blob", iconShape: "blob",
iconColor: "green", iconColor: "green",
}, },
{ {
text: t("pages.home.numberedCards.cards.card2.text"), text: t("pages.home.cardSteps.cards.card2.text"),
iconShape: "gear", iconShape: "gear",
iconColor: "purple", iconColor: "purple",
}, },
{ {
text: t("pages.home.numberedCards.cards.card3.text"), text: t("pages.home.cardSteps.cards.card3.text"),
iconShape: "star", iconShape: "star",
iconColor: "orange", iconColor: "orange",
}, },
@@ -92,7 +94,7 @@ export default function Page() {
<div> <div>
<HeroBanner {...heroBannerData} /> <HeroBanner {...heroBannerData} />
<LogoWall /> <LogoWall />
<NumberedCards {...numberedCardsData} /> <CardSteps {...cardStepsData} />
<Suspense <Suspense
fallback={ fallback={
<section className="py-[var(--spacing-scale-032)] min-h-[400px]" /> <section className="py-[var(--spacing-scale-032)] min-h-[400px]" />
+2 -2
View File
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getPublicPublishedRuleById } from "../../../../lib/server/publishedRules"; import { getPublicPublishedRuleById } from "../../../../lib/server/publishedRules";
import { parseDocumentSectionsForDisplay } from "../../../../lib/create/buildPublishPayload"; import { parseDocumentSectionsForDisplay } from "../../../../lib/create/buildPublishPayload";
import CommunityRuleDocument from "../../../components/sections/CommunityRuleDocument"; import CommunityRule from "../../../components/type/CommunityRule";
import HeaderLockup from "../../../components/type/HeaderLockup"; import HeaderLockup from "../../../components/type/HeaderLockup";
interface PageProps { interface PageProps {
@@ -65,7 +65,7 @@ export default async function PublicRuleDetailPage({ params }: PageProps) {
size="L" size="L"
palette="inverse" palette="inverse"
/> />
<CommunityRuleDocument sections={sections} /> <CommunityRule sections={sections} />
</div> </div>
</div> </div>
); );
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Avatar";
export type { AvatarSizeValue } from "./Avatar";
@@ -1,14 +1,15 @@
"use client"; "use client";
import Image from "next/image";
import { memo } from "react"; import { memo } from "react";
import ArrowBackIcon from "./icon/arrow_back.svg"; import ArrowBackIcon from "./arrow_back.svg";
import ContentCopyIcon from "./icon/content_copy.svg"; import ContentCopyIcon from "./content_copy.svg";
import EditIcon from "./icon/edit.svg"; import EditIcon from "./edit.svg";
import ExclamationIcon from "./icon/exclamation.svg"; import ExclamationIcon from "./exclamation.svg";
import ChevronRightIcon from "./icon/chevron_right.svg"; import ChevronRightIcon from "./chevron_right.svg";
import LogOutIcon from "./icon/log_out.svg"; import LogOutIcon from "./log_out.svg";
import MailIcon from "./icon/mail.svg"; import MailIcon from "./mail.svg";
import WarningIcon from "./icon/warning.svg"; import WarningIcon from "./warning.svg";
export const ICON_NAME_OPTIONS = [ export const ICON_NAME_OPTIONS = [
"arrow_back", "arrow_back",
@@ -75,12 +76,13 @@ function IconComponent({
// Turbopack/webpack mismatch: `.svg` may be a URL string instead of SVGR output. // Turbopack/webpack mismatch: `.svg` may be a URL string instead of SVGR output.
if (typeof resolved === "string") { if (typeof resolved === "string") {
return ( return (
<img <Image
src={resolved} src={resolved}
width={size} width={size}
height={size} height={size}
className={className} className={className}
alt="" alt=""
unoptimized
aria-hidden={ariaHidden} aria-hidden={ariaHidden}
/> />
); );
+3
View File
@@ -0,0 +1,3 @@
export { default } from "./Icon";
export { ICON_NAME_OPTIONS } from "./Icon";
export type { IconName, IconProps } from "./Icon";
-3
View File
@@ -1,3 +0,0 @@
export { default as Icon, ICON_NAME_OPTIONS } from "./Icon";
export type { IconName, IconProps } from "./Icon";
export { default as Logo } from "./logo";
@@ -4,7 +4,7 @@ import type {
ButtonTypeValue, ButtonTypeValue,
ButtonPaletteValue, ButtonPaletteValue,
ButtonStateValue, ButtonStateValue,
} from "../../../lib/propNormalization"; } from "../../../../lib/propNormalization";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode; children: React.ReactNode;
+1
View File
@@ -0,0 +1 @@
export { default } from "./Button";
@@ -0,0 +1,2 @@
export { default } from "./InlineTextButton";
export type { InlineTextButtonProps } from "./InlineTextButton";
-101
View File
@@ -1,101 +0,0 @@
"use client";
import Tag from "../../utility/Tag";
import type { CardViewProps } from "./Card.types";
function InfoIcon() {
return (
<span
className="flex h-[var(--spacing-scale-016)] w-[var(--spacing-scale-016)] shrink-0 items-center justify-center rounded-full border border-[var(--color-content-invert-brand-secondary)] bg-transparent font-inter text-[10px] font-medium leading-none text-[var(--color-content-invert-brand-secondary)]"
aria-hidden
>
?
</span>
);
}
function CardTag({
recommended,
selected,
}: {
recommended: boolean;
selected: boolean;
}) {
if (selected) return <Tag variant="selected" />;
if (recommended) return <Tag variant="recommended" />;
return null;
}
export function CardView({
label,
supportText,
recommended,
selected,
orientation,
showInfoIcon,
id: cardId,
className,
onClick,
onKeyDown,
}: CardViewProps) {
const borderClass = "border border-[var(--color-border-default-primary)]";
const selectedBorder = selected
? "outline outline-2 outline-dashed outline-black outline-offset-[-2px]"
: "";
const baseClasses = `select-none rounded-[var(--radius-measures-radius-small)] bg-[#FFFFFF] p-4 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`;
if (orientation === "horizontal") {
return (
<div
{...(cardId ? { "data-card-id": cardId } : {})}
role="button"
tabIndex={0}
aria-label={supportText ? `${label}: ${supportText}` : label}
className={baseClasses}
onClick={onClick}
onKeyDown={onKeyDown}
>
<div className="flex flex-col gap-2 items-start w-full">
<CardTag recommended={recommended} selected={selected} />
<span className="font-inter text-base font-semibold leading-6 text-black w-full">
{label}
</span>
{supportText ? (
<p className="font-inter text-sm font-normal leading-5 text-black w-full">
{supportText}
</p>
) : null}
</div>
</div>
);
}
return (
<div
{...(cardId ? { "data-card-id": cardId } : {})}
role="button"
tabIndex={0}
aria-label={supportText ? `${label}: ${supportText}` : label}
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
onClick={onClick}
onKeyDown={onKeyDown}
>
<div className="min-w-0 flex-1 flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-inter text-base font-semibold leading-6 text-black">
{label}
</span>
{showInfoIcon ? <InfoIcon /> : null}
</div>
{supportText ? (
<p className="font-inter text-sm font-normal leading-5 text-black">
{supportText}
</p>
) : null}
</div>
<div className="shrink-0 w-[6rem]">
<CardTag recommended={recommended} selected={selected} />
</div>
</div>
);
}
-2
View File
@@ -1,2 +0,0 @@
export { default } from "./Card.container";
export type { CardProps } from "./Card.types";
@@ -8,7 +8,8 @@ const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
const DEFAULT_SHOW_LESS_LABEL = "Show less"; const DEFAULT_SHOW_LESS_LABEL = "Show less";
/** /**
* Figma: "Utility / CardStack" (TODO(figma)). Selectable stack of cards with * Figma: "Utility / CardStack"; canonical code under `cards/`.
* Selectable stack of cards with
* an optional "see all"/"show less" expand toggle. * an optional "see all"/"show less" expand toggle.
*/ */
const CardStackContainer = memo<CardStackProps>( const CardStackContainer = memo<CardStackProps>(
@@ -1,7 +1,7 @@
"use client"; "use client";
import HeaderLockup from "../../type/HeaderLockup"; import HeaderLockup from "../../type/HeaderLockup";
import Card from "../../cards/Card"; import Selection from "../Selection";
import type { CardStackViewProps } from "./CardStack.types"; import type { CardStackViewProps } from "./CardStack.types";
export function CardStackView({ export function CardStackView({
@@ -59,7 +59,7 @@ export function CardStackView({
) : null} ) : null}
<div className="flex w-full min-w-0 flex-col gap-2"> <div className="flex w-full min-w-0 flex-col gap-2">
{displayedCards.map((item) => ( {displayedCards.map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -103,7 +103,7 @@ export function CardStackView({
{expanded ? ( {expanded ? (
<div className="mx-auto grid w-full max-w-[min(100%,860px)] grid-cols-1 gap-x-4 gap-y-6 md:grid-cols-2"> <div className="mx-auto grid w-full max-w-[min(100%,860px)] grid-cols-1 gap-x-4 gap-y-6 md:grid-cols-2">
{cards.map((item) => ( {cards.map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -120,7 +120,7 @@ export function CardStackView({
<> <>
<div className="flex w-full flex-col gap-2 md:hidden"> <div className="flex w-full flex-col gap-2 md:hidden">
{compactCards.map((item) => ( {compactCards.map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -143,7 +143,7 @@ export function CardStackView({
<div className="hidden flex-col gap-2 lg:flex"> <div className="hidden flex-col gap-2 lg:flex">
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
{compactCards.slice(0, 3).map((item) => ( {compactCards.slice(0, 3).map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -162,7 +162,7 @@ export function CardStackView({
{compactCards {compactCards
.slice(3, compactRecommendedLimit) .slice(3, compactRecommendedLimit)
.map((item) => ( .map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -181,7 +181,7 @@ export function CardStackView({
<div className="hidden flex-col gap-2 md:flex lg:hidden"> <div className="hidden flex-col gap-2 md:flex lg:hidden">
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
{compactCards.slice(0, 2).map((item) => ( {compactCards.slice(0, 2).map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -197,7 +197,7 @@ export function CardStackView({
</div> </div>
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
{compactCards.slice(2, 4).map((item) => ( {compactCards.slice(2, 4).map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -213,7 +213,7 @@ export function CardStackView({
</div> </div>
{compactCards[4] ? ( {compactCards[4] ? (
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
<Card <Selection
id={compactCards[4].id} id={compactCards[4].id}
label={compactCards[4].label} label={compactCards[4].label}
supportText={compactCards[4].supportText} supportText={compactCards[4].supportText}
@@ -233,7 +233,7 @@ export function CardStackView({
<> <>
<div className="flex w-full flex-col gap-2 md:hidden"> <div className="flex w-full flex-col gap-2 md:hidden">
{compactCards.map((item) => ( {compactCards.map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -253,7 +253,7 @@ export function CardStackView({
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-col gap-2 md:flex lg:hidden"> <div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-col gap-2 md:flex lg:hidden">
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
{compactCards.slice(0, 2).map((item) => ( {compactCards.slice(0, 2).map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -268,7 +268,7 @@ export function CardStackView({
))} ))}
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center">
<Card <Selection
id={compactCards[2].id} id={compactCards[2].id}
label={compactCards[2].label} label={compactCards[2].label}
supportText={compactCards[2].supportText} supportText={compactCards[2].supportText}
@@ -283,7 +283,7 @@ export function CardStackView({
</div> </div>
<div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-wrap justify-center gap-2 lg:flex"> <div className="mx-auto hidden w-full max-w-[min(100%,860px)] flex-wrap justify-center gap-2 lg:flex">
{compactCards.map((item) => ( {compactCards.map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -305,7 +305,7 @@ export function CardStackView({
key={item.id} key={item.id}
className="flex w-full min-w-0 shrink-0 justify-center md:w-[281px] md:max-w-[281px]" className="flex w-full min-w-0 shrink-0 justify-center md:w-[281px] md:max-w-[281px]"
> >
<Card <Selection
id={item.id} id={item.id}
label={item.label} label={item.label}
supportText={item.supportText} supportText={item.supportText}
@@ -326,7 +326,7 @@ export function CardStackView({
{/* Compact under 640: single column, up to 5 recommended cards */} {/* Compact under 640: single column, up to 5 recommended cards */}
<div className="flex w-full flex-col gap-2 md:hidden"> <div className="flex w-full flex-col gap-2 md:hidden">
{compactCards.map((item) => ( {compactCards.map((item) => (
<Card <Selection
key={item.id} key={item.id}
id={item.id} id={item.id}
label={item.label} label={item.label}
@@ -352,7 +352,7 @@ export function CardStackView({
: "md:col-start-4 md:col-span-2"; : "md:col-start-4 md:col-span-2";
return ( return (
<div key={item.id} className={colClass}> <div key={item.id} className={colClass}>
<Card <Selection
id={item.id} id={item.id}
label={item.label} label={item.label}
supportText={item.supportText} supportText={item.supportText}
@@ -1,10 +1,10 @@
"use client"; "use client";
import { memo } from "react"; import { memo } from "react";
import { IconCardView } from "./IconCard.view"; import { IconView } from "./Icon.view";
import type { IconCardProps } from "./IconCard.types"; import type { IconProps } from "./Icon.types";
const IconCardContainer = memo<IconCardProps>( const IconContainer = memo<IconProps>(
({ icon, title, description, className = "", onClick }) => { ({ icon, title, description, className = "", onClick }) => {
const handleClick = () => { const handleClick = () => {
if (onClick) onClick(); if (onClick) onClick();
@@ -18,7 +18,7 @@ const IconCardContainer = memo<IconCardProps>(
}; };
return ( return (
<IconCardView <IconView
icon={icon} icon={icon}
title={title} title={title}
description={description} description={description}
@@ -30,6 +30,6 @@ const IconCardContainer = memo<IconCardProps>(
}, },
); );
IconCardContainer.displayName = "IconCard"; IconContainer.displayName = "Icon";
export default IconCardContainer; export default IconContainer;
@@ -1,4 +1,4 @@
export interface IconCardProps { export interface IconProps {
icon: React.ReactNode; icon: React.ReactNode;
title: string; title: string;
description: string; description: string;
@@ -6,7 +6,7 @@ export interface IconCardProps {
onClick?: () => void; onClick?: () => void;
} }
export interface IconCardViewProps { export interface IconViewProps {
icon: React.ReactNode; icon: React.ReactNode;
title: string; title: string;
description: string; description: string;
@@ -1,15 +1,15 @@
"use client"; "use client";
import type { IconCardViewProps } from "./IconCard.types"; import type { IconViewProps } from "./Icon.types";
export function IconCardView({ export function IconView({
icon, icon,
title, title,
description, description,
className, className,
onClick, onClick,
onKeyDown, onKeyDown,
}: IconCardViewProps) { }: IconViewProps) {
return ( return (
<div <div
className={`border border-[var(--color-border-default-primary)] flex flex-col h-[350px] items-start justify-between p-[var(--measures-spacing-020)] relative w-[288px] bg-transparent cursor-pointer transition-all duration-200 hover:scale-[1.02] hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-2 ${className}`} className={`border border-[var(--color-border-default-primary)] flex flex-col h-[350px] items-start justify-between p-[var(--measures-spacing-020)] relative w-[288px] bg-transparent cursor-pointer transition-all duration-200 hover:scale-[1.02] hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-2 ${className}`}
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Icon.container";
export type { IconProps } from "./Icon.types";
-2
View File
@@ -1,2 +0,0 @@
export { default } from "./IconCard.container";
export type { IconCardProps } from "./IconCard.types";
@@ -1,10 +1,10 @@
"use client"; "use client";
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import MiniCardView from "./MiniCard.view"; import MiniView from "./Mini.view";
import type { MiniCardProps } from "./MiniCard.types"; import type { MiniProps } from "./Mini.types";
const MiniCardContainer = memo<MiniCardProps>( const MiniContainer = memo<MiniProps>(
({ ({
children, children,
className = "", className = "",
@@ -75,7 +75,7 @@ const MiniCardContainer = memo<MiniCardProps>(
}, [href, onClick, computedAriaLabel]); }, [href, onClick, computedAriaLabel]);
return ( return (
<MiniCardView <MiniView
className={className} className={className}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
panelContent={panelContent} panelContent={panelContent}
@@ -87,11 +87,11 @@ const MiniCardContainer = memo<MiniCardProps>(
wrapperProps={wrapperProps} wrapperProps={wrapperProps}
> >
{children} {children}
</MiniCardView> </MiniView>
); );
}, },
); );
MiniCardContainer.displayName = "MiniCard"; MiniContainer.displayName = "Mini";
export default MiniCardContainer; export default MiniContainer;
@@ -1,4 +1,4 @@
export interface MiniCardProps { export interface MiniProps {
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
backgroundColor?: string; backgroundColor?: string;
@@ -11,7 +11,7 @@ export interface MiniCardProps {
ariaLabel?: string; ariaLabel?: string;
} }
export interface MiniCardViewProps { export interface MiniViewProps {
children?: React.ReactNode; children?: React.ReactNode;
className: string; className: string;
backgroundColor: string; backgroundColor: string;
@@ -2,9 +2,9 @@
import { memo } from "react"; import { memo } from "react";
import Image from "next/image"; import Image from "next/image";
import type { MiniCardViewProps } from "./MiniCard.types"; import type { MiniViewProps } from "./Mini.types";
function MiniCardView({ function MiniView({
children, children,
className, className,
backgroundColor, backgroundColor,
@@ -15,7 +15,7 @@ function MiniCardView({
computedAriaLabel, computedAriaLabel,
wrapperElement, wrapperElement,
wrapperProps, wrapperProps,
}: MiniCardViewProps) { }: MiniViewProps) {
const cardContentElement = ( const cardContentElement = (
<div className={`h-[186px] flex flex-col gap-[7px] ${className}`}> <div className={`h-[186px] flex flex-col gap-[7px] ${className}`}>
{/* Top part - Inner panel */} {/* Top part - Inner panel */}
@@ -81,6 +81,6 @@ function MiniCardView({
); );
} }
MiniCardView.displayName = "MiniCardView"; MiniView.displayName = "MiniView";
export default memo(MiniCardView); export default memo(MiniView);
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Mini.container";
export type { MiniProps } from "./Mini.types";
-2
View File
@@ -1,2 +0,0 @@
export { default } from "./MiniCard.container";
export type { MiniCardProps } from "./MiniCard.types";
@@ -1,8 +1,8 @@
"use client"; "use client";
import { memo } from "react"; import { memo } from "react";
import { RuleCardView } from "./RuleCard.view"; import { RuleView } from "./Rule.view";
import type { RuleCardProps } from "./RuleCard.types"; import type { RuleProps } from "./Rule.types";
declare global { declare global {
interface Window { interface Window {
@@ -21,7 +21,7 @@ declare global {
* Figma: "Card / Rule" e.g. profile `22143:900771` when **Has bottom link** is on * Figma: "Card / Rule" e.g. profile `22143:900771` when **Has bottom link** is on
* (`hasBottomLinks` + `bottomLinks` / optional `bottomStatusLabel`). * (`hasBottomLinks` + `bottomLinks` / optional `bottomStatusLabel`).
*/ */
const RuleCardContainer = memo<RuleCardProps>( const RuleContainer = memo<RuleProps>(
({ ({
title, title,
description, description,
@@ -72,7 +72,7 @@ const RuleCardContainer = memo<RuleCardProps>(
}; };
return ( return (
<RuleCardView <RuleView
title={title} title={title}
description={description} description={description}
icon={icon} icon={icon}
@@ -95,6 +95,6 @@ const RuleCardContainer = memo<RuleCardProps>(
}, },
); );
RuleCardContainer.displayName = "RuleCard"; RuleContainer.displayName = "Rule";
export default RuleCardContainer; export default RuleContainer;
@@ -1,4 +1,5 @@
import type { ChipOption } from "../../controls/MultiSelect/MultiSelect.types"; import type { ChipOption } from "../../controls/MultiSelect/MultiSelect.types";
import type { RuleSizeValue } from "../../../../lib/propNormalization";
export interface Category { export interface Category {
name: string; name: string;
@@ -14,14 +15,14 @@ export interface Category {
} }
/** Bottom row for `Card / Rule` when Figma **Has bottom link** is on (profile, etc.). */ /** Bottom row for `Card / Rule` when Figma **Has bottom link** is on (profile, etc.). */
export interface RuleCardBottomLink { export interface RuleBottomLink {
id: string; id: string;
label: string; label: string;
href?: string; href?: string;
onClick?: () => void; onClick?: () => void;
} }
export interface RuleCardProps { export interface RuleProps {
title: string; title: string;
description?: string; description?: string;
icon?: React.ReactNode; icon?: React.ReactNode;
@@ -29,7 +30,7 @@ export interface RuleCardProps {
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
expanded?: boolean; expanded?: boolean;
size?: "XS" | "S" | "M" | "L"; size?: RuleSizeValue;
categories?: Category[]; categories?: Category[];
logoUrl?: string; logoUrl?: string;
logoAlt?: string; logoAlt?: string;
@@ -44,10 +45,10 @@ export interface RuleCardProps {
hasBottomLinks?: boolean; hasBottomLinks?: boolean;
/** Uppercase chip (e.g. IN PROGRESS); omit when no left badge. */ /** Uppercase chip (e.g. IN PROGRESS); omit when no left badge. */
bottomStatusLabel?: string; bottomStatusLabel?: string;
bottomLinks?: RuleCardBottomLink[]; bottomLinks?: RuleBottomLink[];
} }
export interface RuleCardViewProps { export interface RuleViewProps {
title: string; title: string;
description?: string; description?: string;
icon?: React.ReactNode; icon?: React.ReactNode;
@@ -56,7 +57,7 @@ export interface RuleCardViewProps {
onClick?: () => void; onClick?: () => void;
onKeyDown?: (_event: React.KeyboardEvent<HTMLDivElement>) => void; onKeyDown?: (_event: React.KeyboardEvent<HTMLDivElement>) => void;
expanded: boolean; expanded: boolean;
size: "XS" | "S" | "M" | "L"; size: RuleSizeValue;
categories?: Category[]; categories?: Category[];
logoUrl?: string; logoUrl?: string;
logoAlt?: string; logoAlt?: string;
@@ -64,5 +65,5 @@ export interface RuleCardViewProps {
hideCategoryAddButton?: boolean; hideCategoryAddButton?: boolean;
hasBottomLinks?: boolean; hasBottomLinks?: boolean;
bottomStatusLabel?: string; bottomStatusLabel?: string;
bottomLinks?: RuleCardBottomLink[]; bottomLinks?: RuleBottomLink[];
} }
@@ -4,9 +4,9 @@ import Image from "next/image";
import { useTranslation } from "../../../contexts/MessagesContext"; import { useTranslation } from "../../../contexts/MessagesContext";
import MultiSelect from "../../controls/MultiSelect"; import MultiSelect from "../../controls/MultiSelect";
import NavigationLink from "../../navigation/Link"; import NavigationLink from "../../navigation/Link";
import type { RuleCardBottomLink, RuleCardViewProps } from "./RuleCard.types"; import type { RuleBottomLink, RuleViewProps } from "./Rule.types";
export function RuleCardView({ export function RuleView({
title, title,
description, description,
icon, icon,
@@ -24,7 +24,7 @@ export function RuleCardView({
hasBottomLinks = false, hasBottomLinks = false,
bottomStatusLabel, bottomStatusLabel,
bottomLinks, bottomLinks,
}: RuleCardViewProps) { }: RuleViewProps) {
const t = useTranslation("ruleCard"); const t = useTranslation("ruleCard");
const ariaLabel = t("ariaLabel")?.replace("{title}", title) || title; const ariaLabel = t("ariaLabel")?.replace("{title}", title) || title;
const interactiveCard = !hasBottomLinks; const interactiveCard = !hasBottomLinks;
@@ -181,7 +181,7 @@ export function RuleCardView({
? "rounded-[var(--measures-radius-300,12px)]" ? "rounded-[var(--measures-radius-300,12px)]"
: "rounded-[var(--radius-measures-radius-small)]"; : "rounded-[var(--radius-measures-radius-small)]";
function renderBottomLink(link: RuleCardBottomLink) { function renderBottomLink(link: RuleBottomLink) {
const shared = { const shared = {
variant: "paragraph" as const, variant: "paragraph" as const,
type: "primary" as const, type: "primary" as const,
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Rule.container";
export type { Category, RuleBottomLink, RuleProps } from "./Rule.types";
-5
View File
@@ -1,5 +0,0 @@
export { default } from "./RuleCard.container";
export type {
RuleCardBottomLink,
RuleCardProps,
} from "./RuleCard.types";
@@ -1,10 +1,14 @@
"use client"; "use client";
import { memo } from "react"; import { memo } from "react";
import { CardView } from "./Card.view"; import { SelectionView } from "./Selection.view";
import type { CardProps } from "./Card.types"; import type { SelectionProps } from "./Selection.types";
const CardContainer = memo<CardProps>( /**
* Figma: "Card / CardSelection" stacked tile e.g. `16775:28762` (recommended + label + supportText).
* `orientation="horizontal"` selects that vertical stack; `"vertical"` is label + optional info icon with tag on the right (CardStack expanded / single-column).
*/
const SelectionContainer = memo<SelectionProps>(
({ ({
label, label,
supportText = "", supportText = "",
@@ -28,7 +32,7 @@ const CardContainer = memo<CardProps>(
}; };
return ( return (
<CardView <SelectionView
label={label} label={label}
supportText={supportText} supportText={supportText}
recommended={recommended} recommended={recommended}
@@ -44,6 +48,6 @@ const CardContainer = memo<CardProps>(
}, },
); );
CardContainer.displayName = "Card"; SelectionContainer.displayName = "Selection";
export default CardContainer; export default SelectionContainer;
@@ -1,17 +1,17 @@
export interface CardProps { export interface SelectionProps {
label: string; label: string;
supportText?: string; supportText?: string;
recommended?: boolean; recommended?: boolean;
selected?: boolean; selected?: boolean;
orientation: "horizontal" | "vertical"; orientation: "horizontal" | "vertical";
showInfoIcon?: boolean; showInfoIcon?: boolean;
/** Optional id for the card root (e.g. data-card-id for focus after modal close). */ /** Optional id for the root (e.g. `data-card-id` for focus after modal close). */
id?: string; id?: string;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
} }
export interface CardViewProps { export interface SelectionViewProps {
label: string; label: string;
supportText: string; supportText: string;
recommended: boolean; recommended: boolean;
@@ -0,0 +1,104 @@
"use client";
import Tag from "../../utility/Tag";
import type { SelectionViewProps } from "./Selection.types";
function InfoIcon() {
return (
<span
className="flex h-[var(--spacing-scale-016)] w-[var(--spacing-scale-016)] shrink-0 items-center justify-center rounded-full border border-[var(--color-content-invert-brand-secondary)] bg-transparent font-inter text-[10px] font-medium leading-none text-[var(--color-content-invert-brand-secondary)]"
aria-hidden
>
?
</span>
);
}
function SelectionTag({
recommended,
selected,
}: {
recommended: boolean;
selected: boolean;
}) {
if (selected) return <Tag variant="selected" />;
if (recommended) return <Tag variant="recommended" />;
return null;
}
export function SelectionView({
label,
supportText,
recommended,
selected,
orientation,
showInfoIcon,
id: selectionId,
className,
onClick,
onKeyDown,
}: SelectionViewProps) {
const borderClass = "border border-[var(--color-border-default-primary)]";
const selectedBorder = selected
? "outline outline-2 outline-dashed outline-black outline-offset-[-2px]"
: "";
// Figma: "Card / CardSelection" vertical stack — node `16775:28762` (dev).
// Prop `orientation="horizontal"` is this stacked layout (historical naming).
if (orientation === "horizontal") {
const baseClasses = `select-none rounded-[var(--measures-radius-200,8px)] bg-[var(--color-gray-000)] px-4 py-3 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`;
return (
<div
{...(selectionId ? { "data-card-id": selectionId } : {})}
role="button"
tabIndex={0}
aria-label={supportText ? `${label}: ${supportText}` : label}
className={`${baseClasses} flex min-h-0 w-full flex-col items-start justify-center gap-1`}
onClick={onClick}
onKeyDown={onKeyDown}
>
<SelectionTag recommended={recommended} selected={selected} />
<span className="w-full font-inter text-base font-medium leading-5 text-[var(--color-content-invert-secondary)]">
{label}
</span>
{supportText ? (
<p className="w-full font-inter text-xs font-normal leading-4 text-[var(--color-content-invert-tertiary)]">
{supportText}
</p>
) : null}
</div>
);
}
const baseClasses = `select-none rounded-[var(--measures-radius-200,8px)] bg-[var(--color-gray-000)] p-4 transition-[border-color,box-shadow,outline] duration-200 cursor-pointer ${borderClass} ${selectedBorder} ${className}`;
return (
<div
{...(selectionId ? { "data-card-id": selectionId } : {})}
role="button"
tabIndex={0}
aria-label={supportText ? `${label}: ${supportText}` : label}
className={`${baseClasses} flex flex-row items-center justify-between gap-4`}
onClick={onClick}
onKeyDown={onKeyDown}
>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex items-center gap-1">
<span className="font-inter text-base font-medium leading-5 text-[var(--color-content-invert-secondary)]">
{label}
</span>
{showInfoIcon ? <InfoIcon /> : null}
</div>
{supportText ? (
<p className="font-inter text-xs font-normal leading-4 text-[var(--color-content-invert-tertiary)]">
{supportText}
</p>
) : null}
</div>
<div className="shrink-0">
<SelectionTag recommended={recommended} selected={selected} />
</div>
</div>
);
}
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Selection.container";
export type { SelectionProps } from "./Selection.types";
@@ -1,19 +1,18 @@
"use client"; "use client";
import { memo } from "react"; import { memo } from "react";
import SectionNumber from "../sections/SectionNumber"; import type { StepSizeValue } from "../../../../lib/propNormalization";
import SectionNumber from "../../sections/SectionNumber";
export type NumberCardSizeValue = "small" | "medium" | "large" | "xlarge"; interface StepProps {
interface NumberCardProps {
number: number; number: number;
text: string; text: string;
size?: NumberCardSizeValue; size?: StepSizeValue;
iconShape?: string; iconShape?: string;
iconColor?: string; iconColor?: string;
} }
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => { const Step = memo<StepProps>(({ number, text, size: sizeProp }) => {
const baseClasses = const baseClasses =
"bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg"; "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
@@ -101,6 +100,6 @@ const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
); );
}); });
NumberCard.displayName = "NumberCard"; Step.displayName = "Step";
export default NumberCard; export default Step;
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./Step";
export type { StepSizeValue } from "../../../../lib/propNormalization";
@@ -3,7 +3,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import Create from "../../modals/Create"; import Create from "../../modals/Create";
import Chip from "../../controls/Chip"; import Chip from "../../controls/Chip";
import InputLabel from "../../utility/InputLabel"; import InputLabel from "../../type/InputLabel";
import ContentLockup from "../../type/ContentLockup"; import ContentLockup from "../../type/ContentLockup";
import ModalTextAreaField from "../../../(app)/create/components/ModalTextAreaField"; import ModalTextAreaField from "../../../(app)/create/components/ModalTextAreaField";
import { useMessages, useTranslation } from "../../../contexts/MessagesContext"; import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
@@ -2,11 +2,8 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import RuleCard from "../RuleCard"; import Rule from "../Rule";
import type { import type { Category, RuleProps } from "../Rule";
Category,
RuleCardProps,
} from "../RuleCard/RuleCard.types";
import { getAssetPath } from "../../../../lib/assetUtils"; import { getAssetPath } from "../../../../lib/assetUtils";
import type { RuleTemplateDto } from "../../../../lib/create/fetchTemplates"; import type { RuleTemplateDto } from "../../../../lib/create/fetchTemplates";
import { import {
@@ -21,14 +18,14 @@ import { TemplateChipDetailModal } from "./TemplateChipDetailModal";
export interface TemplateReviewCardProps { export interface TemplateReviewCardProps {
template: RuleTemplateDto; template: RuleTemplateDto;
/** Merged onto RuleCard `className` (e.g. final-review desktop vs mobile radius/padding). */ /** Merged onto Rule `className` (e.g. final-review desktop vs mobile radius/padding). */
ruleCardClassName?: string; ruleCardClassName?: string;
/** RuleCard size; create-flow passes `L` at/above `md`, `M` below (640px). */ /** Rule size; create-flow passes `L` at/above `md`, `M` below (640px). */
size?: RuleCardProps["size"]; size?: RuleProps["size"];
} }
/** /**
* Expanded RuleCard for template review: surfaces + icon from Figma catalog (21764-16435); * Expanded Rule for template review: surfaces + icon from Figma catalog (21764-16435);
* tag rows from API `body`. Chip clicks open a read-only detail modal per * tag rows from API `body`. Chip clicks open a read-only detail modal per
* facet group (values / communication / membership / decision-making / conflict * facet group (values / communication / membership / decision-making / conflict
* management) so reviewers can see what each chip means without editing. * management) so reviewers can see what each chip means without editing.
@@ -56,7 +53,7 @@ export function TemplateReviewCard({
setActiveChipId(chipId); setActiveChipId(chipId);
}, },
})), })),
[rawCategories], [rawCategories, setActiveChipId],
); );
const activeDetail = const activeDetail =
@@ -64,7 +61,7 @@ export function TemplateReviewCard({
return ( return (
<> <>
<RuleCard <Rule
title={template.title} title={template.title}
description={summary} description={summary}
expanded expanded
@@ -6,7 +6,7 @@ import { CheckboxView } from "./Checkbox.view";
import type { CheckboxProps } from "./Checkbox.types"; import type { CheckboxProps } from "./Checkbox.types";
/** /**
* Figma: "Control / Checkbox" (TODO(figma)). Single boolean checkbox with * Figma: "Control / Checkbox". Single boolean checkbox with
* optional label, supporting standard and inverse modes. * optional label, supporting standard and inverse modes.
*/ */
const CheckboxContainer = memo<CheckboxProps>( const CheckboxContainer = memo<CheckboxProps>(
@@ -5,7 +5,7 @@ import { CheckboxGroupView } from "./CheckboxGroup.view";
import type { CheckboxGroupProps } from "./CheckboxGroup.types"; import type { CheckboxGroupProps } from "./CheckboxGroup.types";
/** /**
* Figma: "Control / CheckboxGroup" (TODO(figma)). Group of checkboxes sharing * Figma: "Control / CheckboxGroup". Group of checkboxes sharing
* a name that emits the array of currently selected values. * a name that emits the array of currently selected values.
*/ */
const CheckboxGroupContainer = ({ const CheckboxGroupContainer = ({
@@ -5,7 +5,7 @@ import ChipView from "./Chip.view";
import type { ChipProps } from "./Chip.types"; import type { ChipProps } from "./Chip.types";
/** /**
* Figma: "Control / Chip" (TODO(figma)). Compact pill-shaped tag with * Figma: "Control / Chip". Compact pill-shaped tag with
* selectable, removable, and inline-editable (custom) states. * selectable, removable, and inline-editable (custom) states.
*/ */
const ChipContainer = memo<ChipProps>( const ChipContainer = memo<ChipProps>(
@@ -2,7 +2,7 @@ import type { IncrementerProps } from "../Incrementer/Incrementer.types";
import type { import type {
InputLabelPaletteValue, InputLabelPaletteValue,
InputLabelSizeValue, InputLabelSizeValue,
} from "../../utility/InputLabel/InputLabel.types"; } from "../../type/InputLabel/InputLabel.types";
export interface IncrementerBlockProps extends IncrementerProps { export interface IncrementerBlockProps extends IncrementerProps {
/** Label text displayed above the incrementer. */ /** Label text displayed above the incrementer. */
@@ -2,7 +2,7 @@
import { memo } from "react"; import { memo } from "react";
import Incrementer from "../Incrementer"; import Incrementer from "../Incrementer";
import InputLabel from "../../utility/InputLabel"; import InputLabel from "../../type/InputLabel";
import type { IncrementerBlockViewProps } from "./IncrementerBlock.types"; import type { IncrementerBlockViewProps } from "./IncrementerBlock.types";
function IncrementerBlockView({ function IncrementerBlockView({
@@ -5,8 +5,9 @@ import InfoMessageBoxView from "./InfoMessageBox.view";
import type { InfoMessageBoxProps } from "./InfoMessageBox.types"; import type { InfoMessageBoxProps } from "./InfoMessageBox.types";
/** /**
* Figma: "Utility / InfoMessageBox" (TODO(figma)). Bordered message box that * Figma: "Utility / InfoMessageBox"; canonical code under `controls/`.
* lists checkbox items under a title with an optional leading icon. * Bordered message box that lists checkbox items under a title with optional
* leading icon.
*/ */
const InfoMessageBoxContainer = memo<InfoMessageBoxProps>( const InfoMessageBoxContainer = memo<InfoMessageBoxProps>(
({ ({
@@ -1,7 +1,7 @@
"use client"; "use client";
import { memo } from "react"; import { memo } from "react";
import CheckboxGroup from "../../controls/CheckboxGroup"; import CheckboxGroup from "../CheckboxGroup";
import type { InfoMessageBoxViewProps } from "./InfoMessageBox.types"; import type { InfoMessageBoxViewProps } from "./InfoMessageBox.types";
/** Exclamation icon per Figma 19751:35053 vertical bar + dot inside circle; circle bg white 10% opacity, no border */ /** Exclamation icon per Figma 19751:35053 vertical bar + dot inside circle; circle bg white 10% opacity, no border */
@@ -5,7 +5,7 @@ import { InputWithCounterView } from "./InputWithCounter.view";
import type { InputWithCounterProps } from "./InputWithCounter.types"; import type { InputWithCounterProps } from "./InputWithCounter.types";
/** /**
* Figma: "Control / InputWithCounter" (TODO(figma)). * Figma: "Control / InputWithCounter".
* Single-line text input with a label, optional help glyph, and a live * Single-line text input with a label, optional help glyph, and a live
* `value.length / maxLength` counter underneath. * `value.length / maxLength` counter underneath.
*/ */
@@ -5,7 +5,7 @@ import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types"; import type { MultiSelectProps } from "./MultiSelect.types";
/** /**
* Figma: "Control / MultiSelect" (TODO(figma)). Labelled set of chips for * Figma: "Control / MultiSelect". Labelled set of chips for
* picking multiple values, with an optional add button for custom entries. * picking multiple values, with an optional add button for custom entries.
*/ */
const MultiSelectContainer = memo<MultiSelectProps>( const MultiSelectContainer = memo<MultiSelectProps>(
@@ -2,7 +2,7 @@
import { memo } from "react"; import { memo } from "react";
import Chip from "../Chip"; import Chip from "../Chip";
import InputLabel from "../../utility/InputLabel"; import InputLabel from "../../type/InputLabel";
import type { MultiSelectViewProps } from "./MultiSelect.types"; import type { MultiSelectViewProps } from "./MultiSelect.types";
function MultiSelectView({ function MultiSelectView({
@@ -88,7 +88,7 @@ function MultiSelectView({
}} }}
className={ className={
!addButtonText !addButtonText
? // Circular button with border (RuleCard style) ? // Circular button with border (Rule style)
`bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity` `bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both) : // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
`flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${ `flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${
@@ -5,7 +5,7 @@ import { RadioGroupView } from "./RadioGroup.view";
import type { RadioGroupProps } from "./RadioGroup.types"; import type { RadioGroupProps } from "./RadioGroup.types";
/** /**
* Figma: "Control / RadioGroup" (TODO(figma)). Group of radio buttons sharing * Figma: "Control / RadioGroup". Group of radio buttons sharing
* a name that emits the single currently selected value. * a name that emits the single currently selected value.
*/ */
const RadioGroupContainer = ({ const RadioGroupContainer = ({
@@ -18,7 +18,7 @@ import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types"; import type { SelectInputProps } from "./SelectInput.types";
/** /**
* Figma: "Control / SelectInput" (TODO(figma)). Custom-styled select dropdown * Figma: "Control / SelectInput". Custom-styled select dropdown
* with a labelled trigger button and floating option menu. * with a labelled trigger button and floating option menu.
*/ */
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>( const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
@@ -5,7 +5,7 @@ import { SelectOptionView } from "./SelectOption.view";
import type { SelectOptionProps } from "./SelectOption.types"; import type { SelectOptionProps } from "./SelectOption.types";
/** /**
* Figma: "Control / SelectOption" (TODO(figma)). Single option row rendered * Figma: "Control / SelectOption". Single option row rendered
* inside `SelectInput`'s dropdown menu. * inside `SelectInput`'s dropdown menu.
*/ */
const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>( const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
@@ -5,7 +5,7 @@ import { SwitchView } from "./Switch.view";
import type { SwitchProps } from "./Switch.types"; import type { SwitchProps } from "./Switch.types";
/** /**
* Figma: "Control / Switch" (TODO(figma)). Animated on/off toggle switch, * Figma: "Control / Switch". Animated on/off toggle switch,
* optionally paired with a trailing text label. * optionally paired with a trailing text label.
*/ */
const SwitchContainer = memo( const SwitchContainer = memo(
@@ -6,7 +6,7 @@ import { TextAreaView } from "./TextArea.view";
import type { TextAreaProps } from "./TextArea.types"; import type { TextAreaProps } from "./TextArea.types";
/** /**
* Figma: "Control / TextArea" (TODO(figma)). Multi-line text input with size * Figma: "Control / TextArea". Multi-line text input with size
* variants, an embedded appearance, and an optional label and help glyph. * variants, an embedded appearance, and an optional label and help glyph.
*/ */
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>( const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
@@ -6,7 +6,7 @@ import { TextInputView } from "./TextInput.view";
import type { TextInputProps } from "./TextInput.types"; import type { TextInputProps } from "./TextInput.types";
/** /**
* Figma: "Control / TextInput" (TODO(figma)). Single-line text input with size * Figma: "Control / TextInput". Single-line text input with size
* variants and managed default/active/focus/error states. * variants and managed default/active/focus/error states.
*/ */
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>( const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
@@ -5,7 +5,7 @@ import { ToggleView } from "./Toggle.view";
import type { ToggleProps } from "./Toggle.types"; import type { ToggleProps } from "./Toggle.types";
/** /**
* Figma: "Control / Toggle" (TODO(figma)). Pill-shaped toggle button with * Figma: "Control / Toggle". Pill-shaped toggle button with
* checked/unchecked states and optional leading icon and text. * checked/unchecked states and optional leading icon and text.
*/ */
const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>( const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>(
@@ -5,7 +5,7 @@ import { ToggleGroupView } from "./ToggleGroup.view";
import type { ToggleGroupProps } from "./ToggleGroup.types"; import type { ToggleGroupProps } from "./ToggleGroup.types";
/** /**
* Figma: "Control / ToggleGroup" (TODO(figma)). Segmented row of `Toggle` * Figma: "Control / ToggleGroup". Segmented row of `Toggle`
* buttons whose corner radii are shared based on position (left/middle/right). * buttons whose corner radii are shared based on position (left/middle/right).
*/ */
const ToggleGroupContainer = memo( const ToggleGroupContainer = memo(
@@ -5,7 +5,7 @@ import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types"; import type { UploadProps } from "./Upload.types";
/** /**
* Figma: "Control / Upload" (TODO(figma)). Click-to-upload tile with a label * Figma: "Control / Upload". Click-to-upload tile with a label
* and hint text used to add an image from the user's device. * and hint text used to add an image from the user's device.
*/ */
const UploadContainer = memo<UploadProps>( const UploadContainer = memo<UploadProps>(
@@ -1,7 +1,7 @@
"use client"; "use client";
import { memo } from "react"; import { memo } from "react";
import InputLabel from "../../utility/InputLabel"; import InputLabel from "../../type/InputLabel";
import type { UploadViewProps } from "./Upload.types"; import type { UploadViewProps } from "./Upload.types";
function UploadView({ function UploadView({
+1 -1
View File
@@ -1,4 +1,4 @@
import type { IconName } from "../../asset/Icon"; import type { IconName } from "../../asset/icon";
import type { import type {
ListEntryVariant, ListEntryVariant,
ListSize, ListSize,
@@ -1,4 +1,4 @@
import type { IconName } from "../../asset/Icon"; import type { IconName } from "../../asset/icon";
export const LIST_SIZE_OPTIONS = ["s", "m", "l"] as const; export const LIST_SIZE_OPTIONS = ["s", "m", "l"] as const;
export type ListSize = (typeof LIST_SIZE_OPTIONS)[number]; export type ListSize = (typeof LIST_SIZE_OPTIONS)[number];
@@ -2,7 +2,7 @@
import { memo } from "react"; import { memo } from "react";
import Link from "next/link"; import Link from "next/link";
import Icon, { type IconName } from "../../asset/Icon"; import Icon, { type IconName } from "../../asset/icon";
import Divider from "../../utility/Divider"; import Divider from "../../utility/Divider";
import { FIGMA_LIST_ENTRY_OUTER, listEntrySizeLayout } from "../listSizeLayout"; import { FIGMA_LIST_ENTRY_OUTER, listEntrySizeLayout } from "../listSizeLayout";
import type { import type {
@@ -1,41 +0,0 @@
"use client";
import { forwardRef, memo } from "react";
interface ContextMenuProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
children?: React.ReactNode;
}
const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(
({ className = "", children, ...props }, ref) => {
const menuClasses = `
bg-black
border border-[var(--color-border-default-tertiary)]
rounded-[var(--measures-radius-medium)]
shadow-lg
p-[4px]
min-w-[200px]
max-w-[300px]
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div
ref={ref}
className={menuClasses}
role="menu"
style={{ backgroundColor: "#000000" }}
{...props}
>
{children}
</div>
);
},
);
ContextMenu.displayName = "ContextMenu";
export default memo(ContextMenu);
@@ -1,27 +0,0 @@
"use client";
import { forwardRef, memo } from "react";
interface ContextMenuDividerProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
const ContextMenuDivider = forwardRef<HTMLDivElement, ContextMenuDividerProps>(
({ className = "", ...props }, ref) => {
const dividerClasses = `
border-t border-[var(--color-border-default-tertiary)]
my-1
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div ref={ref} className={dividerClasses} role="separator" {...props} />
);
},
);
ContextMenuDivider.displayName = "ContextMenuDivider";
export default memo(ContextMenuDivider);
@@ -1,36 +0,0 @@
"use client";
import { forwardRef, memo } from "react";
interface ContextMenuSectionProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string;
children?: React.ReactNode;
className?: string;
}
const ContextMenuSection = forwardRef<HTMLDivElement, ContextMenuSectionProps>(
({ title, children, className = "", ...props }, ref) => {
const sectionClasses = `
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div ref={ref} className={sectionClasses} role="group" {...props}>
{title && (
<div className="px-3 py-2">
<div className="text-[var(--color-content-default-primary)] text-sm font-medium">
{title}
</div>
</div>
)}
{children}
</div>
);
},
);
ContextMenuSection.displayName = "ContextMenuSection";
export default memo(ContextMenuSection);
@@ -1,101 +0,0 @@
"use client";
import { forwardRef, memo, useCallback } from "react";
import { ContextMenuItemView } from "./ContextMenuItem.view";
import type { ContextMenuItemProps } from "./ContextMenuItem.types";
const ContextMenuItemContainer = forwardRef<
HTMLDivElement,
ContextMenuItemProps
>(
(
{
children,
selected = false,
hasSubmenu = false,
disabled = false,
className = "",
onClick,
size: sizeProp = "medium",
...props
},
ref,
) => {
const size = sizeProp;
const getTextSize = (): string => {
switch (size) {
case "small":
return "text-[10px] leading-[14px]";
case "medium":
return "text-[14px] leading-[20px]";
case "large":
return "text-[16px] leading-[24px]";
default:
return "text-[14px] leading-[20px]";
}
};
const itemClasses = `
flex items-center justify-between
px-[8px] py-[4px]
text-[var(--color-content-default-brand-primary)]
${getTextSize()}
cursor-pointer
transition-colors duration-150
${
selected
? "bg-[var(--color-surface-default-secondary)] rounded-[var(--measures-radius-small)]"
: ""
}
${
disabled
? "opacity-50 cursor-not-allowed"
: "hover:!bg-[var(--color-surface-default-secondary)] hover:!rounded-[var(--measures-radius-small)]"
}
${className}
`
.trim()
.replace(/\s+/g, " ");
const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!disabled && onClick) {
onClick(e);
}
},
[disabled, onClick],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (!disabled && onClick) {
onClick(e);
}
}
},
[disabled, onClick],
);
return (
<ContextMenuItemView
ref={ref}
selected={selected}
hasSubmenu={hasSubmenu}
disabled={disabled}
className={className}
itemClasses={itemClasses}
handleClick={handleClick}
handleKeyDown={handleKeyDown}
{...props}
>
{children}
</ContextMenuItemView>
);
},
);
ContextMenuItemContainer.displayName = "ContextMenuItem";
export default memo(ContextMenuItemContainer);
@@ -1,27 +0,0 @@
export type ContextMenuItemSizeValue = "small" | "medium" | "large";
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
selected?: boolean;
hasSubmenu?: boolean;
disabled?: boolean;
className?: string;
onClick?: (
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => void;
/**
* Context menu item size.
*/
size?: ContextMenuItemSizeValue;
}
export interface ContextMenuItemViewProps {
children?: React.ReactNode;
selected: boolean;
hasSubmenu: boolean;
disabled: boolean;
className: string;
itemClasses: string;
handleClick: (_e: React.MouseEvent<HTMLDivElement>) => void;
handleKeyDown: (_e: React.KeyboardEvent<HTMLDivElement>) => void;
}
@@ -1,71 +0,0 @@
import { forwardRef } from "react";
import type { ContextMenuItemViewProps } from "./ContextMenuItem.types";
export const ContextMenuItemView = forwardRef<
HTMLDivElement,
ContextMenuItemViewProps
>(
(
{
children,
selected,
hasSubmenu,
disabled,
itemClasses,
handleClick,
handleKeyDown,
...props
},
ref,
) => {
return (
<div
ref={ref}
className={itemClasses}
role="menuitem"
tabIndex={disabled ? -1 : 0}
aria-current={selected ? "true" : undefined}
aria-disabled={disabled}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...props}
>
<div className="flex items-center gap-[8px]">
{selected && (
<svg
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
<span>{children}</span>
</div>
{hasSubmenu && (
<svg
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
)}
</div>
);
},
);
ContextMenuItemView.displayName = "ContextMenuItemView";
@@ -1,2 +0,0 @@
export { default } from "./ContextMenuItem.container";
export type { ContextMenuItemProps } from "./ContextMenuItem.types";
+2 -2
View File
@@ -1,8 +1,8 @@
"use client"; "use client";
import ContentLockup from "../../type/ContentLockup"; import ContentLockup from "../../type/ContentLockup";
import ModalFooter from "../../utility/ModalFooter"; import ModalFooter from "../ModalFooter";
import ModalHeader from "../../utility/ModalHeader"; import ModalHeader from "../ModalHeader";
import { CreateModalFrameView } from "./CreateModalFrame.view"; import { CreateModalFrameView } from "./CreateModalFrame.view";
import type { CreateViewProps } from "./Create.types"; import type { CreateViewProps } from "./Create.types";
@@ -78,5 +78,5 @@ export function useCreateModalA11y(
document.removeEventListener("keydown", handleTab); document.removeEventListener("keydown", handleTab);
previousActiveElementRef.current?.focus(); previousActiveElementRef.current?.focus();
}; };
}, [isOpen]); }, [dialogRef, isOpen]);
} }

Some files were not shown because too many files have changed in this diff Show More