Implement about page
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useId, useState } from "react";
|
||||
import AccordionView from "./Accordion.view";
|
||||
import type { AccordionProps, AccordionSizeValue } from "./Accordion.types";
|
||||
|
||||
/**
|
||||
* Figma: "Layout / Accordion" (21842-2813); Medium 22135-890258; optional `lgSize` / `xlSize` stacking (FAQ **s**→**m** `lg`; **l** `xl`, 22135-890328).
|
||||
*/
|
||||
const AccordionContainer = memo<AccordionProps>(
|
||||
({
|
||||
title,
|
||||
subhead,
|
||||
children,
|
||||
size: sizeProp = "l",
|
||||
lgSize,
|
||||
xlSize,
|
||||
defaultOpen = false,
|
||||
className = "",
|
||||
}) => {
|
||||
const size: AccordionSizeValue = sizeProp;
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const panelId = useId();
|
||||
const buttonId = useId();
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
setIsOpen((open) => !open);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AccordionView
|
||||
title={title}
|
||||
subhead={subhead}
|
||||
children={children}
|
||||
size={size}
|
||||
lgSize={lgSize}
|
||||
xlSize={xlSize}
|
||||
isOpen={isOpen}
|
||||
panelId={panelId}
|
||||
buttonId={buttonId}
|
||||
onToggle={onToggle}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AccordionContainer.displayName = "Accordion";
|
||||
|
||||
export default AccordionContainer;
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type AccordionSizeValue = "s" | "m" | "l";
|
||||
|
||||
export interface AccordionProps {
|
||||
title: string;
|
||||
subhead?: string;
|
||||
children?: ReactNode;
|
||||
size?: AccordionSizeValue;
|
||||
/**
|
||||
* From `lg` up, use this size’s header / type / panel styles (e.g. FAQ: `s` + `lgSize="m"`).
|
||||
*/
|
||||
lgSize?: AccordionSizeValue;
|
||||
/**
|
||||
* From `xl` up, override with this size (e.g. FAQ: `xlSize="l"` at wide desktop — Figma **22135:890328**).
|
||||
*/
|
||||
xlSize?: AccordionSizeValue;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface AccordionViewProps {
|
||||
title: string;
|
||||
subhead?: string;
|
||||
children?: ReactNode;
|
||||
size: AccordionSizeValue;
|
||||
lgSize?: AccordionSizeValue;
|
||||
xlSize?: AccordionSizeValue;
|
||||
isOpen: boolean;
|
||||
panelId: string;
|
||||
buttonId: string;
|
||||
onToggle: () => void;
|
||||
className: string;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Icon from "../../asset/icon/Icon";
|
||||
import Divider from "../../utility/Divider";
|
||||
import type { AccordionSizeValue, AccordionViewProps } from "./Accordion.types";
|
||||
|
||||
const SIZE_CLASSES: Record<
|
||||
AccordionSizeValue,
|
||||
{ header: string; title: string; subhead: string }
|
||||
> = {
|
||||
s: {
|
||||
header:
|
||||
"gap-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-020)] items-center",
|
||||
title: "text-[14px] font-medium leading-[18px]",
|
||||
subhead: "text-[12px] leading-[14px]",
|
||||
},
|
||||
/** Figma: Layout / Accordion — Medium (22135-890258; header gap/px/py + Large/Label 18/24). */
|
||||
m: {
|
||||
header:
|
||||
"gap-[var(--spacing-scale-024)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-024)] items-center",
|
||||
title: "text-[18px] font-medium leading-6",
|
||||
subhead: "text-[14px] leading-[18px]",
|
||||
},
|
||||
l: {
|
||||
header:
|
||||
"gap-[var(--spacing-scale-048)] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-032)] items-center",
|
||||
/** Figma Large: X Large Label 24 Regular, lh 28 (21842-2869). */
|
||||
title: "text-[24px] font-normal leading-7",
|
||||
subhead: "text-[18px] leading-6",
|
||||
},
|
||||
};
|
||||
|
||||
const PANEL_CLASSES: Record<AccordionSizeValue, string> = {
|
||||
s: "px-[var(--spacing-scale-016)] pb-[var(--spacing-scale-020)] font-inter text-[14px] font-normal leading-5 text-[var(--color-content-default-secondary,#d2d2d2)]",
|
||||
m: "px-[var(--spacing-scale-016)] pb-[var(--spacing-scale-024)] font-inter text-[16px] font-normal leading-6 text-[var(--color-content-default-secondary,#d2d2d2)]",
|
||||
l: "px-[var(--spacing-scale-016)] pb-[var(--spacing-scale-032)] font-inter text-[18px] font-normal leading-[26px] text-[var(--color-content-default-secondary,#d2d2d2)]",
|
||||
};
|
||||
|
||||
function withLgClasses(base: string, lg: string): string {
|
||||
const prefixed = lg
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((c) => `lg:${c}`)
|
||||
.join(" ");
|
||||
return `${base} ${prefixed}`.trim();
|
||||
}
|
||||
|
||||
function withXlClasses(base: string, xl: string): string {
|
||||
const prefixed = xl
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((c) => `xl:${c}`)
|
||||
.join(" ");
|
||||
return `${base} ${prefixed}`.trim();
|
||||
}
|
||||
|
||||
function resolvedLayoutClasses(
|
||||
size: AccordionSizeValue,
|
||||
lgSize: AccordionSizeValue | undefined,
|
||||
xlSize: AccordionSizeValue | undefined,
|
||||
): { header: string; title: string; subhead: string; panel: string } {
|
||||
const sm = SIZE_CLASSES[size];
|
||||
let header = sm.header;
|
||||
let title = sm.title;
|
||||
let subhead = sm.subhead;
|
||||
let panel = PANEL_CLASSES[size];
|
||||
|
||||
if (lgSize && lgSize !== size) {
|
||||
const lg = SIZE_CLASSES[lgSize];
|
||||
header = withLgClasses(header, lg.header);
|
||||
title = withLgClasses(title, lg.title);
|
||||
subhead = withLgClasses(subhead, lg.subhead);
|
||||
panel = withLgClasses(panel, PANEL_CLASSES[lgSize]);
|
||||
}
|
||||
|
||||
if (
|
||||
xlSize === undefined ||
|
||||
xlSize === size ||
|
||||
xlSize === lgSize
|
||||
) {
|
||||
return { header, title, subhead, panel };
|
||||
}
|
||||
|
||||
const xls = SIZE_CLASSES[xlSize];
|
||||
header = withXlClasses(header, xls.header);
|
||||
title = withXlClasses(title, xls.title);
|
||||
subhead = withXlClasses(subhead, xls.subhead);
|
||||
panel = withXlClasses(panel, PANEL_CLASSES[xlSize]);
|
||||
|
||||
return { header, title, subhead, panel };
|
||||
}
|
||||
|
||||
/**
|
||||
* Figma: "Layout / Accordion" (21842-2813); Medium 22135-890258; FAQ **s**→**m** `lg`, **l** `xl` (22135-890328) via `lgSize` / `xlSize`.
|
||||
*/
|
||||
function AccordionView({
|
||||
title,
|
||||
subhead,
|
||||
children,
|
||||
size,
|
||||
lgSize,
|
||||
xlSize,
|
||||
isOpen,
|
||||
panelId,
|
||||
buttonId,
|
||||
onToggle,
|
||||
className,
|
||||
}: AccordionViewProps) {
|
||||
const sizeClass = resolvedLayoutClasses(size, lgSize, xlSize);
|
||||
|
||||
return (
|
||||
<div className={`w-full ${className}`.trim()}>
|
||||
<h3 className="m-0">
|
||||
<button
|
||||
id={buttonId}
|
||||
type="button"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={panelId}
|
||||
onClick={onToggle}
|
||||
className={`flex w-full ${sizeClass.header} text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-content-default-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[#141414]`}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-scale-004)]">
|
||||
<span
|
||||
className={`font-inter text-[var(--color-content-default-primary,white)] ${sizeClass.title}`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
{subhead ? (
|
||||
<span
|
||||
className={`font-inter font-medium text-[var(--color-content-default-tertiary,#b4b4b4)] ${sizeClass.subhead}`}
|
||||
>
|
||||
{subhead}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span
|
||||
className={`flex size-6 shrink-0 items-center justify-center text-[var(--color-content-default-primary,white)] transition-transform ${isOpen ? "-rotate-90" : "rotate-90"}`}
|
||||
aria-hidden
|
||||
>
|
||||
<Icon name="chevron_right" size={24} />
|
||||
</span>
|
||||
</button>
|
||||
</h3>
|
||||
{isOpen && children ? (
|
||||
<div
|
||||
id={panelId}
|
||||
role="region"
|
||||
aria-labelledby={buttonId}
|
||||
className={sizeClass.panel}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
<Divider type="content" orientation="horizontal" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AccordionView.displayName = "AccordionView";
|
||||
|
||||
export default memo(AccordionView);
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./Accordion.container";
|
||||
export type { AccordionProps, AccordionSizeValue } from "./Accordion.types";
|
||||
Reference in New Issue
Block a user