chore(ui,icons,ui-preset,toolbox): Move design system packages to monorepo (#5470)

This commit is contained in:
Kasper Fabricius Kristensen
2023-11-07 22:17:44 +01:00
committed by GitHub
parent 71853eafdd
commit e4ce2f4e07
722 changed files with 30300 additions and 186 deletions

View File

@@ -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",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./avatar"

View File

@@ -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()
})
})

View File

@@ -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",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./badge"

View File

@@ -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()
})
})

View File

@@ -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",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./button"

View File

@@ -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,
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./calendar"

View File

@@ -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()
})
})

View File

@@ -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",
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./checkbox"

View File

@@ -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>
)
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./code-block"

View File

@@ -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",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./code"

View File

@@ -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 />,
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./command-bar"

View File

@@ -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>
)
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./command"

View File

@@ -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",
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./container"

View 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()
})
})

View File

@@ -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>,
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./copy"

View File

@@ -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",
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./currency-input"

View File

@@ -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()
})
})
})

View File

@@ -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 />,
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./date-picker"

View File

@@ -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>
)
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./drawer"

View File

@@ -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 />
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./dropdown-menu"

View File

@@ -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>
)
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./focus-modal"

View File

@@ -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()
})
})

View File

@@ -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",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./heading"

View File

@@ -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.",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./hint"

View File

@@ -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",
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./icon-badge"

View File

@@ -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()
})
})

View File

@@ -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,
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./icon-button"

View File

@@ -0,0 +1 @@
export * from "./input"

View File

@@ -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()
})
})

View File

@@ -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",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./kbd"

View 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: "⌘",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./label"

View File

@@ -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")
})

View File

@@ -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",
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./popover"

View 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 }

View File

@@ -0,0 +1 @@
export * from "./progress-accordion"

View File

@@ -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: {},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./progress-tabs"

View File

@@ -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 />,
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./prompt"

View File

@@ -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()
})
})

View File

@@ -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>
)
},
}

View 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 }

View File

@@ -0,0 +1 @@
export * from "./radio-group"

View File

@@ -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>
)
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./select"

View File

@@ -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 />
// },
// }

View 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 }

View File

@@ -0,0 +1 @@
export * from "./status-badge"

View File

@@ -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",
},
}

View File

@@ -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 }

View File

@@ -0,0 +1 @@
export * from "./switch"

Some files were not shown because too many files have changed in this diff Show More