Confirm stakeholder template

This commit is contained in:
adilallo
2026-04-04 10:36:26 -06:00
parent 3a3e54d455
commit 5d6530e914
6 changed files with 199 additions and 29 deletions
@@ -78,7 +78,7 @@ function MultiSelectView({
/>
))}
{/* Add button - Circular button with border (not ghost) when no text, ghost style when text provided */}
{/* Add button — icon-only: bordered circle + brand icon (chips stay yellow). With label: Figma 19688:38288 — brand + icon, primary label text, no fill/border. */}
{addButton && (
<button
type="button"
@@ -91,18 +91,26 @@ function MultiSelectView({
!addButtonText
? // Circular button with border (RuleCard style)
`bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary,#141414)]" : "border-[var(--color-border-default-tertiary,#464646)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
: // Ghost button style (standalone MultiSelect)
`flex ${isSmall ? "gap-[var(--measures-spacing-050,2px)]" : "gap-[var(--measures-spacing-150,6px)]"} items-center justify-center ${isSmall ? "p-[var(--measures-spacing-200,8px)]" : "p-[var(--measures-spacing-300,12px)]"} rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
`flex items-center justify-center overflow-hidden rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity ${
isSmall
? "gap-[var(--measures-spacing-100,4px)] px-[var(--measures-spacing-300,12px)] py-[var(--measures-spacing-200,8px)]"
: "gap-[var(--measures-spacing-150,6px)] px-[var(--space-400,16px)] py-[var(--measures-spacing-300,12px)]"
}`
}
>
{/* Plus icon */}
{/* Plus icon — brand accent; selection chips keep full yellow fill separately */}
<svg
width={isSmall ? "14" : "20"}
height={isSmall ? "14" : "20"}
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`${isInverse ? "text-[var(--color-content-inverse-primary,black)]" : "text-[var(--color-content-default-brand-primary,#fefcc9)]"} shrink-0`}
className={`shrink-0 ${
!addButtonText && isInverse
? "text-[var(--color-content-inverse-primary,black)]"
: "text-[var(--color-content-default-brand-primary,#fefcc9)]"
}`}
>
<path
d="M7 3V11M3 7H11"
@@ -112,10 +120,13 @@ function MultiSelectView({
strokeLinejoin="round"
/>
</svg>
{/* Text - only show if addButtonText is provided */}
{addButtonText && (
<span
className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${isInverse ? "text-[color:var(--color-content-inverse-primary,black)]" : "text-[color:var(--color-content-default-brand-primary,#fefcc9)]"}`}
className={`font-inter font-medium ${isSmall ? "text-[length:var(--sizing-300,12px)] leading-[14px]" : "text-[length:var(--sizing-400,16px)] leading-[20px]"} ${
isInverse
? "text-[color:var(--color-content-inverse-primary,black)]"
: "text-[color:var(--color-content-default-primary,white)]"
}`}
>
{addButtonText}
</span>
+107
View File
@@ -0,0 +1,107 @@
"use client";
import { useState, useEffect } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import MultiSelect from "../../components/controls/MultiSelect";
import Alert from "../../components/modals/Alert";
import type { ChipOption } from "../../components/controls/MultiSelect/MultiSelect.types";
const TITLE =
"Do other stakeholders need to be involved in creating your community?";
const DESCRIPTION =
"Adding people at this step will invite them to see your proposed CommunityRule and make their own proposals.";
const DRAFT_TOAST_TITLE =
"Congratulations! You've drafted your CommunityRule!";
/**
* Confirm stakeholders step — stacked lockup + MultiSelect (not split columns).
* Figma: 21104-46594.
*/
export default function ConfirmStakeholdersPage() {
const [isMounted, setIsMounted] = useState(false);
const [toastDismissed, setToastDismissed] = useState(false);
const [stakeholderOptions, setStakeholderOptions] = useState<ChipOption[]>([]);
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash
setIsMounted(true);
}, []);
const effectiveMdOrLarger = !isMounted || isMdOrLarger;
const handleAddStakeholder = () => {
setStakeholderOptions((prev) => [
...prev,
{ id: crypto.randomUUID(), label: "", state: "Custom" },
]);
};
const handleCustomChipConfirm = (chipId: string, value: string) => {
setStakeholderOptions((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: "Selected" }
: opt,
),
);
};
const handleCustomChipClose = (chipId: string) => {
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
};
const handleChipClick = (chipId: string) => {
setStakeholderOptions((prev) => prev.filter((opt) => opt.id !== chipId));
};
return (
<>
<div className="w-full flex flex-col items-center px-[var(--spacing-measures-spacing-500,20px)] md:px-[var(--measures-spacing-1800,64px)] pb-28 md:pb-32">
<div className="flex w-full max-w-[640px] flex-col gap-[var(--measures-spacing-300,12px)] items-start">
<div className="flex w-full flex-col gap-[var(--measures-spacing-200,8px)] py-[12px]">
<HeaderLockup
title={TITLE}
description={DESCRIPTION}
justification="left"
size={effectiveMdOrLarger ? "L" : "M"}
/>
</div>
<MultiSelect
formHeader={false}
showHelpIcon={false}
size="S"
options={stakeholderOptions}
onChipClick={handleChipClick}
onAddClick={handleAddStakeholder}
onCustomChipConfirm={handleCustomChipConfirm}
onCustomChipClose={handleCustomChipClose}
addButton
addButtonText="Add stakeholder"
/>
</div>
</div>
{!toastDismissed && (
<div
className="fixed left-1/2 z-10 w-[min(640px,calc(100%-2.5rem))] max-w-[640px] -translate-x-1/2 bottom-[5.25rem] md:bottom-[5.5rem]"
role="status"
aria-live="polite"
>
<Alert
type="banner"
status="positive"
title={DRAFT_TOAST_TITLE}
hasLeadingIcon={false}
hasBodyText={false}
onClose={() => setToastDismissed(true)}
className="w-full !px-[var(--space-600,24px)] !py-[var(--space-400,16px)] md:!py-4"
/>
</div>
)}
</>
);
}
+3 -1
View File
@@ -87,7 +87,9 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) {
>
{currentStep === "final-review"
? "Finalize CommunityRule"
: "Next"}
: currentStep === "confirm-stakeholders"
? "Confirm Stakeholders"
: "Next"}
</Button>
) : null
}
+69 -21
View File
@@ -1,9 +1,33 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo, type Dispatch, type SetStateAction } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import HeaderLockup from "../../components/type/HeaderLockup";
import MultiSelect from "../../components/controls/MultiSelect";
import type { ChipOption } from "../../components/controls/MultiSelect/MultiSelect.types";
function createListCustomHandlers(
setList: Dispatch<SetStateAction<ChipOption[]>>,
confirmState: "Unselected" | "Selected",
) {
return {
onAddClick: () =>
setList((prev) => [
...prev,
{ id: crypto.randomUUID(), label: "", state: "Custom" },
]),
onCustomChipConfirm: (chipId: string, value: string) =>
setList((prev) =>
prev.map((opt) =>
opt.id === chipId
? { ...opt, label: value, state: confirmState }
: opt,
),
),
onCustomChipClose: (chipId: string) =>
setList((prev) => prev.filter((o) => o.id !== chipId)),
};
}
/**
* Select page for the create flow
@@ -24,30 +48,48 @@ export default function SelectPage() {
const effectiveMdOrLarger = !isMounted || isMdOrLarger;
// Sample options for MultiSelect components
const [communitySizeOptions, setCommunitySizeOptions] = useState([
{ id: "1", label: "1 member", state: "Unselected" as const },
{ id: "2", label: "2-10 members", state: "Unselected" as const },
{ id: "3", label: "10-24 members", state: "Unselected" as const },
{ id: "4", label: "24-64 members", state: "Unselected" as const },
{ id: "5", label: "64-128 members", state: "Unselected" as const },
{ id: "6", label: "125-1000 members", state: "Unselected" as const },
{ id: "7", label: "1000+ members", state: "Unselected" as const },
const [communitySizeOptions, setCommunitySizeOptions] = useState<ChipOption[]>(
[
{ 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 [organizationTypeOptions, setOrganizationTypeOptions] = useState<
ChipOption[]
>([
{ id: "1", label: "Non-profit", state: "Unselected" },
{ id: "2", label: "For-profit", state: "Unselected" },
{ id: "3", label: "Community", state: "Unselected" },
{ id: "4", label: "Educational", state: "Unselected" },
]);
const [organizationTypeOptions, setOrganizationTypeOptions] = useState([
{ id: "1", label: "Non-profit", state: "Unselected" as const },
{ id: "2", label: "For-profit", state: "Unselected" as const },
{ id: "3", label: "Community", state: "Unselected" as const },
{ id: "4", label: "Educational", state: "Unselected" as const },
const [governanceStyleOptions, setGovernanceStyleOptions] = useState<
ChipOption[]
>([
{ id: "1", label: "Democratic", state: "Unselected" },
{ id: "2", label: "Consensus", state: "Unselected" },
{ id: "3", label: "Hierarchical", state: "Unselected" },
{ id: "4", label: "Flat", state: "Unselected" },
]);
const [governanceStyleOptions, setGovernanceStyleOptions] = useState([
{ id: "1", label: "Democratic", state: "Unselected" as const },
{ id: "2", label: "Consensus", state: "Unselected" as const },
{ id: "3", label: "Hierarchical", state: "Unselected" as const },
{ id: "4", label: "Flat", state: "Unselected" as const },
]);
const communityCustomHandlers = useMemo(
() => createListCustomHandlers(setCommunitySizeOptions, "Unselected"),
[],
);
const organizationCustomHandlers = useMemo(
() => createListCustomHandlers(setOrganizationTypeOptions, "Unselected"),
[],
);
const governanceCustomHandlers = useMemo(
() => createListCustomHandlers(setGovernanceStyleOptions, "Unselected"),
[],
);
const handleCommunitySizeClick = (chipId: string) => {
setCommunitySizeOptions((prev) =>
@@ -110,6 +152,7 @@ export default function SelectPage() {
size="S"
options={communitySizeOptions}
onChipClick={handleCommunitySizeClick}
{...communityCustomHandlers}
addButton={true}
addButtonText="Add organization type"
/>
@@ -118,6 +161,7 @@ export default function SelectPage() {
size="S"
options={organizationTypeOptions}
onChipClick={handleOrganizationTypeClick}
{...organizationCustomHandlers}
addButton={true}
addButtonText="Add organization type"
/>
@@ -126,6 +170,7 @@ export default function SelectPage() {
size="S"
options={governanceStyleOptions}
onChipClick={handleGovernanceStyleClick}
{...governanceCustomHandlers}
addButton={true}
addButtonText="Add organization type"
/>
@@ -148,6 +193,7 @@ export default function SelectPage() {
size="S"
options={communitySizeOptions}
onChipClick={handleCommunitySizeClick}
{...communityCustomHandlers}
addButton={true}
addButtonText="Add organization type"
/>
@@ -156,6 +202,7 @@ export default function SelectPage() {
size="S"
options={organizationTypeOptions}
onChipClick={handleOrganizationTypeClick}
{...organizationCustomHandlers}
addButton={true}
addButtonText="Add organization type"
/>
@@ -164,6 +211,7 @@ export default function SelectPage() {
size="S"
options={governanceStyleOptions}
onChipClick={handleGovernanceStyleClick}
{...governanceCustomHandlers}
addButton={true}
addButtonText="Add organization type"
/>
+1
View File
@@ -16,6 +16,7 @@ export type CreateFlowStep =
| "review"
| "cards"
| "right-rail"
| "confirm-stakeholders"
| "final-review"
| "completed";
+1
View File
@@ -17,6 +17,7 @@ export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
"review",
"cards",
"right-rail",
"confirm-stakeholders",
"final-review",
"completed",
] as const;