├── .babelrc.json ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .nvmrc ├── .nycrc ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── .storybook ├── main.ts └── preview.ts ├── .typescript ├── build │ └── tsconfig.json └── test │ └── tsconfig.json ├── .vscode ├── konva-timeline-scripts.code-snippets └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── column-width.png ├── completed-percentage.png ├── customRes.png ├── gantt.png ├── label-display.png ├── localized.png ├── personalized-tooltip.png ├── resolution-2hrs.png ├── resolution-5mins.png ├── row-height.png ├── sample.gif ├── selected-area.png ├── summary.png ├── timezones-dst.png ├── usage-empty.png └── usage-final.png ├── commitlint.config.ts ├── docs ├── 167.6e7593ac.iframe.bundle.js ├── 272.16fc9b14.iframe.bundle.js ├── 29.e177b690.iframe.bundle.js ├── 294.df985fa0.iframe.bundle.js ├── 326.fed19fdd.iframe.bundle.js ├── 326.fed19fdd.iframe.bundle.js.LICENSE.txt ├── 340.97a648a0.iframe.bundle.js ├── 364.b0c5aa16.iframe.bundle.js ├── 469.a923e37c.iframe.bundle.js ├── 52.902cfeae.iframe.bundle.js ├── 562.2842e84a.iframe.bundle.js ├── 593.7bb0237e.iframe.bundle.js ├── 713.c708b291.iframe.bundle.js ├── 713.c708b291.iframe.bundle.js.LICENSE.txt ├── 713.c708b291.iframe.bundle.js.map ├── 735.ac364800.iframe.bundle.js ├── 839.794cbf5c.iframe.bundle.js ├── 839.794cbf5c.iframe.bundle.js.LICENSE.txt ├── 839.794cbf5c.iframe.bundle.js.map ├── 84.db190d20.iframe.bundle.js ├── 869.0727f12e.iframe.bundle.js ├── 977.3c7ed0cb.iframe.bundle.js ├── favicon.svg ├── iframe.html ├── index.html ├── index.json ├── main.d7054dd0.iframe.bundle.js ├── nunito-sans-bold-italic.woff2 ├── nunito-sans-bold.woff2 ├── nunito-sans-italic.woff2 ├── nunito-sans-regular.woff2 ├── project.json ├── runtime~main.3082cb60.iframe.bundle.js ├── sb-addons │ ├── essentials-actions-3 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── essentials-backgrounds-4 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── essentials-controls-2 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── essentials-measure-7 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── essentials-outline-8 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── essentials-toolbars-6 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── essentials-viewport-5 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── interactions-9 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ ├── links-1 │ │ ├── manager-bundle.js │ │ └── manager-bundle.js.LEGAL.txt │ └── storybook-core-core-server-presets-0 │ │ ├── common-manager-bundle.js │ │ └── common-manager-bundle.js.LEGAL.txt ├── sb-common-assets │ ├── favicon.svg │ ├── nunito-sans-bold-italic.woff2 │ ├── nunito-sans-bold.woff2 │ ├── nunito-sans-italic.woff2 │ └── nunito-sans-regular.woff2 ├── sb-manager │ ├── globals-module-info.js │ ├── globals-runtime.js │ ├── globals.js │ └── runtime.js ├── sb-preview │ ├── globals.js │ └── runtime.js ├── src-KonvaTimeline-index-stories.847b19e7.iframe.bundle.js ├── src-KonvaTimeline-scenario-gantt-stories.f51f20b8.iframe.bundle.js ├── src-KonvaTimeline-scenario-monthly-stories.e9b97071.iframe.bundle.js ├── src-KonvaTimeline-scenario-yearly-stories.492906fc.iframe.bundle.js ├── src-resources-components-Header-index-stories.3175ab2b.iframe.bundle.js ├── src-resources-components-Layer-index-stories.2880c71c.iframe.bundle.js ├── src-stories-index-mdx.18118867.iframe.bundle.js ├── src-tasks-components-Layer-index-stories.bf03dd76.iframe.bundle.js ├── src-tasks-components-Task-index-stories.139734d7.iframe.bundle.js └── src-tasks-components-Tooltip-index-stories.e269595d.iframe.bundle.js ├── package-lock.json ├── package.json └── src ├── @konva └── index.tsx ├── @stories ├── assets │ ├── code-brackets.svg │ ├── colors.svg │ ├── comments.svg │ ├── direction.svg │ ├── flow.svg │ ├── plugin.svg │ ├── repo.svg │ └── stackalt.svg └── index.mdx ├── KonvaTimeline ├── index.stories.tsx ├── index.tsx ├── scenario-gantt.stories.tsx ├── scenario-monthly.stories.tsx └── scenario-yearly.stories.tsx ├── grid ├── Cell │ └── index.tsx ├── CellGroup │ └── index.tsx ├── Cells │ └── index.tsx ├── Layer │ └── index.tsx ├── Row │ └── index.tsx └── Rows │ └── index.tsx ├── index.ts ├── resources ├── components │ ├── Header │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Layer │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── Summary │ │ └── index.tsx │ └── SummaryHeader │ │ └── index.tsx └── utils │ ├── resources.test.ts │ └── resources.ts ├── tasks ├── components │ ├── Layer │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── LayerLine │ │ └── index.tsx │ ├── Line │ │ └── index.tsx │ ├── Task │ │ ├── index.stories.tsx │ │ └── index.tsx │ ├── TaskLine │ │ └── index.tsx │ ├── TaskResizeHandle │ │ └── index.tsx │ └── Tooltip │ │ ├── DefaultToolTip │ │ └── index.tsx │ │ ├── index.stories.tsx │ │ └── index.tsx └── utils │ ├── line.ts │ ├── tasks.test.ts │ └── tasks.ts ├── timeline ├── TimelineContext.tsx └── index.tsx └── utils ├── dimensions.ts ├── konva.ts ├── logger.ts ├── operations.ts ├── stories ├── decorators │ ├── Gantt.tsx │ ├── Tasks.tsx │ └── Timeline.tsx └── utils.ts ├── theme.test.ts ├── theme.ts ├── time-resolution.ts ├── time.test.ts ├── time.ts ├── timeBlockArray.ts ├── timeline.ts └── utils.ts /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /tsconfig.json 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /dist 15 | /docs 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | report.*.json 24 | CHANGELOG.md 25 | 26 | # log files 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local libs 32 | /.yalc 33 | yalc.lock 34 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:prettier/recommended", 8 | "plugin:react/recommended", 9 | "plugin:react-hooks/recommended", 10 | "plugin:storybook/recommended", 11 | "prettier" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "plugins": ["@typescript-eslint", "prettier", "simple-import-sort"], 15 | "overrides": [ 16 | { 17 | "files": ["*.js", "*.jsx", "*.ts", "*.tsx"], 18 | "rules": { 19 | "no-console": ["error", { "allow": ["error", "warn"] }], 20 | "simple-import-sort/imports": [ 21 | "error", 22 | { 23 | "groups": [ 24 | ["^\\u0000"], 25 | ["^react", "^@?\\w"], 26 | ["^\\.\\.(?!/?$)", "^\\.\\./?$"], 27 | ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"] 28 | ] 29 | } 30 | ] 31 | } 32 | } 33 | ], 34 | "rules": { 35 | "prettier/prettier": ["error"], 36 | "simple-import-sort/imports": "error" 37 | }, 38 | "settings": { 39 | "react": { 40 | "version": "detect" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Konva Timeline - Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Install NodeJS 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: lts/iron 21 | 22 | - name: Cache Dependencies 23 | id: cache-dependencies 24 | uses: actions/cache@v3 25 | env: 26 | cache-name: konva-timeline-dependencies 27 | with: 28 | path: node_modules 29 | key: konva-timeline-dependencies-${{ hashFiles('**/package-lock.json') }} 30 | 31 | - name: Install Dependencies 32 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 33 | run: npm ci 34 | 35 | - name: Git Config 36 | run: | 37 | git config --local user.email "${{ secrets.GIT_USER_EMAIL }}" 38 | git config --local user.name "${{ secrets.GIT_USER_NAME }}" 39 | git pull 40 | 41 | - name: Run ESLint and Prettier 42 | id: code-format 43 | run: | 44 | npm run clean:code 45 | git add . 46 | echo "formatted-files=$(git status -s -uno | wc -l)" >> $GITHUB_OUTPUT 47 | 48 | - name: Commit Changes 49 | if: steps.code-format.outputs.formatted-files > 0 50 | run: | 51 | git commit -m "refactor: 💡 Clean code" -a 52 | 53 | - name: Unit tests 54 | run: | 55 | npm run test:ci 56 | 57 | - name: Generate Docs 58 | id: generate-docs 59 | run: | 60 | npm run build:storybook 61 | git add . 62 | echo "generated-docs=$(git status -s -uno | wc -l)" >> $GITHUB_OUTPUT 63 | 64 | - name: Commit Changes 65 | if: steps.generate-docs.outputs.generated-docs > 0 66 | run: | 67 | git commit -m "docs: ✏️ Updated StoryBook" -a 68 | 69 | - name: Test runner 70 | run: | 71 | npx playwright install --with-deps 72 | npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ 73 | "npx http-server docs --port 6006 --silent" \ 74 | "npx wait-on tcp:127.0.0.1:6006 && npm run test:sb" 75 | 76 | - name: Build 77 | run: npm run build 78 | 79 | - name: Release 80 | env: 81 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_USER_EMAIL }} 82 | GIT_AUTHOR_NAME: ${{ secrets.GIT_USER_NAME }} 83 | GIT_COMMITTER_EMAIL: ${{ secrets.GIT_USER_EMAIL }} 84 | GIT_COMMITTER_NAME: ${{ secrets.GIT_USER_NAME }} 85 | GITHUB_TOKEN: ${{ secrets.PA_TOKEN }} 86 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 87 | run: npx semantic-release 88 | 89 | - name: Send Notification 90 | uses: Co-qn/google-chat-notification@master 91 | with: 92 | name: Konva Timeline - Release 93 | url: ${{ secrets.GOOGLE_CHAT }} 94 | status: ${{ job.status }} 95 | if: always() 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | report.*.json 22 | 23 | # log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local libs 29 | /.yalc 30 | yalc.lock 31 | 32 | # typescript local config 33 | /tsconfig.json 34 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit ${1} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run pre:commit 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run pre:push 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.15.1 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": true, 4 | "branches": 30, 5 | "functions": 30, 6 | "lines": 30, 7 | "statements": 30 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /tsconfig.json 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /dist 15 | /docs 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | report.*.json 24 | CHANGELOG.md 25 | 26 | # log files 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local libs 32 | /.yalc 33 | yalc.lock 34 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [{ "name": "main" }], 3 | "plugins": [ 4 | [ 5 | "@semantic-release/commit-analyzer", 6 | { 7 | "preset": "angular", 8 | "releaseRules": [ 9 | { "type": "chore", "release": "patch" }, 10 | { "type": "ci", "release": "patch" }, 11 | { "type": "docs", "release": "patch" }, 12 | { "type": "perf", "release": "patch" }, 13 | { "type": "refactor", "release": "patch" }, 14 | { "type": "style", "release": "patch" } 15 | ], 16 | "parserOpts": { 17 | "noteKeywords": ["BREAKING", "BREAKING CHANGE", "BREAKING CHANGES"] 18 | } 19 | } 20 | ], 21 | "@semantic-release/release-notes-generator", 22 | "@semantic-release/changelog", 23 | "@semantic-release/npm", 24 | "@semantic-release/github", 25 | "@semantic-release/git" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-webpack5"; 2 | 3 | const config: StorybookConfig = { 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | "@storybook/addon-coverage", 9 | "@storybook/addon-webpack5-compiler-babel", 10 | "@storybook/addon-mdx-gfm", 11 | ], 12 | docs: {}, 13 | framework: { 14 | name: "@storybook/react-webpack5", 15 | options: {}, 16 | }, 17 | stories: ["../(src|stories)/**/*.mdx", "../(src|stories)/**/*.stories.@(js|jsx|ts|tsx)"], 18 | typescript: { 19 | check: false, 20 | reactDocgen: "react-docgen-typescript", 21 | skipCompiler: false, 22 | }, 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export default preview; 15 | -------------------------------------------------------------------------------- /.typescript/build/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react", 8 | "module": "Node16", 9 | "moduleResolution": "Node16", 10 | "outDir": "./dist", 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "target": "ES2016" 14 | }, 15 | "include": ["src"], 16 | "exclude": ["dist", "node_modules", "src/**/*.test.*", "src/**/*.stories.*", "src/@stories/*", "src/utils/stories/*"] 17 | } 18 | -------------------------------------------------------------------------------- /.typescript/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react", 8 | "module": "Node16", 9 | "moduleResolution": "Node16", 10 | "noEmit": true, 11 | "outDir": "./dist", 12 | "skipLibCheck": true, 13 | "strict": true, 14 | "target": "ES2016" 15 | }, 16 | "include": ["src"], 17 | "exclude": ["dist", "node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/konva-timeline-scripts.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "TSReact Component": { 3 | "prefix": "tsreact-component", 4 | "description": "Create a new TSReact component", 5 | "body": [ 6 | "import React from \"react\";", 7 | "", 8 | "interface $1Props {}", 9 | "", 10 | "const $1 = ({}: $1Props) => {", 11 | " return null;", 12 | "};", 13 | "", 14 | "export default $1;", 15 | ], 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | "files.eol": "\n", 6 | "typescript.tsdk": "node_modules/typescript/lib" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Melfore 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 | -------------------------------------------------------------------------------- /assets/column-width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/column-width.png -------------------------------------------------------------------------------- /assets/completed-percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/completed-percentage.png -------------------------------------------------------------------------------- /assets/customRes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/customRes.png -------------------------------------------------------------------------------- /assets/gantt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/gantt.png -------------------------------------------------------------------------------- /assets/label-display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/label-display.png -------------------------------------------------------------------------------- /assets/localized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/localized.png -------------------------------------------------------------------------------- /assets/personalized-tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/personalized-tooltip.png -------------------------------------------------------------------------------- /assets/resolution-2hrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/resolution-2hrs.png -------------------------------------------------------------------------------- /assets/resolution-5mins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/resolution-5mins.png -------------------------------------------------------------------------------- /assets/row-height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/row-height.png -------------------------------------------------------------------------------- /assets/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/sample.gif -------------------------------------------------------------------------------- /assets/selected-area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/selected-area.png -------------------------------------------------------------------------------- /assets/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/summary.png -------------------------------------------------------------------------------- /assets/timezones-dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/timezones-dst.png -------------------------------------------------------------------------------- /assets/usage-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/usage-empty.png -------------------------------------------------------------------------------- /assets/usage-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/assets/usage-final.png -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from "@commitlint/types"; 2 | 3 | const configuration: UserConfig = { 4 | extends: ["@commitlint/config-conventional"], 5 | ignores: [(message) => message.startsWith("chore(release)")], 6 | }; 7 | 8 | export default configuration; 9 | -------------------------------------------------------------------------------- /docs/167.6e7593ac.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[167],{"./node_modules/@storybook/addon-docs/dist/DocsRenderer-CFRXHY34.mjs":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.d(__webpack_exports__,{DocsRenderer:function(){return DocsRenderer}});var react=__webpack_require__("./node_modules/react/index.js"),dist=__webpack_require__("./node_modules/@storybook/blocks/dist/index.mjs"),react_18=__webpack_require__("./node_modules/@storybook/react-dom-shim/dist/react-18.mjs"),defaultComponents={code:dist.XA,a:dist.zE,...dist.Sw},ErrorBoundary=class extends react.Component{constructor(){super(...arguments),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(err){let{showException:showException}=this.props;showException(err)}render(){let{hasError:hasError}=this.state,{children:children}=this.props;return hasError?null:react.createElement(react.Fragment,null,children)}},DocsRenderer=class{constructor(){this.render=async(context,docsParameter,element)=>{let components={...defaultComponents,...docsParameter?.components},TDocs=dist.kQ;return new Promise(((resolve,reject)=>{__webpack_require__.e(294).then(__webpack_require__.bind(__webpack_require__,"./node_modules/@mdx-js/react/index.js")).then((({MDXProvider:MDXProvider})=>(0,react_18.renderElement)(react.createElement(ErrorBoundary,{showException:reject,key:Math.random()},react.createElement(MDXProvider,{components:components},react.createElement(TDocs,{context:context,docsParameter:docsParameter}))),element))).then((()=>resolve()))}))},this.unmount=element=>{(0,react_18.unmountElement)(element)}}};__webpack_require__("./node_modules/@storybook/addon-docs/dist/chunk-H6MOWX77.mjs")},"./node_modules/@storybook/blocks/dist sync recursive":function(module){function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=function(){return[]},webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/@storybook/blocks/dist sync recursive",module.exports=webpackEmptyContext},"./node_modules/@storybook/core/dist/components sync recursive":function(module){function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=function(){return[]},webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/@storybook/core/dist/components sync recursive",module.exports=webpackEmptyContext},"./node_modules/@storybook/core/dist/theming sync recursive":function(module){function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=function(){return[]},webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/@storybook/core/dist/theming sync recursive",module.exports=webpackEmptyContext},"./node_modules/@storybook/react-dom-shim/dist/react-18.mjs":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.d(__webpack_exports__,{renderElement:function(){return renderElement},unmountElement:function(){return unmountElement}});var react__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/react/index.js"),react_dom_client__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__("./node_modules/react-dom/client.js"),nodes=new Map;var WithCallback=({callback:callback,children:children})=>{let once=react__WEBPACK_IMPORTED_MODULE_0__.useRef();return react__WEBPACK_IMPORTED_MODULE_0__.useLayoutEffect((()=>{once.current!==callback&&(once.current=callback,callback())}),[callback]),children};typeof Promise.withResolvers>"u"&&(Promise.withResolvers=()=>{let resolve=null,reject=null;return{promise:new Promise(((res,rej)=>{resolve=res,reject=rej})),resolve:resolve,reject:reject}});var renderElement=async(node,el,rootOptions)=>{let root=await getReactRoot(el,rootOptions);if(function getIsReactActEnvironment(){return globalThis.IS_REACT_ACT_ENVIRONMENT}())return void root.render(node);let{promise:promise,resolve:resolve}=Promise.withResolvers();return root.render(react__WEBPACK_IMPORTED_MODULE_0__.createElement(WithCallback,{callback:resolve},node)),promise},unmountElement=(el,shouldUseNewRootApi)=>{let root=nodes.get(el);root&&(root.unmount(),nodes.delete(el))},getReactRoot=async(el,rootOptions)=>{let root=nodes.get(el);return root||(root=react_dom_client__WEBPACK_IMPORTED_MODULE_1__.createRoot(el,rootOptions),nodes.set(el,root)),root}}}]); -------------------------------------------------------------------------------- /docs/294.df985fa0.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[294],{"./node_modules/@mdx-js/react/index.js":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{MDXProvider:function(){return _lib_index_js__WEBPACK_IMPORTED_MODULE_0__.x},useMDXComponents:function(){return _lib_index_js__WEBPACK_IMPORTED_MODULE_0__.R}});var _lib_index_js__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@mdx-js/react/lib/index.js")},"./node_modules/@mdx-js/react/lib/index.js":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){__webpack_require__.d(__webpack_exports__,{R:function(){return useMDXComponents},x:function(){return MDXProvider}});var react__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/react/index.js");const emptyComponents={},MDXContext=react__WEBPACK_IMPORTED_MODULE_0__.createContext(emptyComponents);function useMDXComponents(components){const contextComponents=react__WEBPACK_IMPORTED_MODULE_0__.useContext(MDXContext);return react__WEBPACK_IMPORTED_MODULE_0__.useMemo((function(){return"function"==typeof components?components(contextComponents):{...contextComponents,...components}}),[contextComponents,components])}function MDXProvider(properties){let allComponents;return allComponents=properties.disableParentContext?"function"==typeof properties.components?properties.components(emptyComponents):properties.components||emptyComponents:useMDXComponents(properties.components),react__WEBPACK_IMPORTED_MODULE_0__.createElement(MDXContext.Provider,{value:allComponents},properties.children)}}}]); -------------------------------------------------------------------------------- /docs/326.fed19fdd.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-reconciler-constants.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react-reconciler.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | -------------------------------------------------------------------------------- /docs/364.b0c5aa16.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[364],{"./node_modules/storybook/core/components/index.js":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){__webpack_require__.d(__webpack_exports__,{createCopyToClipboardFunction:function(){return _storybook_core_components__WEBPACK_IMPORTED_MODULE_0__.zH}});var _storybook_core_components__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@storybook/core/dist/components/index.js")}}]); -------------------------------------------------------------------------------- /docs/713.c708b291.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-jsx-runtime.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | -------------------------------------------------------------------------------- /docs/735.ac364800.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[735,773],{"./node_modules/@storybook/react-dom-shim/dist/react-18.mjs":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){__webpack_require__.d(__webpack_exports__,{renderElement:function(){return renderElement},unmountElement:function(){return unmountElement}});var react__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/react/index.js"),react_dom_client__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__("./node_modules/react-dom/client.js"),nodes=new Map;var WithCallback=({callback:callback,children:children})=>{let once=react__WEBPACK_IMPORTED_MODULE_0__.useRef();return react__WEBPACK_IMPORTED_MODULE_0__.useLayoutEffect((()=>{once.current!==callback&&(once.current=callback,callback())}),[callback]),children};typeof Promise.withResolvers>"u"&&(Promise.withResolvers=()=>{let resolve=null,reject=null;return{promise:new Promise(((res,rej)=>{resolve=res,reject=rej})),resolve:resolve,reject:reject}});var renderElement=async(node,el,rootOptions)=>{let root=await getReactRoot(el,rootOptions);if(function getIsReactActEnvironment(){return globalThis.IS_REACT_ACT_ENVIRONMENT}())return void root.render(node);let{promise:promise,resolve:resolve}=Promise.withResolvers();return root.render(react__WEBPACK_IMPORTED_MODULE_0__.createElement(WithCallback,{callback:resolve},node)),promise},unmountElement=(el,shouldUseNewRootApi)=>{let root=nodes.get(el);root&&(root.unmount(),nodes.delete(el))},getReactRoot=async(el,rootOptions)=>{let root=nodes.get(el);return root||(root=react_dom_client__WEBPACK_IMPORTED_MODULE_1__.createRoot(el,rootOptions),nodes.set(el,root)),root}},"./node_modules/react-dom/client.js":function(__unused_webpack_module,exports,__webpack_require__){var m=__webpack_require__("./node_modules/react-dom/index.js");exports.createRoot=m.createRoot,exports.hydrateRoot=m.hydrateRoot}}]); -------------------------------------------------------------------------------- /docs/839.794cbf5c.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-dom-test-utils.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react-dom.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | 21 | /** 22 | * @license React 23 | * react.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** 32 | * @license React 33 | * scheduler.production.min.js 34 | * 35 | * Copyright (c) Facebook, Inc. and its affiliates. 36 | * 37 | * This source code is licensed under the MIT license found in the 38 | * LICENSE file in the root directory of this source tree. 39 | */ 40 | -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @storybook/core - Storybook 7 | 8 | 9 | 10 | 11 | 12 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 75 | 76 | 77 | 78 | 79 |
80 | 81 | 82 | 144 | 145 | 146 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /docs/main.d7054dd0.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[792],{"./node_modules/@storybook/instrumenter/dist sync recursive":function(module){function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=function(){return[]},webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/@storybook/instrumenter/dist sync recursive",module.exports=webpackEmptyContext},"./storybook-config-entry.js":function(__unused_webpack_module,__unused_webpack___webpack_exports__,__webpack_require__){"use strict";var external_STORYBOOK_MODULE_CHANNELS_=__webpack_require__("storybook/internal/channels"),external_STORYBOOK_MODULE_PREVIEW_API_=__webpack_require__("storybook/internal/preview-api"),external_STORYBOOK_MODULE_GLOBAL_=__webpack_require__("@storybook/global");const importers=[async path=>{if(!/^\.[\\/](?:(src|stories)(?:\/(?!\.)(?:(?:(?!(?:^|\/)\.).)*?)\/|\/|$)(?!\.)(?=.)[^/]*?\.mdx)$/.exec(path))return;const pathRemainder=path.substring(2);return __webpack_require__("./. lazy recursive ^\\.\\/.*$ include: (?%21.*node_modules)(?:(src%7Cstories)(?:\\/(?%21\\.)(?:(?:(?%21(?:^%7C\\/)\\.).)*?)\\/%7C\\/%7C$)(?%21\\.)(?=.)[^/]*?\\.mdx)$")("./"+pathRemainder)},async path=>{if(!/^\.[\\/](?:(src|stories)(?:\/(?!\.)(?:(?:(?!(?:^|\/)\.).)*?)\/|\/|$)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|ts|tsx))$/.exec(path))return;const pathRemainder=path.substring(2);return __webpack_require__("./. lazy recursive ^\\.\\/.*$ include: (?%21.*node_modules)(?:(src%7Cstories)(?:\\/(?%21\\.)(?:(?:(?%21(?:^%7C\\/)\\.).)*?)\\/%7C\\/%7C$)(?%21\\.)(?=.)[^/]*?\\.stories\\.(js%7Cjsx%7Cts%7Ctsx))$")("./"+pathRemainder)}];const channel=(0,external_STORYBOOK_MODULE_CHANNELS_.createBrowserChannel)({page:"preview"});external_STORYBOOK_MODULE_PREVIEW_API_.addons.setChannel(channel),"DEVELOPMENT"===external_STORYBOOK_MODULE_GLOBAL_.global.CONFIG_TYPE&&(window.__STORYBOOK_SERVER_CHANNEL__=channel);const preview=new external_STORYBOOK_MODULE_PREVIEW_API_.PreviewWeb((async function importFn(path){for(let i=0;iimporters[i](path),x());if(moduleExports)return moduleExports}var x}),(()=>(0,external_STORYBOOK_MODULE_PREVIEW_API_.composeConfigs)([__webpack_require__("./node_modules/@storybook/react/dist/entry-preview.mjs"),__webpack_require__("./node_modules/@storybook/react/dist/entry-preview-docs.mjs"),__webpack_require__("./node_modules/@storybook/addon-links/dist/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/docs/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/actions/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/backgrounds/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/viewport/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/measure/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/outline/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-essentials/dist/highlight/preview.mjs"),__webpack_require__("./node_modules/@storybook/addon-interactions/dist/preview.mjs"),__webpack_require__("./.storybook/preview.ts")])));window.__STORYBOOK_PREVIEW__=preview,window.__STORYBOOK_STORY_STORE__=preview.storyStore,window.__STORYBOOK_ADDONS_CHANNEL__=channel},"./node_modules/@storybook/test/dist sync recursive":function(module){function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=function(){return[]},webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/@storybook/test/dist sync recursive",module.exports=webpackEmptyContext},"./.storybook/preview.ts":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__);__webpack_exports__.default={parameters:{controls:{matchers:{color:/(background|color)$/i,date:/Date$/}}}}},"./. lazy recursive ^\\.\\/.*$ include: (?%21.*node_modules)(?:(src%7Cstories)(?:\\/(?%21\\.)(?:(?:(?%21(?:^%7C\\/)\\.).)*?)\\/%7C\\/%7C$)(?%21\\.)(?=.)[^/]*?\\.mdx)$":function(module,__unused_webpack_exports,__webpack_require__){var map={"./src/@stories/index.mdx":["./src/@stories/index.mdx",326,713,52,272,340,29,593,562,84,469,666]};function webpackAsyncContext(req){if(!__webpack_require__.o(map,req))return Promise.resolve().then((function(){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}));var ids=map[req],id=ids[0];return Promise.all(ids.slice(1).map(__webpack_require__.e)).then((function(){return __webpack_require__(id)}))}webpackAsyncContext.keys=function(){return Object.keys(map)},webpackAsyncContext.id="./. lazy recursive ^\\.\\/.*$ include: (?%21.*node_modules)(?:(src%7Cstories)(?:\\/(?%21\\.)(?:(?:(?%21(?:^%7C\\/)\\.).)*?)\\/%7C\\/%7C$)(?%21\\.)(?=.)[^/]*?\\.mdx)$",module.exports=webpackAsyncContext},"./. lazy recursive ^\\.\\/.*$ include: (?%21.*node_modules)(?:(src%7Cstories)(?:\\/(?%21\\.)(?:(?:(?%21(?:^%7C\\/)\\.).)*?)\\/%7C\\/%7C$)(?%21\\.)(?=.)[^/]*?\\.stories\\.(js%7Cjsx%7Cts%7Ctsx))$":function(module,__unused_webpack_exports,__webpack_require__){var map={"./src/KonvaTimeline/index.stories":["./src/KonvaTimeline/index.stories.tsx",326,52,272,340,29,593,562,84,469,773],"./src/KonvaTimeline/index.stories.tsx":["./src/KonvaTimeline/index.stories.tsx",326,52,272,340,29,593,562,84,469,773],"./src/KonvaTimeline/scenario-gantt.stories":["./src/KonvaTimeline/scenario-gantt.stories.tsx",326,52,272,340,29,593,562,84,40],"./src/KonvaTimeline/scenario-gantt.stories.tsx":["./src/KonvaTimeline/scenario-gantt.stories.tsx",326,52,272,340,29,593,562,84,40],"./src/KonvaTimeline/scenario-monthly.stories":["./src/KonvaTimeline/scenario-monthly.stories.tsx",326,52,272,340,29,593,562,84,889],"./src/KonvaTimeline/scenario-monthly.stories.tsx":["./src/KonvaTimeline/scenario-monthly.stories.tsx",326,52,272,340,29,593,562,84,889],"./src/KonvaTimeline/scenario-yearly.stories":["./src/KonvaTimeline/scenario-yearly.stories.tsx",326,52,272,340,29,593,562,84,676],"./src/KonvaTimeline/scenario-yearly.stories.tsx":["./src/KonvaTimeline/scenario-yearly.stories.tsx",326,52,272,340,29,593,562,84,676],"./src/resources/components/Header/index.stories":["./src/resources/components/Header/index.stories.tsx",326,52,272,593,977,1],"./src/resources/components/Header/index.stories.tsx":["./src/resources/components/Header/index.stories.tsx",326,52,272,593,977,1],"./src/resources/components/Layer/index.stories":["./src/resources/components/Layer/index.stories.tsx",326,52,272,593,977,427],"./src/resources/components/Layer/index.stories.tsx":["./src/resources/components/Layer/index.stories.tsx",326,52,272,593,977,427],"./src/tasks/components/Layer/index.stories":["./src/tasks/components/Layer/index.stories.tsx",326,52,272,340,29,562,977,204],"./src/tasks/components/Layer/index.stories.tsx":["./src/tasks/components/Layer/index.stories.tsx",326,52,272,340,29,562,977,204],"./src/tasks/components/Task/index.stories":["./src/tasks/components/Task/index.stories.tsx",326,52,272,340,977,420],"./src/tasks/components/Task/index.stories.tsx":["./src/tasks/components/Task/index.stories.tsx",326,52,272,340,977,420],"./src/tasks/components/Tooltip/index.stories":["./src/tasks/components/Tooltip/index.stories.tsx",326,52,29,977,50],"./src/tasks/components/Tooltip/index.stories.tsx":["./src/tasks/components/Tooltip/index.stories.tsx",326,52,29,977,50]};function webpackAsyncContext(req){if(!__webpack_require__.o(map,req))return Promise.resolve().then((function(){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}));var ids=map[req],id=ids[0];return Promise.all(ids.slice(1).map(__webpack_require__.e)).then((function(){return __webpack_require__(id)}))}webpackAsyncContext.keys=function(){return Object.keys(map)},webpackAsyncContext.id="./. lazy recursive ^\\.\\/.*$ include: (?%21.*node_modules)(?:(src%7Cstories)(?:\\/(?%21\\.)(?:(?:(?%21(?:^%7C\\/)\\.).)*?)\\/%7C\\/%7C$)(?%21\\.)(?=.)[^/]*?\\.stories\\.(js%7Cjsx%7Cts%7Ctsx))$",module.exports=webpackAsyncContext},"storybook/internal/channels":function(module){"use strict";module.exports=__STORYBOOK_MODULE_CHANNELS__},"storybook/internal/client-logger":function(module){"use strict";module.exports=__STORYBOOK_MODULE_CLIENT_LOGGER__},"storybook/internal/preview-errors":function(module){"use strict";module.exports=__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__},"storybook/internal/core-events":function(module){"use strict";module.exports=__STORYBOOK_MODULE_CORE_EVENTS__},"@storybook/global":function(module){"use strict";module.exports=__STORYBOOK_MODULE_GLOBAL__},"storybook/internal/preview-api":function(module){"use strict";module.exports=__STORYBOOK_MODULE_PREVIEW_API__}},function(__webpack_require__){__webpack_require__.O(0,[839],(function(){return moduleId="./storybook-config-entry.js",__webpack_require__(__webpack_require__.s=moduleId);var moduleId}));__webpack_require__.O()}]); -------------------------------------------------------------------------------- /docs/nunito-sans-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/nunito-sans-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/nunito-sans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/nunito-sans-bold.woff2 -------------------------------------------------------------------------------- /docs/nunito-sans-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/nunito-sans-italic.woff2 -------------------------------------------------------------------------------- /docs/nunito-sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/nunito-sans-regular.woff2 -------------------------------------------------------------------------------- /docs/project.json: -------------------------------------------------------------------------------- 1 | {"generatedAt":1747918514848,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@types/jest":"29.5.14"},"packageManager":{"type":"npm","version":"10.8.3"},"typescriptOptions":{"check":false,"reactDocgen":"react-docgen-typescript","skipCompiler":false},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-webpack5","options":{}},"builder":"@storybook/builder-webpack5","renderer":"@storybook/react","portableStoriesFileCount":2,"storybookVersion":"8.4.7","storybookVersionSpecifier":"^8.4.7","language":"typescript","storybookPackages":{"@storybook/blocks":{"version":"8.4.7"},"@storybook/react":{"version":"8.4.7"},"@storybook/react-webpack5":{"version":"8.4.7"},"@storybook/test":{"version":"8.4.7"},"@storybook/test-runner":{"version":"0.21.0"},"eslint-plugin-storybook":{"version":"0.9.0"},"storybook":{"version":"8.4.7"}},"addons":{"@storybook/addon-links":{"version":"8.4.7"},"@storybook/addon-essentials":{"version":"8.4.7"},"@storybook/addon-interactions":{"version":"8.4.7"},"@storybook/addon-coverage":{"version":"1.0.5"},"@storybook/addon-webpack5-compiler-babel":{"version":"3.0.5"},"@storybook/addon-mdx-gfm":{"version":"8.4.7"}}} -------------------------------------------------------------------------------- /docs/runtime~main.3082cb60.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var deferred,leafPrototypes,getProto,inProgress,__webpack_modules__={},__webpack_module_cache__={};function __webpack_require__(moduleId){var cachedModule=__webpack_module_cache__[moduleId];if(void 0!==cachedModule)return cachedModule.exports;var module=__webpack_module_cache__[moduleId]={id:moduleId,loaded:!1,exports:{}};return __webpack_modules__[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}__webpack_require__.m=__webpack_modules__,__webpack_require__.amdO={},deferred=[],__webpack_require__.O=function(result,chunkIds,fn,priority){if(!chunkIds){var notFulfilled=1/0;for(i=0;i=priority)&&Object.keys(__webpack_require__.O).every((function(key){return __webpack_require__.O[key](chunkIds[j])}))?chunkIds.splice(j--,1):(fulfilled=!1,priority0&&deferred[i-1][2]>priority;i--)deferred[i]=deferred[i-1];deferred[i]=[chunkIds,fn,priority]},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function(){return module.default}:function(){return module};return __webpack_require__.d(getter,{a:getter}),getter},getProto=Object.getPrototypeOf?function(obj){return Object.getPrototypeOf(obj)}:function(obj){return obj.__proto__},__webpack_require__.t=function(value,mode){if(1&mode&&(value=this(value)),8&mode)return value;if("object"==typeof value&&value){if(4&mode&&value.__esModule)return value;if(16&mode&&"function"==typeof value.then)return value}var ns=Object.create(null);__webpack_require__.r(ns);var def={};leafPrototypes=leafPrototypes||[null,getProto({}),getProto([]),getProto(getProto)];for(var current=2&mode&&value;"object"==typeof current&&!~leafPrototypes.indexOf(current);current=getProto(current))Object.getOwnPropertyNames(current).forEach((function(key){def[key]=function(){return value[key]}}));return def.default=function(){return value},__webpack_require__.d(ns,def),ns},__webpack_require__.d=function(exports,definition){for(var key in definition)__webpack_require__.o(definition,key)&&!__webpack_require__.o(exports,key)&&Object.defineProperty(exports,key,{enumerable:!0,get:definition[key]})},__webpack_require__.f={},__webpack_require__.e=function(chunkId){return Promise.all(Object.keys(__webpack_require__.f).reduce((function(promises,key){return __webpack_require__.f[key](chunkId,promises),promises}),[]))},__webpack_require__.u=function(chunkId){return({1:"src-resources-components-Header-index-stories",40:"src-KonvaTimeline-scenario-gantt-stories",50:"src-tasks-components-Tooltip-index-stories",204:"src-tasks-components-Layer-index-stories",420:"src-tasks-components-Task-index-stories",427:"src-resources-components-Layer-index-stories",666:"src-stories-index-mdx",676:"src-KonvaTimeline-scenario-yearly-stories",773:"src-KonvaTimeline-index-stories",889:"src-KonvaTimeline-scenario-monthly-stories"}[chunkId]||chunkId)+"."+{1:"3175ab2b",29:"e177b690",40:"f51f20b8",50:"e269595d",52:"902cfeae",84:"db190d20",167:"6e7593ac",204:"bf03dd76",272:"16fc9b14",294:"df985fa0",326:"fed19fdd",340:"97a648a0",364:"b0c5aa16",420:"139734d7",427:"2880c71c",469:"a923e37c",562:"2842e84a",593:"7bb0237e",666:"18118867",676:"492906fc",713:"c708b291",735:"ac364800",773:"847b19e7",869:"0727f12e",889:"e9b97071",977:"3c7ed0cb"}[chunkId]+".iframe.bundle.js"},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.hmd=function(module){return(module=Object.create(module)).children||(module.children=[]),Object.defineProperty(module,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+module.id)}}),module},__webpack_require__.o=function(obj,prop){return Object.prototype.hasOwnProperty.call(obj,prop)},inProgress={},__webpack_require__.l=function(url,done,key,chunkId){if(inProgress[url])inProgress[url].push(done);else{var script,needAttach;if(void 0!==key)for(var scripts=document.getElementsByTagName("script"),i=0;i{var t=__REACT__,{Children:f,Component:k,Fragment:R,Profiler:P,PureComponent:w,StrictMode:L,Suspense:E,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:D,cloneElement:M,createContext:H,createElement:v,createFactory:x,createRef:F,forwardRef:U,isValidElement:N,lazy:G,memo:W,startTransition:K,unstable_act:Y,useCallback:u,useContext:q,useDebugValue:V,useDeferredValue:Z,useEffect:d,useId:z,useImperativeHandle:J,useInsertionEffect:Q,useLayoutEffect:X,useMemo:$,useReducer:j,useRef:oo,useState:no,useSyncExternalStore:eo,useTransition:co,version:to}=__REACT__;var so=__STORYBOOK_API__,{ActiveTabs:io,Consumer:uo,ManagerContext:mo,Provider:po,RequestResponseError:So,addons:l,combineParameters:Co,controlOrMetaKey:ho,controlOrMetaSymbol:Ao,eventMatchesShortcut:bo,eventToShortcut:To,experimental_requestResponse:_o,isMacLike:go,isShortcutTaken:yo,keyToSymbol:Bo,merge:Oo,mockChannel:fo,optionOrAltSymbol:ko,shortcutMatchesShortcut:Ro,shortcutToHumanString:Po,types:m,useAddonState:wo,useArgTypes:Lo,useArgs:Eo,useChannel:Do,useGlobalTypes:Mo,useGlobals:p,useParameter:Ho,useSharedState:vo,useStoryPrepared:xo,useStorybookApi:S,useStorybookState:Fo}=__STORYBOOK_API__;var Ko=__STORYBOOK_COMPONENTS__,{A:Yo,ActionBar:qo,AddonPanel:Vo,Badge:Zo,Bar:zo,Blockquote:Jo,Button:Qo,ClipboardCode:Xo,Code:$o,DL:jo,Div:on,DocumentWrapper:nn,EmptyTabContent:en,ErrorFormatter:cn,FlexBar:tn,Form:rn,H1:In,H2:an,H3:ln,H4:sn,H5:un,H6:dn,HR:mn,IconButton:C,IconButtonSkeleton:pn,Icons:Sn,Img:Cn,LI:hn,Link:An,ListItem:bn,Loader:Tn,Modal:_n,OL:gn,P:yn,Placeholder:Bn,Pre:On,ResetWrapper:fn,ScrollArea:kn,Separator:Rn,Spaced:Pn,Span:wn,StorybookIcon:Ln,StorybookLogo:En,Symbols:Dn,SyntaxHighlighter:Mn,TT:Hn,TabBar:vn,TabButton:xn,TabWrapper:Fn,Table:Un,Tabs:Nn,TabsState:Gn,TooltipLinkList:Wn,TooltipMessage:Kn,TooltipNote:Yn,UL:qn,WithTooltip:Vn,WithTooltipPure:Zn,Zoom:zn,codeCommon:Jn,components:Qn,createCopyToClipboardFunction:Xn,getStoryHref:$n,icons:jn,interleaveSeparators:oe,nameSpaceClassNames:ne,resetComponents:ee,withReset:ce}=__STORYBOOK_COMPONENTS__;var le=__STORYBOOK_ICONS__,{AccessibilityAltIcon:se,AccessibilityIcon:ie,AddIcon:ue,AdminIcon:de,AlertAltIcon:me,AlertIcon:pe,AlignLeftIcon:Se,AlignRightIcon:Ce,AppleIcon:he,ArrowBottomLeftIcon:Ae,ArrowBottomRightIcon:be,ArrowDownIcon:Te,ArrowLeftIcon:_e,ArrowRightIcon:ge,ArrowSolidDownIcon:ye,ArrowSolidLeftIcon:Be,ArrowSolidRightIcon:Oe,ArrowSolidUpIcon:fe,ArrowTopLeftIcon:ke,ArrowTopRightIcon:Re,ArrowUpIcon:Pe,AzureDevOpsIcon:we,BackIcon:Le,BasketIcon:Ee,BatchAcceptIcon:De,BatchDenyIcon:Me,BeakerIcon:He,BellIcon:ve,BitbucketIcon:xe,BoldIcon:Fe,BookIcon:Ue,BookmarkHollowIcon:Ne,BookmarkIcon:Ge,BottomBarIcon:We,BottomBarToggleIcon:Ke,BoxIcon:Ye,BranchIcon:qe,BrowserIcon:Ve,ButtonIcon:Ze,CPUIcon:ze,CalendarIcon:Je,CameraIcon:Qe,CategoryIcon:Xe,CertificateIcon:$e,ChangedIcon:je,ChatIcon:oc,CheckIcon:nc,ChevronDownIcon:ec,ChevronLeftIcon:cc,ChevronRightIcon:tc,ChevronSmallDownIcon:rc,ChevronSmallLeftIcon:Ic,ChevronSmallRightIcon:ac,ChevronSmallUpIcon:lc,ChevronUpIcon:sc,ChromaticIcon:ic,ChromeIcon:uc,CircleHollowIcon:dc,CircleIcon:mc,ClearIcon:pc,CloseAltIcon:Sc,CloseIcon:Cc,CloudHollowIcon:hc,CloudIcon:Ac,CogIcon:bc,CollapseIcon:Tc,CommandIcon:_c,CommentAddIcon:gc,CommentIcon:yc,CommentsIcon:Bc,CommitIcon:Oc,CompassIcon:fc,ComponentDrivenIcon:kc,ComponentIcon:Rc,ContrastIcon:Pc,ControlsIcon:wc,CopyIcon:Lc,CreditIcon:Ec,CrossIcon:Dc,DashboardIcon:Mc,DatabaseIcon:Hc,DeleteIcon:vc,DiamondIcon:xc,DirectionIcon:Fc,DiscordIcon:Uc,DocChartIcon:Nc,DocListIcon:Gc,DocumentIcon:Wc,DownloadIcon:Kc,DragIcon:Yc,EditIcon:qc,EllipsisIcon:Vc,EmailIcon:Zc,ExpandAltIcon:zc,ExpandIcon:Jc,EyeCloseIcon:Qc,EyeIcon:Xc,FaceHappyIcon:$c,FaceNeutralIcon:jc,FaceSadIcon:ot,FacebookIcon:nt,FailedIcon:et,FastForwardIcon:ct,FigmaIcon:tt,FilterIcon:rt,FlagIcon:It,FolderIcon:at,FormIcon:lt,GDriveIcon:st,GithubIcon:it,GitlabIcon:ut,GlobeIcon:dt,GoogleIcon:mt,GraphBarIcon:pt,GraphLineIcon:St,GraphqlIcon:Ct,GridAltIcon:ht,GridIcon:At,GrowIcon:bt,HeartHollowIcon:Tt,HeartIcon:_t,HomeIcon:gt,HourglassIcon:yt,InfoIcon:Bt,ItalicIcon:Ot,JumpToIcon:ft,KeyIcon:kt,LightningIcon:Rt,LightningOffIcon:Pt,LinkBrokenIcon:wt,LinkIcon:Lt,LinkedinIcon:Et,LinuxIcon:Dt,ListOrderedIcon:Mt,ListUnorderedIcon:Ht,LocationIcon:vt,LockIcon:xt,MarkdownIcon:Ft,MarkupIcon:Ut,MediumIcon:Nt,MemoryIcon:Gt,MenuIcon:Wt,MergeIcon:Kt,MirrorIcon:Yt,MobileIcon:qt,MoonIcon:Vt,NutIcon:Zt,OutboxIcon:zt,OutlineIcon:Jt,PaintBrushIcon:Qt,PaperClipIcon:Xt,ParagraphIcon:$t,PassedIcon:jt,PhoneIcon:or,PhotoDragIcon:nr,PhotoIcon:er,PinAltIcon:cr,PinIcon:tr,PlayAllHollowIcon:rr,PlayBackIcon:Ir,PlayHollowIcon:ar,PlayIcon:lr,PlayNextIcon:sr,PlusIcon:ir,PointerDefaultIcon:ur,PointerHandIcon:dr,PowerIcon:mr,PrintIcon:pr,ProceedIcon:Sr,ProfileIcon:Cr,PullRequestIcon:hr,QuestionIcon:Ar,RSSIcon:br,RedirectIcon:Tr,ReduxIcon:_r,RefreshIcon:gr,ReplyIcon:yr,RepoIcon:Br,RequestChangeIcon:Or,RewindIcon:fr,RulerIcon:h,SaveIcon:kr,SearchIcon:Rr,ShareAltIcon:Pr,ShareIcon:wr,ShieldIcon:Lr,SideBySideIcon:Er,SidebarAltIcon:Dr,SidebarAltToggleIcon:Mr,SidebarIcon:Hr,SidebarToggleIcon:vr,SpeakerIcon:xr,StackedIcon:Fr,StarHollowIcon:Ur,StarIcon:Nr,StatusFailIcon:Gr,StatusPassIcon:Wr,StatusWarnIcon:Kr,StickerIcon:Yr,StopAltHollowIcon:qr,StopAltIcon:Vr,StopIcon:Zr,StorybookIcon:zr,StructureIcon:Jr,SubtractIcon:Qr,SunIcon:Xr,SupportIcon:$r,SwitchAltIcon:jr,SyncIcon:oI,TabletIcon:nI,ThumbsUpIcon:eI,TimeIcon:cI,TimerIcon:tI,TransferIcon:rI,TrashIcon:II,TwitterIcon:aI,TypeIcon:lI,UbuntuIcon:sI,UndoIcon:iI,UnfoldIcon:uI,UnlockIcon:dI,UnpinIcon:mI,UploadIcon:pI,UserAddIcon:SI,UserAltIcon:CI,UserIcon:hI,UsersIcon:AI,VSCodeIcon:bI,VerifiedIcon:TI,VideoIcon:_I,WandIcon:gI,WatchIcon:yI,WindowsIcon:BI,WrenchIcon:OI,XIcon:fI,YoutubeIcon:kI,ZoomIcon:RI,ZoomOutIcon:PI,ZoomResetIcon:wI,iconList:LI}=__STORYBOOK_ICONS__;var s="storybook/measure-addon",A=`${s}/tool`,b=()=>{let[r,c]=p(),{measureEnabled:I}=r,i=S(),a=u(()=>c({measureEnabled:!I}),[c,I]);return d(()=>{i.setAddonShortcut(s,{label:"Toggle Measure [M]",defaultShortcut:["M"],actionName:"measure",showInMenu:!1,action:a})},[a,i]),t.createElement(C,{key:A,active:I,title:"Enable measure",onClick:a},t.createElement(h,null))};l.register(s,()=>{l.add(A,{type:m.TOOL,title:"Measure",match:({viewMode:r,tabId:c})=>r==="story"&&!c,render:()=>t.createElement(b,null)})});})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/essentials-measure-7/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-addons/essentials-measure-7/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-addons/essentials-outline-8/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var t=__REACT__,{Children:k,Component:R,Fragment:P,Profiler:w,PureComponent:L,StrictMode:E,Suspense:D,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:H,cloneElement:v,createContext:x,createElement:M,createFactory:F,createRef:U,forwardRef:N,isValidElement:G,lazy:W,memo:u,startTransition:K,unstable_act:Y,useCallback:d,useContext:q,useDebugValue:V,useDeferredValue:Z,useEffect:p,useId:z,useImperativeHandle:J,useInsertionEffect:Q,useLayoutEffect:X,useMemo:$,useReducer:j,useRef:oo,useState:no,useSyncExternalStore:eo,useTransition:co,version:to}=__REACT__;var io=__STORYBOOK_API__,{ActiveTabs:so,Consumer:uo,ManagerContext:po,Provider:mo,RequestResponseError:So,addons:l,combineParameters:Co,controlOrMetaKey:ho,controlOrMetaSymbol:Ao,eventMatchesShortcut:To,eventToShortcut:_o,experimental_requestResponse:bo,isMacLike:go,isShortcutTaken:yo,keyToSymbol:Oo,merge:Bo,mockChannel:fo,optionOrAltSymbol:ko,shortcutMatchesShortcut:Ro,shortcutToHumanString:Po,types:m,useAddonState:wo,useArgTypes:Lo,useArgs:Eo,useChannel:Do,useGlobalTypes:Ho,useGlobals:S,useParameter:vo,useSharedState:xo,useStoryPrepared:Mo,useStorybookApi:C,useStorybookState:Fo}=__STORYBOOK_API__;var Ko=__STORYBOOK_COMPONENTS__,{A:Yo,ActionBar:qo,AddonPanel:Vo,Badge:Zo,Bar:zo,Blockquote:Jo,Button:Qo,ClipboardCode:Xo,Code:$o,DL:jo,Div:on,DocumentWrapper:nn,EmptyTabContent:en,ErrorFormatter:cn,FlexBar:tn,Form:rn,H1:In,H2:an,H3:ln,H4:sn,H5:un,H6:dn,HR:pn,IconButton:h,IconButtonSkeleton:mn,Icons:Sn,Img:Cn,LI:hn,Link:An,ListItem:Tn,Loader:_n,Modal:bn,OL:gn,P:yn,Placeholder:On,Pre:Bn,ResetWrapper:fn,ScrollArea:kn,Separator:Rn,Spaced:Pn,Span:wn,StorybookIcon:Ln,StorybookLogo:En,Symbols:Dn,SyntaxHighlighter:Hn,TT:vn,TabBar:xn,TabButton:Mn,TabWrapper:Fn,Table:Un,Tabs:Nn,TabsState:Gn,TooltipLinkList:Wn,TooltipMessage:Kn,TooltipNote:Yn,UL:qn,WithTooltip:Vn,WithTooltipPure:Zn,Zoom:zn,codeCommon:Jn,components:Qn,createCopyToClipboardFunction:Xn,getStoryHref:$n,icons:jn,interleaveSeparators:oe,nameSpaceClassNames:ne,resetComponents:ee,withReset:ce}=__STORYBOOK_COMPONENTS__;var le=__STORYBOOK_ICONS__,{AccessibilityAltIcon:ie,AccessibilityIcon:se,AddIcon:ue,AdminIcon:de,AlertAltIcon:pe,AlertIcon:me,AlignLeftIcon:Se,AlignRightIcon:Ce,AppleIcon:he,ArrowBottomLeftIcon:Ae,ArrowBottomRightIcon:Te,ArrowDownIcon:_e,ArrowLeftIcon:be,ArrowRightIcon:ge,ArrowSolidDownIcon:ye,ArrowSolidLeftIcon:Oe,ArrowSolidRightIcon:Be,ArrowSolidUpIcon:fe,ArrowTopLeftIcon:ke,ArrowTopRightIcon:Re,ArrowUpIcon:Pe,AzureDevOpsIcon:we,BackIcon:Le,BasketIcon:Ee,BatchAcceptIcon:De,BatchDenyIcon:He,BeakerIcon:ve,BellIcon:xe,BitbucketIcon:Me,BoldIcon:Fe,BookIcon:Ue,BookmarkHollowIcon:Ne,BookmarkIcon:Ge,BottomBarIcon:We,BottomBarToggleIcon:Ke,BoxIcon:Ye,BranchIcon:qe,BrowserIcon:Ve,ButtonIcon:Ze,CPUIcon:ze,CalendarIcon:Je,CameraIcon:Qe,CategoryIcon:Xe,CertificateIcon:$e,ChangedIcon:je,ChatIcon:oc,CheckIcon:nc,ChevronDownIcon:ec,ChevronLeftIcon:cc,ChevronRightIcon:tc,ChevronSmallDownIcon:rc,ChevronSmallLeftIcon:Ic,ChevronSmallRightIcon:ac,ChevronSmallUpIcon:lc,ChevronUpIcon:ic,ChromaticIcon:sc,ChromeIcon:uc,CircleHollowIcon:dc,CircleIcon:pc,ClearIcon:mc,CloseAltIcon:Sc,CloseIcon:Cc,CloudHollowIcon:hc,CloudIcon:Ac,CogIcon:Tc,CollapseIcon:_c,CommandIcon:bc,CommentAddIcon:gc,CommentIcon:yc,CommentsIcon:Oc,CommitIcon:Bc,CompassIcon:fc,ComponentDrivenIcon:kc,ComponentIcon:Rc,ContrastIcon:Pc,ControlsIcon:wc,CopyIcon:Lc,CreditIcon:Ec,CrossIcon:Dc,DashboardIcon:Hc,DatabaseIcon:vc,DeleteIcon:xc,DiamondIcon:Mc,DirectionIcon:Fc,DiscordIcon:Uc,DocChartIcon:Nc,DocListIcon:Gc,DocumentIcon:Wc,DownloadIcon:Kc,DragIcon:Yc,EditIcon:qc,EllipsisIcon:Vc,EmailIcon:Zc,ExpandAltIcon:zc,ExpandIcon:Jc,EyeCloseIcon:Qc,EyeIcon:Xc,FaceHappyIcon:$c,FaceNeutralIcon:jc,FaceSadIcon:ot,FacebookIcon:nt,FailedIcon:et,FastForwardIcon:ct,FigmaIcon:tt,FilterIcon:rt,FlagIcon:It,FolderIcon:at,FormIcon:lt,GDriveIcon:it,GithubIcon:st,GitlabIcon:ut,GlobeIcon:dt,GoogleIcon:pt,GraphBarIcon:mt,GraphLineIcon:St,GraphqlIcon:Ct,GridAltIcon:ht,GridIcon:At,GrowIcon:Tt,HeartHollowIcon:_t,HeartIcon:bt,HomeIcon:gt,HourglassIcon:yt,InfoIcon:Ot,ItalicIcon:Bt,JumpToIcon:ft,KeyIcon:kt,LightningIcon:Rt,LightningOffIcon:Pt,LinkBrokenIcon:wt,LinkIcon:Lt,LinkedinIcon:Et,LinuxIcon:Dt,ListOrderedIcon:Ht,ListUnorderedIcon:vt,LocationIcon:xt,LockIcon:Mt,MarkdownIcon:Ft,MarkupIcon:Ut,MediumIcon:Nt,MemoryIcon:Gt,MenuIcon:Wt,MergeIcon:Kt,MirrorIcon:Yt,MobileIcon:qt,MoonIcon:Vt,NutIcon:Zt,OutboxIcon:zt,OutlineIcon:A,PaintBrushIcon:Jt,PaperClipIcon:Qt,ParagraphIcon:Xt,PassedIcon:$t,PhoneIcon:jt,PhotoDragIcon:or,PhotoIcon:nr,PinAltIcon:er,PinIcon:cr,PlayAllHollowIcon:tr,PlayBackIcon:rr,PlayHollowIcon:Ir,PlayIcon:ar,PlayNextIcon:lr,PlusIcon:ir,PointerDefaultIcon:sr,PointerHandIcon:ur,PowerIcon:dr,PrintIcon:pr,ProceedIcon:mr,ProfileIcon:Sr,PullRequestIcon:Cr,QuestionIcon:hr,RSSIcon:Ar,RedirectIcon:Tr,ReduxIcon:_r,RefreshIcon:br,ReplyIcon:gr,RepoIcon:yr,RequestChangeIcon:Or,RewindIcon:Br,RulerIcon:fr,SaveIcon:kr,SearchIcon:Rr,ShareAltIcon:Pr,ShareIcon:wr,ShieldIcon:Lr,SideBySideIcon:Er,SidebarAltIcon:Dr,SidebarAltToggleIcon:Hr,SidebarIcon:vr,SidebarToggleIcon:xr,SpeakerIcon:Mr,StackedIcon:Fr,StarHollowIcon:Ur,StarIcon:Nr,StatusFailIcon:Gr,StatusPassIcon:Wr,StatusWarnIcon:Kr,StickerIcon:Yr,StopAltHollowIcon:qr,StopAltIcon:Vr,StopIcon:Zr,StorybookIcon:zr,StructureIcon:Jr,SubtractIcon:Qr,SunIcon:Xr,SupportIcon:$r,SwitchAltIcon:jr,SyncIcon:oI,TabletIcon:nI,ThumbsUpIcon:eI,TimeIcon:cI,TimerIcon:tI,TransferIcon:rI,TrashIcon:II,TwitterIcon:aI,TypeIcon:lI,UbuntuIcon:iI,UndoIcon:sI,UnfoldIcon:uI,UnlockIcon:dI,UnpinIcon:pI,UploadIcon:mI,UserAddIcon:SI,UserAltIcon:CI,UserIcon:hI,UsersIcon:AI,VSCodeIcon:TI,VerifiedIcon:_I,VideoIcon:bI,WandIcon:gI,WatchIcon:yI,WindowsIcon:OI,WrenchIcon:BI,XIcon:fI,YoutubeIcon:kI,ZoomIcon:RI,ZoomOutIcon:PI,ZoomResetIcon:wI,iconList:LI}=__STORYBOOK_ICONS__;var i="storybook/outline",T="outline",_=u(function(){let[c,r]=S(),s=C(),I=[!0,"true"].includes(c[T]),a=d(()=>r({[T]:!I}),[I]);return p(()=>{s.setAddonShortcut(i,{label:"Toggle Outline",defaultShortcut:["alt","O"],actionName:"outline",showInMenu:!1,action:a})},[a,s]),t.createElement(h,{key:"outline",active:I,title:"Apply outlines to the preview",onClick:a},t.createElement(A,null))});l.register(i,()=>{l.add(i,{title:"Outline",type:m.TOOL,match:({viewMode:c,tabId:r})=>!!(c&&c.match(/^(story|docs)$/))&&!r,render:()=>t.createElement(_,null)})});})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/essentials-outline-8/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-addons/essentials-outline-8/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-addons/essentials-toolbars-6/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var l=__REACT__,{Children:se,Component:ie,Fragment:ue,Profiler:ce,PureComponent:pe,StrictMode:me,Suspense:de,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:be,cloneElement:Se,createContext:Te,createElement:ye,createFactory:_e,createRef:fe,forwardRef:Ce,isValidElement:ve,lazy:Ie,memo:Oe,startTransition:Ee,unstable_act:xe,useCallback:C,useContext:ge,useDebugValue:ke,useDeferredValue:he,useEffect:g,useId:Ae,useImperativeHandle:Re,useInsertionEffect:Le,useLayoutEffect:Be,useMemo:Me,useReducer:Pe,useRef:L,useState:B,useSyncExternalStore:Ne,useTransition:we,version:Ve}=__REACT__;var We=__STORYBOOK_API__,{ActiveTabs:Ke,Consumer:Ye,ManagerContext:$e,Provider:qe,RequestResponseError:ze,addons:k,combineParameters:Ue,controlOrMetaKey:je,controlOrMetaSymbol:Ze,eventMatchesShortcut:Je,eventToShortcut:Qe,experimental_requestResponse:Xe,isMacLike:et,isShortcutTaken:tt,keyToSymbol:ot,merge:rt,mockChannel:at,optionOrAltSymbol:lt,shortcutMatchesShortcut:nt,shortcutToHumanString:st,types:M,useAddonState:it,useArgTypes:ut,useArgs:ct,useChannel:pt,useGlobalTypes:P,useGlobals:h,useParameter:mt,useSharedState:dt,useStoryPrepared:bt,useStorybookApi:N,useStorybookState:St}=__STORYBOOK_API__;var Ct=__STORYBOOK_COMPONENTS__,{A:vt,ActionBar:It,AddonPanel:Ot,Badge:Et,Bar:xt,Blockquote:gt,Button:kt,ClipboardCode:ht,Code:At,DL:Rt,Div:Lt,DocumentWrapper:Bt,EmptyTabContent:Mt,ErrorFormatter:Pt,FlexBar:Nt,Form:wt,H1:Vt,H2:Dt,H3:Ht,H4:Ft,H5:Gt,H6:Wt,HR:Kt,IconButton:w,IconButtonSkeleton:Yt,Icons:A,Img:$t,LI:qt,Link:zt,ListItem:Ut,Loader:jt,Modal:Zt,OL:Jt,P:Qt,Placeholder:Xt,Pre:eo,ResetWrapper:to,ScrollArea:oo,Separator:V,Spaced:ro,Span:ao,StorybookIcon:lo,StorybookLogo:no,Symbols:so,SyntaxHighlighter:io,TT:uo,TabBar:co,TabButton:po,TabWrapper:mo,Table:bo,Tabs:So,TabsState:To,TooltipLinkList:D,TooltipMessage:yo,TooltipNote:_o,UL:fo,WithTooltip:H,WithTooltipPure:Co,Zoom:vo,codeCommon:Io,components:Oo,createCopyToClipboardFunction:Eo,getStoryHref:xo,icons:go,interleaveSeparators:ko,nameSpaceClassNames:ho,resetComponents:Ao,withReset:Ro}=__STORYBOOK_COMPONENTS__;var K={type:"item",value:""},Y=(o,t)=>({...t,name:t.name||o,description:t.description||o,toolbar:{...t.toolbar,items:t.toolbar.items.map(e=>{let r=typeof e=="string"?{value:e,title:e}:e;return r.type==="reset"&&t.toolbar.icon&&(r.icon=t.toolbar.icon,r.hideIcon=!0),{...K,...r}})}}),$=["reset"],q=o=>o.filter(t=>!$.includes(t.type)).map(t=>t.value),S="addon-toolbars",z=async(o,t,e)=>{e&&e.next&&await o.setAddonShortcut(S,{label:e.next.label,defaultShortcut:e.next.keys,actionName:`${t}:next`,action:e.next.action}),e&&e.previous&&await o.setAddonShortcut(S,{label:e.previous.label,defaultShortcut:e.previous.keys,actionName:`${t}:previous`,action:e.previous.action}),e&&e.reset&&await o.setAddonShortcut(S,{label:e.reset.label,defaultShortcut:e.reset.keys,actionName:`${t}:reset`,action:e.reset.action})},U=o=>t=>{let{id:e,toolbar:{items:r,shortcuts:a}}=t,c=N(),[T,i]=h(),n=L([]),u=T[e],v=C(()=>{i({[e]:""})},[i]),I=C(()=>{let s=n.current,m=s.indexOf(u),d=m===s.length-1?0:m+1,p=n.current[d];i({[e]:p})},[n,u,i]),O=C(()=>{let s=n.current,m=s.indexOf(u),d=m>-1?m:0,p=d===0?s.length-1:d-1,b=n.current[p];i({[e]:b})},[n,u,i]);return g(()=>{a&&z(c,e,{next:{...a.next,action:I},previous:{...a.previous,action:O},reset:{...a.reset,action:v}})},[c,e,a,I,O,v]),g(()=>{n.current=q(r)},[]),l.createElement(o,{cycleValues:n.current,...t})},F=({currentValue:o,items:t})=>o!=null&&t.find(e=>e.value===o&&e.type!=="reset"),j=({currentValue:o,items:t})=>{let e=F({currentValue:o,items:t});if(e)return e.icon},Z=({currentValue:o,items:t})=>{let e=F({currentValue:o,items:t});if(e)return e.title},J=({active:o,disabled:t,title:e,icon:r,description:a,onClick:c})=>l.createElement(w,{active:o,title:a,disabled:t,onClick:t?()=>{}:c},r&&l.createElement(A,{icon:r,__suppressDeprecationWarning:!0}),e?`\xA0${e}`:null),Q=({right:o,title:t,value:e,icon:r,hideIcon:a,onClick:c,disabled:T,currentValue:i})=>{let n=r&&l.createElement(A,{style:{opacity:1},icon:r}),u={id:e??"_reset",active:i===e,right:o,title:t,disabled:T,onClick:c};return r&&!a&&(u.icon=n),u},X=U(({id:o,name:t,description:e,toolbar:{icon:r,items:a,title:c,preventDynamicIcon:T,dynamicTitle:i}})=>{let[n,u,v]=h(),[I,O]=B(!1),s=n[o],m=!!s,d=o in v,p=r,b=c;T||(p=j({currentValue:s,items:a})||p),i&&(b=Z({currentValue:s,items:a})||b),!b&&!p&&console.warn(`Toolbar '${t}' has no title or icon`);let G=C(x=>{u({[o]:x})},[o,u]);return l.createElement(H,{placement:"top",tooltip:({onHide:x})=>{let W=a.filter(({type:E})=>{let R=!0;return E==="reset"&&!s&&(R=!1),R}).map(E=>Q({...E,currentValue:s,disabled:d,onClick:()=>{G(E.value),x()}}));return l.createElement(D,{links:W})},closeOnOutsideClick:!0,onVisibleChange:O},l.createElement(J,{active:I||m,disabled:d,description:e||"",icon:p,title:b||""}))}),ee=()=>{let o=P(),t=Object.keys(o).filter(e=>!!o[e].toolbar);return t.length?l.createElement(l.Fragment,null,l.createElement(V,null),t.map(e=>{let r=Y(e,o[e]);return l.createElement(X,{key:e,id:e,...r})})):null};k.register(S,()=>k.add(S,{title:S,type:M.TOOL,match:({tabId:o})=>!o,render:()=>l.createElement(ee,null)}));})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/essentials-toolbars-6/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-addons/essentials-toolbars-6/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-addons/essentials-viewport-5/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-addons/essentials-viewport-5/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-addons/interactions-9/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-addons/interactions-9/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-addons/links-1/manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var E=__STORYBOOK_API__,{ActiveTabs:T,Consumer:h,ManagerContext:p,Provider:A,RequestResponseError:b,addons:a,combineParameters:O,controlOrMetaKey:R,controlOrMetaSymbol:k,eventMatchesShortcut:v,eventToShortcut:g,experimental_requestResponse:I,isMacLike:C,isShortcutTaken:M,keyToSymbol:P,merge:x,mockChannel:f,optionOrAltSymbol:q,shortcutMatchesShortcut:D,shortcutToHumanString:G,types:K,useAddonState:V,useArgTypes:$,useArgs:B,useChannel:N,useGlobalTypes:Q,useGlobals:U,useParameter:Y,useSharedState:H,useStoryPrepared:L,useStorybookApi:j,useStorybookState:w}=__STORYBOOK_API__;var e="storybook/links",n={NAVIGATE:`${e}/navigate`,REQUEST:`${e}/request`,RECEIVE:`${e}/receive`};a.register(e,t=>{t.on(n.REQUEST,({kind:u,name:S})=>{let c=t.storyId(u,S);t.emit(n.RECEIVE,c)})});})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/links-1/manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-addons/links-1/manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js: -------------------------------------------------------------------------------- 1 | try{ 2 | (()=>{var T=__STORYBOOK_API__,{ActiveTabs:_,Consumer:O,ManagerContext:f,Provider:A,RequestResponseError:v,addons:n,combineParameters:P,controlOrMetaKey:k,controlOrMetaSymbol:x,eventMatchesShortcut:M,eventToShortcut:R,experimental_requestResponse:w,isMacLike:C,isShortcutTaken:G,keyToSymbol:I,merge:K,mockChannel:q,optionOrAltSymbol:B,shortcutMatchesShortcut:F,shortcutToHumanString:Y,types:j,useAddonState:E,useArgTypes:H,useArgs:L,useChannel:N,useGlobalTypes:z,useGlobals:D,useParameter:J,useSharedState:Q,useStoryPrepared:U,useStorybookApi:V,useStorybookState:W}=__STORYBOOK_API__;var c=(()=>{let e;return typeof window<"u"?e=window:typeof globalThis<"u"?e=globalThis:typeof window<"u"?e=window:typeof self<"u"?e=self:e={},e})(),S="tag-filters",d="static-filter";n.register(S,e=>{let u=Object.entries(c.TAGS_OPTIONS??{}).reduce((t,r)=>{let[o,i]=r;return i.excludeFromSidebar&&(t[o]=!0),t},{});e.experimental_setFilter(d,t=>{let r=t.tags??[];return(r.includes("dev")||t.type==="docs")&&r.filter(o=>u[o]).length===0})});})(); 3 | }catch(e){ console.error("[Storybook] One of your manager-entries failed: " + import.meta.url, e); } 4 | -------------------------------------------------------------------------------- /docs/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js.LEGAL.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js.LEGAL.txt -------------------------------------------------------------------------------- /docs/sb-common-assets/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-bold-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-common-assets/nunito-sans-bold-italic.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-common-assets/nunito-sans-bold.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-common-assets/nunito-sans-italic.woff2 -------------------------------------------------------------------------------- /docs/sb-common-assets/nunito-sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melfore/konva-timeline/feffef3cd990b01339b384ccab0ac68d48ea3c93/docs/sb-common-assets/nunito-sans-regular.woff2 -------------------------------------------------------------------------------- /docs/sb-manager/globals.js: -------------------------------------------------------------------------------- 1 | import ESM_COMPAT_Module from "node:module"; 2 | import { fileURLToPath as ESM_COMPAT_fileURLToPath } from 'node:url'; 3 | import { dirname as ESM_COMPAT_dirname } from 'node:path'; 4 | const __filename = ESM_COMPAT_fileURLToPath(import.meta.url); 5 | const __dirname = ESM_COMPAT_dirname(__filename); 6 | const require = ESM_COMPAT_Module.createRequire(import.meta.url); 7 | 8 | // src/manager/globals/globals.ts 9 | var _ = { 10 | react: "__REACT__", 11 | "react-dom": "__REACT_DOM__", 12 | "react-dom/client": "__REACT_DOM_CLIENT__", 13 | "@storybook/icons": "__STORYBOOK_ICONS__", 14 | "storybook/internal/manager-api": "__STORYBOOK_API__", 15 | "@storybook/manager-api": "__STORYBOOK_API__", 16 | "@storybook/core/manager-api": "__STORYBOOK_API__", 17 | "storybook/internal/components": "__STORYBOOK_COMPONENTS__", 18 | "@storybook/components": "__STORYBOOK_COMPONENTS__", 19 | "@storybook/core/components": "__STORYBOOK_COMPONENTS__", 20 | "storybook/internal/channels": "__STORYBOOK_CHANNELS__", 21 | "@storybook/channels": "__STORYBOOK_CHANNELS__", 22 | "@storybook/core/channels": "__STORYBOOK_CHANNELS__", 23 | "storybook/internal/core-errors": "__STORYBOOK_CORE_EVENTS__", 24 | "@storybook/core-events": "__STORYBOOK_CORE_EVENTS__", 25 | "@storybook/core/core-events": "__STORYBOOK_CORE_EVENTS__", 26 | "storybook/internal/manager-errors": "__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__", 27 | "@storybook/core-events/manager-errors": "__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__", 28 | "@storybook/core/manager-errors": "__STORYBOOK_CORE_EVENTS_MANAGER_ERRORS__", 29 | "storybook/internal/router": "__STORYBOOK_ROUTER__", 30 | "@storybook/router": "__STORYBOOK_ROUTER__", 31 | "@storybook/core/router": "__STORYBOOK_ROUTER__", 32 | "storybook/internal/theming": "__STORYBOOK_THEMING__", 33 | "@storybook/theming": "__STORYBOOK_THEMING__", 34 | "@storybook/core/theming": "__STORYBOOK_THEMING__", 35 | "storybook/internal/theming/create": "__STORYBOOK_THEMING_CREATE__", 36 | "@storybook/theming/create": "__STORYBOOK_THEMING_CREATE__", 37 | "@storybook/core/theming/create": "__STORYBOOK_THEMING_CREATE__", 38 | "storybook/internal/client-logger": "__STORYBOOK_CLIENT_LOGGER__", 39 | "@storybook/client-logger": "__STORYBOOK_CLIENT_LOGGER__", 40 | "@storybook/core/client-logger": "__STORYBOOK_CLIENT_LOGGER__", 41 | "storybook/internal/types": "__STORYBOOK_TYPES__", 42 | "@storybook/types": "__STORYBOOK_TYPES__", 43 | "@storybook/core/types": "__STORYBOOK_TYPES__" 44 | }, o = Object.keys(_); 45 | export { 46 | o as globalPackages, 47 | _ as globalsNameReferenceMap 48 | }; 49 | -------------------------------------------------------------------------------- /docs/sb-preview/globals.js: -------------------------------------------------------------------------------- 1 | import ESM_COMPAT_Module from "node:module"; 2 | import { fileURLToPath as ESM_COMPAT_fileURLToPath } from 'node:url'; 3 | import { dirname as ESM_COMPAT_dirname } from 'node:path'; 4 | const __filename = ESM_COMPAT_fileURLToPath(import.meta.url); 5 | const __dirname = ESM_COMPAT_dirname(__filename); 6 | const require = ESM_COMPAT_Module.createRequire(import.meta.url); 7 | 8 | // src/preview/globals/globals.ts 9 | var _ = { 10 | "@storybook/global": "__STORYBOOK_MODULE_GLOBAL__", 11 | "storybook/internal/channels": "__STORYBOOK_MODULE_CHANNELS__", 12 | "@storybook/channels": "__STORYBOOK_MODULE_CHANNELS__", 13 | "@storybook/core/channels": "__STORYBOOK_MODULE_CHANNELS__", 14 | "storybook/internal/client-logger": "__STORYBOOK_MODULE_CLIENT_LOGGER__", 15 | "@storybook/client-logger": "__STORYBOOK_MODULE_CLIENT_LOGGER__", 16 | "@storybook/core/client-logger": "__STORYBOOK_MODULE_CLIENT_LOGGER__", 17 | "storybook/internal/core-events": "__STORYBOOK_MODULE_CORE_EVENTS__", 18 | "@storybook/core-events": "__STORYBOOK_MODULE_CORE_EVENTS__", 19 | "@storybook/core/core-events": "__STORYBOOK_MODULE_CORE_EVENTS__", 20 | "storybook/internal/preview-errors": "__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__", 21 | "@storybook/core-events/preview-errors": "__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__", 22 | "@storybook/core/preview-errors": "__STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__", 23 | "storybook/internal/preview-api": "__STORYBOOK_MODULE_PREVIEW_API__", 24 | "@storybook/preview-api": "__STORYBOOK_MODULE_PREVIEW_API__", 25 | "@storybook/core/preview-api": "__STORYBOOK_MODULE_PREVIEW_API__", 26 | "storybook/internal/types": "__STORYBOOK_MODULE_TYPES__", 27 | "@storybook/types": "__STORYBOOK_MODULE_TYPES__", 28 | "@storybook/core/types": "__STORYBOOK_MODULE_TYPES__" 29 | }, O = Object.keys(_); 30 | export { 31 | O as globalPackages, 32 | _ as globalsNameReferenceMap 33 | }; 34 | -------------------------------------------------------------------------------- /docs/src-KonvaTimeline-index-stories.847b19e7.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[773],{"./node_modules/react-dom/client.js":function(__unused_webpack_module,exports,__webpack_require__){var m=__webpack_require__("./node_modules/react-dom/index.js");exports.createRoot=m.createRoot,exports.hydrateRoot=m.hydrateRoot}}]); -------------------------------------------------------------------------------- /docs/src-resources-components-Header-index-stories.3175ab2b.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | (self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[1,773],{"./src/resources/components/Header/index.stories.tsx":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{Last:function(){return Last},Primary:function(){return Primary},__namedExportsOrder:function(){return __namedExportsOrder}});var _utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./src/utils/stories/decorators/Tasks.tsx");const meta={title:"Components/Resources/Header",component:__webpack_require__("./src/resources/components/Header/index.tsx").n,decorators:[_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.Jy],tags:["autodocs"],argTypes:{}};__webpack_exports__.default=meta;const Primary={args:{resource:{id:"1",color:"red",label:"Resource #1"},index:0}},Last={args:{...Primary.args,isLast:!0}},__namedExportsOrder=["Primary","Last"];Primary.parameters={...Primary.parameters,docs:{...Primary.parameters?.docs,source:{originalSource:'{\n args: {\n resource: {\n id: "1",\n color: "red",\n label: "Resource #1"\n },\n index: 0\n }\n}',...Primary.parameters?.docs?.source}}},Last.parameters={...Last.parameters,docs:{...Last.parameters?.docs,source:{originalSource:"{\n args: {\n ...Primary.args,\n isLast: true\n }\n}",...Last.parameters?.docs?.source}}}},"./node_modules/react-dom/client.js":function(__unused_webpack_module,exports,__webpack_require__){"use strict";var m=__webpack_require__("./node_modules/react-dom/index.js");exports.createRoot=m.createRoot,exports.hydrateRoot=m.hydrateRoot},"./node_modules/react-konva-utils/es/index.js":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.d(__webpack_exports__,{Ed:function(){return Html}});var react=__webpack_require__("./node_modules/react/index.js"),client=__webpack_require__("./node_modules/react-dom/client.js"),ReactKonva=__webpack_require__("./node_modules/react-konva/es/ReactKonva.js"),dist=__webpack_require__("./node_modules/its-fine/dist/index.js"),__rest=function(s,e){var t={};for(var p in s)Object.prototype.hasOwnProperty.call(s,p)&&e.indexOf(p)<0&&(t[p]=s[p]);if(null!=s&&"function"==typeof Object.getOwnPropertySymbols){var i=0;for(p=Object.getOwnPropertySymbols(s);i{const Bridge=(0,dist.y3)(),groupRef=react.useRef(null),[div]=react.useState((()=>document.createElement("div"))),root=react.useMemo((()=>client.createRoot(div)),[div]),shouldTransform=null==transform||transform,handleTransform=function useEvent(fn=()=>{}){const ref=react.useRef(fn);return ref.current=fn,react.useCallback(((...args)=>ref.current.apply(null,args)),[])}((()=>{if(shouldTransform&&groupRef.current){let attrs=groupRef.current.getAbsoluteTransform().decompose();transformFunc&&(attrs=transformFunc(attrs)),div.style.position="absolute",div.style.zIndex="10",div.style.top="0px",div.style.left="0px",div.style.transform=`translate(${attrs.x}px, ${attrs.y}px) rotate(${attrs.rotation}deg) scaleX(${attrs.scaleX}) scaleY(${attrs.scaleY})`,div.style.transformOrigin="top left"}else div.style.position="",div.style.zIndex="",div.style.top="",div.style.left="",div.style.transform="",div.style.transformOrigin="";const _a=divProps||{},{style:style}=_a,restProps=__rest(_a,["style"]);Object.assign(div.style,style),Object.assign(div,restProps)}));return react.useLayoutEffect((()=>{var _a;const group=groupRef.current;if(!group)return;const parent=null===(_a=group.getStage())||void 0===_a?void 0:_a.container();return parent?(parent.appendChild(div),shouldTransform&&(el=>{const pos=window.getComputedStyle(el).position;return!("absolute"===pos||"relative"===pos)})(parent)&&(parent.style.position="relative"),group.on("absoluteTransformChange",handleTransform),handleTransform(),()=>{var _a;group.off("absoluteTransformChange",handleTransform),null===(_a=div.parentNode)||void 0===_a||_a.removeChild(div)}):void 0}),[shouldTransform]),react.useLayoutEffect((()=>{handleTransform()}),[divProps,transformFunc]),react.useLayoutEffect((()=>{root.render(react.createElement(Bridge,null,children))})),react.useLayoutEffect((()=>()=>{setTimeout((()=>{root.unmount()}))}),[]),react.createElement(ReactKonva.YJ,Object.assign({ref:groupRef},groupProps))};__webpack_require__("./node_modules/use-image/index.js")},"./node_modules/use-image/index.js":function(module,__unused_webpack_exports,__webpack_require__){var React=__webpack_require__("./node_modules/react/index.js");module.exports=function useImage(url,crossOrigin,referrerpolicy){const statusRef=React.useRef("loading"),imageRef=React.useRef(),[_,setStateToken]=React.useState(0),oldUrl=React.useRef(),oldCrossOrigin=React.useRef(),oldReferrerPolicy=React.useRef();return oldUrl.current===url&&oldCrossOrigin.current===crossOrigin&&oldReferrerPolicy.current===referrerpolicy||(statusRef.current="loading",imageRef.current=void 0,oldUrl.current=url,oldCrossOrigin.current=crossOrigin,oldReferrerPolicy.current=referrerpolicy),React.useLayoutEffect((function(){if(url){var img=document.createElement("img");return img.addEventListener("load",onload),img.addEventListener("error",onerror),crossOrigin&&(img.crossOrigin=crossOrigin),referrerpolicy&&(img.referrerPolicy=referrerpolicy),img.src=url,function cleanup(){img.removeEventListener("load",onload),img.removeEventListener("error",onerror)}}function onload(){statusRef.current="loaded",imageRef.current=img,setStateToken(Math.random())}function onerror(){statusRef.current="failed",imageRef.current=void 0,setStateToken(Math.random())}}),[url,crossOrigin,referrerpolicy]),[imageRef.current,statusRef.current]}}}]); -------------------------------------------------------------------------------- /docs/src-tasks-components-Layer-index-stories.bf03dd76.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[204,773],{"./src/tasks/components/Layer/index.stories.tsx":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{Primary:function(){return Primary},__namedExportsOrder:function(){return __namedExportsOrder}});var _utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./src/utils/stories/decorators/Tasks.tsx");const meta={title:"Components/TaskLayer",component:__webpack_require__("./src/tasks/components/Layer/index.tsx").A,decorators:[_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.xJ],tags:["autodocs"],argTypes:{}};__webpack_exports__.default=meta;const Primary={args:{onTaskEvent:()=>{},setTaskTooltip:()=>{},taskTooltip:null}},__namedExportsOrder=["Primary"];Primary.parameters={...Primary.parameters,docs:{...Primary.parameters?.docs,source:{originalSource:"{\n args: {\n onTaskEvent: () => {},\n setTaskTooltip: () => {},\n taskTooltip: null\n }\n}",...Primary.parameters?.docs?.source}}}},"./node_modules/react-dom/client.js":function(__unused_webpack_module,exports,__webpack_require__){var m=__webpack_require__("./node_modules/react-dom/index.js");exports.createRoot=m.createRoot,exports.hydrateRoot=m.hydrateRoot}}]); -------------------------------------------------------------------------------- /docs/src-tasks-components-Task-index-stories.139734d7.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[420],{"./src/tasks/components/Task/index.stories.tsx":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{Primary:function(){return Primary},__namedExportsOrder:function(){return __namedExportsOrder}});var _utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./src/utils/stories/decorators/Tasks.tsx");const meta={title:"Components/Task",component:__webpack_require__("./src/tasks/components/Task/index.tsx").h,decorators:[_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.Jy],tags:["autodocs"],argTypes:{fill:_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.cL,stroke:_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.cL}};__webpack_exports__.default=meta;const Primary={args:{data:_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.zM.tasks[0],onLeave:()=>{},onOver:()=>{},onTaskEvent:()=>{},width:100,x:50,y:50}},__namedExportsOrder=["Primary"];Primary.parameters={...Primary.parameters,docs:{...Primary.parameters?.docs,source:{originalSource:"{\n args: {\n data: STORY_DATA.tasks[0],\n onLeave: () => {},\n onOver: () => {},\n onTaskEvent: () => {},\n width: 100,\n x: 50,\n y: 50\n }\n}",...Primary.parameters?.docs?.source}}}}}]); -------------------------------------------------------------------------------- /docs/src-tasks-components-Tooltip-index-stories.e269595d.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunk_melfore_konva_timeline=self.webpackChunk_melfore_konva_timeline||[]).push([[50,773],{"./src/tasks/components/Tooltip/index.stories.tsx":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{Primary:function(){return Primary},__namedExportsOrder:function(){return __namedExportsOrder}});var _utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./src/utils/stories/decorators/Tasks.tsx");const meta={title:"Components/TaskTooltip",component:__webpack_require__("./src/tasks/components/Tooltip/index.tsx").A,decorators:[_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.P0,_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.xJ],tags:["autodocs"],argTypes:{}};__webpack_exports__.default=meta;const Primary={args:{task:_utils_stories_decorators_Tasks__WEBPACK_IMPORTED_MODULE_0__.zM.tasks[0],x:50,y:50}},__namedExportsOrder=["Primary"];Primary.parameters={...Primary.parameters,docs:{...Primary.parameters?.docs,source:{originalSource:"{\n args: {\n task: STORY_DATA.tasks[0],\n x: 50,\n y: 50\n }\n}",...Primary.parameters?.docs?.source}}}},"./node_modules/react-dom/client.js":function(__unused_webpack_module,exports,__webpack_require__){var m=__webpack_require__("./node_modules/react-dom/index.js");exports.createRoot=m.createRoot,exports.hydrateRoot=m.hydrateRoot}}]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@melfore/konva-timeline", 3 | "version": "1.40.0", 4 | "description": "Melfore ReactJS Timeline library made with Konva", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "npm run pre:build && tsc", 12 | "build:test": "npm run pre:test && tsc", 13 | "build:storybook": "storybook build -o docs --docs --quiet", 14 | "clean:code": "eslint ./ --ext .js,.jsx,.ts,.tsx --fix && prettier ./ --write", 15 | "clean:install": "rimraf dist && rimraf coverage && rimraf node_modules && npm i", 16 | "commit": "git-cz", 17 | "pre:build": "copyfiles -a -f ./.typescript/build/tsconfig.json ./", 18 | "pre:commit": "npm run clean:code", 19 | "pre:push": "npm run test:ci && npm run build:test", 20 | "pre:test": "copyfiles -a -f ./.typescript/test/tsconfig.json ./", 21 | "prepare": "husky", 22 | "start": "npm run pre:test && storybook dev -p 6006", 23 | "test": "npm run pre:test && jest --watch", 24 | "test:ci": "npm run pre:test && jest --ci", 25 | "test:sb": "test-storybook --coverage --coverageDirectory coverage/storybook --ci --verbose", 26 | "test:sb:coverage": "npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook", 27 | "semantic-release": "semantic-release", 28 | "update:packages": "npx npm-check-updates --interactive --upgrade --doctor --doctorTest \"npm run pre:push\"" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/melfore/konva-timeline.git" 33 | }, 34 | "author": "@melfore", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/melfore/konva-timeline/issues" 38 | }, 39 | "homepage": "https://github.com/melfore/konva-timeline#readme", 40 | "peerDependencies": { 41 | "konva": ">= 9.2.0 < 10", 42 | "luxon": ">= 3.3.0 < 4", 43 | "react": ">= 18.2.0 < 20", 44 | "react-dom": ">= 18.2.0 < 20", 45 | "react-konva": ">= 18.2.9 < 20", 46 | "react-konva-utils": ">= 1.0.5 < 2" 47 | }, 48 | "devDependencies": { 49 | "@babel/plugin-transform-private-property-in-object": "^7.25.9", 50 | "@babel/preset-env": "^7.26.0", 51 | "@babel/preset-react": "^7.26.3", 52 | "@babel/preset-typescript": "^7.26.0", 53 | "@commitlint/cli": "^19.6.1", 54 | "@commitlint/config-conventional": "^19.6.0", 55 | "@semantic-release/changelog": "^6.0.3", 56 | "@semantic-release/commit-analyzer": "^13.0.1", 57 | "@semantic-release/git": "^10.0.1", 58 | "@semantic-release/github": "^11.0.1", 59 | "@semantic-release/npm": "^12.0.1", 60 | "@semantic-release/release-notes-generator": "^14.0.3", 61 | "@storybook/addon-coverage": "^1.0.5", 62 | "@storybook/addon-essentials": "^8.4.7", 63 | "@storybook/addon-interactions": "^8.4.7", 64 | "@storybook/addon-links": "^8.4.7", 65 | "@storybook/addon-mdx-gfm": "^8.4.7", 66 | "@storybook/addon-webpack5-compiler-babel": "^3.0.5", 67 | "@storybook/blocks": "^8.4.7", 68 | "@storybook/react": "^8.4.7", 69 | "@storybook/react-webpack5": "^8.4.7", 70 | "@storybook/test": "^8.4.7", 71 | "@storybook/test-runner": "^0.21.0", 72 | "@types/jest": "^29.5.14", 73 | "@types/luxon": "^3.4.2", 74 | "@types/node": "^20.14.5", 75 | "@types/react": "^18.3.10", 76 | "@types/react-dom": "^18.3.0", 77 | "@typescript-eslint/eslint-plugin": "^7.16.0", 78 | "@typescript-eslint/parser": "^7.16.0", 79 | "commitizen": "^4.3.1", 80 | "copyfiles": "^2.4.1", 81 | "cz-conventional-changelog": "^3.3.0", 82 | "eslint": "^8.57.0", 83 | "eslint-config-prettier": "^9.1.0", 84 | "eslint-plugin-prettier": "^5.2.1", 85 | "eslint-plugin-react": "^7.37.3", 86 | "eslint-plugin-react-hooks": "^4.6.2", 87 | "eslint-plugin-simple-import-sort": "^12.1.1", 88 | "eslint-plugin-storybook": "^0.9.0", 89 | "git-cz": "^4.9.0", 90 | "husky": "^9.1.7", 91 | "konva": "^9.3.18", 92 | "luxon": "^3.5.0", 93 | "prettier": "^3.4.2", 94 | "prop-types": "^15.8.1", 95 | "react": "^18.3.1", 96 | "react-dom": "^18.3.1", 97 | "react-konva": "^18.2.10", 98 | "react-konva-utils": "^1.0.7", 99 | "rimraf": "^6.0.1", 100 | "semantic-release": "^24.2.1", 101 | "storybook": "^8.4.7", 102 | "typescript": "^5.7.3" 103 | }, 104 | "browserslist": { 105 | "production": [ 106 | ">= 0.5%", 107 | "last 2 versions", 108 | "not dead" 109 | ], 110 | "development": [ 111 | "last 1 chrome version", 112 | "last 1 edge version", 113 | "last 1 firefox version", 114 | "last 1 safari version" 115 | ] 116 | }, 117 | "config": { 118 | "commitizen": { 119 | "path": "git-cz" 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/@konva/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Group, Layer, Line, Rect, Text } from "react-konva"; 3 | import Konva from "konva"; 4 | 5 | export const KonvaGroup = (props: Konva.GroupConfig) => { 6 | return ; 7 | }; 8 | 9 | export const KonvaLayer = (props: Konva.LayerConfig) => { 10 | return ; 11 | }; 12 | 13 | export const KonvaLine = (props: Konva.LineConfig) => { 14 | return ; 15 | }; 16 | 17 | export const KonvaRect = (props: Konva.RectConfig) => { 18 | return ; 19 | }; 20 | 21 | export const KonvaText = (props: Konva.TextConfig) => { 22 | return ; 23 | }; 24 | -------------------------------------------------------------------------------- /src/@stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /src/@stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /src/@stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /src/@stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /src/@stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /src/@stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/@stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /src/@stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/@stories/index.mdx: -------------------------------------------------------------------------------- 1 | import { ArgTypes, Canvas, Meta } from "@storybook/blocks"; 2 | 3 | import { Primary } from "../KonvaTimeline/index.stories.tsx"; 4 | 5 | 6 | 7 | # @melfore/konva-timeline 8 | 9 | `@melfore/konva-timeline` is a TypeScript ReactJS library that uses `konva` and `react-konva` to render a timeline component using canvas. 10 | 11 | ## Install 12 | 13 | To install the library run: 14 | 15 | ``` 16 | npm i @melfore/konva-timeline 17 | ``` 18 | 19 | This library has the following required peerDependencies: 20 | 21 | ``` 22 | "konva": ">= 9.2.0 < 10", 23 | "luxon": ">= 3.3.0 < 4", 24 | "react": ">= 18.2.0 < 19", 25 | "react-dom": ">= 18.2.0 < 19", 26 | "react-konva": ">= 18.2.9 < 19" 27 | "react-konva-utils": ">= 1.0.5 < 2" 28 | ``` 29 | 30 | ### ⚠️ Installing with npm < 7 31 | 32 | If installing with versions of npm < 7, you have to manually install them. 33 | 34 | ``` 35 | npm i konva luxon react react-dom react-konva 36 | ``` 37 | 38 | Beware to check the versions installed, they must match peerDependencies ranges. 39 | 40 | ## Usage 41 | 42 | Import the `KonvaTimeline` component from `@melfore/konva-timeline` library: 43 | 44 | ``` 45 | import { KonvaTimeline } from "@melfore/konva-timeline"; 46 | ``` 47 | 48 | Use the component passing the minimum set of required properties: 49 | 50 | 51 | 52 | ### Props 53 | 54 | Below the reference for all accepted properties: 55 | 56 | 57 | 58 | #### Resource 59 | 60 | ``` 61 | interface Resource { 62 | /** 63 | * Unique identifier of the resource 64 | */ 65 | id: string; 66 | /** 67 | * Label of the resource 68 | */ 69 | label: string; 70 | /** 71 | * Color assigned to the resource 72 | */ 73 | color: string; 74 | } 75 | ``` 76 | 77 | #### TaskData 78 | 79 | ``` 80 | interface TaskData { 81 | /** 82 | * Unique identifier of the task 83 | */ 84 | id: string; 85 | /** 86 | * Label of the task 87 | */ 88 | label: string; 89 | /** 90 | * Id of assigned resource 91 | */ 92 | resourceId: string; 93 | /** 94 | * Task time range 95 | */ 96 | time: TimeRange; 97 | } 98 | ``` 99 | 100 | #### TimeRange 101 | 102 | ``` 103 | interface TimeRange { 104 | /** 105 | * Start of time range interval 106 | */ 107 | start: number | string; 108 | /** 109 | * End of time range interval 110 | */ 111 | end: number | string; 112 | } 113 | ``` 114 | 115 | #### KonvaTimelineError 116 | 117 | ``` 118 | interface KonvaTimelineError { 119 | /** 120 | * The entity that thrown the error 121 | */ 122 | entity: DataEntity; 123 | /** 124 | * The error level (error or warn) 125 | */ 126 | level: ErrorLevel; 127 | /** 128 | * The error message 129 | */ 130 | message: string; 131 | /** 132 | * The refId for entity item 133 | */ 134 | refId?: string; 135 | } 136 | ``` 137 | -------------------------------------------------------------------------------- /src/KonvaTimeline/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import Timeline from "../timeline"; 4 | import { TimelineProvider, TimelineProviderProps } from "../timeline/TimelineContext"; 5 | 6 | const KonvaTimeline: FC = (props) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default KonvaTimeline; 15 | -------------------------------------------------------------------------------- /src/KonvaTimeline/scenario-gantt.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import DecoratoGantt from "../utils/stories/decorators/Gantt"; 4 | import { generateStoryData } from "../utils/stories/utils"; 5 | 6 | import KonvaTimeline from "."; 7 | 8 | const meta = { 9 | title: "Scenario/Gantt", 10 | component: KonvaTimeline, 11 | decorators: [DecoratoGantt], 12 | tags: ["autodocs"], 13 | argTypes: { 14 | onTaskClick: { 15 | type: "function", 16 | }, 17 | onTaskChange: { 18 | type: "function", 19 | }, 20 | }, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | const { resources } = generateStoryData({ 27 | averageTaskDurationInMinutes: 200, 28 | resourcesCount: 3, 29 | tasksCount: 5, 30 | timeRangeInDays: 1, 31 | }); 32 | 33 | export const Line: Story = { 34 | args: { 35 | onAreaSelect: undefined, 36 | resources, 37 | resolution: "2weeks", 38 | enableLines: true, 39 | toolTip: false, 40 | onTaskClick: (task) => task, 41 | initialDateTime: 1698357600000, 42 | range: { 43 | start: 1698357600000, 44 | end: 1702095200000, 45 | }, 46 | tasks: [ 47 | { 48 | id: "4", 49 | label: "Task4", 50 | resourceId: "2", 51 | time: { 52 | start: 1698357600000, 53 | end: 1698613200000, 54 | }, 55 | relatedTasks: ["1"], 56 | }, 57 | { 58 | id: "1", 59 | label: "Task1", 60 | resourceId: "1", 61 | time: { 62 | start: 1698793200000, 63 | end: 1699434800000, 64 | }, 65 | relatedTasks: ["3", "2"], 66 | }, 67 | { 68 | id: "3", 69 | label: "Task3", 70 | resourceId: "3", 71 | time: { 72 | start: 1699734800000, 73 | end: 1700234800000, 74 | }, 75 | }, 76 | { 77 | id: "2", 78 | label: "Task2", 79 | resourceId: "2", 80 | time: { 81 | start: 1699900000000, 82 | end: 1700048000000, 83 | }, 84 | relatedTasks: ["5"], 85 | }, 86 | { 87 | id: "5", 88 | label: "Task5", 89 | resourceId: "1", 90 | time: { 91 | start: 1700505200000, 92 | end: 1700805200000, 93 | }, 94 | }, 95 | ], 96 | onTaskChange: (task, opts) => { 97 | task.id, opts; 98 | }, 99 | }, 100 | }; 101 | -------------------------------------------------------------------------------- /src/KonvaTimeline/scenario-monthly.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import TimelineDecorator from "../utils/stories/decorators/Timeline"; 4 | import { generateStoryData } from "../utils/stories/utils"; 5 | 6 | import KonvaTimeline from "."; 7 | 8 | const meta = { 9 | title: "Scenario/Monthly Report", 10 | component: KonvaTimeline, 11 | decorators: [TimelineDecorator], 12 | tags: ["autodocs"], 13 | argTypes: { 14 | onTaskChange: { 15 | type: "function", 16 | }, 17 | }, 18 | } satisfies Meta; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | const monthlyStoryData = generateStoryData({ 24 | averageTaskDurationInMinutes: 10, 25 | resourcesCount: 16, 26 | tasksCount: 5000, 27 | timeRangeInDays: 60, 28 | }); 29 | 30 | export const MonthlyReport: Story = { 31 | args: { 32 | ...monthlyStoryData, 33 | resolution: "1min", 34 | onAreaSelect: undefined, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/KonvaTimeline/scenario-yearly.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import TimelineDecorator from "../utils/stories/decorators/Timeline"; 4 | import { generateStoryData } from "../utils/stories/utils"; 5 | 6 | import KonvaTimeline from "."; 7 | 8 | const meta = { 9 | title: "Scenario/Yearly Report", 10 | component: KonvaTimeline, 11 | decorators: [TimelineDecorator], 12 | tags: ["autodocs"], 13 | argTypes: { 14 | onTaskChange: { 15 | type: "function", 16 | }, 17 | }, 18 | } satisfies Meta; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | const yearlyStoryData = generateStoryData({ 24 | averageTaskDurationInMinutes: 600, 25 | resourcesCount: 20, 26 | tasksCount: 3000, 27 | timeRangeInDays: 365 * 5, 28 | }); 29 | 30 | export const YearlyReport: Story = { 31 | args: { 32 | ...yearlyStoryData, 33 | resolution: "1day", 34 | initialDateTime: yearlyStoryData.range.start, 35 | onAreaSelect: undefined, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/grid/Cell/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import { Interval } from "luxon"; 3 | 4 | import { KonvaGroup, KonvaLine, KonvaText } from "../../@konva"; 5 | import { useTimelineContext } from "../../timeline/TimelineContext"; 6 | import { DEFAULT_GRID_COLUMN_WIDTH } from "../../utils/dimensions"; 7 | import { DEFAULT_STROKE_DARK_MODE, DEFAULT_STROKE_LIGHT_MODE } from "../../utils/theme"; 8 | import { displayInterval } from "../../utils/time-resolution"; 9 | 10 | interface GridCellProps { 11 | column: Interval; 12 | height: number; 13 | index: number; 14 | hourInfo: { 15 | backHour?: boolean; 16 | nextHour?: boolean; 17 | }; 18 | } 19 | 20 | const GridCell = ({ column, height, index, hourInfo: visibleDayInfo }: GridCellProps) => { 21 | const { 22 | blocksOffset, 23 | columnWidth, 24 | resolution: { unit: resolutionUnit }, 25 | rowHeight, 26 | dateLocale, 27 | theme: { color: themeColor }, 28 | } = useTimelineContext(); 29 | 30 | const cellLabel = useMemo(() => { 31 | return displayInterval(column, resolutionUnit, dateLocale!); 32 | }, [column, resolutionUnit, dateLocale]); 33 | 34 | const xPos = useMemo(() => { 35 | if (resolutionUnit === "day") { 36 | if (visibleDayInfo.backHour) { 37 | return columnWidth * (index + blocksOffset) + columnWidth / 24; 38 | } 39 | 40 | if (visibleDayInfo.nextHour) { 41 | return columnWidth * (index + blocksOffset) - columnWidth / 24; 42 | } 43 | } 44 | if (resolutionUnit === "week") { 45 | if (visibleDayInfo.backHour) { 46 | return columnWidth * (index + blocksOffset) + columnWidth / 168; 47 | } 48 | 49 | if (visibleDayInfo.nextHour) { 50 | return columnWidth * (index + blocksOffset) - columnWidth / 168; 51 | } 52 | } 53 | 54 | return columnWidth * (index + blocksOffset); 55 | }, [blocksOffset, columnWidth, index, visibleDayInfo, resolutionUnit]); 56 | 57 | const yPos = useMemo(() => rowHeight * 0.8, [rowHeight]); 58 | 59 | const stroke = useMemo(() => { 60 | if (themeColor === "black") { 61 | return DEFAULT_STROKE_LIGHT_MODE; 62 | } 63 | return DEFAULT_STROKE_DARK_MODE; 64 | }, [themeColor]); 65 | 66 | const fontSize = useMemo(() => { 67 | const maxSubtractSize = resolutionUnit === "hour" ? 5 : 4; 68 | const percentMultiplier = maxSubtractSize / 100; 69 | const widthDifference = DEFAULT_GRID_COLUMN_WIDTH - columnWidth; 70 | const negative = (100 / 40) * widthDifference * percentMultiplier; 71 | return negative >= 0 && negative <= maxSubtractSize ? negative : 0; 72 | }, [columnWidth, resolutionUnit]); 73 | 74 | return ( 75 | 76 | 77 | 86 | 87 | ); 88 | }; 89 | 90 | export default memo(GridCell); 91 | -------------------------------------------------------------------------------- /src/grid/CellGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { Duration, Interval } from "luxon"; 3 | 4 | import { KonvaGroup, KonvaLine, KonvaText } from "../../@konva"; 5 | import { useTimelineContext } from "../../timeline/TimelineContext"; 6 | import { DEFAULT_GRID_COLUMN_WIDTH } from "../../utils/dimensions"; 7 | import { DEFAULT_STROKE_DARK_MODE, DEFAULT_STROKE_LIGHT_MODE } from "../../utils/theme"; 8 | import { displayAboveInterval } from "../../utils/time-resolution"; 9 | 10 | interface GridCellGroupProps { 11 | column: Interval; 12 | index: number; 13 | dayInfo?: { 14 | thisMonth?: number; 15 | untilNow?: number; 16 | }; 17 | hourInfo?: { 18 | backHour?: boolean; 19 | nextHour?: boolean; 20 | }; 21 | } 22 | 23 | const GridCellGroup = ({ column, index, dayInfo, hourInfo }: GridCellGroupProps) => { 24 | const { 25 | columnWidth, 26 | resolution: { sizeInUnits, unit, unitAbove }, 27 | rowHeight, 28 | theme: { color: themeColor }, 29 | dateLocale, 30 | } = useTimelineContext(); 31 | 32 | const cellLabel = useMemo( 33 | () => displayAboveInterval(column, unitAbove, dateLocale!), 34 | [column, unitAbove, dateLocale] 35 | ); 36 | 37 | const points = useMemo(() => [0, 0, 0, rowHeight], [rowHeight]); 38 | 39 | const unitAboveInUnitBelow = useMemo(() => { 40 | if (unitAbove === "month") { 41 | return Duration.fromObject({ ["day"]: dayInfo!.thisMonth }).as("week") / sizeInUnits; 42 | } 43 | 44 | return Duration.fromObject({ [unitAbove]: 1 }).as(unit) / sizeInUnits; 45 | }, [sizeInUnits, dayInfo, unitAbove, unit]); 46 | 47 | const unitAboveSpanInPx = useMemo(() => { 48 | return unitAboveInUnitBelow * columnWidth; 49 | }, [columnWidth, unitAboveInUnitBelow]); 50 | 51 | const xPos = useMemo(() => { 52 | if (unitAbove === "month") { 53 | const pxUntil = 54 | dayInfo!.untilNow !== dayInfo!.thisMonth 55 | ? Duration.fromObject({ ["day"]: dayInfo!.untilNow! - dayInfo!.thisMonth! }).as("week") / sizeInUnits 56 | : 0; 57 | 58 | if (hourInfo!.backHour) { 59 | const hourInMonthPx = columnWidth / 168; 60 | return pxUntil * columnWidth + unitAboveSpanInPx + hourInMonthPx; 61 | } 62 | 63 | if (hourInfo!.nextHour) { 64 | const hourInMonthPx = columnWidth / 168; 65 | return pxUntil * columnWidth + unitAboveSpanInPx - hourInMonthPx; 66 | } 67 | 68 | return pxUntil * columnWidth + unitAboveSpanInPx; 69 | } 70 | 71 | if (unitAbove === "day") { 72 | if (hourInfo!.backHour) { 73 | return index * unitAboveSpanInPx + columnWidth / sizeInUnits; 74 | } 75 | 76 | if (hourInfo!.nextHour) { 77 | return index * unitAboveSpanInPx - columnWidth / sizeInUnits; 78 | } 79 | } 80 | 81 | if (unitAbove === "week") { 82 | if (hourInfo!.backHour) { 83 | return index * unitAboveSpanInPx + columnWidth / 24; 84 | } 85 | 86 | if (hourInfo!.nextHour) { 87 | return index * unitAboveSpanInPx - columnWidth / 24; 88 | } 89 | } 90 | 91 | return index * unitAboveSpanInPx; 92 | }, [index, unitAboveSpanInPx, columnWidth, sizeInUnits, dayInfo, unitAbove, hourInfo]); 93 | 94 | const yPos = useMemo(() => rowHeight * 0.3, [rowHeight]); 95 | 96 | const xPosLabel = useMemo(() => { 97 | if (unitAbove === "month") { 98 | return xPos - unitAboveSpanInPx; 99 | } 100 | if (unitAbove === "day") { 101 | if (hourInfo!.backHour) { 102 | return index * unitAboveSpanInPx + columnWidth / sizeInUnits; 103 | } 104 | 105 | if (hourInfo!.nextHour) { 106 | return index * unitAboveSpanInPx - columnWidth / sizeInUnits; 107 | } 108 | } 109 | return index * unitAboveSpanInPx; 110 | }, [xPos, unitAboveSpanInPx, unitAbove, index, columnWidth, sizeInUnits, hourInfo]); 111 | const stroke = useMemo(() => { 112 | if (themeColor === "black") { 113 | return DEFAULT_STROKE_LIGHT_MODE; 114 | } 115 | return DEFAULT_STROKE_DARK_MODE; 116 | }, [themeColor]); 117 | 118 | const fontSize = useMemo(() => { 119 | const percent = 4 / 100; 120 | const cc = DEFAULT_GRID_COLUMN_WIDTH - columnWidth; 121 | const negative = (100 / 40) * cc * percent; 122 | return negative >= 0 && negative <= 4 ? negative : 0; 123 | }, [columnWidth]); 124 | 125 | return ( 126 | 127 | 128 | 137 | 138 | ); 139 | }; 140 | 141 | export default GridCellGroup; 142 | -------------------------------------------------------------------------------- /src/grid/Cells/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | 3 | import { KonvaGroup } from "../../@konva"; 4 | import { useTimelineContext } from "../../timeline/TimelineContext"; 5 | import { getAboveTimeBlocksVisible, getDaysNumberOfMonths, getTimeBlocksTzInfo } from "../../utils/timeBlockArray"; 6 | import GridCell from "../Cell"; 7 | import GridCellGroup from "../CellGroup"; 8 | 9 | interface GridCellsProps { 10 | height: number; 11 | } 12 | 13 | const GridCells = ({ height }: GridCellsProps) => { 14 | const { 15 | interval, 16 | aboveTimeBlocks, 17 | visibleTimeBlocks, 18 | resolution: { unitAbove }, 19 | } = useTimelineContext(); 20 | 21 | const tz = useMemo(() => interval.start!.toISO()!.slice(-6), [interval]); 22 | 23 | const dayInfo = useMemo( 24 | () => getDaysNumberOfMonths(unitAbove, aboveTimeBlocks, interval), 25 | [unitAbove, aboveTimeBlocks, interval] 26 | ); 27 | 28 | const aboveHourInfo = useMemo(() => getTimeBlocksTzInfo(aboveTimeBlocks, tz), [tz, aboveTimeBlocks]); 29 | const visibileHourInfo = useMemo(() => getTimeBlocksTzInfo(visibleTimeBlocks, tz), [tz, visibleTimeBlocks]); 30 | 31 | const startUnitAbove = useMemo( 32 | () => (visibleTimeBlocks.length !== 0 ? visibleTimeBlocks[0].start!.startOf(unitAbove) : null), 33 | [visibleTimeBlocks, unitAbove] 34 | ); 35 | const endUnitAbove = useMemo( 36 | () => 37 | visibleTimeBlocks.length !== 0 ? visibleTimeBlocks[visibleTimeBlocks.length - 1].end!.endOf(unitAbove) : null, 38 | [visibleTimeBlocks, unitAbove] 39 | ); 40 | 41 | const arrayIndex: number[] = useMemo(() => { 42 | if (visibleTimeBlocks) { 43 | return []; 44 | } 45 | return []; 46 | }, [visibleTimeBlocks]); 47 | 48 | const aboveTimeBlocksVisible = useMemo( 49 | () => getAboveTimeBlocksVisible(visibleTimeBlocks, aboveTimeBlocks, startUnitAbove, endUnitAbove, arrayIndex), 50 | [visibleTimeBlocks, aboveTimeBlocks, startUnitAbove, endUnitAbove, arrayIndex] 51 | ); 52 | return ( 53 | 54 | {aboveTimeBlocksVisible.map((column, index) => { 55 | return ( 56 | 63 | ); 64 | })} 65 | {visibleTimeBlocks.map((column, index) => ( 66 | 73 | ))} 74 | 75 | ); 76 | }; 77 | 78 | export default memo(GridCells); 79 | -------------------------------------------------------------------------------- /src/grid/Layer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, memo } from "react"; 2 | 3 | import { KonvaLayer } from "../../@konva"; 4 | import GridCells from "../Cells"; 5 | import GridRows from "../Rows"; 6 | 7 | interface GridLayerProps { 8 | height: number; 9 | } 10 | 11 | const GridLayer: FC = ({ height }) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default memo(GridLayer); 21 | -------------------------------------------------------------------------------- /src/grid/Row/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | 3 | import { KonvaGroup, KonvaLine, KonvaRect } from "../../@konva"; 4 | import { useTimelineContext } from "../../timeline/TimelineContext"; 5 | import { 6 | ALTERNATIVE_ROW, 7 | DEFAULT_ROW_DARK_MODE, 8 | DEFAULT_ROW_LIGHT_MODE, 9 | DEFAULT_STROKE_DARK_MODE, 10 | DEFAULT_STROKE_LIGHT_MODE, 11 | } from "../../utils/theme"; 12 | 13 | interface GridRowProps { 14 | index: number; 15 | } 16 | 17 | const GridRow = ({ index }: GridRowProps) => { 18 | const { 19 | drawRange: { start: drawRangeStart, end: drawRangeEnd }, 20 | rowHeight, 21 | theme: { color: themeColor }, 22 | } = useTimelineContext(); 23 | 24 | const yPos = useMemo(() => rowHeight * (index + 1), [index, rowHeight]); 25 | 26 | const fill = useMemo(() => { 27 | if (themeColor === "black") { 28 | return index % 2 === 0 ? DEFAULT_ROW_LIGHT_MODE : ALTERNATIVE_ROW; 29 | } 30 | return index % 2 === 0 ? DEFAULT_ROW_DARK_MODE : ALTERNATIVE_ROW; 31 | }, [index, themeColor]); 32 | 33 | const stroke = useMemo(() => { 34 | if (themeColor === "black") { 35 | return DEFAULT_STROKE_LIGHT_MODE; 36 | } 37 | return DEFAULT_STROKE_DARK_MODE; 38 | }, [themeColor]); 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default memo(GridRow); 49 | -------------------------------------------------------------------------------- /src/grid/Rows/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | 3 | import { KonvaGroup } from "../../@konva"; 4 | import { useTimelineContext } from "../../timeline/TimelineContext"; 5 | import GridRow from "../Row"; 6 | 7 | const GridRows = () => { 8 | const { resources } = useTimelineContext(); 9 | 10 | return ( 11 | 12 | {resources.map(({ id }, index) => ( 13 | 14 | ))} 15 | 16 | ); 17 | }; 18 | 19 | export default memo(GridRows); 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Resource } from "./resources/utils/resources"; 2 | export { TaskData } from "./tasks/utils/tasks"; 3 | export { KonvaTimelineError } from "./utils/operations"; 4 | export { TimeRange } from "./utils/time"; 5 | export { RESOLUTIONS, Resolution } from "./utils/time-resolution"; 6 | 7 | export { default as KonvaTimeline } from "./KonvaTimeline"; 8 | -------------------------------------------------------------------------------- /src/resources/components/Header/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { TaskDecorator } from "../../../utils/stories/decorators/Tasks"; 4 | 5 | import { ResourceHeaderDocs } from "."; 6 | 7 | const meta = { 8 | title: "Components/Resources/Header", 9 | component: ResourceHeaderDocs, 10 | decorators: [TaskDecorator], 11 | tags: ["autodocs"], 12 | argTypes: {}, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Primary: Story = { 19 | args: { 20 | resource: { 21 | id: "1", 22 | color: "red", 23 | label: "Resource #1", 24 | }, 25 | index: 0, 26 | }, 27 | }; 28 | 29 | export const Last: Story = { 30 | args: { 31 | ...Primary.args, 32 | isLast: true, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/resources/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useMemo } from "react"; 2 | import { Group, Rect } from "react-konva"; 3 | import { Html } from "react-konva-utils"; 4 | 5 | import { KonvaLine, KonvaRect, KonvaText } from "../../../@konva"; 6 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 7 | import { DEFAULT_TEXT_SIZE } from "../../../utils/dimensions"; 8 | import { 9 | ALTERNATIVE_ROW, 10 | DEFAULT_ROW_DARK_MODE, 11 | DEFAULT_ROW_LIGHT_MODE, 12 | DEFAULT_STROKE_DARK_MODE, 13 | DEFAULT_STROKE_LIGHT_MODE, 14 | } from "../../../utils/theme"; 15 | import { Resource, RESOURCE_HEADER_WIDTH, RESOURCE_TEXT_OFFSET } from "../../utils/resources"; 16 | 17 | export interface ResourceHeaderProps { 18 | /** 19 | * The row index of current resource 20 | */ 21 | index: number; 22 | /** 23 | * Flag to identify if resource is last to be shown 24 | */ 25 | isLast?: boolean; 26 | /** 27 | * The resource object to handle 28 | */ 29 | resource: Resource; 30 | /** 31 | * On click event 32 | */ 33 | onClick?: () => void; 34 | /** 35 | * Prop that idicate if resource is header 36 | */ 37 | header?: boolean; 38 | } 39 | 40 | /** 41 | * This component renders a resource header. It displays a text (`resource.label`) and a delimiter line. 42 | */ 43 | const ResourceHeader = ({ index, isLast = false, resource, header }: ResourceHeaderProps) => { 44 | const { 45 | rowHeight, 46 | theme: { color: themeColor }, 47 | onResourceClick, 48 | customResources, 49 | } = useTimelineContext(); 50 | 51 | const rowPoints = useMemo(() => [0, rowHeight, RESOURCE_HEADER_WIDTH, rowHeight], [rowHeight]); 52 | 53 | const yCoordinate = useMemo(() => rowHeight * index, [index, rowHeight]); 54 | 55 | const fill = useMemo(() => { 56 | if (themeColor === "black") { 57 | return index % 2 === 0 ? DEFAULT_ROW_LIGHT_MODE : ALTERNATIVE_ROW; 58 | } 59 | return index % 2 === 0 ? DEFAULT_ROW_DARK_MODE : ALTERNATIVE_ROW; 60 | }, [index, themeColor]); 61 | 62 | const stroke = useMemo(() => { 63 | if (themeColor === "black") { 64 | return DEFAULT_STROKE_LIGHT_MODE; 65 | } 66 | return DEFAULT_STROKE_DARK_MODE; 67 | }, [themeColor]); 68 | 69 | const onClick = useCallback( 70 | () => onResourceClick && !header && onResourceClick(resource), 71 | [resource, header, onResourceClick] 72 | ); 73 | 74 | const resData = useMemo(() => { 75 | return { resource, dimension: { width: RESOURCE_HEADER_WIDTH, height: rowHeight } }; 76 | }, [resource, rowHeight]); 77 | 78 | return ( 79 | 80 | 81 | {customResources && !header ? ( 82 | 83 | { 84 |
92 | {customResources(resData)} 93 |
94 | } 95 | 96 | ) : ( 97 | 105 | )} 106 | {!isLast && ( 107 | 108 | 109 | 110 | 111 | )} 112 |
113 | ); 114 | }; 115 | 116 | export const ResourceHeaderDocs = ResourceHeader; 117 | 118 | export default memo(ResourceHeader); 119 | -------------------------------------------------------------------------------- /src/resources/components/Layer/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { TasksLayerDecorator } from "../../../utils/stories/decorators/Tasks"; 4 | 5 | import ResourcesLayer from "."; 6 | 7 | const meta = { 8 | title: "Components/Resources/Layer", 9 | component: ResourcesLayer, 10 | decorators: [TasksLayerDecorator], 11 | tags: ["autodocs"], 12 | argTypes: {}, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Primary: Story = { 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/resources/components/Layer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Layer } from "react-konva"; 3 | 4 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 5 | import ResourceHeader from "../Header"; 6 | 7 | export interface ResourcesLayerProps {} 8 | 9 | /** 10 | * This component renders a Konva layer containing one header for each resource (`ResourceHeader`). 11 | */ 12 | const ResourcesLayer: FC = () => { 13 | const { resources } = useTimelineContext(); 14 | 15 | return ( 16 | 17 | {resources.map((resource, index) => { 18 | const isLast = index === resources.length - 1; 19 | const header = index === 0 ? true : false; 20 | 21 | return ( 22 | 29 | ); 30 | })} 31 | 32 | ); 33 | }; 34 | 35 | export default ResourcesLayer; 36 | -------------------------------------------------------------------------------- /src/resources/components/Summary/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { Layer } from "react-konva"; 3 | 4 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 5 | import SummaryHeader from "../SummaryHeader"; 6 | 7 | interface ResourcesLayerProps {} 8 | 9 | /** 10 | * This component renders a Konva layer containing one header for each resource (`Summary`). 11 | */ 12 | const SummaryLayer: FC = () => { 13 | const { resources } = useTimelineContext(); 14 | 15 | return ( 16 | 17 | {resources.map((data, index) => { 18 | const isLast = index === resources.length - 1; 19 | 20 | return ; 21 | })} 22 | 23 | ); 24 | }; 25 | 26 | export default SummaryLayer; 27 | -------------------------------------------------------------------------------- /src/resources/components/SummaryHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import { Group, Rect } from "react-konva"; 3 | 4 | import { KonvaLine, KonvaRect, KonvaText } from "../../../@konva"; 5 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 6 | import { DEFAULT_TEXT_SIZE } from "../../../utils/dimensions"; 7 | import { 8 | ALTERNATIVE_ROW, 9 | DEFAULT_ROW_DARK_MODE, 10 | DEFAULT_ROW_LIGHT_MODE, 11 | DEFAULT_STROKE_DARK_MODE, 12 | DEFAULT_STROKE_LIGHT_MODE, 13 | } from "../../../utils/theme"; 14 | //import { RESOURCE_HEADER_WIDTH } from "../../utils/resources"; 15 | 16 | interface SummaryHeaderProps { 17 | /** 18 | * The row index of current resource 19 | */ 20 | index: number; 21 | /** 22 | * Flag to identify if resource is last to be shown 23 | */ 24 | isLast?: boolean; 25 | /** 26 | * The resource object to handle 27 | */ 28 | id: string; 29 | } 30 | 31 | /** 32 | * This component renders a resource header. It displays a text (`resource.label`) and a delimiter line. 33 | */ 34 | const SummaryHeader = ({ index, isLast = false, id }: SummaryHeaderProps) => { 35 | const { 36 | rowHeight, 37 | theme: { color: themeColor }, 38 | summaryWidth, 39 | summary, 40 | summaryHeader, 41 | //onResourceClick, 42 | } = useTimelineContext(); 43 | 44 | const rowPoints = useMemo(() => [0, rowHeight, summaryWidth, rowHeight], [rowHeight, summaryWidth]); 45 | 46 | const yCoordinate = useMemo(() => rowHeight * index, [index, rowHeight]); 47 | 48 | const fill = useMemo(() => { 49 | if (themeColor === "black") { 50 | return index % 2 === 0 ? DEFAULT_ROW_LIGHT_MODE : ALTERNATIVE_ROW; 51 | } 52 | return index % 2 === 0 ? DEFAULT_ROW_DARK_MODE : ALTERNATIVE_ROW; 53 | }, [index, themeColor]); 54 | 55 | const stroke = useMemo(() => { 56 | if (themeColor === "black") { 57 | return DEFAULT_STROKE_LIGHT_MODE; 58 | } 59 | return DEFAULT_STROKE_DARK_MODE; 60 | }, [themeColor]); 61 | 62 | const konvaText = useMemo(() => { 63 | if (!summary) { 64 | return "🚫"; 65 | } 66 | if (index === 0) { 67 | return summaryHeader ? summaryHeader : summary[0].label; 68 | } 69 | const data = summary.find((i) => i.id === id); 70 | if (!data) { 71 | return "🚫"; 72 | } 73 | return data.label; 74 | }, [summary, id, index, summaryHeader]); 75 | 76 | /*const onClick = useCallback( 77 | () => onResourceClick && !header && onResourceClick(resource), 78 | [resource, header, onResourceClick] 79 | );*/ 80 | return ( 81 | 82 | 83 | 94 | {!isLast && ( 95 | 96 | 97 | 98 | 99 | )} 100 | 101 | ); 102 | }; 103 | 104 | export const SummaryHeaderDocs = SummaryHeader; 105 | 106 | export default memo(SummaryHeader); 107 | -------------------------------------------------------------------------------- /src/resources/utils/resources.test.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_GRID_ROW_HEIGHT } from "../../utils/dimensions"; 2 | 3 | import { addHeaderResource, findResourceByCoordinate, Resource } from "./resources"; 4 | 5 | let resources: Resource[] = [ 6 | { id: "1", color: "red", label: "Resource #1" }, 7 | { id: "2", color: "green", label: "Resource #2" }, 8 | { id: "3", color: "blue", label: "Resource #3" }, 9 | ]; 10 | 11 | beforeAll(() => { 12 | const initialLength = resources.length; 13 | resources = addHeaderResource(resources); 14 | expect(resources).toHaveLength(initialLength + 1); 15 | }); 16 | 17 | describe("findResourceByCoordinate", () => { 18 | it("base", () => { 19 | const resourceId1 = findResourceByCoordinate(27, DEFAULT_GRID_ROW_HEIGHT, resources); 20 | expect(resourceId1).toEqual(resources[1]); 21 | 22 | const resourceId2 = findResourceByCoordinate(82, DEFAULT_GRID_ROW_HEIGHT, resources); 23 | expect(resourceId2).toEqual(resources[1]); 24 | 25 | const resourceId3 = findResourceByCoordinate(118, DEFAULT_GRID_ROW_HEIGHT, resources); 26 | expect(resourceId3).toEqual(resources[2]); 27 | 28 | const resourceId4 = findResourceByCoordinate(173, DEFAULT_GRID_ROW_HEIGHT, resources); 29 | expect(resourceId4).toEqual(resources[3]); 30 | }); 31 | 32 | it("negative", () => { 33 | const resource = findResourceByCoordinate(-10, DEFAULT_GRID_ROW_HEIGHT, resources); 34 | expect(resource).toEqual(resources[1]); 35 | }); 36 | 37 | it("low bound", () => { 38 | const lowBound = 0 * DEFAULT_GRID_ROW_HEIGHT; 39 | const resource = findResourceByCoordinate(lowBound, DEFAULT_GRID_ROW_HEIGHT, resources); 40 | expect(resource).toEqual(resources[1]); 41 | }); 42 | 43 | it("high bound", () => { 44 | const highBound = resources.length * DEFAULT_GRID_ROW_HEIGHT; 45 | const resource = findResourceByCoordinate(highBound, DEFAULT_GRID_ROW_HEIGHT, resources); 46 | 47 | expect(resource).toEqual(resources[3]); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/resources/utils/resources.ts: -------------------------------------------------------------------------------- 1 | export interface Resource { 2 | /** 3 | * Unique identifier of the resource 4 | */ 5 | id: string; 6 | /** 7 | * Label of the resource 8 | */ 9 | label: string; 10 | /** 11 | * Color assigned to the resource, accept only HEX 12 | */ 13 | color: string; 14 | /** 15 | * Color assigned to the resource for incomplete part of Task, accept only HEX 16 | */ 17 | toCompleteColor?: string; 18 | } 19 | 20 | const RESOURCE_HEADER: Resource = { 21 | id: "-1", 22 | color: "transparent", 23 | label: "Header", 24 | }; 25 | 26 | export const RESOURCE_HEADER_WIDTH = 200; 27 | 28 | export const RESOURCE_TEXT_OFFSET = 12; 29 | 30 | /** 31 | * Adds header resource to incoming list of resources 32 | * @param resources the list of all resources 33 | */ 34 | export const addHeaderResource = (resources: Resource[], headerLabel?: string): Resource[] => [ 35 | { ...RESOURCE_HEADER, label: headerLabel || RESOURCE_HEADER.label }, 36 | ...resources, 37 | ]; 38 | 39 | /** 40 | * Finds resource index given a y coordinate. Used when determining the resource from pointer position. 41 | * Excludes the header resource, hence resources are considered index 1 based. 42 | * @param coordinate the y coordinate from pointer position 43 | * @param rowHeight the current height of rows 44 | * @param resources the list of all resources 45 | * @throws if resources is empty 46 | */ 47 | export const findResourceIndexByCoordinate = (coordinate: number, rowHeight: number, resources: Resource[]): number => { 48 | if (!resources || !resources.length) { 49 | // TODO#lb: improve adding KonvaTimeline error 50 | throw new Error("Resources is empty"); 51 | } 52 | 53 | let resourceIndex = Math.floor(coordinate / rowHeight); 54 | if (resourceIndex < 1) { 55 | resourceIndex = 1; 56 | } 57 | 58 | if (resources.length <= resourceIndex) { 59 | resourceIndex = resources.length - 1; 60 | } 61 | 62 | return resourceIndex; 63 | }; 64 | 65 | /** 66 | * Finds resource object given a y coordinate. Used when determining the resource from pointer position. 67 | * Excludes the header resource, hence resources are considered index 1 based. 68 | * @param coordinate the y coordinate from pointer position 69 | * @param rowHeight the current height of rows 70 | * @param resources the list of all resources 71 | */ 72 | export const findResourceByCoordinate = (coordinate: number, rowHeight: number, resources: Resource[]): Resource => { 73 | const resourceIndex = findResourceIndexByCoordinate(coordinate, rowHeight, resources); 74 | return resources[resourceIndex]; 75 | }; 76 | -------------------------------------------------------------------------------- /src/tasks/components/Layer/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { TasksLayerDecorator } from "../../../utils/stories/decorators/Tasks"; 4 | 5 | import TasksLayer from "."; 6 | 7 | const meta = { 8 | title: "Components/TaskLayer", 9 | component: TasksLayer, 10 | decorators: [TasksLayerDecorator], 11 | tags: ["autodocs"], 12 | argTypes: {}, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Primary: Story = { 19 | args: { 20 | onTaskEvent: () => {}, 21 | setTaskTooltip: () => {}, 22 | taskTooltip: null, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/tasks/components/Layer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from "react"; 2 | import { Layer } from "react-konva"; 3 | import { DateTime } from "luxon"; 4 | 5 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 6 | import { KonvaPoint } from "../../../utils/konva"; 7 | import { InternalTimeRange } from "../../../utils/time"; 8 | import { getTaskYCoordinate } from "../../utils/tasks"; 9 | import Task from "../Task"; 10 | import TaskTooltip, { TaskTooltipProps } from "../Tooltip"; 11 | 12 | export interface TasksLayerProps { 13 | taskTooltip: TaskTooltipProps | null; 14 | setTaskTooltip: (tooltip: TaskTooltipProps | null) => void; 15 | create?: boolean; 16 | onTaskEvent: (value: boolean) => void; 17 | } 18 | 19 | /** 20 | * This component renders a set of tasks as a Konva Layer. 21 | * Tasks are displayed accordingly to their assigned resource (different vertical / row position) and their timing (different horizontal / column position) 22 | * `TasksLayer` is also responsible of handling callback for task components offering base implementation for click, leave and over. 23 | * 24 | * The playground has a canvas that simulates 1 day of data with 1 hour resolution. 25 | * Depending on your screen size you might be able to test also the horizontal scrolling behaviour. 26 | */ 27 | const TasksLayer: FC = ({ setTaskTooltip, taskTooltip, create, onTaskEvent }) => { 28 | const { 29 | columnWidth, 30 | drawRange, 31 | interval: { start: intervalStart, end: intervalEnd }, 32 | resolution, 33 | resources, 34 | rowHeight, 35 | tasks, 36 | toolTip, 37 | } = useTimelineContext(); 38 | 39 | const getResourceById = useCallback( 40 | (resourceId: string) => resources.findIndex(({ id }) => resourceId === id), 41 | [resources] 42 | ); 43 | 44 | const getTaskById = useCallback((taskId: string) => tasks.find(({ id }) => taskId === id), [tasks]); 45 | 46 | const onTaskLeave = useCallback(() => setTaskTooltip(null), [setTaskTooltip]); 47 | 48 | const onTaskOver = useCallback( 49 | (taskId: string, point: KonvaPoint) => { 50 | const task = getTaskById(taskId); 51 | if (!task) { 52 | return setTaskTooltip(null); 53 | } 54 | 55 | const { x, y } = point; 56 | setTaskTooltip({ task, x, y }); 57 | }, 58 | [getTaskById, setTaskTooltip] 59 | ); 60 | 61 | const getXCoordinate = useCallback( 62 | (offset: number) => (offset * columnWidth) / resolution.sizeInUnits, 63 | [columnWidth, resolution.sizeInUnits] 64 | ); 65 | 66 | const getTaskXCoordinate = useCallback( 67 | (startTime: number) => { 68 | const timeStart = DateTime.fromMillis(startTime); 69 | const startOffsetInUnit = timeStart.diff(intervalStart!).as(resolution.unit); 70 | return getXCoordinate(startOffsetInUnit); 71 | }, 72 | [getXCoordinate, intervalStart, resolution.unit] 73 | ); 74 | 75 | const getTaskWidth = useCallback( 76 | ({ start, end }: InternalTimeRange) => { 77 | const timeStart = DateTime.fromMillis(start); 78 | const timeEnd = DateTime.fromMillis(end); 79 | const widthOffsetInUnit = timeEnd.diff(timeStart).as(resolution.unit); 80 | return getXCoordinate(widthOffsetInUnit); 81 | }, 82 | [getXCoordinate, resolution.unit] 83 | ); 84 | 85 | if (!intervalStart || !intervalEnd) { 86 | return null; 87 | } 88 | 89 | if (drawRange.end - drawRange.start <= 0) { 90 | return null; 91 | } 92 | 93 | return ( 94 | 95 | {tasks.map((taskData) => { 96 | const { resourceId, time } = taskData; 97 | const resourceIndex = getResourceById(resourceId); 98 | if (resourceIndex < 0) { 99 | return null; 100 | } 101 | 102 | const { color: resourceColor, toCompleteColor } = resources[resourceIndex]; 103 | const xCoordinate = getTaskXCoordinate(time.start); 104 | const yCoordinate = getTaskYCoordinate(resourceIndex, rowHeight); 105 | const width = getTaskWidth(time); 106 | if (xCoordinate > drawRange.end || xCoordinate + width < drawRange.start) { 107 | return null; 108 | } 109 | 110 | return ( 111 | 124 | ); 125 | })} 126 | {toolTip && taskTooltip && } 127 | 128 | ); 129 | }; 130 | 131 | export default TasksLayer; 132 | -------------------------------------------------------------------------------- /src/tasks/components/LayerLine/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useState } from "react"; 2 | import { Layer } from "react-konva"; 3 | import { DateTime } from "luxon"; 4 | 5 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 6 | import { KonvaPoint } from "../../../utils/konva"; 7 | import { InternalTimeRange } from "../../../utils/time"; 8 | import { LINE_OFFSET } from "../../utils/line"; 9 | import { getTaskYCoordinate, TASK_HEIGHT_OFFSET } from "../../utils/tasks"; 10 | import LineKonva from "../Line"; 11 | import TaskLine from "../TaskLine"; 12 | import TaskTooltip, { TaskTooltipProps } from "../Tooltip"; 13 | 14 | interface TasksLayerProps { 15 | taskTooltip: TaskTooltipProps | null; 16 | setTaskTooltip: (tooltip: TaskTooltipProps | null) => void; 17 | create?: boolean; 18 | onTaskEvent: (value: boolean) => void; 19 | } 20 | 21 | /** 22 | * This component renders a set of tasks as a Konva Layer. 23 | * Tasks are displayed accordingly to their assigned resource (different vertical / row position) and their timing (different horizontal / column position) 24 | * `TasksLayer` is also responsible of handling callback for task components offering base implementation for click, leave and over. 25 | * 26 | * The playground has a canvas that simulates 1 day of data with 1 hour resolution. 27 | * Depending on your screen size you might be able to test also the horizontal scrolling behaviour. 28 | */ 29 | const LayerLine: FC = ({ setTaskTooltip, taskTooltip, create, onTaskEvent }) => { 30 | const { columnWidth, drawRange, interval, resolution, resources, rowHeight, tasks, toolTip, validLine } = 31 | useTimelineContext(); 32 | 33 | const [workLine, setWorkLine] = useState([""]); 34 | const { start: intervalStart, end: intervalEnd } = interval; 35 | 36 | const getResourceById = useCallback( 37 | (resourceId: string) => resources.findIndex(({ id }) => resourceId === id), 38 | [resources] 39 | ); 40 | 41 | const getTaskById = useCallback((taskId: string) => tasks.find(({ id }) => taskId === id), [tasks]); 42 | 43 | const onTaskLeave = useCallback(() => setTaskTooltip(null), [setTaskTooltip]); 44 | 45 | const onTaskOver = useCallback( 46 | (taskId: string, point: KonvaPoint) => { 47 | const task = getTaskById(taskId); 48 | if (!task) { 49 | return setTaskTooltip(null); 50 | } 51 | 52 | const { x, y } = point; 53 | setTaskTooltip({ task, x, y }); 54 | }, 55 | [getTaskById, setTaskTooltip] 56 | ); 57 | 58 | const getXCoordinate = useCallback( 59 | (offset: number) => (offset * columnWidth) / resolution.sizeInUnits, 60 | [columnWidth, resolution.sizeInUnits] 61 | ); 62 | 63 | const getTaskXCoordinate = useCallback( 64 | (startTime: number) => { 65 | const timeStart = DateTime.fromMillis(startTime); 66 | const startOffsetInUnit = timeStart.diff(intervalStart!).as(resolution.unit); 67 | return getXCoordinate(startOffsetInUnit); 68 | }, 69 | [getXCoordinate, intervalStart, resolution.unit] 70 | ); 71 | 72 | const getTaskWidth = useCallback( 73 | ({ start, end }: InternalTimeRange) => { 74 | const timeStart = DateTime.fromMillis(start); 75 | const timeEnd = DateTime.fromMillis(end); 76 | const widthOffsetInUnit = timeEnd.diff(timeStart).as(resolution.unit); 77 | return getXCoordinate(widthOffsetInUnit); 78 | }, 79 | [getXCoordinate, resolution] 80 | ); 81 | 82 | if (!intervalStart || !intervalEnd) { 83 | return null; 84 | } 85 | 86 | if (drawRange.end - drawRange.start <= 0) { 87 | return null; 88 | } 89 | return ( 90 | 91 | {validLine && 92 | validLine.map((data) => { 93 | const startResourceIndex = getResourceById(data.startResId); 94 | if (startResourceIndex < 0 || workLine.includes(data.id)) { 95 | return null; 96 | } 97 | //line 98 | const yCoordinate = getTaskYCoordinate(startResourceIndex, rowHeight) + (rowHeight * TASK_HEIGHT_OFFSET) / 2; 99 | const endResourceIndex = getResourceById(data.endResId); 100 | const endY = getTaskYCoordinate(endResourceIndex, rowHeight) + (rowHeight * TASK_HEIGHT_OFFSET) / 2; 101 | const startLine = getTaskXCoordinate(data.start); 102 | const endLine = getTaskXCoordinate(data.end); 103 | 104 | return ( 105 | 118 | ); 119 | })} 120 | {tasks.map((taskData) => { 121 | const { resourceId, time } = taskData; 122 | const resourceIndex = getResourceById(resourceId); 123 | if (resourceIndex < 0) { 124 | return null; 125 | } 126 | 127 | const { color: resourceColor, toCompleteColor } = resources[resourceIndex]; 128 | const xCoordinate = getTaskXCoordinate(time.start); 129 | const yCoordinate = getTaskYCoordinate(resourceIndex, rowHeight); 130 | const width = getTaskWidth(time); 131 | if (xCoordinate > drawRange.end || xCoordinate + width < drawRange.start) { 132 | return null; 133 | } 134 | 135 | return ( 136 | 150 | ); 151 | })} 152 | {toolTip && taskTooltip && } 153 | 154 | ); 155 | }; 156 | 157 | export default LayerLine; 158 | -------------------------------------------------------------------------------- /src/tasks/components/Line/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Circle, Group, Line } from "react-konva"; 3 | 4 | import { 5 | CIRCLE_POINT_COLOR, 6 | CIRCLE_POINT_OFFSET, 7 | CIRCLE_POINT_STROKE, 8 | LINE_COLOR, 9 | LINE_TENSION, 10 | LINE_WIDTH, 11 | LineType, 12 | } from "../../utils/line"; 13 | 14 | const LineKonva = ({ points }: LineType) => { 15 | return ( 16 | 17 | 18 | 26 | 34 | 35 | ); 36 | }; 37 | 38 | export default LineKonva; 39 | -------------------------------------------------------------------------------- /src/tasks/components/Task/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { COLOR_ARG_TYPE, STORY_DATA, TaskDecorator } from "../../../utils/stories/decorators/Tasks"; 4 | 5 | import { TaskDocs } from "."; 6 | 7 | const meta = { 8 | title: "Components/Task", 9 | component: TaskDocs, 10 | decorators: [TaskDecorator], 11 | tags: ["autodocs"], 12 | argTypes: { 13 | fill: COLOR_ARG_TYPE, 14 | stroke: COLOR_ARG_TYPE, 15 | }, 16 | } satisfies Meta; 17 | 18 | export default meta; 19 | type Story = StoryObj; 20 | 21 | export const Primary: Story = { 22 | args: { 23 | data: STORY_DATA.tasks[0], 24 | onLeave: () => {}, 25 | onOver: () => {}, 26 | onTaskEvent: () => {}, 27 | width: 100, 28 | x: 50, 29 | y: 50, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/tasks/components/TaskResizeHandle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from "react"; 2 | import { Rect } from "react-konva"; 3 | import { KonvaEventObject } from "konva/lib/Node"; 4 | 5 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 6 | import { TASK_BORDER_RADIUS } from "../../utils/tasks"; 7 | 8 | interface TaskResizeHandleProps { 9 | height: number; 10 | onResizeStart: (e: KonvaEventObject) => void; 11 | onResizeMove: (e: KonvaEventObject, position: "lx" | "rx") => void; 12 | onResizeEnd: (e: KonvaEventObject) => void; 13 | opacity: number; 14 | position: "lx" | "rx"; 15 | taskId: string; 16 | xCoordinate: number; 17 | } 18 | 19 | const TaskResizeHandle = ({ 20 | height, 21 | onResizeEnd, 22 | onResizeMove, 23 | onResizeStart, 24 | opacity, 25 | position, 26 | taskId, 27 | xCoordinate, 28 | }: TaskResizeHandleProps) => { 29 | const { enableResize } = useTimelineContext(); 30 | 31 | const onDragMove = useCallback( 32 | (e: KonvaEventObject) => onResizeMove(e, position), 33 | [onResizeMove, position] 34 | ); 35 | 36 | const onMouseLeave = useCallback( 37 | (e: KonvaEventObject) => { 38 | e.cancelBubble = true; 39 | const stage = e.target.getStage(); 40 | if (!stage || !enableResize) { 41 | return; 42 | } 43 | 44 | stage.container().style.cursor = "default"; 45 | }, 46 | [enableResize] 47 | ); 48 | 49 | const onMouseOver = useCallback( 50 | (e: KonvaEventObject) => { 51 | e.cancelBubble = true; 52 | const stage = e.target.getStage(); 53 | if (!stage || !enableResize) { 54 | return; 55 | } 56 | 57 | const mouseCursor = `${position === "lx" ? "w" : "e"}-resize`; 58 | stage.container().style.cursor = mouseCursor; 59 | }, 60 | [enableResize, position] 61 | ); 62 | 63 | const handleId = useMemo(() => `${taskId}-resize-${position}`, [position, taskId]); 64 | 65 | return ( 66 | 81 | ); 82 | }; 83 | 84 | export default TaskResizeHandle; 85 | -------------------------------------------------------------------------------- /src/tasks/components/Tooltip/DefaultToolTip/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import { Localized } from "../../../../timeline/TimelineContext"; 4 | 5 | type DefaultToolTip = { 6 | localized: Localized; 7 | startDuration: string; 8 | endDuration: string; 9 | duration: { time: number; unit: string }; 10 | completedPercentage?: number; 11 | percentage: string; 12 | label: string; 13 | }; 14 | 15 | const DefaultToolTip: FC = ({ 16 | localized, 17 | startDuration, 18 | endDuration, 19 | duration, 20 | completedPercentage, 21 | percentage, 22 | label, 23 | }) => { 24 | return ( 25 |
38 | 45 | {label} 46 | 47 |
48 | 49 |
50 | {localized.start}: 51 |     52 | {startDuration} 53 |
54 |

55 |
56 | {localized.end}: 57 |     58 | {endDuration} 59 |
60 |

61 | 62 |
63 | {localized.duration}: 64 |     65 | 66 | {duration.time} {duration.unit}(s) 67 | 68 |
69 |

70 | {completedPercentage && ( 71 |
72 | {localized.completed}: 73 |     74 | {percentage} 75 |
76 | )} 77 |
78 | ); 79 | }; 80 | 81 | export default DefaultToolTip; 82 | -------------------------------------------------------------------------------- /src/tasks/components/Tooltip/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { LayerDecorator, STORY_DATA, TasksLayerDecorator } from "../../../utils/stories/decorators/Tasks"; 4 | 5 | import TaskTooltip from "."; 6 | 7 | const meta = { 8 | title: "Components/TaskTooltip", 9 | component: TaskTooltip, 10 | decorators: [LayerDecorator, TasksLayerDecorator], 11 | tags: ["autodocs"], 12 | argTypes: {}, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Primary: Story = { 19 | args: { 20 | task: STORY_DATA.tasks[0], 21 | x: 50, 22 | y: 50, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/tasks/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useMemo } from "react"; 2 | import { Label } from "react-konva"; 3 | import { Html } from "react-konva-utils"; 4 | import { DateTime, Duration } from "luxon"; 5 | 6 | import { useTimelineContext } from "../../../timeline/TimelineContext"; 7 | import { KonvaPoint } from "../../../utils/konva"; 8 | import { TaskData } from "../../utils/tasks"; 9 | 10 | import DefaultToolTip from "./DefaultToolTip"; 11 | 12 | export interface TaskTooltipProps extends KonvaPoint { 13 | task: TaskData; 14 | } 15 | const rightMarginOffsetX = -230; 16 | const standardMarginOffsetX = 15; 17 | const marginOffsetY = 25; 18 | const sevenHourinMillis = 25200000; 19 | const twoDayinMillis = 172800000; 20 | /** 21 | * This component renders a task tooltip inside a canvas. 22 | */ 23 | const TaskTooltip: FC = ({ task, x, y }) => { 24 | const { 25 | drawRange: { end: drawEnd }, 26 | resources, 27 | localized, 28 | customToolTip, 29 | } = useTimelineContext(); 30 | 31 | const { 32 | label, 33 | completedPercentage, 34 | time: { start, end }, 35 | resourceId, 36 | } = task; 37 | const startDuration = useMemo(() => { 38 | return DateTime.fromMillis(Number(start)).toFormat("dd/MM/yyyy HH:mm:ss"); 39 | }, [start]); 40 | const endDuration = useMemo(() => { 41 | return DateTime.fromMillis(Number(end)).toFormat("dd/MM/yyyy HH:mm:ss"); 42 | }, [end]); 43 | const percentage = useMemo(() => { 44 | return completedPercentage + "%"; 45 | }, [completedPercentage]); 46 | 47 | const duration = useMemo(() => { 48 | const part = Number(end) - Number(start); 49 | if (part < sevenHourinMillis) { 50 | const min = Duration.fromObject({ ["millisecond"]: part }).as("minute"); 51 | return { time: Math.round(min * 10) / 10, unit: "min" }; 52 | } 53 | if (part < twoDayinMillis) { 54 | const hour = Duration.fromObject({ ["millisecond"]: part }).as("hour"); 55 | return { time: Math.round(hour * 10) / 10, unit: "hour" }; 56 | } 57 | const day = Duration.fromObject({ ["millisecond"]: part }).as("day"); 58 | return { time: Math.round(day * 10) / 10, unit: "Day" }; 59 | }, [start, end]); 60 | 61 | const offsetToolTip = useMemo(() => { 62 | if (resourceId === resources[1].id) { 63 | if (x > drawEnd + rightMarginOffsetX) { 64 | return { x: rightMarginOffsetX, y: marginOffsetY }; 65 | } 66 | return { x: standardMarginOffsetX, y: marginOffsetY }; 67 | } 68 | 69 | if (resourceId === resources[resources.length - 1].id) { 70 | if (x > drawEnd + rightMarginOffsetX) { 71 | return { x: rightMarginOffsetX, y: marginOffsetY * 4 }; 72 | } 73 | return { x: standardMarginOffsetX, y: marginOffsetY * 4 }; 74 | } 75 | 76 | if (x > drawEnd + rightMarginOffsetX) { 77 | return { x: rightMarginOffsetX, y: marginOffsetY * 2 }; 78 | } 79 | return { x: standardMarginOffsetX, y: marginOffsetY * 2 }; 80 | }, [drawEnd, resourceId, x, resources]); 81 | 82 | const customToolTipData = useMemo(() => { 83 | return { ...task, start: startDuration, end: endDuration }; 84 | }, [task, startDuration, endDuration]); 85 | 86 | const toolTip = useMemo(() => { 87 | return !customToolTip ? ( 88 | 97 | ) : ( 98 |
99 | {customToolTip(customToolTipData)} 100 |
101 | ); 102 | }, [ 103 | completedPercentage, 104 | duration, 105 | endDuration, 106 | label, 107 | localized, 108 | startDuration, 109 | percentage, 110 | customToolTip, 111 | customToolTipData, 112 | ]); 113 | 114 | return ( 115 | 118 | ); 119 | }; 120 | 121 | export default TaskTooltip; 122 | -------------------------------------------------------------------------------- /src/tasks/utils/line.ts: -------------------------------------------------------------------------------- 1 | import { LineData } from "../../timeline/TimelineContext"; 2 | 3 | import { TASK_HEIGHT_OFFSET } from "./tasks"; 4 | 5 | export type LineType = { 6 | points: number[]; 7 | }; 8 | 9 | export type AnchorPoint = { x: number; y: number }; 10 | 11 | export const CIRCLE_POINT_OFFSET = 4; 12 | export const LINE_COLOR = "rgb(135,133,239)"; 13 | export const CIRCLE_POINT_COLOR = "rgb(141,141,141)"; 14 | export const CIRCLE_POINT_STROKE = "rgb(74,88,97)"; 15 | export const LINE_TENSION = 0.5; 16 | export const LINE_WIDTH = 2; 17 | export const LINE_OFFSET = 20; 18 | 19 | export const getLineData = ( 20 | connectLine: LineData[], 21 | rowHeight: number, 22 | 23 | getTaskXCoordinate: (startTime: number) => number, 24 | getTaskYCoordinate: (rowIndex: number, rowHeight: number) => number, 25 | type: "back" | "front" 26 | ) => { 27 | const anchorArr: AnchorPoint[] = []; 28 | const workLineArr: string[] = []; 29 | const taskY = type === "back" ? "startResId" : "endResId"; 30 | const taskX = type === "back" ? "start" : "end"; 31 | connectLine.forEach((i) => { 32 | const anchY = getTaskYCoordinate(+i[taskY], rowHeight) + (rowHeight * TASK_HEIGHT_OFFSET) / 2; 33 | const anchX = getTaskXCoordinate(i[taskX]); 34 | anchorArr.push({ x: anchX, y: anchY }); 35 | workLineArr.push(i.id); 36 | }); 37 | return { anchorArr, workLineArr }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/tasks/utils/tasks.test.ts: -------------------------------------------------------------------------------- 1 | import { generateStoryData } from "../../utils/stories/utils"; 2 | import { InternalTimeRange } from "../../utils/time"; 3 | 4 | import { getTaskYCoordinate, TASK_HEIGHT_OFFSET, validateTasks } from "./tasks"; 5 | 6 | // From: Sunday, 1 January 2023 00:00:00 GMT+01:00 7 | // To: Monday, 2 January 2023 00:00:00 GMT+01:00 8 | const range: InternalTimeRange = { start: 1672527600000, end: 1672614000000 }; 9 | const timezone = "Europe/Rome"; 10 | 11 | describe("getTaskYCoordinate", () => { 12 | it("valid", () => { 13 | const ROW_HEIGHT = 50; 14 | const resourceIndex = Math.ceil(Math.random() * 10); 15 | const yCoordinate = getTaskYCoordinate(resourceIndex, ROW_HEIGHT); 16 | expect(yCoordinate % ROW_HEIGHT).toEqual(ROW_HEIGHT * +((1 - TASK_HEIGHT_OFFSET) / 2).toFixed(2)); 17 | }); 18 | }); 19 | 20 | describe("validateTasks", () => { 21 | it("empty", () => { 22 | const tasks = validateTasks([], range, timezone); 23 | expect(tasks).toEqual({ 24 | errors: [{ entity: "timeline", level: "warn", message: "No data" }], 25 | items: [], 26 | }); 27 | }); 28 | 29 | it("task invalid", () => { 30 | const tasks = validateTasks( 31 | [{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672578000000, end: 1672563600000 } }], 32 | range, 33 | timezone 34 | ); 35 | 36 | expect(tasks).toEqual({ 37 | errors: [{ entity: "task", level: "error", message: "Invalid time", refId: "1" }], 38 | items: [], 39 | }); 40 | }); 41 | 42 | it("task out of interval", () => { 43 | const tasks = validateTasks( 44 | [{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672470000000, end: 1672477200000 } }], 45 | range, 46 | timezone 47 | ); 48 | 49 | expect(tasks).toEqual({ 50 | errors: [{ entity: "task", level: "warn", message: "Outside range", refId: "1" }], 51 | items: [], 52 | }); 53 | }); 54 | 55 | it("valid", () => { 56 | const tasks = validateTasks( 57 | [{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672556400000, end: 1672578000000 } }], 58 | range, 59 | timezone 60 | ); 61 | 62 | expect(tasks).toEqual({ 63 | errors: [], 64 | items: [{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672556400000, end: 1672578000000 } }], 65 | }); 66 | }); 67 | 68 | it("mixed", () => { 69 | const tasks = validateTasks( 70 | [ 71 | { id: "1", label: "Task #1", resourceId: "1", time: { start: 1672556400000, end: 1672578000000 } }, 72 | { id: "2", label: "Task #2", resourceId: "1", time: { start: 1672470000000, end: 1672477200000 } }, 73 | { id: "3", label: "Task #3", resourceId: "1", time: { start: 1672578000000, end: 1672563600000 } }, 74 | ], 75 | range, 76 | timezone 77 | ); 78 | 79 | expect(tasks).toEqual({ 80 | errors: [ 81 | { entity: "task", level: "warn", message: "Outside range", refId: "2" }, 82 | { entity: "task", level: "error", message: "Invalid time", refId: "3" }, 83 | ], 84 | items: [{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672556400000, end: 1672578000000 } }], 85 | }); 86 | }); 87 | 88 | it("bulk - monthly", () => { 89 | const { range, tasks: allTasks } = generateStoryData({ 90 | averageTaskDurationInMinutes: 10, 91 | resourcesCount: 16, 92 | tasksCount: 100000, 93 | timeRangeInDays: 60, 94 | }); 95 | 96 | const start = new Date().valueOf(); 97 | validateTasks(allTasks, range, timezone); 98 | const end = new Date().valueOf(); 99 | const operationLength = end - start; 100 | console.warn(`Filter tasks: ${operationLength} ms`); 101 | expect(operationLength).toBeLessThan(100); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/utils/dimensions.ts: -------------------------------------------------------------------------------- 1 | export const BASE_DIMENSION = 8; 2 | 3 | // export const DEFAULT_GRID_ROW_HEIGHT = BASE_DIMENSION * 6; 4 | export const DEFAULT_GRID_ROW_HEIGHT = 50; 5 | export const MINIMUM_GRID_ROW_HEIGHT = DEFAULT_GRID_ROW_HEIGHT / 2; 6 | 7 | // export const DEFAULT_GRID_COLUMN_WIDTH = BASE_DIMENSION * 8; 8 | export const DEFAULT_GRID_COLUMN_WIDTH = 60; 9 | export const MIN_GRID_COLUMN_WIDTH = 20; 10 | 11 | export const DEFAULT_TEXT_SIZE = BASE_DIMENSION * 1.5; 12 | 13 | export const DEFAULT_STROKE_WIDTH = 1; 14 | -------------------------------------------------------------------------------- /src/utils/konva.ts: -------------------------------------------------------------------------------- 1 | import { KonvaEventObject } from "konva/lib/Node"; 2 | 3 | export interface KonvaDrawable { 4 | /** 5 | * The fill color of a canvas item 6 | */ 7 | fill?: string; 8 | /** 9 | * The fill color of a canvas item 10 | */ 11 | fillToComplete?: string; 12 | /** 13 | * The stroke color of a canvas item 14 | */ 15 | stroke?: string; 16 | } 17 | 18 | type KonvaMouseEvent = KonvaEventObject; 19 | 20 | export type KonvaMouseEventCallback = (e: KonvaMouseEvent) => void; 21 | 22 | export interface KonvaMouseEvents { 23 | /** 24 | * On mouse click event handler 25 | */ 26 | onClick?: KonvaMouseEventCallback; 27 | /** 28 | * On mouse leave event handler 29 | */ 30 | onMouseLeave?: KonvaMouseEventCallback; 31 | /** 32 | * On mouse over event handler 33 | */ 34 | onMouseOver?: KonvaMouseEventCallback; 35 | } 36 | 37 | export interface KonvaPoint { 38 | /** 39 | * The x coordinate of a point on canvas 40 | */ 41 | x: number; 42 | /** 43 | * The y coordinate of a point on canvas 44 | */ 45 | y: number; 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = "debug" | "error" | "warn"; 2 | 3 | const TAG = "[@melfore/konva-timeline]"; 4 | 5 | /** 6 | * Logs message for given level and component 7 | * @param level the level of the message to log (e.g. "error") 8 | * @param component the component subject of the log 9 | * @param message the message of the log 10 | */ 11 | const logger = (level: LogLevel, component: string, message: string) => { 12 | const text = `${TAG} ${component} - ${message}`; 13 | 14 | switch (level) { 15 | case "debug": 16 | // eslint-disable-next-line no-console 17 | console.info(text); 18 | return; 19 | case "error": 20 | console.error(text); 21 | return; 22 | case "warn": 23 | console.warn(text); 24 | return; 25 | } 26 | }; 27 | 28 | /** 29 | * Logs message for info level and component only if debug mode enabled 30 | * @param component the component subject of the log 31 | * @param message the message of the log 32 | */ 33 | export const logDebug = (component: string, message: string) => { 34 | if (!window.__MELFORE_KONVA_TIMELINE_DEBUG__) { 35 | return; 36 | } 37 | 38 | logger("debug", component, message); 39 | }; 40 | 41 | /** 42 | * Logs message for error level and component 43 | * @param component the component subject of the log 44 | * @param message the message of the log 45 | */ 46 | export const logError = (component: string, message: string) => logger("error", component, message); 47 | 48 | /** 49 | * Logs message for warn level and component 50 | * @param component the component subject of the log 51 | * @param message the message of the log 52 | */ 53 | export const logWarn = (component: string, message: string) => logger("warn", component, message); 54 | -------------------------------------------------------------------------------- /src/utils/operations.ts: -------------------------------------------------------------------------------- 1 | type Entity = "interval" | "task" | "timeline"; 2 | 3 | type Level = "error" | "warn"; 4 | 5 | export interface KonvaTimelineError { 6 | entity: Entity; 7 | level: Level; 8 | message: string; 9 | refId?: string; 10 | } 11 | 12 | export type Operation = { 13 | items: T[]; 14 | errors: KonvaTimelineError[]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/stories/decorators/Gantt.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement, ReactElement, useCallback, useEffect, useState } from "react"; 2 | import React from "react"; 3 | import { Decorator } from "@storybook/react"; 4 | 5 | import { TaskData } from "../../.."; 6 | import { TimelineProviderProps } from "../../../timeline/TimelineContext"; 7 | 8 | const GanttMock = ({ 9 | children, 10 | onTaskChange: externalOnTaskChange, 11 | tasks: externalTasks, 12 | ...props 13 | }: TimelineProviderProps) => { 14 | const [tasks, setTasks] = useState(externalTasks || []); 15 | 16 | useEffect(() => { 17 | if (externalTasks !== tasks) { 18 | setTasks(externalTasks || []); 19 | } 20 | // eslint-disable-next-line react-hooks/exhaustive-deps 21 | }, [externalTasks]); 22 | 23 | const onTaskChange = useCallback( 24 | (task: TaskData, opts?: { tasksId: string[]; addTime: number }) => { 25 | externalOnTaskChange && externalOnTaskChange(task, opts); 26 | const newTasks = tasks.map((i) => { 27 | if (opts && opts.tasksId.includes(i.id)) { 28 | const start = i.time.start; 29 | return { ...i, time: { start: Number(start) + opts.addTime, end: Number(i.time.end) + opts.addTime } }; 30 | } 31 | 32 | if (i.id === task.id) { 33 | return task; 34 | } 35 | 36 | return i; 37 | }); 38 | 39 | setTasks(newTasks); 40 | }, 41 | [externalOnTaskChange, tasks] 42 | ); 43 | 44 | return <>{cloneElement(children as ReactElement, { onTaskChange, tasks, ...props })}; 45 | }; 46 | 47 | const GanttDecorator: Decorator = (Story, { args }) => ( 48 | {Story()} 49 | ); 50 | 51 | export default GanttDecorator; 52 | -------------------------------------------------------------------------------- /src/utils/stories/decorators/Tasks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef } from "react"; 2 | import { Layer, Stage } from "react-konva"; 3 | import { Decorator } from "@storybook/react"; 4 | 5 | import { TimelineProvider, useTimelineContext } from "../../../timeline/TimelineContext"; 6 | import { generateStoryData } from "../utils"; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | export const COLOR_ARG_TYPE: any = { 10 | control: { 11 | type: "color", 12 | }, 13 | }; 14 | 15 | export const STORY_DATA = generateStoryData({ 16 | averageTaskDurationInMinutes: 180, 17 | resourcesCount: 3, 18 | tasksCount: 10, 19 | timeRangeInDays: 1, 20 | }); 21 | 22 | export const LayerDecorator: Decorator = (Story) => { 23 | return {Story()}; 24 | }; 25 | 26 | export const TaskDecorator: Decorator = (Story) => { 27 | const wrapperRef = useRef(null); 28 | 29 | const [width, setWidth] = React.useState(0); 30 | 31 | useEffect(() => { 32 | if (!wrapperRef.current) { 33 | return; 34 | } 35 | 36 | const { width } = wrapperRef.current.getBoundingClientRect(); 37 | setWidth(width); 38 | }, []); 39 | 40 | return ( 41 | alert(`OnTaskClick event handler - TaskId: ${task.id}`)} 44 | onTaskChange={(task) => alert(`OnTaskChange event handler - TaskId: ${task.id}`)} 45 | resolution="1hrs" 46 | > 47 |
48 | 49 | {Story()} 50 | 51 |
52 |
53 | ); 54 | }; 55 | 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 57 | const TasksLayerInternalDecorator = ({ storyFn }: any) => { 58 | const { setDrawRange } = useTimelineContext(); 59 | 60 | const stageWidth = useMemo(() => 60 * 24, []); 61 | 62 | useEffect(() => { 63 | setDrawRange({ start: 0, end: stageWidth + 200 }); 64 | }, [setDrawRange, stageWidth]); 65 | 66 | return ( 67 |
68 | 69 | {storyFn()} 70 | 71 |
72 | ); 73 | }; 74 | 75 | export const TasksLayerDecorator: Decorator = (Story) => { 76 | return ( 77 | alert(`OnTaskClick event handler - TaskId: ${task.id}`)} 80 | onTaskChange={(task) => alert(`OnTaskChange event handler - TaskId: ${task.id}`)} 81 | resolution="1hrs" 82 | > 83 | 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/utils/stories/decorators/Timeline.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement, ReactElement, useCallback, useEffect, useState } from "react"; 2 | import React from "react"; 3 | import { Decorator } from "@storybook/react"; 4 | 5 | import { KonvaTimelineError, TaskData } from "../../.."; 6 | import { TimelineProviderProps } from "../../../timeline/TimelineContext"; 7 | 8 | const TimeLineMock = ({ 9 | children, 10 | onErrors: externalOnErrors, 11 | onTaskChange: externalOnTaskChange, 12 | tasks: externalTasks, 13 | ...props 14 | }: TimelineProviderProps) => { 15 | const [tasks, setTasks] = useState(externalTasks || []); 16 | 17 | useEffect(() => { 18 | if (externalTasks !== tasks) { 19 | setTasks(externalTasks || []); 20 | } 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | }, [externalTasks]); 23 | 24 | const onTaskChange = useCallback( 25 | (task: TaskData) => { 26 | externalOnTaskChange && externalOnTaskChange(task); 27 | setTasks((prevTasks) => prevTasks.map((currentTask) => (currentTask.id === task.id ? task : currentTask))); 28 | }, 29 | [externalOnTaskChange] 30 | ); 31 | 32 | const onErrors = useCallback( 33 | (errors: KonvaTimelineError[]) => { 34 | if (!errors || !errors.length) { 35 | return; 36 | } 37 | 38 | externalOnErrors && externalOnErrors(errors); 39 | // eslint-disable-next-line no-console 40 | errors.forEach((error) => console[error.level]({ ...error })); 41 | }, 42 | [externalOnErrors] 43 | ); 44 | 45 | return <>{cloneElement(children as ReactElement, { onErrors, onTaskChange, tasks, ...props })}; 46 | }; 47 | 48 | //export default TimelineDecorator; 49 | const TimelineDecorator: Decorator = (Story, { args }) => ( 50 | {Story()} 51 | ); 52 | 53 | export default TimelineDecorator; 54 | -------------------------------------------------------------------------------- /src/utils/stories/utils.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "../../resources/utils/resources"; 2 | import { TaskData } from "../../tasks/utils/tasks"; 3 | import { InternalTimeRange } from "../time"; 4 | 5 | interface StoryDataInput { 6 | resourcesCount: number; 7 | tasksCount: number; 8 | timeRangeInDays: number; 9 | averageTaskDurationInMinutes: number; 10 | } 11 | 12 | export interface StoryData { 13 | resources: Resource[]; 14 | tasks: TaskData[]; 15 | range: InternalTimeRange; 16 | } 17 | 18 | export const HOUR_IN_MILLISECONDS = 1000 * 60 * 60; 19 | 20 | export const TIME_RANGE_START_DATE = new Date("2019-12-31T23:00:00.000Z"); 21 | 22 | const getRandomColor = () => { 23 | let color = ""; 24 | while (color.length !== 6) { 25 | color = Math.floor(Math.random() * 16777215).toString(16); 26 | } 27 | 28 | return color; 29 | }; 30 | 31 | const generateResources = (count: number): Resource[] => { 32 | const resources: Resource[] = []; 33 | 34 | for (let i = 1; i <= count; i++) { 35 | resources.push({ 36 | id: `${i}`, 37 | label: `Resource #${i}`, 38 | color: `#${getRandomColor()}`, 39 | }); 40 | } 41 | 42 | return resources; 43 | }; 44 | 45 | const generateTasks = ( 46 | count: number, 47 | avgDurationInMinutes: number, 48 | resourcesCount: number 49 | ): TaskData[] => { 50 | const tasks: TaskData[] = []; 51 | 52 | for (let i = 1; i <= count; i++) { 53 | const resourceId = `${Math.floor(Math.random() * resourcesCount) + 1}`; 54 | const lastTaskForResource = tasks.reverse().find((task) => task.resourceId === resourceId); 55 | 56 | let start = TIME_RANGE_START_DATE.valueOf(); 57 | if (lastTaskForResource) { 58 | start = 59 | lastTaskForResource.time.end + 60 | Math.floor(avgDurationInMinutes / 2) + 61 | Math.floor(Math.random() * (avgDurationInMinutes * 2)) * 60 * 1000; 62 | } 63 | 64 | const end = start + Math.floor(Math.random() * (avgDurationInMinutes * 2)) * 60 * 1000; 65 | 66 | tasks.push({ 67 | id: `${i}`, 68 | resourceId, 69 | label: `Task #${i}`, 70 | time: { start, end }, 71 | }); 72 | } 73 | 74 | return tasks; 75 | }; 76 | 77 | const generateTimeRange = (durationInDays: number): InternalTimeRange => { 78 | const start = TIME_RANGE_START_DATE.valueOf(); 79 | const end = TIME_RANGE_START_DATE.setDate(TIME_RANGE_START_DATE.getDate() + durationInDays).valueOf(); 80 | 81 | return { start, end }; 82 | }; 83 | 84 | export const generateStoryData = ({ 85 | averageTaskDurationInMinutes, 86 | resourcesCount, 87 | tasksCount, 88 | timeRangeInDays, 89 | }: StoryDataInput): StoryData => { 90 | const resources = generateResources(resourcesCount); 91 | const tasks = generateTasks(tasksCount, averageTaskDurationInMinutes, resourcesCount); 92 | const range = generateTimeRange(timeRangeInDays); 93 | 94 | return { resources, tasks, range }; 95 | }; 96 | -------------------------------------------------------------------------------- /src/utils/theme.test.ts: -------------------------------------------------------------------------------- 1 | import { getContrastColor } from "./theme"; 2 | 3 | describe("getContrastTextColor", () => { 4 | it("empty", () => { 5 | expect(() => getContrastColor("")).toThrowError("Missing HEX color!"); 6 | }); 7 | 8 | it("< 6 digits", () => { 9 | expect(() => getContrastColor("#93c41")).toThrowError("Invalid HEX color!"); 10 | }); 11 | 12 | it("black", () => { 13 | const ctxColor = getContrastColor("#000000"); 14 | expect(ctxColor).toEqual("#FFFFFF"); 15 | }); 16 | 17 | it("dark - 6 digits", () => { 18 | const ctxColor = getContrastColor("#101a00"); 19 | expect(ctxColor).toEqual("#FFFFFF"); 20 | }); 21 | 22 | it("white", () => { 23 | const ctxColor = getContrastColor("#FFFFFF"); 24 | expect(ctxColor).toEqual("#000000"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | const HEX_BASE = 16; 2 | const HEX_COLOR_LENGTH = 6; 3 | 4 | const LUMA_FACTOR_R = 299; 5 | const LUMA_FACTOR_G = 587; 6 | const LUMA_FACTOR_B = 114; 7 | const LUMA_FACTOR_BW = 128; 8 | 9 | type RGBType = { r: number; g: number; b: number }; 10 | 11 | export const DEFAULT_ROW_LIGHT_MODE = "#F0F0F0"; 12 | export const ALTERNATIVE_ROW = "transparent"; 13 | export const DEFAULT_ROW_DARK_MODE = "#A8A8A8"; 14 | export const DEFAULT_STROKE_LIGHT_MODE = "grey"; 15 | export const DEFAULT_STROKE_DARK_MODE = "white"; 16 | 17 | export const getRGB = (hex: string) => { 18 | if (!hex || !hex.length) { 19 | throw new Error("Missing HEX color!"); 20 | } 21 | 22 | let hexColor = hex; 23 | if (hexColor.indexOf("#") === 0) { 24 | hexColor = hexColor.slice(1); 25 | } 26 | 27 | if (hexColor.length === HEX_COLOR_LENGTH / 2) { 28 | hexColor = `${hexColor[0]}${hexColor[0]}${hexColor[1]}${hexColor[1]}${hexColor[2]}${hexColor[2]}`; 29 | } 30 | 31 | if (hexColor.length !== HEX_COLOR_LENGTH) { 32 | throw new Error("Invalid HEX color!"); 33 | } 34 | 35 | const r = parseInt(hexColor.substring(0, 2), HEX_BASE); 36 | const g = parseInt(hexColor.substring(2, 4), HEX_BASE); 37 | const b = parseInt(hexColor.substring(4, 6), HEX_BASE); 38 | 39 | return { r: r, g: g, b: b }; 40 | }; 41 | 42 | export const getRGBA = (hex: string) => { 43 | if (!hex || !hex.length) { 44 | throw new Error("Missing HEX color!"); 45 | } 46 | 47 | let hexColor = hex; 48 | if (hexColor.indexOf("#") === 0) { 49 | hexColor = hexColor.slice(1); 50 | } 51 | 52 | if (hexColor.length === HEX_COLOR_LENGTH / 2) { 53 | hexColor = `${hexColor[0]}${hexColor[0]}${hexColor[1]}${hexColor[1]}${hexColor[2]}${hexColor[2]}`; 54 | } 55 | if (hexColor.length === HEX_COLOR_LENGTH + 2) { 56 | const r = parseInt(hexColor.substring(0, 2), HEX_BASE); 57 | const g = parseInt(hexColor.substring(2, 4), HEX_BASE); 58 | const b = parseInt(hexColor.substring(4, 6), HEX_BASE); 59 | const a = parseInt(hexColor.substring(6, 8)) / 100; 60 | return { r: r, g: g, b: b, a: a }; 61 | } 62 | 63 | if (hexColor.length !== HEX_COLOR_LENGTH) { 64 | throw new Error("Invalid HEX color!"); 65 | } 66 | 67 | const r = parseInt(hexColor.substring(0, 2), HEX_BASE); 68 | const g = parseInt(hexColor.substring(2, 4), HEX_BASE); 69 | const b = parseInt(hexColor.substring(4, 6), HEX_BASE); 70 | 71 | return { r: r, g: g, b: b }; 72 | }; 73 | 74 | /** 75 | * Gets the black / white contrast color for given color 76 | * @param hex the color to be contrasted in hex format (e.g. '#000000') 77 | */ 78 | export const getContrastColor = (hex: string) => { 79 | const rgb = getRGB(hex); 80 | const luma = (rgb.r * LUMA_FACTOR_R + rgb.g * LUMA_FACTOR_G + rgb.b * LUMA_FACTOR_B) / 1000; 81 | 82 | return luma >= LUMA_FACTOR_BW ? "#000000" : "#FFFFFF"; 83 | }; 84 | 85 | export const RGBFromRGBA = (opacity: number, rgb: RGBType) => { 86 | const r3 = Math.round((1 - opacity) * 255 + opacity * rgb.r); 87 | const g3 = Math.round((1 - opacity) * 255 + opacity * rgb.g); 88 | const b3 = Math.round((1 - opacity) * 255 + opacity * rgb.b); 89 | return "rgb(" + r3 + "," + g3 + "," + b3 + ")"; 90 | }; 91 | -------------------------------------------------------------------------------- /src/utils/time-resolution.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Interval } from "luxon"; 2 | 3 | import { DEFAULT_GRID_COLUMN_WIDTH } from "./dimensions"; 4 | 5 | export type Scale = "minute" | "hour" | "day" | "week" | "month" | "year"; 6 | 7 | export type Resolution = 8 | | "1min" 9 | | "5min" 10 | | "10min" 11 | | "15min" 12 | | "30min" 13 | | "1hrs" 14 | | "2hrs" 15 | | "6hrs" 16 | | "12hrs" 17 | | "1day" 18 | | "1week" 19 | | "2weeks"; 20 | 21 | export type ResolutionData = { 22 | columnSize: number; 23 | label: string; 24 | sizeInUnits: number; 25 | unit: Scale; 26 | unitAbove: Scale; 27 | }; 28 | 29 | type ResolutionsData = { 30 | [key in Resolution]: ResolutionData; 31 | }; 32 | 33 | const RESOLUTIONS_DATA: ResolutionsData = { 34 | "1min": { 35 | columnSize: DEFAULT_GRID_COLUMN_WIDTH / 2, 36 | label: "1 Minute", 37 | sizeInUnits: 1, 38 | unit: "minute", 39 | unitAbove: "hour", 40 | }, 41 | "5min": { 42 | columnSize: DEFAULT_GRID_COLUMN_WIDTH / 2, 43 | label: "5 Minutes", 44 | sizeInUnits: 5, 45 | unit: "minute", 46 | unitAbove: "hour", 47 | }, 48 | "10min": { 49 | columnSize: DEFAULT_GRID_COLUMN_WIDTH / 2, 50 | label: "10 Minutes", 51 | sizeInUnits: 10, 52 | unit: "minute", 53 | unitAbove: "hour", 54 | }, 55 | "15min": { 56 | columnSize: DEFAULT_GRID_COLUMN_WIDTH, 57 | label: "15 Minutes", 58 | sizeInUnits: 15, 59 | unit: "minute", 60 | unitAbove: "hour", 61 | }, 62 | "30min": { 63 | columnSize: DEFAULT_GRID_COLUMN_WIDTH, 64 | label: "30 Minutes", 65 | sizeInUnits: 30, 66 | unit: "minute", 67 | unitAbove: "hour", 68 | }, 69 | "1hrs": { 70 | columnSize: DEFAULT_GRID_COLUMN_WIDTH, 71 | label: "1 Hour", 72 | sizeInUnits: 1, 73 | unit: "hour", 74 | unitAbove: "day", 75 | }, 76 | "2hrs": { 77 | columnSize: DEFAULT_GRID_COLUMN_WIDTH, 78 | label: "2 Hours", 79 | sizeInUnits: 2, 80 | unit: "hour", 81 | unitAbove: "day", 82 | }, 83 | "6hrs": { 84 | columnSize: DEFAULT_GRID_COLUMN_WIDTH * 2, 85 | label: "1/4 of Day", 86 | sizeInUnits: 6, 87 | unit: "hour", 88 | unitAbove: "day", 89 | }, 90 | "12hrs": { 91 | columnSize: DEFAULT_GRID_COLUMN_WIDTH * 3, 92 | label: "1/2 of Day", 93 | sizeInUnits: 12, 94 | unit: "hour", 95 | unitAbove: "day", 96 | }, 97 | "1day": { 98 | columnSize: DEFAULT_GRID_COLUMN_WIDTH * 3, 99 | label: "1 Day", 100 | sizeInUnits: 1, 101 | unit: "day", 102 | unitAbove: "week", 103 | }, 104 | "1week": { 105 | columnSize: DEFAULT_GRID_COLUMN_WIDTH * 10, 106 | label: "1 Week", 107 | sizeInUnits: 1, 108 | unit: "week", 109 | unitAbove: "month", 110 | }, 111 | "2weeks": { 112 | columnSize: DEFAULT_GRID_COLUMN_WIDTH * 10, 113 | label: "2 Weeks", 114 | sizeInUnits: 2, 115 | unit: "week", 116 | unitAbove: "month", 117 | }, 118 | }; 119 | 120 | export const RESOLUTIONS: Resolution[] = [ 121 | "1min", 122 | "5min", 123 | "10min", 124 | "15min", 125 | "30min", 126 | "1hrs", 127 | "2hrs", 128 | "6hrs", 129 | "12hrs", 130 | "1day", 131 | "1week", 132 | "2weeks", 133 | ]; 134 | 135 | /** 136 | * Util to display an interval in a human readable format 137 | * @param interval the interval to display 138 | * @param unit the unit in which to display the interval 139 | */ 140 | export const displayAboveInterval = (interval: Interval, unit: Scale, locale: string): string => { 141 | const { start } = interval; 142 | if (!start) { 143 | return "-"; 144 | } 145 | switch (unit) { 146 | case "minute": 147 | case "hour": 148 | return start.setLocale(locale).toFormat("dd/MM/yy HH:mm"); 149 | case "day": 150 | return start.setLocale(locale).toFormat("DDDD"); 151 | case "week": 152 | return `${start.setLocale(locale).toFormat("MMMM yyyy")} CW ${start.toFormat("WW")}`; 153 | case "month": 154 | return start.setLocale(locale).toFormat("MMMM yyyy"); 155 | default: 156 | return "N/A"; 157 | } 158 | }; 159 | 160 | export const getMonth = (interval: Interval): string => { 161 | const { start } = interval; 162 | if (!start) { 163 | return "-"; 164 | } 165 | 166 | return start.toFormat("M"); 167 | }; 168 | export const getYear = (interval: Interval): string => { 169 | const { start } = interval; 170 | if (!start) { 171 | return "-"; 172 | } 173 | 174 | return start.toFormat("yyyy"); 175 | }; 176 | 177 | export const getStartMonthsDay = (start: DateTime): string => { 178 | if (!start) { 179 | return "-"; 180 | } 181 | 182 | return start.toFormat("d"); 183 | }; 184 | 185 | export const daysInMonth = (month: number, year: number) => { 186 | return new Date(year, month, 0).getDate(); 187 | }; 188 | 189 | /** 190 | * Util to display an interval in a human readable format 191 | * @param interval the interval to display 192 | * @param unit the unit in which to display the interval 193 | */ 194 | export const displayInterval = (interval: Interval, unit: Scale, locale: string): string => { 195 | const { start } = interval; 196 | if (!start) { 197 | return "-"; 198 | } 199 | 200 | switch (unit) { 201 | case "minute": 202 | return start.setLocale(locale).toFormat("mm"); 203 | case "hour": 204 | return start.setLocale(locale).toFormat("HH:mm"); 205 | case "day": 206 | return start.setLocale(locale).toFormat("ccc dd"); 207 | case "week": 208 | return `CW ${start.setLocale(locale).toFormat("WW")}`; 209 | case "month": 210 | return start.setLocale(locale).toFormat("MMM yyyy"); 211 | default: 212 | return "N/A"; 213 | } 214 | }; 215 | 216 | /** 217 | * Gets the resolution data for the given key 218 | * @param key key of the resolution to get 219 | */ 220 | export const getResolutionData = (key: Resolution): ResolutionData => RESOLUTIONS_DATA[key]; 221 | -------------------------------------------------------------------------------- /src/utils/time.test.ts: -------------------------------------------------------------------------------- 1 | import { getValidTime } from "./time"; 2 | 3 | describe("getValidTime", () => { 4 | it("invalid", () => { 5 | expect(getValidTime(NaN, "utc")).toBe(NaN); 6 | }); 7 | 8 | it("millis", () => { 9 | expect(getValidTime(1697531400000, "utc")).toEqual(1697531400000); 10 | }); 11 | 12 | it("ISO from UTC to machine", () => { 13 | expect(getValidTime("2023-10-19T00:00:00.000Z", undefined)).toEqual(1697673600000); 14 | expect(getValidTime("2023-10-19T00:00:00.000Z", "utc")).toEqual(1697673600000); 15 | }); 16 | 17 | it("ISO from machine to machine", () => { 18 | expect(getValidTime("2023-10-19T00:00:00.000+02:00", undefined)).toEqual(1697666400000); 19 | expect(getValidTime("2023-10-19T00:00:00.000+02:00", "utc")).toEqual(1697666400000); 20 | }); 21 | 22 | it("ISO from any to any", () => { 23 | expect(getValidTime("2023-10-19T00:00:00.000+02:00", "America/New_York")).toEqual(1697666400000); 24 | }); 25 | 26 | it("load - numbers", () => { 27 | const now = new Date().getTime(); 28 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 29 | const dates = new Array(10000).fill(0).map((_d) => { 30 | return Math.floor(Math.random() * now); 31 | }); 32 | 33 | const start = new Date().valueOf(); 34 | dates.forEach((d) => getValidTime(d, "utc")); 35 | const end = new Date().valueOf(); 36 | const operationLength = end - start; 37 | console.warn(`Validate dates Number: ${operationLength} ms`); 38 | expect(operationLength).toBeLessThan(500); 39 | }); 40 | 41 | it("load - strings", () => { 42 | const now = new Date().getTime(); 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | const dates = new Array(10000).fill(0).map((_d) => { 45 | const millis = Math.floor(Math.random() * now); 46 | return new Date(millis).toISOString(); 47 | }); 48 | 49 | const start = new Date().valueOf(); 50 | dates.forEach((d) => getValidTime(d, "utc")); 51 | const end = new Date().valueOf(); 52 | const operationLength = end - start; 53 | console.warn(`Validate dates String: ${operationLength} ms`); 54 | expect(operationLength).toBeLessThan(500); 55 | }); 56 | 57 | it("load - Date", () => { 58 | const now = new Date().getTime(); 59 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 60 | const dates = new Array(10000).fill(0).map((_d) => { 61 | const millis = Math.floor(Math.random() * now); 62 | return new Date(millis); 63 | }); 64 | 65 | const start = new Date().valueOf(); 66 | dates.forEach((d) => getValidTime(d, "utc")); 67 | const end = new Date().valueOf(); 68 | const operationLength = end - start; 69 | console.warn(`Validate dates Date: ${operationLength} ms`); 70 | expect(operationLength).toBeLessThan(500); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Interval } from "luxon"; 2 | 3 | import { logError } from "./logger"; 4 | import { ResolutionData } from "./time-resolution"; 5 | 6 | type TimeInput = number | string | Date; 7 | 8 | export interface TimeRange { 9 | /** 10 | * Start of time range interval 11 | */ 12 | start: TimeInput; 13 | /** 14 | * End of time range interval 15 | */ 16 | end: TimeInput; 17 | } 18 | 19 | export interface InternalTimeRange { 20 | start: number; 21 | end: number; 22 | } 23 | 24 | /** 25 | * Returns valid date based on input, otherwise now 26 | * @param date the input date (number or string formats) 27 | */ 28 | export const getValidTime = (date: TimeInput, timezone: string | undefined): number => { 29 | const tz = timezone || "system"; 30 | let dateInMillis; 31 | switch (typeof date) { 32 | case "number": 33 | dateInMillis = date; 34 | break; 35 | case "string": 36 | dateInMillis = DateTime.fromISO(date, { zone: tz }).toMillis(); 37 | break; 38 | case "object": 39 | dateInMillis = DateTime.fromJSDate(date, { zone: tz }).toMillis(); 40 | break; 41 | } 42 | 43 | return dateInMillis; 44 | }; 45 | 46 | export const getValidRangeTime = (date: TimeInput, timezone: string | undefined): number => { 47 | const tz = timezone || "system"; 48 | const validDate = new Date(date); 49 | const dateInMillis = DateTime.fromJSDate(validDate, { zone: tz }).toMillis(); 50 | 51 | return dateInMillis; 52 | }; 53 | 54 | export const isValidRangeTime = (date: TimeInput, name: string): boolean => { 55 | const validDate = new Date(date); 56 | const isValidDateTime = DateTime.fromJSDate(validDate).isValid; 57 | if (isValidDateTime) { 58 | return true; 59 | } 60 | logError(name, "Invalid Date"); 61 | return false; 62 | }; 63 | 64 | /** 65 | * Converts a TimeRange to a luxon Interval 66 | * @param range TimeRange to convert 67 | */ 68 | export const getIntervalFromInternalTimeRange = ( 69 | { start, end }: InternalTimeRange, 70 | resolution: ResolutionData, 71 | timezone: string | undefined 72 | ): Interval => { 73 | const tz = timezone || "system"; 74 | const startDateTime = DateTime.fromMillis(start, { zone: tz }).startOf( 75 | resolution.unitAbove !== "month" ? resolution.unitAbove : resolution.unit 76 | ); 77 | const endDateTime = DateTime.fromMillis(end, { zone: tz }).endOf( 78 | resolution.unitAbove !== "month" ? resolution.unitAbove : resolution.unit 79 | ); 80 | return Interval.fromDateTimes(startDateTime, endDateTime); 81 | }; 82 | 83 | export const getXCoordinateFromTime = ( 84 | sizePx: number, 85 | resolution: ResolutionData, 86 | columnWidth: number, 87 | interval: Interval 88 | ): number => { 89 | const timeOffset = (sizePx * resolution.sizeInUnits) / columnWidth; 90 | const start = interval.start!.plus({ [resolution.unit]: timeOffset }).toMillis(); 91 | return start; 92 | }; 93 | -------------------------------------------------------------------------------- /src/utils/timeBlockArray.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Interval } from "luxon"; 2 | 3 | import { daysInMonth, getMonth, getStartMonthsDay, getYear, Scale } from "./time-resolution"; 4 | 5 | interface VisibleHourInfoProps { 6 | backHour?: boolean; 7 | nextHour?: boolean; 8 | } 9 | 10 | interface DayDetailProps { 11 | thisMonth?: number; 12 | untilNow?: number; 13 | } 14 | 15 | export const getTimeBlocksTzInfo = (timeBlock: Interval[], initialTz?: string) => { 16 | const dayInfoArray: VisibleHourInfoProps[] = []; 17 | 18 | timeBlock.forEach((column) => { 19 | const tzStart = column.start!.toISO()?.slice(-6); 20 | 21 | if (initialTz !== tzStart) { 22 | if (Number(initialTz?.slice(1, 3)) - Number(tzStart!.slice(1, 3)) > 0) { 23 | dayInfoArray.push({ 24 | backHour: true, 25 | nextHour: false, 26 | }); 27 | return; 28 | } 29 | 30 | if (Number(initialTz?.slice(1, 3)) - Number(tzStart!.slice(1, 3)) < 0) { 31 | dayInfoArray.push({ 32 | backHour: false, 33 | nextHour: true, 34 | }); 35 | return; 36 | } 37 | } 38 | 39 | dayInfoArray.push({ 40 | backHour: false, 41 | nextHour: false, 42 | }); 43 | 44 | return; 45 | }); 46 | 47 | return dayInfoArray; 48 | }; 49 | 50 | export const getDaysNumberOfMonths = (unitAbove: Scale, aboveTimeBlocks: Interval[], interval: Interval) => { 51 | if (unitAbove === "month") { 52 | const dayInfo: DayDetailProps[] = []; 53 | 54 | aboveTimeBlocks.forEach((column, index) => { 55 | const month = getMonth(column); 56 | const year = getYear(column); 57 | const currentMonthDays = daysInMonth(Number(month), Number(year)); 58 | 59 | if (index === 0) { 60 | const startDay = getStartMonthsDay(interval.start!); 61 | const daysToMonthEnd = currentMonthDays - Number(startDay) + 1; 62 | dayInfo.push({ 63 | thisMonth: daysToMonthEnd, 64 | untilNow: daysToMonthEnd, 65 | }); 66 | 67 | return; 68 | } 69 | 70 | const n = dayInfo[index - 1].untilNow! + currentMonthDays; 71 | dayInfo.push({ 72 | thisMonth: currentMonthDays, 73 | untilNow: n, 74 | }); 75 | }); 76 | 77 | return dayInfo; 78 | } 79 | return []; 80 | }; 81 | 82 | export const getAboveTimeBlocksVisible = ( 83 | visibleTimeBlocks: Interval[], 84 | aboveTimeBlocks: Interval[], 85 | startUnitAbove: DateTime | null, 86 | endUnitAbove: DateTime | null, 87 | arrayIndex: number[] 88 | ) => { 89 | if (visibleTimeBlocks.length !== 0) { 90 | const blocksArray: Interval[] = []; 91 | aboveTimeBlocks.forEach((i, index) => { 92 | const startMillis = i.start!.toMillis(); 93 | const endMillis = i.end!.toMillis(); 94 | if (endMillis > startUnitAbove!.toMillis() && endMillis <= endUnitAbove!.toMillis()) { 95 | arrayIndex.push(index); 96 | blocksArray.push(i); 97 | return; 98 | } 99 | 100 | if (startMillis >= startUnitAbove!.toMillis() && startMillis < endUnitAbove!.toMillis()) { 101 | arrayIndex.push(index); 102 | blocksArray.push(i); 103 | return; 104 | } 105 | }); 106 | 107 | return blocksArray; 108 | } 109 | return []; 110 | }; 111 | -------------------------------------------------------------------------------- /src/utils/timeline.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "../resources/utils/resources"; 2 | import { TaskData } from "../tasks/utils/tasks"; 3 | 4 | import { TimeRange } from "./time"; 5 | import { Resolution } from "./time-resolution"; 6 | 7 | export type TimelineInput = { 8 | /** 9 | * Custom column width (defaults to 60px), minimum custom width allowed 20px 10 | */ 11 | columnWidth?: number; 12 | /** 13 | * Enables tasks label display 14 | */ 15 | displayTasksLabel?: boolean; 16 | /** 17 | * Drag and drop resolution (if not passed, defaults to resolution) 18 | */ 19 | dragResolution?: Resolution; 20 | /** 21 | * Flag to hide resource column (defaults to false) 22 | */ 23 | hideResources?: boolean; 24 | /** 25 | * Resolution to display data in konva-timeline (defaults to 1min) 26 | */ 27 | resolution?: Resolution; 28 | /** 29 | * Custom row height (defaults to 50px) 30 | */ 31 | rowHeight?: number; 32 | /** 33 | * List of tasks to be displayed (defaults to []) 34 | */ 35 | tasks?: TaskData[]; 36 | /** 37 | * Time range to be displayed 38 | */ 39 | range: TimeRange; 40 | /** 41 | * List of resources to be displayed 42 | */ 43 | resources: Resource[]; 44 | }; 45 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from "luxon"; 2 | 3 | import { logDebug } from "./logger"; 4 | 5 | export const executeWithPerfomanceCheck = (tag: string, item: string, fn: () => T): T => { 6 | if (window.__MELFORE_KONVA_TIMELINE_DEBUG__) { 7 | logDebug(tag, `Running ${item}`); 8 | const start = DateTime.now().toMillis(); 9 | const fnResult = fn(); 10 | const end = DateTime.now().toMillis(); 11 | logDebug(tag, `${item} calculation took ${end - start} ms`); 12 | return fnResult; 13 | } 14 | const result = fn(); 15 | return result; 16 | }; 17 | --------------------------------------------------------------------------------