Profile page UI and functionality implemented
This commit is contained in:
@@ -7,9 +7,28 @@ import Logo from "../asset/logo";
|
||||
import Separator from "../utility/Separator";
|
||||
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
|
||||
|
||||
/**
|
||||
* Figma: "Navigation / Footer" (18411-62917).
|
||||
* Tiers: smallest viewports (below `md`), `md` through `lg`, `lg` and up.
|
||||
* Matches `--breakpoint-md: 640px`, `--breakpoint-lg: 1024px` in `app/tailwind.css`.
|
||||
*/
|
||||
const Footer = memo(() => {
|
||||
const t = useTranslation("footer");
|
||||
|
||||
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";
|
||||
|
||||
const bodyTextClass =
|
||||
"text-[var(--color-content-default-primary)] font-inter text-base font-medium leading-5 tracking-[0%] lg:text-2xl lg:font-normal lg:leading-7";
|
||||
|
||||
/** Figma 18411:62925 (1024+): org name is one line, `w-full whitespace-nowrap`. */
|
||||
const orgNameClass = `${bodyTextClass} lg:whitespace-nowrap`;
|
||||
|
||||
const primaryLinkClass = `text-[var(--color-content-default-primary)] font-inter text-base font-medium leading-5 tracking-[0%] ${linkFocusClass} p-2 -m-2 cursor-pointer lg:text-2xl lg:font-normal lg:leading-7`;
|
||||
|
||||
/** Figma 18411:62944: 40px gaps, w-[396px] link block; `p-2` on links overruns 396px—tighten x at `md+` row. */
|
||||
const legalLinkClass = `text-[var(--color-content-default-secondary)] font-inter text-sm font-normal leading-5 tracking-[0%] ${linkFocusClass} p-2 -m-2 cursor-pointer underline decoration-solid [text-decoration-skip-ink:none] md:px-0 md:py-1 md:mx-0 md:text-xs md:leading-4 md:whitespace-nowrap md:no-underline md:text-[var(--color-content-default-primary)] lg:text-sm lg:leading-5 lg:text-[var(--color-content-default-primary)]`;
|
||||
|
||||
// Schema markup for organization information
|
||||
const schemaData = {
|
||||
"@context": "https://schema.org",
|
||||
@@ -28,126 +47,160 @@ const Footer = memo(() => {
|
||||
/>
|
||||
<footer className="bg-[var(--color-surface-default-primary)] w-full">
|
||||
<div
|
||||
className="flex flex-col items-start mx-auto
|
||||
className="mx-auto flex max-w-[1920px] flex-col
|
||||
gap-[var(--spacing-measures-spacing-040)]
|
||||
px-[var(--spacing-measures-spacing-016)]
|
||||
py-[var(--spacing-measures-spacing-040)]
|
||||
gap-[var(--spacing-measures-spacing-040)]
|
||||
sm:px-[var(--spacing-measures-spacing-032)]
|
||||
sm:py-[var(--spacing-measures-spacing-024)]
|
||||
sm:gap-[var(--spacing-measures-spacing-024)]
|
||||
lg:px-[var(--spacing-measures-spacing-120,120px)]
|
||||
lg:py-[var(--spacing-measures-spacing-096,96px)]
|
||||
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
|
||||
md:gap-[var(--spacing-measures-spacing-024)]
|
||||
md:px-[var(--spacing-measures-spacing-032)]
|
||||
md:py-[var(--spacing-measures-spacing-024)]
|
||||
lg:gap-[var(--spacing-measures-spacing-060,60px)]
|
||||
lg:px-[var(--spacing-scale-064)]
|
||||
lg:py-[var(--spacing-scale-096)]"
|
||||
>
|
||||
{/* Logo */}
|
||||
<Logo size="footer" wordmark />
|
||||
<div
|
||||
className="flex w-full flex-col
|
||||
gap-[var(--spacing-scale-032)]
|
||||
md:gap-[var(--spacing-scale-048)]
|
||||
lg:gap-[var(--spacing-measures-spacing-060,60px)]"
|
||||
>
|
||||
<Logo size="footer" wordmark />
|
||||
|
||||
{/* Content section */}
|
||||
<div className="flex flex-col items-start w-full gap-[var(--spacing-measures-spacing-048,48px)] sm:flex-row sm:justify-between sm:gap-0">
|
||||
{/* Branding Section */}
|
||||
<div className="flex flex-col items-start gap-[var(--spacing-measures-spacing-064,64px)] order-2 sm:order-1">
|
||||
{/* Contact info */}
|
||||
<div className="flex flex-col items-start gap-[var(--spacing-measures-spacing-016,16px)]">
|
||||
<div className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal">
|
||||
{t("organization.name")}
|
||||
<div
|
||||
className="flex w-full flex-col
|
||||
gap-[var(--spacing-scale-048)]
|
||||
md:flex-row md:items-start md:justify-between md:gap-0"
|
||||
>
|
||||
<div
|
||||
className="order-2 flex flex-col
|
||||
gap-[var(--spacing-scale-048)]
|
||||
md:order-1 md:max-w-[min(100%,334px)]
|
||||
lg:max-w-[min(100%,334px)]
|
||||
lg:gap-[var(--spacing-scale-064)]"
|
||||
>
|
||||
<div
|
||||
className="flex flex-col
|
||||
gap-[var(--spacing-measures-spacing-016,16px)]"
|
||||
>
|
||||
<div className={orgNameClass}>{t("organization.name")}</div>
|
||||
<a
|
||||
href={`mailto:${t("organization.email")}`}
|
||||
className={`${bodyTextClass} ${linkFocusClass} p-2 -m-2 cursor-pointer`}
|
||||
>
|
||||
{t("organization.email")}
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href={`mailto:${t("organization.email")}`}
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal 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 p-2 -m-2 cursor-pointer"
|
||||
|
||||
<div
|
||||
className="flex flex-col
|
||||
gap-[var(--spacing-measures-spacing-016,16px)]"
|
||||
>
|
||||
{t("organization.email")}
|
||||
</a>
|
||||
<a
|
||||
href={t("social.bluesky.url")}
|
||||
className={`group flex items-center gap-[var(--spacing-measures-spacing-06,6px)] ${linkFocusClass} p-2 -m-2 cursor-pointer`}
|
||||
aria-label={t("social.bluesky.ariaLabel")}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- social logo */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
|
||||
alt="Bluesky"
|
||||
width={24}
|
||||
height={22}
|
||||
className="h-[21px] w-[24px] flex-shrink-0 transition-transform group-hover:scale-110"
|
||||
/>
|
||||
<div className={bodyTextClass}>{t("social.bluesky.handle")}</div>
|
||||
</a>
|
||||
<a
|
||||
href={t("social.gitlab.url")}
|
||||
className={`group flex items-center gap-[var(--spacing-measures-spacing-06,6px)] ${linkFocusClass} p-2 -m-2 cursor-pointer`}
|
||||
aria-label={t("social.gitlab.ariaLabel")}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.GITLAB_ICON)}
|
||||
alt="GitLab"
|
||||
width={22}
|
||||
height={22}
|
||||
className="h-5 w-[22px] flex-shrink-0 grayscale transition-transform group-hover:scale-110"
|
||||
/>
|
||||
<div className={bodyTextClass}>{t("social.gitlab.handle")}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social media links */}
|
||||
<div className="flex flex-col items-start gap-[var(--spacing-measures-spacing-016,16px)]">
|
||||
<a
|
||||
href={t("social.bluesky.url")}
|
||||
className="flex items-center gap-[var(--spacing-measures-spacing-06,6px)] 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 p-2 -m-2 cursor-pointer group"
|
||||
aria-label={t("social.bluesky.ariaLabel")}
|
||||
<nav
|
||||
aria-label="Footer"
|
||||
className="order-1 flex w-full max-w-full flex-col
|
||||
items-start
|
||||
gap-[var(--spacing-scale-032)]
|
||||
md:order-2 md:w-auto md:items-end md:text-right
|
||||
md:gap-[var(--spacing-scale-032)]"
|
||||
>
|
||||
<Link
|
||||
href="#"
|
||||
className={`w-full text-left ${primaryLinkClass} md:w-auto md:text-right`}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- social logo */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.BLUESKY_LOGO)}
|
||||
alt="Bluesky"
|
||||
width={24}
|
||||
height={22}
|
||||
className="flex-shrink-0 group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
<div className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal">
|
||||
{t("social.bluesky.handle")}
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={t("social.gitlab.url")}
|
||||
className="flex items-center gap-[var(--spacing-measures-spacing-06,6px)] 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 p-2 -m-2 cursor-pointer group"
|
||||
aria-label={t("social.gitlab.ariaLabel")}
|
||||
{t("navigation.useCases")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/learn"
|
||||
className={`w-full text-left ${primaryLinkClass} md:w-auto md:text-right`}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- social icon */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.GITLAB_ICON)}
|
||||
alt="GitLab"
|
||||
width={22}
|
||||
height={22}
|
||||
className="flex-shrink-0 grayscale group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
<div className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal">
|
||||
{t("social.gitlab.handle")}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links Section */}
|
||||
<div className="flex flex-col items-start gap-[var(--spacing-measures-spacing-016,16px)] order-1 sm:order-2">
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal 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 p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
{t("navigation.useCases")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/learn"
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal 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 p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
{t("navigation.learn")}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-primary)] font-inter text-base leading-5 font-medium tracking-[0%] lg:text-2xl lg:leading-7 lg:font-normal 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 p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
{t("navigation.about")}
|
||||
</Link>
|
||||
{t("navigation.learn")}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className={`w-full text-left ${primaryLinkClass} md:w-auto md:text-right`}
|
||||
>
|
||||
{t("navigation.about")}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Legal Links */}
|
||||
<div className="flex flex-col items-start gap-[var(--spacing-measures-spacing-016,16px)] sm:flex-row sm:gap-[var(--spacing-measures-spacing-024,24px)]">
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6 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 p-2 -m-2 cursor-pointer"
|
||||
<div
|
||||
className="flex w-full flex-col
|
||||
gap-[var(--spacing-scale-032)]
|
||||
text-[var(--color-content-default-primary)]
|
||||
md:flex-row md:items-start md:justify-between md:gap-[var(--spacing-scale-040)]
|
||||
md:whitespace-nowrap
|
||||
md:text-xs md:leading-4
|
||||
lg:text-sm lg:leading-5"
|
||||
>
|
||||
<p
|
||||
className="w-full font-inter text-sm font-normal leading-5 tracking-[0%]
|
||||
text-[var(--color-content-default-secondary)]
|
||||
md:w-auto
|
||||
md:text-xs md:leading-4
|
||||
lg:text-sm lg:leading-5"
|
||||
>
|
||||
{t("legal.privacyPolicy")}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6 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 p-2 -m-2 cursor-pointer"
|
||||
{t("copyright")}
|
||||
</p>
|
||||
<div
|
||||
className="flex w-full min-w-0 flex-col flex-wrap
|
||||
gap-[var(--spacing-scale-032)]
|
||||
font-inter text-sm
|
||||
text-[var(--color-content-default-primary)]
|
||||
md:max-w-[min(100%,396px)]
|
||||
md:flex-row md:flex-nowrap md:content-center md:items-center md:justify-end
|
||||
md:gap-[var(--spacing-scale-040)]
|
||||
md:text-xs md:leading-4
|
||||
lg:max-w-none
|
||||
lg:gap-10
|
||||
lg:text-sm lg:leading-5"
|
||||
>
|
||||
{t("legal.termsOfService")}
|
||||
</Link>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6 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 p-2 -m-2 cursor-pointer"
|
||||
>
|
||||
{t("legal.cookiesSettings")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="text-[var(--color-content-default-secondary)] font-inter text-sm leading-5 font-normal tracking-[0%] lg:text-base lg:leading-6">
|
||||
{t("copyright")}
|
||||
<Link href="#" className={legalLinkClass}>
|
||||
{t("legal.privacyPolicy")}
|
||||
</Link>
|
||||
<Link href="#" className={legalLinkClass}>
|
||||
{t("legal.termsOfService")}
|
||||
</Link>
|
||||
<Link href="#" className={legalLinkClass}>
|
||||
{t("legal.cookiesSettings")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import LinkView from "./Link.view";
|
||||
import type { LinkProps } from "./Link.types";
|
||||
|
||||
/**
|
||||
* Figma: "Link" in Navigation — "Link, CTA" (21861:21428). Paragraph uses the
|
||||
* same border-b + pb-0.5 spacing as default, with the rule visible at rest.
|
||||
*/
|
||||
const Link = memo<LinkProps>(
|
||||
({
|
||||
children,
|
||||
className = "",
|
||||
type: linkType = "primary",
|
||||
variant = "default",
|
||||
theme = "light",
|
||||
leadingIcon = true,
|
||||
trailingIcon = true,
|
||||
href,
|
||||
onClick,
|
||||
prefetch,
|
||||
replace,
|
||||
scroll,
|
||||
rel,
|
||||
target,
|
||||
id,
|
||||
"aria-label": ariaLabel,
|
||||
"aria-current": ariaCurrent,
|
||||
}) => {
|
||||
return (
|
||||
<LinkView
|
||||
className={className}
|
||||
type={linkType}
|
||||
variant={variant}
|
||||
theme={theme}
|
||||
leadingIcon={variant === "default" ? leadingIcon : false}
|
||||
trailingIcon={variant === "default" ? trailingIcon : false}
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
prefetch={prefetch}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
rel={rel}
|
||||
target={target}
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
aria-current={ariaCurrent}
|
||||
>
|
||||
{children}
|
||||
</LinkView>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Link.displayName = "Link";
|
||||
|
||||
export default Link;
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { AriaAttributes, ReactNode } from "react";
|
||||
|
||||
export const LINK_TYPE_OPTIONS = ["primary", "secondary"] as const;
|
||||
export type LinkTypeValue = (typeof LINK_TYPE_OPTIONS)[number];
|
||||
|
||||
export const LINK_VARIANT_OPTIONS = ["default", "paragraph"] as const;
|
||||
export type LinkVariantValue = (typeof LINK_VARIANT_OPTIONS)[number];
|
||||
|
||||
export const LINK_THEME_OPTIONS = ["light", "dark"] as const;
|
||||
export type LinkThemeValue = (typeof LINK_THEME_OPTIONS)[number];
|
||||
|
||||
/**
|
||||
* Figma: "Link" in Navigation — `21861:21428`. Interaction states are
|
||||
* implemented with CSS; there is no `state` prop.
|
||||
*/
|
||||
export type LinkProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Figma: Type (primary or secondary). */
|
||||
type?: LinkTypeValue;
|
||||
/** Figma: default (with icons) or paragraph (underlined). */
|
||||
variant?: LinkVariantValue;
|
||||
/** Figma: light or dark surface. */
|
||||
theme?: LinkThemeValue;
|
||||
/** Figma "default" variant: 16px plus before text. Ignored for `paragraph`. */
|
||||
leadingIcon?: boolean;
|
||||
/** Figma "default" variant: 16px plus after text. Ignored for `paragraph`. */
|
||||
trailingIcon?: boolean;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
|
||||
/** Passed to `next/link` when `href` is set. */
|
||||
prefetch?: boolean;
|
||||
replace?: boolean;
|
||||
scroll?: boolean;
|
||||
rel?: string;
|
||||
target?: string;
|
||||
id?: string;
|
||||
"aria-label"?: string;
|
||||
"aria-current"?: AriaAttributes["aria-current"];
|
||||
};
|
||||
|
||||
export type LinkViewProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
type: LinkTypeValue;
|
||||
variant: LinkVariantValue;
|
||||
theme: LinkThemeValue;
|
||||
leadingIcon: boolean;
|
||||
trailingIcon: boolean;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
|
||||
dataFigmaNode?: string;
|
||||
prefetch?: boolean;
|
||||
replace?: boolean;
|
||||
scroll?: boolean;
|
||||
rel?: string;
|
||||
target?: string;
|
||||
id?: string;
|
||||
"aria-label"?: string;
|
||||
"aria-current"?: AriaAttributes["aria-current"];
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import NextLink from "next/link";
|
||||
import { memo } from "react";
|
||||
import type { MouseEventHandler, ReactNode } from "react";
|
||||
import type { LinkTypeValue, LinkViewProps, LinkThemeValue, LinkVariantValue } from "./Link.types";
|
||||
|
||||
const FIGMA_ROOT = "21861:21428";
|
||||
|
||||
/** Profile & card small viewports: Figma Sizing/300 + label line (350). ≥640px: 18px / 1.3. */
|
||||
const LINK_TYPOGRAPHY =
|
||||
"font-inter font-normal text-[length:var(--sizing-300)] leading-[var(--sizing-350)] min-[640px]:text-[18px] min-[640px]:leading-[1.3]";
|
||||
|
||||
function linkFocusRing(theme: LinkThemeValue) {
|
||||
return theme === "light"
|
||||
? "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--color-border-link-focus)] focus-visible:rounded-lg"
|
||||
: "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--color-border-link-invert-focus)] focus-visible:rounded-lg";
|
||||
}
|
||||
|
||||
function defaultRootClass(theme: LinkThemeValue, linkType: LinkTypeValue) {
|
||||
const focusRing = linkFocusRing(theme);
|
||||
if (theme === "light" && linkType === "primary") {
|
||||
return `group inline-flex min-h-8 max-h-16 w-fit max-w-full shrink-0 items-center gap-2 rounded-lg ${LINK_TYPOGRAPHY} text-[var(--color-link-primary)] hover:text-[var(--color-link-primary-hover)] focus-visible:text-[var(--color-link-primary-focus)] active:text-[var(--color-link-primary-active)] ${focusRing}`;
|
||||
}
|
||||
if (theme === "light" && linkType === "secondary") {
|
||||
return `group inline-flex min-h-8 max-h-16 w-fit max-w-full shrink-0 items-center gap-2 rounded-lg ${LINK_TYPOGRAPHY} text-[var(--color-link-secondary)] hover:text-[var(--color-link-secondary-hover)] focus-visible:text-[var(--color-link-secondary-focus)] active:text-[var(--color-link-secondary-active)] ${focusRing}`;
|
||||
}
|
||||
if (theme === "dark" && linkType === "primary") {
|
||||
return `group inline-flex min-h-8 max-h-16 w-fit max-w-full shrink-0 items-center gap-2 rounded-lg ${LINK_TYPOGRAPHY} text-[var(--color-link-invert-primary)] hover:text-[var(--color-link-invert-primary-hover)] focus-visible:text-[var(--color-link-invert-primary-focus)] active:text-[var(--color-link-invert-primary-active)] ${focusRing}`;
|
||||
}
|
||||
return `group inline-flex min-h-8 max-h-16 w-fit max-w-full shrink-0 items-center gap-2 rounded-lg ${LINK_TYPOGRAPHY} text-[var(--color-link-invert-secondary)] hover:text-[var(--color-link-invert-secondary-hover)] focus-visible:text-[var(--color-link-invert-secondary-focus)] active:text-[var(--color-link-invert-secondary-active)] ${focusRing}`;
|
||||
}
|
||||
|
||||
function defaultUnderlineClass(theme: LinkThemeValue, linkType: LinkTypeValue) {
|
||||
if (theme === "light" && linkType === "primary") {
|
||||
return "inline-block min-w-0 max-w-full border-b border-transparent bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-primary-hover)] group-focus-visible:border-[var(--color-link-primary-focus)] group-active:border-[var(--color-link-primary-active)]";
|
||||
}
|
||||
if (theme === "light" && linkType === "secondary") {
|
||||
return "inline-block min-w-0 max-w-full border-b border-transparent bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-secondary-hover)] group-focus-visible:border-[var(--color-link-secondary-focus)] group-active:border-[var(--color-link-secondary-active)]";
|
||||
}
|
||||
if (theme === "dark" && linkType === "primary") {
|
||||
return "inline-block min-w-0 max-w-full border-b border-transparent bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-invert-primary-hover)] group-focus-visible:border-[var(--color-link-invert-primary-focus)] group-active:border-[var(--color-link-invert-primary-active)]";
|
||||
}
|
||||
return "inline-block min-w-0 max-w-full border-b border-transparent bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-invert-secondary-hover)] group-focus-visible:border-[var(--color-link-invert-secondary-focus)] group-active:border-[var(--color-link-invert-secondary-active)]";
|
||||
}
|
||||
|
||||
/** Same `pb-0.5` + `border-b` as default, but the rule is visible at rest. */
|
||||
function paragraphUnderlineClass(theme: LinkThemeValue, linkType: LinkTypeValue) {
|
||||
if (theme === "light" && linkType === "primary") {
|
||||
return "inline-block min-w-0 max-w-full border-b border-[var(--color-link-primary)] bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-primary-hover)] group-focus-visible:border-[var(--color-link-primary-focus)] group-active:border-[var(--color-link-primary-active)]";
|
||||
}
|
||||
if (theme === "light" && linkType === "secondary") {
|
||||
return "inline-block min-w-0 max-w-full border-b border-[var(--color-link-secondary)] bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-secondary-hover)] group-focus-visible:border-[var(--color-link-secondary-focus)] group-active:border-[var(--color-link-secondary-active)]";
|
||||
}
|
||||
if (theme === "dark" && linkType === "primary") {
|
||||
return "inline-block min-w-0 max-w-full border-b border-[var(--color-link-invert-primary)] bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-invert-primary-hover)] group-focus-visible:border-[var(--color-link-invert-primary-focus)] group-active:border-[var(--color-link-invert-primary-active)]";
|
||||
}
|
||||
return "inline-block min-w-0 max-w-full border-b border-[var(--color-link-invert-secondary)] bg-transparent px-0 pb-0.5 text-left text-inherit transition-[border-color] group-hover:border-[var(--color-link-invert-secondary-hover)] group-focus-visible:border-[var(--color-link-invert-secondary-focus)] group-active:border-[var(--color-link-invert-secondary-active)]";
|
||||
}
|
||||
|
||||
function LinkPlus12() {
|
||||
return (
|
||||
<span className="inline-flex size-4 shrink-0 items-center justify-center text-inherit" aria-hidden>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="shrink-0"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M5.25 0h1.5v4.5H12v1.5H6.75V12h-1.5V6.75H0V5.25h5.25V0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkViewInner({
|
||||
variant,
|
||||
theme,
|
||||
type,
|
||||
leadingIcon,
|
||||
trailingIcon,
|
||||
children,
|
||||
}: {
|
||||
variant: LinkVariantValue;
|
||||
theme: LinkThemeValue;
|
||||
type: LinkTypeValue;
|
||||
leadingIcon: boolean;
|
||||
trailingIcon: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
if (variant === "paragraph") {
|
||||
return (
|
||||
<span className={`min-h-0 min-w-0 max-w-full shrink ${paragraphUnderlineClass(theme, type)}`}>
|
||||
<span className="block min-w-0 whitespace-normal [overflow-wrap:anywhere] text-inherit">
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{leadingIcon ? <LinkPlus12 /> : null}
|
||||
<span className={`min-h-0 min-w-0 max-w-full shrink ${defaultUnderlineClass(theme, type)}`}>
|
||||
<span className="block min-w-0 whitespace-normal [overflow-wrap:anywhere] text-inherit">
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
{trailingIcon ? <LinkPlus12 /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkView({
|
||||
children,
|
||||
className,
|
||||
type,
|
||||
variant,
|
||||
theme,
|
||||
leadingIcon,
|
||||
trailingIcon,
|
||||
href,
|
||||
onClick,
|
||||
dataFigmaNode = FIGMA_ROOT,
|
||||
prefetch,
|
||||
replace,
|
||||
scroll,
|
||||
rel,
|
||||
target,
|
||||
id,
|
||||
"aria-label": ariaLabel,
|
||||
"aria-current": ariaCurrent,
|
||||
}: LinkViewProps) {
|
||||
const root = [defaultRootClass(theme, type), className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const content = (
|
||||
<LinkViewInner
|
||||
variant={variant}
|
||||
theme={theme}
|
||||
type={type}
|
||||
leadingIcon={leadingIcon}
|
||||
trailingIcon={trailingIcon}
|
||||
>
|
||||
{children}
|
||||
</LinkViewInner>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<NextLink
|
||||
href={href}
|
||||
className={root}
|
||||
data-figma-node={dataFigmaNode}
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
aria-current={ariaCurrent}
|
||||
prefetch={prefetch}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
rel={rel}
|
||||
target={target}
|
||||
onClick={onClick as MouseEventHandler<HTMLAnchorElement> | undefined}
|
||||
>
|
||||
{content}
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`${root} m-0 cursor-pointer border-0 bg-transparent p-0 text-left font-inherit [font-family:inherit]`}
|
||||
data-figma-node={dataFigmaNode}
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
aria-current={ariaCurrent}
|
||||
onClick={onClick as MouseEventHandler<HTMLButtonElement> | undefined}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
LinkView.displayName = "LinkView";
|
||||
|
||||
export default memo(LinkView);
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default } from "./Link.container";
|
||||
export type { LinkProps, LinkTypeValue, LinkVariantValue, LinkThemeValue } from "./Link.types";
|
||||
export {
|
||||
LINK_TYPE_OPTIONS,
|
||||
LINK_VARIANT_OPTIONS,
|
||||
LINK_THEME_OPTIONS,
|
||||
} from "./Link.types";
|
||||
@@ -162,9 +162,17 @@ function TopNavView({
|
||||
aria-label={t("ariaLabels.mainNavigationHeader")}
|
||||
>
|
||||
<nav
|
||||
className="flex items-center gap-[var(--spacing-scale-002)] sm:justify-between mx-auto h-[var(--spacing-scale-040)] lg:h-[84px] xl:h-[88px] px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] sm:px-[var(--spacing-measures-spacing-016)] sm:py-[var(--spacing-measures-spacing-008)] lg:px-[var(--spacing-measures-spacing-64,64px)] lg:py-[var(--spacing-measures-spacing-016,16px)] sm:gap-0"
|
||||
role="navigation"
|
||||
aria-label={t("ariaLabels.mainNavigation")}
|
||||
className="flex items-center gap-[var(--spacing-scale-002)] sm:justify-between mx-auto
|
||||
h-[var(--spacing-scale-040)]
|
||||
lg:h-auto
|
||||
px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)]
|
||||
sm:px-[var(--spacing-measures-spacing-016)] sm:py-[var(--spacing-measures-spacing-008)]
|
||||
lg:px-[var(--spacing-measures-spacing-64,64px)]
|
||||
lg:py-[var(--spacing-scale-020)]
|
||||
xl:py-[var(--spacing-scale-024)]
|
||||
sm:gap-0"
|
||||
role="navigation"
|
||||
aria-label={t("ariaLabels.mainNavigation")}
|
||||
>
|
||||
{/* Logo - Consistent left positioning across all breakpoints */}
|
||||
<Logo
|
||||
|
||||
Reference in New Issue
Block a user