Compare commits

...

4 Commits

Author SHA1 Message Date
Trisha Lim
cd62ce254a Remove dropdown component 2025-01-25 18:11:44 +08:00
Trisha Lim
b0c326b98d Move kicker to a separate component 2025-01-25 17:54:32 +08:00
Trisha Lim
1f7330b6d3 Create a more flexible component for headings 2025-01-25 17:48:51 +08:00
Trisha Lim
60a69cc59b Design system improvements 2025-01-25 17:37:18 +08:00
8 changed files with 142 additions and 111 deletions

View File

@@ -1,11 +1,11 @@
import { clsx } from "clsx";
import Link from "next/link";
import { forwardRef } from "react";
import { Icon } from "../atoms/Icon";
import { Icon } from "./Icon";
import { Spinner } from "./Spinner";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "tertiary";
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
size?: "sm" | "md" | "lg";
href?: string;
newTab?: boolean;
@@ -42,6 +42,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
loading,
loadingText,
icon,
type = "button",
...buttonProps
},
ref,
@@ -58,16 +59,21 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
secondary:
"text-stone-900 border font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
tertiary: "text-blue underline underline-offset-4",
destructive:
"bg-red-600 border-red-600 text-white font-medium hover:bg-red-700 hover:border-red-700",
};
const classNames = clsx(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
sizeClasses[size],
variantClasses[variant],
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
);
const classNames =
variant === "plain"
? className
: clsx(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
sizeClasses[size],
variantClasses[variant],
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
);
if (href) {
return (
@@ -95,6 +101,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
{...buttonProps}
disabled={disabled || loading}
className={classNames}
type={type}
>
<ButtonIcon icon={icon} loading={loading} />

View File

@@ -0,0 +1,48 @@
import clsx from "clsx";
type HeadingProps = {
level?: 1 | 2 | 3 | 4 | 5 | 6;
size?: 1 | 2 | 3 | 4 | 5 | 6;
} & React.ComponentPropsWithoutRef<"h1" | "h2" | "h3" | "h4" | "h5" | "h6">;
const classes = {
1: [
"font-display",
"text-stone-950 dark:text-white",
"text-5xl lg:text-6xl",
"mb-3",
"font-medium",
"tracking-tighter",
],
2: [
"font-display",
"text-stone-950 dark:text-white",
"text-2xl md:text-4xl",
"mb-2",
"font-semibold",
"tracking-tight",
],
3: [
"font-display",
"text-stone-950 dark:text-white",
"text-xl md:text-2xl",
"mb-2",
"font-semibold",
"tracking-tight",
],
4: ["text-bold"],
5: [],
6: [],
};
export function Heading({
className,
level = 1,
size: customSize,
...props
}: HeadingProps) {
let Element: `h${typeof level}` = `h${level}`;
const size = customSize || level;
return <Element {...props} className={clsx(classes[size])} />;
}

View File

@@ -1,85 +1,38 @@
import clsx from "clsx";
import { Heading } from "./Heading";
interface HeadingProps {
children: React.ReactNode;
className?: string;
id?: string;
export function H1(
props: React.ComponentPropsWithoutRef<"h1"> & React.PropsWithChildren,
) {
return <Heading level={1} {...props} />;
}
export function H1({ children, className, id }: HeadingProps) {
return (
<h1
id={id}
className={clsx(
className,
"font-display",
"text-stone-950 dark:text-white",
"text-5xl lg:text-6xl",
"mb-3",
"font-medium",
"tracking-tighter",
)}
>
{children}
</h1>
);
export function H2(
props: React.ComponentPropsWithoutRef<"h2"> & React.PropsWithChildren,
) {
return <Heading level={2} {...props} />;
}
export function H2({ children, className, id }: HeadingProps) {
return (
<h2
id={id}
className={clsx(
className,
"font-display",
"text-stone-950 dark:text-white",
"text-2xl md:text-4xl",
"mb-2",
"font-semibold",
"tracking-tight",
)}
>
{children}
</h2>
);
export function H3(
props: React.ComponentPropsWithoutRef<"h3"> & React.PropsWithChildren,
) {
return <Heading level={3} {...props} />;
}
export function H3({ children, className, id }: HeadingProps) {
return (
<h3
id={id}
className={clsx(
className,
"font-display",
"text-stone-950 dark:text-white",
"text-xl md:text-2xl",
"mb-2",
"font-semibold",
"tracking-tight",
)}
>
{children}
</h3>
);
export function H4(
props: React.ComponentPropsWithoutRef<"h4"> & React.PropsWithChildren,
) {
return <Heading level={4} {...props} />;
}
export function H4({ children, className, id }: HeadingProps) {
return (
<h4 id={id} className={clsx(className, "text-bold")}>
{children}
</h4>
);
export function H5(
props: React.ComponentPropsWithoutRef<"h5"> & React.PropsWithChildren,
) {
return <Heading level={5} {...props} />;
}
export function Kicker({ children, className }: HeadingProps) {
return (
<p
className={clsx(
className,
"uppercase text-blue tracking-widest text-sm font-medium dark:text-stone-400",
)}
>
{children}
</p>
);
export function H6(
props: React.ComponentPropsWithoutRef<"h6"> & React.PropsWithChildren,
) {
return <Heading level={6} {...props} />;
}

View File

@@ -0,0 +1,21 @@
import clsx from "clsx";
export function Kicker({
children,
className,
as,
}: React.ComponentPropsWithoutRef<"p"> & {
as?: React.ElementType;
}) {
const Element = as ?? "p";
return (
<Element
className={clsx(
className,
"uppercase text-blue tracking-widest text-sm font-medium dark:text-stone-400",
)}
>
{children}
</Element>
);
}

View File

@@ -1,32 +1,33 @@
import { clsx } from "clsx";
import { useId } from "react";
import { forwardRef, useId } from "react";
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
// label is required for a11y, but you can hide it with a "label:sr-only" className
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
// label can be hidden with a "label:sr-only" className
label: string;
type?: "text" | "email" | "number";
className?: string;
id?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, className, id: customId, ...inputProps }, ref) => {
const generatedId = useId();
const id = customId || generatedId;
export function Input(props: Props) {
const { label, id: customId, className, type = "text" } = props;
const generatedId = useId();
const id = customId || generatedId;
const inputClassName = clsx(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"font-medium text-stone-900",
"dark:text-white dark:bg-stone-925",
);
const inputClassName = clsx(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"font-medium text-stone-900",
"dark:text-white",
);
const containerClassName = clsx("grid gap-1", className);
const containerClassName = clsx("grid gap-1", className);
return (
<div className={containerClassName}>
<label htmlFor={id} className="text-stone-600 dark:text-stone-300">
{label}
</label>
return (
<div className={containerClassName}>
<label htmlFor={id} className="text-stone-600 dark:text-stone-300">
{label}
</label>
<input {...props} type={type} id={id} className={inputClassName} />
</div>
);
}
<input ref={ref} {...inputProps} id={id} className={inputClassName} />
</div>
);
},
);

View File

@@ -1,6 +1,7 @@
import clsx from "clsx";
import { ReactNode } from "react";
import { H2, Kicker } from "../atoms/Headings";
import { H2 } from "../atoms/Headings";
import { Kicker } from "../atoms/Kicker";
import { Prose } from "./Prose";
function H2Sub({ children }: { children: React.ReactNode }) {

View File

@@ -14,10 +14,9 @@ export function Select(
const containerClassName = clsx("grid gap-1", className);
const selectClassName = clsx(
"g-select",
"w-full rounded-md border shadow-sm px-2 py-1.5 text-sm",
"font-medium text-stone-900",
"dark:text-white",
"dark:text-white dark:bg-stone-925",
"appearance-none",
);

View File

@@ -1,3 +1,4 @@
import { H1 } from "gcmp-design-system/src/app/components/atoms/Headings";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import Link from "next/link";
@@ -44,10 +45,10 @@ export function HeroSection() {
<p className="uppercase text-blue tracking-widest text-sm font-medium dark:text-stone-400">
Local-first development toolkit
</p>
<h1 className="font-display text-stone-950 dark:text-white text-4xl md:text-5xl lg:text-6xl font-medium tracking-tighter">
<H1>
<span className="inline-block">Ship top-tier apps</span>{" "}
<span className="inline-block">at high tempo.</span>
</h1>
</H1>
<Prose size="lg" className="text-pretty max-w-2xl dark:text-stone-200">
<p>