App reorganization

This commit is contained in:
adilallo
2026-04-18 14:12:49 -06:00
parent f866d11ff8
commit e9dab04b34
288 changed files with 2698 additions and 5029 deletions
+7
View File
@@ -0,0 +1,7 @@
import type { ReactNode } from "react";
// Operator/admin dashboards (e.g. `/monitor`) intentionally render without the
// public marketing footer. Auth/access is enforced upstream.
export default function AdminLayout({ children }: { children: ReactNode }) {
return <main className="flex-1">{children}</main>;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import WebVitalsDashboard from "../../components/WebVitalsDashboard";
import WebVitalsDashboard from "../../components/sections/WebVitalsDashboard";
import TopNav from "../../components/navigation/TopNav";
import Footer from "../../components/navigation/Footer";
@@ -11,35 +11,35 @@ import { usePathname, useRouter } from "next/navigation";
import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
import CreateFlowTopNav from "../../components/utility/CreateFlowTopNav";
import { getNextStep, getStepIndex } from "./utils/flowSteps";
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
import {
createFlowStepUsesCenteredTextLayout,
createFlowStepUsesCardLayout,
} from "./utils/createFlowScreenRegistry";
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
import Button from "../components/buttons/Button";
import { buildPublishPayload } from "../../lib/create/buildPublishPayload";
import { isValidCreateFlowSaveEmail } from "../../lib/create/isValidCreateFlowSaveEmail";
import CreateFlowFooter from "../../components/utility/CreateFlowFooter";
import Button from "../../components/buttons/Button";
import { buildPublishPayload } from "../../../lib/create/buildPublishPayload";
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
import {
fetchAuthSession,
publishRule,
requestMagicLink,
} from "../../lib/create/api";
import { safeInternalPath } from "../../lib/safeInternalPath";
} from "../../../lib/create/api";
import { safeInternalPath } from "../../../lib/safeInternalPath";
import { setTransferPendingFlag } from "./utils/anonymousDraftStorage";
import { writeLastPublishedRule } from "../../lib/create/lastPublishedRule";
import { writeLastPublishedRule } from "../../../lib/create/lastPublishedRule";
import {
fetchTemplateBySlug,
type RuleTemplateDto,
} from "../../lib/create/fetchTemplates";
import messages from "../../messages/en/index";
import { useAuthModal } from "../contexts/AuthModalContext";
import { useMessages, useTranslation } from "../contexts/MessagesContext";
} from "../../../lib/create/fetchTemplates";
import messages from "../../../messages/en/index";
import { useAuthModal } from "../../contexts/AuthModalContext";
import { useMessages, useTranslation } from "../../contexts/MessagesContext";
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
import { SignedInDraftHydration } from "./SignedInDraftHydration";
import Alert from "../components/modals/Alert";
import Alert from "../../components/modals/Alert";
import {
CreateFlowDraftSaveBannerProvider,
useCreateFlowDraftSaveBanner,
@@ -9,8 +9,8 @@ import {
} from "./utils/anonymousDraftStorage";
import { useCreateFlow } from "./context/CreateFlowContext";
import { parseCreateFlowScreenFromPathname } from "./utils/flowSteps";
import { saveDraftToServer } from "../../lib/create/api";
import messages from "../../messages/en/index";
import { saveDraftToServer } from "../../../lib/create/api";
import messages from "../../../messages/en/index";
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
@@ -3,15 +3,15 @@
import { useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import type { CreateFlowState } from "./types";
import { createFlowStateHasKeys } from "../../lib/create/draftHydrationUtils";
import { createFlowStateHasKeys } from "../../../lib/create/draftHydrationUtils";
import {
clearAnonymousCreateFlowStorage,
hasTransferPendingFlag,
readAnonymousCreateFlowState,
} from "./utils/anonymousDraftStorage";
import { useCreateFlow } from "./context/CreateFlowContext";
import { fetchDraftFromServer } from "../../lib/create/api";
import messages from "../../messages/en/index";
import { fetchDraftFromServer } from "../../../lib/create/api";
import messages from "../../../messages/en/index";
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
@@ -8,8 +8,8 @@
*/
import { memo, useState } from "react";
import Chip from "../../components/controls/Chip";
import InputLabel from "../../components/utility/InputLabel";
import Chip from "../../../components/controls/Chip";
import InputLabel from "../../../components/utility/InputLabel";
export interface ApplicableScopeFieldProps {
/** Label rendered above the capsule row. */
@@ -74,9 +74,9 @@ function ApplicableScopeFieldComponent({
<Chip
key={scope}
label={scope}
state={isSelected ? "Selected" : "Disabled"}
palette="Default"
size="S"
state={isSelected ? "selected" : "disabled"}
palette="default"
size="s"
disabled={false}
onClick={() => onToggleScope(scope)}
ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`}
@@ -1,7 +1,7 @@
"use client";
import HeaderLockup from "../../components/type/HeaderLockup";
import type { HeaderLockupProps } from "../../components/type/HeaderLockup/HeaderLockup.types";
import HeaderLockup from "../../../components/type/HeaderLockup";
import type { HeaderLockupProps } from "../../../components/type/HeaderLockup/HeaderLockup.types";
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
export type CreateFlowHeaderLockupProps = Omit<HeaderLockupProps, "size"> & {
@@ -7,8 +7,8 @@
*/
import { memo, useId } from "react";
import TextArea from "../../components/controls/TextArea";
import InputLabel from "../../components/utility/InputLabel";
import TextArea from "../../../components/controls/TextArea";
import InputLabel from "../../../components/utility/InputLabel";
export interface ModalTextAreaFieldProps {
/** Label rendered above the text area. */
@@ -2,8 +2,8 @@
import { useCallback } from "react";
import type { CreateFlowState, CreateFlowStep } from "../types";
import { saveDraftToServer } from "../../../lib/create/api";
import messages from "../../../messages/en/index";
import { saveDraftToServer } from "../../../../lib/create/api";
import messages from "../../../../messages/en/index";
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import { useMediaQuery } from "../../../hooks/useMediaQuery";
/** `--breakpoint-lg` (1024px); same SSR/first-paint pattern as `useCreateFlowMdUp`. */
const CREATE_FLOW_MIN_WIDTH_LG = "(min-width: 1024px)";
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import { useMediaQuery } from "../../../hooks/useMediaQuery";
/** `--breakpoint-md` (640px); same SSR/first-paint pattern as `useCreateFlowLgUp`. */
const CREATE_FLOW_MIN_WIDTH_MD = "(min-width: 640px)";
@@ -1,15 +1,15 @@
"use client";
import { use, useEffect, useState } from "react";
import { TemplateReviewCard } from "../../../components/cards/TemplateReviewCard";
import { useTranslation } from "../../../contexts/MessagesContext";
import { TemplateReviewCard } from "../../../../components/cards/TemplateReviewCard";
import { useTranslation } from "../../../../contexts/MessagesContext";
import {
fetchTemplateBySlug,
isTemplatesFetchAborted,
type RuleTemplateDto,
} from "../../../../lib/create/fetchTemplates";
import messages from "../../../../messages/en/index";
import Alert from "../../../components/modals/Alert";
} from "../../../../../lib/create/fetchTemplates";
import messages from "../../../../../messages/en/index";
import Alert from "../../../../components/modals/Alert";
import {
CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS,
CreateFlowLockupCardStepShell,
@@ -10,13 +10,13 @@
*/
import { useState, useCallback, useMemo } from "react";
import { useMessages } from "../../../contexts/MessagesContext";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import CardStack from "../../../components/utility/CardStack";
import Create from "../../../components/modals/Create";
import InlineTextButton from "../../../components/buttons/InlineTextButton";
import CardStack from "../../../../components/utility/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
@@ -12,13 +12,13 @@
*/
import { useState, useCallback, useMemo } from "react";
import { useMessages } from "../../../contexts/MessagesContext";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import CardStack from "../../../components/utility/CardStack";
import Create from "../../../components/modals/Create";
import InlineTextButton from "../../../components/buttons/InlineTextButton";
import CardStack from "../../../../components/utility/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
@@ -12,13 +12,13 @@
*/
import { useState, useCallback, useMemo } from "react";
import { useMessages } from "../../../contexts/MessagesContext";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import CardStack from "../../../components/utility/CardStack";
import Create from "../../../components/modals/Create";
import InlineTextButton from "../../../components/buttons/InlineTextButton";
import CardStack from "../../../../components/utility/CardStack";
import Create from "../../../../components/modals/Create";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
import {
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
@@ -1,12 +1,12 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import CommunityRuleDocument from "../../../components/sections/CommunityRuleDocument";
import type { CommunityRuleDocumentSection } from "../../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
import Alert from "../../../components/modals/Alert";
import { useMessages } from "../../../contexts/MessagesContext";
import { parseDocumentSectionsForDisplay } from "../../../../lib/create/buildPublishPayload";
import { readLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
import CommunityRuleDocument from "../../../../components/sections/CommunityRuleDocument";
import type { CommunityRuleDocumentSection } from "../../../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
import Alert from "../../../../components/modals/Alert";
import { useMessages } from "../../../../contexts/MessagesContext";
import { parseDocumentSectionsForDisplay } from "../../../../../lib/create/buildPublishPayload";
import { readLastPublishedRule } from "../../../../../lib/create/lastPublishedRule";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import {
@@ -1,8 +1,8 @@
"use client";
import type { ReactNode } from "react";
import NumberedList from "../../../components/type/NumberedList";
import { useMessages } from "../../../contexts/MessagesContext";
import NumberedList from "../../../../components/type/NumberedList";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
@@ -1,7 +1,7 @@
"use client";
import RuleCard from "../../../components/cards/RuleCard";
import { useTranslation } from "../../../contexts/MessagesContext";
import RuleCard from "../../../../components/cards/RuleCard";
import { useTranslation } from "../../../../contexts/MessagesContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowLgUp } from "../../hooks/useCreateFlowLgUp";
@@ -1,9 +1,9 @@
"use client";
import { useMemo } from "react";
import RuleCard from "../../../components/cards/RuleCard";
import type { Category } from "../../../components/cards/RuleCard/RuleCard.types";
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
import RuleCard from "../../../../components/cards/RuleCard";
import type { Category } from "../../../../components/cards/RuleCard/RuleCard.types";
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import {
@@ -14,14 +14,14 @@
*/
import { useState, useCallback, useMemo } from "react";
import DecisionMakingSidebar from "../../../components/utility/DecisionMakingSidebar";
import CardStack from "../../../components/utility/CardStack";
import Create from "../../../components/modals/Create";
import IncrementerBlock from "../../../components/controls/IncrementerBlock";
import InlineTextButton from "../../../components/buttons/InlineTextButton";
import type { InfoMessageBoxItem } from "../../../components/utility/InfoMessageBox/InfoMessageBox.types";
import type { CardStackItem } from "../../../components/utility/CardStack/CardStack.types";
import { useMessages } from "../../../contexts/MessagesContext";
import DecisionMakingSidebar from "../../../../components/utility/DecisionMakingSidebar";
import CardStack from "../../../../components/utility/CardStack";
import Create from "../../../../components/modals/Create";
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import type { InfoMessageBoxItem } from "../../../../components/utility/InfoMessageBox/InfoMessageBox.types";
import type { CardStackItem } from "../../../../components/utility/CardStack/CardStack.types";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
@@ -1,9 +1,9 @@
"use client";
import { useState, useEffect } from "react";
import MultiSelect from "../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
import { useMessages } from "../../../contexts/MessagesContext";
import MultiSelect from "../../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
@@ -14,13 +14,13 @@ function chipRowsFromLabels(
return rows.map((row, i) => ({
id: String(i + 1),
label: row.label,
state: "Unselected" as const,
state: "unselected" as const,
}));
}
function selectedIdsFromOptions(options: ChipOption[]): string[] {
return options
.filter((o) => o.state === "Selected")
.filter((o) => o.state === "selected")
.map((o) => o.id);
}
@@ -37,7 +37,7 @@ export function CommunitySizeSelectScreen() {
const selected = new Set(state.selectedCommunitySizeIds ?? []);
return base.map((opt) => ({
...opt,
state: selected.has(opt.id) ? ("Selected" as const) : ("Unselected" as const),
state: selected.has(opt.id) ? ("selected" as const) : ("unselected" as const),
}));
});
@@ -45,13 +45,13 @@ export function CommunitySizeSelectScreen() {
const selected = new Set(state.selectedCommunitySizeIds ?? []);
setCommunitySizeOptions((prev) =>
prev.map((opt) =>
opt.state === "Custom"
opt.state === "custom"
? opt
: {
...opt,
state: selected.has(opt.id)
? ("Selected" as const)
: ("Unselected" as const),
? ("selected" as const)
: ("unselected" as const),
},
),
);
@@ -71,9 +71,9 @@ export function CommunitySizeSelectScreen() {
? {
...opt,
state:
opt.state === "Selected"
? ("Unselected" as const)
: ("Selected" as const),
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
);
@@ -83,7 +83,7 @@ export function CommunitySizeSelectScreen() {
const multiSelectBlock = (
<MultiSelect
formHeader={false}
size="M"
size="m"
options={communitySizeOptions}
onChipClick={handleCommunitySizeClick}
addButton={false}
@@ -7,9 +7,9 @@ import {
type Dispatch,
type SetStateAction,
} from "react";
import MultiSelect from "../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
import { useMessages } from "../../../contexts/MessagesContext";
import MultiSelect from "../../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import type { CommunityStructureChipSnapshotRow } from "../../types";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
@@ -17,7 +17,7 @@ import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoCo
function createListCustomHandlers(
setList: Dispatch<SetStateAction<ChipOption[]>>,
confirmState: "Unselected" | "Selected",
confirmState: "unselected" | "selected",
onInteraction?: () => void,
) {
const touch = () => onInteraction?.();
@@ -26,7 +26,7 @@ function createListCustomHandlers(
touch();
setList((prev) => [
...prev,
{ id: crypto.randomUUID(), label: "", state: "Custom" },
{ id: crypto.randomUUID(), label: "", state: "custom" },
]);
},
onCustomChipConfirm: (chipId: string, value: string) => {
@@ -52,7 +52,7 @@ function chipRowsFromLabels(
return rows.map((row, i) => ({
id: String(i + 1),
label: row.label,
state: "Unselected" as const,
state: "unselected" as const,
}));
}
@@ -62,20 +62,20 @@ function applySavedSelection(
): ChipOption[] {
const selected = new Set(saved ?? []);
return options.map((opt) =>
opt.state === "Custom"
opt.state === "custom"
? opt
: {
...opt,
state: selected.has(opt.id)
? ("Selected" as const)
: ("Unselected" as const),
? ("selected" as const)
: ("unselected" as const),
},
);
}
function selectedIdsFromOptions(options: ChipOption[]): string[] {
return options
.filter((o) => o.state === "Selected")
.filter((o) => o.state === "selected")
.map((o) => o.id);
}
@@ -194,7 +194,7 @@ export function CommunityStructureSelectScreen() {
() =>
createListCustomHandlers(
setOrganizationTypeOptions,
"Unselected",
"unselected",
markCreateFlowInteraction,
),
[markCreateFlowInteraction],
@@ -203,7 +203,7 @@ export function CommunityStructureSelectScreen() {
() =>
createListCustomHandlers(
setScaleOptions,
"Unselected",
"unselected",
markCreateFlowInteraction,
),
[markCreateFlowInteraction],
@@ -212,7 +212,7 @@ export function CommunityStructureSelectScreen() {
() =>
createListCustomHandlers(
setMaturityOptions,
"Unselected",
"unselected",
markCreateFlowInteraction,
),
[markCreateFlowInteraction],
@@ -258,9 +258,9 @@ export function CommunityStructureSelectScreen() {
? {
...opt,
state:
opt.state === "Selected"
? ("Unselected" as const)
: ("Selected" as const),
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
),
@@ -274,9 +274,9 @@ export function CommunityStructureSelectScreen() {
? {
...opt,
state:
opt.state === "Selected"
? ("Unselected" as const)
: ("Selected" as const),
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
),
@@ -290,9 +290,9 @@ export function CommunityStructureSelectScreen() {
? {
...opt,
state:
opt.state === "Selected"
? ("Unselected" as const)
: ("Selected" as const),
opt.state === "selected"
? ("unselected" as const)
: ("selected" as const),
}
: opt,
),
@@ -304,7 +304,7 @@ export function CommunityStructureSelectScreen() {
<MultiSelect
label={cs.organizationMultiSelect.label}
showHelpIcon
size="S"
size="s"
options={organizationTypeOptions}
onChipClick={handleOrganizationTypeClick}
{...organizationCustomHandlers}
@@ -314,7 +314,7 @@ export function CommunityStructureSelectScreen() {
<MultiSelect
label={cs.scaleMultiSelect.label}
showHelpIcon
size="S"
size="s"
options={scaleOptions}
onChipClick={handleScaleClick}
{...scaleCustomHandlers}
@@ -324,7 +324,7 @@ export function CommunityStructureSelectScreen() {
<MultiSelect
label={cs.maturityMultiSelect.label}
showHelpIcon
size="S"
size="s"
options={maturityOptions}
onChipClick={handleMaturityClick}
{...maturityCustomHandlers}
@@ -1,10 +1,10 @@
"use client";
import { useState } from "react";
import MultiSelect from "../../../components/controls/MultiSelect";
import Alert from "../../../components/modals/Alert";
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
import { useTranslation } from "../../../contexts/MessagesContext";
import MultiSelect from "../../../../components/controls/MultiSelect";
import Alert from "../../../../components/modals/Alert";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import { useTranslation } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
@@ -22,7 +22,7 @@ export function ConfirmStakeholdersScreen() {
markCreateFlowInteraction();
setStakeholderOptions((prev) => [
...prev,
{ id: crypto.randomUUID(), label: "", state: "Custom" },
{ id: crypto.randomUUID(), label: "", state: "custom" },
]);
};
@@ -30,7 +30,7 @@ export function ConfirmStakeholdersScreen() {
markCreateFlowInteraction();
setStakeholderOptions((prev) =>
prev.map((opt) =>
opt.id === chipId ? { ...opt, label: value, state: "Selected" } : opt,
opt.id === chipId ? { ...opt, label: value, state: "selected" } : opt,
),
);
};
@@ -64,7 +64,7 @@ export function ConfirmStakeholdersScreen() {
<MultiSelect
formHeader={false}
showHelpIcon={false}
size="S"
size="s"
options={stakeholderOptions}
onChipClick={handleChipClick}
onAddClick={handleAddStakeholder}
@@ -1,12 +1,12 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import MultiSelect from "../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
import TextArea from "../../../components/controls/TextArea";
import Create from "../../../components/modals/Create";
import ContentLockup from "../../../components/type/ContentLockup";
import { useMessages } from "../../../contexts/MessagesContext";
import MultiSelect from "../../../../components/controls/MultiSelect";
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
import TextArea from "../../../../components/controls/TextArea";
import Create from "../../../../components/modals/Create";
import ContentLockup from "../../../../components/type/ContentLockup";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import type { CommunityStructureChipSnapshotRow } from "../../types";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
@@ -46,7 +46,7 @@ function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[]
return presets.map((row, i) => ({
id: String(i + 1),
label: row.label,
state: "Unselected" as const,
state: "unselected" as const,
}));
}
@@ -56,20 +56,20 @@ function applySavedSelection(
): ChipOption[] {
const selected = new Set(saved ?? []);
return options.map((opt) =>
opt.state === "Custom"
opt.state === "custom"
? opt
: {
...opt,
state: selected.has(opt.id)
? ("Selected" as const)
: ("Unselected" as const),
? ("selected" as const)
: ("unselected" as const),
},
);
}
function selectedIdsFromOptions(options: ChipOption[]): string[] {
return options
.filter((o) => o.state === "Selected")
.filter((o) => o.state === "selected")
.map((o) => o.id);
}
@@ -194,7 +194,7 @@ export function CoreValuesSelectScreen() {
if (activeModalChipId && modalSession === "pending") {
const next = coreValueOptions.map((opt) =>
opt.id === activeModalChipId
? { ...opt, state: "Unselected" as const }
? { ...opt, state: "unselected" as const }
: opt,
);
persistCoreValues(next);
@@ -226,16 +226,16 @@ export function CoreValuesSelectScreen() {
const handleChipClick = (chipId: string) => {
const target = coreValueOptions.find((o) => o.id === chipId);
if (!target || target.state === "Custom") return;
if (!target || target.state === "custom") return;
const selectedCount = coreValueOptions.filter(
(o) => o.state === "Selected",
(o) => o.state === "selected",
).length;
if (target.state === "Selected") {
if (target.state === "selected") {
const next: ChipOption[] = coreValueOptions.map((opt) =>
opt.id === chipId
? { ...opt, state: "Unselected" as const }
? { ...opt, state: "unselected" as const }
: opt,
);
persistCoreValues(next);
@@ -246,7 +246,7 @@ export function CoreValuesSelectScreen() {
const next: ChipOption[] = coreValueOptions.map((opt) =>
opt.id === chipId
? { ...opt, state: "Selected" as const }
? { ...opt, state: "selected" as const }
: opt,
);
persistCoreValues(next);
@@ -259,7 +259,7 @@ export function CoreValuesSelectScreen() {
setCoreValueOptions((prev) => {
const next: ChipOption[] = [
...prev,
{ id: crypto.randomUUID(), label: "", state: "Custom" },
{ id: crypto.randomUUID(), label: "", state: "custom" },
];
queueMicrotask(() => syncCoreValuesToDraft(next));
return next;
@@ -270,17 +270,17 @@ export function CoreValuesSelectScreen() {
setCoreValueOptions((prev) => {
const withLabel = prev.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Unselected" as const }
? { ...opt, label: value, state: "unselected" as const }
: opt,
);
const selectedCount = withLabel.filter(
(o) => o.state === "Selected",
(o) => o.state === "selected",
).length;
const canSelect = selectedCount < MAX_CORE_VALUES;
const next = canSelect
? withLabel.map((opt) =>
opt.id === chipId
? { ...opt, state: "Selected" as const }
? { ...opt, state: "selected" as const }
: opt,
)
: withLabel;
@@ -343,7 +343,7 @@ export function CoreValuesSelectScreen() {
>
<MultiSelect
formHeader={false}
size="M"
size="m"
options={coreValueOptions}
onChipClick={handleChipClick}
onAddClick={addHandlers.onAddClick}
@@ -1,9 +1,9 @@
"use client";
import { useState, useEffect, type HTMLInputTypeAttribute } from "react";
import TextInput from "../../../components/controls/TextInput";
import type { HeaderLockupJustificationValue } from "../../../components/type/HeaderLockup/HeaderLockup.types";
import { useTranslation } from "../../../contexts/MessagesContext";
import TextInput from "../../../../components/controls/TextInput";
import type { HeaderLockupJustificationValue } from "../../../../components/type/HeaderLockup/HeaderLockup.types";
import { useTranslation } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
@@ -1,7 +1,7 @@
"use client";
import Upload from "../../../components/controls/Upload";
import { useMessages } from "../../../contexts/MessagesContext";
import Upload from "../../../../components/controls/Upload";
import { useMessages } from "../../../../contexts/MessagesContext";
import { useCreateFlow } from "../../context/CreateFlowContext";
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
@@ -1,5 +1,5 @@
import type { CreateFlowState } from "../types";
import { migrateLegacyCreateFlowState } from "../../../lib/create/migrateLegacyCreateFlowState";
import { migrateLegacyCreateFlowState } from "../../../../lib/create/migrateLegacyCreateFlowState";
/** Anonymous in-progress create flow (local only until magic-link transfer). */
export const CREATE_FLOW_ANONYMOUS_KEY = "create-flow-anonymous" as const;
@@ -75,15 +75,6 @@ export function hasTransferPendingFlag(): boolean {
}
}
export function clearTransferPendingFlag(): void {
if (typeof window === "undefined") return;
try {
window.localStorage.removeItem(CREATE_FLOW_TRANSFER_PENDING_KEY);
} catch {
// ignore
}
}
/** One-time cleanup of preanonymous-draft keys. */
export function clearLegacyCreateFlowKeysOnce(): void {
if (typeof window === "undefined") return;
@@ -1,4 +1,4 @@
import type { ProportionBarState } from "../../components/progress/ProportionBar/ProportionBar.types";
import type { ProportionBarState } from "../../../components/progress/ProportionBar/ProportionBar.types";
import type { CreateFlowStep } from "../types";
import { FLOW_STEP_ORDER, getStepIndex } from "./flowSteps";
@@ -2,10 +2,10 @@ import type { CreateFlowStep } from "../types";
/**
* Figma layout families for the create flow (not encoded in the URL).
* `app/create/screens/<kind>/` mirrors these names: e.g. `layoutKind: "select"` `screens/select/`,
* `app/(app)/create/screens/<kind>/` mirrors these names: e.g. `layoutKind: "select"` `screens/select/`,
* `"card"` `screens/card/` (compact card-stack frames, distinct from two-column chip selects).
*/
export type CreateFlowLayoutKind =
type CreateFlowLayoutKind =
| "informational"
| "text"
| "select"
@@ -15,7 +15,7 @@ export type CreateFlowLayoutKind =
| "right-rail"
| "completed";
export interface CreateFlowScreenDefinition {
interface CreateFlowScreenDefinition {
layoutKind: CreateFlowLayoutKind;
/** Figma node id (file Community-Rule-System), dev mode. */
figmaNodeId: string;
+8
View File
@@ -0,0 +1,8 @@
import type { ReactNode } from "react";
// Signed-in product surfaces (`/create/*`, `/login`, `/profile`) intentionally
// run without the marketing footer. Per-route chrome (e.g. CreateFlow's own
// header/footer lockup) is composed in nested layouts.
export default function AppLayout({ children }: { children: ReactNode }) {
return <main className="flex-1">{children}</main>;
}
@@ -3,18 +3,19 @@
import { Suspense } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTranslation } from "../contexts/MessagesContext";
import Login from "../components/modals/Login";
import LoginForm from "../components/modals/Login/LoginForm";
import { useTranslation } from "../../contexts/MessagesContext";
import Login from "../../components/modals/Login";
import LoginForm from "../../components/modals/Login/LoginForm";
const loginPageBgClass =
"min-h-[100dvh] bg-[var(--color-surface-inverse-brand-primary)]";
function LoginLoadingFallback() {
const t = useTranslation("pages.login");
return (
<div className={`${loginPageBgClass} flex items-center justify-center`}>
<p className="font-inter text-[14px] text-[var(--color-content-default-primary)]">
Loading
{t("loadingFallback")}
</p>
</div>
);
@@ -1,9 +1,9 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "../contexts/MessagesContext";
import Button from "../components/buttons/Button";
import { fetchAuthSession, logout } from "../../lib/create/api";
import { useTranslation } from "../../contexts/MessagesContext";
import Button from "../../components/buttons/Button";
import { fetchAuthSession, logout } from "../../../lib/create/api";
export default function ProfilePageClient() {
const t = useTranslation("pages.profile");
+103 -104
View File
@@ -14,27 +14,26 @@ let ruleCardIdCounter = 0;
interface ChipData {
id: string;
label: string;
state: "Unselected" | "Selected" | "Custom";
palette: "Default" | "Inverse";
size: "S" | "M";
state: "unselected" | "selected" | "custom";
palette: "default" | "inverse";
size: "s" | "m";
}
// MultiSelect example component with state management
function MultiSelectExample({ size }: { size: "S" | "M" }) {
function MultiSelectExample({ size }: { size: "s" | "m" }) {
const [options, setOptions] = useState<
Array<{
id: string;
label: string;
state: "Unselected" | "Selected" | "Custom";
state: "unselected" | "selected" | "custom";
}>
>([
{ id: "1", label: "1 member", state: "Unselected" },
{ id: "2", label: "2-10 members", state: "Unselected" },
{ id: "3", label: "10-24 members", state: "Unselected" },
{ id: "4", label: "24-64 members", state: "Unselected" },
{ id: "5", label: "64-128 members", state: "Unselected" },
{ id: "6", label: "125-1000 members", state: "Unselected" },
{ id: "7", label: "1000+ members", state: "Unselected" },
{ id: "1", label: "1 member", state: "unselected" },
{ id: "2", label: "2-10 members", state: "unselected" },
{ id: "3", label: "10-24 members", state: "unselected" },
{ id: "4", label: "24-64 members", state: "unselected" },
{ id: "5", label: "64-128 members", state: "unselected" },
{ id: "6", label: "125-1000 members", state: "unselected" },
{ id: "7", label: "1000+ members", state: "unselected" },
]);
const handleChipClick = (chipId: string) => {
@@ -43,7 +42,7 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
opt.id === chipId
? {
...opt,
state: opt.state === "Selected" ? "Unselected" : "Selected",
state: opt.state === "selected" ? "unselected" : "selected",
}
: opt,
),
@@ -52,14 +51,14 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
const handleAddClick = () => {
const newId = `custom-${Date.now()}`;
setOptions((prev) => [...prev, { id: newId, label: "", state: "Custom" }]);
setOptions((prev) => [...prev, { id: newId, label: "", state: "custom" }]);
};
const handleCustomConfirm = (chipId: string, value: string) => {
setOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Selected" as const }
? { ...opt, label: value, state: "selected" as const }
: opt,
),
);
@@ -72,7 +71,7 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
return (
<div className="space-y-[var(--spacing-scale-016)]">
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
{size === "S" ? "Small (S)" : "Medium (M)"}
{size === "s" ? "Small (S)" : "Medium (M)"}
</h3>
<MultiSelect
label="Label"
@@ -91,12 +90,12 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
export default function ComponentsPreview() {
const [chipStates, setChipStates] = useState<
Record<string, "Unselected" | "Selected">
Record<string, "unselected" | "selected">
>({
"default-s": "Unselected",
"default-m": "Unselected",
"inverse-s": "Unselected",
"inverse-m": "Unselected",
"default-s": "unselected",
"default-m": "unselected",
"inverse-s": "unselected",
"inverse-m": "unselected",
});
// Manage custom chips separately
@@ -104,16 +103,16 @@ export default function ComponentsPreview() {
{
id: "custom-1",
label: "",
state: "Custom",
palette: "Default",
size: "S",
state: "custom",
palette: "default",
size: "s",
},
{
id: "custom-2",
label: "",
state: "Custom",
palette: "Default",
size: "M",
state: "custom",
palette: "default",
size: "m",
},
]);
@@ -124,7 +123,7 @@ export default function ComponentsPreview() {
chipOptions: Array<{
id: string;
label: string;
state: "Unselected" | "Selected" | "Custom";
state: "unselected" | "selected" | "custom";
}>;
onChipClick?: (_categoryName: string, _chipId: string) => void;
onAddClick?: (_categoryName: string) => void;
@@ -139,11 +138,11 @@ export default function ComponentsPreview() {
{
name: "Values",
chipOptions: [
{ id: "values-1", label: "Consciousness", state: "Unselected" },
{ id: "values-2", label: "Ecology", state: "Unselected" },
{ id: "values-3", label: "Abundance", state: "Unselected" },
{ id: "values-4", label: "Art", state: "Unselected" },
{ id: "values-5", label: "Decisiveness", state: "Unselected" },
{ id: "values-1", label: "Consciousness", state: "unselected" },
{ id: "values-2", label: "Ecology", state: "unselected" },
{ id: "values-3", label: "Abundance", state: "unselected" },
{ id: "values-4", label: "Art", state: "unselected" },
{ id: "values-5", label: "Decisiveness", state: "unselected" },
],
onChipClick: (categoryName: string, chipId: string) => {
setRuleCardCategories((prev) =>
@@ -156,9 +155,9 @@ export default function ComponentsPreview() {
? {
...opt,
state:
opt.state === "Selected"
? "Unselected"
: "Selected",
opt.state === "selected"
? "unselected"
: "selected",
}
: opt,
),
@@ -176,7 +175,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: [
...cat.chipOptions,
{ id: newId, label: "", state: "Custom" },
{ id: newId, label: "", state: "custom" },
],
}
: cat,
@@ -195,7 +194,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: cat.chipOptions.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Selected" }
? { ...opt, label: value, state: "selected" }
: opt,
),
}
@@ -220,7 +219,7 @@ export default function ComponentsPreview() {
},
{
name: "Communication",
chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
chipOptions: [{ id: "comm-1", label: "Signal", state: "unselected" }],
onChipClick: (categoryName: string, chipId: string) => {
setRuleCardCategories((prev) =>
prev.map((cat) =>
@@ -232,9 +231,9 @@ export default function ComponentsPreview() {
? {
...opt,
state:
opt.state === "Selected"
? "Unselected"
: "Selected",
opt.state === "selected"
? "unselected"
: "selected",
}
: opt,
),
@@ -252,7 +251,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: [
...cat.chipOptions,
{ id: newId, label: "", state: "Custom" },
{ id: newId, label: "", state: "custom" },
],
}
: cat,
@@ -271,7 +270,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: cat.chipOptions.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Selected" }
? { ...opt, label: value, state: "selected" }
: opt,
),
}
@@ -297,7 +296,7 @@ export default function ComponentsPreview() {
{
name: "Membership",
chipOptions: [
{ id: "membership-1", label: "Open Admission", state: "Unselected" },
{ id: "membership-1", label: "Open Admission", state: "unselected" },
],
onChipClick: (categoryName: string, chipId: string) => {
setRuleCardCategories((prev) =>
@@ -310,9 +309,9 @@ export default function ComponentsPreview() {
? {
...opt,
state:
opt.state === "Selected"
? "Unselected"
: "Selected",
opt.state === "selected"
? "unselected"
: "selected",
}
: opt,
),
@@ -330,7 +329,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: [
...cat.chipOptions,
{ id: newId, label: "", state: "Custom" },
{ id: newId, label: "", state: "custom" },
],
}
: cat,
@@ -349,7 +348,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: cat.chipOptions.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Selected" }
? { ...opt, label: value, state: "selected" }
: opt,
),
}
@@ -375,8 +374,8 @@ export default function ComponentsPreview() {
{
name: "Decision-making",
chipOptions: [
{ id: "decision-1", label: "Lazy Consensus", state: "Unselected" },
{ id: "decision-2", label: "Modified Consensus", state: "Unselected" },
{ id: "decision-1", label: "Lazy Consensus", state: "unselected" },
{ id: "decision-2", label: "Modified Consensus", state: "unselected" },
],
onChipClick: (categoryName: string, chipId: string) => {
setRuleCardCategories((prev) =>
@@ -389,9 +388,9 @@ export default function ComponentsPreview() {
? {
...opt,
state:
opt.state === "Selected"
? "Unselected"
: "Selected",
opt.state === "selected"
? "unselected"
: "selected",
}
: opt,
),
@@ -409,7 +408,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: [
...cat.chipOptions,
{ id: newId, label: "", state: "Custom" },
{ id: newId, label: "", state: "custom" },
],
}
: cat,
@@ -428,7 +427,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: cat.chipOptions.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Selected" }
? { ...opt, label: value, state: "selected" }
: opt,
),
}
@@ -454,8 +453,8 @@ export default function ComponentsPreview() {
{
name: "Conflict management",
chipOptions: [
{ id: "conflict-1", label: "Code of Conduct", state: "Unselected" },
{ id: "conflict-2", label: "Restorative Justice", state: "Unselected" },
{ id: "conflict-1", label: "Code of Conduct", state: "unselected" },
{ id: "conflict-2", label: "Restorative Justice", state: "unselected" },
],
onChipClick: (categoryName: string, chipId: string) => {
setRuleCardCategories((prev) =>
@@ -468,9 +467,9 @@ export default function ComponentsPreview() {
? {
...opt,
state:
opt.state === "Selected"
? "Unselected"
: "Selected",
opt.state === "selected"
? "unselected"
: "selected",
}
: opt,
),
@@ -488,7 +487,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: [
...cat.chipOptions,
{ id: newId, label: "", state: "Custom" },
{ id: newId, label: "", state: "custom" },
],
}
: cat,
@@ -507,7 +506,7 @@ export default function ComponentsPreview() {
...cat,
chipOptions: cat.chipOptions.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Selected" }
? { ...opt, label: value, state: "selected" }
: opt,
),
}
@@ -560,45 +559,45 @@ export default function ComponentsPreview() {
<Chip
label="Small"
state={chipStates["default-s"]}
palette="Default"
size="S"
palette="default"
size="s"
onClick={() =>
setChipStates((prev) => ({
...prev,
"default-s":
prev["default-s"] === "Selected"
? "Unselected"
: "Selected",
prev["default-s"] === "selected"
? "unselected"
: "selected",
}))
}
/>
<Chip
label="Medium"
state={chipStates["default-m"]}
palette="Default"
size="M"
palette="default"
size="m"
onClick={() =>
setChipStates((prev) => ({
...prev,
"default-m":
prev["default-m"] === "Selected"
? "Unselected"
: "Selected",
prev["default-m"] === "selected"
? "unselected"
: "selected",
}))
}
/>
<Chip
label="Disabled"
state="Disabled"
palette="Default"
size="S"
state="disabled"
palette="default"
size="s"
/>
{customChips
.filter((chip) => chip.palette === "Default")
.filter((chip) => chip.palette === "default")
.map((chip) => (
<Chip
key={chip.id}
label={chip.state === "Custom" ? "" : chip.label}
label={chip.state === "custom" ? "" : chip.label}
state={chip.state}
palette={chip.palette}
size={chip.size}
@@ -607,7 +606,7 @@ export default function ComponentsPreview() {
setCustomChips((prev) =>
prev.map((c) =>
c.id === chip.id
? { ...c, label: value, state: "Selected" }
? { ...c, label: value, state: "selected" }
: c,
),
);
@@ -622,8 +621,8 @@ export default function ComponentsPreview() {
e.stopPropagation();
// Only toggle if the chip is in Selected or Unselected state (not Custom)
if (
chip.state === "Selected" ||
chip.state === "Unselected"
chip.state === "selected" ||
chip.state === "unselected"
) {
setCustomChips((prev) =>
prev.map((c) =>
@@ -631,9 +630,9 @@ export default function ComponentsPreview() {
? {
...c,
state:
c.state === "Selected"
? "Unselected"
: "Selected",
c.state === "selected"
? "unselected"
: "selected",
}
: c,
),
@@ -652,9 +651,9 @@ export default function ComponentsPreview() {
{
id: newId,
label: "",
state: "Custom",
palette: "Default",
size: "S",
state: "custom",
palette: "default",
size: "s",
},
]);
}}
@@ -698,38 +697,38 @@ export default function ComponentsPreview() {
<Chip
label="Small"
state={chipStates["inverse-s"]}
palette="Inverse"
size="S"
palette="inverse"
size="s"
onClick={() =>
setChipStates((prev) => ({
...prev,
"inverse-s":
prev["inverse-s"] === "Selected"
? "Unselected"
: "Selected",
prev["inverse-s"] === "selected"
? "unselected"
: "selected",
}))
}
/>
<Chip
label="Medium"
state={chipStates["inverse-m"]}
palette="Inverse"
size="M"
palette="inverse"
size="m"
onClick={() =>
setChipStates((prev) => ({
...prev,
"inverse-m":
prev["inverse-m"] === "Selected"
? "Unselected"
: "Selected",
prev["inverse-m"] === "selected"
? "unselected"
: "selected",
}))
}
/>
<Chip
label="Disabled"
state="Disabled"
palette="Inverse"
size="S"
state="disabled"
palette="inverse"
size="s"
/>
</div>
</div>
@@ -959,10 +958,10 @@ export default function ComponentsPreview() {
</h2>
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
{/* Small size */}
<MultiSelectExample size="S" />
<MultiSelectExample size="s" />
{/* Medium size */}
<MultiSelectExample size="M" />
<MultiSelectExample size="m" />
</div>
</section>
</div>
+7
View File
@@ -0,0 +1,7 @@
import type { ReactNode } from "react";
// Development-only previews (e.g. `/components-preview`) — no public chrome.
// Routes here are gated by NODE_ENV checks at the page level.
export default function DevLayout({ children }: { children: ReactNode }) {
return <main className="flex-1">{children}</main>;
}
@@ -1,9 +1,9 @@
import dynamic from "next/dynamic";
import { listRuleTemplatesFromDb } from "../../lib/server/ruleTemplates";
import { GOVERNANCE_TEMPLATE_HOME_SLUGS } from "../../lib/templates/governanceTemplateCatalog";
import { gridEntriesForSlugOrderWithCatalogFallback } from "../../lib/templates/templateGridPresentation";
import { listRuleTemplatesFromDb } from "../../../lib/server/ruleTemplates";
import { GOVERNANCE_TEMPLATE_HOME_SLUGS } from "../../../lib/templates/governanceTemplateCatalog";
import { gridEntriesForSlugOrderWithCatalogFallback } from "../../../lib/templates/templateGridPresentation";
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
const RuleStack = dynamic(() => import("../../components/sections/RuleStack"), {
loading: () => (
<section className="py-[var(--spacing-scale-032)] min-h-[400px]" />
),
+21
View File
@@ -0,0 +1,21 @@
import dynamic from "next/dynamic";
import type { ReactNode } from "react";
// Site footer is part of the public marketing chrome only — not rendered for
// signed-in product surfaces, admin dashboards, or dev previews. See
// `.cursor/rules/routes.mdc` for the full chrome composition map.
const Footer = dynamic(() => import("../components/navigation/Footer"), {
loading: () => (
<footer className="bg-[var(--color-surface-default-primary)] w-full min-h-[200px]" />
),
ssr: true,
});
export default function MarketingLayout({ children }: { children: ReactNode }) {
return (
<>
<main className="flex-1">{children}</main>
<Footer />
</>
);
}
+1 -1
View File
@@ -4,7 +4,7 @@ import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
import HeroBanner from "../components/sections/HeroBanner";
import AskOrganizer from "../components/sections/AskOrganizer";
import { MarketingRuleStackSection } from "./MarketingRuleStackSection";
import { MarketingRuleStackSection } from "./_components/MarketingRuleStackSection";
// Code split below-the-fold components to reduce initial bundle size
const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
+7 -18
View File
@@ -5,34 +5,26 @@ import type {
ButtonPaletteValue,
ButtonStateValue,
} from "../../../lib/propNormalization";
import {
normalizeSize,
normalizeButtonType,
normalizeButtonPalette,
} from "../../../lib/propNormalization";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
/**
* Button type (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Button type (Figma prop).
* @default "filled"
*/
buttonType?: ButtonTypeValue;
/**
* Button palette (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses "Invert", codebase uses "inverse" - both are supported.
* Button palette (Figma prop).
* @default "default"
*/
palette?: ButtonPaletteValue;
/**
* Button size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Button size.
* @default "xsmall"
*/
size?: SizeValue;
/**
* Button state (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
* Button state (Figma prop).
* @default "default"
*/
state?: ButtonStateValue;
@@ -83,12 +75,9 @@ const Button = memo<ButtonProps>(
ariaLabel,
...props
}) => {
// Normalize props
const buttonType = normalizeButtonType(typeProp, "filled");
const buttonPalette = normalizeButtonPalette(paletteProp, "default");
const size = normalizeSize(sizeProp);
// State prop is for Figma alignment - actual state is handled by CSS pseudo-classes
// We accept it for API alignment but don't use it for styling (CSS handles states)
const buttonType = typeProp ?? "filled";
const buttonPalette = paletteProp ?? "default";
const size = sizeProp;
// Map type + palette to variant string for styling (internal use only)
const getVariantFromTypeAndPalette = (
+19 -42
View File
@@ -3,80 +3,57 @@
import { memo } from "react";
import SectionNumber from "../sections/SectionNumber";
import { normalizeNumberCardSize } from "../../../lib/propNormalization";
export type NumberCardSizeValue =
| "Small"
| "Medium"
| "Large"
| "XLarge"
| "small"
| "medium"
| "large"
| "xlarge";
export type NumberCardSizeValue = "small" | "medium" | "large" | "xlarge";
interface NumberCardProps {
number: number;
text: string;
/**
* Number card size. Accepts both PascalCase (Figma default) and lowercase (case-insensitive).
* Figma uses PascalCase, codebase uses PascalCase - both are supported.
*/
size?: NumberCardSizeValue;
iconShape?: string;
iconColor?: string;
}
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
// Base classes common to all sizes
const baseClasses =
"bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
// If size prop is provided, use explicit size classes
// Otherwise, use responsive breakpoints for backward compatibility
if (sizeProp) {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeNumberCardSize(sizeProp);
// Size-specific classes
const size = sizeProp;
const sizeClasses = {
Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
Medium: "flex flex-row items-center gap-8 p-8 relative",
Large:
small: "flex flex-col items-end justify-center gap-4 p-5 relative",
medium: "flex flex-row items-center gap-8 p-8 relative",
large:
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
XLarge:
xlarge:
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
};
// Text size classes
const textClasses = {
Small:
small:
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
Medium:
medium:
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
Large:
large:
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
XLarge:
xlarge:
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
};
// Section number wrapper classes - Small doesn't need a wrapper
const sectionNumberWrapperClasses = {
Small: "relative shrink-0",
Medium: "flex justify-start flex-shrink-0",
Large: "absolute top-8 right-8",
XLarge: "absolute top-8 right-8",
small: "relative shrink-0",
medium: "flex justify-start flex-shrink-0",
large: "absolute top-8 right-8",
xlarge: "absolute top-8 right-8",
};
// Content container classes
const contentClasses = {
Small: "min-w-full relative shrink-0",
Medium: "flex-1",
Large: "absolute bottom-8 left-8 right-16",
XLarge: "absolute bottom-8 left-8 right-16",
small: "min-w-full relative shrink-0",
medium: "flex-1",
large: "absolute bottom-8 left-8 right-16",
xlarge: "absolute bottom-8 left-8 right-16",
};
// Small variant has section number as direct child, others need wrapper
if (size === "Small") {
if (size === "small") {
return (
<div className={`${baseClasses} ${sizeClasses[size]}`}>
{/* Section Number - Direct child for Small */}
@@ -3,7 +3,6 @@
import { memo } from "react";
import { RuleCardView } from "./RuleCard.view";
import type { RuleCardProps } from "./RuleCard.types";
import { normalizeRuleCardSize } from "../../../../lib/propNormalization";
declare global {
interface Window {
@@ -33,8 +32,7 @@ const RuleCardContainer = memo<RuleCardProps>(
logoAlt,
communityInitials,
}) => {
// Normalize size prop
const size = normalizeRuleCardSize(sizeProp, "L");
const size = sizeProp ?? "L";
const handleClick = () => {
// Basic analytics event tracking
@@ -21,7 +21,7 @@ export interface RuleCardProps {
className?: string;
onClick?: () => void;
expanded?: boolean;
size?: "XS" | "S" | "M" | "L" | "xs" | "s" | "m" | "l";
size?: "XS" | "S" | "M" | "L";
categories?: Category[];
logoUrl?: string;
logoAlt?: string;
@@ -261,8 +261,8 @@ export function RuleCardView({
key={categoryIndex}
label={category.name}
showHelpIcon={false}
size="S"
palette="Inverse"
size="s"
palette="inverse"
options={category.chipOptions}
onChipClick={(chipId) => {
category.onChipClick?.(category.name, chipId);
@@ -4,12 +4,10 @@ import { memo } from "react";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import ContentContainerView from "./ContentContainer.view";
import type { ContentContainerProps } from "./ContentContainer.types";
import { normalizeContentContainerSize } from "../../../../lib/propNormalization";
const ContentContainerContainer = memo<ContentContainerProps>(
({ post, width = "200px", size: sizeProp = "responsive" }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeContentContainerSize(sizeProp);
const size = sizeProp;
// Get the corresponding icon based on the same logic as background images
const getIconImage = (slug: string): string => {
const icons = [
@@ -1,17 +1,12 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentContainerSizeValue =
| "xs"
| "responsive"
| "Xs"
| "Responsive";
export type ContentContainerSizeValue = "xs" | "responsive";
export interface ContentContainerProps {
post: BlogPost;
width?: string;
/**
* Content container size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Content container size.
*/
size?: ContentContainerSizeValue;
}
@@ -4,12 +4,10 @@ import { memo } from "react";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import ContentThumbnailTemplateView from "./ContentThumbnailTemplate.view";
import type { ContentThumbnailTemplateProps } from "./ContentThumbnailTemplate.types";
import { normalizeContentThumbnailVariant } from "../../../../lib/propNormalization";
const ContentThumbnailTemplateContainer = memo<ContentThumbnailTemplateProps>(
({ post, className = "", variant: variantProp = "vertical" }) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const variant = normalizeContentThumbnailVariant(variantProp);
const variant = variantProp;
// Get article-specific background image from frontmatter
const getBackgroundImage = (
post: ContentThumbnailTemplateProps["post"],
@@ -1,17 +1,12 @@
import type { BlogPost } from "../../../../lib/content";
export type ContentThumbnailTemplateVariantValue =
| "vertical"
| "horizontal"
| "Vertical"
| "Horizontal";
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal";
export interface ContentThumbnailTemplateProps {
post: BlogPost;
className?: string;
/**
* Content thumbnail variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Content thumbnail variant.
*/
variant?: ContentThumbnailTemplateVariantValue;
slugOrder?: string[];
@@ -4,11 +4,11 @@ import { memo } from "react";
import { useComponentId } from "../../../hooks";
import { CheckboxView } from "./Checkbox.view";
import type { CheckboxProps } from "./Checkbox.types";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / Checkbox" (TODO(figma)). Single boolean checkbox with
* optional label, supporting standard and inverse modes.
*/
const CheckboxContainer = memo<CheckboxProps>(
({
checked = false,
@@ -24,9 +24,8 @@ const CheckboxContainer = memo<CheckboxProps>(
ariaLabel,
...props
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const state = normalizeState(stateProp);
const mode = modeProp;
const state = stateProp;
const isInverse = mode === "inverse";
const isStandard = mode === "standard";
@@ -2,15 +2,9 @@ import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
export interface CheckboxProps {
checked?: boolean;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
/** Mode variant (Figma: Mode). */
mode?: ModeValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
/** Visual state (Figma: State). */
state?: StateValue;
disabled?: boolean;
label?: string;
@@ -3,8 +3,11 @@
import { memo, useCallback, useId, useState } from "react";
import { CheckboxGroupView } from "./CheckboxGroup.view";
import type { CheckboxGroupProps } from "./CheckboxGroup.types";
import { normalizeMode } from "../../../../lib/propNormalization";
/**
* Figma: "Control / CheckboxGroup" (TODO(figma)). Group of checkboxes sharing
* a name that emits the array of currently selected values.
*/
const CheckboxGroupContainer = ({
name,
value,
@@ -15,8 +18,7 @@ const CheckboxGroupContainer = ({
className = "",
...props
}: CheckboxGroupProps) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const mode = modeProp;
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const groupId = name || `checkbox-group-${generatedId}`;
@@ -12,8 +12,7 @@ export interface CheckboxGroupProps {
value?: string[];
onChange?: (_data: { value: string[] }) => void;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Mode variant.
*/
mode?: ModeValue;
disabled?: boolean;
+10 -11
View File
@@ -3,18 +3,17 @@
import { memo, useState, useEffect, useRef } from "react";
import ChipView from "./Chip.view";
import type { ChipProps } from "./Chip.types";
import {
normalizeChipPalette,
normalizeChipSize,
normalizeChipState,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / Chip" (TODO(figma)). Compact pill-shaped tag with
* selectable, removable, and inline-editable (custom) states.
*/
const ChipContainer = memo<ChipProps>(
({
label,
state: stateProp = "Unselected",
palette: paletteProp = "Default",
size: sizeProp = "S",
state: stateProp = "unselected",
palette: paletteProp = "default",
size: sizeProp = "s",
className = "",
disabled,
onClick,
@@ -23,9 +22,9 @@ const ChipContainer = memo<ChipProps>(
onClose,
ariaLabel,
}) => {
const state = normalizeChipState(stateProp);
const palette = normalizeChipPalette(paletteProp);
const size = normalizeChipSize(sizeProp);
const state = stateProp;
const palette = paletteProp;
const size = sizeProp;
const isDisabled = disabled ?? state === "disabled";
const isCustom = state === "custom";
+13 -19
View File
@@ -7,38 +7,32 @@ import type {
export interface ChipProps {
label: string;
/**
* Visual state of the chip, aligned with Figma:
* - "Unselected"
* - "Selected"
* - "Disabled"
* - "Custom" (editable chips with check/close buttons)
*
* Accepts both PascalCase (Figma) and lowercase values.
* Visual state of the chip:
* - "unselected"
* - "selected"
* - "disabled"
* - "custom" (editable chips with check/close buttons)
*/
state?: ChipStateValue;
/**
* Palette of the chip, aligned with Figma:
* - "Default"
* - "Inverse"
*
* Accepts both PascalCase (Figma) and lowercase values.
* Palette of the chip:
* - "default"
* - "inverse"
*/
palette?: ChipPaletteValue;
/**
* Size of the chip, aligned with Figma:
* - "S"
* - "M"
*
* Accepts both uppercase (Figma) and lowercase values.
* Size of the chip:
* - "s"
* - "m"
*/
size?: ChipSizeValue;
className?: string;
/**
* Whether the chip should be non-interactive. Defaults to `true` when
* `state === "disabled"` to preserve historical behavior. Pass
* `disabled={false}` alongside `state="Disabled"` to render the dimmed
* `disabled={false}` alongside `state="disabled"` to render the dimmed
* "disabled" visual while keeping the chip clickable — useful for toggle
* groups where the unselected state is the disabled Figma visual.
* groups where the unselected state is the disabled visual.
*/
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
@@ -0,0 +1,18 @@
"use client";
import { memo } from "react";
import { InputWithCounterView } from "./InputWithCounter.view";
import type { InputWithCounterProps } from "./InputWithCounter.types";
/**
* Figma: "Control / InputWithCounter" (TODO(figma)).
* Single-line text input with a label, optional help glyph, and a live
* `value.length / maxLength` counter underneath.
*/
const InputWithCounterContainer = memo<InputWithCounterProps>((props) => {
return <InputWithCounterView {...props} />;
});
InputWithCounterContainer.displayName = "InputWithCounter";
export default InputWithCounterContainer;
@@ -1,2 +1,2 @@
export { InputWithCounterView as default } from "./InputWithCounter.view";
export { default } from "./InputWithCounter.container";
export type { InputWithCounterProps } from "./InputWithCounter.types";
@@ -3,17 +3,17 @@
import { memo } from "react";
import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types";
import {
normalizeMultiSelectSize,
normalizeChipPalette,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / MultiSelect" (TODO(figma)). Labelled set of chips for
* picking multiple values, with an optional add button for custom entries.
*/
const MultiSelectContainer = memo<MultiSelectProps>(
({
label,
showHelpIcon = true,
size: sizeProp = "M",
palette: paletteProp = "Default",
size: sizeProp = "m",
palette: paletteProp = "default",
options,
onChipClick,
onAddClick,
@@ -24,8 +24,8 @@ const MultiSelectContainer = memo<MultiSelectProps>(
onCustomChipClose,
className = "",
}) => {
const size = normalizeMultiSelectSize(sizeProp);
const palette = normalizeChipPalette(paletteProp);
const size = sizeProp;
const palette = paletteProp;
return (
<MultiSelectView
@@ -9,7 +9,7 @@ export interface ChipOption {
state?: ChipStateValue;
}
export type MultiSelectSizeValue = "S" | "M" | "s" | "m";
export type MultiSelectSizeValue = "s" | "m";
export interface MultiSelectProps {
/**
@@ -21,13 +21,11 @@ export interface MultiSelectProps {
*/
showHelpIcon?: boolean;
/**
* Size variant: "S" (small) or "M" (medium)
* Accepts both uppercase (Figma) and lowercase values.
* Size variant: "s" (small) or "m" (medium)
*/
size?: MultiSelectSizeValue;
/**
* Palette for chips: "Default" or "Inverse"
* Accepts both PascalCase (Figma) and lowercase values.
* Palette for chips: "default" or "inverse"
*/
palette?: ChipPaletteValue;
/**
@@ -28,7 +28,7 @@ function MultiSelectView({
? "gap-[var(--measures-spacing-200,8px)]"
: "gap-[var(--measures-spacing-300,12px)]";
const chipSize = isSmall ? "S" : "M";
const chipSize = size;
return (
<div
@@ -41,8 +41,8 @@ function MultiSelectView({
helpIcon={showHelpIcon}
asterisk={false}
helperText={false}
size={size === "s" ? "S" : "M"}
palette={palette === "inverse" ? "Inverse" : "Default"}
size={size}
palette={palette}
/>
)}
@@ -53,13 +53,12 @@ function MultiSelectView({
{options.map((option) => (
<Chip
key={option.id}
label={option.state === "Custom" ? "" : option.label}
state={option.state || "Unselected"}
palette={palette === "inverse" ? "Inverse" : "Default"}
label={option.state === "custom" ? "" : option.label}
state={option.state || "unselected"}
palette={palette}
size={chipSize}
onClick={() => {
// Only toggle if not in Custom state
if (option.state !== "Custom" && onChipClick) {
if (option.state !== "custom" && onChipClick) {
onChipClick(option.id);
}
}}
@@ -3,10 +3,6 @@
import { memo, useCallback, useId } from "react";
import { RadioButtonView } from "./RadioButton.view";
import type { RadioButtonProps } from "./RadioButton.types";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
const RadioButtonContainer = ({
checked = false,
@@ -22,9 +18,8 @@ const RadioButtonContainer = ({
ariaLabel,
className = "",
}: RadioButtonProps) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
const state = normalizeState(stateProp);
const mode = modeProp;
const state = stateProp;
// If state is "selected", it means checked in Figma terms
const normalizedState = state === "selected" || checked ? "selected" : state;
@@ -3,14 +3,12 @@ import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
export interface RadioButtonProps {
checked?: boolean;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Mode variant.
*/
mode?: ModeValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus", "selected"/"Selected" (case-insensitive).
* Visual state.
* Note: "selected" state is represented by the `checked` prop in practice.
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
state?: StateValue;
/**
@@ -3,11 +3,11 @@
import { memo, useCallback, useId } from "react";
import { RadioGroupView } from "./RadioGroup.view";
import type { RadioGroupProps } from "./RadioGroup.types";
import {
normalizeMode,
normalizeState,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / RadioGroup" (TODO(figma)). Group of radio buttons sharing
* a name that emits the single currently selected value.
*/
const RadioGroupContainer = ({
name,
value,
@@ -19,14 +19,11 @@ const RadioGroupContainer = ({
className = "",
...props
}: RadioGroupProps) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const mode = normalizeMode(modeProp);
// Normalize state, but handle "With Subtext" separately (it's represented by options with subtext)
const state =
typeof stateProp === "string" &&
(stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext")
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
: normalizeState(stateProp);
const mode = modeProp;
const state: "default" | "hover" | "focus" | "selected" =
stateProp === "With Subtext" || stateProp === "with subtext"
? "default"
: stateProp;
// Generate unique ID for accessibility if not provided
const generatedId = useId();
const groupId = name || `radio-group-${generatedId}`;
@@ -12,14 +12,12 @@ export interface RadioGroupProps {
value?: string;
onChange?: (_data: { value: string }) => void;
/**
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Mode variant.
*/
mode?: ModeValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Visual state.
* Figma also supports "With Subtext" state, which is handled via RadioOption.subtext.
* Figma uses PascalCase, codebase uses lowercase - both are supported.
*/
state?: StateValue | "With Subtext" | "with subtext";
disabled?: boolean;
@@ -16,12 +16,11 @@ import React, {
import { useClickOutside } from "../../../hooks";
import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types";
import {
normalizeState,
normalizeSmallMediumLargeSize,
normalizeLabelVariant,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / SelectInput" (TODO(figma)). Custom-styled select dropdown
* with a labelled trigger button and floating option menu.
*/
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
(
{
@@ -53,22 +52,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
const shouldShowLabel =
showLabel !== undefined ? showLabel : labelText !== undefined;
// Normalize state - handle "state5" as disabled
let normalizedState = externalStateProp;
if (normalizedState === "state5" || normalizedState === "State5") {
normalizedState = "default"; // Map to default, disabled prop handles the disabled state
normalizedState = "default";
}
const externalState = normalizeState(normalizedState);
const externalState = normalizedState;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
const _labelVariant = labelVariantProp
? normalizeLabelVariant(labelVariantProp)
: undefined;
const _size = sizeProp
? normalizeSmallMediumLargeSize(sizeProp)
: undefined;
// Mark as intentionally unused for future implementation
const _labelVariant = labelVariantProp;
const _size = sizeProp;
void _labelVariant;
void _size;
@@ -7,18 +7,8 @@ export interface SelectOptionData {
import type { StateValue } from "../../../../lib/propNormalization";
export type SelectInputLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type SelectInputSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type SelectInputLabelVariantValue = "default" | "horizontal";
export type SelectInputSizeValue = "small" | "medium" | "large";
export interface SelectInputProps {
id?: string;
@@ -33,18 +23,15 @@ export interface SelectInputProps {
*/
showLabel?: boolean;
/**
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Label variant.
*/
labelVariant?: SelectInputLabelVariantValue;
/**
* Select input size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Select input size.
*/
size?: SelectInputSizeValue;
/**
* Visual state. Accepts "default"/"Default", "active"/"Active", "focus"/"Focus", "error"/"Error", "state5"/"State5" (State5 = Disabled).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state. "state5" maps to disabled.
*/
state?: StateValue | "state5" | "State5";
/**
@@ -1,7 +1,7 @@
import React, { Children, type ReactNode } from "react";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import SelectDropdown from "./SelectDropdown";
import SelectOption from "./SelectOption";
import SelectOption from "../SelectOption";
import type { SelectOptionData } from "./SelectInput.types";
export interface SelectInputViewProps {
@@ -3,8 +3,11 @@
import { forwardRef, memo, useCallback } from "react";
import { SelectOptionView } from "./SelectOption.view";
import type { SelectOptionProps } from "./SelectOption.types";
import { normalizeContextMenuItemSize } from "../../../../../lib/propNormalization";
/**
* Figma: "Control / SelectOption" (TODO(figma)). Single option row rendered
* inside `SelectInput`'s dropdown menu.
*/
const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
(
{
@@ -18,8 +21,7 @@ const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeContextMenuItemSize(sizeProp);
const size = sizeProp;
const getTextSize = (): string => {
switch (size) {
case "small":
@@ -1,10 +1,4 @@
export type SelectOptionSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type SelectOptionSizeValue = "small" | "medium" | "large";
export interface SelectOptionProps {
children?: React.ReactNode;
@@ -15,8 +9,7 @@ export interface SelectOptionProps {
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => void;
/**
* Select option size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Select option size.
*/
size?: SelectOptionSizeValue;
}
@@ -3,8 +3,11 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { SwitchView } from "./Switch.view";
import type { SwitchProps } from "./Switch.types";
import { normalizeState } from "../../../../lib/propNormalization";
/**
* Figma: "Control / Switch" (TODO(figma)). Animated on/off toggle switch,
* optionally paired with a trailing text label.
*/
const SwitchContainer = memo(
forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
const {
@@ -18,8 +21,7 @@ const SwitchContainer = memo(
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const state = normalizeState(stateProp);
const state = stateProp;
const switchId = useId();
@@ -17,8 +17,7 @@ export interface SwitchProps extends Omit<
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: StateValue;
/**
@@ -4,13 +4,11 @@ import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { TextAreaView } from "./TextArea.view";
import type { TextAreaProps } from "./TextArea.types";
import {
normalizeInputState,
normalizeSmallMediumLargeSize,
normalizeLabelVariant,
normalizeTextAreaAppearance,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / TextArea" (TODO(figma)). Multi-line text input with size
* variants, an embedded appearance, and an optional label and help glyph.
*/
const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
{
@@ -37,11 +35,10 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const size = normalizeSmallMediumLargeSize(sizeProp);
const labelVariant = normalizeLabelVariant(labelVariantProp);
const state = normalizeInputState(stateProp);
const appearance = normalizeTextAreaAppearance(appearanceProp);
const size = sizeProp;
const labelVariant = labelVariantProp;
const state = stateProp;
const appearance = appearanceProp;
// Generate unique ID for accessibility if not provided
const { id: textareaId, labelId } = useComponentId("textarea", id);
@@ -1,41 +1,24 @@
import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextAreaSizeValue =
| "small"
| "medium"
| "large"
| "Small"
| "Medium"
| "Large";
export type TextAreaLabelVariantValue =
| "default"
| "horizontal"
| "Default"
| "Horizontal";
export type TextAreaSizeValue = "small" | "medium" | "large";
export type TextAreaLabelVariantValue = "default" | "horizontal";
export type TextAreaAppearanceValue =
| "default"
| "embedded"
| "Default"
| "Embedded";
export type TextAreaAppearanceValue = "default" | "embedded";
export interface TextAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
"size" | "onChange" | "onFocus" | "onBlur"
> {
/**
* Text area size. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Text area size.
*/
size?: TextAreaSizeValue;
/**
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Label variant.
*/
labelVariant?: TextAreaLabelVariantValue;
/**
* Visual state. Accepts "default"/"Default", "active"/"Active", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: InputStateValue;
disabled?: boolean;
@@ -4,11 +4,11 @@ import { memo, forwardRef, useState, useRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { TextInputView } from "./TextInput.view";
import type { TextInputProps } from "./TextInput.types";
import {
normalizeInputState,
normalizeTextInputSize,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / TextInput" (TODO(figma)). Single-line text input with size
* variants and managed default/active/focus/error states.
*/
const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
(
{
@@ -33,9 +33,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const externalState = normalizeInputState(externalStateProp);
const inputSize = normalizeTextInputSize(inputSizeProp);
const externalState = externalStateProp;
const inputSize = inputSizeProp;
// Generate unique ID for accessibility if not provided
const { id: inputId, labelId } = useComponentId("text-input", id);
@@ -1,19 +1,17 @@
import type { InputStateValue } from "../../../../lib/propNormalization";
export type TextInputSizeValue = "small" | "medium" | "Small" | "Medium";
export type TextInputSizeValue = "small" | "medium";
export interface TextInputProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"size" | "onChange" | "onFocus" | "onBlur"
> {
/**
* Visual state. Accepts "default"/"Default", "active"/"Active", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: InputStateValue;
/**
* Size variant. Accepts both PascalCase (Figma) and lowercase (codebase).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Size variant.
* @default "medium"
*/
inputSize?: TextInputSizeValue;
@@ -3,8 +3,11 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { ToggleView } from "./Toggle.view";
import type { ToggleProps } from "./Toggle.types";
import { normalizeState } from "../../../../lib/propNormalization";
/**
* Figma: "Control / Toggle" (TODO(figma)). Pill-shaped toggle button with
* checked/unchecked states and optional leading icon and text.
*/
const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>(
(
{
@@ -24,8 +27,7 @@ const ToggleContainer = forwardRef<HTMLButtonElement, ToggleProps>(
},
ref,
) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const state = normalizeState(stateProp);
const state = stateProp;
const toggleId = useId();
const labelId = useId();
@@ -15,8 +15,7 @@ export interface ToggleProps extends Omit<
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
disabled?: boolean;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: StateValue;
showIcon?: boolean;
@@ -3,11 +3,11 @@
import { memo, useCallback, useId, forwardRef } from "react";
import { ToggleGroupView } from "./ToggleGroup.view";
import type { ToggleGroupProps } from "./ToggleGroup.types";
import {
normalizeToggleState,
normalizeToggleGroupPosition,
} from "../../../../lib/propNormalization";
/**
* Figma: "Control / ToggleGroup" (TODO(figma)). Segmented row of `Toggle`
* buttons whose corner radii are shared based on position (left/middle/right).
*/
const ToggleGroupContainer = memo(
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
const {
@@ -23,9 +23,8 @@ const ToggleGroupContainer = memo(
...rest
} = props;
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const position = normalizeToggleGroupPosition(positionProp);
const state = normalizeToggleState(stateProp);
const position = positionProp;
const state = stateProp;
const groupId = useId();
@@ -1,12 +1,6 @@
import type { StateValue } from "../../../../lib/propNormalization";
export type ToggleGroupPositionValue =
| "left"
| "middle"
| "right"
| "Left"
| "Middle"
| "Right";
export type ToggleGroupPositionValue = "left" | "middle" | "right";
export interface ToggleGroupProps extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -15,15 +9,13 @@ export interface ToggleGroupProps extends Omit<
children?: React.ReactNode;
className?: string;
/**
* Toggle group position. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Toggle group position.
*/
position?: ToggleGroupPositionValue;
/**
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus", "selected"/"Selected" (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Visual state.
*/
state?: StateValue | "selected" | "Selected";
state?: StateValue | "selected";
showText?: boolean;
ariaLabel?: string;
onChange?: (
@@ -4,6 +4,10 @@ import { memo } from "react";
import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types";
/**
* Figma: "Control / Upload" (TODO(figma)). Click-to-upload tile with a label
* and hint text used to add an image from the user's device.
*/
const UploadContainer = memo<UploadProps>(
({
active = true,

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