From 3e3d2881f5fb1af8f4cd449706825894cad016d3 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:12:50 -0700 Subject: [PATCH] Completed template --- app/(marketing)/blog/[slug]/page.tsx | 2 +- app/(marketing)/page.tsx | 30 +-- .../ContextMenuItem/ContextMenuItem.types.ts | 8 +- app/components/asset/Icon.tsx | 13 +- app/components/asset/index.ts | 1 + app/components/asset/logo/Logo.tsx | 134 ++++++++++++ app/components/asset/logo/index.ts | 1 + app/components/buttons/Button.tsx | 15 +- app/components/cards/NumberCard.tsx | 35 ++-- .../cards/RuleCard/RuleCard.types.ts | 6 +- .../cards/RuleCard/RuleCard.view.tsx | 138 +++++++----- .../ContentContainer.types.ts | 6 +- .../ContentThumbnailTemplate.types.ts | 6 +- .../controls/Checkbox/Checkbox.container.tsx | 11 +- .../CheckboxGroup/CheckboxGroup.container.tsx | 6 +- .../CheckboxGroup/CheckboxGroup.view.tsx | 5 +- .../controls/Chip/Chip.container.tsx | 11 +- app/components/controls/Chip/Chip.types.ts | 1 - app/components/controls/Chip/Chip.view.tsx | 55 +++-- app/components/controls/Chip/index.tsx | 1 - .../MultiSelect/MultiSelect.container.tsx | 5 +- .../controls/MultiSelect/MultiSelect.types.ts | 5 +- .../controls/MultiSelect/MultiSelect.view.tsx | 12 +- .../RadioButton/RadioButton.container.tsx | 26 ++- .../controls/RadioButton/RadioButton.view.tsx | 4 +- .../RadioGroup/RadioGroup.container.tsx | 12 +- .../controls/RadioGroup/RadioGroup.view.tsx | 5 +- .../SelectInput/SelectInput.container.tsx | 30 ++- .../controls/SelectInput/SelectInput.types.ts | 14 +- .../SelectOption/SelectOption.types.ts | 8 +- .../controls/Switch/Switch.container.tsx | 2 +- .../controls/TextArea/TextArea.container.tsx | 7 +- .../controls/TextArea/TextArea.types.ts | 20 +- .../TextInput/TextInput.container.tsx | 70 ++++--- .../ToggleGroup/ToggleGroup.container.tsx | 7 +- .../controls/ToggleGroup/ToggleGroup.types.ts | 8 +- .../controls/Upload/Upload.container.tsx | 8 +- .../controls/Upload/Upload.view.tsx | 12 +- app/components/icons/Avatar.tsx | 13 +- app/components/icons/Logo.tsx | 115 ---------- .../modals/Alert/Alert.container.tsx | 5 +- app/components/modals/Create/Create.view.tsx | 2 +- .../modals/Tooltip/Tooltip.container.tsx | 8 +- app/components/navigation/Footer.tsx | 4 +- app/components/navigation/MenuBar.tsx | 2 +- .../MenuBarItem/MenuBarItem.container.tsx | 63 +++--- .../MenuBarItem/MenuBarItem.types.ts | 9 +- .../NavigationItem.container.tsx | 5 +- .../NavigationItem/NavigationItem.types.ts | 6 +- .../navigation/TopNav/TopNav.view.tsx | 22 +- .../AskOrganizer/AskOrganizer.container.tsx | 4 +- .../CommunityRuleDocument.types.ts | 16 ++ .../CommunityRuleDocument.view.tsx | 65 ++++++ .../sections/CommunityRuleDocument/index.tsx | 2 + app/components/sections/SectionHeader.tsx | 6 +- .../ContentLockup/ContentLockup.container.tsx | 5 +- .../type/ContentLockup/ContentLockup.view.tsx | 19 +- .../HeaderLockup/HeaderLockup.container.tsx | 4 + .../type/HeaderLockup/HeaderLockup.types.ts | 17 +- .../type/HeaderLockup/HeaderLockup.view.tsx | 17 +- app/components/utility/AvatarContainer.tsx | 10 +- .../utility/CardStack/CardStack.container.tsx | 8 +- .../CreateFlowFooter.view.tsx | 4 +- .../CreateFlowTopNav.container.tsx | 2 + .../CreateFlowTopNav.types.ts | 7 +- .../CreateFlowTopNav.view.tsx | 13 +- .../DecisionMakingSidebar.types.ts | 4 +- .../DecisionMakingSidebar.view.tsx | 4 +- .../utility/InfoMessageBox/index.tsx | 5 +- .../utility/InputLabel/InputLabel.types.ts | 6 +- .../utility/InputLabel/InputLabel.view.tsx | 3 +- .../utility/ModalFooter/ModalFooter.view.tsx | 16 +- app/components/utility/Tag/Tag.container.tsx | 18 +- app/create/[step]/page.tsx | 2 +- app/create/completed/page.tsx | 196 ++++++++++++++++++ app/create/context/CreateFlowContext.tsx | 8 +- app/create/final-review/page.tsx | 6 +- app/create/informational/page.tsx | 2 +- app/create/layout.tsx | 76 ++++--- app/create/select/page.tsx | 29 ++- app/create/text/page.tsx | 2 +- app/tailwind.css | 14 +- lib/assetUtils.ts | 2 +- lib/propNormalization.ts | 122 +++++++---- public/assets/{ => logo}/Logo.svg | 0 stories/buttons/Button.stories.js | 12 +- stories/buttons/Button.visual.stories.js | 8 +- stories/cards/Card.stories.js | 9 +- stories/cards/RuleCard.stories.js | 42 ++-- stories/controls/Checkbox.stories.js | 17 +- stories/controls/RadioButton.stories.js | 44 +++- stories/controls/Switch.stories.js | 3 +- stories/controls/TextArea.stories.js | 3 +- stories/controls/Upload.stories.js | 6 +- stories/icons/Logo.stories.js | 55 +++-- stories/modals/Create.stories.js | 3 +- stories/navigation/MenuBarItem.stories.js | 15 +- stories/navigation/TopNav.stories.js | 10 +- stories/type/HeaderLockup.stories.js | 3 +- stories/utility/CreateFlowFooter.stories.js | 3 +- tests/components/CompletedPage.test.tsx | 72 +++++++ tests/components/Logo.test.tsx | 15 +- tests/pages/user-journey.test.jsx | 4 +- 103 files changed, 1410 insertions(+), 622 deletions(-) create mode 100644 app/components/asset/logo/Logo.tsx create mode 100644 app/components/asset/logo/index.ts delete mode 100644 app/components/icons/Logo.tsx create mode 100644 app/components/sections/CommunityRuleDocument/CommunityRuleDocument.types.ts create mode 100644 app/components/sections/CommunityRuleDocument/CommunityRuleDocument.view.tsx create mode 100644 app/components/sections/CommunityRuleDocument/index.tsx create mode 100644 app/create/completed/page.tsx rename public/assets/{ => logo}/Logo.svg (100%) create mode 100644 tests/components/CompletedPage.test.tsx diff --git a/app/(marketing)/blog/[slug]/page.tsx b/app/(marketing)/blog/[slug]/page.tsx index bdd91e2..a2a7c21 100644 --- a/app/(marketing)/blog/[slug]/page.tsx +++ b/app/(marketing)/blog/[slug]/page.tsx @@ -189,7 +189,7 @@ export default async function BlogPostPage({ params }: PageProps) { url: "https://communityrule.com", logo: { "@type": "ImageObject", - url: "https://communityrule.com/assets/Logo.svg", + url: "https://communityrule.com/assets/logo/Logo.svg", }, }, datePublished: post.frontmatter.date, diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index 525b6ef..7daabfd 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -12,12 +12,15 @@ const LogoWall = dynamic(() => import("../components/sections/LogoWall"), { ssr: true, }); -const NumberedCards = dynamic(() => import("../components/sections/NumberedCards"), { - loading: () => ( -
- ), - ssr: true, -}); +const NumberedCards = dynamic( + () => import("../components/sections/NumberedCards"), + { + loading: () => ( +
+ ), + ssr: true, + }, +); const RuleStack = dynamic(() => import("../components/sections/RuleStack"), { loading: () => ( @@ -26,12 +29,15 @@ const RuleStack = dynamic(() => import("../components/sections/RuleStack"), { ssr: true, }); -const FeatureGrid = dynamic(() => import("../components/sections/FeatureGrid"), { - loading: () => ( -
- ), - ssr: true, -}); +const FeatureGrid = dynamic( + () => import("../components/sections/FeatureGrid"), + { + loading: () => ( +
+ ), + ssr: true, + }, +); const QuoteBlock = dynamic(() => import("../components/sections/QuoteBlock"), { loading: () => ( diff --git a/app/components/ContextMenu/ContextMenuItem/ContextMenuItem.types.ts b/app/components/ContextMenu/ContextMenuItem/ContextMenuItem.types.ts index ab57073..c78a934 100644 --- a/app/components/ContextMenu/ContextMenuItem/ContextMenuItem.types.ts +++ b/app/components/ContextMenu/ContextMenuItem/ContextMenuItem.types.ts @@ -1,4 +1,10 @@ -export type ContextMenuItemSizeValue = "small" | "medium" | "large" | "Small" | "Medium" | "Large"; +export type ContextMenuItemSizeValue = + | "small" + | "medium" + | "large" + | "Small" + | "Medium" + | "Large"; export interface ContextMenuItemProps extends React.HTMLAttributes { children?: React.ReactNode; diff --git a/app/components/asset/Icon.tsx b/app/components/asset/Icon.tsx index 105424b..6adaea1 100644 --- a/app/components/asset/Icon.tsx +++ b/app/components/asset/Icon.tsx @@ -8,7 +8,8 @@ export type IconName = "exclamation"; /** SVG import may be a React component or a module object { default: Component } (e.g. with Turbopack) */ const iconMap: Record< IconName, - React.ComponentType> | { default: React.ComponentType> } + | React.ComponentType> + | { default: React.ComponentType> } > = { exclamation: ExclamationIcon, }; @@ -31,8 +32,14 @@ function IconComponent({ if (!SvgModule) return null; // Turbopack/bundler may expose SVG as { default: Component } instead of the component directly const Svg = - typeof SvgModule === "object" && SvgModule !== null && "default" in SvgModule - ? (SvgModule as { default: React.ComponentType> }).default + typeof SvgModule === "object" && + SvgModule !== null && + "default" in SvgModule + ? ( + SvgModule as { + default: React.ComponentType>; + } + ).default : (SvgModule as React.ComponentType>); if (typeof Svg !== "function") return null; return ( diff --git a/app/components/asset/index.ts b/app/components/asset/index.ts index b97923c..47bc8b8 100644 --- a/app/components/asset/index.ts +++ b/app/components/asset/index.ts @@ -1,2 +1,3 @@ export { default as Icon } from "./Icon"; export type { IconName, IconProps } from "./Icon"; +export { default as Logo } from "./logo"; diff --git a/app/components/asset/logo/Logo.tsx b/app/components/asset/logo/Logo.tsx new file mode 100644 index 0000000..1e494f0 --- /dev/null +++ b/app/components/asset/logo/Logo.tsx @@ -0,0 +1,134 @@ +import { memo } from "react"; +import Link from "next/link"; +import { getAssetPath, ASSETS } from "../../../../lib/assetUtils"; + +interface LogoProps { + size?: + | "default" + | "footer" + | "createFlow" + | "topNavFolderTop" + | "topNavHeader"; + /** + * Visual style: default (dark on light) or inverse (e.g. black/white on teal). + * @default "default" + */ + palette?: "default" | "inverse"; + /** + * Whether to show the "CommunityRule" wordmark. + * @default true + */ + wordmark?: boolean; +} + +interface SizeConfig { + containerHeight: string; + gap: string; + textSize: string; + lineHeight: string; + iconSize: string; +} + +const Logo = memo( + ({ size = "default", palette = "default", wordmark = true }) => { + // Size configurations + const sizes: Record = { + default: { + containerHeight: "h-[41px]", + gap: "gap-[8.28px]", + textSize: "text-[21.97px]", + lineHeight: "leading-[27.05px]", + iconSize: "w-[27.05px] h-[27.05px]", + }, + footer: { + containerHeight: + "h-[41px] sm:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]", + gap: "gap-[8.28px] sm:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]", + textSize: + "text-[21.97px] sm:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]", + lineHeight: + "leading-[27.05px] sm:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]", + iconSize: + "w-[27.05px] h-[27.05px] sm:w-[calc(27.05px*1.37)] sm:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]", + }, + createFlow: { + containerHeight: "h-[30px] md:h-[41px]", + gap: "gap-[6px] md:gap-[8.28px]", + textSize: "text-[16.48px] md:text-[21.97px]", + lineHeight: "leading-[20.28px] md:leading-[27.05px]", + iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]", + }, + topNavFolderTop: { + containerHeight: + "h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]", + gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]", + textSize: + "text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]", + lineHeight: + "leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]", + iconSize: + "w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]", + }, + topNavHeader: { + containerHeight: + "h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]", + gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]", + textSize: + "text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]", + lineHeight: + "leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]", + iconSize: + "w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]", + }, + }; + + const config = sizes[size || "default"] || sizes.default; + const isInverse = palette === "inverse"; + const textColorClass = isInverse + ? "text-[var(--color-content-invert-primary)]" + : "text-[var(--color-content-default-primary)]"; + const wordmarkVisibilityClass = + size === "topNavFolderTop" || size === "topNavHeader" + ? wordmark + ? "hidden sm:block" + : "hidden" + : wordmark + ? "" + : "hidden"; + + return ( + +
+ {/* Logo Text - responsive visibility for topNav sizes */} +
+ CommunityRule +
+ + {/* Vector Icon */} + {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ + ); + }, +); + +Logo.displayName = "Logo"; + +export default Logo; diff --git a/app/components/asset/logo/index.ts b/app/components/asset/logo/index.ts new file mode 100644 index 0000000..f252704 --- /dev/null +++ b/app/components/asset/logo/index.ts @@ -0,0 +1 @@ +export { default } from "./Logo"; diff --git a/app/components/buttons/Button.tsx b/app/components/buttons/Button.tsx index 2aa2c6a..50b8c2d 100644 --- a/app/components/buttons/Button.tsx +++ b/app/components/buttons/Button.tsx @@ -109,15 +109,11 @@ const Button = memo( const variant = getVariantFromTypeAndPalette(buttonType, buttonPalette); const sizeStyles: Record = { - xsmall: - "p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]", - small: - "p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]", + xsmall: "p-[var(--spacing-scale-004)] gap-[var(--spacing-scale-002)]", + small: "p-[var(--spacing-scale-008)] gap-[var(--spacing-scale-002)]", medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]", - large: - "p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]", - xlarge: - "p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]", + large: "p-[var(--spacing-scale-012)] gap-[var(--spacing-scale-006)]", + xlarge: "p-[var(--spacing-scale-016)] gap-[var(--spacing-scale-008)]", }; const fontStyles: Record = { @@ -135,7 +131,8 @@ const Button = memo( "bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] border-[1.5px] border-transparent hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", outline: "bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-border-invert-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-default-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-invert-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[1.5px] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", - "outline-inverse": "bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-invert-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-default-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", + "outline-inverse": + "bg-transparent text-[var(--color-content-invert-primary)] border-[1.5px] border-[var(--color-border-default-primary)] hover:bg-transparent hover:text-[var(--color-content-invert-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-border-invert-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-invert-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-border-default-primary)] focus:shadow-[0_0_0px_2px_var(--color-border-invert-primary),0_0_0px_4px_var(--color-border-default-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-invert-primary)] active:border-[1.5px] active:border-[var(--color-border-invert-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-invert-secondary)] disabled:text-[var(--color-content-default-tertiary)] disabled:border-[1.5px] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", ghost: "bg-transparent text-[var(--color-content-default-brand-primary)] border-[1.5px] border-transparent hover:bg-transparent hover:text-[var(--color-content-default-primary)] hover:border-transparent hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:border-transparent focus:shadow-[0_0_0px_2px_var(--color-border-default-primary),0_0_0px_4px_var(--color-border-invert-primary)] focus:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-primary)] active:border-[var(--color-border-default-brand-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-invert-tertiary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100", "ghost-inverse": diff --git a/app/components/cards/NumberCard.tsx b/app/components/cards/NumberCard.tsx index cddb2b5..5d0e311 100644 --- a/app/components/cards/NumberCard.tsx +++ b/app/components/cards/NumberCard.tsx @@ -29,7 +29,8 @@ interface NumberCardProps { const NumberCard = memo(({ number, text, size: sizeProp }) => { // Base classes common to all sizes - const baseClasses = "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg"; + 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 @@ -40,16 +41,22 @@ const NumberCard = memo(({ number, text, 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: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", - XLarge: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", + Large: + "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", + XLarge: + "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", }; // Text size classes const textClasses = { - Small: "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]", - Medium: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", - Large: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", - XLarge: "font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]", + Small: + "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]", + Medium: + "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", + Large: + "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", + XLarge: + "font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]", }; // Section number wrapper classes - Small doesn't need a wrapper @@ -74,11 +81,9 @@ const NumberCard = memo(({ number, text, size: sizeProp }) => {
{/* Section Number - Direct child for Small */} - + {/* Card Content */} -

- {text} -

+

{text}

); } @@ -92,9 +97,7 @@ const NumberCard = memo(({ number, text, size: sizeProp }) => { {/* Card Content */}
-

- {text} -

+

{text}

); @@ -103,7 +106,9 @@ const NumberCard = memo(({ number, text, size: sizeProp }) => { // Responsive breakpoints for backward compatibility (matches original behavior) // Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl) return ( -
+
{/* Section Number - Responsive positioning */}
diff --git a/app/components/cards/RuleCard/RuleCard.types.ts b/app/components/cards/RuleCard/RuleCard.types.ts index 516f9ed..c766789 100644 --- a/app/components/cards/RuleCard/RuleCard.types.ts +++ b/app/components/cards/RuleCard/RuleCard.types.ts @@ -5,7 +5,11 @@ export interface Category { chipOptions: ChipOption[]; onChipClick?: (categoryName: string, chipId: string) => void; onAddClick?: (categoryName: string) => void; - onCustomChipConfirm?: (categoryName: string, chipId: string, value: string) => void; + onCustomChipConfirm?: ( + categoryName: string, + chipId: string, + value: string, + ) => void; onCustomChipClose?: (categoryName: string, chipId: string) => void; } diff --git a/app/components/cards/RuleCard/RuleCard.view.tsx b/app/components/cards/RuleCard/RuleCard.view.tsx index 1a0ab30..dd7af0c 100644 --- a/app/components/cards/RuleCard/RuleCard.view.tsx +++ b/app/components/cards/RuleCard/RuleCard.view.tsx @@ -28,34 +28,39 @@ export function RuleCardView({ const isMedium = size === "M"; const isSmall = size === "S"; const isExtraSmall = size === "XS"; - + // Card dimensions - use CSS classes from className if provided, otherwise use size-based logic // Check if className already has padding/gap classes - const hasResponsivePadding = className?.includes("p-[") || className?.includes("px-[") || className?.includes("py-[") || className?.includes("pt-[") || className?.includes("pb-["); + const hasResponsivePadding = + className?.includes("p-[") || + className?.includes("px-[") || + className?.includes("py-[") || + className?.includes("pt-[") || + className?.includes("pb-["); const hasResponsiveGap = className?.includes("gap-["); - + const cardPadding = hasResponsivePadding ? "" // If className has responsive padding, don't add size-based padding : isLarge || isSmall - ? "p-[24px]" - : isMedium - ? "p-[16px]" - : "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding + ? "p-[24px]" + : isMedium + ? "p-[16px]" + : "pb-[24px] pt-[12px] px-[12px]"; // XS: asymmetric padding const cardGap = expanded ? "gap-[16px]" : hasResponsiveGap - ? "" // If className has responsive gap, don't add size-based gap - : isLarge - ? "gap-[10px]" - : isMedium - ? "gap-[12px]" - : "gap-[18px]"; // XS and S: 18px gap + ? "" // If className has responsive gap, don't add size-based gap + : isLarge + ? "gap-[10px]" + : isMedium + ? "gap-[12px]" + : "gap-[18px]"; // XS and S: 18px gap const cardWidth = expanded ? isLarge ? "w-[568px]" : isMedium - ? "w-[398px]" - : "" // XS and S: no fixed width + ? "w-[398px]" + : "" // XS and S: no fixed width : ""; // Logo/Icon dimensions - use CSS responsive classes @@ -81,19 +86,21 @@ export function RuleCardView({ const descriptionClass = isLarge ? "font-inter font-medium text-[18px] leading-[24px]" : isMedium - ? "font-inter font-medium text-[14px] leading-[16px]" - : isSmall - ? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter - : "font-inter font-medium text-[12px] leading-[14px]"; // XS: 12px, medium, Inter + ? "font-inter font-medium text-[14px] leading-[16px]" + : isSmall + ? "font-inter font-medium text-[14px] leading-[16px]" // S: 14px, medium, Inter + : "font-inter font-medium text-[12px] leading-[14px]"; // XS: 12px, medium, Inter // Render logo/icon const renderLogo = () => { if (logoUrl) { // Check if it's a localhost URL or external URL that needs regular img tag - const isLocalhost = logoUrl.startsWith("http://localhost") || logoUrl.startsWith("https://localhost"); - + const isLocalhost = + logoUrl.startsWith("http://localhost") || + logoUrl.startsWith("https://localhost"); + const containerClass = `${logoContainerClass} relative rounded-full overflow-hidden mix-blend-luminosity max-[639px]:p-[16px] min-[640px]:max-[1023px]:p-[12px]`; - + if (isLocalhost) { return (
@@ -108,7 +115,7 @@ export function RuleCardView({
); } - + return (
); } - + if (icon) { return ( -
+
{icon}
); } - + if (communityInitials) { const initialsSize = ` max-[639px]:text-[16px] @@ -138,26 +147,29 @@ export function RuleCardView({ min-[1440px]:text-[36px] `; return ( -
- +
+ {communityInitials}
); } - + return null; }; - // Border radius - use CSS classes if provided via className, otherwise use size-based logic - const borderRadiusClass = className?.includes("rounded-") + const borderRadiusClass = className?.includes("rounded-") ? "" // If className already has border radius, don't add size-based one - : isExtraSmall - ? "rounded-[var(--measures-radius-200,8px)]" - : isSmall - ? "rounded-[var(--measures-radius-300,12px)]" - : "rounded-[var(--radius-measures-radius-small)]"; + : isExtraSmall + ? "rounded-[var(--measures-radius-200,8px)]" + : isSmall + ? "rounded-[var(--measures-radius-300,12px)]" + : "rounded-[var(--radius-measures-radius-small)]"; return (
{/* Outermost container with bottom border - taller to match Figma */} -
+ `} + > {/* Logo/Icon - fixed width/height, vertically centered, does not touch bottom */} {renderLogo() && ( -
+ `} + > {renderLogo()}
)} {/* Spacing between icon and title */} -
+ " + /> {/* Container with no padding and left border - extends full height to touch bottom */} {title && ( -
+ `} + > {/* Inner container for header text with padding */} -
-

- {title} -

+ `} + > +

+ {title} +

)} @@ -237,7 +261,11 @@ export function RuleCardView({ category.onAddClick?.(category.name); }} onCustomChipConfirm={(chipId, value) => { - category.onCustomChipConfirm?.(category.name, chipId, value); + category.onCustomChipConfirm?.( + category.name, + chipId, + value, + ); }} onCustomChipClose={(chipId) => { category.onCustomChipClose?.(category.name, chipId); @@ -250,11 +278,9 @@ export function RuleCardView({
)} {/* Footer: Description */} - {description && ( + {description && (
-

- {description} -

+

{description}

)} @@ -263,8 +289,8 @@ export function RuleCardView({ description && (

- {description} -

+ {description} +

) )} diff --git a/app/components/content/ContentContainer/ContentContainer.types.ts b/app/components/content/ContentContainer/ContentContainer.types.ts index 18716f8..7fbe41d 100644 --- a/app/components/content/ContentContainer/ContentContainer.types.ts +++ b/app/components/content/ContentContainer/ContentContainer.types.ts @@ -1,6 +1,10 @@ import type { BlogPost } from "../../../../lib/content"; -export type ContentContainerSizeValue = "xs" | "responsive" | "Xs" | "Responsive"; +export type ContentContainerSizeValue = + | "xs" + | "responsive" + | "Xs" + | "Responsive"; export interface ContentContainerProps { post: BlogPost; diff --git a/app/components/content/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts b/app/components/content/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts index 3c8fe75..425ee5b 100644 --- a/app/components/content/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts +++ b/app/components/content/ContentThumbnailTemplate/ContentThumbnailTemplate.types.ts @@ -1,6 +1,10 @@ import type { BlogPost } from "../../../../lib/content"; -export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal" | "Vertical" | "Horizontal"; +export type ContentThumbnailTemplateVariantValue = + | "vertical" + | "horizontal" + | "Vertical" + | "Horizontal"; export interface ContentThumbnailTemplateProps { post: BlogPost; diff --git a/app/components/controls/Checkbox/Checkbox.container.tsx b/app/components/controls/Checkbox/Checkbox.container.tsx index 7650f3a..df9e74d 100644 --- a/app/components/controls/Checkbox/Checkbox.container.tsx +++ b/app/components/controls/Checkbox/Checkbox.container.tsx @@ -4,7 +4,10 @@ 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"; +import { + normalizeMode, + normalizeState, +} from "../../../../lib/propNormalization"; const CheckboxContainer = memo( ({ @@ -24,7 +27,7 @@ const CheckboxContainer = memo( // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) const mode = normalizeMode(modeProp); const state = normalizeState(stateProp); - + const isInverse = mode === "inverse"; const isStandard = mode === "standard"; @@ -43,7 +46,9 @@ const CheckboxContainer = memo( transition-all duration-200 ease-in-out - `.trim().replace(/\s+/g, " "); + ` + .trim() + .replace(/\s+/g, " "); // Get box styles based on state and checked status per Figma designs const getBoxStyles = (): string => { diff --git a/app/components/controls/CheckboxGroup/CheckboxGroup.container.tsx b/app/components/controls/CheckboxGroup/CheckboxGroup.container.tsx index 2fac6e8..ea96562 100644 --- a/app/components/controls/CheckboxGroup/CheckboxGroup.container.tsx +++ b/app/components/controls/CheckboxGroup/CheckboxGroup.container.tsx @@ -22,8 +22,10 @@ const CheckboxGroupContainer = ({ const groupId = name || `checkbox-group-${generatedId}`; // Internal state to track checked values (only used if value prop is not provided) - const [internalCheckedValues, setInternalCheckedValues] = useState([]); - + const [internalCheckedValues, setInternalCheckedValues] = useState( + [], + ); + // Use controlled value if provided, otherwise use internal state const checkedValues = value !== undefined ? value : internalCheckedValues; diff --git a/app/components/controls/CheckboxGroup/CheckboxGroup.view.tsx b/app/components/controls/CheckboxGroup/CheckboxGroup.view.tsx index 8c2d7e0..b43b6f4 100644 --- a/app/components/controls/CheckboxGroup/CheckboxGroup.view.tsx +++ b/app/components/controls/CheckboxGroup/CheckboxGroup.view.tsx @@ -23,10 +23,7 @@ export function CheckboxGroupView({ // If there's subtext, render checkbox without label and handle layout separately if (option.subtext) { return ( -
+
( } }, [isCustom]); - const handleCheck = (value: string, event: React.MouseEvent) => { + const handleCheck = ( + value: string, + event: React.MouseEvent, + ) => { if (onCheck && value.trim()) { onCheck(value.trim(), event); // Reset input after successful check @@ -63,7 +66,10 @@ const ChipContainer = memo( const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter" && inputValue.trim() && onCheck) { event.preventDefault(); - handleCheck(inputValue.trim(), event as unknown as React.MouseEvent); + handleCheck( + inputValue.trim(), + event as unknown as React.MouseEvent, + ); } else if (event.key === "Escape" && onClose) { event.preventDefault(); handleClose(event as unknown as React.MouseEvent); @@ -95,4 +101,3 @@ const ChipContainer = memo( ChipContainer.displayName = "Chip"; export default ChipContainer; - diff --git a/app/components/controls/Chip/Chip.types.ts b/app/components/controls/Chip/Chip.types.ts index 13ca6ed..dab6332 100644 --- a/app/components/controls/Chip/Chip.types.ts +++ b/app/components/controls/Chip/Chip.types.ts @@ -68,4 +68,3 @@ export interface ChipViewProps { inputRef?: React.RefObject; ariaLabel?: string; } - diff --git a/app/components/controls/Chip/Chip.view.tsx b/app/components/controls/Chip/Chip.view.tsx index 2402994..a393411 100644 --- a/app/components/controls/Chip/Chip.view.tsx +++ b/app/components/controls/Chip/Chip.view.tsx @@ -42,32 +42,26 @@ function ChipView({ // Palette + state styling based on Figma examples // Use consistent border width to prevent layout shift const borderWidth = isSmall ? "border-[1.25px]" : "border-2"; - - let background = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]"; - let border = - `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`; + + let background = + "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]"; + let border = `${borderWidth} border-[var(--color-border-default-tertiary,#464646)]`; let textColor = "text-[color:var(--color-content-default-brand-primary,#fefcc9)]"; if (isDefault) { if (state === "custom") { - background = - "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom + background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background for custom border = "border-none"; - textColor = - "text-[color:var(--color-content-default-tertiary,#b4b4b4)]"; + textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]"; } else if (state === "disabled") { - background = - "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background + background = "bg-[var(--color-surface-default-secondary,#141414)]"; // dark background border = "border-none"; - textColor = - "text-[color:var(--color-content-default-tertiary,#b4b4b4)]"; + textColor = "text-[color:var(--color-content-default-tertiary,#b4b4b4)]"; } else if (isSelected) { - background = - "bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected + background = "bg-[var(--color-surface-inverse-brandaccent,#fdfaa8)]"; // yellow selected border = `${borderWidth} border-[var(--color-border-default-brand-primary,#fdfaa8)]`; - textColor = - "text-[color:var(--color-content-inverse-primary,black)]"; + textColor = "text-[color:var(--color-content-inverse-primary,black)]"; } else { // Unselected default background = @@ -78,24 +72,20 @@ function ChipView({ } } else if (isInverse) { if (state === "disabled") { - background = - "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]"; + background = "bg-[var(--color-surface-inverse-tertiary,#d2d2d2)]"; border = "border-none"; - textColor = - "text-[color:var(--color-content-inverse-primary,black)]"; + textColor = "text-[color:var(--color-content-inverse-primary,black)]"; } else if (isSelected) { background = "bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.05))]"; border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`; - textColor = - "text-[color:var(--color-content-inverse-primary,black)]"; + textColor = "text-[color:var(--color-content-inverse-primary,black)]"; } else { // Unselected / custom inverse background = "bg-[var(--color-surface-default-transparent,rgba(0,0,0,0))]"; border = `${borderWidth} border-[var(--color-border-default-primary,#141414)]`; - textColor = - "text-[color:var(--color-content-inverse-primary,black)]"; + textColor = "text-[color:var(--color-content-inverse-primary,black)]"; } } @@ -134,7 +124,9 @@ function ChipView({ .filter(Boolean) .join(" "); - const handleClick = (event: React.MouseEvent) => { + const handleClick = ( + event: React.MouseEvent, + ) => { if (isDisabled) { event.preventDefault(); return; @@ -162,7 +154,9 @@ function ChipView({ }} {...sharedA11y} > -
+
{/* Check button */} {onCheck && ( {/* Description text */} -
+

Add images, PDFs, and other files to the policy

diff --git a/app/components/icons/Avatar.tsx b/app/components/icons/Avatar.tsx index f3bc8d4..3a85066 100644 --- a/app/components/icons/Avatar.tsx +++ b/app/components/icons/Avatar.tsx @@ -1,7 +1,15 @@ import { memo } from "react"; import { normalizeSize } from "../../../lib/propNormalization"; -export type AvatarSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge"; +export type AvatarSizeValue = + | "small" + | "medium" + | "large" + | "xlarge" + | "Small" + | "Medium" + | "Large" + | "XLarge"; interface AvatarProps extends React.ImgHTMLAttributes { src: string; @@ -19,7 +27,8 @@ const Avatar = memo( // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) const size = normalizeSize(sizeProp, "small"); const sizeStyles: Record = { - small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)] border-[1.5px] border-[#FFFFFF4D] border-solid", + small: + "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)] border-[1.5px] border-[#FFFFFF4D] border-solid", medium: "w-[var(--spacing-scale-018)] h-[var(--spacing-scale-018)]", large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]", xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]", diff --git a/app/components/icons/Logo.tsx b/app/components/icons/Logo.tsx deleted file mode 100644 index e932010..0000000 --- a/app/components/icons/Logo.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { memo } from "react"; -import Link from "next/link"; -import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; - -interface LogoProps { - size?: - | "default" - | "footer" - | "createFlow" - | "topNavFolderTop" - | "topNavHeader"; - showText?: boolean; -} - -interface SizeConfig { - containerHeight: string; - gap: string; - textSize: string; - lineHeight: string; - iconSize: string; -} - -const Logo = memo(({ size = "default", showText = true }) => { - // Size configurations - const sizes: Record = { - default: { - containerHeight: "h-[41px]", - gap: "gap-[8.28px]", - textSize: "text-[21.97px]", - lineHeight: "leading-[27.05px]", - iconSize: "w-[27.05px] h-[27.05px]", - }, - footer: { - containerHeight: "h-[41px] sm:h-[calc(40px*1.37)] lg:h-[calc(40px*2.05)]", - gap: "gap-[8.28px] sm:gap-[calc(8px*1.37)] lg:gap-[calc(8px*2.05)]", - textSize: "text-[21.97px] sm:text-[calc(21.97px*1.37)] lg:text-[calc(21.97px*2.05)]", - lineHeight: "leading-[27.05px] sm:leading-[calc(27.05px*1.37)] lg:leading-[calc(27.05px*2.05)]", - iconSize: "w-[27.05px] h-[27.05px] sm:w-[calc(27.05px*1.37)] sm:h-[calc(27.05px*1.37)] lg:w-[calc(27.05px*2.05)] lg:h-[calc(27.05px*2.05)]", - }, - createFlow: { - containerHeight: "h-[30px] md:h-[41px]", - gap: "gap-[6px] md:gap-[8.28px]", - textSize: "text-[16.48px] md:text-[21.97px]", - lineHeight: "leading-[20.28px] md:leading-[27.05px]", - iconSize: "w-[20.28px] h-[20.28px] md:w-[27.05px] md:h-[27.05px]", - }, - topNavFolderTop: { - containerHeight: "h-[14.11px] sm:h-[21.06px] md:h-[32.24px] lg:h-[28px] xl:h-[36px]", - gap: "gap-0 sm:gap-[3.19px] md:gap-[4.89px] lg:gap-[6.55px] xl:gap-[8.64px]", - textSize: "text-[11.57px] sm:text-[11.69px] md:text-[17.89px] lg:text-[21.97px] xl:text-[29.01px]", - lineHeight: "leading-[14.24px] sm:leading-[14.39px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[35.7px]", - iconSize: "w-[14.11px] h-[14.11px] sm:w-[14.39px] sm:h-[14.39px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[35.7px] xl:h-[35.7px]", - }, - topNavHeader: { - containerHeight: "h-[20.85px] sm:h-[20.85px] md:h-[17.91px] lg:h-[28px] xl:h-[34px]", - gap: "gap-0 sm:gap-[4.21px] md:gap-[6.51px] lg:gap-[6.55px] xl:gap-[8.19px]", - textSize: "text-[11.57px] sm:text-[11.57px] md:text-[17.89px] lg:text-[21.97px] xl:text-[27.47px]", - lineHeight: "leading-[14.24px] sm:leading-[14.24px] md:leading-[22.02px] lg:leading-[27.05px] xl:leading-[33.81px]", - iconSize: "w-[14.24px] h-[14.24px] sm:w-[14.24px] sm:h-[14.24px] md:w-[22.02px] md:h-[22.02px] lg:w-[27.05px] lg:h-[27.05px] xl:w-[33.81px] xl:h-[33.81px]", - }, - }; - - const config = sizes[size || "default"] || sizes.default; - - return ( - -
- {/* Logo Text - responsive visibility for topNav sizes */} -
- CommunityRule -
- - {/* Vector Icon */} - {/* eslint-disable-next-line @next/next/no-img-element */} - -
- - ); -}); - -Logo.displayName = "Logo"; - -export default Logo; diff --git a/app/components/modals/Alert/Alert.container.tsx b/app/components/modals/Alert/Alert.container.tsx index df27eff..e01624a 100644 --- a/app/components/modals/Alert/Alert.container.tsx +++ b/app/components/modals/Alert/Alert.container.tsx @@ -3,7 +3,10 @@ import { memo } from "react"; import { AlertView } from "./Alert.view"; import type { AlertProps } from "./Alert.types"; -import { normalizeAlertStatus, normalizeAlertType } from "../../../../lib/propNormalization"; +import { + normalizeAlertStatus, + normalizeAlertType, +} from "../../../../lib/propNormalization"; const AlertContainer = memo( ({ diff --git a/app/components/modals/Create/Create.view.tsx b/app/components/modals/Create/Create.view.tsx index fe30413..112d624 100644 --- a/app/components/modals/Create/Create.view.tsx +++ b/app/components/modals/Create/Create.view.tsx @@ -56,7 +56,7 @@ export function CreateView({ {/* Header: custom headerContent (when provided) or default title/description */} {headerContent !== undefined ? (
{headerContent}
- ) : (title || description) ? ( + ) : title || description ? (
( - ({ children, text, position: positionProp = "top", className = "", disabled = false }) => { + ({ + children, + text, + position: positionProp = "top", + className = "", + disabled = false, + }) => { // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) const position = normalizeTooltipPosition(positionProp); const [isVisible, setIsVisible] = useState(false); diff --git a/app/components/navigation/Footer.tsx b/app/components/navigation/Footer.tsx index c9865cf..e154462 100644 --- a/app/components/navigation/Footer.tsx +++ b/app/components/navigation/Footer.tsx @@ -3,7 +3,7 @@ import { memo } from "react"; import { useTranslation } from "../../contexts/MessagesContext"; import Link from "next/link"; -import Logo from "../icons/Logo"; +import Logo from "../asset/logo"; import Separator from "../utility/Separator"; import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; @@ -40,7 +40,7 @@ const Footer = memo(() => { lg:gap-[var(--spacing-measures-spacing-060,60px)]" > {/* Logo */} - + {/* Content section */}
diff --git a/app/components/navigation/MenuBar.tsx b/app/components/navigation/MenuBar.tsx index 0335472..803bfc0 100644 --- a/app/components/navigation/MenuBar.tsx +++ b/app/components/navigation/MenuBar.tsx @@ -25,7 +25,7 @@ const MenuBar = memo( ({ children, className = "", size: sizeProp = "X Small", ...props }) => { const size = normalizeMenuBarSize(sizeProp); const t = useTranslation("menuBar"); - + // Size styles based on Figma specifications const sizeStyles: Record< "X Small" | "Small" | "Medium" | "Large" | "X Large", diff --git a/app/components/navigation/MenuBarItem/MenuBarItem.container.tsx b/app/components/navigation/MenuBarItem/MenuBarItem.container.tsx index 17d5feb..7e87897 100644 --- a/app/components/navigation/MenuBarItem/MenuBarItem.container.tsx +++ b/app/components/navigation/MenuBarItem/MenuBarItem.container.tsx @@ -32,11 +32,17 @@ const MenuBarItemContainer = memo( "X Small" | "Small" | "Medium" | "Large" | "X Large", string > = { - "X Small": reducedPadding ? "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]" : "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]", + "X Small": reducedPadding + ? "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]" + : "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]", Small: "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)]", - Medium: reducedPadding ? "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] h-[32px]" : "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] h-[32px]", - Large: "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]", - "X Large": "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]", + Medium: reducedPadding + ? "px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] h-[32px]" + : "px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] h-[32px]", + Large: + "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]", + "X Large": + "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] h-[44px]", }; // Text styles based on Figma specifications @@ -46,41 +52,34 @@ const MenuBarItemContainer = memo( > = { "X Small": "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]", - Small: - "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", - Medium: - "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", - Large: - "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]", + Small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", + Medium: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]", + Large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]", "X Large": "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]", }; // State styles for Default mode (yellow text on dark background) - const defaultModeStyles: Record< - "default" | "hover" | "selected", - string - > = { - default: - "bg-transparent text-[var(--color-content-default-brand-primary,#fefcc9)] hover:bg-[var(--color-gray-800)] hover:text-[var(--color-content-default-brand-primary,#fefcc9)]", - hover: - "bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]", - selected: - "border border-[var(--color-border-default-brand-primary,#fdfaa8)] text-[var(--color-content-default-brand-primary,#fefcc9)] bg-transparent hover:bg-[var(--color-gray-800)]", - }; + const defaultModeStyles: Record<"default" | "hover" | "selected", string> = + { + default: + "bg-transparent text-[var(--color-content-default-brand-primary,#fefcc9)] hover:bg-[var(--color-gray-800)] hover:text-[var(--color-content-default-brand-primary,#fefcc9)]", + hover: + "bg-[var(--color-gray-800)] text-[var(--color-content-default-brand-primary,#fefcc9)]", + selected: + "border border-[var(--color-border-default-brand-primary,#fdfaa8)] text-[var(--color-content-default-brand-primary,#fefcc9)] bg-transparent hover:bg-[var(--color-gray-800)]", + }; // State styles for Inverse mode (black text on yellow background) - const inverseModeStyles: Record< - "default" | "hover" | "selected", - string - > = { - default: - "bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-brand-accent,#4d4a00)] hover:text-[var(--color-content-inverse-primary,black)]", - hover: - "bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]", - selected: - "border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-brand-accent,#4d4a00)]", - }; + const inverseModeStyles: Record<"default" | "hover" | "selected", string> = + { + default: + "bg-transparent text-[var(--color-content-inverse-primary,black)] hover:bg-[var(--color-surface-brand-accent,#4d4a00)] hover:text-[var(--color-content-inverse-primary,black)]", + hover: + "bg-[var(--color-surface-brand-accent,#4d4a00)] text-[var(--color-content-inverse-primary,black)]", + selected: + "border border-[var(--color-border-default-primary,#141414)] text-[var(--color-content-inverse-primary,black)] bg-transparent hover:bg-[var(--color-surface-brand-accent,#4d4a00)]", + }; // Get state styles based on mode const stateStyles = diff --git a/app/components/navigation/MenuBarItem/MenuBarItem.types.ts b/app/components/navigation/MenuBarItem/MenuBarItem.types.ts index cd26b9e..6354cae 100644 --- a/app/components/navigation/MenuBarItem/MenuBarItem.types.ts +++ b/app/components/navigation/MenuBarItem/MenuBarItem.types.ts @@ -5,14 +5,9 @@ export type MenuBarItemSizeValue = | "Large" | "X Large"; -export type MenuBarItemStateValue = - | "default" - | "hover" - | "selected"; +export type MenuBarItemStateValue = "default" | "hover" | "selected"; -export type MenuBarItemModeValue = - | "default" - | "inverse"; +export type MenuBarItemModeValue = "default" | "inverse"; export interface MenuBarItemProps extends React.AnchorHTMLAttributes { href?: string; diff --git a/app/components/navigation/NavigationItem/NavigationItem.container.tsx b/app/components/navigation/NavigationItem/NavigationItem.container.tsx index 3afe5aa..0e15c57 100644 --- a/app/components/navigation/NavigationItem/NavigationItem.container.tsx +++ b/app/components/navigation/NavigationItem/NavigationItem.container.tsx @@ -3,7 +3,10 @@ import { memo } from "react"; import NavigationItemView from "./NavigationItem.view"; import type { NavigationItemProps } from "./NavigationItem.types"; -import { normalizeNavigationItemVariant, normalizeNavigationItemSize } from "../../../../lib/propNormalization"; +import { + normalizeNavigationItemVariant, + normalizeNavigationItemSize, +} from "../../../../lib/propNormalization"; const NavigationItemContainer = memo( ({ diff --git a/app/components/navigation/NavigationItem/NavigationItem.types.ts b/app/components/navigation/NavigationItem/NavigationItem.types.ts index 2338b59..e99e2cd 100644 --- a/app/components/navigation/NavigationItem/NavigationItem.types.ts +++ b/app/components/navigation/NavigationItem/NavigationItem.types.ts @@ -1,5 +1,9 @@ export type NavigationItemVariantValue = "default" | "Default"; -export type NavigationItemSizeValue = "default" | "xsmall" | "Default" | "XSmall"; +export type NavigationItemSizeValue = + | "default" + | "xsmall" + | "Default" + | "XSmall"; export interface NavigationItemProps extends Omit< React.AnchorHTMLAttributes, diff --git a/app/components/navigation/TopNav/TopNav.view.tsx b/app/components/navigation/TopNav/TopNav.view.tsx index 013ec8d..bcb5f72 100644 --- a/app/components/navigation/TopNav/TopNav.view.tsx +++ b/app/components/navigation/TopNav/TopNav.view.tsx @@ -7,7 +7,7 @@ import { getAssetPath } from "../../../../lib/assetUtils"; import MenuBar from "../MenuBar"; import type { TopNavViewProps } from "./TopNav.types"; -import Logo from "../../icons/Logo"; +import Logo from "../../asset/logo"; function TopNavView({ folderTop, @@ -44,7 +44,11 @@ function TopNavView({ {/* Header Tab - Yellow tab container with decorative Union images */}
{/* Logo - Consistent left positioning within HeaderTab */} - + {/* XSmall menu bar - positioned next to logo */}
@@ -90,7 +94,9 @@ function TopNavView({ {/* 640-1023px (md: breakpoint): MenuBar Small */}
- {renderNavigationItems("homeMd")} + + {renderNavigationItems("homeMd")} +
{/* 1024-1440px (lg: breakpoint): MenuBar Large */} @@ -161,7 +167,11 @@ function TopNavView({ aria-label={t("ariaLabels.mainNavigation")} > {/* Logo - Consistent left positioning across all breakpoints */} - + {/* Navigation Links - Consistent center positioning */}
@@ -193,7 +203,9 @@ function TopNavView({
- {renderNavigationItems("xlarge")} + + {renderNavigationItems("xlarge")} +
diff --git a/app/components/sections/AskOrganizer/AskOrganizer.container.tsx b/app/components/sections/AskOrganizer/AskOrganizer.container.tsx index bb5ee50..33c703b 100644 --- a/app/components/sections/AskOrganizer/AskOrganizer.container.tsx +++ b/app/components/sections/AskOrganizer/AskOrganizer.container.tsx @@ -44,7 +44,9 @@ const AskOrganizerContainer = memo( onContactClick, }) => { // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) - const variant = normalizeAskOrganizerVariant(variantProp) as AskOrganizerVariant; + const variant = normalizeAskOrganizerVariant( + variantProp, + ) as AskOrganizerVariant; const t = useTranslation(); const defaultButtonText = buttonText ?? t("askOrganizer.buttonText"); const defaultButtonHref = buttonHref ?? t("askOrganizer.buttonHref"); diff --git a/app/components/sections/CommunityRuleDocument/CommunityRuleDocument.types.ts b/app/components/sections/CommunityRuleDocument/CommunityRuleDocument.types.ts new file mode 100644 index 0000000..b812bb7 --- /dev/null +++ b/app/components/sections/CommunityRuleDocument/CommunityRuleDocument.types.ts @@ -0,0 +1,16 @@ +export interface CommunityRuleDocumentEntry { + title: string; + body: string; +} + +export interface CommunityRuleDocumentSection { + categoryName: string; + entries: CommunityRuleDocumentEntry[]; +} + +export interface CommunityRuleDocumentProps { + sections: CommunityRuleDocumentSection[]; + className?: string; + /** When true, wrap in white background with left teal bar (small breakpoint). */ + useCardStyle?: boolean; +} diff --git a/app/components/sections/CommunityRuleDocument/CommunityRuleDocument.view.tsx b/app/components/sections/CommunityRuleDocument/CommunityRuleDocument.view.tsx new file mode 100644 index 0000000..35db080 --- /dev/null +++ b/app/components/sections/CommunityRuleDocument/CommunityRuleDocument.view.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { memo } from "react"; +import type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types"; + +const SECTION_GAP = "var(--measures-spacing-1200, 64px)"; +const TEAL_BG = "var(--color-teal-teal50, #c9fef9)"; +const SECTION_LINE_COLOR = "var(--color-border-default-tertiary, #464646)"; + +function CommunityRuleDocumentView({ + sections, + className = "", + useCardStyle = false, +}: CommunityRuleDocumentProps) { + const rootClass = useCardStyle + ? `rounded-[12px] bg-white pl-3 border-l-4 ${className}` + : className; + const rootStyle = useCardStyle ? { borderLeftColor: TEAL_BG } : undefined; + + const sectionLineStyle = useCardStyle + ? undefined + : { borderLeftColor: SECTION_LINE_COLOR }; + + return ( +
+ {sections.map((section, sectionIndex) => ( +
+ {/* Section content: line runs full height of this block via border-left */} +
+

+ {section.categoryName} +

+
+ {section.entries.map((entry, entryIndex) => ( +
+

+ {entry.title} +

+

+ {entry.body} +

+
+ ))} +
+
+
+ ))} +
+ ); +} + +CommunityRuleDocumentView.displayName = "CommunityRuleDocumentView"; + +export default memo(CommunityRuleDocumentView); diff --git a/app/components/sections/CommunityRuleDocument/index.tsx b/app/components/sections/CommunityRuleDocument/index.tsx new file mode 100644 index 0000000..c184ed7 --- /dev/null +++ b/app/components/sections/CommunityRuleDocument/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./CommunityRuleDocument.view"; +export type { CommunityRuleDocumentProps } from "./CommunityRuleDocument.types"; diff --git a/app/components/sections/SectionHeader.tsx b/app/components/sections/SectionHeader.tsx index b018da3..0a5224e 100644 --- a/app/components/sections/SectionHeader.tsx +++ b/app/components/sections/SectionHeader.tsx @@ -3,7 +3,11 @@ import { memo } from "react"; import { normalizeSectionHeaderVariant } from "../../../lib/propNormalization"; -export type SectionHeaderVariantValue = "default" | "multi-line" | "Default" | "Multi-Line"; +export type SectionHeaderVariantValue = + | "default" + | "multi-line" + | "Default" + | "Multi-Line"; interface SectionHeaderProps { title: string; diff --git a/app/components/type/ContentLockup/ContentLockup.container.tsx b/app/components/type/ContentLockup/ContentLockup.container.tsx index e14427b..1a5c3f4 100644 --- a/app/components/type/ContentLockup/ContentLockup.container.tsx +++ b/app/components/type/ContentLockup/ContentLockup.container.tsx @@ -3,7 +3,10 @@ import { memo } from "react"; import ContentLockupView from "./ContentLockup.view"; import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types"; -import { normalizeContentLockupVariant, normalizeAlignment } from "../../../../lib/propNormalization"; +import { + normalizeContentLockupVariant, + normalizeAlignment, +} from "../../../../lib/propNormalization"; const ContentLockupContainer = memo( ({ diff --git a/app/components/type/ContentLockup/ContentLockup.view.tsx b/app/components/type/ContentLockup/ContentLockup.view.tsx index b8346ff..0b5565a 100644 --- a/app/components/type/ContentLockup/ContentLockup.view.tsx +++ b/app/components/type/ContentLockup/ContentLockup.view.tsx @@ -96,19 +96,32 @@ function ContentLockupView({
{/* Small button for xsm and sm breakpoints */}
-
{/* Large button for md and lg breakpoints */}
-
{/* XLarge button for xl breakpoint */}
-
diff --git a/app/components/type/HeaderLockup/HeaderLockup.container.tsx b/app/components/type/HeaderLockup/HeaderLockup.container.tsx index 23a8183..132037f 100644 --- a/app/components/type/HeaderLockup/HeaderLockup.container.tsx +++ b/app/components/type/HeaderLockup/HeaderLockup.container.tsx @@ -6,6 +6,7 @@ import type { HeaderLockupProps } from "./HeaderLockup.types"; import { normalizeHeaderLockupJustification, normalizeHeaderLockupSize, + normalizeHeaderLockupPalette, } from "../../../../lib/propNormalization"; const HeaderLockupContainer = memo( @@ -14,10 +15,12 @@ const HeaderLockupContainer = memo( description, justification: justificationProp = "left", size: sizeProp = "L", + palette: paletteProp = "default", }) => { // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) const justification = normalizeHeaderLockupJustification(justificationProp); const size = normalizeHeaderLockupSize(sizeProp); + const palette = normalizeHeaderLockupPalette(paletteProp); return ( ( description={description} justification={justification} size={size} + palette={palette} /> ); }, diff --git a/app/components/type/HeaderLockup/HeaderLockup.types.ts b/app/components/type/HeaderLockup/HeaderLockup.types.ts index 556d8f4..cac76f2 100644 --- a/app/components/type/HeaderLockup/HeaderLockup.types.ts +++ b/app/components/type/HeaderLockup/HeaderLockup.types.ts @@ -1,5 +1,14 @@ -export type HeaderLockupJustificationValue = "left" | "center" | "Left" | "Center"; +export type HeaderLockupJustificationValue = + | "left" + | "center" + | "Left" + | "Center"; export type HeaderLockupSizeValue = "L" | "M" | "l" | "m"; +export type HeaderLockupPaletteValue = + | "default" + | "inverse" + | "Default" + | "Inverse"; export interface HeaderLockupProps { /** @@ -20,6 +29,11 @@ export interface HeaderLockupProps { * Figma uses PascalCase, codebase uses lowercase - both are supported. */ size?: HeaderLockupSizeValue; + /** + * Palette. Default = light text (dark bg); Inverse = dark text (light bg). + * Accepts both PascalCase (Figma) and lowercase (codebase). + */ + palette?: HeaderLockupPaletteValue; } export interface HeaderLockupViewProps { @@ -27,4 +41,5 @@ export interface HeaderLockupViewProps { description?: string; justification: "left" | "center"; size: "L" | "M"; + palette: "default" | "inverse"; } diff --git a/app/components/type/HeaderLockup/HeaderLockup.view.tsx b/app/components/type/HeaderLockup/HeaderLockup.view.tsx index 581507f..b68ffae 100644 --- a/app/components/type/HeaderLockup/HeaderLockup.view.tsx +++ b/app/components/type/HeaderLockup/HeaderLockup.view.tsx @@ -8,9 +8,18 @@ function HeaderLockupView({ description, justification, size, + palette, }: HeaderLockupViewProps) { const isL = size === "L"; const isLeft = justification === "left"; + const isInverse = palette === "inverse"; + + const titleColorClass = isInverse + ? "text-[var(--color-content-invert-primary)]" + : "text-[var(--color-content-default-primary,white)]"; + const descriptionColorClass = isInverse + ? "text-[#2d2d2d]" + : "text-[var(--color-content-default-tertiary,#b4b4b4)]"; return (

{description} diff --git a/app/components/utility/AvatarContainer.tsx b/app/components/utility/AvatarContainer.tsx index f3599ae..8eb4ff1 100644 --- a/app/components/utility/AvatarContainer.tsx +++ b/app/components/utility/AvatarContainer.tsx @@ -1,7 +1,15 @@ import { memo } from "react"; import { normalizeSize } from "../../../lib/propNormalization"; -export type AvatarContainerSizeValue = "small" | "medium" | "large" | "xlarge" | "Small" | "Medium" | "Large" | "XLarge"; +export type AvatarContainerSizeValue = + | "small" + | "medium" + | "large" + | "xlarge" + | "Small" + | "Medium" + | "Large" + | "XLarge"; interface AvatarContainerProps extends React.HTMLAttributes { children?: React.ReactNode; diff --git a/app/components/utility/CardStack/CardStack.container.tsx b/app/components/utility/CardStack/CardStack.container.tsx index 5626ea8..48bc071 100644 --- a/app/components/utility/CardStack/CardStack.container.tsx +++ b/app/components/utility/CardStack/CardStack.container.tsx @@ -24,7 +24,9 @@ const CardStackContainer = memo( className = "", }) => { const [internalExpanded, setInternalExpanded] = useState(false); - const [internalSelectedIds, setInternalSelectedIds] = useState([]); + const [internalSelectedIds, setInternalSelectedIds] = useState( + [], + ); const expanded = controlledExpanded !== undefined ? controlledExpanded : internalExpanded; @@ -41,7 +43,9 @@ const CardStackContainer = memo( controlledSelectedIds !== undefined ? controlledSelectedIds : controlledSelectedId !== undefined - ? (controlledSelectedId ? [controlledSelectedId] : []) + ? controlledSelectedId + ? [controlledSelectedId] + : [] : internalSelectedIds; const handleCardSelect = useCallback( diff --git a/app/components/utility/CreateFlowFooter/CreateFlowFooter.view.tsx b/app/components/utility/CreateFlowFooter/CreateFlowFooter.view.tsx index 34bc736..4929653 100644 --- a/app/components/utility/CreateFlowFooter/CreateFlowFooter.view.tsx +++ b/app/components/utility/CreateFlowFooter/CreateFlowFooter.view.tsx @@ -36,9 +36,7 @@ export function CreateFlowFooterView({ {/* Second Button - Right */} - {secondButton && ( -
{secondButton}
- )} + {secondButton &&
{secondButton}
}

); diff --git a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx index 56de7eb..707aa78 100644 --- a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx +++ b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.container.tsx @@ -15,6 +15,7 @@ const CreateFlowTopNavContainer = memo( onExport, onEdit, onExit, + buttonPalette, className = "", }) => { const router = useRouter(); @@ -38,6 +39,7 @@ const CreateFlowTopNavContainer = memo( onExport={onExport} onEdit={onEdit} onExit={handleExit} + buttonPalette={buttonPalette} className={className} /> ); diff --git a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts index 0559ad5..c5a9af4 100644 --- a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts +++ b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.types.ts @@ -1,6 +1,6 @@ /** * Type definitions for CreateFlowTopNav component - * + * * Top navigation bar for the create rule flow. * Includes logo and action buttons (Share, Export, Edit, Exit). */ @@ -42,6 +42,11 @@ export interface CreateFlowTopNavProps { * Callback when Exit/Save & Exit button is clicked */ onExit?: () => void; + /** + * Palette for nav buttons (e.g. "inverse" on completed page to match teal background) + * @default "default" + */ + buttonPalette?: "default" | "inverse"; /** * Additional CSS classes */ diff --git a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx index c721b6a..24f486e 100644 --- a/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx +++ b/app/components/utility/CreateFlowTopNav/CreateFlowTopNav.view.tsx @@ -1,4 +1,4 @@ -import Logo from "../../icons/Logo"; +import Logo from "../../asset/logo"; import Button from "../../buttons/Button"; import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types"; @@ -11,6 +11,7 @@ export function CreateFlowTopNavView({ onExport, onEdit, onExit, + buttonPalette = "default", className = "", }: CreateFlowTopNavProps) { const exitButtonText = loggedIn ? "Save & Exit" : "Exit"; @@ -27,14 +28,14 @@ export function CreateFlowTopNavView({ aria-label="Create Flow Navigation" > {/* Logo - Left */} - + {/* Button Group - Right */}
{hasShare && (
diff --git a/app/components/utility/Tag/Tag.container.tsx b/app/components/utility/Tag/Tag.container.tsx index b052b1c..4eef24b 100644 --- a/app/components/utility/Tag/Tag.container.tsx +++ b/app/components/utility/Tag/Tag.container.tsx @@ -9,16 +9,14 @@ const DEFAULT_LABELS: Record = { selected: "SELECTED", }; -const TagContainer = memo( - ({ variant, children, className = "" }) => { - const content = children ?? DEFAULT_LABELS[variant]; - return ( - - {content} - - ); - }, -); +const TagContainer = memo(({ variant, children, className = "" }) => { + const content = children ?? DEFAULT_LABELS[variant]; + return ( + + {content} + + ); +}); TagContainer.displayName = "Tag"; diff --git a/app/create/[step]/page.tsx b/app/create/[step]/page.tsx index 5c0239b..90f096a 100644 --- a/app/create/[step]/page.tsx +++ b/app/create/[step]/page.tsx @@ -25,7 +25,7 @@ const VALID_STEPS: CreateFlowStep[] = [ /** * Dynamic route handler for create flow steps - * + * * Handles all flow steps via dynamic routing: /create/[step] * Validates step exists and renders appropriate template (placeholder for now) */ diff --git a/app/create/completed/page.tsx b/app/create/completed/page.tsx new file mode 100644 index 0000000..80cc5ba --- /dev/null +++ b/app/create/completed/page.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useMediaQuery } from "../../hooks/useMediaQuery"; +import HeaderLockup from "../../components/type/HeaderLockup"; +import CommunityRuleDocument from "../../components/sections/CommunityRuleDocument"; +import type { CommunityRuleDocumentSection } from "../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types"; +import Alert from "../../components/modals/Alert"; + +const TITLE = "Mutual Aid Mondays"; +const DESCRIPTION = + "Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness."; + +const TOAST_TITLE = "This is what folks see when you share your CommunityRule"; +const TOAST_DESCRIPTION = + "Your group can use this document as an operating manual."; + +const SOLIDARITY_BODY = + "Food Not Bombs is not a charity. It is a project of solidarity. Charity is vertical. It moves from those who have to those who have not and maintains the hierarchy between them. Solidarity is horizontal. It moves between equals who recognize that our liberation is bound together. We do not help the poor. We share resources among community members because access to food is a human right rather than a privilege of wealth."; + +/** Static sections for the completed Community Rule document (placeholder data). */ +const COMPLETED_RULE_SECTIONS: CommunityRuleDocumentSection[] = [ + { + categoryName: "Values", + entries: [ + { title: "Solidarity Forever", body: SOLIDARITY_BODY }, + { + title: "Shared Leadership", + body: "We operate without bosses or managers. This does not mean we are disorganized. It means we are self-organized. Authority in this chapter is temporary and task-specific rather than permanent or personal. We believe the people doing the work should make the decisions about that work. By distributing responsibility we prevent burnout and ensure the movement survives beyond any single leader.", + }, + { + title: "Organizing Offline", + body: "We use digital tools to coordinate but we build power in the physical world. An algorithm cannot cook a meal and a group chat cannot look someone in the eye. We prioritize face-to-face connection and resist the pull of digital metrics.", + }, + { + title: "Circular Food Systems", + body: "We intervene in the ecological crisis by addressing food waste and food recovery. We redirect surplus food to where it is needed and model a circular economy at the scale of our communities.", + }, + ], + }, + { + categoryName: "Communication", + entries: [ + { + title: "Signal", + body: "We use Signal for sensitive coordination. Encrypted messaging helps protect our members and our plans from surveillance.", + }, + ], + }, + { + categoryName: "Membership", + entries: [ + { + title: "Open Admission", + body: "Anyone who shares our values and is willing to contribute is welcome. We do not require applications or approval processes for general participation.", + }, + ], + }, + { + categoryName: "Decision-making", + entries: [ + { + title: "Lazy Consensus", + body: "We use lazy consensus for most decisions: proposals move forward unless someone raises a blocking concern. This keeps us moving without requiring everyone to approve every detail.", + }, + { + title: "Modified Consensus", + body: "For larger or more consequential decisions we use modified consensus, with clear timelines and a fallback to a supermajority vote if needed.", + }, + ], + }, + { + categoryName: "Conflict management", + entries: [ + { + title: "Code of Conduct", + body: "We have a code of conduct that sets expectations for behavior and outlines how we address harm.", + }, + { + title: "Restorative Justice", + body: "When conflict arises we prioritize restoration and learning over punishment. We use facilitated circles and other restorative practices where appropriate.", + }, + ], + }, +]; + +/** + * Completed create flow page. + * Figma: 20907-213286 (main), 18002-28017 (toast). + */ +export default function CompletedPage() { + const [isMounted, setIsMounted] = useState(false); + const [toastDismissed, setToastDismissed] = useState(false); + 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 showDesktopLayout = !isMounted || isMdOrLarger; + + if (showDesktopLayout) { + return ( +
+
+
+ {/* Left column: community title + header, centered, does not scroll */} +
+ +
+ {/* Right column: Community Rule document — this column scrolls independently; padding inside scroll so content isn't clipped */} +
+ {/* Soft fade at top: gradient wash only (no blur) so no sharp cutoff line */} +
+
+ +
+
+
+
+ + {!toastDismissed && ( +
+ setToastDismissed(true)} + className="w-full" + /> +
+ )} +
+ ); + } + + return ( + <> +
+
+ + +
+
+ + {!toastDismissed && ( +
+ setToastDismissed(true)} + className="w-full" + /> +
+ )} + + ); +} diff --git a/app/create/context/CreateFlowContext.tsx b/app/create/context/CreateFlowContext.tsx index 3c98858..bf9431d 100644 --- a/app/create/context/CreateFlowContext.tsx +++ b/app/create/context/CreateFlowContext.tsx @@ -16,7 +16,7 @@ interface CreateFlowProviderProps { /** * Provider component for Create Flow state management - * + * * This is a basic implementation that will be expanded in CR-56 * with full navigation logic, state persistence, and validation. */ @@ -25,9 +25,7 @@ export function CreateFlowProvider({ initialStep = null, }: CreateFlowProviderProps) { const [state, setState] = useState({}); - const [currentStep] = useState( - initialStep, - ); + const [currentStep] = useState(initialStep); const updateState = (updates: Partial) => { setState((prevState) => ({ @@ -51,7 +49,7 @@ export function CreateFlowProvider({ /** * Hook to access Create Flow context - * + * * @throws Error if used outside CreateFlowProvider * @returns CreateFlowContextValue */ diff --git a/app/create/final-review/page.tsx b/app/create/final-review/page.tsx index 3f50a17..26d5ddb 100644 --- a/app/create/final-review/page.tsx +++ b/app/create/final-review/page.tsx @@ -32,9 +32,7 @@ const FINAL_REVIEW_CATEGORIES: Category[] = [ }, { name: "Membership", - chipOptions: [ - { id: "m1", label: "Open Admission", state: "unselected" }, - ], + chipOptions: [{ id: "m1", label: "Open Admission", state: "unselected" }], }, { name: "Decision-making", @@ -70,7 +68,7 @@ export default function FinalReviewPage() { if (showDesktopLayout) { return ( -
+
{ - if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) { + if ( + typeof document !== "undefined" && + document.activeElement instanceof HTMLElement + ) { document.activeElement.blur(); } if (nextStep) { @@ -68,7 +71,10 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) { }; const handleBack = () => { - if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) { + if ( + typeof document !== "undefined" && + document.activeElement instanceof HTMLElement + ) { document.activeElement.blur(); } if (previousStep) { @@ -76,30 +82,52 @@ function CreateFlowLayoutContent({ children }: { children: ReactNode }) { } }; + const isCompletedStep = currentStep === "completed"; + return ( -
- -
+
+ router.push("/create/final-review") + : undefined + } + buttonPalette={isCompletedStep ? "inverse" : undefined} + className={ + isCompletedStep ? "!bg-[var(--color-teal-teal50,#c9fef9)]" : undefined + } + /> +
{children}
- - {currentStep === "final-review" - ? "Finalize CommunityRule" - : "Next"} - - ) : null - } - onBackClick={previousStep ? handleBack : undefined} - /> + {!isCompletedStep && ( + + {currentStep === "final-review" + ? "Finalize CommunityRule" + : "Next"} + + ) : null + } + onBackClick={previousStep ? handleBack : undefined} + /> + )}
); } diff --git a/app/create/select/page.tsx b/app/create/select/page.tsx index 6b25d7a..bf52820 100644 --- a/app/create/select/page.tsx +++ b/app/create/select/page.tsx @@ -7,7 +7,7 @@ import MultiSelect from "../../components/controls/MultiSelect"; /** * Select page for the create flow - * + * * Displays selection options using HeaderLockup and MultiSelect components. * Responsive layout: two-column at 640px+, single column below 640px. * Responsive sizing: uses L/M for HeaderLockup and S for MultiSelect based on 640px breakpoint. @@ -44,9 +44,12 @@ export default function SelectPage() { setCommunitySizeOptions((prev) => prev.map((opt) => opt.id === chipId - ? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" } - : opt - ) + ? { + ...opt, + state: opt.state === "Selected" ? "Unselected" : "Selected", + } + : opt, + ), ); }; @@ -54,9 +57,12 @@ export default function SelectPage() { setOrganizationTypeOptions((prev) => prev.map((opt) => opt.id === chipId - ? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" } - : opt - ) + ? { + ...opt, + state: opt.state === "Selected" ? "Unselected" : "Selected", + } + : opt, + ), ); }; @@ -64,9 +70,12 @@ export default function SelectPage() { setGovernanceStyleOptions((prev) => prev.map((opt) => opt.id === chipId - ? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" } - : opt - ) + ? { + ...opt, + state: opt.state === "Selected" ? "Unselected" : "Selected", + } + : opt, + ), ); }; diff --git a/app/create/text/page.tsx b/app/create/text/page.tsx index 66da4ab..103af4b 100644 --- a/app/create/text/page.tsx +++ b/app/create/text/page.tsx @@ -7,7 +7,7 @@ import TextInput from "../../components/controls/TextInput"; /** * Text page for the create flow - * + * * Displays a text input field for user input using HeaderLockup and TextInput components. * Responsive sizing: uses L/M for HeaderLockup and medium/small for TextInput based on 640px breakpoint. */ diff --git a/app/tailwind.css b/app/tailwind.css index 84ce2d9..af1a35e 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -18,31 +18,31 @@ /* Design system scrollbar (Figma node 20612-36521): dark track + thumb with states */ .scrollbar-design { scrollbar-width: thin; /* Firefox: narrow scrollbar */ - scrollbar-color: #545B64 #292D32; /* Firefox: thumb track */ + scrollbar-color: #545b64 #292d32; /* Firefox: thumb track */ } .scrollbar-design::-webkit-scrollbar { width: 16px; height: 16px; } .scrollbar-design::-webkit-scrollbar-track { - background: #292D32; + background: #292d32; } .scrollbar-design::-webkit-scrollbar-thumb { - background: #545B64; + background: #545b64; border-radius: 4px; - border: 4px solid #292D32; /* visual padding: thumb appears 8px within 16px track */ + border: 4px solid #292d32; /* visual padding: thumb appears 8px within 16px track */ background-clip: padding-box; } .scrollbar-design::-webkit-scrollbar-thumb:hover { - background: #787F8A; + background: #787f8a; border-width: 2px; /* hover: thumb expands to 12px */ } .scrollbar-design::-webkit-scrollbar-thumb:active { - background: #3F434C; + background: #3f434c; border-width: 2px; } .scrollbar-design::-webkit-scrollbar-corner { - background: #292D32; + background: #292d32; } @theme inline { diff --git a/lib/assetUtils.ts b/lib/assetUtils.ts index 6f62476..b75662f 100644 --- a/lib/assetUtils.ts +++ b/lib/assetUtils.ts @@ -30,7 +30,7 @@ export function getAssetPath(assetPath: string): string { */ export const ASSETS = { // Logo - LOGO: "assets/Logo.svg", + LOGO: "assets/logo/Logo.svg", // Avatars AVATAR_1: "assets/Avatar_1.png", diff --git a/lib/propNormalization.ts b/lib/propNormalization.ts index 79ae1ce..f022601 100644 --- a/lib/propNormalization.ts +++ b/lib/propNormalization.ts @@ -1,7 +1,7 @@ /** * Utility functions for normalizing component props to match Figma specifications * while maintaining backward compatibility with existing lowercase usage. - * + * * Figma uses PascalCase (e.g., "Standard", "Inverse") but codebase uses lowercase. * These helpers accept both formats and normalize to lowercase for internal use. */ @@ -11,7 +11,7 @@ */ export function normalizeMode( value: string | undefined, - defaultValue: "standard" | "inverse" = "standard" + defaultValue: "standard" | "inverse" = "standard", ): "standard" | "inverse" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -26,7 +26,7 @@ export function normalizeMode( */ export function normalizeState( value: string | undefined, - defaultValue: "default" | "hover" | "focus" | "selected" = "default" + defaultValue: "default" | "hover" | "focus" | "selected" = "default", ): "default" | "hover" | "focus" | "selected" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -46,7 +46,7 @@ export function normalizeState( */ export function normalizeInputState( value: string | undefined, - defaultValue: "default" | "active" | "hover" | "focus" = "default" + defaultValue: "default" | "active" | "hover" | "focus" = "default", ): "default" | "active" | "hover" | "focus" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -66,7 +66,7 @@ export function normalizeInputState( */ export function normalizeToggleState( value: string | undefined, - defaultValue: "default" | "hover" | "focus" | "selected" = "default" + defaultValue: "default" | "hover" | "focus" | "selected" = "default", ): "default" | "hover" | "focus" | "selected" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -112,13 +112,12 @@ export type InputStateValue = | "Hover" | "Focus"; - /** * Normalize button size prop values */ export function normalizeSize( value: string | undefined, - defaultValue: "xsmall" | "small" | "medium" | "large" | "xlarge" = "xsmall" + defaultValue: "xsmall" | "small" | "medium" | "large" | "xlarge" = "xsmall", ): "xsmall" | "small" | "medium" | "large" | "xlarge" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -134,7 +133,7 @@ export function normalizeSize( */ export function normalizeAlertStatus( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" | "positive" | "warning" | "danger" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -150,7 +149,7 @@ export function normalizeAlertStatus( */ export function normalizeAlertType( value: string | undefined, - defaultValue: "toast" = "toast" + defaultValue: "toast" = "toast", ): "toast" | "banner" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -166,7 +165,7 @@ export function normalizeAlertType( */ export function normalizeTooltipPosition( value: string | undefined, - defaultValue: "top" = "top" + defaultValue: "top" = "top", ): "top" | "bottom" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -199,7 +198,12 @@ export type SizeValue = */ export function normalizeMenuBarSize( value: string | undefined, - defaultValue: "X Small" | "Small" | "Medium" | "Large" | "X Large" = "X Small" + defaultValue: + | "X Small" + | "Small" + | "Medium" + | "Large" + | "X Large" = "X Small", ): "X Small" | "Small" | "Medium" | "Large" | "X Large" { if (!value) return defaultValue; if ( @@ -219,7 +223,7 @@ export function normalizeMenuBarSize( */ export function normalizeNavigationItemVariant( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -234,7 +238,7 @@ export function normalizeNavigationItemVariant( */ export function normalizeNavigationItemSize( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" | "xsmall" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -250,7 +254,7 @@ export function normalizeNavigationItemSize( */ export function normalizeContentLockupVariant( value: string | undefined, - defaultValue: "hero" = "hero" + defaultValue: "hero" = "hero", ): "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -266,7 +270,7 @@ export function normalizeContentLockupVariant( */ export function normalizeAlignment( value: string | undefined, - defaultValue: "center" = "center" + defaultValue: "center" = "center", ): "center" | "left" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -282,7 +286,7 @@ export function normalizeAlignment( */ export function normalizeNumberedListSize( value: string | undefined, - defaultValue: "M" = "M" + defaultValue: "M" = "M", ): "M" | "S" { if (!value) return defaultValue; const normalized = value.toUpperCase(); @@ -297,7 +301,7 @@ export function normalizeNumberedListSize( */ export function normalizeHeaderLockupJustification( value: string | undefined, - defaultValue: "left" = "left" + defaultValue: "left" = "left", ): "left" | "center" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -312,7 +316,7 @@ export function normalizeHeaderLockupJustification( */ export function normalizeHeaderLockupSize( value: string | undefined, - defaultValue: "L" = "L" + defaultValue: "L" = "L", ): "L" | "M" { if (!value) return defaultValue; const normalized = value.toUpperCase(); @@ -322,12 +326,27 @@ export function normalizeHeaderLockupSize( return defaultValue; } +/** + * Normalize header lockup palette prop values (Default/Inverse -> default/inverse) + */ +export function normalizeHeaderLockupPalette( + value: string | undefined, + defaultValue: "default" = "default", +): "default" | "inverse" { + if (!value) return defaultValue; + const normalized = value.toLowerCase(); + if (normalized === "default" || normalized === "inverse") { + return normalized; + } + return defaultValue; +} + /** * Normalize text input size prop values */ export function normalizeTextInputSize( value: string | undefined, - defaultValue: "medium" = "medium" + defaultValue: "medium" = "medium", ): "small" | "medium" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -342,7 +361,7 @@ export function normalizeTextInputSize( */ export function normalizeContentContainerSize( value: string | undefined, - defaultValue: "responsive" = "responsive" + defaultValue: "responsive" = "responsive", ): "xs" | "responsive" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -358,7 +377,7 @@ export function normalizeContentContainerSize( */ export function normalizeContentThumbnailVariant( value: string | undefined, - defaultValue: "vertical" = "vertical" + defaultValue: "vertical" = "vertical", ): "vertical" | "horizontal" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -374,7 +393,7 @@ export function normalizeContentThumbnailVariant( */ export function normalizeSectionHeaderVariant( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" | "multi-line" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -390,7 +409,7 @@ export function normalizeSectionHeaderVariant( */ export function normalizeQuoteBlockVariant( value: string | undefined, - defaultValue: "standard" = "standard" + defaultValue: "standard" = "standard", ): "compact" | "standard" | "extended" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -406,11 +425,16 @@ export function normalizeQuoteBlockVariant( */ export function normalizeNumberCardSize( value: string | undefined, - defaultValue: "Medium" = "Medium" + defaultValue: "Medium" = "Medium", ): "Small" | "Medium" | "Large" | "XLarge" { if (!value) return defaultValue; // Check if already PascalCase - if (value === "Small" || value === "Medium" || value === "Large" || value === "XLarge") { + if ( + value === "Small" || + value === "Medium" || + value === "Large" || + value === "XLarge" + ) { return value; } // Normalize lowercase to PascalCase @@ -427,7 +451,7 @@ export function normalizeNumberCardSize( */ export function normalizeAskOrganizerVariant( value: string | undefined, - defaultValue: "centered" = "centered" + defaultValue: "centered" = "centered", ): "centered" | "left-aligned" | "compact" | "inverse" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -443,7 +467,7 @@ export function normalizeAskOrganizerVariant( */ export function normalizeContextMenuItemSize( value: string | undefined, - defaultValue: "medium" = "medium" + defaultValue: "medium" = "medium", ): "small" | "medium" | "large" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -459,7 +483,7 @@ export function normalizeContextMenuItemSize( */ export function normalizeImagePlaceholderColor( value: string | undefined, - defaultValue: "blue" = "blue" + defaultValue: "blue" = "blue", ): "blue" | "green" | "purple" | "red" | "orange" | "teal" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -475,7 +499,7 @@ export function normalizeImagePlaceholderColor( */ export function normalizeToggleGroupPosition( value: string | undefined, - defaultValue: "left" = "left" + defaultValue: "left" = "left", ): "left" | "middle" | "right" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -491,7 +515,7 @@ export function normalizeToggleGroupPosition( */ export function normalizeLabelVariant( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" | "horizontal" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -507,7 +531,7 @@ export function normalizeLabelVariant( */ export function normalizeTextAreaAppearance( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" | "embedded" { if (!value) return defaultValue; const n = value.toLowerCase(); @@ -519,7 +543,7 @@ export function normalizeTextAreaAppearance( */ export function normalizeSmallMediumLargeSize( value: string | undefined, - defaultValue: "medium" = "medium" + defaultValue: "medium" = "medium", ): "small" | "medium" | "large" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -535,11 +559,16 @@ export function normalizeSmallMediumLargeSize( */ export function normalizeRuleCardSize( value: string | undefined, - defaultValue: "L" = "L" + defaultValue: "L" = "L", ): "XS" | "S" | "M" | "L" { if (!value) return defaultValue; const normalized = value.toUpperCase(); - if (normalized === "XS" || normalized === "S" || normalized === "M" || normalized === "L") { + if ( + normalized === "XS" || + normalized === "S" || + normalized === "M" || + normalized === "L" + ) { return normalized; } return defaultValue; @@ -561,11 +590,7 @@ export type ChipStateValue = /** * Type helper for case-insensitive Chip palette prop */ -export type ChipPaletteValue = - | "default" - | "inverse" - | "Default" - | "Inverse"; +export type ChipPaletteValue = "default" | "inverse" | "Default" | "Inverse"; /** * Type helper for case-insensitive Chip size prop @@ -673,7 +698,7 @@ export function normalizeInputLabelPalette( */ export function normalizeMenuBarItemState( value: string | undefined, - defaultValue: "default" | "hover" | "selected" = "default" + defaultValue: "default" | "hover" | "selected" = "default", ): "default" | "hover" | "selected" { if (!value) return defaultValue; if (value === "default" || value === "hover" || value === "selected") { @@ -689,7 +714,7 @@ export function normalizeMenuBarItemState( */ export function normalizeMenuBarItemMode( value: string | undefined, - defaultValue: "default" | "inverse" = "default" + defaultValue: "default" | "inverse" = "default", ): "default" | "inverse" { if (!value) return defaultValue; if (value === "default" || value === "inverse") { @@ -704,7 +729,12 @@ export function normalizeMenuBarItemMode( */ export function normalizeMenuBarItemSize( value: string | undefined, - defaultValue: "X Small" | "Small" | "Medium" | "Large" | "X Large" = "X Small" + defaultValue: + | "X Small" + | "Small" + | "Medium" + | "Large" + | "X Large" = "X Small", ): "X Small" | "Small" | "Medium" | "Large" | "X Large" { if (!value) return defaultValue; if ( @@ -724,7 +754,7 @@ export function normalizeMenuBarItemSize( */ export function normalizeButtonType( value: string | undefined, - defaultValue: "filled" = "filled" + defaultValue: "filled" = "filled", ): "filled" | "outline" | "ghost" | "danger" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -740,7 +770,7 @@ export function normalizeButtonType( */ export function normalizeButtonPalette( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" | "inverse" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -759,7 +789,7 @@ export function normalizeButtonPalette( */ export function normalizeButtonState( value: string | undefined, - defaultValue: "default" = "default" + defaultValue: "default" = "default", ): "default" | "focus" | "active" | "hover" | "disabled" { if (!value) return defaultValue; const normalized = value.toLowerCase(); @@ -806,4 +836,4 @@ export type ButtonStateValue = | "Focus" | "Active" | "Hover" - | "Disabled"; \ No newline at end of file + | "Disabled"; diff --git a/public/assets/Logo.svg b/public/assets/logo/Logo.svg similarity index 100% rename from public/assets/Logo.svg rename to public/assets/logo/Logo.svg diff --git a/stories/buttons/Button.stories.js b/stories/buttons/Button.stories.js index 3dbdbfd..ba0ced7 100644 --- a/stories/buttons/Button.stories.js +++ b/stories/buttons/Button.stories.js @@ -179,7 +179,9 @@ export const AllVariants = {
-

Filled Inverse Variant

+

+ Filled Inverse Variant +

-

Outline Inverse Variant

+

+ Outline Inverse Variant +

-

Danger Inverse Variant

+

+ Danger Inverse Variant +

- + @@ -341,7 +343,9 @@ export const EdgeCases = {
- + diff --git a/stories/cards/Card.stories.js b/stories/cards/Card.stories.js index 1c4af0a..7f37b5d 100644 --- a/stories/cards/Card.stories.js +++ b/stories/cards/Card.stories.js @@ -46,8 +46,7 @@ export default { export const Default = { args: { label: "Label", - supportText: - "Members vote to resolve a dispute democratically.", + supportText: "Members vote to resolve a dispute democratically.", recommended: true, selected: false, orientation: "horizontal", @@ -69,8 +68,7 @@ export const HorizontalRecommended = { export const HorizontalSelected = { args: { label: "Label", - supportText: - "Members vote to resolve a dispute democratically.", + supportText: "Members vote to resolve a dispute democratically.", recommended: false, selected: true, orientation: "horizontal", @@ -157,8 +155,7 @@ export const AllVariants = { parameters: { docs: { description: { - story: - "All four variants: horizontal/vertical × recommended/selected.", + story: "All four variants: horizontal/vertical × recommended/selected.", }, }, }, diff --git a/stories/cards/RuleCard.stories.js b/stories/cards/RuleCard.stories.js index 3dafa02..9bca2ba 100644 --- a/stories/cards/RuleCard.stories.js +++ b/stories/cards/RuleCard.stories.js @@ -75,7 +75,8 @@ export const Expanded = { backgroundColor: "bg-[#b7d9d5]", expanded: true, size: "L", - logoUrl: "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png", + logoUrl: + "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png", logoAlt: "Mutual Aid Mondays", categories: [ { @@ -96,9 +97,7 @@ export const Expanded = { }, { name: "Communication", - chipOptions: [ - { id: "comm-1", label: "Signal", state: "Unselected" }, - ], + chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }], onChipClick: (categoryName, chipId) => { console.log(`Chip clicked: ${categoryName} - ${chipId}`); }, @@ -122,7 +121,11 @@ export const Expanded = { name: "Decision-making", chipOptions: [ { id: "decision-1", label: "Lazy Consensus", state: "Unselected" }, - { id: "decision-2", label: "Modified Consensus", state: "Unselected" }, + { + id: "decision-2", + label: "Modified Consensus", + state: "Unselected", + }, ], onChipClick: (categoryName, chipId) => { console.log(`Chip clicked: ${categoryName} - ${chipId}`); @@ -135,7 +138,11 @@ export const Expanded = { name: "Conflict management", chipOptions: [ { id: "conflict-1", label: "Code of Conduct", state: "Unselected" }, - { id: "conflict-2", label: "Restorative Justice", state: "Unselected" }, + { + id: "conflict-2", + label: "Restorative Justice", + state: "Unselected", + }, ], onChipClick: (categoryName, chipId) => { console.log(`Chip clicked: ${categoryName} - ${chipId}`); @@ -232,7 +239,8 @@ export const ExpandedMedium = { backgroundColor: "bg-[#b7d9d5]", expanded: true, size: "M", - logoUrl: "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png", + logoUrl: + "http://localhost:3845/assets/d2513a6ab56f2b2927e8a7c442c06326e7a29541.png", logoAlt: "Mutual Aid Mondays", categories: [ { @@ -247,9 +255,7 @@ export const ExpandedMedium = { }, { name: "Communication", - chipOptions: [ - { id: "comm-1", label: "Signal", state: "Unselected" }, - ], + chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }], }, { name: "Membership", @@ -261,14 +267,22 @@ export const ExpandedMedium = { name: "Decision-making", chipOptions: [ { id: "decision-1", label: "Lazy Consensus", state: "Unselected" }, - { id: "decision-2", label: "Modified Consensus", state: "Unselected" }, + { + id: "decision-2", + label: "Modified Consensus", + state: "Unselected", + }, ], }, { name: "Conflict management", chipOptions: [ { id: "conflict-1", label: "Code of Conduct", state: "Unselected" }, - { id: "conflict-2", label: "Restorative Justice", state: "Unselected" }, + { + id: "conflict-2", + label: "Restorative Justice", + state: "Unselected", + }, ], }, ], @@ -393,9 +407,7 @@ export const InteractiveStates = { }, { name: "Communication", - chipOptions: [ - { id: "comm-1", label: "Signal", state: "Unselected" }, - ], + chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }], onChipClick: (categoryName, chipId) => { console.log(`Chip clicked: ${categoryName} - ${chipId}`); }, diff --git a/stories/controls/Checkbox.stories.js b/stories/controls/Checkbox.stories.js index b38afe7..2963390 100644 --- a/stories/controls/Checkbox.stories.js +++ b/stories/controls/Checkbox.stories.js @@ -47,12 +47,14 @@ export default { mode: { control: "select", options: ["standard", "inverse", "Standard", "Inverse"], - description: "Visual mode of the checkbox (case-insensitive: accepts both lowercase and PascalCase)", + description: + "Visual mode of the checkbox (case-insensitive: accepts both lowercase and PascalCase)", }, state: { control: "select", options: ["default", "hover", "focus", "Default", "Hover", "Focus"], - description: "Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)", + description: + "Interaction state for static display (case-insensitive: accepts both lowercase and PascalCase)", }, disabled: { control: "boolean", @@ -215,9 +217,12 @@ export const FigmaPascalCase = () => { return (
-

Figma PascalCase Props (Standard/Inverse)

+

+ Figma PascalCase Props (Standard/Inverse) +

- These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values. + These components accept both PascalCase (from Figma) and lowercase + (from codebase) prop values.

{
-

Mixed Case (backward compatibility)

+

+ Mixed Case (backward compatibility) +

{ return (
-

Standard Mode - Unselected

+

+ Standard Mode - Unselected +

{
-

Standard Mode - Selected

+

+ Standard Mode - Selected +

{ return (
-

Inverse Mode - Unselected

+

+ Inverse Mode - Unselected +

{
-

Inverse Mode - Selected

+

+ Inverse Mode - Selected +

{ return (
-

Figma PascalCase Props (Standard/Inverse)

+

+ Figma PascalCase Props (Standard/Inverse) +

- These components accept both PascalCase (from Figma) and lowercase (from codebase) prop values. + These components accept both PascalCase (from Figma) and lowercase + (from codebase) prop values.

{
-

Mixed Case (backward compatibility)

+

+ Mixed Case (backward compatibility) +

(
@@ -76,7 +82,7 @@ export const Sizes = { export const IconOnly = { args: { size: "default", - showText: false, + wordmark: false, }, render: (args) => (
@@ -123,11 +129,11 @@ export const TopNavContext = {
FolderTop: - +
Header: - +
@@ -148,13 +154,11 @@ export const CreateFlowContext = { render: () => (
-

- Create Flow Context -

+

Create Flow Context

CreateFlow: - +
@@ -169,3 +173,30 @@ export const CreateFlowContext = { }, }, }; + +export const CreateFlowCompletedInverse = { + args: {}, + render: () => ( +
+
+

+ Completed page (inverse on teal) +

+
+ +
+
+
+ ), + parameters: { + docs: { + description: { + story: + "Same size as CreateFlowTopNav with inverse palette, as used on the completed page.", + }, + }, + }, +}; diff --git a/stories/modals/Create.stories.js b/stories/modals/Create.stories.js index 8b1a039..7d8ddc8 100644 --- a/stories/modals/Create.stories.js +++ b/stories/modals/Create.stories.js @@ -139,7 +139,8 @@ WithCustomHeader.args = { children: (

- When headerContent is provided, the default title and description are not shown. + When headerContent is provided, the default title and description are + not shown.

), diff --git a/stories/navigation/MenuBarItem.stories.js b/stories/navigation/MenuBarItem.stories.js index 9e5a45f..645bc38 100644 --- a/stories/navigation/MenuBarItem.stories.js +++ b/stories/navigation/MenuBarItem.stories.js @@ -136,9 +136,15 @@ export const AllModes = {

Default Mode

- X Small - Large - X Large + + X Small + + + Large + + + X Large +
@@ -173,8 +179,7 @@ export const AllModes = { parameters: { docs: { description: { - story: - "Complete overview of all menu item modes, sizes, and states.", + story: "Complete overview of all menu item modes, sizes, and states.", }, }, }, diff --git a/stories/navigation/TopNav.stories.js b/stories/navigation/TopNav.stories.js index c1b6d26..d52a538 100644 --- a/stories/navigation/TopNav.stories.js +++ b/stories/navigation/TopNav.stories.js @@ -15,11 +15,13 @@ export default { argTypes: { folderTop: { control: "boolean", - description: "When true, renders the home page variant with yellow tab container. When false, renders the standard header variant.", + description: + "When true, renders the home page variant with yellow tab container. When false, renders the standard header variant.", }, loggedIn: { control: "boolean", - description: "Whether the user is logged in (affects displayed elements).", + description: + "Whether the user is logged in (affects displayed elements).", }, profile: { control: "boolean", @@ -123,8 +125,8 @@ export const StandardInPageContext = {

This demonstrates how the standard header looks in a realistic page - context. The header maintains its responsive behavior while providing - navigation for the page content. + context. The header maintains its responsive behavior while + providing navigation for the page content.

{[1, 2, 3, 4, 5, 6].map((i) => ( diff --git a/stories/type/HeaderLockup.stories.js b/stories/type/HeaderLockup.stories.js index 4e3aa34..f8175d8 100644 --- a/stories/type/HeaderLockup.stories.js +++ b/stories/type/HeaderLockup.stories.js @@ -44,8 +44,7 @@ export const SizeM = { export const CenterJustified = { args: { title: "How should conflicts be resolved?", - description: - "You can also combine or add new approaches to the list", + description: "You can also combine or add new approaches to the list", justification: "center", size: "L", }, diff --git a/stories/utility/CreateFlowFooter.stories.js b/stories/utility/CreateFlowFooter.stories.js index 4943f23..086f196 100644 --- a/stories/utility/CreateFlowFooter.stories.js +++ b/stories/utility/CreateFlowFooter.stories.js @@ -20,7 +20,8 @@ export default { }, secondButton: { control: false, - description: "The second button (typically Next) to display on the right side", + description: + "The second button (typically Next) to display on the right side", }, }, tags: ["autodocs"], diff --git a/tests/components/CompletedPage.test.tsx b/tests/components/CompletedPage.test.tsx new file mode 100644 index 0000000..51d8ed2 --- /dev/null +++ b/tests/components/CompletedPage.test.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { describe, it, expect } from "vitest"; +import { renderWithProviders as render, screen } from "../utils/test-utils"; +import "@testing-library/jest-dom/vitest"; +import CompletedPage from "../../app/create/completed/page"; + +describe("CompletedPage", () => { + it("renders without crashing", () => { + render(); + expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument(); + }); + + it("renders HeaderLockup with expected title", () => { + render(); + expect( + screen.getByRole("heading", { + name: "Mutual Aid Mondays", + }), + ).toBeInTheDocument(); + }); + + it("renders HeaderLockup with expected description", () => { + render(); + expect( + screen.getByText( + /Mutual Aid Monday is a grassroots community in Denver, founded in November 2020 by Kelsang Virya, dedicated to supporting neighbors experiencing homelessness./i, + ), + ).toBeInTheDocument(); + }); + + it("renders Community Rule document with section labels", () => { + render(); + expect(screen.getByText("Values")).toBeInTheDocument(); + expect(screen.getByText("Communication")).toBeInTheDocument(); + expect(screen.getByText("Membership")).toBeInTheDocument(); + expect(screen.getByText("Decision-making")).toBeInTheDocument(); + expect(screen.getByText("Conflict management")).toBeInTheDocument(); + }); + + it("renders document entry titles", () => { + render(); + expect(screen.getByText("Solidarity Forever")).toBeInTheDocument(); + expect(screen.getByText("Shared Leadership")).toBeInTheDocument(); + expect(screen.getByText("Organizing Offline")).toBeInTheDocument(); + expect(screen.getByText("Circular Food Systems")).toBeInTheDocument(); + }); + + it("renders toast alert when page loads", () => { + render(); + expect( + screen.getByText( + "This is what folks see when you share your CommunityRule", + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + "Your group can use this document as an operating manual.", + ), + ).toBeInTheDocument(); + }); + + it("renders toast with role status", () => { + render(); + const statusRegions = screen.getAllByRole("status"); + expect(statusRegions.length).toBeGreaterThanOrEqual(1); + expect( + statusRegions.some((el) => + el.textContent?.includes("This is what folks see when you share"), + ), + ).toBe(true); + }); +}); diff --git a/tests/components/Logo.test.tsx b/tests/components/Logo.test.tsx index ad6fde4..ffcbd66 100644 --- a/tests/components/Logo.test.tsx +++ b/tests/components/Logo.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; -import Logo from "../../app/components/icons/Logo"; +import Logo from "../../app/components/asset/logo"; import { componentTestSuite, ComponentTestSuiteConfig, @@ -45,13 +45,22 @@ describe("Logo (behavioral tests)", () => { expect(screen.getByText("CommunityRule")).toBeInTheDocument(); }); - it("hides text when showText is false", () => { - const { container } = render(); + it("hides wordmark when wordmark is false", () => { + const { container } = render(); const textElement = container.querySelector(".hidden"); expect(textElement).toBeInTheDocument(); expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument(); }); + it("applies inverse palette styling when palette is inverse", () => { + render(); + const link = screen.getByRole("link"); + const textEl = link.querySelector(".font-bricolage-grotesque"); + const img = link.querySelector("img"); + expect(textEl).toHaveClass("text-[var(--color-content-invert-primary)]"); + expect(img).toHaveClass("brightness-0"); + }); + it("renders with different size variants", () => { const { rerender } = render(); expect(screen.getByRole("link")).toBeInTheDocument(); diff --git a/tests/pages/user-journey.test.jsx b/tests/pages/user-journey.test.jsx index e1bcbbc..955526f 100644 --- a/tests/pages/user-journey.test.jsx +++ b/tests/pages/user-journey.test.jsx @@ -17,9 +17,7 @@ vi.mock("next/dynamic", () => { function DynamicWrapper(props) { const [Component, setComponent] = React.useState(null); React.useEffect(() => { - importFn().then((mod) => - setComponent(() => mod.default || mod), - ); + importFn().then((mod) => setComponent(() => mod.default || mod)); }, []); if (!Component) { return options?.loading ? options.loading() : null;