Full cleanup pass

This commit is contained in:
adilallo
2026-05-21 23:25:56 -06:00
parent 28de8ef3bc
commit 99f535f821
149 changed files with 2623 additions and 1242 deletions
+9 -4
View File
@@ -1,5 +1,8 @@
"use client";
import { memo } from "react";
import Link from "next/link";
import { useTranslation } from "../../../contexts/MessagesContext";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
interface LogoProps {
@@ -31,6 +34,8 @@ interface SizeConfig {
const Logo = memo<LogoProps>(
({ size = "default", palette = "default", wordmark = true }) => {
const t = useTranslation("controlsChrome");
// Size configurations
const sizes: Record<string, SizeConfig> = {
default: {
@@ -97,7 +102,7 @@ const Logo = memo<LogoProps>(
: "hidden";
return (
<Link href="/" className="block" aria-label="CommunityRule Logo">
<Link href="/" className="block" aria-label={t("logoAlt")}>
<div
className={`flex items-center ${config.containerHeight} ${
wordmark ? config.gap : ""
@@ -106,16 +111,16 @@ const Logo = memo<LogoProps>(
{/* Logo Text - responsive visibility for topNav sizes */}
<div
className={`font-bricolage-grotesque ${textColorClass} ${config.textSize} ${config.lineHeight} font-normal tracking-[0px] transition-colors duration-200 ${wordmarkVisibilityClass}`}
aria-label="CommunityRule"
aria-label={t("logoText")}
>
CommunityRule
{t("logoText")}
</div>
{/* Vector Icon */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getAssetPath(ASSETS.LOGO)}
alt="CommunityRule Logo Icon"
alt={t("logoAlt")}
width={27.05}
height={27.05}
className={`flex-shrink-0 ${config.iconSize} transition-all duration-200 ${
@@ -1,11 +1,11 @@
"use client";
import { memo, useCallback, useState } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { CardStackView } from "./CardStack.view";
import type { CardStackProps } from "./CardStack.types";
const DEFAULT_TOGGLE_LABEL = "See all communication approaches";
const DEFAULT_SHOW_LESS_LABEL = "Show less";
/**
* Figma: "Utility / CardStack"; canonical code under `cards/`.
@@ -22,7 +22,7 @@ const CardStackContainer = memo<CardStackProps>(
onToggleExpand: controlledOnToggleExpand,
hasMore = true,
toggleLabel = DEFAULT_TOGGLE_LABEL,
showLessLabel = DEFAULT_SHOW_LESS_LABEL,
showLessLabel,
title = "",
description = "",
layout = "default",
@@ -37,6 +37,7 @@ const CardStackContainer = memo<CardStackProps>(
addCardAriaLabel = "",
onAddCard,
}) => {
const t = useTranslation("controlsChrome");
const [internalExpanded, setInternalExpanded] = useState(false);
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>(
[],
@@ -84,7 +85,7 @@ const CardStackContainer = memo<CardStackProps>(
onToggleExpand={handleToggleExpand}
hasMore={hasMore}
toggleLabel={toggleLabel}
showLessLabel={showLessLabel}
showLessLabel={showLessLabel ?? t("cardStackShowLess")}
title={title}
description={description}
layout={layout}
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Card / Icon" (see registry)
*/
import { memo, useId } from "react";
import { IconView } from "./Icon.view";
import type { IconProps } from "./Icon.types";
+9 -2
View File
@@ -1,6 +1,11 @@
"use client";
/**
* Figma: "Card / Mini" (see registry)
*/
import { memo, useMemo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import MiniView from "./Mini.view";
import type { MiniProps } from "./Mini.types";
@@ -17,14 +22,16 @@ const MiniContainer = memo<MiniProps>(
href,
ariaLabel,
}) => {
const t = useTranslation("controlsChrome");
// Compute aria-label
const computedAriaLabel = useMemo(
() =>
ariaLabel ||
(labelLine1 && labelLine2
? `${labelLine1} ${labelLine2}`
: label || "Feature card"),
[ariaLabel, labelLine1, labelLine2, label],
: label || t("miniFeatureFallback")),
[ariaLabel, labelLine1, labelLine2, label, t],
);
// Determine wrapper element and props
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Card / Stat" (21598-18215)
*/
import { memo } from "react";
import StatView from "./Stat.view";
import type { StatProps } from "./Stat.types";
@@ -20,7 +20,7 @@ function ContentContainerView({
return (
<div
className={containerClasses}
style={size === "responsive" || size === "xs" ? {} : { width }}
style={size === "xs" ? {} : { width }}
>
{/* Content Container - gap between icon and text */}
<div className={contentGapClasses}>
@@ -1,6 +1,7 @@
"use client";
import { memo, useState, useEffect, useRef } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import ChipView from "./Chip.view";
import type { ChipProps } from "./Chip.types";
@@ -22,6 +23,7 @@ const ChipContainer = memo<ChipProps>(
onClose,
ariaLabel,
}) => {
const t = useTranslation("controlsChrome");
const state = stateProp;
const palette = paletteProp;
const size = sizeProp;
@@ -92,6 +94,9 @@ const ChipContainer = memo<ChipProps>(
onInputKeyDown={isCustom ? handleKeyDown : undefined}
inputRef={isCustom ? inputRef : undefined}
ariaLabel={ariaLabel}
confirmAriaLabel={t("chipConfirm")}
typeToAddPlaceholder={t("chipTypeToAdd")}
closeAriaLabel={t("chipClose")}
/>
);
},
@@ -68,4 +68,7 @@ export interface ChipViewProps {
onInputKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
inputRef?: React.RefObject<HTMLInputElement>;
ariaLabel?: string;
confirmAriaLabel: string;
typeToAddPlaceholder: string;
closeAriaLabel: string;
}
+6 -3
View File
@@ -19,6 +19,9 @@ function ChipView({
onInputKeyDown,
inputRef,
ariaLabel,
confirmAriaLabel,
typeToAddPlaceholder,
closeAriaLabel,
}: ChipViewProps) {
// The container is the source of truth for `disabled`. This allows
// `state="disabled"` to be used purely as a visual (for toggle-group chips
@@ -167,7 +170,7 @@ function ChipView({
<button
type="button"
className="flex items-center justify-center p-[var(--measures-spacing-150,6px)] rounded-full hover:bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.1))] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Confirm"
aria-label={confirmAriaLabel}
disabled={!inputValue || !inputValue.trim()}
onClick={(event) => {
event.stopPropagation();
@@ -204,7 +207,7 @@ function ChipView({
value={inputValue ?? ""}
onChange={(e) => onInputChange?.(e.target.value)}
onKeyDown={onInputKeyDown}
placeholder="Type to add"
placeholder={typeToAddPlaceholder}
className="bg-transparent border-none outline-none flex-1 min-w-0 font-inter font-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[color:var(--color-content-default-tertiary,#b4b4b4)]"
style={{
fontSize: isSmall
@@ -222,7 +225,7 @@ function ChipView({
<button
type="button"
className="flex items-center justify-center p-[var(--measures-spacing-150,6px)] rounded-full hover:bg-[var(--color-surface-default-semi-opaque,rgba(0,0,0,0.1))] transition-colors"
aria-label="Close"
aria-label={closeAriaLabel}
onClick={(event) => {
event.stopPropagation();
onClose(event);
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import MultiSelectView from "./MultiSelect.view";
import type { MultiSelectProps } from "./MultiSelect.types";
@@ -18,12 +19,13 @@ const MultiSelectContainer = memo<MultiSelectProps>(
onChipClick,
onAddClick,
addButton: addButtonProp = true,
addButtonText = "Add organization type",
addButtonText,
formHeader = true,
onCustomChipConfirm,
onCustomChipClose,
className = "",
}) => {
const t = useTranslation("controlsChrome");
const size = sizeProp;
const palette = paletteProp;
@@ -38,6 +40,9 @@ const MultiSelectContainer = memo<MultiSelectProps>(
onAddClick={onAddClick}
addButton={addButtonProp}
addButtonText={addButtonText}
addButtonAriaLabel={
addButtonText || t("multiSelectAddFallback")
}
formHeader={formHeader}
onCustomChipConfirm={onCustomChipConfirm}
onCustomChipClose={onCustomChipClose}
@@ -74,7 +74,8 @@ export interface MultiSelectViewProps {
onChipClick?: (chipId: string) => void;
onAddClick?: () => void;
addButton: boolean;
addButtonText: string;
addButtonText?: string;
addButtonAriaLabel: string;
formHeader: boolean;
onCustomChipConfirm?: (chipId: string, value: string) => void;
onCustomChipClose?: (chipId: string) => void;
@@ -15,6 +15,7 @@ function MultiSelectView({
onAddClick,
addButton,
addButtonText,
addButtonAriaLabel,
formHeader = true,
onCustomChipConfirm,
onCustomChipClose,
@@ -81,7 +82,7 @@ function MultiSelectView({
{addButton && (
<button
type="button"
aria-label={addButtonText || "Add option"}
aria-label={addButtonAriaLabel}
onClick={(e) => {
e.stopPropagation();
onAddClick?.();
@@ -5,10 +5,11 @@ import { forwardRef, memo } from "react";
interface SelectDropdownProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
children?: React.ReactNode;
ariaLabel: string;
}
const SelectDropdown = forwardRef<HTMLDivElement, SelectDropdownProps>(
({ className = "", children, ...props }, ref) => {
({ className = "", children, ariaLabel, ...props }, ref) => {
const menuClasses = `
bg-black
border border-[var(--color-border-default-tertiary)]
@@ -27,7 +28,7 @@ const SelectDropdown = forwardRef<HTMLDivElement, SelectDropdownProps>(
ref={ref}
className={menuClasses}
role="listbox"
aria-label="Select an option"
aria-label={ariaLabel}
style={{ backgroundColor: "#000000" }}
{...props}
>
@@ -14,6 +14,7 @@ import React, {
useEffect,
} from "react";
import { useClickOutside } from "../../../hooks";
import { useTranslation } from "../../../contexts/MessagesContext";
import { SelectInputView } from "./SelectInput.view";
import type { SelectInputProps } from "./SelectInput.types";
@@ -38,7 +39,7 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
textHint = false,
disabled = false,
error = false,
placeholder = "Choose an option",
placeholder,
className = "",
children,
value,
@@ -48,6 +49,9 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
},
ref,
) => {
const t = useTranslation("controlsChrome");
const resolvedPlaceholder = placeholder ?? t("selectPlaceholder");
// Determine if label should be shown
const shouldShowLabel =
showLabel !== undefined ? showLabel : labelText !== undefined;
@@ -181,13 +185,13 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
// Get display text for selected value
const getDisplayText = (): string => {
if (!selectedValue) return placeholder;
if (!selectedValue) return resolvedPlaceholder;
if (options && Array.isArray(options)) {
const selectedOption = options.find(
(option) => option.value === selectedValue,
);
return selectedOption ? selectedOption.label : placeholder;
return selectedOption ? selectedOption.label : resolvedPlaceholder;
}
const selectedOption = Children.toArray(children).find(
@@ -207,13 +211,13 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
);
return selectedOption
? String(selectedOption.props.children)
: placeholder;
: resolvedPlaceholder;
};
return (
<SelectInputView
label={shouldShowLabel ? labelText : undefined}
placeholder={placeholder}
placeholder={resolvedPlaceholder}
state={actualState}
disabled={disabled}
error={error}
@@ -241,6 +245,8 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
textData={textData}
iconRight={iconRight}
textHint={textHint}
selectAriaLabel={t("selectAriaLabel")}
hintDefault={t("hintDefault")}
{...props}
/>
);
@@ -40,6 +40,8 @@ export interface SelectInputViewProps {
textData?: boolean;
iconRight?: boolean;
textHint?: boolean;
selectAriaLabel: string;
hintDefault: string;
}
export function SelectInputView({
@@ -72,6 +74,8 @@ export function SelectInputView({
textData = true,
iconRight = true,
textHint = false,
selectAriaLabel,
hintDefault,
}: SelectInputViewProps) {
// Styles based on Figma design
const containerClasses = "flex flex-col gap-[8px]";
@@ -222,7 +226,7 @@ export function SelectInputView({
ref={menuRef}
className="absolute top-full left-0 right-0 z-50 mt-1"
>
<SelectDropdown>
<SelectDropdown ariaLabel={selectAriaLabel}>
{options && Array.isArray(options)
? options.map((option) => (
<SelectOption
@@ -268,7 +272,7 @@ export function SelectInputView({
{textHint && (
<div className="flex items-start relative shrink-0 w-full">
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
Hint text here
{hintDefault}
</p>
</div>
)}
@@ -1,6 +1,7 @@
"use client";
import { memo, useCallback, useId, forwardRef } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { SwitchView } from "./Switch.view";
import type { SwitchProps } from "./Switch.types";
@@ -10,6 +11,7 @@ import type { SwitchProps } from "./Switch.types";
*/
const SwitchContainer = memo(
forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
const t = useTranslation("controlsChrome");
const {
propSwitch = false,
onChange,
@@ -154,6 +156,7 @@ const SwitchContainer = memo(
trackClasses={trackClasses}
thumbClasses={thumbClasses}
labelClasses={labelClasses}
switchAriaLabel={text ?? t("toggleSwitch")}
onClick={handleClick}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
@@ -37,6 +37,7 @@ export interface SwitchViewProps {
trackClasses: string;
thumbClasses: string;
labelClasses: string;
switchAriaLabel: string;
onClick: (_e: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
onFocus: (_e: React.FocusEvent<HTMLButtonElement>) => void;
@@ -11,6 +11,7 @@ export const SwitchView = forwardRef<HTMLButtonElement, SwitchViewProps>(
trackClasses,
thumbClasses,
labelClasses,
switchAriaLabel,
onClick,
onKeyDown,
onFocus,
@@ -27,7 +28,7 @@ export const SwitchView = forwardRef<HTMLButtonElement, SwitchViewProps>(
type="button"
role="switch"
aria-checked={propSwitch}
aria-label={text || "Toggle switch"}
aria-label={switchAriaLabel}
onClick={onClick}
onKeyDown={onKeyDown}
onFocus={onFocus}
@@ -2,6 +2,7 @@
import { memo, forwardRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { useTranslation } from "../../../contexts/MessagesContext";
import { TextAreaView } from "./TextArea.view";
import type { TextAreaProps } from "./TextArea.types";
@@ -35,6 +36,7 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
},
ref,
) => {
const t = useTranslation("controlsChrome");
const size = sizeProp;
const labelVariant = labelVariantProp;
const state = stateProp;
@@ -200,6 +202,8 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
formHeader={formHeader}
showHelpIcon={showHelpIcon}
appearance={appearance}
helpIconAlt={t("helpIconAlt")}
hintDefault={t("hintDefault")}
{...props}
/>
);
@@ -79,4 +79,6 @@ export interface TextAreaViewProps {
formHeader?: boolean;
showHelpIcon?: boolean;
appearance?: "default" | "embedded";
helpIconAlt: string;
hintDefault: string;
}
@@ -25,6 +25,8 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
formHeader = true,
showHelpIcon = false,
appearance: _appearance,
helpIconAlt,
hintDefault,
// Component-only props: do not pass to DOM
size: _size,
labelVariant: _labelVariant,
@@ -51,7 +53,7 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
alt={helpIconAlt}
className="block max-w-none size-full"
/>
</div>
@@ -81,7 +83,7 @@ export const TextAreaView = forwardRef<HTMLTextAreaElement, TextAreaViewProps>(
{textHint ? (
<div className="flex items-start relative shrink-0 w-full">
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
{typeof textHint === "string" ? textHint : "Hint text here"}
{typeof textHint === "string" ? textHint : hintDefault}
</p>
</div>
) : null}
@@ -2,6 +2,7 @@
import { memo, forwardRef, useState, useRef } from "react";
import { useComponentId, useFormField } from "../../../hooks";
import { useTranslation } from "../../../contexts/MessagesContext";
import { TextInputView } from "./TextInput.view";
import type { TextInputProps } from "./TextInput.types";
@@ -34,6 +35,7 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
},
ref,
) => {
const t = useTranslation("controlsChrome");
const externalState = externalStateProp;
const inputSize = inputSizeProp;
@@ -244,6 +246,8 @@ const TextInputContainer = forwardRef<HTMLInputElement, TextInputProps>(
textHint={textHint}
formHeader={formHeader}
maxLength={maxLength}
helpIconAlt={t("helpIconAlt")}
hintDefault={t("hintDefault")}
{...props}
/>
);
@@ -65,4 +65,6 @@ export interface TextInputViewProps {
textHint?: boolean | string;
formHeader?: boolean;
maxLength?: number;
helpIconAlt: string;
hintDefault: string;
}
@@ -29,6 +29,8 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
textHint = false,
formHeader = true,
maxLength,
helpIconAlt,
hintDefault,
},
ref,
) => {
@@ -49,7 +51,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
alt={helpIconAlt}
className="block max-w-none size-full"
/>
</div>
@@ -83,7 +85,7 @@ export const TextInputView = forwardRef<HTMLInputElement, TextInputViewProps>(
{textHint && (
<div className="flex items-start relative shrink-0 w-full">
<p className="flex-[1_0_0] font-inter font-normal leading-[16px] min-h-px min-w-px relative text-[color:var(--color-content-default-tertiary,#b4b4b4)] text-[length:var(--sizing-300,12px)]">
{typeof textHint === "string" ? textHint : "Hint text here"}
{typeof textHint === "string" ? textHint : hintDefault}
</p>
</div>
)}
@@ -1,6 +1,7 @@
"use client";
import { memo, useCallback, useId, forwardRef } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { ToggleGroupView } from "./ToggleGroup.view";
import type { ToggleGroupProps } from "./ToggleGroup.types";
@@ -10,6 +11,7 @@ import type { ToggleGroupProps } from "./ToggleGroup.types";
*/
const ToggleGroupContainer = memo(
forwardRef<HTMLButtonElement, ToggleGroupProps>((props, _ref) => {
const t = useTranslation("controlsChrome");
const {
children,
className = "",
@@ -131,6 +133,7 @@ const ToggleGroupContainer = memo(
state={state}
showText={showText}
ariaLabel={ariaLabel}
defaultToggleOptionAriaLabel={t("toggleOption")}
toggleClasses={toggleClasses}
onClick={handleClick}
onKeyDown={handleKeyDown}
@@ -35,6 +35,7 @@ export interface ToggleGroupViewProps {
state: "default" | "hover" | "focus" | "selected";
showText: boolean;
ariaLabel?: string;
defaultToggleOptionAriaLabel: string;
toggleClasses: string;
onClick: (_e: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown: (_e: React.KeyboardEvent<HTMLButtonElement>) => void;
@@ -8,6 +8,7 @@ export function ToggleGroupView({
state: _state,
showText,
ariaLabel,
defaultToggleOptionAriaLabel,
toggleClasses,
onClick,
onKeyDown,
@@ -20,7 +21,7 @@ export function ToggleGroupView({
id={groupId}
type="button"
role="button"
aria-label={ariaLabel || (showText ? undefined : "Toggle option")}
aria-label={ariaLabel || (showText ? undefined : defaultToggleOptionAriaLabel)}
onClick={onClick}
onKeyDown={onKeyDown}
onFocus={onFocus}
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import UploadView from "./Upload.view";
import type { UploadProps } from "./Upload.types";
@@ -13,16 +14,20 @@ const UploadContainer = memo<UploadProps>(
active = true,
label,
showHelpIcon = true,
hintText = "Add image from your device",
hintText,
onClick,
className = "",
}) => {
const t = useTranslation("controlsChrome");
return (
<UploadView
active={active}
label={label}
showHelpIcon={showHelpIcon}
hintText={hintText}
hintText={hintText ?? t("uploadHintDefault")}
uploadButtonLabel={t("uploadButton")}
uploadAriaLabel={t("uploadAriaLabel")}
onClick={onClick}
className={className}
/>
@@ -35,6 +35,8 @@ export interface UploadViewProps {
label?: string;
showHelpIcon: boolean;
hintText: string;
uploadButtonLabel: string;
uploadAriaLabel: string;
onClick?: () => void;
className: string;
}
@@ -9,6 +9,8 @@ function UploadView({
label,
showHelpIcon = true,
hintText,
uploadButtonLabel,
uploadAriaLabel,
onClick,
className = "",
}: UploadViewProps) {
@@ -56,7 +58,7 @@ function UploadView({
type="button"
onClick={onClick}
className={`${buttonBgClass} flex gap-[var(--measures-spacing-150,6px)] items-center justify-center overflow-clip px-[var(--space-400,16px)] py-[var(--measures-spacing-300,12px)] rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`}
aria-label="Upload"
aria-label={uploadAriaLabel}
>
{/* Upload icon */}
<div className={`relative shrink-0 size-[20px] ${iconColor}`}>
@@ -98,7 +100,7 @@ function UploadView({
<div
className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}
>
<p className="leading-[20px]">Upload</p>
<p className="leading-[20px]">{uploadButtonLabel}</p>
</div>
</button>
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "localization/LanguageSwitcher" (see registry)
*/
import { memo } from "react";
import LanguageSwitcherView from "./LanguageSwitcher.view";
import type { LanguageSwitcherProps } from "./LanguageSwitcher.types";
@@ -6,6 +6,7 @@
*/
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { AlertView } from "./Alert.view";
import type { AlertProps } from "./Alert.types";
@@ -74,6 +75,7 @@ const AlertContainer = memo<AlertProps>(
onClose,
className = "",
}) => {
const t = useTranslation("controlsChrome");
const status = statusProp;
const type = typeProp;
const size = sizeProp;
@@ -175,6 +177,7 @@ const AlertContainer = memo<AlertProps>(
iconColor={statusStyles.iconColor}
closeButtonIconColor={statusStyles.closeButtonIconColor}
onClose={onClose}
closeAlertAriaLabel={t("closeAlert")}
/>
);
},
@@ -57,4 +57,5 @@ export interface AlertViewProps {
iconColor: string;
closeButtonIconColor: string;
onClose?: () => void;
closeAlertAriaLabel: string;
}
+2 -1
View File
@@ -17,6 +17,7 @@ export function AlertView({
iconColor,
closeButtonIconColor,
onClose,
closeAlertAriaLabel,
}: AlertViewProps) {
const getIcon = () => {
// Use the Icon_Alert.svg with dynamic fill color
@@ -61,7 +62,7 @@ export function AlertView({
palette="default"
size="large"
onClick={onClose}
ariaLabel="Close alert"
ariaLabel={closeAlertAriaLabel}
className="shrink-0 [&_svg_path]:transition-colors [&_svg_path]:duration-200 hover:[&_svg_path]:fill-[var(--color-content-default-primary)]"
>
<svg
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Modal / Create" (20874-172292)
*/
import { memo, useRef } from "react";
import { CreateView } from "./Create.view";
import type { CreateProps } from "./Create.types";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Dialog" (see registry)
*/
import { memo, useId, useRef } from "react";
import { useCreateModalA11y } from "../Create/useCreateModalA11y";
import { DialogView } from "./Dialog.view";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Modal / Login" (see registry)
*/
import { memo, useEffect, useLayoutEffect, useRef, useState } from "react";
import { LoginView } from "./Login.view";
import type { LoginProps } from "./Login.types";
@@ -1,6 +1,7 @@
"use client";
import { memo, useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { ModalHeaderView } from "./ModalHeader.view";
import type { ModalHeaderProps } from "./ModalHeader.types";
@@ -10,7 +11,14 @@ import type { ModalHeaderProps } from "./ModalHeader.types";
* (right) icon buttons.
*/
const ModalHeaderContainer = memo<ModalHeaderProps>((props) => {
const { menuItems = [] } = props;
const t = useTranslation("controlsChrome");
const {
closeButtonAriaLabel = t("closeDialog"),
moreOptionsAriaLabel = t("moreOptions"),
menuAriaLabel = t("moreOptionsMenu"),
menuItems = [],
...rest
} = props;
const hasMenu = menuItems.length > 0;
const [menuOpen, setMenuOpen] = useState(false);
const menuId = useId();
@@ -44,7 +52,11 @@ const ModalHeaderContainer = memo<ModalHeaderProps>((props) => {
return (
<div ref={menuWrapRef}>
<ModalHeaderView
{...props}
{...rest}
menuItems={menuItems}
closeButtonAriaLabel={closeButtonAriaLabel}
moreOptionsAriaLabel={moreOptionsAriaLabel}
menuAriaLabel={menuAriaLabel}
menuId={menuId}
menuOpen={menuOpen}
onToggleMenu={hasMenu ? () => setMenuOpen((open) => !open) : undefined}
@@ -11,9 +11,9 @@ export function ModalHeaderView({
onMoreOptions,
showCloseButton = true,
showMoreOptionsButton = true,
closeButtonAriaLabel = "Close dialog",
moreOptionsAriaLabel = "More options",
menuAriaLabel = "More options menu",
closeButtonAriaLabel,
moreOptionsAriaLabel,
menuAriaLabel,
menuItems = [],
menuId,
menuOpen = false,
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Modal / Tooltip" (see registry)
*/
import { memo, useState } from "react";
import { TooltipView } from "./Tooltip.view";
import type { TooltipProps } from "./Tooltip.types";
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { CreateFlowFooterView } from "./CreateFlowFooter.view";
import type { CreateFlowFooterProps } from "./CreateFlowFooter.types";
@@ -16,7 +17,10 @@ const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
proportionBarVariant,
onBackClick,
className = "",
footerAriaLabel,
}) => {
const t = useTranslation("controlsChrome");
return (
<CreateFlowFooterView
secondButton={secondButton}
@@ -25,6 +29,7 @@ const CreateFlowFooterContainer = memo<CreateFlowFooterProps>(
proportionBarVariant={proportionBarVariant}
onBackClick={onBackClick}
className={className}
footerAriaLabel={footerAriaLabel ?? t("createFlowFooterAriaLabel")}
/>
);
},
@@ -36,4 +36,8 @@ export interface CreateFlowFooterProps {
* Additional CSS classes
*/
className?: string;
/**
* Accessible name for the footer landmark.
*/
footerAriaLabel?: string;
}
@@ -9,13 +9,14 @@ export function CreateFlowFooterView({
proportionBarVariant: proportionBarVariantProp,
onBackClick,
className = "",
footerAriaLabel,
}: CreateFlowFooterProps) {
const proportionBarVariant = proportionBarVariantProp ?? "default";
return (
<footer
className={`bg-black w-full ${className}`}
role="contentinfo"
aria-label="Create Flow Footer"
aria-label={footerAriaLabel}
>
{/* Progress Bar - Top */}
{progressBar && (
@@ -1,10 +1,14 @@
"use client";
import { memo } from "react";
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useCreateFlowSm2Up } from "../../../(app)/create/hooks/useCreateFlowSm2Up";
import { useTranslation } from "../../../contexts/MessagesContext";
import { CreateFlowTopNavView } from "./CreateFlowTopNav.view";
import type { CreateFlowTopNavProps } from "./CreateFlowTopNav.types";
import type {
CreateFlowTopNavActionMenuItem,
CreateFlowTopNavProps,
} from "./CreateFlowTopNav.types";
/**
* Figma: Utility / CreateFlowTopNav — wizard header (create-flow chrome).
@@ -34,15 +38,168 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
const router = useRouter();
const t = useTranslation("create.topNav");
const tPopover = useTranslation("modals.popoverExport");
const sm2Up = useCreateFlowSm2Up();
const exitButtonText =
exitLabel ?? (saveDraftOnExit ? t("saveAndExit") : t("exit"));
const [exportMenuOpen, setExportMenuOpen] = useState(false);
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
const exportWrapRef = useRef<HTMLDivElement>(null);
const actionsWrapRef = useRef<HTMLDivElement>(null);
const exportMenuId = useId();
const actionsMenuId = useId();
const handleExit = (options?: { saveDraft?: boolean }) => {
if (onExit) {
onExit(options);
} else {
// Default behavior: navigate to home
router.push("/");
const handleExit = useCallback(
(options?: { saveDraft?: boolean }) => {
if (onExit) {
onExit(options);
} else {
// Default behavior: navigate to home
router.push("/");
}
},
[onExit, router],
);
const hasSecondaryActions =
hasShare ||
hasExport ||
hasEdit ||
hasDuplicate ||
hasManageStakeholders;
const useKebabMenu = hasSecondaryActions && !sm2Up;
const actionMenuItems = useMemo((): CreateFlowTopNavActionMenuItem[] => {
const items: CreateFlowTopNavActionMenuItem[] = [];
if (hasShare && onShare) {
items.push({
id: "share",
label: t("share"),
leadingIcon: "mail",
onClick: onShare,
});
}
};
if (hasExport && onSelectExportFormat) {
items.push(
{
id: "export-pdf",
label: tPopover("downloadPdf"),
leadingIcon: "picture_as_pdf",
onClick: () => onSelectExportFormat("pdf"),
},
{
id: "export-csv",
label: tPopover("downloadCsv"),
leadingIcon: "csv",
onClick: () => onSelectExportFormat("csv"),
},
{
id: "export-markdown",
label: tPopover("downloadMarkdown"),
leadingIcon: "markdown_copy",
onClick: () => onSelectExportFormat("markdown"),
},
);
}
if (hasDuplicate && onDuplicate) {
items.push({
id: "duplicate",
label: duplicateLabel ?? t("edit"),
leadingIcon: "content_copy",
onClick: onDuplicate,
});
} else if (hasEdit && onEdit) {
items.push({
id: "edit",
label: t("edit"),
leadingIcon: "edit",
onClick: onEdit,
});
}
if (hasManageStakeholders && onManageStakeholders) {
items.push({
id: "manage-stakeholders",
label: t("manageStakeholders"),
leadingIcon: "tags",
onClick: onManageStakeholders,
});
}
items.push({
id: "exit",
label: exitButtonText,
leadingIcon: "log_out",
onClick: () => void handleExit({ saveDraft: saveDraftOnExit }),
});
return items;
}, [
duplicateLabel,
exitButtonText,
handleExit,
hasDuplicate,
hasEdit,
hasExport,
hasManageStakeholders,
hasShare,
onDuplicate,
onEdit,
onManageStakeholders,
onSelectExportFormat,
onShare,
saveDraftOnExit,
t,
tPopover,
]);
useEffect(() => {
if (!exportMenuOpen) return;
const onDoc = (e: MouseEvent) => {
if (
exportWrapRef.current &&
!exportWrapRef.current.contains(e.target as Node)
) {
setExportMenuOpen(false);
}
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [exportMenuOpen]);
useEffect(() => {
if (!actionsMenuOpen) return;
const onDoc = (e: MouseEvent) => {
if (
actionsWrapRef.current &&
!actionsWrapRef.current.contains(e.target as Node)
) {
setActionsMenuOpen(false);
}
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [actionsMenuOpen]);
useEffect(() => {
if (!exportMenuOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setExportMenuOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [exportMenuOpen]);
useEffect(() => {
if (!actionsMenuOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setActionsMenuOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [actionsMenuOpen]);
return (
<CreateFlowTopNavView
@@ -63,6 +220,17 @@ const CreateFlowTopNavContainer = memo<CreateFlowTopNavProps>(
duplicateAriaLabel={duplicateAriaLabel}
buttonPalette={buttonPalette}
className={className}
exitButtonText={exitButtonText}
useKebabMenu={useKebabMenu}
exportMenuOpen={exportMenuOpen}
setExportMenuOpen={setExportMenuOpen}
actionsMenuOpen={actionsMenuOpen}
setActionsMenuOpen={setActionsMenuOpen}
exportWrapRef={exportWrapRef}
actionsWrapRef={actionsWrapRef}
exportMenuId={exportMenuId}
actionsMenuId={actionsMenuId}
actionMenuItems={actionMenuItems}
exportPopoverMenuAriaLabel={tPopover("menuAriaLabel")}
exportPopoverPdfLabel={tPopover("downloadPdf")}
exportPopoverCsvLabel={tPopover("downloadCsv")}
@@ -5,6 +5,16 @@
* Includes logo and action buttons (Share, Export, Edit, Exit).
*/
import type { Dispatch, RefObject, SetStateAction } from "react";
import type { IconName } from "../../asset/icon";
export type CreateFlowTopNavActionMenuItem = {
id: string;
label: string;
leadingIcon: IconName;
onClick: () => void;
};
export interface CreateFlowTopNavProps {
/**
* Whether to show the Share button
@@ -81,8 +91,19 @@ export interface CreateFlowTopNavProps {
className?: string;
}
/** Resolved copy for the export popover; supplied by the container. */
/** Resolved copy and menu state; supplied by the container. */
export type CreateFlowTopNavViewProps = CreateFlowTopNavProps & {
exitButtonText: string;
useKebabMenu: boolean;
exportMenuOpen: boolean;
setExportMenuOpen: Dispatch<SetStateAction<boolean>>;
actionsMenuOpen: boolean;
setActionsMenuOpen: Dispatch<SetStateAction<boolean>>;
exportWrapRef: RefObject<HTMLDivElement | null>;
actionsWrapRef: RefObject<HTMLDivElement | null>;
exportMenuId: string;
actionsMenuId: string;
actionMenuItems: CreateFlowTopNavActionMenuItem[];
exportPopoverMenuAriaLabel: string;
exportPopoverPdfLabel: string;
exportPopoverCsvLabel: string;
@@ -1,12 +1,9 @@
"use client";
import { useEffect, useId, useMemo, useRef, useState } from "react";
import type { IconName } from "../../asset/icon";
import Logo from "../../asset/Logo";
import Button from "../../buttons/Button";
import ListItem from "../../layout/ListItem";
import Popover from "../../modals/Popover";
import { useCreateFlowSm2Up } from "../../../(app)/create/hooks/useCreateFlowSm2Up";
import { useTranslation } from "../../../contexts/MessagesContext";
import type { CreateFlowTopNavViewProps } from "./CreateFlowTopNav.types";
@@ -16,13 +13,6 @@ const outlineButtonClass =
const exitButtonFigmaClass =
"!rounded-[var(--radius-measures-radius-full,9999px)] !border-[1.25px] !px-[var(--spacing-measures-spacing-250,10px)] !py-[var(--spacing-measures-spacing-200,8px)] md:!text-[12px] md:!leading-[14px]";
type ActionMenuItem = {
id: string;
label: string;
leadingIcon: IconName;
onClick: () => void;
};
function KebabIcon({ className = "" }: { className?: string }) {
return (
<svg
@@ -54,11 +44,21 @@ export function CreateFlowTopNavView({
onDuplicate,
onManageStakeholders,
onExit,
exitLabel,
duplicateLabel,
duplicateAriaLabel,
buttonPalette = "default",
className = "",
exitButtonText,
useKebabMenu,
exportMenuOpen,
setExportMenuOpen,
actionsMenuOpen,
setActionsMenuOpen,
exportWrapRef,
actionsWrapRef,
exportMenuId,
actionsMenuId,
actionMenuItems,
exportPopoverMenuAriaLabel,
exportPopoverPdfLabel,
exportPopoverCsvLabel,
@@ -67,15 +67,6 @@ export function CreateFlowTopNavView({
actionsMenuAriaLabel,
}: CreateFlowTopNavViewProps) {
const t = useTranslation("create.topNav");
const sm2Up = useCreateFlowSm2Up();
const exitButtonText =
exitLabel ?? (saveDraftOnExit ? t("saveAndExit") : t("exit"));
const [exportMenuOpen, setExportMenuOpen] = useState(false);
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
const exportWrapRef = useRef<HTMLDivElement>(null);
const actionsWrapRef = useRef<HTMLDivElement>(null);
const exportMenuId = useId();
const actionsMenuId = useId();
const hasSecondaryActions =
hasShare ||
@@ -83,142 +74,6 @@ export function CreateFlowTopNavView({
hasEdit ||
hasDuplicate ||
hasManageStakeholders;
const useKebabMenu = hasSecondaryActions && !sm2Up;
const actionMenuItems = useMemo((): ActionMenuItem[] => {
const items: ActionMenuItem[] = [];
if (hasShare && onShare) {
items.push({
id: "share",
label: t("share"),
leadingIcon: "mail",
onClick: onShare,
});
}
if (hasExport && onSelectExportFormat) {
items.push(
{
id: "export-pdf",
label: exportPopoverPdfLabel,
leadingIcon: "picture_as_pdf",
onClick: () => onSelectExportFormat("pdf"),
},
{
id: "export-csv",
label: exportPopoverCsvLabel,
leadingIcon: "csv",
onClick: () => onSelectExportFormat("csv"),
},
{
id: "export-markdown",
label: exportPopoverMarkdownLabel,
leadingIcon: "markdown_copy",
onClick: () => onSelectExportFormat("markdown"),
},
);
}
if (hasDuplicate && onDuplicate) {
items.push({
id: "duplicate",
label: duplicateLabel ?? t("edit"),
leadingIcon: "content_copy",
onClick: onDuplicate,
});
} else if (hasEdit && onEdit) {
items.push({
id: "edit",
label: t("edit"),
leadingIcon: "edit",
onClick: onEdit,
});
}
if (hasManageStakeholders && onManageStakeholders) {
items.push({
id: "manage-stakeholders",
label: t("manageStakeholders"),
leadingIcon: "tags",
onClick: onManageStakeholders,
});
}
items.push({
id: "exit",
label: exitButtonText,
leadingIcon: "log_out",
onClick: () => void onExit?.({ saveDraft: saveDraftOnExit }),
});
return items;
}, [
duplicateLabel,
exitButtonText,
exportPopoverCsvLabel,
exportPopoverMarkdownLabel,
exportPopoverPdfLabel,
hasDuplicate,
hasEdit,
hasExport,
hasManageStakeholders,
hasShare,
onDuplicate,
onEdit,
onExit,
onManageStakeholders,
onSelectExportFormat,
onShare,
saveDraftOnExit,
t,
]);
useEffect(() => {
if (!exportMenuOpen) return;
const onDoc = (e: MouseEvent) => {
if (
exportWrapRef.current &&
!exportWrapRef.current.contains(e.target as Node)
) {
setExportMenuOpen(false);
}
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [exportMenuOpen]);
useEffect(() => {
if (!actionsMenuOpen) return;
const onDoc = (e: MouseEvent) => {
if (
actionsWrapRef.current &&
!actionsWrapRef.current.contains(e.target as Node)
) {
setActionsMenuOpen(false);
}
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [actionsMenuOpen]);
useEffect(() => {
if (!exportMenuOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setExportMenuOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [exportMenuOpen]);
useEffect(() => {
if (!actionsMenuOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setActionsMenuOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [actionsMenuOpen]);
const inlineActions = (
<>
+2 -1
View File
@@ -14,6 +14,7 @@ import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
*/
const Footer = memo(() => {
const t = useTranslation("footer");
const tChrome = useTranslation("controlsChrome");
const linkFocusClass =
"hover:opacity-80 active:opacity-60 focus:opacity-80 focus:outline-none focus:ring-2 focus:ring-[var(--color-content-default-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface-default-primary)] transition-opacity";
@@ -129,7 +130,7 @@ const Footer = memo(() => {
</div>
<nav
aria-label="Footer"
aria-label={tChrome("footerAriaLabel")}
className="order-1 flex w-full max-w-full flex-col
items-start
gap-[var(--spacing-scale-032)]
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Utility / Menu Item" (see registry)
*/
import { memo } from "react";
import MenuItemView from "./MenuItem.view";
import type { MenuItemProps } from "./MenuItem.types";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Navigation / NavigationItem" (see registry)
*/
import { memo } from "react";
import NavigationItemView from "./NavigationItem.view";
import type { NavigationItemProps } from "./NavigationItem.types";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Navigation / Top" (22078-808559)
*/
import { memo, useCallback } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useAuthModal } from "../../../contexts/AuthModalContext";
@@ -24,11 +28,17 @@ const NAV_SIZE_TO_MENU_ITEM_SIZE: Record<NavSize, MenuClusterSize> = {
xlarge: "X Large",
};
export const avatarImages = [
{ src: getAssetPath(ASSETS.AVATAR_3), alt: "Avatar 3" },
{ src: getAssetPath(ASSETS.AVATAR_2), alt: "Avatar 2" },
{ src: getAssetPath(ASSETS.AVATAR_1), alt: "Avatar 1" },
];
export const avatarImageSources = [
getAssetPath(ASSETS.AVATAR_3),
getAssetPath(ASSETS.AVATAR_2),
getAssetPath(ASSETS.AVATAR_1),
] as const;
/** @deprecated Use `avatarImageSources` — alts are resolved in `TopContainer` via `topNav` messages. */
export const avatarImages = avatarImageSources.map((src, index) => ({
src,
alt: `Avatar ${3 - index}`,
}));
const TopContainer = memo<TopProps>(
({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => {
@@ -36,6 +46,7 @@ const TopContainer = memo<TopProps>(
const router = useRouter();
const { openLogin } = useAuthModal();
const t = useTranslation("header");
const tTopNav = useTranslation("topNav");
/**
* `Top` is hidden on `/create` routes by ConditionalNavigationClient, so
@@ -58,7 +69,7 @@ const TopContainer = memo<TopProps>(
name: "CommunityRule",
url: "https://communityrule.com",
...(folderTop && {
description: "Build operating manuals for successful communities",
description: tTopNav("schemaDescription"),
}),
potentialAction: {
"@type": "SearchAction",
@@ -110,11 +121,11 @@ const TopContainer = memo<TopProps>(
) => {
return (
<AvatarContainer size={containerSize}>
{avatarImages.map((avatar, index) => (
{avatarImageSources.map((src, index) => (
<Avatar
key={index}
src={avatar.src}
alt={avatar.alt}
src={src}
alt={tTopNav(`avatarAlts.${3 - index}`)}
size={avatarSize}
/>
))}
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Progress / Bar" (17861-33241)
*/
import { memo } from "react";
import { ProportionBarView } from "./ProportionBar.view";
import type { ProportionBarProps } from "./ProportionBar.types";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Progress / Stepper" (see registry)
*/
import { memo } from "react";
import { StepperView } from "./Stepper.view";
import type { StepperProps } from "./Stepper.types";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Sections / FeatureGrid" (see registry)
*/
import { memo, useMemo } from "react";
import { getAssetPath, featurePanelPath } from "../../../../lib/assetUtils";
import { useTranslation } from "../../../contexts/MessagesContext";
@@ -1,3 +1,7 @@
"use client";
import { useTranslation } from "../../../contexts/MessagesContext";
/**
* Placeholder grid matching GovernanceTemplateGrid layout (loading state).
*/
@@ -8,6 +12,7 @@ export function GovernanceTemplateGridSkeleton({
count: number;
twoColumnsFromMd?: boolean;
}) {
const t = useTranslation("controlsChrome");
const gridLayoutClasses = twoColumnsFromMd
? `
flex flex-col gap-[18px]
@@ -24,7 +29,7 @@ export function GovernanceTemplateGridSkeleton({
<div
className={gridLayoutClasses}
aria-busy
aria-label="Loading templates"
aria-label={t("governanceTemplateGridLoading")}
>
{Array.from({ length: count }, (_, i) => (
<div
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Sections / Hero" (see registry)
*/
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import ContentLockup from "../../type/ContentLockup";
@@ -1,55 +1,64 @@
"use client";
/**
* Figma: "Sections / LogoWall" (see registry)
*/
import { memo, useState, useEffect, useMemo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import { getAssetPath, partnerLogoPath } from "../../../../lib/assetUtils";
import LogoWallView from "./LogoWall.view";
import type { LogoWallProps } from "./LogoWall.types";
const defaultLogos = [
{
src: getAssetPath(partnerLogoPath("food-not-bombs")),
alt: "Food Not Bombs",
size: "h-11 lg:h-14 xl:h-[70px]",
order: "order-1 sm:order-4", // Mobile: row 1 col 1, SM: row 2 col 1 (bottom left)
},
{
src: getAssetPath(partnerLogoPath("start-coop")),
alt: "Start COOP",
size: "h-[42px] lg:h-[53px] xl:h-[66px]",
order: "order-2 sm:order-2", // Mobile: row 1 col 2, SM: row 1 col 2 (top middle)
},
{
src: getAssetPath(partnerLogoPath("metagov")),
alt: "Metagov",
size: "h-6 lg:h-8 xl:h-[41px]",
order: "order-3 sm:order-1", // Mobile: row 2 col 1, SM: row 1 col 1 (top left)
},
{
src: getAssetPath(partnerLogoPath("open-civics")),
alt: "Open Civics",
size: "h-8 lg:h-10 xl:h-[50px]",
order: "order-4 sm:order-5 md:order-6", // Mobile: row 2 col 2, SM: row 2 col 2, MD: swapped with Mutual Aid CO
},
{
src: getAssetPath(partnerLogoPath("mutual-aid-co")),
alt: "Mutual Aid CO",
size: "h-11 lg:h-14 xl:h-[70px]",
order: "order-5 sm:order-6 md:order-5", // Mobile: row 3 col 1, SM: row 2 col 3, MD: swapped with OpenCivics
},
{
src: getAssetPath(partnerLogoPath("cu-boulder")),
alt: "CU Boulder",
size: "h-10 lg:h-12 xl:h-[60px]",
order: "order-6 sm:order-3", // Mobile: row 3 col 2, SM: row 1 col 3 (top right)
},
];
const LogoWallContainer = memo<LogoWallProps>(({ logos, className = "" }) => {
const t = useTranslation("logoWall");
const [isVisible, setIsVisible] = useState(false);
const defaultLogos = useMemo(
() => [
{
src: getAssetPath(partnerLogoPath("food-not-bombs")),
alt: t("partners.foodNotBombs"),
size: "h-11 lg:h-14 xl:h-[70px]",
order: "order-1 sm:order-4",
},
{
src: getAssetPath(partnerLogoPath("start-coop")),
alt: t("partners.startCoop"),
size: "h-[42px] lg:h-[53px] xl:h-[66px]",
order: "order-2 sm:order-2",
},
{
src: getAssetPath(partnerLogoPath("metagov")),
alt: t("partners.metagov"),
size: "h-6 lg:h-8 xl:h-[41px]",
order: "order-3 sm:order-1",
},
{
src: getAssetPath(partnerLogoPath("open-civics")),
alt: t("partners.openCivics"),
size: "h-8 lg:h-10 xl:h-[50px]",
order: "order-4 sm:order-5 md:order-6",
},
{
src: getAssetPath(partnerLogoPath("mutual-aid-co")),
alt: t("partners.mutualAidCo"),
size: "h-11 lg:h-14 xl:h-[70px]",
order: "order-5 sm:order-6 md:order-5",
},
{
src: getAssetPath(partnerLogoPath("cu-boulder")),
alt: t("partners.cuBoulder"),
size: "h-10 lg:h-12 xl:h-[60px]",
order: "order-6 sm:order-3",
},
],
[t],
);
const displayLogos = useMemo(
() => (logos && logos.length > 0 ? logos : defaultLogos),
[logos],
[logos, defaultLogos],
);
useEffect(() => {
@@ -3,7 +3,7 @@
import { memo } from "react";
import { getAssetPath, quoteStatementShapePath } from "../../../../lib/assetUtils";
/** Figma: Section / Quote — **`shape-qoute.svg`** (22137:890679). */
/** Figma: Section / Quote — **`shape-quote.svg`** (22137:890679). */
const EDGE_MASK =
"linear-gradient(to right, #fff 0%, #fff 14%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 70%, #fff 86%, #fff 100%)";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Sections / RelatedArticles" (22112-872308)
*/
import { useState, useEffect, memo, useMemo, useCallback } from "react";
import { useIsMobile } from "../../../hooks";
import { useMessages } from "../../../contexts/MessagesContext";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Sections / RuleStack" (22085-860413)
*/
import { memo, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { logger } from "../../../../lib/logger";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Sections / SectionNumber" (see registry)
*/
import { memo } from "react";
import { getAssetPath, sectionNumberPath } from "../../../lib/assetUtils";
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Type / ContentLockup" (see registry)
*/
import { memo } from "react";
import ContentLockupView from "./ContentLockup.view";
import type { ContentLockupProps, VariantStyle } from "./ContentLockup.types";
@@ -2,7 +2,7 @@
import { memo } from "react";
import Button from "../../buttons/Button";
import { getAssetPath } from "../../../../lib/assetUtils";
import { contentLockupShapePath, getAssetPath } from "../../../../lib/assetUtils";
import type { ContentLockupViewProps } from "./ContentLockup.types";
function ContentLockupView({
@@ -75,7 +75,7 @@ function ContentLockupView({
<>
{/* eslint-disable-next-line @next/next/no-img-element -- decorative shape SVG */}
<img
src={getAssetPath("assets/shapes/shapes-1.svg")}
src={getAssetPath(contentLockupShapePath())}
alt=""
className={styles.shape}
role="presentation"
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Type / HeaderLockup" (see registry)
*/
import { memo } from "react";
import HeaderLockupView from "./HeaderLockup.view";
import type { HeaderLockupProps } from "./HeaderLockup.types";
@@ -1,6 +1,7 @@
"use client";
import { memo } from "react";
import { useTranslation } from "../../../contexts/MessagesContext";
import InputLabelView from "./InputLabel.view";
import type { InputLabelProps } from "./InputLabel.types";
@@ -19,6 +20,7 @@ const InputLabelContainer = memo<InputLabelProps>(
palette: paletteProp = "default",
className = "",
}) => {
const t = useTranslation("controlsChrome");
const size = sizeProp;
const palette = paletteProp;
@@ -31,6 +33,8 @@ const InputLabelContainer = memo<InputLabelProps>(
size={size}
palette={palette}
className={className}
helpIconAlt={t("helpIconAlt")}
helperTextDefault={t("inputLabelOptional")}
/>
);
},
@@ -39,4 +39,6 @@ export interface InputLabelViewProps {
size: "s" | "m";
palette: "default" | "inverse";
className: string;
helpIconAlt: string;
helperTextDefault: string;
}
@@ -12,6 +12,8 @@ function InputLabelView({
size,
palette,
className = "",
helpIconAlt,
helperTextDefault,
}: InputLabelViewProps) {
const isSmall = size === "s";
const isInverse = palette === "inverse";
@@ -79,7 +81,7 @@ function InputLabelView({
{/* eslint-disable-next-line @next/next/no-img-element -- icon from asset path */}
<img
src={getAssetPath(ASSETS.ICON_HELP)}
alt="Help"
alt={helpIconAlt}
className="block max-w-none size-full"
style={
helpIconFilter
@@ -96,7 +98,7 @@ function InputLabelView({
<p
className={`flex-[1_0_0] font-inter font-normal ${helperTextSize} min-h-px min-w-px relative ${helperTextColor} text-right`}
>
{typeof helperText === "string" ? helperText : "Optional text"}
{typeof helperText === "string" ? helperText : helperTextDefault}
</p>
)}
</div>
@@ -1,5 +1,9 @@
"use client";
/**
* Figma: "Type / Numbered List" (see registry)
*/
import { memo } from "react";
import NumberedListView from "./NumberedList.view";
import type { NumberedListProps } from "./NumberedList.types";
@@ -2,7 +2,7 @@
import Image from "next/image";
import { memo } from "react";
import { getAssetPath } from "../../../../lib/assetUtils";
import { getAssetPath, tripleStepShapePath } from "../../../../lib/assetUtils";
import AssetIcon from "../../asset/icon";
import Button from "../../buttons/Button";
import type { TripleStepViewProps } from "./TripleStep.types";
@@ -22,7 +22,7 @@ function TripleStepView({
className = "",
}: TripleStepViewProps) {
/** Decorative column art — `public/assets/shapes/triple-step.svg` (288×576 viewBox). */
const shapeSrc = getAssetPath("assets/shapes/triple-step.svg");
const shapeSrc = getAssetPath(tripleStepShapePath());
return (
<section