Right rail template

This commit is contained in:
adilallo
2026-02-28 23:16:10 -07:00
parent f5bfb25f5e
commit 0799636c78
60 changed files with 1255 additions and 305 deletions
+14 -11
View File
@@ -52,8 +52,7 @@ const ADD_PLATFORM_MODALS: Record<
},
[VIDEO_MEETINGS_CARD_ID]: {
title: "Video Meetings",
description:
"Synchronous video calls for remote face-to-face interaction.",
description: "Synchronous video calls for remote face-to-face interaction.",
nextButtonText: "Add Platform",
},
};
@@ -93,12 +92,12 @@ const ADD_PLATFORM_SECTION_DEFAULTS: Record<
*/
function CreateModalSection({
title,
value,
value: _value,
onChange,
}: {
title: string;
value: string;
onChange: (value: string) => void;
onChange: (_value: string) => void;
}) {
return (
<div className="flex flex-col gap-2">
@@ -115,7 +114,7 @@ function CreateModalSection({
</div>
<TextArea
formHeader={false}
value={value}
value={_value}
onChange={(e) => onChange(e.target.value)}
size="large"
rows={6}
@@ -126,9 +125,15 @@ function CreateModalSection({
}
/** Body for any "Add platform" modal: three editable sections (TextArea only). */
function AddPlatformModalContent({ platformCardId }: { platformCardId: string }) {
function AddPlatformModalContent({
platformCardId,
}: {
platformCardId: string;
}) {
const defaults = ADD_PLATFORM_SECTION_DEFAULTS[platformCardId];
const [sectionValues, setSectionValues] = useState<Record<SectionKey, string>>(
const [sectionValues, setSectionValues] = useState<
Record<SectionKey, string>
>(
defaults ?? {
"Core Principle & Scope": "",
"Logistics, Admin & Norms": "",
@@ -168,15 +173,13 @@ const SAMPLE_CARDS = [
{
id: SIGNAL_CARD_ID,
label: "Signal",
supportText:
"Encrypted messaging for high-security, private coordination.",
supportText: "Encrypted messaging for high-security, private coordination.",
recommended: true,
},
{
id: VIDEO_MEETINGS_CARD_ID,
label: "Video Meetings",
supportText:
"Synchronous video calls for remote face-to-face interaction.",
supportText: "Synchronous video calls for remote face-to-face interaction.",
recommended: true,
},
{
+185
View File
@@ -0,0 +1,185 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import DecisionMakingSidebar from "../../components/utility/DecisionMakingSidebar";
import CardStack from "../../components/utility/CardStack";
import type { InfoMessageBoxItem } from "../../components/utility/InfoMessageBox/InfoMessageBox.types";
import type { CardStackItem } from "../../components/utility/CardStack/CardStack.types";
const SIDEBAR_TITLE = "How should conflicts be resolved?";
const SIDEBAR_DESCRIPTION = (
<>
You can also combine or <span className="underline">add</span> new
approaches to the list
</>
);
const MESSAGE_BOX_TITLE =
"Consider defining approaches to steward key resources:";
const MESSAGE_BOX_ITEMS: InfoMessageBoxItem[] = [
{ id: "amend", label: "Amend your CommunityRule" },
{ id: "finances", label: "Steward finances" },
{ id: "project", label: "Project level decisions" },
{ id: "discipline", label: "Discipline and member termination" },
];
const SAMPLE_CARDS: CardStackItem[] = [
{
id: "mediation",
label: "Mediation",
supportText:
"Collaborative work to reach a resolution that all parties can agree upon.",
recommended: true,
},
{
id: "facilitation",
label: "Facilitated dialogue",
supportText:
"Structured sessions where parties collaboratively resolve disputes.",
recommended: true,
},
{
id: "invite-only",
label: "Invite-only",
supportText: "Private discussions with selected participants.",
recommended: true,
},
{
id: "arbitration",
label: "Arbitration",
supportText: "Arbitrators are chosen specifically for a particular case.",
recommended: true,
},
{
id: "direct-dialogue",
label: "Direct dialogue",
supportText:
"Encouraging direct, respectful dialogue between those involved.",
recommended: true,
},
// Extra cards to test scrolling (only visible when "See all" is expanded)
{ id: "label-1", label: "Label", supportText: "", recommended: false },
{ id: "label-2", label: "Label", supportText: "", recommended: false },
{ id: "label-3", label: "Label", supportText: "", recommended: false },
{ id: "label-4", label: "Label", supportText: "", recommended: false },
{ id: "label-5", label: "Label", supportText: "", recommended: false },
{ id: "label-6", label: "Label", supportText: "", recommended: false },
{ id: "label-7", label: "Label", supportText: "", recommended: false },
{ id: "label-8", label: "Label", supportText: "", recommended: false },
{ id: "label-9", label: "Label", supportText: "", recommended: false },
{ id: "label-10", label: "Label", supportText: "", recommended: false },
];
/**
* Right Rail step of the create flow.
* Two-column layout (sidebar + card stack) at 640+, single column at 320-639.
*/
export default function RightRailPage() {
const [isMounted, setIsMounted] = useState(false);
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
const [messageBoxCheckedIds, setMessageBoxCheckedIds] = useState<string[]>(
[],
);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [expanded, setExpanded] = useState(false);
// Avoid flash: only use breakpoint after mount so SSR and first paint use same layout (desktop).
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional: defer layout breakpoint until after mount to prevent flash
setIsMounted(true);
}, []);
const showDesktopLayout = !isMounted || isMdOrLarger;
const handleMessageBoxCheckboxChange = useCallback(
(id: string, checked: boolean) => {
setMessageBoxCheckedIds((prev) =>
checked ? [...prev, id] : prev.filter((x) => x !== id),
);
},
[],
);
const handleCardSelect = useCallback((id: string) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
);
}, []);
const handleToggleExpand = useCallback(() => {
setExpanded((prev) => !prev);
}, []);
if (showDesktopLayout) {
return (
<div className="w-full flex flex-col items-center px-5 md:px-12">
<div className="flex gap-12 items-stretch w-full max-w-[1280px] min-w-0">
<div className="flex flex-1 flex-col justify-center gap-3 min-w-0">
<DecisionMakingSidebar
title={SIDEBAR_TITLE}
description={SIDEBAR_DESCRIPTION}
messageBoxTitle={MESSAGE_BOX_TITLE}
messageBoxItems={MESSAGE_BOX_ITEMS}
messageBoxCheckedIds={messageBoxCheckedIds}
onMessageBoxCheckboxChange={handleMessageBoxCheckboxChange}
size="L"
justification="left"
/>
</div>
<div className="flex flex-1 flex-col gap-6 items-center min-w-0">
<CardStack
cards={SAMPLE_CARDS}
selectedIds={selectedIds}
onCardSelect={handleCardSelect}
expanded={expanded}
onToggleExpand={handleToggleExpand}
hasMore={true}
toggleLabel="See all decision approaches"
showLessLabel="Show less"
title=""
description=""
layout="singleStack"
className="w-full"
/>
</div>
</div>
</div>
);
}
return (
<div className="w-full flex flex-col items-center px-5">
<div className="flex flex-col gap-6 w-full min-w-0">
<DecisionMakingSidebar
title={SIDEBAR_TITLE}
description={SIDEBAR_DESCRIPTION}
messageBoxTitle={MESSAGE_BOX_TITLE}
messageBoxItems={MESSAGE_BOX_ITEMS}
messageBoxCheckedIds={messageBoxCheckedIds}
onMessageBoxCheckboxChange={handleMessageBoxCheckboxChange}
size="M"
justification="center"
/>
<div className="flex flex-col gap-6 items-center w-full">
<CardStack
cards={SAMPLE_CARDS}
selectedIds={selectedIds}
onCardSelect={handleCardSelect}
expanded={expanded}
onToggleExpand={handleToggleExpand}
hasMore={true}
toggleLabel="See all decision approaches"
showLessLabel="Show less"
title=""
description=""
layout="singleStack"
className="w-full"
/>
</div>
</div>
</div>
);
}
+2 -2
View File
@@ -35,7 +35,7 @@ export interface CreateFlowState {
export interface CreateFlowContextValue {
state: CreateFlowState;
currentStep: CreateFlowStep | null;
updateState: (updates: Partial<CreateFlowState>) => void;
updateState: (_updates: Partial<CreateFlowState>) => void;
// Navigation handlers will be added in CR-56
}
@@ -55,7 +55,7 @@ export interface PageTemplateProps {
export interface NavigationHandlers {
goToNextStep: () => void;
goToPreviousStep: () => void;
goToStep: (step: CreateFlowStep) => void;
goToStep: (_step: CreateFlowStep) => void;
canGoNext: () => boolean;
canGoBack: () => boolean;
}
+2 -3
View File
@@ -6,7 +6,7 @@ import Upload from "../../components/controls/Upload";
/**
* Upload page for the create flow
*
*
* Displays upload functionality using HeaderLockup and Upload components.
* Responsive layout: centered at 640px+, left-aligned below 640px.
* Responsive sizing: uses L/M for HeaderLockup based on 640px breakpoint.
@@ -15,8 +15,7 @@ export default function UploadPage() {
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
const handleUploadClick = () => {
// Handle upload button click
console.log("Upload clicked");
// TODO: Handle upload button click (e.g. open file picker)
};
return (