diff --git a/.vscode/settings.json b/.vscode/settings.json index cb56bf9fab..02d49400e5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "typescript.tsdk": "node_modules/typescript/lib", "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "tailwindCSS.classFunctions": ["clsx"] } diff --git a/src/app/conf/_design-system/README.md b/src/app/conf/_design-system/README.md new file mode 100644 index 0000000000..5e2a979477 --- /dev/null +++ b/src/app/conf/_design-system/README.md @@ -0,0 +1 @@ +UI from 2025's designs diff --git a/src/app/conf/_design-system/anchor.tsx b/src/app/conf/_design-system/anchor.tsx new file mode 100644 index 0000000000..72c5635b83 --- /dev/null +++ b/src/app/conf/_design-system/anchor.tsx @@ -0,0 +1,42 @@ +import { ForwardedRef, forwardRef, ReactElement } from "react" +import NextLink from "next/link" +import type { LinkProps as NextLinkProps } from "next/link" + +// eslint-disable-next-line @typescript-eslint/no-namespace +export declare namespace AnchorProps { + interface IntrinsicAnchorProps + extends React.DetailedHTMLProps< + React.AnchorHTMLAttributes, + HTMLAnchorElement + > { + href: `#${string}` | `http${string}` + } + + interface InternalAnchorProps extends NextLinkProps {} +} + +export type AnchorProps = + | AnchorProps.IntrinsicAnchorProps + | AnchorProps.InternalAnchorProps + +export const Anchor = forwardRef(function Anchor( + props: AnchorProps, + ref: ForwardedRef, +) { + return isInternal(props) ? ( + + ) : ( + + ) +}) as (props: AnchorProps) => ReactElement + +function isInternal( + props: AnchorProps, +): props is AnchorProps.InternalAnchorProps { + return ( + typeof props.href === "object" || + (typeof props.href === "string" && + !props.href.startsWith("http") && + !props.href.startsWith("#")) + ) +} diff --git a/src/app/conf/_design-system/button.tsx b/src/app/conf/_design-system/button.tsx new file mode 100644 index 0000000000..af1220cf49 --- /dev/null +++ b/src/app/conf/_design-system/button.tsx @@ -0,0 +1,92 @@ +import { clsx } from "clsx" +import { Anchor } from "./anchor" + +type Size = "md" | "lg" +type Variant = "primary" | "secondary" + +// eslint-disable-next-line @typescript-eslint/no-namespace +export declare namespace ButtonProps { + export interface BaseProps { + size?: Size + variant?: Variant + } + + export interface AnchorProps + extends BaseProps, + React.DetailedHTMLProps< + React.AnchorHTMLAttributes, + HTMLAnchorElement + > { + href: string + as?: never + className?: string + } + + export interface ButtonProps + extends BaseProps, + React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement + > { + href?: never + as?: never + className?: string + disabled?: boolean + type?: "button" | "submit" | "reset" + onClick?: React.MouseEventHandler + } + + /** + * Use inside `` or as visual part of bigger interactive element. + * Prefer `a` and `button` Buttons otherwise. + */ + export interface NonInteractiveProps + extends BaseProps, + React.DetailedHTMLProps, HTMLElement> { + href?: never + as: "span" | "div" + className?: string + } +} + +export type ButtonProps = + | ButtonProps.AnchorProps + | ButtonProps.ButtonProps + | ButtonProps.NonInteractiveProps + +export function Button(props: ButtonProps) { + const className = clsx( + "relative flex items-center justify-center gap-2.5 font-normal text-base/none text-neu-0 bg-neu-900 hover:bg-neu-800 active:bg-neu-700 font-sans h-14 px-8 data-[size=md]:h-12 data-[variant=secondary]:bg-neu-100 data-[variant=secondary]:text-neu-900 dark:data-[variant=secondary]:text-neu-0 data-[variant=secondary]:hover:bg-neu-200/75 data-[variant=secondary]:active:bg-neu-200/90", + props.className, + ) + + const styleAttrs = { "data-size": props.size, "data-variant": props.variant } + + if ("href" in props && typeof props.href === "string") { + const { className: _1, size: _2, children, ...rest } = props + + return ( + + {children} + + ) + } + + if (props.as) { + const { className: _1, size: _2, children, as, ...rest } = props + const Root = as as "span" // we don't need HTMLDivElement type + return ( + + {children} + + ) + } + + const { className: _1, size: _2, children, ...rest } = props + + return ( + + ) +}