├── .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 |
--------------------------------------------------------------------------------