├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── pull_request_template.md └── workflows │ └── playwright.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.ts ├── manager.ts └── preview.ts ├── LICENSE ├── README.md ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── images │ ├── github-banner.png │ ├── tremor-logo-dark.svg │ └── tremor-logo-light.svg ├── scripts └── component-folder.js ├── src ├── components │ ├── Accordion │ │ ├── Accordion.tsx │ │ ├── accordion.spec.ts │ │ ├── accordion.stories.tsx │ │ └── changelog.md │ ├── AreaChart │ │ ├── AreaChart.tsx │ │ ├── areachart.spec.ts │ │ ├── areachart.stories.tsx │ │ └── changelog.md │ ├── Badge │ │ ├── Badge.tsx │ │ ├── badge.spec.ts │ │ ├── badge.stories.tsx │ │ └── changelog.md │ ├── BarChart │ │ ├── BarChart.tsx │ │ ├── barchart.spec.ts │ │ ├── barchart.stories.tsx │ │ └── changelog.md │ ├── BarList │ │ ├── BarList.tsx │ │ ├── barlist.spec.ts │ │ ├── barlist.stories.tsx │ │ └── changelog.md │ ├── Button │ │ ├── Button.tsx │ │ ├── button.spec.ts │ │ ├── button.stories.tsx │ │ └── changelog.md │ ├── Calendar │ │ ├── Calendar.tsx │ │ ├── calendar.spec.ts │ │ ├── calendar.stories.tsx │ │ └── changelog.md │ ├── Callout │ │ ├── Callout.tsx │ │ ├── callout.spec.ts │ │ ├── callout.stories.tsx │ │ └── changelog.md │ ├── Card │ │ ├── Card.tsx │ │ ├── card.spec.ts │ │ ├── card.stories.tsx │ │ └── changelog.md │ ├── CategoryBar │ │ ├── CategoryBar.tsx │ │ ├── categorybar.spec.ts │ │ ├── categorybar.stories.tsx │ │ └── changelog.md │ ├── Checkbox │ │ ├── Checkbox.tsx │ │ ├── changelog.md │ │ ├── checkbox.spec.ts │ │ └── checkbox.stories.tsx │ ├── ComboChart │ │ ├── ComboChart.tsx │ │ ├── changelog.md │ │ ├── combochart.spec.ts │ │ └── combochart.stories.tsx │ ├── DatePicker │ │ ├── DatePicker.tsx │ │ ├── changelog.md │ │ ├── datepicker.spec.ts │ │ ├── datepicker.stories.tsx │ │ └── daterangepicker.stories.tsx │ ├── Dialog │ │ ├── Dialog.tsx │ │ ├── changelog.md │ │ ├── dialog.spec.ts │ │ └── dialog.stories.tsx │ ├── Divider │ │ ├── Divider.tsx │ │ ├── changelog.md │ │ ├── divider.spec.ts │ │ └── divider.stories.tsx │ ├── DonutChart │ │ ├── DonutChart.tsx │ │ ├── changelog.md │ │ ├── donutchart.spec.ts │ │ └── donutchart.stories.tsx │ ├── Drawer │ │ ├── Drawer.tsx │ │ ├── changelog.md │ │ ├── drawer.spec.ts │ │ └── drawer.stories.tsx │ ├── DropdownMenu │ │ ├── DropdownMenu.tsx │ │ ├── changelog.md │ │ ├── dropdownmenu.spec.ts │ │ └── dropdownmenu.stories.tsx │ ├── Input │ │ ├── Input.tsx │ │ ├── changelog.md │ │ ├── input.spec.ts │ │ └── input.stories.tsx │ ├── Label │ │ ├── Label.tsx │ │ ├── changelog.md │ │ ├── label.spec.ts │ │ └── label.stories.tsx │ ├── LineChart │ │ ├── LineChart.tsx │ │ ├── changelog.md │ │ ├── linechart.spec.ts │ │ └── linechart.stories.tsx │ ├── Popover │ │ ├── Popover.tsx │ │ ├── changelog.md │ │ ├── popover.spec.ts │ │ └── popover.stories.tsx │ ├── ProgressBar │ │ ├── ProgressBar.tsx │ │ ├── changelog.md │ │ ├── progressbar.spec.ts │ │ └── progressbar.stories.tsx │ ├── ProgressCircle │ │ ├── ProgressCircle.tsx │ │ ├── changelog.md │ │ ├── progresscircle.spec.ts │ │ └── progresscircle.stories.tsx │ ├── RadioCardGroup │ │ ├── RadioCardGroup.tsx │ │ ├── changelog.md │ │ ├── radiocardgroup.spec.ts │ │ └── radiocardgroup.stories.tsx │ ├── RadioGroup │ │ ├── RadioGroup.tsx │ │ ├── changelog.md │ │ ├── radiogroup.spec.ts │ │ └── radiogroup.stories.tsx │ ├── Select │ │ ├── Select.tsx │ │ ├── changelog.md │ │ ├── select.spec.ts │ │ └── select.stories.tsx │ ├── SelectNative │ │ ├── SelectNative.tsx │ │ ├── changelog.md │ │ ├── selectnative.spec.ts │ │ └── selectnative.stories.tsx │ ├── Slider │ │ ├── Slider.tsx │ │ ├── changelog.md │ │ ├── slider.spec.ts │ │ └── slider.stories.tsx │ ├── SparkChart │ │ ├── SparkChart.tsx │ │ ├── changelog.md │ │ ├── sparkareachart.stories.tsx │ │ ├── sparkbarchart.stories.tsx │ │ ├── sparkchart.spec.ts │ │ └── sparklinechart.stories.tsx │ ├── Switch │ │ ├── Switch.tsx │ │ ├── changelog.md │ │ ├── switch.spec.ts │ │ └── switch.stories.tsx │ ├── TabNavigation │ │ ├── TabNavigation.tsx │ │ ├── changelog.md │ │ ├── tabnavigation.spec.tsx │ │ └── tabnavigation.stories.tsx │ ├── Table │ │ ├── Table.tsx │ │ ├── changelog.md │ │ ├── table.spec.ts │ │ └── table.stories.tsx │ ├── Tabs │ │ ├── Tabs.tsx │ │ ├── changelog.md │ │ ├── tabs.spec.tsx │ │ └── tabs.stories.tsx │ ├── Textarea │ │ ├── Textarea.tsx │ │ ├── changelog.md │ │ ├── textarea.spec.ts │ │ └── textarea.stories.tsx │ ├── Toast │ │ ├── Toast.tsx │ │ ├── Toaster.tsx │ │ ├── changelog.md │ │ └── toast.stories.tsx │ ├── Toggle │ │ ├── Toggle.tsx │ │ ├── changelog.md │ │ ├── toggle.spec.ts │ │ └── toggle.stories.tsx │ ├── Tooltip │ │ ├── Tooltip.tsx │ │ ├── changelog.md │ │ ├── tooltip.spec.ts │ │ └── tooltip.stories.tsx │ └── Tracker │ │ ├── Tracker.tsx │ │ ├── changelog.md │ │ ├── tracker.spec.ts │ │ └── tracker.stories.tsx ├── globals.css ├── hooks │ ├── changelog.md │ ├── useOnWindowResize.ts │ └── useToast.ts └── utils │ ├── changelog.md │ ├── chartColors.ts │ ├── cx.ts │ ├── focusInput.ts │ ├── focusRing.ts │ ├── getYAxisDomain.ts │ ├── hasErrorInput.ts │ ├── hasOnlyOneValueForKey.ts │ └── utils.spec.ts ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json ├── vite.config.ts └── vitest.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | "plugin:storybook/recommended", 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs", "node_modules"], 11 | parser: "@typescript-eslint/parser", 12 | plugins: ["react-refresh"], 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug report" 2 | description: Create a report to help us improve 3 | title: "[Bug]: " 4 | labels: ["Triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Before submitting a bug report** 10 | 11 | The issue list is reserved for bug reports and feature requests. If you have a usage question, you can: 12 | 13 | - Read the [documentation](https://www.tremor.so/docs/getting-started/installation) 14 | – Also try to search for your issue/feature - another user may have already requested something similar, thanks! 15 | 16 | - type: input 17 | id: version 18 | attributes: 19 | label: Tremor Raw Component Version 20 | validations: 21 | required: false 22 | - type: input 23 | id: reproduction-link 24 | attributes: 25 | label: Link to minimal reproduction 26 | description: | 27 | The easiest way to provide a reproduction is provide a link to a public GitHub repository or a tools like [Stackblitz](https://stackblitz.com) or [Codesandbox](https://codesandbox.io). 28 | 29 | The reproduction should be **minimal**. This means, it should contain only the bare minimum amount of code needed 30 | to show the bug. 31 | placeholder: Reproduction Link 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: steps-to-reproduce 36 | attributes: 37 | label: Steps to reproduce 38 | description: | 39 | What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code. 40 | placeholder: Steps to reproduce 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: expected 45 | attributes: 46 | label: What is expected? 47 | validations: 48 | required: true 49 | - type: textarea 50 | id: actually-happening 51 | attributes: 52 | label: What is actually happening? 53 | validations: 54 | required: true 55 | - type: dropdown 56 | id: browsers 57 | attributes: 58 | label: What browsers are you seeing the problem on? 59 | multiple: true 60 | options: 61 | - Chrome 62 | - Microsoft Edge 63 | - Safari 64 | - Firefox 65 | - Vivaldi 66 | - Brave 67 | - Other 68 | - type: textarea 69 | id: additional-comments 70 | attributes: 71 | label: Any additional comments? 72 | description: E.g. some background/context of how you ran into this bug. 73 | - type: markdown 74 | attributes: 75 | value: | 76 | This bug report template was inspired by the issue template from [vuejs](https://github.com/vuejs/core) 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Slack Community 4 | url: https://join.slack.com/t/tremor-community/shared_invite/zt-1u8jqmcmq-Fdr9B6MbnO7u8FkGh~2Ylg 5 | about: Please ask and answer usage questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 New feature proposal" 2 | description: Suggest an idea for this project 3 | title: "[Feature]: " 4 | labels: ["Triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Before submitting a feature request** 10 | 11 | The issue list is reserved for bug reports and feature requests. If you have a usage question, you can: 12 | 13 | - Read the [documentation](https://www.tremor.so/docs/getting-started/installation) 14 | – Also try to search for your issue/feature - another user may have already requested something similar, thanks! 15 | 16 | - type: textarea 17 | id: problem-description 18 | attributes: 19 | label: What problem does this feature solve? 20 | description: | 21 | Explain your use case, context, and rationale behind this feature request. 22 | 23 | An important design goal of Tremor is keeping the API small. The problem should be common enough to justify the addition. 24 | placeholder: Problem description 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: proposed-API 29 | attributes: 30 | label: What does the proposed API look like? 31 | description: | 32 | Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format your code blocks. 33 | placeholder: Assumed API 34 | - type: markdown 35 | attributes: 36 | value: | 37 | This bug report template was inspired by the feature template from [vuejs](https://github.com/vuejs/core) 38 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Description** 2 | 3 | 4 | 5 | **Related issue(s)** 6 | 7 | 8 | 9 | 10 | 11 | 12 | **What kind of change does this PR introduce?** (check at least one) 13 | 14 | 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New Feature (non-breaking change which adds functionality) 18 | - [ ] New Feature (BREAKING CHANGE which adds functionality) 19 | - [ ] Refactor 20 | - [ ] Build-related changes 21 | - [ ] Other, please describe: 22 | 23 | **Does this PR introduce a breaking change?** (check one) 24 | 25 | - [ ] Yes 26 | - [ ] No 27 | 28 | If yes, please describe the impact and migration path for existing applications: 29 | 30 | **How has this been tested?** 31 | 32 | 33 | 34 | 35 | 36 | **Screenshots (if appropriate):** 37 | 38 | **The PR fulfils these requirements:** 39 | 40 | - [ ] It's submitted to the `main` branch 41 | - [ ] Add refs #XXX or fixes #XXX to the related issue section if your PR refers to or fixes an issue. 42 | - [ ] My change requires a change to the documentation. (Managed by Tremor Team) 43 | - [ ] I have added tests to cover my changes 44 | - [ ] Check the ["Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating your PR. 45 | - [ ] By contributing to Tremor, you confirm that you have read and agreed to Tremor's [CONTRIBUTING.md](https://github.com/tremorlabs/tremor-npm/blob/main/CONTRIBUTING.md) guideline. You also agree that your contributions will be licensed under the [Apache License 2.0](https://github.com/tremorlabs/tremor?tab=Apache-2.0-1-ov-file#readme) license. 46 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: lts/* 16 | - name: Install dependencies 17 | run: npm install -g pnpm && pnpm install 18 | - name: Install Playwright Browsers 19 | run: pnpm exec playwright install --with-deps 20 | - name: Run Playwright tests 21 | run: pnpm exec playwright test 22 | - uses: actions/upload-artifact@v4 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | *storybook.log 27 | 28 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 29 | 30 | # dependencies 31 | /node_modules 32 | /.pnp 33 | .pnp.js 34 | .yarn/install-state.gz 35 | 36 | # testing 37 | /coverage 38 | 39 | # next.js 40 | /.next/ 41 | /out/ 42 | 43 | # production 44 | /build 45 | 46 | # misc 47 | .DS_Store 48 | *.pem 49 | 50 | # debug 51 | npm-debug.log* 52 | yarn-debug.log* 53 | yarn-error.log* 54 | 55 | # local env files 56 | .env*.local 57 | 58 | # vercel 59 | .vercel 60 | 61 | # typescript 62 | *.tsbuildinfo 63 | next-env.d.ts 64 | 65 | *storybook.log 66 | /test-results/ 67 | /playwright-report/ 68 | /blob-report/ 69 | /playwright/.cache/ 70 | /test-results/ 71 | /playwright-report/ 72 | /blob-report/ 73 | /playwright/.cache/ 74 | /storybook-static/ 75 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | build -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": false, 4 | "trailingComma": "all", 5 | "endOfLine": "lf", 6 | "semi": false, 7 | "tabWidth": 2, 8 | "importOrder": [ 9 | "^(react/(.*)$)|^(react$)", 10 | "^(next/(.*)$)|^(next$)", 11 | "^(radix/(.*)$)|^(radix$)", 12 | "", 13 | "", 14 | "^types$", 15 | "^@/types/(.*)$", 16 | "^@/config/(.*)$", 17 | "^@/lib/(.*)$", 18 | "^@/hooks/(.*)$", 19 | "^@/components/ui/(.*)$", 20 | "^@/components/(.*)$", 21 | "^@/registry/(.*)$", 22 | "^@/styles/(.*)$", 23 | "^@/app/(.*)$", 24 | "", 25 | "^[./]" 26 | ], 27 | "importOrderSeparation": false, 28 | "importOrderSortSpecifiers": true, 29 | "importOrderBuiltinModulesToTop": true, 30 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], 31 | "importOrderMergeDuplicateImports": true, 32 | "importOrderCombineTypeAndValueImports": true, 33 | "plugins": [ 34 | "@ianvs/prettier-plugin-sort-imports", 35 | "prettier-plugin-tailwindcss" 36 | ], 37 | "tailwindFunctions": ["tv", "cx"] 38 | } 39 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite" 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | 6 | addons: [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions", 10 | "@storybook/addon-a11y", 11 | ], 12 | 13 | framework: { 14 | name: "@storybook/react-vite", 15 | options: {}, 16 | }, 17 | 18 | docs: {}, 19 | 20 | typescript: { 21 | reactDocgen: "react-docgen-typescript", 22 | }, 23 | } 24 | export default config 25 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/manager-api" 2 | 3 | addons.setConfig({ 4 | panelPosition: "bottom", 5 | initialActive: "canvas", 6 | }) 7 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import { themes } from "@storybook/theming"; 3 | 4 | import "../src/globals.css"; 5 | 6 | const preview: Preview = { 7 | parameters: { 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/i, 12 | }, 13 | }, 14 | docs: { 15 | theme: window.matchMedia("(prefers-color-scheme: dark)").matches 16 | ? themes.dark 17 | : themes.light, 18 | }, 19 | backgrounds: { 20 | default: "white", 21 | values: [ 22 | { 23 | name: "white", 24 | value: "#ffffff", 25 | }, 26 | { 27 | name: "gray 950", 28 | value: "#030712", 29 | }, 30 | { 31 | name: "gray 900", 32 | value: "#111827", 33 | }, 34 | ], 35 | }, 36 | }, 37 | }; 38 | 39 | export default preview; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 | 6 | 7 | 8 | Tremor Logo 9 | 10 | 11 |

12 |
13 |
14 | 15 | Read the documentation 16 | 17 | 18 | License Apache 2.0 19 | 20 | 21 | Follow at Tremorlabs 22 | 23 |
24 |
25 |
26 | 27 |

Copy & Paste React components to build
charts and dashboards

28 | 29 | [Tremor](https://tremor.so/) offers 35+ customizable, accessible React components to build dashboards and modern web applications. Built on top of Tailwind CSS and Radix UI. 30 | 31 |
32 | 33 | ![Tremor Banner](public/images/github-banner.png) 34 | 35 |
36 | 37 | ## Getting Started 38 | 39 | See our [Installation Guide](https://tremor.so/docs/getting-started/installation) to get started. 40 | 41 | ## Socials 42 | 43 | - [Tremor Website](https://tremor.so) 44 | - [Tremor on X (formerly Twitter)](https://twitter.com/tremorlabs) 45 | - [Tremor on Slack](https://tremor.so/slack) 46 | 47 | ## Community and Contribution 48 | 49 | We are always looking for new ideas or other ways to improve Tremor Raw. If you have developed anything cool or found a bug, send us a pull request. Check out our Contributor License Agreement [here](https://www.tremor.so/contributors). 50 | 51 | ## License 52 | 53 | [Apache License 2.0](https://github.com/tremorlabs/tremor?tab=Apache-2.0-1-ov-file#readme) 54 | 55 | Copyright © 2025 Tremor. All rights reserved. 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tremor-raw", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "storybook": "storybook dev -p 6006", 12 | "build-storybook": "storybook build", 13 | "component-folder": "node ./scripts/component-folder.js ./src", 14 | "test:all": "npx vitest run && npx playwright test" 15 | }, 16 | "dependencies": { 17 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1", 18 | "@internationalized/date": "^3.8.0", 19 | "@radix-ui/react-accordion": "^1.2.4", 20 | "@radix-ui/react-checkbox": "^1.1.5", 21 | "@radix-ui/react-dialog": "^1.1.7", 22 | "@radix-ui/react-dropdown-menu": "^2.1.7", 23 | "@radix-ui/react-hover-card": "^1.1.7", 24 | "@radix-ui/react-label": "^2.1.3", 25 | "@radix-ui/react-navigation-menu": "^1.2.6", 26 | "@radix-ui/react-popover": "^1.1.7", 27 | "@radix-ui/react-radio-group": "^1.2.4", 28 | "@radix-ui/react-select": "^2.1.7", 29 | "@radix-ui/react-slider": "^1.2.4", 30 | "@radix-ui/react-slot": "^1.2.0", 31 | "@radix-ui/react-switch": "^1.1.4", 32 | "@radix-ui/react-tabs": "^1.1.4", 33 | "@radix-ui/react-toast": "^1.2.7", 34 | "@radix-ui/react-toggle": "^1.1.3", 35 | "@radix-ui/react-toggle-group": "^1.1.3", 36 | "@radix-ui/react-tooltip": "^1.2.0", 37 | "@react-aria/datepicker": "^3.14.2", 38 | "@react-stately/datepicker": "^3.14.0", 39 | "@remixicon/react": "^4.6.0", 40 | "@storybook/addon-a11y": "^8.6.12", 41 | "@storybook/manager-api": "^8.6.12", 42 | "@storybook/theming": "^8.6.12", 43 | "clsx": "^2.1.1", 44 | "date-fns": "^3.6.0", 45 | "prettier-plugin-tailwindcss": "^0.6.11", 46 | "react": "^18.3.1", 47 | "react-day-picker": "^8.10.1", 48 | "react-dom": "^18.3.1", 49 | "recharts": "^2.15.2", 50 | "tailwind-merge": "^3.2.0", 51 | "tailwind-variants": "^1.0.0" 52 | }, 53 | "devDependencies": { 54 | "@playwright/test": "^1.51.1", 55 | "@storybook/addon-essentials": "^8.6.12", 56 | "@storybook/addon-interactions": "^8.6.12", 57 | "@storybook/addon-links": "^8.6.12", 58 | "@storybook/blocks": "^8.6.12", 59 | "@storybook/react": "^8.6.12", 60 | "@storybook/react-vite": "^8.6.12", 61 | "@tailwindcss/postcss": "^4.1.3", 62 | "@types/node": "^22.14.1", 63 | "@types/react": "^18.3.20", 64 | "@types/react-dom": "^18.3.6", 65 | "@typescript-eslint/eslint-plugin": "^8.29.1", 66 | "@typescript-eslint/parser": "^8.29.1", 67 | "@vitejs/plugin-react": "^4.3.4", 68 | "eslint": "^8.57.1", 69 | "eslint-plugin-react-hooks": "^4.6.2", 70 | "eslint-plugin-react-refresh": "^0.4.19", 71 | "eslint-plugin-storybook": "^0.12.0", 72 | "postcss": "^8.5.3", 73 | "prettier": "3.5.3", 74 | "storybook": "^8.6.12", 75 | "tailwindcss": "^4.1.3", 76 | "typescript": "^5.8.3", 77 | "vite": "^5.4.18", 78 | "vite-tsconfig-paths": "^5.1.4", 79 | "vitest": "^2.1.9" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test" 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: "./src/components", 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: "html", 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: "on-first-retry", 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: "chromium", 38 | use: { ...devices["Desktop Chrome"] }, 39 | }, 40 | 41 | { 42 | name: "firefox", 43 | use: { ...devices["Desktop Firefox"] }, 44 | }, 45 | 46 | { 47 | name: "webkit", 48 | use: { ...devices["Desktop Safari"] }, 49 | }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | 61 | /* Test against branded browsers. */ 62 | // { 63 | // name: 'Microsoft Edge', 64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 65 | // }, 66 | // { 67 | // name: 'Google Chrome', 68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 69 | // }, 70 | ], 71 | 72 | /* Run your local dev server before starting the tests */ 73 | webServer: { 74 | command: "npm run storybook", 75 | url: "http://localhost:6006", 76 | reuseExistingServer: !process.env.CI, 77 | }, 78 | }) 79 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/images/github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tremorlabs/tremor/16eb81cd4f4ee7cceb7e93ab78f04aa5f285d7c1/public/images/github-banner.png -------------------------------------------------------------------------------- /public/images/tremor-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/images/tremor-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/component-folder.js: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as path from "path" 3 | 4 | const sourceDirectory = process.argv[2] 5 | const targetDirectoryBase = "src/" 6 | const utilsImportRegex = 7 | /import\s+{([^}]+)}\s+from\s+["']\.\.\/\.\.\/utils\/([^'"]+)["']?\s*/g 8 | 9 | const consolidateImports = (content) => { 10 | const importMatches = [...content.matchAll(utilsImportRegex)] 11 | if (importMatches.length === 0) { 12 | return content // No utility imports found, return original content 13 | } 14 | 15 | // Consolidate all imports into a single object 16 | const allImports = importMatches.reduce((acc, match) => { 17 | match[1].split(",").forEach((importName) => { 18 | const trimmedName = importName.trim() 19 | if (trimmedName) acc[trimmedName] = true 20 | }) 21 | return acc 22 | }, {}) 23 | 24 | // Create the consolidated import statement with an extra newline at the end 25 | const consolidatedImport = `import { ${Object.keys(allImports).join(", ")} } from "@/lib/utils"\n\n` 26 | 27 | // Replace the first import and remove the rest 28 | let firstImportIndex = content.indexOf(importMatches[0][0]) 29 | let beforeFirstImport = content.substring(0, firstImportIndex) 30 | let afterFirstImport = content 31 | .substring(firstImportIndex) 32 | .replace(utilsImportRegex, "") 33 | return `${beforeFirstImport}${consolidatedImport}${afterFirstImport}` 34 | } 35 | 36 | const copyAndModifyTsxFiles = (sourceDir, targetDirBase) => { 37 | const targetDir = path.join(targetDirBase, "components-folder") 38 | 39 | const createDirRecursive = (dirPath) => { 40 | if (!fs.existsSync(dirPath)) { 41 | fs.mkdirSync(dirPath, { recursive: true }) 42 | } 43 | } 44 | 45 | const copyAndModifyFile = (source, target) => { 46 | let content = fs.readFileSync(source, { encoding: "utf8" }) 47 | content = consolidateImports(content) 48 | fs.writeFileSync(target, content) 49 | console.log(`Processed and copied: ${source} -> ${target}`) 50 | } 51 | 52 | const isTsxFile = (filePath) => 53 | filePath.endsWith(".tsx") && /^[A-Z]/.test(path.basename(filePath)) 54 | 55 | const traverseDir = (currentPath) => { 56 | const contents = fs.readdirSync(currentPath, { withFileTypes: true }) 57 | contents.forEach((dirent) => { 58 | const fullPath = path.join(currentPath, dirent.name) 59 | if (dirent.isDirectory()) { 60 | traverseDir(fullPath) 61 | } else if (isTsxFile(fullPath)) { 62 | const targetPath = path.join(targetDir, path.basename(fullPath)) 63 | createDirRecursive(path.dirname(targetPath)) 64 | copyAndModifyFile(fullPath, targetPath) 65 | } 66 | }) 67 | } 68 | 69 | createDirRecursive(targetDir) 70 | traverseDir(sourceDir) 71 | } 72 | 73 | if (!sourceDirectory) { 74 | console.error("Usage: node script.js ") 75 | process.exit(1) 76 | } 77 | 78 | copyAndModifyTsxFiles(sourceDirectory, targetDirectoryBase) 79 | -------------------------------------------------------------------------------- /src/components/Accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Accordion [v1.0.0] 2 | 3 | import React from "react" 4 | import * as AccordionPrimitives from "@radix-ui/react-accordion" 5 | import { RiAddLine } from "@remixicon/react" 6 | 7 | import { cx } from "../../utils/cx" 8 | 9 | const Accordion = AccordionPrimitives.Root 10 | 11 | Accordion.displayName = "AccordionItem" 12 | 13 | const AccordionTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, children, ...props }, forwardedRef) => ( 17 | 18 | 33 | {children} 34 | 47 | 48 | )) 49 | 50 | AccordionTrigger.displayName = "AccordionTrigger" 51 | 52 | const AccordionContent = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, children, ...props }, forwardedRef) => ( 56 | 63 |
72 | {children} 73 |
74 |
75 | )) 76 | 77 | AccordionContent.displayName = "AccordionContent" 78 | 79 | const AccordionItem = React.forwardRef< 80 | React.ElementRef, 81 | React.ComponentPropsWithoutRef 82 | >(({ className, ...props }, forwardedRef) => ( 83 | 95 | )) 96 | 97 | AccordionItem.displayName = "AccordionItem" 98 | 99 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } 100 | -------------------------------------------------------------------------------- /src/components/Accordion/accordion.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto("http://localhost:6006/?path=/story/ui-accordion--default") 5 | }) 6 | 7 | test.describe("Expect default accordion", () => { 8 | test("to be rendered", async ({ page }) => { 9 | await expect( 10 | page 11 | .frameLocator('iframe[title="storybook-preview-iframe"]') 12 | .getByRole("button", { name: "In the app" }), 13 | ).toBeVisible() 14 | await expect( 15 | page 16 | .frameLocator('iframe[title="storybook-preview-iframe"]') 17 | .getByRole("button", { name: "Via browser extension" }), 18 | ).toBeVisible() 19 | }) 20 | 21 | test("to be open and closeable", async ({ page }) => { 22 | await page 23 | .frameLocator('iframe[title="storybook-preview-iframe"]') 24 | .getByRole("button", { name: "In the app" }) 25 | .click() 26 | await expect( 27 | page 28 | .frameLocator('iframe[title="storybook-preview-iframe"]') 29 | .getByText("Step 1:"), 30 | ).toBeVisible() 31 | await expect( 32 | page 33 | .frameLocator('iframe[title="storybook-preview-iframe"]') 34 | .getByText("Step 2:"), 35 | ).toBeVisible() 36 | await page 37 | .frameLocator('iframe[title="storybook-preview-iframe"]') 38 | .getByRole("button", { name: "In the app" }) 39 | .click() 40 | }) 41 | 42 | test("have a disabled item", async ({ page }) => { 43 | await expect( 44 | page 45 | .frameLocator('iframe[title="storybook-preview-iframe"]') 46 | .getByRole("button", { name: "Via email forwarding" }), 47 | ).toBeDisabled() 48 | }) 49 | }) 50 | 51 | test.describe("Expect accordion multiple to", () => { 52 | test("have all elements opened", async ({ page }) => { 53 | await page.goto( 54 | "http://localhost:6006/?path=/story/ui-accordion--type-multiple", 55 | ) 56 | await page 57 | .frameLocator('iframe[title="storybook-preview-iframe"]') 58 | .getByRole("button", { name: "Does NASA provide public" }) 59 | .click() 60 | await page 61 | .frameLocator('iframe[title="storybook-preview-iframe"]') 62 | .getByRole("button", { name: "Are NASA's educational" }) 63 | .click() 64 | await page 65 | .frameLocator('iframe[title="storybook-preview-iframe"]') 66 | .getByRole("button", { name: "Can the public participate in" }) 67 | .click() 68 | await expect( 69 | page 70 | .frameLocator('iframe[title="storybook-preview-iframe"]') 71 | .getByText("Absolutely. NASA offers open"), 72 | ).toBeVisible() 73 | await expect( 74 | page 75 | .frameLocator('iframe[title="storybook-preview-iframe"]') 76 | .getByText("Yes. NASA provides a wide"), 77 | ).toBeVisible() 78 | await expect( 79 | page 80 | .frameLocator('iframe[title="storybook-preview-iframe"]') 81 | .getByText("Yes! Through various citizen"), 82 | ).toBeVisible() 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/components/Accordion/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Accordion Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.1 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | -------------------------------------------------------------------------------- /src/components/AreaChart/areachart.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto( 5 | "http://localhost:6006/?path=/story/visualization-areachart--default", 6 | ) 7 | }) 8 | 9 | test.describe("Expect default area chart", () => { 10 | test("to be rendered", async ({ page }) => { 11 | await expect( 12 | page 13 | .frameLocator('iframe[title="storybook-preview-iframe"]') 14 | .getByTestId("area-chart"), 15 | ).toBeVisible() 16 | }) 17 | 18 | test("to render legend two items", async ({ page }) => { 19 | await expect( 20 | page 21 | .frameLocator('iframe[title="storybook-preview-iframe"]') 22 | .locator("li") 23 | .filter({ hasText: "SolarCells" }), 24 | ).toBeVisible() 25 | await expect( 26 | page 27 | .frameLocator('iframe[title="storybook-preview-iframe"]') 28 | .locator("li") 29 | .filter({ hasText: "Glass" }), 30 | ).toBeVisible() 31 | }) 32 | 33 | test("to render an x-axis", async ({ page }) => { 34 | await expect( 35 | page 36 | .frameLocator('iframe[title="storybook-preview-iframe"]') 37 | .locator(".recharts-xAxis"), 38 | ).toHaveClass(/recharts-xAxis/) 39 | }) 40 | 41 | test("to render an y-axis", async ({ page }) => { 42 | await expect( 43 | page 44 | .frameLocator('iframe[title="storybook-preview-iframe"]') 45 | .locator(".recharts-yAxis"), 46 | ).toHaveClass(/recharts-yAxis/) 47 | }) 48 | 49 | test("to render two lines", async ({ page }) => { 50 | await expect( 51 | page 52 | .frameLocator('iframe[title="storybook-preview-iframe"]') 53 | .locator('path.recharts-curve.recharts-area-curve[name="SolarCells"]'), 54 | ).toBeVisible() 55 | await expect( 56 | page 57 | .frameLocator('iframe[title="storybook-preview-iframe"]') 58 | .locator('path.recharts-curve.recharts-area-curve[name="Glass"]'), 59 | ).toBeVisible() 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/components/AreaChart/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor AreaChart Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.3.1 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.3.0 16 | 17 | ### Changes 18 | 19 | - Feat: customTooltip 20 | 21 | ## 0.2.3 22 | 23 | ### Changes 24 | 25 | - Chore: Gradient fill values 26 | - Chore: Padding if no y-axis and startEndOnly 27 | 28 | ## 0.2.2 29 | 30 | ### Changes 31 | 32 | - Fix: Legend scroll distance 33 | 34 | ## 0.2.1 35 | 36 | ### Changes 37 | 38 | - Fix: Gradient category id 39 | 40 | ## 0.2.0 41 | 42 | ### Changes 43 | 44 | - Feat: Add tooltipCallback prop 45 | - Feat: Add fill prop 46 | 47 | ## 0.1.0 48 | 49 | ### Changes 50 | 51 | - Feat: Add legendPosition prop 52 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Badge [v1.0.0] 2 | 3 | import React from "react" 4 | import { tv, type VariantProps } from "tailwind-variants" 5 | 6 | import { cx } from "../../utils/cx" 7 | 8 | const badgeVariants = tv({ 9 | base: cx( 10 | "inline-flex items-center gap-x-1 whitespace-nowrap rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset", 11 | ), 12 | variants: { 13 | variant: { 14 | default: [ 15 | "bg-blue-50 text-blue-900 ring-blue-500/30", 16 | "dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30", 17 | ], 18 | neutral: [ 19 | "bg-gray-50 text-gray-900 ring-gray-500/30", 20 | "dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20", 21 | ], 22 | success: [ 23 | "bg-emerald-50 text-emerald-900 ring-emerald-600/30", 24 | "dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-emerald-400/20", 25 | ], 26 | error: [ 27 | "bg-red-50 text-red-900 ring-red-600/20", 28 | "dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20", 29 | ], 30 | warning: [ 31 | "bg-yellow-50 text-yellow-900 ring-yellow-600/30", 32 | "dark:bg-yellow-400/10 dark:text-yellow-500 dark:ring-yellow-400/20", 33 | ], 34 | }, 35 | }, 36 | defaultVariants: { 37 | variant: "default", 38 | }, 39 | }) 40 | 41 | interface BadgeProps 42 | extends React.ComponentPropsWithoutRef<"span">, 43 | VariantProps { } 44 | 45 | const Badge = React.forwardRef( 46 | ({ className, variant, ...props }: BadgeProps, forwardedRef) => { 47 | return ( 48 | 54 | ) 55 | }, 56 | ) 57 | 58 | Badge.displayName = "Badge" 59 | 60 | export { Badge, badgeVariants, type BadgeProps } 61 | -------------------------------------------------------------------------------- /src/components/Badge/badge.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect default badge", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-badge--default") 6 | await page 7 | .frameLocator('iframe[title="storybook-preview-iframe"]') 8 | .getByText("Badge") 9 | .click() 10 | await expect( 11 | page 12 | .frameLocator('iframe[title="storybook-preview-iframe"]') 13 | .getByText("Badge"), 14 | ).toBeVisible() 15 | await expect( 16 | page 17 | .frameLocator('iframe[title="storybook-preview-iframe"]') 18 | .locator("#storybook-root"), 19 | ).toContainText("Badge") 20 | }) 21 | }) 22 | 23 | test.describe("Expect accordion variants", () => { 24 | test("to inlcude all variants", async ({ page }) => { 25 | await page.goto("http://localhost:6006/?path=/story/ui-badge--variants") 26 | await expect( 27 | page 28 | .frameLocator('iframe[title="storybook-preview-iframe"]') 29 | .getByText("Neutral"), 30 | ).toBeVisible() 31 | await expect( 32 | page 33 | .frameLocator('iframe[title="storybook-preview-iframe"]') 34 | .locator("#storybook-root") 35 | .getByText("Default"), 36 | ).toBeVisible() 37 | await expect( 38 | page 39 | .frameLocator('iframe[title="storybook-preview-iframe"]') 40 | .getByText("Success"), 41 | ).toBeVisible() 42 | await expect( 43 | page 44 | .frameLocator('iframe[title="storybook-preview-iframe"]') 45 | .getByText("Error"), 46 | ).toBeVisible() 47 | await expect( 48 | page 49 | .frameLocator('iframe[title="storybook-preview-iframe"]') 50 | .getByText("Warning"), 51 | ).toBeVisible() 52 | }) 53 | }) 54 | 55 | test.describe("Expect badge as link", () => { 56 | test("to be a link", async ({ page }) => { 57 | await page.goto( 58 | "http://localhost:6006/?path=/story/ui-badge--anchor-with-badge-variants-style", 59 | ) 60 | await expect( 61 | page 62 | .frameLocator('iframe[title="storybook-preview-iframe"]') 63 | .getByRole("link"), 64 | ).toBeDefined() 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/components/Badge/badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react" 2 | 3 | import { cx } from "../../utils/cx" 4 | import { Badge, badgeVariants } from "./Badge" 5 | 6 | const meta: Meta = { 7 | title: "ui/Badge", 8 | component: Badge, 9 | args: { 10 | children: "Badge", 11 | }, 12 | argTypes: { 13 | variant: { 14 | control: "radio", 15 | options: [...Object.keys(badgeVariants.variants.variant)], 16 | }, 17 | }, 18 | } 19 | 20 | export default meta 21 | type Story = StoryObj 22 | 23 | export const Default: Story = {} 24 | 25 | export const Variants: Story = { 26 | render: () => ( 27 |
28 | Neutral 29 | Default 30 | Success 31 | Error 32 | Warning 33 |
34 | ), 35 | } 36 | 37 | export const AnchorWithBadgeVariantsStyle: Story = { 38 | render: () => ( 39 | 46 | ), 47 | } 48 | 49 | export const CustomisedBadge: Story = { 50 | render: () => ( 51 |
52 |
53 | 54 | Export Request 55 | 56 | 57 | Your export is ready for download:{" "} 58 | 263 transactions 59 | 60 |
61 | 64 |
65 | ), 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Badge/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Badge Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.1 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | -------------------------------------------------------------------------------- /src/components/BarChart/barchart.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto( 5 | "http://localhost:6006/?path=/story/visualization-barchart--default", 6 | ) 7 | }) 8 | 9 | test.describe("Expect default bar chart", () => { 10 | test("to be rendered", async ({ page }) => { 11 | await expect( 12 | page 13 | .frameLocator('iframe[title="storybook-preview-iframe"]') 14 | .getByTestId("bar-chart"), 15 | ).toBeVisible() 16 | }) 17 | 18 | test("to render legend two items", async ({ page }) => { 19 | await expect( 20 | page 21 | .frameLocator('iframe[title="storybook-preview-iframe"]') 22 | .locator("li") 23 | .filter({ hasText: "SolarCells" }), 24 | ).toBeVisible() 25 | await expect( 26 | page 27 | .frameLocator('iframe[title="storybook-preview-iframe"]') 28 | .locator("li") 29 | .filter({ hasText: "Glass" }), 30 | ).toBeVisible() 31 | }) 32 | 33 | test("to render an x-axis", async ({ page }) => { 34 | await expect( 35 | page 36 | .frameLocator('iframe[title="storybook-preview-iframe"]') 37 | .locator(".recharts-xAxis"), 38 | ).toHaveClass(/recharts-xAxis/) 39 | }) 40 | 41 | test("to render an y-axis", async ({ page }) => { 42 | await expect( 43 | page 44 | .frameLocator('iframe[title="storybook-preview-iframe"]') 45 | .locator(".recharts-yAxis"), 46 | ).toHaveClass(/recharts-yAxis/) 47 | }) 48 | 49 | test("to render first two bars", async ({ page }) => { 50 | await expect( 51 | page 52 | .frameLocator('iframe[title="storybook-preview-iframe"]') 53 | .locator(".recharts-layer > rect") 54 | .first(), 55 | ).toBeVisible() 56 | await expect( 57 | page 58 | .frameLocator('iframe[title="storybook-preview-iframe"]') 59 | .locator("g:nth-child(8) > g > g > rect") 60 | .first(), 61 | ).toBeVisible() 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/components/BarChart/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor BarChart Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.2.1 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.2.0 16 | 17 | ### Changes 18 | 19 | - Feat: customTooltip 20 | 21 | ## 0.1.2 22 | 23 | ### Changes 24 | 25 | - Chore: Padding if no y-axis and startEndOnly 26 | 27 | ## 0.1.1 28 | 29 | ### Changes 30 | 31 | - Fix: Legend scroll distance 32 | 33 | ## 0.1.0 34 | 35 | ### Changes 36 | 37 | - Feat: Add tooltipCallback prop 38 | 39 | ## 0.0.1 40 | 41 | ### Changes 42 | 43 | - Fix: Dynamic tooltip position on vertical layout 44 | - Fix: `barCategoryGap` propagation 45 | -------------------------------------------------------------------------------- /src/components/BarList/barlist.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import type { Meta, StoryObj } from "@storybook/react" 3 | 4 | import { BarList } from "./BarList" 5 | 6 | const meta: Meta = { 7 | title: "visualization/BarList", 8 | component: BarList, 9 | render: (args) => , 10 | } 11 | 12 | export default meta 13 | type Story = StoryObj 14 | 15 | const data = [ 16 | { name: "/home", value: 843 }, 17 | { name: "/imprint", value: 46 }, 18 | { name: "/cancellation", value: 3 }, 19 | { name: "/blocks", value: 108 }, 20 | { name: "/documentation", value: 384 }, 21 | ] 22 | 23 | const dataSame = [ 24 | { name: "/home", value: 100 }, 25 | { name: "/imprint", value: 100 }, 26 | { name: "/cancellation", value: 100 }, 27 | { name: "/blocks", value: 100 }, 28 | { name: "/documentation", value: 100 }, 29 | ] 30 | 31 | const dataHref = [ 32 | { name: "/home", value: 843, href: "https://tremor.so" }, 33 | { name: "/imprint", value: 46, href: "https://tremor.so" }, 34 | { name: "/cancellation", value: 3, href: "https://tremor.so" }, 35 | { name: "/blocks", value: 108, href: "https://tremor.so" }, 36 | { name: "/documentation", value: 384, href: "https://tremor.so" }, 37 | ] 38 | 39 | export const Default: Story = { 40 | args: { 41 | data: data, 42 | }, 43 | } 44 | 45 | export const WithHrefAndValueFormatter: Story = { 46 | args: { 47 | data: dataHref, 48 | valueFormatter: (value) => `${value} Visitors`, 49 | }, 50 | } 51 | 52 | export const WithSortOrderAscending: Story = { 53 | args: { 54 | data: data, 55 | sortOrder: "ascending", 56 | }, 57 | } 58 | 59 | export const WithSortOrderNone: Story = { 60 | args: { 61 | data: data, 62 | sortOrder: "none", 63 | }, 64 | } 65 | 66 | export const WithOnValueChange: Story = { 67 | args: { 68 | data: data, 69 | }, 70 | render: (args) => { 71 | const [selectedItem, setSelectedItem] = React.useState("") 72 | return ( 73 |
74 | 77 | setSelectedItem(JSON.stringify(item, null, 2)) 78 | } 79 | {...args} 80 | /> 81 |
 82 |           {selectedItem === "" ? "Click on a bar" : selectedItem}
 83 |         
84 |
85 | ) 86 | }, 87 | } 88 | 89 | export const WithOnValueChangeAndHref: Story = { 90 | args: { 91 | data: dataHref, 92 | }, 93 | render: (args) => { 94 | const [selectedItem, setSelectedItem] = React.useState("") 95 | return ( 96 |
97 | 100 | setSelectedItem(JSON.stringify(item, null, 2)) 101 | } 102 | {...args} 103 | /> 104 |
105 |           {selectedItem === "" ? "Click on a bar" : selectedItem}
106 |         
107 |
108 | ) 109 | }, 110 | } 111 | 112 | export const DataSameWithOnValueChange: Story = { 113 | args: { 114 | onValueChange: () => {}, 115 | data: dataSame, 116 | }, 117 | } 118 | 119 | export const DataSame: Story = { 120 | args: { 121 | data: dataSame, 122 | }, 123 | } 124 | -------------------------------------------------------------------------------- /src/components/BarList/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor BarList Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.1.1 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.1.0 16 | 17 | ### Changes 18 | 19 | - Added sortOrder: `none` option 20 | 21 | ## 0.0.1 22 | 23 | ### Changes 24 | 25 | - Improve spacing when bar is a button. 26 | - Improve padding for long labels. 27 | -------------------------------------------------------------------------------- /src/components/Button/button.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect button variant", () => { 4 | test("primary to exist", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-button--primary") 6 | await expect( 7 | page 8 | .frameLocator('iframe[title="storybook-preview-iframe"]') 9 | .getByRole("button", { name: "Primary" }), 10 | ).toBeVisible() 11 | }) 12 | test("secondary to exist", async ({ page }) => { 13 | await page.goto("http://localhost:6006/?path=/story/ui-button--secondary") 14 | await expect( 15 | page 16 | .frameLocator('iframe[title="storybook-preview-iframe"]') 17 | .getByRole("button", { name: "Secondary" }), 18 | ).toBeVisible() 19 | }) 20 | test("light to exist", async ({ page }) => { 21 | await page.goto("http://localhost:6006/?path=/story/ui-button--light") 22 | await expect( 23 | page 24 | .frameLocator('iframe[title="storybook-preview-iframe"]') 25 | .getByRole("button", { name: "Light" }), 26 | ).toBeVisible() 27 | }) 28 | test("ghost to exist", async ({ page }) => { 29 | await page.goto("http://localhost:6006/?path=/story/ui-button--ghost") 30 | await expect( 31 | page 32 | .frameLocator('iframe[title="storybook-preview-iframe"]') 33 | .getByRole("button", { name: "Ghost" }), 34 | ).toBeVisible() 35 | }) 36 | test("destructive to exist", async ({ page }) => { 37 | await page.goto("http://localhost:6006/?path=/story/ui-button--destructive") 38 | await expect( 39 | page 40 | .frameLocator('iframe[title="storybook-preview-iframe"]') 41 | .getByRole("button", { name: "Destructive" }), 42 | ).toBeVisible() 43 | }) 44 | }) 45 | 46 | test.describe("Expect button disabled", () => { 47 | test("to be disabled", async ({ page }) => { 48 | await page.goto( 49 | "http://localhost:6006/?path=/story/ui-button--with-disabled", 50 | ) 51 | await expect( 52 | page 53 | .frameLocator('iframe[title="storybook-preview-iframe"]') 54 | .getByRole("button", { name: "Disabled" }), 55 | ).toBeDisabled() 56 | }) 57 | }) 58 | 59 | test.describe("Expect button as link", () => { 60 | test("to be a link", async ({ page }) => { 61 | await page.goto( 62 | "http://localhost:6006/?path=/story/ui-button--as-child-anchor", 63 | ) 64 | await expect( 65 | page 66 | .frameLocator('iframe[title="storybook-preview-iframe"]') 67 | .getByRole("link", { name: "API Reference" }), 68 | ).toBeDefined() 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /src/components/Button/button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react" 2 | 3 | import { cx } from "../../utils/cx" 4 | import { Button, buttonVariants } from "./Button" 5 | 6 | const meta: Meta = { 7 | title: "ui/Button", 8 | component: Button, 9 | argTypes: { 10 | variant: { 11 | control: "radio", 12 | options: [...Object.keys(buttonVariants.variants.variant)], 13 | }, 14 | }, 15 | } 16 | 17 | export default meta 18 | type Story = StoryObj 19 | 20 | export const Primary: Story = { 21 | args: { 22 | variant: "primary", 23 | children: "Primary", 24 | }, 25 | } 26 | 27 | export const Secondary: Story = { 28 | args: { 29 | variant: "secondary", 30 | children: "Secondary", 31 | }, 32 | } 33 | 34 | export const Light: Story = { 35 | args: { 36 | variant: "light", 37 | children: "Light", 38 | }, 39 | } 40 | 41 | export const Ghost: Story = { 42 | args: { 43 | variant: "ghost", 44 | children: "Ghost", 45 | }, 46 | } 47 | 48 | export const Destructive: Story = { 49 | args: { 50 | variant: "destructive", 51 | children: "Destructive", 52 | }, 53 | } 54 | 55 | export const WithDisabled: Story = { 56 | args: { 57 | children: "Disabled", 58 | disabled: true, 59 | }, 60 | } 61 | 62 | export const IsLoading: Story = { 63 | args: { 64 | isLoading: true, 65 | }, 66 | } 67 | 68 | export const IsLoadingWithChildren: Story = { 69 | args: { 70 | isLoading: true, 71 | children: "Add item", 72 | }, 73 | } 74 | 75 | export const IsLoadingWithLoadingText: Story = { 76 | args: { 77 | loadingText: "Custom loading text", 78 | isLoading: true, 79 | }, 80 | } 81 | 82 | export const AsChildAnchor: Story = { 83 | render: () => ( 84 | 87 | ), 88 | } 89 | 90 | export const AnchorWithBadgeVariantsStyle: Story = { 91 | render: () => ( 92 | 97 | ), 98 | } 99 | -------------------------------------------------------------------------------- /src/components/Button/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Button Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.2.0 10 | 11 | ### Changes 12 | 13 | - Feat: Update base color 14 | 15 | ## 0.1.2 16 | 17 | ### Changes 18 | 19 | - Chore: Add `tremor-id` 20 | 21 | ## 0.1.1 22 | 23 | ### Changes 24 | 25 | - Fix: Default height 26 | - Fix: Display name 27 | 28 | ## 0.1.0 29 | 30 | ### Changes 31 | 32 | - Feat: Ghost variant 33 | -------------------------------------------------------------------------------- /src/components/Calendar/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Calendar Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.1.0 10 | 11 | ### Changes 12 | 13 | - Feat: Update base color 14 | 15 | ## 0.0.5 16 | 17 | ### Changes 18 | 19 | - Chore: Add `tremor-id` 20 | 21 | ## 0.0.4 22 | 23 | ### Changes 24 | 25 | - Fix: Z-index day 26 | - Fix: showOutsideDays logic 27 | 28 | ## 0.0.3 29 | 30 | ### Changes 31 | 32 | - Fix: Default height 33 | 34 | ## 0.0.2 35 | 36 | ### Changes 37 | 38 | - Fix: Today indicator disabled style 39 | 40 | ## 0.0.1 41 | 42 | ### Changes 43 | 44 | - Fix: Button Effect issues 45 | - Fix: Icons aria hidden 46 | -------------------------------------------------------------------------------- /src/components/Callout/Callout.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Callout [v0.0.1] 2 | 3 | import React from "react" 4 | import { tv, type VariantProps } from "tailwind-variants" 5 | 6 | import { cx } from "../../utils/cx" 7 | 8 | const calloutVariants = tv({ 9 | base: "flex flex-col overflow-hidden rounded-md p-4 text-sm", 10 | variants: { 11 | variant: { 12 | default: [ 13 | // text color 14 | "text-blue-900 dark:text-blue-400", 15 | // background color 16 | "bg-blue-50 dark:bg-blue-950/70", 17 | ], 18 | success: [ 19 | // text color 20 | "text-emerald-900 dark:text-emerald-500", 21 | // background color 22 | "bg-emerald-50 dark:bg-emerald-950/70", 23 | ], 24 | error: [ 25 | // text color 26 | "text-red-900 dark:text-red-500", 27 | // background color 28 | "bg-red-50 dark:bg-red-950/70", 29 | ], 30 | warning: [ 31 | // text color 32 | "text-yellow-900 dark:text-yellow-500", 33 | // background color 34 | "bg-yellow-50 dark:bg-yellow-950/70", 35 | ], 36 | neutral: [ 37 | // text color 38 | "text-gray-900 dark:text-gray-400", 39 | // background color 40 | "bg-gray-100 dark:bg-gray-800/70", 41 | ], 42 | }, 43 | }, 44 | defaultVariants: { 45 | variant: "default", 46 | }, 47 | }) 48 | 49 | interface CalloutProps 50 | extends React.ComponentPropsWithoutRef<"div">, 51 | VariantProps { 52 | title: string 53 | icon?: React.ElementType | React.ReactElement 54 | } 55 | 56 | const Callout = React.forwardRef( 57 | ( 58 | { title, icon: Icon, className, variant, children, ...props }: CalloutProps, 59 | forwardedRef, 60 | ) => { 61 | return ( 62 |
68 |
69 | {Icon && typeof Icon === "function" ? ( 70 |
79 |
80 | {children} 81 |
82 |
83 | ) 84 | }, 85 | ) 86 | 87 | Callout.displayName = "Callout" 88 | 89 | export { Callout, calloutVariants, type CalloutProps } 90 | -------------------------------------------------------------------------------- /src/components/Callout/callout.stories.tsx: -------------------------------------------------------------------------------- 1 | import { RiErrorWarningFill, RiInformation2Fill } from "@remixicon/react" 2 | import type { Meta, StoryObj } from "@storybook/react" 3 | 4 | import { Callout, calloutVariants } from "./Callout" 5 | 6 | const meta: Meta = { 7 | title: "ui/Callout", 8 | component: Callout, 9 | args: { 10 | title: "Sales Performance", 11 | children: 12 | "System Update: Enhanced Salesforce and Dynamics 365 integration now delivers key sales performance metrics directly to your dashboard for improved target achievement.", 13 | }, 14 | argTypes: { 15 | variant: { 16 | control: "radio", 17 | options: [...Object.keys(calloutVariants.variants.variant)], 18 | }, 19 | }, 20 | } 21 | 22 | export default meta 23 | type Story = StoryObj 24 | 25 | export const Default: Story = {} 26 | 27 | export const Success: Story = { 28 | args: { 29 | variant: "success", 30 | }, 31 | } 32 | 33 | export const Error: Story = { 34 | args: { 35 | variant: "error", 36 | }, 37 | } 38 | 39 | export const Warning: Story = { 40 | args: { 41 | variant: "warning", 42 | }, 43 | } 44 | 45 | export const Neutral: Story = { 46 | args: { 47 | variant: "neutral", 48 | }, 49 | } 50 | 51 | export const WithIcon: Story = { 52 | args: { 53 | icon: RiErrorWarningFill, 54 | title: "AWS Credit Alert", 55 | children: 56 | "Warning: Your AWS credits are nearly depleted. Please review your usage and consider adding more credits to avoid service interruptions. Visit your account dashboard for details.", 57 | }, 58 | } 59 | 60 | export const WithIconElement: Story = { 61 | args: { 62 | icon: , 63 | title: "Information", 64 | children: "Visit your account dashboard for details.", 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Callout/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Callout Changelog 2 | 3 | ## 0.0.1 4 | 5 | ### Changes 6 | 7 | - Chore: Add `tremor-id` 8 | -------------------------------------------------------------------------------- /src/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Card [v1.0.0] 2 | 3 | import React from "react" 4 | import { Slot } from "@radix-ui/react-slot" 5 | 6 | import { cx } from "../../utils/cx" 7 | 8 | interface CardProps extends React.ComponentPropsWithoutRef<"div"> { 9 | asChild?: boolean 10 | } 11 | 12 | const Card = React.forwardRef( 13 | ({ className, asChild, ...props }, forwardedRef) => { 14 | const Component = asChild ? Slot : "div" 15 | return ( 16 | 30 | ) 31 | }, 32 | ) 33 | 34 | Card.displayName = "Card" 35 | 36 | export { Card, type CardProps } 37 | -------------------------------------------------------------------------------- /src/components/Card/card.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect card", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-card--default") 6 | await expect( 7 | page 8 | .frameLocator('iframe[title="storybook-preview-iframe"]') 9 | .locator("#storybook-root div"), 10 | ).toBeVisible() 11 | }) 12 | }) 13 | 14 | test.describe("Expect card", () => { 15 | test("to inlcude text", async ({ page }) => { 16 | await page.goto("http://localhost:6006/?path=/story/ui-card--with-text") 17 | await expect( 18 | page 19 | .frameLocator('iframe[title="storybook-preview-iframe"]') 20 | .getByRole("heading", { name: "Perseverance Rover's Latest" }), 21 | ).toBeVisible() 22 | await expect( 23 | page 24 | .frameLocator('iframe[title="storybook-preview-iframe"]') 25 | .getByText("NASA's Perseverance Rover has"), 26 | ).toBeVisible() 27 | }) 28 | }) 29 | 30 | test.describe("Expect card as list item", () => { 31 | test("to be defined", async ({ page }) => { 32 | await page.goto("http://localhost:6006/?path=/story/ui-card--as-child-list") 33 | await expect( 34 | page 35 | .frameLocator('iframe[title="storybook-preview-iframe"]') 36 | .getByRole("listitem", { 37 | name: "This card will be turned into a
  • element", 38 | }), 39 | ).toBeDefined() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/components/Card/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Card Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Fix: Roundness 20 | -------------------------------------------------------------------------------- /src/components/CategoryBar/categorybar.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("CategoryBar Component", () => { 4 | const STORY_URL = 5 | "http://localhost:6006/?path=/story/visualization-categorybar--default" 6 | 7 | test("renders the category bar component", async ({ page }) => { 8 | await page.goto(STORY_URL) 9 | const storyFrame = page.frameLocator( 10 | 'iframe[title="storybook-preview-iframe"]', 11 | ) 12 | 13 | await expect(storyFrame.getByTestId("category-bar")).toBeVisible() 14 | }) 15 | 16 | test("displays correct label values", async ({ page }) => { 17 | await page.goto(STORY_URL) 18 | const storyFrame = page.frameLocator( 19 | 'iframe[title="storybook-preview-iframe"]', 20 | ) 21 | await expect(storyFrame.getByText("0").first()).toBeVisible() 22 | await expect(storyFrame.getByText("70")).toBeVisible() 23 | await expect(storyFrame.getByText("88")).toBeVisible() 24 | await expect(storyFrame.getByText("100")).toBeVisible() 25 | }) 26 | 27 | test("renders all category segments", async ({ page }) => { 28 | await page.goto(STORY_URL) 29 | const storyFrame = page.frameLocator( 30 | 'iframe[title="storybook-preview-iframe"]', 31 | ) 32 | const categoryBar = storyFrame.getByTestId("category-bar") 33 | const segments = categoryBar.locator("div.h-full[style*='width']") 34 | 35 | await expect(segments).toHaveCount(3) 36 | }) 37 | 38 | test("renders with marker when provided", async ({ page }) => { 39 | await page.goto( 40 | "http://localhost:6006/?path=/story/visualization-categorybar--with-marker", 41 | ) 42 | const storyFrame = page.frameLocator( 43 | 'iframe[title="storybook-preview-iframe"]', 44 | ) 45 | 46 | const marker = storyFrame.locator(".absolute.w-2.-translate-x-1\\/2") 47 | await expect(marker).toBeVisible() 48 | }) 49 | 50 | test("handles marker tooltip interaction", async ({ page }) => { 51 | await page.goto( 52 | "http://localhost:6006/?path=/story/visualization-categorybar--with-marker", 53 | ) 54 | const storyFrame = page.frameLocator( 55 | 'iframe[title="storybook-preview-iframe"]', 56 | ) 57 | 58 | const marker = storyFrame.locator(".absolute.w-2.-translate-x-1\\/2") 59 | await marker.hover() 60 | }) 61 | 62 | test("maintains accessibility attributes", async ({ page }) => { 63 | await page.goto(STORY_URL) 64 | const storyFrame = page.frameLocator( 65 | 'iframe[title="storybook-preview-iframe"]', 66 | ) 67 | const categoryBar = storyFrame.getByTestId("category-bar") 68 | 69 | await expect(categoryBar).toHaveAttribute("aria-label", "Category bar") 70 | await expect(categoryBar).toHaveAttribute("tremor-id", "tremor-raw") 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /src/components/CategoryBar/categorybar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react" 2 | 3 | import { CategoryBar } from "./CategoryBar" 4 | 5 | const meta: Meta = { 6 | title: "visualization/CategoryBar", 7 | render: (args) => , 8 | component: CategoryBar, 9 | } 10 | 11 | export default meta 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | args: { 16 | values: [70, 18, 12], 17 | }, 18 | } 19 | 20 | export const WithFloatingPointValues: Story = { 21 | args: { 22 | values: [70.1, 18.3, 11.6], 23 | }, 24 | } 25 | 26 | export const WithMarker: Story = { 27 | args: { 28 | values: [60, 10, 15, 15], 29 | marker: { value: 65, tooltip: "65" }, 30 | showLabels: true, 31 | }, 32 | } 33 | 34 | export const WithColors: Story = { 35 | args: { 36 | values: [60, 25, 15], 37 | colors: ["pink", "amber", "emerald"], 38 | }, 39 | } 40 | 41 | export const WithMarkerOnLabel: Story = { 42 | args: { 43 | values: [0, 50, 50], 44 | marker: { value: 0, tooltip: "0" }, 45 | showLabels: true, 46 | }, 47 | } 48 | 49 | export const WithSmallStartValue: Story = { 50 | args: { 51 | values: [10, 25, 45, 20], 52 | colors: ["amber", "gray", "pink", "cyan"], 53 | marker: { value: 50, tooltip: "50%" }, 54 | showLabels: true, 55 | }, 56 | } 57 | 58 | export const WithCloseEndValue: Story = { 59 | args: { 60 | values: [10, 25, 50, 15], 61 | marker: { value: 50, tooltip: "50%" }, 62 | showLabels: true, 63 | }, 64 | } 65 | 66 | export const WithValuesMoreThan100: Story = { 67 | args: { 68 | values: [400, 400, 800], 69 | marker: { value: 1400, tooltip: "50%" }, 70 | showLabels: true, 71 | }, 72 | } 73 | 74 | export const WithValuesLessThan100: Story = { 75 | args: { 76 | values: [8, 7, 9, 8], 77 | marker: { value: 20, tooltip: "20%" }, 78 | showLabels: true, 79 | }, 80 | } 81 | 82 | export const WithConsecutiveSmallValues: Story = { 83 | args: { 84 | values: [10, 5, 5, 5, 5, 5, 50, 15, 0], 85 | marker: { value: 50, tooltip: "50%" }, 86 | showLabels: true, 87 | }, 88 | } 89 | 90 | export const WithNoLabelsInbetween: Story = { 91 | args: { 92 | values: [5, 95], 93 | marker: { value: 50, tooltip: "50%" }, 94 | showLabels: true, 95 | }, 96 | } 97 | 98 | export const WithZero: Story = { 99 | args: { 100 | values: [0, 100], 101 | }, 102 | } 103 | -------------------------------------------------------------------------------- /src/components/CategoryBar/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Category Bar Changelog 2 | 3 | ## 0.0.3 4 | 5 | ### Changes 6 | 7 | - Fix: Hidden aria elements 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Fix: Long floating point issues with labels 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Chore: Add `tremor-id` 20 | -------------------------------------------------------------------------------- /src/components/Checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Checkbox [v1.0.0] 2 | 3 | import React from "react" 4 | import * as CheckboxPrimitives from "@radix-ui/react-checkbox" 5 | 6 | import { cx } from "../../utils/cx" 7 | import { focusRing } from "../../utils/focusRing" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, checked, ...props }, forwardedRef) => { 13 | return ( 14 | 40 | 44 | {checked === "indeterminate" ? ( 45 | 63 | ) : ( 64 | 80 | )} 81 | 82 | 83 | ) 84 | }) 85 | 86 | Checkbox.displayName = "Checkbox" 87 | 88 | export { Checkbox } 89 | -------------------------------------------------------------------------------- /src/components/Checkbox/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Checkbox Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.3 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.0.2 16 | 17 | ### Changes 18 | 19 | - Fix: Layout shift 20 | 21 | ## 0.0.1 22 | 23 | ### Changes 24 | 25 | - Fix: Add aria-hidden to svg 26 | -------------------------------------------------------------------------------- /src/components/Checkbox/checkbox.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect checkbox", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-checkbox--default") 6 | await expect( 7 | page 8 | .frameLocator('iframe[title="storybook-preview-iframe"]') 9 | .getByRole("checkbox"), 10 | ).toBeVisible() 11 | }) 12 | }) 13 | 14 | test.describe("Expect checkbox", () => { 15 | test("to be checkable", async ({ page }) => { 16 | await page.goto("http://localhost:6006/?path=/story/ui-checkbox--default") 17 | await expect( 18 | page 19 | .frameLocator('iframe[title="storybook-preview-iframe"]') 20 | .getByRole("checkbox"), 21 | ).toBeVisible() 22 | await page 23 | .frameLocator('iframe[title="storybook-preview-iframe"]') 24 | .getByRole("checkbox") 25 | .click() 26 | await expect( 27 | page 28 | .frameLocator('iframe[title="storybook-preview-iframe"]') 29 | .getByRole("checkbox"), 30 | ).toBeChecked() 31 | }) 32 | }) 33 | 34 | test.describe("Expect checkbox disabled", () => { 35 | test("to be disabled", async ({ page }) => { 36 | await page.goto("http://localhost:6006/?path=/story/ui-checkbox--disabled") 37 | await expect( 38 | page 39 | .frameLocator('iframe[title="storybook-preview-iframe"]') 40 | .getByRole("checkbox") 41 | .first(), 42 | ).toBeDisabled() 43 | await expect( 44 | page 45 | .frameLocator('iframe[title="storybook-preview-iframe"]') 46 | .getByRole("checkbox") 47 | .nth(1), 48 | ).toBeDisabled() 49 | await expect( 50 | page 51 | .frameLocator('iframe[title="storybook-preview-iframe"]') 52 | .getByRole("checkbox") 53 | .nth(2), 54 | ).toBeDisabled() 55 | }) 56 | }) 57 | 58 | test.describe("Expect checkbox with label", () => { 59 | test("to be checkable", async ({ page }) => { 60 | await page.goto( 61 | "http://localhost:6006/?path=/story/ui-checkbox--with-label", 62 | ) 63 | await expect( 64 | page 65 | .frameLocator('iframe[title="storybook-preview-iframe"]') 66 | .getByLabel("I'd like to be notified by"), 67 | ).toBeVisible() 68 | await page 69 | .frameLocator('iframe[title="storybook-preview-iframe"]') 70 | .getByLabel("I'd like to be notified by") 71 | .click() 72 | await expect( 73 | page 74 | .frameLocator('iframe[title="storybook-preview-iframe"]') 75 | .getByLabel("I'd like to be notified by"), 76 | ).toBeChecked() 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/components/Checkbox/checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import type { Meta, StoryObj } from "@storybook/react" 3 | 4 | import { Button } from "../Button/Button" 5 | import { Label } from "../Label/Label" 6 | import { Checkbox } from "./Checkbox" 7 | 8 | const meta: Meta = { 9 | title: "ui/Checkbox", 10 | component: Checkbox, 11 | } 12 | 13 | export default meta 14 | type Story = StoryObj 15 | 16 | export const Default: Story = {} 17 | 18 | export const Checked: Story = { 19 | args: { 20 | checked: true, 21 | }, 22 | } 23 | 24 | export const Indeterminate: Story = { 25 | args: { 26 | checked: "indeterminate", 27 | }, 28 | } 29 | 30 | export const Disabled: Story = { 31 | render: (args) => ( 32 |
    33 |
    34 | 35 |
    36 |
    37 | 38 |
    39 |
    40 | 41 | 42 |
    43 |
    44 | ), 45 | args: { 46 | disabled: true, 47 | }, 48 | } 49 | 50 | export const WithLabel: Story = { 51 | render: () => ( 52 |
    53 | 54 | 57 |
    58 | ), 59 | } 60 | 61 | export const Controlled: Story = { 62 | render: () => { 63 | const [checked, setChecked] = React.useState(true) 64 | 65 | return ( 66 |
    67 | setChecked(!checked)} 70 | /> 71 | 74 |
    75 | ) 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ComboChart/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor ComboChart Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.0 10 | 11 | ### Changes 12 | -------------------------------------------------------------------------------- /src/components/DatePicker/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Date Picker Changelog 2 | 3 | ## 2.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 1.0.5 10 | 11 | ### Changes 12 | 13 | - Fix: State types 14 | - Fix: TimeInput visibility 15 | 16 | ## 1.0.4 17 | 18 | ### Changes 19 | 20 | - Fix: Overflow styles 21 | 22 | ## 1.0.3 23 | 24 | ### Changes 25 | 26 | - Chore: Add `tremor-id` 27 | 28 | ## 1.0.2 29 | 30 | ### Changes 31 | 32 | - Fix: One export 33 | - Fix: Border style 34 | 35 | ## 1.0.1 36 | 37 | ### Changes 38 | 39 | - Fix: Add transition class to time input 40 | 41 | ## 1.0.0 42 | 43 | ### Changes 44 | 45 | - New: Add time functionality (showTimePicker) 46 | 47 | ## 0.0.1 48 | 49 | ### Changes 50 | 51 | - New: Align prop to horizontally align the popover ("center" | "end" | "start") 52 | -------------------------------------------------------------------------------- /src/components/Dialog/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Dialog Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.1 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | -------------------------------------------------------------------------------- /src/components/Dialog/dialog.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Dialog Component", () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-dialog--default") 6 | }) 7 | 8 | test("should open and display dialog content", async ({ page }) => { 9 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 10 | 11 | await frame.getByRole("button", { name: "Open Dialog" }).click() 12 | 13 | await expect( 14 | frame.getByRole("heading", { name: "Account Created Successfully" }), 15 | ).toBeVisible() 16 | await expect( 17 | frame.getByText( 18 | "Your account has been created successfully. You can now login to your account. For more information, please contact us.", 19 | ), 20 | ).toBeVisible() 21 | await expect(frame.getByRole("button", { name: "Go back" })).toBeVisible() 22 | await expect( 23 | frame.getByRole("button", { name: "Ok, got it!" }), 24 | ).toBeVisible() 25 | }) 26 | 27 | test("should close the dialog when 'Go back' button is clicked", async ({ 28 | page, 29 | }) => { 30 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 31 | 32 | await frame.getByRole("button", { name: "Open Dialog" }).click() 33 | 34 | await frame.getByRole("button", { name: "Go back" }).click() 35 | 36 | await expect( 37 | frame.getByRole("button", { name: "Open Dialog" }), 38 | ).toBeVisible() 39 | }) 40 | 41 | test("should close the dialog when 'Ok, got it!' button is clicked", async ({ 42 | page, 43 | }) => { 44 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 45 | 46 | await frame.getByRole("button", { name: "Open Dialog" }).click() 47 | 48 | await frame.getByRole("button", { name: "Ok, got it!" }).click() 49 | 50 | await expect( 51 | frame.getByRole("button", { name: "Open Dialog" }), 52 | ).toBeVisible() 53 | }) 54 | 55 | test("should be accessible via keyboard interactions", async ({ page }) => { 56 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 57 | 58 | await frame.getByRole("button", { name: "Open Dialog" }).focus() 59 | await page.keyboard.press("Enter") 60 | 61 | await expect( 62 | frame.getByRole("heading", { name: "Account Created Successfully" }), 63 | ).toBeVisible() 64 | 65 | await page.keyboard.press("Escape") 66 | 67 | await expect( 68 | frame.getByRole("button", { name: "Open Dialog" }), 69 | ).toBeVisible() 70 | }) 71 | 72 | test("should support custom class names and styles", async ({ page }) => { 73 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 74 | 75 | await frame.getByRole("button", { name: "Open Dialog" }).click() 76 | 77 | const dialogContent = frame.locator(".sm\\:max-w-lg") 78 | await expect(dialogContent).toHaveClass(/sm:max-w-lg/) 79 | }) 80 | 81 | test("should handle content updates correctly", async ({ page }) => { 82 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 83 | 84 | await frame.getByRole("button", { name: "Open Dialog" }).click() 85 | 86 | await expect( 87 | frame.getByText( 88 | "Your account has been created successfully. You can now login to your account. For more information, please contact us.", 89 | ), 90 | ).toBeVisible() 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /src/components/Dialog/dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import type { Meta, StoryObj } from "@storybook/react" 3 | 4 | import { Button } from "../Button/Button" 5 | import { 6 | Dialog, 7 | DialogClose, 8 | DialogContent, 9 | DialogDescription, 10 | DialogFooter, 11 | DialogHeader, 12 | DialogTitle, 13 | DialogTrigger, 14 | } from "./Dialog" 15 | 16 | const meta: Meta = { 17 | title: "ui/Dialog", 18 | component: Dialog, 19 | parameters: { 20 | layout: "centered", 21 | }, 22 | } 23 | 24 | export default meta 25 | type Story = StoryObj 26 | 27 | export const Default: Story = { 28 | render: () => { 29 | return ( 30 | <> 31 | 32 | 33 | 34 | 35 | 36 | 37 | Account Created Successfully 38 | 39 | Your account has been created successfully. You can now login to 40 | your account. For more information, please contact us. 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ) 60 | }, 61 | } 62 | 63 | export const Controlled: Story = { 64 | render: () => { 65 | const [open, setOpen] = React.useState(false) 66 | return ( 67 | <> 68 | 69 | 70 | 71 | 72 | 73 | 74 | Account Created Successfully 75 | 76 | Your account has been created successfully. You can now login to 77 | your account. For more information, please contact us. 78 | 79 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ) 97 | }, 98 | } 99 | -------------------------------------------------------------------------------- /src/components/Divider/Divider.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Divider [v1.0.0] 2 | 3 | import React from "react" 4 | 5 | import { cx } from "../../utils/cx" 6 | 7 | type DividerProps = React.ComponentPropsWithoutRef<"div"> 8 | 9 | const Divider = React.forwardRef( 10 | ({ className, children, ...props }, forwardedRef) => ( 11 |
    23 | {children ? ( 24 | <> 25 |
    33 |
    {children}
    34 |
    42 | 43 | ) : ( 44 |
    52 | )} 53 |
    54 | ), 55 | ) 56 | 57 | Divider.displayName = "Divider" 58 | 59 | export { Divider } 60 | -------------------------------------------------------------------------------- /src/components/Divider/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Divider Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | - Fix: Supertype divider 15 | 16 | ## 0.0.1 17 | 18 | ### Changes 19 | 20 | - Fix: Typo 21 | -------------------------------------------------------------------------------- /src/components/Divider/divider.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect divider", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-divider--default") 6 | await expect( 7 | page 8 | .frameLocator('iframe[title="storybook-preview-iframe"]') 9 | .locator("#storybook-root div") 10 | .nth(1), 11 | ).toBeVisible() 12 | }) 13 | }) 14 | 15 | test.describe("Expect divider", () => { 16 | test("to render with icon child", async ({ page }) => { 17 | await page.goto( 18 | "http://localhost:6006/?path=/story/ui-divider--with-children", 19 | ) 20 | await expect( 21 | page 22 | .frameLocator('iframe[title="storybook-preview-iframe"]') 23 | .locator(".w-96 > div:nth-child(2)"), 24 | ).toBeVisible() 25 | await expect( 26 | page 27 | .frameLocator('iframe[title="storybook-preview-iframe"]') 28 | .getByRole("img"), 29 | ).toBeVisible() 30 | }) 31 | test("to render with text child", async ({ page }) => { 32 | await page.goto( 33 | "http://localhost:6006/?path=/story/ui-divider--with-children", 34 | ) 35 | await expect( 36 | page 37 | .frameLocator('iframe[title="storybook-preview-iframe"]') 38 | .locator("div") 39 | .filter({ hasText: "Standard" }) 40 | .nth(2), 41 | ).toBeVisible() 42 | await expect( 43 | page 44 | .frameLocator('iframe[title="storybook-preview-iframe"]') 45 | .getByText("Standard"), 46 | ).toBeVisible() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/Divider/divider.stories.tsx: -------------------------------------------------------------------------------- 1 | import { RiCalendar2Line } from "@remixicon/react" 2 | import type { Meta, StoryObj } from "@storybook/react" 3 | 4 | import { Button } from "../Button/Button" 5 | import { Divider } from "./Divider" 6 | 7 | const meta: Meta = { 8 | title: "ui/Divider", 9 | component: Divider, 10 | } 11 | 12 | export default meta 13 | type Story = StoryObj 14 | 15 | export const Default: Story = { 16 | render: () => , 17 | } 18 | 19 | export const WithChildren: Story = { 20 | render: () => ( 21 |
    22 | 23 | 24 | 25 | 26 | Standard 27 | 28 | With little bit more space 29 | 30 |
    31 | ), 32 | } 33 | 34 | export const MoreText: Story = { 35 | render: () => ( 36 | <> 37 |

    Tickets Sold

    38 |

    39 | 1,587 40 |

    41 | Details 42 |

    43 | Ticket sales peaked in March, largely due to the "March Mountain 44 | Madness" event on March 12th, drawing significant tourist interest. 45 | Operational efficiencies and local hotel partnerships further boosted 46 | sales. Additionally, targeted social media promotions ahead of the event 47 | significantly increased online bookings. 48 |

    49 | 50 | ), 51 | } 52 | 53 | export const ButtonChild: Story = { 54 | render: () => ( 55 | 56 | 59 | 60 | ), 61 | } 62 | -------------------------------------------------------------------------------- /src/components/DonutChart/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor DonutChart Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.1 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | -------------------------------------------------------------------------------- /src/components/DonutChart/donutchart.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto( 5 | "http://localhost:6006/?path=/story/visualization-donutchart--default", 6 | ) 7 | }) 8 | 9 | test.describe("DonutChart Tests", () => { 10 | test("Default DonutChart renders correctly", async ({ page }) => { 11 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 12 | await expect(frame.getByTestId("donut-chart")).toBeVisible() 13 | await expect(frame.locator(".recharts-sector")).toHaveCount(7) 14 | }) 15 | 16 | test("ValueFormatter displays formatted values", async ({ page }) => { 17 | await page.goto( 18 | "http://localhost:6006/?path=/story/visualization-donutchart--value-formatter", 19 | ) 20 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 21 | await expect(frame.getByText(/\d+ units/)).toBeVisible() 22 | }) 23 | 24 | test("LabelDisabled does not show label", async ({ page }) => { 25 | await page.goto( 26 | "http://localhost:6006/?path=/story/visualization-donutchart--label-disabled", 27 | ) 28 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 29 | await expect(frame.getByText("Custom Label")).not.toBeVisible() 30 | }) 31 | 32 | test("OtherColors uses provided colors", async ({ page }) => { 33 | await page.goto( 34 | "http://localhost:6006/?path=/story/visualization-donutchart--other-colors", 35 | ) 36 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 37 | const sectors = frame.locator(".recharts-sector") 38 | const expectedColors = [ 39 | "blue", 40 | "amber", 41 | "pink", 42 | "emerald", 43 | "violet", 44 | "cyan", 45 | ] 46 | for (let i = 0; i < expectedColors.length; i++) { 47 | await expect(sectors.nth(i)).toHaveClass( 48 | new RegExp(`fill-${expectedColors[i]}-500`), 49 | ) 50 | } 51 | await expect(sectors.nth(6)).toHaveClass(/fill-blue-500/) 52 | }) 53 | 54 | test("MoreDatapointsThanColors handles excess data points", async ({ 55 | page, 56 | }) => { 57 | await page.goto( 58 | "http://localhost:6006/?path=/story/visualization-donutchart--more-datapoints-than-colors", 59 | ) 60 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 61 | await expect(frame.locator(".recharts-sector")).toHaveCount(14) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/components/Drawer/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Drawer Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Fix: `z-index` content 20 | -------------------------------------------------------------------------------- /src/components/Drawer/drawer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Drawer Component", () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-drawer--default") 6 | }) 7 | 8 | test("should open and display drawer content", async ({ page }) => { 9 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 10 | 11 | await frame.getByRole("button", { name: "Open Drawer" }).click() 12 | 13 | await expect( 14 | frame.getByRole("heading", { name: "Account Created Successfully" }), 15 | ).toBeVisible() 16 | await expect( 17 | frame.getByText( 18 | "Your account has been created successfully. You can now login to your account. For more information, please contact us.", 19 | ), 20 | ).toBeVisible() 21 | await expect( 22 | frame.getByText("This is they body of the drawer, content goes here."), 23 | ).toBeVisible() 24 | await expect(frame.getByRole("button", { name: "Go back" })).toBeVisible() 25 | await expect( 26 | frame.getByRole("button", { name: "Ok, got it!" }), 27 | ).toBeVisible() 28 | }) 29 | 30 | test("should close the drawer when 'Go back' button is clicked", async ({ 31 | page, 32 | }) => { 33 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 34 | 35 | await frame.getByRole("button", { name: "Open Drawer" }).click() 36 | 37 | await frame.getByRole("button", { name: "Go back" }).click() 38 | 39 | await expect( 40 | frame.getByRole("button", { name: "Open Drawer" }), 41 | ).toBeVisible() 42 | }) 43 | 44 | test("should close the drawer when 'Ok, got it!' button is clicked", async ({ 45 | page, 46 | }) => { 47 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 48 | 49 | await frame.getByRole("button", { name: "Open Drawer" }).click() 50 | 51 | await frame.getByRole("button", { name: "Ok, got it!" }).click() 52 | 53 | await expect( 54 | frame.getByRole("button", { name: "Open Drawer" }), 55 | ).toBeVisible() 56 | }) 57 | 58 | test("should be accessible via keyboard interactions", async ({ page }) => { 59 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 60 | 61 | await frame.getByRole("button", { name: "Open Drawer" }).focus() 62 | await page.keyboard.press("Enter") 63 | 64 | await expect( 65 | frame.getByRole("heading", { name: "Account Created Successfully" }), 66 | ).toBeVisible() 67 | 68 | await page.keyboard.press("Escape") 69 | 70 | await expect( 71 | frame.getByRole("button", { name: "Open Drawer" }), 72 | ).toBeVisible() 73 | }) 74 | 75 | test("should handle content updates correctly", async ({ page }) => { 76 | const frame = page.frameLocator('iframe[title="storybook-preview-iframe"]') 77 | 78 | await frame.getByRole("button", { name: "Open Drawer" }).click() 79 | 80 | await expect( 81 | frame.getByText("This is they body of the drawer, content goes here."), 82 | ).toBeVisible() 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/components/Drawer/drawer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import type { Meta, StoryObj } from "@storybook/react" 3 | 4 | import { Button } from "../Button/Button" 5 | import { 6 | Drawer, 7 | DrawerBody, 8 | DrawerClose, 9 | DrawerContent, 10 | DrawerDescription, 11 | DrawerFooter, 12 | DrawerHeader, 13 | DrawerTitle, 14 | DrawerTrigger, 15 | } from "./Drawer" 16 | 17 | const meta: Meta = { 18 | title: "ui/Drawer", 19 | component: Drawer, 20 | parameters: { 21 | layout: "centered", 22 | }, 23 | } 24 | 25 | export default meta 26 | type Story = StoryObj 27 | 28 | export const Default: Story = { 29 | render: () => { 30 | return ( 31 | <> 32 | 33 | 34 | 35 | 36 | 37 | 38 | Account Created Successfully 39 | 40 | Your account has been created successfully. You can now login to 41 | your account. For more information, please contact us. 42 | 43 | 44 | 45 | This is they body of the drawer, content goes here. 46 | 47 | 48 | 49 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ) 64 | }, 65 | } 66 | 67 | export const Controlled: Story = { 68 | render: () => { 69 | const [open, setOpen] = React.useState(false) 70 | return ( 71 | <> 72 | 73 | 74 | 75 | 76 | 77 | 78 | Account Created Successfully 79 | 80 | Your account has been created successfully. You can now login to 81 | your account. For more information, please contact us. 82 | 83 | 84 | 85 | This is they body of the drawer, content goes here. 86 | 87 | 88 | 89 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ) 104 | }, 105 | } 106 | -------------------------------------------------------------------------------- /src/components/DropdownMenu/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Dropdown Menu Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Fix: Omit `asChild` in items 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Chore: Add `tremor-id` 20 | -------------------------------------------------------------------------------- /src/components/Input/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Input Changelog 2 | 3 | ## 2.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 1.0.5 10 | 11 | ### Changes 12 | 13 | - fix: Remove redundant input css 14 | 15 | ## 1.0.4 16 | 17 | ### Changes 18 | 19 | - Chore: Add `tremor-id` 20 | 21 | ## 1.0.3 22 | 23 | ### Changes 24 | 25 | - Fix: enableStepper logic 26 | 27 | ## 1.0.2 28 | 29 | ### Changes 30 | 31 | - Fix: Default height 32 | 33 | ## 1.0.1 34 | 35 | ### Changes 36 | 37 | - Fix: Harmonised input height type: file 38 | 39 | ## 1.0.0 40 | 41 | ### Changes 42 | 43 | - Feat: Adds an inputClassName prop to pass styles to the nested input 44 | - Fix: Making it full width to behave like a native input 45 | - Fix: Pulls className into the outer most component 46 | - Fix: Adds transition class 47 | 48 | ## 0.0.1 49 | 50 | ### Changes 51 | 52 | - Fix: Improved focus mode 53 | -------------------------------------------------------------------------------- /src/components/Input/input.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect Input default", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-input--default") 6 | await expect( 7 | page 8 | .frameLocator('iframe[title="storybook-preview-iframe"]') 9 | .getByRole("textbox"), 10 | ).toBeVisible() 11 | }) 12 | 13 | test("to be editable", async ({ page }) => { 14 | await page.goto("http://localhost:6006/?path=/story/ui-input--default") 15 | await page 16 | .frameLocator('iframe[title="storybook-preview-iframe"]') 17 | .getByRole("textbox") 18 | .click() 19 | await page 20 | .frameLocator('iframe[title="storybook-preview-iframe"]') 21 | .getByRole("textbox") 22 | .fill("Add some text") 23 | await expect( 24 | page 25 | .frameLocator('iframe[title="storybook-preview-iframe"]') 26 | .getByRole("textbox"), 27 | ).toBeVisible() 28 | await expect( 29 | page 30 | .frameLocator('iframe[title="storybook-preview-iframe"]') 31 | .getByRole("textbox"), 32 | ).toHaveValue("Add some text") 33 | await page 34 | .frameLocator('iframe[title="storybook-preview-iframe"]') 35 | .getByRole("textbox") 36 | .click() 37 | await page 38 | .frameLocator('iframe[title="storybook-preview-iframe"]') 39 | .getByRole("textbox") 40 | .fill("") 41 | await expect( 42 | page 43 | .frameLocator('iframe[title="storybook-preview-iframe"]') 44 | .getByRole("textbox"), 45 | ).toBeEmpty() 46 | }) 47 | 48 | test("to be disbaled", async ({ page }) => { 49 | await page.goto("http://localhost:6006/?path=/story/ui-input--disabled") 50 | await expect( 51 | page 52 | .frameLocator('iframe[title="storybook-preview-iframe"]') 53 | .getByRole("textbox"), 54 | ).toBeDisabled() 55 | }) 56 | 57 | test("to have a placeholder", async ({ page }) => { 58 | await page.goto( 59 | "http://localhost:6006/?path=/story/ui-input--with-placeholder", 60 | ) 61 | await expect( 62 | page 63 | .frameLocator('iframe[title="storybook-preview-iframe"]') 64 | .getByPlaceholder("With Placeholder"), 65 | ).toBeVisible() 66 | }) 67 | }) 68 | 69 | test.describe("Expect Input password", () => { 70 | test("to be a password", async ({ page }) => { 71 | await page.goto( 72 | "http://localhost:6006/?path=/story/ui-input--type-password", 73 | ) 74 | await expect( 75 | page 76 | .frameLocator('iframe[title="storybook-preview-iframe"]') 77 | .getByRole("textbox"), 78 | ).toBeVisible() 79 | await expect( 80 | page 81 | .frameLocator('iframe[title="storybook-preview-iframe"]') 82 | .getByPlaceholder("Enter password"), 83 | ).toBeVisible() 84 | await expect( 85 | page 86 | .frameLocator('iframe[title="storybook-preview-iframe"]') 87 | .getByPlaceholder("Enter password"), 88 | ).toBeEmpty() 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /src/components/Label/Label.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Label [v0.0.2] 2 | 3 | import React from "react" 4 | import * as LabelPrimitives from "@radix-ui/react-label" 5 | 6 | import { cx } from "../../utils/cx" 7 | 8 | interface LabelProps 9 | extends React.ComponentPropsWithoutRef { 10 | disabled?: boolean 11 | } 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | LabelProps 16 | >(({ className, disabled, ...props }, forwardedRef) => ( 17 | 34 | )) 35 | 36 | Label.displayName = "Label" 37 | 38 | export { Label } 39 | -------------------------------------------------------------------------------- /src/components/Label/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Label Changelog 2 | 3 | ## 0.0.2 4 | 5 | ### Changes 6 | 7 | - Chore: Add `tremor-id` 8 | 9 | ## 0.0.1 10 | 11 | ### Changes 12 | 13 | - Fix: Label props name 14 | -------------------------------------------------------------------------------- /src/components/Label/label.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect label", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-label--default") 6 | await expect( 7 | page 8 | .frameLocator('iframe[title="storybook-preview-iframe"]') 9 | .getByText("Label"), 10 | ).toBeVisible() 11 | }) 12 | }) 13 | 14 | test.describe("Expect label disabled", () => { 15 | test("to be disabeld", async ({ page }) => { 16 | await page.goto("http://localhost:6006/?path=/story/ui-label--disabled") 17 | await expect( 18 | page 19 | .frameLocator('iframe[title="storybook-preview-iframe"]') 20 | .getByTestId("label-disabled"), 21 | ).toBeVisible() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/components/Label/label.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react" 2 | 3 | import { Checkbox } from "../Checkbox/Checkbox" 4 | import { Input } from "../Input/Input" 5 | import { Label } from "./Label" 6 | 7 | const meta: Meta = { 8 | title: "ui/Label", 9 | component: Label, 10 | } 11 | 12 | export default meta 13 | type Story = StoryObj 14 | 15 | export const Default: Story = { 16 | args: { 17 | children: "Label", 18 | }, 19 | } 20 | 21 | export const LabelWithInput: Story = { 22 | render: () => ( 23 |
    24 | 25 | 31 |
    32 | ), 33 | } 34 | export const LabelWithCheckbox: Story = { 35 | render: () => ( 36 |
    37 | 38 | 39 | 40 | ), 41 | } 42 | 43 | export const Disabled: Story = { 44 | render: () => ( 45 | 48 | ), 49 | } 50 | -------------------------------------------------------------------------------- /src/components/LineChart/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor LineChart Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.3.2 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.3.1 16 | 17 | ### Changes 18 | 19 | - Fix: tooltip payload 20 | 21 | ## 0.3.0 22 | 23 | ### Changes 24 | 25 | - Feat: customTooltip 26 | 27 | ## 0.2.2 28 | 29 | ### Changes 30 | 31 | - Chore: Padding if no y-axis and startEndOnly 32 | 33 | ## 0.2.1 34 | 35 | ### Changes 36 | 37 | - Fix: Legend scroll distance 38 | 39 | ## 0.2.0 40 | 41 | ### Changes 42 | 43 | - Feat: Add tooltipCallback prop 44 | 45 | ## 0.1.0 46 | 47 | ### Changes 48 | 49 | - Feat: Added legendPosition prop 50 | -------------------------------------------------------------------------------- /src/components/LineChart/linechart.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.beforeEach(async ({ page }) => { 4 | await page.goto( 5 | "http://localhost:6006/?path=/story/visualization-linechart--default", 6 | ) 7 | }) 8 | 9 | test.describe("Expect default line chart", () => { 10 | test("to be rendered", async ({ page }) => { 11 | await expect( 12 | page 13 | .frameLocator('iframe[title="storybook-preview-iframe"]') 14 | .getByTestId("line-chart"), 15 | ).toBeVisible() 16 | }) 17 | 18 | test("to render legend two items", async ({ page }) => { 19 | await expect( 20 | page 21 | .frameLocator('iframe[title="storybook-preview-iframe"]') 22 | .locator("li") 23 | .filter({ hasText: "SolarCells" }), 24 | ).toBeVisible() 25 | await expect( 26 | page 27 | .frameLocator('iframe[title="storybook-preview-iframe"]') 28 | .locator("li") 29 | .filter({ hasText: "Glass" }), 30 | ).toBeVisible() 31 | }) 32 | 33 | test("to render an x-axis", async ({ page }) => { 34 | await expect( 35 | page 36 | .frameLocator('iframe[title="storybook-preview-iframe"]') 37 | .locator(".recharts-xAxis"), 38 | ).toHaveClass(/recharts-xAxis/) 39 | }) 40 | 41 | test("to render an y-axis", async ({ page }) => { 42 | await expect( 43 | page 44 | .frameLocator('iframe[title="storybook-preview-iframe"]') 45 | .locator(".recharts-yAxis"), 46 | ).toHaveClass(/recharts-yAxis/) 47 | }) 48 | 49 | test("to render two lines", async ({ page }) => { 50 | await expect( 51 | page 52 | .frameLocator('iframe[title="storybook-preview-iframe"]') 53 | .locator('path.recharts-curve.recharts-line-curve[name="SolarCells"]'), 54 | ).toBeVisible() 55 | await expect( 56 | page 57 | .frameLocator('iframe[title="storybook-preview-iframe"]') 58 | .locator('path.recharts-curve.recharts-line-curve[name="Glass"]'), 59 | ).toBeVisible() 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /src/components/Popover/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Popover Changelog 2 | 3 | ## 0.0.3 4 | 5 | ### Changes 6 | 7 | - Chore: Add `tremor-id` 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Fix: Border color 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Fix: Unused collision prop 20 | -------------------------------------------------------------------------------- /src/components/Popover/popover.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect popover default", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-popover--default") 6 | await page 7 | .frameLocator('iframe[title="storybook-preview-iframe"]') 8 | .getByText("Open") 9 | .click() 10 | await expect( 11 | page 12 | .frameLocator('iframe[title="storybook-preview-iframe"]') 13 | .getByText("Place content for the popover"), 14 | ).toBeVisible() 15 | }) 16 | 17 | test("to be rendered and closeable", async ({ page }) => { 18 | await page.goto("http://localhost:6006/?path=/story/ui-popover--default") 19 | await page 20 | .frameLocator('iframe[title="storybook-preview-iframe"]') 21 | .getByText("Open") 22 | .click() 23 | await expect( 24 | page 25 | .frameLocator('iframe[title="storybook-preview-iframe"]') 26 | .getByText("Place content for the popover"), 27 | ).toBeVisible() 28 | await page 29 | .frameLocator('iframe[title="storybook-preview-iframe"]') 30 | .getByText("Open") 31 | .click() 32 | await expect( 33 | page 34 | .frameLocator('iframe[title="storybook-preview-iframe"]') 35 | .getByText("Place content for the popover"), 36 | ).toHaveCount(0) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | // Tremor ProgressBar [v0.0.3] 2 | 3 | import React from "react" 4 | import { tv, type VariantProps } from "tailwind-variants" 5 | 6 | import { cx } from "../../utils/cx" 7 | 8 | const progressBarVariants = tv({ 9 | slots: { 10 | background: "", 11 | bar: "", 12 | }, 13 | variants: { 14 | variant: { 15 | default: { 16 | background: "bg-blue-200 dark:bg-blue-500/30", 17 | bar: "bg-blue-500 dark:bg-blue-500", 18 | }, 19 | neutral: { 20 | background: "bg-gray-200 dark:bg-gray-500/40", 21 | bar: "bg-gray-500 dark:bg-gray-500", 22 | }, 23 | warning: { 24 | background: "bg-yellow-200 dark:bg-yellow-500/30", 25 | bar: "bg-yellow-500 dark:bg-yellow-500", 26 | }, 27 | error: { 28 | background: "bg-red-200 dark:bg-red-500/30", 29 | bar: "bg-red-500 dark:bg-red-500", 30 | }, 31 | success: { 32 | background: "bg-emerald-200 dark:bg-emerald-500/30", 33 | bar: "bg-emerald-500 dark:bg-emerald-500", 34 | }, 35 | }, 36 | }, 37 | defaultVariants: { 38 | variant: "default", 39 | }, 40 | }) 41 | 42 | interface ProgressBarProps 43 | extends React.HTMLProps, 44 | VariantProps { 45 | value?: number 46 | max?: number 47 | showAnimation?: boolean 48 | label?: string 49 | } 50 | 51 | const ProgressBar = React.forwardRef( 52 | ( 53 | { 54 | value = 0, 55 | max = 100, 56 | label, 57 | showAnimation = false, 58 | variant, 59 | className, 60 | ...props 61 | }: ProgressBarProps, 62 | forwardedRef, 63 | ) => { 64 | const safeValue = Math.min(max, Math.max(value, 0)) 65 | const { background, bar } = progressBarVariants({ variant }) 66 | return ( 67 |
    77 |
    83 |
    94 |
    95 | {label ? ( 96 | 104 | {label} 105 | 106 | ) : null} 107 |
    108 | ) 109 | }, 110 | ) 111 | 112 | ProgressBar.displayName = "ProgressBar" 113 | 114 | export { ProgressBar, progressBarVariants, type ProgressBarProps } 115 | -------------------------------------------------------------------------------- /src/components/ProgressBar/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor ProgressBar Changelog 2 | 3 | ## 0.0.3 4 | 5 | ### Changes 6 | 7 | - Fix: Acessibility ARIA roles 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Fix: Animation behaviour 20 | -------------------------------------------------------------------------------- /src/components/ProgressBar/progressbar.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect progressbar default", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto( 6 | "http://localhost:6006/?path=/story/visualization-progressbar--default", 7 | ) 8 | await expect( 9 | page 10 | .frameLocator('iframe[title="storybook-preview-iframe"]') 11 | .getByTestId("progressbar"), 12 | ).toBeVisible() 13 | }) 14 | 15 | test("to have a background bar", async ({ page }) => { 16 | await page.goto( 17 | "http://localhost:6006/?path=/story/visualization-progressbar--default", 18 | ) 19 | await expect( 20 | page 21 | .frameLocator('iframe[title="storybook-preview-iframe"]') 22 | .getByTestId("progressbar"), 23 | ).toBeVisible() 24 | await expect( 25 | page 26 | .frameLocator('iframe[title="storybook-preview-iframe"]') 27 | .getByLabel("Progress bar"), 28 | ).toBeVisible() 29 | }) 30 | 31 | test("renders with background and indicator bar properly", async ({ 32 | page, 33 | }) => { 34 | await page.goto( 35 | "http://localhost:6006/?path=/story/visualization-progressbar--default", 36 | ) 37 | 38 | const storyFrame = page.frameLocator( 39 | 'iframe[title="storybook-preview-iframe"]', 40 | ) 41 | 42 | const progressBar = storyFrame.getByRole("progressbar", { 43 | name: "Progress bar", 44 | }) 45 | await expect(progressBar).toBeVisible() 46 | 47 | const barBackground = progressBar.locator(".bg-blue-200") 48 | await expect(barBackground).toBeVisible() 49 | 50 | const indicatorBar = progressBar.locator(".bg-blue-500") 51 | await expect(indicatorBar).toBeVisible() 52 | 53 | await expect(progressBar).toHaveAttribute("aria-valuenow") 54 | await expect(progressBar).toHaveAttribute("aria-valuemax", "100") 55 | }) 56 | 57 | test("updates progress value through controls", async ({ page }) => { 58 | await page.goto( 59 | "http://localhost:6006/?path=/story/visualization-progressbar--default", 60 | ) 61 | 62 | const storyFrame = page.frameLocator( 63 | 'iframe[title="storybook-preview-iframe"]', 64 | ) 65 | const progressBar = storyFrame.getByRole("progressbar", { 66 | name: "Progress bar", 67 | }) 68 | await expect(progressBar).toHaveAttribute("aria-valuenow", "62") 69 | }) 70 | 71 | test("displays label correctly", async ({ page }) => { 72 | await page.goto( 73 | "http://localhost:6006/?path=/story/visualization-progressbar--default", 74 | ) 75 | 76 | const storyFrame = page.frameLocator( 77 | 'iframe[title="storybook-preview-iframe"]', 78 | ) 79 | 80 | await expect(storyFrame.getByText("62%")).toBeVisible() 81 | }) 82 | 83 | test("to have a label", async ({ page }) => { 84 | await page.goto( 85 | "http://localhost:6006/?path=/story/visualization-progressbar--default", 86 | ) 87 | await expect( 88 | page 89 | .frameLocator('iframe[title="storybook-preview-iframe"]') 90 | .getByTestId("progressbar"), 91 | ).toBeVisible() 92 | await page.goto( 93 | "http://localhost:6006/?path=/story/visualization-progressbar--default", 94 | ) 95 | await expect( 96 | page 97 | .frameLocator('iframe[title="storybook-preview-iframe"]') 98 | .getByText("%"), 99 | ).toBeVisible() 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /src/components/ProgressBar/progressbar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react" 2 | 3 | import { Card } from "../Card/Card" 4 | import { ProgressBar } from "./ProgressBar" 5 | 6 | const meta: Meta = { 7 | title: "visualization/ProgressBar", 8 | render: (args) => ( 9 | 10 | ), 11 | component: ProgressBar, 12 | } 13 | 14 | export default meta 15 | type Story = StoryObj 16 | 17 | export const Default: Story = { 18 | args: { 19 | value: 62, 20 | label: "62%", 21 | }, 22 | } 23 | 24 | export const Value0: Story = { 25 | args: { 26 | value: 0, 27 | }, 28 | } 29 | 30 | export const Value120: Story = { 31 | args: { 32 | value: 120, 33 | }, 34 | } 35 | 36 | export const Max: Story = { 37 | args: { 38 | value: 40, 39 | max: 50, 40 | }, 41 | } 42 | 43 | export const VariantsComposition: Story = { 44 | render: () => ( 45 |
    46 |
    47 | 48 | 49 | Default 50 | 51 |
    52 |
    53 | 54 | 55 | Neutral 56 | 57 |
    58 |
    59 | 60 | 61 | Success 62 | 63 |
    64 |
    65 | 66 | 67 | Warning 68 | 69 |
    70 |
    71 | 72 | 73 | Error 74 | 75 |
    76 |
    77 | ), 78 | } 79 | 80 | export const WithKPICard: Story = { 81 | render: () => ( 82 | 83 |

    84 | Sales Progress 85 |

    86 |
    87 | 88 | Goal 2023 89 | 90 | 91 |
    92 |
    93 | ), 94 | } 95 | -------------------------------------------------------------------------------- /src/components/ProgressCircle/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor ProgressCircle Changelog 2 | 3 | ## 0.0.3 4 | 5 | ### Changes 6 | 7 | - Fix: Acessibility ARIA roles 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Fix: Animation behaviour 20 | -------------------------------------------------------------------------------- /src/components/ProgressCircle/progresscircle.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect progresscircle default", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto( 6 | "http://localhost:6006/?path=/story/visualization-progresscircle--default", 7 | ) 8 | await expect( 9 | page 10 | .frameLocator('iframe[title="storybook-preview-iframe"]') 11 | .getByTestId("progresscircle"), 12 | ).toBeVisible() 13 | }) 14 | }) 15 | test.describe("Expect progresscircle with children", () => { 16 | test("to be rendered", async ({ page }) => { 17 | await page.goto( 18 | "http://localhost:6006/?path=/story/visualization-progresscircle--with-children", 19 | ) 20 | await expect( 21 | page 22 | .frameLocator('iframe[title="storybook-preview-iframe"]') 23 | .getByTestId("progresscircle"), 24 | ).toBeVisible() 25 | }) 26 | test("to have children", async ({ page }) => { 27 | await page.goto( 28 | "http://localhost:6006/?path=/story/visualization-progresscircle--with-children", 29 | ) 30 | await expect( 31 | page 32 | .frameLocator('iframe[title="storybook-preview-iframe"]') 33 | .getByTestId("progresscircle"), 34 | ).toBeVisible() 35 | 36 | await expect( 37 | page 38 | .frameLocator('iframe[title="storybook-preview-iframe"]') 39 | .getByText("%"), 40 | ).toBeVisible() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/components/RadioCardGroup/RadioCardGroup.tsx: -------------------------------------------------------------------------------- 1 | // Tremor Radio Card [v1.0.0] 2 | 3 | import React from "react" 4 | import * as RadioGroupPrimitives from "@radix-ui/react-radio-group" 5 | 6 | import { cx } from "../../utils/cx" 7 | import { focusInput } from "../../utils/focusInput" 8 | import { focusRing } from "../../utils/focusRing" 9 | 10 | const RadioCardGroup = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, forwardedRef) => { 14 | return ( 15 | 21 | ) 22 | }) 23 | 24 | RadioCardGroup.displayName = "RadioCardGroup" 25 | 26 | const RadioCardItem = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, children, ...props }, forwardedRef) => { 30 | return ( 31 | 50 | {children} 51 | 52 | ) 53 | }) 54 | 55 | RadioCardItem.displayName = "RadioCardItem" 56 | 57 | const RadioCardIndicator = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, forwardedRef) => { 61 | return ( 62 |
    80 | 85 |
    95 | 96 |
    97 | ) 98 | }) 99 | 100 | RadioCardIndicator.displayName = "RadioCardIndicator" 101 | 102 | export { RadioCardGroup, RadioCardIndicator, RadioCardItem } 103 | -------------------------------------------------------------------------------- /src/components/RadioCardGroup/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor RadioCardGroup Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.3 10 | 11 | ### Changes 12 | 13 | - fix: RadioCard border color 14 | 15 | ## 0.0.2 16 | 17 | ### Changes 18 | 19 | - Chore: Add `tremor-id` 20 | 21 | ## 0.0.1 22 | 23 | ### Changes 24 | 25 | - Fix: Border color 26 | -------------------------------------------------------------------------------- /src/components/RadioCardGroup/radiocardgroup.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect radiocardgroup default", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto( 6 | "http://localhost:6006/?path=/story/ui-radiocardgroup--default", 7 | ) 8 | await expect( 9 | page 10 | .frameLocator('iframe[title="storybook-preview-iframe"]') 11 | .getByRole("radiogroup"), 12 | ).toBeVisible() 13 | }) 14 | test("to render radiogroupitems", async ({ page }) => { 15 | await page.goto( 16 | "http://localhost:6006/?path=/story/ui-radiocardgroup--default", 17 | ) 18 | await expect( 19 | page 20 | .frameLocator('iframe[title="storybook-preview-iframe"]') 21 | .getByRole("radiogroup"), 22 | ).toBeVisible() 23 | await expect( 24 | page 25 | .frameLocator('iframe[title="storybook-preview-iframe"]') 26 | .getByRole("radio", { name: "Software Engineer" }), 27 | ).toBeVisible() 28 | await expect( 29 | page 30 | .frameLocator('iframe[title="storybook-preview-iframe"]') 31 | .getByRole("radio", { name: "Platform Engineer" }), 32 | ).toBeVisible() 33 | await expect( 34 | page 35 | .frameLocator('iframe[title="storybook-preview-iframe"]') 36 | .getByRole("radio", { name: "Hardware Engineer" }), 37 | ).toBeVisible() 38 | await expect( 39 | page 40 | .frameLocator('iframe[title="storybook-preview-iframe"]') 41 | .getByRole("radio"), 42 | ).toHaveCount(3) 43 | }) 44 | 45 | test("to be checkable", async ({ page }) => { 46 | await page.goto( 47 | "http://localhost:6006/?path=/story/ui-radiocardgroup--default", 48 | ) 49 | await page 50 | .frameLocator('iframe[title="storybook-preview-iframe"]') 51 | .getByRole("radio", { name: "Platform Engineer" }) 52 | .click() 53 | await expect( 54 | page 55 | .frameLocator('iframe[title="storybook-preview-iframe"]') 56 | .getByRole("radio", { name: "Platform Engineer" }), 57 | ).toBeVisible() 58 | await expect( 59 | page 60 | .frameLocator('iframe[title="storybook-preview-iframe"]') 61 | .getByRole("radio", { name: "Platform Engineer" }), 62 | ).toBeChecked() 63 | }) 64 | }) 65 | 66 | test.describe("Expect radiogroup disabled", () => { 67 | test("to be disabled", async ({ page }) => { 68 | await page.goto( 69 | "http://localhost:6006/?path=/story/ui-radiocardgroup--disabled", 70 | ) 71 | await expect( 72 | page 73 | .frameLocator('iframe[title="storybook-preview-iframe"]') 74 | .getByRole("radio", { name: "Hardware Engineer" }), 75 | ).toBeDisabled() 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/components/RadioGroup/RadioGroup.tsx: -------------------------------------------------------------------------------- 1 | // Tremor RadioGroup [v1.0.0] 2 | 3 | import React from "react" 4 | import * as RadioGroupPrimitives from "@radix-ui/react-radio-group" 5 | 6 | import { cx } from "../../utils/cx" 7 | import { focusRing } from "../../utils/focusRing" 8 | 9 | const RadioGroup = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, forwardedRef) => { 13 | return ( 14 | 20 | ) 21 | }) 22 | 23 | RadioGroup.displayName = "RadioGroup" 24 | 25 | const RadioGroupIndicator = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, forwardedRef) => { 29 | return ( 30 | 35 |
    45 | 46 | ) 47 | }) 48 | 49 | RadioGroupIndicator.displayName = "RadioGroupIndicator" 50 | 51 | const RadioGroupItem = React.forwardRef< 52 | React.ElementRef, 53 | React.ComponentPropsWithoutRef 54 | >(({ className, ...props }, forwardedRef) => { 55 | return ( 56 | 64 |
    82 | 83 |
    84 |
    85 | ) 86 | }) 87 | 88 | RadioGroupItem.displayName = "RadioGroupItem" 89 | 90 | export { RadioGroup, RadioGroupItem } 91 | -------------------------------------------------------------------------------- /src/components/RadioGroup/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor RadioGroup Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.2 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.0.1 16 | 17 | ### Changes 18 | 19 | - Fix: Remove duplicate size 20 | -------------------------------------------------------------------------------- /src/components/Select/changelog.md: -------------------------------------------------------------------------------- 1 | # Tremor Select Changelog 2 | 3 | ## 1.0.0 4 | 5 | ### Changes 6 | 7 | - BREAKING CHANGE: Tailwind CSS v4 8 | 9 | ## 0.0.3 10 | 11 | ### Changes 12 | 13 | - Chore: Add `tremor-id` 14 | 15 | ## 0.0.2 16 | 17 | ### Changes 18 | 19 | - Fix: Border color 20 | - Fix: Placeholder color 21 | 22 | ## 0.0.1 23 | 24 | ### Changes 25 | 26 | - Fix: Add gap to trigger style 27 | -------------------------------------------------------------------------------- /src/components/Select/select.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test" 2 | 3 | test.describe("Expect select default", () => { 4 | test("to be rendered", async ({ page }) => { 5 | await page.goto("http://localhost:6006/?path=/story/ui-select--default") 6 | await expect( 7 | page 8 | .frameLocator('iframe[title="storybook-preview-iframe"]') 9 | .getByRole("combobox"), 10 | ).toBeVisible() 11 | }) 12 | 13 | test("to make a selection", async ({ page }) => { 14 | await page.goto("http://localhost:6006/?path=/story/ui-select--default") 15 | await expect( 16 | page 17 | .frameLocator('iframe[title="storybook-preview-iframe"]') 18 | .getByRole("combobox"), 19 | ).toBeVisible() 20 | await page 21 | .frameLocator('iframe[title="storybook-preview-iframe"]') 22 | .getByRole("combobox") 23 | .click() 24 | await page 25 | .frameLocator('iframe[title="storybook-preview-iframe"]') 26 | .getByLabel("Striped Dress Shirt") 27 | .click() 28 | await expect( 29 | page 30 | .frameLocator('iframe[title="storybook-preview-iframe"]') 31 | .getByRole("combobox"), 32 | ).toContainText("Striped Dress Shirt") 33 | }) 34 | 35 | test("to have first item selected and subsequent not", async ({ page }) => { 36 | await page.goto("http://localhost:6006/?path=/story/ui-select--default") 37 | await page 38 | .frameLocator('iframe[title="storybook-preview-iframe"]') 39 | .getByRole("combobox") 40 | .click() 41 | await page 42 | .frameLocator('iframe[title="storybook-preview-iframe"]') 43 | .getByLabel("Striped Dress Shirt") 44 | .click() 45 | await page 46 | .frameLocator('iframe[title="storybook-preview-iframe"]') 47 | .getByRole("combobox") 48 | .click() 49 | await expect( 50 | page 51 | .frameLocator('iframe[title="storybook-preview-iframe"]') 52 | .getByLabel("Striped Dress Shirt") 53 | .locator("svg"), 54 | ).toHaveCount(1) 55 | await expect( 56 | page 57 | .frameLocator('iframe[title="storybook-preview-iframe"]') 58 | .getByLabel("Relaxed Fit Button Down") 59 | .locator("svg"), 60 | ).toHaveCount(0) 61 | }) 62 | }) 63 | 64 | test.describe("Expect select with group", () => { 65 | test("to render a group separator", async ({ page }) => { 66 | await page.goto("http://localhost:6006/?path=/story/ui-select--with-groups") 67 | await page 68 | .frameLocator('iframe[title="storybook-preview-iframe"]') 69 | .getByRole("combobox") 70 | .click() 71 | await expect( 72 | page 73 | .frameLocator('iframe[title="storybook-preview-iframe"]') 74 | .getByText("Shirts", { exact: true }), 75 | ).toBeVisible() 76 | }) 77 | }) 78 | 79 | test.describe("Expect select disabled", () => { 80 | test("to be disabled", async ({ page }) => { 81 | await page.goto("http://localhost:6006/?path=/story/ui-select--disabled") 82 | await expect( 83 | page 84 | .frameLocator('iframe[title="storybook-preview-iframe"]') 85 | .getByRole("combobox"), 86 | ).toBeDisabled() 87 | }) 88 | }) 89 | 90 | test.describe("Expect select disabled item", () => { 91 | test("to render a disabled item", async ({ page }) => { 92 | await page.goto( 93 | "http://localhost:6006/?path=/story/ui-select--disabled-item", 94 | ) 95 | await page 96 | .frameLocator('iframe[title="storybook-preview-iframe"]') 97 | .getByRole("combobox") 98 | .click() 99 | await expect( 100 | page 101 | .frameLocator('iframe[title="storybook-preview-iframe"]') 102 | .getByLabel("Solid Dress Shirt"), 103 | ).toBeDisabled() 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /src/components/SelectNative/SelectNative.tsx: -------------------------------------------------------------------------------- 1 | // Tremor SelectNative [v1.0.0] 2 | 3 | import React from "react" 4 | import { tv, type VariantProps } from "tailwind-variants" 5 | 6 | import { cx } from "../../utils/cx" 7 | import { focusInput } from "../../utils/focusInput" 8 | import { hasErrorInput } from "../../utils/hasErrorInput" 9 | 10 | const selectNativeStyles = tv({ 11 | base: [ 12 | // base 13 | "peer w-full cursor-pointer appearance-none truncate rounded-md border py-2 pl-3 pr-7 shadow-xs outline-hidden transition-all sm:text-sm", 14 | // background color 15 | "bg-white dark:bg-gray-950", 16 | // border color 17 | "border-gray-300 dark:border-gray-800", 18 | // text color 19 | "text-gray-900 dark:text-gray-50", 20 | // placeholder color 21 | "placeholder-gray-400 dark:placeholder-gray-500", 22 | // hover 23 | "hover:bg-gray-50 dark:hover:bg-gray-950/50", 24 | // disabled 25 | "disabled:pointer-events-none", 26 | "disabled:bg-gray-100 disabled:text-gray-400", 27 | "dark:disabled:border-gray-700 dark:disabled:bg-gray-800 dark:disabled:text-gray-500", 28 | // focus 29 | focusInput, 30 | // invalid (optional) 31 | // "dark:aria-invalid:ring-red-400/20 aria-invalid:ring-2 aria-invalid:ring-red-200 aria-invalid:border-red-500 invalid:ring-2 invalid:ring-red-200 invalid:border-red-500" 32 | ], 33 | variants: { 34 | hasError: { 35 | true: hasErrorInput, 36 | }, 37 | }, 38 | }) 39 | 40 | interface SelectNativeProps 41 | extends React.InputHTMLAttributes, 42 | VariantProps {} 43 | 44 | const SelectNative = React.forwardRef( 45 | ({ className, hasError, ...props }: SelectNativeProps, forwardedRef) => { 46 | return ( 47 |