chore(ui,icons,ui-preset,toolbox): Move design system packages to monorepo (#5470)
This commit is contained in:
committed by
GitHub
parent
71853eafdd
commit
e4ce2f4e07
@@ -0,0 +1,52 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Avatar } from "./avatar"
|
||||
|
||||
const meta: Meta<typeof Avatar> = {
|
||||
title: "Components/Avatar",
|
||||
component: Avatar,
|
||||
argTypes: {
|
||||
src: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
fallback: {
|
||||
control: {
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["rounded", "squared"],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["default", "large"],
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Avatar>
|
||||
|
||||
export const WithImage: Story = {
|
||||
args: {
|
||||
src: "https://avatars.githubusercontent.com/u/10656202?v=4",
|
||||
fallback: "J",
|
||||
},
|
||||
}
|
||||
|
||||
export const WithFallback: Story = {
|
||||
args: {
|
||||
fallback: "J",
|
||||
},
|
||||
}
|
||||
90
packages/design-system/ui/src/components/avatar/avatar.tsx
Normal file
90
packages/design-system/ui/src/components/avatar/avatar.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import * as Primitives from "@radix-ui/react-avatar"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const avatarVariants = cva(
|
||||
"border-ui-border-strong flex shrink-0 items-center justify-center overflow-hidden border",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
squared: "rounded-lg",
|
||||
rounded: "rounded-full",
|
||||
},
|
||||
size: {
|
||||
base: "h-8 w-8",
|
||||
large: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "rounded",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const innerVariants = cva("aspect-square object-cover object-center", {
|
||||
variants: {
|
||||
variant: {
|
||||
squared: "rounded-lg",
|
||||
rounded: "rounded-full",
|
||||
},
|
||||
size: {
|
||||
base: "txt-compact-small-plus h-6 w-6",
|
||||
large: "txt-compact-medium-plus h-8 w-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "rounded",
|
||||
size: "base",
|
||||
},
|
||||
})
|
||||
|
||||
interface AvatarProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Root>,
|
||||
"asChild" | "children" | "size"
|
||||
>,
|
||||
VariantProps<typeof avatarVariants> {
|
||||
src?: string
|
||||
fallback: string
|
||||
}
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Root>,
|
||||
AvatarProps
|
||||
>(
|
||||
(
|
||||
{ src, fallback, variant = "rounded", size = "base", className, ...props },
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Primitives.Root
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clx(avatarVariants({ variant, size }), className)}
|
||||
>
|
||||
{src && (
|
||||
<Primitives.Image
|
||||
src={src}
|
||||
className={innerVariants({ variant, size })}
|
||||
/>
|
||||
)}
|
||||
<Primitives.Fallback
|
||||
className={clx(
|
||||
innerVariants({ variant, size }),
|
||||
"bg-ui-bg-component text-ui-fg-subtle pointer-events-none flex select-none items-center justify-center"
|
||||
)}
|
||||
>
|
||||
{fallback}
|
||||
</Primitives.Fallback>
|
||||
</Primitives.Root>
|
||||
)
|
||||
}
|
||||
)
|
||||
Avatar.displayName = "Avatar"
|
||||
|
||||
export { Avatar }
|
||||
1
packages/design-system/ui/src/components/avatar/index.ts
Normal file
1
packages/design-system/ui/src/components/avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./avatar"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Badge } from "./badge"
|
||||
|
||||
describe("Badge", () => {
|
||||
it("should render", async () => {
|
||||
render(<Badge>Badge</Badge>)
|
||||
expect(screen.getByText("Badge")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render as child", async () => {
|
||||
render(
|
||||
<Badge asChild>
|
||||
<a href="#">Changelog</a>
|
||||
</Badge>
|
||||
)
|
||||
|
||||
expect(screen.getByRole("link")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Badge } from "./badge"
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: "Components/Badge",
|
||||
component: Badge,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
render: ({ children, ...args }) => (
|
||||
<Badge {...args}>{children || "Badge"}</Badge>
|
||||
),
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Badge>
|
||||
|
||||
export const Grey: Story = {
|
||||
args: {
|
||||
color: "grey",
|
||||
},
|
||||
}
|
||||
|
||||
export const Green: Story = {
|
||||
args: {
|
||||
color: "green",
|
||||
},
|
||||
}
|
||||
|
||||
export const Red: Story = {
|
||||
args: {
|
||||
color: "red",
|
||||
},
|
||||
}
|
||||
|
||||
export const Blue: Story = {
|
||||
args: {
|
||||
color: "blue",
|
||||
},
|
||||
}
|
||||
|
||||
export const Orange: Story = {
|
||||
args: {
|
||||
color: "orange",
|
||||
},
|
||||
}
|
||||
|
||||
export const Purple: Story = {
|
||||
args: {
|
||||
color: "purple",
|
||||
},
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
rounded: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const Rounded: Story = {
|
||||
args: {
|
||||
rounded: "full",
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const Base: Story = {
|
||||
args: {
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
80
packages/design-system/ui/src/components/badge/badge.tsx
Normal file
80
packages/design-system/ui/src/components/badge/badge.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const badgeColorVariants = cva("", {
|
||||
variants: {
|
||||
color: {
|
||||
green:
|
||||
"bg-ui-tag-green-bg text-ui-tag-green-text [&_svg]:text-ui-tag-green-icon border-ui-tag-green-border",
|
||||
red: "bg-ui-tag-red-bg text-ui-tag-red-text [&_svg]:text-ui-tag-red-icon border-ui-tag-red-border",
|
||||
blue: "bg-ui-tag-blue-bg text-ui-tag-blue-text [&_svg]:text-ui-tag-blue-icon border-ui-tag-blue-border",
|
||||
orange:
|
||||
"bg-ui-tag-orange-bg text-ui-tag-orange-text [&_svg]:text-ui-tag-orange-icon border-ui-tag-orange-border",
|
||||
grey: "bg-ui-tag-neutral-bg text-ui-tag-neutral-text [&_svg]:text-ui-tag-neutral-icon border-ui-tag-neutral-border",
|
||||
purple:
|
||||
"bg-ui-tag-purple-bg text-ui-tag-purple-text [&_svg]:text-ui-tag-purple-icon border-ui-tag-purple-border",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "grey",
|
||||
},
|
||||
})
|
||||
|
||||
const badgeSizeVariants = cva("inline-flex items-center gap-x-0.5 border", {
|
||||
variants: {
|
||||
size: {
|
||||
small: "txt-compact-xsmall-plus px-1.5",
|
||||
base: "txt-compact-small-plus px-2 py-0.5",
|
||||
large: "txt-compact-medium-plus px-2.5 py-1",
|
||||
},
|
||||
rounded: {
|
||||
base: "rounded-md",
|
||||
full: "rounded-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
rounded: "base",
|
||||
},
|
||||
})
|
||||
|
||||
interface BadgeProps
|
||||
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "color">,
|
||||
VariantProps<typeof badgeSizeVariants>,
|
||||
VariantProps<typeof badgeColorVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
size = "base",
|
||||
rounded = "base",
|
||||
color = "grey",
|
||||
asChild = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Component = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={clx(
|
||||
badgeColorVariants({ color }),
|
||||
badgeSizeVariants({ size, rounded }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Badge.displayName = "Badge"
|
||||
|
||||
export { Badge, badgeColorVariants }
|
||||
1
packages/design-system/ui/src/components/badge/index.ts
Normal file
1
packages/design-system/ui/src/components/badge/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./badge"
|
||||
@@ -0,0 +1,23 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "./button"
|
||||
|
||||
describe("Button", () => {
|
||||
it("renders a button", () => {
|
||||
render(<Button>Click me</Button>)
|
||||
const button = screen.getByRole("button", { name: "Click me" })
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders a button as a link", () => {
|
||||
render(
|
||||
<Button asChild>
|
||||
<a href="https://www.medusajs.com">Go to website</a>
|
||||
</Button>
|
||||
)
|
||||
|
||||
const button = screen.getByRole("link", { name: "Go to website" })
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { PlusMini } from "@medusajs/icons"
|
||||
import { Button } from "./button"
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "Components/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Button>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
variant: "secondary",
|
||||
},
|
||||
}
|
||||
|
||||
export const Transparent: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
variant: "transparent",
|
||||
},
|
||||
}
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
variant: "danger",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
children: ["Action", <PlusMini key={1} />],
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
isLoading: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const XLarge: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
size: "xlarge",
|
||||
},
|
||||
}
|
||||
118
packages/design-system/ui/src/components/button/button.tsx
Normal file
118
packages/design-system/ui/src/components/button/button.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
import { Spinner } from "@medusajs/icons"
|
||||
|
||||
const buttonVariants = cva(
|
||||
clx(
|
||||
"transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none",
|
||||
"disabled:bg-ui-bg-disabled disabled:border-ui-border-base disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral disabled:after:hidden",
|
||||
"after:transition-fg after:absolute after:inset-0 after:content-['']"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: clx(
|
||||
"shadow-buttons-inverted text-ui-fg-on-inverted bg-ui-button-inverted after:button-inverted-gradient",
|
||||
"hover:bg-ui-button-inverted-hover hover:after:button-inverted-hover-gradient",
|
||||
"active:bg-ui-button-inverted-pressed active:after:button-inverted-pressed-gradient",
|
||||
"focus:!shadow-buttons-inverted-focus"
|
||||
),
|
||||
secondary: clx(
|
||||
"shadow-buttons-neutral text-ui-fg-base bg-ui-button-neutral after:button-neutral-gradient",
|
||||
"hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient",
|
||||
"active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient",
|
||||
"focus:shadow-buttons-neutral-focus"
|
||||
),
|
||||
transparent: clx(
|
||||
"after:hidden",
|
||||
"text-ui-fg-base bg-ui-button-transparent",
|
||||
"hover:bg-ui-button-transparent-hover",
|
||||
"active:bg-ui-button-transparent-pressed",
|
||||
"focus:shadow-buttons-neutral-focus focus:bg-ui-bg-base",
|
||||
"disabled:!bg-transparent disabled:!shadow-none"
|
||||
),
|
||||
danger: clx(
|
||||
"shadow-buttons-colored shadow-buttons-danger text-ui-fg-on-color bg-ui-button-danger after:button-danger-gradient",
|
||||
"hover:bg-ui-button-danger-hover hover:after:button-danger-hover-gradient",
|
||||
"active:bg-ui-button-danger-pressed active:after:button-danger-pressed-gradient",
|
||||
"focus:shadow-buttons-danger-focus"
|
||||
),
|
||||
},
|
||||
size: {
|
||||
base: "txt-compact-small-plus gap-x-1.5 px-3 py-1.5",
|
||||
large: "txt-compact-medium-plus gap-x-1.5 px-4 py-2.5",
|
||||
xlarge: "txt-compact-large-plus gap-x-1.5 px-5 py-3.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
variant: "primary",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface ButtonProps
|
||||
extends React.ComponentPropsWithoutRef<"button">,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
isLoading?: boolean
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
variant = "primary",
|
||||
size = "base",
|
||||
className,
|
||||
asChild = false,
|
||||
children,
|
||||
isLoading = false,
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Component = asChild ? Slot : "button"
|
||||
|
||||
/**
|
||||
* In the case of a button where asChild is true, and isLoading is true, we ensure that
|
||||
* only on element is passed as a child to the Slot component. This is because the Slot
|
||||
* component only accepts a single child.
|
||||
*/
|
||||
const renderInner = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<span className="pointer-events-none">
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-disabled absolute inset-0 flex items-center justify-center rounded-md"
|
||||
)}
|
||||
>
|
||||
<Spinner className="animate-spin" />
|
||||
</div>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clx(buttonVariants({ variant, size }), className)}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{renderInner()}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
1
packages/design-system/ui/src/components/button/index.ts
Normal file
1
packages/design-system/ui/src/components/button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./button"
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Text } from "@/components/text"
|
||||
import { DateRange } from "react-day-picker"
|
||||
import { Calendar } from "./calendar"
|
||||
|
||||
const Demo = ({ mode, ...args }: Parameters<typeof Calendar>[0]) => {
|
||||
const [date, setDate] = React.useState<Date | undefined>(new Date())
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-y-4">
|
||||
<Calendar
|
||||
{...(args as any)}
|
||||
mode={mode as "single" | "range"}
|
||||
selected={mode === "single" ? date : dateRange}
|
||||
onSelect={mode === "single" ? setDate : setDateRange}
|
||||
/>
|
||||
|
||||
{mode === "single" && (
|
||||
<Text className="text-ui-fg-base">
|
||||
Selected Date: {date ? date.toDateString() : "None"}
|
||||
</Text>
|
||||
)}
|
||||
{mode === "range" && (
|
||||
<Text className="text-ui-fg-base">
|
||||
Selected Range:{" "}
|
||||
{dateRange
|
||||
? `${dateRange.from?.toDateString()} – ${
|
||||
dateRange.to?.toDateString() ?? ""
|
||||
}`
|
||||
: "None"}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof Calendar> = {
|
||||
title: "Components/Calendar",
|
||||
component: Calendar,
|
||||
render: Demo,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Calendar>
|
||||
|
||||
export const Single: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
},
|
||||
}
|
||||
|
||||
export const TwoMonthSingle: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
numberOfMonths: 2,
|
||||
},
|
||||
}
|
||||
|
||||
export const Range: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
},
|
||||
}
|
||||
|
||||
export const TwoMonthRange: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
numberOfMonths: 2,
|
||||
},
|
||||
}
|
||||
145
packages/design-system/ui/src/components/calendar/calendar.tsx
Normal file
145
packages/design-system/ui/src/components/calendar/calendar.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
"use client"
|
||||
|
||||
import { ChevronLeftMini, ChevronRightMini } from "@medusajs/icons"
|
||||
import * as React from "react"
|
||||
import {
|
||||
DayPicker,
|
||||
useDayRender,
|
||||
type DayPickerRangeProps,
|
||||
type DayPickerSingleProps,
|
||||
type DayProps,
|
||||
} from "react-day-picker"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
import { iconButtonVariants } from "../icon-button"
|
||||
|
||||
type OmitKeys<T, K extends keyof T> = {
|
||||
[P in keyof T as P extends K ? never : P]: T[P]
|
||||
}
|
||||
|
||||
type KeysToOmit = "showWeekNumber" | "captionLayout" | "mode"
|
||||
|
||||
type SingleProps = OmitKeys<DayPickerSingleProps, KeysToOmit>
|
||||
type RangeProps = OmitKeys<DayPickerRangeProps, KeysToOmit>
|
||||
|
||||
type CalendarProps =
|
||||
| ({
|
||||
mode: "single"
|
||||
} & SingleProps)
|
||||
| ({
|
||||
mode?: undefined
|
||||
} & SingleProps)
|
||||
| ({
|
||||
mode: "range"
|
||||
} & RangeProps)
|
||||
|
||||
const Calendar = ({
|
||||
className,
|
||||
classNames,
|
||||
mode = "single",
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) => {
|
||||
return (
|
||||
<DayPicker
|
||||
mode={mode}
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={clx(className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row",
|
||||
month: "space-y-2 p-3",
|
||||
caption: "flex justify-center relative items-center h-9",
|
||||
caption_label:
|
||||
"txt-compact-small-plus absolute bottom-0 left-0 right-0 top-1 flex items-center justify-center text-ui-fg-base",
|
||||
nav: "space-x-1 flex items-center bg-ui-bg-base-pressed rounded-md w-full h-full justify-between p-0.5",
|
||||
nav_button: clx(
|
||||
iconButtonVariants({ variant: "primary", size: "base" })
|
||||
),
|
||||
nav_button_previous: "!absolute left-0.5",
|
||||
nav_button_next: "!absolute right-0.5",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex w-full gap-x-2",
|
||||
head_cell: clx(
|
||||
"txt-compact-small-plus text-ui-fg-muted m-0 box-border flex h-8 w-8 items-center justify-center p-0"
|
||||
),
|
||||
row: "flex w-full mt-2 gap-x-2",
|
||||
cell: "txt-compact-small-plus relative rounded-md p-0 text-center focus-within:relative",
|
||||
day: "txt-compact-small-plus text-ui-fg-base bg-ui-bg-base hover:bg-ui-bg-base-hover focus:shadow-borders-interactive-with-focus h-8 w-8 rounded-md p-0 text-center outline-none transition-all",
|
||||
day_selected:
|
||||
"bg-ui-bg-interactive text-ui-fg-on-color hover:bg-ui-bg-interactive focus:bg-ui-bg-interactive",
|
||||
day_outside: "text-ui-fg-disabled aria-selected:text-ui-fg-on-color",
|
||||
day_disabled: "text-ui-fg-disabled",
|
||||
day_range_middle:
|
||||
"aria-selected:!bg-ui-bg-highlight aria-selected:!text-ui-fg-interactive",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: () => <ChevronLeftMini />,
|
||||
IconRight: () => <ChevronRightMini />,
|
||||
Day: Day,
|
||||
}}
|
||||
{...(props as SingleProps & RangeProps)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
const Day = ({ date, displayMonth }: DayProps) => {
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
const { activeModifiers, buttonProps, divProps, isButton, isHidden } =
|
||||
useDayRender(date, displayMonth, ref)
|
||||
|
||||
const { selected, today, disabled, range_middle } = activeModifiers
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selected) {
|
||||
ref.current?.focus()
|
||||
}
|
||||
}, [selected])
|
||||
|
||||
if (isHidden) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
if (!isButton) {
|
||||
return (
|
||||
<div
|
||||
{...divProps}
|
||||
className={clx("flex items-center justify-center", divProps.className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
children: buttonChildren,
|
||||
className: buttonClassName,
|
||||
...buttonPropsRest
|
||||
} = buttonProps
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...buttonPropsRest}
|
||||
type="button"
|
||||
className={clx("relative", buttonClassName)}
|
||||
>
|
||||
{buttonChildren}
|
||||
{today && (
|
||||
<span
|
||||
className={clx(
|
||||
"absolute right-[5px] top-[5px] h-1 w-1 rounded-full",
|
||||
{
|
||||
"bg-ui-fg-interactive": !selected,
|
||||
"bg-ui-fg-on-color": selected,
|
||||
"!bg-ui-fg-interactive": selected && range_middle,
|
||||
"bg-ui-fg-disabled": disabled,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./calendar"
|
||||
@@ -0,0 +1,12 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Checkbox } from "./checkbox"
|
||||
|
||||
describe("Checkbox", () => {
|
||||
it("renders a checkbox", () => {
|
||||
render(<Checkbox />)
|
||||
|
||||
expect(screen.getByRole("checkbox")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Checkbox } from "./checkbox"
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: "Components/Checkbox",
|
||||
component: Checkbox,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Checkbox>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Checked: Story = {
|
||||
args: {
|
||||
checked: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Indeterminate: Story = {
|
||||
args: {
|
||||
checked: "indeterminate",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const DisabledChecked: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
checked: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const DisabledIndeterminate: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
checked: "indeterminate",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { CheckMini, MinusMini } from "@medusajs/icons"
|
||||
import * as Primitives from "@radix-ui/react-checkbox"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Root>
|
||||
>(({ className, checked, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Root
|
||||
{...props}
|
||||
ref={ref}
|
||||
checked={checked}
|
||||
className={clx(
|
||||
"group relative inline-flex h-5 w-5 items-center justify-center outline-none ",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="text-ui-fg-on-inverted bg-ui-bg-base shadow-borders-base group-hover:bg-ui-bg-base-hover group-focus:!shadow-borders-interactive-with-focus group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow group-data-[state=indeterminate]:bg-ui-bg-interactive group-data-[state=indeterminate]:shadow-borders-interactive-with-shadow [&_path]:shadow-details-contrast-on-bg-interactive group-disabled:text-ui-fg-disabled group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base transition-fg h-[14px] w-[14px] rounded-[3px]">
|
||||
<Primitives.Indicator className="absolute inset-0">
|
||||
{checked === "indeterminate" ? <MinusMini /> : <CheckMini />}
|
||||
</Primitives.Indicator>
|
||||
</div>
|
||||
</Primitives.Root>
|
||||
)
|
||||
})
|
||||
Checkbox.displayName = "Checkbox"
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./checkbox"
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { CodeBlock } from "./code-block"
|
||||
import { Label } from "../label"
|
||||
|
||||
const meta: Meta<typeof CodeBlock> = {
|
||||
title: "Components/CodeBlock",
|
||||
component: CodeBlock,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof CodeBlock>
|
||||
|
||||
const snippets = [
|
||||
{
|
||||
label: "cURL",
|
||||
language: "markdown",
|
||||
code: `curl -H 'x-publishable-key: YOUR_API_KEY' 'http://localhost:9000/store/products/PRODUCT_ID'`,
|
||||
},
|
||||
{
|
||||
label: "Medusa JS Client",
|
||||
language: "jsx",
|
||||
code: `// Install the JS Client in your storefront project: @medusajs/medusa-js\n\nimport Medusa from "@medusajs/medusa-js"\n\nconst medusa = new Medusa({ publishableApiKey: "YOUR_API_KEY"})\nconst product = await medusa.products.retrieve("PRODUCT_ID")\nconsole.log(product.id)`,
|
||||
},
|
||||
{
|
||||
label: "Medusa React",
|
||||
language: "tsx",
|
||||
code: `// Install the React SDK and required dependencies in your storefront project:\n// medusa-react @tanstack/react-query @medusajs/medusa\n\nimport { useProduct } from "medusa-react"\n\nconst { product } = useProduct("PRODUCT_ID")\nconsole.log(product.id)`,
|
||||
},
|
||||
]
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="h-[300px] w-[700px]">
|
||||
<CodeBlock snippets={snippets}>
|
||||
<CodeBlock.Header>
|
||||
<CodeBlock.Header.Meta>
|
||||
<Label weight={"plus"}>/product-detail.js</Label>
|
||||
</CodeBlock.Header.Meta>
|
||||
</CodeBlock.Header>
|
||||
<CodeBlock.Body />
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
"use client"
|
||||
import { Highlight, themes } from "prism-react-renderer"
|
||||
import * as React from "react"
|
||||
|
||||
import { Copy } from "@/components/copy"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
export type CodeSnippet = {
|
||||
label: string
|
||||
language: string
|
||||
code: string
|
||||
hideLineNumbers?: boolean
|
||||
}
|
||||
|
||||
type CodeBlockState = {
|
||||
snippets: CodeSnippet[]
|
||||
active: CodeSnippet
|
||||
setActive: (active: CodeSnippet) => void
|
||||
} | null
|
||||
|
||||
const CodeBlockContext = React.createContext<CodeBlockState>(null)
|
||||
|
||||
const useCodeBlockContext = () => {
|
||||
const context = React.useContext(CodeBlockContext)
|
||||
|
||||
if (context === null)
|
||||
throw new Error(
|
||||
"useCodeBlockContext can only be used within a CodeBlockContext"
|
||||
)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type RootProps = {
|
||||
snippets: CodeSnippet[]
|
||||
}
|
||||
|
||||
const Root = ({
|
||||
snippets,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & RootProps) => {
|
||||
const [active, setActive] = React.useState(snippets[0])
|
||||
|
||||
return (
|
||||
<CodeBlockContext.Provider value={{ snippets, active, setActive }}>
|
||||
<div
|
||||
className={clx(
|
||||
"border-ui-code-border overflow-hidden rounded-lg border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CodeBlockContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
hideLabels?: boolean
|
||||
}
|
||||
|
||||
const HeaderComponent = ({
|
||||
children,
|
||||
className,
|
||||
hideLabels = false,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & HeaderProps) => {
|
||||
const { snippets, active, setActive } = useCodeBlockContext()
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
"border-b-ui-code-border bg-ui-code-bg-header flex items-center gap-2 border-b px-4 py-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!hideLabels &&
|
||||
snippets.map((snippet) => (
|
||||
<div
|
||||
className={clx(
|
||||
"text-ui-code-text-subtle txt-compact-small-plus cursor-pointer rounded-full border border-transparent px-3 py-2 transition-all",
|
||||
{
|
||||
"text-ui-code-text-base border-ui-code-border bg-ui-code-bg-base cursor-default":
|
||||
active.label === snippet.label,
|
||||
}
|
||||
)}
|
||||
key={snippet.label}
|
||||
onClick={() => setActive(snippet)}
|
||||
>
|
||||
{snippet.label}
|
||||
</div>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Meta = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={clx("text-ui-code-text-subtle ml-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = Object.assign(HeaderComponent, { Meta })
|
||||
|
||||
const Body = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
const { active } = useCodeBlockContext()
|
||||
return (
|
||||
<div
|
||||
className={clx("bg-ui-code-bg-base relative p-4", className)}
|
||||
{...props}
|
||||
>
|
||||
<Copy
|
||||
content={active.code}
|
||||
className="text-ui-code-icon absolute right-4 top-4"
|
||||
/>
|
||||
<div className="max-w-[90%]">
|
||||
<Highlight
|
||||
theme={{
|
||||
...themes.palenight,
|
||||
plain: {
|
||||
color: "rgba(249, 250, 251, 1)",
|
||||
backgroundColor: "#111827",
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
types: ["keyword"],
|
||||
style: {
|
||||
color: "var(--fg-on-color)",
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ["maybe-class-name"],
|
||||
style: {
|
||||
color: "rgb(255, 203, 107)",
|
||||
},
|
||||
},
|
||||
...themes.palenight.styles,
|
||||
],
|
||||
}}
|
||||
code={active.code}
|
||||
language={active.language}
|
||||
>
|
||||
{({ style, tokens, getLineProps, getTokenProps }) => (
|
||||
<pre
|
||||
className="txt-compact-small whitespace-pre-wrap bg-transparent font-mono"
|
||||
style={{
|
||||
...style,
|
||||
background: "transparent",
|
||||
}}
|
||||
>
|
||||
{tokens.map((line, i) => (
|
||||
<div key={i} {...getLineProps({ line })} className="flex">
|
||||
{!active.hideLineNumbers && (
|
||||
<span className="text-ui-code-text-subtle">{i + 1}</span>
|
||||
)}
|
||||
<div className="pl-4">
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CodeBlock = Object.assign(Root, { Body, Header, Meta })
|
||||
|
||||
export { CodeBlock }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./code-block"
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Code } from "./code"
|
||||
|
||||
const meta: Meta<typeof Code> = {
|
||||
title: "Components/Code",
|
||||
component: Code,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Code>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "yarn add -D @medusajs/ui-preset",
|
||||
},
|
||||
}
|
||||
22
packages/design-system/ui/src/components/code/code.tsx
Normal file
22
packages/design-system/ui/src/components/code/code.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { clx } from "@/utils/clx"
|
||||
import * as React from "react"
|
||||
|
||||
const Code = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"code">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<code
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"border-ui-tag-neutral-border bg-ui-tag-neutral-bg text-ui-tag-neutral-text txt-compact-small inline-flex rounded-md border px-[6px] font-mono",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Code.displayName = "Code"
|
||||
|
||||
export { Code }
|
||||
1
packages/design-system/ui/src/components/code/index.tsx
Normal file
1
packages/design-system/ui/src/components/code/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./code"
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "../button"
|
||||
import { CommandBar } from "./command-bar"
|
||||
|
||||
const meta: Meta<typeof CommandBar> = {
|
||||
title: "Components/CommandBar",
|
||||
component: CommandBar,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof CommandBar>
|
||||
|
||||
const CommandBarDemo = () => {
|
||||
const [active, setActive] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<Button onClick={() => setActive(!active)}>
|
||||
{active ? "Hide" : "Show"}
|
||||
</Button>
|
||||
<CommandBar open={active}>
|
||||
<CommandBar.Bar>
|
||||
<CommandBar.Value>1 selected</CommandBar.Value>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
label="Edit"
|
||||
action={() => {
|
||||
console.log("Edit")
|
||||
}}
|
||||
shortcut="e"
|
||||
/>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
label="Delete"
|
||||
action={() => {
|
||||
console.log("Delete")
|
||||
}}
|
||||
shortcut="d"
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <CommandBarDemo />,
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client"
|
||||
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import * as Portal from "@radix-ui/react-portal"
|
||||
import * as React from "react"
|
||||
|
||||
import { Kbd } from "@/components/kbd"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
type CommandBarProps = React.PropsWithChildren<{
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
defaultOpen?: boolean
|
||||
disableAutoFocus?: boolean
|
||||
}>
|
||||
|
||||
const Root = ({
|
||||
open = false,
|
||||
onOpenChange,
|
||||
defaultOpen = false,
|
||||
disableAutoFocus = true,
|
||||
children,
|
||||
}: CommandBarProps) => {
|
||||
return (
|
||||
<Popover.Root
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
defaultOpen={defaultOpen}
|
||||
>
|
||||
<Portal.Root>
|
||||
<Popover.Anchor
|
||||
className={clx("fixed bottom-8 left-1/2 h-px w-px -translate-x-1/2")}
|
||||
/>
|
||||
</Portal.Root>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
side="top"
|
||||
sideOffset={0}
|
||||
onOpenAutoFocus={(e) => {
|
||||
if (disableAutoFocus) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
className={clx(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
Root.displayName = "CommandBar"
|
||||
|
||||
const Value = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"txt-compact-small-plus text-ui-contrast-fg-secondary px-3 py-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Value.displayName = "CommandBar.Value"
|
||||
|
||||
const Bar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-contrast-bg-base relative flex items-center overflow-hidden rounded-full px-1",
|
||||
"after:shadow-elevation-flyout after:pointer-events-none after:absolute after:inset-0 after:rounded-full after:content-['']",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Bar.displayName = "CommandBar.Bar"
|
||||
|
||||
const Seperator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
Omit<React.ComponentPropsWithoutRef<"div">, "children">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx("bg-ui-contrast-border-base h-10 w-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Seperator.displayName = "CommandBar.Seperator"
|
||||
|
||||
interface CommandProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<"button">,
|
||||
"children" | "onClick"
|
||||
> {
|
||||
action: () => void | Promise<void>
|
||||
label: string
|
||||
shortcut: string
|
||||
}
|
||||
|
||||
const Command = React.forwardRef<HTMLButtonElement, CommandProps>(
|
||||
(
|
||||
{ className, type = "button", label, action, shortcut, disabled, ...props },
|
||||
ref
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === shortcut) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
if (!disabled) {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [action, shortcut, disabled])
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-contrast-bg-base txt-compact-small-plus transition-fg text-ui-contrast-fg-primary flex items-center gap-x-2 px-3 py-2.5 outline-none",
|
||||
"focus:bg-ui-contrast-bg-highlight focus:hover:bg-ui-contrast-bg-base-hover hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus:active:bg-ui-contrast-bg-base-pressed disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||
"last-of-type:-mr-1 last-of-type:pr-4",
|
||||
className
|
||||
)}
|
||||
type={type}
|
||||
onClick={action}
|
||||
{...props}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary">
|
||||
{shortcut.toUpperCase()}
|
||||
</Kbd>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
Command.displayName = "CommandBar.Command"
|
||||
|
||||
const CommandBar = Object.assign(Root, {
|
||||
Command,
|
||||
Value,
|
||||
Bar,
|
||||
Seperator,
|
||||
})
|
||||
|
||||
export { CommandBar }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./command-bar"
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import { Command } from "./command"
|
||||
import { Badge } from "../badge"
|
||||
|
||||
const meta: Meta<typeof Command> = {
|
||||
title: "Components/Command",
|
||||
component: Command,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Command>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="w-[500px]">
|
||||
<Command>
|
||||
<Badge color="green">Get</Badge>
|
||||
<code>localhost:9000/store/products</code>
|
||||
<Command.Copy content="localhost:9000/store/products" />
|
||||
</Command>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
25
packages/design-system/ui/src/components/command/command.tsx
Normal file
25
packages/design-system/ui/src/components/command/command.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { Copy } from "@/components/copy"
|
||||
import { clx } from "@/utils/clx"
|
||||
import React from "react"
|
||||
|
||||
const CommandComponent = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-code-bg-header border-ui-code-border flex items-center rounded-lg border px-3 py-2",
|
||||
"[&>code]:text-ui-code-text-base [&>code]:txt-compact-small [&>code]:mx-3 [&>code]:font-mono [&>code]:leading-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Command = Object.assign(CommandComponent, { Copy })
|
||||
|
||||
export { Command }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./command"
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Heading } from "@/components/heading"
|
||||
import { Text } from "@/components/text"
|
||||
import { Container } from "./container"
|
||||
|
||||
const meta: Meta<typeof Container> = {
|
||||
title: "Components/Container",
|
||||
component: Container,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Container>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: <Text>Hello World</Text>,
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export const InLayout: Story = {
|
||||
render: () => (
|
||||
<div className="flex h-screen w-screen">
|
||||
<div className="border-ui-border-base w-full max-w-[216px] border-r p-4">
|
||||
<Heading level="h3">Menubar</Heading>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-y-2 px-8 pb-8 pt-6">
|
||||
<Container>
|
||||
<Heading>Section 1</Heading>
|
||||
</Container>
|
||||
<Container>
|
||||
<Heading>Section 2</Heading>
|
||||
</Container>
|
||||
<Container>
|
||||
<Heading>Section 3</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Container = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"shadow-elevation-card-rest bg-ui-bg-base w-full rounded-lg px-8 pb-8 pt-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Container.displayName = "Container"
|
||||
|
||||
export { Container }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./container"
|
||||
11
packages/design-system/ui/src/components/copy/copy.spec.tsx
Normal file
11
packages/design-system/ui/src/components/copy/copy.spec.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Copy } from "./copy"
|
||||
|
||||
describe("Copy", () => {
|
||||
it("should render", () => {
|
||||
render(<Copy content="Hello world" />)
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Copy } from "./copy"
|
||||
|
||||
const meta: Meta<typeof Copy> = {
|
||||
title: "Components/Copy",
|
||||
component: Copy,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Copy>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
content: "Hello world",
|
||||
},
|
||||
}
|
||||
|
||||
export const AsChild: Story = {
|
||||
args: {
|
||||
content: "Hello world",
|
||||
asChild: true,
|
||||
children: <Button className="text-ui-fg-on-color">Copy</Button>,
|
||||
},
|
||||
}
|
||||
62
packages/design-system/ui/src/components/copy/copy.tsx
Normal file
62
packages/design-system/ui/src/components/copy/copy.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client"
|
||||
|
||||
import { Tooltip } from "@/components/tooltip"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import copy from "copy-to-clipboard"
|
||||
import React, { useState } from "react"
|
||||
|
||||
type CopyProps = {
|
||||
content: string
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Copy = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.HTMLAttributes<HTMLButtonElement> & CopyProps
|
||||
>(({ children, className, content, asChild = false, ...props }, ref) => {
|
||||
const [done, setDone] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [text, setText] = useState("Copy")
|
||||
|
||||
const copyToClipboard = () => {
|
||||
setDone(true)
|
||||
copy(content)
|
||||
|
||||
setTimeout(() => {
|
||||
setDone(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (done) {
|
||||
setText("Copied")
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setText("Copy")
|
||||
}, 500)
|
||||
}, [done])
|
||||
|
||||
const Component = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Tooltip content={text} open={done || open} onOpenChange={setOpen}>
|
||||
<Component
|
||||
ref={ref}
|
||||
aria-label="Copy code snippet"
|
||||
type="button"
|
||||
className={clx("text-ui-code-icon h-fit w-fit", className)}
|
||||
onClick={copyToClipboard}
|
||||
{...props}
|
||||
>
|
||||
{children ? children : done ? <CheckCircleSolid /> : <SquareTwoStack />}
|
||||
</Component>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
Copy.displayName = "Copy"
|
||||
|
||||
export { Copy }
|
||||
1
packages/design-system/ui/src/components/copy/index.tsx
Normal file
1
packages/design-system/ui/src/components/copy/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./copy"
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { CurrencyInput } from "./currency-input"
|
||||
|
||||
const meta: Meta<typeof CurrencyInput> = {
|
||||
title: "Components/CurrencyInput",
|
||||
component: CurrencyInput,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof CurrencyInput>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
symbol: "$",
|
||||
code: "usd",
|
||||
},
|
||||
}
|
||||
|
||||
export const InGrid: Story = {
|
||||
render: (args) => {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-3">
|
||||
<CurrencyInput {...args} />
|
||||
<CurrencyInput {...args} />
|
||||
<CurrencyInput {...args} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
args: {
|
||||
symbol: "$",
|
||||
code: "usd",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Primitive from "react-currency-input-field"
|
||||
|
||||
import { Text } from "@/components/text"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
|
||||
const currencyInputVariants = cva(
|
||||
clx(
|
||||
"flex items-center gap-x-1",
|
||||
"bg-ui-bg-field hover:bg-ui-bg-field-hover shadow-buttons-neutral placeholder-ui-fg-muted text-ui-fg-base transition-fg relative w-full rounded-md",
|
||||
"focus-within:shadow-borders-interactive-with-active"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "txt-compact-medium h-10 px-3",
|
||||
small: "txt-compact-small h-8 px-2",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface CurrencyInputProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<typeof Primitive>,
|
||||
"prefix" | "suffix" | "size"
|
||||
>,
|
||||
VariantProps<typeof currencyInputVariants> {
|
||||
symbol: string
|
||||
code: string
|
||||
}
|
||||
|
||||
const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
|
||||
(
|
||||
{ size = "base", symbol, code, disabled, onInvalid, className, ...props },
|
||||
ref
|
||||
) => {
|
||||
const innerRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
React.useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
|
||||
ref,
|
||||
() => innerRef.current
|
||||
)
|
||||
|
||||
const [valid, setValid] = React.useState(true)
|
||||
|
||||
const onInnerInvalid = React.useCallback(
|
||||
(event: React.FormEvent<HTMLInputElement>) => {
|
||||
setValid(event.currentTarget.validity.valid)
|
||||
|
||||
if (onInvalid) {
|
||||
onInvalid(event)
|
||||
}
|
||||
},
|
||||
[onInvalid]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.focus()
|
||||
}
|
||||
}}
|
||||
className={clx(
|
||||
"w-full cursor-text justify-between overflow-hidden",
|
||||
currencyInputVariants({ size }),
|
||||
{
|
||||
"text-ui-fg-disabled !bg-ui-bg-disabled !shadow-buttons-neutral !placeholder-ui-fg-disabled cursor-not-allowed":
|
||||
disabled,
|
||||
"!shadow-borders-error invalid:!shadow-borders-error":
|
||||
props["aria-invalid"] || !valid,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={clx("w-fit", {
|
||||
"py-[9px]": size === "base",
|
||||
"py-[5px]": size === "small",
|
||||
})}
|
||||
role="presentation"
|
||||
>
|
||||
<Text
|
||||
className={clx(
|
||||
"text-ui-fg-muted pointer-events-none select-none uppercase",
|
||||
{
|
||||
"text-ui-fg-disabled": disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{code}
|
||||
</Text>
|
||||
</span>
|
||||
<Primitive
|
||||
className="h-full min-w-0 flex-1 appearance-none bg-transparent text-right outline-none disabled:cursor-not-allowed"
|
||||
disabled={disabled}
|
||||
onInvalid={onInnerInvalid}
|
||||
ref={innerRef}
|
||||
{...props}
|
||||
/>
|
||||
<span
|
||||
className={clx("w-fit min-w-[16px] text-right", {
|
||||
"py-[9px]": size === "base",
|
||||
"py-[5px]": size === "small",
|
||||
})}
|
||||
role="presentation"
|
||||
>
|
||||
<Text
|
||||
className={clx("text-ui-fg-muted pointer-events-none select-none", {
|
||||
"text-ui-fg-disabled": disabled,
|
||||
})}
|
||||
>
|
||||
{symbol}
|
||||
</Text>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
CurrencyInput.displayName = "CurrencyInput"
|
||||
|
||||
export { CurrencyInput }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./currency-input"
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { DatePicker } from "./date-picker"
|
||||
|
||||
describe("DatePicker", () => {
|
||||
describe("Preset validation", () => {
|
||||
it("should throw an error if a preset is before the min year", async () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<DatePicker
|
||||
fromYear={1800}
|
||||
presets={[
|
||||
{
|
||||
label: "Year of the first US census",
|
||||
date: new Date(1790, 0, 1),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
).toThrowError(
|
||||
/Preset Year of the first US census is before fromYear 1800./
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error if a preset is after the max year", async () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<DatePicker
|
||||
toYear={2012}
|
||||
presets={[
|
||||
{
|
||||
label: "End of the Mayan calendar",
|
||||
date: new Date(2025, 0, 1),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
).toThrowError(/Preset End of the Mayan calendar is after toYear 2012./)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Single", () => {
|
||||
it("should render", async () => {
|
||||
render(<DatePicker />)
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Range", () => {
|
||||
it("should render", async () => {
|
||||
render(<DatePicker mode={"range"} />)
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,243 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { DatePicker } from "./date-picker"
|
||||
import { Popover } from "@/components/popover"
|
||||
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
title: "Components/DatePicker",
|
||||
component: DatePicker,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<div className="w-[200px]">
|
||||
<DatePicker {...args} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof DatePicker>
|
||||
|
||||
const presets = [
|
||||
{
|
||||
label: "Today",
|
||||
date: new Date(),
|
||||
},
|
||||
{
|
||||
label: "Tomorrow",
|
||||
date: new Date(new Date().setDate(new Date().getDate() + 1)),
|
||||
},
|
||||
{
|
||||
label: "A week from now",
|
||||
date: new Date(new Date().setDate(new Date().getDate() + 7)),
|
||||
},
|
||||
{
|
||||
label: "A month from now",
|
||||
date: new Date(new Date().setMonth(new Date().getMonth() + 1)),
|
||||
},
|
||||
{
|
||||
label: "6 months from now",
|
||||
date: new Date(new Date().setMonth(new Date().getMonth() + 6)),
|
||||
},
|
||||
{
|
||||
label: "A year from now",
|
||||
date: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
},
|
||||
]
|
||||
|
||||
export const Single: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const SingleWithPresets: Story = {
|
||||
args: {
|
||||
presets,
|
||||
},
|
||||
}
|
||||
|
||||
export const SingleWithTimePicker: Story = {
|
||||
args: {
|
||||
showTimePicker: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const SingleWithTimePickerAndPresets: Story = {
|
||||
args: {
|
||||
showTimePicker: true,
|
||||
presets,
|
||||
},
|
||||
}
|
||||
|
||||
const rangePresets = [
|
||||
{
|
||||
label: "Today",
|
||||
dateRange: {
|
||||
from: new Date(),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 3 months",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setMonth(new Date().getMonth() - 3)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 6 months",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setMonth(new Date().getMonth() - 6)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Month to date",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setDate(1)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Year to date",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setFullYear(new Date().getFullYear(), 0, 1)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const Range: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
},
|
||||
}
|
||||
|
||||
export const RangeWithPresets: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
presets: rangePresets,
|
||||
},
|
||||
}
|
||||
|
||||
export const RangeWithTimePicker: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
showTimePicker: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const RangeWithTimePickerAndPresets: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
showTimePicker: true,
|
||||
presets: rangePresets,
|
||||
},
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [value, setValue] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col gap-y-4">
|
||||
<DatePicker
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
setValue(value)
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => setValue(undefined)}>Reset</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => <ControlledDemo />,
|
||||
}
|
||||
|
||||
const ControlledRangeDemo = () => {
|
||||
const [value, setValue] = React.useState<DateRange | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("Value changed: ", value)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col gap-y-4">
|
||||
<DatePicker
|
||||
mode="range"
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
setValue(value)
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => setValue(undefined)}>Reset</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ControlledRange: Story = {
|
||||
render: () => <ControlledRangeDemo />,
|
||||
}
|
||||
|
||||
type NestedProps = {
|
||||
value?: Date
|
||||
onChange?: (value: Date | undefined) => void
|
||||
}
|
||||
const Nested = ({ value, onChange }: NestedProps) => {
|
||||
return (
|
||||
<Popover>
|
||||
<Popover.Trigger asChild>
|
||||
<Button>Open</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<div className="px-3 py-2">
|
||||
<DatePicker value={value} onChange={onChange} />
|
||||
</div>
|
||||
<Popover.Seperator />
|
||||
<div className="px-3 py-2">
|
||||
<DatePicker value={value} onChange={onChange} />
|
||||
</div>
|
||||
<Popover.Seperator />
|
||||
<div className="flex items-center justify-between gap-x-2 px-3 py-2 [&_button]:w-full">
|
||||
<Button variant="secondary">Clear</Button>
|
||||
<Button>Apply</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const NestedDemo = () => {
|
||||
const [value, setValue] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col gap-y-4">
|
||||
<Nested value={value} onChange={setValue} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const NestedControlled: Story = {
|
||||
render: () => <NestedDemo />,
|
||||
}
|
||||
@@ -0,0 +1,908 @@
|
||||
"use client"
|
||||
|
||||
import { Time } from "@internationalized/date"
|
||||
import { Calendar as CalendarIcon, Minus } from "@medusajs/icons"
|
||||
import * as Primitives from "@radix-ui/react-popover"
|
||||
import { TimeValue } from "@react-aria/datepicker"
|
||||
import { format } from "date-fns"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Calendar as CalendarPrimitive } from "@/components/calendar"
|
||||
import { TimeInput } from "@/components/time-input"
|
||||
import type { DateRange } from "@/types"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { isBrowserLocaleClockType24h } from "@/utils/is-browser-locale-hour-cycle-24h"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
const displayVariants = cva(
|
||||
clx(
|
||||
"text-ui-fg-base bg-ui-bg-field transition-fg shadow-buttons-neutral flex w-full items-center gap-x-2 rounded-md outline-none",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"focus:shadow-borders-interactive-with-active data-[state=open]:shadow-borders-interactive-with-active",
|
||||
"disabled:bg-ui-bg-disabled disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral",
|
||||
"aria-[invalid=true]:!shadow-borders-error"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "txt-compact-medium h-10 px-3 py-2.5",
|
||||
small: "txt-compact-small h-8 px-2 py-1.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Display = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
placeholder?: string
|
||||
size?: "small" | "base"
|
||||
}
|
||||
>(({ className, children, placeholder, size = "base", ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Trigger asChild>
|
||||
<button
|
||||
ref={ref}
|
||||
className={clx(displayVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<CalendarIcon className="text-ui-fg-muted" />
|
||||
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-left">
|
||||
{children ? (
|
||||
children
|
||||
) : placeholder ? (
|
||||
<span className="text-ui-fg-muted">{placeholder}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
</Primitives.Trigger>
|
||||
)
|
||||
})
|
||||
Display.displayName = "DatePicker.Display"
|
||||
|
||||
const Flyout = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Content>,
|
||||
React.ComponentProps<typeof Primitives.Content>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Portal>
|
||||
<Primitives.Content
|
||||
ref={ref}
|
||||
sideOffset={8}
|
||||
align="center"
|
||||
className={clx(
|
||||
"txt-compact-small shadow-elevation-flyout bg-ui-bg-base w-fit rounded-lg",
|
||||
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Primitives.Content>
|
||||
</Primitives.Portal>
|
||||
)
|
||||
})
|
||||
Flyout.displayName = "DatePicker.Flyout"
|
||||
|
||||
interface Preset {
|
||||
label: string
|
||||
}
|
||||
|
||||
interface DatePreset extends Preset {
|
||||
date: Date
|
||||
}
|
||||
|
||||
interface DateRangePreset extends Preset {
|
||||
dateRange: DateRange
|
||||
}
|
||||
|
||||
type PresetContainerProps<TPreset extends Preset, TValue> = {
|
||||
presets: TPreset[] | TPreset[]
|
||||
onSelect: (value: TValue) => void
|
||||
currentValue?: TValue
|
||||
}
|
||||
|
||||
const PresetContainer = <TPreset extends Preset, TValue>({
|
||||
presets,
|
||||
onSelect,
|
||||
currentValue,
|
||||
}: PresetContainerProps<TPreset, TValue>) => {
|
||||
const isDateRangePresets = (preset: any): preset is DateRangePreset => {
|
||||
return "dateRange" in preset
|
||||
}
|
||||
|
||||
const isDatePresets = (preset: any): preset is DatePreset => {
|
||||
return "date" in preset
|
||||
}
|
||||
|
||||
const handleClick = (preset: TPreset) => {
|
||||
if (isDateRangePresets(preset)) {
|
||||
onSelect(preset.dateRange as TValue)
|
||||
} else if (isDatePresets(preset)) {
|
||||
onSelect(preset.date as TValue)
|
||||
}
|
||||
}
|
||||
|
||||
const compareDates = (date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getDate() === date2.getDate() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getFullYear() === date2.getFullYear()
|
||||
)
|
||||
}
|
||||
|
||||
const compareRanges = (range1: DateRange, range2: DateRange) => {
|
||||
const from1 = range1.from
|
||||
const from2 = range2.from
|
||||
|
||||
let equalFrom = false
|
||||
|
||||
if (from1 && from2) {
|
||||
const sameFrom = compareDates(from1, from2)
|
||||
|
||||
if (sameFrom) {
|
||||
equalFrom = true
|
||||
}
|
||||
}
|
||||
|
||||
const to1 = range1.to
|
||||
const to2 = range2.to
|
||||
|
||||
let equalTo = false
|
||||
|
||||
if (to1 && to2) {
|
||||
const sameTo = compareDates(to1, to2)
|
||||
|
||||
if (sameTo) {
|
||||
equalTo = true
|
||||
}
|
||||
}
|
||||
|
||||
return equalFrom && equalTo
|
||||
}
|
||||
|
||||
const matchesCurrent = (preset: TPreset) => {
|
||||
if (isDateRangePresets(preset)) {
|
||||
const value = currentValue as DateRange | undefined
|
||||
|
||||
return value && compareRanges(value, preset.dateRange)
|
||||
} else if (isDatePresets(preset)) {
|
||||
const value = currentValue as Date | undefined
|
||||
|
||||
return value && compareDates(value, preset.date)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="flex flex-col items-start">
|
||||
{presets.map((preset, index) => {
|
||||
return (
|
||||
<li key={index} className="w-full">
|
||||
<button
|
||||
className={clx(
|
||||
"txt-compact-small-plus w-full overflow-hidden text-ellipsis whitespace-nowrap rounded-md p-2 text-left",
|
||||
"text-ui-fg-subtle hover:bg-ui-bg-base-hover outline-none transition-all",
|
||||
"focus:bg-ui-bg-base-hover",
|
||||
{
|
||||
"!bg-ui-bg-base-pressed": matchesCurrent(preset),
|
||||
}
|
||||
)}
|
||||
onClick={() => handleClick(preset)}
|
||||
aria-label={`Select ${preset.label}`}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
const formatDate = (date: Date, includeTime?: boolean) => {
|
||||
const usesAmPm = !isBrowserLocaleClockType24h()
|
||||
|
||||
if (includeTime) {
|
||||
if (usesAmPm) {
|
||||
return format(date, "MMM d, yyyy h:mm a")
|
||||
}
|
||||
|
||||
return format(date, "MMM d, yyyy HH:mm")
|
||||
}
|
||||
|
||||
return format(date, "MMM d, yyyy")
|
||||
}
|
||||
|
||||
type CalendarProps = {
|
||||
fromYear?: number
|
||||
toYear?: number
|
||||
fromMonth?: Date
|
||||
toMonth?: Date
|
||||
fromDay?: Date
|
||||
toDay?: Date
|
||||
fromDate?: Date
|
||||
toDate?: Date
|
||||
}
|
||||
|
||||
interface PickerProps extends CalendarProps {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
size?: "small" | "base"
|
||||
showTimePicker?: boolean
|
||||
id?: string
|
||||
"aria-invalid"?: boolean
|
||||
"aria-label"?: string
|
||||
"aria-labelledby"?: string
|
||||
"aria-required"?: boolean
|
||||
}
|
||||
|
||||
interface SingleProps extends PickerProps {
|
||||
presets?: DatePreset[]
|
||||
defaultValue?: Date
|
||||
value?: Date
|
||||
onChange?: (date: Date | undefined) => void
|
||||
}
|
||||
|
||||
const SingleDatePicker = ({
|
||||
defaultValue,
|
||||
value,
|
||||
size = "base",
|
||||
onChange,
|
||||
presets,
|
||||
showTimePicker,
|
||||
disabled,
|
||||
className,
|
||||
...props
|
||||
}: SingleProps) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
value ?? defaultValue ?? undefined
|
||||
)
|
||||
const [month, setMonth] = React.useState<Date | undefined>(date)
|
||||
|
||||
const [time, setTime] = React.useState<TimeValue>(
|
||||
value
|
||||
? new Time(value.getHours(), value.getMinutes())
|
||||
: defaultValue
|
||||
? new Time(defaultValue.getHours(), defaultValue.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
|
||||
const initialDate = React.useMemo(() => {
|
||||
return date
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
/**
|
||||
* Update the date when the value changes.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
setDate(value ?? defaultValue ?? undefined)
|
||||
}, [value, defaultValue])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (date) {
|
||||
setMonth(date)
|
||||
}
|
||||
}, [date])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setMonth(date)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
const onCancel = () => {
|
||||
setDate(initialDate)
|
||||
setTime(
|
||||
initialDate
|
||||
? new Time(initialDate.getHours(), initialDate.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onCancel()
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const onDateChange = (date: Date | undefined) => {
|
||||
const newDate = date
|
||||
|
||||
if (showTimePicker) {
|
||||
/**
|
||||
* If the time is cleared, and the date is
|
||||
* changed then we want to reset the time.
|
||||
*/
|
||||
if (newDate && !time) {
|
||||
setTime(new Time(0, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* If the time is set, and the date is changed
|
||||
* then we want to update the date with the
|
||||
* time.
|
||||
*/
|
||||
if (newDate && time) {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
}
|
||||
|
||||
setDate(newDate)
|
||||
}
|
||||
|
||||
const onTimeChange = (time: TimeValue) => {
|
||||
setTime(time)
|
||||
|
||||
if (!date) {
|
||||
return
|
||||
}
|
||||
|
||||
const newDate = new Date(date.getTime())
|
||||
|
||||
if (!time) {
|
||||
/**
|
||||
* When a segment of the time input is cleared,
|
||||
* it will return `null` as the value is no longer
|
||||
* a valid time. In this case, we want to set the
|
||||
* time to for the date, effectivly resetting the
|
||||
* input field.
|
||||
*/
|
||||
newDate.setHours(0)
|
||||
newDate.setMinutes(0)
|
||||
} else {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
|
||||
setDate(newDate)
|
||||
}
|
||||
|
||||
const formattedDate = React.useMemo(() => {
|
||||
if (!date) {
|
||||
return null
|
||||
}
|
||||
|
||||
return formatDate(date, showTimePicker)
|
||||
}, [date, showTimePicker])
|
||||
|
||||
const onApply = () => {
|
||||
setOpen(false)
|
||||
onChange?.(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<Primitives.Root open={open} onOpenChange={onOpenChange}>
|
||||
<Display
|
||||
placeholder="Pick a date"
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
aria-required={props.required || props["aria-required"]}
|
||||
aria-invalid={props["aria-invalid"]}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-labelledby={props["aria-labelledby"]}
|
||||
size={size}
|
||||
>
|
||||
{formattedDate}
|
||||
</Display>
|
||||
<Flyout>
|
||||
<div className="flex">
|
||||
<div className="flex items-start">
|
||||
{presets && presets.length > 0 && (
|
||||
<div className="relative h-full w-[160px] border-r">
|
||||
<div className="absolute inset-0 overflow-y-scroll p-3">
|
||||
<PresetContainer
|
||||
currentValue={date}
|
||||
presets={presets}
|
||||
onSelect={onDateChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CalendarPrimitive
|
||||
mode="single"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
selected={date}
|
||||
onSelect={onDateChange}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
{showTimePicker && (
|
||||
<div className="border-ui-border-base border-t p-3">
|
||||
<TimeInput
|
||||
aria-label="Time"
|
||||
onChange={onTimeChange}
|
||||
isDisabled={!date}
|
||||
value={time}
|
||||
isRequired={props.required}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-ui-border-base flex items-center gap-x-2 border-t p-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Flyout>
|
||||
</Primitives.Root>
|
||||
)
|
||||
}
|
||||
|
||||
interface RangeProps extends PickerProps {
|
||||
presets?: DateRangePreset[]
|
||||
defaultValue?: DateRange
|
||||
value?: DateRange
|
||||
onChange?: (dateRange: DateRange | undefined) => void
|
||||
}
|
||||
|
||||
const RangeDatePicker = ({
|
||||
defaultValue,
|
||||
value,
|
||||
onChange,
|
||||
size = "base",
|
||||
showTimePicker,
|
||||
presets,
|
||||
disabled,
|
||||
className,
|
||||
...props
|
||||
}: RangeProps) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [range, setRange] = React.useState<DateRange | undefined>(
|
||||
value ?? defaultValue ?? undefined
|
||||
)
|
||||
const [month, setMonth] = React.useState<Date | undefined>(range?.from)
|
||||
|
||||
const [startTime, setStartTime] = React.useState<TimeValue>(
|
||||
value?.from
|
||||
? new Time(value.from.getHours(), value.from.getMinutes())
|
||||
: defaultValue?.from
|
||||
? new Time(defaultValue.from.getHours(), defaultValue.from.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
const [endTime, setEndTime] = React.useState<TimeValue>(
|
||||
value?.to
|
||||
? new Time(value.to.getHours(), value.to.getMinutes())
|
||||
: defaultValue?.to
|
||||
? new Time(defaultValue.to.getHours(), defaultValue.to.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
|
||||
const initialRange = React.useMemo(() => {
|
||||
return range
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
/**
|
||||
* Update the range when the value changes.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
setRange(value ?? defaultValue ?? undefined)
|
||||
}, [value, defaultValue])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (range) {
|
||||
setMonth(range.from)
|
||||
}
|
||||
}, [range])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setMonth(range?.from)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
const onRangeChange = (range: DateRange | undefined) => {
|
||||
const newRange = range
|
||||
|
||||
if (showTimePicker) {
|
||||
if (newRange?.from && !startTime) {
|
||||
setStartTime(new Time(0, 0))
|
||||
}
|
||||
|
||||
if (newRange?.to && !endTime) {
|
||||
setEndTime(new Time(0, 0))
|
||||
}
|
||||
|
||||
if (newRange?.from && startTime) {
|
||||
newRange.from.setHours(startTime.hour)
|
||||
newRange.from.setMinutes(startTime.minute)
|
||||
}
|
||||
|
||||
if (newRange?.to && endTime) {
|
||||
newRange.to.setHours(endTime.hour)
|
||||
newRange.to.setMinutes(endTime.minute)
|
||||
}
|
||||
}
|
||||
|
||||
setRange(newRange)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setRange(initialRange)
|
||||
setStartTime(
|
||||
initialRange?.from
|
||||
? new Time(initialRange.from.getHours(), initialRange.from.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
setEndTime(
|
||||
initialRange?.to
|
||||
? new Time(initialRange.to.getHours(), initialRange.to.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onCancel()
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const onTimeChange = (time: TimeValue, pos: "start" | "end") => {
|
||||
switch (pos) {
|
||||
case "start":
|
||||
setStartTime(time)
|
||||
break
|
||||
case "end":
|
||||
setEndTime(time)
|
||||
break
|
||||
}
|
||||
|
||||
if (!range) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pos === "start") {
|
||||
if (!range.from) {
|
||||
return
|
||||
}
|
||||
|
||||
const newDate = new Date(range.from.getTime())
|
||||
|
||||
if (!time) {
|
||||
newDate.setHours(0)
|
||||
newDate.setMinutes(0)
|
||||
} else {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
|
||||
setRange({
|
||||
...range,
|
||||
from: newDate,
|
||||
})
|
||||
}
|
||||
|
||||
if (pos === "end") {
|
||||
if (!range.to) {
|
||||
return
|
||||
}
|
||||
|
||||
const newDate = new Date(range.to.getTime())
|
||||
|
||||
if (!time) {
|
||||
newDate.setHours(0)
|
||||
newDate.setMinutes(0)
|
||||
} else {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
|
||||
setRange({
|
||||
...range,
|
||||
to: newDate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const displayRange = React.useMemo(() => {
|
||||
if (!range) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${range.from ? formatDate(range.from, showTimePicker) : ""} - ${
|
||||
range.to ? formatDate(range.to, showTimePicker) : ""
|
||||
}`
|
||||
}, [range, showTimePicker])
|
||||
|
||||
const onApply = () => {
|
||||
setOpen(false)
|
||||
onChange?.(range)
|
||||
}
|
||||
|
||||
return (
|
||||
<Primitives.Root open={open} onOpenChange={onOpenChange}>
|
||||
<Display
|
||||
placeholder="Pick a date"
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
aria-required={props.required || props["aria-required"]}
|
||||
aria-invalid={props["aria-invalid"]}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-labelledby={props["aria-labelledby"]}
|
||||
size={size}
|
||||
>
|
||||
{displayRange}
|
||||
</Display>
|
||||
<Flyout>
|
||||
<div className="flex">
|
||||
<div className="flex items-start">
|
||||
{presets && presets.length > 0 && (
|
||||
<div className="relative h-full w-[160px] border-r">
|
||||
<div className="absolute inset-0 overflow-y-scroll p-3">
|
||||
<PresetContainer
|
||||
currentValue={range}
|
||||
presets={presets}
|
||||
onSelect={onRangeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CalendarPrimitive
|
||||
mode="range"
|
||||
selected={range}
|
||||
onSelect={onRangeChange}
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
numberOfMonths={2}
|
||||
disabled={disabled}
|
||||
classNames={{
|
||||
months: "flex flex-row divide-x divide-ui-border-base",
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
{showTimePicker && (
|
||||
<div className="border-ui-border-base flex items-center justify-evenly gap-x-3 border-t p-3">
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<span className="text-ui-fg-subtle">Start:</span>
|
||||
<TimeInput
|
||||
value={startTime}
|
||||
onChange={(v) => onTimeChange(v, "start")}
|
||||
aria-label="Start date time"
|
||||
isDisabled={!range?.from}
|
||||
isRequired={props.required}
|
||||
/>
|
||||
</div>
|
||||
<Minus className="text-ui-fg-muted" />
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<span className="text-ui-fg-subtle">End:</span>
|
||||
<TimeInput
|
||||
value={endTime}
|
||||
onChange={(v) => onTimeChange(v, "end")}
|
||||
aria-label="End date time"
|
||||
isDisabled={!range?.to}
|
||||
isRequired={props.required}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between border-t p-3">
|
||||
<p className={clx("text-ui-fg-subtle txt-compact-small-plus")}>
|
||||
<span className="text-ui-fg-muted">Range:</span>{" "}
|
||||
{displayRange}
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button variant="secondary" type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="button" onClick={onApply}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Flyout>
|
||||
</Primitives.Root>
|
||||
)
|
||||
}
|
||||
|
||||
type DatePickerProps = (
|
||||
| {
|
||||
mode?: "single"
|
||||
presets?: DatePreset[]
|
||||
defaultValue?: Date
|
||||
value?: Date
|
||||
onChange?: (date: Date | undefined) => void
|
||||
}
|
||||
| {
|
||||
mode: "range"
|
||||
presets?: DateRangePreset[]
|
||||
defaultValue?: DateRange
|
||||
value?: DateRange
|
||||
onChange?: (dateRange: DateRange | undefined) => void
|
||||
}
|
||||
) &
|
||||
PickerProps
|
||||
|
||||
const validatePresets = (
|
||||
presets: DateRangePreset[] | DatePreset[],
|
||||
rules: PickerProps
|
||||
) => {
|
||||
const { toYear, fromYear, fromMonth, toMonth, fromDay, toDay } = rules
|
||||
|
||||
if (presets && presets.length > 0) {
|
||||
const fromYearToUse = fromYear
|
||||
const toYearToUse = toYear
|
||||
|
||||
presets.forEach((preset) => {
|
||||
if ("date" in preset) {
|
||||
const presetYear = preset.date.getFullYear()
|
||||
|
||||
if (fromYear && presetYear < fromYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is before fromYear ${fromYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (toYear && presetYear > toYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is after toYear ${toYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (fromMonth) {
|
||||
const presetMonth = preset.date.getMonth()
|
||||
|
||||
if (presetMonth < fromMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is before fromMonth ${fromMonth}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toMonth) {
|
||||
const presetMonth = preset.date.getMonth()
|
||||
|
||||
if (presetMonth > toMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is after toMonth ${toMonth}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (fromDay) {
|
||||
const presetDay = preset.date.getDate()
|
||||
|
||||
if (presetDay < fromDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is before fromDay ${fromDay}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toDay) {
|
||||
const presetDay = preset.date.getDate()
|
||||
|
||||
if (presetDay > toDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is after toDay ${format(
|
||||
toDay,
|
||||
"MMM dd, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("dateRange" in preset) {
|
||||
const presetFromYear = preset.dateRange.from?.getFullYear()
|
||||
const presetToYear = preset.dateRange.to?.getFullYear()
|
||||
|
||||
if (presetFromYear && fromYear && presetFromYear < fromYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'from' is before fromYear ${fromYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (presetToYear && toYear && presetToYear > toYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'to' is after toYear ${toYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (fromMonth) {
|
||||
const presetMonth = preset.dateRange.from?.getMonth()
|
||||
|
||||
if (presetMonth && presetMonth < fromMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'from' is before fromMonth ${format(
|
||||
fromMonth,
|
||||
"MMM, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toMonth) {
|
||||
const presetMonth = preset.dateRange.to?.getMonth()
|
||||
|
||||
if (presetMonth && presetMonth > toMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'to' is after toMonth ${format(
|
||||
toMonth,
|
||||
"MMM, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (fromDay) {
|
||||
const presetDay = preset.dateRange.from?.getDate()
|
||||
|
||||
if (presetDay && presetDay < fromDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${
|
||||
preset.dateRange.from
|
||||
}'s 'from' is before fromDay ${format(fromDay, "MMM dd, yyyy")}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toDay) {
|
||||
const presetDay = preset.dateRange.to?.getDate()
|
||||
|
||||
if (presetDay && presetDay > toDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'to' is after toDay ${format(
|
||||
toDay,
|
||||
"MMM dd, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const DatePicker = ({ mode = "single", ...props }: DatePickerProps) => {
|
||||
if (props.presets) {
|
||||
validatePresets(props.presets, props)
|
||||
}
|
||||
|
||||
if (mode === "single") {
|
||||
return <SingleDatePicker {...(props as SingleProps)} />
|
||||
}
|
||||
|
||||
return <RangeDatePicker {...(props as RangeProps)} />
|
||||
}
|
||||
|
||||
export { DatePicker }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./date-picker"
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Drawer } from "./drawer"
|
||||
|
||||
const meta: Meta<typeof Drawer> = {
|
||||
title: "Components/Drawer",
|
||||
component: Drawer,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Drawer>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<Drawer>
|
||||
<Drawer.Trigger asChild>
|
||||
<Button>Edit Variant</Button>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Edit Variant</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<Drawer.Body></Drawer.Body>
|
||||
<Drawer.Footer>
|
||||
<Drawer.Close asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
</Drawer.Close>
|
||||
<Button>Save</Button>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer>
|
||||
)
|
||||
},
|
||||
}
|
||||
175
packages/design-system/ui/src/components/drawer/drawer.tsx
Normal file
175
packages/design-system/ui/src/components/drawer/drawer.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client"
|
||||
|
||||
import { XMark } from "@medusajs/icons"
|
||||
import * as DrawerPrimitives from "@radix-ui/react-dialog"
|
||||
import * as React from "react"
|
||||
|
||||
import { Heading } from "@/components/heading"
|
||||
import { IconButton } from "@/components/icon-button"
|
||||
import { Kbd } from "@/components/kbd"
|
||||
import { Text } from "@/components/text"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const DrawerRoot = (
|
||||
props: React.ComponentPropsWithoutRef<typeof DrawerPrimitives.Root>
|
||||
) => {
|
||||
return <DrawerPrimitives.Root {...props} />
|
||||
}
|
||||
DrawerRoot.displayName = "Drawer.Root"
|
||||
|
||||
const DrawerTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitives.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitives.Trigger>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<DrawerPrimitives.Trigger ref={ref} className={clx(className)} {...props} />
|
||||
)
|
||||
})
|
||||
DrawerTrigger.displayName = "Drawer.Trigger"
|
||||
|
||||
const DrawerClose = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<DrawerPrimitives.Close ref={ref} className={clx(className)} {...props} />
|
||||
)
|
||||
})
|
||||
DrawerClose.displayName = "Drawer.Close"
|
||||
|
||||
const DrawerPortal = (props: DrawerPrimitives.DialogPortalProps) => {
|
||||
return <DrawerPrimitives.DialogPortal {...props} />
|
||||
}
|
||||
DrawerPortal.displayName = "Drawer.Portal"
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitives.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitives.Overlay>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<DrawerPrimitives.Overlay
|
||||
ref={ref}
|
||||
className={clx("bg-ui-bg-overlay fixed inset-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
DrawerOverlay.displayName = "Drawer.Overlay"
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitives.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitives.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base shadow-elevation-modal border-ui-border-base fixed inset-y-2 right-2 flex w-full max-w-[560px] flex-1 flex-col rounded-lg border focus:outline-none",
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right-1/2 data-[state=open]:slide-in-from-right-1/2 duration-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DrawerPortal>
|
||||
)
|
||||
})
|
||||
DrawerContent.displayName = "Drawer.Content"
|
||||
|
||||
const DrawerHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="border-ui-border-base flex items-start justify-between gap-x-4 border-b px-8 py-6"
|
||||
{...props}
|
||||
>
|
||||
<div className={clx("flex flex-col gap-y-1", className)}>{children}</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Kbd>esc</Kbd>
|
||||
<DrawerPrimitives.Close asChild>
|
||||
<IconButton variant="transparent">
|
||||
<XMark />
|
||||
</IconButton>
|
||||
</DrawerPrimitives.Close>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
DrawerHeader.displayName = "Drawer.Header"
|
||||
|
||||
const DrawerBody = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx("flex-1 px-8 pb-16 pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
DrawerBody.displayName = "Drawer.Body"
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
"border-ui-border-base flex items-center justify-end space-x-2 overflow-y-scroll border-t px-8 pb-6 pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DrawerFooter.displayName = "Drawer.Footer"
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitives.Title>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPrimitives.Title
|
||||
ref={ref}
|
||||
className={clx(className)}
|
||||
asChild
|
||||
{...props}
|
||||
>
|
||||
<Heading level="h1">{children}</Heading>
|
||||
</DrawerPrimitives.Title>
|
||||
))
|
||||
DrawerTitle.displayName = "Drawer.Title"
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitives.Description>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPrimitives.Description
|
||||
ref={ref}
|
||||
className={clx(className)}
|
||||
asChild
|
||||
{...props}
|
||||
>
|
||||
<Text>{children}</Text>
|
||||
</DrawerPrimitives.Description>
|
||||
))
|
||||
DrawerDescription.displayName = "Drawer.Description"
|
||||
|
||||
const Drawer = Object.assign(DrawerRoot, {
|
||||
Body: DrawerBody,
|
||||
Close: DrawerClose,
|
||||
Content: DrawerContent,
|
||||
Description: DrawerDescription,
|
||||
Footer: DrawerFooter,
|
||||
Header: DrawerHeader,
|
||||
Title: DrawerTitle,
|
||||
Trigger: DrawerTrigger,
|
||||
})
|
||||
|
||||
export { Drawer }
|
||||
1
packages/design-system/ui/src/components/drawer/index.ts
Normal file
1
packages/design-system/ui/src/components/drawer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./drawer"
|
||||
@@ -0,0 +1,275 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Plus, Trash } from "@medusajs/icons"
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { IconButton } from "@/components/icon-button"
|
||||
import { Select } from "@/components/select"
|
||||
|
||||
import { DatePicker } from "../date-picker"
|
||||
import { FocusModal } from "../focus-modal"
|
||||
import { DropdownMenu } from "./dropdown-menu"
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: "Components/DropdownMenu",
|
||||
component: DropdownMenu,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof DropdownMenu>
|
||||
|
||||
type SortingState = "asc" | "desc" | "alpha" | "alpha-reverse" | "none"
|
||||
|
||||
const SortingDemo = () => {
|
||||
const [sort, setSort] = React.useState<SortingState>("none")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="primary">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="w-[300px]">
|
||||
<DropdownMenu.RadioGroup
|
||||
value={sort}
|
||||
onValueChange={(v) => setSort(v as SortingState)}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="none">
|
||||
No Sorting
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.RadioItem value="alpha">
|
||||
Alphabetical
|
||||
<DropdownMenu.Hint>A-Z</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="alpha-reverse">
|
||||
Reverse Alphabetical
|
||||
<DropdownMenu.Hint>Z-A</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="asc">
|
||||
Created At - Ascending
|
||||
<DropdownMenu.Hint>1 - 30</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="desc">
|
||||
Created At - Descending
|
||||
<DropdownMenu.Hint>30 - 1</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<div>
|
||||
<pre>Sorting by: {sort}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SortingMenu: Story = {
|
||||
render: () => {
|
||||
return <SortingDemo />
|
||||
},
|
||||
}
|
||||
|
||||
const SelectDemo = () => {
|
||||
const [currencies, setCurrencies] = React.useState<string[]>([])
|
||||
const [regions, setRegions] = React.useState<string[]>([])
|
||||
|
||||
const onSelectCurrency = (currency: string) => {
|
||||
if (currencies.includes(currency)) {
|
||||
setCurrencies(currencies.filter((c) => c !== currency))
|
||||
} else {
|
||||
setCurrencies([...currencies, currency])
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectRegion = (region: string) => {
|
||||
if (regions.includes(region)) {
|
||||
setRegions(regions.filter((r) => r !== region))
|
||||
} else {
|
||||
setRegions([...regions, region])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton>
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="w-[300px]">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Currencies</DropdownMenu.Label>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={currencies.includes("EUR")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectCurrency("EUR")
|
||||
}}
|
||||
>
|
||||
EUR
|
||||
<DropdownMenu.Hint>Euro</DropdownMenu.Hint>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={currencies.includes("USD")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectCurrency("USD")
|
||||
}}
|
||||
>
|
||||
USD
|
||||
<DropdownMenu.Hint>US Dollar</DropdownMenu.Hint>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={currencies.includes("DKK")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectCurrency("DKK")
|
||||
}}
|
||||
>
|
||||
DKK
|
||||
<DropdownMenu.Hint>Danish Krone</DropdownMenu.Hint>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Regions</DropdownMenu.Label>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={regions.includes("NA")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectRegion("NA")
|
||||
}}
|
||||
>
|
||||
North America
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={regions.includes("EU")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectRegion("EU")
|
||||
}}
|
||||
>
|
||||
Europe
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={regions.includes("DK")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectRegion("DK")
|
||||
}}
|
||||
>
|
||||
Denmark
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<div>
|
||||
<pre>Currencies: {currencies.join(", ")}</pre>
|
||||
<pre>Regions: {regions.join(", ")}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SelectMenu: Story = {
|
||||
render: () => {
|
||||
return <SelectDemo />
|
||||
},
|
||||
}
|
||||
|
||||
export const SimpleMenu: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton>
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
Edit
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Plus className="text-ui-fg-subtle" />
|
||||
Add
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const ComplexMenuDemo = () => {
|
||||
return (
|
||||
<FocusModal>
|
||||
<FocusModal.Trigger asChild>
|
||||
<Button>Open</Button>
|
||||
</FocusModal.Trigger>
|
||||
<FocusModal.Content>
|
||||
<FocusModal.Header>
|
||||
<Button>Save</Button>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="item-center flex justify-center">
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button>View</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
Edit
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Plus className="text-ui-fg-subtle" />
|
||||
Add
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
Delete
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<div className="flex flex-col gap-y-2 p-2">
|
||||
<Select>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Select" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="1">One</Select.Item>
|
||||
<Select.Item value="2">Two</Select.Item>
|
||||
<Select.Item value="3">Three</Select.Item>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
<DatePicker />
|
||||
</div>
|
||||
<div className="border-ui-border-base flex items-center gap-x-2 border-t p-2">
|
||||
<Button variant="secondary">Clear</Button>
|
||||
<Button>Apply</Button>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
export const ComplexMenu: Story = {
|
||||
render: () => {
|
||||
return <ComplexMenuDemo />
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
"use client"
|
||||
|
||||
import { CheckMini, ChevronRightMini, EllipseMiniSolid } from "@medusajs/icons"
|
||||
import * as Primitives from "@radix-ui/react-dropdown-menu"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Root = Primitives.Root
|
||||
Root.displayName = "DropdownMenu.Root"
|
||||
|
||||
const Trigger = Primitives.Trigger
|
||||
Trigger.displayName = "DropdownMenu.Trigger"
|
||||
|
||||
const Group = Primitives.Group
|
||||
Group.displayName = "DropdownMenu.Group"
|
||||
|
||||
const SubMenu = Primitives.Sub
|
||||
SubMenu.displayName = "DropdownMenu.SubMenu"
|
||||
|
||||
const RadioGroup = Primitives.RadioGroup
|
||||
RadioGroup.displayName = "DropdownMenu.RadioGroup"
|
||||
|
||||
const SubMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.SubTrigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Primitives.SubTrigger
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"focus:bg-ui-bg-base-pressed data-[state=open]:bg-ui-bg-base-pressed txt-compact-small flex cursor-default select-none items-center rounded-sm px-3 py-2 outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightMini className="ml-auto" />
|
||||
</Primitives.SubTrigger>
|
||||
))
|
||||
SubMenuTrigger.displayName = "DropdownMenu.SubMenuTrigger"
|
||||
|
||||
const SubMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.SubContent>
|
||||
>(({ className, collisionPadding = 8, ...props }, ref) => (
|
||||
<Primitives.Portal>
|
||||
<Primitives.SubContent
|
||||
ref={ref}
|
||||
collisionPadding={collisionPadding}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] min-w-[8rem] overflow-hidden rounded-lg border p-1",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Primitives.Portal>
|
||||
))
|
||||
SubMenuContent.displayName = "DropdownMenu.SubMenuContent"
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Content>
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
sideOffset = 8,
|
||||
collisionPadding = 8,
|
||||
align = "start",
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<Primitives.Portal>
|
||||
<Primitives.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
collisionPadding={collisionPadding}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] min-w-[220px] overflow-hidden rounded-lg p-1",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Primitives.Portal>
|
||||
)
|
||||
)
|
||||
Content.displayName = "DropdownMenu.Content"
|
||||
|
||||
const Item = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitives.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base focus:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex cursor-pointer select-none items-center rounded-md px-3 py-2 outline-none transition-colors data-[disabled]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Item.displayName = "DropdownMenu.Item"
|
||||
|
||||
const CheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<Primitives.CheckboxItem
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"focus:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled relative flex cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-3 text-sm outline-none transition-colors data-[disabled]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-3 flex h-5 w-5 items-center justify-center">
|
||||
<Primitives.ItemIndicator>
|
||||
<CheckMini />
|
||||
</Primitives.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</Primitives.CheckboxItem>
|
||||
))
|
||||
CheckboxItem.displayName = "DropdownMenu.CheckboxItem"
|
||||
|
||||
const RadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Primitives.RadioItem
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"focus:bg-ui-bg-base-pressed hover:bg-ui-base-hover bg-ui-bg-base txt-compact-small relative flex cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-3 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[state=checked]:font-medium data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-3 flex h-5 w-5 items-center justify-center">
|
||||
<Primitives.ItemIndicator>
|
||||
<EllipseMiniSolid className="text-ui-fg-base" />
|
||||
</Primitives.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</Primitives.RadioItem>
|
||||
))
|
||||
RadioItem.displayName = "DropdownMenu.RadioItem"
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitives.Label
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"text-ui-fg-subtle txt-compact-xsmall-plus px-2 py-1.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = "DropdownMenu.Label"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitives.Separator
|
||||
ref={ref}
|
||||
className={clx("bg-ui-border-base -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = "DropdownMenu.Separator"
|
||||
|
||||
const Shortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={clx(
|
||||
"text-ui-fg-subtle txt-compact-small ml-auto tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Shortcut.displayName = "DropdownMenu.Shortcut"
|
||||
|
||||
const Hint = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={clx(
|
||||
"text-ui-fg-subtle txt-compact-small ml-auto tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Hint.displayName = "DropdownMenu.Hint"
|
||||
|
||||
const DropdownMenu = Object.assign(Root, {
|
||||
Trigger,
|
||||
Group,
|
||||
SubMenu,
|
||||
SubMenuContent,
|
||||
SubMenuTrigger,
|
||||
Content,
|
||||
Item,
|
||||
CheckboxItem,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
Label,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Hint,
|
||||
})
|
||||
|
||||
export { DropdownMenu }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./dropdown-menu"
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { FocusModal } from "./focus-modal"
|
||||
|
||||
const meta: Meta<typeof FocusModal> = {
|
||||
title: "Components/FocusModal",
|
||||
component: FocusModal,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof FocusModal>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<FocusModal>
|
||||
<FocusModal.Trigger asChild>
|
||||
<Button>Edit Variant</Button>
|
||||
</FocusModal.Trigger>
|
||||
<FocusModal.Content>
|
||||
<FocusModal.Header>
|
||||
<Button>Save</Button>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body></FocusModal.Body>
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
|
||||
import { XMark } from "@medusajs/icons"
|
||||
import * as FocusModalPrimitives from "@radix-ui/react-dialog"
|
||||
import * as React from "react"
|
||||
|
||||
import { IconButton } from "@/components/icon-button"
|
||||
import { Kbd } from "@/components/kbd"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const FocusModalRoot = (
|
||||
props: React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Root>
|
||||
) => {
|
||||
return <FocusModalPrimitives.Root {...props} />
|
||||
}
|
||||
FocusModalRoot.displayName = "FocusModal"
|
||||
|
||||
const FocusModalTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof FocusModalPrimitives.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Trigger>
|
||||
>((props, ref) => {
|
||||
return <FocusModalPrimitives.Trigger ref={ref} {...props} />
|
||||
})
|
||||
FocusModalTrigger.displayName = "FocusModal.Trigger"
|
||||
|
||||
const FocusModalPortal = ({
|
||||
className,
|
||||
...props
|
||||
}: FocusModalPrimitives.DialogPortalProps) => {
|
||||
return (
|
||||
<FocusModalPrimitives.DialogPortal className={clx(className)} {...props} />
|
||||
)
|
||||
}
|
||||
FocusModalPortal.displayName = "FocusModal.Portal"
|
||||
|
||||
const FocusModalOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof FocusModalPrimitives.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Overlay>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<FocusModalPrimitives.Overlay
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-overlay fixed inset-0",
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FocusModalOverlay.displayName = "FocusModal.Overlay"
|
||||
|
||||
const FocusModalContent = React.forwardRef<
|
||||
React.ElementRef<typeof FocusModalPrimitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<FocusModalPortal>
|
||||
<FocusModalOverlay />
|
||||
<FocusModalPrimitives.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base shadow-elevation-modal fixed inset-2 flex flex-col overflow-hidden rounded-lg border focus:outline-none",
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 duration-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</FocusModalPortal>
|
||||
)
|
||||
})
|
||||
FocusModalContent.displayName = "FocusModal.Content"
|
||||
|
||||
const FocusModalHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"border-ui-border-base flex items-center justify-between gap-x-4 border-b px-4 py-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<FocusModalPrimitives.Close asChild>
|
||||
<IconButton variant="transparent">
|
||||
<XMark />
|
||||
</IconButton>
|
||||
</FocusModalPrimitives.Close>
|
||||
<Kbd>esc</Kbd>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
FocusModalHeader.displayName = "FocusModal.Header"
|
||||
|
||||
const FocusModalBody = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return <div ref={ref} className={clx("flex-1", className)} {...props} />
|
||||
})
|
||||
FocusModalBody.displayName = "FocusModal.Body"
|
||||
|
||||
const FocusModal = Object.assign(FocusModalRoot, {
|
||||
Trigger: FocusModalTrigger,
|
||||
Content: FocusModalContent,
|
||||
Header: FocusModalHeader,
|
||||
Body: FocusModalBody,
|
||||
})
|
||||
|
||||
export { FocusModal }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./focus-modal"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Heading } from "./heading"
|
||||
|
||||
describe("Heading", () => {
|
||||
it("should render a h1 successfully", async () => {
|
||||
render(<Heading>Header</Heading>)
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render a h2 successfully", async () => {
|
||||
render(<Heading level="h2">Header</Heading>)
|
||||
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render a h3 successfully", async () => {
|
||||
render(<Heading level="h3">Header</Heading>)
|
||||
expect(screen.getByRole("heading", { level: 3 })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Heading } from "./heading"
|
||||
|
||||
const meta: Meta<typeof Heading> = {
|
||||
title: "Components/Heading",
|
||||
component: Heading,
|
||||
argTypes: {
|
||||
level: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: ["h1", "h2", "h3"],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Heading>
|
||||
|
||||
export const H1: Story = {
|
||||
args: {
|
||||
level: "h1",
|
||||
children: "I am a H1 heading",
|
||||
},
|
||||
}
|
||||
|
||||
export const H2: Story = {
|
||||
args: {
|
||||
level: "h2",
|
||||
children: "I am a H2 heading",
|
||||
},
|
||||
}
|
||||
|
||||
export const H3: Story = {
|
||||
args: {
|
||||
level: "h3",
|
||||
children: "I am a H3 heading",
|
||||
},
|
||||
}
|
||||
33
packages/design-system/ui/src/components/heading/heading.tsx
Normal file
33
packages/design-system/ui/src/components/heading/heading.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const headingVariants = cva("font-sans font-medium", {
|
||||
variants: {
|
||||
level: {
|
||||
h1: "h1-core",
|
||||
h2: "h2-core",
|
||||
h3: "h3-core",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
level: "h1",
|
||||
},
|
||||
})
|
||||
|
||||
type HeadingProps = VariantProps<typeof headingVariants> &
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
const Heading = ({ level, className, ...props }: HeadingProps) => {
|
||||
const Component = level ? level : "h1"
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={clx(headingVariants({ level }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Heading, headingVariants }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./heading"
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import { Hint } from "./hint"
|
||||
|
||||
const meta: Meta<typeof Hint> = {
|
||||
title: "Components/Hint",
|
||||
component: Hint,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Hint>
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
children: "This is a hint text to help user.",
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
variant: "error",
|
||||
children: "This is a hint text to help user.",
|
||||
},
|
||||
}
|
||||
41
packages/design-system/ui/src/components/hint/hint.tsx
Normal file
41
packages/design-system/ui/src/components/hint/hint.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { ExclamationCircleSolid } from "@medusajs/icons"
|
||||
import { clx } from "../../utils/clx"
|
||||
|
||||
const hintVariants = cva(
|
||||
"txt-compact-xsmall inline-flex items-center gap-x-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
info: "text-ui-fg-subtle",
|
||||
error: "text-ui-fg-error",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "info",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type HintProps = VariantProps<typeof hintVariants> &
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
|
||||
const Hint = React.forwardRef<HTMLSpanElement, HintProps>(
|
||||
({ className, variant = "info", children, ...props }, ref) => {
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={clx(hintVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{variant === "error" && <ExclamationCircleSolid />}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
)
|
||||
Hint.displayName = "Hint"
|
||||
|
||||
export { Hint }
|
||||
1
packages/design-system/ui/src/components/hint/index.ts
Normal file
1
packages/design-system/ui/src/components/hint/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./hint"
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { BuildingTax } from "@medusajs/icons"
|
||||
import { IconBadge } from "./icon-badge"
|
||||
|
||||
const meta: Meta<typeof IconBadge> = {
|
||||
title: "Components/IconBadge",
|
||||
component: IconBadge,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof IconBadge>
|
||||
|
||||
export const GreyBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "grey",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const GreyLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "grey",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const BlueBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "blue",
|
||||
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const BlueLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "blue",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const GreenBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "green",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const GreenLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "green",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const RedBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "red",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const RedLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "red",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const OrangeBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "orange",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const OrangeLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "orange",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const PurpleBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "purple",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const PurpleLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "purple",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { badgeColorVariants } from "@/components/badge"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const iconBadgeVariants = cva(
|
||||
"flex items-center justify-center overflow-hidden rounded-md border",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "h-6 w-6",
|
||||
large: "h-7 w-7",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface IconBadgeProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<"span">, "color">,
|
||||
VariantProps<typeof badgeColorVariants>,
|
||||
VariantProps<typeof iconBadgeVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const IconBadge = React.forwardRef<HTMLSpanElement, IconBadgeProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
color = "grey",
|
||||
size = "base",
|
||||
asChild = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Component = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={clx(
|
||||
badgeColorVariants({ color }),
|
||||
iconBadgeVariants({ size }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
)
|
||||
IconBadge.displayName = "IconBadge"
|
||||
|
||||
export { IconBadge }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./icon-badge"
|
||||
@@ -0,0 +1,30 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Plus } from "@medusajs/icons"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
describe("IconButton", () => {
|
||||
it("renders a IconButton", () => {
|
||||
render(
|
||||
<IconButton>
|
||||
<Plus />
|
||||
</IconButton>
|
||||
)
|
||||
const button = screen.getByRole("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders a button as a link", () => {
|
||||
render(
|
||||
<IconButton asChild>
|
||||
<a href="https://www.medusajs.com">
|
||||
<Plus />
|
||||
</a>
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
const button = screen.getByRole("link")
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Plus } from "@medusajs/icons"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: "Components/IconButton",
|
||||
component: IconButton,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof IconButton>
|
||||
|
||||
export const BasePrimary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const BaseTransparent: Story = {
|
||||
args: {
|
||||
variant: "transparent",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const LargePrimary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "large",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const LargeTransparent: Story = {
|
||||
args: {
|
||||
variant: "transparent",
|
||||
size: "large",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const XLargePrimary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "xlarge",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const XLargeTransparent: Story = {
|
||||
args: {
|
||||
variant: "transparent",
|
||||
size: "xlarge",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const IsLoading: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
isLoading: true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Spinner } from "@medusajs/icons"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const iconButtonVariants = cva(
|
||||
clx(
|
||||
"transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none",
|
||||
"disabled:bg-ui-bg-disabled disabled:shadow-buttons-neutral disabled:text-ui-fg-disabled disabled:after:hidden"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: clx(
|
||||
"shadow-buttons-neutral text-ui-fg-subtle bg-ui-button-neutral after:button-neutral-gradient",
|
||||
"hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient",
|
||||
"active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient",
|
||||
"focus:shadow-buttons-neutral-focus",
|
||||
"after:absolute after:inset-0 after:content-['']"
|
||||
),
|
||||
transparent: clx(
|
||||
"text-ui-fg-subtle bg-ui-button-transparent",
|
||||
"hover:bg-ui-button-transparent-hover",
|
||||
"active:bg-ui-button-transparent-pressed",
|
||||
"focus:shadow-buttons-neutral-focus focus:bg-ui-bg-base",
|
||||
"disabled:!bg-transparent disabled:!shadow-none"
|
||||
),
|
||||
},
|
||||
size: {
|
||||
base: "h-8 w-8 p-1.5",
|
||||
large: "h-10 w-10 p-2.5",
|
||||
xlarge: "h-12 w-12 p-3.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface IconButtonProps
|
||||
extends React.ComponentPropsWithoutRef<"button">,
|
||||
VariantProps<typeof iconButtonVariants> {
|
||||
asChild?: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{
|
||||
variant = "primary",
|
||||
size = "base",
|
||||
asChild = false,
|
||||
className,
|
||||
children,
|
||||
isLoading = false,
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Component = asChild ? Slot : "button"
|
||||
|
||||
/**
|
||||
* In the case of a button where asChild is true, and isLoading is true, we ensure that
|
||||
* only on element is passed as a child to the Slot component. This is because the Slot
|
||||
* component only accepts a single child.
|
||||
*/
|
||||
const renderInner = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<span className="pointer-events-none">
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-disabled absolute inset-0 flex items-center justify-center rounded-md"
|
||||
)}
|
||||
>
|
||||
<Spinner className="animate-spin" />
|
||||
</div>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clx(iconButtonVariants({ variant, size }), className)}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{renderInner()}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
)
|
||||
IconButton.displayName = "IconButton"
|
||||
|
||||
export { IconButton, iconButtonVariants }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./icon-button"
|
||||
1
packages/design-system/ui/src/components/input/index.ts
Normal file
1
packages/design-system/ui/src/components/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./input"
|
||||
@@ -0,0 +1,11 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Input } from "./input"
|
||||
|
||||
describe("Input", () => {
|
||||
it("should render the component", () => {
|
||||
render(<Input />)
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Input } from "./input"
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "Components/Input",
|
||||
component: Input,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Input>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: "Placeholder",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
value: "Floyd Mayweather",
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Invalid: Story = {
|
||||
args: {
|
||||
placeholder: "Placeholder",
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Password: Story = {
|
||||
args: {
|
||||
type: "password",
|
||||
},
|
||||
}
|
||||
|
||||
export const Search: Story = {
|
||||
args: {
|
||||
type: "search",
|
||||
placeholder: "Search",
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
placeholder: "Placeholder",
|
||||
},
|
||||
}
|
||||
104
packages/design-system/ui/src/components/input/input.tsx
Normal file
104
packages/design-system/ui/src/components/input/input.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client"
|
||||
|
||||
import { Eye, EyeSlash, MagnifyingGlassMini } from "@medusajs/icons"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const inputBaseStyles = clx(
|
||||
"caret-ui-fg-base bg-ui-bg-field hover:bg-ui-bg-field-hover shadow-borders-base placeholder-ui-fg-muted text-ui-fg-base transition-fg relative w-full appearance-none rounded-md outline-none",
|
||||
"focus:shadow-borders-interactive-with-active",
|
||||
"disabled:text-ui-fg-disabled disabled:!bg-ui-bg-disabled disabled:placeholder-ui-fg-disabled disabled:cursor-not-allowed",
|
||||
"aria-[invalid=true]:!shadow-borders-error invalid:!shadow-borders-error"
|
||||
)
|
||||
|
||||
const inputVariants = cva(
|
||||
clx(
|
||||
inputBaseStyles,
|
||||
"[&::--webkit-search-cancel-button]:hidden [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "txt-compact-medium h-10 px-3 py-[9px]",
|
||||
small: "txt-compact-small h-8 px-2 py-[5px]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Input = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
VariantProps<typeof inputVariants> &
|
||||
Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">
|
||||
>(({ className, type, size = "base", ...props }, ref) => {
|
||||
const [typeState, setTypeState] = React.useState(type)
|
||||
|
||||
const isPassword = type === "password"
|
||||
const isSearch = type === "search"
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
ref={ref}
|
||||
type={isPassword ? typeState : type}
|
||||
className={clx(
|
||||
inputVariants({ size: size }),
|
||||
{
|
||||
"pr-11": isPassword && size === "base",
|
||||
"pl-11": isSearch && size === "base",
|
||||
"pr-9": isPassword && size === "small",
|
||||
"pl-9": isSearch && size === "small",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{isSearch && (
|
||||
<div
|
||||
className={clx(
|
||||
"text-ui-fg-muted absolute bottom-0 left-0 flex items-center justify-center",
|
||||
{
|
||||
"h-10 w-11": size === "base",
|
||||
"h-8 w-9": size === "small",
|
||||
}
|
||||
)}
|
||||
role="img"
|
||||
>
|
||||
<MagnifyingGlassMini />
|
||||
</div>
|
||||
)}
|
||||
{isPassword && (
|
||||
<div
|
||||
className={clx(
|
||||
"absolute bottom-0 right-0 flex w-11 items-center justify-center",
|
||||
{
|
||||
"h-10 w-11": size === "base",
|
||||
"h-8 w-9": size === "small",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="text-ui-fg-muted hover:text-ui-fg-base focus:text-ui-fg-base focus:shadow-borders-interactive-w-focus active:text-ui-fg-base h-fit w-fit rounded-sm outline-none transition-all"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTypeState(typeState === "password" ? "text" : "password")
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{typeState === "password" ? "Show password" : "Hide password"}
|
||||
</span>
|
||||
{typeState === "password" ? <Eye /> : <EyeSlash />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input, inputBaseStyles }
|
||||
1
packages/design-system/ui/src/components/kbd/index.ts
Normal file
1
packages/design-system/ui/src/components/kbd/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./kbd"
|
||||
21
packages/design-system/ui/src/components/kbd/kbd.stories.tsx
Normal file
21
packages/design-system/ui/src/components/kbd/kbd.stories.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Kbd } from "./kbd"
|
||||
|
||||
const meta: Meta<typeof Kbd> = {
|
||||
title: "Components/Kbd",
|
||||
component: Kbd,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Kbd>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "⌘",
|
||||
},
|
||||
}
|
||||
25
packages/design-system/ui/src/components/kbd/kbd.tsx
Normal file
25
packages/design-system/ui/src/components/kbd/kbd.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Kbd = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"kbd">
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<kbd
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-tag-neutral-bg text-ui-tag-neutral-text border-ui-tag-neutral-border inline-flex h-5 w-fit min-w-[20px] items-center justify-center rounded-md border px-1",
|
||||
"txt-compact-xsmall-plus",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</kbd>
|
||||
)
|
||||
})
|
||||
Kbd.displayName = "Kbd"
|
||||
|
||||
export { Kbd }
|
||||
1
packages/design-system/ui/src/components/label/index.ts
Normal file
1
packages/design-system/ui/src/components/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./label"
|
||||
@@ -0,0 +1,12 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Label } from "./label"
|
||||
|
||||
test("renders a label", () => {
|
||||
render(<Label>I am a label</Label>)
|
||||
|
||||
const text = screen.getByText("I am a label")
|
||||
expect(text).toBeInTheDocument()
|
||||
expect(text.tagName).toBe("LABEL")
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Label } from "./label"
|
||||
|
||||
const meta: Meta<typeof Label> = {
|
||||
title: "Components/Label",
|
||||
component: Label,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: ["small", "xsmall", "base", "large"],
|
||||
},
|
||||
weight: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: ["regular", "plus"],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Label>
|
||||
|
||||
export const BaseRegular: Story = {
|
||||
args: {
|
||||
size: "base",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const BasePlus: Story = {
|
||||
args: {
|
||||
size: "base",
|
||||
weight: "plus",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const LargeRegular: Story = {
|
||||
args: {
|
||||
size: "large",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const LargePlus: Story = {
|
||||
args: {
|
||||
size: "large",
|
||||
weight: "plus",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallRegular: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallPlus: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
weight: "plus",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const XSmallRegular: Story = {
|
||||
args: {
|
||||
size: "xsmall",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
45
packages/design-system/ui/src/components/label/label.tsx
Normal file
45
packages/design-system/ui/src/components/label/label.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import * as Primitives from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const labelVariants = cva("font-sans", {
|
||||
variants: {
|
||||
size: {
|
||||
xsmall: "txt-compact-xsmall",
|
||||
small: "txt-compact-small",
|
||||
base: "txt-compact-medium",
|
||||
large: "txt-compact-large",
|
||||
},
|
||||
weight: {
|
||||
regular: "font-normal",
|
||||
plus: "font-medium",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
weight: "regular",
|
||||
},
|
||||
})
|
||||
|
||||
interface LabelProps
|
||||
extends React.ComponentPropsWithoutRef<"label">,
|
||||
VariantProps<typeof labelVariants> {}
|
||||
|
||||
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, size = "base", weight = "regular", ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Root
|
||||
ref={ref}
|
||||
className={clx(labelVariants({ size, weight }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Label.displayName = "Label"
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./popover"
|
||||
95
packages/design-system/ui/src/components/popover/popover.tsx
Normal file
95
packages/design-system/ui/src/components/popover/popover.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as Primitives from "@radix-ui/react-popover"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Root = (
|
||||
props: React.ComponentPropsWithoutRef<typeof Primitives.Root>
|
||||
) => {
|
||||
return <Primitives.Root {...props} />
|
||||
}
|
||||
Root.displayName = "Popover"
|
||||
|
||||
const Trigger = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Trigger>
|
||||
>((props, ref) => {
|
||||
return <Primitives.Trigger ref={ref} {...props} />
|
||||
})
|
||||
Trigger.displayName = "Popover.Trigger"
|
||||
|
||||
const Anchor = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Anchor>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Anchor>
|
||||
>((props, ref) => {
|
||||
return <Primitives.Anchor ref={ref} {...props} />
|
||||
})
|
||||
Anchor.displayName = "Popover.Anchor"
|
||||
|
||||
const Close = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Close>
|
||||
>((props, ref) => {
|
||||
return <Primitives.Close ref={ref} {...props} />
|
||||
})
|
||||
Close.displayName = "Popover.Close"
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Content>
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
sideOffset = 8,
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
collisionPadding,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Primitives.Portal>
|
||||
<Primitives.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
side={side}
|
||||
align={align}
|
||||
collisionPadding={collisionPadding}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] min-w-[220px] overflow-hidden rounded-lg p-1",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Primitives.Portal>
|
||||
)
|
||||
}
|
||||
)
|
||||
Content.displayName = "Popover.Content"
|
||||
|
||||
const Seperator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx("bg-ui-border-base -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Seperator.displayName = "Popover.Seperator"
|
||||
|
||||
const Popover = Object.assign(Root, {
|
||||
Trigger,
|
||||
Anchor,
|
||||
Close,
|
||||
Content,
|
||||
Seperator,
|
||||
})
|
||||
|
||||
export { Popover }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./progress-accordion"
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Container } from "@/components/container"
|
||||
import { ProgressAccordion } from "./progress-accordion"
|
||||
|
||||
const meta: Meta<typeof ProgressAccordion> = {
|
||||
title: "Components/ProgressAccordion",
|
||||
component: ProgressAccordion,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof ProgressAccordion>
|
||||
|
||||
const AccordionDemo = () => {
|
||||
const [value, setValue] = React.useState(["1"])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Container className="p-0">
|
||||
<ProgressAccordion
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
type="multiple"
|
||||
className="w-full"
|
||||
>
|
||||
<ProgressAccordion.Item value="1">
|
||||
<ProgressAccordion.Header>Trigger 1</ProgressAccordion.Header>
|
||||
<ProgressAccordion.Content>
|
||||
<div className="pb-6">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. A
|
||||
recusandae officiis aliquam quia, natus saepe obcaecati eligendi
|
||||
non animi fuga culpa, cum unde consequuntur architecto quos
|
||||
reiciendis deleniti eos iste!
|
||||
</div>
|
||||
</ProgressAccordion.Content>
|
||||
</ProgressAccordion.Item>
|
||||
<ProgressAccordion.Item value="2">
|
||||
<ProgressAccordion.Header>Trigger 2</ProgressAccordion.Header>
|
||||
<ProgressAccordion.Content>
|
||||
<div className="pb-6">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. A
|
||||
recusandae officiis aliquam quia, natus saepe obcaecati eligendi
|
||||
non animi fuga culpa, cum unde consequuntur architecto quos
|
||||
reiciendis deleniti eos iste!
|
||||
</div>
|
||||
</ProgressAccordion.Content>
|
||||
</ProgressAccordion.Item>
|
||||
</ProgressAccordion>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return <AccordionDemo />
|
||||
},
|
||||
args: {},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CheckCircleSolid,
|
||||
CircleDottedLine,
|
||||
CircleHalfSolid,
|
||||
Plus,
|
||||
} from "@medusajs/icons"
|
||||
import * as Primitves from "@radix-ui/react-accordion"
|
||||
import * as React from "react"
|
||||
|
||||
import { ProgressStatus } from "@/types"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { IconButton } from "../icon-button"
|
||||
|
||||
const Root = (props: React.ComponentPropsWithoutRef<typeof Primitves.Root>) => {
|
||||
return <Primitves.Root {...props} />
|
||||
}
|
||||
Root.displayName = "ProgressAccordion"
|
||||
|
||||
const Item = React.forwardRef<
|
||||
React.ElementRef<typeof Primitves.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitves.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitves.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"border-ui-border-base border-b last-of-type:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Item.displayName = "ProgressAccordion.Item"
|
||||
|
||||
interface HeaderProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Primitves.Header> {
|
||||
status?: ProgressStatus
|
||||
}
|
||||
|
||||
interface StatusIndicatorProps extends React.ComponentPropsWithoutRef<"span"> {
|
||||
status: ProgressStatus
|
||||
}
|
||||
|
||||
const ProgressIndicator = ({ status, ...props }: StatusIndicatorProps) => {
|
||||
const Icon = React.useMemo(() => {
|
||||
switch (status) {
|
||||
case "not-started":
|
||||
return CircleDottedLine
|
||||
case "in-progress":
|
||||
return CircleHalfSolid
|
||||
case "completed":
|
||||
return CheckCircleSolid
|
||||
default:
|
||||
return CircleDottedLine
|
||||
}
|
||||
}, [status])
|
||||
|
||||
return (
|
||||
<span
|
||||
className="text-ui-fg-muted group-data-[state=open]:text-ui-fg-interactive flex h-12 w-12 items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = React.forwardRef<
|
||||
React.ElementRef<typeof Primitves.Header>,
|
||||
HeaderProps
|
||||
>(({ className, status = "not-started", children, ...props }, ref) => {
|
||||
return (
|
||||
<Primitves.Header
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"h3-core text-ui-fg-base group flex w-full flex-1 items-center gap-4 px-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressIndicator status={status} />
|
||||
{children}
|
||||
<Primitves.Trigger asChild className="ml-auto">
|
||||
<IconButton variant="transparent">
|
||||
<Plus className="transform transition-transform group-data-[state=open]:rotate-45" />
|
||||
</IconButton>
|
||||
</Primitves.Trigger>
|
||||
</Primitves.Header>
|
||||
)
|
||||
})
|
||||
Header.displayName = "ProgressAccordion.Header"
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof Primitves.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitves.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitves.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"overflow-hidden",
|
||||
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down pl-24 pr-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Content.displayName = "ProgressAccordion.Content"
|
||||
|
||||
const ProgressAccordion = Object.assign(Root, {
|
||||
Item,
|
||||
Header,
|
||||
Content,
|
||||
})
|
||||
|
||||
export { ProgressAccordion }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./progress-tabs"
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Container } from "../container"
|
||||
import { ProgressTabs } from "./progress-tabs"
|
||||
|
||||
const meta: Meta<typeof ProgressTabs> = {
|
||||
title: "Components/ProgressTabs",
|
||||
component: ProgressTabs,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof ProgressTabs>
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<div className="h-screen max-h-[500px] w-screen max-w-[700px] overflow-hidden p-4">
|
||||
<Container className="h-full w-full overflow-hidden p-0">
|
||||
<ProgressTabs defaultValue="tab1">
|
||||
<ProgressTabs.List>
|
||||
<ProgressTabs.Trigger value="tab1">Tab 1</ProgressTabs.Trigger>
|
||||
<ProgressTabs.Trigger value="tab2">Tab 2</ProgressTabs.Trigger>
|
||||
<ProgressTabs.Trigger value="tab3" disabled>
|
||||
Tab 3
|
||||
</ProgressTabs.Trigger>
|
||||
</ProgressTabs.List>
|
||||
<div className="txt-compact-medium text-ui-fg-base border-ui-border-base h-full border-t p-3">
|
||||
<ProgressTabs.Content value="tab1">
|
||||
Tab 1 content
|
||||
</ProgressTabs.Content>
|
||||
<ProgressTabs.Content value="tab2">
|
||||
Tab 2 content
|
||||
</ProgressTabs.Content>
|
||||
<ProgressTabs.Content value="tab3">
|
||||
Tab 3 content
|
||||
</ProgressTabs.Content>
|
||||
</div>
|
||||
</ProgressTabs>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <Demo />,
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CheckCircleSolid,
|
||||
CircleDottedLine,
|
||||
CircleHalfSolid,
|
||||
} from "@medusajs/icons"
|
||||
import * as ProgressTabsPrimitives from "@radix-ui/react-tabs"
|
||||
import * as React from "react"
|
||||
|
||||
import { ProgressStatus } from "@/types"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const ProgressTabsRoot = (props: ProgressTabsPrimitives.TabsProps) => {
|
||||
return <ProgressTabsPrimitives.Root {...props} />
|
||||
}
|
||||
ProgressTabsRoot.displayName = "ProgressTabs"
|
||||
|
||||
interface IndicatorProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<"span">, "children"> {
|
||||
status?: ProgressStatus
|
||||
}
|
||||
|
||||
const ProgressIndicator = ({ status, className, ...props }: IndicatorProps) => {
|
||||
const Icon = React.useMemo(() => {
|
||||
switch (status) {
|
||||
case "not-started":
|
||||
return CircleDottedLine
|
||||
case "in-progress":
|
||||
return CircleHalfSolid
|
||||
case "completed":
|
||||
return CheckCircleSolid
|
||||
default:
|
||||
return CircleDottedLine
|
||||
}
|
||||
}, [status])
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clx(
|
||||
"text-ui-fg-muted group-data-[state=active]/trigger:text-ui-fg-interactive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProgressTabsTriggerProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<typeof ProgressTabsPrimitives.Trigger>,
|
||||
"asChild"
|
||||
> {
|
||||
status?: ProgressStatus
|
||||
}
|
||||
|
||||
const ProgressTabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressTabsPrimitives.Trigger>,
|
||||
ProgressTabsTriggerProps
|
||||
>(({ className, children, status = "not-started", ...props }, ref) => (
|
||||
<ProgressTabsPrimitives.Trigger
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"txt-compact-small-plus transition-fg text-ui-fg-muted bg-ui-bg-subtle border-r-ui-border-base inline-flex h-14 w-full max-w-[200px] flex-1 items-center gap-x-2 border-r px-4 text-left outline-none",
|
||||
"group/trigger overflow-hidden text-ellipsis whitespace-nowrap",
|
||||
"disabled:bg-ui-bg-disabled disabled:text-ui-fg-muted",
|
||||
"hover:bg-ui-bg-subtle-hover",
|
||||
"focus:bg-ui-bg-base focus:z-[1]",
|
||||
"data-[state=active]:text-ui-fg-base data-[state=active]:bg-ui-bg-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressIndicator status={status} />
|
||||
{children}
|
||||
</ProgressTabsPrimitives.Trigger>
|
||||
))
|
||||
ProgressTabsTrigger.displayName = "ProgressTabs.Trigger"
|
||||
|
||||
const ProgressTabsList = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressTabsPrimitives.List>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressTabsPrimitives.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ProgressTabsPrimitives.List
|
||||
ref={ref}
|
||||
className={clx("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ProgressTabsList.displayName = "ProgressTabs.List"
|
||||
|
||||
const ProgressTabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressTabsPrimitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressTabsPrimitives.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<ProgressTabsPrimitives.Content
|
||||
ref={ref}
|
||||
className={clx("outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
ProgressTabsContent.displayName = "ProgressTabs.Content"
|
||||
|
||||
const ProgressTabs = Object.assign(ProgressTabsRoot, {
|
||||
Trigger: ProgressTabsTrigger,
|
||||
List: ProgressTabsList,
|
||||
Content: ProgressTabsContent,
|
||||
})
|
||||
|
||||
export { ProgressTabs }
|
||||
1
packages/design-system/ui/src/components/prompt/index.ts
Normal file
1
packages/design-system/ui/src/components/prompt/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./prompt"
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
RenderResult,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
} from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Prompt } from "./prompt"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
|
||||
const TRIGGER_TEXT = "Open"
|
||||
const TITLE_TEXT = "Delete something"
|
||||
const DESCRIPTION_TEXT = "Are you sure? This cannot be undone."
|
||||
const CANCEL_TEXT = "Cancel"
|
||||
const CONFIRM_TEXT = "Confirm"
|
||||
|
||||
describe("Prompt", () => {
|
||||
let rendered: RenderResult
|
||||
let trigger: HTMLElement
|
||||
|
||||
beforeEach(() => {
|
||||
rendered = render(
|
||||
<Prompt>
|
||||
<Prompt.Trigger asChild>
|
||||
<Button>{TRIGGER_TEXT}</Button>
|
||||
</Prompt.Trigger>
|
||||
<Prompt.Content>
|
||||
<Prompt.Header>
|
||||
<Prompt.Title>{TITLE_TEXT}</Prompt.Title>
|
||||
<Prompt.Description>{DESCRIPTION_TEXT}</Prompt.Description>
|
||||
</Prompt.Header>
|
||||
<Prompt.Footer>
|
||||
<Prompt.Cancel>{CANCEL_TEXT}</Prompt.Cancel>
|
||||
<Prompt.Action>{CONFIRM_TEXT}</Prompt.Action>
|
||||
</Prompt.Footer>
|
||||
</Prompt.Content>
|
||||
</Prompt>
|
||||
)
|
||||
|
||||
trigger = rendered.getByText(TRIGGER_TEXT)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("renders a basic alert dialog when the trigger is clicked", async () => {
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const title = await rendered.findByText(TITLE_TEXT)
|
||||
const description = await rendered.findByText(DESCRIPTION_TEXT)
|
||||
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(description).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("close the dialog when the cancel button is clicked", async () => {
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const title = rendered.queryByText(TITLE_TEXT)
|
||||
const description = rendered.queryByText(DESCRIPTION_TEXT)
|
||||
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(description).toBeInTheDocument()
|
||||
|
||||
const cancelButton = await rendered.findByText(CANCEL_TEXT)
|
||||
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(title).not.toBeInTheDocument()
|
||||
expect(description).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("close the dialog when the confirm button is clicked", async () => {
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const title = rendered.queryByText(TITLE_TEXT)
|
||||
const description = rendered.queryByText(DESCRIPTION_TEXT)
|
||||
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(description).toBeInTheDocument()
|
||||
|
||||
const confirmButton = await rendered.findByText(CONFIRM_TEXT)
|
||||
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
expect(title).not.toBeInTheDocument()
|
||||
expect(description).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Prompt } from "./prompt"
|
||||
|
||||
const meta: Meta<typeof Prompt> = {
|
||||
title: "Components/Prompt",
|
||||
component: Prompt,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Prompt>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<Prompt>
|
||||
<Prompt.Trigger asChild>
|
||||
<Button>Open</Button>
|
||||
</Prompt.Trigger>
|
||||
<Prompt.Content>
|
||||
<Prompt.Header>
|
||||
<Prompt.Title>Delete something</Prompt.Title>
|
||||
<Prompt.Description>
|
||||
Are you sure? This cannot be undone.
|
||||
</Prompt.Description>
|
||||
</Prompt.Header>
|
||||
<Prompt.Footer>
|
||||
<Prompt.Cancel>Cancel</Prompt.Cancel>
|
||||
<Prompt.Action>Delete</Prompt.Action>
|
||||
</Prompt.Footer>
|
||||
</Prompt.Content>
|
||||
</Prompt>
|
||||
)
|
||||
},
|
||||
}
|
||||
149
packages/design-system/ui/src/components/prompt/prompt.tsx
Normal file
149
packages/design-system/ui/src/components/prompt/prompt.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client"
|
||||
|
||||
import * as Primitives from "@radix-ui/react-alert-dialog"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Heading } from "@/components/heading"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Root = Primitives.AlertDialog
|
||||
Root.displayName = "Prompt.Root"
|
||||
|
||||
const Trigger = Primitives.Trigger
|
||||
Trigger.displayName = "Prompt.Trigger"
|
||||
|
||||
const Portal = ({ className, ...props }: Primitives.AlertDialogPortalProps) => {
|
||||
return <Primitives.AlertDialogPortal className={clx(className)} {...props} />
|
||||
}
|
||||
Portal.displayName = "Prompt.Portal"
|
||||
|
||||
const Overlay = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Overlay>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Overlay
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-overlay fixed inset-0",
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", // Re-enable when Admin UI has been cleaned up
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Overlay.displayName = "Prompt.Overlay"
|
||||
|
||||
const Title = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Title>,
|
||||
Omit<React.ComponentPropsWithoutRef<typeof Primitives.Title>, "asChild">
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Title ref={ref} className={clx(className)} {...props} asChild>
|
||||
<Heading level="h2" className="text-ui-fg-base">
|
||||
{children}
|
||||
</Heading>
|
||||
</Primitives.Title>
|
||||
)
|
||||
})
|
||||
Title.displayName = "Prompt.Title"
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Overlay />
|
||||
<Primitives.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base shadow-elevation-flyout fixed left-[50%] top-[50%] flex w-full max-w-[400px] translate-x-[-50%] translate-y-[-50%] flex-col rounded-lg border focus:outline-none",
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] duration-200", // Re-enable when Admin UI has been cleaned up
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
Content.displayName = "Prompt.Content"
|
||||
|
||||
const Description = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Description>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Description
|
||||
ref={ref}
|
||||
className={clx("text-ui-fg-subtle txt-compact-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Description.displayName = "Prompt.Description"
|
||||
|
||||
const Action = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Action>,
|
||||
Omit<React.ComponentPropsWithoutRef<typeof Primitives.Action>, "asChild">
|
||||
>(({ className, children, type, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Action ref={ref} className={className} {...props} asChild>
|
||||
<Button type={type} variant="danger">
|
||||
{children}
|
||||
</Button>
|
||||
</Primitives.Action>
|
||||
)
|
||||
})
|
||||
Action.displayName = "Prompt.Action"
|
||||
|
||||
const Cancel = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Cancel>,
|
||||
Omit<React.ComponentPropsWithoutRef<typeof Primitives.Cancel>, "asChild">
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Cancel ref={ref} className={clx(className)} {...props} asChild>
|
||||
<Button variant="secondary">{children}</Button>
|
||||
</Primitives.Cancel>
|
||||
)
|
||||
})
|
||||
Cancel.displayName = "Prompt.Cancel"
|
||||
|
||||
const Header = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={clx("flex flex-col gap-y-1 px-6 pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Footer = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={clx("flex items-center justify-end gap-x-2 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Prompt = Object.assign(Root, {
|
||||
Trigger,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
Action,
|
||||
Cancel,
|
||||
Header,
|
||||
Footer,
|
||||
})
|
||||
|
||||
export { Prompt }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./radio-group"
|
||||
@@ -0,0 +1,161 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Label } from "@/components/label"
|
||||
import { Text } from "@/components/text"
|
||||
import { RadioGroup } from "./radio-group"
|
||||
|
||||
const meta: Meta<typeof RadioGroup> = {
|
||||
title: "Components/RadioGroup",
|
||||
component: RadioGroup,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof RadioGroup>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<RadioGroup>
|
||||
<RadioGroup.Item value="1" />
|
||||
<RadioGroup.Item value="2" />
|
||||
<RadioGroup.Item value="3" />
|
||||
</RadioGroup>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithLabel: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<RadioGroup>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<RadioGroup.Item value="1" id="radio_1" />
|
||||
<Label htmlFor="radio_1" weight="plus">
|
||||
Radio 1
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<RadioGroup.Item value="2" id="radio_2" />
|
||||
<Label htmlFor="radio_2" weight="plus">
|
||||
Radio 2
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<RadioGroup.Item value="3" id="radio_3" />
|
||||
<Label htmlFor="radio_3" weight="plus">
|
||||
Radio 3
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithLabelAndDescription: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<RadioGroup>
|
||||
<div className="flex items-start gap-x-3">
|
||||
<RadioGroup.Item value="1" id="radio_1" />
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<Label htmlFor="radio_1" weight="plus">
|
||||
Radio 1
|
||||
</Label>
|
||||
<Text className="text-ui-fg-subtle">
|
||||
The quick brown fox jumps over a lazy dog.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-x-3">
|
||||
<RadioGroup.Item value="2" id="radio_2" />
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<Label htmlFor="radio_2" weight="plus">
|
||||
Radio 2
|
||||
</Label>
|
||||
<Text className="text-ui-fg-subtle">
|
||||
The quick brown fox jumps over a lazy dog.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-x-3">
|
||||
<RadioGroup.Item value="3" id="radio_3" />
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<Label htmlFor="radio_3" weight="plus">
|
||||
Radio 3
|
||||
</Label>
|
||||
<Text className="text-ui-fg-subtle">
|
||||
The quick brown fox jumps over a lazy dog.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<RadioGroup>
|
||||
<RadioGroup.Item value="1" disabled />
|
||||
<RadioGroup.Item value="2" />
|
||||
<RadioGroup.Item value="3" disabled checked />
|
||||
</RadioGroup>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const ChoiceBox: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<RadioGroup>
|
||||
<RadioGroup.ChoiceBox
|
||||
value="1"
|
||||
label="One"
|
||||
description="The quick brown fox jumps over a lazy dog."
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value="2"
|
||||
label="Two"
|
||||
description="The quick brown fox jumps over a lazy dog."
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value="3"
|
||||
label="Three"
|
||||
description="The quick brown fox jumps over a lazy dog."
|
||||
disabled
|
||||
/>
|
||||
</RadioGroup>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const ChoiceBoxDisabledSelected: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<RadioGroup defaultValue={"3"}>
|
||||
<RadioGroup.ChoiceBox
|
||||
value="1"
|
||||
label="One"
|
||||
description="The quick brown fox jumps over a lazy dog."
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value="2"
|
||||
label="Two"
|
||||
description="The quick brown fox jumps over a lazy dog."
|
||||
/>
|
||||
<RadioGroup.ChoiceBox
|
||||
value="3"
|
||||
label="Three"
|
||||
description="The quick brown fox jumps over a lazy dog."
|
||||
disabled
|
||||
/>
|
||||
</RadioGroup>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client"
|
||||
|
||||
import * as Primitives from "@radix-ui/react-radio-group"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
import { Hint } from "../hint"
|
||||
import { Label } from "../label"
|
||||
|
||||
const Root = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Root
|
||||
className={clx("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Root.displayName = "RadioGroup.Root"
|
||||
|
||||
const Indicator = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Indicator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Indicator
|
||||
ref={ref}
|
||||
className={clx("flex items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-base shadow-details-contrast-on-bg-interactive group-disabled:bg-ui-fg-disabled h-1.5 w-1.5 rounded-full group-disabled:shadow-none"
|
||||
)}
|
||||
/>
|
||||
</Primitives.Indicator>
|
||||
)
|
||||
})
|
||||
Indicator.displayName = "RadioGroup.Indicator"
|
||||
|
||||
const Item = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"group relative flex h-5 w-5 items-center justify-center outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"shadow-borders-base bg-ui-bg-base transition-fg flex h-[14px] w-[14px] items-center justify-center rounded-full",
|
||||
"group-hover:bg-ui-bg-base-hover",
|
||||
"group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow",
|
||||
"group-focus:!shadow-borders-interactive-with-focus",
|
||||
"group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base"
|
||||
)}
|
||||
>
|
||||
<Indicator />
|
||||
</div>
|
||||
</Primitives.Item>
|
||||
)
|
||||
})
|
||||
Item.displayName = "RadioGroup.Item"
|
||||
|
||||
interface ChoiceBoxProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Primitives.Item> {
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const ChoiceBox = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Item>,
|
||||
ChoiceBoxProps
|
||||
>(({ className, id, label, description, ...props }, ref) => {
|
||||
const generatedId = React.useId()
|
||||
|
||||
if (!id) {
|
||||
id = generatedId
|
||||
}
|
||||
|
||||
const descriptionId = `${id}-description`
|
||||
|
||||
return (
|
||||
<Primitives.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"shadow-borders-base bg-ui-bg-base focus:shadow-borders-interactive-with-focus transition-fg disabled:bg-ui-bg-disabled group flex items-start gap-x-2 rounded-lg p-3 disabled:cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
id={id}
|
||||
aria-describedby={descriptionId}
|
||||
>
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
<div className="shadow-borders-base bg-ui-bg-base group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow transition-fg group-disabled:!bg-ui-bg-disabled group-hover:bg-ui-bg-base-hover group-disabled:!shadow-borders-base flex h-3.5 w-3.5 items-center justify-center rounded-full">
|
||||
<Indicator />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start">
|
||||
<Label
|
||||
htmlFor={id}
|
||||
size="base"
|
||||
weight="plus"
|
||||
className="group-disabled:text-ui-fg-disabled cursor-pointer group-disabled:cursor-not-allowed"
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<Hint
|
||||
className="txt-compact-medium text-ui-fg-subtle group-disabled:text-ui-fg-disabled"
|
||||
id={descriptionId}
|
||||
>
|
||||
{description}
|
||||
</Hint>
|
||||
</div>
|
||||
</Primitives.Item>
|
||||
)
|
||||
})
|
||||
ChoiceBox.displayName = "RadioGroup.ChoiceBox"
|
||||
|
||||
const RadioGroup = Object.assign(Root, { Item, ChoiceBox })
|
||||
|
||||
export { RadioGroup }
|
||||
1
packages/design-system/ui/src/components/select/index.ts
Normal file
1
packages/design-system/ui/src/components/select/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./select"
|
||||
@@ -0,0 +1,219 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Select } from "./select"
|
||||
|
||||
const meta: Meta<typeof Select> = {
|
||||
title: "Components/Select",
|
||||
component: Select,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Select>
|
||||
|
||||
const data = [
|
||||
{
|
||||
label: "Shirts",
|
||||
items: [
|
||||
{
|
||||
value: "dress-shirt-striped",
|
||||
label: "Striped Dress Shirt",
|
||||
},
|
||||
{
|
||||
value: "relaxed-button-down",
|
||||
label: "Relaxed Fit Button Down",
|
||||
},
|
||||
{
|
||||
value: "slim-button-down",
|
||||
label: "Slim Fit Button Down",
|
||||
},
|
||||
{
|
||||
value: "dress-shirt-solid",
|
||||
label: "Solid Dress Shirt",
|
||||
},
|
||||
{
|
||||
value: "dress-shirt-check",
|
||||
label: "Check Dress Shirt",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "T-Shirts",
|
||||
items: [
|
||||
{
|
||||
value: "v-neck",
|
||||
label: "V-Neck",
|
||||
},
|
||||
{
|
||||
value: "crew-neck",
|
||||
label: "Crew Neck",
|
||||
},
|
||||
{
|
||||
value: "henley",
|
||||
label: "Henley",
|
||||
},
|
||||
{
|
||||
value: "polo",
|
||||
label: "Polo",
|
||||
},
|
||||
{
|
||||
value: "mock-neck",
|
||||
label: "Mock Neck",
|
||||
},
|
||||
{
|
||||
value: "turtleneck",
|
||||
label: "Turtleneck",
|
||||
},
|
||||
{
|
||||
value: "scoop-neck",
|
||||
label: "Scoop Neck",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="w-[250px]">
|
||||
<Select>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Select" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{data.map((group) => (
|
||||
<Select.Group key={group.label}>
|
||||
<Select.Label>{group.label}</Select.Label>
|
||||
{group.items.map((item) => (
|
||||
<Select.Item key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Group>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="w-[250px]">
|
||||
<Select>
|
||||
<Select.Trigger disabled={true}>
|
||||
<Select.Value placeholder="Select" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{data.map((group) => (
|
||||
<Select.Group key={group.label}>
|
||||
<Select.Label>{group.label}</Select.Label>
|
||||
{group.items.map((item) => (
|
||||
<Select.Item key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Group>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="w-[250px]">
|
||||
<Select size="small">
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Select" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{data.map((group) => (
|
||||
<Select.Group key={group.label}>
|
||||
<Select.Label>{group.label}</Select.Label>
|
||||
{group.items.map((item) => (
|
||||
<Select.Item key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Group>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// const InModalDemo = () => {
|
||||
// const [open, setOpen] = React.useState(false)
|
||||
// const prompt = usePrompt()
|
||||
|
||||
// const onClose = async () => {
|
||||
// const res = await prompt({
|
||||
// title: "Are you sure?",
|
||||
// description: "You have unsaved changes. Are you sure you want to close?",
|
||||
// confirmText: "Yes",
|
||||
// cancelText: "Cancel",
|
||||
// })
|
||||
|
||||
// if (!res) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// setOpen(false)
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <Drawer open={open} onOpenChange={setOpen}>
|
||||
// <Drawer.Trigger asChild>
|
||||
// <Button>Edit Variant</Button>
|
||||
// </Drawer.Trigger>
|
||||
// <Drawer.Content>
|
||||
// <Drawer.Header>
|
||||
// <Drawer.Title>Edit Variant</Drawer.Title>
|
||||
// </Drawer.Header>
|
||||
// <Drawer.Body>
|
||||
// <Select size="small">
|
||||
// <Select.Trigger>
|
||||
// <Select.Value placeholder="Select" />
|
||||
// </Select.Trigger>
|
||||
// <Select.Content>
|
||||
// {data.map((group) => (
|
||||
// <Select.Group key={group.label}>
|
||||
// <Select.Label>{group.label}</Select.Label>
|
||||
// {group.items.map((item) => (
|
||||
// <Select.Item key={item.value} value={item.value}>
|
||||
// {item.label}
|
||||
// </Select.Item>
|
||||
// ))}
|
||||
// </Select.Group>
|
||||
// ))}
|
||||
// </Select.Content>
|
||||
// </Select>
|
||||
// </Drawer.Body>
|
||||
// <Drawer.Footer>
|
||||
// <Button variant="secondary" onClick={onClose}>
|
||||
// Cancel
|
||||
// </Button>
|
||||
// <Button>Save</Button>
|
||||
// </Drawer.Footer>
|
||||
// </Drawer.Content>
|
||||
// </Drawer>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export const InModal: Story = {
|
||||
// render: () => {
|
||||
// return <InModalDemo />
|
||||
// },
|
||||
// }
|
||||
206
packages/design-system/ui/src/components/select/select.tsx
Normal file
206
packages/design-system/ui/src/components/select/select.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
"use client"
|
||||
|
||||
import { ChevronUpDown, EllipseMiniSolid } from "@medusajs/icons"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
interface SelectProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> {
|
||||
size?: "base" | "small"
|
||||
}
|
||||
|
||||
type SelectContextValue = {
|
||||
size: "base" | "small"
|
||||
}
|
||||
|
||||
const SelectContext = React.createContext<SelectContextValue | null>(null)
|
||||
|
||||
const useSelectContext = () => {
|
||||
const context = React.useContext(SelectContext)
|
||||
|
||||
if (context === null) {
|
||||
throw new Error("useSelectContext must be used within a SelectProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Root = ({ children, size = "base", ...props }: SelectProps) => {
|
||||
return (
|
||||
<SelectContext.Provider value={React.useMemo(() => ({ size }), [size])}>
|
||||
<SelectPrimitive.Root {...props}>{children}</SelectPrimitive.Root>
|
||||
</SelectContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const Group = SelectPrimitive.Group
|
||||
|
||||
const Value = SelectPrimitive.Value
|
||||
|
||||
const triggerVariants = cva(
|
||||
clx(
|
||||
"bg-ui-bg-field txt-compact-medium shadow-buttons-neutral transition-fg flex w-full select-none items-center justify-between rounded-md outline-none",
|
||||
"data-[placeholder]:text-ui-fg-muted text-ui-fg-base",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"focus:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
|
||||
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
|
||||
"invalid::border-ui-border-error invalid:shadow-borders-error",
|
||||
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||
"group/trigger"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "h-10 px-3 py-[9px]",
|
||||
small: "h-8 px-2 py-[5px]",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Trigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { size } = useSelectContext()
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={clx(triggerVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronUpDown className="text-ui-fg-muted group-disabled/trigger:text-ui-fg-disabled" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
})
|
||||
Trigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
sideOffset = 8,
|
||||
collisionPadding = 24,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout relative max-h-[200px] min-w-[var(--radix-select-trigger-width)] overflow-hidden rounded-lg",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
{
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1":
|
||||
position === "popper",
|
||||
},
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
sideOffset={sideOffset}
|
||||
collisionPadding={collisionPadding}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={clx(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
)
|
||||
Content.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"txt-compact-xsmall-plus text-ui-fg-subtle px-3 py-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const Item = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { size } = useSelectContext()
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"txt-compact-medium bg-ui-bg-base grid cursor-pointer grid-cols-[20px_1fr] gap-x-2 rounded-md px-3 py-2 outline-none transition-colors",
|
||||
"hover:bg-ui-bg-base-hover focus:bg-ui-bg-base-hover",
|
||||
{
|
||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "base",
|
||||
"txt-compact-small data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "small",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="flex h-5 w-5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<EllipseMiniSolid />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText className="flex-1 truncate">
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
})
|
||||
Item.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={clx("bg-ui-border-base -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
const Select = Object.assign(Root, {
|
||||
Group,
|
||||
Value,
|
||||
Trigger,
|
||||
Content,
|
||||
Label,
|
||||
Item,
|
||||
Separator,
|
||||
})
|
||||
|
||||
export { Select }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./status-badge"
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { StatusBadge } from "./status-badge"
|
||||
|
||||
const meta: Meta<typeof StatusBadge> = {
|
||||
title: "Components/StatusBadge",
|
||||
component: StatusBadge,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof StatusBadge>
|
||||
|
||||
export const Grey: Story = {
|
||||
args: {
|
||||
children: "Draft",
|
||||
color: "grey",
|
||||
},
|
||||
}
|
||||
|
||||
export const Green: Story = {
|
||||
args: {
|
||||
children: "Published",
|
||||
color: "green",
|
||||
},
|
||||
}
|
||||
|
||||
export const Red: Story = {
|
||||
args: {
|
||||
children: "Expired",
|
||||
color: "red",
|
||||
},
|
||||
}
|
||||
|
||||
export const Blue: Story = {
|
||||
args: {
|
||||
children: "Pending",
|
||||
color: "blue",
|
||||
},
|
||||
}
|
||||
|
||||
export const Orange: Story = {
|
||||
args: {
|
||||
children: "Requires Attention",
|
||||
color: "orange",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
import {
|
||||
EllipseBlueSolid,
|
||||
EllipseGreenSolid,
|
||||
EllipseGreySolid,
|
||||
EllipseOrangeSolid,
|
||||
EllipsePurpleSolid,
|
||||
EllipseRedSolid,
|
||||
} from "@medusajs/icons"
|
||||
|
||||
interface StatusBadgeProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<"span">, "color"> {
|
||||
color?: "green" | "red" | "blue" | "orange" | "grey" | "purple"
|
||||
}
|
||||
|
||||
const StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(
|
||||
({ children, className, color = "grey", ...props }, ref) => {
|
||||
const StatusIndicator = {
|
||||
green: EllipseGreenSolid,
|
||||
red: EllipseRedSolid,
|
||||
orange: EllipseOrangeSolid,
|
||||
blue: EllipseBlueSolid,
|
||||
purple: EllipsePurpleSolid,
|
||||
grey: EllipseGreySolid,
|
||||
}[color]
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base border-ui-border-base txt-compact-small text-ui-fg-base inline-flex items-center justify-center rounded-full border py-1 pl-1 pr-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<StatusIndicator className="mr-0.5" />
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
)
|
||||
StatusBadge.displayName = "StatusBadge"
|
||||
|
||||
export { StatusBadge }
|
||||
1
packages/design-system/ui/src/components/switch/index.ts
Normal file
1
packages/design-system/ui/src/components/switch/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./switch"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user