Profile page UI and functionality implemented

This commit is contained in:
adilallo
2026-04-25 17:57:58 -06:00
parent 7dd2562bae
commit 68517796a9
103 changed files with 4439 additions and 1476 deletions
@@ -0,0 +1,17 @@
"use client";
import { memo } from "react";
import { ListView } from "./List.view";
import type { ListProps } from "./List.types";
/**
* Figma: "List Edit" list frame — S (21863:45631), M (21863:45493), L (21844:4405).
* Composes {@link ListEntry} rows with a shared list-level top rule when enabled.
*/
const ListContainer = memo<ListProps>((props) => {
return <ListView {...props} />;
});
ListContainer.displayName = "List";
export default ListContainer;
+29
View File
@@ -0,0 +1,29 @@
import type { IconName } from "../../asset/Icon";
import type {
ListEntryVariant,
ListSize,
} from "../ListEntry/ListEntry.types";
export type ListItem = {
id: string;
title: string;
description: string;
href?: string;
onClick?: () => void;
/** Per-row icon; falls back to list-level {@link ListProps.leadingIcon}. */
leadingIcon?: IconName;
variant?: ListEntryVariant;
showDescription?: boolean;
};
export type ListProps = {
items: ListItem[];
size?: ListSize;
topDivider?: boolean;
leadingIcon?: IconName;
className?: string;
};
export type { ListEntryVariant, ListSize };
export type ListViewProps = ListProps;
+47
View File
@@ -0,0 +1,47 @@
"use client";
import { memo } from "react";
import Divider from "../../utility/Divider";
import ListEntry from "../ListEntry";
import { FIGMA_LIST_ROOT } from "../listSizeLayout";
import type { ListViewProps } from "./List.types";
export const ListView = memo(function ListView({
items,
size = "m",
topDivider = true,
leadingIcon = "edit",
className = "",
}: ListViewProps) {
return (
<div
className={`flex w-full max-w-[1590px] flex-col items-start ${className}`}
data-figma-node={FIGMA_LIST_ROOT[size]}
>
{topDivider ? <Divider type="content" orientation="horizontal" /> : null}
<ul className="m-0 flex w-full list-none flex-col items-start p-0">
{items.map((item) => (
<li
key={item.id}
className="flex w-full flex-col items-stretch [list-style:none]"
>
<ListEntry
title={item.title}
description={item.description}
showDescription={item.showDescription}
href={item.href}
onClick={item.onClick}
size={size}
leadingIcon={item.leadingIcon ?? leadingIcon}
variant={item.variant}
topDivider={false}
bottomDivider
/>
</li>
))}
</ul>
</div>
);
});
ListView.displayName = "ListView";
+3
View File
@@ -0,0 +1,3 @@
export { default } from "./List.container";
export type { ListProps, ListItem, ListSize, ListViewProps } from "./List.types";
export { LIST_SIZE_OPTIONS } from "../ListEntry/ListEntry.types";
@@ -0,0 +1,17 @@
"use client";
import { memo } from "react";
import { ListEntryView } from "./ListEntry.view";
import type { ListEntryProps } from "./ListEntry.types";
/**
* Figma: "Base / Interactive" (21844:4118). Single list row: optional top rule,
* leading icon, title, optional description, chevron, optional bottom rule.
*/
const ListEntryContainer = memo<ListEntryProps>((props) => {
return <ListEntryView {...props} />;
});
ListEntryContainer.displayName = "ListEntry";
export default ListEntryContainer;
@@ -0,0 +1,27 @@
import type { IconName } from "../../asset/Icon";
export const LIST_SIZE_OPTIONS = ["s", "m", "l"] as const;
export type ListSize = (typeof LIST_SIZE_OPTIONS)[number];
export const LIST_ENTRY_VARIANT_OPTIONS = ["default", "danger", "muted"] as const;
export type ListEntryVariant = (typeof LIST_ENTRY_VARIANT_OPTIONS)[number];
export type ListEntryProps = {
title: string;
description?: string;
/** @default true */
showDescription?: boolean;
href?: string;
onClick?: () => void;
size?: ListSize;
leadingIcon?: IconName;
/** Row tone (e.g. profile destructive / disabled rows). @default "default" */
variant?: ListEntryVariant;
/** Renders a line above the row (Base / Interactive). @default false */
topDivider?: boolean;
/** Renders a line under the row. @default true */
bottomDivider?: boolean;
className?: string;
};
export type ListEntryViewProps = ListEntryProps;
@@ -0,0 +1,173 @@
"use client";
import { memo } from "react";
import Link from "next/link";
import Icon, { type IconName } from "../../asset/Icon";
import Divider from "../../utility/Divider";
import { FIGMA_LIST_ENTRY_OUTER, listEntrySizeLayout } from "../listSizeLayout";
import type {
ListEntryViewProps,
ListEntryVariant,
ListSize,
} from "./ListEntry.types";
type RowCoreProps = {
title: string;
description?: string;
showDescription: boolean;
href?: string;
onClick?: () => void;
leadingIcon: IconName;
size: ListSize;
variant: ListEntryVariant;
};
const ListEntryRow = memo(function ListEntryRow({
title,
description,
showDescription,
href,
onClick,
leadingIcon,
size,
variant,
}: RowCoreProps) {
const layout = listEntrySizeLayout[size];
const leadingBoxClass =
size === "s"
? "flex h-6 w-6 shrink-0 items-center justify-center"
: size === "m"
? "flex size-8 shrink-0 items-center justify-center"
: "flex size-10 shrink-0 items-center justify-center";
const chevronSize = size === "s" ? 16 : size === "l" ? 32 : 24;
const shellExtra =
variant === "muted" ? "opacity-60 hover:!bg-transparent" : "";
const titleClass =
variant === "danger"
? `${layout.title} !text-[var(--color-content-default-negative-primary)]`
: layout.title;
const leadingToneClass =
variant === "danger"
? "text-[var(--color-content-default-negative-primary)]"
: "text-[var(--color-content-default-primary)]";
const chevronToneClass =
variant === "danger"
? "text-[var(--color-content-default-negative-primary)]"
: "text-[var(--color-content-default-primary)]";
const leadingSlot = (
<div className={`${leadingBoxClass} ${leadingToneClass}`}>
<Icon name={leadingIcon} size={24} />
</div>
);
const chevronSlot = (
<div
className={
size === "s"
? `flex size-4 shrink-0 items-center justify-center ${chevronToneClass}`
: size === "l"
? `flex size-8 shrink-0 items-center justify-center ${chevronToneClass}`
: `flex size-6 shrink-0 items-center justify-center ${chevronToneClass}`
}
>
<Icon name="chevron_right" size={chevronSize} />
</div>
);
const textBlock = (
<>
<div className="flex w-full min-w-0 items-center justify-between">
<p className={titleClass}>{title}</p>
</div>
{showDescription && description != null && description !== "" ? (
<p className={layout.description}>{description}</p>
) : null}
</>
);
const inner = (
<>
{leadingSlot}
<div className={layout.textCol}>{textBlock}</div>
{chevronSlot}
</>
);
const shellClass = `${layout.shell} ${shellExtra}`.trim();
if (href) {
return (
<Link
href={href}
className={shellClass}
data-figma-node={layout.rowFigma}
>
{inner}
</Link>
);
}
if (onClick) {
return (
<button
type="button"
onClick={onClick}
className={shellClass}
data-figma-node={layout.rowFigma}
>
{inner}
</button>
);
}
return (
<div className={shellClass} data-figma-node={layout.rowFigma}>
{inner}
</div>
);
});
ListEntryRow.displayName = "ListEntryRow";
export const ListEntryView = memo(function ListEntryView({
title,
description = "",
showDescription = true,
href,
onClick,
size = "m",
leadingIcon = "edit",
variant = "default",
topDivider = false,
bottomDivider = true,
className = "",
}: ListEntryViewProps) {
return (
<div
className={`flex w-full flex-col items-start ${className}`}
data-figma-node={FIGMA_LIST_ENTRY_OUTER[size]}
>
{topDivider ? <Divider type="content" orientation="horizontal" /> : null}
<ListEntryRow
title={title}
description={description}
showDescription={showDescription}
href={href}
onClick={onClick}
leadingIcon={leadingIcon}
size={size}
variant={variant}
/>
{bottomDivider ? <Divider type="content" orientation="horizontal" /> : null}
</div>
);
});
ListEntryView.displayName = "ListEntryView";
@@ -0,0 +1,3 @@
export { default } from "./ListEntry.container";
export type { ListEntryProps, ListSize } from "./ListEntry.types";
export { LIST_SIZE_OPTIONS } from "./ListEntry.types";
+69
View File
@@ -0,0 +1,69 @@
import type { ListSize } from "./ListEntry/ListEntry.types";
export const rowShellBase =
"flex w-full cursor-pointer items-center text-left text-[var(--color-content-default-primary)] outline-none " +
"transition-colors " +
"hover:bg-[var(--color-surface-default-tertiary)] " +
"focus-visible:ring-2 focus-visible:ring-[var(--color-content-default-primary)] " +
"focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]";
/**
* Figma: "ListEntry" / Base Interactive (21844:4118) — S/M/L row + outer shell node ids.
* Full list frame roots: 21863:45631 (S), 21863:45493 (M), 21844:4405 (L).
*/
export const FIGMA_LIST_ENTRY_OUTER: Record<ListSize, string> = {
s: "21863:45436",
m: "21863:45422",
l: "21844:4119",
};
export const FIGMA_LIST_ROOT: Record<ListSize, string> = {
s: "21863:45631",
m: "21863:45493",
l: "21844:4405",
};
export const FIGMA_LIST_ENTRY_ROW: Record<ListSize, string> = {
s: "21863:45438",
m: "21863:45424",
l: "21844:4120",
};
type RowLayout = {
shell: string;
textCol: string;
title: string;
description: string;
rowFigma: string;
};
export const listEntrySizeLayout: Record<ListSize, RowLayout> = {
s: {
shell: `${rowShellBase} min-h-0 gap-1.5 py-[var(--spacing-scale-012)]`,
textCol: "flex min-w-0 flex-1 flex-col items-start justify-center",
title:
"min-w-0 flex-1 font-inter text-sm font-medium leading-[18px] text-[var(--color-content-default-primary)]",
description:
"w-full font-inter text-xs font-normal leading-4 text-[var(--color-content-default-secondary)]",
rowFigma: FIGMA_LIST_ENTRY_ROW.s,
},
m: {
shell: `${rowShellBase} min-h-16 gap-[var(--spacing-scale-008)] py-[var(--spacing-scale-012)]`,
textCol: "flex min-w-0 flex-1 flex-col items-start justify-center",
title:
"min-w-0 flex-1 font-inter text-lg font-medium leading-6 text-[var(--color-content-default-primary)]",
description:
"w-full font-inter text-base font-normal leading-6 text-[var(--color-content-default-secondary)]",
rowFigma: FIGMA_LIST_ENTRY_ROW.m,
},
l: {
shell: `${rowShellBase} min-h-16 gap-[var(--spacing-scale-012)] py-[var(--spacing-scale-016)]`,
textCol:
"flex min-w-0 flex-1 flex-col items-start justify-center gap-[var(--spacing-scale-004)]",
title:
"min-w-0 flex-1 font-inter text-2xl font-normal leading-7 text-[var(--color-content-default-primary)]",
description:
"w-full font-inter text-lg font-normal leading-[1.3] text-[var(--color-content-default-secondary)]",
rowFigma: FIGMA_LIST_ENTRY_ROW.l,
},
};