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
================================================
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.

## 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
);
}
```
## 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 && (
{xAxisLabel}
)}
{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) => {
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}
)}
) : (
{xAxisLabel && (
{xAxisLabel}
)}
)}
{layout !== "vertical" ? (
`${(value * 100).toString()} %` : valueFormatter
}
allowDecimals={allowDecimals}
>
{yAxisLabel && (
{yAxisLabel}
)}
) : (
{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 ? (
) : 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 && (
{xAxisLabel}
)}
{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) => (
{
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 && (
{xAxisLabel}
)}
) : null}
{y ? (
{yAxisLabel && (
{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) => (
);
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 (
{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) => (
);
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 (
{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 (
);
};
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 ? (
toggleIsPasswordVisible()}
aria-label={isPasswordVisible ? "Hide password" : "Show Password"}
>
{isPasswordVisible ? (
) : (
)}
) : 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
{showButtonIconOrSpinner && iconPosition !== HorizontalPositions.Right ? (
) : null}
{showLoadingText || children ? (
{showLoadingText ? loadingText : children}
) : null}
{showButtonIconOrSpinner && iconPosition === HorizontalPositions.Right ? (
) : null}
);
});
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),
)}
>
{formattedSelection}
{isClearEnabled && selectedValue ? (
{
e.preventDefault();
handleReset();
}}
>
) : 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),
)}
>
{formattedSelection}
{isClearEnabled && selectedStartDate ? (
{
e.preventDefault();
handleReset();
}}
>
) : 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 (
{
e.preventDefault();
}}
name={name}
disabled={disabled}
multiple
id={id}
onFocus={() => {
const listboxButton = listboxButtonRef.current;
if (listboxButton) listboxButton.focus();
}}
>
{placeholder}
{filteredOptions.map((child: any) => {
const value = child.props.value;
const name = child.props.children;
return (
{name}
);
})}
{
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 ? (
{
e.preventDefault();
handleReset();
}}
>
) : 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 (
{
e.preventDefault();
}}
name={name}
disabled={disabled}
id={id}
onFocus={() => {
const comboboxInput = comboboxInputRef.current;
if (comboboxInput) comboboxInput.focus();
}}
>
{placeholder}
{filteredOptions.map((child: any) => {
const value = child.props.value;
const name = child.props.children;
return (
{name}
);
})}
{
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 ? (
{
e.preventDefault();
handleReset();
}}
>
) : 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 (
{
e.preventDefault();
}}
name={name}
disabled={disabled}
id={id}
onFocus={() => {
const listboxButton = listboxButtonRef.current;
if (listboxButton) listboxButton.focus();
}}
>
{placeholder}
{childrenArray.map((child: any) => {
const value = child.props.value;
const name = child.props.children;
return (
{name}
);
})}
{
onValueChange?.(value);
setSelectedValue(value);
}) as any
}
disabled={disabled}
id={id}
{...other}
>
{({ value }) => (
<>
{Icon && (
)}
{value ? (valueToNameMapping.get(value) ?? placeholder) : placeholder}
{enableClear && selectedValue ? (
{
e.preventDefault();
handleReset();
}}
>
) : 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 (
<>