├── .all-contributorsrc
├── .changeset
├── README.md
└── config.json
├── .github
├── FUNDING.yml
├── canary-version.mjs
└── workflows
│ ├── pr.yml
│ ├── release-canary.yml
│ └── release.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── .storybook
├── main.ts
├── manager-head.html
├── preview-head.html
├── preview.ts
└── test-runner.ts
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── UPGRADE.md
├── biome.json
├── package.json
├── pnpm-lock.yaml
├── react-trello-README.md
├── react-trello.gif
├── src
├── components
│ ├── AddCardLink.tsx
│ ├── Card.tsx
│ ├── Card
│ │ └── Tag.tsx
│ ├── Lane
│ │ ├── LaneFooter.tsx
│ │ ├── LaneHeader.tsx
│ │ └── LaneHeader
│ │ │ └── LaneMenu.tsx
│ ├── Loader.tsx
│ ├── NewCardForm.tsx
│ ├── NewLaneForm.tsx
│ ├── NewLaneSection.tsx
│ └── index.ts
├── controllers
│ ├── Board.tsx
│ ├── BoardContainer.tsx
│ └── Lane.tsx
├── dnd
│ ├── Container.tsx
│ └── Draggable.tsx
├── helpers
│ ├── autosize.ts
│ ├── createTranslate.ts
│ └── deprecationWarnings.ts
├── index.tsx
├── locales
│ ├── en
│ │ └── translation.json
│ ├── index.ts
│ └── ru
│ │ └── translation.json
├── store
│ ├── store.ts
│ └── useBoard.ts
├── styles
│ ├── Base.ts
│ ├── Elements.ts
│ └── Loader.ts
├── types
│ ├── Board.ts
│ ├── EventBus.ts
│ └── utilities.ts
└── widgets
│ ├── DeleteButton.tsx
│ ├── EditableLabel.tsx
│ ├── InlineInput.tsx
│ ├── NewLaneTitleEditor.tsx
│ └── index.ts
├── stories
├── AdvancedFeatures.story.tsx
├── BasicFunctions.story.tsx
├── CustomAddCardLink.story.tsx
├── CustomCard.story.tsx
├── CustomCardWithDrag.story.tsx
├── CustomLaneFooter.story.tsx
├── CustomLaneHeader.story.tsx
├── CustomNewCardForm.story.tsx
├── CustomNewLaneForm.story.tsx
├── CustomNewLaneSection.story.tsx
├── Deprecations.story.tsx
├── DragDrop.story.tsx
├── EditableBoard.story.tsx
├── I18n.story.tsx
├── MultipleBoards.story.tsx
├── PaginationAndEvents.story.tsx
├── Realtime.story.tsx
├── RestrictedLanes.story.tsx
├── Styling.story.tsx
├── __snapshots__
│ ├── AdvancedFeatures.story.tsx.snap
│ ├── BasicFunctions.story.tsx.snap
│ ├── CustomAddCardLink.story.tsx.snap
│ ├── CustomCard.story.tsx.snap
│ ├── CustomCardWithDrag.story.tsx.snap
│ ├── CustomLaneFooter.story.tsx.snap
│ ├── CustomLaneHeader.story.tsx.snap
│ ├── CustomNewCardForm.story.tsx.snap
│ ├── CustomNewLaneForm.story.tsx.snap
│ ├── CustomNewLaneSection.story.tsx.snap
│ ├── Deprecations.story.tsx.snap
│ ├── DragDrop.story.tsx.snap
│ ├── EditableBoard.story.tsx.snap
│ ├── I18n.story.tsx.snap
│ ├── MultipleBoards.story.tsx.snap
│ ├── PaginationAndEvents.story.tsx.snap
│ ├── Realtime.story.tsx.snap
│ ├── RestrictedLanes.story.tsx.snap
│ └── Styling.story.tsx.snap
├── board.css
├── data
│ ├── base.json
│ ├── board_with_custom_width.json
│ ├── collapsible.json
│ ├── data-sort.json
│ ├── drag-drop.json
│ └── other-board.json
├── drag.css
└── helpers
│ ├── debug.js
│ └── i18n.js
└── tsconfig.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "KaiSpencer",
10 | "name": "Kai Spencer",
11 | "avatar_url": "https://avatars.githubusercontent.com/u/51139521?v=4",
12 | "profile": "https://github.com/KaiSpencer",
13 | "contributions": [
14 | "code",
15 | "doc",
16 | "maintenance"
17 | ]
18 | }
19 | ],
20 | "contributorsPerLine": 7,
21 | "projectName": "react-trello-ts",
22 | "projectOwner": "KaiSpencer",
23 | "repoType": "github",
24 | "repoHost": "https://github.com",
25 | "skipCi": true
26 | }
27 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch"
10 | }
11 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [KaiSpencer]
4 |
--------------------------------------------------------------------------------
/.github/canary-version.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { exec } from "child_process";
4 | import fs from "fs";
5 |
6 | try {
7 | exec("git rev-parse --short HEAD", (err, stdout) => {
8 | if (err) {
9 | console.log(err);
10 | process.exit(1);
11 | }
12 | const commitHash = stdout.trim();
13 |
14 | const pkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));
15 | const oldVersion = pkg.version;
16 | const [major, minor, patch] = oldVersion.split(".").map(Number);
17 | const newVersion = `${major}.${minor}.${patch + 1}-canary.${commitHash}`;
18 |
19 | pkg.version = newVersion;
20 |
21 | const content = `${JSON.stringify(pkg, null, "\t")}\n`;
22 | const newContent = content
23 | .replace(
24 | new RegExp(`"@uploadthing/\\*": "${oldVersion}"`, "g"),
25 | `"@uploadthing/*": "${newVersion}"`,
26 | )
27 | .replace(
28 | new RegExp(`"uploadthing": "${oldVersion}"`, "g"),
29 | `"uploadthing": "${newVersion}"`,
30 | );
31 |
32 | fs.writeFileSync("package.json", newContent);
33 | });
34 | } catch (error) {
35 | console.error(error);
36 | process.exit(1);
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: Checks
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize]
6 |
7 | jobs:
8 | lint:
9 | name: Node ${{ matrix.node-version }}
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | node-version: [18, 20, 22]
14 | timeout-minutes: 15
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 |
21 | - name: Install Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 |
26 | - uses: pnpm/action-setup@v2
27 | name: Install pnpm
28 | id: pnpm-install
29 | with:
30 | version: 10.6.0
31 | run_install: false
32 |
33 | - name: Get pnpm store directory
34 | id: pnpm-cache
35 | shell: bash
36 | run: |
37 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
38 |
39 | - uses: actions/cache@v3
40 | name: Setup pnpm cache
41 | with:
42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
44 | restore-keys: |
45 | ${{ runner.os }}-pnpm-store-
46 |
47 | - name: Install dependencies
48 | run: pnpm install
49 |
50 | - name: Playwright Browsers
51 | run: pnpm exec playwright install chromium
52 |
53 | - name: Lint
54 | run: pnpm lint
55 |
56 | - name: Build
57 | run: pnpm build
58 |
59 | - name: Check publish configuration
60 | run: pnpm publint --strict
61 |
62 | - name: Check library type configuration
63 | run: pnpm attw --pack .
64 |
65 | - name: Unit Test
66 | run: pnpm test
67 |
--------------------------------------------------------------------------------
/.github/workflows/release-canary.yml:
--------------------------------------------------------------------------------
1 | name: Release - Canary
2 |
3 | on:
4 | pull_request:
5 | types: [labeled]
6 | branches:
7 | - main
8 | jobs:
9 | release:
10 | if: contains(github.event.pull_request.labels.*.name, 'release canary')
11 | name: Build & Publish a canary release
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v3
17 |
18 | - name: Install Node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 18
22 |
23 | - uses: pnpm/action-setup@v2
24 | name: Install pnpm
25 | id: pnpm-install
26 | with:
27 | version: 8
28 | run_install: false
29 |
30 | - name: Get pnpm store directory
31 | id: pnpm-cache
32 | run: |
33 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
34 |
35 | - name: Setup pnpm cache
36 | uses: actions/cache@v3
37 | with:
38 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
39 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
40 | restore-keys: |
41 | ${{ runner.os }}-pnpm-store-
42 |
43 | - name: Install dependencies
44 | run: pnpm install
45 |
46 | - name: Check packages for common errors
47 | run: pnpm build
48 |
49 | - name: Bump version to canary
50 | run: node .github/canary-version.mjs
51 |
52 | - name: Authenticate to npm and publish
53 | run: |
54 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
55 | pnpm publish --access public --tag canary --no-git-checks
56 |
57 | - name: Create a new comment notifying of the new canary version
58 | uses: actions/github-script@v6
59 | with:
60 | github-token: ${{ secrets.GITHUB_TOKEN }}
61 | script: |
62 | // Get package version
63 | const fs = require("fs");
64 | let text = 'A new canary is available for testing. You can install this latest build in your project with:\n\n```sh\n'
65 |
66 | const packageJson = JSON.parse(fs.readFileSync("package.json"));
67 | const version = packageJson.version;
68 | text += `pnpm add react-trello-ts@${version}`
69 | text += '\n```\n\n'
70 | text += `Current bundle size (minified + gzipped): \n\n`
71 | text += `New bundle size (minified + gzipped): \n\n`
72 |
73 | // Create a comment on the PR with the new canary version
74 | github.rest.issues.createComment({
75 | owner: context.repo.owner,
76 | repo: context.repo.repo,
77 | issue_number: context.payload.pull_request.number,
78 | body: text,
79 | })
80 |
81 | // Remove the label
82 | github.rest.issues.removeLabel({
83 | owner: context.repo.owner,
84 | repo: context.repo.repo,
85 | issue_number: context.payload.pull_request.number,
86 | name: 'release canary',
87 | });
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 |
18 | - name: Install Node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 18
22 |
23 | - uses: pnpm/action-setup@v2
24 | name: Install pnpm
25 | id: pnpm-install
26 | with:
27 | version: 8
28 | run_install: false
29 |
30 | - name: Get pnpm store directory
31 | id: pnpm-cache
32 | shell: bash
33 | run: |
34 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
35 |
36 | - uses: actions/cache@v3
37 | name: Setup pnpm cache
38 | with:
39 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
41 | restore-keys: |
42 | ${{ runner.os }}-pnpm-store-
43 |
44 | - name: Install dependencies
45 | run: pnpm install
46 |
47 | - name: Create Release Pull Request or Publish to npm
48 | id: changesets
49 | uses: changesets/action@v1
50 | with:
51 | publish: pnpm run release
52 | env:
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | .idea
5 | npm-debug.log
6 | dist/
7 | .history/
8 | storybook-build
9 | .npmrc
10 | coverage
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | src
4 | test
5 | examples
6 | coverage
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v18
2 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from "@storybook/react-vite";
2 |
3 | export default ({
4 | addons: ["@storybook/addon-essentials", "@chromatic-com/storybook"],
5 | framework: {
6 | name: "@storybook/react-vite",
7 | options: {},
8 | },
9 | stories: ["../stories/**/*.story.@(js|tsx|mdx)"],
10 | core: {},
11 | docs: {},
12 | typescript: {
13 | reactDocgen: "react-docgen-typescript",
14 | },
15 | } satisfies StorybookConfig);
16 |
--------------------------------------------------------------------------------
/.storybook/manager-head.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaiSpencer/react-trello-ts/304d413fec4b327b68e4c0c89ccbd985fcf76971/.storybook/manager-head.html
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | tags: [],
3 | };
4 |
--------------------------------------------------------------------------------
/.storybook/test-runner.ts:
--------------------------------------------------------------------------------
1 | import type { TestRunnerConfig } from "@storybook/test-runner";
2 |
3 | const config: TestRunnerConfig = {
4 | async postVisit(page, context) {
5 | // the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root
6 | const elementHandler = await page.$("#storybook-root");
7 | const innerHTML = await elementHandler?.innerHTML();
8 | if (!innerHTML) {
9 | throw new Error("No innerHTML found");
10 | }
11 | expect(innerHTML).toMatchSnapshot();
12 | },
13 | };
14 |
15 | export default config;
16 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "editor.defaultFormatter": "biomejs.biome",
4 |
5 | "editor.formatOnSave": true,
6 | "prettier.enable": false,
7 | "[typescriptreact]": {
8 | "editor.defaultFormatter": "biomejs.biome"
9 | },
10 | "[typescript]": {
11 | "editor.defaultFormatter": "biomejs.biome"
12 | },
13 | "[javascript]": {
14 | "editor.defaultFormatter": "biomejs.biome"
15 | },
16 | "[json]": {
17 | "editor.defaultFormatter": "biomejs.biome"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # react-trello-ts
2 |
3 | ## 2.1.2
4 |
5 | ### Patch Changes
6 |
7 | - 7bd0dc4: Update dependencies
8 |
9 | ## 2.1.1
10 |
11 | ### Patch Changes
12 |
13 | - fcffa93: Update dependencies
14 |
15 | ## 2.1.0
16 |
17 | ### Minor Changes
18 |
19 | - 4320234: repo rework, no expected functional change.
20 |
21 | ## 2.0.16
22 |
23 | ### Patch Changes
24 |
25 | - dd24717: bug/ onCardUpdate should fire - #26
26 |
27 | ## 2.0.15
28 |
29 | ### Patch Changes
30 |
31 | - c90dd1d: Unused style prop in Lane.tsx
32 |
33 | ## 2.0.14
34 |
35 | ### Patch Changes
36 |
37 | - b38da98: Add changesets
38 |
39 | Linting fix to prefer template strings over concat
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 RC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Trello TS
2 |
3 |
4 | [](https://badge.fury.io/js/react-trello-ts)
5 | [](https://npmjs.com/react-trello-ts)
6 |
7 | [](#contributors-)
8 |
9 |
10 |
11 | Typescript fork of [rcdexta/react-trello](https://github.com/rcdexta/react-trello)
12 |
13 | Pluggable components to add a Trello (like) kanban board to your application
14 |
15 | > This library is not affiliated, associated, authorized, endorsed by or in any way officially connected to Trello, Inc. `Trello` is a registered trademark of Atlassian, Inc.
16 |
17 | ## react-trello compatability
18 |
19 | For a direct typescript implementation of the original react-trello, see the `v1` branch.
20 |
21 | No new features will be added to this branch, only bug fixes and commits inline with `react-trello`
22 |
23 | For a react-trello-ts latest features see the main branch.
24 |
25 | ### Releases
26 |
27 | The `v1` branch will be released as 1.x.x
28 |
29 | The main branch will be released as 2.x.x
30 |
31 | ## Getting Started
32 |
33 | Install using npm or yarn
34 |
35 | ```bash
36 | $ npm install --save react-trello-ts
37 | ```
38 |
39 | or
40 |
41 | ```bash
42 | $ yarn add react-trello-ts
43 | ```
44 |
45 | ## Documentation
46 |
47 | See the LINK_TO_DOCS_SITE for more information.
48 |
49 | Whilst the docs site is under development (see issue [#6](https://github.com/KaiSpencer/react-trello-ts/issues/6)), you can find the legacy docs for `v1` in the `react-trello-README.md` file.
50 |
51 | ## Contributors ✨
52 |
53 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
54 |
55 |
56 |
57 |
58 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
70 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | # Upgrade instictions
2 |
3 | ## Upgrade from 2.1 to 2.2
4 |
5 | ### Texts
6 |
7 | Boths text properties are removed: `addLaneTitle` and `addCardLink`.
8 | Use translation function `t` to full control texts:
9 |
10 | | Legacy property | Key name in components object|
11 | | ------------------- | ---------------------------- |
12 | | addLaneTitle | "Add another lane" |
13 | | addCardLink | "Click to add card" |
14 |
15 | [Complete list of available translation keys](src/locales/en/translation.json)
16 |
17 | #### Migration example
18 |
19 | Instead of
20 |
21 | ```javascript
22 |
26 | ```
27 |
28 | Use
29 |
30 | ```javascript
31 |
32 | import { createTranslate } from 'react-trello'
33 |
34 | const TEXTS = {
35 | "Add another lane": "NEW LANE",
36 | "Click to add card": "Click to add card",
37 | "Delete lane": "Delete lane",
38 | "Lane actions": "Lane actions",
39 | "button": {
40 | "Add lane": "Add lane",
41 | "Add card": "ADD CARD",
42 | "Cancel": "Cancel"
43 | },
44 | "placeholder": {
45 | "title": "title",
46 | "description": "description",
47 | "label": "label"
48 | }
49 | }
50 |
51 | ```
52 |
53 | ### Components customization
54 |
55 | These properties are removed: `addCardLink`, `customLaneHeader`, `newCardTemplate`, `newLaneTemplate`
56 | and `customCardLayout` with `children` element.
57 |
58 | You must use `components` property, that contains map of custom
59 | `React.Component`'s (not elements/templates)
60 |
61 | | Legacy property | Key name in components map|
62 | | ------------------- | ---------------------------- |
63 | | addCardLink | AddCardLink |
64 | | customLaneHeader | LaneHeader |
65 | | newCardTemplate | NewCardForm |
66 | | newLaneTemplate | NewLaneSection |
67 | | customCardLayout (children) | Card |
68 |
69 | Full list of available components keys -
70 | [src/components/index.js](src/components/index.js)
71 |
72 | #### Migration example
73 |
74 | Instead of
75 |
76 | ```javascript
77 | New Card}
79 | customLaneHeader={}
80 | newCardTemplate ={}
81 | newLaneTemplate ={}
82 | customCardLayout
83 | >
84 |
85 |
86 |
87 | ```
88 |
89 | Use
90 |
91 | ```javascript
92 | const components = {
93 | AddCardLink: () => ,
94 | LaneHeader: CustomLaneHeader,
95 | NewCardForm: NewCard,
96 | NewLaneSection: NewLane,
97 | Card: CustomCard
98 | };
99 |
100 | ```
101 |
102 | That's all. Have a nice day! )
103 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json",
3 | "linter": {
4 | "enabled": true,
5 | "rules": {
6 | "recommended": true,
7 | "suspicious": {
8 | "noExplicitAny": "off"
9 | },
10 | "a11y": { "useButtonType": "off" }
11 | },
12 |
13 | "ignore": [
14 | "coverage",
15 | "node_modules",
16 | "*.story.tsx",
17 | "dist",
18 | "src/helpers/autosize.ts"
19 | ]
20 | },
21 | "formatter": {
22 | "enabled": true,
23 | "formatWithErrors": true,
24 | "ignore": ["node_modules", "dist"]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-trello-ts",
3 | "version": "2.1.2",
4 | "description": "Pluggable typesafe components to add a trello like kanban board to your application",
5 | "main": "./dist/index.mjs",
6 | "type": "module",
7 | "exports": {
8 | ".": {
9 | "import": "./dist/index.mjs",
10 | "require": "./dist/index.cjs"
11 | }
12 | },
13 | "types": "./dist/index.d.ts",
14 | "files": [
15 | "dist",
16 | "README"
17 | ],
18 | "engines": {
19 | "node": ">=18"
20 | },
21 | "scripts": {
22 | "prepublish": "pnpm build",
23 | "storybook": "storybook dev -p 6006",
24 | "storybook:no-open": "pnpm storybook --no-open",
25 | "test": "start-server-and-test storybook:no-open http://localhost:6006 test-storybook",
26 | "test-storybook": "test-storybook",
27 | "build": "unbuild",
28 | "docs": "storybook build -o docs",
29 | "build-storybook": "storybook build -o storybook-build",
30 | "lint": "pnpm biome check .",
31 | "release": "pnpm build && pnpm changeset publish",
32 | "changeset": "changeset"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "https://github.com/KaiSpencer/react-trello-ts"
37 | },
38 | "keywords": [
39 | "react",
40 | "trello",
41 | "board",
42 | "typescript"
43 | ],
44 | "author": "Kai Spencer",
45 | "license": "MIT",
46 | "bugs": {
47 | "url": "https://github.com/KaiSpencer/react-trello-ts/issues"
48 | },
49 | "homepage": "https://github.com/KaiSpencer/react-trello-ts",
50 | "dependencies": {
51 | "react-popopo": "^2.1.9",
52 | "react-trello-ts-smooth-dnd": "^0.0.3",
53 | "uuid": "^11.1.0",
54 | "zustand": "^5.0.3"
55 | },
56 | "devDependencies": {
57 | "@arethetypeswrong/cli": "^0.15.3",
58 | "@biomejs/biome": "1.4.1",
59 | "@changesets/cli": "^2.27.5",
60 | "@chromatic-com/storybook": "^1.5.0",
61 | "@microsoft/api-extractor": "^7.47.0",
62 | "@storybook/addon-essentials": "^8.6.4",
63 | "@storybook/cli": "^8.6.4",
64 | "@storybook/core-events": "^8.6.4",
65 | "@storybook/react": "^8.6.4",
66 | "@storybook/react-vite": "^8.6.4",
67 | "@storybook/test-runner": "^0.22.0",
68 | "@types/node": "^18.19.39",
69 | "@types/react": "^16.14.60",
70 | "@types/react-dom": "^16.9.24",
71 | "@types/styled-components": "^5.1.34",
72 | "@types/uuid": "^10",
73 | "i18next": "^17.3.1",
74 | "playwright": "^1.51.0",
75 | "publint": "^0.3.8",
76 | "react": "^18.3.1",
77 | "react-dom": "^18.3.1",
78 | "react-i18next": "^10.13.2",
79 | "start-server-and-test": "^2.0.4",
80 | "storybook": "^8.6.4",
81 | "styled-components": "^5.3.11",
82 | "typescript": "^5.8.2",
83 | "unbuild": "3.5.0"
84 | },
85 | "peerDependencies": {
86 | "react": ">=16.8.0 <=18",
87 | "react-dom": ">=16.8.0 <=18",
88 | "styled-components": ">= 4.0.3"
89 | },
90 | "packageManager": "pnpm@10.6.0",
91 | "pnpm": {
92 | "onlyBuiltDependencies": [
93 | "@swc/core",
94 | "esbuild"
95 | ]
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/react-trello.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaiSpencer/react-trello-ts/304d413fec4b327b68e4c0c89ccbd985fcf76971/react-trello.gif
--------------------------------------------------------------------------------
/src/components/AddCardLink.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes, PropsWithChildren } from "react";
2 | import { createTranslate } from "..";
3 | import { AddCardLink as _AddCardLink } from "../styles/Base";
4 |
5 | /**
6 | * AddCardLink component type
7 | *
8 | * Pass in a type to the optional generic to add custom properties
9 | *
10 | * @example
11 | *
12 | * type Props = {
13 | * customProperty: string;
14 | * }
15 | *
16 | * const CustomAddCardLink: AddCardLinkComponent = ({ customProperty, ...props }) => {
17 | * return (
18 | * ...
19 | * {customProperty}
20 | * ...
21 | * )
22 | * }
23 | */
24 | export type AddCardLinkComponent =
25 | FC>;
26 |
27 | interface AddCardLinkProps
28 | extends HTMLAttributes {
29 | t: typeof createTranslate;
30 | }
31 | export const AddCardLink: FC> = ({
32 | onClick,
33 | t,
34 | }) => <_AddCardLink onClick={onClick}>{t("Click to add card")};
35 |
--------------------------------------------------------------------------------
/src/components/Card.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, FC, PropsWithChildren } from "react";
2 | import { StyledComponent } from "styled-components";
3 | import { createTranslate } from "..";
4 | import {
5 | CardHeader,
6 | CardRightContent,
7 | CardTitle,
8 | Detail,
9 | Footer,
10 | MovableCardWrapper,
11 | } from "../styles/Base";
12 | import { Card as ICard } from "../types/Board";
13 | import { InlineInput } from "../widgets";
14 | import { DeleteButton } from "../widgets";
15 | import { Tag, TagProps } from "./Card/Tag";
16 |
17 | /**
18 | * Card component type
19 | *
20 | * Pass in a type to the optional generic to add custom properties to the card
21 | *
22 | * @example
23 | *
24 | * type CustomCardProps = {
25 | * dueOn: string;
26 | * }
27 | *
28 | * const CustomCard: CardComponent = ({ dueOn, ...props }) => {
29 | * return (
30 | *
31 | * {dueOn}
32 | *
33 | * )
34 | * }
35 | */
36 | export type CardComponent = FC<
37 | PropsWithChildren
38 | >;
39 |
40 | export type CardProps = {
41 | showDeleteButton?: boolean;
42 | onDelete?: () => void;
43 | onClick?: (e) => void;
44 | onChange?: (card: ICard) => void;
45 | style?: CSSProperties;
46 | tagStyle?: CSSProperties;
47 | className?: string;
48 | id: string;
49 | index: number;
50 | title?: string;
51 | label?: string;
52 | description?: string;
53 | tags?: TagProps[];
54 | cardDraggable?: boolean;
55 | editable?: boolean;
56 | metadata?: Record;
57 | t: typeof createTranslate;
58 | };
59 |
60 | export const Card: CardComponent = ({
61 | onDelete,
62 | onChange,
63 | id,
64 | onClick,
65 | style,
66 | className,
67 | description,
68 | label,
69 | t,
70 | tags,
71 | title,
72 | cardDraggable,
73 | editable,
74 | showDeleteButton,
75 | tagStyle,
76 | }) => {
77 | const _onDelete = (
78 | e:
79 | | React.MouseEvent
80 | | React.MouseEvent>,
81 | ) => {
82 | onDelete();
83 | e.stopPropagation();
84 | };
85 | const updateCard = (card: Partial) => {
86 | onChange({ ...card, id });
87 | };
88 |
89 | return (
90 |
96 |
97 |
98 | {editable ? (
99 | updateCard({ title: value })}
105 | />
106 | ) : (
107 | title
108 | )}
109 |
110 |
111 | {editable ? (
112 | updateCard({ label: value })}
118 | />
119 | ) : (
120 | label
121 | )}
122 |
123 | {showDeleteButton && }
124 |
125 |
126 | {editable ? (
127 |
133 | updateCard({ description: value })
134 | }
135 | />
136 | ) : (
137 | description
138 | )}
139 |
140 | {tags && tags.length > 0 && (
141 |
146 | )}
147 |
148 | );
149 | };
150 |
--------------------------------------------------------------------------------
/src/components/Card/Tag.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, FC, PropsWithChildren } from "react";
2 | import { TagSpan } from "../../styles/Base";
3 |
4 | export interface TagProps {
5 | title: string;
6 | color?: string;
7 | bgcolor?: string;
8 | tagStyle?: CSSProperties;
9 | }
10 | export const Tag: FC> = ({
11 | title,
12 | color,
13 | bgcolor,
14 | tagStyle,
15 | ...otherProps
16 | }) => {
17 | const style = {
18 | color: color || "white",
19 | backgroundColor: bgcolor || "orange",
20 | ...tagStyle,
21 | };
22 | return (
23 |
24 | {title}
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/Lane/LaneFooter.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes, PropsWithChildren } from "react";
2 |
3 | import { LaneFooter as _LaneFooter } from "../../styles/Base";
4 | import { CollapseBtn, ExpandBtn } from "../../styles/Elements";
5 |
6 | export type LaneFooterComponent = FC>;
7 |
8 | interface LaneFooterProps extends HTMLAttributes {
9 | collapsed?: boolean;
10 | }
11 | export const LaneFooter: LaneFooterComponent = ({
12 | onClick,
13 | onKeyDown,
14 | collapsed,
15 | }) => (
16 | <_LaneFooter onClick={onClick} onKeyDown={onKeyDown}>
17 | {collapsed ? : }
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/components/Lane/LaneHeader.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | CSSProperties,
3 | FC,
4 | HTMLAttributes,
5 | PropsWithChildren,
6 | } from "react";
7 | import createTranslate from "../../helpers/createTranslate";
8 | import {
9 | LaneHeader as _LaneHeader,
10 | RightContent,
11 | Title,
12 | } from "../../styles/Base";
13 | import { InlineInput } from "../../widgets/InlineInput";
14 | import { LaneMenu } from "./LaneHeader/LaneMenu";
15 |
16 | export type LaneHeaderProps = _LaneHeaderProps & {
17 | [key: string]: any;
18 | };
19 | interface _LaneHeaderProps extends HTMLAttributes {
20 | updateTitle?: (title: string) => void;
21 | canAddLanes?: boolean;
22 | onDelete?: () => void;
23 | editLaneTitle?: boolean;
24 | label?: string;
25 | titleStyle?: CSSProperties;
26 | labelStyle?: CSSProperties;
27 | laneDraggable?: boolean;
28 | t: typeof createTranslate;
29 | }
30 | export const LaneHeader: FC> = ({
31 | updateTitle = () => {},
32 | canAddLanes = false,
33 | onDelete,
34 | onDoubleClick,
35 | editLaneTitle,
36 | label,
37 | title,
38 | titleStyle,
39 | labelStyle,
40 | t,
41 | laneDraggable,
42 | }) => {
43 | return (
44 | <_LaneHeader onDoubleClick={onDoubleClick} editLaneTitle={editLaneTitle}>
45 |
46 | {editLaneTitle ? (
47 |
54 | ) : (
55 | title
56 | )}
57 |
58 | {label && (
59 |
60 | {label}
61 |
62 | )}
63 | {canAddLanes && }
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/components/Lane/LaneHeader/LaneMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, PropsWithChildren } from "react";
2 | import { Popover } from "react-popopo";
3 |
4 | import createTranslate from "../../../helpers/createTranslate";
5 | import {
6 | CustomPopoverContainer,
7 | CustomPopoverContent,
8 | } from "../../../styles/Base";
9 | import {
10 | DeleteWrapper,
11 | GenDelButton,
12 | LaneMenuContent,
13 | LaneMenuHeader,
14 | LaneMenuItem,
15 | LaneMenuTitle,
16 | MenuButton,
17 | } from "../../../styles/Elements";
18 |
19 | interface LaneMenuProps {
20 | t: typeof createTranslate;
21 | onDelete: () => void;
22 | }
23 | export const LaneMenu: FC> = ({
24 | t,
25 | onDelete,
26 | }) => (
27 | ⋮}
32 | >
33 |
34 | {t("Lane actions")}
35 |
36 | ✖
37 |
38 |
39 |
40 | {t("Delete lane")}
41 |
42 |
43 | );
44 |
--------------------------------------------------------------------------------
/src/components/Loader.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { LoaderDiv, LoadingBar } from "../styles/Loader";
3 |
4 | export const Loader = () => (
5 |
6 |
7 |
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/NewCardForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, FC, PropsWithChildren, useState } from "react";
2 | import createTranslate from "../helpers/createTranslate";
3 | import {
4 | CardForm,
5 | CardHeader,
6 | CardRightContent,
7 | CardTitle,
8 | CardWrapper,
9 | Detail,
10 | } from "../styles/Base";
11 | import { AddButton, CancelButton } from "../styles/Elements";
12 | import { EditableLabel } from "../widgets/EditableLabel";
13 |
14 | export interface FormState {
15 | title: string;
16 | description: string;
17 | label: string;
18 | laneId: string;
19 | }
20 |
21 | export interface NewCardFormProps {
22 | laneId: string;
23 | onCancel: () => void;
24 | onAdd: (formState: FormState) => void;
25 | t: typeof createTranslate;
26 | }
27 | export const NewCardForm: FC = ({
28 | laneId,
29 | onCancel,
30 | onAdd,
31 | t,
32 | }) => {
33 | const [formState, setFormState] = useState();
34 |
35 | const handleAdd = () => {
36 | onAdd({ ...formState, laneId });
37 | };
38 |
39 | const updateField = (
40 | field: K,
41 | value: FormState[K],
42 | ) => {
43 | setFormState({ ...formState, [field]: value });
44 | };
45 |
46 | return (
47 |
48 |
49 |
50 |
51 | updateField("title", val)}
54 | autoFocus={true}
55 | />
56 |
57 |
58 | updateField("label", val)}
61 | />
62 |
63 |
64 |
65 |
68 | updateField("description", val)
69 | }
70 | />
71 |
72 |
73 | {t("button.Add card")}
74 | {t("button.Cancel")}
75 |
76 | );
77 | };
78 |
--------------------------------------------------------------------------------
/src/components/NewLaneForm.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FC,
3 | HTMLAttributes,
4 | PropsWithChildren,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import { ThemedStyledFunction } from "styled-components";
9 | import { v1 } from "uuid";
10 | import createTranslate from "../helpers/createTranslate";
11 | import { LaneTitle, NewLaneButtons, Section } from "../styles/Base";
12 | import { AddButton, CancelButton } from "../styles/Elements";
13 | import { NewLaneTitleEditor } from "../widgets/NewLaneTitleEditor";
14 |
15 | interface NewLaneFormProps
16 | extends HTMLAttributes<
17 | ThemedStyledFunction<"section", object, object, never>
18 | > {
19 | onCancel: () => void;
20 | onAdd: ({ id, title }: { id: string; title: string }) => void;
21 | t: typeof createTranslate;
22 | }
23 | export const NewLaneForm: FC> = ({
24 | onAdd,
25 | onCancel,
26 | t,
27 | }) => {
28 | const titleRef = useRef();
29 | const handleSubmit = () => {
30 | onAdd({
31 | id: v1(),
32 | title: titleRef.current.value,
33 | });
34 | };
35 |
36 | return (
37 |
38 |
39 |
48 |
49 |
50 | {t("button.Add lane")}
51 | {t("button.Cancel")}
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/NewLaneSection.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, PropsWithChildren } from "react";
2 | import { createTranslate } from "..";
3 | import { NewLaneSection as _NewLaneSection } from "../styles/Base";
4 | import { AddLaneLink } from "../styles/Elements";
5 |
6 | export const NewLaneSection: FC<
7 | PropsWithChildren<{ t: typeof createTranslate; onClick: () => void }>
8 | > = ({ t, onClick }) => (
9 | <_NewLaneSection>
10 | {t("Add another lane")}
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BoardWrapper,
3 | GlobalStyle,
4 | ScrollableLane,
5 | Section,
6 | } from "../styles/Base";
7 | import { AddCardLink } from "./AddCardLink";
8 | import { Card } from "./Card";
9 | import { LaneFooter } from "./Lane/LaneFooter";
10 | import { LaneHeader } from "./Lane/LaneHeader";
11 | import { Loader } from "./Loader";
12 | import { NewCardForm } from "./NewCardForm";
13 | import { NewLaneForm } from "./NewLaneForm";
14 | import { NewLaneSection } from "./NewLaneSection";
15 |
16 | export {
17 | GlobalStyle,
18 | BoardWrapper,
19 | Loader,
20 | ScrollableLane,
21 | LaneHeader,
22 | LaneFooter,
23 | Section,
24 | NewLaneForm,
25 | NewLaneSection,
26 | NewCardForm,
27 | Card,
28 | AddCardLink,
29 | };
30 |
--------------------------------------------------------------------------------
/src/controllers/Board.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, PropsWithChildren, createContext } from "react";
2 | import { v1 } from "uuid";
3 | import * as DefaultComponets from "../components";
4 | import { store } from "../store/store";
5 | import { BoardData } from "../types/Board";
6 | import { BoardContainer } from "./BoardContainer";
7 |
8 | export const Board: FC<
9 | PropsWithChildren<{
10 | id?: string;
11 | className?: string;
12 | components: typeof DefaultComponets;
13 | data: BoardData;
14 | t?: any;
15 | }>
16 | > = ({ data, children, className, components, id, t, ...rest }) => {
17 | const allClassNames = className
18 | ? `react-trello-board ${className}`
19 | : "react-trello-board";
20 |
21 | return (
22 |
23 | {/*
24 | // @ts-ignore */}
25 |
26 |
34 |
35 | );
36 | };
37 |
38 | export const BoardContext = createContext(store);
39 |
--------------------------------------------------------------------------------
/src/controllers/BoardContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | CSSProperties,
3 | FC,
4 | PropsWithChildren,
5 | useEffect,
6 | useState,
7 | } from "react";
8 | import { PopoverWrapper } from "react-popopo";
9 | import Container from "../dnd/Container";
10 | import { Draggable } from "../dnd/Draggable";
11 | import { Lane } from "./Lane";
12 |
13 | import { createTranslate } from "..";
14 | import { FormState as NewCardFormState } from "../components/NewCardForm";
15 | import { useBoard } from "../store/useBoard";
16 | import { BoardData, Card, Lane as ILane } from "../types/Board";
17 | import { EventBusHandle } from "../types/EventBus";
18 | import * as DefaultComponents from "./../components";
19 | export interface BoardContainerProps {
20 | id?: string;
21 | components?: Partial;
22 | className?: string;
23 | data: BoardData;
24 | reducerData?: BoardData;
25 | onDataChange?: (reducerData: BoardData) => void;
26 | eventBusHandle?: (handle: EventBusHandle) => void;
27 | onLaneScroll?: (requestedPage: any, laneId: any) => Promise;
28 | onCardClick?: (
29 | cardId: Card["id"],
30 | metadata: { id: string },
31 | card: Card,
32 | ) => void;
33 | onBeforeCardDelete?: () => void;
34 | onCardDelete?: (cardId: string, laneId: string) => void;
35 | onCardAdd?: (card: Card, laneId: string) => void;
36 | onCardUpdate?: (cardId: string, data: Card) => void;
37 | onLaneAdd?: (laneAddParams: NewCardFormState) => void;
38 | onLaneDelete?: () => void;
39 | onLaneClick?: (laneId: string) => void;
40 | onLaneUpdate?: (laneId: string, data: ILane) => void;
41 | laneSortFunction?: (cardA: Card, cardB: Card) => number;
42 | draggable?: boolean;
43 | collapsibleLanes?: boolean;
44 | editable?: boolean;
45 | canAddLanes?: boolean;
46 | hideCardDeleteAction?: boolean;
47 | hideCardDeleteIcon?: boolean;
48 | handleDragStart?: (cardId: string, laneId: string) => void;
49 | handleDragEnd?: (
50 | cardId: Card["id"],
51 | sourceLandId: ILane["id"],
52 | targetLaneId: ILane["id"],
53 | position: number,
54 | card: Card,
55 | ) => void;
56 | handleLaneDragStart?: (payloadId: string) => void;
57 | handleLaneDragEnd?: (
58 | removedIndex: string,
59 | addedIndex: string,
60 | payload: ILane,
61 | ) => void;
62 | style?: CSSProperties;
63 | tagStyle?: CSSProperties;
64 | laneStyle?: CSSProperties;
65 | cardStyle?: CSSProperties;
66 | laneDraggable?: boolean;
67 | cardDraggable?: boolean;
68 | cardDragClass?: string;
69 | laneDragClass?: string;
70 | laneDropClass?: string;
71 | editLaneTitle?: boolean;
72 | onCardMoveAcrossLanes?: (
73 | fromLaneId: string,
74 | toLaneId: string,
75 | cardId: string,
76 | addedIndex: string,
77 | ) => void;
78 | t?: typeof createTranslate;
79 | }
80 |
81 | export const BoardContainer: FC> = ({
82 | components,
83 | handleDragStart = () => {},
84 | handleDragEnd = () => {},
85 | handleLaneDragStart = () => {},
86 | className,
87 | style,
88 | id,
89 | draggable = false,
90 | laneDraggable = true,
91 | data,
92 | cardDragClass = "react_trello_dragClass",
93 | collapsibleLanes = false,
94 | laneDragClass = "react_trello_dragLaneClass",
95 | laneDropClass = "",
96 | onDataChange = () => {},
97 | cardDraggable = true,
98 | onCardAdd,
99 | onCardUpdate = () => {},
100 | onCardClick,
101 | onBeforeCardDelete,
102 | handleLaneDragEnd = () => {},
103 | onCardDelete,
104 | cardStyle,
105 | hideCardDeleteIcon = false,
106 | onLaneScroll,
107 | onLaneClick,
108 | onLaneAdd = () => {},
109 | onLaneDelete = () => {},
110 | onLaneUpdate = () => {},
111 | editable = false,
112 | canAddLanes = false,
113 | eventBusHandle,
114 | tagStyle,
115 | editLaneTitle,
116 | laneStyle,
117 | laneSortFunction,
118 | onCardMoveAcrossLanes = () => {},
119 | t,
120 | ...otherProps
121 | }) => {
122 | const [addLaneMode, setAddLaneMode] = useState(false);
123 | const board = useBoard();
124 | // biome-ignore lint/correctness/useExhaustiveDependencies:
125 | useEffect(() => {
126 | board.initializeLanes(data.lanes);
127 | if (eventBusHandle) {
128 | wireEventBus();
129 | }
130 | return () => {
131 | board.refreshBoard([]);
132 | };
133 | }, []);
134 |
135 | // biome-ignore lint/correctness/useExhaustiveDependencies:
136 | useEffect(() => {
137 | if (JSON.stringify(board.data) !== JSON.stringify(data)) {
138 | onDataChange(data);
139 | board.initializeLanes(data.lanes);
140 | }
141 | }, [data]);
142 |
143 | const wireEventBus = () => {
144 | const eventBus: EventBusHandle = {
145 | publish: (event) => {
146 | switch (event.type) {
147 | case "ADD_CARD":
148 | return board.addCard(event.card, event.laneId, event.index);
149 | case "UPDATE_CARD":
150 | return board.updateCard(event.laneId, event.card);
151 | case "REMOVE_CARD":
152 | return board.removeCard(event.laneId, event.cardId);
153 | case "REFRESH_BOARD":
154 | return board.refreshBoard(event.data.lanes);
155 | case "MOVE_CARD":
156 | return board.moveCard(
157 | event.fromLaneId,
158 | event.toLaneId,
159 | event.cardId,
160 | event.index,
161 | );
162 | case "UPDATE_CARDS":
163 | return board.updateCards(event.laneId, event.cards);
164 | case "UPDATE_LANES":
165 | return board.updateLanes(event.lanes);
166 | case "UPDATE_LANE":
167 | return board.updateLane(event.lane);
168 | }
169 | },
170 | };
171 | eventBusHandle(eventBus);
172 | };
173 | const getLaneDetails = (index) => {
174 | return board.data.lanes[index];
175 | };
176 | const getCardDetails = (laneId, cardIndex) => {
177 | return board.data.lanes.find((lane) => lane.id === laneId).cards[cardIndex];
178 | };
179 | const groupName = `TrelloBoard${id}`;
180 | return (
181 |
182 |
183 | {
187 | handleLaneDragStart(payload.id);
188 | }}
189 | dragClass={laneDragClass}
190 | dropClass={laneDropClass}
191 | onDrop={({ removedIndex, addedIndex, payload }) => {
192 | if (removedIndex !== addedIndex) {
193 | board.moveLane(removedIndex, addedIndex);
194 | handleLaneDragEnd(removedIndex, addedIndex, payload);
195 | }
196 | }}
197 | lockAxis="x"
198 | getChildPayload={(index) => getLaneDetails(index)}
199 | groupName={groupName}
200 | >
201 | {board.data.lanes.map((lane, index) => {
202 | const { id, droppable, ...otherProps } = lane;
203 |
204 | const laneToRender = (
205 |
241 | );
242 | return draggable && laneDraggable ? (
243 | {laneToRender}
244 | ) : (
245 | laneToRender
246 | );
247 | })}
248 |
249 |
250 | {canAddLanes && (
251 |
252 | {editable && !addLaneMode ? (
253 | setAddLaneMode(true)}
256 | />
257 | ) : (
258 | addLaneMode && (
259 | setAddLaneMode(false)}
261 | onAdd={({ id, title }) => {
262 | setAddLaneMode(false);
263 | board.addLane({ id, title, cards: [] });
264 | onLaneAdd({
265 | laneId: id,
266 | title,
267 | description: "",
268 | label: "",
269 | });
270 | }}
271 | t={t}
272 | />
273 | )
274 | )}
275 |
276 | )}
277 |
278 | );
279 | };
280 |
--------------------------------------------------------------------------------
/src/controllers/Lane.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, FC, PropsWithChildren, useEffect } from "react";
2 | import { v1 } from "uuid";
3 |
4 | import { components, createTranslate } from "..";
5 | import Container from "../dnd/Container";
6 | import { Draggable } from "../dnd/Draggable";
7 | import { useBoard } from "../store/useBoard";
8 | import { Card, Lane as ILane } from "../types/Board";
9 |
10 | interface LaneProps {
11 | id: string;
12 | index?: number;
13 | boardId?: string;
14 | title?: string;
15 | label?: string;
16 | cards?: any[];
17 | style?: any;
18 | collapsibleLanes?: boolean;
19 | className?: string;
20 | titleStyle?: any;
21 | titleClassName?: string;
22 | labelStyle?: any;
23 | labelClassName?: string;
24 | cardStyle?: any;
25 | cardClassName?: string;
26 | currentPage?: number;
27 | draggable?: boolean;
28 | droppable?: boolean;
29 | editable?: boolean;
30 | canAddLanes?: boolean;
31 | laneSortFunction?: (cardA: Card, cardB: Card) => number;
32 | hideCardDeleteIcon?: boolean;
33 | cardDraggable?: boolean;
34 | cardDragClass?: string;
35 | cardDropClass?: string;
36 | tagStyle?: CSSProperties;
37 | components?: Partial;
38 | onLaneScroll?: (page: number, laneId: string) => Promise;
39 | onLaneAdd?: (params: any) => void;
40 | onLaneDelete?: (laneId: string) => void;
41 | onLaneUpdate?: (laneId: string, data: ILane) => void;
42 | onCardClick?: (cardId: string, metadata: { id: string }, card: Card) => void;
43 | onCardAdd?: (card: any, laneId: string) => void;
44 | onCardDelete?: (cardId: string, laneId: string) => void;
45 | onCardUpdate?: (laneId: string, card: any) => void;
46 | onBeforeCardDelete?: (callback: () => void) => void;
47 | onCardMoveAcrossLanes?: (
48 | fromLaneId: string,
49 | toLaneId: string,
50 | cardId: string,
51 | index: string,
52 | ) => void;
53 | onLaneClick?: (laneId: string) => void;
54 | handleDragStart?: (cardId: string, laneId: string) => void;
55 | handleDragEnd?: (
56 | cardId: string,
57 | payloadLaneId: string,
58 | laneId: string,
59 | addedIndex: number,
60 | newCard: Card,
61 | ) => void;
62 | getCardDetails?: (laneId: string, cardIndex: number) => any;
63 | t?: typeof createTranslate;
64 | }
65 | export const Lane: FC> = ({
66 | id,
67 | index,
68 | boardId,
69 | title,
70 | label,
71 | cards,
72 | style,
73 | collapsibleLanes,
74 | className,
75 | titleStyle,
76 | titleClassName,
77 | labelStyle,
78 | labelClassName,
79 | cardStyle,
80 | cardClassName,
81 | currentPage,
82 | draggable,
83 | droppable,
84 | editable,
85 | canAddLanes,
86 | laneSortFunction,
87 | hideCardDeleteIcon,
88 | cardDraggable,
89 | cardDragClass,
90 | cardDropClass,
91 | tagStyle,
92 | components,
93 | onLaneScroll,
94 | onLaneAdd,
95 | onLaneDelete,
96 | onLaneUpdate,
97 | onCardClick,
98 | onCardAdd,
99 | onCardDelete,
100 | onCardUpdate,
101 | onBeforeCardDelete,
102 | onCardMoveAcrossLanes,
103 | onLaneClick,
104 | handleDragStart,
105 | handleDragEnd,
106 | getCardDetails,
107 | t,
108 | children,
109 | ...otherProps
110 | }) => {
111 | const board = useBoard();
112 | const [loading, setLoading] = React.useState(false);
113 | const [currentPageState, setCurrentPageState] = React.useState(currentPage);
114 | const [collapsed, setCollapsed] = React.useState(false);
115 | const [addCardMode, setAddCardMode] = React.useState(false);
116 | const [isDraggingOver, setIsDraggingOver] = React.useState(false);
117 |
118 | // biome-ignore lint/correctness/useExhaustiveDependencies:
119 | useEffect(() => {
120 | setCurrentPageState(currentPage);
121 | }, [cards]);
122 |
123 | const sortCards = (cards: Card[], sortFunction: typeof laneSortFunction) => {
124 | if (!cards) {
125 | return [];
126 | }
127 | if (!sortFunction) {
128 | return cards;
129 | }
130 | return cards.concat().sort((card1, card2) => sortFunction(card1, card2));
131 | };
132 | const addNewCard = (params: {
133 | title?: string;
134 | laneId?: string;
135 | description?: string;
136 | label?: string;
137 | }) => {
138 | const laneId = params.laneId || id;
139 | const _id = v1();
140 | setAddCardMode(false);
141 | const card = { id: _id, ...params };
142 | board.addCard(card, laneId);
143 | onCardAdd?.(card, laneId);
144 | };
145 | const updateCard = (updatedCard: Card) => {
146 | board.updateCard(id, updatedCard);
147 | onCardUpdate?.(id, updatedCard);
148 | };
149 | const updateTitle = (value) => {
150 | board.updateLane({ id, title: value });
151 | onLaneUpdate?.(id, { id, title: value });
152 | };
153 | const laneDidMount = (node: HTMLDivElement) => {
154 | if (node) {
155 | node.addEventListener("scroll", handleScroll);
156 | }
157 | };
158 | const removeLane = () => {
159 | board.removeLane(id);
160 | onLaneDelete?.(id);
161 | };
162 | const handleScroll = (evt) => {
163 | const node = evt.target;
164 | const elemScrollPosition =
165 | node.scrollHeight - node.scrollTop - node.clientHeight;
166 | // In some browsers and/or screen sizes a decimal rest value between 0 and 1 exists, so it should be checked on < 1 instead of < 0
167 | if (elemScrollPosition < 1 && onLaneScroll && !loading) {
168 | setLoading(true);
169 | const nextPage = currentPageState + 1;
170 | onLaneScroll(nextPage, id).then((moreCards: Card[]) => {
171 | if ((moreCards || []).length > 0) {
172 | board.paginateLane(id, moreCards, nextPage);
173 | }
174 | setLoading(false);
175 | });
176 | }
177 | };
178 | const groupName = `TrelloBoard${boardId}Lane`;
179 | const handleCardClick = (e, card) => {
180 | onCardClick?.(card.id, card.metadata, card.laneId);
181 | e.stopPropagation();
182 | };
183 | const toggleLaneCollapsed = () => {
184 | collapsibleLanes && setCollapsed(!collapsed);
185 | };
186 | const removeCard = (cardId: Card["id"]) => {
187 | if (onBeforeCardDelete && typeof onBeforeCardDelete === "function") {
188 | onBeforeCardDelete(() => {
189 | board.removeCard(id, cardId);
190 | onCardDelete?.(cardId, id);
191 | });
192 | } else {
193 | board.removeCard(id, cardId);
194 | onCardDelete?.(cardId, id);
195 | }
196 | };
197 | const onDragStart = ({ payload }) => {
198 | handleDragStart?.(payload.id, id);
199 | };
200 | const onDragEnd = (
201 | laneId: string,
202 | result: { addedIndex: any; payload: any },
203 | ) => {
204 | const { addedIndex, payload } = result;
205 |
206 | if (isDraggingOver) {
207 | setIsDraggingOver(false);
208 | }
209 |
210 | if (addedIndex != null) {
211 | const newCard = { ...payload, laneId };
212 | handleDragEnd?.(payload.id, laneId, id, addedIndex, newCard);
213 |
214 | const response = handleDragEnd
215 | ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard)
216 | : true;
217 | if (response === undefined || !!response) {
218 | board.moveCard(payload.laneId, laneId, payload.id, addedIndex);
219 | onCardMoveAcrossLanes?.(payload.laneId, laneId, payload.id, addedIndex);
220 | }
221 | return response;
222 | }
223 | };
224 |
225 | const renderDragContainer = (isDraggingOver: boolean) => {
226 | const showableCards = collapsed ? [] : cards;
227 | const cardList = sortCards(showableCards, laneSortFunction).map(
228 | (card, idx) => {
229 | const onDeleteCard = () => removeCard(card.id);
230 | const cardToRender = (
231 | handleCardClick(e, card)}
238 | onChange={(updatedCard) => updateCard(updatedCard)}
239 | showDeleteButton={!hideCardDeleteIcon}
240 | tagStyle={tagStyle}
241 | cardDraggable={cardDraggable}
242 | editable={editable}
243 | title={card.title}
244 | description={card.description}
245 | label={card.label}
246 | metadata={card.metadata}
247 | id={card.id}
248 | t={t}
249 | {...card}
250 | />
251 | );
252 | return cardDraggable &&
253 | (!Object.prototype.hasOwnProperty.call(card, "draggable") ||
254 | card.draggable) ? (
255 | {cardToRender}
256 | ) : (
257 | {cardToRender}
258 | );
259 | },
260 | );
261 |
262 | return (
263 |
267 | {
274 | onDragEnd(id, e);
275 | }}
276 | onDragEnter={() => setIsDraggingOver(true)}
277 | onDragLeave={() => setIsDraggingOver(false)}
278 | shouldAcceptDrop={(sourceContainerOptions) =>
279 | droppable && sourceContainerOptions.groupName === groupName
280 | }
281 | getChildPayload={(index) => getCardDetails(id, index)}
282 | >
283 | {cardList}
284 |
285 | {editable && !addCardMode && (
286 | setAddCardMode(true)} t={t} />
287 | )}
288 | {addCardMode && (
289 | setAddCardMode(false)}
291 | t={t}
292 | laneId={id}
293 | onAdd={addNewCard}
294 | />
295 | )}
296 |
297 | );
298 | };
299 |
300 | const allClassNames = className
301 | ? `react-trello-lane ${className}`
302 | : "react-trello-lane";
303 | const showFooter = collapsibleLanes && cards.length > 0;
304 | return (
305 | onLaneClick?.(id)}
309 | draggable={false}
310 | className={allClassNames}
311 | style={style}
312 | >
313 |
325 | {renderDragContainer(isDraggingOver)}
326 | {loading && }
327 | {showFooter && (
328 |
332 | )}
333 |
334 | );
335 | };
336 |
--------------------------------------------------------------------------------
/src/dnd/Container.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactDOM from "react-dom";
3 | import container, { dropHandlers } from "react-trello-ts-smooth-dnd";
4 |
5 | container.dropHandler = dropHandlers.reactDropHandler().handler;
6 | container.wrapChild = (p: any) => p; // dont wrap children they will already be wrapped
7 |
8 | export interface ContainerProps {
9 | behaviour?: "move" | "copy" | "drag-zone";
10 | groupName?: string;
11 | orientation?: "horizontal" | "vertical";
12 | style?: object;
13 | dragHandleSelector?: string;
14 | className?: string;
15 | nonDragAreaSelector?: string;
16 | dragBeginDelay?: number;
17 | animationDuration?: number;
18 | autoScrollEnabled?: string;
19 | lockAxis?: string;
20 | dragClass?: string;
21 | dropClass?: string;
22 | onDragStart?: (params: any) => void;
23 | onDragEnd?: (
24 | laneId: string,
25 | result: { addedIndex: any; payload: any },
26 | ) => void;
27 | onDrop?: (params: any) => void;
28 | onDropReady?: (params: any) => void;
29 | getChildPayload?: (index: number) => any;
30 | shouldAnimateDrop?: (params: any) => boolean;
31 | shouldAcceptDrop?: (params: any) => boolean;
32 | onDragEnter?: (params: any) => void;
33 | onDragLeave?: (params: any) => void;
34 | render?: (params: any) => any;
35 | getGhostParent?: () => any;
36 | removeOnDropOut?: boolean;
37 | }
38 |
39 | class Container extends Component {
40 | containerDiv: typeof Container | Element | Text;
41 | prevContainer: typeof Container | Element | Text;
42 | container: any;
43 | constructor(props) {
44 | super(props);
45 | this.getContainerOptions = this.getContainerOptions.bind(this);
46 | this.setRef = this.setRef.bind(this);
47 | this.prevContainer = null;
48 | }
49 |
50 | componentDidMount() {
51 | this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this);
52 | this.prevContainer = this.containerDiv;
53 | this.container = container(this.containerDiv, this.getContainerOptions());
54 | }
55 |
56 | componentWillUnmount() {
57 | this.container.dispose();
58 | this.container = null;
59 | }
60 |
61 | componentDidUpdate() {
62 | this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this);
63 | if (this.containerDiv) {
64 | if (this.prevContainer && this.prevContainer !== this.containerDiv) {
65 | this.container.dispose();
66 | this.container = container(
67 | this.containerDiv,
68 | this.getContainerOptions(),
69 | );
70 | this.prevContainer = this.containerDiv;
71 | }
72 | }
73 | }
74 |
75 | render() {
76 | if (this.props.render) {
77 | return this.props.render(this.setRef);
78 | }
79 | return (
80 |
81 | {this.props.children}
82 |
83 | );
84 | }
85 |
86 | setRef(element) {
87 | this.containerDiv = element;
88 | }
89 |
90 | getContainerOptions() {
91 | const functionProps: Pick<
92 | ContainerProps,
93 | | "onDragEnd"
94 | | "onDragStart"
95 | | "onDrop"
96 | | "getChildPayload"
97 | | "shouldAnimateDrop"
98 | | "shouldAcceptDrop"
99 | | "onDragEnter"
100 | | "onDragLeave"
101 | | "render"
102 | | "onDropReady"
103 | | "getGhostParent"
104 | > = {};
105 |
106 | if (this.props.onDragStart) {
107 | functionProps.onDragStart = (...p) => this.props.onDragStart(...p);
108 | }
109 |
110 | if (this.props.onDragEnd) {
111 | functionProps.onDragEnd = (...p) => this.props.onDragEnd(...p);
112 | }
113 |
114 | if (this.props.onDrop) {
115 | functionProps.onDrop = (...p) => this.props.onDrop(...p);
116 | }
117 |
118 | if (this.props.getChildPayload) {
119 | functionProps.getChildPayload = (...p) =>
120 | this.props.getChildPayload(...p);
121 | }
122 |
123 | if (this.props.shouldAnimateDrop) {
124 | functionProps.shouldAnimateDrop = (...p) =>
125 | this.props.shouldAnimateDrop(...p);
126 | }
127 |
128 | if (this.props.shouldAcceptDrop) {
129 | functionProps.shouldAcceptDrop = (...p) =>
130 | this.props.shouldAcceptDrop(...p);
131 | }
132 |
133 | if (this.props.onDragEnter) {
134 | functionProps.onDragEnter = (...p) => this.props.onDragEnter(...p);
135 | }
136 |
137 | if (this.props.onDragLeave) {
138 | functionProps.onDragLeave = (...p) => this.props.onDragLeave(...p);
139 | }
140 |
141 | if (this.props.render) {
142 | functionProps.render = (...p) => this.props.render(...p);
143 | }
144 |
145 | if (this.props.onDropReady) {
146 | functionProps.onDropReady = (...p) => this.props.onDropReady(...p);
147 | }
148 |
149 | if (this.props.getGhostParent) {
150 | functionProps.getGhostParent = (...p) => this.props.getGhostParent(...p);
151 | }
152 |
153 | return Object.assign({}, this.props, functionProps);
154 | }
155 | }
156 |
157 | export default Container;
158 |
--------------------------------------------------------------------------------
/src/dnd/Draggable.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ReactElement } from "react";
2 | import { constants } from "react-trello-ts-smooth-dnd";
3 |
4 | const { wrapperClass } = constants;
5 |
6 | export class Draggable extends Component<{
7 | render?: () => ReactElement;
8 | className?: string;
9 | }> {
10 | render() {
11 | if (this.props.render) {
12 | return React.cloneElement(this.props.render(), {
13 | className: wrapperClass,
14 | });
15 | }
16 |
17 | const clsName = this.props.className ? `${this.props.className} ` : "";
18 | return (
19 |
20 | {this.props.children}
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/helpers/autosize.ts:
--------------------------------------------------------------------------------
1 | type AutosizeOptions = {};
2 |
3 | interface AutosizeMethods {
4 | destroy: () => void;
5 | update: () => void;
6 | }
7 |
8 | interface SetHeightOptions {
9 | restoreTextAlign: string | null;
10 | testForHeightReduction: boolean;
11 | }
12 |
13 | type ScrollTopCache = [HTMLElement, number][];
14 | type RestoreScrollTops = () => void;
15 |
16 | const assignedElements = new Map();
17 |
18 | function assign(ta: HTMLTextAreaElement, options?: AutosizeOptions): void {
19 | if (
20 | !ta ||
21 | !ta.nodeName ||
22 | ta.nodeName !== "TEXTAREA" ||
23 | assignedElements.has(ta)
24 | )
25 | return;
26 |
27 | let previousHeight: number | null = null;
28 |
29 | function cacheScrollTops(el: HTMLElement): RestoreScrollTops {
30 | const arr: ScrollTopCache = [];
31 |
32 | while (el && el.parentNode && el.parentNode instanceof Element) {
33 | if (el.parentNode.scrollTop) {
34 | arr.push([el.parentNode as HTMLElement, el.parentNode.scrollTop]);
35 | }
36 | el = el.parentNode as HTMLElement;
37 | }
38 |
39 | return () =>
40 | arr.forEach(([node, scrollTop]) => {
41 | node.style.scrollBehavior = "auto";
42 | node.scrollTop = scrollTop;
43 | node.style.scrollBehavior = null;
44 | });
45 | }
46 |
47 | const computed = window.getComputedStyle(ta);
48 |
49 | function setHeight({
50 | restoreTextAlign = null,
51 | testForHeightReduction = true,
52 | }: Partial = {}): void {
53 | const initialOverflowY = computed.overflowY;
54 |
55 | if (ta.scrollHeight === 0) {
56 | // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
57 | return;
58 | }
59 |
60 | // disallow vertical resizing
61 | if (computed.resize === "vertical") {
62 | ta.style.resize = "none";
63 | } else if (computed.resize === "both") {
64 | ta.style.resize = "horizontal";
65 | }
66 |
67 | let restoreScrollTops: RestoreScrollTops | undefined;
68 |
69 | // remove inline height style to accurately measure situations where the textarea should shrink
70 | // however, skip this step if the new value appends to the previous value, as textarea height should only have grown
71 | if (testForHeightReduction) {
72 | // ensure the scrollTop values of parent elements are not modified as a consequence of shrinking the textarea height
73 | restoreScrollTops = cacheScrollTops(ta);
74 | ta.style.height = "";
75 | }
76 |
77 | let newHeight: number;
78 |
79 | if (computed.boxSizing === "content-box") {
80 | newHeight =
81 | ta.scrollHeight -
82 | (parseFloat(computed.paddingTop) + parseFloat(computed.paddingBottom));
83 | } else {
84 | newHeight =
85 | ta.scrollHeight +
86 | parseFloat(computed.borderTopWidth) +
87 | parseFloat(computed.borderBottomWidth);
88 | }
89 |
90 | if (
91 | computed.maxHeight !== "none" &&
92 | newHeight > parseFloat(computed.maxHeight)
93 | ) {
94 | if (computed.overflowY === "hidden") {
95 | ta.style.overflow = "scroll";
96 | }
97 | newHeight = parseFloat(computed.maxHeight);
98 | } else if (computed.overflowY !== "hidden") {
99 | ta.style.overflow = "hidden";
100 | }
101 |
102 | ta.style.height = newHeight + "px";
103 |
104 | if (restoreTextAlign) {
105 | ta.style.textAlign = restoreTextAlign;
106 | }
107 |
108 | if (restoreScrollTops) {
109 | restoreScrollTops();
110 | }
111 |
112 | if (previousHeight !== newHeight) {
113 | ta.dispatchEvent(new Event("autosize:resized", { bubbles: true }));
114 | previousHeight = newHeight;
115 | }
116 |
117 | if (initialOverflowY !== computed.overflow && !restoreTextAlign) {
118 | const textAlign = computed.textAlign;
119 |
120 | if (computed.overflow === "hidden") {
121 | // Webkit fails to reflow text after overflow is hidden,
122 | // even if hiding overflow would allow text to fit more compactly.
123 | // The following is intended to force the necessary text reflow.
124 | ta.style.textAlign = textAlign === "start" ? "end" : "start";
125 | }
126 |
127 | setHeight({
128 | restoreTextAlign: textAlign,
129 | testForHeightReduction: true,
130 | });
131 | }
132 | }
133 |
134 | function fullSetHeight(): void {
135 | setHeight({
136 | testForHeightReduction: true,
137 | restoreTextAlign: null,
138 | });
139 | }
140 |
141 | const handleInput = (() => {
142 | let previousValue = ta.value;
143 |
144 | return (): void => {
145 | setHeight({
146 | // if previousValue is '', check for height shrinkage because the placeholder may be taking up space instead
147 | // if new value is merely appending to previous value, skip checking for height deduction
148 | testForHeightReduction:
149 | previousValue === "" || !ta.value.startsWith(previousValue),
150 | restoreTextAlign: null,
151 | });
152 |
153 | previousValue = ta.value;
154 | };
155 | })();
156 |
157 | interface SavedStyles {
158 | height: string;
159 | resize: string;
160 | textAlign: string;
161 | overflowY: string;
162 | overflowX: string;
163 | wordWrap: string;
164 | }
165 |
166 | const destroy = ((style: SavedStyles) =>
167 | function destroyAutosize(): void {
168 | ta.removeEventListener("autosize:destroy", destroy);
169 | ta.removeEventListener("autosize:update", fullSetHeight);
170 | ta.removeEventListener("input", handleInput);
171 | window.removeEventListener("resize", fullSetHeight); // future todo: consider replacing with ResizeObserver
172 | Object.keys(style).forEach(
173 | (key) =>
174 | (ta.style[key as keyof SavedStyles] =
175 | style[key as keyof SavedStyles]),
176 | );
177 | assignedElements.delete(ta);
178 | })({
179 | height: ta.style.height,
180 | resize: ta.style.resize,
181 | textAlign: ta.style.textAlign,
182 | overflowY: ta.style.overflowY,
183 | overflowX: ta.style.overflowX,
184 | wordWrap: ta.style.wordWrap,
185 | } as SavedStyles);
186 |
187 | ta.addEventListener("autosize:destroy", destroy as EventListener);
188 | ta.addEventListener("autosize:update", fullSetHeight);
189 | ta.addEventListener("input", handleInput);
190 | window.addEventListener("resize", fullSetHeight); // future todo: consider replacing with ResizeObserver
191 | ta.style.overflowX = "hidden";
192 | ta.style.wordWrap = "break-word";
193 |
194 | assignedElements.set(ta, {
195 | destroy,
196 | update: fullSetHeight,
197 | });
198 |
199 | fullSetHeight();
200 | }
201 |
202 | function destroy(ta: HTMLTextAreaElement): void {
203 | const methods = assignedElements.get(ta);
204 | if (methods) {
205 | methods.destroy();
206 | }
207 | }
208 |
209 | function update(ta: HTMLTextAreaElement): void {
210 | const methods = assignedElements.get(ta);
211 | if (methods) {
212 | methods.update();
213 | }
214 | }
215 |
216 | interface Autosize {
217 | (
218 | el:
219 | | HTMLTextAreaElement
220 | | HTMLTextAreaElement[]
221 | | NodeListOf,
222 | options?: AutosizeOptions,
223 | ):
224 | | HTMLTextAreaElement
225 | | HTMLTextAreaElement[]
226 | | NodeListOf;
227 | destroy: (
228 | el:
229 | | HTMLTextAreaElement
230 | | HTMLTextAreaElement[]
231 | | NodeListOf,
232 | ) =>
233 | | HTMLTextAreaElement
234 | | HTMLTextAreaElement[]
235 | | NodeListOf;
236 | update: (
237 | el:
238 | | HTMLTextAreaElement
239 | | HTMLTextAreaElement[]
240 | | NodeListOf,
241 | ) =>
242 | | HTMLTextAreaElement
243 | | HTMLTextAreaElement[]
244 | | NodeListOf;
245 | }
246 |
247 | let autosize: Autosize;
248 |
249 | // Do nothing in Node.js environment
250 | if (typeof window === "undefined") {
251 | const noop = (el: T): T => el;
252 | autosize = noop as unknown as Autosize;
253 | autosize.destroy = noop;
254 | autosize.update = noop;
255 | } else {
256 | const autosizeFunction = (
257 | el:
258 | | HTMLTextAreaElement
259 | | HTMLTextAreaElement[]
260 | | NodeListOf,
261 | options?: AutosizeOptions,
262 | ) => {
263 | if (el) {
264 | // Check if el is an array-like object with a length property
265 | const isArrayLike = (obj: any): obj is { length: number } =>
266 | obj && typeof obj.length === "number";
267 |
268 | // Use the type guard to safely access length
269 | if (isArrayLike(el) && el.length) {
270 | Array.prototype.forEach.call(el, (x: HTMLTextAreaElement) =>
271 | assign(x, options),
272 | );
273 | } else {
274 | // Single element case
275 | assign(el as HTMLTextAreaElement, options);
276 | }
277 | }
278 | return el;
279 | };
280 |
281 | autosizeFunction.destroy = (
282 | el:
283 | | HTMLTextAreaElement
284 | | HTMLTextAreaElement[]
285 | | NodeListOf,
286 | ) => {
287 | if (el) {
288 | const isArrayLike = (obj: any): obj is { length: number } =>
289 | obj && typeof obj.length === "number";
290 |
291 | if (isArrayLike(el) && el.length) {
292 | Array.prototype.forEach.call(el, destroy);
293 | } else {
294 | destroy(el as HTMLTextAreaElement);
295 | }
296 | }
297 | return el;
298 | };
299 |
300 | autosizeFunction.update = (
301 | el:
302 | | HTMLTextAreaElement
303 | | HTMLTextAreaElement[]
304 | | NodeListOf,
305 | ) => {
306 | if (el) {
307 | const isArrayLike = (obj: any): obj is { length: number } =>
308 | obj && typeof obj.length === "number";
309 |
310 | if (isArrayLike(el) && el.length) {
311 | Array.prototype.forEach.call(el, update);
312 | } else {
313 | update(el as HTMLTextAreaElement);
314 | }
315 | }
316 | return el;
317 | };
318 |
319 | autosize = autosizeFunction;
320 | }
321 |
322 | export default autosize;
323 |
--------------------------------------------------------------------------------
/src/helpers/createTranslate.ts:
--------------------------------------------------------------------------------
1 | const getValue = (table) => (key) => {
2 | const keys = key.includes(".") ? key.split(".") : [key];
3 | let value = table;
4 | for (const k of keys) {
5 | if (value && k in value) {
6 | value = value[k];
7 | } else {
8 | return undefined;
9 | }
10 | }
11 | return value;
12 | };
13 |
14 | export default (table) => (key) => getValue(table)(key);
15 |
--------------------------------------------------------------------------------
/src/helpers/deprecationWarnings.ts:
--------------------------------------------------------------------------------
1 | const REPLACE_TABLE = {
2 | addCardLink: "components.Card",
3 | customLaneHeader: "components.LaneHeader",
4 | newLaneTemplate: "components.NewLaneSection",
5 | newCardTemplate: "components.NewCardForm",
6 | children: "components.Card",
7 | customCardLayout: "components.Card",
8 | addLaneTitle: '`t` function with key "Add another lane"',
9 | // addCardLink: '`t` function with key "Click to add card"'
10 | };
11 |
12 | const warn = (prop: keyof typeof REPLACE_TABLE) => {
13 | const use = REPLACE_TABLE[prop];
14 | console.warn(
15 | `react-trello property '${prop}' is removed. Use '${use}' instead. More - https://github.com/rcdexta/react-trello/blob/master/UPGRADE.md`,
16 | );
17 | };
18 |
19 | export default (props) => {
20 | for (const key in REPLACE_TABLE) {
21 | if (Object.prototype.hasOwnProperty.call(REPLACE_TABLE, key)) {
22 | warn(key as keyof typeof REPLACE_TABLE);
23 | } else {
24 | console.warn(`react-trello property '${key}' is removed`);
25 | }
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import * as DefaultComponents from "./components";
4 | import {
5 | BoardContainer,
6 | BoardContainerProps,
7 | } from "./controllers/BoardContainer";
8 | import { Lane } from "./controllers/Lane";
9 | import Container from "./dnd/Container";
10 | import { Draggable } from "./dnd/Draggable";
11 | import deprecationWarnings from "./helpers/deprecationWarnings";
12 | import locales from "./locales";
13 |
14 | export * from "./widgets";
15 |
16 | import { Board } from "./controllers/Board";
17 | import createTranslate from "./helpers/createTranslate";
18 |
19 | export { Draggable, Container, BoardContainer, Lane, createTranslate, locales };
20 |
21 | export { DefaultComponents as components };
22 |
23 | const DEFAULT_LANG = "en";
24 |
25 | function DefaultBoard({
26 | components,
27 | lang = DEFAULT_LANG,
28 | ...otherProps
29 | }: BoardContainerProps & { lang?: keyof typeof locales }) {
30 | deprecationWarnings(otherProps);
31 |
32 | const translate = createTranslate(locales[lang || "en"].translation);
33 | return (
34 |
39 | );
40 | }
41 |
42 | export default DefaultBoard;
43 |
--------------------------------------------------------------------------------
/src/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "Add another lane": "+ Add another lane",
3 | "Click to add card": "Click to add card",
4 | "Delete lane": "Delete lane",
5 | "Lane actions": "Lane actions",
6 | "button": {
7 | "Add lane": "Add lane",
8 | "Add card": "Add card",
9 | "Cancel": "Cancel"
10 | },
11 | "placeholder": {
12 | "title": "title",
13 | "description": "description",
14 | "label": "label"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/locales/index.ts:
--------------------------------------------------------------------------------
1 | // i18next support structure
2 | import eng from "./en/translation.json";
3 | import rus from "./ru/translation.json";
4 | export default {
5 | en: {
6 | translation: eng,
7 | },
8 | ru: {
9 | translation: rus,
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/src/locales/ru/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "Add another lane": "+Добавить колонку",
3 | "Click to add card": "+Добавить карточку",
4 | "Delete lane": "Удалить колонку",
5 | "Lane actions": "Действия над колонкой",
6 | "button": {
7 | "Add card": "Добавить карту",
8 | "Add lane": "Добавить колонку",
9 | "Cancel": "Отменить"
10 | },
11 | "placeholder": {
12 | "title": "Название",
13 | "description": "Описание",
14 | "label": "Метка"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { BoardData, Card, Lane } from "../types/Board";
3 |
4 | export interface State {
5 | data: BoardData;
6 | initializeLanes: (lanes: BoardData["lanes"]) => void;
7 | refreshBoard: (lanes?: Lane[]) => void;
8 | addCard: (card: Card, laneId: string, index?: number) => void;
9 | removeCard: (laneId: string, cardId: string) => void;
10 | moveCard: (
11 | fromLaneId: string,
12 | toLaneId: string,
13 | cardId: string,
14 | index: number,
15 | ) => void;
16 | updateCards: (laneId: string, cards: Card[]) => void;
17 | updateCard: (laneId: string, card: Card) => void;
18 | updateLanes: (lanes: Lane[]) => void;
19 | updateLane: (lane: Partial) => void;
20 | paginateLane: (laneId: string, newCards: Card[], nextPage: number) => void;
21 | moveLane: (fromIndex: number, toIndex: number) => void;
22 | removeLane: (laneId: string) => void;
23 | addLane: (lane: Lane) => void;
24 | }
25 | export const store = create()((set) => {
26 | // Helper function to find a lane by ID and throw if not found
27 | const findLaneIndex = (
28 | state: State,
29 | laneId: string,
30 | errorMessage = "Lane not found",
31 | ) => {
32 | const laneIndex = state.data.lanes.findIndex((l) => l.id === laneId);
33 | if (laneIndex === -1) {
34 | throw new Error(errorMessage);
35 | }
36 | return laneIndex;
37 | };
38 |
39 | // Helper function to update a single lane
40 | const updateLaneById = (
41 | state: State,
42 | laneId: string,
43 | updates: Partial,
44 | ) => {
45 | const laneIndex = findLaneIndex(state, laneId);
46 | const updatedLanes = [...state.data.lanes];
47 | updatedLanes[laneIndex] = { ...updatedLanes[laneIndex], ...updates };
48 |
49 | return { data: { ...state.data, lanes: updatedLanes } };
50 | };
51 |
52 | return {
53 | data: { lanes: [] },
54 | initializeLanes: (lanes: Lane[]) =>
55 | set((state) => ({
56 | data: {
57 | ...state.data,
58 | lanes: lanes.map((lane) => ({
59 | ...lane,
60 | currentPage: 1,
61 | cards: lane.cards?.map((c) => ({ ...c, laneId: lane.id })),
62 | })),
63 | },
64 | })),
65 | refreshBoard: (lanes = []) => set(() => ({ data: { lanes } })),
66 | addCard: (card, laneId, index) =>
67 | set((state) => {
68 | const laneIndex = findLaneIndex(state, laneId);
69 | const lane = state.data.lanes[laneIndex];
70 |
71 | let updatedCards;
72 | if (index === undefined) {
73 | updatedCards = [...lane.cards, card];
74 | } else {
75 | updatedCards = [...lane.cards];
76 | updatedCards[index] = card;
77 | }
78 |
79 | return updateLaneById(state, laneId, { cards: updatedCards });
80 | }),
81 | removeCard: (laneId, cardId) =>
82 | set((state) => {
83 | const laneIndex = findLaneIndex(state, laneId);
84 | const lane = state.data.lanes[laneIndex];
85 | const updatedCards = lane.cards.filter((c) => c.id !== cardId);
86 |
87 | return updateLaneById(state, laneId, { cards: updatedCards });
88 | }),
89 | moveCard: (fromLaneId, toLaneId, cardId, index) =>
90 | set((state) => {
91 | const fromLaneIndex = state.data.lanes.findIndex(
92 | (l) => l.id === fromLaneId,
93 | );
94 | if (fromLaneIndex === -1) {
95 | throw new Error("fromLane not found");
96 | }
97 |
98 | const toLaneIndex = state.data.lanes.findIndex(
99 | (l) => l.id === toLaneId,
100 | );
101 | if (toLaneIndex === -1) {
102 | throw new Error("toLane not found");
103 | }
104 |
105 | const fromLane = state.data.lanes[fromLaneIndex];
106 | const toLane = state.data.lanes[toLaneIndex];
107 |
108 | const cardIndex = fromLane.cards.findIndex((c) => c.id === cardId);
109 | if (cardIndex === -1) {
110 | return state;
111 | }
112 |
113 | const card = fromLane.cards[cardIndex];
114 | const newCard = { ...card, laneId: toLaneId };
115 |
116 | const updatedFromCards = fromLane.cards.filter(
117 | (_, i) => i !== cardIndex,
118 | );
119 |
120 | let updatedToCards;
121 | if (index !== undefined) {
122 | updatedToCards = [...toLane.cards];
123 | updatedToCards.splice(index, 0, newCard);
124 | } else {
125 | updatedToCards = [...toLane.cards, newCard];
126 | }
127 |
128 | const updatedLanes = [...state.data.lanes];
129 | updatedLanes[fromLaneIndex] = { ...fromLane, cards: updatedFromCards };
130 | updatedLanes[toLaneIndex] = { ...toLane, cards: updatedToCards };
131 |
132 | return { data: { ...state.data, lanes: updatedLanes } };
133 | }),
134 | updateCards: (laneId, cards) =>
135 | set((state) => updateLaneById(state, laneId, { cards })),
136 | updateCard: (laneId, card) =>
137 | set((state) => {
138 | const laneIndex = findLaneIndex(state, laneId);
139 | const lane = state.data.lanes[laneIndex];
140 | const cardIndex = lane.cards.findIndex((c) => c.id === card.id);
141 |
142 | if (cardIndex === -1) {
143 | return state;
144 | }
145 |
146 | const updatedCards = [...lane.cards];
147 | updatedCards[cardIndex] = card;
148 |
149 | return updateLaneById(state, laneId, { cards: updatedCards });
150 | }),
151 | updateLanes: (lanes) =>
152 | set((state) => ({
153 | data: { ...state.data, lanes },
154 | })),
155 | updateLane: (lane) =>
156 | set((state) => {
157 | if (!lane.id) return state;
158 |
159 | const laneIndex = state.data.lanes.findIndex((l) => l.id === lane.id);
160 | if (laneIndex === -1) {
161 | return state;
162 | }
163 |
164 | return updateLaneById(state, lane.id, lane);
165 | }),
166 | paginateLane: (laneId, newCards, nextPage) =>
167 | set((state) => {
168 | const updatedLanes = state.data.lanes.map((l) =>
169 | l.id === laneId
170 | ? { ...l, cards: [...l.cards, ...newCards], currentPage: nextPage }
171 | : l,
172 | );
173 |
174 | return { data: { ...state.data, lanes: updatedLanes } };
175 | }),
176 | moveLane: (fromIndex, toIndex) =>
177 | set((state) => {
178 | const updatedLanes = [...state.data.lanes];
179 | const [lane] = updatedLanes.splice(fromIndex, 1);
180 | updatedLanes.splice(toIndex, 0, lane);
181 |
182 | return { data: { ...state.data, lanes: updatedLanes } };
183 | }),
184 | removeLane: (laneId) =>
185 | set((state) => {
186 | const updatedLanes = state.data.lanes.filter((l) => l.id !== laneId);
187 | return { data: { ...state.data, lanes: updatedLanes } };
188 | }),
189 | addLane: (lane) =>
190 | set((state) => ({
191 | data: { ...state.data, lanes: [...state.data.lanes, lane] },
192 | })),
193 | };
194 | });
195 |
--------------------------------------------------------------------------------
/src/store/useBoard.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { useStore } from "zustand";
3 | import { BoardContext } from "../controllers/Board";
4 |
5 | export const useBoard = () => {
6 | const store = useContext(BoardContext);
7 | return useStore(store);
8 | };
9 |
--------------------------------------------------------------------------------
/src/styles/Base.ts:
--------------------------------------------------------------------------------
1 | import { PopoverContainer, PopoverContent } from "react-popopo";
2 | import styled, { createGlobalStyle, css } from "styled-components";
3 |
4 | export const GlobalStyle = createGlobalStyle`
5 | .comPlainTextContentEditable {
6 | -webkit-user-modify: read-write-plaintext-only;
7 | cursor: text;
8 | }
9 |
10 | .comPlainTextContentEditable--has-placeholder::before {
11 | content: attr(placeholder);
12 | opacity: 0.5;
13 | color: inherit;
14 | cursor: text;
15 | }
16 |
17 | .react_trello_dragClass {
18 | transform: rotate(3deg);
19 | }
20 |
21 | .react_trello_dragLaneClass {
22 | transform: rotate(3deg);
23 | }
24 |
25 | .icon-overflow-menu-horizontal:before {
26 | content: "\\E91F";
27 | }
28 | .icon-lg, .icon-sm {
29 | color: #798d99;
30 | }
31 | .icon-lg {
32 | height: 32px;
33 | font-size: 16px;
34 | line-height: 32px;
35 | width: 32px;
36 | }
37 | `;
38 |
39 | export const CustomPopoverContainer = styled(PopoverContainer)`
40 | position: absolute;
41 | right: 10px;
42 | flex-flow: column nowrap;
43 | `;
44 |
45 | export const CustomPopoverContent = styled(PopoverContent)`
46 | visibility: hidden;
47 | margin-top: -5px;
48 | opacity: 0;
49 | position: absolute;
50 | z-index: 10;
51 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
52 | transition: all 0.3s ease 0ms;
53 | border-radius: 3px;
54 | min-width: 7em;
55 | flex-flow: column nowrap;
56 | background-color: #fff;
57 | color: #000;
58 | padding: 5px;
59 | left: 50%;
60 | transform: translateX(-50%);
61 | ${(props) =>
62 | props.active &&
63 | `
64 | visibility: visible;
65 | opacity: 1;
66 | transition-delay: 100ms;
67 | `} &::before {
68 | visibility: hidden;
69 | }
70 | a {
71 | color: rgba(255, 255, 255, 0.56);
72 | padding: 0.5em 1em;
73 | margin: 0;
74 | text-decoration: none;
75 | &:hover {
76 | background-color: #00bcd4 !important;
77 | color: #37474f;
78 | }
79 | }
80 | `;
81 |
82 | export const BoardWrapper = styled.div`
83 | background-color: #3179ba;
84 | overflow-y: hidden;
85 | padding: 5px;
86 | color: #393939;
87 | display: flex;
88 | flex-direction: row;
89 | align-items: flex-start;
90 | height: 100vh;
91 | `;
92 |
93 | export const Header = styled.header<{ editLaneTitle?: boolean }>`
94 | margin-bottom: 10px;
95 | display: flex;
96 | flex-direction: row;
97 | align-items: flex-start;
98 | `;
99 |
100 | export const Section = styled.section`
101 | background-color: #e3e3e3;
102 | border-radius: 3px;
103 | margin: 5px 5px;
104 | position: relative;
105 | padding: 10px;
106 | display: inline-flex;
107 | height: auto;
108 | max-height: 90%;
109 | flex-direction: column;
110 | `;
111 |
112 | export const LaneHeader = styled(Header)`
113 | margin-bottom: 0px;
114 | ${(props) =>
115 | props.editLaneTitle &&
116 | css`
117 | padding: 0px;
118 | line-height: 30px;
119 | `} ${(props) =>
120 | !props.editLaneTitle &&
121 | css`
122 | padding: 0px 5px;
123 | `};
124 | `;
125 |
126 | export const LaneFooter = styled.div`
127 | display: flex;
128 | justify-content: center;
129 | align-items: center;
130 | width: 100%;
131 | position: relative;
132 | height: 10px;
133 | `;
134 |
135 | export const ScrollableLane = styled.div<{ isDraggingOver?: boolean }>`
136 | flex: 1;
137 | overflow-y: auto;
138 | min-width: 250px;
139 | overflow-x: hidden;
140 | align-self: center;
141 | max-height: 90vh;
142 | margin-top: 10px;
143 | flex-direction: column;
144 | justify-content: space-between;
145 | `;
146 |
147 | export const Title = styled.span`
148 | font-weight: bold;
149 | font-size: 15px;
150 | line-height: 18px;
151 | cursor: ${(props) => (props.draggable ? "grab" : "auto")};
152 | width: 70%;
153 | `;
154 |
155 | export const RightContent = styled.span`
156 | width: 38%;
157 | text-align: right;
158 | padding-right: 10px;
159 | font-size: 13px;
160 | `;
161 | export const CardWrapper = styled.article`
162 | border-radius: 3px;
163 | border-bottom: 1px solid #ccc;
164 | background-color: #fff;
165 | position: relative;
166 | padding: 10px;
167 | cursor: pointer;
168 | max-width: 250px;
169 | margin-bottom: 7px;
170 | min-width: 230px;
171 | `;
172 |
173 | export const MovableCardWrapper = styled(CardWrapper)`
174 | &:hover {
175 | background-color: #f0f0f0;
176 | color: #000;
177 | }
178 | `;
179 |
180 | export const CardHeader = styled(Header)`
181 | border-bottom: 1px solid #eee;
182 | padding-bottom: 6px;
183 | color: #000;
184 | `;
185 |
186 | export const CardTitle = styled(Title)`
187 | font-size: 14px;
188 | `;
189 |
190 | export const CardRightContent = styled(RightContent)`
191 | font-size: 10px;
192 | `;
193 |
194 | export const Detail = styled.div`
195 | font-size: 12px;
196 | color: #4d4d4d;
197 | white-space: pre-wrap;
198 | `;
199 |
200 | export const Footer = styled.div`
201 | border-top: 1px solid #eee;
202 | padding-top: 6px;
203 | text-align: right;
204 | display: flex;
205 | justify-content: flex-end;
206 | flex-direction: row;
207 | flex-wrap: wrap;
208 | `;
209 |
210 | export const TagSpan = styled.span`
211 | padding: 2px 3px;
212 | border-radius: 3px;
213 | margin: 2px 5px;
214 | font-size: 70%;
215 | `;
216 |
217 | export const AddCardLink = styled.a`
218 | border-radius: 0 0 3px 3px;
219 | color: #838c91;
220 | display: block;
221 | padding: 5px 2px;
222 | margin-top: 10px;
223 | position: relative;
224 | text-decoration: none;
225 | cursor: pointer;
226 |
227 | &:hover {
228 | //background-color: #cdd2d4;
229 | color: #4d4d4d;
230 | text-decoration: underline;
231 | }
232 | `;
233 |
234 | export const LaneTitle = styled.div`
235 | font-size: 15px;
236 | width: 268px;
237 | height: auto;
238 | `;
239 |
240 | export const LaneSection = styled.section`
241 | background-color: #2b6aa3;
242 | border-radius: 3px;
243 | margin: 5px;
244 | position: relative;
245 | padding: 5px;
246 | display: inline-flex;
247 | height: auto;
248 | flex-direction: column;
249 | `;
250 |
251 | export const NewLaneSection = styled(LaneSection)`
252 | width: 200px;
253 | `;
254 |
255 | export const NewLaneButtons = styled.div`
256 | margin-top: 10px;
257 | `;
258 |
259 | export const CardForm = styled.div`
260 | background-color: #e3e3e3;
261 | `;
262 |
263 | export const InlineInput = styled.textarea<{
264 | border: boolean;
265 | resize?: "vertical" | "horizontal" | "none";
266 | }>`
267 | overflow-x: hidden; /* for Firefox (issue #5) */
268 | word-wrap: break-word;
269 | min-height: 18px;
270 | max-height: 112px; /* optional, but recommended */
271 | resize: none;
272 | width: 100%;
273 | height: 18px;
274 | font-size: inherit;
275 | font-weight: inherit;
276 | line-height: inherit;
277 | text-align: inherit;
278 | background-color: transparent;
279 | box-shadow: none;
280 | box-sizing: border-box;
281 | border-radius: 3px;
282 | border: 0;
283 | padding: 0 8px;
284 | outline: 0;
285 | ${(props) =>
286 | props.border &&
287 | css`
288 | &:focus {
289 | box-shadow: inset 0 0 0 2px #0079bf;
290 | }
291 | `} &:focus {
292 | background-color: white;
293 | }
294 | ${(props) =>
295 | props.resize &&
296 | css`
297 | resize: ${props.resize};
298 | `};
299 | `;
300 |
--------------------------------------------------------------------------------
/src/styles/Elements.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { CardWrapper, MovableCardWrapper } from "./Base";
3 |
4 | export const DeleteWrapper = styled.div`
5 | text-align: center;
6 | position: absolute;
7 | top: -1px;
8 | right: 2px;
9 | cursor: pointer;
10 | `;
11 |
12 | export const GenDelButton = styled.button`
13 | transition: all 0.5s ease;
14 | display: inline-block;
15 | border: none;
16 | font-size: 15px;
17 | height: 15px;
18 | padding: 0;
19 | margin-top: 5px;
20 | text-align: center;
21 | width: 15px;
22 | background: inherit;
23 | cursor: pointer;
24 | `;
25 |
26 | export const DelButton = styled.button`
27 | transition: all 0.5s ease;
28 | display: inline-block;
29 | border: none;
30 | font-size: 8px;
31 | height: 15px;
32 | line-height: 1px;
33 | margin: 0 0 8px;
34 | padding: 0;
35 | text-align: center;
36 | width: 15px;
37 | background: inherit;
38 | cursor: pointer;
39 | opacity: 0;
40 | ${MovableCardWrapper}:hover & {
41 | opacity: 1;
42 | }
43 | `;
44 |
45 | export const MenuButton = styled.button`
46 | transition: all 0.5s ease;
47 | display: inline-block;
48 | border: none;
49 | outline: none;
50 | font-size: 16px;
51 | font-weight: bold;
52 | height: 15px;
53 | line-height: 1px;
54 | margin: 0 0 8px;
55 | padding: 0;
56 | text-align: center;
57 | width: 15px;
58 | background: inherit;
59 | cursor: pointer;
60 | `;
61 |
62 | export const LaneMenuHeader = styled.div`
63 | position: relative;
64 | margin-bottom: 4px;
65 | text-align: center;
66 | `;
67 |
68 | export const LaneMenuContent = styled.div`
69 | overflow-x: hidden;
70 | overflow-y: auto;
71 | padding: 0 12px 12px;
72 | `;
73 |
74 | export const LaneMenuItem = styled.div`
75 | cursor: pointer;
76 | display: block;
77 | font-weight: 700;
78 | padding: 6px 12px;
79 | position: relative;
80 | margin: 0 -12px;
81 | text-decoration: none;
82 |
83 | &:hover {
84 | background-color: #3179BA;
85 | color: #fff;
86 | }
87 | `;
88 |
89 | export const LaneMenuTitle = styled.span`
90 | box-sizing: border-box;
91 | color: #6b808c;
92 | display: block;
93 | line-height: 30px;
94 | border-bottom: 1px solid rgba(9,45,66,.13);
95 | margin: 0 6px;
96 | overflow: hidden;
97 | padding: 0 32px;
98 | position: relative;
99 | text-overflow: ellipsis;
100 | white-space: nowrap;
101 | z-index: 1;
102 | `;
103 |
104 | export const DeleteIcon = styled.span`
105 | position: relative;
106 | display: inline-block;
107 | width: 4px;
108 | height: 4px;
109 | opacity: 1;
110 | overflow: hidden;
111 | border: 1px solid #83bd42;
112 | border-radius: 50%;
113 | padding: 4px;
114 | background-color: #83bd42;
115 |
116 | ${CardWrapper}:hover & {
117 | opacity: 1;
118 | }
119 |
120 | &:hover::before,
121 | &:hover::after {
122 | background: red;
123 | }
124 |
125 | &:before,
126 | &:after {
127 | content: '';
128 | position: absolute;
129 | height: 2px;
130 | width: 60%;
131 | top: 45%;
132 | left: 20%;
133 | background: #fff;
134 | border-radius: 5px;
135 | }
136 |
137 | &:before {
138 | -webkit-transform: rotate(45deg);
139 | -moz-transform: rotate(45deg);
140 | -o-transform: rotate(45deg);
141 | transform: rotate(45deg);
142 | }
143 |
144 | &:after {
145 | -webkit-transform: rotate(-45deg);
146 | -moz-transform: rotate(-45deg);
147 | -o-transform: rotate(-45deg);
148 | transform: rotate(-45deg);
149 | }
150 | `;
151 |
152 | export const ExpandCollapseBase = styled.span`
153 | width: 36px;
154 | margin: 0 auto;
155 | font-size: 14px;
156 | position: relative;
157 | cursor: pointer;
158 | `;
159 |
160 | export const CollapseBtn = styled(ExpandCollapseBase)`
161 | &:before {
162 | content: '';
163 | position: absolute;
164 | top: 0;
165 | left: 0;
166 | border-bottom: 7px solid #444;
167 | border-left: 7px solid transparent;
168 | border-right: 7px solid transparent;
169 | border-radius: 6px;
170 | }
171 | &:after {
172 | content: '';
173 | position: absolute;
174 | left: 4px;
175 | top: 4px;
176 | border-bottom: 3px solid #e3e3e3;
177 | border-left: 3px solid transparent;
178 | border-right: 3px solid transparent;
179 | }
180 | `;
181 |
182 | export const ExpandBtn = styled(ExpandCollapseBase)`
183 | &:before {
184 | content: '';
185 | position: absolute;
186 | top: 0;
187 | left: 0;
188 | border-top: 7px solid #444;
189 | border-left: 7px solid transparent;
190 | border-right: 7px solid transparent;
191 | border-radius: 6px;
192 | }
193 | &:after {
194 | content: '';
195 | position: absolute;
196 | left: 4px;
197 | top: 0px;
198 | border-top: 3px solid #e3e3e3;
199 | border-left: 3px solid transparent;
200 | border-right: 3px solid transparent;
201 | }
202 | `;
203 |
204 | export const AddButton = styled.button`
205 | background: #5aac44;
206 | color: #fff;
207 | transition: background 0.3s ease;
208 | min-height: 32px;
209 | padding: 4px 16px;
210 | vertical-align: top;
211 | margin-top: 0;
212 | margin-right: 8px;
213 | font-weight: bold;
214 | border-radius: 3px;
215 | font-size: 14px;
216 | cursor: pointer;
217 | margin-bottom: 0;
218 | `;
219 |
220 | export const CancelButton = styled.button`
221 | background: #999999;
222 | color: #fff;
223 | transition: background 0.3s ease;
224 | min-height: 32px;
225 | padding: 4px 16px;
226 | vertical-align: top;
227 | margin-top: 0;
228 | font-weight: bold;
229 | border-radius: 3px;
230 | font-size: 14px;
231 | cursor: pointer;
232 | margin-bottom: 0;
233 | `;
234 | export const AddLaneLink = styled.button`
235 | background: #2b6aa3;
236 | border: none;
237 | color: #fff;
238 | transition: background 0.3s ease;
239 | min-height: 32px;
240 | padding: 4px 16px;
241 | vertical-align: top;
242 | margin-top: 0;
243 | margin-right: 0px;
244 | border-radius: 4px;
245 | font-size: 13px;
246 | cursor: pointer;
247 | margin-bottom: 0;
248 | `;
249 |
--------------------------------------------------------------------------------
/src/styles/Loader.ts:
--------------------------------------------------------------------------------
1 | import styled, { keyframes } from "styled-components";
2 |
3 | const keyframeAnimation = keyframes`
4 | 0% {
5 | transform: scale(1);
6 | }
7 | 20% {
8 | transform: scale(1, 2.2);
9 | }
10 | 40% {
11 | transform: scale(1);
12 | }
13 | `;
14 | export const LoaderDiv = styled.div`
15 | text-align: center;
16 | margin: 15px 0;
17 | `;
18 |
19 | export const LoadingBar = styled.div`
20 | display: inline-block;
21 | margin: 0 2px;
22 | width: 4px;
23 | height: 18px;
24 | border-radius: 4px;
25 | animation: ${keyframeAnimation} 1s ease-in-out infinite;
26 | background-color: #777;
27 |
28 | &:nth-child(1) {
29 | animation-delay: 0.0001s;
30 | }
31 | &:nth-child(2) {
32 | animation-delay: 0.09s;
33 | }
34 | &:nth-child(3) {
35 | animation-delay: 0.18s;
36 | }
37 | &:nth-child(4) {
38 | animation-delay: 0.27s;
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/types/Board.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from "react";
2 |
3 | export interface BoardData {
4 | lanes: Lane[];
5 | }
6 |
7 | export interface Lane {
8 | id: string;
9 | title?: string;
10 | label?: string;
11 | style?: CSSProperties;
12 | cards?: Card[];
13 | currentPage?: number;
14 | droppable?: boolean;
15 | labelStyle?: CSSProperties;
16 | cardStyle?: CSSProperties;
17 | disallowAddingCard?: boolean;
18 | [key: string]: any;
19 | }
20 |
21 | export interface Card {
22 | id: string;
23 | title?: string;
24 | label?: string;
25 | description?: string;
26 | laneId?: string;
27 | style?: CSSProperties;
28 | draggable?: boolean;
29 | [key: string]: any;
30 | }
31 |
--------------------------------------------------------------------------------
/src/types/EventBus.ts:
--------------------------------------------------------------------------------
1 | import { BoardData, Card, Lane } from "./Board";
2 | export interface EventBusHandle {
3 | publish: (event: EventBusEvent) => any;
4 | }
5 |
6 | type EventBusEvent =
7 | | {
8 | type: "ADD_CARD";
9 | laneId: string;
10 | card: Card;
11 | index?: number;
12 | }
13 | | {
14 | type: "UPDATE_CARD";
15 | laneId: string;
16 | card: Card;
17 | }
18 | | {
19 | type: "REMOVE_CARD";
20 | laneId: string;
21 | cardId: string;
22 | }
23 | | {
24 | type: "REFRESH_BOARD";
25 | data: BoardData;
26 | }
27 | | {
28 | type: "MOVE_CARD";
29 | fromLaneId: string;
30 | toLaneId: string;
31 | cardId: string;
32 | index: number;
33 | }
34 | | {
35 | type: "UPDATE_CARDS";
36 | laneId: string;
37 | cards: Card[];
38 | }
39 | | {
40 | type: "UPDATE_LANES";
41 | lanes: BoardData["lanes"];
42 | }
43 | | {
44 | type: "UPDATE_LANE";
45 | laneId: string;
46 | lane: Lane;
47 | };
48 |
--------------------------------------------------------------------------------
/src/types/utilities.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Given a type `TType` and a type `TRequired` which is a subset of required keys of `TType`, return a new type which is the
3 | * same as `TType` but with all keys optional except those in `TRequired`.
4 | */
5 | export type PartialExcept<
6 | TType,
7 | TRequired extends keyof TType,
8 | > = Partial & Pick;
9 |
--------------------------------------------------------------------------------
/src/widgets/DeleteButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, HTMLAttributes, PropsWithChildren } from "react";
2 | import { StyledComponent, ThemedStyledFunction } from "styled-components";
3 | import { DelButton, DeleteWrapper } from "../styles/Elements";
4 |
5 | type DeleteButtonProps = HTMLAttributes;
6 | export const DeleteButton: FC> = ({
7 | ...rest
8 | }) => {
9 | return (
10 |
11 | ✖
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/widgets/EditableLabel.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FC,
3 | PropsWithChildren,
4 | createRef,
5 | useEffect,
6 | useState,
7 | } from "react";
8 | import createTranslate from "../helpers/createTranslate";
9 |
10 | interface EditableLabelProps {
11 | onChange?: (labelText: string) => void;
12 | placeholder?: string;
13 | autoFocus?: boolean;
14 | inline?: boolean;
15 | value?: string;
16 | }
17 | export const EditableLabel: FC> = ({
18 | autoFocus = false,
19 | inline = false,
20 | onChange = () => {},
21 | placeholder = "",
22 | value = "",
23 | children,
24 | }) => {
25 | const [labelText, setLabelText] = useState("");
26 | const divRef = createRef();
27 | useEffect(() => {
28 | if (autoFocus) {
29 | divRef.current.focus();
30 | }
31 | });
32 |
33 | const getClassName = () => {
34 | const placeholder =
35 | labelText === "" ? "comPlainTextContentEditable--has-placeholder" : "";
36 | return `comPlainTextContentEditable ${placeholder}`;
37 | };
38 | const onPaste = (event: React.ClipboardEvent) => {
39 | event.preventDefault();
40 | navigator.clipboard.writeText(event.clipboardData.getData("text"));
41 | };
42 | const onBlur = () => {
43 | onChange(labelText);
44 | };
45 | const onKeyDown = (event: React.KeyboardEvent) => {
46 | if (event.key === "Enter") {
47 | event.preventDefault();
48 | onChange(labelText);
49 | divRef.current.blur();
50 | }
51 | if (event.key === "Escape") {
52 | divRef.current.textContent = labelText;
53 | event.preventDefault();
54 | event.stopPropagation();
55 | }
56 | };
57 | const onTextChange = (event: React.FormEvent) => {
58 | const value = event.currentTarget.innerText;
59 | setLabelText(value);
60 | };
61 |
62 | return (
63 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/src/widgets/InlineInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useRef, useState } from "react";
2 | import autosize from "../helpers/autosize";
3 | import { InlineInput as _InlineInput } from "../styles/Base";
4 |
5 | interface InlineInputProps {
6 | onSave?: (inputValue: string) => void;
7 | onCancel?: () => void;
8 | border?: boolean;
9 | placeholder?: string;
10 | value?: string;
11 | autoFocus?: boolean;
12 | resize?: "none" | "vertical" | "horizontal";
13 | }
14 |
15 | export const InlineInput: FC = ({
16 | autoFocus = false,
17 | border = false,
18 | onSave = () => {},
19 | onCancel = () => {},
20 | placeholder = "",
21 | value = "",
22 | resize = "none",
23 | }) => {
24 | const [inputValue, setInputValue] = useState(value);
25 | const inputRef = useRef(null);
26 |
27 | const onFocus = (e: React.FocusEvent) =>
28 | e.target.select();
29 |
30 | const onMouseDown = (e: React.MouseEvent) => {
31 | if (document.activeElement !== e.target) {
32 | e.preventDefault();
33 | inputRef.current.focus();
34 | }
35 | };
36 |
37 | const onBlur = () => {
38 | updateValue();
39 | };
40 |
41 | const onKeyDown = (e: React.KeyboardEvent) => {
42 | if (e.key === "Enter") {
43 | inputRef.current.blur();
44 | e.preventDefault();
45 | }
46 | if (e.key === "Escape") {
47 | setValue(value);
48 | inputRef.current.blur();
49 | e.preventDefault();
50 | }
51 | if (e.key === "Tab") {
52 | if (inputValue.length === 0) {
53 | onCancel();
54 | }
55 | inputRef.current.blur();
56 | e.preventDefault();
57 | }
58 | };
59 |
60 | const getValue = () => inputRef.current.value || "";
61 | const setValue = (newValue: string) => {
62 | if (inputRef.current) {
63 | inputRef.current.value = newValue;
64 | }
65 | };
66 |
67 | const updateValue = () => {
68 | if (getValue() !== value) {
69 | onSave(getValue());
70 | }
71 | };
72 |
73 | const setRef = (ref: HTMLTextAreaElement) => {
74 | inputRef.current = ref;
75 | if (resize !== "none") {
76 | autosize(inputRef.current);
77 | }
78 | };
79 |
80 | useEffect(() => {
81 | setInputValue(value);
82 | }, [value]);
83 |
84 | return (
85 | <_InlineInput
86 | ref={setRef}
87 | border={border}
88 | onMouseDown={onMouseDown}
89 | onFocus={onFocus}
90 | onBlur={onBlur}
91 | onKeyDown={onKeyDown}
92 | placeholder={value.length === 0 ? undefined : placeholder}
93 | defaultValue={value}
94 | autoComplete="off"
95 | autoCorrect="off"
96 | autoCapitalize="off"
97 | spellCheck="false"
98 | rows={1}
99 | autoFocus={autoFocus}
100 | />
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/src/widgets/NewLaneTitleEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes, PropsWithChildren, useState } from "react";
2 | import autosize from "../helpers/autosize";
3 | import { InlineInput } from "../styles/Base";
4 |
5 | interface NewLaneTitleEditorProps extends HTMLAttributes {
6 | onSave?: (inputValue: string) => void;
7 | onCancel?: () => void;
8 | border?: boolean;
9 | placeholder?: string;
10 | value?: string;
11 | autoFocus?: boolean;
12 | autoResize?: boolean;
13 | resize?: "vertical" | "horizontal" | "none";
14 | inputRef: React.MutableRefObject;
15 | }
16 | export const NewLaneTitleEditor: React.FC<
17 | PropsWithChildren
18 | > = ({
19 | autoFocus = false,
20 | border = false,
21 | onCancel = () => {},
22 | onSave = () => {},
23 | placeholder = "",
24 | resize = "none",
25 | value = "",
26 | inputRef,
27 | }) => {
28 | const [inputValue, setInputValue] = useState(value);
29 | const onKeyDown = (e: React.KeyboardEvent) => {
30 | if (e.key === "Enter") {
31 | inputRef.current.blur();
32 | saveValue();
33 | e.preventDefault();
34 | }
35 | if (e.key === "Escape") {
36 | setInputValue(value);
37 | inputRef.current.blur();
38 | cancel();
39 | e.preventDefault();
40 | }
41 | if (e.key === "Tab") {
42 | if (inputValue.length === 0) {
43 | onCancel();
44 | }
45 | inputRef.current.blur();
46 | e.preventDefault();
47 | }
48 | };
49 | const setRef = (ref: HTMLTextAreaElement) => {
50 | inputRef.current = ref;
51 | if (resize !== "none") {
52 | autosize(inputRef.current);
53 | }
54 | };
55 | const cancel = () => {
56 | setInputValue("");
57 | onCancel();
58 | inputRef.current.blur();
59 | };
60 | const saveValue = () => {
61 | if (inputValue !== value) {
62 | onSave(inputValue);
63 | }
64 | };
65 | return (
66 |
76 | );
77 | };
78 |
--------------------------------------------------------------------------------
/src/widgets/index.ts:
--------------------------------------------------------------------------------
1 | export { DeleteButton } from "./DeleteButton";
2 | export { EditableLabel } from "./EditableLabel";
3 | export { InlineInput } from "./InlineInput";
4 |
--------------------------------------------------------------------------------
/stories/AdvancedFeatures.story.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import { Meta, StoryObj } from "@storybook/react";
4 | import Board from "../src";
5 |
6 | import data from "./data/base.json";
7 | import debug from "./helpers/debug";
8 | Board.displayName = "Board";
9 |
10 | class AsyncBoard extends Component {
11 | state = {
12 | boardData: { lanes: [{ id: "loading", title: "loading..", cards: [] }] },
13 | };
14 |
15 | componentDidMount() {
16 | setTimeout(this.getBoard.bind(this), 1000);
17 | }
18 |
19 | getBoard() {
20 | this.setState({ boardData: data });
21 | }
22 |
23 | render() {
24 | return ;
25 | }
26 | }
27 |
28 | const meta: Meta = {
29 | title: "Advanced Features",
30 | component: Board,
31 | };
32 |
33 | export default meta;
34 | type Story = StoryObj;
35 |
36 | export const AsyncLoad: Story = {
37 | render(args) {
38 | return ;
39 | },
40 | };
41 |
42 | export const CollapsibleLanes: Story = {
43 | args: {
44 | data,
45 | draggable: true,
46 | collapsibleLanes: true,
47 | onDataChange: (nextData) => {
48 | debug("data has changed");
49 | debug(nextData);
50 | },
51 | },
52 | };
53 |
54 | /**
55 | * This story implements onCardClick and onLaneClick handlers.
56 | * These handlers are called when a card or lane is clicked.
57 | * Try clicking on a card or lane to see an alert.
58 | */
59 | export const EventHandling: Story = {
60 | name: "Event Handling",
61 | args: {
62 | data,
63 | draggable: true,
64 | onCardClick: (cardId, metadata, laneId) =>
65 | alert(
66 | `Card with id:${cardId} clicked. Has metadata.id: ${metadata.id}. Card in lane: ${laneId}`,
67 | ),
68 | onLaneClick: (laneId) => alert(`Lane with id:${laneId} clicked`),
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/stories/BasicFunctions.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import React from "react";
3 | import Board from "../src";
4 |
5 | import data from "./data/base.json";
6 | import dataSortedLane from "./data/data-sort.json";
7 | Board.displayName = "Board";
8 |
9 | const meta: Meta = {
10 | title: "Basic Functions",
11 | parameters: {
12 | info: "A demonstration of onDragStart and onDragEnd hooks for card and lanes",
13 | },
14 | component: Board,
15 | };
16 | export default meta;
17 | type Story = StoryObj;
18 |
19 | export const FullBoardExample: Story = {
20 | args: { data },
21 | };
22 |
23 | export const SortedLane: Story = {
24 | args: {
25 | data: dataSortedLane,
26 | laneSortFunction: (cardA, cardB) => {
27 | const dateA = new Date(cardA.metadata.completedAt);
28 | const dateB = new Date(cardB.metadata.completedAt);
29 | return dateA.getTime() - dateB.getTime();
30 | },
31 | },
32 | };
33 |
34 | export const ReverseSortedLane: Story = {
35 | args: {
36 | data: dataSortedLane,
37 | laneSortFunction(cardA, cardB) {
38 | const dateA = new Date(cardA.metadata.completedAt);
39 | const dateB = new Date(cardB.metadata.completedAt);
40 | return dateB.getTime() - dateA.getTime();
41 | },
42 | },
43 | };
44 |
45 | const PER_PAGE = 15;
46 | function generateCards(requestedPage = 1) {
47 | const cards = [];
48 | const fetchedItems = (requestedPage - 1) * PER_PAGE;
49 | for (let i = fetchedItems + 1; i <= fetchedItems + PER_PAGE; i++) {
50 | cards.push({
51 | id: `${i}`,
52 | title: `Card${i}`,
53 | description: `Description for #${i}`,
54 | });
55 | }
56 | return cards;
57 | }
58 | function delayedPromise(durationInMs, resolutionPayload) {
59 | return new Promise((resolve) => {
60 | setTimeout(() => {
61 | resolve(resolutionPayload);
62 | }, durationInMs);
63 | });
64 | }
65 | function paginate(requestedPage, laneId) {
66 | // simulate no more cards after page 2
67 | if (requestedPage > 2) {
68 | return delayedPromise(2000, []);
69 | }
70 | const newCards = generateCards(requestedPage);
71 | return delayedPromise(2000, newCards);
72 | }
73 | export const BasicFunctions: Story = {
74 | args: {
75 | data: {
76 | lanes: [
77 | {
78 | id: "Lane1",
79 | title: "Lane1",
80 | cards: generateCards(),
81 | },
82 | ],
83 | },
84 | laneSortFunction: (card1, card2) => parseInt(card1.id) - parseInt(card2.id),
85 | onLaneScroll: paginate,
86 | },
87 | };
88 |
89 | export const Tags: Story = {
90 | name: "Tags",
91 | args: {
92 | data: {
93 | lanes: [
94 | {
95 | id: "lane1",
96 | title: "Planned Tasks",
97 | cards: [
98 | {
99 | id: "Card1",
100 | title: "Card1",
101 | description: "foo card",
102 | metadata: { cardId: "Card1" },
103 | tags: [
104 | { title: "High", color: "white", bgcolor: "#EB5A46" },
105 | { title: "Tech Debt", color: "white", bgcolor: "#0079BF" },
106 | {
107 | title: "Very long tag that is",
108 | color: "white",
109 | bgcolor: "#61BD4F",
110 | },
111 | { title: "One more", color: "white", bgcolor: "#61BD4F" },
112 | ],
113 | },
114 | {
115 | id: "Card2",
116 | title: "Card2",
117 | description: "bar card",
118 | metadata: { cardId: "Card2" },
119 | tags: [{ title: "Low" }],
120 | },
121 | ],
122 | },
123 | ],
124 | },
125 | tagStyle: { fontSize: "80%" },
126 | },
127 | };
128 |
--------------------------------------------------------------------------------
/stories/CustomAddCardLink.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react";
2 | import React from "react";
3 |
4 | import Board from "../src";
5 | import { AddCardLinkComponent } from "../src/components/AddCardLink";
6 |
7 | import data from "./data/collapsible.json";
8 | Board.displayName = "Board";
9 |
10 | export default ({
11 | title: "Custom Components",
12 | component: () => {
13 | const CustomAddCardLink: AddCardLinkComponent = ({ onClick, t }) => (
14 |
15 | );
16 | return (
17 |
22 | );
23 | },
24 | } satisfies Meta);
25 |
26 | export const AddCardLink = {};
27 |
--------------------------------------------------------------------------------
/stories/CustomCard.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react";
2 | import React, { CSSProperties } from "react";
3 | import Board from "../src";
4 | import { CardComponent } from "../src/components/Card";
5 | import { Tag } from "../src/components/Card/Tag";
6 | import { MovableCardWrapper } from "../src/styles/Base";
7 | import { BoardData } from "../src/types/Board";
8 | import { DeleteButton } from "./../src/widgets/DeleteButton";
9 | Board.displayName = "Board";
10 |
11 | const CustomCardExample: CardComponent<{
12 | name: string;
13 | cardStyle: CSSProperties;
14 | body: string;
15 | dueOn: string;
16 | cardColor: CSSProperties["color"];
17 | subTitle: string;
18 | escalationText: string;
19 | }> = ({
20 | onClick,
21 | className,
22 | name,
23 | cardStyle,
24 | body,
25 | dueOn,
26 | cardColor,
27 | subTitle,
28 | tagStyle,
29 | escalationText,
30 | tags,
31 | showDeleteButton,
32 | onDelete,
33 | id,
34 | index,
35 | t,
36 | }) => {
37 | const clickDelete = (e) => {
38 | onDelete();
39 | e.stopPropagation();
40 | };
41 |
42 | return (
43 |
48 |
59 | {name}
60 | {dueOn}
61 | {showDeleteButton && }
62 |
63 |
64 |
{subTitle}
65 |
66 | {body}
67 |
68 |
77 | {escalationText}
78 |
79 | {tags && (
80 |
90 | {tags.map((tag) => (
91 |
92 | ))}
93 |
94 | )}
95 |
96 |
97 | );
98 | };
99 |
100 | const data: BoardData = {
101 | lanes: [
102 | {
103 | id: "lane1",
104 | title: "Planned Tasks",
105 | label: "12/12",
106 | style: { backgroundColor: "cyan", padding: 20 },
107 | titleStyle: { fontSize: 20, marginBottom: 15 },
108 | labelStyle: { color: "#009688", fontWeight: "bold" },
109 | cards: [
110 | {
111 | id: "Card1",
112 | name: "John Smith",
113 | dueOn: "due in a day",
114 | subTitle: "SMS received at 12:13pm today",
115 | body: "Thanks. Please schedule me for an estimate on Monday.",
116 | escalationText: "Escalated to OPS-ESCALATIONS!",
117 | cardColor: "#BD3B36",
118 | cardStyle: {
119 | borderRadius: 6,
120 | boxShadow: "0 0 6px 1px #BD3B36",
121 | marginBottom: 15,
122 | },
123 | metadata: { id: "Card1" },
124 | },
125 | {
126 | id: "Card2",
127 | name: "Card Weathers",
128 | dueOn: "due now",
129 | subTitle: "Email received at 1:14pm",
130 | body: "Is the estimate free, and can someone call me soon?",
131 | escalationText: "Escalated to Admin",
132 | cardColor: "#E08521",
133 | cardStyle: {
134 | borderRadius: 6,
135 | boxShadow: "0 0 6px 1px #E08521",
136 | marginBottom: 15,
137 | },
138 | metadata: { id: "Card1" },
139 | },
140 | ],
141 | },
142 | {
143 | id: "lane2",
144 | title: "Long Lane name this is i suppose ha!",
145 | cards: [
146 | {
147 | id: "Card3",
148 | name: "Michael Caine",
149 | dueOn: "due in a day",
150 | subTitle: "Email received at 4:23pm today",
151 | body:
152 | "You are welcome. Interested in doing business with you" + " again",
153 | escalationText: "Escalated to OPS-ESCALATIONS!",
154 | cardColor: "#BD3B36",
155 | cardStyle: {
156 | borderRadius: 6,
157 | boxShadow: "0 0 6px 1px #BD3B36",
158 | marginBottom: 15,
159 | },
160 | metadata: { id: "Card1" },
161 | tags: [
162 | { title: "Critical", color: "white", bgcolor: "red" },
163 | { title: "2d ETA", color: "white", bgcolor: "#0079BF" },
164 | ],
165 | },
166 | ],
167 | },
168 | ],
169 | };
170 |
171 | export default ({
172 | title: "Custom Components",
173 | component: () => {
174 | return (
175 |
181 | alert(
182 | `Card with id:${cardId} clicked. Has metadata.id: ${metadata.id}`,
183 | )
184 | }
185 | />
186 | );
187 | },
188 | } satisfies Meta);
189 |
190 | export const Card: Meta = {};
191 |
--------------------------------------------------------------------------------
/stories/CustomCardWithDrag.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react";
2 | import React from "react";
3 |
4 | import { MovableCardWrapper } from "../src/styles/Base";
5 | import debug from "./helpers/debug";
6 |
7 | import Board from "../src";
8 | import { BoardData } from "../src/types/Board";
9 | Board.displayName = "Board";
10 |
11 | const CustomCard = (props) => {
12 | return (
13 |
19 |
31 |
32 |
33 | {props.subTitle}
34 |
35 |
36 | {props.body}
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | const customCardData = {
44 | lanes: [
45 | {
46 | id: "lane1",
47 | title: "Planned",
48 | cards: [
49 | {
50 | id: "Card1",
51 | name: "John Smith",
52 | subTitle: "SMS received at 12:13pm today",
53 | body: "Thanks. Please schedule me for an estimate on Monday.",
54 | metadata: { id: "Card1" },
55 | },
56 | {
57 | id: "Card2",
58 | name: "Card Weathers",
59 | subTitle: "Email received at 1:14pm",
60 | body: "Is the estimate free, and can someone call me soon?",
61 | metadata: { id: "Card1" },
62 | },
63 | ],
64 | },
65 | {
66 | id: "lane2",
67 | title: "Work In Progress",
68 | cards: [
69 | {
70 | id: "Card3",
71 | name: "Michael Caine",
72 | subTitle: "Email received at 4:23pm today",
73 | body: "You are welcome. Interested in doing business with you again",
74 | metadata: { id: "Card1" },
75 | },
76 | ],
77 | },
78 | ],
79 | };
80 |
81 | const BoardWithCustomCard = () => {
82 | const [boardData, setBoardData] = React.useState(customCardData);
83 | const onDragEnd = (cardId, sourceLandId, targetLaneId, index, card) => {
84 | debug("Calling onDragEnd");
85 |
86 | // Create updated card without immer
87 | const updatedCard = {
88 | ...card,
89 | cardColor: "#d0fdd2",
90 | };
91 |
92 | // Create updated board without immer
93 | const updatedBoard = {
94 | ...boardData,
95 | lanes: boardData.lanes.map((lane) => {
96 | // Source lane - remove the card
97 | if (lane.id === sourceLandId) {
98 | return {
99 | ...lane,
100 | cards: lane.cards?.filter((c) => c.id !== cardId) ?? [],
101 | };
102 | }
103 | // Target lane - add the card at the specified index
104 | if (lane.id === targetLaneId) {
105 | const newCards = [...(lane.cards ?? [])];
106 | newCards.splice(index, 0, updatedCard);
107 | return {
108 | ...lane,
109 | cards: newCards,
110 | };
111 | }
112 | // Other lanes remain unchanged
113 | return lane;
114 | }),
115 | };
116 |
117 | setBoardData(updatedBoard);
118 | };
119 | return (
120 |
126 | alert(`Card with id:${cardId} clicked. Has metadata.id: ${metadata.id}`)
127 | }
128 | components={{ Card: CustomCard }}
129 | />
130 | );
131 | };
132 |
133 | export default ({
134 | title: "Custom Components",
135 | component: BoardWithCustomCard,
136 | } satisfies Meta);
137 |
138 | export const DragnDropStyling = {};
139 |
--------------------------------------------------------------------------------
/stories/CustomLaneFooter.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import React from "react";
3 |
4 | import Board from "../src";
5 | import type { LaneFooterComponent } from "../src/components/Lane/LaneFooter";
6 |
7 | import data from "./data/collapsible.json";
8 | Board.displayName = "Board";
9 |
10 | const LaneFooterComponent: LaneFooterComponent = ({
11 | onClick,
12 | onKeyDown,
13 | collapsed,
14 | }) => (
15 |
16 | {collapsed ? "click to expand" : "click to collapse"}
17 |
18 | );
19 |
20 | export default ({
21 | title: "Custom Components",
22 | component: Board,
23 | } satisfies Meta);
24 | type Story = StoryObj;
25 | export const LaneFooter: Story = {
26 | render: (args) => {
27 | return (
28 |
33 | );
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/stories/CustomLaneHeader.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, storiesOf } from "@storybook/react";
2 | import React from "react";
3 |
4 | import Board from "../src";
5 | import { LaneHeaderProps } from "../src/components/Lane/LaneHeader";
6 | Board.displayName = "Board";
7 |
8 | const CustomLaneHeader = ({
9 | label,
10 | cards,
11 | title,
12 | current,
13 | target,
14 | }: LaneHeaderProps) => {
15 | const buttonHandler = () => {
16 | alert(
17 | `The label passed to the lane was: ${label}. The lane has ${cards.length} cards!`,
18 | );
19 | };
20 | return (
21 |
22 |
32 | {title}
33 | {label && (
34 |
35 |
42 |
43 | )}
44 |
45 |
46 | Percentage: {current || 0}/{target}
47 |
48 |
49 | );
50 | };
51 |
52 | export default ({
53 | title: "Custom Components",
54 | component: () => {
55 | const data = {
56 | lanes: [
57 | {
58 | id: "lane1",
59 | title: "Planned Tasks",
60 | current: "70", // custom property
61 | target: "100", // custom property
62 | label: "First Lane here",
63 | cards: [
64 | {
65 | id: "Card1",
66 | title: "John Smith",
67 | description:
68 | "Thanks. Please schedule me for an estimate on Monday.",
69 | },
70 | {
71 | id: "Card2",
72 | title: "Card Weathers",
73 | description: "Email received at 1:14pm",
74 | },
75 | ],
76 | },
77 | {
78 | id: "lane2",
79 | title: "Completed Tasks",
80 | label: "Second Lane here",
81 | current: "30", // custom property
82 | target: "100", // custom property
83 | cards: [
84 | {
85 | id: "Card3",
86 | title: "Michael Caine",
87 | description:
88 | "You are welcome. Interested in doing business with you" +
89 | " again",
90 | tags: [
91 | { title: "Critical", color: "white", bgcolor: "red" },
92 | { title: "2d ETA", color: "white", bgcolor: "#0079BF" },
93 | ],
94 | },
95 | ],
96 | },
97 | ],
98 | };
99 |
100 | return ;
101 | },
102 | } satisfies Meta);
103 |
104 | export const LaneHeader = {};
105 |
--------------------------------------------------------------------------------
/stories/CustomNewCardForm.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react";
2 | import React, { Component } from "react";
3 |
4 | import Board from "../src";
5 | import { NewCardFormProps } from "../src/components/NewCardForm";
6 |
7 | import data from "./data/base.json";
8 | Board.displayName = "Board";
9 |
10 | class NewCardFormComponent extends Component {
11 | private titleRef: any;
12 | private descRef: any;
13 | handleAdd = () =>
14 | this.props.onAdd({
15 | label: "",
16 | laneId: this.props.laneId,
17 | title: this.titleRef.value,
18 | description: this.descRef.value,
19 | });
20 | setTitleRef = (ref) => (this.titleRef = ref);
21 | setDescRef = (ref) => (this.descRef = ref);
22 | render() {
23 | const { onCancel } = this.props;
24 | return (
25 |
33 |
34 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | export default ({
55 | title: "Custom Components",
56 | component: () => (
57 |
63 | ),
64 | } satisfies Meta);
65 |
66 | export const NewCardForm = {};
67 |
--------------------------------------------------------------------------------
/stories/CustomNewLaneForm.story.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import Board from "../src";
4 |
5 | import data from "./data/data-sort.json";
6 | Board.displayName = "Board";
7 |
8 | class NewLaneFormComponent extends Component {
9 | render() {
10 | const { onCancel, t } = this.props;
11 | const handleAdd = () => this.props.onAdd({ title: this.inputRef.value });
12 | const setInputRef = (ref) => (this.inputRef = ref);
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default {
24 | title: "Custom Components",
25 | component: () => (
26 |
32 | ),
33 | };
34 |
35 | export const NewLaneForm = {};
36 |
--------------------------------------------------------------------------------
/stories/CustomNewLaneSection.story.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import Board from "../src";
4 |
5 | import data from "./data/data-sort.json";
6 | Board.displayName = "Board";
7 |
8 | const NewLaneSectionComponent = ({ t, onClick }) => (
9 |
10 | );
11 |
12 | export default {
13 | title: "Custom Components",
14 | component: () => (
15 |
21 | ),
22 | };
23 |
24 | export const NewLaneSection = {};
25 |
--------------------------------------------------------------------------------
/stories/Deprecations.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@storybook/react";
2 | // @ts-nocheck
3 | import React, { Component } from "react";
4 |
5 | import Board from "../src";
6 |
7 | import { CardProps } from "../src/components/Card";
8 | import { Tag } from "../src/components/Card/Tag";
9 | import "./board.css";
10 | Board.displayName = "Board";
11 |
12 | import data from "./data/base.json";
13 |
14 | const CustomLaneHeader = (props) => {
15 | const buttonHandler = () => {
16 | alert(
17 | `The label passed to the lane was: ${props.label}. The lane has ${props.cards.length} cards!`,
18 | );
19 | };
20 | return (
21 |
22 |
32 | {props.title}
33 | {props.label && (
34 |
35 |
38 |
39 | )}
40 |
41 |
42 | );
43 | };
44 |
45 | class NewCard extends Component {
46 | updateField = (field, evt) => {
47 | this.setState({ [field]: evt.target.value });
48 | };
49 |
50 | handleAdd = () => {
51 | this.props.onAdd(this.state);
52 | };
53 |
54 | render() {
55 | const { onCancel } = this.props;
56 | return (
57 |
65 |
66 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 | }
89 | const CustomCard = (props) => {
90 | return (
91 |
92 |
103 | {props.name}
104 | {props.dueOn}
105 |
106 |
107 |
108 | {props.subTitle}
109 |
110 |
111 | {props.body}
112 |
113 |
122 | {props.escalationText}
123 |
124 | {props.tags && (
125 |
135 | {props.tags.map((tag) => (
136 |
137 | ))}
138 |
139 | )}
140 |
141 |
142 | );
143 | };
144 |
145 | export default ({
146 | title: "Deprecation warnings",
147 | component: () => (
148 | New Card}
152 | customLaneHeader={}
153 | newLaneTemplate={new lane
}
154 | newCardTemplate={}
155 | customCardLayout={true}
156 | >
157 |
158 |
159 | ),
160 | } satisfies Meta);
161 |
162 | export const v2_2_warnings = {};
163 |
--------------------------------------------------------------------------------
/stories/DragDrop.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import React from "react";
3 | import debug from "./helpers/debug";
4 |
5 | import Board from "../src";
6 |
7 | import data from "./data/base.json";
8 |
9 | export default ({
10 | title: "Drag-n-Drop",
11 | component: Board,
12 | parameters: {
13 | info: "A demonstration of onDragStart and onDragEnd hooks for card and lanes",
14 | },
15 | } satisfies Meta);
16 | type Story = StoryObj;
17 | Board.displayName = "Board";
18 |
19 | export const DragNDrop: Story = {
20 | name: "Basic",
21 | render: (args) => {
22 | const handleDragStart = (cardId, laneId) => {
23 | debug("drag started");
24 | debug(`cardId: ${cardId}`);
25 | debug(`laneId: ${laneId}`);
26 | };
27 |
28 | const handleDragEnd = (
29 | cardId,
30 | sourceLaneId,
31 | targetLaneId,
32 | position,
33 | card,
34 | ) => {
35 | debug("drag ended");
36 | debug(`cardId: ${cardId}`);
37 | debug(`sourceLaneId: ${sourceLaneId}`);
38 | debug(`targetLaneId: ${targetLaneId}`);
39 | debug(`newPosition: ${position}`);
40 | debug("cardDetails:");
41 | debug(card);
42 | };
43 |
44 | const handleLaneDragStart = (laneId) => {
45 | debug(`lane drag started for ${laneId}`);
46 | };
47 |
48 | const handleLaneDragEnd = (removedIndex, addedIndex, { id }) => {
49 | debug(`lane drag ended from position ${removedIndex} for laneId=${id}`);
50 | debug(`New lane position: ${addedIndex}`);
51 | };
52 |
53 | const shouldReceiveNewData = (nextData) => {
54 | debug("data has changed");
55 | debug(nextData);
56 | };
57 |
58 | const onCardMoveAcrossLanes = (
59 | fromLaneId,
60 | toLaneId,
61 | cardId,
62 | addedIndex,
63 | ) => {
64 | debug(
65 | `onCardMoveAcrossLanes: ${fromLaneId}, ${toLaneId}, ${cardId}, ${addedIndex}`,
66 | );
67 | };
68 |
69 | return (
70 |
81 | );
82 | },
83 | };
84 |
85 | export const DragStyling: Story = {
86 | render: (args) => (
87 |
93 | ),
94 | };
95 |
--------------------------------------------------------------------------------
/stories/EditableBoard.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj, storiesOf } from "@storybook/react";
2 | import React, { Component } from "react";
3 | import debug from "./helpers/debug";
4 |
5 | import Board from "../src";
6 |
7 | import data from "./data/base.json";
8 | import smallData from "./data/data-sort.json";
9 |
10 | const disallowAddingCardData = { ...data };
11 | disallowAddingCardData.lanes[0].title = "Disallowed adding card";
12 | disallowAddingCardData.lanes[0].disallowAddingCard = true;
13 | Board.displayName = "Board";
14 |
15 | export default ({
16 | title: "Editable Board",
17 | component: Board,
18 | } satisfies Meta);
19 | type Story = StoryObj;
20 | export const AddDeleteCards: Story = {
21 | name: "Add/Delete Cards",
22 | render: (args) => {
23 | const shouldReceiveNewData = (nextData) => {
24 | debug("Board has changed");
25 | debug(nextData);
26 | };
27 |
28 | const handleCardDelete = (cardId, laneId) => {
29 | debug(`Card: ${cardId} deleted from lane: ${laneId}`);
30 | };
31 |
32 | const handleCardAdd = (card, laneId) => {
33 | debug(`New card added to lane ${laneId}`);
34 | debug(card);
35 | };
36 |
37 | return (
38 |
46 | alert(`Card with id:${cardId} clicked. Card in lane: ${laneId}`)
47 | }
48 | editable={true}
49 | />
50 | );
51 | },
52 | };
53 |
54 | export const AddNewLane: Story = {
55 | name: "Add New Lane",
56 | render: (args) => {
57 | return (
58 | debug(`You added a line with title ${t.title}`)}
63 | />
64 | );
65 | },
66 | };
67 |
68 | export const DisallowAddingCardForSpecificLane: Story = {
69 | name: "Disallow Adding Card for specific Lane",
70 | render: (args) => {
71 | return ;
72 | },
73 | };
74 |
75 | export const InlineEditLaneTitleAndCards: Story = {
76 | render: (args) => {
77 | return (
78 |
84 | debug(`onCardUpdate: ${cardId} -> ${JSON.stringify(data, null, 2)}`)
85 | }
86 | onLaneUpdate={(laneId, data) =>
87 | debug(`onLaneUpdate: ${laneId} -> ${data.title}`)
88 | }
89 | onLaneAdd={(t) => debug(`You added a line with title ${t.title}`)}
90 | />
91 | );
92 | },
93 | };
94 |
--------------------------------------------------------------------------------
/stories/I18n.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import React from "react";
3 | import { I18nextProvider, useTranslation } from "react-i18next";
4 |
5 | import Board from "../src";
6 | import createTranslate from "../src/helpers/createTranslate";
7 |
8 | import smallData from "./data/data-sort.json";
9 | import i18n from "./helpers/i18n";
10 | Board.displayName = "Board";
11 |
12 | const I18nBoard = () => {
13 | const { t } = useTranslation();
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
27 |
28 | );
29 | };
30 |
31 | export default ({
32 | title: "I18n",
33 | component: Board,
34 | } satisfies Meta);
35 | type Story = StoryObj;
36 | export const CustomTexts: Story = {
37 | name: "Custom texts",
38 | render: (args) => {
39 | const TEXTS = {
40 | "Add another lane": "NEW LANE",
41 | "Click to add card": "Click to add card",
42 | "Delete lane": "Delete lane",
43 | "Lane actions": "Lane actions",
44 | button: {
45 | "Add lane": "Add lane",
46 | "Add card": "Add card",
47 | Cancel: "Cancel",
48 | },
49 | placeholder: {
50 | title: "title",
51 | description: "description",
52 | label: "label",
53 | },
54 | };
55 |
56 | const customTranslation = createTranslate(TEXTS);
57 | return (
58 |
65 | );
66 | },
67 | };
68 |
69 | export const FlatTranslationTable: Story = {
70 | name: "Flat translation table",
71 | render: (args) => {
72 | const FLAT_TRANSLATION_TABLE = {
73 | "Add another lane": "+ Weitere Liste erstellen",
74 | "Click to add card": "Klicken zum Erstellen einer Karte",
75 | "Delete lane": "Liste löschen",
76 | "Lane actions": "Listenaktionen",
77 | "button.Add lane": "Liste hinzufügen",
78 | "button.Add card": "Karte hinzufügen",
79 | "button.Cancel": "Abbrechen",
80 | "placeholder.title": "Titel",
81 | "placeholder.description": "Beschreibung",
82 | "placeholder.label": "Label",
83 | };
84 |
85 | return (
86 | FLAT_TRANSLATION_TABLE[key]}
89 | editable={true}
90 | canAddLanes={true}
91 | draggable={true}
92 | />
93 | );
94 | },
95 | };
96 |
97 | export const UsingI18next: Story = {
98 | name: "Using i18next",
99 | render: (args) => {
100 | return (
101 |
102 |
103 |
104 | );
105 | },
106 | };
107 |
--------------------------------------------------------------------------------
/stories/MultipleBoards.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import React from "react";
3 |
4 | import Board from "../src";
5 |
6 | Board.displayName = "Board";
7 |
8 | import data1 from "./data/base.json";
9 | import data2 from "./data/other-board.json";
10 |
11 | const containerStyles = {
12 | height: 500,
13 | padding: 20,
14 | };
15 |
16 | export default ({
17 | title: "Multiple Boards",
18 | tags: ["autodocs"],
19 | component: Board,
20 | } satisfies Meta);
21 | type Story = StoryObj;
22 | export const MultipleBoards: Story = {
23 | render: (args) => {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/stories/PaginationAndEvents.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj, storiesOf } from "@storybook/react";
2 | import React, { FC, PropsWithChildren } from "react";
3 |
4 | import Board from "../src";
5 | import { BoardData } from "../src/types/Board";
6 | import { EventBusHandle } from "../src/types/EventBus";
7 |
8 | const PER_PAGE = 15;
9 | Board.displayName = "Board";
10 |
11 | function generateCards(requestedPage = 1) {
12 | const cards = [];
13 | const fetchedItems = (requestedPage - 1) * PER_PAGE;
14 | for (let i = fetchedItems + 1; i <= fetchedItems + PER_PAGE; i++) {
15 | cards.push({
16 | id: `${i}`,
17 | title: `Card${i}`,
18 | description: `Description for #${i}`,
19 | metadata: { cardId: `${i}` },
20 | });
21 | }
22 | return cards;
23 | }
24 | const delayedPromise = (durationInMs, resolutionPayload) => {
25 | return new Promise((resolve) => {
26 | setTimeout(() => {
27 | resolve(resolutionPayload);
28 | }, durationInMs);
29 | });
30 | };
31 | const BoardWrapper: FC> = ({
32 | data,
33 | children,
34 | }) => {
35 | const [boardData, setBoardData] = React.useState(data);
36 | const [eventBus, setEventBus] = React.useState();
37 | const onDataChange = (newData: BoardData) => {
38 | setBoardData(newData);
39 | };
40 | const refreshCards = () => {
41 | setBoardData({
42 | lanes: [
43 | {
44 | id: "Lane1",
45 | title: "Changed Lane",
46 | cards: [],
47 | },
48 | ],
49 | });
50 | };
51 | const addCard = () => {
52 | eventBus.publish({
53 | type: "ADD_CARD",
54 | laneId: "Lane1",
55 | card: {
56 | id: "000",
57 | title: "EC2 Instance Down",
58 | label: "30 mins",
59 | description: "Main EC2 instance down",
60 | metadata: { cardId: "000" },
61 | },
62 | });
63 | };
64 | const paginate = (requestedPage, laneId) => {
65 | const newCards = generateCards(requestedPage);
66 | return delayedPromise(2000, newCards);
67 | };
68 |
69 | return (
70 |
71 |
74 |
77 | setEventBus(eventBus)}
80 | laneSortFunction={(card1, card2) =>
81 | parseInt(card1.id) - parseInt(card2.id)
82 | }
83 | onLaneScroll={paginate}
84 | onDataChange={onDataChange}
85 | />
86 |
87 | );
88 | };
89 |
90 | export default ({
91 | title: "Advanced Features",
92 | component: Board,
93 | } satisfies Meta);
94 |
95 | type Story = StoryObj;
96 |
97 | export const ScrollingAndEvents: Story = {
98 | name: "Scrolling and Events",
99 | render: (args) => {
100 | const data = {
101 | lanes: [
102 | {
103 | id: "Lane1",
104 | title: "Lane1",
105 | cards: generateCards(),
106 | },
107 | ],
108 | };
109 |
110 | return ;
111 | },
112 | };
113 |
--------------------------------------------------------------------------------
/stories/Realtime.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, storiesOf } from "@storybook/react";
2 | import React, { Component } from "react";
3 | import debug from "./helpers/debug";
4 |
5 | import Board from "../src";
6 |
7 | import data from "./data/base.json";
8 | Board.displayName = "Board";
9 |
10 | class RealtimeBoard extends Component {
11 | state = { boardData: data, eventBus: undefined };
12 |
13 | setEventBus = (handle) => {
14 | this.state.eventBus = handle;
15 | };
16 |
17 | completeMilkEvent = () => {
18 | this.state.eventBus.publish({
19 | type: "REMOVE_CARD",
20 | laneId: "PLANNED",
21 | cardId: "Milk",
22 | });
23 | this.state.eventBus.publish({
24 | type: "ADD_CARD",
25 | laneId: "COMPLETED",
26 | card: {
27 | id: "Milk",
28 | title: "Buy Milk",
29 | label: "15 mins",
30 | description: "Use Headspace app",
31 | },
32 | });
33 | };
34 |
35 | addBlockedEvent = () => {
36 | this.state.eventBus.publish({
37 | type: "ADD_CARD",
38 | laneId: "BLOCKED",
39 | card: {
40 | id: "Ec2Error",
41 | title: "EC2 Instance Down",
42 | label: "30 mins",
43 | description: "Main EC2 instance down",
44 | },
45 | });
46 | };
47 |
48 | modifyLaneTitle = () => {
49 | const data = {
50 | ...this.state.boardData,
51 | lanes: this.state.boardData.lanes.map((lane, index) => {
52 | if (index === 1) {
53 | return { ...lane, title: "New Lane Title" };
54 | }
55 | return lane;
56 | }),
57 | };
58 |
59 | this.setState({ boardData: data });
60 | };
61 |
62 | modifyCardTitle = () => {
63 | const data = {
64 | ...this.state.boardData,
65 | lanes: this.state.boardData.lanes.map((lane, index) => {
66 | if (index === 1) {
67 | return {
68 | ...lane,
69 | cards: lane.cards.map((card, index) => {
70 | if (index === 0) {
71 | return { ...card, title: "New Card Title" };
72 | }
73 | return card;
74 | }),
75 | };
76 | }
77 | return lane;
78 | }),
79 | };
80 | this.setState({ boardData: data });
81 | };
82 |
83 | updateCard = () => {
84 | this.state.eventBus.publish({
85 | type: "UPDATE_CARD",
86 | laneId: "PLANNED",
87 | card: {
88 | id: "Plan2",
89 | title: "UPDATED Dispose Garbage",
90 | label: "45 mins",
91 | description: "UPDATED Sort out recyclable and waste as needed",
92 | },
93 | });
94 | };
95 |
96 | prioritizeWriteBlog = () => {
97 | this.state.eventBus.publish({
98 | type: "MOVE_CARD",
99 | fromLaneId: "PLANNED",
100 | toLaneId: "WIP",
101 | cardId: "Plan3",
102 | index: 0,
103 | });
104 | };
105 |
106 | shouldReceiveNewData = (nextData) => {
107 | debug("data has changed");
108 | debug(nextData);
109 | };
110 |
111 | addCardWithIndex = () => {
112 | this.state.eventBus.publish({
113 | type: "ADD_CARD",
114 | laneId: "WIP",
115 | card: {
116 | id: "AddWithIndex",
117 | title: "Urgent Task",
118 | label: "5 mins",
119 | description: "Everything is down",
120 | },
121 | index: 0,
122 | });
123 | };
124 |
125 | render() {
126 | return (
127 |
128 |
131 |
134 |
137 |
140 |
143 |
146 |
149 |
154 |
155 | );
156 | }
157 | }
158 |
159 | export default ({
160 | title: "Advanced Features",
161 | component: Board,
162 | } satisfies Meta);
163 |
164 | export const RealtimeEvents = {
165 | name: "Realtime Events",
166 | render: (args) => ,
167 | };
168 |
--------------------------------------------------------------------------------
/stories/RestrictedLanes.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj, storiesOf } from "@storybook/react";
2 | import React from "react";
3 |
4 | import Board from "../src";
5 |
6 | import data from "./data/drag-drop.json";
7 | Board.displayName = "Board";
8 |
9 | export default ({
10 | title: "Drag-n-Drop",
11 | component: Board,
12 | parameters: {
13 | info: "A demonstration of onDragStart and onDragEnd hooks for card and lanes",
14 | },
15 | } satisfies Meta);
16 |
17 | type Story = StoryObj;
18 |
19 | export const RestrictLanes: Story = {
20 | name: "Restrict lanes",
21 | render: (args) => {
22 | return ;
23 | },
24 | };
25 |
26 | export const DragCardsNotLanes: Story = {
27 | name: "Drag Cards not Lanes",
28 | render: (args) => {
29 | return ;
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/stories/Styling.story.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from "@storybook/react";
2 | import React from "react";
3 |
4 | import Board from "../src";
5 | import { BoardData } from "../src/types/Board";
6 | import "./board.css";
7 | import data from "./data/base.json";
8 | Board.displayName = "Board";
9 |
10 | const dataWithLaneStyles = {
11 | lanes: [
12 | {
13 | id: "PLANNED",
14 | title: "Planned Tasks",
15 | label: "20/70",
16 | style: {
17 | width: 280,
18 | backgroundColor: "#3179ba",
19 | color: "#fff",
20 | boxShadow: "2px 2px 4px 0px rgba(0,0,0,0.75)",
21 | },
22 | cards: [
23 | {
24 | id: "Milk",
25 | title: "Buy milk",
26 | label: "15 mins",
27 | description: "2 Gallons of milk at the Deli store",
28 | },
29 | {
30 | id: "Plan2",
31 | title: "Dispose Garbage",
32 | label: "10 mins",
33 | description: "Sort out recyclable and waste as needed",
34 | },
35 | ],
36 | },
37 | {
38 | id: "DONE",
39 | title: "Doned tasks",
40 | label: "10/70",
41 | style: {
42 | width: 280,
43 | backgroundColor: "#ba7931",
44 | color: "#fff",
45 | boxShadow: "2px 2px 4px 0px rgba(0,0,0,0.75)",
46 | },
47 | cards: [
48 | {
49 | id: "burn",
50 | title: "Burn Garbage",
51 | label: "10 mins",
52 | description: "Sort out recyclable and waste as needed",
53 | },
54 | ],
55 | },
56 | {
57 | id: "ARCHIVE",
58 | title: "Archived tasks",
59 | label: "1/2",
60 | cards: [
61 | {
62 | id: "archived",
63 | title: "Archived",
64 | label: "10 mins",
65 | },
66 | ],
67 | },
68 | ],
69 | };
70 |
71 | const dataWithCardStyles: BoardData = {
72 | lanes: [
73 | {
74 | id: "PLANNED",
75 | title: "Planned Tasks",
76 | label: "20/70",
77 | cards: [
78 | {
79 | id: "Milk",
80 | title: "Buy milk",
81 | label: "15 mins",
82 | description: "2 Gallons of milk at the Deli store",
83 | style: { backgroundColor: "#eec" },
84 | },
85 | {
86 | id: "Plan2",
87 | title: "Dispose Garbage",
88 | label: "10 mins",
89 | description: "Sort out recyclable and waste as needed",
90 | },
91 | {
92 | id: "Plan3",
93 | title: "Burn Garbage",
94 | label: "20 mins",
95 | },
96 | ],
97 | },
98 | ],
99 | };
100 |
101 | export default ({
102 | title: "Styling",
103 | component: Board,
104 | } satisfies Meta);
105 |
106 | type Story = StoryObj;
107 |
108 | export const BoardStyling: Story = {
109 | name: "Board Styling",
110 | render: (args) => (
111 |
116 | ),
117 | };
118 |
119 | export const LaneStyling: Story = {
120 | name: "Lane Styling",
121 | render: (args) => (
122 |
127 | ),
128 | };
129 |
130 | export const CardStyling: Story = {
131 | name: "Card Styling",
132 | render: (args) => (
133 |
134 | ),
135 | };
136 |
--------------------------------------------------------------------------------
/stories/__snapshots__/CustomCard.story.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Custom Components Card smoke-test 1`] = `
4 |
7 |
8 |
9 |
10 |
14 |
15 |
18 | Planned Tasks
19 |
20 |
21 |
22 | 12/12
23 |
24 |
25 |
26 |
27 |
28 |
29 |
32 |
33 |
34 | John Smith
35 |
36 |
37 | due in a day
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 | SMS received at 12:13pm today
48 |
49 |
50 |
51 | Thanks. Please schedule me for an estimate on Monday.
52 |
53 |
54 |
55 | Escalated to OPS-ESCALATIONS!
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 | Card Weathers
67 |
68 |
69 | due now
70 |
71 |
72 |
75 |
76 |
77 |
78 |
79 | Email received at 1:14pm
80 |
81 |
82 |
83 | Is the estimate free, and can someone call me soon?
84 |
85 |
86 |
87 | Escalated to Admin
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
100 |
101 |
102 | Long Lane name this is i suppose ha!
103 |
104 |
105 |
106 |
107 |
108 |
111 |
124 |
125 |
126 | Email received at 4:23pm today
127 |
128 |
129 |
130 | You are welcome. Interested in doing business with you again
131 |
132 |
133 |
134 | Escalated to OPS-ESCALATIONS!
135 |
136 |
137 |
140 | Critical
141 |
142 |
145 | 2d ETA
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | `;
159 |
--------------------------------------------------------------------------------
/stories/__snapshots__/CustomCardWithDrag.story.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Custom Components DragnDropStyling smoke-test 1`] = `
4 |
7 |
8 |
9 |
10 |
13 |
14 |
15 | Planned
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 | John Smith
28 |
29 |
30 |
31 |
32 | SMS received at 12:13pm today
33 |
34 |
35 |
36 | Thanks. Please schedule me for an estimate on Monday.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
48 |
49 | Card Weathers
50 |
51 |
52 |
53 |
54 | Email received at 1:14pm
55 |
56 |
57 |
58 | Is the estimate free, and can someone call me soon?
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 | Work In Progress
75 |
76 |
77 |
78 |
79 |
80 |
84 |
85 |
86 | Michael Caine
87 |
88 |
89 |
90 |
91 | Email received at 4:23pm today
92 |
93 |
94 |
95 | You are welcome. Interested in doing business with you again
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | `;
109 |
--------------------------------------------------------------------------------
/stories/__snapshots__/CustomLaneHeader.story.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Custom Components LaneHeader smoke-test 1`] = `
4 |
7 |
8 |
9 |
13 |
14 |
15 |
16 | Planned Tasks
17 |
18 |
19 |
24 |
25 |
26 |
27 | Percentage: 0/
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
40 | John Smith
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 | Thanks. Please schedule me for an estimate on Monday.
52 |
53 |
54 |
55 |
56 |
59 |
60 |
63 | Card Weathers
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 | Email received at 1:14pm
75 |
76 |
77 |
78 |
79 |
80 |
81 |
85 |
86 |
98 |
99 | Percentage: 0/
100 |
101 |
102 |
103 |
104 |
105 |
108 |
122 |
123 | You are welcome. Interested in doing business with you again
124 |
125 |
126 |
129 | Critical
130 |
131 |
134 | 2d ETA
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | `;
146 |
--------------------------------------------------------------------------------
/stories/__snapshots__/CustomNewLaneForm.story.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Custom Components NewLaneForm smoke-test 1`] = `
4 |
7 |
8 |
9 |
12 |
13 |
14 | Sorted Lane
15 |
16 |
17 |
18 | 20/70
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
32 |
43 |
44 |
45 |
56 |
57 |
58 |
61 |
62 |
63 |
64 |
75 |
76 |
77 |
78 |
79 |
82 |
117 |
118 |
129 |
130 |
131 |
132 |
133 |
136 |
171 |
172 |
183 |
184 |
185 |
186 |
187 |
190 |
225 |
226 |
237 |
238 |
239 |
240 |
241 |
242 | Click to add card
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
253 |
254 |
255 |
256 | `;
257 |
--------------------------------------------------------------------------------
/stories/__snapshots__/CustomNewLaneSection.story.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Custom Components NewLaneSection smoke-test 1`] = `
4 |
7 |
8 |
9 |
12 |
13 |
14 | Sorted Lane
15 |
16 |
17 |
18 | 20/70
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
32 |
43 |
44 |
45 |
56 |
57 |
58 |
61 |
62 |
63 |
64 |
75 |
76 |
77 |
78 |
79 |
82 |
117 |
118 |
129 |
130 |
131 |
132 |
133 |
136 |
171 |
172 |
183 |
184 |
185 |
186 |
187 |
190 |
225 |
226 |
237 |
238 |
239 |
240 |
241 |
242 | Click to add card
243 |
244 |
245 |
246 |
247 |
248 |
249 |
252 |
253 |
254 | `;
255 |
--------------------------------------------------------------------------------
/stories/board.css:
--------------------------------------------------------------------------------
1 | .boardContainer {
2 | background-color: #4BBF6B;
3 | }
--------------------------------------------------------------------------------
/stories/data/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Planned Tasks",
6 | "label": "20/70",
7 | "style": { "width": 280 },
8 | "cards": [
9 | {
10 | "id": "Milk",
11 | "title": "Buy milk",
12 | "label": "15 mins",
13 | "description": "2 Gallons of milk at the Deli store"
14 | },
15 | {
16 | "id": "Plan2",
17 | "title": "Dispose Garbage",
18 | "label": "10 mins",
19 | "description": "Sort out recyclable and waste as needed"
20 | },
21 | {
22 | "id": "Plan3",
23 | "title": "Write Blog",
24 | "label": "30 mins",
25 | "description": "Can AI make memes?"
26 | },
27 | {
28 | "id": "Plan4",
29 | "title": "Pay Rent",
30 | "label": "5 mins",
31 | "description": "Transfer to bank account"
32 | }
33 | ]
34 | },
35 | {
36 | "id": "WIP",
37 | "title": "Work In Progress",
38 | "label": "10/20",
39 | "style": { "width": 280 },
40 | "cards": [
41 | {
42 | "id": "Wip1",
43 | "title": "Clean House",
44 | "label": "30 mins",
45 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
46 | }
47 | ]
48 | },
49 | {
50 | "id": "BLOCKED",
51 | "title": "Blocked",
52 | "label": "0/0",
53 | "style": { "width": 280 },
54 | "cards": []
55 | },
56 | {
57 | "id": "COMPLETED",
58 | "title": "Completed",
59 | "style": { "width": 280 },
60 | "label": "2/5",
61 | "cards": [
62 | {
63 | "id": "Completed1",
64 | "title": "Practice Meditation",
65 | "label": "15 mins",
66 | "description": "Use Headspace app"
67 | },
68 | {
69 | "id": "Completed2",
70 | "title": "Maintain Daily Journal",
71 | "label": "15 mins",
72 | "description": "Use Spreadsheet for now"
73 | }
74 | ]
75 | },
76 | {
77 | "id": "REPEAT",
78 | "title": "Repeat",
79 | "style": { "width": 280 },
80 | "label": "1/1",
81 | "cards": [
82 | {
83 | "id": "Repeat1",
84 | "title": "Morning Jog",
85 | "label": "30 mins",
86 | "description": "Track using fitbit"
87 | }
88 | ]
89 | },
90 | {
91 | "id": "ARCHIVED",
92 | "title": "Archived",
93 | "style": { "width": 280 },
94 | "label": "1/1",
95 | "cards": [
96 | {
97 | "id": "Archived1",
98 | "title": "Go Trekking",
99 | "label": "300 mins",
100 | "description": "Completed 10km on cycle"
101 | }
102 | ]
103 | },
104 | {
105 | "id": "ARCHIVED2",
106 | "title": "Archived2",
107 | "style": { "width": 280 },
108 | "label": "1/1",
109 | "cards": [
110 | {
111 | "id": "Archived2",
112 | "title": "Go Jogging",
113 | "label": "300 mins",
114 | "description": "Completed 10km on cycle"
115 | }
116 | ]
117 | },
118 | {
119 | "id": "ARCHIVED3",
120 | "title": "Archived3",
121 | "style": { "width": 280 },
122 | "label": "1/1",
123 | "cards": [
124 | {
125 | "id": "Archived3",
126 | "title": "Go Cycling",
127 | "label": "300 mins",
128 | "description": "Completed 10km on cycle"
129 | }
130 | ]
131 | }
132 | ]
133 | }
134 |
--------------------------------------------------------------------------------
/stories/data/board_with_custom_width.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Planned Tasks",
6 | "label": "20/70",
7 | "style": { "width": 280 },
8 | "cards": [
9 | {
10 | "id": "Milk",
11 | "title": "Buy milk",
12 | "label": "15 mins",
13 | "cardStyle": {
14 | "width": 270,
15 | "maxWidth": 270,
16 | "margin": "auto",
17 | "marginBottom": 5
18 | },
19 | "description": "2 Gallons of milk at the Deli store"
20 | },
21 | {
22 | "id": "Plan2",
23 | "title": "Dispose Garbage",
24 | "label": "10 mins",
25 | "cardStyle": {
26 | "width": 270,
27 | "maxWidth": 270,
28 | "margin": "auto",
29 | "marginBottom": 5
30 | },
31 | "description": "Sort out recyclable and waste as needed"
32 | },
33 | {
34 | "id": "Plan3",
35 | "title": "Write Blog",
36 | "label": "30 mins",
37 | "cardStyle": {
38 | "width": 270,
39 | "maxWidth": 270,
40 | "margin": "auto",
41 | "marginBottom": 5
42 | },
43 | "description": "Can AI make memes?"
44 | },
45 | {
46 | "id": "Plan4",
47 | "title": "Pay Rent",
48 | "label": "5 mins",
49 | "cardStyle": {
50 | "width": 270,
51 | "maxWidth": 270,
52 | "margin": "auto",
53 | "marginBottom": 5
54 | },
55 | "description": "Transfer to bank account"
56 | }
57 | ]
58 | },
59 | {
60 | "id": "WIP",
61 | "title": "Work In Progress",
62 | "label": "10/20",
63 | "style": { "width": 280 },
64 | "cards": [
65 | {
66 | "id": "Wip1",
67 | "title": "Clean House",
68 | "label": "30 mins",
69 | "cardStyle": {
70 | "width": 270,
71 | "maxWidth": 270,
72 | "margin": "auto",
73 | "marginBottom": 5
74 | },
75 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
76 | }
77 | ]
78 | },
79 | {
80 | "id": "BLOCKED",
81 | "title": "Blocked",
82 | "label": "0/0",
83 | "style": { "width": 280 },
84 | "cards": []
85 | },
86 | {
87 | "id": "COMPLETED",
88 | "title": "Completed",
89 | "style": { "width": 280 },
90 | "label": "2/5",
91 | "cards": [
92 | {
93 | "id": "Completed1",
94 | "title": "Practice Meditation",
95 | "label": "15 mins",
96 | "cardStyle": {
97 | "width": 270,
98 | "maxWidth": 270,
99 | "margin": "auto",
100 | "marginBottom": 5
101 | },
102 | "description": "Use Headspace app"
103 | },
104 | {
105 | "id": "Completed2",
106 | "title": "Maintain Daily Journal",
107 | "label": "15 mins",
108 | "cardStyle": {
109 | "width": 270,
110 | "maxWidth": 270,
111 | "margin": "auto",
112 | "marginBottom": 5
113 | },
114 | "description": "Use Spreadsheet for now"
115 | }
116 | ]
117 | },
118 | {
119 | "id": "REPEAT",
120 | "title": "Repeat",
121 | "style": { "width": 280 },
122 | "label": "1/1",
123 | "cards": [
124 | {
125 | "id": "Repeat1",
126 | "title": "Morning Jog",
127 | "label": "30 mins",
128 | "cardStyle": {
129 | "width": 270,
130 | "maxWidth": 270,
131 | "margin": "auto",
132 | "marginBottom": 5
133 | },
134 | "description": "Track using fitbit"
135 | }
136 | ]
137 | },
138 | {
139 | "id": "ARCHIVED",
140 | "title": "Archived",
141 | "style": { "width": 280 },
142 | "label": "1/1",
143 | "cards": [
144 | {
145 | "id": "Archived1",
146 | "title": "Go Trekking",
147 | "label": "300 mins",
148 | "cardStyle": {
149 | "width": 270,
150 | "maxWidth": 270,
151 | "margin": "auto",
152 | "marginBottom": 5
153 | },
154 | "description": "Completed 10km on cycle"
155 | }
156 | ]
157 | },
158 | {
159 | "id": "ARCHIVED2",
160 | "title": "Archived2",
161 | "style": { "width": 280 },
162 | "label": "1/1",
163 | "cards": [
164 | {
165 | "id": "Archived1",
166 | "title": "Go Trekking",
167 | "label": "300 mins",
168 | "cardStyle": {
169 | "width": 270,
170 | "maxWidth": 270,
171 | "margin": "auto",
172 | "marginBottom": 5
173 | },
174 | "description": "Completed 10km on cycle"
175 | }
176 | ]
177 | },
178 | {
179 | "id": "ARCHIVED3",
180 | "title": "Archived3",
181 | "style": { "width": 280 },
182 | "label": "1/1",
183 | "cards": [
184 | {
185 | "id": "Archived1",
186 | "title": "Go Trekking",
187 | "label": "300 mins",
188 | "cardStyle": {
189 | "width": 270,
190 | "maxWidth": 270,
191 | "margin": "auto",
192 | "marginBottom": 5
193 | },
194 | "description": "Completed 10km on cycle"
195 | }
196 | ]
197 | }
198 | ]
199 | }
200 |
--------------------------------------------------------------------------------
/stories/data/collapsible.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Double Click Here",
6 | "label": "20/70",
7 | "style": { "width": 280 },
8 | "cards": [
9 | {
10 | "id": "Milk",
11 | "title": "Buy milk",
12 | "label": "15 mins",
13 | "description": "2 Gallons of milk at the Deli store"
14 | },
15 | {
16 | "id": "Plan2",
17 | "title": "Dispose Garbage",
18 | "label": "10 mins",
19 | "description": "Sort out recyclable and waste as needed"
20 | },
21 | {
22 | "id": "Plan3",
23 | "title": "Write Blog",
24 | "label": "30 mins",
25 | "description": "Can AI make memes?"
26 | },
27 | {
28 | "id": "Plan4",
29 | "title": "Pay Rent",
30 | "label": "5 mins",
31 | "description": "Transfer to bank account"
32 | }
33 | ]
34 | },
35 | {
36 | "id": "WIP",
37 | "title": "Work In Progress",
38 | "label": "10/20",
39 | "style": { "width": 280 },
40 | "cards": [
41 | {
42 | "id": "Wip1",
43 | "title": "Clean House",
44 | "label": "30 mins",
45 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
46 | }
47 | ]
48 | },
49 | {
50 | "id": "BLOCKED",
51 | "title": "Blocked",
52 | "label": "0/0",
53 | "style": { "width": 280 },
54 | "cards": []
55 | },
56 | {
57 | "id": "COMPLETED",
58 | "title": "Completed",
59 | "style": { "width": 280 },
60 | "label": "2/5",
61 | "cards": [
62 | {
63 | "id": "Completed1",
64 | "title": "Practice Meditation",
65 | "label": "15 mins",
66 | "description": "Use Headspace app"
67 | },
68 | {
69 | "id": "Completed2",
70 | "title": "Maintain Daily Journal",
71 | "label": "15 mins",
72 | "description": "Use Spreadsheet for now"
73 | }
74 | ]
75 | },
76 | {
77 | "id": "REPEAT",
78 | "title": "Repeat",
79 | "style": { "width": 280 },
80 | "label": "1/1",
81 | "cards": [
82 | {
83 | "id": "Repeat1",
84 | "title": "Morning Jog",
85 | "label": "30 mins",
86 | "description": "Track using fitbit"
87 | }
88 | ]
89 | },
90 | {
91 | "id": "ARCHIVED",
92 | "title": "Archived",
93 | "style": { "width": 280 },
94 | "label": "1/1",
95 | "cards": [
96 | {
97 | "id": "Archived1",
98 | "title": "Go Trekking",
99 | "label": "300 mins",
100 | "description": "Completed 10km on cycle"
101 | }
102 | ]
103 | },
104 | {
105 | "id": "ARCHIVED2",
106 | "title": "Archived2",
107 | "style": { "width": 280 },
108 | "label": "1/1",
109 | "cards": [
110 | {
111 | "id": "Archived2",
112 | "title": "Go Jogging",
113 | "label": "300 mins",
114 | "description": "Completed 10km on cycle"
115 | }
116 | ]
117 | },
118 | {
119 | "id": "ARCHIVED3",
120 | "title": "Archived3",
121 | "style": { "width": 280 },
122 | "label": "1/1",
123 | "cards": [
124 | {
125 | "id": "Archived3",
126 | "title": "Go Cycling",
127 | "label": "300 mins",
128 | "description": "Completed 10km on cycle"
129 | }
130 | ]
131 | }
132 | ]
133 | }
134 |
--------------------------------------------------------------------------------
/stories/data/data-sort.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "SORTED_LANE",
5 | "title": "Sorted Lane",
6 | "label": "20/70",
7 | "cards": [
8 | {
9 | "id": "Card1",
10 | "title": "Buy milk",
11 | "label": "2017-12-01",
12 | "description": "2 Gallons of milk at the Deli store",
13 | "metadata": {
14 | "completedAt": "2017-12-01T10:00:00Z",
15 | "shortCode": "abc"
16 | }
17 | },
18 | {
19 | "id": "Card2",
20 | "title": "Dispose Garbage",
21 | "label": "2017-11-01",
22 | "description": "Sort out recyclable and waste as needed",
23 | "metadata": {
24 | "completedAt": "2017-11-01T10:00:00Z",
25 | "shortCode": "aaa"
26 | }
27 | },
28 | {
29 | "id": "Card3",
30 | "title": "Write Blog",
31 | "label": "2017-10-01",
32 | "description": "Can AI make memes?",
33 | "metadata": {
34 | "completedAt": "2017-10-01T10:00:00Z",
35 | "shortCode": "fa1"
36 | }
37 | },
38 | {
39 | "id": "Card4",
40 | "title": "Pay Rent",
41 | "label": "2017-09-01",
42 | "description": "Transfer to bank account",
43 | "metadata": {
44 | "completedAt": "2017-09-01T10:00:00Z",
45 | "shortCode": "ga2"
46 | }
47 | }
48 | ]
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/stories/data/drag-drop.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "PLANNED",
5 | "title": "Planned Tasks",
6 | "label": "20/70",
7 | "cards": [
8 | {
9 | "id": "Milk",
10 | "title": "Buy milk",
11 | "label": "15 mins",
12 | "description": "2 Gallons of milk at the Deli store"
13 | },
14 | {
15 | "id": "Plan2",
16 | "title": "Dispose Garbage",
17 | "label": "10 mins",
18 | "description": "Sort out recyclable and waste as needed"
19 | },
20 | {
21 | "id": "Plan3",
22 | "title": "Write Blog",
23 | "label": "30 mins",
24 | "description": "Can AI make memes?"
25 | },
26 | {
27 | "id": "Plan4",
28 | "title": "Pay Rent",
29 | "label": "5 mins",
30 | "description": "Transfer to bank account"
31 | }
32 | ]
33 | },
34 | {
35 | "id": "WIP",
36 | "title": "Work In Progress (Not Droppable)",
37 | "label": "10/20",
38 | "droppable": false,
39 | "cards": [
40 | {
41 | "id": "Wip1",
42 | "title": "Clean House",
43 | "label": "30 mins",
44 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
45 | }
46 | ]
47 | },
48 | {
49 | "id": "COMPLETED",
50 | "title": "Completed (Droppable)",
51 | "label": "0/0",
52 | "style": { "width": 280 },
53 | "cards": []
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/stories/data/other-board.json:
--------------------------------------------------------------------------------
1 | {
2 | "lanes": [
3 | {
4 | "id": "yesterday",
5 | "title": "Yesterday",
6 | "label": "20/70",
7 | "cards": [
8 | {
9 | "id": "Wip1",
10 | "title": "Clean House",
11 | "label": "30 mins",
12 | "description": "Soap wash and polish floor. Polish windows and doors. Scrap all broken glasses"
13 | }
14 | ]
15 | },
16 | {
17 | "id": "today",
18 | "title": "Today",
19 | "label": "10/20",
20 | "droppable": false,
21 | "cards": [
22 | {
23 | "id": "Milk",
24 | "title": "Buy milk",
25 | "label": "15 mins",
26 | "description": "2 Gallons of milk at the Deli store"
27 | },
28 | {
29 | "id": "Plan2",
30 | "title": "Dispose Garbage",
31 | "label": "10 mins",
32 | "description": "Sort out recyclable and waste as needed"
33 | },
34 | {
35 | "id": "Plan3",
36 | "title": "Write Blog",
37 | "label": "30 mins",
38 | "description": "Can AI make memes?"
39 | },
40 | {
41 | "id": "Plan4",
42 | "title": "Pay Rent",
43 | "label": "5 mins",
44 | "description": "Transfer to bank account"
45 | }
46 | ]
47 | },
48 | {
49 | "id": "tomorrow",
50 | "title": "Tomorrow",
51 | "label": "0/0",
52 | "cards": []
53 | }
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/stories/drag.css:
--------------------------------------------------------------------------------
1 | .draggingCard {
2 | background-color: #7fffd4;
3 | border: 1px dashed #a5916c;
4 | transform: rotate(2deg);
5 | }
6 |
7 | .draggingLane {
8 | background-color: #ffaecf;
9 | transform: rotate(2deg);
10 | border: 1px dashed #a5916c;
11 | }
12 |
--------------------------------------------------------------------------------
/stories/helpers/debug.js:
--------------------------------------------------------------------------------
1 | export default (message) => {
2 | if (process.env.NODE_ENV === "test") {
3 | return;
4 | }
5 | if (typeof message === "object") {
6 | console.dir(message);
7 | } else {
8 | console.log(message);
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/stories/helpers/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 |
4 | import resources from "../../src/locales";
5 |
6 | i18n
7 | .use(initReactI18next) // passes i18n down to react-i18next
8 | .init({
9 | resources,
10 | lng: "en",
11 | });
12 |
13 | export default i18n;
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
4 | "jsx": "react", /* Specify what JSX code is generated. */
5 | "moduleResolution": "Node",
6 | "declaration": true, /* Specify what module code is generated. */
7 | "baseUrl": ".",/* Specify the base directory to resolve non-relative module names. */
8 | "paths": {"rt/*":["./src/*"], "@/*": ["./src/*"]}, /* Specify a set of entries that re-map imports to additional lookup locations. */
9 | "resolveJsonModule": true, /* Enable importing .json files. */
10 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
11 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
12 | "allowJs": true,
13 | "strict": false, /* Enable all strict type-checking options. */
14 | "strictFunctionTypes": false, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
15 | "skipLibCheck": true,
16 | "emitDeclarationOnly": true,
17 | "allowSyntheticDefaultImports": true,
18 | },
19 | "include": [
20 | "./src/**/*.ts",
21 | "./src/**/*.tsx",
22 | "./src/**/*.js",
23 | "./src/**/*.jsx"
24 | ],
25 | "exclude": ["node_modules", "dist", "stories"]
26 | }
27 |
--------------------------------------------------------------------------------