chore(ui,icons,ui-preset,toolbox): Move design system packages to monorepo (#5470)
This commit is contained in:
committed by
GitHub
parent
71853eafdd
commit
e4ce2f4e07
@@ -0,0 +1,3 @@
|
||||
node_modules/*
|
||||
**/dist
|
||||
**/build
|
||||
@@ -0,0 +1 @@
|
||||
.vercel
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite"
|
||||
import { mergeConfig } from "vite"
|
||||
import turbosnap from "vite-plugin-turbosnap"
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-styling",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
async viteFinal(config, { configType }) {
|
||||
return mergeConfig(config, {
|
||||
plugins:
|
||||
configType === "PRODUCTION"
|
||||
? [turbosnap({ rootDir: config.root ?? process.cwd() })]
|
||||
: [],
|
||||
})
|
||||
},
|
||||
}
|
||||
export default config
|
||||
@@ -0,0 +1,29 @@
|
||||
import { withThemeByDataAttribute } from "@storybook/addon-styling"
|
||||
import type { Preview } from "@storybook/react"
|
||||
|
||||
import "../src/main.css"
|
||||
|
||||
export const decorators = [
|
||||
withThemeByDataAttribute({
|
||||
themes: {
|
||||
Light: "light",
|
||||
Dark: "dark",
|
||||
},
|
||||
defaultTheme: "light",
|
||||
attributeName: "data-mode",
|
||||
}),
|
||||
]
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default preview
|
||||
@@ -0,0 +1,115 @@
|
||||
# @medusajs/ui
|
||||
|
||||
## 2.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 7f58964: fix(ui): 2.2.0
|
||||
|
||||
# Changelog
|
||||
|
||||
## `@medusajs/ui`
|
||||
|
||||
This minor release contains a few bug fixes and improvements, as well as a new primitive component.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixes an issue that was preventing the onChange event from firing for the `DatePicker` component when `showTimePicker` was false.
|
||||
- Fixes an issue where the `DatePicker` component would fire the onChange event when clicking outside of the component. It now only fires the event when the "Apply" button is clicked.
|
||||
|
||||
### New Components
|
||||
|
||||
- Adds a new `Popover` component. This component is a primitive component that can be used to create popovers. It shares much of the same styling as the `DropdownMenu` component, and can be used as a replacement when building highly customized dropdowns where the `DropdownMenu` component is not flexible enough.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 035fa72: feat(ui,ui-preset): Release 2.1.0
|
||||
|
||||
## `@medusajs/ui`
|
||||
|
||||
- The styling of buttons, inputs, and the CommandBar has been adjusted to have a more consistent look and feel.
|
||||
- Fixed an issue that caused DropdownMenu.Content to overflow the viewport.
|
||||
- Fixed an issue with the DatePicker component where deleting a time segment would throw an error.
|
||||
- The Text component now accepts a `leading` prop to adjust the line height. It can be set to `normal` (default) or `compact`. This change in the API is fully backwards compatible.
|
||||
- Adds a new subcomponent to RadioGroup called RadioGroup.ChoiceBox. This component wraps the RadioGroup.Item component with a mandatory label and description.
|
||||
|
||||
## `@medusajs/ui-preset`
|
||||
|
||||
- Updated several colors, shadows, and gradient effects.
|
||||
|
||||
## `@medusajs/icons`
|
||||
|
||||
- Introduces 6 new icons: QuestionMark, SparklesMiniSolid, SparklesMini, ThumbDown, ThumbUp, and UserCircleMini.
|
||||
- There have been slight adjustments made to ArrowPathMini, EllipseBlueSolid, EllipseGreenSolid, EllipseGreySolid, EllipseOrangeSolid, EllipsePurpleSolid, and EllipseRedSolid.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [035fa72]
|
||||
- @medusajs/icons@1.1.0
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- ef98084: feat(ui,icons,ui-preset): Update to Medusa UI, including new components, icons, and preset styles.
|
||||
|
||||
# Changes in `@medusajs/ui`
|
||||
|
||||
## New components
|
||||
|
||||
- `IconButton` - A button that only contains an icon.
|
||||
- `IconBadge` - A badge that only contains an icon.
|
||||
- `StatusBadge` - A badge component specifically designed to be used for displaying statuses.
|
||||
- `Tabs` - A tab component that can be used to switch between different views.
|
||||
- `ProgressTabs` - A tab component specifically designed to be used for building multi-step tasks.
|
||||
- `ProgressAccordion` - An accordion component specifically designed to be used for building multi-step tasks.
|
||||
- `CurrencyInput` - An input component that can be used to input currency values.
|
||||
- `CommandBar` - A component that can be used to display a list of keyboard commands omn the screen.
|
||||
- `CurrencyInput` - An input component that can be used to input currency values, such as prices.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
Several components have been reorganized to streamline their API. The following components have breaking changes:
|
||||
|
||||
- Button - The `format` property has been removed. To create a Icon only button, use the new `IconButton` component.
|
||||
- Badge - The `format` property has been removed. To create a Icon only badge, use the new `IconBadge` component. The border radius of the component is now controlled using the new `rounded` property.
|
||||
- CodeBlock - The `hideLineNumbers` property has been moved to the `snippets` property. This allows users to control the visibility of line numbers on a per snippet basis.
|
||||
|
||||
## Other changes
|
||||
|
||||
- The `z-index`'s of all components have been cleaned up to to make stacking portalled components easier.
|
||||
- `Table.Pagination` has been tweaked to ensure that it displays the correct number of pages when there is no data.
|
||||
- `Calendar` has been tweaked to prevent clicking a date from submitting any forms that precede it in the DOM.
|
||||
|
||||
# Changes in `@medusajs/icons`
|
||||
|
||||
## New icons
|
||||
|
||||
- `X`
|
||||
- `AcademicCap`
|
||||
- `Figma`
|
||||
- `Photo`
|
||||
- `PuzzleSolid`
|
||||
- `Text`
|
||||
|
||||
# Changes in `@medusajs/ui-preset`
|
||||
|
||||
Minor tweaks to colors, typography, and animations.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ef98084]
|
||||
- @medusajs/icons@1.0.1
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 8d31ce6: Release of the Medusa UI design system, includes three new packages: `@medusajs/ui` a set of React components, hooks, and utils; `@medusajs/icons` a set of React icons; `@medusajs/ui-preset` a Tailwind CSS preset containing Medusa UI design tokens.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8d31ce6]
|
||||
- @medusajs/icons@1.0.0
|
||||
@@ -0,0 +1,62 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for considering contributing to Medusa! This document will outline how to submit changes to this repository and which conventions to follow. If you are ever in doubt about anything we encourage you to reach out either by submitting an issue here or reaching out [via Discord](https://discord.gg/xpCwq3Kfn8).
|
||||
|
||||
If you're contributing to our documentation, make sure to also check out the [contribution guidelines on our documentation website](https://docs.medusajs.com/contribution-guidelines).
|
||||
|
||||
### Important
|
||||
|
||||
Our core maintainers prioritize pull requests (PRs) from within our organization. External contributions are regularly triaged, but not at any fixed cadence. It varies depending on how busy the maintainers are. This is applicable to all types of PRs, so we kindly ask for your patience.
|
||||
|
||||
As this package contains components for the Medusa UI design system, we do not accept PRs for new components. If you have a suggestion for a new component, please open an issue instead and label it with `feature request`.
|
||||
|
||||
If you, as a community contributor, wish to work on more extensive features, please reach out to CODEOWNERS instead of directly submitting a PR with all the changes. This approach saves us both time, especially if the PR is not accepted (which will be the case if it does not align with our roadmap), and helps us effectively review and evaluate your contribution if it is accepted.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **You're familiar with GitHub Issues and Pull Requests**
|
||||
- **You've read the [docs](https://docs.medusajs.com).**
|
||||
- **You've setup a test project with `medusa new`**
|
||||
|
||||
## Issues before PRs
|
||||
|
||||
1. Before you start working on a change please make sure that there is an issue for what you will be working on. You can either find and [existing issue](https://github.com/medusajs/ui/issues) or [open a new issue](https://github.com/medusajs/ui/issues/new) if none exists. Doing this makes sure that others can contribute with thoughts or suggest alternatives, ultimately making sure that we only add changes that make
|
||||
|
||||
2. When you are ready to start working on a change you should first [fork the Medusa UI repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) and [branch out](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository) from the `develop` branch.
|
||||
3. Make your changes.
|
||||
4. [Open a pull request towards the develop branch in the Medusa UI repo](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). Within a couple of days a Medusa team member will review, comment and eventually approve your PR.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Branches
|
||||
|
||||
All changes should be part of a branch and submitted as a pull request - your branches should be prefixed with one of:
|
||||
|
||||
- `fix/` for bug fixes
|
||||
- `feat/` for features
|
||||
- `docs/` for documentation changes
|
||||
|
||||
### Commits
|
||||
|
||||
Strive towards keeping your commits small and isolated - this helps the reviewer understand what is going on and makes it easier to process your requests.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Once your changes are ready you must submit your branch as a pull request. Your pull request should be opened against the `develop` branch in the main Medusa UI repo.
|
||||
|
||||
In your PR's description you should follow the structure:
|
||||
|
||||
- **What** - what changes are in this PR
|
||||
- **Why** - why are these changes relevant
|
||||
- **How** - how have the changes been implemented
|
||||
- **Testing** - how has the changes been tested or how can the reviewer test the feature
|
||||
|
||||
We highly encourage that you do a self-review prior to requesting a review. To do a self review click the review button in the top right corner, go through your code and annotate your changes. This makes it easier for the reviewer to process your PR.
|
||||
|
||||
#### Merge Style
|
||||
|
||||
All pull requests are squashed and merged.
|
||||
|
||||
### Release
|
||||
|
||||
The Medusa team will regularly create releases from the develop branch.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2023 Medusajs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,52 @@
|
||||
<p align="center">
|
||||
<a href="https://www.medusajs.com">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/59018053/229103275-b5e482bb-4601-46e6-8142-244f531cebdb.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
|
||||
<img alt="Medusa logo" src="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Medusa UI
|
||||
</h1>
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://docs.medusajs.com/ui">Documentation</a> |
|
||||
<a href="https://www.medusajs.com">Website</a>
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
Medusa's admin component library.
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/medusajs/medusa/blob/develop/LICENSE">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Medusa is released under the MIT license." />
|
||||
</a>
|
||||
<a href="https://discord.gg/xpCwq3Kfn8">
|
||||
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/follow?screen_name=medusajs">
|
||||
<img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
yarn add @medusajs/medusa-ui
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```jsx
|
||||
import { Button } from "@medusajs/ui"
|
||||
|
||||
const App = () => <Button variant="primary">Hello World</Button>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find the documentation for Medusa UI [on the documentation site](https://docs.medusajs.com/ui).
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"name": "@medusajs/ui",
|
||||
"version": "2.2.0",
|
||||
"author": "Kasper Kristensen <kasper@medusajs.com>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa.git",
|
||||
"directory": "packages/design-system/ui"
|
||||
},
|
||||
"main": "./dist/cjs/index.js",
|
||||
"types": "./dist/cjs/index.d.ts",
|
||||
"module": "./dist/esm/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/**",
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn clean && yarn build:js:cjs && yarn build:js:esm",
|
||||
"build:js:cjs": "tsc --project tsconfig.cjs.json && tsc-alias -p tsconfig.cjs.json",
|
||||
"build:js:esm": "tsc --project tsconfig.esm.json && tsc-alias -p tsconfig.esm.json",
|
||||
"clean": "rimraf dist",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest --run --coverage",
|
||||
"lint": "eslint \"**/*.ts*\"",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@medusajs/ui-preset": "^1.0.2",
|
||||
"@storybook/addon-essentials": "^7.0.23",
|
||||
"@storybook/addon-interactions": "^7.0.23",
|
||||
"@storybook/addon-links": "^7.0.23",
|
||||
"@storybook/addon-styling": "^1.3.6",
|
||||
"@storybook/blocks": "^7.0.23",
|
||||
"@storybook/react": "^7.0.23",
|
||||
"@storybook/react-vite": "^7.0.23",
|
||||
"@storybook/testing-library": "^0.0.14-next.2",
|
||||
"@testing-library/dom": "^9.3.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitest/coverage-v8": "^0.32.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"chromatic": "^6.20.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"jsdom": "^22.1.0",
|
||||
"postcss": "^8.4.24",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rimraf": "^5.0.1",
|
||||
"storybook": "^7.0.23",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-turbosnap": "^1.0.2",
|
||||
"vitest": "^0.32.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/icons": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||
"@radix-ui/react-avatar": "^1.0.3",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.6",
|
||||
"@radix-ui/react-portal": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.4",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@react-aria/datepicker": "^3.5.0",
|
||||
"@react-stately/datepicker": "^3.5.0",
|
||||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"prism-react-renderer": "^2.0.6",
|
||||
"react-currency-input-field": "^3.6.11",
|
||||
"react-day-picker": "^8.8.0",
|
||||
"tailwind-merge": "^1.13.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 chrome versions",
|
||||
"last 3 firefox versions",
|
||||
"last 3 opera versions",
|
||||
"last 3 edge versions",
|
||||
"last 3 safari versions",
|
||||
"last 3 chromeandroid versions",
|
||||
"last 1 firefoxandroid versions",
|
||||
"ios >= 13.4"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import "@testing-library/jest-dom"
|
||||
import { JSDOM } from "jsdom"
|
||||
import ResizeObserver from "resize-observer-polyfill"
|
||||
|
||||
const { window } = new JSDOM()
|
||||
|
||||
window.ResizeObserver = ResizeObserver
|
||||
global.ResizeObserver = ResizeObserver
|
||||
window.Element.prototype.scrollTo = () => {
|
||||
// no-op
|
||||
}
|
||||
window.requestAnimationFrame = (cb) => setTimeout(cb, 1000 / 60)
|
||||
|
||||
Object.assign(global, { window, document: window.document })
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./avatar"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Badge } from "./badge"
|
||||
|
||||
describe("Badge", () => {
|
||||
it("should render", async () => {
|
||||
render(<Badge>Badge</Badge>)
|
||||
expect(screen.getByText("Badge")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render as child", async () => {
|
||||
render(
|
||||
<Badge asChild>
|
||||
<a href="#">Changelog</a>
|
||||
</Badge>
|
||||
)
|
||||
|
||||
expect(screen.getByRole("link")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Badge } from "./badge"
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
title: "Components/Badge",
|
||||
component: Badge,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
render: ({ children, ...args }) => (
|
||||
<Badge {...args}>{children || "Badge"}</Badge>
|
||||
),
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Badge>
|
||||
|
||||
export const Grey: Story = {
|
||||
args: {
|
||||
color: "grey",
|
||||
},
|
||||
}
|
||||
|
||||
export const Green: Story = {
|
||||
args: {
|
||||
color: "green",
|
||||
},
|
||||
}
|
||||
|
||||
export const Red: Story = {
|
||||
args: {
|
||||
color: "red",
|
||||
},
|
||||
}
|
||||
|
||||
export const Blue: Story = {
|
||||
args: {
|
||||
color: "blue",
|
||||
},
|
||||
}
|
||||
|
||||
export const Orange: Story = {
|
||||
args: {
|
||||
color: "orange",
|
||||
},
|
||||
}
|
||||
|
||||
export const Purple: Story = {
|
||||
args: {
|
||||
color: "purple",
|
||||
},
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
rounded: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const Rounded: Story = {
|
||||
args: {
|
||||
rounded: "full",
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
},
|
||||
}
|
||||
|
||||
export const Base: Story = {
|
||||
args: {
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./badge"
|
||||
@@ -0,0 +1,23 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "./button"
|
||||
|
||||
describe("Button", () => {
|
||||
it("renders a button", () => {
|
||||
render(<Button>Click me</Button>)
|
||||
const button = screen.getByRole("button", { name: "Click me" })
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders a button as a link", () => {
|
||||
render(
|
||||
<Button asChild>
|
||||
<a href="https://www.medusajs.com">Go to website</a>
|
||||
</Button>
|
||||
)
|
||||
|
||||
const button = screen.getByRole("link", { name: "Go to website" })
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { PlusMini } from "@medusajs/icons"
|
||||
import { Button } from "./button"
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: "Components/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Button>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
variant: "secondary",
|
||||
},
|
||||
}
|
||||
|
||||
export const Transparent: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
variant: "transparent",
|
||||
},
|
||||
}
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
variant: "danger",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
children: ["Action", <PlusMini key={1} />],
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
isLoading: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const XLarge: Story = {
|
||||
args: {
|
||||
children: "Action",
|
||||
size: "xlarge",
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./button"
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Text } from "@/components/text"
|
||||
import { DateRange } from "react-day-picker"
|
||||
import { Calendar } from "./calendar"
|
||||
|
||||
const Demo = ({ mode, ...args }: Parameters<typeof Calendar>[0]) => {
|
||||
const [date, setDate] = React.useState<Date | undefined>(new Date())
|
||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-y-4">
|
||||
<Calendar
|
||||
{...(args as any)}
|
||||
mode={mode as "single" | "range"}
|
||||
selected={mode === "single" ? date : dateRange}
|
||||
onSelect={mode === "single" ? setDate : setDateRange}
|
||||
/>
|
||||
|
||||
{mode === "single" && (
|
||||
<Text className="text-ui-fg-base">
|
||||
Selected Date: {date ? date.toDateString() : "None"}
|
||||
</Text>
|
||||
)}
|
||||
{mode === "range" && (
|
||||
<Text className="text-ui-fg-base">
|
||||
Selected Range:{" "}
|
||||
{dateRange
|
||||
? `${dateRange.from?.toDateString()} – ${
|
||||
dateRange.to?.toDateString() ?? ""
|
||||
}`
|
||||
: "None"}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof Calendar> = {
|
||||
title: "Components/Calendar",
|
||||
component: Calendar,
|
||||
render: Demo,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Calendar>
|
||||
|
||||
export const Single: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
},
|
||||
}
|
||||
|
||||
export const TwoMonthSingle: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
numberOfMonths: 2,
|
||||
},
|
||||
}
|
||||
|
||||
export const Range: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
},
|
||||
}
|
||||
|
||||
export const TwoMonthRange: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
numberOfMonths: 2,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client"
|
||||
|
||||
import { ChevronLeftMini, ChevronRightMini } from "@medusajs/icons"
|
||||
import * as React from "react"
|
||||
import {
|
||||
DayPicker,
|
||||
useDayRender,
|
||||
type DayPickerRangeProps,
|
||||
type DayPickerSingleProps,
|
||||
type DayProps,
|
||||
} from "react-day-picker"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
import { iconButtonVariants } from "../icon-button"
|
||||
|
||||
type OmitKeys<T, K extends keyof T> = {
|
||||
[P in keyof T as P extends K ? never : P]: T[P]
|
||||
}
|
||||
|
||||
type KeysToOmit = "showWeekNumber" | "captionLayout" | "mode"
|
||||
|
||||
type SingleProps = OmitKeys<DayPickerSingleProps, KeysToOmit>
|
||||
type RangeProps = OmitKeys<DayPickerRangeProps, KeysToOmit>
|
||||
|
||||
type CalendarProps =
|
||||
| ({
|
||||
mode: "single"
|
||||
} & SingleProps)
|
||||
| ({
|
||||
mode?: undefined
|
||||
} & SingleProps)
|
||||
| ({
|
||||
mode: "range"
|
||||
} & RangeProps)
|
||||
|
||||
const Calendar = ({
|
||||
className,
|
||||
classNames,
|
||||
mode = "single",
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) => {
|
||||
return (
|
||||
<DayPicker
|
||||
mode={mode}
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={clx(className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row",
|
||||
month: "space-y-2 p-3",
|
||||
caption: "flex justify-center relative items-center h-9",
|
||||
caption_label:
|
||||
"txt-compact-small-plus absolute bottom-0 left-0 right-0 top-1 flex items-center justify-center text-ui-fg-base",
|
||||
nav: "space-x-1 flex items-center bg-ui-bg-base-pressed rounded-md w-full h-full justify-between p-0.5",
|
||||
nav_button: clx(
|
||||
iconButtonVariants({ variant: "primary", size: "base" })
|
||||
),
|
||||
nav_button_previous: "!absolute left-0.5",
|
||||
nav_button_next: "!absolute right-0.5",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex w-full gap-x-2",
|
||||
head_cell: clx(
|
||||
"txt-compact-small-plus text-ui-fg-muted m-0 box-border flex h-8 w-8 items-center justify-center p-0"
|
||||
),
|
||||
row: "flex w-full mt-2 gap-x-2",
|
||||
cell: "txt-compact-small-plus relative rounded-md p-0 text-center focus-within:relative",
|
||||
day: "txt-compact-small-plus text-ui-fg-base bg-ui-bg-base hover:bg-ui-bg-base-hover focus:shadow-borders-interactive-with-focus h-8 w-8 rounded-md p-0 text-center outline-none transition-all",
|
||||
day_selected:
|
||||
"bg-ui-bg-interactive text-ui-fg-on-color hover:bg-ui-bg-interactive focus:bg-ui-bg-interactive",
|
||||
day_outside: "text-ui-fg-disabled aria-selected:text-ui-fg-on-color",
|
||||
day_disabled: "text-ui-fg-disabled",
|
||||
day_range_middle:
|
||||
"aria-selected:!bg-ui-bg-highlight aria-selected:!text-ui-fg-interactive",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: () => <ChevronLeftMini />,
|
||||
IconRight: () => <ChevronRightMini />,
|
||||
Day: Day,
|
||||
}}
|
||||
{...(props as SingleProps & RangeProps)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
const Day = ({ date, displayMonth }: DayProps) => {
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
const { activeModifiers, buttonProps, divProps, isButton, isHidden } =
|
||||
useDayRender(date, displayMonth, ref)
|
||||
|
||||
const { selected, today, disabled, range_middle } = activeModifiers
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selected) {
|
||||
ref.current?.focus()
|
||||
}
|
||||
}, [selected])
|
||||
|
||||
if (isHidden) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
if (!isButton) {
|
||||
return (
|
||||
<div
|
||||
{...divProps}
|
||||
className={clx("flex items-center justify-center", divProps.className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
children: buttonChildren,
|
||||
className: buttonClassName,
|
||||
...buttonPropsRest
|
||||
} = buttonProps
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...buttonPropsRest}
|
||||
type="button"
|
||||
className={clx("relative", buttonClassName)}
|
||||
>
|
||||
{buttonChildren}
|
||||
{today && (
|
||||
<span
|
||||
className={clx(
|
||||
"absolute right-[5px] top-[5px] h-1 w-1 rounded-full",
|
||||
{
|
||||
"bg-ui-fg-interactive": !selected,
|
||||
"bg-ui-fg-on-color": selected,
|
||||
"!bg-ui-fg-interactive": selected && range_middle,
|
||||
"bg-ui-fg-disabled": disabled,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./calendar"
|
||||
@@ -0,0 +1,12 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Checkbox } from "./checkbox"
|
||||
|
||||
describe("Checkbox", () => {
|
||||
it("renders a checkbox", () => {
|
||||
render(<Checkbox />)
|
||||
|
||||
expect(screen.getByRole("checkbox")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Checkbox } from "./checkbox"
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: "Components/Checkbox",
|
||||
component: Checkbox,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Checkbox>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Checked: Story = {
|
||||
args: {
|
||||
checked: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Indeterminate: Story = {
|
||||
args: {
|
||||
checked: "indeterminate",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const DisabledChecked: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
checked: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const DisabledIndeterminate: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
checked: "indeterminate",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { CheckMini, MinusMini } from "@medusajs/icons"
|
||||
import * as Primitives from "@radix-ui/react-checkbox"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Root>
|
||||
>(({ className, checked, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Root
|
||||
{...props}
|
||||
ref={ref}
|
||||
checked={checked}
|
||||
className={clx(
|
||||
"group relative inline-flex h-5 w-5 items-center justify-center outline-none ",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="text-ui-fg-on-inverted bg-ui-bg-base shadow-borders-base group-hover:bg-ui-bg-base-hover group-focus:!shadow-borders-interactive-with-focus group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow group-data-[state=indeterminate]:bg-ui-bg-interactive group-data-[state=indeterminate]:shadow-borders-interactive-with-shadow [&_path]:shadow-details-contrast-on-bg-interactive group-disabled:text-ui-fg-disabled group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base transition-fg h-[14px] w-[14px] rounded-[3px]">
|
||||
<Primitives.Indicator className="absolute inset-0">
|
||||
{checked === "indeterminate" ? <MinusMini /> : <CheckMini />}
|
||||
</Primitives.Indicator>
|
||||
</div>
|
||||
</Primitives.Root>
|
||||
)
|
||||
})
|
||||
Checkbox.displayName = "Checkbox"
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./checkbox"
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { CodeBlock } from "./code-block"
|
||||
import { Label } from "../label"
|
||||
|
||||
const meta: Meta<typeof CodeBlock> = {
|
||||
title: "Components/CodeBlock",
|
||||
component: CodeBlock,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof CodeBlock>
|
||||
|
||||
const snippets = [
|
||||
{
|
||||
label: "cURL",
|
||||
language: "markdown",
|
||||
code: `curl -H 'x-publishable-key: YOUR_API_KEY' 'http://localhost:9000/store/products/PRODUCT_ID'`,
|
||||
},
|
||||
{
|
||||
label: "Medusa JS Client",
|
||||
language: "jsx",
|
||||
code: `// Install the JS Client in your storefront project: @medusajs/medusa-js\n\nimport Medusa from "@medusajs/medusa-js"\n\nconst medusa = new Medusa({ publishableApiKey: "YOUR_API_KEY"})\nconst product = await medusa.products.retrieve("PRODUCT_ID")\nconsole.log(product.id)`,
|
||||
},
|
||||
{
|
||||
label: "Medusa React",
|
||||
language: "tsx",
|
||||
code: `// Install the React SDK and required dependencies in your storefront project:\n// medusa-react @tanstack/react-query @medusajs/medusa\n\nimport { useProduct } from "medusa-react"\n\nconst { product } = useProduct("PRODUCT_ID")\nconsole.log(product.id)`,
|
||||
},
|
||||
]
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="h-[300px] w-[700px]">
|
||||
<CodeBlock snippets={snippets}>
|
||||
<CodeBlock.Header>
|
||||
<CodeBlock.Header.Meta>
|
||||
<Label weight={"plus"}>/product-detail.js</Label>
|
||||
</CodeBlock.Header.Meta>
|
||||
</CodeBlock.Header>
|
||||
<CodeBlock.Body />
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
"use client"
|
||||
import { Highlight, themes } from "prism-react-renderer"
|
||||
import * as React from "react"
|
||||
|
||||
import { Copy } from "@/components/copy"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
export type CodeSnippet = {
|
||||
label: string
|
||||
language: string
|
||||
code: string
|
||||
hideLineNumbers?: boolean
|
||||
}
|
||||
|
||||
type CodeBlockState = {
|
||||
snippets: CodeSnippet[]
|
||||
active: CodeSnippet
|
||||
setActive: (active: CodeSnippet) => void
|
||||
} | null
|
||||
|
||||
const CodeBlockContext = React.createContext<CodeBlockState>(null)
|
||||
|
||||
const useCodeBlockContext = () => {
|
||||
const context = React.useContext(CodeBlockContext)
|
||||
|
||||
if (context === null)
|
||||
throw new Error(
|
||||
"useCodeBlockContext can only be used within a CodeBlockContext"
|
||||
)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type RootProps = {
|
||||
snippets: CodeSnippet[]
|
||||
}
|
||||
|
||||
const Root = ({
|
||||
snippets,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & RootProps) => {
|
||||
const [active, setActive] = React.useState(snippets[0])
|
||||
|
||||
return (
|
||||
<CodeBlockContext.Provider value={{ snippets, active, setActive }}>
|
||||
<div
|
||||
className={clx(
|
||||
"border-ui-code-border overflow-hidden rounded-lg border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CodeBlockContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
hideLabels?: boolean
|
||||
}
|
||||
|
||||
const HeaderComponent = ({
|
||||
children,
|
||||
className,
|
||||
hideLabels = false,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & HeaderProps) => {
|
||||
const { snippets, active, setActive } = useCodeBlockContext()
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
"border-b-ui-code-border bg-ui-code-bg-header flex items-center gap-2 border-b px-4 py-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!hideLabels &&
|
||||
snippets.map((snippet) => (
|
||||
<div
|
||||
className={clx(
|
||||
"text-ui-code-text-subtle txt-compact-small-plus cursor-pointer rounded-full border border-transparent px-3 py-2 transition-all",
|
||||
{
|
||||
"text-ui-code-text-base border-ui-code-border bg-ui-code-bg-base cursor-default":
|
||||
active.label === snippet.label,
|
||||
}
|
||||
)}
|
||||
key={snippet.label}
|
||||
onClick={() => setActive(snippet)}
|
||||
>
|
||||
{snippet.label}
|
||||
</div>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Meta = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={clx("text-ui-code-text-subtle ml-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = Object.assign(HeaderComponent, { Meta })
|
||||
|
||||
const Body = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
const { active } = useCodeBlockContext()
|
||||
return (
|
||||
<div
|
||||
className={clx("bg-ui-code-bg-base relative p-4", className)}
|
||||
{...props}
|
||||
>
|
||||
<Copy
|
||||
content={active.code}
|
||||
className="text-ui-code-icon absolute right-4 top-4"
|
||||
/>
|
||||
<div className="max-w-[90%]">
|
||||
<Highlight
|
||||
theme={{
|
||||
...themes.palenight,
|
||||
plain: {
|
||||
color: "rgba(249, 250, 251, 1)",
|
||||
backgroundColor: "#111827",
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
types: ["keyword"],
|
||||
style: {
|
||||
color: "var(--fg-on-color)",
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ["maybe-class-name"],
|
||||
style: {
|
||||
color: "rgb(255, 203, 107)",
|
||||
},
|
||||
},
|
||||
...themes.palenight.styles,
|
||||
],
|
||||
}}
|
||||
code={active.code}
|
||||
language={active.language}
|
||||
>
|
||||
{({ style, tokens, getLineProps, getTokenProps }) => (
|
||||
<pre
|
||||
className="txt-compact-small whitespace-pre-wrap bg-transparent font-mono"
|
||||
style={{
|
||||
...style,
|
||||
background: "transparent",
|
||||
}}
|
||||
>
|
||||
{tokens.map((line, i) => (
|
||||
<div key={i} {...getLineProps({ line })} className="flex">
|
||||
{!active.hideLineNumbers && (
|
||||
<span className="text-ui-code-text-subtle">{i + 1}</span>
|
||||
)}
|
||||
<div className="pl-4">
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CodeBlock = Object.assign(Root, { Body, Header, Meta })
|
||||
|
||||
export { CodeBlock }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./code-block"
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Code } from "./code"
|
||||
|
||||
const meta: Meta<typeof Code> = {
|
||||
title: "Components/Code",
|
||||
component: Code,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Code>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "yarn add -D @medusajs/ui-preset",
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./code"
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "../button"
|
||||
import { CommandBar } from "./command-bar"
|
||||
|
||||
const meta: Meta<typeof CommandBar> = {
|
||||
title: "Components/CommandBar",
|
||||
component: CommandBar,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof CommandBar>
|
||||
|
||||
const CommandBarDemo = () => {
|
||||
const [active, setActive] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center">
|
||||
<Button onClick={() => setActive(!active)}>
|
||||
{active ? "Hide" : "Show"}
|
||||
</Button>
|
||||
<CommandBar open={active}>
|
||||
<CommandBar.Bar>
|
||||
<CommandBar.Value>1 selected</CommandBar.Value>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
label="Edit"
|
||||
action={() => {
|
||||
console.log("Edit")
|
||||
}}
|
||||
shortcut="e"
|
||||
/>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
label="Delete"
|
||||
action={() => {
|
||||
console.log("Delete")
|
||||
}}
|
||||
shortcut="d"
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <CommandBarDemo />,
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client"
|
||||
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import * as Portal from "@radix-ui/react-portal"
|
||||
import * as React from "react"
|
||||
|
||||
import { Kbd } from "@/components/kbd"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
type CommandBarProps = React.PropsWithChildren<{
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
defaultOpen?: boolean
|
||||
disableAutoFocus?: boolean
|
||||
}>
|
||||
|
||||
const Root = ({
|
||||
open = false,
|
||||
onOpenChange,
|
||||
defaultOpen = false,
|
||||
disableAutoFocus = true,
|
||||
children,
|
||||
}: CommandBarProps) => {
|
||||
return (
|
||||
<Popover.Root
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
defaultOpen={defaultOpen}
|
||||
>
|
||||
<Portal.Root>
|
||||
<Popover.Anchor
|
||||
className={clx("fixed bottom-8 left-1/2 h-px w-px -translate-x-1/2")}
|
||||
/>
|
||||
</Portal.Root>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
side="top"
|
||||
sideOffset={0}
|
||||
onOpenAutoFocus={(e) => {
|
||||
if (disableAutoFocus) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
className={clx(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
Root.displayName = "CommandBar"
|
||||
|
||||
const Value = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"txt-compact-small-plus text-ui-contrast-fg-secondary px-3 py-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Value.displayName = "CommandBar.Value"
|
||||
|
||||
const Bar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-contrast-bg-base relative flex items-center overflow-hidden rounded-full px-1",
|
||||
"after:shadow-elevation-flyout after:pointer-events-none after:absolute after:inset-0 after:rounded-full after:content-['']",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Bar.displayName = "CommandBar.Bar"
|
||||
|
||||
const Seperator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
Omit<React.ComponentPropsWithoutRef<"div">, "children">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx("bg-ui-contrast-border-base h-10 w-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Seperator.displayName = "CommandBar.Seperator"
|
||||
|
||||
interface CommandProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<"button">,
|
||||
"children" | "onClick"
|
||||
> {
|
||||
action: () => void | Promise<void>
|
||||
label: string
|
||||
shortcut: string
|
||||
}
|
||||
|
||||
const Command = React.forwardRef<HTMLButtonElement, CommandProps>(
|
||||
(
|
||||
{ className, type = "button", label, action, shortcut, disabled, ...props },
|
||||
ref
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === shortcut) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
if (!disabled) {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [action, shortcut, disabled])
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-contrast-bg-base txt-compact-small-plus transition-fg text-ui-contrast-fg-primary flex items-center gap-x-2 px-3 py-2.5 outline-none",
|
||||
"focus:bg-ui-contrast-bg-highlight focus:hover:bg-ui-contrast-bg-base-hover hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus:active:bg-ui-contrast-bg-base-pressed disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||
"last-of-type:-mr-1 last-of-type:pr-4",
|
||||
className
|
||||
)}
|
||||
type={type}
|
||||
onClick={action}
|
||||
{...props}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary">
|
||||
{shortcut.toUpperCase()}
|
||||
</Kbd>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
Command.displayName = "CommandBar.Command"
|
||||
|
||||
const CommandBar = Object.assign(Root, {
|
||||
Command,
|
||||
Value,
|
||||
Bar,
|
||||
Seperator,
|
||||
})
|
||||
|
||||
export { CommandBar }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./command-bar"
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import { Command } from "./command"
|
||||
import { Badge } from "../badge"
|
||||
|
||||
const meta: Meta<typeof Command> = {
|
||||
title: "Components/Command",
|
||||
component: Command,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Command>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="w-[500px]">
|
||||
<Command>
|
||||
<Badge color="green">Get</Badge>
|
||||
<code>localhost:9000/store/products</code>
|
||||
<Command.Copy content="localhost:9000/store/products" />
|
||||
</Command>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { Copy } from "@/components/copy"
|
||||
import { clx } from "@/utils/clx"
|
||||
import React from "react"
|
||||
|
||||
const CommandComponent = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-code-bg-header border-ui-code-border flex items-center rounded-lg border px-3 py-2",
|
||||
"[&>code]:text-ui-code-text-base [&>code]:txt-compact-small [&>code]:mx-3 [&>code]:font-mono [&>code]:leading-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Command = Object.assign(CommandComponent, { Copy })
|
||||
|
||||
export { Command }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./command"
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Heading } from "@/components/heading"
|
||||
import { Text } from "@/components/text"
|
||||
import { Container } from "./container"
|
||||
|
||||
const meta: Meta<typeof Container> = {
|
||||
title: "Components/Container",
|
||||
component: Container,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Container>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: <Text>Hello World</Text>,
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export const InLayout: Story = {
|
||||
render: () => (
|
||||
<div className="flex h-screen w-screen">
|
||||
<div className="border-ui-border-base w-full max-w-[216px] border-r p-4">
|
||||
<Heading level="h3">Menubar</Heading>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-y-2 px-8 pb-8 pt-6">
|
||||
<Container>
|
||||
<Heading>Section 1</Heading>
|
||||
</Container>
|
||||
<Container>
|
||||
<Heading>Section 2</Heading>
|
||||
</Container>
|
||||
<Container>
|
||||
<Heading>Section 3</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Container = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"shadow-elevation-card-rest bg-ui-bg-base w-full rounded-lg px-8 pb-8 pt-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Container.displayName = "Container"
|
||||
|
||||
export { Container }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./container"
|
||||
@@ -0,0 +1,11 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Copy } from "./copy"
|
||||
|
||||
describe("Copy", () => {
|
||||
it("should render", () => {
|
||||
render(<Copy content="Hello world" />)
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Copy } from "./copy"
|
||||
|
||||
const meta: Meta<typeof Copy> = {
|
||||
title: "Components/Copy",
|
||||
component: Copy,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Copy>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
content: "Hello world",
|
||||
},
|
||||
}
|
||||
|
||||
export const AsChild: Story = {
|
||||
args: {
|
||||
content: "Hello world",
|
||||
asChild: true,
|
||||
children: <Button className="text-ui-fg-on-color">Copy</Button>,
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./copy"
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { CurrencyInput } from "./currency-input"
|
||||
|
||||
const meta: Meta<typeof CurrencyInput> = {
|
||||
title: "Components/CurrencyInput",
|
||||
component: CurrencyInput,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof CurrencyInput>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
symbol: "$",
|
||||
code: "usd",
|
||||
},
|
||||
}
|
||||
|
||||
export const InGrid: Story = {
|
||||
render: (args) => {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-3">
|
||||
<CurrencyInput {...args} />
|
||||
<CurrencyInput {...args} />
|
||||
<CurrencyInput {...args} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
args: {
|
||||
symbol: "$",
|
||||
code: "usd",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Primitive from "react-currency-input-field"
|
||||
|
||||
import { Text } from "@/components/text"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
|
||||
const currencyInputVariants = cva(
|
||||
clx(
|
||||
"flex items-center gap-x-1",
|
||||
"bg-ui-bg-field hover:bg-ui-bg-field-hover shadow-buttons-neutral placeholder-ui-fg-muted text-ui-fg-base transition-fg relative w-full rounded-md",
|
||||
"focus-within:shadow-borders-interactive-with-active"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "txt-compact-medium h-10 px-3",
|
||||
small: "txt-compact-small h-8 px-2",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface CurrencyInputProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<typeof Primitive>,
|
||||
"prefix" | "suffix" | "size"
|
||||
>,
|
||||
VariantProps<typeof currencyInputVariants> {
|
||||
symbol: string
|
||||
code: string
|
||||
}
|
||||
|
||||
const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
|
||||
(
|
||||
{ size = "base", symbol, code, disabled, onInvalid, className, ...props },
|
||||
ref
|
||||
) => {
|
||||
const innerRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
React.useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
|
||||
ref,
|
||||
() => innerRef.current
|
||||
)
|
||||
|
||||
const [valid, setValid] = React.useState(true)
|
||||
|
||||
const onInnerInvalid = React.useCallback(
|
||||
(event: React.FormEvent<HTMLInputElement>) => {
|
||||
setValid(event.currentTarget.validity.valid)
|
||||
|
||||
if (onInvalid) {
|
||||
onInvalid(event)
|
||||
}
|
||||
},
|
||||
[onInvalid]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.focus()
|
||||
}
|
||||
}}
|
||||
className={clx(
|
||||
"w-full cursor-text justify-between overflow-hidden",
|
||||
currencyInputVariants({ size }),
|
||||
{
|
||||
"text-ui-fg-disabled !bg-ui-bg-disabled !shadow-buttons-neutral !placeholder-ui-fg-disabled cursor-not-allowed":
|
||||
disabled,
|
||||
"!shadow-borders-error invalid:!shadow-borders-error":
|
||||
props["aria-invalid"] || !valid,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={clx("w-fit", {
|
||||
"py-[9px]": size === "base",
|
||||
"py-[5px]": size === "small",
|
||||
})}
|
||||
role="presentation"
|
||||
>
|
||||
<Text
|
||||
className={clx(
|
||||
"text-ui-fg-muted pointer-events-none select-none uppercase",
|
||||
{
|
||||
"text-ui-fg-disabled": disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{code}
|
||||
</Text>
|
||||
</span>
|
||||
<Primitive
|
||||
className="h-full min-w-0 flex-1 appearance-none bg-transparent text-right outline-none disabled:cursor-not-allowed"
|
||||
disabled={disabled}
|
||||
onInvalid={onInnerInvalid}
|
||||
ref={innerRef}
|
||||
{...props}
|
||||
/>
|
||||
<span
|
||||
className={clx("w-fit min-w-[16px] text-right", {
|
||||
"py-[9px]": size === "base",
|
||||
"py-[5px]": size === "small",
|
||||
})}
|
||||
role="presentation"
|
||||
>
|
||||
<Text
|
||||
className={clx("text-ui-fg-muted pointer-events-none select-none", {
|
||||
"text-ui-fg-disabled": disabled,
|
||||
})}
|
||||
>
|
||||
{symbol}
|
||||
</Text>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
CurrencyInput.displayName = "CurrencyInput"
|
||||
|
||||
export { CurrencyInput }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./currency-input"
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { DatePicker } from "./date-picker"
|
||||
|
||||
describe("DatePicker", () => {
|
||||
describe("Preset validation", () => {
|
||||
it("should throw an error if a preset is before the min year", async () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<DatePicker
|
||||
fromYear={1800}
|
||||
presets={[
|
||||
{
|
||||
label: "Year of the first US census",
|
||||
date: new Date(1790, 0, 1),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
).toThrowError(
|
||||
/Preset Year of the first US census is before fromYear 1800./
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error if a preset is after the max year", async () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<DatePicker
|
||||
toYear={2012}
|
||||
presets={[
|
||||
{
|
||||
label: "End of the Mayan calendar",
|
||||
date: new Date(2025, 0, 1),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
).toThrowError(/Preset End of the Mayan calendar is after toYear 2012./)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Single", () => {
|
||||
it("should render", async () => {
|
||||
render(<DatePicker />)
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Range", () => {
|
||||
it("should render", async () => {
|
||||
render(<DatePicker mode={"range"} />)
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,243 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
import { DateRange } from "react-day-picker"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { DatePicker } from "./date-picker"
|
||||
import { Popover } from "@/components/popover"
|
||||
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
title: "Components/DatePicker",
|
||||
component: DatePicker,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<div className="w-[200px]">
|
||||
<DatePicker {...args} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof DatePicker>
|
||||
|
||||
const presets = [
|
||||
{
|
||||
label: "Today",
|
||||
date: new Date(),
|
||||
},
|
||||
{
|
||||
label: "Tomorrow",
|
||||
date: new Date(new Date().setDate(new Date().getDate() + 1)),
|
||||
},
|
||||
{
|
||||
label: "A week from now",
|
||||
date: new Date(new Date().setDate(new Date().getDate() + 7)),
|
||||
},
|
||||
{
|
||||
label: "A month from now",
|
||||
date: new Date(new Date().setMonth(new Date().getMonth() + 1)),
|
||||
},
|
||||
{
|
||||
label: "6 months from now",
|
||||
date: new Date(new Date().setMonth(new Date().getMonth() + 6)),
|
||||
},
|
||||
{
|
||||
label: "A year from now",
|
||||
date: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
||||
},
|
||||
]
|
||||
|
||||
export const Single: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const SingleWithPresets: Story = {
|
||||
args: {
|
||||
presets,
|
||||
},
|
||||
}
|
||||
|
||||
export const SingleWithTimePicker: Story = {
|
||||
args: {
|
||||
showTimePicker: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const SingleWithTimePickerAndPresets: Story = {
|
||||
args: {
|
||||
showTimePicker: true,
|
||||
presets,
|
||||
},
|
||||
}
|
||||
|
||||
const rangePresets = [
|
||||
{
|
||||
label: "Today",
|
||||
dateRange: {
|
||||
from: new Date(),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setDate(new Date().getDate() - 30)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 3 months",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setMonth(new Date().getMonth() - 3)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Last 6 months",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setMonth(new Date().getMonth() - 6)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Month to date",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setDate(1)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Year to date",
|
||||
dateRange: {
|
||||
from: new Date(new Date().setFullYear(new Date().getFullYear(), 0, 1)),
|
||||
to: new Date(),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const Range: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
},
|
||||
}
|
||||
|
||||
export const RangeWithPresets: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
presets: rangePresets,
|
||||
},
|
||||
}
|
||||
|
||||
export const RangeWithTimePicker: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
showTimePicker: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const RangeWithTimePickerAndPresets: Story = {
|
||||
args: {
|
||||
mode: "range",
|
||||
showTimePicker: true,
|
||||
presets: rangePresets,
|
||||
},
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [value, setValue] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col gap-y-4">
|
||||
<DatePicker
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
setValue(value)
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => setValue(undefined)}>Reset</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => <ControlledDemo />,
|
||||
}
|
||||
|
||||
const ControlledRangeDemo = () => {
|
||||
const [value, setValue] = React.useState<DateRange | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("Value changed: ", value)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col gap-y-4">
|
||||
<DatePicker
|
||||
mode="range"
|
||||
value={value}
|
||||
onChange={(value) => {
|
||||
setValue(value)
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => setValue(undefined)}>Reset</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ControlledRange: Story = {
|
||||
render: () => <ControlledRangeDemo />,
|
||||
}
|
||||
|
||||
type NestedProps = {
|
||||
value?: Date
|
||||
onChange?: (value: Date | undefined) => void
|
||||
}
|
||||
const Nested = ({ value, onChange }: NestedProps) => {
|
||||
return (
|
||||
<Popover>
|
||||
<Popover.Trigger asChild>
|
||||
<Button>Open</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<div className="px-3 py-2">
|
||||
<DatePicker value={value} onChange={onChange} />
|
||||
</div>
|
||||
<Popover.Seperator />
|
||||
<div className="px-3 py-2">
|
||||
<DatePicker value={value} onChange={onChange} />
|
||||
</div>
|
||||
<Popover.Seperator />
|
||||
<div className="flex items-center justify-between gap-x-2 px-3 py-2 [&_button]:w-full">
|
||||
<Button variant="secondary">Clear</Button>
|
||||
<Button>Apply</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const NestedDemo = () => {
|
||||
const [value, setValue] = React.useState<Date | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col gap-y-4">
|
||||
<Nested value={value} onChange={setValue} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const NestedControlled: Story = {
|
||||
render: () => <NestedDemo />,
|
||||
}
|
||||
@@ -0,0 +1,908 @@
|
||||
"use client"
|
||||
|
||||
import { Time } from "@internationalized/date"
|
||||
import { Calendar as CalendarIcon, Minus } from "@medusajs/icons"
|
||||
import * as Primitives from "@radix-ui/react-popover"
|
||||
import { TimeValue } from "@react-aria/datepicker"
|
||||
import { format } from "date-fns"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Calendar as CalendarPrimitive } from "@/components/calendar"
|
||||
import { TimeInput } from "@/components/time-input"
|
||||
import type { DateRange } from "@/types"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { isBrowserLocaleClockType24h } from "@/utils/is-browser-locale-hour-cycle-24h"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
const displayVariants = cva(
|
||||
clx(
|
||||
"text-ui-fg-base bg-ui-bg-field transition-fg shadow-buttons-neutral flex w-full items-center gap-x-2 rounded-md outline-none",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"focus:shadow-borders-interactive-with-active data-[state=open]:shadow-borders-interactive-with-active",
|
||||
"disabled:bg-ui-bg-disabled disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral",
|
||||
"aria-[invalid=true]:!shadow-borders-error"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "txt-compact-medium h-10 px-3 py-2.5",
|
||||
small: "txt-compact-small h-8 px-2 py-1.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Display = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
placeholder?: string
|
||||
size?: "small" | "base"
|
||||
}
|
||||
>(({ className, children, placeholder, size = "base", ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Trigger asChild>
|
||||
<button
|
||||
ref={ref}
|
||||
className={clx(displayVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<CalendarIcon className="text-ui-fg-muted" />
|
||||
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-left">
|
||||
{children ? (
|
||||
children
|
||||
) : placeholder ? (
|
||||
<span className="text-ui-fg-muted">{placeholder}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
</Primitives.Trigger>
|
||||
)
|
||||
})
|
||||
Display.displayName = "DatePicker.Display"
|
||||
|
||||
const Flyout = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Content>,
|
||||
React.ComponentProps<typeof Primitives.Content>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Portal>
|
||||
<Primitives.Content
|
||||
ref={ref}
|
||||
sideOffset={8}
|
||||
align="center"
|
||||
className={clx(
|
||||
"txt-compact-small shadow-elevation-flyout bg-ui-bg-base w-fit rounded-lg",
|
||||
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Primitives.Content>
|
||||
</Primitives.Portal>
|
||||
)
|
||||
})
|
||||
Flyout.displayName = "DatePicker.Flyout"
|
||||
|
||||
interface Preset {
|
||||
label: string
|
||||
}
|
||||
|
||||
interface DatePreset extends Preset {
|
||||
date: Date
|
||||
}
|
||||
|
||||
interface DateRangePreset extends Preset {
|
||||
dateRange: DateRange
|
||||
}
|
||||
|
||||
type PresetContainerProps<TPreset extends Preset, TValue> = {
|
||||
presets: TPreset[] | TPreset[]
|
||||
onSelect: (value: TValue) => void
|
||||
currentValue?: TValue
|
||||
}
|
||||
|
||||
const PresetContainer = <TPreset extends Preset, TValue>({
|
||||
presets,
|
||||
onSelect,
|
||||
currentValue,
|
||||
}: PresetContainerProps<TPreset, TValue>) => {
|
||||
const isDateRangePresets = (preset: any): preset is DateRangePreset => {
|
||||
return "dateRange" in preset
|
||||
}
|
||||
|
||||
const isDatePresets = (preset: any): preset is DatePreset => {
|
||||
return "date" in preset
|
||||
}
|
||||
|
||||
const handleClick = (preset: TPreset) => {
|
||||
if (isDateRangePresets(preset)) {
|
||||
onSelect(preset.dateRange as TValue)
|
||||
} else if (isDatePresets(preset)) {
|
||||
onSelect(preset.date as TValue)
|
||||
}
|
||||
}
|
||||
|
||||
const compareDates = (date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getDate() === date2.getDate() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getFullYear() === date2.getFullYear()
|
||||
)
|
||||
}
|
||||
|
||||
const compareRanges = (range1: DateRange, range2: DateRange) => {
|
||||
const from1 = range1.from
|
||||
const from2 = range2.from
|
||||
|
||||
let equalFrom = false
|
||||
|
||||
if (from1 && from2) {
|
||||
const sameFrom = compareDates(from1, from2)
|
||||
|
||||
if (sameFrom) {
|
||||
equalFrom = true
|
||||
}
|
||||
}
|
||||
|
||||
const to1 = range1.to
|
||||
const to2 = range2.to
|
||||
|
||||
let equalTo = false
|
||||
|
||||
if (to1 && to2) {
|
||||
const sameTo = compareDates(to1, to2)
|
||||
|
||||
if (sameTo) {
|
||||
equalTo = true
|
||||
}
|
||||
}
|
||||
|
||||
return equalFrom && equalTo
|
||||
}
|
||||
|
||||
const matchesCurrent = (preset: TPreset) => {
|
||||
if (isDateRangePresets(preset)) {
|
||||
const value = currentValue as DateRange | undefined
|
||||
|
||||
return value && compareRanges(value, preset.dateRange)
|
||||
} else if (isDatePresets(preset)) {
|
||||
const value = currentValue as Date | undefined
|
||||
|
||||
return value && compareDates(value, preset.date)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="flex flex-col items-start">
|
||||
{presets.map((preset, index) => {
|
||||
return (
|
||||
<li key={index} className="w-full">
|
||||
<button
|
||||
className={clx(
|
||||
"txt-compact-small-plus w-full overflow-hidden text-ellipsis whitespace-nowrap rounded-md p-2 text-left",
|
||||
"text-ui-fg-subtle hover:bg-ui-bg-base-hover outline-none transition-all",
|
||||
"focus:bg-ui-bg-base-hover",
|
||||
{
|
||||
"!bg-ui-bg-base-pressed": matchesCurrent(preset),
|
||||
}
|
||||
)}
|
||||
onClick={() => handleClick(preset)}
|
||||
aria-label={`Select ${preset.label}`}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
const formatDate = (date: Date, includeTime?: boolean) => {
|
||||
const usesAmPm = !isBrowserLocaleClockType24h()
|
||||
|
||||
if (includeTime) {
|
||||
if (usesAmPm) {
|
||||
return format(date, "MMM d, yyyy h:mm a")
|
||||
}
|
||||
|
||||
return format(date, "MMM d, yyyy HH:mm")
|
||||
}
|
||||
|
||||
return format(date, "MMM d, yyyy")
|
||||
}
|
||||
|
||||
type CalendarProps = {
|
||||
fromYear?: number
|
||||
toYear?: number
|
||||
fromMonth?: Date
|
||||
toMonth?: Date
|
||||
fromDay?: Date
|
||||
toDay?: Date
|
||||
fromDate?: Date
|
||||
toDate?: Date
|
||||
}
|
||||
|
||||
interface PickerProps extends CalendarProps {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
size?: "small" | "base"
|
||||
showTimePicker?: boolean
|
||||
id?: string
|
||||
"aria-invalid"?: boolean
|
||||
"aria-label"?: string
|
||||
"aria-labelledby"?: string
|
||||
"aria-required"?: boolean
|
||||
}
|
||||
|
||||
interface SingleProps extends PickerProps {
|
||||
presets?: DatePreset[]
|
||||
defaultValue?: Date
|
||||
value?: Date
|
||||
onChange?: (date: Date | undefined) => void
|
||||
}
|
||||
|
||||
const SingleDatePicker = ({
|
||||
defaultValue,
|
||||
value,
|
||||
size = "base",
|
||||
onChange,
|
||||
presets,
|
||||
showTimePicker,
|
||||
disabled,
|
||||
className,
|
||||
...props
|
||||
}: SingleProps) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
value ?? defaultValue ?? undefined
|
||||
)
|
||||
const [month, setMonth] = React.useState<Date | undefined>(date)
|
||||
|
||||
const [time, setTime] = React.useState<TimeValue>(
|
||||
value
|
||||
? new Time(value.getHours(), value.getMinutes())
|
||||
: defaultValue
|
||||
? new Time(defaultValue.getHours(), defaultValue.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
|
||||
const initialDate = React.useMemo(() => {
|
||||
return date
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
/**
|
||||
* Update the date when the value changes.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
setDate(value ?? defaultValue ?? undefined)
|
||||
}, [value, defaultValue])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (date) {
|
||||
setMonth(date)
|
||||
}
|
||||
}, [date])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setMonth(date)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
const onCancel = () => {
|
||||
setDate(initialDate)
|
||||
setTime(
|
||||
initialDate
|
||||
? new Time(initialDate.getHours(), initialDate.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onCancel()
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const onDateChange = (date: Date | undefined) => {
|
||||
const newDate = date
|
||||
|
||||
if (showTimePicker) {
|
||||
/**
|
||||
* If the time is cleared, and the date is
|
||||
* changed then we want to reset the time.
|
||||
*/
|
||||
if (newDate && !time) {
|
||||
setTime(new Time(0, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* If the time is set, and the date is changed
|
||||
* then we want to update the date with the
|
||||
* time.
|
||||
*/
|
||||
if (newDate && time) {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
}
|
||||
|
||||
setDate(newDate)
|
||||
}
|
||||
|
||||
const onTimeChange = (time: TimeValue) => {
|
||||
setTime(time)
|
||||
|
||||
if (!date) {
|
||||
return
|
||||
}
|
||||
|
||||
const newDate = new Date(date.getTime())
|
||||
|
||||
if (!time) {
|
||||
/**
|
||||
* When a segment of the time input is cleared,
|
||||
* it will return `null` as the value is no longer
|
||||
* a valid time. In this case, we want to set the
|
||||
* time to for the date, effectivly resetting the
|
||||
* input field.
|
||||
*/
|
||||
newDate.setHours(0)
|
||||
newDate.setMinutes(0)
|
||||
} else {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
|
||||
setDate(newDate)
|
||||
}
|
||||
|
||||
const formattedDate = React.useMemo(() => {
|
||||
if (!date) {
|
||||
return null
|
||||
}
|
||||
|
||||
return formatDate(date, showTimePicker)
|
||||
}, [date, showTimePicker])
|
||||
|
||||
const onApply = () => {
|
||||
setOpen(false)
|
||||
onChange?.(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<Primitives.Root open={open} onOpenChange={onOpenChange}>
|
||||
<Display
|
||||
placeholder="Pick a date"
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
aria-required={props.required || props["aria-required"]}
|
||||
aria-invalid={props["aria-invalid"]}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-labelledby={props["aria-labelledby"]}
|
||||
size={size}
|
||||
>
|
||||
{formattedDate}
|
||||
</Display>
|
||||
<Flyout>
|
||||
<div className="flex">
|
||||
<div className="flex items-start">
|
||||
{presets && presets.length > 0 && (
|
||||
<div className="relative h-full w-[160px] border-r">
|
||||
<div className="absolute inset-0 overflow-y-scroll p-3">
|
||||
<PresetContainer
|
||||
currentValue={date}
|
||||
presets={presets}
|
||||
onSelect={onDateChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CalendarPrimitive
|
||||
mode="single"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
selected={date}
|
||||
onSelect={onDateChange}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
{showTimePicker && (
|
||||
<div className="border-ui-border-base border-t p-3">
|
||||
<TimeInput
|
||||
aria-label="Time"
|
||||
onChange={onTimeChange}
|
||||
isDisabled={!date}
|
||||
value={time}
|
||||
isRequired={props.required}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-ui-border-base flex items-center gap-x-2 border-t p-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Flyout>
|
||||
</Primitives.Root>
|
||||
)
|
||||
}
|
||||
|
||||
interface RangeProps extends PickerProps {
|
||||
presets?: DateRangePreset[]
|
||||
defaultValue?: DateRange
|
||||
value?: DateRange
|
||||
onChange?: (dateRange: DateRange | undefined) => void
|
||||
}
|
||||
|
||||
const RangeDatePicker = ({
|
||||
defaultValue,
|
||||
value,
|
||||
onChange,
|
||||
size = "base",
|
||||
showTimePicker,
|
||||
presets,
|
||||
disabled,
|
||||
className,
|
||||
...props
|
||||
}: RangeProps) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [range, setRange] = React.useState<DateRange | undefined>(
|
||||
value ?? defaultValue ?? undefined
|
||||
)
|
||||
const [month, setMonth] = React.useState<Date | undefined>(range?.from)
|
||||
|
||||
const [startTime, setStartTime] = React.useState<TimeValue>(
|
||||
value?.from
|
||||
? new Time(value.from.getHours(), value.from.getMinutes())
|
||||
: defaultValue?.from
|
||||
? new Time(defaultValue.from.getHours(), defaultValue.from.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
const [endTime, setEndTime] = React.useState<TimeValue>(
|
||||
value?.to
|
||||
? new Time(value.to.getHours(), value.to.getMinutes())
|
||||
: defaultValue?.to
|
||||
? new Time(defaultValue.to.getHours(), defaultValue.to.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
|
||||
const initialRange = React.useMemo(() => {
|
||||
return range
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
/**
|
||||
* Update the range when the value changes.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
setRange(value ?? defaultValue ?? undefined)
|
||||
}, [value, defaultValue])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (range) {
|
||||
setMonth(range.from)
|
||||
}
|
||||
}, [range])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setMonth(range?.from)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
const onRangeChange = (range: DateRange | undefined) => {
|
||||
const newRange = range
|
||||
|
||||
if (showTimePicker) {
|
||||
if (newRange?.from && !startTime) {
|
||||
setStartTime(new Time(0, 0))
|
||||
}
|
||||
|
||||
if (newRange?.to && !endTime) {
|
||||
setEndTime(new Time(0, 0))
|
||||
}
|
||||
|
||||
if (newRange?.from && startTime) {
|
||||
newRange.from.setHours(startTime.hour)
|
||||
newRange.from.setMinutes(startTime.minute)
|
||||
}
|
||||
|
||||
if (newRange?.to && endTime) {
|
||||
newRange.to.setHours(endTime.hour)
|
||||
newRange.to.setMinutes(endTime.minute)
|
||||
}
|
||||
}
|
||||
|
||||
setRange(newRange)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setRange(initialRange)
|
||||
setStartTime(
|
||||
initialRange?.from
|
||||
? new Time(initialRange.from.getHours(), initialRange.from.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
setEndTime(
|
||||
initialRange?.to
|
||||
? new Time(initialRange.to.getHours(), initialRange.to.getMinutes())
|
||||
: new Time(0, 0)
|
||||
)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onCancel()
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const onTimeChange = (time: TimeValue, pos: "start" | "end") => {
|
||||
switch (pos) {
|
||||
case "start":
|
||||
setStartTime(time)
|
||||
break
|
||||
case "end":
|
||||
setEndTime(time)
|
||||
break
|
||||
}
|
||||
|
||||
if (!range) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pos === "start") {
|
||||
if (!range.from) {
|
||||
return
|
||||
}
|
||||
|
||||
const newDate = new Date(range.from.getTime())
|
||||
|
||||
if (!time) {
|
||||
newDate.setHours(0)
|
||||
newDate.setMinutes(0)
|
||||
} else {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
|
||||
setRange({
|
||||
...range,
|
||||
from: newDate,
|
||||
})
|
||||
}
|
||||
|
||||
if (pos === "end") {
|
||||
if (!range.to) {
|
||||
return
|
||||
}
|
||||
|
||||
const newDate = new Date(range.to.getTime())
|
||||
|
||||
if (!time) {
|
||||
newDate.setHours(0)
|
||||
newDate.setMinutes(0)
|
||||
} else {
|
||||
newDate.setHours(time.hour)
|
||||
newDate.setMinutes(time.minute)
|
||||
}
|
||||
|
||||
setRange({
|
||||
...range,
|
||||
to: newDate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const displayRange = React.useMemo(() => {
|
||||
if (!range) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${range.from ? formatDate(range.from, showTimePicker) : ""} - ${
|
||||
range.to ? formatDate(range.to, showTimePicker) : ""
|
||||
}`
|
||||
}, [range, showTimePicker])
|
||||
|
||||
const onApply = () => {
|
||||
setOpen(false)
|
||||
onChange?.(range)
|
||||
}
|
||||
|
||||
return (
|
||||
<Primitives.Root open={open} onOpenChange={onOpenChange}>
|
||||
<Display
|
||||
placeholder="Pick a date"
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
aria-required={props.required || props["aria-required"]}
|
||||
aria-invalid={props["aria-invalid"]}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-labelledby={props["aria-labelledby"]}
|
||||
size={size}
|
||||
>
|
||||
{displayRange}
|
||||
</Display>
|
||||
<Flyout>
|
||||
<div className="flex">
|
||||
<div className="flex items-start">
|
||||
{presets && presets.length > 0 && (
|
||||
<div className="relative h-full w-[160px] border-r">
|
||||
<div className="absolute inset-0 overflow-y-scroll p-3">
|
||||
<PresetContainer
|
||||
currentValue={range}
|
||||
presets={presets}
|
||||
onSelect={onRangeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CalendarPrimitive
|
||||
mode="range"
|
||||
selected={range}
|
||||
onSelect={onRangeChange}
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
numberOfMonths={2}
|
||||
disabled={disabled}
|
||||
classNames={{
|
||||
months: "flex flex-row divide-x divide-ui-border-base",
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
{showTimePicker && (
|
||||
<div className="border-ui-border-base flex items-center justify-evenly gap-x-3 border-t p-3">
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<span className="text-ui-fg-subtle">Start:</span>
|
||||
<TimeInput
|
||||
value={startTime}
|
||||
onChange={(v) => onTimeChange(v, "start")}
|
||||
aria-label="Start date time"
|
||||
isDisabled={!range?.from}
|
||||
isRequired={props.required}
|
||||
/>
|
||||
</div>
|
||||
<Minus className="text-ui-fg-muted" />
|
||||
<div className="flex flex-1 items-center gap-x-2">
|
||||
<span className="text-ui-fg-subtle">End:</span>
|
||||
<TimeInput
|
||||
value={endTime}
|
||||
onChange={(v) => onTimeChange(v, "end")}
|
||||
aria-label="End date time"
|
||||
isDisabled={!range?.to}
|
||||
isRequired={props.required}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between border-t p-3">
|
||||
<p className={clx("text-ui-fg-subtle txt-compact-small-plus")}>
|
||||
<span className="text-ui-fg-muted">Range:</span>{" "}
|
||||
{displayRange}
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button variant="secondary" type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="button" onClick={onApply}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Flyout>
|
||||
</Primitives.Root>
|
||||
)
|
||||
}
|
||||
|
||||
type DatePickerProps = (
|
||||
| {
|
||||
mode?: "single"
|
||||
presets?: DatePreset[]
|
||||
defaultValue?: Date
|
||||
value?: Date
|
||||
onChange?: (date: Date | undefined) => void
|
||||
}
|
||||
| {
|
||||
mode: "range"
|
||||
presets?: DateRangePreset[]
|
||||
defaultValue?: DateRange
|
||||
value?: DateRange
|
||||
onChange?: (dateRange: DateRange | undefined) => void
|
||||
}
|
||||
) &
|
||||
PickerProps
|
||||
|
||||
const validatePresets = (
|
||||
presets: DateRangePreset[] | DatePreset[],
|
||||
rules: PickerProps
|
||||
) => {
|
||||
const { toYear, fromYear, fromMonth, toMonth, fromDay, toDay } = rules
|
||||
|
||||
if (presets && presets.length > 0) {
|
||||
const fromYearToUse = fromYear
|
||||
const toYearToUse = toYear
|
||||
|
||||
presets.forEach((preset) => {
|
||||
if ("date" in preset) {
|
||||
const presetYear = preset.date.getFullYear()
|
||||
|
||||
if (fromYear && presetYear < fromYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is before fromYear ${fromYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (toYear && presetYear > toYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is after toYear ${toYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (fromMonth) {
|
||||
const presetMonth = preset.date.getMonth()
|
||||
|
||||
if (presetMonth < fromMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is before fromMonth ${fromMonth}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toMonth) {
|
||||
const presetMonth = preset.date.getMonth()
|
||||
|
||||
if (presetMonth > toMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is after toMonth ${toMonth}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (fromDay) {
|
||||
const presetDay = preset.date.getDate()
|
||||
|
||||
if (presetDay < fromDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is before fromDay ${fromDay}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toDay) {
|
||||
const presetDay = preset.date.getDate()
|
||||
|
||||
if (presetDay > toDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label} is after toDay ${format(
|
||||
toDay,
|
||||
"MMM dd, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("dateRange" in preset) {
|
||||
const presetFromYear = preset.dateRange.from?.getFullYear()
|
||||
const presetToYear = preset.dateRange.to?.getFullYear()
|
||||
|
||||
if (presetFromYear && fromYear && presetFromYear < fromYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'from' is before fromYear ${fromYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (presetToYear && toYear && presetToYear > toYear) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'to' is after toYear ${toYearToUse}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (fromMonth) {
|
||||
const presetMonth = preset.dateRange.from?.getMonth()
|
||||
|
||||
if (presetMonth && presetMonth < fromMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'from' is before fromMonth ${format(
|
||||
fromMonth,
|
||||
"MMM, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toMonth) {
|
||||
const presetMonth = preset.dateRange.to?.getMonth()
|
||||
|
||||
if (presetMonth && presetMonth > toMonth.getMonth()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'to' is after toMonth ${format(
|
||||
toMonth,
|
||||
"MMM, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (fromDay) {
|
||||
const presetDay = preset.dateRange.from?.getDate()
|
||||
|
||||
if (presetDay && presetDay < fromDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${
|
||||
preset.dateRange.from
|
||||
}'s 'from' is before fromDay ${format(fromDay, "MMM dd, yyyy")}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (toDay) {
|
||||
const presetDay = preset.dateRange.to?.getDate()
|
||||
|
||||
if (presetDay && presetDay > toDay.getDate()) {
|
||||
throw new Error(
|
||||
`Preset ${preset.label}'s 'to' is after toDay ${format(
|
||||
toDay,
|
||||
"MMM dd, yyyy"
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const DatePicker = ({ mode = "single", ...props }: DatePickerProps) => {
|
||||
if (props.presets) {
|
||||
validatePresets(props.presets, props)
|
||||
}
|
||||
|
||||
if (mode === "single") {
|
||||
return <SingleDatePicker {...(props as SingleProps)} />
|
||||
}
|
||||
|
||||
return <RangeDatePicker {...(props as RangeProps)} />
|
||||
}
|
||||
|
||||
export { DatePicker }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./date-picker"
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Drawer } from "./drawer"
|
||||
|
||||
const meta: Meta<typeof Drawer> = {
|
||||
title: "Components/Drawer",
|
||||
component: Drawer,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Drawer>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<Drawer>
|
||||
<Drawer.Trigger asChild>
|
||||
<Button>Edit Variant</Button>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Edit Variant</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<Drawer.Body></Drawer.Body>
|
||||
<Drawer.Footer>
|
||||
<Drawer.Close asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
</Drawer.Close>
|
||||
<Button>Save</Button>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./drawer"
|
||||
@@ -0,0 +1,275 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Plus, Trash } from "@medusajs/icons"
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { IconButton } from "@/components/icon-button"
|
||||
import { Select } from "@/components/select"
|
||||
|
||||
import { DatePicker } from "../date-picker"
|
||||
import { FocusModal } from "../focus-modal"
|
||||
import { DropdownMenu } from "./dropdown-menu"
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: "Components/DropdownMenu",
|
||||
component: DropdownMenu,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof DropdownMenu>
|
||||
|
||||
type SortingState = "asc" | "desc" | "alpha" | "alpha-reverse" | "none"
|
||||
|
||||
const SortingDemo = () => {
|
||||
const [sort, setSort] = React.useState<SortingState>("none")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="primary">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="w-[300px]">
|
||||
<DropdownMenu.RadioGroup
|
||||
value={sort}
|
||||
onValueChange={(v) => setSort(v as SortingState)}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="none">
|
||||
No Sorting
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.RadioItem value="alpha">
|
||||
Alphabetical
|
||||
<DropdownMenu.Hint>A-Z</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="alpha-reverse">
|
||||
Reverse Alphabetical
|
||||
<DropdownMenu.Hint>Z-A</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="asc">
|
||||
Created At - Ascending
|
||||
<DropdownMenu.Hint>1 - 30</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="desc">
|
||||
Created At - Descending
|
||||
<DropdownMenu.Hint>30 - 1</DropdownMenu.Hint>
|
||||
</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<div>
|
||||
<pre>Sorting by: {sort}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SortingMenu: Story = {
|
||||
render: () => {
|
||||
return <SortingDemo />
|
||||
},
|
||||
}
|
||||
|
||||
const SelectDemo = () => {
|
||||
const [currencies, setCurrencies] = React.useState<string[]>([])
|
||||
const [regions, setRegions] = React.useState<string[]>([])
|
||||
|
||||
const onSelectCurrency = (currency: string) => {
|
||||
if (currencies.includes(currency)) {
|
||||
setCurrencies(currencies.filter((c) => c !== currency))
|
||||
} else {
|
||||
setCurrencies([...currencies, currency])
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectRegion = (region: string) => {
|
||||
if (regions.includes(region)) {
|
||||
setRegions(regions.filter((r) => r !== region))
|
||||
} else {
|
||||
setRegions([...regions, region])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton>
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="w-[300px]">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Currencies</DropdownMenu.Label>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={currencies.includes("EUR")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectCurrency("EUR")
|
||||
}}
|
||||
>
|
||||
EUR
|
||||
<DropdownMenu.Hint>Euro</DropdownMenu.Hint>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={currencies.includes("USD")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectCurrency("USD")
|
||||
}}
|
||||
>
|
||||
USD
|
||||
<DropdownMenu.Hint>US Dollar</DropdownMenu.Hint>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={currencies.includes("DKK")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectCurrency("DKK")
|
||||
}}
|
||||
>
|
||||
DKK
|
||||
<DropdownMenu.Hint>Danish Krone</DropdownMenu.Hint>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Regions</DropdownMenu.Label>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={regions.includes("NA")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectRegion("NA")
|
||||
}}
|
||||
>
|
||||
North America
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={regions.includes("EU")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectRegion("EU")
|
||||
}}
|
||||
>
|
||||
Europe
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={regions.includes("DK")}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
onSelectRegion("DK")
|
||||
}}
|
||||
>
|
||||
Denmark
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<div>
|
||||
<pre>Currencies: {currencies.join(", ")}</pre>
|
||||
<pre>Regions: {regions.join(", ")}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SelectMenu: Story = {
|
||||
render: () => {
|
||||
return <SelectDemo />
|
||||
},
|
||||
}
|
||||
|
||||
export const SimpleMenu: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton>
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
Edit
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Plus className="text-ui-fg-subtle" />
|
||||
Add
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const ComplexMenuDemo = () => {
|
||||
return (
|
||||
<FocusModal>
|
||||
<FocusModal.Trigger asChild>
|
||||
<Button>Open</Button>
|
||||
</FocusModal.Trigger>
|
||||
<FocusModal.Content>
|
||||
<FocusModal.Header>
|
||||
<Button>Save</Button>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="item-center flex justify-center">
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button>View</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
Edit
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Plus className="text-ui-fg-subtle" />
|
||||
Add
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
Delete
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<div className="flex flex-col gap-y-2 p-2">
|
||||
<Select>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Select" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="1">One</Select.Item>
|
||||
<Select.Item value="2">Two</Select.Item>
|
||||
<Select.Item value="3">Three</Select.Item>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
<DatePicker />
|
||||
</div>
|
||||
<div className="border-ui-border-base flex items-center gap-x-2 border-t p-2">
|
||||
<Button variant="secondary">Clear</Button>
|
||||
<Button>Apply</Button>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
export const ComplexMenu: Story = {
|
||||
render: () => {
|
||||
return <ComplexMenuDemo />
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
"use client"
|
||||
|
||||
import { CheckMini, ChevronRightMini, EllipseMiniSolid } from "@medusajs/icons"
|
||||
import * as Primitives from "@radix-ui/react-dropdown-menu"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Root = Primitives.Root
|
||||
Root.displayName = "DropdownMenu.Root"
|
||||
|
||||
const Trigger = Primitives.Trigger
|
||||
Trigger.displayName = "DropdownMenu.Trigger"
|
||||
|
||||
const Group = Primitives.Group
|
||||
Group.displayName = "DropdownMenu.Group"
|
||||
|
||||
const SubMenu = Primitives.Sub
|
||||
SubMenu.displayName = "DropdownMenu.SubMenu"
|
||||
|
||||
const RadioGroup = Primitives.RadioGroup
|
||||
RadioGroup.displayName = "DropdownMenu.RadioGroup"
|
||||
|
||||
const SubMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.SubTrigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Primitives.SubTrigger
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"focus:bg-ui-bg-base-pressed data-[state=open]:bg-ui-bg-base-pressed txt-compact-small flex cursor-default select-none items-center rounded-sm px-3 py-2 outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightMini className="ml-auto" />
|
||||
</Primitives.SubTrigger>
|
||||
))
|
||||
SubMenuTrigger.displayName = "DropdownMenu.SubMenuTrigger"
|
||||
|
||||
const SubMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.SubContent>
|
||||
>(({ className, collisionPadding = 8, ...props }, ref) => (
|
||||
<Primitives.Portal>
|
||||
<Primitives.SubContent
|
||||
ref={ref}
|
||||
collisionPadding={collisionPadding}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] min-w-[8rem] overflow-hidden rounded-lg border p-1",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Primitives.Portal>
|
||||
))
|
||||
SubMenuContent.displayName = "DropdownMenu.SubMenuContent"
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Content>
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
sideOffset = 8,
|
||||
collisionPadding = 8,
|
||||
align = "start",
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<Primitives.Portal>
|
||||
<Primitives.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
collisionPadding={collisionPadding}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] min-w-[220px] overflow-hidden rounded-lg p-1",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Primitives.Portal>
|
||||
)
|
||||
)
|
||||
Content.displayName = "DropdownMenu.Content"
|
||||
|
||||
const Item = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitives.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base focus:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled txt-compact-small relative flex cursor-pointer select-none items-center rounded-md px-3 py-2 outline-none transition-colors data-[disabled]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Item.displayName = "DropdownMenu.Item"
|
||||
|
||||
const CheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<Primitives.CheckboxItem
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"focus:bg-ui-bg-base-pressed text-ui-fg-base data-[disabled]:text-ui-fg-disabled relative flex cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-3 text-sm outline-none transition-colors data-[disabled]:pointer-events-none",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-3 flex h-5 w-5 items-center justify-center">
|
||||
<Primitives.ItemIndicator>
|
||||
<CheckMini />
|
||||
</Primitives.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</Primitives.CheckboxItem>
|
||||
))
|
||||
CheckboxItem.displayName = "DropdownMenu.CheckboxItem"
|
||||
|
||||
const RadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Primitives.RadioItem
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"focus:bg-ui-bg-base-pressed hover:bg-ui-base-hover bg-ui-bg-base txt-compact-small relative flex cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-3 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[state=checked]:font-medium data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-3 flex h-5 w-5 items-center justify-center">
|
||||
<Primitives.ItemIndicator>
|
||||
<EllipseMiniSolid className="text-ui-fg-base" />
|
||||
</Primitives.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</Primitives.RadioItem>
|
||||
))
|
||||
RadioItem.displayName = "DropdownMenu.RadioItem"
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitives.Label
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"text-ui-fg-subtle txt-compact-xsmall-plus px-2 py-1.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = "DropdownMenu.Label"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitives.Separator
|
||||
ref={ref}
|
||||
className={clx("bg-ui-border-base -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = "DropdownMenu.Separator"
|
||||
|
||||
const Shortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={clx(
|
||||
"text-ui-fg-subtle txt-compact-small ml-auto tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Shortcut.displayName = "DropdownMenu.Shortcut"
|
||||
|
||||
const Hint = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={clx(
|
||||
"text-ui-fg-subtle txt-compact-small ml-auto tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Hint.displayName = "DropdownMenu.Hint"
|
||||
|
||||
const DropdownMenu = Object.assign(Root, {
|
||||
Trigger,
|
||||
Group,
|
||||
SubMenu,
|
||||
SubMenuContent,
|
||||
SubMenuTrigger,
|
||||
Content,
|
||||
Item,
|
||||
CheckboxItem,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
Label,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Hint,
|
||||
})
|
||||
|
||||
export { DropdownMenu }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./dropdown-menu"
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { FocusModal } from "./focus-modal"
|
||||
|
||||
const meta: Meta<typeof FocusModal> = {
|
||||
title: "Components/FocusModal",
|
||||
component: FocusModal,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof FocusModal>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<FocusModal>
|
||||
<FocusModal.Trigger asChild>
|
||||
<Button>Edit Variant</Button>
|
||||
</FocusModal.Trigger>
|
||||
<FocusModal.Content>
|
||||
<FocusModal.Header>
|
||||
<Button>Save</Button>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body></FocusModal.Body>
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
|
||||
import { XMark } from "@medusajs/icons"
|
||||
import * as FocusModalPrimitives from "@radix-ui/react-dialog"
|
||||
import * as React from "react"
|
||||
|
||||
import { IconButton } from "@/components/icon-button"
|
||||
import { Kbd } from "@/components/kbd"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const FocusModalRoot = (
|
||||
props: React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Root>
|
||||
) => {
|
||||
return <FocusModalPrimitives.Root {...props} />
|
||||
}
|
||||
FocusModalRoot.displayName = "FocusModal"
|
||||
|
||||
const FocusModalTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof FocusModalPrimitives.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Trigger>
|
||||
>((props, ref) => {
|
||||
return <FocusModalPrimitives.Trigger ref={ref} {...props} />
|
||||
})
|
||||
FocusModalTrigger.displayName = "FocusModal.Trigger"
|
||||
|
||||
const FocusModalPortal = ({
|
||||
className,
|
||||
...props
|
||||
}: FocusModalPrimitives.DialogPortalProps) => {
|
||||
return (
|
||||
<FocusModalPrimitives.DialogPortal className={clx(className)} {...props} />
|
||||
)
|
||||
}
|
||||
FocusModalPortal.displayName = "FocusModal.Portal"
|
||||
|
||||
const FocusModalOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof FocusModalPrimitives.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Overlay>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<FocusModalPrimitives.Overlay
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-overlay fixed inset-0",
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FocusModalOverlay.displayName = "FocusModal.Overlay"
|
||||
|
||||
const FocusModalContent = React.forwardRef<
|
||||
React.ElementRef<typeof FocusModalPrimitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof FocusModalPrimitives.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<FocusModalPortal>
|
||||
<FocusModalOverlay />
|
||||
<FocusModalPrimitives.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base shadow-elevation-modal fixed inset-2 flex flex-col overflow-hidden rounded-lg border focus:outline-none",
|
||||
// "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 duration-200",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</FocusModalPortal>
|
||||
)
|
||||
})
|
||||
FocusModalContent.displayName = "FocusModal.Content"
|
||||
|
||||
const FocusModalHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"border-ui-border-base flex items-center justify-between gap-x-4 border-b px-4 py-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<FocusModalPrimitives.Close asChild>
|
||||
<IconButton variant="transparent">
|
||||
<XMark />
|
||||
</IconButton>
|
||||
</FocusModalPrimitives.Close>
|
||||
<Kbd>esc</Kbd>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
FocusModalHeader.displayName = "FocusModal.Header"
|
||||
|
||||
const FocusModalBody = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return <div ref={ref} className={clx("flex-1", className)} {...props} />
|
||||
})
|
||||
FocusModalBody.displayName = "FocusModal.Body"
|
||||
|
||||
const FocusModal = Object.assign(FocusModalRoot, {
|
||||
Trigger: FocusModalTrigger,
|
||||
Content: FocusModalContent,
|
||||
Header: FocusModalHeader,
|
||||
Body: FocusModalBody,
|
||||
})
|
||||
|
||||
export { FocusModal }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./focus-modal"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Heading } from "./heading"
|
||||
|
||||
describe("Heading", () => {
|
||||
it("should render a h1 successfully", async () => {
|
||||
render(<Heading>Header</Heading>)
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render a h2 successfully", async () => {
|
||||
render(<Heading level="h2">Header</Heading>)
|
||||
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should render a h3 successfully", async () => {
|
||||
render(<Heading level="h3">Header</Heading>)
|
||||
expect(screen.getByRole("heading", { level: 3 })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Heading } from "./heading"
|
||||
|
||||
const meta: Meta<typeof Heading> = {
|
||||
title: "Components/Heading",
|
||||
component: Heading,
|
||||
argTypes: {
|
||||
level: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: ["h1", "h2", "h3"],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Heading>
|
||||
|
||||
export const H1: Story = {
|
||||
args: {
|
||||
level: "h1",
|
||||
children: "I am a H1 heading",
|
||||
},
|
||||
}
|
||||
|
||||
export const H2: Story = {
|
||||
args: {
|
||||
level: "h2",
|
||||
children: "I am a H2 heading",
|
||||
},
|
||||
}
|
||||
|
||||
export const H3: Story = {
|
||||
args: {
|
||||
level: "h3",
|
||||
children: "I am a H3 heading",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const headingVariants = cva("font-sans font-medium", {
|
||||
variants: {
|
||||
level: {
|
||||
h1: "h1-core",
|
||||
h2: "h2-core",
|
||||
h3: "h3-core",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
level: "h1",
|
||||
},
|
||||
})
|
||||
|
||||
type HeadingProps = VariantProps<typeof headingVariants> &
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
const Heading = ({ level, className, ...props }: HeadingProps) => {
|
||||
const Component = level ? level : "h1"
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={clx(headingVariants({ level }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Heading, headingVariants }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./heading"
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import { Hint } from "./hint"
|
||||
|
||||
const meta: Meta<typeof Hint> = {
|
||||
title: "Components/Hint",
|
||||
component: Hint,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Hint>
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
children: "This is a hint text to help user.",
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
variant: "error",
|
||||
children: "This is a hint text to help user.",
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./hint"
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { BuildingTax } from "@medusajs/icons"
|
||||
import { IconBadge } from "./icon-badge"
|
||||
|
||||
const meta: Meta<typeof IconBadge> = {
|
||||
title: "Components/IconBadge",
|
||||
component: IconBadge,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof IconBadge>
|
||||
|
||||
export const GreyBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "grey",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const GreyLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "grey",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const BlueBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "blue",
|
||||
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const BlueLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "blue",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const GreenBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "green",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const GreenLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "green",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const RedBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "red",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const RedLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "red",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const OrangeBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "orange",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const OrangeLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "orange",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
|
||||
export const PurpleBase: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "purple",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
|
||||
export const PurpleLarge: Story = {
|
||||
args: {
|
||||
children: <BuildingTax />,
|
||||
color: "purple",
|
||||
size: "large",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { badgeColorVariants } from "@/components/badge"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const iconBadgeVariants = cva(
|
||||
"flex items-center justify-center overflow-hidden rounded-md border",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
base: "h-6 w-6",
|
||||
large: "h-7 w-7",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface IconBadgeProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<"span">, "color">,
|
||||
VariantProps<typeof badgeColorVariants>,
|
||||
VariantProps<typeof iconBadgeVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const IconBadge = React.forwardRef<HTMLSpanElement, IconBadgeProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
color = "grey",
|
||||
size = "base",
|
||||
asChild = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Component = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={clx(
|
||||
badgeColorVariants({ color }),
|
||||
iconBadgeVariants({ size }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
)
|
||||
IconBadge.displayName = "IconBadge"
|
||||
|
||||
export { IconBadge }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./icon-badge"
|
||||
@@ -0,0 +1,30 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Plus } from "@medusajs/icons"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
describe("IconButton", () => {
|
||||
it("renders a IconButton", () => {
|
||||
render(
|
||||
<IconButton>
|
||||
<Plus />
|
||||
</IconButton>
|
||||
)
|
||||
const button = screen.getByRole("button")
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders a button as a link", () => {
|
||||
render(
|
||||
<IconButton asChild>
|
||||
<a href="https://www.medusajs.com">
|
||||
<Plus />
|
||||
</a>
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
const button = screen.getByRole("link")
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Plus } from "@medusajs/icons"
|
||||
import { IconButton } from "./icon-button"
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: "Components/IconButton",
|
||||
component: IconButton,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof IconButton>
|
||||
|
||||
export const BasePrimary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const BaseTransparent: Story = {
|
||||
args: {
|
||||
variant: "transparent",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const LargePrimary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "large",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const LargeTransparent: Story = {
|
||||
args: {
|
||||
variant: "transparent",
|
||||
size: "large",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const XLargePrimary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "xlarge",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const XLargeTransparent: Story = {
|
||||
args: {
|
||||
variant: "transparent",
|
||||
size: "xlarge",
|
||||
children: <Plus />,
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const IsLoading: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
children: <Plus />,
|
||||
isLoading: true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Spinner } from "@medusajs/icons"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const iconButtonVariants = cva(
|
||||
clx(
|
||||
"transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none",
|
||||
"disabled:bg-ui-bg-disabled disabled:shadow-buttons-neutral disabled:text-ui-fg-disabled disabled:after:hidden"
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: clx(
|
||||
"shadow-buttons-neutral text-ui-fg-subtle bg-ui-button-neutral after:button-neutral-gradient",
|
||||
"hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient",
|
||||
"active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient",
|
||||
"focus:shadow-buttons-neutral-focus",
|
||||
"after:absolute after:inset-0 after:content-['']"
|
||||
),
|
||||
transparent: clx(
|
||||
"text-ui-fg-subtle bg-ui-button-transparent",
|
||||
"hover:bg-ui-button-transparent-hover",
|
||||
"active:bg-ui-button-transparent-pressed",
|
||||
"focus:shadow-buttons-neutral-focus focus:bg-ui-bg-base",
|
||||
"disabled:!bg-transparent disabled:!shadow-none"
|
||||
),
|
||||
},
|
||||
size: {
|
||||
base: "h-8 w-8 p-1.5",
|
||||
large: "h-10 w-10 p-2.5",
|
||||
xlarge: "h-12 w-12 p-3.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "base",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface IconButtonProps
|
||||
extends React.ComponentPropsWithoutRef<"button">,
|
||||
VariantProps<typeof iconButtonVariants> {
|
||||
asChild?: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
(
|
||||
{
|
||||
variant = "primary",
|
||||
size = "base",
|
||||
asChild = false,
|
||||
className,
|
||||
children,
|
||||
isLoading = false,
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Component = asChild ? Slot : "button"
|
||||
|
||||
/**
|
||||
* In the case of a button where asChild is true, and isLoading is true, we ensure that
|
||||
* only on element is passed as a child to the Slot component. This is because the Slot
|
||||
* component only accepts a single child.
|
||||
*/
|
||||
const renderInner = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<span className="pointer-events-none">
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-disabled absolute inset-0 flex items-center justify-center rounded-md"
|
||||
)}
|
||||
>
|
||||
<Spinner className="animate-spin" />
|
||||
</div>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clx(iconButtonVariants({ variant, size }), className)}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{renderInner()}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
)
|
||||
IconButton.displayName = "IconButton"
|
||||
|
||||
export { IconButton, iconButtonVariants }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./icon-button"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./input"
|
||||
@@ -0,0 +1,11 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Input } from "./input"
|
||||
|
||||
describe("Input", () => {
|
||||
it("should render the component", () => {
|
||||
render(<Input />)
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Input } from "./input"
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "Components/Input",
|
||||
component: Input,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Input>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
placeholder: "Placeholder",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
value: "Floyd Mayweather",
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Invalid: Story = {
|
||||
args: {
|
||||
placeholder: "Placeholder",
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Password: Story = {
|
||||
args: {
|
||||
type: "password",
|
||||
},
|
||||
}
|
||||
|
||||
export const Search: Story = {
|
||||
args: {
|
||||
type: "search",
|
||||
placeholder: "Search",
|
||||
},
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
placeholder: "Placeholder",
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./kbd"
|
||||
@@ -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: "⌘",
|
||||
},
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./label"
|
||||
@@ -0,0 +1,12 @@
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Label } from "./label"
|
||||
|
||||
test("renders a label", () => {
|
||||
render(<Label>I am a label</Label>)
|
||||
|
||||
const text = screen.getByText("I am a label")
|
||||
expect(text).toBeInTheDocument()
|
||||
expect(text.tagName).toBe("LABEL")
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
|
||||
import { Label } from "./label"
|
||||
|
||||
const meta: Meta<typeof Label> = {
|
||||
title: "Components/Label",
|
||||
component: Label,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: ["small", "xsmall", "base", "large"],
|
||||
},
|
||||
weight: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: ["regular", "plus"],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Label>
|
||||
|
||||
export const BaseRegular: Story = {
|
||||
args: {
|
||||
size: "base",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const BasePlus: Story = {
|
||||
args: {
|
||||
size: "base",
|
||||
weight: "plus",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const LargeRegular: Story = {
|
||||
args: {
|
||||
size: "large",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const LargePlus: Story = {
|
||||
args: {
|
||||
size: "large",
|
||||
weight: "plus",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallRegular: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const SmallPlus: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
weight: "plus",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
|
||||
export const XSmallRegular: Story = {
|
||||
args: {
|
||||
size: "xsmall",
|
||||
weight: "regular",
|
||||
children: "I am a label",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import * as Primitives from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const labelVariants = cva("font-sans", {
|
||||
variants: {
|
||||
size: {
|
||||
xsmall: "txt-compact-xsmall",
|
||||
small: "txt-compact-small",
|
||||
base: "txt-compact-medium",
|
||||
large: "txt-compact-large",
|
||||
},
|
||||
weight: {
|
||||
regular: "font-normal",
|
||||
plus: "font-medium",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
weight: "regular",
|
||||
},
|
||||
})
|
||||
|
||||
interface LabelProps
|
||||
extends React.ComponentPropsWithoutRef<"label">,
|
||||
VariantProps<typeof labelVariants> {}
|
||||
|
||||
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, size = "base", weight = "regular", ...props }, ref) => {
|
||||
return (
|
||||
<Primitives.Root
|
||||
ref={ref}
|
||||
className={clx(labelVariants({ size, weight }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Label.displayName = "Label"
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./popover"
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as Primitives from "@radix-ui/react-popover"
|
||||
import * as React from "react"
|
||||
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const Root = (
|
||||
props: React.ComponentPropsWithoutRef<typeof Primitives.Root>
|
||||
) => {
|
||||
return <Primitives.Root {...props} />
|
||||
}
|
||||
Root.displayName = "Popover"
|
||||
|
||||
const Trigger = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Trigger>
|
||||
>((props, ref) => {
|
||||
return <Primitives.Trigger ref={ref} {...props} />
|
||||
})
|
||||
Trigger.displayName = "Popover.Trigger"
|
||||
|
||||
const Anchor = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Anchor>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Anchor>
|
||||
>((props, ref) => {
|
||||
return <Primitives.Anchor ref={ref} {...props} />
|
||||
})
|
||||
Anchor.displayName = "Popover.Anchor"
|
||||
|
||||
const Close = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Close>
|
||||
>((props, ref) => {
|
||||
return <Primitives.Close ref={ref} {...props} />
|
||||
})
|
||||
Close.displayName = "Popover.Close"
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof Primitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitives.Content>
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
sideOffset = 8,
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
collisionPadding,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Primitives.Portal>
|
||||
<Primitives.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
side={side}
|
||||
align={align}
|
||||
collisionPadding={collisionPadding}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout max-h-[var(--radix-popper-available-height)] min-w-[220px] overflow-hidden rounded-lg p-1",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Primitives.Portal>
|
||||
)
|
||||
}
|
||||
)
|
||||
Content.displayName = "Popover.Content"
|
||||
|
||||
const Seperator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clx("bg-ui-border-base -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Seperator.displayName = "Popover.Seperator"
|
||||
|
||||
const Popover = Object.assign(Root, {
|
||||
Trigger,
|
||||
Anchor,
|
||||
Close,
|
||||
Content,
|
||||
Seperator,
|
||||
})
|
||||
|
||||
export { Popover }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./progress-accordion"
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Container } from "@/components/container"
|
||||
import { ProgressAccordion } from "./progress-accordion"
|
||||
|
||||
const meta: Meta<typeof ProgressAccordion> = {
|
||||
title: "Components/ProgressAccordion",
|
||||
component: ProgressAccordion,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof ProgressAccordion>
|
||||
|
||||
const AccordionDemo = () => {
|
||||
const [value, setValue] = React.useState(["1"])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Container className="p-0">
|
||||
<ProgressAccordion
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
type="multiple"
|
||||
className="w-full"
|
||||
>
|
||||
<ProgressAccordion.Item value="1">
|
||||
<ProgressAccordion.Header>Trigger 1</ProgressAccordion.Header>
|
||||
<ProgressAccordion.Content>
|
||||
<div className="pb-6">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. A
|
||||
recusandae officiis aliquam quia, natus saepe obcaecati eligendi
|
||||
non animi fuga culpa, cum unde consequuntur architecto quos
|
||||
reiciendis deleniti eos iste!
|
||||
</div>
|
||||
</ProgressAccordion.Content>
|
||||
</ProgressAccordion.Item>
|
||||
<ProgressAccordion.Item value="2">
|
||||
<ProgressAccordion.Header>Trigger 2</ProgressAccordion.Header>
|
||||
<ProgressAccordion.Content>
|
||||
<div className="pb-6">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. A
|
||||
recusandae officiis aliquam quia, natus saepe obcaecati eligendi
|
||||
non animi fuga culpa, cum unde consequuntur architecto quos
|
||||
reiciendis deleniti eos iste!
|
||||
</div>
|
||||
</ProgressAccordion.Content>
|
||||
</ProgressAccordion.Item>
|
||||
</ProgressAccordion>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return <AccordionDemo />
|
||||
},
|
||||
args: {},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CheckCircleSolid,
|
||||
CircleDottedLine,
|
||||
CircleHalfSolid,
|
||||
Plus,
|
||||
} from "@medusajs/icons"
|
||||
import * as Primitves from "@radix-ui/react-accordion"
|
||||
import * as React from "react"
|
||||
|
||||
import { ProgressStatus } from "@/types"
|
||||
import { clx } from "@/utils/clx"
|
||||
import { IconButton } from "../icon-button"
|
||||
|
||||
const Root = (props: React.ComponentPropsWithoutRef<typeof Primitves.Root>) => {
|
||||
return <Primitves.Root {...props} />
|
||||
}
|
||||
Root.displayName = "ProgressAccordion"
|
||||
|
||||
const Item = React.forwardRef<
|
||||
React.ElementRef<typeof Primitves.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitves.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitves.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"border-ui-border-base border-b last-of-type:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Item.displayName = "ProgressAccordion.Item"
|
||||
|
||||
interface HeaderProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Primitves.Header> {
|
||||
status?: ProgressStatus
|
||||
}
|
||||
|
||||
interface StatusIndicatorProps extends React.ComponentPropsWithoutRef<"span"> {
|
||||
status: ProgressStatus
|
||||
}
|
||||
|
||||
const ProgressIndicator = ({ status, ...props }: StatusIndicatorProps) => {
|
||||
const Icon = React.useMemo(() => {
|
||||
switch (status) {
|
||||
case "not-started":
|
||||
return CircleDottedLine
|
||||
case "in-progress":
|
||||
return CircleHalfSolid
|
||||
case "completed":
|
||||
return CheckCircleSolid
|
||||
default:
|
||||
return CircleDottedLine
|
||||
}
|
||||
}, [status])
|
||||
|
||||
return (
|
||||
<span
|
||||
className="text-ui-fg-muted group-data-[state=open]:text-ui-fg-interactive flex h-12 w-12 items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = React.forwardRef<
|
||||
React.ElementRef<typeof Primitves.Header>,
|
||||
HeaderProps
|
||||
>(({ className, status = "not-started", children, ...props }, ref) => {
|
||||
return (
|
||||
<Primitves.Header
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"h3-core text-ui-fg-base group flex w-full flex-1 items-center gap-4 px-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressIndicator status={status} />
|
||||
{children}
|
||||
<Primitves.Trigger asChild className="ml-auto">
|
||||
<IconButton variant="transparent">
|
||||
<Plus className="transform transition-transform group-data-[state=open]:rotate-45" />
|
||||
</IconButton>
|
||||
</Primitves.Trigger>
|
||||
</Primitves.Header>
|
||||
)
|
||||
})
|
||||
Header.displayName = "ProgressAccordion.Header"
|
||||
|
||||
const Content = React.forwardRef<
|
||||
React.ElementRef<typeof Primitves.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitves.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Primitves.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"overflow-hidden",
|
||||
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down pl-24 pr-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Content.displayName = "ProgressAccordion.Content"
|
||||
|
||||
const ProgressAccordion = Object.assign(Root, {
|
||||
Item,
|
||||
Header,
|
||||
Content,
|
||||
})
|
||||
|
||||
export { ProgressAccordion }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./progress-tabs"
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Container } from "../container"
|
||||
import { ProgressTabs } from "./progress-tabs"
|
||||
|
||||
const meta: Meta<typeof ProgressTabs> = {
|
||||
title: "Components/ProgressTabs",
|
||||
component: ProgressTabs,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof ProgressTabs>
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<div className="h-screen max-h-[500px] w-screen max-w-[700px] overflow-hidden p-4">
|
||||
<Container className="h-full w-full overflow-hidden p-0">
|
||||
<ProgressTabs defaultValue="tab1">
|
||||
<ProgressTabs.List>
|
||||
<ProgressTabs.Trigger value="tab1">Tab 1</ProgressTabs.Trigger>
|
||||
<ProgressTabs.Trigger value="tab2">Tab 2</ProgressTabs.Trigger>
|
||||
<ProgressTabs.Trigger value="tab3" disabled>
|
||||
Tab 3
|
||||
</ProgressTabs.Trigger>
|
||||
</ProgressTabs.List>
|
||||
<div className="txt-compact-medium text-ui-fg-base border-ui-border-base h-full border-t p-3">
|
||||
<ProgressTabs.Content value="tab1">
|
||||
Tab 1 content
|
||||
</ProgressTabs.Content>
|
||||
<ProgressTabs.Content value="tab2">
|
||||
Tab 2 content
|
||||
</ProgressTabs.Content>
|
||||
<ProgressTabs.Content value="tab3">
|
||||
Tab 3 content
|
||||
</ProgressTabs.Content>
|
||||
</div>
|
||||
</ProgressTabs>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <Demo />,
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
CheckCircleSolid,
|
||||
CircleDottedLine,
|
||||
CircleHalfSolid,
|
||||
} from "@medusajs/icons"
|
||||
import * as ProgressTabsPrimitives from "@radix-ui/react-tabs"
|
||||
import * as React from "react"
|
||||
|
||||
import { ProgressStatus } from "@/types"
|
||||
import { clx } from "@/utils/clx"
|
||||
|
||||
const ProgressTabsRoot = (props: ProgressTabsPrimitives.TabsProps) => {
|
||||
return <ProgressTabsPrimitives.Root {...props} />
|
||||
}
|
||||
ProgressTabsRoot.displayName = "ProgressTabs"
|
||||
|
||||
interface IndicatorProps
|
||||
extends Omit<React.ComponentPropsWithoutRef<"span">, "children"> {
|
||||
status?: ProgressStatus
|
||||
}
|
||||
|
||||
const ProgressIndicator = ({ status, className, ...props }: IndicatorProps) => {
|
||||
const Icon = React.useMemo(() => {
|
||||
switch (status) {
|
||||
case "not-started":
|
||||
return CircleDottedLine
|
||||
case "in-progress":
|
||||
return CircleHalfSolid
|
||||
case "completed":
|
||||
return CheckCircleSolid
|
||||
default:
|
||||
return CircleDottedLine
|
||||
}
|
||||
}, [status])
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clx(
|
||||
"text-ui-fg-muted group-data-[state=active]/trigger:text-ui-fg-interactive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProgressTabsTriggerProps
|
||||
extends Omit<
|
||||
React.ComponentPropsWithoutRef<typeof ProgressTabsPrimitives.Trigger>,
|
||||
"asChild"
|
||||
> {
|
||||
status?: ProgressStatus
|
||||
}
|
||||
|
||||
const ProgressTabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressTabsPrimitives.Trigger>,
|
||||
ProgressTabsTriggerProps
|
||||
>(({ className, children, status = "not-started", ...props }, ref) => (
|
||||
<ProgressTabsPrimitives.Trigger
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"txt-compact-small-plus transition-fg text-ui-fg-muted bg-ui-bg-subtle border-r-ui-border-base inline-flex h-14 w-full max-w-[200px] flex-1 items-center gap-x-2 border-r px-4 text-left outline-none",
|
||||
"group/trigger overflow-hidden text-ellipsis whitespace-nowrap",
|
||||
"disabled:bg-ui-bg-disabled disabled:text-ui-fg-muted",
|
||||
"hover:bg-ui-bg-subtle-hover",
|
||||
"focus:bg-ui-bg-base focus:z-[1]",
|
||||
"data-[state=active]:text-ui-fg-base data-[state=active]:bg-ui-bg-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressIndicator status={status} />
|
||||
{children}
|
||||
</ProgressTabsPrimitives.Trigger>
|
||||
))
|
||||
ProgressTabsTrigger.displayName = "ProgressTabs.Trigger"
|
||||
|
||||
const ProgressTabsList = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressTabsPrimitives.List>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressTabsPrimitives.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ProgressTabsPrimitives.List
|
||||
ref={ref}
|
||||
className={clx("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ProgressTabsList.displayName = "ProgressTabs.List"
|
||||
|
||||
const ProgressTabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressTabsPrimitives.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressTabsPrimitives.Content>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<ProgressTabsPrimitives.Content
|
||||
ref={ref}
|
||||
className={clx("outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
ProgressTabsContent.displayName = "ProgressTabs.Content"
|
||||
|
||||
const ProgressTabs = Object.assign(ProgressTabsRoot, {
|
||||
Trigger: ProgressTabsTrigger,
|
||||
List: ProgressTabsList,
|
||||
Content: ProgressTabsContent,
|
||||
})
|
||||
|
||||
export { ProgressTabs }
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./prompt"
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
RenderResult,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
} from "@testing-library/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Prompt } from "./prompt"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
|
||||
const TRIGGER_TEXT = "Open"
|
||||
const TITLE_TEXT = "Delete something"
|
||||
const DESCRIPTION_TEXT = "Are you sure? This cannot be undone."
|
||||
const CANCEL_TEXT = "Cancel"
|
||||
const CONFIRM_TEXT = "Confirm"
|
||||
|
||||
describe("Prompt", () => {
|
||||
let rendered: RenderResult
|
||||
let trigger: HTMLElement
|
||||
|
||||
beforeEach(() => {
|
||||
rendered = render(
|
||||
<Prompt>
|
||||
<Prompt.Trigger asChild>
|
||||
<Button>{TRIGGER_TEXT}</Button>
|
||||
</Prompt.Trigger>
|
||||
<Prompt.Content>
|
||||
<Prompt.Header>
|
||||
<Prompt.Title>{TITLE_TEXT}</Prompt.Title>
|
||||
<Prompt.Description>{DESCRIPTION_TEXT}</Prompt.Description>
|
||||
</Prompt.Header>
|
||||
<Prompt.Footer>
|
||||
<Prompt.Cancel>{CANCEL_TEXT}</Prompt.Cancel>
|
||||
<Prompt.Action>{CONFIRM_TEXT}</Prompt.Action>
|
||||
</Prompt.Footer>
|
||||
</Prompt.Content>
|
||||
</Prompt>
|
||||
)
|
||||
|
||||
trigger = rendered.getByText(TRIGGER_TEXT)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it("renders a basic alert dialog when the trigger is clicked", async () => {
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const title = await rendered.findByText(TITLE_TEXT)
|
||||
const description = await rendered.findByText(DESCRIPTION_TEXT)
|
||||
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(description).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("close the dialog when the cancel button is clicked", async () => {
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const title = rendered.queryByText(TITLE_TEXT)
|
||||
const description = rendered.queryByText(DESCRIPTION_TEXT)
|
||||
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(description).toBeInTheDocument()
|
||||
|
||||
const cancelButton = await rendered.findByText(CANCEL_TEXT)
|
||||
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(title).not.toBeInTheDocument()
|
||||
expect(description).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("close the dialog when the confirm button is clicked", async () => {
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const title = rendered.queryByText(TITLE_TEXT)
|
||||
const description = rendered.queryByText(DESCRIPTION_TEXT)
|
||||
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(description).toBeInTheDocument()
|
||||
|
||||
const confirmButton = await rendered.findByText(CONFIRM_TEXT)
|
||||
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
expect(title).not.toBeInTheDocument()
|
||||
expect(description).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/button"
|
||||
import { Prompt } from "./prompt"
|
||||
|
||||
const meta: Meta<typeof Prompt> = {
|
||||
title: "Components/Prompt",
|
||||
component: Prompt,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof Prompt>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<Prompt>
|
||||
<Prompt.Trigger asChild>
|
||||
<Button>Open</Button>
|
||||
</Prompt.Trigger>
|
||||
<Prompt.Content>
|
||||
<Prompt.Header>
|
||||
<Prompt.Title>Delete something</Prompt.Title>
|
||||
<Prompt.Description>
|
||||
Are you sure? This cannot be undone.
|
||||
</Prompt.Description>
|
||||
</Prompt.Header>
|
||||
<Prompt.Footer>
|
||||
<Prompt.Cancel>Cancel</Prompt.Cancel>
|
||||
<Prompt.Action>Delete</Prompt.Action>
|
||||
</Prompt.Footer>
|
||||
</Prompt.Content>
|
||||
</Prompt>
|
||||
)
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user