Skip to content
Snippets Groups Projects
Commit 5d82ff08 authored by Alissa Cheng's avatar Alissa Cheng
Browse files

Merge branch 'new-components' into 'main'

New components

See merge request !21
parents f0cc3b4e 633ed103
Branches
Tags
1 merge request!21New components
Pipeline #71006 passed
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 14L11 8L5 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M5 14L11 8L5 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
\ No newline at end of file
import { Breadcrumbs, BreadcrumbItem } from "@nextui-org/react";
import { clsx } from "clsx";
import Icon from "./Icon.tsx";
import Typography from "./Typography.tsx";
import { textColor } from "./utils/classes.ts";
import { MenuComponent, MenuItemProps } from "./Menu.tsx";
type BreadcrumbProps = {
items: { name: string; href: string }[];
};
const Breadcrumb = ({ items }: BreadcrumbProps) => {
if (items.length === 0) items = [{ name: "Home", href: "/" }];
return (
<Breadcrumbs
maxItems={5}
itemsBeforeCollapse={1}
itemsAfterCollapse={2}
separator={
<Icon name="next" color="body" customClass="w-[7px] h-[7px]" />
}
itemClasses={{
item: clsx(
textColor("primary"),
"data-[current=true]:text-mediumGrey dark:data-[current=true]:text-baseWhite"
),
}}
renderEllipsis={({ items, ellipsisIcon, separator }) => {
return (
<div className="flex items-center">
<MenuComponent
menuTrigger={ellipsisIcon}
items={items as MenuItemProps[]}
/>
{separator}
</div>
);
}}
>
{items.map((item, idx) => (
<BreadcrumbItem href={item.href} isCurrent={idx === items.length - 1}>
<Typography text={item.name} variant="note" customColor={true} />
</BreadcrumbItem>
))}
</Breadcrumbs>
);
};
export default Breadcrumb;
...@@ -15,6 +15,9 @@ export type ButtonProps = { ...@@ -15,6 +15,9 @@ export type ButtonProps = {
onClick?: () => void; onClick?: () => void;
}; };
export const buttonBaseClass: string =
"px-[9px] h-[32px] min-w-fit max-w-fit py-[5px] text-baseWhite rounded-[4px] flex focus:!outline-0";
/** /**
* A simple button with optional icon * A simple button with optional icon
* *
...@@ -34,11 +37,6 @@ const Button = ({ ...@@ -34,11 +37,6 @@ const Button = ({
}: ButtonProps) => { }: ButtonProps) => {
if (!label && !icon) throw Error("Button cannot be empty"); if (!label && !icon) throw Error("Button cannot be empty");
const pxClass = (icon: boolean, label: boolean) => {
if (icon && label) return "px-[8px]";
else if (icon) return "px-[9px]";
else if (label) return "px-[10px]";
};
const bgClass = (isDisabled: boolean) => const bgClass = (isDisabled: boolean) =>
isDisabled ? "bg-lightGrey !opacity-100" : bgColor(color); isDisabled ? "bg-lightGrey !opacity-100" : bgColor(color);
...@@ -46,10 +44,9 @@ const Button = ({ ...@@ -46,10 +44,9 @@ const Button = ({
<NextButton <NextButton
startContent={icon && <Icon name={icon} customClass={iconClass} />} startContent={icon && <Icon name={icon} customClass={iconClass} />}
className={clsx( className={clsx(
pxClass(!!icon, !!label),
bgClass(isDisabled), bgClass(isDisabled),
cursorPointer(isDisabled), cursorPointer(isDisabled),
"h-[32px] min-w-fit max-w-fit py-[5px] text-baseWhite rounded-[4px] flex focus:!outline-0" buttonBaseClass
)} )}
isDisabled={isDisabled} isDisabled={isDisabled}
type={type} type={type}
......
import { Dispatch, forwardRef, SetStateAction } from "react"; import { forwardRef } from "react";
import DatePicker from "react-datepicker"; import DatePicker, { ReactDatePickerCustomHeaderProps } from "react-datepicker";
import { format } from "date-fns"; import { format } from "date-fns";
import { BaseInput } from "./utils/baseComponents.tsx"; import { BaseInput } from "./utils/baseComponents.tsx";
...@@ -8,11 +8,17 @@ import Typography from "./Typography.tsx"; ...@@ -8,11 +8,17 @@ import Typography from "./Typography.tsx";
import { textColor } from "./utils/classes.ts"; import { textColor } from "./utils/classes.ts";
export type DateNull = Date | null;
export type DateArray = [DateNull, DateNull];
type DateInputProps = { type DateInputProps = {
isRequired?: boolean; isRequired?: boolean;
label: string; label: string;
value?: Date; isRange?: boolean;
onValueChange: Dispatch<SetStateAction<Date | undefined>>; value?: DateNull;
startDate?: DateNull;
endDate?: DateNull;
onValueChange: (date: Date | DateArray) => void;
isInvalid?: boolean; isInvalid?: boolean;
errorMessage?: string; errorMessage?: string;
}; };
...@@ -21,10 +27,18 @@ const DateInput = ({ ...@@ -21,10 +27,18 @@ const DateInput = ({
isRequired = false, isRequired = false,
label, label,
value, value,
startDate,
endDate,
onValueChange, onValueChange,
isInvalid = false, isInvalid = false,
errorMessage = "", errorMessage = "",
isRange = false,
}: DateInputProps) => { }: DateInputProps) => {
if (value && (startDate || endDate || isRange))
throw Error(
"Supply `value` when `isRange` is false (default), OR supply `startDate` and `endDate` when `isRange` is true."
);
const dateFormat = "dd/MM/yyyy"; const dateFormat = "dd/MM/yyyy";
type inputProps = { type inputProps = {
...@@ -48,10 +62,11 @@ const DateInput = ({ ...@@ -48,10 +62,11 @@ const DateInput = ({
endContent={ endContent={
<Icon <Icon
name="datepicker" name="datepicker"
// Note: textColor("body") has a different grey, hence the below is used
customClass="text-grey dark:text-baseWhite" customClass="text-grey dark:text-baseWhite"
/> />
} }
placeholder="DD/MM/YYYY" placeholder={isRange ? " " : "DD/MM/YYYY"}
value={value} value={value}
onValueChange={() => {}} onValueChange={() => {}}
isInvalid={isInvalid} isInvalid={isInvalid}
...@@ -89,31 +104,24 @@ const DateInput = ({ ...@@ -89,31 +104,24 @@ const DateInput = ({
</div> </div>
); );
return ( const datePickerBaseProps: object = {
<DatePicker dateFormat: dateFormat,
dateFormat={dateFormat} customInput: <CustomInput isRequired={isRequired} />,
openToDate={value || new Date()} showPopperArrow: false,
selected={value} popperClassName: "!transform-none !relative",
onSelect={(date) => onValueChange(date)} calendarStartDay: 1,
onChange={(date) => !!date && onValueChange(date)} formatWeekDay: (day: string) => <div>{day.charAt(0)}</div>,
customInput={<CustomInput isRequired={isRequired} />} weekDayClassName: () =>
showPopperArrow={false} "bg-baseWhite dark:bg-mediumGrey text-grey w-[33px] h-[30px] pt-[2px] m-0 mb-[5px]",
calendarStartDay={1} calendarClassName:
formatWeekDay={(day) => <div>{day.charAt(0)}</div>} "!bg-baseWhite dark:!bg-mediumGrey !font-body border-none rounded-[4px] shadow-3xl",
weekDayClassName={() => renderCustomHeader: ({
"bg-baseWhite dark:bg-mediumGrey text-grey w-[33px] h-[30px] pt-[2px] m-0 mb-[5px]"
}
calendarClassName="!bg-baseWhite dark:!bg-mediumGrey !font-body border-none rounded-[4px] shadow-3xl"
dayClassName={() =>
"!text-foreground-body !bg-default aria-selected:!bg-lightBlue"
}
renderCustomHeader={({
date, date,
decreaseYear, decreaseYear,
increaseYear, increaseYear,
decreaseMonth, decreaseMonth,
increaseMonth, increaseMonth,
}) => ( }: ReactDatePickerCustomHeaderProps) => (
<div className="!bg-baseWhite dark:!bg-mediumGrey rounded-t-[4px]"> <div className="!bg-baseWhite dark:!bg-mediumGrey rounded-t-[4px]">
<CustomPagination <CustomPagination
text={format(date, "yyyy")} text={format(date, "yyyy")}
...@@ -126,7 +134,36 @@ const DateInput = ({ ...@@ -126,7 +134,36 @@ const DateInput = ({
onClickNext={increaseMonth} onClickNext={increaseMonth}
/> />
</div> </div>
)} ),
};
const singleDatePickerProps: object = {
openToDate: value || new Date(),
selected: value,
dayClassName: () =>
"!text-foreground-body !bg-default aria-selected:!bg-lightBlue",
};
const rangeDatePickerProps: object = {
openToDate: startDate || new Date(),
selected: startDate,
startDate: startDate,
endDate: endDate,
dayClassName: () =>
"!text-foreground-body bg-default hover:aria-[selected=false]:bg-default mx-0 w-8",
};
const datePickerProps = {
...datePickerBaseProps,
...(isRange ? rangeDatePickerProps : singleDatePickerProps),
};
return (
<DatePicker
selectsRange={isRange}
onSelect={(date) => onValueChange(date)}
onChange={(date) => !!date && onValueChange(date)}
{...datePickerProps}
/> />
); );
}; };
......
...@@ -3,6 +3,7 @@ import { Dispatch, SetStateAction } from "react"; ...@@ -3,6 +3,7 @@ import { Dispatch, SetStateAction } from "react";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { baseInputClassNames } from "./utils/baseComponents.tsx"; import { baseInputClassNames } from "./utils/baseComponents.tsx";
import Icon from "./Icon.tsx"; import Icon from "./Icon.tsx";
import { textColor } from "./utils/classes.ts";
type DropdownProps = { type DropdownProps = {
isRequired?: boolean; isRequired?: boolean;
...@@ -41,8 +42,10 @@ const Dropdown = ({ ...@@ -41,8 +42,10 @@ const Dropdown = ({
selectorIcon={<Icon name="dropdown" />} selectorIcon={<Icon name="dropdown" />}
isClearable={false} // so the "cross" button does not show isClearable={false} // so the "cross" button does not show
classNames={{ classNames={{
selectorButton: selectorButton: clsx(
"data-[hover=true]:bg-default text-foreground-body opacity-100", textColor("body"),
"data-[hover=true]:bg-default opacity-100"
),
popoverContent: "rounded-[4px]", popoverContent: "rounded-[4px]",
}} }}
> >
......
import {
Avatar,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
} from "@nextui-org/react";
import { ReactNode } from "react";
import { Button as NextButton } from "@nextui-org/button";
import { clsx } from "clsx";
import { BreadcrumbItemProps } from "@react-types/breadcrumbs";
import Icon, { IconName } from "./Icon.tsx";
import { buttonBaseClass } from "./Button.tsx";
import Typography from "./Typography.tsx";
import { textColor } from "./utils/classes.ts";
type MenuComponentProps = {
menuTrigger: ReactNode;
onAction?: (key: string | number) => void;
items: MenuItemProps[];
};
export interface MenuItemProps extends BreadcrumbItemProps {
iconName?: IconName | null;
key: string;
itemName: string;
href?: string;
danger?: boolean;
}
export const MenuComponent = ({
menuTrigger,
onAction,
items,
}: MenuComponentProps) => {
if (!items.length) throw Error("`items` must not be empty");
const MenuItem = (
{ iconName, key, itemName, href, danger = false, children }: MenuItemProps,
idx: number
) => {
const itemContent = () => {
if (children) return children;
return (
<>
{iconName && <Icon name={iconName} />}
<Typography text={itemName} variant="paragraph" customColor={true} />
</>
);
};
return (
<DropdownItem
key={key}
href={href || ""}
showDivider={idx < items.length - 1}
>
<div
className={clsx(
"flex flex-row gap-3 items-center pl-2",
danger ? textColor("negative") : textColor("body")
)}
>
{itemContent()}
</div>
</DropdownItem>
);
};
const padding: string = "py-0.5 px-0";
return (
<Dropdown classNames={{ content: padding }}>
<DropdownTrigger>{menuTrigger}</DropdownTrigger>
<DropdownMenu onAction={onAction} classNames={{ base: padding }}>
{items.map((item, idx) => MenuItem(item, idx))}
</DropdownMenu>
</Dropdown>
);
};
type MenuProps = {
label: string;
items: MenuItemProps[];
onAction?: (key: string | number) => void;
};
export const ActionMenu = ({ label, items, onAction }: MenuProps) => {
return (
<MenuComponent
items={items}
onAction={onAction}
menuTrigger={
<NextButton
startContent={<Icon name="dropdown" />}
className={buttonBaseClass} // same as Button component (src/components/Button.tsx)
type="button"
color="primary"
>
{label && (
<Typography text={label} variant="paragraph" customColor={true} />
)}
</NextButton>
}
/>
);
};
export const ProfileMenu = ({ label, items, onAction }: MenuProps) => {
const getInitials = (name: string) => {
const [firstName, lastName] = name.split(" ");
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase();
};
return (
<MenuComponent
items={items}
onAction={onAction}
menuTrigger={
<div className="min-w-72 max-w-fit flex flex-row gap-2 items-center aria-expanded:scale-1 aria-expanded:opacity-100">
<Typography
text={label}
variant="overtitle"
customColor={true}
customClass={textColor("body")}
/>
<Avatar
isFocusable
color="primary"
className="w-[28px] h-[28px] text-baseWhite"
fallback={
<Typography
text={getInitials(label)}
variant="note"
customColor={true}
/>
}
/>
<Icon name="dropdown" color="primary" />
</div>
}
/>
);
};
...@@ -5,9 +5,11 @@ import { ...@@ -5,9 +5,11 @@ import {
ModalHeader, ModalHeader,
Modal as NextModal, Modal as NextModal,
} from "@nextui-org/react"; } from "@nextui-org/react";
import { clsx } from "clsx";
import Button, { ButtonProps } from "./Button.tsx"; import Button, { ButtonProps } from "./Button.tsx";
import Icon from "./Icon.tsx"; import Icon from "./Icon.tsx";
import Typography from "./Typography.tsx"; import Typography from "./Typography.tsx";
import { textColor } from "./utils/classes.ts";
type ModalProps = { type ModalProps = {
isOpen: boolean; isOpen: boolean;
...@@ -48,7 +50,7 @@ const Modal = ({ ...@@ -48,7 +50,7 @@ const Modal = ({
backdrop: "bg-darkBlue/90 dark:bg-baseBlack/90", backdrop: "bg-darkBlue/90 dark:bg-baseBlack/90",
base: "rounded-[8px] min-w-[500px] shadow-2xl p-[19px]", base: "rounded-[8px] min-w-[500px] shadow-2xl p-[19px]",
body: "p-0 pb-[10px] pr-[20px]", body: "p-0 pb-[10px] pr-[20px]",
closeButton: "text-foreground-heading mt-[17px] mr-[17px] p-0", closeButton: clsx(textColor("heading"), "mt-[17px] mr-[17px] p-0"),
footer: "flex flex-row justify-start gap-x-5 p-0 pt-[10px]", footer: "flex flex-row justify-start gap-x-5 p-0 pt-[10px]",
header: "flex flex-col gap-1 px-0 py-[10px]", header: "flex flex-col gap-1 px-0 py-[10px]",
}} }}
......
import { Progress } from "@nextui-org/react";
type ProgressBarProps = {
value?: number;
};
const ProgressBar = ({ value }: ProgressBarProps) => {
return (
<Progress
isIndeterminate={!value}
value={value as number}
aria-label="progress"
color="primary"
disableAnimation
classNames={{
track: "bg-lightGrey dark:bg-mediumGrey",
}}
/>
);
};
export default ProgressBar;
...@@ -6,7 +6,6 @@ import { colorOptions } from "src/components/utils/colors.ts"; ...@@ -6,7 +6,6 @@ import { colorOptions } from "src/components/utils/colors.ts";
const meta = { const meta = {
title: "Components/Badge", title: "Components/Badge",
component: Badge, component: Badge,
tags: ["autodocs"],
argTypes: { argTypes: {
color: { control: "radio", options: colorOptions }, color: { control: "radio", options: colorOptions },
backgroundColor: { control: "color" }, backgroundColor: { control: "color" },
......
import { Meta, StoryObj } from "@storybook/react";
import Breadcrumb from "src/components/Breadcrumb.tsx";
const meta = {
title: "Components/Breadcrumb",
component: Breadcrumb,
} satisfies Meta;
export default meta;
export const OneLevel: StoryObj = {
args: {
items: [{ name: "Home", href: "/" }],
},
};
export const MultipleLevels: StoryObj = {
args: {
items: [
{ name: "Home", href: "/" },
{ name: "Category", href: "/category" },
{ name: "Subcategory", href: "/category/subcategory" },
{ name: "This page", href: "/category/subcategory/this-page" },
],
},
};
export const MoreThanFourLevels: StoryObj = {
args: {
items: [
{ name: "Home", href: "/" },
{ name: "Level 1", href: "/level-1" },
{ name: "Level 2", href: "/level-1/level-2" },
{ name: "Level 3", href: "/level-1/level-2/level-3" },
{ name: "This page", href: "/level-1/level-2/level-3/this-page" },
],
},
};
import { Meta } from "@storybook/react"; import { Meta } from "@storybook/react";
import { useState } from "react"; import { useState } from "react";
import DateInput from "src/components/DateInput.tsx"; import DateInput, { DateArray, DateNull } from "src/components/DateInput.tsx";
const meta = { const meta = {
title: "Components/Date Input", title: "Components/Date Input",
component: DateInput,
} satisfies Meta; } satisfies Meta;
export default meta; export default meta;
...@@ -16,7 +15,30 @@ export const WithoutLimits = () => { ...@@ -16,7 +15,30 @@ export const WithoutLimits = () => {
<DateInput <DateInput
label="When's your birthday?" label="When's your birthday?"
value={value} value={value}
onValueChange={setValue} onValueChange={(date) => setValue(date as Date)}
/>
);
};
export const DateRange = () => {
const [startDate, setStartDate] = useState<DateNull>();
const [endDate, setEndDate] = useState<DateNull>();
const onChange = (dates: DateArray) => {
if (dates.length === 2) {
const [start, end] = dates;
setStartDate(start);
setEndDate(end);
}
};
return (
<DateInput
label="When's your holiday?"
startDate={startDate}
endDate={endDate}
onValueChange={(dates) => onChange(dates as DateArray)}
isRange={true}
/> />
); );
}; };
import { Meta } from "@storybook/react";
import { useState } from "react";
import {
ActionMenu,
MenuItemProps,
ProfileMenu,
} from "src/components/Menu.tsx";
import Typography from "src/components/Typography.tsx";
const meta = {
title: "Components/Menu",
} satisfies Meta;
export default meta;
const items = [
{ key: "expand", itemName: "Expand", iconName: "expand" },
{ key: "add", itemName: "Add", iconName: "plus" },
{ key: "remove", itemName: "Remove", iconName: "cross", danger: true },
];
export const ActionMenuWithoutIcons = () => {
const [selectedKey, setSelectedKey] = useState("");
return (
<>
<Typography
text={`You selected: ${selectedKey || "nothing"}`}
variant="paragraph"
/>
<ActionMenu
label="Menu"
items={
items.map((item: object): object => ({
...item,
iconName: null,
})) as MenuItemProps[]
}
onAction={(key) => setSelectedKey(key as string)}
/>
</>
);
};
export const ActionMenuWithIcons = () => {
const [selectedKey, setSelectedKey] = useState("");
return (
<>
<Typography
text={`You selected: ${selectedKey || "nothing"}`}
variant="paragraph"
/>
<ActionMenu
label="Menu"
items={items as MenuItemProps[]}
onAction={(key) => setSelectedKey(key as string)}
/>
</>
);
};
export const ProfileMenuWithIcons = () => {
const [selectedKey, setSelectedKey] = useState("");
return (
<>
<Typography
text={`You selected: ${selectedKey || "nothing"}`}
variant="paragraph"
/>
<ProfileMenu
label="Johnny Depp"
items={items as MenuItemProps[]}
onAction={(key) => setSelectedKey(key as string)}
/>
</>
);
};
import { Meta } from "@storybook/react";
import { useState } from "react";
import ProgressBar from "src/components/ProgressBar.tsx";
const meta = {
title: "Components/Progress Bar",
component: ProgressBar,
} satisfies Meta;
export default meta;
/** The value of the progress bar can be set and controlled via state. */
export const WithValue = () => {
const [value, setValue] = useState(0);
setTimeout(() => {
if (value < 100) setValue(value + 1);
else setValue(0);
}, 50);
return (
<div className="max-w-md">
<ProgressBar value={value} />
</div>
);
};
export const IndeterminateValue = () => (
<div className="max-w-md">
<ProgressBar />
</div>
);
...@@ -28,6 +28,39 @@ ...@@ -28,6 +28,39 @@
margin: 0; margin: 0;
} }
.react-datepicker__day--outside-month { .dark .react-datepicker__day--outside-month {
color: #dadada !important;
}
.light .react-datepicker__day--outside-month {
color: #777777 !important; color: #777777 !important;
} }
.dark {
.react-datepicker__day--in-range,
.react-datepicker__day--in-selecting-range {
background-color: #777777;
border-radius: 0;
}
}
.light {
.react-datepicker__day--in-range,
.react-datepicker__day--in-selecting-range {
background-color: #dadada;
border-radius: 0;
}
}
.react-datepicker__day--range-start,
.react-datepicker__day--range-end,
.react-datepicker__day--selecting-range-start,
.react-datepicker__day--selecting-range-end,
.react-datepicker__day--selected {
background-color: #00adee !important;
border-radius: 0.3rem !important;
}
.react-datepicker__day--keyboard-selected {
background-color: transparent;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment