App reorganization
This commit is contained in:
@@ -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,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";
|
||||
|
||||
|
||||
+13
-13
@@ -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";
|
||||
|
||||
+5
-5
@@ -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}`}
|
||||
+2
-2
@@ -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"> & {
|
||||
+2
-2
@@ -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)";
|
||||
+5
-5
@@ -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,
|
||||
+4
-4
@@ -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,
|
||||
+4
-4
@@ -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,
|
||||
+4
-4
@@ -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,
|
||||
+6
-6
@@ -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 {
|
||||
+2
-2
@@ -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";
|
||||
+2
-2
@@ -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";
|
||||
+3
-3
@@ -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 {
|
||||
+8
-8
@@ -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";
|
||||
+13
-13
@@ -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}
|
||||
+25
-25
@@ -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}
|
||||
+7
-7
@@ -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}
|
||||
+22
-22
@@ -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}
|
||||
+3
-3
@@ -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";
|
||||
+2
-2
@@ -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
-10
@@ -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 pre–anonymous-draft keys. */
|
||||
export function clearLegacyCreateFlowKeysOnce(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
+1
-1
@@ -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";
|
||||
|
||||
+3
-3
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
+4
-4
@@ -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]" />
|
||||
),
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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"), {
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+1
-3
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+5
-3
@@ -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":
|
||||
+2
-9
@@ -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
Reference in New Issue
Block a user