Component cleanup
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, memo } from "react";
|
||||
|
||||
interface ContextMenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(
|
||||
({ className = "", children, ...props }, ref) => {
|
||||
const menuClasses = `
|
||||
bg-black
|
||||
border border-[var(--color-border-default-tertiary)]
|
||||
rounded-[var(--measures-radius-medium)]
|
||||
shadow-lg
|
||||
p-[4px]
|
||||
min-w-[200px]
|
||||
max-w-[300px]
|
||||
${className}
|
||||
`
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={menuClasses}
|
||||
role="menu"
|
||||
style={{ backgroundColor: "#000000" }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContextMenu.displayName = "ContextMenu";
|
||||
|
||||
export default memo(ContextMenu);
|
||||
@@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, memo } from "react";
|
||||
|
||||
interface ContextMenuDividerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ContextMenuDivider = forwardRef<HTMLDivElement, ContextMenuDividerProps>(
|
||||
({ className = "", ...props }, ref) => {
|
||||
const dividerClasses = `
|
||||
border-t border-[var(--color-border-default-tertiary)]
|
||||
my-1
|
||||
${className}
|
||||
`
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
return (
|
||||
<div ref={ref} className={dividerClasses} role="separator" {...props} />
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContextMenuDivider.displayName = "ContextMenuDivider";
|
||||
|
||||
export default memo(ContextMenuDivider);
|
||||
@@ -1,36 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, memo } from "react";
|
||||
|
||||
interface ContextMenuSectionProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ContextMenuSection = forwardRef<HTMLDivElement, ContextMenuSectionProps>(
|
||||
({ title, children, className = "", ...props }, ref) => {
|
||||
const sectionClasses = `
|
||||
${className}
|
||||
`
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
return (
|
||||
<div ref={ref} className={sectionClasses} role="group" {...props}>
|
||||
{title && (
|
||||
<div className="px-3 py-2">
|
||||
<div className="text-[var(--color-content-default-primary)] text-sm font-medium">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContextMenuSection.displayName = "ContextMenuSection";
|
||||
|
||||
export default memo(ContextMenuSection);
|
||||
@@ -1,101 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, memo, useCallback } from "react";
|
||||
import { ContextMenuItemView } from "./ContextMenuItem.view";
|
||||
import type { ContextMenuItemProps } from "./ContextMenuItem.types";
|
||||
|
||||
const ContextMenuItemContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
ContextMenuItemProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
selected = false,
|
||||
hasSubmenu = false,
|
||||
disabled = false,
|
||||
className = "",
|
||||
onClick,
|
||||
size: sizeProp = "medium",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const size = sizeProp;
|
||||
const getTextSize = (): string => {
|
||||
switch (size) {
|
||||
case "small":
|
||||
return "text-[10px] leading-[14px]";
|
||||
case "medium":
|
||||
return "text-[14px] leading-[20px]";
|
||||
case "large":
|
||||
return "text-[16px] leading-[24px]";
|
||||
default:
|
||||
return "text-[14px] leading-[20px]";
|
||||
}
|
||||
};
|
||||
|
||||
const itemClasses = `
|
||||
flex items-center justify-between
|
||||
px-[8px] py-[4px]
|
||||
text-[var(--color-content-default-brand-primary)]
|
||||
${getTextSize()}
|
||||
cursor-pointer
|
||||
transition-colors duration-150
|
||||
${
|
||||
selected
|
||||
? "bg-[var(--color-surface-default-secondary)] rounded-[var(--measures-radius-small)]"
|
||||
: ""
|
||||
}
|
||||
${
|
||||
disabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:!bg-[var(--color-surface-default-secondary)] hover:!rounded-[var(--measures-radius-small)]"
|
||||
}
|
||||
${className}
|
||||
`
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
},
|
||||
[disabled, onClick],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
[disabled, onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<ContextMenuItemView
|
||||
ref={ref}
|
||||
selected={selected}
|
||||
hasSubmenu={hasSubmenu}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
itemClasses={itemClasses}
|
||||
handleClick={handleClick}
|
||||
handleKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuItemView>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContextMenuItemContainer.displayName = "ContextMenuItem";
|
||||
|
||||
export default memo(ContextMenuItemContainer);
|
||||
@@ -1,27 +0,0 @@
|
||||
export type ContextMenuItemSizeValue = "small" | "medium" | "large";
|
||||
|
||||
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
selected?: boolean;
|
||||
hasSubmenu?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick?: (
|
||||
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
/**
|
||||
* Context menu item size.
|
||||
*/
|
||||
size?: ContextMenuItemSizeValue;
|
||||
}
|
||||
|
||||
export interface ContextMenuItemViewProps {
|
||||
children?: React.ReactNode;
|
||||
selected: boolean;
|
||||
hasSubmenu: boolean;
|
||||
disabled: boolean;
|
||||
className: string;
|
||||
itemClasses: string;
|
||||
handleClick: (_e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
handleKeyDown: (_e: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { forwardRef } from "react";
|
||||
import type { ContextMenuItemViewProps } from "./ContextMenuItem.types";
|
||||
|
||||
export const ContextMenuItemView = forwardRef<
|
||||
HTMLDivElement,
|
||||
ContextMenuItemViewProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
selected,
|
||||
hasSubmenu,
|
||||
disabled,
|
||||
itemClasses,
|
||||
handleClick,
|
||||
handleKeyDown,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={itemClasses}
|
||||
role="menuitem"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
aria-current={selected ? "true" : undefined}
|
||||
aria-disabled={disabled}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
{selected && (
|
||||
<svg
|
||||
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
{hasSubmenu && (
|
||||
<svg
|
||||
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContextMenuItemView.displayName = "ContextMenuItemView";
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./ContextMenuItem.container";
|
||||
export type { ContextMenuItemProps } from "./ContextMenuItem.types";
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import ModalFooter from "../../utility/ModalFooter";
|
||||
import ModalHeader from "../../utility/ModalHeader";
|
||||
import ModalFooter from "../ModalFooter";
|
||||
import ModalHeader from "../ModalHeader";
|
||||
import { CreateModalFrameView } from "./CreateModalFrame.view";
|
||||
import type { CreateViewProps } from "./Create.types";
|
||||
|
||||
|
||||
@@ -78,5 +78,5 @@ export function useCreateModalA11y(
|
||||
document.removeEventListener("keydown", handleTab);
|
||||
previousActiveElementRef.current?.focus();
|
||||
};
|
||||
}, [isOpen]);
|
||||
}, [dialogRef, isOpen]);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ const DialogContainer = memo<DialogProps>(
|
||||
title={title}
|
||||
description={description}
|
||||
footer={footer}
|
||||
children={children}
|
||||
className={className}
|
||||
ariaLabel={ariaLabel}
|
||||
ariaLabelledBy={titleId}
|
||||
@@ -40,7 +39,9 @@ const DialogContainer = memo<DialogProps>(
|
||||
backdropVariant={backdropVariant}
|
||||
overlayRef={overlayRef}
|
||||
dialogRef={dialogRef}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</DialogView>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { memo } from "react";
|
||||
import ContentLockup from "../../type/ContentLockup";
|
||||
import ModalFooter from "../../utility/ModalFooter";
|
||||
import ModalHeader from "../../utility/ModalHeader";
|
||||
import ModalFooter from "../ModalFooter";
|
||||
import ModalHeader from "../ModalHeader";
|
||||
import { CreateModalFrameView } from "../Create/CreateModalFrame.view";
|
||||
import type { DialogViewProps } from "./Dialog.types";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalHeader from "../../utility/ModalHeader";
|
||||
import ModalHeader from "../ModalHeader";
|
||||
import type { LoginBackdropVariant, LoginViewProps } from "./Login.types";
|
||||
|
||||
const backdropClasses: Record<LoginBackdropVariant, string> = {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { ModalFooterView } from "./ModalFooter.view";
|
||||
import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
|
||||
/**
|
||||
* Figma: "Utility / ModalFooter". Lives under `modals/` with other composed modal chrome.
|
||||
* Sticky modal footer slot used by the create-flow + login modals to host
|
||||
* primary/secondary actions.
|
||||
*/
|
||||
const ModalFooterContainer = memo<ModalFooterProps>((props) => {
|
||||
return <ModalFooterView {...props} />;
|
||||
});
|
||||
|
||||
ModalFooterContainer.displayName = "ModalFooter";
|
||||
|
||||
export default ModalFooterContainer;
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface ModalFooterProps {
|
||||
showBackButton?: boolean;
|
||||
showNextButton?: boolean;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
/**
|
||||
* Custom back button text. If not provided, uses localized "Back" from common.json
|
||||
*/
|
||||
backButtonText?: string;
|
||||
/**
|
||||
* Custom next button text. If not provided, uses localized "Next" from common.json
|
||||
*/
|
||||
nextButtonText?: string;
|
||||
nextButtonDisabled?: boolean;
|
||||
currentStep?: number;
|
||||
totalSteps?: number;
|
||||
/**
|
||||
* Whether to show the stepper component in the footer (Figma prop).
|
||||
* Defaults to true if currentStep and totalSteps are provided.
|
||||
* @default true
|
||||
*/
|
||||
stepper?: boolean;
|
||||
footerContent?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import Button from "../../buttons/Button";
|
||||
import Stepper from "../../progress/Stepper";
|
||||
import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
|
||||
export function ModalFooterView({
|
||||
showBackButton = false,
|
||||
showNextButton = false,
|
||||
onBack,
|
||||
onNext,
|
||||
backButtonText,
|
||||
nextButtonText,
|
||||
nextButtonDisabled = false,
|
||||
currentStep,
|
||||
totalSteps,
|
||||
stepper: stepperProp,
|
||||
footerContent,
|
||||
className = "",
|
||||
}: ModalFooterProps) {
|
||||
const t = useTranslation("common");
|
||||
|
||||
// Use localized defaults if text not provided
|
||||
const defaultBackText = backButtonText || t("buttons.back");
|
||||
const defaultNextText = nextButtonText || t("buttons.next");
|
||||
|
||||
// Determine if stepper should be shown
|
||||
// Defaults to true if currentStep and totalSteps are provided, unless explicitly set to false
|
||||
const shouldShowStepper =
|
||||
stepperProp !== undefined
|
||||
? stepperProp
|
||||
: currentStep !== undefined && totalSteps !== undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-[64px] bg-[var(--color-surface-default-primary)] rounded-bl-[var(--radius-300,12px)] rounded-br-[var(--radius-300,12px)] shrink-0 relative ${className}`}
|
||||
>
|
||||
{/* Back Button - Absolutely positioned bottom left */}
|
||||
{showBackButton && (
|
||||
<div className="absolute left-[16px] top-[12px]">
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="medium"
|
||||
onClick={onBack}
|
||||
>
|
||||
{defaultBackText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stepper (Centered) */}
|
||||
{shouldShowStepper && currentStep && totalSteps && (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Stepper active={currentStep} totalSteps={totalSteps} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Button - Absolutely positioned bottom right */}
|
||||
{showNextButton && (
|
||||
<div className="absolute right-[16px] top-[12px]">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="medium"
|
||||
onClick={onNext}
|
||||
disabled={nextButtonDisabled}
|
||||
>
|
||||
{defaultNextText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Footer Content */}
|
||||
{footerContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./ModalFooter.container";
|
||||
export type { ModalFooterProps } from "./ModalFooter.types";
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { ModalHeaderView } from "./ModalHeader.view";
|
||||
import type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
|
||||
/**
|
||||
* Figma: "Utility / ModalHeader". Lives under `modals/` with other composed modal chrome.
|
||||
* Sticky 48px modal header with optional close (left) and more-options
|
||||
* (right) icon buttons.
|
||||
*/
|
||||
const ModalHeaderContainer = memo<ModalHeaderProps>((props) => {
|
||||
return <ModalHeaderView {...props} />;
|
||||
});
|
||||
|
||||
ModalHeaderContainer.displayName = "ModalHeader";
|
||||
|
||||
export default ModalHeaderContainer;
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ModalHeaderProps {
|
||||
onClose?: () => void;
|
||||
onMoreOptions?: () => void;
|
||||
showCloseButton?: boolean;
|
||||
showMoreOptionsButton?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { getAssetPath } from "../../../../lib/assetUtils";
|
||||
import type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
|
||||
const iconButtonClass =
|
||||
"absolute bg-[var(--color-surface-default-secondary)] h-[24px] w-[24px] rounded-full flex items-center justify-center cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-invert-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]";
|
||||
|
||||
export function ModalHeaderView({
|
||||
onClose,
|
||||
onMoreOptions,
|
||||
showCloseButton = true,
|
||||
showMoreOptionsButton = true,
|
||||
className = "",
|
||||
}: ModalHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={`border-b border-[var(--color-border-default-secondary)] h-[48px] shrink-0 sticky top-0 bg-[var(--color-surface-default-primary)] z-[2] ${className}`}
|
||||
>
|
||||
{/* Close Button - Left */}
|
||||
{showCloseButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={`${iconButtonClass} left-[24px] top-[12px]`}
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- icon asset */}
|
||||
<img
|
||||
src={getAssetPath("assets/Icon_Close.svg")}
|
||||
alt=""
|
||||
className="w-[16px] h-[16px]"
|
||||
style={{
|
||||
filter: "brightness(0) invert(1)",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* More Options Button - Right */}
|
||||
{showMoreOptionsButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onMoreOptions}
|
||||
className={`${iconButtonClass} right-[24px] top-[12px]`}
|
||||
aria-label="More options"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="4" cy="8" r="1.5" fill="white" />
|
||||
<circle cx="8" cy="8" r="1.5" fill="white" />
|
||||
<circle cx="12" cy="8" r="1.5" fill="white" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./ModalHeader.container";
|
||||
export type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
Reference in New Issue
Block a user