Implement create component
This commit is contained in:
@@ -6,6 +6,10 @@ import Alert from "../components/Alert";
|
||||
import Button from "../components/Button";
|
||||
import Stepper from "../components/Stepper";
|
||||
import Progress from "../components/Progress";
|
||||
import Create from "../components/Create";
|
||||
import Input from "../components/Input";
|
||||
import InputWithCounter from "../components/InputWithCounter";
|
||||
import { getAssetPath } from "../../lib/assetUtils";
|
||||
|
||||
export default function ComponentsPreview() {
|
||||
const [alertVisible, setAlertVisible] = useState({
|
||||
@@ -16,6 +20,10 @@ export default function ComponentsPreview() {
|
||||
banner: true,
|
||||
});
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createStep, setCreateStep] = useState(1);
|
||||
const [policyName, setPolicyName] = useState("");
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]">
|
||||
<div className="max-w-[1200px] mx-auto space-y-[var(--spacing-scale-064)]">
|
||||
@@ -306,6 +314,106 @@ export default function ComponentsPreview() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Create Component Section */}
|
||||
<section className="space-y-[var(--spacing-scale-024)]">
|
||||
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||
Create Component
|
||||
</h2>
|
||||
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||
<div className="space-y-[var(--spacing-scale-016)]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
Open Create Dialog
|
||||
</Button>
|
||||
|
||||
<div className="space-y-[var(--spacing-scale-008)]">
|
||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)]">
|
||||
Step {createStep} of 3
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => setCreateStep((prev) => Math.max(1, prev - 1))}
|
||||
disabled={createStep === 1}
|
||||
>
|
||||
Previous Step
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => setCreateStep((prev) => Math.min(3, prev + 1))}
|
||||
disabled={createStep === 3}
|
||||
>
|
||||
Next Step
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Create
|
||||
isOpen={createOpen}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
title={
|
||||
createStep === 1
|
||||
? "What do you call your group's new policy?"
|
||||
: createStep === 2
|
||||
? "How should conflicts be resolved?"
|
||||
: "Review your policy"
|
||||
}
|
||||
description="You can also combine or add new approaches to the list"
|
||||
showBackButton={true}
|
||||
showNextButton={true}
|
||||
onBack={() => setCreateStep((prev) => Math.max(1, prev - 1))}
|
||||
onNext={() => setCreateStep((prev) => Math.min(3, prev + 1))}
|
||||
backButtonText="Back"
|
||||
nextButtonText={createStep === 3 ? "Finish" : "Next"}
|
||||
nextButtonDisabled={createStep === 1 && !policyName.trim()}
|
||||
currentStep={createStep}
|
||||
totalSteps={3}
|
||||
>
|
||||
<div className="space-y-[var(--spacing-scale-024)]">
|
||||
{createStep === 1 && (
|
||||
<InputWithCounter
|
||||
label="Label"
|
||||
placeholder="Policy name"
|
||||
value={policyName}
|
||||
onChange={setPolicyName}
|
||||
maxLength={48}
|
||||
showHelpIcon
|
||||
/>
|
||||
)}
|
||||
{createStep === 2 && (
|
||||
<div className="space-y-[var(--spacing-scale-008)]">
|
||||
<Input
|
||||
label="Conflict Resolution Method"
|
||||
placeholder="Enter method"
|
||||
value=""
|
||||
/>
|
||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-primary)]">
|
||||
Select how conflicts should be resolved in your group.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{createStep === 3 && (
|
||||
<div className="space-y-[var(--spacing-scale-016)]">
|
||||
<p className="font-inter text-[16px] leading-[24px] text-[var(--color-content-default-primary)]">
|
||||
Review your policy configuration before finalizing.
|
||||
</p>
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-200,8px)] p-[var(--spacing-scale-016)]">
|
||||
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)]">
|
||||
Policy details will appear here
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Create>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -91,6 +91,18 @@ const ContentLockupContainer = memo<ContentLockupProps>(
|
||||
shape:
|
||||
"w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]",
|
||||
},
|
||||
modal: {
|
||||
container:
|
||||
"flex flex-col gap-[var(--spacing-scale-008)] items-start justify-center py-[12px] relative w-full",
|
||||
textContainer: "flex flex-col gap-[var(--spacing-scale-008)] w-full",
|
||||
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)] w-full",
|
||||
titleContainer: "flex items-center justify-start w-full",
|
||||
title:
|
||||
"font-bricolage-grotesque font-bold text-[28px] leading-[36px] tracking-[0] text-[var(--color-content-default-primary)] text-left",
|
||||
description:
|
||||
"font-inter font-normal text-[16px] leading-[24px] tracking-[0] text-[var(--color-content-default-tertiary)] text-left",
|
||||
shape: "w-[16px] h-[16px]",
|
||||
},
|
||||
};
|
||||
|
||||
const styles = variantStyles[variant] || variantStyles.hero;
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface ContentLockupProps {
|
||||
ctaText?: string;
|
||||
ctaHref?: string;
|
||||
buttonClassName?: string;
|
||||
variant?: "hero" | "feature" | "learn" | "ask" | "ask-inverse";
|
||||
variant?: "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal";
|
||||
linkText?: string;
|
||||
linkHref?: string;
|
||||
alignment?: "center" | "left";
|
||||
@@ -34,7 +34,7 @@ export interface ContentLockupViewProps {
|
||||
ctaText?: string;
|
||||
ctaHref?: string;
|
||||
buttonClassName: string;
|
||||
variant: "hero" | "feature" | "learn" | "ask" | "ask-inverse";
|
||||
variant: "hero" | "feature" | "learn" | "ask" | "ask-inverse" | "modal";
|
||||
linkText?: string;
|
||||
linkHref?: string;
|
||||
alignment: "center" | "left";
|
||||
|
||||
@@ -20,16 +20,20 @@ function ContentLockupView({
|
||||
}: ContentLockupViewProps) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{variant === "ask" || variant === "ask-inverse" ? (
|
||||
/* Simplified structure for ask variant */
|
||||
{variant === "ask" || variant === "ask-inverse" || variant === "modal" ? (
|
||||
/* Simplified structure for ask and modal variants */
|
||||
<div
|
||||
className={`${styles.titleGroup} ${
|
||||
alignment === "left" ? "text-left" : "text-center"
|
||||
alignment === "left" || variant === "modal"
|
||||
? "text-left"
|
||||
: "text-center"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`${styles.titleContainer} ${
|
||||
alignment === "left" ? "justify-start" : "justify-center"
|
||||
alignment === "left" || variant === "modal"
|
||||
? "justify-start"
|
||||
: "justify-center"
|
||||
}`}
|
||||
>
|
||||
{title ? (
|
||||
@@ -39,6 +43,9 @@ function ContentLockupView({
|
||||
) : null}
|
||||
</div>
|
||||
{subtitle ? <h2 className={styles.subtitle}>{subtitle}</h2> : null}
|
||||
{variant === "modal" && description && (
|
||||
<p className={styles.description}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* Full structure for other variants */
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import { CreateView } from "./Create.view";
|
||||
import type { CreateProps } from "./Create.types";
|
||||
|
||||
const CreateContainer = memo<CreateProps>(
|
||||
({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
footerContent,
|
||||
showBackButton = true,
|
||||
showNextButton = true,
|
||||
onBack,
|
||||
onNext,
|
||||
backButtonText = "Back",
|
||||
nextButtonText = "Next",
|
||||
nextButtonDisabled = false,
|
||||
currentStep,
|
||||
totalSteps,
|
||||
className = "",
|
||||
ariaLabel,
|
||||
ariaLabelledBy,
|
||||
}) => {
|
||||
const createRef = useRef<HTMLDivElement>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const previousActiveElementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
// Handle ESC key to close
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Focus trap and body scroll lock
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
// Store previous active element
|
||||
previousActiveElementRef.current =
|
||||
document.activeElement as HTMLElement;
|
||||
|
||||
// Lock body scroll
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
// Focus the first focusable element in the create dialog
|
||||
if (createRef.current) {
|
||||
const focusableElements = createRef.current.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
const firstElement = focusableElements[0] as HTMLElement;
|
||||
if (firstElement) {
|
||||
firstElement.focus();
|
||||
} else {
|
||||
// Fallback: make create dialog focusable and focus it
|
||||
createRef.current.setAttribute("tabindex", "-1");
|
||||
createRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Focus trap
|
||||
const handleTab = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Tab" || !createRef.current) return;
|
||||
|
||||
const focusableElements = createRef.current.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
const firstElement = focusableElements[0] as HTMLElement;
|
||||
const lastElement = focusableElements[
|
||||
focusableElements.length - 1
|
||||
] as HTMLElement;
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift + Tab
|
||||
if (document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement?.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab
|
||||
if (document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleTab);
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
document.removeEventListener("keydown", handleTab);
|
||||
// Restore focus to previous element
|
||||
previousActiveElementRef.current?.focus();
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
return (
|
||||
<CreateView
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
description={description}
|
||||
children={children}
|
||||
footerContent={footerContent}
|
||||
showBackButton={showBackButton}
|
||||
showNextButton={showNextButton}
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
backButtonText={backButtonText}
|
||||
nextButtonText={nextButtonText}
|
||||
nextButtonDisabled={nextButtonDisabled}
|
||||
currentStep={currentStep}
|
||||
totalSteps={totalSteps}
|
||||
className={className}
|
||||
ariaLabel={ariaLabel}
|
||||
ariaLabelledBy={ariaLabelledBy}
|
||||
createRef={createRef}
|
||||
overlayRef={overlayRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CreateContainer.displayName = "Create";
|
||||
|
||||
export default CreateContainer;
|
||||
@@ -0,0 +1,43 @@
|
||||
export interface CreateProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
footerContent?: React.ReactNode;
|
||||
showBackButton?: boolean;
|
||||
showNextButton?: boolean;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
backButtonText?: string;
|
||||
nextButtonText?: string;
|
||||
nextButtonDisabled?: boolean;
|
||||
currentStep?: number;
|
||||
totalSteps?: number;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
ariaLabelledBy?: string;
|
||||
}
|
||||
|
||||
export interface CreateViewProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
footerContent?: React.ReactNode;
|
||||
showBackButton: boolean;
|
||||
showNextButton: boolean;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
backButtonText: string;
|
||||
nextButtonText: string;
|
||||
nextButtonDisabled: boolean;
|
||||
currentStep?: number;
|
||||
totalSteps?: number;
|
||||
className: string;
|
||||
ariaLabel?: string;
|
||||
ariaLabelledBy?: string;
|
||||
createRef: React.RefObject<HTMLDivElement>;
|
||||
overlayRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { createPortal } from "react-dom";
|
||||
import ContentLockup from "../ContentLockup";
|
||||
import ModalFooter from "../ModalFooter";
|
||||
import ModalHeader from "../ModalHeader";
|
||||
import type { CreateViewProps } from "./Create.types";
|
||||
|
||||
export function CreateView({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
footerContent,
|
||||
showBackButton,
|
||||
showNextButton,
|
||||
onBack,
|
||||
onNext,
|
||||
backButtonText,
|
||||
nextButtonText,
|
||||
nextButtonDisabled,
|
||||
currentStep,
|
||||
totalSteps,
|
||||
className,
|
||||
ariaLabel,
|
||||
ariaLabelledBy,
|
||||
createRef,
|
||||
overlayRef,
|
||||
}: CreateViewProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const createContent = (
|
||||
<>
|
||||
{/* Overlay */}
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className="fixed inset-0 bg-black/50 z-[9998]"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Create Dialog */}
|
||||
<div
|
||||
ref={createRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
className={`fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-[var(--color-surface-default-primary)] rounded-[var(--radius-500,20px)] shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] w-[560px] max-h-[728px] flex flex-col overflow-hidden z-[9999] ${className}`}
|
||||
>
|
||||
{/* Header with close buttons */}
|
||||
<ModalHeader onClose={onClose} onMoreOptions={onClose} />
|
||||
|
||||
{/* Header Lockup Section (Sticky) */}
|
||||
{(title || description) && (
|
||||
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0 sticky top-[48px] z-[2]">
|
||||
<ContentLockup
|
||||
title={title}
|
||||
description={description}
|
||||
variant="modal"
|
||||
alignment="left"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Area (Scrollable) */}
|
||||
<div className="flex flex-col gap-[var(--spacing-scale-024)] px-[24px] pb-[96px] overflow-x-clip overflow-y-auto relative shrink-0 flex-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<ModalFooter
|
||||
showBackButton={showBackButton}
|
||||
showNextButton={showNextButton}
|
||||
onBack={onBack}
|
||||
onNext={onNext}
|
||||
backButtonText={backButtonText}
|
||||
nextButtonText={nextButtonText}
|
||||
nextButtonDisabled={nextButtonDisabled}
|
||||
currentStep={currentStep}
|
||||
totalSteps={totalSteps}
|
||||
footerContent={footerContent}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// Portal to body
|
||||
if (typeof window !== "undefined") {
|
||||
return createPortal(createContent, document.body);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Create.container";
|
||||
export type { CreateProps } from "./Create.types";
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface InputWithCounterProps {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
maxLength: number;
|
||||
showHelpIcon?: boolean;
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { getAssetPath } from "../../../lib/assetUtils";
|
||||
import type { InputWithCounterProps } from "./InputWithCounter.types";
|
||||
|
||||
export function InputWithCounterView({
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
maxLength,
|
||||
showHelpIcon = false,
|
||||
className = "",
|
||||
inputClassName = "",
|
||||
}: InputWithCounterProps) {
|
||||
return (
|
||||
<div className={`space-y-[var(--spacing-scale-008)] ${className}`}>
|
||||
{/* Label with help icon */}
|
||||
{label && (
|
||||
<div className="flex items-center gap-[var(--spacing-scale-002)]">
|
||||
<label className="font-inter text-[14px] leading-[20px] font-medium text-[var(--color-content-default-primary)]">
|
||||
{label}
|
||||
</label>
|
||||
{showHelpIcon && (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="shrink-0 w-[12px] h-[12px]"
|
||||
>
|
||||
<mask
|
||||
id="mask0_21296_8257"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="12"
|
||||
height="12"
|
||||
>
|
||||
<rect width="12" height="12" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_21296_8257)">
|
||||
<path
|
||||
d="M5.99449 8.80766C6.13725 8.80766 6.25784 8.75838 6.35624 8.6598C6.45463 8.56123 6.50383 8.44055 6.50383 8.29779C6.50383 8.15502 6.45454 8.03444 6.35596 7.93605C6.25739 7.83765 6.13672 7.78845 5.99395 7.78845C5.85118 7.78845 5.7306 7.83774 5.63221 7.93631C5.53381 8.03489 5.48461 8.15556 5.48461 8.29833C5.48461 8.44109 5.5339 8.56168 5.63248 8.66008C5.73105 8.75847 5.85172 8.80766 5.99449 8.80766ZM5.64038 7.01729H6.34421C6.35062 6.77114 6.38668 6.5745 6.45239 6.42739C6.51809 6.28028 6.67754 6.08525 6.93075 5.8423C7.15062 5.62243 7.31905 5.41938 7.43604 5.23316C7.55302 5.04695 7.61151 4.82703 7.61151 4.57343C7.61151 4.14307 7.45687 3.80689 7.14759 3.5649C6.8383 3.32292 6.47243 3.20193 6.04999 3.20193C5.63269 3.20193 5.28734 3.3133 5.01394 3.53606C4.74055 3.75881 4.54552 4.02115 4.42885 4.32306L5.07114 4.58075C5.13204 4.41473 5.2362 4.25303 5.38364 4.09566C5.53108 3.93829 5.74999 3.8596 6.04038 3.8596C6.33589 3.8596 6.55432 3.94053 6.69566 4.1024C6.83701 4.26426 6.90769 4.4423 6.90769 4.63654C6.90769 4.80641 6.85929 4.96185 6.76249 5.10288C6.6657 5.2439 6.5423 5.38012 6.3923 5.51154C6.0641 5.80769 5.85673 6.0439 5.77019 6.22019C5.68365 6.39647 5.64038 6.66217 5.64038 7.01729ZM6.00082 10.75C5.34386 10.75 4.72634 10.6253 4.14828 10.376C3.5702 10.1266 3.06736 9.78827 2.63975 9.36085C2.21213 8.93343 1.8736 8.43081 1.62416 7.85299C1.37472 7.27518 1.25 6.65779 1.25 6.00083C1.25 5.34386 1.37467 4.72634 1.624 4.14828C1.87333 3.5702 2.21171 3.06736 2.63913 2.63975C3.06655 2.21213 3.56917 1.8736 4.14699 1.62416C4.7248 1.37472 5.34218 1.25 5.99915 1.25C6.65612 1.25 7.27363 1.37467 7.8517 1.624C8.42978 1.87333 8.93262 2.21171 9.36023 2.63913C9.78784 3.06655 10.1264 3.56917 10.3758 4.14699C10.6253 4.7248 10.75 5.34218 10.75 5.99915C10.75 6.65612 10.6253 7.27363 10.376 7.8517C10.1266 8.42978 9.78827 8.93262 9.36085 9.36023C8.93343 9.78784 8.43081 10.1264 7.85299 10.3758C7.27518 10.6253 6.65779 10.75 6.00082 10.75ZM5.99999 9.99999C7.11665 9.99999 8.06249 9.61249 8.83749 8.83749C9.61249 8.06249 9.99999 7.11665 9.99999 5.99999C9.99999 4.88332 9.61249 3.93749 8.83749 3.16249C8.06249 2.38749 7.11665 1.99999 5.99999 1.99999C4.88332 1.99999 3.93749 2.38749 3.16249 3.16249C2.38749 3.93749 1.99999 4.88332 1.99999 5.99999C1.99999 7.11665 2.38749 8.06249 3.16249 8.83749C3.93749 9.61249 4.88332 9.99999 5.99999 9.99999Z"
|
||||
fill="var(--color-content-brand-darker-accent-2)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input field */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.length <= maxLength) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
maxLength={maxLength}
|
||||
className={`w-full h-[36px] px-[12px] py-[8px] bg-[var(--color-surface-default-primary)] border-2 border-[var(--color-border-default-tertiary)] rounded-[var(--measures-radius-medium,8px)] text-[16px] leading-[24px] text-[var(--color-content-default-primary)] placeholder:text-[var(--color-content-default-tertiary)] focus:outline-none ${inputClassName}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Character counter */}
|
||||
<p className="font-inter text-[12px] leading-[16px] text-[var(--color-content-default-tertiary)]">
|
||||
{value.length}/{maxLength}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { InputWithCounterView as default } from "./InputWithCounter.view";
|
||||
export type { InputWithCounterProps } from "./InputWithCounter.types";
|
||||
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
footerContent?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import Button from "../Button";
|
||||
import Stepper from "../Stepper";
|
||||
import type { ModalFooterProps } from "./ModalFooter.types";
|
||||
|
||||
export function ModalFooterView({
|
||||
showBackButton = false,
|
||||
showNextButton = false,
|
||||
onBack,
|
||||
onNext,
|
||||
backButtonText,
|
||||
nextButtonText,
|
||||
nextButtonDisabled = false,
|
||||
currentStep,
|
||||
totalSteps,
|
||||
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");
|
||||
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
|
||||
variant="outlined"
|
||||
size="medium"
|
||||
onClick={onBack}
|
||||
>
|
||||
{defaultBackText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stepper (Centered) */}
|
||||
{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
|
||||
variant="default"
|
||||
size="medium"
|
||||
onClick={onNext}
|
||||
disabled={nextButtonDisabled}
|
||||
>
|
||||
{defaultNextText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Footer Content */}
|
||||
{footerContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ModalFooterView as default } from "./ModalFooter.view";
|
||||
export type { ModalFooterProps } from "./ModalFooter.types";
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ModalHeaderProps {
|
||||
onClose?: () => void;
|
||||
onMoreOptions?: () => void;
|
||||
showCloseButton?: boolean;
|
||||
showMoreOptionsButton?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getAssetPath } from "../../../lib/assetUtils";
|
||||
import type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
|
||||
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
|
||||
onClick={onClose}
|
||||
className="absolute bg-[var(--color-surface-default-secondary)] h-[24px] w-[24px] rounded-full left-[24px] top-[12px] flex items-center justify-center cursor-pointer"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<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
|
||||
onClick={onMoreOptions}
|
||||
className="absolute bg-[var(--color-surface-default-secondary)] h-[24px] w-[24px] rounded-full right-[24px] top-[12px] flex items-center justify-center cursor-pointer"
|
||||
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 { ModalHeaderView as default } from "./ModalHeader.view";
|
||||
export type { ModalHeaderProps } from "./ModalHeader.types";
|
||||
Reference in New Issue
Block a user