Repository: tremorlabs/tremor-npm Branch: main Commit: 7613bff631f7 Files: 317 Total size: 599.3 KB Directory structure: gitextract_8mwyg1k6/ ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yaml │ │ ├── config.yaml │ │ └── feature-request.yaml │ ├── pull_request_template.md │ └── workflows/ │ ├── build.yaml │ ├── lint.yaml │ └── release.yaml ├── .gitignore ├── .prettierrc.json ├── .storybook/ │ ├── main.js │ ├── manager.js │ ├── preview.js │ └── tremorTheme.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── License ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── postcss.config.js ├── rollup.config.js ├── setupTests.js ├── src/ │ ├── assets/ │ │ ├── ArrowDownHeadIcon.tsx │ │ ├── ArrowDownIcon.tsx │ │ ├── ArrowDownRightIcon.tsx │ │ ├── ArrowLeftHeadIcon.tsx │ │ ├── ArrowRightHeadIcon.tsx │ │ ├── ArrowRightIcon.tsx │ │ ├── ArrowUpHeadIcon.tsx │ │ ├── ArrowUpIcon.tsx │ │ ├── ArrowUpRightIcon.tsx │ │ ├── CalendarIcon.tsx │ │ ├── ChevronLeftFill.tsx │ │ ├── ChevronRightFill.tsx │ │ ├── DoubleArrowLeftHeadIcon.tsx │ │ ├── DoubleArrowRightHeadIcon.tsx │ │ ├── ExclamationFilledIcon.tsx │ │ ├── EyeIcon.tsx │ │ ├── EyeOffIcon.tsx │ │ ├── LoadingSpinner.tsx │ │ ├── MinusIcon.tsx │ │ ├── PlusIcon.tsx │ │ ├── SearchIcon.tsx │ │ ├── XCircleIcon.tsx │ │ ├── XIcon.tsx │ │ └── index.ts │ ├── components/ │ │ ├── chart-elements/ │ │ │ ├── AreaChart/ │ │ │ │ ├── AreaChart.tsx │ │ │ │ └── index.ts │ │ │ ├── BarChart/ │ │ │ │ ├── BarChart.tsx │ │ │ │ └── index.ts │ │ │ ├── DonutChart/ │ │ │ │ ├── DonutChart.tsx │ │ │ │ ├── DonutChartTooltip.tsx │ │ │ │ ├── index.ts │ │ │ │ └── inputParser.ts │ │ │ ├── FunnelChart/ │ │ │ │ ├── FunnelChart.tsx │ │ │ │ └── index.ts │ │ │ ├── LineChart/ │ │ │ │ ├── LineChart.tsx │ │ │ │ └── index.ts │ │ │ ├── ScatterChart/ │ │ │ │ ├── ScatterChart.tsx │ │ │ │ ├── ScatterChartTooltip.tsx │ │ │ │ └── index.tsx │ │ │ ├── common/ │ │ │ │ ├── BaseAnimationTimingProps.tsx │ │ │ │ ├── BaseChartProps.tsx │ │ │ │ ├── ChartLegend.tsx │ │ │ │ ├── ChartTooltip.tsx │ │ │ │ ├── CustomTooltipProps.tsx │ │ │ │ ├── NoData.tsx │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ └── index.ts │ │ ├── icon-elements/ │ │ │ ├── Badge/ │ │ │ │ ├── Badge.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.ts │ │ │ ├── BadgeDelta/ │ │ │ │ ├── BadgeDelta.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.ts │ │ │ ├── Icon/ │ │ │ │ ├── Icon.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── input-elements/ │ │ │ ├── BaseInput.tsx │ │ │ ├── Button/ │ │ │ │ ├── Button.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.ts │ │ │ ├── Calendar/ │ │ │ │ ├── Calendar.tsx │ │ │ │ ├── NavButton.tsx │ │ │ │ └── index.ts │ │ │ ├── DatePicker/ │ │ │ │ ├── DatePicker.tsx │ │ │ │ ├── datePickerUtils.tsx │ │ │ │ └── index.ts │ │ │ ├── DateRangePicker/ │ │ │ │ ├── DateRangePicker.tsx │ │ │ │ ├── DateRangePickerItem.tsx │ │ │ │ ├── dateRangePickerUtils.tsx │ │ │ │ └── index.ts │ │ │ ├── MultiSelect/ │ │ │ │ ├── MultiSelect.tsx │ │ │ │ ├── MultiSelectItem.tsx │ │ │ │ └── index.ts │ │ │ ├── NumberInput/ │ │ │ │ ├── NumberInput.tsx │ │ │ │ └── index.ts │ │ │ ├── SearchSelect/ │ │ │ │ ├── SearchSelect.tsx │ │ │ │ ├── SearchSelectItem.tsx │ │ │ │ └── index.ts │ │ │ ├── Select/ │ │ │ │ ├── Select.tsx │ │ │ │ ├── SelectItem.tsx │ │ │ │ └── index.ts │ │ │ ├── Switch/ │ │ │ │ ├── Switch.tsx │ │ │ │ └── index.ts │ │ │ ├── Tabs/ │ │ │ │ ├── Tab.tsx │ │ │ │ ├── TabGroup.tsx │ │ │ │ ├── TabList.tsx │ │ │ │ ├── TabPanel.tsx │ │ │ │ ├── TabPanels.tsx │ │ │ │ └── index.ts │ │ │ ├── TextInput/ │ │ │ │ ├── TextInput.tsx │ │ │ │ └── index.ts │ │ │ ├── Textarea/ │ │ │ │ ├── Textarea.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── selectUtils.ts │ │ ├── layout-elements/ │ │ │ ├── Accordion/ │ │ │ │ ├── Accordion.tsx │ │ │ │ ├── AccordionBody.tsx │ │ │ │ ├── AccordionHeader.tsx │ │ │ │ ├── AccordionList.tsx │ │ │ │ └── index.ts │ │ │ ├── Card/ │ │ │ │ ├── Card.tsx │ │ │ │ └── index.ts │ │ │ ├── Dialog/ │ │ │ │ ├── Dialog.tsx │ │ │ │ ├── DialogPanel.tsx │ │ │ │ └── index.ts │ │ │ ├── Divider/ │ │ │ │ ├── Divider.tsx │ │ │ │ └── index.ts │ │ │ ├── Flex/ │ │ │ │ ├── Flex.tsx │ │ │ │ └── index.ts │ │ │ ├── Grid/ │ │ │ │ ├── Col.tsx │ │ │ │ ├── Grid.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.ts │ │ │ └── index.ts │ │ ├── list-elements/ │ │ │ ├── List/ │ │ │ │ ├── List.tsx │ │ │ │ ├── ListItem.tsx │ │ │ │ └── index.ts │ │ │ ├── Table/ │ │ │ │ ├── Table.tsx │ │ │ │ ├── TableBody.tsx │ │ │ │ ├── TableCell.tsx │ │ │ │ ├── TableFoot.tsx │ │ │ │ ├── TableFooterCell.tsx │ │ │ │ ├── TableHead.tsx │ │ │ │ ├── TableHeaderCell.tsx │ │ │ │ ├── TableRow.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── spark-elements/ │ │ │ ├── SparkAreaChart/ │ │ │ │ ├── SparkAreaChart.tsx │ │ │ │ └── index.ts │ │ │ ├── SparkBarChart/ │ │ │ │ ├── SparkBarChart.tsx │ │ │ │ └── index.ts │ │ │ ├── SparkLineChart/ │ │ │ │ ├── SparkLineChart.tsx │ │ │ │ └── index.ts │ │ │ ├── common/ │ │ │ │ └── BaseSparkChartProps.tsx │ │ │ └── index.ts │ │ ├── text-elements/ │ │ │ ├── Bold/ │ │ │ │ ├── Bold.tsx │ │ │ │ └── index.ts │ │ │ ├── Callout/ │ │ │ │ ├── Callout.tsx │ │ │ │ └── index.ts │ │ │ ├── Italic/ │ │ │ │ ├── Italic.tsx │ │ │ │ └── index.ts │ │ │ ├── Legend/ │ │ │ │ ├── Legend.tsx │ │ │ │ └── index.ts │ │ │ ├── Metric/ │ │ │ │ ├── Metric.tsx │ │ │ │ └── index.ts │ │ │ ├── Subtitle/ │ │ │ │ ├── Subtitle.tsx │ │ │ │ └── index.ts │ │ │ ├── Text/ │ │ │ │ ├── Text.tsx │ │ │ │ └── index.ts │ │ │ ├── Title/ │ │ │ │ ├── Title.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── util-elements/ │ │ │ ├── Tooltip/ │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ └── vis-elements/ │ │ ├── BarList/ │ │ │ ├── BarList.tsx │ │ │ └── index.ts │ │ ├── CategoryBar/ │ │ │ ├── CategoryBar.tsx │ │ │ └── index.ts │ │ ├── DeltaBar/ │ │ │ ├── DeltaBar.tsx │ │ │ ├── index.ts │ │ │ └── styles.ts │ │ ├── MarkerBar/ │ │ │ ├── MarkerBar.tsx │ │ │ └── index.ts │ │ ├── ProgressBar/ │ │ │ ├── ProgressBar.tsx │ │ │ └── index.ts │ │ ├── ProgressCircle/ │ │ │ ├── ProgressCircle.tsx │ │ │ └── index.ts │ │ ├── Tracker/ │ │ │ ├── Tracker.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── contexts/ │ │ ├── BaseColorContext.tsx │ │ ├── IndexContext.tsx │ │ ├── RootStylesContext.tsx │ │ ├── SelectedValueContext.tsx │ │ └── index.ts │ ├── hooks/ │ │ ├── index.ts │ │ ├── useInternalState.tsx │ │ └── useOnWindowResize.tsx │ ├── index.ts │ ├── lib/ │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── inputTypes.ts │ │ ├── theme.ts │ │ ├── tremorTwMerge.ts │ │ └── utils.tsx │ ├── stories/ │ │ ├── chart-elements/ │ │ │ ├── AreaChart.stories.tsx │ │ │ ├── BarChart.stories.tsx │ │ │ ├── DonutChart.stories.tsx │ │ │ ├── FunnelChart.stories.tsx │ │ │ ├── LineChart.stories.tsx │ │ │ ├── ScatterChart.stories.tsx │ │ │ └── helpers/ │ │ │ ├── testData.tsx │ │ │ ├── testDataScatterChart.tsx │ │ │ └── utils.ts │ │ ├── icon-elements/ │ │ │ ├── Badge.stories.tsx │ │ │ ├── BadgeDelta.stories.tsx │ │ │ └── Icon.stories.tsx │ │ ├── input-elements/ │ │ │ ├── Button.stories.tsx │ │ │ ├── DatePicker.stories.tsx │ │ │ ├── DateRangePicker.stories.tsx │ │ │ ├── MultiSelect.stories.tsx │ │ │ ├── NumberInput.stories.tsx │ │ │ ├── SearchSelect.stories.tsx │ │ │ ├── Select.stories.tsx │ │ │ ├── Switch.stories.tsx │ │ │ ├── Tabs.stories.tsx │ │ │ ├── TextArea.stories.tsx │ │ │ ├── TextInput.stories.tsx │ │ │ └── helpers/ │ │ │ ├── SimpleMultiSelect.tsx │ │ │ ├── SimpleNumberInput.tsx │ │ │ ├── SimpleSearchSelect.tsx │ │ │ ├── SimpleSelect.tsx │ │ │ ├── SimpleSwitch.tsx │ │ │ ├── SimpleTextInput.tsx │ │ │ └── testData.ts │ │ ├── layout-elements/ │ │ │ ├── Accordion.stories.tsx │ │ │ ├── AccordionList.stories.tsx │ │ │ ├── Card.stories.tsx │ │ │ ├── Dialog.stories.tsx │ │ │ ├── Divider.stories.tsx │ │ │ ├── Flex.stories.tsx │ │ │ ├── Grid.stories.tsx │ │ │ └── helpers/ │ │ │ ├── SimpleAccordion.tsx │ │ │ ├── SimpleCard.tsx │ │ │ └── SimpleText.tsx │ │ ├── list-elements/ │ │ │ ├── List.stories.tsx │ │ │ └── Table.stories.tsx │ │ ├── spark-elements/ │ │ │ ├── SparkAreaChart.stories.tsx │ │ │ ├── SparkBarChart.stories.tsx │ │ │ ├── SparkLineChart.stories.tsx │ │ │ └── helpers/ │ │ │ ├── ExampleCard.tsx │ │ │ └── testData.ts │ │ ├── text-elements/ │ │ │ ├── Callout.stories.tsx │ │ │ ├── Legend.stories.tsx │ │ │ ├── Metric.stories.tsx │ │ │ ├── Subtitle.stories.tsx │ │ │ ├── Text.stories.tsx │ │ │ ├── TextElements.stories.tsx │ │ │ └── Title.stories.tsx │ │ └── vis-elements/ │ │ ├── BarList.stories.tsx │ │ ├── CategoryBar.stories.tsx │ │ ├── DeltaBar.stories.tsx │ │ ├── MarkerBar.stories.tsx │ │ ├── ProgressBar.stories.tsx │ │ ├── ProgressCircle.stories.tsx │ │ └── Tracker.stories.tsx │ ├── styles.css │ └── tests/ │ ├── chart-elements/ │ │ ├── AreaChart.test.tsx │ │ └── BarChart.test.tsx │ ├── icon-elements/ │ │ ├── Badge.test.tsx │ │ ├── BadgeDelta.test.tsx │ │ └── Icon.test.tsx │ ├── input-elements/ │ │ ├── Button.test.tsx │ │ ├── DatePicker.test.tsx │ │ ├── DateRangePicker.test.tsx │ │ ├── MultiSelect.test.tsx │ │ ├── NumberInput.test.tsx │ │ ├── SearchSelect.test.tsx │ │ ├── Select.test.tsx │ │ ├── Switch.text.tsx │ │ ├── Tabs.test.tsx │ │ ├── TextInput.test.tsx │ │ └── Textarea.test.tsx │ ├── layout-elements/ │ │ ├── Accordion.test.tsx │ │ ├── AccordionList.test.tsx │ │ ├── Card.test.tsx │ │ ├── Dialog.test.tsx │ │ ├── Divider.test.tsx │ │ ├── Flex.test.tsx │ │ └── Grid.test.tsx │ ├── list-elements/ │ │ ├── List.test.tsx │ │ └── Table.test.tsx │ ├── spark-elements/ │ │ └── SparkAreaChart.test.tsx │ ├── text-elements/ │ │ ├── Callout.test.tsx │ │ ├── Legend.test.tsx │ │ ├── Metric.test.tsx │ │ ├── Subtitle.test.tsx │ │ ├── Text.test.tsx │ │ └── Title.test.tsx │ └── vis-elements/ │ ├── BarList.test.tsx │ ├── CategoryBar.test.tsx │ ├── DeltaBar.test.tsx │ ├── MarkerBar.test.tsx │ ├── ProgressBar.test.tsx │ ├── ProgressCircle.text.tsx │ └── Tracker.test.tsx ├── tailwind.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ node_modules dist types ================================================ FILE: .eslintrc ================================================ { "root": true, "extends": [ "prettier", "plugin:prettier/recommended", "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint", "prettier", "react", "react-hooks" ], "rules": { "react/button-has-type": "error", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off", "react/prop-types": "off" }, "settings": { "react": { "version": "detect" } }, "env": { "browser": true, "node": true }, "globals": { "JSX": true } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yaml ================================================ name: "\U0001F41E Bug report" description: Create a report to help us improve title: "[Bug]: " labels: ["Triage"] body: - type: markdown attributes: value: | **Before submitting a bug report** The issue list is reserved for bug reports and feature requests. If you have a usage question, you can: - Read the [documentation](https://npm.tremor.so/docs/getting-started/installation) - Ask questions on [Slack](https://join.slack.com/t/tremor-community/shared_invite/zt-1u8jqmcmq-Fdr9B6MbnO7u8FkGh~2Ylg) Also try to search for your issue/feature - another user may have already requested something similar, thanks! - type: input id: version attributes: label: Tremor Version validations: required: true - type: input id: reproduction-link attributes: label: Link to minimal reproduction description: | 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). The reproduction should be **minimal**. This means, it should contain only the bare minimum amount of code needed to show the bug. placeholder: Reproduction Link validations: required: true - type: textarea id: steps-to-reproduce attributes: label: Steps to reproduce description: | 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. placeholder: Steps to reproduce validations: required: true - type: textarea id: expected attributes: label: What is expected? validations: required: true - type: textarea id: actually-happening attributes: label: What is actually happening? validations: required: true - type: dropdown id: browsers attributes: label: What browsers are you seeing the problem on? multiple: true options: - Chrome - Microsoft Edge - Safari - Firefox - Vivaldi - Brave - Other - type: textarea id: additional-comments attributes: label: Any additional comments? description: E.g. some background/context of how you ran into this bug. - type: markdown attributes: value: | This bug report template was inspired by the issue template from [vuejs](https://github.com/vuejs/core) ================================================ FILE: .github/ISSUE_TEMPLATE/config.yaml ================================================ blank_issues_enabled: true contact_links: - name: Slack Community url: https://join.slack.com/t/tremor-community/shared_invite/zt-1u8jqmcmq-Fdr9B6MbnO7u8FkGh~2Ylg about: Please ask and answer usage questions here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yaml ================================================ name: "\U0001F680 New feature proposal" description: Suggest an idea for this project title: "[Feature]: " labels: ["Triage"] body: - type: markdown attributes: value: | **Before submitting a feature request** The issue list is reserved for bug reports and feature requests. If you have a usage question, you can: - Read the [documentation](https://npm.tremor.so/docs/getting-started/installation) - Ask questions on [Slack](https://join.slack.com/t/tremor-community/shared_invite/zt-1u8jqmcmq-Fdr9B6MbnO7u8FkGh~2Ylg) Also try to search for your issue/feature - another user may have already requested something similar, thanks! - type: textarea id: problem-description attributes: label: What problem does this feature solve? description: | Explain your use case, context, and rationale behind this feature request. An important design goal of Tremor is keeping the API small. The problem should be common enough to justify the addition. placeholder: Problem description validations: required: true - type: textarea id: proposed-API attributes: label: What does the proposed API look like? description: | 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. placeholder: Assumed API - type: markdown attributes: value: | This bug report template was inspired by the feature template from [vuejs](https://github.com/vuejs/core) ================================================ FILE: .github/pull_request_template.md ================================================ **Description** **Related issue(s)** **What kind of change does this PR introduce?** (check at least one) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] New Feature (BREAKING CHANGE which adds functionality) - [ ] Refactor - [ ] Build-related changes - [ ] Other, please describe: **Does this PR introduce a breaking change?** (check one) - [ ] Yes - [ ] No If yes, please describe the impact and migration path for existing applications: **How has this been tested?** **Screenshots (if appropriate):** **The PR fulfils these requirements:** - [ ] It's submitted to the `main` branch - [ ] When resolving a specific issue, it's referenced in the related issue section above - [ ] My change requires a change to the documentation. (Managed by Tremor Team) - [ ] I have added tests to cover my changes - [ ] 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. - [ ] Add refs #XXX or fixes #XXX to the related issue section if your PR refers to or fixes an issue. - [ ] 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-npm/blob/main/License) license. ================================================ FILE: .github/workflows/build.yaml ================================================ name: "Build" on: pull_request: types: - opened - edited - synchronize push: branches: - "**" - "!main" jobs: build: name: Build dist runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 - name: node uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org - name: install react run: npm i react - name: install dependencies run: npm ci - name: lint checks run: npm run lint - name: unit tests run: npm run tests - name: build run: npm run build ================================================ FILE: .github/workflows/lint.yaml ================================================ name: "Lint PR" on: pull_request: types: - opened - edited - synchronize pull_request_target: types: - opened - edited - synchronize jobs: lint-pr: name: Validate PR title runs-on: ubuntu-latest steps: - name: Lint PR uses: amannn/action-semantic-pull-request@v4.6.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yaml ================================================ name: "Release" on: push: branches: - main - beta - beta-** jobs: release: name: Release runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v4 - name: node uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org - name: install react run: npm i react - name: install dependencies run: npm ci - name: build run: npm run build - name: release env: NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release ================================================ FILE: .gitignore ================================================ node_modules dist .DS_Store storybook-static package-lock.json .vscode yarn.lock ================================================ FILE: .prettierrc.json ================================================ { "bracketSpacing": true, "singleQuote": false, "trailingComma": "all", "tabWidth": 2, "semi": true, "printWidth": 100, "jsxSingleQuote": false, "endOfLine": "auto" } ================================================ FILE: .storybook/main.js ================================================ var path = require("path"); module.exports = { stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], addons: [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/addon-styling-webpack", "@storybook/addon-themes", "@storybook/addon-a11y", { name: "@storybook/addon-styling-webpack", options: { rules: [ { test: /\.css$/, sideEffects: true, use: [ require.resolve("style-loader"), { loader: require.resolve("css-loader"), options: { importLoaders: 1, }, }, { loader: require.resolve("postcss-loader"), options: { implementation: require.resolve("postcss"), }, }, ], }, ], }, }, "@storybook/addon-webpack5-compiler-babel", "@chromatic-com/storybook", ], framework: { name: "@storybook/react-webpack5", options: {}, }, features: { previewMdx2: true, }, webpackFinal: async (config) => { config.resolve.modules = [...(config.resolve.modules || []), path.resolve(__dirname, "../src")]; return config; }, docs: {}, typescript: { reactDocgen: "react-docgen-typescript", }, }; ================================================ FILE: .storybook/manager.js ================================================ import { addons } from "@storybook/manager-api"; import { themes } from "@storybook/theming"; import tremorTheme from "./tremorTheme"; addons.setConfig({ theme: tremorTheme, }); ================================================ FILE: .storybook/preview.js ================================================ import "../src/styles.css"; import { withThemeByDataAttribute } from "@storybook/addon-themes"; export const parameters = { controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, backgrounds: { default: "light", values: [ { name: "light", value: "#ffffff", }, { name: "dark", value: "#0f172a", }, ], }, }; export const decorators = [ withThemeByDataAttribute({ themes: { light: "light", dark: "dark", }, defaultTheme: "light", attributeName: "data-mode", }), ]; export const tags = ["autodocs"]; ================================================ FILE: .storybook/tremorTheme.js ================================================ import { create } from "@storybook/theming/create"; export default create({ base: "light", brandTitle: "Tremor Storybook", brandUrl: "https://storybook.tremor.so", // brandImage: 'images/tremor-logo.svg', brandTarget: "_self", // colorSecondary: "#3b82f6", // UI appBg: "#ffffff", appContentBg: "#ffffff", // appBorderColor: '#585C6D', appBorderRadius: 0, // barTextColor: "#9E9E9E", barSelectedColor: "#3b82f6", barBg: "#ffffff", }); ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ ## **Contributing to Tremor** Thanks for your interest in contributing to Tremor. Please take a moment to review this document before submitting a pull request. This document will outline how to submit changes to this repository and which conventions to follow. If you are ever in doubt about anything we encourage you to reach out on [Slack](https://tremor-community.slack.com/join/shared_invite/zt-2a95vjndc-YCKurK3HVAkYtjialnT2_A#/shared-invite/email), [open a discussion](#discussions), or [shoot us an email](mailto:hello@tremor.so). ### **Prerequisites** - You are familiar with [issues](#issues) and [pull requests](#pulls). - You have read the [docs](https://npm.tremor.so/docs/getting-started/installation). ### **Issues before PRs** 1. Before you start working on a change please make sure that there is an issue for what you will be working on. You can either find an [existing issue](https://github.com/tremorlabs/tremor-npm/issues) or [open a new issue](https://github.com/tremorlabs/tremor-npm/issues/new/choose) if none exists. 2. When you are ready to start working on a change you should first [fork the Tremor repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) and [branch out](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository) from the `main` branch. 3. Make your changes. 4. [Open a pull request towards the main branch in the Tremor repo](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). Then, our team will review, comment and eventually approve your PR. ### **Branches** All changes should be part of a branch and submitted as a pull request - your branches should be prefixed with one of: - `fix/` for bug fixes - `feat/` for features ### **Commits** Strive towards keeping your commits small and isolated - this helps the reviewer understand what is going on and makes it easier to process your requests. ### **Pull Requests** Once your changes are ready you must submit your branch as a pull request. Your pull request should be opened against the `main` branch in the main Tremor repo. In your PR's description, you should follow the structure as outlined in the PR template: 1. Description: Describe your changes in detail. 2. Related issue(s): Please link to the issue. 3. What kind of change does this PR introduce?: Select from template options. 4. Does this PR introduce a breaking change?: Select Yes/No. 5. How has This been tested?: Please describe how you tested your changes. 6. Screenshots (if appropriate): **The PR fulfills these requirements:** - [ ] It's submitted to the `main` branch. - [ ] When resolving a specific issue, it's referenced in the related issue section above. - [ ] My change requires a change to the documentation. (Managed by Tremor Team). - [ ] I have added tests to cover my changes. * Be sure to check the "Allow edits from maintainers" option while creating your PR. * If your PR refers to or fixes an issue, be sure to add refs #XXX or fixes #XXX to the related issue section. Replacing XXX with the respective issue number. Be sure to fill the PR Template accordingly. We encourage that you do a self-review prior to requesting a review. To do a self review click the review button in the top right corner, go through your code and annotate your changes. This makes it easier for the reviewer to process your PR. ### **Documentation** - We generally encourage you to document your changes through comments in your code. - If you alter user-facing behavior you must provide documentation for such changes, for reference, check out [our documentation](<[url](https://npm.tremor.so/docs/getting-started/introduction)>). ### **Licensing** By contributing to Tremor, you agree that your contributions will be licensed under the [Apache License 2.0](https://github.com/tremorlabs/tremor-npm/blob/main/License) license. By submitting your pull request, you agree to our [Contributor License Agreement (CLA)](https://tremor.so/contributors). This agreement clarifies our ability to incorporate your contributions. ================================================ FILE: License ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: README.md ================================================

Tremor Logo



npm Read the documentation License Apache 2.0 Join Slack Follow at Tremorlabs

DocumentationWebsite


React components to build charts and dashboards

[Tremor NPM](https://npm.tremor.so/) 20+ open-source components built on top of Tailwind CSS to make visualizing data simple again. Fully open-source, made by data scientists and software engineers with a sweet spot for design.
![Tremor Banner](images/banner-github-readme.png)
## Getting Started See our [Installation Guide](https://npm.tremor.so/docs/getting-started/installation). To make use of the library we also need Tailwind CSS setup in the project. ## Example With Tremor creating an analytical interface is easy. ```jsx "use client"; import { AreaChart, Card } from "@tremor/react"; const chartdata = [ { date: "Jan 23", "Route Requests": 289, "Station Requests": 233, }, // ... { date: "Oct 23", "Route Requests": 283, "Station Requests": 247, }, ]; export default function Example() { return ( Total Requests

6,568

); } ```
Tremor Example ## Community and Contribution We are always looking for new ideas or other ways to improve Tremor NPM. If you have developed anything cool or found a bug, send us a pull request. Check out our Contributor License Agreement [here](https://tremor.so/contributors). ## License [Apache License 2.0](https://github.com/tremorlabs/tremor-npm/blob/main/License) Copyright © 2025 Tremor Labs, Inc. All rights reserved. ================================================ FILE: babel.config.js ================================================ /* eslint-disable no-undef */ module.exports = { presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], }; ================================================ FILE: jest.config.js ================================================ /* eslint-disable no-undef */ module.exports = { testEnvironment: "jsdom", moduleDirectories: ["node_modules", "src"], moduleNameMapper: { ".(css|less|scss)$": "identity-obj-proxy", "components/(.*)": "/src/components/$1", "assets/(.*)": "/src/assets/$1", }, transformIgnorePatterns: ["/node_modules/(?!react-dnd|dnd-core|@react-dnd)"], }; ================================================ FILE: package.json ================================================ { "name": "@tremor/react", "version": "0.0.0-development", "description": "The React library to build dashboards faster.", "scripts": { "prebuild": "rm -rf dist", "build": "rollup -c", "lint": "eslint \"{**/*,*}.{js,ts,jsx,tsx}\"", "prettier": "prettier --write \"{src,types,tests,example/src}/**/*.{js,ts,jsx,tsx}\"", "tests": "jest", "fix-lint": "eslint . --ext .ts --ext .tsx --fix", "build-storybook": "storybook build", "semantic-release": "semantic-release", "storybook": "storybook dev -p 6006" }, "repository": { "type": "git", "url": "git+https://github.com/tremorlabs/tremor-npm.git" }, "author": "tremor", "license": "Apache 2.0", "bugs": { "url": "https://github.com/tremorlabs/tremor-npm/issues" }, "homepage": "https://github.com/tremorlabs/tremor-npm-npm#readme", "dependencies": { "@floating-ui/react": "^0.19.2", "@headlessui/react": "2.2.0", "date-fns": "^3.6.0", "react-day-picker": "^8.10.1", "react-transition-state": "^2.1.2", "recharts": "^2.13.3", "tailwind-merge": "^2.5.2" }, "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.4", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", "@chromatic-com/storybook": "^3.2.2", "@mdx-js/react": "^2.3.0", "@rollup/plugin-commonjs": "^21.1.0", "@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^8.5.0", "@semantic-release/commit-analyzer": "^13.0.0", "@semantic-release/github": "github:semantic-release/github", "@semantic-release/npm": "github:semantic-release/npm", "@storybook/addon-a11y": "^8.4.7", "@storybook/addon-actions": "^8.4.7", "@storybook/addon-essentials": "^8.4.7", "@storybook/addon-interactions": "^8.4.7", "@storybook/addon-links": "^8.4.7", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-themes": "^8.4.7", "@storybook/addon-webpack5-compiler-babel": "^3.0.3", "@storybook/manager-api": "^8.4.7", "@storybook/mdx2-csf": "^1.1.0", "@storybook/react": "^8.4.7", "@storybook/react-vite": "^8.4.7", "@storybook/react-webpack5": "^8.4.7", "@storybook/test": "^8.4.7", "@storybook/theming": "^8.4.7", "@tailwindcss/forms": "^0.5.9", "@testing-library/react": "^14.3.1", "@types/jest": "^29.5.13", "@types/node": "^22.6.1", "@types/react": "^18.3.9", "@typescript-eslint/eslint-plugin": "^8.7.0", "@typescript-eslint/parser": "^8.7.0", "autoprefixer": "^10.4.20", "babel-jest": "^27.5.1", "babel-loader": "^8.4.1", "conventional-changelog-conventionalcommits": "^5.0.0", "css-loader": "^6.11.0", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.36.1", "eslint-plugin-react-hooks": "^4.6.2", "html-webpack-plugin": "^5.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "postcss": "^8.4.47", "postcss-loader": "^7.3.4", "prettier": "3.4.2", "prop-types": "^15.8.1", "react": "^18.3.1", "react-dom": "^18.3.1", "resize-observer-polyfill": "^1.5.1", "rollup": "^2.79.1", "rollup-plugin-dts": "^4.2.3", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-preserve-directives": "^0.1.1", "rollup-plugin-typescript-paths": "^1.5.0", "semantic-release": "^24.1.1", "storybook": "^8.4.7", "style-loader": "^3.3.4", "tailwindcss": "^3.4.16", "tslib": "^2.7.0", "typescript": "^4.9.5", "webpack": "^5.97.1" }, "peerDependencies": { "react": "^18.0.0", "react-dom": ">=16.6.0" }, "main": "dist/index.cjs", "module": "dist/index.js", "files": [ "dist" ], "types": "dist/index.d.ts", "publishConfig": { "access": "public" }, "release": { "branches": [ "main", { "name": "beta", "prerelease": true }, { "name": "beta-*", "prerelease": true } ], "plugins": [ [ "@semantic-release/commit-analyzer", { "preset": "conventionalcommits", "releaseRules": [ { "type": "build", "release": "minor" } ] } ], "@semantic-release/npm", "@semantic-release/github" ] } } ================================================ FILE: postcss.config.js ================================================ /* eslint-disable no-undef */ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: rollup.config.js ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable no-undef */ import commonjs from "@rollup/plugin-commonjs"; import resolve from "@rollup/plugin-node-resolve"; import typescript from "@rollup/plugin-typescript"; import dts from "rollup-plugin-dts"; import peerDepsExternal from "rollup-plugin-peer-deps-external"; import { typescriptPaths } from "rollup-plugin-typescript-paths"; import terser from "@rollup/plugin-terser"; import preserveDirectives from "rollup-plugin-preserve-directives"; const outputOptions = { sourcemap: false, preserveModules: true, preserveModulesRoot: "src", }; export default [ { input: "src/index.ts", output: [ { dir: "dist", format: "cjs", entryFileNames: "[name].cjs", exports: "auto", ...outputOptions, }, { dir: "dist", format: "esm", ...outputOptions, }, ], external: [/node_modules/], plugins: [ peerDepsExternal(), resolve(), commonjs(), preserveDirectives(), terser(), typescript({ tsconfig: "./tsconfig.json", exclude: ["**/stories/**", "**/tests/**", "./styles.css"], }), typescriptPaths(), ], }, { input: "dist/index.d.ts", output: [{ file: "dist/index.d.ts", format: "esm" }], plugins: [dts()], external: [/\.css$/], }, ]; ================================================ FILE: setupTests.js ================================================ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ global.ResizeObserver = require("resize-observer-polyfill"); ================================================ FILE: src/assets/ArrowDownHeadIcon.tsx ================================================ import React from "react"; const ArrowDownHeadIcon = ({ ...props }) => ( ); export default ArrowDownHeadIcon; ================================================ FILE: src/assets/ArrowDownIcon.tsx ================================================ import React from "react"; const ArrowDownIcon = ({ ...props }) => ( ); export default ArrowDownIcon; ================================================ FILE: src/assets/ArrowDownRightIcon.tsx ================================================ import React from "react"; const ArrowDownRightIcon = ({ ...props }) => ( ); export default ArrowDownRightIcon; ================================================ FILE: src/assets/ArrowLeftHeadIcon.tsx ================================================ import React from "react"; const ArrowLeftHeadIcon = ({ ...props }) => ( ); export default ArrowLeftHeadIcon; ================================================ FILE: src/assets/ArrowRightHeadIcon.tsx ================================================ import React from "react"; const ArrowRightHeadIcon = ({ ...props }) => ( ); export default ArrowRightHeadIcon; ================================================ FILE: src/assets/ArrowRightIcon.tsx ================================================ import React from "react"; const ArrowRightIcon = ({ ...props }) => ( ); export default ArrowRightIcon; ================================================ FILE: src/assets/ArrowUpHeadIcon.tsx ================================================ import React from "react"; const ArrowUpHeadIcon = ({ ...props }) => ( ); export default ArrowUpHeadIcon; ================================================ FILE: src/assets/ArrowUpIcon.tsx ================================================ import React from "react"; const ArrowUpIcon = ({ ...props }) => ( ); export default ArrowUpIcon; ================================================ FILE: src/assets/ArrowUpRightIcon.tsx ================================================ import React from "react"; const ArrowUpRightIcon = ({ ...props }) => ( ); export default ArrowUpRightIcon; ================================================ FILE: src/assets/CalendarIcon.tsx ================================================ import React from "react"; const CalendarIcon = ({ ...props }) => ( ); export default CalendarIcon; ================================================ FILE: src/assets/ChevronLeftFill.tsx ================================================ import React from "react"; const ChevronLeftFill = ({ ...props }) => ( ); export default ChevronLeftFill; ================================================ FILE: src/assets/ChevronRightFill.tsx ================================================ import React from "react"; const ChevronRightFill = ({ ...props }) => ( ); export default ChevronRightFill; ================================================ FILE: src/assets/DoubleArrowLeftHeadIcon.tsx ================================================ import React from "react"; const DoubleArrowLeftHeadIcon = ({ ...props }) => ( ); export default DoubleArrowLeftHeadIcon; ================================================ FILE: src/assets/DoubleArrowRightHeadIcon.tsx ================================================ import React from "react"; const DoubleArrowRightHeadIcon = ({ ...props }) => ( ); export default DoubleArrowRightHeadIcon; ================================================ FILE: src/assets/ExclamationFilledIcon.tsx ================================================ import React from "react"; const ExclamationFilledIcon = ({ ...props }) => ( ); export default ExclamationFilledIcon; ================================================ FILE: src/assets/EyeIcon.tsx ================================================ import React from "react"; const EyeIcon = ({ ...props }) => ( ); export default EyeIcon; ================================================ FILE: src/assets/EyeOffIcon.tsx ================================================ import React from "react"; const EyeOffIcon = ({ ...props }) => ( ); export default EyeOffIcon; ================================================ FILE: src/assets/LoadingSpinner.tsx ================================================ import React from "react"; const LoadingSpinner = ({ ...props }) => ( ); export default LoadingSpinner; ================================================ FILE: src/assets/MinusIcon.tsx ================================================ import React from "react"; const MinusIcon = ({ ...props }) => ( ); export default MinusIcon; ================================================ FILE: src/assets/PlusIcon.tsx ================================================ import React from "react"; const PlusIcon = ({ ...props }) => ( ); export default PlusIcon; ================================================ FILE: src/assets/SearchIcon.tsx ================================================ import React from "react"; const SearchIcon = ({ ...props }) => ( ); export default SearchIcon; ================================================ FILE: src/assets/XCircleIcon.tsx ================================================ import React from "react"; const XCircleIcon = ({ ...props }) => ( ); export default XCircleIcon; ================================================ FILE: src/assets/XIcon.tsx ================================================ import React from "react"; const XIcon = ({ ...props }) => { return ( ); }; export default XIcon; ================================================ FILE: src/assets/index.ts ================================================ export { default as ArrowDownHeadIcon } from "./ArrowDownHeadIcon"; export { default as ArrowDownIcon } from "./ArrowDownIcon"; export { default as ArrowDownRightIcon } from "./ArrowDownRightIcon"; export { default as ArrowLeftHeadIcon } from "./ArrowLeftHeadIcon"; export { default as ArrowRightHeadIcon } from "./ArrowRightHeadIcon"; export { default as ArrowRightIcon } from "./ArrowRightIcon"; export { default as ArrowUpHeadIcon } from "./ArrowUpHeadIcon"; export { default as ArrowUpIcon } from "./ArrowUpIcon"; export { default as ArrowUpRightIcon } from "./ArrowUpRightIcon"; export { default as CalendarIcon } from "./CalendarIcon"; export { default as DoubleArrowLeftHeadIcon } from "./DoubleArrowLeftHeadIcon"; export { default as DoubleArrowRightHeadIcon } from "./DoubleArrowRightHeadIcon"; export { default as ExclamationFilledIcon } from "./ExclamationFilledIcon"; export { default as EyeIcon } from "./EyeIcon"; export { default as EyeOffIcon } from "./EyeOffIcon"; export { default as LoadingSpinner } from "./LoadingSpinner"; export { default as SearchIcon } from "./SearchIcon"; export { default as XCircleIcon } from "./XCircleIcon"; export { default as PlusIcon } from "./PlusIcon"; export { default as MinusIcon } from "./MinusIcon"; export { default as ChevronLeftFill } from "./ChevronLeftFill"; export { default as ChevronRightFill } from "./ChevronRightFill"; ================================================ FILE: src/components/chart-elements/AreaChart/AreaChart.tsx ================================================ "use client"; import React, { Fragment, useState } from "react"; import { Area, AreaChart as ReChartsAreaChart, CartesianGrid, Dot, Legend, Line, ResponsiveContainer, Tooltip, XAxis, YAxis, Label, } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; import BaseChartProps from "../common/BaseChartProps"; import ChartLegend from "../common/ChartLegend"; import ChartTooltip from "../common/ChartTooltip"; import NoData from "../common/NoData"; import { constructCategoryColors, getYAxisDomain, hasOnlyOneValueForThisKey, } from "../common/utils"; import { BaseColors, colorPalette, defaultValueFormatter, getColorClassNames, themeColorRange, tremorTwMerge, } from "lib"; import { CurveType } from "../../../lib/inputTypes"; export interface AreaChartProps extends BaseChartProps { stack?: boolean; curveType?: CurveType; connectNulls?: boolean; showGradient?: boolean; } interface ActiveDot { index?: number; dataKey?: string; } const AreaChart = React.forwardRef((props, ref) => { const { data = [], categories = [], index, stack = false, colors = themeColorRange, valueFormatter = defaultValueFormatter, startEndOnly = false, showXAxis = true, showYAxis = true, yAxisWidth = 56, intervalType = "equidistantPreserveStart", showAnimation = false, animationDuration = 900, showTooltip = true, showLegend = true, showGridLines = true, showGradient = true, autoMinValue = false, curveType = "linear", minValue, maxValue, connectNulls = false, allowDecimals = true, noDataText, className, onValueChange, enableLegendSlider = false, customTooltip, rotateLabelX, padding = (!showXAxis && !showYAxis) || (startEndOnly && !showYAxis) ? { left: 0, right: 0 } : { left: 20, right: 20 }, tickGap = 5, xAxisLabel, yAxisLabel, ...other } = props; const CustomTooltip = customTooltip; const [legendHeight, setLegendHeight] = useState(60); const [activeDot, setActiveDot] = useState(undefined); const [activeLegend, setActiveLegend] = useState(undefined); const categoryColors = constructCategoryColors(categories, colors); const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); const hasOnValueChange = !!onValueChange; function onDotClick(itemData: any, event: React.MouseEvent) { event.stopPropagation(); if (!hasOnValueChange) return; if ( (itemData.index === activeDot?.index && itemData.dataKey === activeDot?.dataKey) || (hasOnlyOneValueForThisKey(data, itemData.dataKey) && activeLegend && activeLegend === itemData.dataKey) ) { setActiveLegend(undefined); setActiveDot(undefined); onValueChange?.(null); } else { setActiveLegend(itemData.dataKey); setActiveDot({ index: itemData.index, dataKey: itemData.dataKey, }); onValueChange?.({ eventType: "dot", categoryClicked: itemData.dataKey, ...itemData.payload, }); } } function onCategoryClick(dataKey: string) { if (!hasOnValueChange) return; if ( (dataKey === activeLegend && !activeDot) || (hasOnlyOneValueForThisKey(data, dataKey) && activeDot && activeDot.dataKey === dataKey) ) { setActiveLegend(undefined); onValueChange?.(null); } else { setActiveLegend(dataKey); onValueChange?.({ eventType: "category", categoryClicked: dataKey, }); } setActiveDot(undefined); } return (
{data?.length ? ( { setActiveDot(undefined); setActiveLegend(undefined); onValueChange?.(null); } : undefined } margin={{ bottom: xAxisLabel ? 30 : undefined, left: yAxisLabel ? 20 : undefined, right: yAxisLabel ? 5 : undefined, top: 5, }} > {showGridLines ? ( ) : null} {xAxisLabel && ( )} {yAxisLabel && ( )} CustomTooltip ? ( ({ ...payloadItem, color: categoryColors.get(payloadItem.dataKey) ?? BaseColors.Gray, }))} active={active} label={label} /> ) : ( ) ) : ( <> ) } position={{ y: 0 }} /> {showLegend ? ( ChartLegend( { payload }, categoryColors, setLegendHeight, activeLegend, hasOnValueChange ? (clickedLegendItem: string) => onCategoryClick(clickedLegendItem) : undefined, enableLegendSlider, ) } /> ) : null} {categories.map((category) => { const gradientId = (categoryColors.get(category) ?? BaseColors.Gray).replace("#", ""); return ( {showGradient ? ( ) : ( )} ); })} {categories.map((category) => { const gradientId = (categoryColors.get(category) ?? BaseColors.Gray).replace("#", ""); return ( { const { cx, cy, stroke, strokeLinecap, strokeLinejoin, strokeWidth, dataKey } = props; return ( onDotClick(props, event)} /> ); }} dot={(props: any) => { const { stroke, strokeLinecap, strokeLinejoin, strokeWidth, cx, cy, dataKey, index, } = props; if ( (hasOnlyOneValueForThisKey(data, category) && !(activeDot || (activeLegend && activeLegend !== category))) || (activeDot?.index === index && activeDot?.dataKey === category) ) { return ( ); } return ; }} key={category} name={category} type={curveType} dataKey={category} stroke="" fill={`url(#${gradientId})`} strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" isAnimationActive={showAnimation} animationDuration={animationDuration} stackId={stack ? "a" : undefined} connectNulls={connectNulls} /> ); })} {onValueChange ? categories.map((category) => ( { event.stopPropagation(); const { name } = props; onCategoryClick(name); }} /> )) : null} ) : ( )}
); }); AreaChart.displayName = "AreaChart"; export default AreaChart; ================================================ FILE: src/components/chart-elements/AreaChart/index.ts ================================================ export { default as AreaChart } from "./AreaChart"; export type { AreaChartProps } from "./AreaChart"; ================================================ FILE: src/components/chart-elements/BarChart/BarChart.tsx ================================================ "use client"; import { colorPalette, getColorClassNames, tremorTwMerge } from "lib"; import React, { useState } from "react"; import { Bar, BarChart as ReChartsBarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis, Label, } from "recharts"; import BaseChartProps from "../common/BaseChartProps"; import ChartLegend from "../common/ChartLegend"; import ChartTooltip from "../common/ChartTooltip"; import NoData from "../common/NoData"; import { constructCategoryColors, deepEqual, getYAxisDomain } from "../common/utils"; import { BaseColors, defaultValueFormatter, themeColorRange } from "lib"; import { AxisDomain } from "recharts/types/util/types"; const renderShape = ( props: any, activeBar: any | undefined, activeLegend: string | undefined, layout: string, ) => { const { fillOpacity, name, payload, value } = props; let { x, width, y, height } = props; if (layout === "horizontal" && height < 0) { y += height; height = Math.abs(height); // height must be a positive number } else if (layout === "vertical" && width < 0) { x += width; width = Math.abs(width); // width must be a positive number } return ( ); }; export interface BarChartProps extends BaseChartProps { layout?: "vertical" | "horizontal"; stack?: boolean; relative?: boolean; barCategoryGap?: string | number; } const BarChart = React.forwardRef((props, ref) => { const { data = [], categories = [], index, colors = themeColorRange, valueFormatter = defaultValueFormatter, layout = "horizontal", stack = false, relative = false, startEndOnly = false, animationDuration = 900, showAnimation = false, showXAxis = true, showYAxis = true, yAxisWidth = 56, intervalType = "equidistantPreserveStart", showTooltip = true, showLegend = true, showGridLines = true, autoMinValue = false, minValue, maxValue, allowDecimals = true, noDataText, onValueChange, enableLegendSlider = false, customTooltip, rotateLabelX, barCategoryGap, tickGap = 5, xAxisLabel, yAxisLabel, className, padding = !showXAxis && !showYAxis ? { left: 0, right: 0 } : { left: 20, right: 20 }, ...other } = props; const CustomTooltip = customTooltip; const [legendHeight, setLegendHeight] = useState(60); const categoryColors = constructCategoryColors(categories, colors); const [activeBar, setActiveBar] = React.useState(undefined); const [activeLegend, setActiveLegend] = useState(undefined); const hasOnValueChange = !!onValueChange; function onBarClick(data: any, idx: number, event: React.MouseEvent) { event.stopPropagation(); if (!onValueChange) return; if (deepEqual(activeBar, { ...data.payload, value: data.value })) { setActiveLegend(undefined); setActiveBar(undefined); onValueChange?.(null); } else { setActiveLegend(data.tooltipPayload?.[0]?.dataKey); setActiveBar({ ...data.payload, value: data.value, }); onValueChange?.({ eventType: "bar", categoryClicked: data.tooltipPayload?.[0]?.dataKey, ...data.payload, }); } } function onCategoryClick(dataKey: string) { if (!hasOnValueChange) return; if (dataKey === activeLegend && !activeBar) { setActiveLegend(undefined); onValueChange?.(null); } else { setActiveLegend(dataKey); onValueChange?.({ eventType: "category", categoryClicked: dataKey, }); } setActiveBar(undefined); } const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); return (
{data?.length ? ( { setActiveBar(undefined); setActiveLegend(undefined); onValueChange?.(null); } : undefined } margin={{ bottom: xAxisLabel ? 30 : undefined, left: yAxisLabel ? 20 : undefined, right: yAxisLabel ? 5 : undefined, top: 5, }} > {showGridLines ? ( ) : null} {layout !== "vertical" ? ( {xAxisLabel && ( )} ) : ( {xAxisLabel && ( )} )} {layout !== "vertical" ? ( `${(value * 100).toString()} %` : valueFormatter } allowDecimals={allowDecimals} > {yAxisLabel && ( )} ) : ( {yAxisLabel && ( )} )} CustomTooltip ? ( ({ ...payloadItem, color: categoryColors.get(payloadItem.dataKey) ?? BaseColors.Gray, }))} active={active} label={label} /> ) : ( ) ) : ( <> ) } position={{ y: 0 }} /> {showLegend ? ( ChartLegend( { payload }, categoryColors, setLegendHeight, activeLegend, hasOnValueChange ? (clickedLegendItem: string) => onCategoryClick(clickedLegendItem) : undefined, enableLegendSlider, ) } /> ) : null} {categories.map((category) => ( renderShape(props, activeBar, activeLegend, layout)} onClick={onBarClick} /> ))} ) : ( )}
); }); BarChart.displayName = "BarChart"; export default BarChart; ================================================ FILE: src/components/chart-elements/BarChart/index.ts ================================================ export { default as BarChart } from "./BarChart"; export type { BarChartProps } from "./BarChart"; ================================================ FILE: src/components/chart-elements/DonutChart/DonutChart.tsx ================================================ "use client"; import { BaseColors, defaultValueFormatter, themeColorRange, tremorTwMerge } from "lib"; import React, { useEffect } from "react"; import { Pie, PieChart as ReChartsDonutChart, ResponsiveContainer, Sector, Tooltip, } from "recharts"; import { Color, ValueFormatter } from "../../../lib/inputTypes"; import NoData from "../common/NoData"; import { DonutChartTooltip } from "./DonutChartTooltip"; import { parseData, parseLabelInput } from "./inputParser"; import type { EventProps } from "components/chart-elements/common"; import { CustomTooltipProps } from "components/chart-elements/common/CustomTooltipProps"; import type BaseAnimationTimingProps from "../common/BaseAnimationTimingProps"; type DonutChartVariant = "donut" | "pie"; export interface DonutChartProps extends BaseAnimationTimingProps { data: any[]; category?: string; index?: string; colors?: (Color | string)[]; variant?: DonutChartVariant; valueFormatter?: ValueFormatter; label?: string; showLabel?: boolean; showAnimation?: boolean; showTooltip?: boolean; noDataText?: string; className?: string; onValueChange?: (value: EventProps) => void; customTooltip?: React.ComponentType; } const renderInactiveShape = (props: any) => { const { cx, cy, // midAngle, innerRadius, outerRadius, startAngle, endAngle, // fill, // payload, // percent, // value, // activeIndex, className, } = props; return ( ); }; const DonutChart = React.forwardRef((props, ref) => { const { data = [], category = "value", index = "name", colors = themeColorRange, variant = "donut", valueFormatter = defaultValueFormatter, label, showLabel = true, animationDuration = 900, showAnimation = false, showTooltip = true, noDataText, onValueChange, customTooltip, className, ...other } = props; const CustomTooltip = customTooltip; const isDonut = variant == "donut"; const parsedLabelInput = parseLabelInput(label, valueFormatter, data, category); const [activeIndex, setActiveIndex] = React.useState(undefined); const hasOnValueChange = !!onValueChange; function onShapeClick(data: any, index: number, event: React.MouseEvent) { event.stopPropagation(); if (!hasOnValueChange) return; if (activeIndex === index) { setActiveIndex(undefined); onValueChange?.(null); } else { setActiveIndex(index); onValueChange?.({ eventType: "slice", ...data.payload.payload, }); } } useEffect(() => { const pieSectors = document.querySelectorAll(".recharts-pie-sector"); if (pieSectors) { pieSectors.forEach((sector) => { sector.setAttribute("style", "outline: none"); }); } }, [activeIndex]); return (
{data?.length ? ( { setActiveIndex(undefined); onValueChange?.(null); } : undefined } margin={{ top: 0, left: 0, right: 0, bottom: 0 }} > {showLabel && isDonut ? ( {parsedLabelInput} ) : null} {/* {showTooltip ? ( ( )} /> ) : null} */} CustomTooltip ? ( ({ ...payloadItem, color: payload?.[0]?.payload?.color ?? BaseColors.Gray, }))} active={active} label={payload?.[0]?.name} /> ) : ( ) ) : ( <> ) } /> ) : ( )}
); }); DonutChart.displayName = "DonutChart"; export default DonutChart; ================================================ FILE: src/components/chart-elements/DonutChart/DonutChartTooltip.tsx ================================================ import React from "react"; import { tremorTwMerge, ValueFormatter } from "lib"; import { ChartTooltipFrame, ChartTooltipRow } from "components/chart-elements/common/ChartTooltip"; export interface DonutChartTooltipProps { active: boolean | undefined; payload: any; valueFormatter: ValueFormatter; } export const DonutChartTooltip = ({ active, payload, valueFormatter }: DonutChartTooltipProps) => { if (active && payload?.[0]) { const payloadRow = payload?.[0]; return (
); } return null; }; ================================================ FILE: src/components/chart-elements/DonutChart/index.ts ================================================ export { default as DonutChart } from "./DonutChart"; export type { DonutChartProps } from "./DonutChart"; ================================================ FILE: src/components/chart-elements/DonutChart/inputParser.ts ================================================ import { BaseColors, colorPalette, getColorClassNames, sumNumericArray } from "lib"; import { Color, ValueFormatter } from "../../../lib/inputTypes"; export const parseData = (data: any[], colors: (Color | string)[]) => data.map((dataPoint: any, idx: number) => { const baseColor = idx < colors.length ? colors[idx] : BaseColors.Gray; return { ...dataPoint, // explicitly adding color key if not present for tooltip coloring color: baseColor, className: getColorClassNames(baseColor ?? BaseColors.Gray, colorPalette.background) .fillColor, fill: "", }; }); const calculateDefaultLabel = (data: any[], category: string) => sumNumericArray(data.map((dataPoint) => dataPoint[category])); export const parseLabelInput = ( labelInput: string | undefined, valueFormatter: ValueFormatter, data: any[], category: string, ) => (labelInput ? labelInput : valueFormatter(calculateDefaultLabel(data, category))); ================================================ FILE: src/components/chart-elements/FunnelChart/FunnelChart.tsx ================================================ import React from "react"; import { ChartTooltipFrame, ChartTooltipRow } from "../common/ChartTooltip"; import { BaseColors, Color, FunnelVariantType, colorPalette, defaultValueFormatter, getColorClassNames, tremorTwMerge, } from "lib"; import { CustomTooltipProps, EventProps } from "../common"; import NoData from "../common/NoData"; import { ArrowRightIcon } from "assets"; type FormattedDataT = DataT & { normalizedValue: number; startX: number; startY: number; barHeight: number; nextValue: number; nextNormalizedValue: number; nextBarHeight: number; nextStartX: number; }; type CalculateFrom = "first" | "previous"; type Tooltip = { x: number; y: number; data?: { className?: string; name: string; fill?: string; dataKey: string; color?: Color | string; value: number; payload?: any; }; index?: number; }; type DataT = { value: number; name: string; }; const GLOBAL_PADDING = 10; const HALF_PADDING = GLOBAL_PADDING / 2; const Y_AXIS_LABELS = ["100%", "75%", "50%", "25%", "0%"]; export interface FunnelChartProps extends React.HTMLAttributes { data: DataT[]; evolutionGradient?: boolean; gradient?: boolean; valueFormatter?: (value: number) => string; calculateFrom?: CalculateFrom; color?: Color | string; variant?: FunnelVariantType; yAxisPadding?: number; showYAxis?: boolean; showXAxis?: boolean; showArrow?: boolean; showGridLines?: boolean; showTooltip?: boolean; onValueChange?: (value: EventProps) => void; customTooltip?: React.ComponentType; noDataText?: string; rotateLabelX?: { angle: number; verticalShift?: number; xAxisHeight?: number; }; barGap?: number | `${number}%`; xAxisLabel?: string; yAxisLabel?: string; } //#region Funnel Chart Primitive const FunnelChartPrimitive = React.forwardRef( (props: FunnelChartProps, ref) => { const { data, evolutionGradient = false, gradient = true, valueFormatter = defaultValueFormatter, className, calculateFrom = "first", color, variant = "base", showGridLines = true, showYAxis = calculateFrom === "previous" ? false : true, showXAxis = true, showArrow = true, xAxisLabel = "", yAxisLabel = "", yAxisPadding = showYAxis ? (yAxisLabel ? 70 : 45) : 0, showTooltip = true, onValueChange, customTooltip, noDataText, rotateLabelX, barGap = "20%", ...other } = props; const DEFAULT_X_AXIS_HEIGHT = showXAxis && xAxisLabel ? 25 : 15; const CustomTooltip = customTooltip; const svgRef = React.useRef(null); const tooltipRef = React.useRef(null); const [width, setWidth] = React.useState(0); const [height, setHeight] = React.useState(0); const [tooltip, setTooltip] = React.useState({ x: 0, y: 0 }); const [activeBar, setActiveBar] = React.useState(undefined); const hasOnValueChange = !!onValueChange; function onBarClick(data: any, idx: number, event: React.MouseEvent) { event.stopPropagation(); if (!hasOnValueChange) return; if (idx === activeBar?.index) { setActiveBar(undefined); onValueChange(undefined); } else { setActiveBar({ data, index: idx }); onValueChange({ eventType: "bar", categoryClicked: data.name, [data.name]: data.value, percentage: data.normalizedValue, }); } } const maxValue = React.useMemo(() => Math.max(...data.map((item) => item.value)), [data]); const widthWithoutPadding = width - GLOBAL_PADDING - yAxisPadding; const gap = React.useMemo(() => { if (typeof barGap === "number") { return barGap; } else if (typeof barGap === "string" && barGap.endsWith("%")) { const percentage = parseFloat(barGap.slice(0, -1)); const totalWidthForGaps = (widthWithoutPadding * percentage) / 100; const numberOfGaps = data.length - 1; return totalWidthForGaps / numberOfGaps; } else { console.error( 'Invalid barGap value. It must be a number or a percentage string (e.g., "10%").', ); return 30; } }, [widthWithoutPadding, data.length, barGap]); const barWidth = React.useMemo( () => (widthWithoutPadding - (data.length - 1) * gap - gap) / data.length, [widthWithoutPadding, gap, data.length], ); const realHeight = height - GLOBAL_PADDING - (showXAxis ? (rotateLabelX?.xAxisHeight || DEFAULT_X_AXIS_HEIGHT) + (showXAxis && xAxisLabel ? 30 : 10) : 0); const isPreviousCalculation = calculateFrom === "previous"; const isVariantCenter = variant === "center"; React.useLayoutEffect(() => { const handleResize = () => { if (svgRef.current) { const boundingBox = svgRef.current.getBoundingClientRect(); setWidth(boundingBox.width); setHeight(boundingBox.height); } }; handleResize(); window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, [className]); React.useEffect(() => { const handleTooltipOverflows = () => { if (tooltipRef.current) { const boundingBox = tooltipRef.current.getBoundingClientRect(); if (boundingBox.right > window.innerWidth) { tooltipRef.current.style.left = `${width - boundingBox.width}px`; } } }; handleTooltipOverflows(); window.addEventListener("resize", handleTooltipOverflows); return () => { window.removeEventListener("resize", handleTooltipOverflows); }; }, [tooltip, width]); const formattedData = React.useMemo(() => { if (realHeight <= 0) return []; return data.reduce((acc: FormattedDataT[], item, index) => { const prev = acc[index - 1]; const value = item.value; const valueToCompareWith = isPreviousCalculation ? (prev?.value ?? maxValue) : maxValue; const calculationHeight = isPreviousCalculation ? (prev?.barHeight ?? realHeight) : realHeight; const normalizedValue = value / valueToCompareWith; const barHeight = normalizedValue * calculationHeight; const startX = index * (barWidth + gap) + 0.5 * gap; const startY = calculationHeight - barHeight + (isPreviousCalculation ? realHeight - (prev?.barHeight ?? realHeight) : 0); const nextValue = data[index + 1]?.value; const nextNormalizedValue = nextValue / valueToCompareWith; const nextBarHeight = nextNormalizedValue * calculationHeight; const nextStartX = (index + 1) * (barWidth + gap) + 0.5 * gap; acc.push({ value, normalizedValue, name: item.name, startX, startY, barHeight, nextValue, nextNormalizedValue, nextBarHeight, nextStartX, }); return acc; }, []); }, [data, realHeight, isPreviousCalculation, barWidth, gap, maxValue]); const handleTooltip = (touch: React.Touch) => { const chartBoundingRect = svgRef.current?.getBoundingClientRect(); if (!chartBoundingRect) return; const chartX = chartBoundingRect.x; const chartY = chartBoundingRect.y; const chartTop = chartY + window.scrollY; const chartLeft = chartX + window.scrollX + yAxisPadding + HALF_PADDING; const chartWidth = chartBoundingRect.width - yAxisPadding - HALF_PADDING; const chartHeight = chartBoundingRect.height - HALF_PADDING - (showXAxis ? DEFAULT_X_AXIS_HEIGHT : 0); const chartRight = chartLeft + chartWidth; const chartBottom = chartTop + chartHeight; if ( touch.pageX < chartLeft || touch.pageX > chartRight || touch.pageY < chartTop || touch.pageY > chartBottom ) { console.log("out of bounds"); return setTooltip({ x: 0, y: 0 }); } const pageX = touch.pageX - chartX - barWidth / 2 - yAxisPadding - HALF_PADDING; const closestBar = formattedData.reduce((acc, current) => { const currentDistance = Math.abs(current.startX - pageX); const accDistance = Math.abs(acc.startX - pageX); return currentDistance < accDistance ? current : acc; }); const closestBarIndex = formattedData.findIndex((bar) => bar === closestBar); setTooltip({ x: closestBar.startX, y: closestBar.startY, data: { dataKey: closestBar.name, name: closestBar.name, value: closestBar.value, color: color ?? BaseColors.Blue, className: tremorTwMerge( getColorClassNames(color ?? BaseColors.Blue, colorPalette.text).textColor, hasOnValueChange ? "cursor-pointer" : "cursor-default", ), fill: "", payload: closestBar, }, index: closestBarIndex, }); }; return (
{data?.length ? ( <> { const fakeTouch = { clientX: e.clientX, clientY: e.clientY, pageX: e.pageX, pageY: e.pageY, } as React.Touch; handleTooltip(fakeTouch); }} onTouchMove={(e) => { const touch = e.touches[0]; handleTooltip(touch); }} onMouseLeave={() => setTooltip({ x: 0, y: 0 })} onTouchEnd={() => setTooltip({ x: 0, y: 0 })} > {/* Draw Y axis labels and lines */} {Y_AXIS_LABELS.map((label, index) => ( {showGridLines ? ( ) : null} {label} ))} {formattedData.map((item, index) => ( {/* Hover gray rect */} {/* Draw gradient bar to fill space */} {gradient ? ( ) : null} {/* Draw bar */} onBarClick(item, index, e)} /> {/* Draw bottom gradient bar to fill space */} {gradient && isVariantCenter ? ( ) : null} {/* Draw label */} {showXAxis ? (
{item.name}
) : null}
))} {/* Draw gradient polygon between bars */} {formattedData.map((item, index) => ( {index < data.length - 1 && evolutionGradient ? ( <> {isVariantCenter ? ( <> ) : ( )} ) : null} {/* Hover transparent rect for tooltip */} {/* handleTooltip(index, item)} onTouchStart={() => handleTooltip(index, item)} onTouchMove={(e) => { const touch = e.touches[0]; const distance = barWidth + gap * 2 + yAxisPadding - touch.clientX; const closestBar = formattedData.reduce((acc, current) => { const currentDistance = Math.abs(current.startX + distance); const accDistance = Math.abs(acc.startX + distance); return currentDistance < accDistance ? current : acc; }); const closestBarIndex = formattedData.findIndex((bar) => bar === closestBar); handleTooltip(closestBarIndex, closestBar); }} onMouseLeave={() => setTooltip({ x: 0, y: 0 })} onTouchEnd={() => setTooltip({ x: 0, y: 0 })} onClick={(e) => onBarClick(item, index, e)} className={tremorTwMerge( hasOnValueChange ? "cursor-pointer" : "cursor-default", )} /> */} {/* Add arrow between labels */} {index < data.length - 1 && showXAxis && showArrow && gap >= 14 ? (
) : null}
))} {showXAxis && xAxisLabel ? ( {xAxisLabel} ) : null} {showYAxis && yAxisLabel ? ( {yAxisLabel} ) : null}
{showTooltip ? (
{CustomTooltip ? ( ) : (

{tooltip?.data?.name}

{tooltip.data ? ( ) : null}
)}
) : null} ) : ( )}
); }, ); FunnelChartPrimitive.displayName = "FunnelChart"; //#region Data Validation const validateData = (data: DataT[], calculatedFrom?: CalculateFrom): string | null => { if (data && data.length > 0) { if (calculatedFrom === "previous" && data[0].value <= 0) { return `The value of the first item "${data[0].name}" is not greater than 0. This is not allowed when setting the "calculateFrom" prop to "previous". Please enter a value greater than 0.`; } for (const item of data) { if (item.value < 0) { return `Item "${item.name}" has a negative value: ${item.value}. This is not allowed. The value must be greater than or equal to 0.`; } } } return null; }; //#region Exports const FunnelChart = ({ data, ...props }: FunnelChartProps) => { const errorMessage = data ? validateData(data, props.calculateFrom) : null; return errorMessage ? ( ) : ( ); }; export default FunnelChart; ================================================ FILE: src/components/chart-elements/FunnelChart/index.ts ================================================ export { default as FunnelChart } from "./FunnelChart"; export type { FunnelChartProps } from "./FunnelChart"; ================================================ FILE: src/components/chart-elements/LineChart/LineChart.tsx ================================================ "use client"; import React, { Fragment, useState } from "react"; import { CartesianGrid, Dot, Legend, Line, LineChart as ReChartsLineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Label, } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; import BaseChartProps from "../common/BaseChartProps"; import ChartLegend from "../common/ChartLegend"; import ChartTooltip from "../common/ChartTooltip"; import NoData from "../common/NoData"; import { constructCategoryColors, getYAxisDomain, hasOnlyOneValueForThisKey, } from "../common/utils"; import { BaseColors, colorPalette, defaultValueFormatter, getColorClassNames, themeColorRange, tremorTwMerge, } from "lib"; import { CurveType } from "../../../lib/inputTypes"; export interface LineChartProps extends BaseChartProps { curveType?: CurveType; connectNulls?: boolean; } interface ActiveDot { index?: number; dataKey?: string; } const LineChart = React.forwardRef((props, ref) => { const { data = [], categories = [], index, colors = themeColorRange, valueFormatter = defaultValueFormatter, startEndOnly = false, showXAxis = true, showYAxis = true, yAxisWidth = 56, intervalType = "equidistantPreserveStart", animationDuration = 900, showAnimation = false, showTooltip = true, showLegend = true, showGridLines = true, autoMinValue = false, curveType = "linear", minValue, maxValue, connectNulls = false, allowDecimals = true, noDataText, className, onValueChange, enableLegendSlider = false, customTooltip, rotateLabelX, padding = !showXAxis && !showYAxis ? { left: 0, right: 0 } : { left: 20, right: 20 }, tickGap = 5, xAxisLabel, yAxisLabel, ...other } = props; const CustomTooltip = customTooltip; const [legendHeight, setLegendHeight] = useState(60); const [activeDot, setActiveDot] = useState(undefined); const [activeLegend, setActiveLegend] = useState(undefined); const categoryColors = constructCategoryColors(categories, colors); const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue); const hasOnValueChange = !!onValueChange; function onDotClick(itemData: any, event: React.MouseEvent) { event.stopPropagation(); if (!hasOnValueChange) return; if ( (itemData.index === activeDot?.index && itemData.dataKey === activeDot?.dataKey) || (hasOnlyOneValueForThisKey(data, itemData.dataKey) && activeLegend && activeLegend === itemData.dataKey) ) { setActiveLegend(undefined); setActiveDot(undefined); onValueChange?.(null); } else { setActiveLegend(itemData.dataKey); setActiveDot({ index: itemData.index, dataKey: itemData.dataKey, }); onValueChange?.({ eventType: "dot", categoryClicked: itemData.dataKey, ...itemData.payload, }); } } function onCategoryClick(dataKey: string) { if (!hasOnValueChange) return; if ( (dataKey === activeLegend && !activeDot) || (hasOnlyOneValueForThisKey(data, dataKey) && activeDot && activeDot.dataKey === dataKey) ) { setActiveLegend(undefined); onValueChange?.(null); } else { setActiveLegend(dataKey); onValueChange?.({ eventType: "category", categoryClicked: dataKey, }); } setActiveDot(undefined); } return (
{data?.length ? ( { setActiveDot(undefined); setActiveLegend(undefined); onValueChange?.(null); } : undefined } margin={{ bottom: xAxisLabel ? 30 : undefined, left: yAxisLabel ? 20 : undefined, right: yAxisLabel ? 5 : undefined, top: 5, }} > {showGridLines ? ( ) : null} {xAxisLabel && ( )} {yAxisLabel && ( )} CustomTooltip ? ( ({ ...payloadItem, color: categoryColors.get(payloadItem.dataKey) ?? BaseColors.Gray, }))} active={active} label={label} /> ) : ( ) ) : ( <> ) } position={{ y: 0 }} /> {showLegend ? ( ChartLegend( { payload }, categoryColors, setLegendHeight, activeLegend, hasOnValueChange ? (clickedLegendItem: string) => onCategoryClick(clickedLegendItem) : undefined, enableLegendSlider, ) } /> ) : null} {categories.map((category) => ( { const { cx, cy, stroke, strokeLinecap, strokeLinejoin, strokeWidth, dataKey } = props; return ( onDotClick(props, event)} /> ); }} dot={(props: any) => { const { stroke, strokeLinecap, strokeLinejoin, strokeWidth, cx, cy, dataKey, index, } = props; if ( (hasOnlyOneValueForThisKey(data, category) && !(activeDot || (activeLegend && activeLegend !== category))) || (activeDot?.index === index && activeDot?.dataKey === category) ) { return ( ); } return ; }} key={category} name={category} type={curveType} dataKey={category} stroke="" strokeWidth={2} strokeLinejoin="round" strokeLinecap="round" isAnimationActive={showAnimation} animationDuration={animationDuration} connectNulls={connectNulls} /> ))} {onValueChange ? categories.map((category) => ( { event.stopPropagation(); const { name } = props; onCategoryClick(name); }} /> )) : null} ) : ( )}
); }); LineChart.displayName = "LineChart"; export default LineChart; ================================================ FILE: src/components/chart-elements/LineChart/index.ts ================================================ export { default as LineChart } from "./LineChart"; export type { LineChartProps } from "./LineChart"; ================================================ FILE: src/components/chart-elements/ScatterChart/ScatterChart.tsx ================================================ "use client"; import React, { useState } from "react"; import { CartesianGrid, Dot, Legend, ResponsiveContainer, Scatter, ScatterChart as ReChartsScatterChart, Tooltip, XAxis, YAxis, ZAxis, Label, } from "recharts"; import { AxisDomain } from "recharts/types/util/types"; import type { EventProps } from "components/chart-elements/common"; import ChartLegend from "components/chart-elements/common/ChartLegend"; import ScatterChartTooltip from "components/chart-elements/ScatterChart/ScatterChartTooltip"; import BaseAnimationTimingProps from "../common/BaseAnimationTimingProps"; import NoData from "../common/NoData"; import { constructCategories, constructCategoryColors, deepEqual, getYAxisDomain, } from "../common/utils"; import { CustomTooltipProps } from "components/chart-elements/common/CustomTooltipProps"; import { BaseColors, colorPalette, defaultValueFormatter, getColorClassNames, themeColorRange, tremorTwMerge, } from "lib"; import { Color, ValueFormatter, IntervalType } from "../../../lib/inputTypes"; export type ScatterChartValueFormatter = { x?: ValueFormatter; y?: ValueFormatter; size?: ValueFormatter; }; export interface ScatterChartProps extends BaseAnimationTimingProps, React.HTMLAttributes { data: any[]; x: string; y: string; category: string; size?: string; valueFormatter?: ScatterChartValueFormatter; sizeRange?: number[]; colors?: (Color | string)[]; showOpacity?: boolean; startEndOnly?: boolean; showXAxis?: boolean; showYAxis?: boolean; yAxisWidth?: number; intervalType?: IntervalType; showTooltip?: boolean; showLegend?: boolean; showGridLines?: boolean; autoMinXValue?: boolean; minXValue?: number; maxXValue?: number; autoMinYValue?: boolean; minYValue?: number; maxYValue?: number; allowDecimals?: boolean; noDataText?: string; enableLegendSlider?: boolean; onValueChange?: (value: EventProps) => void; customTooltip?: React.ComponentType; rotateLabelX?: { angle: number; verticalShift: number; xAxisHeight: number; }; tickGap?: number; xAxisLabel?: string; yAxisLabel?: string; } const renderShape = (props: any, activeNode: any | undefined, activeLegend: string | undefined) => { const { cx, cy, width, node, fillOpacity, name } = props; return ( ); }; const ScatterChart = React.forwardRef((props, ref) => { const { data = [], x, y, size, category, colors = themeColorRange, showOpacity = false, sizeRange = [1, 1000], valueFormatter = { x: defaultValueFormatter, y: defaultValueFormatter, size: defaultValueFormatter, }, startEndOnly = false, showXAxis = true, showYAxis = true, yAxisWidth = 56, intervalType = "equidistantPreserveStart", animationDuration = 900, showAnimation = false, showTooltip = true, showLegend = true, showGridLines = true, autoMinXValue = false, minXValue, maxXValue, autoMinYValue = false, minYValue, maxYValue, allowDecimals = true, noDataText, onValueChange, customTooltip, rotateLabelX, className, enableLegendSlider = false, tickGap = 5, xAxisLabel, yAxisLabel, ...other } = props; const CustomTooltip = customTooltip; const [legendHeight, setLegendHeight] = useState(60); const [activeNode, setActiveNode] = React.useState(undefined); const [activeLegend, setActiveLegend] = useState(undefined); const hasOnValueChange = !!onValueChange; function onNodeClick(data: any, index: number, event: React.MouseEvent) { event.stopPropagation(); if (!hasOnValueChange) return; if (deepEqual(activeNode, data.node)) { setActiveLegend(undefined); setActiveNode(undefined); onValueChange?.(null); } else { setActiveNode(data.node); setActiveLegend(data.payload[category]); onValueChange?.({ eventType: "bubble", categoryClicked: data.payload[category], ...data.payload, }); } } function onCategoryClick(dataKey: string) { if (!hasOnValueChange) return; if (dataKey === activeLegend && !activeNode) { setActiveLegend(undefined); onValueChange?.(null); } else { setActiveLegend(dataKey); onValueChange?.({ eventType: "category", categoryClicked: dataKey, }); } setActiveNode(undefined); } const categories = constructCategories(data, category); const categoryColors = constructCategoryColors(categories, colors); //maybe rename getYAxisDomain to getAxisDomain const xAxisDomain = getYAxisDomain(autoMinXValue, minXValue, maxXValue); const yAxisDomain = getYAxisDomain(autoMinYValue, minYValue, maxYValue); return (
{data?.length ? ( { setActiveNode(undefined); setActiveLegend(undefined); onValueChange?.(null); } : undefined } margin={{ bottom: xAxisLabel ? 20 : undefined, left: 20, right: 20, top: 5, }} > {showGridLines ? ( ) : null} {x ? ( {xAxisLabel && ( )} ) : null} {y ? ( {yAxisLabel && ( )} ) : null} { const color = category ? payload?.[0]?.payload?.[category] : label; return CustomTooltip ? ( ({ ...payloadItem, color: categoryColors.get(color) ?? BaseColors.Gray, }))} active={active} label={color} /> ) : ( ); } ) : ( <> ) } /> {size ? : null} {categories.map((cat) => { return ( d[category] === cat) : data} isAnimationActive={showAnimation} animationDuration={animationDuration} shape={(props: any) => renderShape(props, activeNode, activeLegend)} onClick={onNodeClick} /> ); })} {showLegend ? ( ChartLegend( { payload }, categoryColors, setLegendHeight, activeLegend, hasOnValueChange ? (clickedLegendItem: string) => onCategoryClick(clickedLegendItem) : undefined, enableLegendSlider, ) } /> ) : null} ) : ( )}
); }); ScatterChart.displayName = "ScatterChart"; export default ScatterChart; ================================================ FILE: src/components/chart-elements/ScatterChart/ScatterChartTooltip.tsx ================================================ import React from "react"; import { ScatterChartValueFormatter } from "components/chart-elements/ScatterChart/ScatterChart"; import { BaseColors, getColorClassNames, Color, defaultValueFormatter, tremorTwMerge, colorPalette, } from "lib"; export const ChartTooltipFrame = ({ children }: { children: React.ReactNode }) => (
{children}
); export interface ChartTooltipRowProps { value: string; name: string; } export const ChartTooltipRow = ({ value, name }: ChartTooltipRowProps) => (

{name}

{value}

); export interface ScatterChartTooltipProps { label: string; categoryColors: Map; active: boolean | undefined; payload: any; valueFormatter: ScatterChartValueFormatter; axis: any; category?: string; } const ScatterChartTooltip = ({ label, active, payload, valueFormatter, axis, category, categoryColors, }: ScatterChartTooltipProps) => { if (active && payload) { return (

{label}

{payload.map(({ value, name }: { value: number; name: string }, idx: number) => { const valueFormatterKey = Object.keys(axis).find((key) => axis[key] === name) ?? ""; const valueFormatterFn = valueFormatter[valueFormatterKey as keyof ScatterChartValueFormatter] ?? defaultValueFormatter; return ( ); })}
); } return null; }; export default ScatterChartTooltip; ================================================ FILE: src/components/chart-elements/ScatterChart/index.tsx ================================================ export { default as ScatterChart } from "./ScatterChart"; export type { ScatterChartProps } from "./ScatterChart"; ================================================ FILE: src/components/chart-elements/common/BaseAnimationTimingProps.tsx ================================================ interface BaseAnimationTimingProps { animationDuration?: number; showAnimation?: boolean; } export default BaseAnimationTimingProps; ================================================ FILE: src/components/chart-elements/common/BaseChartProps.tsx ================================================ import { Color, ValueFormatter, IntervalType } from "../../../lib"; import type BaseAnimationTimingProps from "./BaseAnimationTimingProps"; import { CustomTooltipProps } from "./CustomTooltipProps"; type FixedProps = { eventType: "dot" | "category" | "bar" | "slice" | "bubble"; categoryClicked: string; }; type BaseEventProps = FixedProps & { [key: string]: number | string; }; export type EventProps = BaseEventProps | null | undefined; interface BaseChartProps extends BaseAnimationTimingProps, React.HTMLAttributes { data: any[]; categories: string[]; index: string; colors?: (Color | string)[]; valueFormatter?: ValueFormatter; startEndOnly?: boolean; showXAxis?: boolean; showYAxis?: boolean; yAxisWidth?: number; intervalType?: IntervalType; showTooltip?: boolean; showLegend?: boolean; showGridLines?: boolean; autoMinValue?: boolean; minValue?: number; maxValue?: number; allowDecimals?: boolean; noDataText?: string; onValueChange?: (value: EventProps) => void; enableLegendSlider?: boolean; padding?: { left?: number; right?: number }; customTooltip?: React.ComponentType; rotateLabelX?: { angle: number; verticalShift?: number; xAxisHeight?: number; }; tickGap?: number; xAxisLabel?: string; yAxisLabel?: string; } export default BaseChartProps; ================================================ FILE: src/components/chart-elements/common/ChartLegend.tsx ================================================ import React, { useRef } from "react"; import { useOnWindowResize } from "hooks"; import { Legend } from "components/text-elements/Legend"; import { Color } from "../../../lib"; const ChartLegend = ( { payload }: any, categoryColors: Map, setLegendHeight: React.Dispatch>, activeLegend: string | undefined, onClick?: (category: string, color: Color | string) => void, enableLegendSlider?: boolean, ) => { const legendRef = useRef(null); useOnWindowResize(() => { const calculateHeight = (height: number | undefined) => height ? Number(height) + 20 // 20px extra padding : 60; // default height setLegendHeight(calculateHeight(legendRef.current?.clientHeight)); }); const filteredPayload = payload.filter((item: any) => item.type !== "none"); return (
entry.value)} colors={filteredPayload.map((entry: any) => categoryColors.get(entry.value))} onClickLegendItem={onClick} activeLegend={activeLegend} enableLegendSlider={enableLegendSlider} />
); }; export default ChartLegend; ================================================ FILE: src/components/chart-elements/common/ChartTooltip.tsx ================================================ import React from "react"; import { BaseColors, getColorClassNames, tremorTwMerge, Color, ValueFormatter, colorPalette, } from "lib"; export const ChartTooltipFrame = ({ children }: { children: React.ReactNode }) => (
{children}
); export interface ChartTooltipRowProps { value: string; name: string; color: Color | string; } export const ChartTooltipRow = ({ value, name, color }: ChartTooltipRowProps) => (

{name}

{value}

); export interface ChartTooltipProps { active: boolean | undefined; payload: any; label: string; categoryColors: Map; valueFormatter: ValueFormatter; } const ChartTooltip = ({ active, payload, label, categoryColors, valueFormatter, }: ChartTooltipProps) => { if (active && payload) { const filteredPayload = payload.filter((item: any) => item.type !== "none"); return (

{label}

{filteredPayload.map(({ value, name }: { value: number; name: string }, idx: number) => ( ))}
); } return null; }; export default ChartTooltip; ================================================ FILE: src/components/chart-elements/common/CustomTooltipProps.tsx ================================================ import { NameType, Payload } from "recharts/types/component/DefaultTooltipContent"; export type CustomTooltipProps = { payload: Payload[] | undefined; active: boolean | undefined; label: NameType | undefined; }; ================================================ FILE: src/components/chart-elements/common/NoData.tsx ================================================ import { tremorTwMerge } from "lib"; import React from "react"; interface NoDataProps { noDataText?: string; className?: string; } const NoData = ({ className, noDataText = "No data" }: NoDataProps) => { return (

{noDataText}

); }; export default NoData; ================================================ FILE: src/components/chart-elements/common/index.ts ================================================ export type { EventProps } from "./BaseChartProps"; export type { CustomTooltipProps } from "./CustomTooltipProps"; ================================================ FILE: src/components/chart-elements/common/utils.ts ================================================ import { Color } from "../../../lib/inputTypes"; export const constructCategoryColors = ( categories: string[], colors: (Color | string)[], ): Map => { const categoryColors = new Map(); categories.forEach((category, idx) => { categoryColors.set(category, colors[idx % colors.length]); }); return categoryColors; }; export const getYAxisDomain = ( autoMinValue: boolean, minValue: number | undefined, maxValue: number | undefined, ) => { const minDomain = autoMinValue ? "auto" : (minValue ?? 0); const maxDomain = maxValue ?? "auto"; return [minDomain, maxDomain]; }; export const constructCategories = (data: any[], color?: string): string[] => { if (!color) { return []; } const categories = new Set(); data.forEach((datum) => { categories.add(datum[color]); }); return Array.from(categories); }; export function deepEqual(obj1: any, obj2: any) { if (obj1 === obj2) return true; if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null) return false; const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; for (const key of keys1) { if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) return false; } return true; } // export function deepEqual(obj1: unknown, obj2: unknown): boolean { // if (obj1 === obj2) return true; // if (typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null) // return false; // if (Object.prototype.toString.call(obj1) !== Object.prototype.toString.call(obj2)) return false; // const keys1 = Object.keys(obj1); // const keys2 = new Set(Object.keys(obj2)); // if (keys1.length !== keys2.size) return false; // for (const key of keys1) { // if ( // !keys2.has(key) || // !deepEqual((obj1 as Record)[key], (obj2 as Record)[key]) // ) // return false; // } // return true; // } export function hasOnlyOneValueForThisKey(array: any[], keyToCheck: string) { const val = []; for (const obj of array) { if (Object.prototype.hasOwnProperty.call(obj, keyToCheck)) { val.push(obj[keyToCheck]); if (val.length > 1) { return false; } } } return true; } ================================================ FILE: src/components/chart-elements/index.ts ================================================ export * from "./AreaChart"; export * from "./BarChart"; export { EventProps } from "./common/BaseChartProps"; export { CustomTooltipProps } from "./common/CustomTooltipProps"; export * from "./DonutChart"; export * from "./LineChart"; export * from "./ScatterChart"; export * from "./FunnelChart"; ================================================ FILE: src/components/icon-elements/Badge/Badge.tsx ================================================ "use client"; import React from "react"; import Tooltip, { useTooltip } from "components/util-elements/Tooltip/Tooltip"; import { Color, getColorClassNames, makeClassName, mergeRefs, Size, Sizes, tremorTwMerge, colorPalette, } from "lib"; import { badgeProportions, iconSizes } from "./styles"; const makeBadgeClassName = makeClassName("Badge"); export interface BadgeProps extends React.HTMLAttributes { color?: Color; size?: Size; icon?: React.ElementType; tooltip?: string; } const Badge = React.forwardRef((props, ref) => { const { color, icon, size = Sizes.SM, tooltip, className, children, ...other } = props; const Icon = icon ? icon : null; const { tooltipProps, getReferenceProps } = useTooltip(); return ( {Icon ? ( ) : null} {children} ); }); Badge.displayName = "Badge"; export default Badge; ================================================ FILE: src/components/icon-elements/Badge/index.ts ================================================ export { default as Badge } from "./Badge"; export type { BadgeProps } from "./Badge"; ================================================ FILE: src/components/icon-elements/Badge/styles.ts ================================================ export type BadgeProportionTypes = { paddingX: string; paddingY: string; fontSize: string; }; export const badgeProportions: { [char: string]: BadgeProportionTypes } = { xs: { paddingX: "px-2", paddingY: "py-0.5", fontSize: "text-xs", }, sm: { paddingX: "px-2.5", paddingY: "py-0.5", fontSize: "text-sm", }, md: { paddingX: "px-3", paddingY: "py-0.5", fontSize: "text-md", }, lg: { paddingX: "px-3.5", paddingY: "py-0.5", fontSize: "text-lg", }, xl: { paddingX: "px-4", paddingY: "py-1", fontSize: "text-xl", }, }; export const iconSizes: { [size: string]: { height: string; width: string; }; } = { xs: { height: "h-4", width: "w-4", }, sm: { height: "h-4", width: "w-4", }, md: { height: "h-4", width: "w-4", }, lg: { height: "h-5", width: "w-5", }, xl: { height: "h-6", width: "w-6", }, }; ================================================ FILE: src/components/icon-elements/BadgeDelta/BadgeDelta.tsx ================================================ "use client"; import React from "react"; import Tooltip, { useTooltip } from "components/util-elements/Tooltip/Tooltip"; import { DeltaType, DeltaTypes, makeClassName, mapInputsToDeltaType, mergeRefs, Size, Sizes, tremorTwMerge, } from "lib"; import { badgeProportionsIconOnly, badgeProportionsWithText, colors, deltaIcons, iconSizes, } from "./styles"; const makeBadgeDeltaClassName = makeClassName("BadgeDelta"); export interface BadgeDeltaProps extends React.HTMLAttributes { deltaType?: DeltaType; isIncreasePositive?: boolean; size?: Size; tooltip?: string; } const BadgeDelta = React.forwardRef((props, ref) => { const { deltaType = DeltaTypes.Increase, isIncreasePositive = true, size = Sizes.SM, tooltip, children, className, ...other } = props; const Icon = deltaIcons[deltaType]; const mappedDeltaType = mapInputsToDeltaType(deltaType, isIncreasePositive); const badgeProportions = children ? badgeProportionsWithText : badgeProportionsIconOnly; const { tooltipProps, getReferenceProps } = useTooltip(); return ( {children ? ( {children} ) : null} ); }); BadgeDelta.displayName = "BadgeDelta"; export default BadgeDelta; ================================================ FILE: src/components/icon-elements/BadgeDelta/index.ts ================================================ export { default as BadgeDelta } from "./BadgeDelta"; export type { BadgeDeltaProps } from "./BadgeDelta"; ================================================ FILE: src/components/icon-elements/BadgeDelta/styles.ts ================================================ import { BaseColors, DeltaTypes, getColorClassNames, colorPalette } from "lib"; import { ArrowDownIcon, ArrowDownRightIcon, ArrowRightIcon, ArrowUpIcon, ArrowUpRightIcon, } from "assets"; export type BadgeProportionTypes = { paddingX: string; paddingY: string; fontSize: string; }; export const badgeProportionsIconOnly: { [char: string]: BadgeProportionTypes; } = { xs: { paddingX: "px-2", paddingY: "py-0.5", fontSize: "text-xs", }, sm: { paddingX: "px-2.5", paddingY: "py-1", fontSize: "text-sm", }, md: { paddingX: "px-3", paddingY: "py-1.5", fontSize: "text-md", }, lg: { paddingX: "px-3.5", paddingY: "py-1.5", fontSize: "text-lg", }, xl: { paddingX: "px-3.5", paddingY: "py-1.5", fontSize: "text-xl", }, }; export const badgeProportionsWithText: { [char: string]: BadgeProportionTypes; } = { xs: { paddingX: "px-2", paddingY: "py-0.5", fontSize: "text-xs", }, sm: { paddingX: "px-2.5", paddingY: "py-0.5", fontSize: "text-sm", }, md: { paddingX: "px-3", paddingY: "py-0.5", fontSize: "text-md", }, lg: { paddingX: "px-3.5", paddingY: "py-0.5", fontSize: "text-lg", }, xl: { paddingX: "px-4", paddingY: "py-1", fontSize: "text-xl", }, }; export const iconSizes: { [size: string]: { height: string; width: string; }; } = { xs: { height: "h-4", width: "w-4", }, sm: { height: "h-4", width: "w-4", }, md: { height: "h-4", width: "w-4", }, lg: { height: "h-5", width: "w-5", }, xl: { height: "h-6", width: "w-6", }, }; export type ColorTypes = { bgColor: string; textColor: string; ringColor: string; }; export const colors: { [key: string]: ColorTypes } = { [DeltaTypes.Increase]: { bgColor: getColorClassNames(BaseColors.Emerald, colorPalette.background).bgColor, textColor: getColorClassNames(BaseColors.Emerald, colorPalette.iconText).textColor, ringColor: getColorClassNames(BaseColors.Emerald, colorPalette.iconRing).ringColor, }, [DeltaTypes.ModerateIncrease]: { bgColor: getColorClassNames(BaseColors.Emerald, colorPalette.background).bgColor, textColor: getColorClassNames(BaseColors.Emerald, colorPalette.iconText).textColor, ringColor: getColorClassNames(BaseColors.Emerald, colorPalette.iconRing).ringColor, }, [DeltaTypes.Decrease]: { bgColor: getColorClassNames(BaseColors.Red, colorPalette.background).bgColor, textColor: getColorClassNames(BaseColors.Red, colorPalette.iconText).textColor, ringColor: getColorClassNames(BaseColors.Red, colorPalette.iconRing).ringColor, }, [DeltaTypes.ModerateDecrease]: { bgColor: getColorClassNames(BaseColors.Red, colorPalette.background).bgColor, textColor: getColorClassNames(BaseColors.Red, colorPalette.iconText).textColor, ringColor: getColorClassNames(BaseColors.Red, colorPalette.iconRing).ringColor, }, [DeltaTypes.Unchanged]: { bgColor: getColorClassNames(BaseColors.Orange, colorPalette.background).bgColor, textColor: getColorClassNames(BaseColors.Orange, colorPalette.iconText).textColor, ringColor: getColorClassNames(BaseColors.Orange, colorPalette.iconRing).ringColor, }, }; export const deltaIcons: { [key: string]: React.ElementType } = { [DeltaTypes.Increase]: ArrowUpIcon, [DeltaTypes.ModerateIncrease]: ArrowUpRightIcon, [DeltaTypes.Decrease]: ArrowDownIcon, [DeltaTypes.ModerateDecrease]: ArrowDownRightIcon, [DeltaTypes.Unchanged]: ArrowRightIcon, }; ================================================ FILE: src/components/icon-elements/Icon/Icon.tsx ================================================ "use client"; import React from "react"; import Tooltip, { useTooltip } from "components/util-elements/Tooltip/Tooltip"; import { makeClassName, mergeRefs, Sizes, tremorTwMerge, Color, IconVariant, Size } from "lib"; import { getIconColors, iconSizes, shape, wrapperProportions } from "./styles"; const makeIconClassName = makeClassName("Icon"); export const IconVariants: { [key: string]: IconVariant } = { Simple: "simple", Light: "light", Shadow: "shadow", Solid: "solid", Outlined: "outlined", }; export interface IconProps extends React.HTMLAttributes { icon: React.ElementType; variant?: IconVariant; tooltip?: string; size?: Size; color?: Color; } const Icon = React.forwardRef((props, ref) => { const { icon, variant = IconVariants.Simple, tooltip, size = Sizes.SM, color, className, ...other } = props; const Icon = icon; const iconColorStyles = getIconColors(variant, color); const { tooltipProps, getReferenceProps } = useTooltip(); return ( ); }); Icon.displayName = "Icon"; export default Icon; ================================================ FILE: src/components/icon-elements/Icon/index.ts ================================================ export { default as Icon } from "./Icon"; export type { IconProps } from "./Icon"; ================================================ FILE: src/components/icon-elements/Icon/styles.ts ================================================ import { getColorClassNames, tremorTwMerge, colorPalette, Color, IconVariant } from "lib"; export type WrapperProportionTypes = { paddingX: string; paddingY: string; }; export const wrapperProportions: { [size: string]: WrapperProportionTypes } = { xs: { paddingX: "px-1.5", paddingY: "py-1.5", }, sm: { paddingX: "px-1.5", paddingY: "py-1.5", }, md: { paddingX: "px-2", paddingY: "py-2", }, lg: { paddingX: "px-2", paddingY: "py-2", }, xl: { paddingX: "px-2.5", paddingY: "py-2.5", }, }; export const iconSizes: { [size: string]: { height: string; width: string; }; } = { xs: { height: "h-3", width: "w-3", }, sm: { height: "h-5", width: "w-5", }, md: { height: "h-5", width: "w-5", }, lg: { height: "h-7", width: "w-7", }, xl: { height: "h-9", width: "w-9", }, }; export type ShapeTypes = { rounded: string; border: string; ring: string; shadow: string; }; export const shape: { [style: string]: ShapeTypes } = { simple: { rounded: "", border: "", ring: "", shadow: "", }, light: { rounded: "rounded-tremor-default", border: "", ring: "", shadow: "", }, shadow: { rounded: "rounded-tremor-default", border: "border", ring: "", shadow: "shadow-tremor-card dark:shadow-dark-tremor-card", }, solid: { rounded: "rounded-tremor-default", border: "border-2", ring: "ring-1", shadow: "", }, outlined: { rounded: "rounded-tremor-default", border: "border", ring: "ring-2", shadow: "", }, }; export const getIconColors = (variant: IconVariant, color?: Color) => { switch (variant) { case "simple": return { textColor: color ? getColorClassNames(color, colorPalette.text).textColor : "text-tremor-brand dark:text-dark-tremor-brand", bgColor: "", borderColor: "", ringColor: "", }; case "light": return { textColor: color ? getColorClassNames(color, colorPalette.text).textColor : "text-tremor-brand dark:text-dark-tremor-brand", bgColor: color ? tremorTwMerge( getColorClassNames(color, colorPalette.background).bgColor, "bg-opacity-20", ) : "bg-tremor-brand-muted dark:bg-dark-tremor-brand-muted", borderColor: "", ringColor: "", }; case "shadow": return { textColor: color ? getColorClassNames(color, colorPalette.text).textColor : "text-tremor-brand dark:text-dark-tremor-brand", bgColor: color ? tremorTwMerge( getColorClassNames(color, colorPalette.background).bgColor, "bg-opacity-20", ) : "bg-tremor-background dark:bg-dark-tremor-background", borderColor: "border-tremor-border dark:border-dark-tremor-border", ringColor: "", }; case "solid": return { textColor: color ? getColorClassNames(color, colorPalette.text).textColor : "text-tremor-brand-inverted dark:text-dark-tremor-brand-inverted", bgColor: color ? tremorTwMerge( getColorClassNames(color, colorPalette.background).bgColor, "bg-opacity-20", ) : "bg-tremor-brand dark:bg-dark-tremor-brand", borderColor: "border-tremor-brand-inverted dark:border-dark-tremor-brand-inverted", ringColor: "ring-tremor-ring dark:ring-dark-tremor-ring", }; case "outlined": return { textColor: color ? getColorClassNames(color, colorPalette.text).textColor : "text-tremor-brand dark:text-dark-tremor-brand", bgColor: color ? tremorTwMerge( getColorClassNames(color, colorPalette.background).bgColor, "bg-opacity-20", ) : "bg-tremor-background dark:bg-dark-tremor-background", borderColor: color ? getColorClassNames(color, colorPalette.ring).borderColor : "border-tremor-brand-subtle dark:border-dark-tremor-brand-subtle", ringColor: color ? tremorTwMerge(getColorClassNames(color, colorPalette.ring).ringColor, "ring-opacity-40") : "ring-tremor-brand-muted dark:ring-dark-tremor-brand-muted", }; } }; ================================================ FILE: src/components/icon-elements/index.ts ================================================ export * from "./Badge"; export * from "./BadgeDelta"; export * from "./Icon"; ================================================ FILE: src/components/index.ts ================================================ export * from "./chart-elements"; export * from "./icon-elements"; export * from "./input-elements"; export * from "./layout-elements"; export * from "./list-elements"; export * from "./spark-elements"; export * from "./text-elements"; export * from "./vis-elements"; ================================================ FILE: src/components/input-elements/BaseInput.tsx ================================================ "use client"; import React, { ReactNode, useCallback, useRef, useState } from "react"; import { ExclamationFilledIcon, EyeIcon, EyeOffIcon } from "assets"; import { getSelectButtonColors, hasValue } from "components/input-elements/selectUtils"; import { mergeRefs, tremorTwMerge } from "lib"; export interface BaseInputProps extends React.InputHTMLAttributes { type?: "text" | "password" | "email" | "url" | "number" | "search" | "tel"; defaultValue?: string | number; value?: string | number; icon?: React.ElementType | React.JSXElementConstructor; error?: boolean; errorMessage?: string; disabled?: boolean; stepper?: ReactNode; onValueChange?: (value: any) => void; makeInputClassName: (className: string) => string; pattern?: string; } const BaseInput = React.forwardRef((props, ref) => { const { value, defaultValue, type, placeholder = "Type...", icon, error = false, errorMessage, disabled = false, stepper, makeInputClassName, className, onChange, onValueChange, autoFocus, pattern, ...other } = props; const [isFocused, setIsFocused] = useState(autoFocus || false); const [isPasswordVisible, setIsPasswordVisible] = useState(false); const toggleIsPasswordVisible = useCallback( () => setIsPasswordVisible(!isPasswordVisible), [isPasswordVisible, setIsPasswordVisible], ); const Icon = icon; const inputRef = useRef(null); const hasSelection = hasValue(value || defaultValue); React.useEffect(() => { const handleFocus = () => setIsFocused(true); const handleBlur = () => setIsFocused(false); const node = inputRef.current; if (node) { node.addEventListener("focus", handleFocus); node.addEventListener("blur", handleBlur); // Autofocus logic if (autoFocus) { node.focus(); } } return () => { if (node) { node.removeEventListener("focus", handleFocus); node.removeEventListener("blur", handleBlur); } }; }, [autoFocus]); return ( <>
{Icon ? ( ) : null} { onChange?.(e); onValueChange?.(e.target.value); }} pattern={pattern} {...other} /> {type === "password" && !disabled ? ( ) : null} {error ? ( ) : null} {stepper ?? null}
{error && errorMessage ? (

{errorMessage}

) : null} ); }); BaseInput.displayName = "BaseInput"; export default BaseInput; ================================================ FILE: src/components/input-elements/Button/Button.tsx ================================================ "use client"; import Tooltip, { useTooltip } from "components/util-elements/Tooltip/Tooltip"; import React, { useEffect } from "react"; import { useTransition, type TransitionStatus } from "react-transition-state"; import { HorizontalPositions, makeClassName, mergeRefs, Sizes, tremorTwMerge } from "lib"; import { LoadingSpinner } from "assets"; import { ButtonVariant, Color, HorizontalPosition, Size } from "../../../lib"; import { getButtonColors, getButtonProportions, iconSizes } from "./styles"; const makeButtonClassName = makeClassName("Button"); export interface ButtonIconOrSpinnerProps { loading: boolean; iconSize: string; iconPosition: string; Icon: React.ElementType | undefined; needMargin: boolean; transitionStatus: TransitionStatus; } export const ButtonIconOrSpinner = ({ loading, iconSize, iconPosition, Icon, needMargin, transitionStatus, }: ButtonIconOrSpinnerProps) => { Icon = Icon!; const margin = !needMargin ? "" : iconPosition === HorizontalPositions.Left ? tremorTwMerge("-ml-1", "mr-1.5") : tremorTwMerge("-mr-1", "ml-1.5"); const defaultSpinnerSize = tremorTwMerge("w-0 h-0"); const spinnerSize: { [key: string]: any } = { default: defaultSpinnerSize, entering: defaultSpinnerSize, entered: iconSize, exiting: iconSize, exited: defaultSpinnerSize, }; return loading ? ( ) : ( ); }; export interface ButtonProps extends React.ButtonHTMLAttributes { icon?: React.ElementType; iconPosition?: HorizontalPosition; size?: Size; color?: Color; variant?: ButtonVariant; disabled?: boolean; loading?: boolean; loadingText?: string; tooltip?: string; } const Button = React.forwardRef((props, ref) => { const { icon, iconPosition = HorizontalPositions.Left, size = Sizes.SM, color, variant = "primary", disabled, loading = false, loadingText, children, tooltip, className, ...other } = props; const Icon = icon; const isDisabled = loading || disabled; const showButtonIconOrSpinner = Icon !== undefined || loading; const showLoadingText = loading && loadingText; const needIconMargin = children || showLoadingText ? true : false; const iconSize = tremorTwMerge(iconSizes[size].height, iconSizes[size].width); const buttonShapeStyles = variant !== "light" ? tremorTwMerge( // common "rounded-tremor-default border", // light "shadow-tremor-input", // dark "dark:shadow-dark-tremor-input", ) : ""; const buttonColorStyles = getButtonColors(variant, color); const buttonProportionStyles = getButtonProportions(variant)[size]; const delay = 300; const { tooltipProps, getReferenceProps } = useTooltip(delay); const [transitionState, toggleTransition] = useTransition({ timeout: 50 }); useEffect(() => { toggleTransition(loading); }, [loading]); return ( // eslint-disable-next-line react/button-has-type ); }); Button.displayName = "Button"; export default Button; ================================================ FILE: src/components/input-elements/Button/index.ts ================================================ export { default as Button } from "./Button"; export type { ButtonProps } from "./Button"; ================================================ FILE: src/components/input-elements/Button/styles.ts ================================================ import { getColorClassNames, tremorTwMerge, ButtonVariant, Color, colorPalette } from "lib"; export const iconSizes: { [size: string]: { height: string; width: string; }; } = { xs: { height: "h-4", width: "w-4", }, sm: { height: "h-5", width: "w-5", }, md: { height: "h-5", width: "w-5", }, lg: { height: "h-6", width: "w-6", }, xl: { height: "h-6", width: "w-6", }, }; export const getButtonProportions = (variant: ButtonVariant) => { if (!(variant === "light")) { return { xs: { paddingX: "px-2.5", paddingY: "py-1.5", fontSize: "text-xs", }, sm: { paddingX: "px-4", paddingY: "py-2", fontSize: "text-sm", }, md: { paddingX: "px-4", paddingY: "py-2", fontSize: "text-md", }, lg: { paddingX: "px-4", paddingY: "py-2.5", fontSize: "text-lg", }, xl: { paddingX: "px-4", paddingY: "py-3", fontSize: "text-xl", }, }; } return { xs: { paddingX: "", paddingY: "", fontSize: "text-xs", }, sm: { paddingX: "", paddingY: "", fontSize: "text-sm", }, md: { paddingX: "", paddingY: "", fontSize: "text-md", }, lg: { paddingX: "", paddingY: "", fontSize: "text-lg", }, xl: { paddingX: "", paddingY: "", fontSize: "text-xl", }, }; }; export const getButtonColors = (variant: ButtonVariant, color?: Color) => { switch (variant) { case "primary": return { textColor: color ? getColorClassNames("white").textColor : "text-tremor-brand-inverted dark:text-dark-tremor-brand-inverted", hoverTextColor: color ? getColorClassNames("white").textColor : "text-tremor-brand-inverted dark:text-dark-tremor-brand-inverted", bgColor: color ? getColorClassNames(color, colorPalette.background).bgColor : "bg-tremor-brand dark:bg-dark-tremor-brand", hoverBgColor: color ? getColorClassNames(color, colorPalette.darkBackground).hoverBgColor : "hover:bg-tremor-brand-emphasis dark:hover:bg-dark-tremor-brand-emphasis", borderColor: color ? getColorClassNames(color, colorPalette.border).borderColor : "border-tremor-brand dark:border-dark-tremor-brand", hoverBorderColor: color ? getColorClassNames(color, colorPalette.darkBorder).hoverBorderColor : "hover:border-tremor-brand-emphasis dark:hover:border-dark-tremor-brand-emphasis", }; case "secondary": return { textColor: color ? getColorClassNames(color, colorPalette.text).textColor : "text-tremor-brand dark:text-dark-tremor-brand", hoverTextColor: color ? getColorClassNames(color, colorPalette.text).textColor : "hover:text-tremor-brand-emphasis dark:hover:text-dark-tremor-brand-emphasis", bgColor: getColorClassNames("transparent").bgColor, hoverBgColor: color ? tremorTwMerge( getColorClassNames(color, colorPalette.background).hoverBgColor, "hover:bg-opacity-20 dark:hover:bg-opacity-20", ) : "hover:bg-tremor-brand-faint dark:hover:bg-dark-tremor-brand-faint", borderColor: color ? getColorClassNames(color, colorPalette.border).borderColor : "border-tremor-brand dark:border-dark-tremor-brand", }; case "light": return { textColor: color ? getColorClassNames(color, colorPalette.text).textColor : "text-tremor-brand dark:text-dark-tremor-brand", hoverTextColor: color ? getColorClassNames(color, colorPalette.darkText).hoverTextColor : "hover:text-tremor-brand-emphasis dark:hover:text-dark-tremor-brand-emphasis", bgColor: getColorClassNames("transparent").bgColor, borderColor: "", hoverBorderColor: "", }; } }; ================================================ FILE: src/components/input-elements/Calendar/Calendar.tsx ================================================ "use client"; import React from "react"; import { DayPicker, DayPickerRangeProps, DayPickerSingleProps, useNavigation, } from "react-day-picker"; import { ArrowLeftHeadIcon, ArrowRightHeadIcon, DoubleArrowLeftHeadIcon, DoubleArrowRightHeadIcon, } from "assets"; import { addYears, format } from "date-fns"; import { Text } from "../../text-elements/Text"; import { NavButton } from "./NavButton"; function Calendar({ mode, defaultMonth, selected, onSelect, locale, disabled, enableYearNavigation, classNames, weekStartsOn = 0, ...other }: T & { enableYearNavigation: boolean }) { return ( , IconRight: ({ ...props }) => , Caption: ({ ...props }) => { const { goToMonth, nextMonth, previousMonth, currentMonth } = useNavigation(); return (
{enableYearNavigation && ( currentMonth && goToMonth(addYears(currentMonth, -1))} icon={DoubleArrowLeftHeadIcon} /> )} previousMonth && goToMonth(previousMonth)} icon={ArrowLeftHeadIcon} />
{format(props.displayMonth, "LLLL yyy", { locale })}
nextMonth && goToMonth(nextMonth)} icon={ArrowRightHeadIcon} /> {enableYearNavigation && ( currentMonth && goToMonth(addYears(currentMonth, 1))} icon={DoubleArrowRightHeadIcon} /> )}
); }, }} {...other} /> ); } Calendar.displayName = "DateRangePicker"; export default Calendar; ================================================ FILE: src/components/input-elements/Calendar/NavButton.tsx ================================================ import { Icon as IconComponent } from "components/icon-elements"; import { tremorTwMerge } from "lib"; import React from "react"; interface NavButtonProps extends React.HTMLAttributes { onClick: () => void; icon: React.ElementType; } export const NavButton = ({ onClick, icon, ...other }: NavButtonProps) => { const Icon = icon; return ( ); }; ================================================ FILE: src/components/input-elements/Calendar/index.ts ================================================ export { default as Calendar } from "./Calendar"; ================================================ FILE: src/components/input-elements/DatePicker/DatePicker.tsx ================================================ "use client"; import { tremorTwMerge } from "lib"; import React, { useMemo } from "react"; import { DayPickerSingleProps } from "react-day-picker"; import { startOfMonth, startOfToday } from "date-fns"; import { enUS } from "date-fns/locale"; import { Popover, PopoverButton, PopoverPanel, Transition } from "@headlessui/react"; import { CalendarIcon, XCircleIcon } from "assets"; import { Calendar } from "components/input-elements/Calendar"; import { makeDatePickerClassName } from "components/input-elements/DatePicker/datePickerUtils"; import { useInternalState } from "hooks"; import { Color } from "../../../lib/inputTypes"; import { formatSelectedDates } from "../DateRangePicker/dateRangePickerUtils"; import { getSelectButtonColors, hasValue } from "../selectUtils"; const TODAY = startOfToday(); export type Locale = typeof enUS; export type DatePickerValue = Date | undefined; export interface DatePickerProps extends Omit, "value" | "defaultValue"> { value?: Date; defaultValue?: Date; onValueChange?: (value: DatePickerValue) => void; minDate?: Date; maxDate?: Date; placeholder?: string; disabled?: boolean; color?: Color; locale?: Locale; enableClear?: boolean; displayFormat?: string; enableYearNavigation?: boolean; weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; disabledDates?: Date[]; children?: React.ReactElement[] | React.ReactElement; } const DatePicker = React.forwardRef((props, ref) => { const { value, defaultValue, onValueChange, minDate, maxDate, placeholder = "Select date", disabled = false, locale = enUS, enableClear = true, displayFormat, className, enableYearNavigation = false, weekStartsOn = 0, disabledDates, ...other } = props; const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value); const disabledDays = useMemo(() => { const disabledDays = []; if (minDate) disabledDays.push({ before: minDate }); if (maxDate) disabledDays.push({ after: maxDate }); return [...disabledDays, ...(disabledDates ?? [])]; }, [minDate, maxDate, disabledDates]); const formattedSelection = !selectedValue ? placeholder : formatSelectedDates(selectedValue, undefined, locale, displayFormat); const defaultMonth = startOfMonth(selectedValue ?? maxDate ?? TODAY); const isClearEnabled = enableClear && !disabled; const handleReset = () => { onValueChange?.(undefined); setSelectedValue(undefined); }; return ( (selectedValue), disabled), )} > {isClearEnabled && selectedValue ? ( ) : null} {({ close }) => ( showOutsideDays={true} mode="single" defaultMonth={defaultMonth} selected={selectedValue} weekStartsOn={weekStartsOn} onSelect={ ((v: Date) => { onValueChange?.(v); setSelectedValue(v); close(); }) as any } locale={locale} disabled={disabledDays} enableYearNavigation={enableYearNavigation} /> )} ); }); DatePicker.displayName = "DatePicker"; export default DatePicker; ================================================ FILE: src/components/input-elements/DatePicker/datePickerUtils.tsx ================================================ import { makeClassName } from "lib"; export const makeDatePickerClassName = makeClassName("DatePicker"); ================================================ FILE: src/components/input-elements/DatePicker/index.ts ================================================ export { default as DatePicker } from "./DatePicker"; export type { DatePickerProps, DatePickerValue } from "./DatePicker"; ================================================ FILE: src/components/input-elements/DateRangePicker/DateRangePicker.tsx ================================================ "use client"; import { CalendarIcon, XCircleIcon } from "assets"; import { startOfMonth, startOfToday } from "date-fns"; import { tremorTwMerge } from "lib"; import React, { ReactElement, useMemo, useState } from "react"; import { DateRange, DayPickerRangeProps } from "react-day-picker"; import { constructValueToNameMapping, getNodeText, getSelectButtonColors, hasValue, } from "../selectUtils"; import { defaultOptions, formatSelectedDates, makeDateRangePickerClassName, parseEndDate, parseStartDate, } from "./dateRangePickerUtils"; import { Calendar } from "components/input-elements/Calendar"; import { DateRangePickerItemProps } from "components/input-elements/DateRangePicker/DateRangePickerItem"; import { SelectItem } from "components/input-elements/Select"; import { enUS } from "date-fns/locale"; import { useInternalState } from "hooks"; import { Color } from "../../../lib/inputTypes"; import { Popover, PopoverButton, Transition, PopoverPanel, Listbox, ListboxButton, ListboxOptions, } from "@headlessui/react"; const TODAY = startOfToday(); export type Locale = typeof enUS; export type DateRangePickerValue = { from?: Date; to?: Date; selectValue?: string }; export interface DateRangePickerProps extends Omit, "value" | "defaultValue"> { value?: DateRangePickerValue; defaultValue?: DateRangePickerValue; onValueChange?: (value: DateRangePickerValue) => void; enableSelect?: boolean; minDate?: Date; maxDate?: Date; placeholder?: string; selectPlaceholder?: string; disabled?: boolean; color?: Color; locale?: Locale; enableClear?: boolean; displayFormat?: string; enableYearNavigation?: boolean; weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; disabledDates?: Date[]; children?: React.ReactElement[] | React.ReactElement; } const DateRangePicker = React.forwardRef((props, ref) => { const { value, defaultValue, onValueChange, enableSelect = true, minDate, maxDate, placeholder = "Select range", selectPlaceholder = "Select range", disabled = false, locale = enUS, enableClear = true, displayFormat, children, className, enableYearNavigation = false, weekStartsOn = 0, disabledDates, ...other } = props; const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value); const [isCalendarButtonFocused, setIsCalendarButtonFocused] = useState(false); const [isSelectButtonFocused, setIsSelectButtonFocused] = useState(false); const disabledDays = useMemo(() => { const disabledDays = []; if (minDate) disabledDays.push({ before: minDate }); if (maxDate) disabledDays.push({ after: maxDate }); return [...disabledDays, ...(disabledDates ?? [])]; }, [minDate, maxDate, disabledDates]); const selectValues = useMemo(() => { const selectValues = new Map< string, Omit & { text: string } >(); if (children) { React.Children.forEach( children as ReactElement[], (child: React.ReactElement) => { selectValues.set(child.props.value, { text: (getNodeText(child) ?? child.props.value) as string, from: child.props.from, to: child.props.to, }); }, ); } else { defaultOptions.forEach((option) => { selectValues.set(option.value, { text: option.text, from: option.from, to: TODAY, }); }); } return selectValues; }, [children]); const valueToNameMapping = useMemo(() => { if (children) { return constructValueToNameMapping(children); } const valueToNameMapping = new Map(); defaultOptions.forEach((option) => valueToNameMapping.set(option.value, option.text)); return valueToNameMapping; }, [children]); const selectedSelectValue = selectedValue?.selectValue || ""; const selectedStartDate = parseStartDate( selectedValue?.from, minDate, selectedSelectValue, selectValues, ); const selectedEndDate = parseEndDate( selectedValue?.to, maxDate, selectedSelectValue, selectValues, ); const formattedSelection = !selectedStartDate && !selectedEndDate ? placeholder : formatSelectedDates(selectedStartDate, selectedEndDate, locale, displayFormat); const defaultMonth = startOfMonth(selectedEndDate ?? selectedStartDate ?? maxDate ?? TODAY); const isClearEnabled = enableClear && !disabled; const handleSelectClick = (value: string) => { const { from, to } = selectValues.get(value)!; const toDate = to ?? TODAY; onValueChange?.({ from, to: toDate, selectValue: value }); setSelectedValue({ from, to: toDate, selectValue: value }); }; const handleReset = () => { onValueChange?.({}); setSelectedValue({}); }; return (
setIsCalendarButtonFocused(true)} onBlur={() => setIsCalendarButtonFocused(false)} disabled={disabled} className={tremorTwMerge( // common "w-full outline-none text-left whitespace-nowrap truncate focus:ring-2 transition duration-100 rounded-l-tremor-default flex flex-nowrap border pl-3 py-2", // light "rounded-l-tremor-default border-tremor-border text-tremor-content-emphasis focus:border-tremor-brand-subtle focus:ring-tremor-brand-muted", // dark "dark:border-dark-tremor-border dark:text-dark-tremor-content-emphasis dark:focus:border-dark-tremor-brand-subtle dark:focus:ring-dark-tremor-brand-muted", enableSelect ? "rounded-l-tremor-default" : "rounded-tremor-default", isClearEnabled ? "pr-8" : "pr-4", getSelectButtonColors(hasValue(selectedStartDate || selectedEndDate), disabled), )} > {isClearEnabled && selectedStartDate ? ( ) : null}
mode="range" showOutsideDays={true} defaultMonth={defaultMonth} selected={{ from: selectedStartDate, to: selectedEndDate, }} onSelect={ ((v: DateRange) => { onValueChange?.({ from: v?.from, to: v?.to }); setSelectedValue({ from: v?.from, to: v?.to }); }) as any } locale={locale} disabled={disabledDays} enableYearNavigation={enableYearNavigation} classNames={{ day_range_middle: tremorTwMerge( "!rounded-none aria-selected:!bg-tremor-background-subtle aria-selected:dark:!bg-dark-tremor-background-subtle aria-selected:!text-tremor-content aria-selected:dark:!bg-dark-tremor-background-subtle", ), day_range_start: "rounded-r-none rounded-l-tremor-small aria-selected:text-tremor-brand-inverted dark:aria-selected:text-dark-tremor-brand-inverted", day_range_end: "rounded-l-none rounded-r-tremor-small aria-selected:text-tremor-brand-inverted dark:aria-selected:text-dark-tremor-brand-inverted", }} weekStartsOn={weekStartsOn} {...props} />
{enableSelect && ( {({ value }) => ( <> setIsSelectButtonFocused(true)} onBlur={() => setIsSelectButtonFocused(false)} className={tremorTwMerge( // common "w-full outline-none text-left whitespace-nowrap truncate rounded-r-tremor-default transition duration-100 border px-4 py-2", // light "border-tremor-border text-tremor-content-emphasis focus:border-tremor-brand-subtle", // dark "dark:border-dark-tremor-border dark:text-dark-tremor-content-emphasis dark:focus:border-dark-tremor-brand-subtle", getSelectButtonColors(hasValue(value), disabled), )} > {value ? (valueToNameMapping.get(value) ?? selectPlaceholder) : selectPlaceholder} {children ?? defaultOptions.map((option) => ( {option.text} ))} )} )}
); }); DateRangePicker.displayName = "DateRangePicker"; export default DateRangePicker; ================================================ FILE: src/components/input-elements/DateRangePicker/DateRangePickerItem.tsx ================================================ "use client"; import React from "react"; import { SelectItem } from "../Select"; export interface DateRangePickerItemProps extends React.HTMLAttributes { value: string; from: Date; to?: Date; } const DateRangePickerItem = React.forwardRef( (props, ref) => { const { value, className, children, ...other } = props; return ( {children ?? value} ); }, ); DateRangePickerItem.displayName = "DateRangePickerItem"; export default DateRangePickerItem; ================================================ FILE: src/components/input-elements/DateRangePicker/dateRangePickerUtils.tsx ================================================ import { format, isEqual, max, min, startOfDay, startOfMonth, startOfToday, startOfYear, sub, Locale, } from "date-fns"; import { makeClassName } from "lib"; export type DateRangePickerOption = { value: string; text: string; from: Date; to?: Date; }; export type DropdownValues = Map>; export const makeDateRangePickerClassName = makeClassName("DateRangePicker"); export const parseStartDate = ( startDate: Date | undefined, minDate: Date | undefined, selectedDropdownValue: string | undefined, selectValues: DropdownValues, ) => { if (selectedDropdownValue) { startDate = selectValues.get(selectedDropdownValue)?.from; } if (!startDate) return undefined; if (startDate && !minDate) return startOfDay(startDate); return startOfDay(max([startDate as Date, minDate as Date])); }; export const parseEndDate = ( endDate: Date | undefined, maxDate: Date | undefined, selectedDropdownValue: string | undefined, selectValues: DropdownValues, ) => { if (selectedDropdownValue) { endDate = startOfDay(selectValues.get(selectedDropdownValue)?.to ?? startOfToday()); } if (!endDate) return undefined; if (endDate && !maxDate) return startOfDay(endDate); return startOfDay(min([endDate as Date, maxDate as Date])); }; export const defaultOptions: DateRangePickerOption[] = [ { value: "tdy", text: "Today", from: startOfToday(), }, { value: "w", text: "Last 7 days", from: sub(startOfToday(), { days: 7 }), }, { value: "t", text: "Last 30 days", from: sub(startOfToday(), { days: 30 }), }, { value: "m", text: "Month to Date", from: startOfMonth(startOfToday()), }, { value: "y", text: "Year to Date", from: startOfYear(startOfToday()), }, ]; export const formatSelectedDates = ( startDate: Date | undefined, endDate: Date | undefined, locale?: Locale, displayFormat?: string, ) => { const localeCode = locale?.code || "en-US"; if (!startDate && !endDate) { return ""; } else if (startDate && !endDate) { if (displayFormat) return format(startDate, displayFormat); const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", }; return startDate.toLocaleDateString(localeCode, options); } else if (startDate && endDate) { if (isEqual(startDate, endDate)) { if (displayFormat) return format(startDate, displayFormat); const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", }; return startDate.toLocaleDateString(localeCode, options); } else if ( startDate.getMonth() === endDate.getMonth() && startDate.getFullYear() === endDate.getFullYear() ) { if (displayFormat) return `${format(startDate, displayFormat)} - ${format(endDate, displayFormat)}`; const optionsStartDate: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", }; return `${startDate.toLocaleDateString(localeCode, optionsStartDate)} - ${endDate.getDate()}, ${endDate.getFullYear()}`; } else { if (displayFormat) return `${format(startDate, displayFormat)} - ${format(endDate, displayFormat)}`; const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", }; return `${startDate.toLocaleDateString(localeCode, options)} - ${endDate.toLocaleDateString(localeCode, options)}`; } } return ""; }; ================================================ FILE: src/components/input-elements/DateRangePicker/index.ts ================================================ export { default as DateRangePicker } from "./DateRangePicker"; export type { DateRangePickerProps, DateRangePickerValue } from "./DateRangePicker"; export { default as DateRangePickerItem } from "./DateRangePickerItem"; export type { DateRangePickerItemProps } from "./DateRangePickerItem"; ================================================ FILE: src/components/input-elements/MultiSelect/MultiSelect.tsx ================================================ "use client"; import React, { isValidElement, useMemo, useRef, useState } from "react"; import { SelectedValueContext } from "contexts"; import { useInternalState } from "hooks"; import { ArrowDownHeadIcon, SearchIcon, XCircleIcon } from "assets"; import XIcon from "assets/XIcon"; import { makeClassName, tremorTwMerge } from "lib"; import { getFilteredOptions, getSelectButtonColors } from "../selectUtils"; import { Listbox, ListboxButton, ListboxOptions, Transition } from "@headlessui/react"; const makeMultiSelectClassName = makeClassName("MultiSelect"); export interface MultiSelectProps extends React.HTMLAttributes { defaultValue?: string[]; name?: string; value?: string[]; onValueChange?: (value: string[]) => void; placeholder?: string; placeholderSearch?: string; disabled?: boolean; icon?: React.ElementType | React.JSXElementConstructor; required?: boolean; error?: boolean; errorMessage?: string; children: React.ReactNode; } const MultiSelect = React.forwardRef((props, ref) => { const { defaultValue = [], value, onValueChange, placeholder = "Select...", placeholderSearch = "Search", disabled = false, icon, children, className, required, name, error = false, errorMessage, id, ...other } = props; const listboxButtonRef = useRef(null); const Icon = icon; const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value); const { reactElementChildren, optionsAvailable } = useMemo(() => { const reactElementChildren = React.Children.toArray(children).filter(isValidElement); const optionsAvailable = getFilteredOptions("", reactElementChildren); return { reactElementChildren, optionsAvailable }; }, [children]); const [searchQuery, setSearchQuery] = useState(""); // checked if there are selected options // used the same code from the previous version const selectedItems = selectedValue ?? []; const hasSelection = selectedItems.length > 0; const filteredOptions = useMemo( () => (searchQuery ? getFilteredOptions(searchQuery, reactElementChildren) : optionsAvailable), [searchQuery, reactElementChildren, optionsAvailable], ); const handleReset = () => { setSelectedValue([]); onValueChange?.([]); }; const handleResetSearch = () => { setSearchQuery(""); }; return (
{ onValueChange?.(values); setSelectedValue(values); }) as any } disabled={disabled} id={id} multiple {...other} > {({ value }) => ( <> 0, disabled, error), )} ref={listboxButtonRef} > {Icon && ( )}
{value.length > 0 ? (
{optionsAvailable .filter((option) => value.includes(option.props.value)) .map((option, index) => { return (
{option.props.children ?? option.props.value}
{ e.preventDefault(); const newValue = value.filter((v) => v !== option.props.value); onValueChange?.(newValue); setSelectedValue(newValue); }} >
); })}
) : ( {placeholder} )}
{hasSelection && !disabled ? ( ) : null}
{ if (e.code === "Space" && (e.target as HTMLInputElement).value !== "") { e.stopPropagation(); } }} onChange={(e) => setSearchQuery(e.target.value)} value={searchQuery} />
{filteredOptions}
)}
{error && errorMessage ? (

{errorMessage}

) : null}
); }); MultiSelect.displayName = "MultiSelect"; export default MultiSelect; ================================================ FILE: src/components/input-elements/MultiSelect/MultiSelectItem.tsx ================================================ "use client"; import { SelectedValueContext } from "contexts"; import React, { useContext } from "react"; import { isValueInArray, makeClassName, tremorTwMerge } from "lib"; import { ListboxOption } from "@headlessui/react"; const makeMultiSelectItemClassName = makeClassName("MultiSelectItem"); export interface MultiSelectItemProps extends React.HTMLAttributes { value: string; } const MultiSelectItem = React.forwardRef((props, ref) => { const { value, className, children, ...other } = props; const { selectedValue } = useContext(SelectedValueContext); const isSelected = isValueInArray(value, selectedValue); return ( {children ?? value} ); }); MultiSelectItem.displayName = "MultiSelectItem"; export default MultiSelectItem; ================================================ FILE: src/components/input-elements/MultiSelect/index.ts ================================================ export { default as MultiSelect } from "./MultiSelect"; export type { MultiSelectProps } from "./MultiSelect"; export { default as MultiSelectItem } from "./MultiSelectItem"; export type { MultiSelectItemProps } from "./MultiSelectItem"; ================================================ FILE: src/components/input-elements/NumberInput/NumberInput.tsx ================================================ "use client"; import { MinusIcon, PlusIcon } from "assets"; import { makeClassName, mergeRefs, tremorTwMerge } from "lib"; import React, { useRef } from "react"; import BaseInput, { BaseInputProps } from "../BaseInput"; export interface NumberInputProps extends Omit { step?: string | number; enableStepper?: boolean; onSubmit?: (value: number) => void; onValueChange?: (value: number) => void; } const baseArrowClasses = "flex mx-auto text-tremor-content-subtle dark:text-dark-tremor-content-subtle"; const enabledArrowClasses = "cursor-pointer hover:text-tremor-content dark:hover:text-dark-tremor-content"; const NumberInput = React.forwardRef((props, ref) => { const { onSubmit, enableStepper = true, disabled, onValueChange, onChange, ...other } = props; const inputRef = useRef(null); const [isArrowDownPressed, setIsArrowDownPressed] = React.useState(false); const handleArrowDownPress = React.useCallback(() => { setIsArrowDownPressed(true); }, []); const handleArrowDownRelease = React.useCallback(() => { setIsArrowDownPressed(false); }, []); const [isArrowUpPressed, setIsArrowUpPressed] = React.useState(false); const handleArrowUpPress = React.useCallback(() => { setIsArrowUpPressed(true); }, []); const handleArrowUpRelease = React.useCallback(() => { setIsArrowUpPressed(false); }, []); return ( { if (e.key === "Enter" && !e.ctrlKey && !e.altKey && !e.shiftKey) { const value = inputRef.current?.value; onSubmit?.(parseFloat(value ?? "")); } if (e.key === "ArrowDown") { handleArrowDownPress(); } if (e.key === "ArrowUp") { handleArrowUpPress(); } }} onKeyUp={(e) => { if (e.key === "ArrowDown") { handleArrowDownRelease(); } if (e.key === "ArrowUp") { handleArrowUpRelease(); } }} onChange={(e) => { if (disabled) return; onValueChange?.(parseFloat(e.target.value)); onChange?.(e); }} stepper={ enableStepper ? (
e.preventDefault()} onMouseDown={(e) => e.preventDefault()} onTouchStart={(e) => { if (e.cancelable) { e.preventDefault(); } }} onMouseUp={() => { if (disabled) return; inputRef.current?.stepDown(); inputRef.current?.dispatchEvent(new Event("input", { bubbles: true })); }} className={tremorTwMerge( !disabled && enabledArrowClasses, baseArrowClasses, "group py-[10px] px-2.5 border-l border-tremor-border dark:border-dark-tremor-border", )} >
e.preventDefault()} onMouseDown={(e) => e.preventDefault()} onTouchStart={(e) => { if (e.cancelable) { e.preventDefault(); } }} onMouseUp={() => { if (disabled) return; inputRef.current?.stepUp(); inputRef.current?.dispatchEvent(new Event("input", { bubbles: true })); }} className={tremorTwMerge( !disabled && enabledArrowClasses, baseArrowClasses, "group py-[10px] px-2.5 border-l border-tremor-border dark:border-dark-tremor-border", )} >
) : null } {...other} /> ); }); NumberInput.displayName = "NumberInput"; export default NumberInput; ================================================ FILE: src/components/input-elements/NumberInput/index.ts ================================================ export { default as NumberInput } from "./NumberInput"; export type { NumberInputProps } from "./NumberInput"; ================================================ FILE: src/components/input-elements/SearchSelect/SearchSelect.tsx ================================================ "use client"; import { useInternalState } from "hooks"; import React, { isValidElement, useMemo, useRef } from "react"; import { Combobox, ComboboxButton, ComboboxInput, ComboboxOptions, Transition, } from "@headlessui/react"; import { ArrowDownHeadIcon, XCircleIcon } from "assets"; import { makeClassName, tremorTwMerge } from "lib"; import { constructValueToNameMapping, getFilteredOptions, getSelectButtonColors, hasValue, } from "../selectUtils"; const makeSearchSelectClassName = makeClassName("SearchSelect"); export interface SearchSelectProps extends React.HTMLAttributes { defaultValue?: string; name?: string; searchValue?: string; onSearchValueChange?: (value: string) => void; value?: string; onValueChange?: (value: string) => void; placeholder?: string; disabled?: boolean; icon?: React.ElementType | React.JSXElementConstructor; required?: boolean; error?: boolean; errorMessage?: string; enableClear?: boolean; children: React.ReactNode; autoComplete?: string; } const makeSelectClassName = makeClassName("SearchSelect"); const SearchSelect = React.forwardRef((props, ref) => { const { defaultValue = "", searchValue, onSearchValueChange, value, onValueChange, placeholder = "Select...", disabled = false, icon, enableClear = true, name, required, error = false, errorMessage, children, className, id, autoComplete = "off", ...other } = props; const comboboxInputRef = useRef(null); const [searchQuery, setSearchQuery] = useInternalState("", searchValue); const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value); const Icon = icon; const { reactElementChildren, valueToNameMapping } = useMemo(() => { const reactElementChildren = React.Children.toArray(children).filter(isValidElement); const valueToNameMapping = constructValueToNameMapping(reactElementChildren); return { reactElementChildren, valueToNameMapping }; }, [children]); const filteredOptions = useMemo( () => getFilteredOptions(searchQuery ?? "", reactElementChildren), [searchQuery, reactElementChildren], ); const handleReset = () => { setSelectedValue(""); setSearchQuery(""); onValueChange?.(""); onSearchValueChange?.(""); }; return (
{ onValueChange?.(value); setSelectedValue(value); }) as any } disabled={disabled} id={id} {...other} > {({ value }) => ( <> {Icon && ( )} { onSearchValueChange?.(event.target.value); setSearchQuery(event.target.value); }} displayValue={(value: string) => valueToNameMapping.get(value) ?? ""} autoComplete={autoComplete} />
{enableClear && selectedValue ? ( ) : null} {filteredOptions.length > 0 && ( {filteredOptions} )} )}
{error && errorMessage ? (

{errorMessage}

) : null}
); }); SearchSelect.displayName = "SearchSelect"; export default SearchSelect; ================================================ FILE: src/components/input-elements/SearchSelect/SearchSelectItem.tsx ================================================ "use client"; import React from "react"; import { makeClassName, tremorTwMerge } from "lib"; import { ComboboxOption } from "@headlessui/react"; const makeSearchSelectItemClassName = makeClassName("SearchSelectItem"); export interface SearchSelectItemProps extends React.HTMLAttributes { value: string; icon?: React.ElementType; } const SearchSelectItem = React.forwardRef((props, ref) => { const { value, icon, className, children, ...other } = props; const Icon = icon; return ( {Icon && ( )} {children ?? value} ); }); SearchSelectItem.displayName = "SearchSelectItem"; export default SearchSelectItem; ================================================ FILE: src/components/input-elements/SearchSelect/index.ts ================================================ export { default as SearchSelect } from "./SearchSelect"; export type { SearchSelectProps } from "./SearchSelect"; export { default as SearchSelectItem } from "./SearchSelectItem"; export type { SearchSelectItemProps } from "./SearchSelectItem"; ================================================ FILE: src/components/input-elements/Select/Select.tsx ================================================ "use client"; import { ArrowDownHeadIcon, XCircleIcon } from "assets"; import { makeClassName, tremorTwMerge } from "lib"; import React, { Children, isValidElement, useMemo, useRef } from "react"; import { constructValueToNameMapping, getSelectButtonColors, hasValue } from "../selectUtils"; import { Listbox, ListboxButton, ListboxOptions, Transition } from "@headlessui/react"; import { useInternalState } from "hooks"; const makeSelectClassName = makeClassName("Select"); export interface SelectProps extends React.HTMLAttributes { value?: string; name?: string; defaultValue?: string; onValueChange?: (value: string) => void; placeholder?: string; disabled?: boolean; icon?: React.JSXElementConstructor; enableClear?: boolean; required?: boolean; error?: boolean; errorMessage?: string; children: React.ReactNode; } const Select = React.forwardRef((props, ref) => { const { defaultValue = "", value, onValueChange, placeholder = "Select...", disabled = false, icon, enableClear = false, required, children, name, error = false, errorMessage, className, id, ...other } = props; const listboxButtonRef = useRef(null); const childrenArray = Children.toArray(children); // @sev const [selectedValue, setSelectedValue] = useInternalState(defaultValue, value); const Icon = icon; const valueToNameMapping = useMemo(() => { const reactElementChildren = React.Children.toArray(children).filter(isValidElement); const valueToNameMapping = constructValueToNameMapping(reactElementChildren); return valueToNameMapping; }, [children]); const handleReset = () => { setSelectedValue(""); onValueChange?.(""); }; return (
{ onValueChange?.(value); setSelectedValue(value); }) as any } disabled={disabled} id={id} {...other} > {({ value }) => ( <> {Icon && ( )} {value ? (valueToNameMapping.get(value) ?? placeholder) : placeholder} {enableClear && selectedValue ? ( ) : null} {children} )}
{error && errorMessage ? (

{errorMessage}

) : null}
); }); Select.displayName = "Select"; export default Select; ================================================ FILE: src/components/input-elements/Select/SelectItem.tsx ================================================ "use client"; import React from "react"; import { ListboxOption } from "@headlessui/react"; import { makeClassName, tremorTwMerge } from "lib"; const makeSelectItemClassName = makeClassName("SelectItem"); export interface SelectItemProps extends React.HTMLAttributes { value: string; icon?: React.ElementType; } const SelectItem = React.forwardRef((props, ref) => { const { value, icon, className, children, ...other } = props; const Icon = icon; return ( {Icon && ( )} {children ?? value} ); }); SelectItem.displayName = "SelectItem"; export default SelectItem; ================================================ FILE: src/components/input-elements/Select/index.ts ================================================ export { default as Select } from "./Select"; export type { SelectProps } from "./Select"; export { default as SelectItem } from "./SelectItem"; export type { SelectItemProps } from "./SelectItem"; ================================================ FILE: src/components/input-elements/Switch/Switch.tsx ================================================ "use client"; import { Switch as HeadlessSwitch } from "@headlessui/react"; import { useInternalState } from "hooks"; import { Color, makeClassName, tremorTwMerge, colorPalette, getColorClassNames, mergeRefs, } from "lib"; import Tooltip, { useTooltip } from "components/util-elements/Tooltip/Tooltip"; import React, { useState } from "react"; const getSwitchColors = (color?: Color) => { return { bgColor: color ? getColorClassNames(color, colorPalette.background).bgColor : "bg-tremor-brand dark:bg-dark-tremor-brand", ringColor: color ? getColorClassNames(color, colorPalette.ring).ringColor : "ring-tremor-brand-muted dark:ring-dark-tremor-brand-muted", }; }; const makeSwitchClassName = makeClassName("Switch"); export interface SwitchProps extends Omit, "onChange"> { checked?: boolean; defaultChecked?: boolean; onChange?: (value: boolean) => void; color?: Color; name?: string; error?: boolean; errorMessage?: string; disabled?: boolean; required?: boolean; id?: string; tooltip?: string; } const Switch = React.forwardRef((props, ref) => { const { checked, defaultChecked = false, onChange, color, name, error, errorMessage, disabled, required, tooltip, id, ...other } = props; const switchColorStyles = getSwitchColors(color); const [isChecked, setIsChecked] = useInternalState(defaultChecked, checked); const [isFocused, setIsFocused] = useState(false); const delay = 300; const { tooltipProps, getReferenceProps } = useTooltip(delay); return (
{ e.preventDefault(); }} /> { setIsChecked(e); onChange?.(e); }} disabled={disabled} className={tremorTwMerge( makeSwitchClassName("switch"), "w-10 h-5 group relative inline-flex shrink-0 cursor-pointer items-center justify-center rounded-tremor-full", "focus:outline-none", disabled ? "cursor-not-allowed" : "", )} onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} id={id} > Switch {isChecked ? "on" : "off"}
{error && errorMessage ? (

{errorMessage}

) : null}
); }); Switch.displayName = "Switch"; export default Switch; ================================================ FILE: src/components/input-elements/Switch/index.ts ================================================ export { default as Switch } from "./Switch"; export type { SwitchProps } from "./Switch"; ================================================ FILE: src/components/input-elements/Tabs/Tab.tsx ================================================ "use client"; import { Tab as HeadlessTab } from "@headlessui/react"; import { colorPalette, getColorClassNames, tremorTwMerge, makeClassName } from "lib"; import React, { useContext } from "react"; import { TabVariant, TabVariantContext } from "components/input-elements/Tabs/TabList"; import { BaseColorContext } from "contexts"; import { Color } from "../../../lib/inputTypes"; const makeTabClassName = makeClassName("Tab"); function getVariantStyles(tabVariant: TabVariant, color?: Color) { switch (tabVariant) { case "line": return tremorTwMerge( // common "data-[selected]:border-b-2 hover:border-b-2 border-transparent transition duration-100 -mb-px px-2 py-2", // light "hover:border-tremor-content hover:text-tremor-content-emphasis text-tremor-content", // dark "[&:not([data-selected])]:dark:hover:border-dark-tremor-content-emphasis [&:not([data-selected])]:dark:hover:text-dark-tremor-content-emphasis [&:not([data-selected])]:dark:text-dark-tremor-content", // brand color ? getColorClassNames(color, colorPalette.border).selectBorderColor : [ "data-[selected]:border-tremor-brand data-[selected]:text-tremor-brand", "data-[selected]:dark:border-dark-tremor-brand data-[selected]:dark:text-dark-tremor-brand", ], ); case "solid": return tremorTwMerge( // common "border-transparent border rounded-tremor-small px-2.5 py-1", // light "data-[selected]:border-tremor-border data-[selected]:bg-tremor-background data-[selected]:shadow-tremor-input [&:not([data-selected])]:hover:text-tremor-content-emphasis data-[selected]:text-tremor-brand [&:not([data-selected])]:text-tremor-content", // dark "dark:data-[selected]:border-dark-tremor-border dark:data-[selected]:bg-dark-tremor-background dark:data-[selected]:shadow-dark-tremor-input dark:[&:not([data-selected])]:hover:text-dark-tremor-content-emphasis dark:data-[selected]:text-dark-tremor-brand dark:[&:not([data-selected])]:text-dark-tremor-content", // brand color ? getColorClassNames(color, colorPalette.text).selectTextColor : "text-tremor-content dark:text-dark-tremor-content", ); } } export interface TabProps extends React.ButtonHTMLAttributes { icon?: React.ElementType; } const Tab = React.forwardRef((props, ref) => { const { icon, className, children, ...other } = props; const variant = useContext(TabVariantContext); const color = useContext(BaseColorContext); const Icon = icon; return ( {Icon ? ( ) : null} {children ? {children} : null} ); }); Tab.displayName = "Tab"; export default Tab; ================================================ FILE: src/components/input-elements/Tabs/TabGroup.tsx ================================================ "use client"; import { Tab } from "@headlessui/react"; import { makeClassName, tremorTwMerge } from "lib"; import React from "react"; const makeTabGroupClassName = makeClassName("TabGroup"); export interface TabGroupProps extends React.HTMLAttributes { defaultIndex?: number; index?: number; onIndexChange?: (index: number) => void; children: React.ReactElement[] | React.ReactElement; } const TabGroup = React.forwardRef((props, ref) => { const { defaultIndex, index, onIndexChange, children, className, ...other } = props; return ( {children} ); }); TabGroup.displayName = "TabGroup"; export default TabGroup; ================================================ FILE: src/components/input-elements/Tabs/TabList.tsx ================================================ "use client"; import React, { createContext } from "react"; import { BaseColorContext } from "contexts"; import { Tab } from "@headlessui/react"; import { makeClassName, tremorTwMerge, Color } from "lib"; const makeTabListClassName = makeClassName("TabList"); export type TabVariant = "line" | "solid"; export const TabVariantContext = createContext("line"); const variantStyles: { [key in TabVariant]: string } = { line: tremorTwMerge( // common "flex border-b space-x-4", // light "border-tremor-border", // dark "dark:border-dark-tremor-border", ), solid: tremorTwMerge( // common "inline-flex p-0.5 rounded-tremor-default space-x-1.5", // light "bg-tremor-background-subtle", // dark "dark:bg-dark-tremor-background-subtle", ), }; export interface TabListProps extends React.HTMLAttributes { color?: Color; variant?: TabVariant; children: React.ReactElement[] | React.ReactElement; } const TabList = React.forwardRef((props, ref) => { const { color, variant = "line", children, className, ...other } = props; return ( {children} ); }); TabList.displayName = "TabList"; export default TabList; ================================================ FILE: src/components/input-elements/Tabs/TabPanel.tsx ================================================ "use client"; import { IndexContext, SelectedValueContext } from "contexts"; import { makeClassName, tremorTwMerge } from "lib"; import React, { useContext } from "react"; const makeTabPanelClassName = makeClassName("TabPanel"); const TabPanel = React.forwardRef>( (props, ref) => { const { children, className, ...other } = props; const { selectedValue: selectedIndex } = useContext(SelectedValueContext); const index = useContext(IndexContext); const isSelected = selectedIndex === index; return ( // Not using Tab.Panel because of https://github.com/tailwindlabs/headlessui/discussions/2366.
{children}
); }, ); TabPanel.displayName = "TabPanel"; export default TabPanel; ================================================ FILE: src/components/input-elements/Tabs/TabPanels.tsx ================================================ "use client"; import { Tab } from "@headlessui/react"; import { IndexContext, SelectedValueContext } from "contexts"; import { makeClassName, tremorTwMerge } from "lib"; import React from "react"; const makeTabPanelsClassName = makeClassName("TabPanels"); const TabPanels = React.forwardRef>( (props, ref) => { const { children, className, ...other } = props; return ( {({ selectedIndex }) => ( {React.Children.map(children, (child, index) => ( {child} ))} )} ); }, ); TabPanels.displayName = "TabPanels"; export default TabPanels; ================================================ FILE: src/components/input-elements/Tabs/index.ts ================================================ export { default as Tab } from "./Tab"; export type { TabProps } from "./Tab"; export { default as TabGroup } from "./TabGroup"; export type { TabGroupProps } from "./TabGroup"; export { default as TabList } from "./TabList"; export type { TabListProps } from "./TabList"; export { default as TabPanel } from "./TabPanel"; export { default as TabPanels } from "./TabPanels"; ================================================ FILE: src/components/input-elements/TextInput/TextInput.tsx ================================================ "use client"; import React from "react"; import { makeClassName } from "lib"; import BaseInput, { BaseInputProps } from "../BaseInput"; export type TextInputProps = Omit & { defaultValue?: string; value?: string; onValueChange?: (value: string) => void; icon?: React.ElementType | React.JSXElementConstructor; error?: boolean; errorMessage?: string; disabled?: boolean; }; const makeTextInputClassName = makeClassName("TextInput"); const TextInput = React.forwardRef((props, ref) => { const { type = "text", ...other } = props; return ; }); TextInput.displayName = "TextInput"; export default TextInput; ================================================ FILE: src/components/input-elements/TextInput/index.ts ================================================ export { default as TextInput } from "./TextInput"; export type { TextInputProps } from "./TextInput"; ================================================ FILE: src/components/input-elements/Textarea/Textarea.tsx ================================================ "use client"; import { getSelectButtonColors, hasValue } from "components/input-elements/selectUtils"; import { useInternalState } from "hooks"; import { makeClassName, mergeRefs, tremorTwMerge } from "lib"; import React, { useEffect, useRef } from "react"; export interface TextareaProps extends React.TextareaHTMLAttributes { defaultValue?: string | number; value?: string | number; error?: boolean; errorMessage?: string; disabled?: boolean; autoHeight?: boolean; onValueChange?: (value: any) => void; } const makeTextareaClassName = makeClassName("Textarea"); const Textarea = React.forwardRef((props, ref) => { const { value, defaultValue = "", placeholder = "Type...", error = false, errorMessage, disabled = false, className, onChange, onValueChange, autoHeight = false, ...other } = props; const [val, setVal] = useInternalState(defaultValue, value); const inputRef = useRef(null); const hasSelection = hasValue(val); useEffect(() => { const textAreaHTMLRef = inputRef.current; if (autoHeight && textAreaHTMLRef) { textAreaHTMLRef.style.height = "60px"; // Calculates the height dynamically const scrollHeight = textAreaHTMLRef.scrollHeight; textAreaHTMLRef.style.height = scrollHeight + "px"; } }, [autoHeight, inputRef, val]); return ( <>