├── src ├── common │ └── _index.scss ├── components │ ├── ColumnAdder │ │ ├── _ColumnAdder.scss │ │ ├── _index.scss │ │ ├── index.ts │ │ └── ColumnAdder.tsx │ ├── Card │ │ ├── _index.scss │ │ ├── index.ts │ │ ├── _Card.scss │ │ └── Card.tsx │ ├── Column │ │ ├── _index.scss │ │ ├── index.ts │ │ ├── _Column.scss │ │ └── Column.tsx │ ├── DefaultCard │ │ ├── _index.scss │ │ ├── index.ts │ │ ├── DefaultCard.tsx │ │ └── _DefaultCard.scss │ ├── GenericItem │ │ ├── _index.scss │ │ ├── index.ts │ │ ├── _GenericItem.scss │ │ └── GenericItem.tsx │ ├── CardSkeleton │ │ ├── _index.scss │ │ ├── index.ts │ │ ├── CardSkeleton.tsx │ │ └── _CardSkeleton.scss │ ├── ColumnContent │ │ ├── _index.scss │ │ ├── index.ts │ │ ├── _ColumnContent.scss │ │ └── ColumnContent.tsx │ ├── ColumnHeader │ │ ├── _index.scss │ │ ├── index.ts │ │ ├── _ColumnHeader.scss │ │ └── ColumnHeader.tsx │ ├── index.ts │ ├── _Kanban.scss │ ├── _index.scss │ ├── Kanban.tsx │ └── types.ts ├── global │ ├── assets │ │ └── styles │ │ │ ├── abstracts │ │ │ ├── _padding.scss │ │ │ ├── _functions.scss │ │ │ ├── _index.scss │ │ │ ├── _variables.scss │ │ │ ├── _breakpoints.scss │ │ │ ├── _mixins.scss │ │ │ └── _colors.scss │ │ │ └── base │ │ │ └── _index.scss │ ├── _index.scss │ ├── theme-default.scss │ └── dnd │ │ ├── useCardDnd.tsx │ │ └── useColumnDnd.tsx ├── vite-env.d.ts ├── typelib.d.ts ├── utils │ ├── getPrefix.ts │ ├── getSharedProps.ts │ ├── react-dom.d.ts │ ├── virtua.d.ts │ ├── infinite-scroll.ts │ ├── mergeRefs.ts │ ├── columnsUtils.ts │ ├── scroll.ts │ └── atlaskit.d.ts ├── index.ts ├── context │ └── KanbanContext.tsx └── main.tsx ├── rkk-demo ├── src │ ├── vite-env.d.ts │ ├── global │ │ ├── assets │ │ │ └── styles │ │ │ │ ├── base │ │ │ │ ├── _index.scss │ │ │ │ ├── _typography.scss │ │ │ │ └── _reset.scss │ │ │ │ └── abstracts │ │ │ │ ├── _index.scss │ │ │ │ ├── _functions.scss │ │ │ │ ├── _colors.scss │ │ │ │ ├── _breakpoints.scss │ │ │ │ └── _variables.scss │ │ └── _index.scss │ ├── components │ │ ├── LanguageSwitcher │ │ │ ├── index.ts │ │ │ ├── LanguageSwitcher.tsx │ │ │ └── _LanguageSwitcher.scss │ │ ├── Header │ │ │ ├── index.ts │ │ │ ├── Header.tsx │ │ │ └── _Header.scss │ │ ├── Layout │ │ │ ├── index.ts │ │ │ ├── _Layout.scss │ │ │ └── Layout.tsx │ │ ├── Sidebar │ │ │ ├── index.ts │ │ │ ├── Sidebar.tsx │ │ │ └── _Sidebar.scss │ │ ├── Navigation │ │ │ ├── index.ts │ │ │ ├── Navigation.tsx │ │ │ └── _Navigation.scss │ │ ├── _index.scss │ │ └── index.ts │ ├── pages │ │ ├── Overview │ │ │ ├── index.ts │ │ │ └── Overview.tsx │ │ ├── JiraExample │ │ │ ├── index.ts │ │ │ └── JiraExample.tsx │ │ ├── TrelloExample │ │ │ ├── index.ts │ │ │ └── _index.scss │ │ ├── ClickUpExample │ │ │ ├── index.ts │ │ │ ├── _index.scss │ │ │ └── ClickUpExample.tsx │ │ └── index.ts │ ├── main.scss │ ├── main.tsx │ ├── App.tsx │ ├── i18n │ │ ├── index.ts │ │ └── locales │ │ │ ├── ar.json │ │ │ ├── en.json │ │ │ └── fr.json │ ├── assets │ │ └── react.svg │ └── utils │ │ └── kanbanUtils.ts ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── eslint.config.js ├── tsconfig.node.json ├── tsconfig.app.json ├── index.html ├── package.json ├── public │ └── vite.svg └── README.md ├── public ├── 2f91197ad4ce4a078f723019694803ae.jpeg └── vite.svg ├── tsconfig.node.json ├── .gitignore ├── LICENSE ├── index.html ├── tsconfig.json ├── scripts ├── create-component.sh └── build-docker.sh ├── vite.config.ts └── package.json /src/common/_index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ColumnAdder/_ColumnAdder.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Card/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./Card"; 2 | -------------------------------------------------------------------------------- /src/components/Column/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./Column"; 2 | -------------------------------------------------------------------------------- /rkk-demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Card"; 2 | -------------------------------------------------------------------------------- /src/components/ColumnAdder/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./ColumnAdder"; 2 | -------------------------------------------------------------------------------- /src/components/DefaultCard/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./DefaultCard"; 2 | -------------------------------------------------------------------------------- /src/components/GenericItem/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./GenericItem"; 2 | -------------------------------------------------------------------------------- /src/components/CardSkeleton/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./CardSkeleton"; 2 | -------------------------------------------------------------------------------- /src/components/ColumnContent/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./ColumnContent"; 2 | -------------------------------------------------------------------------------- /src/components/ColumnHeader/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./ColumnHeader"; 2 | -------------------------------------------------------------------------------- /src/global/assets/styles/abstracts/_padding.scss: -------------------------------------------------------------------------------- 1 | $padding-level: 30px; 2 | -------------------------------------------------------------------------------- /src/components/Column/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Column } from "./Column"; 2 | -------------------------------------------------------------------------------- /src/components/ColumnAdder/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ColumnAdder"; 2 | -------------------------------------------------------------------------------- /src/components/ColumnHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ColumnHeader"; 2 | -------------------------------------------------------------------------------- /src/components/DefaultCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DefaultCard"; 2 | -------------------------------------------------------------------------------- /src/components/GenericItem/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./GenericItem"; 2 | -------------------------------------------------------------------------------- /src/components/ColumnContent/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ColumnContent"; 2 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/base/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./reset"; 2 | @forward "./typography"; 3 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export { default as Kanban } from "./Kanban"; 3 | -------------------------------------------------------------------------------- /rkk-demo/src/components/LanguageSwitcher/index.ts: -------------------------------------------------------------------------------- 1 | export { LanguageSwitcher } from "./LanguageSwitcher"; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/global/assets/styles/abstracts/_functions.scss: -------------------------------------------------------------------------------- 1 | @function rem($pxValue) { 2 | @return #{calc($pxValue / 16)}rem; 3 | } 4 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from "./Header"; 2 | export { Header as default } from "./Header"; 3 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Layout } from "./Layout"; 2 | export { Layout as default } from "./Layout"; 3 | -------------------------------------------------------------------------------- /rkk-demo/src/global/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./assets/styles/abstracts"; 2 | @forward "./assets/styles/base"; 3 | @forward "./theme-default"; 4 | -------------------------------------------------------------------------------- /src/global/_index.scss: -------------------------------------------------------------------------------- 1 | @use "./assets/styles/base" as *; 2 | @use "./assets/styles/abstracts" as *; 3 | @use "./theme-default.scss" as *; 4 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Sidebar } from "./Sidebar"; 2 | export { Sidebar as default } from "./Sidebar"; 3 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/Overview/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Overview } from "./Overview"; 2 | export { Overview as default } from "./Overview"; 3 | -------------------------------------------------------------------------------- /src/components/CardSkeleton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./CardSkeleton"; 2 | export type { SkeletonAnimationType } from "./CardSkeleton"; 3 | -------------------------------------------------------------------------------- /public/2f91197ad4ce4a078f723019694803ae.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braiekhazem/react-kanban-kit/HEAD/public/2f91197ad4ce4a078f723019694803ae.jpeg -------------------------------------------------------------------------------- /rkk-demo/src/components/Navigation/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Navigation } from "./Navigation"; 2 | export { Navigation as default } from "./Navigation"; 3 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/JiraExample/index.ts: -------------------------------------------------------------------------------- 1 | export { default as JiraExample } from "./JiraExample"; 2 | export { JiraExample as default } from "./JiraExample"; 3 | -------------------------------------------------------------------------------- /src/components/GenericItem/_GenericItem.scss: -------------------------------------------------------------------------------- 1 | @use "./../../global/assets/styles/abstracts/variables" as *; 2 | 3 | // .#{$prefix}-generic-item-wrapper { 4 | // } 5 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Layout/_Layout.scss: -------------------------------------------------------------------------------- 1 | // Layout styles are handled by the global theme file 2 | // This file is kept for consistency with the component structure 3 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/TrelloExample/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TrelloExample } from "./TrelloExample"; 2 | export { TrelloExample as default } from "./TrelloExample"; 3 | -------------------------------------------------------------------------------- /rkk-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/ClickUpExample/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ClickUpExample } from "./ClickUpExample"; 2 | export { ClickUpExample as default } from "./ClickUpExample"; 3 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/abstracts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "variables"; 2 | @forward "colors"; 3 | @forward "functions"; 4 | @forward "breakpoints"; 5 | @forward "mixins"; 6 | -------------------------------------------------------------------------------- /src/global/assets/styles/abstracts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./colors"; 2 | @forward "./breakpoints"; 3 | @forward "./functions"; 4 | @forward "./mixins"; 5 | @forward "./variables"; 6 | -------------------------------------------------------------------------------- /rkk-demo/src/main.scss: -------------------------------------------------------------------------------- 1 | // Import global styles 2 | @use "./global"; 3 | 4 | // Import component styles 5 | @use "./components"; 6 | 7 | // Import page styles 8 | @use "./pages/pages"; 9 | -------------------------------------------------------------------------------- /src/typelib.d.ts: -------------------------------------------------------------------------------- 1 | declare module "mocks"; 2 | 3 | // Declare other missing modules here 4 | declare module "react-dom"; 5 | declare module "react-dom/client"; 6 | declare module "virtua"; 7 | -------------------------------------------------------------------------------- /src/global/assets/styles/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | $prefix: "rkk"; 2 | 3 | $breakpoints: ( 4 | "small": 320px, 5 | "medium": 768px, 6 | "large": 1024px, 7 | "xlarge": 1200px, 8 | ) !default; 9 | -------------------------------------------------------------------------------- /src/components/_Kanban.scss: -------------------------------------------------------------------------------- 1 | @use "./../global/assets/styles/abstracts/variables" as *; 2 | 3 | .#{$prefix}-board { 4 | display: flex; 5 | overflow: auto; 6 | height: 100%; 7 | gap: 8px; 8 | } 9 | -------------------------------------------------------------------------------- /rkk-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /rkk-demo/src/components/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./Layout/Layout"; 2 | @forward "./Header/Header"; 3 | @forward "./Sidebar/Sidebar"; 4 | @forward "./Navigation/Navigation"; 5 | @forward "./LanguageSwitcher/LanguageSwitcher"; 6 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { Overview } from "./Overview"; 2 | export { TrelloExample } from "./TrelloExample"; 3 | export { ClickUpExample } from "./ClickUpExample"; 4 | export { JiraExample } from "./JiraExample"; 5 | -------------------------------------------------------------------------------- /rkk-demo/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Layout } from "./Layout"; 2 | export { Header } from "./Header"; 3 | export { Sidebar } from "./Sidebar"; 4 | export { Navigation } from "./Navigation"; 5 | export { LanguageSwitcher } from "./LanguageSwitcher"; 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/components/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./Kanban"; 2 | @forward "./Column"; 3 | @forward "./ColumnHeader"; 4 | @forward "./ColumnContent"; 5 | @forward "./GenericItem"; 6 | @forward "./CardSkeleton"; 7 | @forward "./Card"; 8 | @forward "./DefaultCard"; 9 | @forward "./ColumnAdder"; 10 | -------------------------------------------------------------------------------- /src/utils/getPrefix.ts: -------------------------------------------------------------------------------- 1 | export const prefix = "rkk"; 2 | 3 | export const withPrefix = (string: string | string[], separator = " ") => { 4 | if (Array.isArray(string)) 5 | return string.map((item) => `${prefix}-${item}`).join(separator); 6 | return `${prefix}-${string}`; 7 | }; 8 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/abstracts/_functions.scss: -------------------------------------------------------------------------------- 1 | // Utility functions 2 | @function with-demo-prefix($name) { 3 | @return "#{$demo-prefix}-#{$name}"; 4 | } 5 | 6 | // Color functions 7 | @function alpha($color, $opacity) { 8 | @return rgba($color, $opacity); 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "./components/_index.scss"; 2 | import { dropHandler, dropColumnHandler } from "./global/dnd/dropManager"; 3 | 4 | export type { BoardProps, BoardItem, BoardData } from "./components/types"; 5 | export { default as Kanban } from "./components/Kanban"; 6 | export { dropHandler, dropColumnHandler }; 7 | -------------------------------------------------------------------------------- /rkk-demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./i18n"; // Initialize i18n 4 | import "./main.scss"; 5 | import App from "./App.tsx"; 6 | 7 | createRoot(document.getElementById("root")!).render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/Card/_Card.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts/variables" as *; 2 | 3 | .#{$prefix}-card-shadow-container { 4 | position: relative; 5 | } 6 | 7 | .#{$prefix}-card-shadow { 8 | border-radius: 8px; 9 | background-color: #ffffffb7; 10 | border: 1px solid var(--Schemes-Outline-Variant, #e8ecf5); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/DefaultCard/DefaultCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CardRenderProps } from "../types"; 3 | import { withPrefix } from "@/utils/getPrefix"; 4 | 5 | const DefaultCard = (props: CardRenderProps) => { 6 | return
{props?.data?.title}
; 7 | }; 8 | 9 | export default DefaultCard; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /rkk-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/components/DefaultCard/_DefaultCard.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts/variables" as *; 2 | 3 | .#{$prefix}-default-card { 4 | background-color: #fff; 5 | padding: 10px; 6 | border-radius: 8px; 7 | border: 1px solid #ccc; 8 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); 9 | cursor: pointer; 10 | -webkit-border-radius: 8px; 11 | -moz-border-radius: 8px; 12 | -ms-border-radius: 8px; 13 | -o-border-radius: 8px; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/getSharedProps.ts: -------------------------------------------------------------------------------- 1 | import { BoardProps } from "@/components"; 2 | 3 | export const getSharedProps = (props: BoardProps) => { 4 | const { 5 | viewOnly = false, 6 | virtualization = true, 7 | cardsGap = 8, 8 | allowColumnAdder = true, 9 | allowListFooter, 10 | } = props; 11 | return { 12 | viewOnly, 13 | virtualization, 14 | cardsGap, 15 | allowColumnAdder, 16 | allowListFooter, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/react-dom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-dom" { 2 | export function createPortal( 3 | children: React.ReactNode, 4 | container: Element | DocumentFragment 5 | ): React.ReactPortal; 6 | } 7 | 8 | declare module "react-dom/client" { 9 | import { Root } from "react-dom/client"; 10 | export interface Root { 11 | render(children: React.ReactNode): void; 12 | unmount(): void; 13 | } 14 | export function createRoot(container: Element | DocumentFragment): Root; 15 | export default { 16 | createRoot, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Hazem braiek 2 | 3 | Licensed under the Apache License, Version 2.0. 4 | You may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /src/components/ColumnAdder/ColumnAdder.tsx: -------------------------------------------------------------------------------- 1 | import { useKanbanContext } from "@/context/KanbanContext"; 2 | import { withPrefix } from "@/utils/getPrefix"; 3 | import React from "react"; 4 | 5 | interface Props { 6 | renderColumnAdder: () => React.ReactNode; 7 | } 8 | 9 | const ColumnAdder = ({ renderColumnAdder }: Props) => { 10 | const { allowColumnAdder = true } = useKanbanContext(); 11 | if (!allowColumnAdder) return null; 12 | 13 | return ( 14 |
{renderColumnAdder?.()}
15 | ); 16 | }; 17 | 18 | export default ColumnAdder; 19 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | import { Header } from "../Header"; 4 | import { Sidebar } from "../Sidebar"; 5 | 6 | interface LayoutProps { 7 | children?: React.ReactNode; 8 | } 9 | 10 | export const Layout: React.FC = ({ children }) => { 11 | return ( 12 |
13 |
14 |
15 | 16 |
{children || }
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Layout; 23 | -------------------------------------------------------------------------------- /src/components/ColumnHeader/_ColumnHeader.scss: -------------------------------------------------------------------------------- 1 | @use "./../../global/assets/styles/abstracts/variables" as *; 2 | 3 | .#{$prefix}-column-header { 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | border-radius: 10px; 8 | height: 24px; 9 | margin-bottom: 10px; 10 | background-color: #f0f0f0; 11 | 12 | &-left { 13 | font-size: 18px; 14 | font-weight: 600; 15 | color: #000; 16 | } 17 | 18 | &-right { 19 | font-size: 15px; 20 | background-color: #37352f14; 21 | color: #37352f; 22 | padding: 5px 10px; 23 | border-radius: 5px; 24 | font-weight: 500; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/virtua.d.ts: -------------------------------------------------------------------------------- 1 | declare module "virtua" { 2 | import { ReactNode, ComponentType } from "react"; 3 | 4 | export interface VListProps { 5 | items: any[]; 6 | overscan?: number; 7 | onScroll?: (offset: any) => void; 8 | height?: number | string; 9 | itemHeight?: number | ((index: number) => number); 10 | children: (index: any) => ReactNode; 11 | tabIndex?: number; 12 | className?: string; 13 | style?: React.CSSProperties; 14 | scrollerRef?: React.RefObject; 15 | customScrollbars?: boolean; 16 | scrollToItemOpts?: any; 17 | } 18 | 19 | export const VList: ComponentType; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/infinite-scroll.ts: -------------------------------------------------------------------------------- 1 | import { withPrefix } from "./getPrefix"; 2 | 3 | export const checkIfSkeletonIsVisible = ({ columnId, limit = 20 }): boolean => { 4 | const skeletons = document.querySelectorAll( 5 | `.${withPrefix("generic-item-skeleton")}[data-rkk-column="${columnId}"]` 6 | ); 7 | 8 | if (!skeletons.length) return false; 9 | 10 | const skeletonsToCheck = Array.from(skeletons).slice(0, limit); 11 | 12 | const isVisible = skeletonsToCheck.some((skeleton) => { 13 | const { top, bottom } = skeleton.getBoundingClientRect(); 14 | return top <= window.innerHeight && bottom >= 0; 15 | }); 16 | 17 | return isVisible; 18 | }; 19 | -------------------------------------------------------------------------------- /rkk-demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import { Layout } from "./components"; 3 | import { Overview, TrelloExample, ClickUpExample, JiraExample } from "./pages"; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 | 10 | } /> 11 | } /> 12 | } /> 13 | } /> 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/context/KanbanContext.tsx: -------------------------------------------------------------------------------- 1 | import { BoardProps } from "@/components"; 2 | import { createContext, useContext } from "react"; 3 | 4 | const KanbanContext = createContext | undefined>(undefined); 5 | 6 | export const useKanbanContext = () => { 7 | const context = useContext(KanbanContext); 8 | if (!context) throw new Error("KanbanContext must be used within a provider"); 9 | return context; 10 | }; 11 | 12 | export const KanbanProvider = ({ 13 | children, 14 | ...props 15 | }: Partial & { children: React.ReactNode }) => { 16 | return ( 17 | {children} 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/mergeRefs.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type CallbackRef = (ref: T | null) => void; 4 | type Ref = React.MutableRefObject | CallbackRef; 5 | 6 | const toFnRef = (ref?: Ref | null) => 7 | !ref || typeof ref === "function" 8 | ? ref 9 | : (value: T | null) => { 10 | ref.current = value; 11 | }; 12 | 13 | export default function mergeRefs( 14 | refA?: Ref | null, 15 | refB?: Ref | null 16 | ): React.RefCallback { 17 | const a = toFnRef(refA); 18 | const b = toFnRef(refB); 19 | return (value: T | null) => { 20 | if (typeof a === "function") a(value); 21 | if (typeof b === "function") b(value); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /rkk-demo/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /rkk-demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /src/global/assets/styles/base/_index.scss: -------------------------------------------------------------------------------- 1 | @use "../abstracts" as *; 2 | 3 | @import url("https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700,wght@0,100..900;1,100..900&display=swap"); 4 | 5 | * { 6 | font-family: "Raleway", sans-serif; 7 | margin: 0; 8 | padding: 0; 9 | box-sizing: border-box; 10 | outline: none; 11 | border: none; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | } 17 | 18 | html { 19 | scroll-behavior: smooth; 20 | scroll-padding-top: 5rem; 21 | text-size-adjust: none; 22 | } 23 | 24 | a, 25 | button { 26 | cursor: pointer; 27 | } 28 | 29 | a { 30 | @include transition(opacity, 0.2s, ease-in-out); 31 | &:active { 32 | opacity: 0.8; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/columnsUtils.ts: -------------------------------------------------------------------------------- 1 | import { BoardData, BoardItem } from "@/components"; 2 | 3 | export const getColumnsFromDataSource = (dataSource: BoardData) => { 4 | if (!dataSource?.root) return []; 5 | return dataSource.root.children?.map((child) => dataSource[child]) || []; 6 | }; 7 | 8 | export const getColumnChildren = (column: BoardItem, dataSource: BoardData) => { 9 | if (!column) return []; 10 | return column.children?.map((child) => dataSource[child]) || []; 11 | }; 12 | 13 | export const getHeaderHeight = (header: HTMLDivElement): number => { 14 | if (!header) return 0; 15 | const style = window.getComputedStyle(header); 16 | const marginTop = parseFloat(style.marginTop) || 0; 17 | const marginBottom = parseFloat(style.marginBottom) || 0; 18 | return header.offsetHeight + marginTop + marginBottom; 19 | }; 20 | -------------------------------------------------------------------------------- /rkk-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ColumnContent/_ColumnContent.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts/variables" as *; 2 | 3 | .#{$prefix}-column-content { 4 | flex: 1; 5 | position: relative; 6 | height: 0; // This forces the flex item to shrink and become scrollable 7 | min-height: 0; // Ensure flex child can shrink below content size 8 | 9 | &-list { 10 | height: 100%; 11 | overflow-y: auto; // Enable vertical scrolling 12 | overflow-x: hidden; 13 | contain: none !important; 14 | scrollbar-width: thin; /* For Firefox */ 15 | scrollbar-color: rgba(0, 0, 0, 0.3) transparent; /* For Firefox - thumb and track color */ 16 | transition: scrollbar-color 0.3s ease; 17 | 18 | // Show scrollbar on hover for better UX 19 | &:hover { 20 | scrollbar-color: rgba(0, 0, 0, 0.3) transparent; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rkk-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | React Kanban Kit Demo 12 | 13 | 14 |
15 | 16 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /rkk-demo/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import LanguageDetector from "i18next-browser-languagedetector"; 4 | 5 | // Import translation files 6 | import en from "./locales/en.json"; 7 | import fr from "./locales/fr.json"; 8 | import ar from "./locales/ar.json"; 9 | 10 | const resources = { 11 | en: { translation: en }, 12 | fr: { translation: fr }, 13 | ar: { translation: ar }, 14 | }; 15 | 16 | i18n 17 | .use(LanguageDetector) 18 | .use(initReactI18next) 19 | .init({ 20 | resources, 21 | fallbackLng: "en", 22 | debug: process.env.NODE_ENV === "development", 23 | 24 | interpolation: { 25 | escapeValue: false, // React already does escaping 26 | }, 27 | 28 | detection: { 29 | order: ["localStorage", "navigator", "htmlTag"], 30 | caches: ["localStorage"], 31 | }, 32 | }); 33 | 34 | export default i18n; 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | react-kanban-kit 12 | 13 | 14 |
15 | 16 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/Column/_Column.scss: -------------------------------------------------------------------------------- 1 | @use "./../../global/assets/styles/abstracts/variables" as *; 2 | 3 | .#{$prefix}-column-outer { 4 | min-width: 264px; 5 | max-width: 264px; 6 | height: 100%; // Changed from max-height to height for proper cascading 7 | display: flex; 8 | flex-direction: column; 9 | transition: all 0.1s ease; 10 | -webkit-transition: all 0.1s ease; 11 | -moz-transition: all 0.1s ease; 12 | -ms-transition: all 0.1s ease; 13 | -o-transition: all 0.1s ease; 14 | 15 | .#{$prefix}-column { 16 | display: flex; 17 | flex-direction: column; 18 | gap: 10px; 19 | padding: 10px; 20 | border-radius: 10px; 21 | background-color: #f0f0f0; 22 | position: relative; 23 | overflow: hidden; 24 | // height: 100%; // Ensure column takes full height 25 | 26 | &-wrapper { 27 | display: flex; 28 | flex-direction: column; 29 | flex: 1; 30 | gap: 16px; 31 | max-height: 100%; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/scroll.ts: -------------------------------------------------------------------------------- 1 | import { BoardItem, ScrollEvent } from "@/components"; 2 | import { withPrefix } from "./getPrefix"; 3 | 4 | export const handleScroll = ( 5 | e: React.UIEvent | number, 6 | virtualization: boolean, 7 | onScroll: (e: ScrollEvent, column: BoardItem) => void, 8 | column: BoardItem 9 | ) => { 10 | const scrollContainer = document.querySelector( 11 | `.${withPrefix("column-content-list")}` 12 | ); 13 | if (!scrollContainer) return; 14 | 15 | const { scrollHeight, clientHeight } = scrollContainer; 16 | let offset: number; 17 | if (virtualization) { 18 | offset = typeof e === "number" ? e : 0; 19 | } else { 20 | const target = (e as React.UIEvent) 21 | .target as HTMLDivElement; 22 | offset = target.scrollTop; 23 | } 24 | 25 | const syntheticEvent = { 26 | target: { 27 | scrollTop: offset, 28 | scrollHeight, 29 | clientHeight, 30 | }, 31 | }; 32 | 33 | onScroll?.(syntheticEvent, column); 34 | }; 35 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/abstracts/_colors.scss: -------------------------------------------------------------------------------- 1 | // Brand colors for different board examples 2 | :root { 3 | // Trello brand colors 4 | --trello-primary: #0079bf; 5 | --trello-secondary: #026aa7; 6 | --trello-bg: #f4f5f7; 7 | --trello-card: #ffffff; 8 | --trello-list: #ebecf0; 9 | 10 | // ClickUp brand colors 11 | --clickup-primary: #7b68ee; 12 | --clickup-secondary: #6c5ce7; 13 | --clickup-bg: #f8f9fa; 14 | --clickup-card: #ffffff; 15 | --clickup-list: #f1f2f4; 16 | 17 | // Jira brand colors 18 | --jira-primary: #0052cc; 19 | --jira-secondary: #0747a6; 20 | --jira-bg: #f4f5f7; 21 | --jira-card: #ffffff; 22 | --jira-list: #ebecf0; 23 | 24 | // Status colors 25 | --status-todo: var(--gray-400); 26 | --status-progress: var(--primary-500); 27 | --status-review: #f59e0b; 28 | --status-done: #10b981; 29 | --status-blocked: #ef4444; 30 | 31 | // Priority colors 32 | --priority-low: #10b981; 33 | --priority-medium: #f59e0b; 34 | --priority-high: #ef4444; 35 | --priority-critical: #dc2626; 36 | } 37 | -------------------------------------------------------------------------------- /rkk-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rkk-demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "i18next": "^25.3.2", 14 | "i18next-browser-languagedetector": "^8.2.0", 15 | "lucide-react": "^0.525.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-i18next": "^15.6.1", 19 | "react-kanban-kit": "^0.0.2-beta.6", 20 | "react-router-dom": "^6.28.0", 21 | "sass": "^1.89.2" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.30.1", 25 | "@types/react": "^18.2.0", 26 | "@types/react-dom": "^18.2.0", 27 | "@vitejs/plugin-react": "^4.3.1", 28 | "eslint": "^9.30.1", 29 | "eslint-plugin-react-hooks": "^5.2.0", 30 | "eslint-plugin-react-refresh": "^0.4.20", 31 | "globals": "^16.3.0", 32 | "typescript": "~5.8.3", 33 | "typescript-eslint": "^8.35.1", 34 | "vite": "^5.4.10" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/atlaskit.d.ts: -------------------------------------------------------------------------------- 1 | // declare module "@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-autoscroll" { 2 | // export const autoScroller: any; 3 | // } 4 | 5 | // declare module "@atlaskit/pragmatic-drag-and-drop/element/adapter" { 6 | // export const monitorForElements: any; 7 | // export const draggable: any; 8 | // export const dropTargetForElements: any; 9 | // } 10 | 11 | // declare module "@atlaskit/pragmatic-drag-and-drop/combine" { 12 | // export const combine: any; 13 | // } 14 | 15 | // declare module "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element" { 16 | // export const autoScrollForElements: any; 17 | // } 18 | 19 | // declare module "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge" { 20 | // export const extractClosestEdge: any; 21 | // } 22 | 23 | // declare module "@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge" { 24 | // export const reorderWithEdge: any; 25 | // } 26 | 27 | // declare module "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview" { 28 | // export const setCustomNativeDragPreview: any; 29 | // } 30 | 31 | // declare module "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source" { 32 | // export const preserveOffsetOnSource: any; 33 | // } 34 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Navigation } from "../Navigation"; 4 | 5 | export const Sidebar: React.FC = () => { 6 | const { t } = useTranslation(); 7 | 8 | return ( 9 | 35 | ); 36 | }; 37 | 38 | export default Sidebar; 39 | -------------------------------------------------------------------------------- /src/components/ColumnHeader/ColumnHeader.tsx: -------------------------------------------------------------------------------- 1 | import { withPrefix } from "@/utils/getPrefix"; 2 | import React, { forwardRef, useRef } from "react"; 3 | import { BoardItem } from "../types"; 4 | import classNames from "classnames"; 5 | 6 | interface Props { 7 | renderColumnHeader?: (column: BoardItem) => React.ReactNode; 8 | columnHeaderStyle?: (column: BoardItem) => React.CSSProperties; 9 | columnHeaderClassName?: string; 10 | data: BoardItem; 11 | } 12 | 13 | const ColumnHeader = forwardRef((props, ref) => { 14 | const { 15 | renderColumnHeader, 16 | columnHeaderStyle, 17 | columnHeaderClassName = "", 18 | data, 19 | } = props; 20 | 21 | const headerClassName = classNames( 22 | withPrefix("column-header"), 23 | columnHeaderClassName 24 | ); 25 | 26 | if (renderColumnHeader) 27 | return
{renderColumnHeader(data)}
; 28 | 29 | return ( 30 |
35 |
{data?.title}
36 |
37 | {data?.totalItemsCount || data?.totalChildrenCount || 0} 38 |
39 |
40 | ); 41 | }); 42 | 43 | export default ColumnHeader; 44 | -------------------------------------------------------------------------------- /src/components/CardSkeleton/CardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withPrefix } from "@/utils/getPrefix"; 3 | 4 | export type SkeletonAnimationType = "shimmer" | "pulse" | "wave"; 5 | 6 | interface CardSkeletonProps { 7 | className?: string; 8 | style?: React.CSSProperties; 9 | animationType?: SkeletonAnimationType; 10 | } 11 | 12 | const CardSkeleton: React.FC = ({ 13 | className, 14 | style, 15 | animationType = "shimmer", 16 | }) => { 17 | const skeletonClass = `${withPrefix("skeleton")} ${ 18 | animationType === "pulse" ? withPrefix("skeleton-pulse") : "" 19 | } ${animationType === "wave" ? withPrefix("skeleton-wave") : ""} ${ 20 | className || "" 21 | }`.trim(); 22 | 23 | const renderDefaultSkeleton = () => ( 24 |
25 |
26 | 27 |
28 |
29 |
34 |
35 |
36 | ); 37 | 38 | return ( 39 |
40 | {renderDefaultSkeleton()} 41 |
42 | ); 43 | }; 44 | 45 | export default CardSkeleton; 46 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rkk-demo/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/JiraExample/JiraExample.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const JiraExample: React.FC = () => { 4 | return ( 5 |
6 |
7 |

Jira Style Board

8 |

9 | A Jira-inspired Kanban board with enterprise features and agile 10 | workflow 11 |

12 |
13 | 14 |
15 |
16 |
🎫
17 |

Jira Example Coming Soon

18 |

19 | This page will showcase a Jira-inspired implementation of React 20 | Kanban Kit featuring: 21 |

22 |
    23 |
  • Jira's blue color scheme and professional layout
  • 24 |
  • Issue types (Story, Bug, Task, Epic)
  • 25 |
  • Sprint planning and backlog management
  • 26 |
  • Story points and estimation
  • 27 |
  • Epic relationships and hierarchy
  • 28 |
  • Workflow transitions and statuses
  • 29 |
  • Reporter and assignee management
  • 30 |
31 |
32 | The implementation will be added by the developer. 33 |
34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default JiraExample; 41 | -------------------------------------------------------------------------------- /src/global/assets/styles/abstracts/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // Small devices 2 | @mixin sm { 3 | @media (min-width: #{map-get($breakpoints, 'small')}) { 4 | @content; 5 | } 6 | } 7 | 8 | // Medium devices 9 | @mixin md { 10 | @media (min-width: #{map-get($breakpoints, 'medium')}) { 11 | @content; 12 | } 13 | } 14 | 15 | // Large devices 16 | @mixin lg { 17 | @media (min-width: #{map-get($breakpoints, 'large')}) { 18 | @content; 19 | } 20 | } 21 | 22 | // Extra large devices 23 | @mixin xl { 24 | @media (min-width: #{map-get($breakpoints, 'xlarge')}) { 25 | @content; 26 | } 27 | } 28 | 29 | // ++++++++++ MAX WIDTH ++++++++++ 30 | 31 | // Small devices 32 | @mixin max-sm { 33 | @media (max-width: #{map-get($breakpoints, 'small')}) { 34 | @content; 35 | } 36 | } 37 | 38 | // Medium devices 39 | @mixin max-md { 40 | @media (max-width: #{map-get($breakpoints, 'medium')}) { 41 | @content; 42 | } 43 | } 44 | 45 | // Large devices 46 | @mixin max-lg { 47 | @media (max-width: #{map-get($breakpoints, 'large')}) { 48 | @content; 49 | } 50 | } 51 | 52 | // Extra large devices 53 | @mixin max-xl { 54 | @media (max-width: #{map-get($breakpoints, 'xlarge')}) { 55 | @content; 56 | } 57 | } 58 | 59 | // Custom devices (min-width) 60 | @mixin rwd($screen) { 61 | @media (min-width: #{$screen}px) { 62 | @content; 63 | } 64 | } 65 | 66 | // Custom devices (max-width) 67 | @mixin max-rwd($screen) { 68 | @media (max-width: #{$screen}px) { 69 | @content; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | // Strict mode 18 | "strict": false, 19 | "alwaysStrict": false, 20 | "noImplicitAny": false, 21 | "noImplicitThis": false, 22 | "strictNullChecks": false, 23 | "strictFunctionTypes": false, 24 | "useUnknownInCatchVariables": false, 25 | 26 | // No unused code 27 | "noUnusedLocals": false, 28 | "noUnusedParameters": false, 29 | "allowUnusedLabels": true, 30 | "allowUnreachableCode": true, 31 | 32 | // No implicit code 33 | "noImplicitOverride": false, 34 | "noImplicitReturns": false, 35 | 36 | // Others 37 | "noUncheckedIndexedAccess": false, 38 | "noPropertyAccessFromIndexSignature": false, 39 | "noFallthroughCasesInSwitch": false, 40 | "exactOptionalPropertyTypes": false, 41 | "forceConsistentCasingInFileNames": true, 42 | "declaration": true, 43 | 44 | "baseUrl": ".", 45 | "paths": { 46 | "@/*": ["./src/*"], 47 | "@src/*": ["./src/*"], 48 | "@components/*": ["./src/components/*"], 49 | "@utils/*": ["./src/utils/*"] 50 | } 51 | }, 52 | "ts-node": { 53 | "esm": true 54 | }, 55 | "include": ["src", "./src/index.ts", "docs/app", "src/**/*.d.ts"], 56 | "references": [{ "path": "./tsconfig.node.json" }] 57 | } 58 | -------------------------------------------------------------------------------- /rkk-demo/src/i18n/locales/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "title": "React Kanban Kit", 4 | "subtitle": "معرض التوضيح", 5 | "github": "GitHub", 6 | "npm": "NPM", 7 | "settings": "الإعدادات" 8 | }, 9 | "navigation": { 10 | "boardExamples": "أمثلة اللوحات", 11 | "documentation": "التوثيق", 12 | "overview": "نظرة عامة", 13 | "overviewDescription": "مثال كانبان أساسي", 14 | "trelloStyle": "نمط تريلو", 15 | "trelloDescription": "تصميم لوحة مستوحى من تريلو", 16 | "clickupStyle": "نمط كليك أب", 17 | "clickupDescription": "تصميم لوحة مستوحى من كليك أب", 18 | "tamStyle": "نمط تام", 19 | "tamDescription": "تصميم لوحة مستوحى من تام" 20 | }, 21 | "language": { 22 | "english": "English", 23 | "french": "Français", 24 | "arabic": "العربية", 25 | "switchLanguage": "تغيير اللغة" 26 | }, 27 | "pages": { 28 | "overview": { 29 | "title": "نظرة عامة على React Kanban Kit", 30 | "description": "مكون لوحة كانبان قوي ومرن لتطبيقات React", 31 | "features": "المميزات", 32 | "gettingStarted": "البدء", 33 | "examples": "أمثلة" 34 | }, 35 | "trello": { 36 | "title": "لوحة نمط تريلو", 37 | "description": "اختبر تخطيط لوحة تريلو المألوف مع وظيفة السحب والإفلات" 38 | }, 39 | "clickup": { 40 | "title": "لوحة نمط كليك أب", 41 | "description": "واجهة إدارة المهام الحديثة المستوحاة من تصميم كليك أب" 42 | } 43 | }, 44 | "common": { 45 | "loading": "جاري التحميل...", 46 | "error": "خطأ", 47 | "success": "نجح", 48 | "cancel": "إلغاء", 49 | "save": "حفظ", 50 | "edit": "تعديل", 51 | "delete": "حذف", 52 | "add": "إضافة", 53 | "close": "إغلاق", 54 | "unassigned": "غير مخصص" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/create-component.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if component name is provided 4 | if [ -z "$1" ]; then 5 | echo "Error: Component name is missing." 6 | echo "Usage: ./create_component.sh " 7 | exit 1 8 | fi 9 | 10 | component_name="$1" 11 | component_dir="./src/components/$component_name" 12 | 13 | # Check if component directory already exists 14 | if [ -d "$component_dir" ]; then 15 | echo "Error: Component directory already exists." 16 | exit 1 17 | fi 18 | 19 | # Create component directory 20 | mkdir -p "$component_dir" 21 | 22 | # Create files 23 | touch "$component_dir/$component_name.tsx" 24 | touch "$component_dir/index.ts" 25 | touch "$component_dir/@types.ts" 26 | 27 | # Create styles files 28 | touch "$component_dir/_$component_name.scss" 29 | touch "$component_dir/_index.scss" 30 | 31 | # Add basic component template to the TypeScript file 32 | echo "@forward './$component_name' 33 | " > "$component_dir/_index.scss" 34 | 35 | # Add basic component template to the TypeScript file 36 | echo "import React from 'react'; 37 | import { ${component_name}Props } from './@types'; 38 | 39 | const ${component_name}: React.FC<${component_name}Props> = () => { 40 | return ( 41 |
42 | {/* Your component JSX here */} 43 |
44 | ); 45 | }; 46 | 47 | export default ${component_name}; 48 | " > "$component_dir/$component_name.tsx" 49 | 50 | # Create index.ts file 51 | echo "export { default } from './${component_name}';" > "$component_dir/index.ts" 52 | 53 | # Create @types.ts file 54 | echo "export interface ${component_name}Props { 55 | // Define props here 56 | }" > "$component_dir/@types.ts" 57 | 58 | echo "Component '$component_name' created successfully in 'src/components' directory." 59 | -------------------------------------------------------------------------------- /rkk-demo/src/i18n/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "title": "React Kanban Kit", 4 | "subtitle": "Demo Showcase", 5 | "github": "GitHub", 6 | "npm": "NPM", 7 | "settings": "Settings" 8 | }, 9 | "navigation": { 10 | "boardExamples": "Board Examples", 11 | "documentation": "Documentation", 12 | "overview": "Overview", 13 | "overviewDescription": "Basic Kanban example", 14 | "trelloStyle": "Trello Style", 15 | "trelloDescription": "Trello-inspired board design", 16 | "clickupStyle": "ClickUp Style", 17 | "clickupDescription": "ClickUp-inspired board design", 18 | "tamStyle": "Tam Style", 19 | "tamDescription": "Tam-inspired board design" 20 | }, 21 | "language": { 22 | "english": "English", 23 | "french": "Français", 24 | "arabic": "العربية", 25 | "switchLanguage": "Switch Language" 26 | }, 27 | "pages": { 28 | "overview": { 29 | "title": "React Kanban Kit Overview", 30 | "description": "A powerful and flexible Kanban board component for React applications", 31 | "features": "Features", 32 | "gettingStarted": "Getting Started", 33 | "examples": "Examples" 34 | }, 35 | "trello": { 36 | "title": "Trello Style Board", 37 | "description": "Experience the familiar Trello board layout with drag-and-drop functionality" 38 | }, 39 | "clickup": { 40 | "title": "ClickUp Style Board", 41 | "description": "Modern task management interface inspired by ClickUp's design" 42 | } 43 | }, 44 | "common": { 45 | "loading": "Loading...", 46 | "error": "Error", 47 | "success": "Success", 48 | "cancel": "Cancel", 49 | "save": "Save", 50 | "edit": "Edit", 51 | "delete": "Delete", 52 | "add": "Add", 53 | "close": "Close", 54 | "unassigned": "Unassigned" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/global/assets/styles/abstracts/_mixins.scss: -------------------------------------------------------------------------------- 1 | //Cross browser CSS3 mixins 2 | 3 | @mixin box-shadow($left, $top, $radius, $color) { 4 | box-shadow: $left $top $radius $color; 5 | -webkit-box-shadow: $left $top $radius $color; 6 | -moz-box-shadow: $left $top $radius $color; 7 | } 8 | 9 | @mixin transition($property, $duration, $easing: linear) { 10 | transition: $property $duration $easing; 11 | -webkit-transition: $property $duration $easing; 12 | -moz-transition: $property $duration $easing; 13 | -ms-transition: $property $duration $easing; 14 | -o-transition: $property $duration $easing; 15 | } 16 | 17 | @mixin border-radius($radius) { 18 | border-radius: $radius; 19 | -webkit-border-radius: $radius; 20 | -moz-border-radius: $radius; 21 | } 22 | 23 | @mixin border-radii($topleft, $topright, $bottomright, $bottomleft) { 24 | border-top-left-radius: $topleft; 25 | border-top-right-radius: $topright; 26 | border-bottom-right-radius: $bottomright; 27 | border-bottom-left-radius: $bottomleft; 28 | -webkit-border-top-left-radius: $topleft; 29 | -webkit-border-top-right-radius: $topright; 30 | -webkit-border-bottom-right-radius: $bottomright; 31 | -webkit-border-bottom-left-radius: $bottomleft; 32 | -moz-border-radius-topleft: $topleft; 33 | -moz-border-radius-topright: $topright; 34 | -moz-border-radius-bottomright: $bottomright; 35 | -moz-border-radius-bottomleft: $bottomleft; 36 | } 37 | 38 | @mixin gradient($color1, $color2) { 39 | background-color: $color1; 40 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=#{$color1}, endColorstr=#{$color2}); 41 | background-image: -moz-linear-gradient(center top, $color1, $color2); 42 | background-image: -webkit-gradient( 43 | linear, 44 | 0% 0%, 45 | 0% 100%, 46 | from($color1), 47 | to($color2) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /rkk-demo/src/i18n/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "title": "React Kanban Kit", 4 | "subtitle": "Vitrine de Démonstration", 5 | "github": "GitHub", 6 | "npm": "NPM", 7 | "settings": "Paramètres" 8 | }, 9 | "navigation": { 10 | "boardExamples": "Exemples de Tableaux", 11 | "documentation": "Documentation", 12 | "overview": "Aperçu", 13 | "overviewDescription": "Exemple Kanban de base", 14 | "trelloStyle": "Style Trello", 15 | "trelloDescription": "Design de tableau inspiré de Trello", 16 | "clickupStyle": "Style ClickUp", 17 | "clickupDescription": "Design de tableau inspiré de ClickUp", 18 | "tamStyle": "Style Tam", 19 | "tamDescription": "Design de tableau inspiré de Tam" 20 | }, 21 | "language": { 22 | "english": "English", 23 | "french": "Français", 24 | "arabic": "العربية", 25 | "switchLanguage": "Changer de Langue" 26 | }, 27 | "pages": { 28 | "overview": { 29 | "title": "Aperçu de React Kanban Kit", 30 | "description": "Un composant de tableau Kanban puissant et flexible pour les applications React", 31 | "features": "Fonctionnalités", 32 | "gettingStarted": "Commencer", 33 | "examples": "Exemples" 34 | }, 35 | "trello": { 36 | "title": "Tableau Style Trello", 37 | "description": "Découvrez la mise en page familière du tableau Trello avec la fonctionnalité de glisser-déposer" 38 | }, 39 | "clickup": { 40 | "title": "Tableau Style ClickUp", 41 | "description": "Interface moderne de gestion des tâches inspirée du design de ClickUp" 42 | } 43 | }, 44 | "common": { 45 | "loading": "Chargement...", 46 | "error": "Erreur", 47 | "success": "Succès", 48 | "cancel": "Annuler", 49 | "save": "Enregistrer", 50 | "edit": "Modifier", 51 | "delete": "Supprimer", 52 | "add": "Ajouter", 53 | "close": "Fermer", 54 | "unassigned": "Non assigné" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/global/assets/styles/abstracts/_colors.scss: -------------------------------------------------------------------------------- 1 | $light: white; 2 | 3 | $main: #d7e7ef; 4 | 5 | $info: #1cc3eb; 6 | $info-50: #ecfdff; 7 | $info-100: #d0f7fd; 8 | $info-200: #a6edfb; 9 | $info-300: #69def7; 10 | $info-400: #1cc3eb; 11 | $info-500: #08a8d2; 12 | $info-600: #0a86b0; 13 | $info-700: #0f6b8f; 14 | $info-800: #165774; 15 | $info-900: #174962; 16 | $info-950: #092f43; 17 | 18 | $success: #4caf50; 19 | $success-50: #f3faf3; 20 | $success-100: #e3f5e3; 21 | $success-200: #c8eac9; 22 | $success-300: #9dd89f; 23 | $success-400: #6bbd6e; 24 | $success-500: #4caf50; 25 | $success-600: #358438; 26 | $success-700: #2d6830; 27 | $success-800: #275429; 28 | $success-900: #224525; 29 | $success-950: #0e2510; 30 | 31 | $danger: #f04438; 32 | $danger-50: #fff9f9; 33 | $danger-100: #ffe3e1; 34 | $danger-200: #ffccc8; 35 | $danger-300: #ffa8a2; 36 | $danger-400: #fc776d; 37 | $danger-500: #f44336; 38 | $danger-600: #e22d20; 39 | $danger-700: #be2217; 40 | $danger-800: #9d2017; 41 | $danger-900: #82211a; 42 | $danger-950: #470c08; 43 | 44 | $warning: #ffc008; 45 | $warning-50: #fffeea; 46 | $warning-100: #fffbc5; 47 | $warning-200: #fff785; 48 | $warning-300: #ffed46; 49 | $warning-400: #ffde1b; 50 | $warning-500: #ffc008; 51 | $warning-600: #e29200; 52 | $warning-700: #bb6802; 53 | $warning-800: #985008; 54 | $warning-900: #7c410b; 55 | $warning-950: #482100; 56 | 57 | $dark: #131f3c; 58 | $dark-50: #edf8ff; 59 | $dark-100: #def2ff; 60 | $dark-200: #c4e5ff; 61 | $dark-300: #a1d2ff; 62 | $dark-400: #7bb6fe; 63 | $dark-500: #5c96f8; 64 | $dark-600: #3e72ed; 65 | $dark-700: #305dd2; 66 | $dark-800: #2a4fa9; 67 | $dark-900: #2a4785; 68 | $dark-950: #131f3c; 69 | 70 | $secondary: #d6d9e1; 71 | $secondary-50: #f6f7f9; 72 | $secondary-100: #f1f2f5; 73 | $secondary-200: #d6d9e1; 74 | $secondary-300: #b1b9c8; 75 | $secondary-400: #8793a9; 76 | $secondary-500: #68758f; 77 | $secondary-600: #535e76; 78 | $secondary-700: #444c60; 79 | $secondary-800: #3b4151; 80 | $secondary-900: #343946; 81 | $secondary-950: #23262e; 82 | 83 | $grey: #91939a; 84 | 85 | $hover: #d7e7ef; 86 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { useTranslation } from "react-i18next"; 4 | import { Kanban, Layers, GitBranch } from "lucide-react"; 5 | 6 | const navigationItems = [ 7 | { 8 | path: "/", 9 | labelKey: "navigation.overview", 10 | icon: Kanban, 11 | descriptionKey: "navigation.overviewDescription", 12 | }, 13 | { 14 | path: "/trello", 15 | labelKey: "navigation.trelloStyle", 16 | icon: Layers, 17 | descriptionKey: "navigation.trelloDescription", 18 | }, 19 | { 20 | path: "/clickup", 21 | labelKey: "navigation.clickupStyle", 22 | icon: GitBranch, 23 | descriptionKey: "navigation.clickupDescription", 24 | }, 25 | // { 26 | // path: "/tam", 27 | // labelKey: "navigation.tamStyle", 28 | // icon: Kanban, 29 | // descriptionKey: "navigation.tamDescription", 30 | // }, 31 | ]; 32 | 33 | export const Navigation: React.FC = () => { 34 | const { t } = useTranslation(); 35 | 36 | return ( 37 | 65 | ); 66 | }; 67 | 68 | export default Navigation; 69 | -------------------------------------------------------------------------------- /scripts/build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the .env file exists 4 | if [ -f ./.env ]; then 5 | source ./.env 6 | else 7 | echo "Error: .env file not found." 8 | exit 1 9 | fi 10 | 11 | # Define variables 12 | IMAGE_NAME="front-end-guidlines" 13 | CONTAINER_NAME="${IMAGE_NAME}-${VITE_APP_BASE_NODE_ENV}" 14 | PORT_MAPPING="${VITE_APP_PORT}:4000" 15 | 16 | # Function to handle errors 17 | handle_error() { 18 | echo "Error: $1" 19 | echo "Exiting..." 20 | exit 1 21 | } 22 | 23 | # Stop and remove existing Docker container if it exists 24 | if docker ps -a | grep -q $CONTAINER_NAME; then 25 | echo "Stopping and removing existing container: $CONTAINER_NAME" 26 | docker stop $CONTAINER_NAME 27 | docker rm $CONTAINER_NAME 28 | fi 29 | 30 | # Build the Docker image 31 | echo "Building Docker image: $IMAGE_NAME" 32 | docker build -t $IMAGE_NAME . || handle_error "Docker image build failed." 33 | 34 | echo "Docker image built successfully: $IMAGE_NAME" 35 | 36 | # Run the Docker container in detached mode and redirect logs to stdout 37 | echo "Starting Docker container: $CONTAINER_NAME" 38 | docker run -p $PORT_MAPPING -d --name $CONTAINER_NAME $IMAGE_NAME || handle_error "Failed to start container." 39 | 40 | # Display logs from the container 41 | echo "Displaying logs from container: $CONTAINER_NAME" 42 | docker logs -f $CONTAINER_NAME 43 | 44 | # Capture the exit status of the container 45 | exit_status=$(docker inspect -f '{{.State.ExitCode}}' $CONTAINER_NAME) 46 | 47 | # Check if the container exited with an error 48 | if [ $exit_status -ne 0 ]; then 49 | handle_error "Container exited with error: $exit_status" 50 | fi 51 | 52 | # Copy the built files from the container to the host 53 | echo "Copying files from container to host..." 54 | sudo docker cp $CONTAINER_NAME:/app/dist "${VITE_APP_DIST_LOCATION}" || handle_error "Failed to copy files from container to host." 55 | 56 | echo "Docker container completed and files copied: $CONTAINER_NAME" 57 | 58 | # Stop and remove the Docker container 59 | echo "Stopping and removing container: $CONTAINER_NAME" 60 | docker stop $CONTAINER_NAME || true 61 | docker rm $CONTAINER_NAME || true 62 | 63 | # Remove the Docker image 64 | echo "Removing Docker image: $IMAGE_NAME" 65 | docker rmi $IMAGE_NAME 66 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useTranslation } from "react-i18next"; 4 | import { Settings, Github, ExternalLink } from "lucide-react"; 5 | import { LanguageSwitcher } from "../LanguageSwitcher"; 6 | 7 | export const Header: React.FC = () => { 8 | const { t } = useTranslation(); 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 |
RKK
17 |
18 | {t("header.title")} 19 | 20 | {t("header.subtitle")} 21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 60 |
61 |
62 |
63 | ); 64 | }; 65 | 66 | export default Header; 67 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import dts from "vite-plugin-dts"; 4 | import { resolve } from "path"; 5 | import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | react({ 11 | jsxRuntime: "automatic", 12 | babel: { 13 | plugins: [ 14 | ["@babel/plugin-transform-react-jsx", { runtime: "automatic" }], 15 | ], 16 | }, 17 | }), 18 | dts({ 19 | include: ["src"], 20 | exclude: ["src/**/*.test.ts", "src/**/*.test.tsx"], 21 | }), 22 | cssInjectedByJsPlugin(), 23 | ], 24 | base: "./", 25 | resolve: { 26 | alias: { 27 | "@": resolve(__dirname, "./src"), 28 | "@src": resolve(__dirname, "./src"), 29 | "@components": resolve(__dirname, "./src/components"), 30 | "@utils": resolve(__dirname, "./src/utils"), 31 | }, 32 | }, 33 | build: { 34 | lib: { 35 | entry: resolve(__dirname, "src/index.ts"), 36 | name: "ReactKanbanKit", 37 | fileName: (format) => `index.${format === "es" ? "es.js" : "cjs.js"}`, 38 | }, 39 | rollupOptions: { 40 | external: ["react", "react-dom", "react/jsx-runtime"], 41 | output: [ 42 | { 43 | format: "es", 44 | dir: "dist", 45 | entryFileNames: "[name].es.js", 46 | preserveModules: true, 47 | preserveModulesRoot: "src", 48 | globals: { 49 | react: "React", 50 | "react-dom": "ReactDOM", 51 | "react/jsx-runtime": "jsxRuntime", 52 | }, 53 | }, 54 | { 55 | format: "cjs", 56 | dir: "dist", 57 | entryFileNames: "[name].cjs.js", 58 | globals: { 59 | react: "React", 60 | "react-dom": "ReactDOM", 61 | "react/jsx-runtime": "jsxRuntime", 62 | }, 63 | }, 64 | ], 65 | }, 66 | sourcemap: true, 67 | minify: "terser", 68 | commonjsOptions: { 69 | include: [/node_modules/], 70 | transformMixedEsModules: true, 71 | }, 72 | }, 73 | 74 | server: { 75 | port: 3000, 76 | 77 | // to get images from the server 78 | // proxy: { 79 | // '^/users': { 80 | // target: 'http://localhost:8000/', 81 | // }, 82 | // }, 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/base/_typography.scss: -------------------------------------------------------------------------------- 1 | // Typography classes 2 | .text { 3 | &-xs { 4 | font-size: 0.75rem; 5 | line-height: 1rem; 6 | } 7 | 8 | &-sm { 9 | font-size: 0.875rem; 10 | line-height: 1.25rem; 11 | } 12 | 13 | &-base { 14 | font-size: 1rem; 15 | line-height: 1.5rem; 16 | } 17 | 18 | &-lg { 19 | font-size: 1.125rem; 20 | line-height: 1.75rem; 21 | } 22 | 23 | &-xl { 24 | font-size: 1.25rem; 25 | line-height: 1.75rem; 26 | } 27 | 28 | &-2xl { 29 | font-size: 1.5rem; 30 | line-height: 2rem; 31 | } 32 | 33 | &-3xl { 34 | font-size: 1.875rem; 35 | line-height: 2.25rem; 36 | } 37 | 38 | &-4xl { 39 | font-size: 2.25rem; 40 | line-height: 2.5rem; 41 | } 42 | } 43 | 44 | // Font weights 45 | .font { 46 | &-light { 47 | font-weight: 300; 48 | } 49 | 50 | &-normal { 51 | font-weight: 400; 52 | } 53 | 54 | &-medium { 55 | font-weight: 500; 56 | } 57 | 58 | &-semibold { 59 | font-weight: 600; 60 | } 61 | 62 | &-bold { 63 | font-weight: 700; 64 | } 65 | } 66 | 67 | // Text colors 68 | .text { 69 | &-gray-900 { 70 | color: var(--gray-900); 71 | } 72 | 73 | &-gray-800 { 74 | color: var(--gray-800); 75 | } 76 | 77 | &-gray-700 { 78 | color: var(--gray-700); 79 | } 80 | 81 | &-gray-600 { 82 | color: var(--gray-600); 83 | } 84 | 85 | &-gray-500 { 86 | color: var(--gray-500); 87 | } 88 | 89 | &-gray-400 { 90 | color: var(--gray-400); 91 | } 92 | 93 | &-primary { 94 | color: var(--primary-600); 95 | } 96 | } 97 | 98 | // Headings 99 | h1, 100 | .h1 { 101 | @extend .text-3xl; 102 | @extend .font-bold; 103 | @extend .text-gray-900; 104 | } 105 | 106 | h2, 107 | .h2 { 108 | @extend .text-2xl; 109 | @extend .font-semibold; 110 | @extend .text-gray-900; 111 | } 112 | 113 | h3, 114 | .h3 { 115 | @extend .text-xl; 116 | @extend .font-semibold; 117 | @extend .text-gray-900; 118 | } 119 | 120 | h4, 121 | .h4 { 122 | @extend .text-lg; 123 | @extend .font-medium; 124 | @extend .text-gray-900; 125 | } 126 | 127 | h5, 128 | .h5 { 129 | @extend .text-base; 130 | @extend .font-medium; 131 | @extend .text-gray-900; 132 | } 133 | 134 | h6, 135 | .h6 { 136 | @extend .text-sm; 137 | @extend .font-medium; 138 | @extend .text-gray-900; 139 | } 140 | -------------------------------------------------------------------------------- /rkk-demo/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config([ 16 | globalIgnores(['dist']), 17 | { 18 | files: ['**/*.{ts,tsx}'], 19 | extends: [ 20 | // Other configs... 21 | 22 | // Remove tseslint.configs.recommended and replace with this 23 | ...tseslint.configs.recommendedTypeChecked, 24 | // Alternatively, use this for stricter rules 25 | ...tseslint.configs.strictTypeChecked, 26 | // Optionally, add this for stylistic rules 27 | ...tseslint.configs.stylisticTypeChecked, 28 | 29 | // Other configs... 30 | ], 31 | languageOptions: { 32 | parserOptions: { 33 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 34 | tsconfigRootDir: import.meta.dirname, 35 | }, 36 | // other options... 37 | }, 38 | }, 39 | ]) 40 | ``` 41 | 42 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 43 | 44 | ```js 45 | // eslint.config.js 46 | import reactX from 'eslint-plugin-react-x' 47 | import reactDom from 'eslint-plugin-react-dom' 48 | 49 | export default tseslint.config([ 50 | globalIgnores(['dist']), 51 | { 52 | files: ['**/*.{ts,tsx}'], 53 | extends: [ 54 | // Other configs... 55 | // Enable lint rules for React 56 | reactX.configs['recommended-typescript'], 57 | // Enable lint rules for React DOM 58 | reactDom.configs.recommended, 59 | ], 60 | languageOptions: { 61 | parserOptions: { 62 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 63 | tsconfigRootDir: import.meta.dirname, 64 | }, 65 | // other options... 66 | }, 67 | }, 68 | ]) 69 | ``` 70 | -------------------------------------------------------------------------------- /rkk-demo/src/components/LanguageSwitcher/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Languages, ChevronDown } from "lucide-react"; 4 | 5 | const languages = [ 6 | { code: "ar", name: "العربية", flag: "🇵🇸" }, 7 | { code: "en", name: "English", flag: "🇺🇸" }, 8 | { code: "fr", name: "Français", flag: "🇫🇷" }, 9 | ]; 10 | 11 | export const LanguageSwitcher: React.FC = () => { 12 | const { i18n, t } = useTranslation(); 13 | const [isOpen, setIsOpen] = useState(false); 14 | 15 | const currentLanguage = 16 | languages.find((lang) => lang.code === i18n.language) || languages[0]; 17 | 18 | const handleLanguageChange = (languageCode: string) => { 19 | i18n.changeLanguage(languageCode); 20 | setIsOpen(false); 21 | 22 | // Update document direction for RTL languages 23 | document.documentElement.dir = languageCode === "ar" ? "rtl" : "ltr"; 24 | document.documentElement.lang = languageCode; 25 | }; 26 | 27 | return ( 28 |
29 | 45 | 46 | {isOpen && ( 47 | <> 48 |
setIsOpen(false)} 51 | /> 52 |
53 | {languages.map((language) => ( 54 | 68 | ))} 69 |
70 | 71 | )} 72 |
73 | ); 74 | }; 75 | 76 | export default LanguageSwitcher; 77 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/abstracts/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // Breakpoints 2 | $breakpoints: ( 3 | xs: 480px, 4 | sm: 640px, 5 | md: 768px, 6 | lg: 1024px, 7 | xl: 1280px, 8 | 2xl: 1536px, 9 | ) !default; 10 | 11 | // Get breakpoint value 12 | @function breakpoint($name) { 13 | @return map-get($breakpoints, $name); 14 | } 15 | 16 | // Media query mixins 17 | @mixin media-up($name) { 18 | $value: breakpoint($name); 19 | @if $value { 20 | @media (min-width: $value) { 21 | @content; 22 | } 23 | } 24 | } 25 | 26 | @mixin media-down($name) { 27 | $value: breakpoint($name); 28 | @if $value { 29 | @media (max-width: ($value - 1px)) { 30 | @content; 31 | } 32 | } 33 | } 34 | 35 | @mixin media-between($lower, $upper) { 36 | $lower-value: breakpoint($lower); 37 | $upper-value: breakpoint($upper); 38 | 39 | @if $lower-value and $upper-value { 40 | @media (min-width: $lower-value) and (max-width: ($upper-value - 1px)) { 41 | @content; 42 | } 43 | } 44 | } 45 | 46 | @mixin media-only($name) { 47 | $value: breakpoint($name); 48 | $next: null; 49 | 50 | // Find next breakpoint 51 | $breakpoint-names: map-keys($breakpoints); 52 | $index: index($breakpoint-names, $name); 53 | 54 | @if $index and $index < length($breakpoint-names) { 55 | $next-name: nth($breakpoint-names, $index + 1); 56 | $next: breakpoint($next-name); 57 | } 58 | 59 | @if $value and $next { 60 | @media (min-width: $value) and (max-width: ($next - 1px)) { 61 | @content; 62 | } 63 | } @else if $value { 64 | @media (min-width: $value) { 65 | @content; 66 | } 67 | } 68 | } 69 | 70 | // Utility mixins for common breakpoints 71 | @mixin mobile-only { 72 | @include media-down(sm) { 73 | @content; 74 | } 75 | } 76 | 77 | @mixin tablet-only { 78 | @include media-between(sm, lg) { 79 | @content; 80 | } 81 | } 82 | 83 | @mixin desktop-only { 84 | @include media-up(lg) { 85 | @content; 86 | } 87 | } 88 | 89 | // High DPI / Retina display mixin 90 | @mixin retina { 91 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { 92 | @content; 93 | } 94 | } 95 | 96 | // Orientation mixins 97 | @mixin landscape { 98 | @media (orientation: landscape) { 99 | @content; 100 | } 101 | } 102 | 103 | @mixin portrait { 104 | @media (orientation: portrait) { 105 | @content; 106 | } 107 | } 108 | 109 | // Reduced motion mixin 110 | @mixin reduced-motion { 111 | @media (prefers-reduced-motion: reduce) { 112 | @content; 113 | } 114 | } 115 | 116 | // Dark mode mixin 117 | @mixin dark-mode { 118 | @media (prefers-color-scheme: dark) { 119 | @content; 120 | } 121 | } 122 | 123 | // High contrast mixin 124 | @mixin high-contrast { 125 | @media (prefers-contrast: high) { 126 | @content; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://github.com/braiekhazem/react-kanban-kit", 3 | "name": "react-kanban-kit", 4 | "author": "hazem braiek", 5 | "private": false, 6 | "version": "0.0.2-beta.6", 7 | "type": "module", 8 | "main": "dist/index.cjs.js", 9 | "module": "dist/index.es.js", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/index.es.js", 14 | "require": "./dist/index.umd.js", 15 | "types": "./dist/index.d.ts" 16 | } 17 | }, 18 | "keywords": [ 19 | "react-kanban-kit", 20 | "board", 21 | "kanban", 22 | "rkk", 23 | "react-kanban", 24 | "drag-and-drop", 25 | "virtualized", 26 | "programmatic-drag-and-drop" 27 | ], 28 | "sideEffects": true, 29 | "files": [ 30 | "/dist" 31 | ], 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/braiekhazem/react-kanban-kit" 38 | }, 39 | "license": "MIT", 40 | "bin": { 41 | "mycli": "./cli.cjs" 42 | }, 43 | "scripts": { 44 | "test": "jest --watchAll --coverage", 45 | "dev": "vite", 46 | "build": "tsc && vite build", 47 | "demo": "cd demo && npm run dev", 48 | "modern-demo": "vite serve demo/modern --config demo/vite.config.ts", 49 | "build-demo": "cd demo && npm run build", 50 | "prepare": "npm run build", 51 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 52 | "preview": "vite preview --port 8080", 53 | "format": "prettier --ignore-path .gitignore --write \"**/*.{ts,tsx,css,scss}\"", 54 | "husky": "husky install", 55 | "create-component": "bash ./scripts/create-component.sh", 56 | "docker:build": "./scripts/build-docker.sh", 57 | "predeploy": "npm run build", 58 | "deploy": "gh-pages -d dist" 59 | }, 60 | "dependencies": { 61 | "@atlaskit/pragmatic-drag-and-drop": "^1.5.0", 62 | "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.0", 63 | "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", 64 | "@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-autoscroll": "^2.0.0", 65 | "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^2.1.0", 66 | "classes": "^0.3.0", 67 | "classnames": "^2.5.1", 68 | "husky": "^8.0.3", 69 | "virtua": "^0.40.4", 70 | "vite-plugin-css-injected-by-js": "^3.4.0", 71 | "vite-plugin-dts": "^3.7.3" 72 | }, 73 | "devDependencies": { 74 | "@babel/plugin-transform-react-jsx": "^7.23.4", 75 | "@types/lodash": "^4.17.16", 76 | "@types/node": "^22.15.21", 77 | "@types/react": "^18.2.0", 78 | "@types/react-dom": "^18.2.0", 79 | "@vitejs/plugin-react": "^4.0.0", 80 | "eslint": "^8.38.0", 81 | "eslint-plugin-storybook": "^0.8.0", 82 | "gh-pages": "^6.1.1", 83 | "prettier": "3.0.0", 84 | "react": "^18.2.0", 85 | "react-dom": "^18.2.0", 86 | "sass": "^1.71.1", 87 | "terser": "^5.29.2", 88 | "typescript": "^5.4.2", 89 | "vite": "^4.3.9", 90 | "vite-plugin-svgr": "^3.2.0" 91 | }, 92 | "peerDependencies": { 93 | "react": ">=18.0.0", 94 | "react-dom": ">=18.0.0" 95 | }, 96 | "peerDependenciesMeta": { 97 | "react": { 98 | "optional": false 99 | }, 100 | "react-dom": { 101 | "optional": false 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/Kanban.tsx: -------------------------------------------------------------------------------- 1 | import { BoardProps } from "./types"; 2 | import { 3 | getColumnChildren, 4 | getColumnsFromDataSource, 5 | } from "@/utils/columnsUtils"; 6 | import { withPrefix } from "@/utils/getPrefix"; 7 | import classNames from "classnames"; 8 | import { Column } from "./Column"; 9 | import { forwardRef, useEffect, useRef } from "react"; 10 | import { autoScroller } from "@atlaskit/pragmatic-drag-and-drop-react-beautiful-dnd-autoscroll"; 11 | import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 12 | import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; 13 | import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; 14 | import { KanbanProvider } from "@/context/KanbanContext"; 15 | import mergeRefs from "@/utils/mergeRefs"; 16 | import { handleCardDrop } from "@/global/dnd/dropManager"; 17 | import { getSharedProps } from "@/utils/getSharedProps"; 18 | import ColumnAdder from "./ColumnAdder"; 19 | 20 | const Kanban = forwardRef((props, ref) => { 21 | const { 22 | dataSource, 23 | rootStyle = {}, 24 | rootClassName, 25 | onColumnMove, 26 | onCardMove, 27 | renderColumnWrapper, 28 | renderColumnAdder, 29 | ...rest 30 | } = props; 31 | 32 | const columns = getColumnsFromDataSource(dataSource); 33 | const internalRef = useRef(null); 34 | 35 | useEffect(() => { 36 | if (!internalRef.current) return; 37 | 38 | return combine( 39 | monitorForElements({ 40 | onDragStart({ location }) { 41 | autoScroller.start({ input: location.current.input }); 42 | }, 43 | onDrag({ location }) { 44 | autoScroller.updateInput({ input: location.current.input }); 45 | }, 46 | onDrop(args) { 47 | autoScroller.stop(); 48 | handleCardDrop({ 49 | source: { 50 | id: (args.source as any).id || "", 51 | data: args.source.data, 52 | }, 53 | location: { 54 | current: { 55 | dropTargets: args.location.current.dropTargets, 56 | }, 57 | }, 58 | columns, 59 | dataSource, 60 | onCardMove, 61 | onColumnMove, 62 | }); 63 | }, 64 | }), 65 | autoScrollForElements({ 66 | element: internalRef.current, 67 | canScroll: () => true, 68 | getConfiguration: () => ({ 69 | maxScrollSpeed: "standard", 70 | }), 71 | }) 72 | ); 73 | }, [columns, dataSource, onCardMove, onColumnMove]); 74 | 75 | const containerClassName = classNames(withPrefix("board"), rootClassName); 76 | 77 | return ( 78 | 79 |
84 | {columns?.map((column, index) => ( 85 | 93 | ))} 94 | 95 |
96 |
97 | ); 98 | }); 99 | 100 | export default Kanban; 101 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/Overview/Overview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { dropHandler, Kanban, type BoardData } from "react-kanban-kit"; 4 | import { User } from "lucide-react"; 5 | import { mockData } from "../../utils/_mock_"; 6 | 7 | const PriorityBadge: React.FC<{ priority: string }> = ({ priority }) => { 8 | const getColorClass = (priority: string) => { 9 | switch (priority) { 10 | case "high": 11 | return "priority-high"; 12 | case "medium": 13 | return "priority-medium"; 14 | case "low": 15 | return "priority-low"; 16 | default: 17 | return "priority-medium"; 18 | } 19 | }; 20 | 21 | return ( 22 | 23 | {priority} 24 | 25 | ); 26 | }; 27 | 28 | export const Overview: React.FC = () => { 29 | const { t } = useTranslation(); 30 | const [dataSource, setDataSource] = useState( 31 | structuredClone(mockData) as BoardData 32 | ); 33 | 34 | return ( 35 |
36 |
37 |

{t("pages.overview.title")}

38 |

{t("pages.overview.description")}

39 |
40 | 41 |
42 | ( 47 |
48 |
49 |

{data.title}

50 | 53 |
54 | 55 |
56 |
57 | 58 | 59 | {data.content?.assignee || 60 | t("common.unassigned", "Unassigned")} 61 | 62 |
63 |
64 |
65 | ), 66 | isDraggable: true, 67 | }, 68 | }} 69 | cardsGap={6} 70 | virtualization={false} 71 | onCardMove={(move) => 72 | setDataSource( 73 | dropHandler( 74 | move, 75 | dataSource, 76 | () => {}, 77 | (newColumn) => { 78 | return { 79 | ...newColumn, 80 | totalItemsCount: (newColumn.totalItemsCount || 0) + 1, 81 | totalChildrenCount: (newColumn.totalChildrenCount || 0) + 1, 82 | }; 83 | }, 84 | (sourceColumn) => { 85 | return { 86 | ...sourceColumn, 87 | totalItemsCount: (sourceColumn.totalItemsCount || 0) - 1, 88 | totalChildrenCount: 89 | (sourceColumn.totalChildrenCount || 0) - 1, 90 | }; 91 | } 92 | ) 93 | ) 94 | } 95 | /> 96 |
97 |
98 | ); 99 | }; 100 | 101 | export default Overview; 102 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /* Modern CSS Reset */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | html { 11 | line-height: 1.5; 12 | -webkit-text-size-adjust: 100%; 13 | -moz-tab-size: 4; 14 | tab-size: 4; 15 | font-family: var(--font-sans); 16 | font-feature-settings: normal; 17 | font-variation-settings: normal; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | line-height: inherit; 23 | color: var(--gray-900); 24 | background-color: var(--gray-50); 25 | overflow: hidden; 26 | } 27 | 28 | hr { 29 | height: 0; 30 | color: inherit; 31 | border-top-width: 1px; 32 | } 33 | 34 | abbr:where([title]) { 35 | text-decoration: underline dotted; 36 | } 37 | 38 | h1, 39 | h2, 40 | h3, 41 | h4, 42 | h5, 43 | h6 { 44 | font-size: inherit; 45 | font-weight: inherit; 46 | } 47 | 48 | a { 49 | color: inherit; 50 | text-decoration: inherit; 51 | } 52 | 53 | b, 54 | strong { 55 | font-weight: bolder; 56 | } 57 | 58 | code, 59 | kbd, 60 | samp, 61 | pre { 62 | font-family: var(--font-mono); 63 | font-size: 1em; 64 | } 65 | 66 | small { 67 | font-size: 80%; 68 | } 69 | 70 | sub, 71 | sup { 72 | font-size: 75%; 73 | line-height: 0; 74 | position: relative; 75 | vertical-align: baseline; 76 | } 77 | 78 | sub { 79 | bottom: -0.25em; 80 | } 81 | 82 | sup { 83 | top: -0.5em; 84 | } 85 | 86 | table { 87 | text-indent: 0; 88 | border-color: inherit; 89 | border-collapse: collapse; 90 | } 91 | 92 | button, 93 | input, 94 | optgroup, 95 | select, 96 | textarea { 97 | font-family: inherit; 98 | font-size: 100%; 99 | font-weight: inherit; 100 | line-height: inherit; 101 | color: inherit; 102 | margin: 0; 103 | padding: 0; 104 | } 105 | 106 | button, 107 | select { 108 | text-transform: none; 109 | } 110 | 111 | button, 112 | [type="button"], 113 | [type="reset"], 114 | [type="submit"] { 115 | -webkit-appearance: button; 116 | background-color: transparent; 117 | background-image: none; 118 | } 119 | 120 | :-moz-focusring { 121 | outline: auto; 122 | } 123 | 124 | :-moz-ui-invalid { 125 | box-shadow: none; 126 | } 127 | 128 | progress { 129 | vertical-align: baseline; 130 | } 131 | 132 | ::-webkit-inner-spin-button, 133 | ::-webkit-outer-spin-button { 134 | height: auto; 135 | } 136 | 137 | [type="search"] { 138 | -webkit-appearance: textfield; 139 | outline-offset: -2px; 140 | } 141 | 142 | ::-webkit-search-decoration { 143 | -webkit-appearance: none; 144 | } 145 | 146 | ::-webkit-file-upload-button { 147 | -webkit-appearance: button; 148 | font: inherit; 149 | } 150 | 151 | summary { 152 | display: list-item; 153 | } 154 | 155 | blockquote, 156 | dl, 157 | dd, 158 | h1, 159 | h2, 160 | h3, 161 | h4, 162 | h5, 163 | h6, 164 | hr, 165 | figure, 166 | p, 167 | pre { 168 | margin: 0; 169 | } 170 | 171 | fieldset { 172 | margin: 0; 173 | padding: 0; 174 | } 175 | 176 | legend { 177 | padding: 0; 178 | } 179 | 180 | ol, 181 | ul, 182 | menu { 183 | list-style: none; 184 | margin: 0; 185 | padding: 0; 186 | } 187 | 188 | textarea { 189 | resize: vertical; 190 | } 191 | 192 | input::placeholder, 193 | textarea::placeholder { 194 | opacity: 1; 195 | color: var(--gray-400); 196 | } 197 | 198 | button, 199 | [role="button"] { 200 | cursor: pointer; 201 | } 202 | 203 | :disabled { 204 | cursor: default; 205 | } 206 | 207 | img, 208 | svg, 209 | video, 210 | canvas, 211 | audio, 212 | iframe, 213 | embed, 214 | object { 215 | display: block; 216 | vertical-align: middle; 217 | } 218 | 219 | img, 220 | video { 221 | max-width: 100%; 222 | height: auto; 223 | } 224 | 225 | [hidden] { 226 | display: none; 227 | } 228 | -------------------------------------------------------------------------------- /rkk-demo/src/components/LanguageSwitcher/_LanguageSwitcher.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts" as *; 2 | 3 | .#{$demo-prefix}-language-switcher { 4 | position: relative; 5 | 6 | &-trigger { 7 | @include flex-start; 8 | gap: var(--space-2); 9 | padding: var(--space-2) var(--space-3); 10 | font-size: var(--text-sm); 11 | font-weight: 500; 12 | color: var(--gray-600); 13 | background: rgba(255, 255, 255, 0.9); 14 | border: 1px solid var(--gray-200); 15 | border-radius: var(--radius-lg); 16 | transition: all var(--transition-base); 17 | cursor: pointer; 18 | backdrop-filter: blur(8px); 19 | 20 | &:hover { 21 | color: var(--primary-600); 22 | background: white; 23 | border-color: var(--primary-200); 24 | box-shadow: var(--shadow-sm); 25 | } 26 | 27 | &:focus-visible { 28 | outline: 2px solid var(--primary-500); 29 | outline-offset: 2px; 30 | } 31 | } 32 | 33 | &-current { 34 | @include flex-start; 35 | gap: var(--space-1-5); 36 | font-size: var(--text-sm); 37 | 38 | @include media-down(sm) { 39 | display: none; 40 | } 41 | } 42 | 43 | &-chevron { 44 | transition: transform var(--transition-base); 45 | 46 | &.open { 47 | transform: rotate(180deg); 48 | } 49 | } 50 | 51 | &-overlay { 52 | position: fixed; 53 | top: 0; 54 | left: 0; 55 | right: 0; 56 | bottom: 0; 57 | z-index: var(--z-overlay); 58 | } 59 | 60 | &-dropdown { 61 | position: absolute; 62 | top: calc(100% + var(--space-2)); 63 | right: 0; 64 | min-width: 200px; 65 | background: white; 66 | border: 1px solid var(--gray-200); 67 | border-radius: var(--radius-lg); 68 | box-shadow: var(--shadow-xl); 69 | backdrop-filter: blur(16px); 70 | z-index: var(--z-dropdown); 71 | overflow: hidden; 72 | animation: dropdown-appear 0.15s ease-out; 73 | 74 | @include media-down(sm) { 75 | right: auto; 76 | left: 0; 77 | min-width: 160px; 78 | } 79 | } 80 | 81 | &-option { 82 | @include flex-start; 83 | gap: var(--space-3); 84 | width: 100%; 85 | padding: var(--space-3) var(--space-4); 86 | font-size: var(--text-sm); 87 | font-weight: 500; 88 | color: var(--gray-700); 89 | background: transparent; 90 | border: none; 91 | text-align: left; 92 | cursor: pointer; 93 | transition: all var(--transition-base); 94 | 95 | &:hover { 96 | background: var(--primary-50); 97 | color: var(--primary-700); 98 | } 99 | 100 | &.active { 101 | background: var(--primary-100); 102 | color: var(--primary-800); 103 | font-weight: 600; 104 | 105 | &::after { 106 | content: "✓"; 107 | margin-left: auto; 108 | color: var(--primary-600); 109 | font-weight: 700; 110 | } 111 | } 112 | 113 | &:not(:last-child) { 114 | border-bottom: 1px solid var(--gray-100); 115 | } 116 | } 117 | 118 | &-flag { 119 | font-size: 1.125rem; 120 | flex-shrink: 0; 121 | } 122 | 123 | &-name { 124 | flex: 1; 125 | } 126 | } 127 | 128 | @keyframes dropdown-appear { 129 | from { 130 | opacity: 0; 131 | transform: translateY(-8px) scale(0.95); 132 | } 133 | to { 134 | opacity: 1; 135 | transform: translateY(0) scale(1); 136 | } 137 | } 138 | 139 | // RTL Support 140 | [dir="rtl"] .#{$demo-prefix}-language-switcher { 141 | &-dropdown { 142 | right: auto; 143 | left: 0; 144 | 145 | @include media-down(sm) { 146 | left: auto; 147 | right: 0; 148 | } 149 | } 150 | 151 | &-option { 152 | text-align: right; 153 | 154 | &.active::after { 155 | margin-left: 0; 156 | margin-right: auto; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /rkk-demo/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/GenericItem/GenericItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BoardItem, BoardProps, ConfigMap, DndState } from "../types"; 3 | import classNames from "classnames"; 4 | import { withPrefix } from "@/utils/getPrefix"; 5 | import CardSkeleton from "../CardSkeleton"; 6 | import Card from "../Card"; 7 | import DefaultCard from "../DefaultCard"; 8 | import { CardShadow } from "../Card/Card"; 9 | 10 | const isCardDraggable = (data: BoardItem, isTypeDraggable: boolean) => { 11 | return data?.isDraggable !== undefined ? data?.isDraggable : isTypeDraggable; 12 | }; 13 | 14 | interface Props { 15 | index: number; 16 | options: { 17 | data: BoardItem; 18 | column: BoardItem; 19 | configMap: ConfigMap; 20 | //isSkeleton is used to show a skeleton UI when the item is not loaded yet 21 | isSkeleton: boolean; 22 | isShadow: boolean; 23 | isListFooter: boolean; 24 | renderListFooter?: (column: BoardItem) => React.ReactNode; 25 | cardWrapperStyle?: ( 26 | card: BoardItem, 27 | column: BoardItem 28 | ) => React.CSSProperties; 29 | cardWrapperClassName?: string; 30 | cardsGap?: number; 31 | renderSkeletonCard?: BoardProps["renderSkeletonCard"]; 32 | onCardDndStateChange?: (info: DndState) => void; 33 | onCardClick?: ( 34 | e: React.MouseEvent, 35 | card: BoardItem 36 | ) => void; 37 | cardOverHeight?: number; 38 | renderCardDragIndicator?: (card: BoardItem, info: any) => React.ReactNode; 39 | renderCardDragPreview?: (card: BoardItem, info: any) => React.ReactNode; 40 | renderGap?: (column: BoardItem) => React.ReactNode; 41 | }; 42 | } 43 | 44 | const GenericItem = (props: Props) => { 45 | const { index, options } = props; 46 | const { 47 | data, 48 | column, 49 | configMap, 50 | isSkeleton, 51 | cardWrapperStyle, 52 | cardWrapperClassName, 53 | cardsGap = 8, 54 | isShadow, 55 | isListFooter, 56 | cardOverHeight = 90, 57 | renderSkeletonCard, 58 | onCardClick, 59 | onCardDndStateChange, 60 | renderCardDragIndicator, 61 | renderListFooter, 62 | renderCardDragPreview, 63 | renderGap, 64 | } = options; 65 | 66 | const { render = DefaultCard, isDraggable = true } = 67 | configMap?.[data?.type] || {}; 68 | 69 | const wrapperClassName = classNames( 70 | withPrefix("generic-item-wrapper"), 71 | cardWrapperClassName 72 | ); 73 | 74 | const renderCardContent = () => { 75 | if (isListFooter) 76 | return ( 77 |
78 | {renderListFooter?.(column) || "Default Footer"} 79 |
80 | ); 81 | else if (isShadow) 82 | return ( 83 | 89 | ); 90 | else if (isSkeleton) 91 | return ( 92 |
97 | {renderSkeletonCard?.({ index, column }) || ( 98 | 99 | )} 100 |
101 | ); 102 | 103 | return ( 104 | 117 | ); 118 | }; 119 | 120 | return ( 121 |
127 | {renderCardContent()} 128 |
129 | ); 130 | }; 131 | 132 | export default GenericItem; 133 | -------------------------------------------------------------------------------- /src/global/theme-default.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --vf-font-familly: "Raleway", sans-serif; 3 | --vf-prefix-class: "vf"; 4 | 5 | //COLORS 6 | 7 | --vf-color-white: #fff; 8 | --vf-color-black: #000; 9 | 10 | --vf-color-primary: #5f55ee; 11 | --vf-color-primary-50: #fcf9ff; 12 | --vf-color-primary-100: #f5eefa; 13 | --vf-color-primary-200: #e0ccef; 14 | --vf-color-primary-300: #c198e0; 15 | --vf-color-primary-400: #ad76d5; 16 | --vf-color-primary-500: #9854cb; 17 | --vf-color-primary-600: #7a43a2; 18 | --vf-color-primary-700: #5b327a; 19 | --vf-color-primary-800: #3d2251; 20 | --vf-color-primary-900: #301942; 21 | --vf-color-primary-950: #1e1129; 22 | 23 | --vf-color-secondary: #fdb022; 24 | --vf-color-secondary-50: #fffcf5; 25 | --vf-color-secondary-100: #fffaeb; 26 | --vf-color-secondary-200: #fef0c7; 27 | --vf-color-secondary-300: #fedf89; 28 | --vf-color-secondary-400: #fec84b; 29 | --vf-color-secondary-500: #fdb022; 30 | --vf-color-secondary-600: #f79009; 31 | --vf-color-secondary-700: #dc6803; 32 | --vf-color-secondary-800: #b54708; 33 | --vf-color-secondary-900: #92330a; 34 | --vf-color-secondary-950: #6c2304; 35 | 36 | --vf-color-red: #ff4141; 37 | --vf-color-red-50: #fff4f4; 38 | --vf-color-red-100: #ffe8e8; 39 | --vf-color-red-200: #ffd4d4; 40 | --vf-color-red-300: #ffb4b4; 41 | --vf-color-red-400: #ff8383; 42 | --vf-color-red-500: #ff4141; 43 | --vf-color-red-600: #ff1616; 44 | --vf-color-red-700: #d80000; 45 | --vf-color-red-800: #b50000; 46 | --vf-color-red-900: #7e040e; 47 | --vf-color-red-950: #64020a; 48 | 49 | --vf-color-gray: #9d9d9d; 50 | --vf-color-gray-50: #ffffff; 51 | --vf-color-gray-100: #f0f0f0; 52 | --vf-color-gray-200: #dadada; 53 | --vf-color-gray-300: #cecece; 54 | --vf-color-gray-400: #b6b6b6; 55 | --vf-color-gray-500: #9d9d9d; 56 | --vf-color-gray-600: #6a6a6a; 57 | --vf-color-gray-700: #545454; 58 | --vf-color-gray-800: #373737; 59 | --vf-color-gray-900: #1c1c1c; 60 | --vf-color-gray-950: #000000; 61 | 62 | --vf-color-green: #29b58b; 63 | --vf-color-green-50: #f1fffb; 64 | --vf-color-green-100: #e2fff6; 65 | --vf-color-green-200: #ccfff0; 66 | --vf-color-green-300: #a3edd7; 67 | --vf-color-green-400: #52c8a4; 68 | --vf-color-green-500: #29b58b; 69 | --vf-color-green-600: #00a372; 70 | --vf-color-green-700: #00825b; 71 | --vf-color-green-800: #006346; 72 | --vf-color-green-900: #00412e; 73 | --vf-color-green-950: #002117; 74 | 75 | --vf-color-orange: #29b58b; 76 | --vf-color-orange-50: #fffdf6; 77 | --vf-color-orange-100: #fffaea; 78 | --vf-color-orange-200: #fff5d4; 79 | --vf-color-orange-300: #ffecaa; 80 | --vf-color-orange-400: #ffe27f; 81 | --vf-color-orange-500: #ffcf2b; 82 | --vf-color-orange-600: #e6ba27; 83 | --vf-color-orange-700: #bf9b20; 84 | --vf-color-orange-800: #806716; 85 | --vf-color-orange-900: #4d3f0d; 86 | --vf-color-orange-950: #342a08; 87 | --vf-color-orange-1000: #fa8900; 88 | 89 | --vf-color-ashgrey: #b3afa1; 90 | --vf-color-ashgrey-50: #fdfdfc; 91 | --vf-color-ashgrey-100: #fbfbf9; 92 | --vf-color-ashgrey-200: #f7f6f2; 93 | --vf-color-ashgrey-300: #f0efea; 94 | --vf-color-ashgrey-400: #dddad0; 95 | --vf-color-ashgrey-500: #b3afa1; 96 | --vf-color-ashgrey-600: #858071; 97 | --vf-color-ashgrey-700: #5e5267; 98 | --vf-color-ashgrey-800: #676252; 99 | --vf-color-ashgrey-900: #39321d; 100 | --vf-color-ashgrey-950: #282210; 101 | 102 | --vf-color-snow: #b3afa1; 103 | --vf-color-snow-50: #fdfcfd; 104 | --vf-color-snow-100: #faf9fb; 105 | --vf-color-snow-200: #f5f2f7; 106 | --vf-color-snow-300: #edeaf0; 107 | --vf-color-snow-400: #d7d0dd; 108 | --vf-color-snow-500: #aba1b3; 109 | --vf-color-snow-600: #7c7185; 110 | --vf-color-snow-700: #5e5267; 111 | --vf-color-snow-800: #463454; 112 | --vf-color-snow-900: #2d1d39; 113 | --vf-color-snow-950: #1e1028; 114 | 115 | --vf-color-gray-0: #fff; 116 | --vf-color-yellow-50: #ffb300; 117 | --vf-color-cyan-50: #00bcd4; 118 | --vf-color-blue-50: #2196f3; 119 | --vf-color-violet-50: #673ab7; 120 | 121 | --vf-color-info: #1677ff; 122 | --vf-color-success: #52c41a; 123 | --vf-color-warning: #faad14; 124 | --vf-color-error: #ff4d4f; 125 | 126 | --vf-control-bar-height: 48px; 127 | 128 | --vf-border-radius: 12px; 129 | 130 | --vf-progress-bar-bg: #fff3; 131 | --vf-progress-bar-load-bg: #fff6; 132 | --vf-progress-bar-play-bg: var(--vf-color-primary); 133 | 134 | --vf-dropdown-menu-item-hover: #ffffff1a; 135 | --vf-sound-icon-size: 18px; 136 | --vf-sound-icon-color: #fff; 137 | } 138 | -------------------------------------------------------------------------------- /src/components/types.ts: -------------------------------------------------------------------------------- 1 | import { TaskCardState } from "@/global/dnd/useCardDnd"; 2 | import { TColumnState } from "@/global/dnd/useColumnDnd"; 3 | import { CSSProperties, ReactNode } from "react"; 4 | 5 | export interface DndState { 6 | state: TaskCardState | TColumnState; 7 | column?: BoardItem; 8 | card?: BoardItem; 9 | } 10 | 11 | export interface ScrollEvent { 12 | target: { 13 | scrollTop: number; 14 | scrollHeight: number; 15 | clientHeight: number; 16 | }; 17 | } 18 | 19 | export type CardRenderProps = { 20 | data: BoardItem; 21 | column: BoardItem; 22 | index: number; 23 | isDraggable: boolean; 24 | }; 25 | 26 | export type ConfigMap = { 27 | [type: string]: { 28 | render: (props: CardRenderProps) => React.ReactNode; 29 | isDraggable?: boolean; 30 | }; 31 | }; 32 | 33 | export interface BoardItem { 34 | id: string; 35 | title: string; 36 | parentId: string | null; 37 | children: string[]; 38 | content?: any; 39 | type?: keyof ConfigMap; 40 | totalItems?: number; 41 | // totalChildrenCount is the total number of children in the column 42 | totalChildrenCount: number; 43 | // totalItemsCount is the total number of items (real content) in the column 44 | totalItemsCount?: number; 45 | isDraggable?: boolean; 46 | } 47 | 48 | export interface BoardData { 49 | root: BoardItem; 50 | [key: string]: BoardItem; 51 | } 52 | 53 | export interface BoardProps { 54 | dataSource: BoardData; 55 | configMap: ConfigMap; 56 | viewOnly?: boolean; 57 | loadMore?: (groupsId: string) => void; 58 | renderSkeletonCard?: ({ 59 | index, 60 | column, 61 | }: { 62 | index: number; 63 | column: BoardItem; 64 | }) => ReactNode; 65 | renderColumnHeader?: (column: BoardItem) => ReactNode; 66 | renderCardDragIndicator?: (card: BoardItem, info: any) => ReactNode; 67 | renderCardDragPreview?: (card: BoardItem, info: any) => ReactNode; 68 | // renderColumnDragIndicator?: (column: BoardItem, info: any) => ReactNode; 69 | // renderColumnDragPreview?: (column: BoardItem, info: any) => ReactNode; 70 | 71 | renderListFooter?: (column: BoardItem) => ReactNode; 72 | allowListFooter?: (column: BoardItem) => boolean; 73 | 74 | renderColumnAdder?: () => ReactNode; 75 | allowColumnAdder?: boolean; 76 | 77 | renderColumnWrapper?: ( 78 | column: BoardItem, 79 | { 80 | children, 81 | className, 82 | style, 83 | }: { children: ReactNode; className?: string; style?: CSSProperties } 84 | ) => ReactNode; 85 | columnWrapperStyle?: (column: BoardItem) => CSSProperties; 86 | columnHeaderStyle?: (column: BoardItem) => CSSProperties; 87 | columnListContentStyle?: (column: BoardItem) => CSSProperties; 88 | columnListContentClassName?: (column: BoardItem) => string; 89 | columnWrapperClassName?: (column: BoardItem) => string; 90 | columnHeaderClassName?: (column: BoardItem) => string; 91 | columnClassName?: (column: BoardItem) => string; 92 | columnStyle?: (column: BoardItem) => CSSProperties; 93 | rootStyle?: CSSProperties; 94 | rootClassName?: string; 95 | cardWrapperStyle?: (card: BoardItem, column: BoardItem) => CSSProperties; 96 | cardWrapperClassName?: string; 97 | virtualization?: boolean; 98 | cardsGap?: number; 99 | // renderGap?: (column: BoardItem) => ReactNode; 100 | onScroll?: (e: ScrollEvent, column: BoardItem) => void; 101 | onColumnMove?: ({ 102 | columnId, 103 | fromIndex, 104 | toIndex, 105 | }: { 106 | columnId: string; 107 | fromIndex: number; 108 | toIndex: number; 109 | }) => void; 110 | onCardMove?: ({ 111 | cardId, 112 | fromColumnId, 113 | toColumnId, 114 | taskAbove, 115 | taskBelow, 116 | position, 117 | }: { 118 | cardId: string; 119 | fromColumnId: string; 120 | toColumnId: string; 121 | taskAbove: string | null; 122 | taskBelow: string | null; 123 | position: number; 124 | }) => void; 125 | renderColumnFooter?: (column: BoardItem) => ReactNode; 126 | onColumnClick?: ( 127 | e: React.MouseEvent, 128 | column: BoardItem 129 | ) => void; 130 | onCardClick?: (e: React.MouseEvent, card: BoardItem) => void; 131 | onCardDndStateChange?: (info: DndState) => void; 132 | onColumnDndStateChange?: (info: DndState) => void; 133 | } 134 | 135 | export interface DropParams { 136 | source: { 137 | id: string; 138 | data: any; 139 | }; 140 | location: { 141 | current: { 142 | dropTargets: Array<{ 143 | data: any; 144 | }>; 145 | }; 146 | }; 147 | columns: BoardItem[]; 148 | dataSource: BoardData; 149 | onCardMove?: BoardProps["onCardMove"]; 150 | onColumnMove?: BoardProps["onColumnMove"]; 151 | } 152 | -------------------------------------------------------------------------------- /rkk-demo/src/utils/kanbanUtils.ts: -------------------------------------------------------------------------------- 1 | import type { BoardData } from "react-kanban-kit"; 2 | 3 | export const getAddCardPlaceholderKey = (columnId: string) => 4 | `add-card-${columnId}`; 5 | 6 | export const addCardPlaceholder = ( 7 | columnId: string, 8 | dataSource: BoardData, 9 | inTop: boolean = true 10 | ): BoardData => { 11 | const addCardPlaceholderKey = getAddCardPlaceholderKey(columnId); 12 | 13 | const alreadyHasAddCardPlaceholder = dataSource[columnId].children.includes( 14 | addCardPlaceholderKey 15 | ); 16 | 17 | return { 18 | ...dataSource, 19 | [columnId]: { 20 | ...dataSource[columnId], 21 | totalChildrenCount: alreadyHasAddCardPlaceholder 22 | ? dataSource[columnId].totalChildrenCount - 1 23 | : dataSource[columnId].totalChildrenCount + 1, 24 | children: alreadyHasAddCardPlaceholder 25 | ? dataSource[columnId].children.filter( 26 | (child: string) => child !== addCardPlaceholderKey 27 | ) 28 | : inTop 29 | ? [addCardPlaceholderKey, ...dataSource[columnId].children] 30 | : [...dataSource[columnId].children, addCardPlaceholderKey], 31 | }, 32 | [addCardPlaceholderKey]: { 33 | id: addCardPlaceholderKey, 34 | title: "Add card", 35 | parentId: columnId, 36 | children: [], 37 | type: "new-card", 38 | content: { 39 | inTop, 40 | id: addCardPlaceholderKey, 41 | }, 42 | }, 43 | } as BoardData; 44 | }; 45 | 46 | export const removeCardPlaceholder = ( 47 | columnId: string, 48 | dataSource: BoardData 49 | ) => { 50 | const addCardPlaceholderKey = getAddCardPlaceholderKey(columnId); 51 | return { 52 | ...dataSource, 53 | [columnId]: { 54 | ...dataSource[columnId], 55 | totalChildrenCount: dataSource[columnId].totalChildrenCount - 1, 56 | children: dataSource[columnId].children.filter( 57 | (child: string) => child !== addCardPlaceholderKey 58 | ), 59 | }, 60 | }; 61 | }; 62 | 63 | export const addCard = ( 64 | columnId: string, 65 | dataSource: BoardData, 66 | title: string, 67 | inTop: boolean = true 68 | ): BoardData => { 69 | const newTaskId = `task-${title}-${Date.now()}`; 70 | return { 71 | ...dataSource, 72 | [columnId]: { 73 | ...dataSource[columnId], 74 | totalItemsCount: (dataSource[columnId].totalItemsCount || 0) + 1, 75 | children: [ 76 | inTop ? newTaskId : null, 77 | ...dataSource[columnId].children.filter( 78 | (child: string) => child !== getAddCardPlaceholderKey(columnId) 79 | ), 80 | !inTop ? newTaskId : null, 81 | ].filter(Boolean), 82 | }, 83 | [newTaskId]: { 84 | id: newTaskId, 85 | title, 86 | parentId: columnId, 87 | children: [], 88 | totalChildrenCount: 0, 89 | type: "card", 90 | content: { 91 | title, 92 | id: newTaskId, 93 | }, 94 | }, 95 | } as BoardData; 96 | }; 97 | 98 | export const toggleCollapsedColumn = ( 99 | columnId: string, 100 | dataSource: BoardData 101 | ): BoardData => { 102 | return { 103 | ...dataSource, 104 | [columnId]: { 105 | ...dataSource[columnId], 106 | content: { 107 | ...dataSource?.[columnId]?.content, 108 | isExpanded: !dataSource?.[columnId]?.content?.isExpanded, 109 | }, 110 | }, 111 | }; 112 | }; 113 | 114 | export const toggleCardOver = ( 115 | columnId: string, 116 | dataSource: BoardData 117 | ): BoardData => { 118 | return { 119 | ...dataSource, 120 | [columnId]: { 121 | ...dataSource[columnId], 122 | content: { 123 | ...dataSource?.[columnId]?.content, 124 | isCardOver: !dataSource?.[columnId]?.content?.isCardOver, 125 | }, 126 | }, 127 | }; 128 | }; 129 | 130 | export const getPriorityColor = (priority: string) => { 131 | const colors = { 132 | high: "#ffc53d", 133 | medium: "#f59e0b", 134 | low: "#bbb", 135 | urgent: "#c62a2f", 136 | }; 137 | return colors[priority as keyof typeof colors] || "#6b7280"; 138 | }; 139 | 140 | export const increaseColumnTotalItemsCount = (dataSource: BoardData) => { 141 | const columnsIds = dataSource?.root?.children; 142 | columnsIds.forEach((columnId: string) => { 143 | dataSource[columnId].totalChildrenCount = 144 | (dataSource[columnId].totalChildrenCount || 0) + 200; 145 | dataSource[columnId].totalItemsCount = 146 | (dataSource[columnId].totalItemsCount || 0) + 200; 147 | }); 148 | return dataSource; 149 | }; 150 | 151 | export const fetchTasks = () => { 152 | return new Promise((resolve) => { 153 | setTimeout(() => { 154 | resolve(200); 155 | }, 1000); 156 | }); 157 | }; 158 | -------------------------------------------------------------------------------- /src/components/Column/Column.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect, useRef, useState } from "react"; 2 | import { 3 | BoardItem, 4 | BoardProps, 5 | ConfigMap, 6 | DndState, 7 | ScrollEvent, 8 | } from "../types"; 9 | import { withPrefix } from "@/utils/getPrefix"; 10 | import classNames from "classnames"; 11 | import ColumnHeader from "../ColumnHeader"; 12 | import ColumnContent from "../ColumnContent"; 13 | import { useColumnDnd } from "@/global/dnd/useColumnDnd"; 14 | 15 | interface Props { 16 | index: number; 17 | data: BoardItem; 18 | configMap: ConfigMap; 19 | loadMore?: (columnId: string) => void; 20 | onColumnClick?: ( 21 | e: React.MouseEvent, 22 | column: BoardItem 23 | ) => void; 24 | onCardClick?: (e: React.MouseEvent, card: BoardItem) => void; 25 | renderColumnHeader?: (column: BoardItem) => React.ReactNode; 26 | renderColumnFooter?: (column: BoardItem) => React.ReactNode; 27 | renderSkeletonCard?: BoardProps["renderSkeletonCard"]; 28 | renderGap?: (column: BoardItem) => React.ReactNode; 29 | renderColumnWrapper: ( 30 | column: BoardItem, 31 | { 32 | children, 33 | className, 34 | style, 35 | ref, 36 | }: { 37 | children: React.ReactNode; 38 | className?: string; 39 | style?: React.CSSProperties; 40 | ref?: React.RefObject; 41 | } 42 | ) => React.ReactNode; 43 | columnWrapperStyle?: (column: BoardItem) => React.CSSProperties; 44 | columnHeaderStyle?: (column: BoardItem) => React.CSSProperties; 45 | columnStyle?: (column: BoardItem) => React.CSSProperties; 46 | columnClassName?: (column: BoardItem) => string; 47 | onCardDndStateChange?: (info: DndState) => void; 48 | onColumnDndStateChange?: (info: DndState) => void; 49 | columnWrapperClassName?: (column: BoardItem) => string; 50 | columnHeaderClassName?: (column: BoardItem) => string; 51 | columnListContentStyle?: (column: BoardItem) => React.CSSProperties; 52 | renderCardDragIndicator?: (card: BoardItem, info: any) => React.ReactNode; 53 | renderColumnDragIndicator?: (column: BoardItem, info: any) => React.ReactNode; 54 | renderCardDragPreview?: (card: BoardItem, info: any) => React.ReactNode; 55 | renderColumnDragPreview?: (column: BoardItem, info: any) => React.ReactNode; 56 | columnListContentClassName?: (column: BoardItem) => string; 57 | renderListFooter?: (column: BoardItem) => React.ReactNode; 58 | renderColumnAdder?: (column: BoardItem) => React.ReactNode; 59 | items: BoardItem[]; 60 | cardWrapperStyle?: ( 61 | card: BoardItem, 62 | column: BoardItem 63 | ) => React.CSSProperties; 64 | cardWrapperClassName?: string; 65 | onScroll?: (e: ScrollEvent, column: BoardItem) => void; 66 | } 67 | 68 | const Column = (props: Props) => { 69 | const { 70 | index, 71 | data, 72 | items, 73 | onColumnClick, 74 | renderColumnHeader, 75 | renderColumnWrapper, 76 | renderColumnFooter, 77 | columnWrapperStyle, 78 | columnHeaderStyle, 79 | onColumnDndStateChange, 80 | columnWrapperClassName, 81 | columnHeaderClassName, 82 | columnListContentClassName, 83 | columnClassName, 84 | columnStyle, 85 | renderColumnAdder, 86 | ...rest 87 | } = props; 88 | 89 | const { 90 | headerRef, 91 | outerFullHeightRef, 92 | innerRef, 93 | state, 94 | cardOverShadowCount, 95 | } = useColumnDnd(data, index, items, onColumnDndStateChange); 96 | 97 | const containerClassName = classNames( 98 | withPrefix("column-outer"), 99 | columnWrapperClassName?.(data) 100 | ); 101 | 102 | const ColumnWrapper = (children: React.ReactNode) => 103 | renderColumnWrapper ? ( 104 | renderColumnWrapper(data, { 105 | children, 106 | className: containerClassName, 107 | style: columnWrapperStyle?.(data), 108 | ref: outerFullHeightRef, 109 | }) 110 | ) : ( 111 |
116 | {children} 117 |
118 | ); 119 | 120 | return ( 121 |
onColumnClick?.(e, data)}> 122 | {ColumnWrapper( 123 |
128 |
129 | 136 | 146 | {renderColumnFooter?.(data)} 147 |
148 |
149 | )} 150 |
151 | ); 152 | }; 153 | 154 | export default Column; 155 | -------------------------------------------------------------------------------- /src/components/CardSkeleton/_CardSkeleton.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts/variables" as *; 2 | 3 | // Shimmer animation keyframes 4 | @keyframes skeleton-shimmer { 5 | 0% { 6 | background-position: -200px 0; 7 | } 8 | 100% { 9 | background-position: calc(200px + 100%) 0; 10 | } 11 | } 12 | 13 | // Wave animation keyframes 14 | @keyframes skeleton-wave { 15 | 0% { 16 | transform: translateX(-100%); 17 | } 18 | 50% { 19 | transform: translateX(100%); 20 | } 21 | 100% { 22 | transform: translateX(100%); 23 | } 24 | } 25 | 26 | // Pulse animation keyframes 27 | @keyframes skeleton-pulse { 28 | 0%, 29 | 100% { 30 | opacity: 1; 31 | } 32 | 50% { 33 | opacity: 0.4; 34 | } 35 | } 36 | 37 | .#{$prefix}-skeleton { 38 | border-radius: 8px; 39 | border: 1px solid #e8ecf5; 40 | background: #fff; 41 | padding: 16px; 42 | margin-bottom: 8px; 43 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 44 | transition: box-shadow 0.2s ease; 45 | position: relative; 46 | overflow: hidden; 47 | 48 | &:hover { 49 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 50 | } 51 | 52 | &-content { 53 | display: flex; 54 | flex-direction: column; 55 | gap: 12px; 56 | } 57 | 58 | // Base skeleton element styles 59 | %skeleton-base { 60 | background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); 61 | background-size: 200px 100%; 62 | animation: skeleton-shimmer 1.5s infinite ease-in-out; 63 | border-radius: 4px; 64 | position: relative; 65 | } 66 | 67 | // Wave animation base 68 | %skeleton-wave-base { 69 | background: #f0f0f0; 70 | position: relative; 71 | overflow: hidden; 72 | border-radius: 4px; 73 | 74 | &::after { 75 | position: absolute; 76 | top: 0; 77 | right: 0; 78 | bottom: 0; 79 | left: 0; 80 | transform: translateX(-100%); 81 | background: linear-gradient( 82 | 90deg, 83 | rgba(255, 255, 255, 0) 0, 84 | rgba(255, 255, 255, 0.2) 20%, 85 | rgba(255, 255, 255, 0.5) 60%, 86 | rgba(255, 255, 255, 0) 87 | ); 88 | animation: skeleton-wave 2s infinite; 89 | content: ""; 90 | } 91 | } 92 | 93 | // Title skeleton 94 | &-title { 95 | @extend %skeleton-base; 96 | height: 20px; 97 | width: 80%; 98 | border-radius: 6px; 99 | } 100 | 101 | // Description section 102 | &-description { 103 | display: flex; 104 | flex-direction: column; 105 | gap: 8px; 106 | } 107 | 108 | &-line { 109 | @extend %skeleton-base; 110 | height: 14px; 111 | width: 100%; 112 | 113 | &-short { 114 | width: 65%; 115 | } 116 | } 117 | 118 | // Priority indicator 119 | &-priority { 120 | @extend %skeleton-base; 121 | height: 4px; 122 | width: 40px; 123 | border-radius: 2px; 124 | margin-bottom: 4px; 125 | } 126 | 127 | // Tags section 128 | &-tags { 129 | display: flex; 130 | gap: 8px; 131 | flex-wrap: wrap; 132 | } 133 | 134 | &-tag { 135 | @extend %skeleton-base; 136 | height: 20px; 137 | width: 60px; 138 | border-radius: 12px; 139 | 140 | &:nth-child(2) { 141 | width: 45px; 142 | } 143 | 144 | &:nth-child(3) { 145 | width: 55px; 146 | } 147 | } 148 | 149 | // Footer section 150 | &-footer { 151 | display: flex; 152 | justify-content: space-between; 153 | align-items: center; 154 | margin-top: 4px; 155 | 156 | &-left { 157 | display: flex; 158 | align-items: center; 159 | gap: 8px; 160 | } 161 | } 162 | 163 | &-avatar { 164 | @extend %skeleton-base; 165 | width: 24px; 166 | height: 24px; 167 | border-radius: 50%; 168 | flex-shrink: 0; 169 | } 170 | 171 | &-assignee { 172 | @extend %skeleton-base; 173 | height: 12px; 174 | width: 60px; 175 | border-radius: 3px; 176 | } 177 | 178 | &-date { 179 | @extend %skeleton-base; 180 | height: 12px; 181 | width: 80px; 182 | border-radius: 3px; 183 | } 184 | } 185 | 186 | // Wave animation variant 187 | .#{$prefix}-skeleton-wave { 188 | .#{$prefix}-skeleton-title, 189 | .#{$prefix}-skeleton-line, 190 | .#{$prefix}-skeleton-tag, 191 | .#{$prefix}-skeleton-avatar, 192 | .#{$prefix}-skeleton-assignee, 193 | .#{$prefix}-skeleton-date, 194 | .#{$prefix}-skeleton-priority { 195 | @extend %skeleton-wave-base; 196 | animation: none; // Remove shimmer animation 197 | } 198 | } 199 | 200 | // Pulse animation variant 201 | .#{$prefix}-skeleton-pulse { 202 | .#{$prefix}-skeleton-title, 203 | .#{$prefix}-skeleton-line, 204 | .#{$prefix}-skeleton-tag, 205 | .#{$prefix}-skeleton-avatar, 206 | .#{$prefix}-skeleton-assignee, 207 | .#{$prefix}-skeleton-date, 208 | .#{$prefix}-skeleton-priority { 209 | background: #f0f0f0; 210 | animation: skeleton-pulse 1.5s ease-in-out infinite; 211 | } 212 | } 213 | 214 | // Responsive design for smaller screens 215 | @media (max-width: 768px) { 216 | .#{$prefix}-skeleton { 217 | padding: 12px; 218 | 219 | &-content { 220 | gap: 10px; 221 | } 222 | 223 | &-title { 224 | height: 18px; 225 | } 226 | 227 | &-line { 228 | height: 12px; 229 | } 230 | 231 | &-tag { 232 | height: 18px; 233 | width: 50px; 234 | 235 | &:nth-child(2) { 236 | width: 40px; 237 | } 238 | } 239 | 240 | &-avatar { 241 | width: 20px; 242 | height: 20px; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /rkk-demo/src/global/assets/styles/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | // Demo app prefix 2 | $demo-prefix: "rkk-demo"; 3 | 4 | // Layout variables with professional sizing 5 | :root { 6 | // Header with professional sizing 7 | --header-height: 64px; 8 | --header-bg: rgba(255, 255, 255, 0.95); 9 | --header-border: rgba(59, 130, 246, 0.08); 10 | --header-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); 11 | --header-backdrop: blur(16px); 12 | 13 | // Sidebar with professional proportions 14 | --sidebar-width: 280px; 15 | --sidebar-collapsed-width: 72px; 16 | --sidebar-bg: linear-gradient(145deg, #fafbff 0%, #f5f7ff 100%); 17 | --sidebar-border: rgba(59, 130, 246, 0.08); 18 | 19 | // Professional color palette 20 | --primary-50: #eff6ff; 21 | --primary-100: #dbeafe; 22 | --primary-200: #bfdbfe; 23 | --primary-300: #93c5fd; 24 | --primary-400: #60a5fa; 25 | --primary-500: #3b82f6; 26 | --primary-600: #2563eb; 27 | --primary-700: #1d4ed8; 28 | --primary-800: #1e40af; 29 | --primary-900: #1e3a8a; 30 | --primary-950: #172554; 31 | 32 | // Clean gray scale 33 | --gray-25: #fcfcfd; 34 | --gray-50: #f9fafb; 35 | --gray-100: #f3f4f6; 36 | --gray-200: #e5e7eb; 37 | --gray-300: #d1d5db; 38 | --gray-400: #9ca3af; 39 | --gray-500: #6b7280; 40 | --gray-600: #4b5563; 41 | --gray-700: #374151; 42 | --gray-800: #1f2937; 43 | --gray-900: #111827; 44 | --gray-950: #030712; 45 | 46 | // Professional accent colors 47 | --accent-success: #10b981; 48 | --accent-warning: #f59e0b; 49 | --accent-danger: #ef4444; 50 | --accent-info: #06b6d4; 51 | 52 | // Professional typography 53 | --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 54 | "Helvetica Neue", Arial, sans-serif; 55 | --font-mono: "JetBrains Mono", "SF Mono", Monaco, Consolas, monospace; 56 | --font-display: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; 57 | 58 | // Clean spacing scale 59 | --space-px: 1px; 60 | --space-0-5: 0.125rem; 61 | --space-1: 0.25rem; 62 | --space-1-5: 0.375rem; 63 | --space-2: 0.5rem; 64 | --space-2-5: 0.625rem; 65 | --space-3: 0.75rem; 66 | --space-3-5: 0.875rem; 67 | --space-4: 1rem; 68 | --space-5: 1.25rem; 69 | --space-6: 1.5rem; 70 | --space-7: 1.75rem; 71 | --space-8: 2rem; 72 | --space-10: 2.5rem; 73 | --space-12: 3rem; 74 | --space-16: 4rem; 75 | --space-20: 5rem; 76 | --space-24: 6rem; 77 | 78 | // Professional border radius 79 | --radius-none: 0px; 80 | --radius-sm: 0.125rem; 81 | --radius-base: 0.25rem; 82 | --radius-md: 0.375rem; 83 | --radius-lg: 0.5rem; 84 | --radius-xl: 0.75rem; 85 | --radius-2xl: 1rem; 86 | --radius-3xl: 1.5rem; 87 | --radius-full: 9999px; 88 | 89 | // Clean shadow system 90 | --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 91 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 92 | --shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 93 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 94 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 95 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 96 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 97 | --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 98 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 99 | --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25); 100 | --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); 101 | 102 | // Professional colored shadows 103 | --shadow-primary: 0 4px 6px -1px rgba(59, 130, 246, 0.1), 104 | 0 2px 4px -1px rgba(59, 130, 246, 0.06); 105 | --shadow-success: 0 4px 6px -1px rgba(16, 185, 129, 0.1), 106 | 0 2px 4px -1px rgba(16, 185, 129, 0.06); 107 | --shadow-warning: 0 4px 6px -1px rgba(245, 158, 11, 0.1), 108 | 0 2px 4px -1px rgba(245, 158, 11, 0.06); 109 | --shadow-danger: 0 4px 6px -1px rgba(239, 68, 68, 0.1), 110 | 0 2px 4px -1px rgba(239, 68, 68, 0.06); 111 | 112 | // Subtle transitions 113 | --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 114 | --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1); 115 | --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); 116 | 117 | // Professional easing 118 | --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); 119 | --ease-out: cubic-bezier(0, 0, 0.2, 1); 120 | --ease-in: cubic-bezier(0.4, 0, 1, 1); 121 | 122 | // Z-index scale 123 | --z-hide: -1; 124 | --z-auto: auto; 125 | --z-base: 0; 126 | --z-docked: 10; 127 | --z-dropdown: 1000; 128 | --z-sticky: 1020; 129 | --z-banner: 1030; 130 | --z-overlay: 1040; 131 | --z-modal: 1050; 132 | --z-popover: 1060; 133 | --z-tooltip: 1090; 134 | 135 | // Professional gradients 136 | --gradient-primary: linear-gradient( 137 | 135deg, 138 | var(--primary-500) 0%, 139 | var(--primary-600) 100% 140 | ); 141 | --gradient-secondary: linear-gradient( 142 | 135deg, 143 | var(--gray-600) 0%, 144 | var(--gray-700) 100% 145 | ); 146 | --gradient-success: linear-gradient( 147 | 135deg, 148 | var(--accent-success) 0%, 149 | #059669 100% 150 | ); 151 | --gradient-warning: linear-gradient( 152 | 135deg, 153 | var(--accent-warning) 0%, 154 | #d97706 100% 155 | ); 156 | --gradient-danger: linear-gradient( 157 | 135deg, 158 | var(--accent-danger) 0%, 159 | #dc2626 100% 160 | ); 161 | 162 | // Subtle background patterns 163 | --bg-pattern-dots: radial-gradient( 164 | circle at 1px 1px, 165 | rgba(59, 130, 246, 0.03) 1px, 166 | transparent 0 167 | ); 168 | --bg-pattern-grid: linear-gradient( 169 | rgba(59, 130, 246, 0.02) 1px, 170 | transparent 1px 171 | ), 172 | linear-gradient(90deg, rgba(59, 130, 246, 0.02) 1px, transparent 1px); 173 | 174 | // Professional spacing tokens 175 | --content-max-width: 1200px; 176 | --sidebar-max-width: 280px; 177 | --header-max-height: 64px; 178 | --card-min-height: 100px; 179 | --button-min-height: 40px; 180 | --input-min-height: 40px; 181 | 182 | // Professional typography scale 183 | --text-xs: 0.75rem; 184 | --text-sm: 0.875rem; 185 | --text-base: 1rem; 186 | --text-lg: 1.125rem; 187 | --text-xl: 1.25rem; 188 | --text-2xl: 1.5rem; 189 | --text-3xl: 1.875rem; 190 | --text-4xl: 2.25rem; 191 | 192 | // Line height scale 193 | --leading-tight: 1.25; 194 | --leading-snug: 1.375; 195 | --leading-normal: 1.5; 196 | --leading-relaxed: 1.625; 197 | 198 | // Letter spacing scale 199 | --tracking-tight: -0.025em; 200 | --tracking-normal: 0em; 201 | --tracking-wide: 0.025em; 202 | } 203 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Sidebar/_Sidebar.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts" as *; 2 | 3 | .#{$demo-prefix}-sidebar { 4 | width: var(--sidebar-width); 5 | background: var(--sidebar-bg); 6 | border-right: 1px solid var(--sidebar-border); 7 | height: calc(100vh - var(--header-height)); 8 | overflow-y: auto; 9 | @include custom-scrollbar( 10 | 6px, 11 | rgba(148, 163, 184, 0.1), 12 | rgba(148, 163, 184, 0.3) 13 | ); 14 | position: relative; 15 | 16 | // Subtle pattern overlay 17 | &::before { 18 | content: ""; 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | background-image: var(--bg-pattern-dots); 25 | background-size: 16px 16px; 26 | opacity: 0.2; 27 | pointer-events: none; 28 | } 29 | 30 | @include media-down(lg) { 31 | width: var(--sidebar-collapsed-width); 32 | } 33 | 34 | @include media-down(md) { 35 | position: fixed; 36 | left: -100%; 37 | top: var(--header-height); 38 | width: var(--sidebar-width); 39 | z-index: var(--z-overlay); 40 | transition: left var(--transition-base); 41 | box-shadow: var(--shadow-xl); 42 | backdrop-filter: blur(16px); 43 | 44 | &.open { 45 | left: 0; 46 | } 47 | } 48 | 49 | &-content { 50 | padding: var(--space-6) 0; 51 | position: relative; 52 | z-index: 1; 53 | 54 | @include media-down(lg) { 55 | padding: var(--space-4) 0; 56 | } 57 | } 58 | 59 | &-section { 60 | padding: 0 var(--space-6); 61 | 62 | @include media-down(lg) { 63 | padding: 0 var(--space-3); 64 | } 65 | 66 | &:not(:last-child) { 67 | margin-bottom: var(--space-8); 68 | padding-bottom: var(--space-6); 69 | position: relative; 70 | 71 | // Clean divider 72 | &::after { 73 | content: ""; 74 | position: absolute; 75 | bottom: 0; 76 | left: var(--space-6); 77 | right: var(--space-6); 78 | height: 1px; 79 | background: linear-gradient( 80 | 90deg, 81 | transparent, 82 | var(--gray-200), 83 | transparent 84 | ); 85 | 86 | @include media-down(lg) { 87 | left: var(--space-3); 88 | right: var(--space-3); 89 | } 90 | } 91 | } 92 | } 93 | 94 | &-title { 95 | font-size: 0.625rem; 96 | font-weight: 700; 97 | text-transform: uppercase; 98 | letter-spacing: 0.1em; 99 | color: var(--gray-500); 100 | margin-bottom: var(--space-4); 101 | position: relative; 102 | @include flex-start; 103 | gap: var(--space-2); 104 | 105 | @include media-down(lg) { 106 | display: none; 107 | } 108 | 109 | // Simple accent line 110 | &::before { 111 | content: ""; 112 | width: 12px; 113 | height: 2px; 114 | background: var(--gradient-primary); 115 | border-radius: var(--radius-full); 116 | flex-shrink: 0; 117 | } 118 | } 119 | 120 | &-nav { 121 | display: flex; 122 | flex-direction: column; 123 | gap: var(--space-2); 124 | 125 | &-item { 126 | @include flex-start; 127 | gap: var(--space-3); 128 | padding: var(--space-3) var(--space-4); 129 | border-radius: var(--radius-lg); 130 | color: var(--gray-600); 131 | text-decoration: none; 132 | font-size: var(--text-sm); 133 | font-weight: 500; 134 | transition: all var(--transition-base); 135 | position: relative; 136 | background: rgba(255, 255, 255, 0.6); 137 | border: 1px solid rgba(255, 255, 255, 0.8); 138 | backdrop-filter: blur(8px); 139 | 140 | &:hover { 141 | color: var(--gray-900); 142 | transform: translateY(-1px); 143 | box-shadow: var(--shadow-md); 144 | border-color: rgba(59, 130, 246, 0.15); 145 | background: rgba(255, 255, 255, 0.9); 146 | 147 | // Icon animation 148 | svg { 149 | transform: scale(1.05); 150 | color: var(--primary-600); 151 | } 152 | } 153 | 154 | &:active { 155 | transform: translateY(0); 156 | box-shadow: var(--shadow-sm); 157 | } 158 | 159 | // Icon styling 160 | svg { 161 | transition: all var(--transition-base); 162 | flex-shrink: 0; 163 | } 164 | 165 | @include media-down(lg) { 166 | justify-content: center; 167 | padding: var(--space-3); 168 | 169 | span { 170 | display: none; 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | // Clean breadcrumb 178 | .#{$demo-prefix}-breadcrumb { 179 | @include flex-start; 180 | gap: var(--space-2); 181 | padding: var(--space-3) var(--space-6); 182 | font-size: 0.75rem; 183 | color: var(--gray-500); 184 | background: rgba(59, 130, 246, 0.02); 185 | border-bottom: 1px solid rgba(59, 130, 246, 0.08); 186 | 187 | @include media-down(lg) { 188 | display: none; 189 | } 190 | 191 | &-item { 192 | &:not(:last-child)::after { 193 | content: "/"; 194 | margin-left: var(--space-2); 195 | color: var(--gray-300); 196 | } 197 | } 198 | 199 | &-current { 200 | color: var(--primary-600); 201 | font-weight: 500; 202 | } 203 | } 204 | 205 | // Mobile sidebar toggle 206 | .#{$demo-prefix}-sidebar-toggle { 207 | @include button-primary; 208 | position: fixed; 209 | bottom: var(--space-6); 210 | right: var(--space-6); 211 | width: 48px; 212 | height: 48px; 213 | border-radius: 50%; 214 | z-index: var(--z-modal); 215 | display: none; 216 | padding: 0; 217 | box-shadow: var(--shadow-xl), var(--shadow-primary); 218 | 219 | @include media-down(md) { 220 | display: flex; 221 | } 222 | 223 | svg { 224 | transition: transform var(--transition-base); 225 | } 226 | 227 | &.active svg { 228 | transform: rotate(180deg); 229 | } 230 | } 231 | 232 | // Active indicator for current page 233 | .#{$demo-prefix}-progress-indicator { 234 | position: absolute; 235 | left: 0; 236 | top: var(--space-1); 237 | bottom: var(--space-1); 238 | width: 3px; 239 | background: var(--gradient-primary); 240 | border-radius: 0 var(--radius-full) var(--radius-full) 0; 241 | opacity: 0; 242 | transform: scaleY(0); 243 | transform-origin: top; 244 | transition: all var(--transition-base); 245 | 246 | .active & { 247 | opacity: 1; 248 | transform: scaleY(1); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import { withPrefix } from "@/utils/getPrefix"; 2 | import React, { Fragment, memo, useMemo } from "react"; 3 | import { BoardItem, DndState } from "../types"; 4 | import { createPortal } from "react-dom"; 5 | import { TaskCardState, useCardDnd } from "@/global/dnd/useCardDnd"; 6 | 7 | export const CardShadow = memo( 8 | ({ 9 | height, 10 | customIndicator, 11 | }: { 12 | height: number; 13 | customIndicator?: React.ReactNode; 14 | }) => { 15 | return ( 16 |
17 | {customIndicator || ( 18 |
22 | )} 23 |
24 | ); 25 | } 26 | ); 27 | 28 | const CardDisplay = (props: { 29 | outerRef?: React.RefObject; 30 | innerRef?: React.RefObject; 31 | state: TaskCardState; 32 | data: BoardItem; 33 | column: BoardItem; 34 | index: number; 35 | isDraggable: boolean; 36 | render: (props: { 37 | data: BoardItem; 38 | column: BoardItem; 39 | index: number; 40 | isDraggable: boolean; 41 | }) => React.ReactNode; 42 | onClick?: (e: React.MouseEvent, card: BoardItem) => void; 43 | cardsGap?: number; 44 | renderCardDragIndicator?: (card: BoardItem, info: any) => React.ReactNode; 45 | renderGap?: (column: BoardItem) => React.ReactNode; 46 | }) => { 47 | const { 48 | outerRef, 49 | innerRef, 50 | state, 51 | data, 52 | column, 53 | index, 54 | isDraggable, 55 | cardsGap, 56 | render, 57 | onClick, 58 | renderCardDragIndicator, 59 | renderGap, 60 | } = props; 61 | 62 | const containerStyle = useMemo(() => { 63 | const styles: React.CSSProperties = {}; 64 | if (state.type === "is-dragging-and-left-self") { 65 | styles.display = "none"; 66 | } 67 | return styles; 68 | }, [state.type]); 69 | 70 | const innerStyle = useMemo(() => { 71 | if (state.type === "is-dragging") { 72 | return { opacity: 0.6 }; 73 | } 74 | if (state.type === "preview") { 75 | return { 76 | width: state.dragging.width, 77 | height: state.dragging.height, 78 | transform: "rotate(4deg)", 79 | }; 80 | } 81 | return {}; 82 | }, [state]); 83 | 84 | const showTopShadow = state.type === "is-over" && state.closestEdge === "top"; 85 | const showBottomShadow = 86 | state.type === "is-over" && state.closestEdge === "bottom"; 87 | const shadowHeight = state.type === "is-over" ? state.dragging.height : 0; 88 | const renderContent = render({ data, column, index, isDraggable }); 89 | const customIndicator = renderCardDragIndicator?.( 90 | state.type === "is-dragging" ? data : null, 91 | { 92 | height: shadowHeight, 93 | } 94 | ); 95 | 96 | return ( 97 | 98 |
onClick?.(e, data)} 102 | style={{ 103 | ...containerStyle, 104 | ...(cardsGap !== undefined ? { marginBottom: cardsGap } : {}), 105 | }} 106 | data-test-id={data?.id} 107 | data-rkk-column={column?.id} 108 | data-rkk-index={index} 109 | > 110 | {showTopShadow && ( 111 | 112 | )} 113 |
122 | {renderContent} 123 |
124 | {showBottomShadow && ( 125 | 126 | )} 127 |
128 | {/* {renderGap?.(column)} */} 129 |
130 | ); 131 | }; 132 | 133 | interface Props { 134 | render: (props: { 135 | data: BoardItem; 136 | column: BoardItem; 137 | index: number; 138 | isDraggable: boolean; 139 | }) => React.ReactNode; 140 | data: BoardItem; 141 | column: BoardItem; 142 | index: number; 143 | isDraggable: boolean; 144 | onClick?: (e: React.MouseEvent, card: BoardItem) => void; 145 | cardsGap?: number; 146 | renderGap?: (column: BoardItem) => React.ReactNode; 147 | onCardDndStateChange?: (info: DndState) => void; 148 | renderCardDragIndicator?: (card: BoardItem, info: any) => React.ReactNode; 149 | renderCardDragPreview?: (card: BoardItem, info: any) => React.ReactNode; 150 | } 151 | 152 | const Card = (props: Props) => { 153 | const { 154 | render, 155 | data, 156 | column, 157 | index, 158 | isDraggable, 159 | cardsGap, 160 | onClick, 161 | onCardDndStateChange, 162 | renderCardDragIndicator, 163 | renderCardDragPreview, 164 | renderGap, 165 | } = props; 166 | const { outerRef, innerRef, state } = useCardDnd( 167 | data, 168 | column, 169 | index, 170 | isDraggable, 171 | onCardDndStateChange 172 | ); 173 | 174 | return ( 175 | <> 176 | 190 | 191 | {state.type === "preview" 192 | ? createPortal( 193 | renderCardDragPreview?.(data, { 194 | state, 195 | data, 196 | column, 197 | index, 198 | isDraggable, 199 | }) || ( 200 | 208 | ), 209 | state.container 210 | ) 211 | : null} 212 | 213 | ); 214 | }; 215 | 216 | export default Card; 217 | -------------------------------------------------------------------------------- /src/global/dnd/useCardDnd.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from "react"; 2 | import { BoardItem, DndState } from "@/components/types"; 3 | import { 4 | draggable, 5 | dropTargetForElements, 6 | } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 7 | import { 8 | attachClosestEdge, 9 | extractClosestEdge, 10 | Edge, 11 | } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; 12 | import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; 13 | import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; 14 | import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source"; 15 | import { useKanbanContext } from "@/context/KanbanContext"; 16 | 17 | export type TaskCardState = 18 | | { 19 | type: "idle"; 20 | } 21 | | { 22 | type: "is-dragging"; 23 | } 24 | | { 25 | type: "is-dragging-and-left-self"; 26 | } 27 | | { 28 | type: "is-over"; 29 | dragging: DOMRect; 30 | closestEdge: Edge; 31 | } 32 | | { 33 | type: "preview"; 34 | container: HTMLElement; 35 | dragging: DOMRect; 36 | }; 37 | 38 | const idle: TaskCardState = { type: "idle" }; 39 | 40 | // Custom hook to handle all drag and drop logic 41 | export const useCardDnd = ( 42 | data: BoardItem = {} as BoardItem, 43 | column: BoardItem = {} as BoardItem, 44 | index: number, 45 | isDraggable: boolean, 46 | onCardDndStateChange?: (info: DndState) => void 47 | ) => { 48 | const { viewOnly } = useKanbanContext(); 49 | const outerRef = useRef(null); 50 | const innerRef = useRef(null); 51 | const [state, setState] = useState(idle); 52 | 53 | // Memoize initial data to prevent recreating on each render 54 | const getInitialData = useCallback( 55 | () => ({ 56 | type: "card", 57 | itemId: data?.id, 58 | columnId: column?.id, 59 | index, 60 | isDraggable, 61 | parentId: data.parentId, 62 | rect: innerRef.current?.getBoundingClientRect() || null, 63 | }), 64 | [data?.id, column?.id, index, isDraggable, data?.parentId] 65 | ); 66 | 67 | const getDropTargetData = useCallback( 68 | ({ input, element }) => { 69 | const cardData = { 70 | type: "card", 71 | "card-drop-target": true, 72 | itemId: data.id, 73 | columnId: column.id, 74 | index, 75 | isDraggable, 76 | parentId: data.parentId, 77 | }; 78 | 79 | return attachClosestEdge(cardData, { 80 | input, 81 | element, 82 | allowedEdges: ["top", "bottom"], 83 | }); 84 | }, 85 | [data?.id, column?.id, index, isDraggable, data?.parentId] 86 | ); 87 | 88 | // Optimize the drop check to avoid recalculating on every drag move 89 | const canDrop = useCallback( 90 | (args) => { 91 | const sourceData = args.source.data; 92 | if (sourceData.itemId === data.parentId) return false; 93 | return sourceData.isDraggable; 94 | }, 95 | [data?.id, data?.parentId] 96 | ); 97 | 98 | // Drag and drop event handlers 99 | const handleGenerateDragPreview = useCallback( 100 | ({ nativeSetDragImage, location }) => { 101 | setCustomNativeDragPreview({ 102 | nativeSetDragImage, 103 | getOffset: preserveOffsetOnSource({ 104 | element: innerRef.current!, 105 | input: location.current.input, 106 | }), 107 | render({ container }) { 108 | const rect = innerRef.current!.getBoundingClientRect(); 109 | setState({ 110 | type: "preview", 111 | container, 112 | dragging: rect, 113 | }); 114 | }, 115 | }); 116 | }, 117 | [] 118 | ); 119 | 120 | const handleDragStart = useCallback(() => { 121 | setState({ type: "is-dragging" }); 122 | }, []); 123 | 124 | const handleDrop = useCallback(() => { 125 | setState(idle); 126 | }, []); 127 | 128 | const handleDragEnter = useCallback( 129 | ({ source, self }) => { 130 | if (source.data.type !== "card") return; 131 | if (source.data.itemId === data.id) return; 132 | 133 | const closestEdge = extractClosestEdge(self.data); 134 | if (!closestEdge) return; 135 | 136 | setState({ 137 | type: "is-over", 138 | dragging: source.data.rect as DOMRect, 139 | closestEdge, 140 | }); 141 | }, 142 | [data?.id] 143 | ); 144 | 145 | const handleDrag = useCallback( 146 | ({ source, self }) => { 147 | if (source.data.type !== "card") return; 148 | if (source.data.itemId === data.id) return; 149 | 150 | const closestEdge = extractClosestEdge(self.data); 151 | if (!closestEdge) return; 152 | 153 | setState({ 154 | type: "is-over", 155 | dragging: source.data.rect as DOMRect, 156 | closestEdge, 157 | }); 158 | }, 159 | [data?.id] 160 | ); 161 | 162 | const handleDragLeave = useCallback( 163 | ({ source }) => { 164 | if (source.data.type !== "card") return; 165 | 166 | if (source.data.itemId === data?.id) { 167 | setState({ type: "is-dragging-and-left-self" }); 168 | return; 169 | } 170 | 171 | setState(idle); 172 | }, 173 | [data.id] 174 | ); 175 | 176 | // Setup drag and drop effects 177 | useEffect(() => { 178 | const outer = outerRef.current; 179 | const inner = innerRef.current; 180 | 181 | if (!outer || !inner) return; 182 | 183 | return combine( 184 | draggable({ 185 | element: inner, 186 | getInitialData, 187 | onGenerateDragPreview: handleGenerateDragPreview, 188 | onDragStart: handleDragStart, 189 | onDrop: handleDrop, 190 | canDrag: () => isDraggable && !viewOnly, 191 | }), 192 | dropTargetForElements({ 193 | element: outer, 194 | canDrop, 195 | getIsSticky: () => true, 196 | getData: getDropTargetData, 197 | onDragEnter: handleDragEnter, 198 | onDrag: handleDrag, 199 | onDragLeave: handleDragLeave, 200 | onDrop: handleDrop, 201 | }) 202 | ); 203 | }, [ 204 | getInitialData, 205 | handleGenerateDragPreview, 206 | handleDragStart, 207 | handleDrop, 208 | isDraggable, 209 | canDrop, 210 | getDropTargetData, 211 | handleDragEnter, 212 | handleDrag, 213 | handleDragLeave, 214 | ]); 215 | 216 | useEffect(() => { 217 | onCardDndStateChange?.({ state, card: data, column }); 218 | }, [state, onCardDndStateChange]); 219 | 220 | return { 221 | outerRef, 222 | innerRef, 223 | state, 224 | }; 225 | }; 226 | -------------------------------------------------------------------------------- /src/global/dnd/useColumnDnd.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from "react"; 2 | import { BoardItem, DndState } from "@/components/types"; 3 | import { withPrefix } from "@/utils/getPrefix"; 4 | import { 5 | draggable, 6 | dropTargetForElements, 7 | } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 8 | import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; 9 | import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source"; 10 | import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; 11 | import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; 12 | import { useKanbanContext } from "@/context/KanbanContext"; 13 | 14 | export type TColumnState = 15 | | { 16 | type: "is-card-over"; 17 | isOverChildCard: boolean; 18 | dragging: DOMRect; 19 | } 20 | | { 21 | type: "is-column-over"; 22 | } 23 | | { 24 | type: "idle"; 25 | } 26 | | { 27 | type: "is-dragging"; 28 | }; 29 | 30 | const isCardData = (data: any) => { 31 | return data.type === "card"; 32 | }; 33 | 34 | const isColumnData = (data: any) => { 35 | return data.type === "column"; 36 | }; 37 | 38 | const idle = { type: "idle" } as TColumnState; 39 | 40 | export const useColumnDnd = ( 41 | data: BoardItem, 42 | index: number, 43 | items: BoardItem[], 44 | onColumnDndStateChange?: (info: DndState) => void 45 | ) => { 46 | const { viewOnly } = useKanbanContext(); 47 | const headerRef = useRef(null); 48 | const outerFullHeightRef = useRef(null); 49 | const innerRef = useRef(null); 50 | const [state, setState] = useState(idle); 51 | 52 | const cardOverShadowCount = 53 | state.type === "is-card-over" && !state.isOverChildCard ? 1 : 0; 54 | const totalTasksCount = data.totalChildrenCount + cardOverShadowCount; 55 | 56 | const setIsCardOver = useCallback( 57 | ({ data, location }: { data: any; location: any }) => { 58 | const innerMost = location.current.dropTargets[0]; 59 | const isOverChildCard = Boolean(innerMost?.data["card-drop-target"]); 60 | 61 | const proposed: TColumnState = { 62 | type: "is-card-over", 63 | dragging: data.rect, 64 | isOverChildCard, 65 | }; 66 | 67 | setState(proposed); 68 | }, 69 | [] 70 | ); 71 | 72 | const handleGenerateDragPreview = useCallback( 73 | ({ location, nativeSetDragImage }) => { 74 | setCustomNativeDragPreview({ 75 | nativeSetDragImage, 76 | getOffset: preserveOffsetOnSource({ 77 | element: headerRef.current!, 78 | input: location.current.input, 79 | }), 80 | render({ container }) { 81 | const rect = innerRef.current!.getBoundingClientRect(); 82 | const preview = innerRef.current!.cloneNode(true) as HTMLElement; 83 | if (!preview) return; 84 | 85 | preview.style.width = `${rect.width}px`; 86 | preview.style.height = `${rect.height}px`; 87 | preview.style.transform = "rotate(4deg)"; 88 | 89 | container.appendChild(preview); 90 | }, 91 | }); 92 | }, 93 | [] 94 | ); 95 | 96 | const handleDragStart = useCallback(() => { 97 | setState({ type: "is-dragging" }); 98 | }, []); 99 | 100 | const handleDrop = useCallback(() => { 101 | setState(idle); 102 | }, []); 103 | 104 | const handleDragEnter = useCallback( 105 | ({ source, location }) => { 106 | if (isCardData(source.data)) { 107 | setIsCardOver({ data: source.data, location }); 108 | return; 109 | } 110 | if (isColumnData(source.data) && source.data.columnId !== data.id) { 111 | setState({ type: "is-column-over" }); 112 | } 113 | }, 114 | [data.id, setIsCardOver] 115 | ); 116 | 117 | const handleDropTargetChange = useCallback( 118 | ({ source, location }) => { 119 | if (isCardData(source.data)) { 120 | setIsCardOver({ data: source.data, location }); 121 | return; 122 | } 123 | }, 124 | [setIsCardOver] 125 | ); 126 | 127 | const handleDragLeave = useCallback( 128 | ({ source }) => { 129 | if (isColumnData(source.data) && source.data.columnId === data.id) { 130 | return; 131 | } 132 | setState(idle); 133 | }, 134 | [data.id] 135 | ); 136 | 137 | const canDrop = useCallback(({ source }) => { 138 | return source.data.type === "card" || source.data.type === "column"; 139 | }, []); 140 | 141 | const canScroll = useCallback(({ source }) => { 142 | return source.data.type === "card"; 143 | }, []); 144 | 145 | const getConfiguration = useCallback(() => { 146 | return { 147 | maxScrollSpeed: "standard" as const, 148 | }; 149 | }, []); 150 | 151 | useEffect(() => { 152 | if ( 153 | !outerFullHeightRef.current || 154 | !innerRef.current || 155 | !headerRef.current 156 | ) { 157 | console.warn("not ready"); 158 | return; 159 | } 160 | 161 | const scroller = outerFullHeightRef.current.querySelector( 162 | `.${withPrefix("column-content-list")}` 163 | ); 164 | 165 | const columnData = { 166 | type: "column", 167 | columnId: data.id, 168 | column: data, 169 | index, 170 | }; 171 | 172 | return combine( 173 | draggable({ 174 | element: headerRef.current, 175 | getInitialData: () => columnData, 176 | onGenerateDragPreview: handleGenerateDragPreview, 177 | onDragStart: handleDragStart, 178 | onDrop: handleDrop, 179 | //TODO: add dnd in columns 180 | canDrag: () => false, 181 | }), 182 | dropTargetForElements({ 183 | element: outerFullHeightRef.current, 184 | getData: () => columnData, 185 | canDrop, 186 | getIsSticky: () => true, 187 | onDragStart: ({ source, location }) => { 188 | if (isCardData(source.data)) { 189 | setIsCardOver({ data: source.data, location }); 190 | } 191 | }, 192 | onDragEnter: handleDragEnter, 193 | onDropTargetChange: handleDropTargetChange, 194 | onDragLeave: handleDragLeave, 195 | onDrop: handleDrop, 196 | }), 197 | autoScrollForElements({ 198 | canScroll, 199 | getConfiguration, 200 | element: scroller, 201 | }) 202 | ); 203 | }, [ 204 | data, 205 | index, 206 | items?.length, 207 | handleGenerateDragPreview, 208 | handleDragStart, 209 | handleDrop, 210 | canDrop, 211 | setIsCardOver, 212 | handleDragEnter, 213 | handleDropTargetChange, 214 | handleDragLeave, 215 | canScroll, 216 | getConfiguration, 217 | ]); 218 | 219 | useEffect(() => { 220 | onColumnDndStateChange?.({ state, column: data }); 221 | }, [state, onColumnDndStateChange]); 222 | 223 | return { 224 | headerRef, 225 | outerFullHeightRef, 226 | innerRef, 227 | state, 228 | cardOverShadowCount, 229 | totalTasksCount, 230 | }; 231 | }; 232 | -------------------------------------------------------------------------------- /src/components/ColumnContent/ColumnContent.tsx: -------------------------------------------------------------------------------- 1 | import { withPrefix } from "@/utils/getPrefix"; 2 | import React, { forwardRef, useEffect } from "react"; 3 | import { 4 | BoardItem, 5 | BoardProps, 6 | ConfigMap, 7 | DndState, 8 | ScrollEvent, 9 | } from "../types"; 10 | import classNames from "classnames"; 11 | import { VList } from "virtua"; 12 | import GenericItem from "../GenericItem"; 13 | import { handleScroll } from "@/utils/scroll"; 14 | import { checkIfSkeletonIsVisible } from "@/utils/infinite-scroll"; 15 | import { useKanbanContext } from "@/context/KanbanContext"; 16 | 17 | interface ListProps { 18 | column: BoardItem; 19 | items: BoardItem[]; 20 | configMap: ConfigMap; 21 | cardWrapperStyle?: ( 22 | card: BoardItem, 23 | column: BoardItem 24 | ) => React.CSSProperties; 25 | cardWrapperClassName?: string; 26 | cardsGap?: number; 27 | cardOverHeight?: number; 28 | cardOverShadowCount?: number; 29 | renderCardDragIndicator?: (card: BoardItem, info: any) => React.ReactNode; 30 | renderCardDragPreview?: (card: BoardItem, info: any) => React.ReactNode; 31 | onCardDndStateChange?: (info: DndState) => void; 32 | renderSkeletonCard?: BoardProps["renderSkeletonCard"]; 33 | onScroll?: (e: React.UIEvent) => void; 34 | onCardClick?: (e: React.MouseEvent, card: BoardItem) => void; 35 | renderListFooter?: (column: BoardItem) => React.ReactNode; 36 | renderGap?: (column: BoardItem) => React.ReactNode; 37 | } 38 | 39 | const renderGenericItem = ( 40 | items: BoardItem[], 41 | index: number, 42 | column: BoardItem, 43 | configMap: ConfigMap, 44 | cardOverShadowCount: number, 45 | renderListFooter: (column: BoardItem) => React.ReactNode, 46 | props: any, 47 | count: number 48 | ) => { 49 | return ( 50 | = items.length, 58 | renderListFooter, 59 | isShadow: 60 | cardOverShadowCount && index === count - (renderListFooter ? 2 : 1), 61 | isListFooter: 62 | renderListFooter && index === count - (renderListFooter ? 1 : 0), 63 | ...props, 64 | }} 65 | /> 66 | ); 67 | }; 68 | 69 | const VirtualizedList = ({ 70 | column, 71 | items, 72 | configMap, 73 | onScroll, 74 | cardOverShadowCount, 75 | renderListFooter, 76 | ...props 77 | }: ListProps) => { 78 | const count = 79 | column?.totalChildrenCount + 80 | cardOverShadowCount + 81 | (renderListFooter ? 1 : 0); 82 | 83 | return ( 84 | 89 | {(index: number) => 90 | renderGenericItem( 91 | items, 92 | index, 93 | column, 94 | configMap, 95 | cardOverShadowCount, 96 | renderListFooter, 97 | props, 98 | count 99 | ) 100 | } 101 | 102 | ); 103 | }; 104 | 105 | const NormalList = ({ 106 | column, 107 | items, 108 | configMap, 109 | onScroll, 110 | cardOverShadowCount, 111 | renderListFooter, 112 | ...props 113 | }: ListProps) => { 114 | const count = 115 | column?.totalChildrenCount + 116 | cardOverShadowCount + 117 | (renderListFooter ? 1 : 0); 118 | 119 | return ( 120 |
121 | {Array.from( 122 | { 123 | length: count, 124 | }, 125 | (_, index) => 126 | renderGenericItem( 127 | items, 128 | index, 129 | column, 130 | configMap, 131 | cardOverShadowCount, 132 | renderListFooter, 133 | props, 134 | count 135 | ) 136 | )} 137 |
138 | ); 139 | }; 140 | 141 | interface Props { 142 | items: BoardItem[]; 143 | column: BoardItem; 144 | columnListContentStyle?: (column: BoardItem) => React.CSSProperties; 145 | columnListContentClassName?: string; 146 | configMap: ConfigMap; 147 | renderSkeletonCard?: BoardProps["renderSkeletonCard"]; 148 | cardWrapperStyle?: ( 149 | card: BoardItem, 150 | column: BoardItem 151 | ) => React.CSSProperties; 152 | cardWrapperClassName?: string; 153 | onScroll?: (e: ScrollEvent, column: BoardItem) => void; 154 | onCardClick?: (e: React.MouseEvent, card: BoardItem) => void; 155 | loadMore?: (columnId: string) => void; 156 | cardOverShadowCount?: number; 157 | cardOverHeight?: number; 158 | onCardDndStateChange?: (info: DndState) => void; 159 | renderCardDragIndicator?: (card: BoardItem, info: any) => React.ReactNode; 160 | renderCardDragPreview?: (card: BoardItem, info: any) => React.ReactNode; 161 | renderListFooter?: (column: BoardItem) => React.ReactNode; 162 | renderGap?: (column: BoardItem) => React.ReactNode; 163 | } 164 | 165 | const ColumnContent = forwardRef((props, ref) => { 166 | const { 167 | items, 168 | column, 169 | configMap, 170 | columnListContentStyle, 171 | columnListContentClassName, 172 | cardWrapperStyle, 173 | renderSkeletonCard, 174 | cardWrapperClassName, 175 | onCardClick, 176 | loadMore, 177 | cardOverShadowCount, 178 | cardOverHeight, 179 | onCardDndStateChange, 180 | renderCardDragIndicator, 181 | renderCardDragPreview, 182 | renderListFooter, 183 | renderGap, 184 | } = props; 185 | const { 186 | virtualization = true, 187 | cardsGap, 188 | allowListFooter, 189 | } = useKanbanContext(); 190 | const containerClassName = classNames( 191 | withPrefix("column-content"), 192 | columnListContentClassName 193 | ); 194 | 195 | const onScroll = (e: ScrollEvent, column: BoardItem) => { 196 | const isSkeletonVisible = checkIfSkeletonIsVisible({ 197 | columnId: column?.id, 198 | }); 199 | if (isSkeletonVisible) loadMore?.(column?.id); 200 | props?.onScroll?.(e, column); 201 | }; 202 | 203 | const List = virtualization ? VirtualizedList : NormalList; 204 | 205 | return ( 206 |
211 | handleScroll(e, virtualization, onScroll, column)} 220 | onCardClick={onCardClick} 221 | cardOverShadowCount={cardOverShadowCount} 222 | onCardDndStateChange={onCardDndStateChange} 223 | renderCardDragIndicator={renderCardDragIndicator} 224 | renderCardDragPreview={renderCardDragPreview} 225 | cardOverHeight={cardOverHeight} 226 | renderGap={renderGap} 227 | renderListFooter={ 228 | (allowListFooter !== undefined && allowListFooter?.(column)) || 229 | allowListFooter === undefined 230 | ? renderListFooter 231 | : null 232 | } 233 | /> 234 |
235 | ); 236 | }); 237 | 238 | export default ColumnContent; 239 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/ClickUpExample/_index.scss: -------------------------------------------------------------------------------- 1 | .clickup-example { 2 | .rkk-column-outer { 3 | width: 264px !important; 4 | min-width: 264px !important; 5 | 6 | &.expanded { 7 | width: 32px !important; 8 | min-width: 32px !important; 9 | .rkk-column-content-list { 10 | display: none !important; 11 | } 12 | 13 | .clickup-column-header { 14 | transform: rotate(90deg); 15 | transition: all 0.2s ease-in-out; 16 | -webkit-transition: all 0.2s ease-in-out; 17 | -moz-transition: all 0.2s ease-in-out; 18 | -ms-transition: all 0.2s ease-in-out; 19 | -o-transition: all 0.2s ease-in-out; 20 | } 21 | 22 | .clickup-column { 23 | height: 150px; 24 | } 25 | } 26 | } 27 | 28 | .clickup-column { 29 | padding: 0px !important; 30 | border-radius: 0.5rem !important; 31 | -webkit-border-radius: 0.5rem !important; 32 | -moz-border-radius: 0.5rem !important; 33 | -ms-border-radius: 0.5rem !important; 34 | -o-border-radius: 0.5rem !important; 35 | 36 | .rkk-column-content-list { 37 | padding: 0 0.25rem 0.25rem !important; 38 | scrollbar-color: var(--cu-border-hover) 39 | var(--board-group-color-translucent); 40 | scrollbar-width: thin; 41 | } 42 | 43 | .rkk-column-wrapper { 44 | gap: 0; 45 | } 46 | 47 | &-header { 48 | display: flex; 49 | justify-content: space-between; 50 | align-items: center; 51 | padding: 0.5rem; 52 | 53 | &:hover { 54 | span:first-child { 55 | opacity: 1; 56 | } 57 | } 58 | &-left { 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | gap: 0.4rem; 63 | color: #fff; 64 | height: 24px; 65 | margin-right: 8px; 66 | padding: 4px 8px 4px 5px; 67 | border-radius: 5px; 68 | text-transform: uppercase; 69 | white-space: nowrap; 70 | span { 71 | font-size: 12px; 72 | font-weight: 500; 73 | 74 | &:first-child { 75 | display: block; 76 | width: 8px; 77 | height: 8px; 78 | border-radius: 50%; 79 | background-color: #fff; 80 | } 81 | } 82 | } 83 | 84 | &-right { 85 | display: flex; 86 | align-items: center; 87 | justify-content: center; 88 | span { 89 | display: flex; 90 | align-items: center; 91 | justify-content: center; 92 | cursor: pointer; 93 | width: 24px; 94 | height: 24px; 95 | border-radius: 0.375rem; 96 | -webkit-border-radius: 0.375rem; 97 | -moz-border-radius: 0.375rem; 98 | -ms-border-radius: 0.375rem; 99 | -o-border-radius: 0.375rem; 100 | &:hover { 101 | background-color: #00000017; 102 | } 103 | 104 | path { 105 | stroke: #6a6a6a; 106 | } 107 | &:first-child { 108 | opacity: 0; 109 | } 110 | transition: all 0.2s ease-in-out; 111 | -webkit-transition: all 0.2s ease-in-out; 112 | -moz-transition: all 0.2s ease-in-out; 113 | -ms-transition: all 0.2s ease-in-out; 114 | -o-transition: all 0.2s ease-in-out; 115 | } 116 | } 117 | 118 | &-count { 119 | font-size: 0.75rem; 120 | font-weight: 500; 121 | color: #838383; 122 | line-height: 1.4; 123 | word-wrap: break-word; 124 | flex: 1; 125 | } 126 | } 127 | } 128 | } 129 | 130 | // ClickUp Card Styling 131 | .clickup-card { 132 | background: #fff; 133 | border: 1px solid rgb(232, 232, 232); 134 | border-radius: 0.5rem; 135 | cursor: pointer; 136 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.055); 137 | -webkit-border-radius: 8px; 138 | -moz-border-radius: 8px; 139 | -ms-border-radius: 8px; 140 | -o-border-radius: 8px; 141 | &-content { 142 | padding: 8px; 143 | } 144 | 145 | &-title { 146 | font-size: 0.865rem; 147 | font-weight: 500; 148 | color: #202020; 149 | line-height: 1.4; 150 | word-wrap: break-word; 151 | } 152 | 153 | &-footer { 154 | margin-top: auto; 155 | padding-top: 4px; 156 | } 157 | 158 | &-icons { 159 | display: flex; 160 | align-items: center; 161 | gap: 5px; 162 | } 163 | 164 | &-icon { 165 | display: flex; 166 | align-items: center; 167 | justify-content: center; 168 | min-width: 22px; 169 | height: 22px; 170 | column-gap: 2px; 171 | border: 1px solid #e2e8f0; 172 | border-radius: 0.375rem; 173 | -webkit-border-radius: 0.375rem; 174 | -moz-border-radius: 0.375rem; 175 | -ms-border-radius: 0.375rem; 176 | -o-border-radius: 0.375rem; 177 | color: #838383; 178 | font-size: 12px; 179 | 180 | svg { 181 | width: 14px; 182 | height: 14px; 183 | stroke-width: 1.5; 184 | } 185 | 186 | &.priority-flag { 187 | span { 188 | font-size: 0.75rem; 189 | font-weight: 500; 190 | color: #646464; 191 | text-transform: capitalize; 192 | margin-right: 2px; 193 | } 194 | } 195 | } 196 | } 197 | 198 | // ClickUp Card Adder Styling 199 | .clickup-example-new-card { 200 | background: #fff; 201 | border: 1px solid #0091ff; 202 | border-radius: 8px; 203 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 204 | overflow: hidden; 205 | 206 | &-header { 207 | display: flex; 208 | align-items: center; 209 | gap: 8px; 210 | padding: 8px; 211 | 212 | input[type="text"] { 213 | flex: 1; 214 | min-height: 32px; 215 | padding: 0; 216 | font-size: 14px; 217 | font-weight: 400; 218 | color: #333; 219 | background: #fff; 220 | outline: none; 221 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 222 | "Noto Sans", "Ubuntu", "Droid Sans", "Helvetica Neue", sans-serif; 223 | border: none; 224 | 225 | &::placeholder { 226 | color: #999; 227 | } 228 | 229 | &:focus { 230 | border-color: none; 231 | box-shadow: none; 232 | } 233 | } 234 | } 235 | 236 | .clickup-save-btn { 237 | background: #2196f3; 238 | color: #fff; 239 | border: none; 240 | border-radius: 6px; 241 | height: 24px; 242 | padding: 0 8px; 243 | font-size: 13px; 244 | font-weight: 500; 245 | cursor: pointer; 246 | transition: background-color 0.15s ease; 247 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 248 | "Noto Sans", "Ubuntu", "Droid Sans", "Helvetica Neue", sans-serif; 249 | -webkit-border-radius: 6px; 250 | -moz-border-radius: 6px; 251 | -ms-border-radius: 6px; 252 | -o-border-radius: 6px; 253 | &:hover { 254 | background: #1976d2; 255 | } 256 | 257 | &:active { 258 | background: #1565c0; 259 | } 260 | } 261 | } 262 | 263 | .clickup-list-footer { 264 | display: flex; 265 | align-items: center; 266 | height: 32px; 267 | gap: 4px; 268 | padding-inline: 11px; 269 | border-radius: 0.5rem; 270 | -webkit-border-radius: 0.5rem; 271 | -moz-border-radius: 0.5rem; 272 | -ms-border-radius: 0.5rem; 273 | -o-border-radius: 0.5rem; 274 | cursor: pointer; 275 | &:hover { 276 | background-color: #00000017; 277 | } 278 | p { 279 | color: #838383; 280 | font-size: 0.865rem; 281 | font-weight: 500; 282 | } 283 | transition: all 0.2s ease-in-out; 284 | } 285 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/TrelloExample/_index.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts" as *; 2 | 3 | .trello-example { 4 | background-image: url(https://d2k1ftgv7pobq7.cloudfront.net/images/backgrounds/gradients/flower.svg); 5 | background-size: cover; 6 | background-position: center; 7 | background-repeat: no-repeat; 8 | .rkk-demo-page-content { 9 | padding-bottom: 0 !important; 10 | } 11 | 12 | .rkk-board { 13 | padding-bottom: 10px; 14 | } 15 | .rkk-column-outer { 16 | width: 272px !important; 17 | min-width: 272px !important; 18 | } 19 | 20 | &-column { 21 | background-color: #ebecf0 !important; 22 | padding: 6px !important; 23 | padding-right: 2px !important; 24 | border-radius: 12px !important; 25 | box-shadow: var( 26 | --ds-shadow-raised, 27 | 0px 1px 1px #091e4240, 28 | 0px 0px 1px #091e424f 29 | ); 30 | -webkit-border-radius: 12px !important; 31 | -moz-border-radius: 12px !important; 32 | -ms-border-radius: 12px !important; 33 | -o-border-radius: 12px !important; 34 | .rkk-column-wrappe { 35 | gap: 8px !important; 36 | } 37 | 38 | .rkk-column-content-list { 39 | padding: 0 3px 0 2px; 40 | -webkit-overflow-scrolling: touch !important; 41 | -webkit-transform: translate3d(0, 0, 0) !important; 42 | scrollbar-color: var(--ds-background-neutral-hovered, #091e4224) 43 | var(--ds-background-neutral, #091e420f) !important; 44 | scrollbar-width: thin !important; 45 | } 46 | } 47 | 48 | &-column-header { 49 | display: flex; 50 | align-items: center; 51 | justify-content: space-between; 52 | height: 32px; 53 | font-size: 14px; 54 | padding-right: 8px !important; 55 | font-weight: 600; 56 | color: #172b4d; 57 | padding: var(--ds-space-075, 6px) 0 var(--ds-space-075, 6px) 58 | var(--ds-space-150, 12px); 59 | 60 | span { 61 | flex: 1; 62 | } 63 | 64 | &-settings { 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | width: 32px; 69 | height: 32px; 70 | border-radius: 3px; 71 | cursor: pointer; 72 | transition: background-color 0.2s ease; 73 | color: #6b778c; 74 | 75 | &:hover { 76 | background-color: #091e4224; 77 | color: #172b4d; 78 | } 79 | } 80 | } 81 | } 82 | 83 | // Trello Card Styling 84 | .trello-card { 85 | background: #fff; 86 | border-radius: 8px; 87 | box-shadow: var( 88 | --ds-shadow-raised, 89 | 0px 1px 1px #091e4240, 90 | 0px 0px 1px #091e424f 91 | ); 92 | cursor: pointer; 93 | overflow: hidden; 94 | 95 | position: relative; 96 | text-decoration: none; 97 | transition: box-shadow 0.15s ease-in-out; 98 | -webkit-border-radius: 8px; 99 | -moz-border-radius: 8px; 100 | -ms-border-radius: 8px; 101 | -o-border-radius: 8px; 102 | &:hover { 103 | outline: 2px solid #0079bf; 104 | } 105 | 106 | // Cover Image 107 | &-cover { 108 | border-radius: 3px 3px 0 0; 109 | height: 160px; 110 | overflow: hidden; 111 | position: relative; 112 | 113 | img { 114 | width: 100%; 115 | height: 100%; 116 | object-fit: cover; 117 | display: block; 118 | } 119 | } 120 | 121 | // Card Content 122 | &-content { 123 | padding: var(--ds-space-100, 8px) var(--ds-space-150, 12px) 124 | var(--ds-space-050, 4px); 125 | position: relative; 126 | } 127 | 128 | // Labels 129 | &-labels { 130 | display: flex; 131 | flex-wrap: wrap; 132 | gap: 4px; 133 | margin-bottom: 4px; 134 | min-height: 0; 135 | } 136 | 137 | &-label { 138 | border-radius: 3px; 139 | color: #fff; 140 | display: block; 141 | font-size: 12px; 142 | font-weight: 700; 143 | height: 16px; 144 | line-height: 16px; 145 | max-width: 198px; 146 | min-width: 40px; 147 | overflow: hidden; 148 | padding: 0 8px; 149 | position: relative; 150 | text-overflow: ellipsis; 151 | white-space: nowrap; 152 | 153 | &:hover { 154 | max-width: none; 155 | padding-right: 8px; 156 | } 157 | } 158 | 159 | // Title 160 | &-title { 161 | color: #172b4d; 162 | font-size: 14px; 163 | font-weight: 500; 164 | line-height: 20px; 165 | margin: 0 0 4px; 166 | overflow-wrap: break-word; 167 | word-wrap: break-word; 168 | } 169 | 170 | // Badges (icons with counts) 171 | &-badges { 172 | display: flex; 173 | flex-wrap: wrap; 174 | gap: 8px; 175 | margin-top: 4px; 176 | align-items: center; 177 | } 178 | 179 | &-badge { 180 | align-items: center; 181 | color: #6b778c; 182 | display: flex; 183 | font-size: 12px; 184 | font-weight: 400; 185 | gap: 2px; 186 | line-height: 16px; 187 | 188 | svg { 189 | flex-shrink: 0; 190 | } 191 | 192 | &.trello-card-due { 193 | background-color: #091e420a; 194 | border-radius: 2px; 195 | padding: 0 4px; 196 | 197 | &.complete { 198 | background-color: #61bd4f; 199 | color: #fff; 200 | } 201 | } 202 | 203 | &.trello-card-checklist { 204 | &.complete { 205 | color: #61bd4f; 206 | } 207 | } 208 | } 209 | 210 | // Members 211 | &-members { 212 | display: flex; 213 | gap: 4px; 214 | margin-top: 8px; 215 | align-items: center; 216 | flex-wrap: wrap; 217 | } 218 | 219 | &-member { 220 | align-items: center; 221 | background-color: #dfe1e6; 222 | border-radius: 50%; 223 | color: #172b4d; 224 | display: flex; 225 | font-size: 12px; 226 | font-weight: 700; 227 | height: 28px; 228 | justify-content: center; 229 | line-height: 1; 230 | text-transform: uppercase; 231 | width: 28px; 232 | border: 2px solid #fff; 233 | margin-left: -4px; 234 | 235 | &:first-child { 236 | margin-left: 0; 237 | } 238 | 239 | // Generate different background colors for members 240 | &:nth-child(1) { 241 | background-color: #dfe1e6; 242 | } 243 | &:nth-child(2) { 244 | background-color: #b3d4fc; 245 | } 246 | &:nth-child(3) { 247 | background-color: #c7f0db; 248 | } 249 | &:nth-child(4) { 250 | background-color: #ffd3a5; 251 | } 252 | &:nth-child(5) { 253 | background-color: #fd9ca7; 254 | } 255 | } 256 | 257 | &-member-more { 258 | align-items: center; 259 | background-color: #f4f5f7; 260 | border: 1px solid #dfe1e6; 261 | border-radius: 50%; 262 | color: #6b778c; 263 | display: flex; 264 | font-size: 11px; 265 | font-weight: 600; 266 | height: 28px; 267 | justify-content: center; 268 | width: 28px; 269 | margin-left: -4px; 270 | } 271 | } 272 | 273 | // Responsive adjustments 274 | @include media-down(sm) { 275 | .trello-example-column { 276 | min-width: 272px; 277 | } 278 | 279 | .trello-card { 280 | &-cover { 281 | height: 120px; 282 | } 283 | 284 | &-members { 285 | margin-top: 6px; 286 | } 287 | 288 | &-member { 289 | height: 24px; 290 | width: 24px; 291 | font-size: 11px; 292 | } 293 | 294 | &-member-more { 295 | height: 24px; 296 | width: 24px; 297 | font-size: 10px; 298 | } 299 | } 300 | } 301 | 302 | .trello-example-list-footer { 303 | display: flex; 304 | align-items: center; 305 | justify-content: space-between; 306 | height: 35px; 307 | padding: var(--ds-space-050, 4px) var(--ds-space-100, 8px); 308 | cursor: pointer; 309 | border-radius: 8px; 310 | -webkit-border-radius: 8px; 311 | -moz-border-radius: 8px; 312 | -ms-border-radius: 8px; 313 | -o-border-radius: 8px; 314 | &-button { 315 | color: #44546f; 316 | font-size: 14px; 317 | font-weight: 500; 318 | line-height: 20px; 319 | svg { 320 | width: 20px; 321 | height: 20px; 322 | color: #44546f; 323 | } 324 | } 325 | 326 | &:hover { 327 | background-color: #091e4224; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Navigation/_Navigation.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts" as *; 2 | 3 | .#{$demo-prefix}-navigation { 4 | display: flex; 5 | flex-direction: column; 6 | gap: var(--space-3); 7 | 8 | &-item { 9 | @include card-interactive; 10 | @include flex-start; 11 | gap: var(--space-4); 12 | padding: var(--space-5); 13 | color: var(--gray-600); 14 | text-decoration: none; 15 | border: 2px solid transparent; 16 | position: relative; 17 | overflow: hidden; 18 | background: rgba(255, 255, 255, 0.9); 19 | backdrop-filter: blur(10px); 20 | 21 | // Subtle gradient border animation 22 | &::before { 23 | content: ""; 24 | position: absolute; 25 | inset: 0; 26 | padding: 2px; 27 | background: linear-gradient( 28 | 135deg, 29 | transparent, 30 | var(--primary-200), 31 | transparent 32 | ); 33 | border-radius: inherit; 34 | mask: 35 | linear-gradient(#fff 0 0) content-box, 36 | linear-gradient(#fff 0 0); 37 | mask-composite: xor; 38 | opacity: 0; 39 | transition: opacity var(--transition-base); 40 | } 41 | 42 | // Hover gradient overlay 43 | &::after { 44 | content: ""; 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | right: 0; 49 | bottom: 0; 50 | background: linear-gradient( 51 | 135deg, 52 | rgba(59, 130, 246, 0.02) 0%, 53 | rgba(147, 197, 253, 0.05) 100% 54 | ); 55 | opacity: 0; 56 | transition: opacity var(--transition-base); 57 | } 58 | 59 | &:hover { 60 | color: var(--gray-900); 61 | transform: translateY(-6px) scale(1.02); 62 | box-shadow: var(--shadow-2xl), var(--shadow-primary); 63 | border-color: rgba(59, 130, 246, 0.1); 64 | 65 | &::before { 66 | opacity: 1; 67 | } 68 | 69 | &::after { 70 | opacity: 1; 71 | } 72 | 73 | .#{$demo-prefix}-navigation-item-icon { 74 | transform: scale(1.1) rotate(5deg); 75 | background: var(--gradient-primary); 76 | color: white; 77 | box-shadow: var(--shadow-primary); 78 | } 79 | 80 | .#{$demo-prefix}-navigation-item-description { 81 | color: var(--primary-600); 82 | } 83 | } 84 | 85 | &.active { 86 | background: linear-gradient( 87 | 135deg, 88 | var(--primary-50), 89 | rgba(59, 130, 246, 0.08) 90 | ); 91 | color: var(--primary-700); 92 | border-color: var(--primary-200); 93 | box-shadow: var(--shadow-lg), var(--shadow-primary); 94 | transform: translateY(-2px); 95 | 96 | &::before { 97 | opacity: 1; 98 | background: var(--gradient-primary); 99 | } 100 | 101 | .#{$demo-prefix}-navigation-item-icon { 102 | background: var(--gradient-primary); 103 | color: white; 104 | box-shadow: var(--shadow-primary); 105 | transform: scale(1.05); 106 | } 107 | 108 | .#{$demo-prefix}-navigation-item-description { 109 | color: var(--primary-600); 110 | } 111 | 112 | // Active indicator 113 | &::after { 114 | content: ""; 115 | position: absolute; 116 | top: 50%; 117 | right: var(--space-5); 118 | width: 8px; 119 | height: 8px; 120 | background: var(--primary-500); 121 | border-radius: 50%; 122 | transform: translateY(-50%); 123 | box-shadow: 0 0 10px rgba(59, 130, 246, 0.4); 124 | opacity: 1; 125 | } 126 | } 127 | 128 | &:active { 129 | transform: translateY(-2px) scale(0.98); 130 | box-shadow: var(--shadow-lg); 131 | } 132 | 133 | @include media-down(lg) { 134 | flex-direction: column; 135 | text-align: center; 136 | padding: var(--space-4); 137 | gap: var(--space-3); 138 | } 139 | 140 | @include media-down(sm) { 141 | padding: var(--space-3); 142 | gap: var(--space-2); 143 | } 144 | 145 | &-icon { 146 | @include flex-center; 147 | width: 52px; 148 | height: 52px; 149 | border-radius: var(--radius-2xl); 150 | background: rgba(148, 163, 184, 0.1); 151 | color: var(--gray-600); 152 | transition: all var(--transition-base); 153 | position: relative; 154 | overflow: hidden; 155 | flex-shrink: 0; 156 | 157 | @include media-down(lg) { 158 | width: 44px; 159 | height: 44px; 160 | } 161 | 162 | @include media-down(sm) { 163 | width: 36px; 164 | height: 36px; 165 | } 166 | 167 | // Inner glow effect 168 | &::before { 169 | content: ""; 170 | position: absolute; 171 | inset: 1px; 172 | background: linear-gradient( 173 | 135deg, 174 | rgba(255, 255, 255, 0.6), 175 | transparent 176 | ); 177 | border-radius: calc(var(--radius-2xl) - 1px); 178 | opacity: 0; 179 | transition: opacity var(--transition-base); 180 | } 181 | 182 | svg { 183 | transition: all var(--transition-base); 184 | z-index: 1; 185 | position: relative; 186 | } 187 | } 188 | 189 | &-content { 190 | flex: 1; 191 | min-width: 0; 192 | position: relative; 193 | z-index: 1; 194 | 195 | @include media-down(lg) { 196 | display: none; 197 | } 198 | } 199 | 200 | &-label { 201 | font-size: 0.9375rem; 202 | font-weight: 600; 203 | color: inherit; 204 | margin-bottom: var(--space-1-5); 205 | @include text-truncate; 206 | position: relative; 207 | letter-spacing: -0.025em; 208 | } 209 | 210 | &-description { 211 | font-size: 0.8125rem; 212 | font-weight: 400; 213 | color: var(--gray-500); 214 | @include text-truncate; 215 | line-height: 1.4; 216 | letter-spacing: 0.025em; 217 | transition: color var(--transition-base); 218 | } 219 | } 220 | 221 | // Loading state 222 | &-loading { 223 | .#{$demo-prefix}-navigation-item { 224 | pointer-events: none; 225 | opacity: 0.6; 226 | 227 | &-icon { 228 | @include shimmer; 229 | } 230 | 231 | &-label, 232 | &-description { 233 | @include shimmer; 234 | border-radius: var(--radius-base); 235 | color: transparent; 236 | } 237 | 238 | &-label { 239 | height: 16px; 240 | margin-bottom: var(--space-2); 241 | } 242 | 243 | &-description { 244 | height: 12px; 245 | } 246 | } 247 | } 248 | 249 | // Empty state 250 | &-empty { 251 | @include flex-col-center; 252 | padding: var(--space-16) var(--space-8); 253 | text-align: center; 254 | color: var(--gray-500); 255 | 256 | &-icon { 257 | font-size: 3rem; 258 | margin-bottom: var(--space-4); 259 | opacity: 0.6; 260 | } 261 | 262 | &-title { 263 | font-size: 0.875rem; 264 | font-weight: 600; 265 | color: var(--gray-700); 266 | margin-bottom: var(--space-2); 267 | } 268 | 269 | &-description { 270 | font-size: 0.75rem; 271 | line-height: 1.5; 272 | } 273 | } 274 | } 275 | 276 | // Badge for new/updated items 277 | .#{$demo-prefix}-navigation-badge { 278 | @include badge-primary; 279 | position: absolute; 280 | top: -6px; 281 | right: -6px; 282 | min-width: 20px; 283 | height: 20px; 284 | padding: 0 var(--space-1-5); 285 | font-size: 0.625rem; 286 | font-weight: 700; 287 | border: 2px solid white; 288 | box-shadow: var(--shadow-sm); 289 | 290 | &.new { 291 | @include badge-success; 292 | animation: pulse 2s infinite; 293 | } 294 | 295 | &.updated { 296 | @include badge-warning; 297 | } 298 | } 299 | 300 | // Pulse animation for new badges 301 | @keyframes pulse { 302 | 0%, 303 | 100% { 304 | opacity: 1; 305 | transform: scale(1); 306 | } 307 | 50% { 308 | opacity: 0.8; 309 | transform: scale(1.05); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /rkk-demo/src/components/Header/_Header.scss: -------------------------------------------------------------------------------- 1 | @use "../../global/assets/styles/abstracts" as *; 2 | 3 | .#{$demo-prefix}-header { 4 | height: var(--header-height); 5 | background: rgba(255, 255, 255, 0.85); 6 | border-bottom: 1px solid rgba(59, 130, 246, 0.1); 7 | box-shadow: 8 | 0 1px 3px rgba(0, 0, 0, 0.1), 9 | 0 1px 20px rgba(59, 130, 246, 0.05); 10 | position: sticky; 11 | top: 0; 12 | z-index: var(--z-sticky); 13 | backdrop-filter: blur(20px); 14 | transition: all var(--transition-base); 15 | 16 | // Modern glassmorphism effect 17 | &::before { 18 | content: ""; 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | background: linear-gradient( 25 | 135deg, 26 | rgba(255, 255, 255, 0.4) 0%, 27 | rgba(255, 255, 255, 0.1) 100% 28 | ); 29 | pointer-events: none; 30 | } 31 | 32 | &-content { 33 | @include flex-between; 34 | max-width: 100%; 35 | height: 100%; 36 | padding: 0 var(--space-6); 37 | position: relative; 38 | z-index: 1; 39 | 40 | @include media-down(md) { 41 | padding: 0 var(--space-4); 42 | } 43 | } 44 | 45 | &-left { 46 | @include flex-start; 47 | } 48 | 49 | &-logo { 50 | text-decoration: none; 51 | color: inherit; 52 | transition: all var(--transition-base); 53 | border-radius: var(--radius-lg); 54 | padding: var(--space-2); 55 | margin: calc(var(--space-2) * -1); 56 | 57 | &:hover { 58 | background-color: rgba(59, 130, 246, 0.05); 59 | } 60 | } 61 | 62 | &-right { 63 | @include flex-start; 64 | } 65 | 66 | &-nav { 67 | @include flex-start; 68 | gap: var(--space-2); 69 | 70 | &-item { 71 | @include flex-start; 72 | gap: var(--space-2); 73 | padding: var(--space-2-5) var(--space-4); 74 | font-size: var(--text-sm); 75 | font-weight: 500; 76 | color: var(--gray-600); 77 | text-decoration: none; 78 | border-radius: var(--radius-xl); 79 | transition: all var(--transition-base); 80 | border: 1px solid transparent; 81 | background: rgba(255, 255, 255, 0.6); 82 | backdrop-filter: blur(8px); 83 | position: relative; 84 | overflow: hidden; 85 | 86 | // Modern hover effect 87 | &::before { 88 | content: ""; 89 | position: absolute; 90 | top: 0; 91 | left: 0; 92 | right: 0; 93 | bottom: 0; 94 | background: linear-gradient( 95 | 135deg, 96 | rgba(59, 130, 246, 0.1) 0%, 97 | rgba(147, 197, 253, 0.05) 100% 98 | ); 99 | opacity: 0; 100 | transition: opacity var(--transition-base); 101 | } 102 | 103 | &:hover { 104 | color: var(--primary-700); 105 | background: rgba(255, 255, 255, 0.9); 106 | border-color: rgba(59, 130, 246, 0.2); 107 | transform: translateY(-1px); 108 | box-shadow: 109 | 0 4px 12px rgba(59, 130, 246, 0.15), 110 | 0 0 0 1px rgba(59, 130, 246, 0.1); 111 | 112 | &::before { 113 | opacity: 1; 114 | } 115 | } 116 | 117 | &:active { 118 | transform: translateY(0); 119 | } 120 | 121 | span { 122 | position: relative; 123 | z-index: 1; 124 | 125 | @include media-down(md) { 126 | display: none; 127 | } 128 | } 129 | 130 | svg { 131 | transition: all var(--transition-base); 132 | position: relative; 133 | z-index: 1; 134 | } 135 | 136 | &:hover svg { 137 | color: var(--primary-600); 138 | transform: scale(1.05); 139 | } 140 | } 141 | } 142 | } 143 | 144 | // Clean logo component 145 | .#{$demo-prefix}-logo { 146 | @include flex-start; 147 | gap: var(--space-3); 148 | 149 | &-icon { 150 | @include flex-center; 151 | width: 40px; 152 | height: 40px; 153 | background: var(--gradient-primary); 154 | color: white; 155 | border-radius: var(--radius-lg); 156 | font-weight: 700; 157 | font-size: 0.875rem; 158 | letter-spacing: var(--tracking-tight); 159 | box-shadow: var(--shadow-primary); 160 | font-family: var(--font-display); 161 | } 162 | 163 | &-text { 164 | display: flex; 165 | flex-direction: column; 166 | 167 | @include media-down(sm) { 168 | display: none; 169 | } 170 | } 171 | 172 | &-title { 173 | font-size: var(--text-lg); 174 | font-weight: 700; 175 | font-family: var(--font-display); 176 | color: var(--gray-900); 177 | line-height: var(--leading-tight); 178 | letter-spacing: var(--tracking-tight); 179 | } 180 | 181 | &-subtitle { 182 | font-size: var(--text-xs); 183 | font-weight: 500; 184 | color: var(--gray-500); 185 | line-height: var(--leading-snug); 186 | letter-spacing: var(--tracking-wide); 187 | text-transform: uppercase; 188 | } 189 | } 190 | 191 | // Mobile menu button 192 | .#{$demo-prefix}-mobile-menu-btn { 193 | @include flex-center; 194 | width: 36px; 195 | height: 36px; 196 | padding: 0; 197 | border: 1px solid var(--gray-200); 198 | background: white; 199 | border-radius: var(--radius-lg); 200 | display: none; 201 | color: var(--gray-600); 202 | transition: all var(--transition-base); 203 | 204 | @include media-down(md) { 205 | display: flex; 206 | } 207 | 208 | &:hover { 209 | background-color: var(--gray-50); 210 | border-color: var(--gray-300); 211 | color: var(--gray-900); 212 | } 213 | 214 | svg { 215 | transition: transform var(--transition-base); 216 | } 217 | 218 | &.active svg { 219 | transform: rotate(90deg); 220 | } 221 | } 222 | 223 | // Clean search box 224 | .#{$demo-prefix}-search { 225 | position: relative; 226 | display: flex; 227 | align-items: center; 228 | max-width: 300px; 229 | width: 100%; 230 | margin: 0 var(--space-4); 231 | 232 | @include media-down(lg) { 233 | display: none; 234 | } 235 | 236 | &-input { 237 | @include input-base; 238 | width: 100%; 239 | padding-left: var(--space-10); 240 | font-size: var(--text-sm); 241 | background: rgba(255, 255, 255, 0.8); 242 | border-color: var(--gray-200); 243 | 244 | &:focus { 245 | background: white; 246 | box-shadow: 247 | var(--shadow-md), 248 | 0 0 0 3px rgba(59, 130, 246, 0.1); 249 | } 250 | 251 | &:hover:not(:focus) { 252 | border-color: var(--gray-300); 253 | } 254 | } 255 | 256 | &-icon { 257 | position: absolute; 258 | left: var(--space-3); 259 | color: var(--gray-400); 260 | transition: color var(--transition-base); 261 | pointer-events: none; 262 | } 263 | 264 | &:focus-within &-icon { 265 | color: var(--primary-500); 266 | } 267 | } 268 | 269 | // Simple notification badge 270 | .#{$demo-prefix}-notification-badge { 271 | position: absolute; 272 | top: -2px; 273 | right: -2px; 274 | width: 8px; 275 | height: 8px; 276 | background: var(--accent-danger); 277 | border: 2px solid white; 278 | border-radius: 50%; 279 | box-shadow: var(--shadow-sm); 280 | } 281 | 282 | // Responsive adjustments 283 | @include media-down(sm) { 284 | .#{$demo-prefix}-header { 285 | &-nav { 286 | gap: var(--space-0-5); 287 | 288 | &-item { 289 | padding: var(--space-1-5) var(--space-2); 290 | font-size: var(--text-xs); 291 | } 292 | } 293 | } 294 | 295 | .#{$demo-prefix}-logo { 296 | gap: var(--space-2); 297 | 298 | &-icon { 299 | width: 36px; 300 | height: 36px; 301 | font-size: 0.75rem; 302 | } 303 | 304 | &-title { 305 | font-size: var(--text-base); 306 | } 307 | } 308 | } 309 | 310 | // RTL Support for Header 311 | [dir="rtl"] .#{$demo-prefix}-header { 312 | &-content { 313 | flex-direction: row-reverse; 314 | } 315 | 316 | &-left { 317 | flex-direction: row-reverse; 318 | } 319 | 320 | &-right { 321 | flex-direction: row-reverse; 322 | } 323 | 324 | &-nav { 325 | flex-direction: row-reverse; 326 | } 327 | 328 | &-logo { 329 | .#{$demo-prefix}-logo { 330 | flex-direction: row-reverse; 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { Kanban } from "./"; 3 | import { mockData } from "./utils/mocks/data"; 4 | import CardSkeleton from "./components/CardSkeleton"; 5 | 6 | const App = () => { 7 | return ( 8 |
9 | { 12 | console.log(); 13 | }} 14 | // renderCardDragPreview={(card, info) => { 15 | // return ( 16 | //
23 | // Preview of {card.title} 24 | //
25 | // ); 26 | // }} 27 | // renderCardDragIndicator={(card, info) => { 28 | // return ( 29 | //
36 | // ); 37 | // }} 38 | // renderColumnFooter={(column) => ( 39 | //
40 | // {column.title} have total as {column?.totalChildrenCount} 41 | //
42 | // )} 43 | // renderColumnWrapper={(column, { children, className, style }) => ( 44 | //
52 | // {collapsed ? column?.title : children} 53 | //
54 | // )} 55 | // renderColumnHeader={(column) => ( 56 | //
57 | // {column.title} have total as {column?.totalChildrenCount} 58 | //
59 | // )} 60 | // columnHeaderStyle={(column) => ({ 61 | // backgroundColor: "red", 62 | // padding: "10px", 63 | // })} 64 | // columnWrapperStyle={(column) => ({ 65 | // backgroundColor: "green", 66 | // padding: "10px", 67 | // })} 68 | 69 | // renderSkeletonCard={({ index, column }) => ( 70 | //
71 | // Loading {index} {column.title} ... 72 | //
73 | // )} 74 | 75 | // Custom skeleton examples: 76 | renderSkeletonCard={({ index, column }) => ( 77 | 78 | )} 79 | // renderSkeletonCard={({ index, column }) => ( 80 | // 84 | // )} 85 | // onScroll={(e, column) => { 86 | // console.log(e, column); 87 | // }} 88 | // columnStyle={(column) => ({ 89 | // backgroundColor: dragOverColumn === column.id ? "red" : "blue", 90 | // })} 91 | onCardMove={(event) => { 92 | console.log({ event }); 93 | }} 94 | allowColumnAdder={true} 95 | renderColumnAdder={() =>
Add new Column
} 96 | renderListFooter={(column) =>
Add new one
} 97 | allowListFooter={(column) => true} 98 | rootClassName="check" 99 | dataSource={{ 100 | root: { 101 | id: "root", 102 | title: "Root", 103 | children: ["col-1", "col-2", "col-3"], 104 | totalChildrenCount: 3, 105 | parentId: null, 106 | }, 107 | "col-1": { 108 | id: "col-1", 109 | title: "To Do", 110 | children: ["task-1", "task-2"], 111 | totalChildrenCount: 2, 112 | parentId: "root", 113 | }, 114 | "col-2": { 115 | id: "col-2", 116 | title: "In Progress", 117 | children: ["task-3"], 118 | totalChildrenCount: 1, 119 | parentId: "root", 120 | }, 121 | "col-3": { 122 | id: "col-3", 123 | title: "Done", 124 | children: ["task-4"], 125 | totalChildrenCount: 1, 126 | parentId: "root", 127 | }, 128 | "task-1": { 129 | id: "task-1", 130 | title: "DesigHomepage", 131 | parentId: "col-1", 132 | children: [], 133 | totalChildrenCount: 0, 134 | type: "card", 135 | content: { 136 | description: "Create wireframeand mockups for thhomepage", 137 | priority: "high", 138 | }, 139 | }, 140 | "task-2": { 141 | id: "task-2", 142 | title: "SetuDatabase", 143 | parentId: "col-1", 144 | children: [], 145 | totalChildrenCount: 0, 146 | type: "card", 147 | }, 148 | "task-3": { 149 | id: "task-3", 150 | title: "Task 3", 151 | parentId: "col-2", 152 | children: [], 153 | totalChildrenCount: 0, 154 | type: "card", 155 | }, 156 | "task-4": { 157 | id: "task-4", 158 | title: "Task 4", 159 | parentId: "col-3", 160 | children: [], 161 | totalChildrenCount: 0, 162 | type: "card", 163 | }, 164 | }} 165 | cardsGap={6} 166 | // cardWrapperStyle={(card, col) => { 167 | // console.log({ col, card }); 168 | // return { 169 | // backgroundColor: "red", 170 | // padding: "10px", 171 | // }; 172 | // }} 173 | renderCardDragPreview={(card, info) => { 174 | console.log({ card, info }); 175 | return ( 176 |
185 | Preview of {card.title} 186 |
187 | ); 188 | }} 189 | cardWrapperClassName="card-hazem" 190 | // loadMore={(columnId) => { 191 | // console.log("loadMore", columnId); 192 | // }} 193 | // onCardDndStateChange={(info) => { 194 | // console.log({ info }); 195 | // if (info.state.type === "idle") { 196 | // setDragOverColumn(info.column?.id); 197 | // } 198 | // }} 199 | // onColumnDndStateChange={(info) => { 200 | // if (info.state.type === "is-card-over") { 201 | // setDragOverColumn(info.column?.id); 202 | // } else { 203 | // setDragOverColumn(null); 204 | // } 205 | // }} 206 | virtualization={true} // Set to false to disable virtualization and use normal map instead 207 | configMap={{ 208 | card: { 209 | render: (props) => ( 210 |
221 | Card {props.data.title} 222 |
223 | ), 224 | isDraggable: true, 225 | }, 226 | // divider: { 227 | // render: (props) => ( 228 | //
235 | // ), 236 | // isDraggable: true, 237 | // }, 238 | cardLoading: { 239 | render: (props) =>
Card Loading
, 240 | isDraggable: true, 241 | }, 242 | footer: { 243 | render: (props) => { 244 | return
Add Task
; 245 | }, 246 | isDraggable: false, 247 | }, 248 | }} 249 | /> 250 |
251 | ); 252 | }; 253 | 254 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 255 | 256 | ); 257 | -------------------------------------------------------------------------------- /rkk-demo/src/pages/ClickUpExample/ClickUpExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Kanban, 4 | type BoardItem, 5 | type BoardData, 6 | dropHandler, 7 | } from "react-kanban-kit"; 8 | import { mockData } from "../../utils/_mock_"; 9 | import { Calendar, User, Flag, ChevronLeft, Plus } from "lucide-react"; 10 | import { 11 | getPriorityColor, 12 | addCard, 13 | addCardPlaceholder, 14 | getAddCardPlaceholderKey, 15 | removeCardPlaceholder, 16 | toggleCollapsedColumn, 17 | } from "../../utils/kanbanUtils"; 18 | 19 | const ClickUpColumnHeader = ({ 20 | column, 21 | toggleCollapsedColumnHandler, 22 | addCardPlaceholderHandler, 23 | }: { 24 | column: BoardItem; 25 | toggleCollapsedColumnHandler: (columnId: string) => void; 26 | addCardPlaceholderHandler: (columnId: string, inTop: boolean) => void; 27 | }) => { 28 | const isExpanded = column?.content?.isExpanded; 29 | 30 | return ( 31 |
32 |
36 | 37 | {column.title} 38 |
39 |

{column.totalItemsCount}

40 | {!isExpanded && ( 41 |
42 | toggleCollapsedColumnHandler(column.id)}> 43 | 44 | 45 | addCardPlaceholderHandler(column.id, true)}> 46 | 47 | 48 |
49 | )} 50 |
51 | ); 52 | }; 53 | 54 | const ClickUpCardAdder: React.FC<{ 55 | columnId: string; 56 | dataSource: BoardData; 57 | setDataSource: (dataSource: BoardData) => void; 58 | inTop: boolean; 59 | }> = ({ columnId, dataSource, setDataSource, inTop }) => { 60 | const [newCardTitle, setNewCardTitle] = useState(""); 61 | 62 | const removeCardPlaceholderHandler = (columnId: string) => { 63 | setDataSource(removeCardPlaceholder(columnId, dataSource)); 64 | }; 65 | 66 | const addCardHandler = (columnId: string, title: string) => { 67 | if (!title.trim()) return; 68 | setDataSource(addCard(columnId, dataSource, title, inTop)); 69 | }; 70 | 71 | return ( 72 |
{ 75 | if (newCardTitle.trim()) addCardHandler(columnId, newCardTitle); 76 | else removeCardPlaceholderHandler(columnId); 77 | }} 78 | tabIndex={0} 79 | > 80 |
81 | setNewCardTitle(e.target.value)} 85 | placeholder="Task name" 86 | autoFocus 87 | onKeyDown={(e) => { 88 | if (e.key === "Enter") { 89 | addCardHandler(columnId, newCardTitle); 90 | } else if (e.key === "Escape") { 91 | removeCardPlaceholderHandler(columnId); 92 | } 93 | }} 94 | /> 95 | 101 |
102 |
103 | ); 104 | }; 105 | 106 | const ClickUpCard = ({ data }: { data: BoardItem }) => { 107 | const priorityColor = getPriorityColor(data.content?.priority); 108 | 109 | return ( 110 |
111 |
112 | {/* Task Title */} 113 |
{data.title}
114 | 115 | {/* Card Footer with Icons */} 116 |
117 |
118 | {/* User Icon */} 119 |
120 | 121 |
122 | 123 |
124 | 125 |
126 | 127 |
128 | 129 | {data.content?.priority} 130 |
131 |
132 |
133 |
134 |
135 | ); 136 | }; 137 | 138 | export const ClickUpExample: React.FC = () => { 139 | const [dataSource, setDataSource] = useState( 140 | structuredClone(mockData) as BoardData 141 | ); 142 | 143 | const addCardPlaceholderHandler = ( 144 | columnId: string, 145 | inTop: boolean = true 146 | ) => { 147 | setDataSource(addCardPlaceholder(columnId, dataSource, inTop)); 148 | }; 149 | 150 | const toggleCollapsedColumnHandler = (columnId: string) => { 151 | setDataSource(toggleCollapsedColumn(columnId, dataSource)); 152 | }; 153 | 154 | return ( 155 |
156 |
157 |

ClickUp-Style Kanban Board

158 |

159 | A ClickUp-inspired board with priority indicators and clean card 160 | design 161 |

162 |
163 | 164 |
165 | , 171 | isDraggable: true, 172 | }, 173 | "new-card": { 174 | render: ({ column, data }) => ( 175 | 181 | ), 182 | isDraggable: false, 183 | }, 184 | }} 185 | columnClassName={() => "clickup-column"} 186 | renderColumnHeader={(column) => ( 187 | 192 | )} 193 | cardsGap={4} 194 | virtualization={true} 195 | onCardMove={(move) => { 196 | setDataSource( 197 | dropHandler( 198 | move, 199 | dataSource, 200 | () => {}, 201 | (newColumn) => { 202 | return { 203 | ...newColumn, 204 | totalItemsCount: (newColumn.totalItemsCount || 0) + 1, 205 | totalChildrenCount: (newColumn.totalChildrenCount || 0) + 1, 206 | }; 207 | }, 208 | (sourceColumn) => { 209 | return { 210 | ...sourceColumn, 211 | totalItemsCount: (sourceColumn.totalItemsCount || 0) - 1, 212 | totalChildrenCount: 213 | (sourceColumn.totalChildrenCount || 0) - 1, 214 | }; 215 | } 216 | ) 217 | ); 218 | }} 219 | columnListContentClassName={() => "clickup-column-list-content"} 220 | columnStyle={(column) => ({ 221 | background: `color-mix(in srgb, ${column?.content?.color}, transparent 92%)`, 222 | })} 223 | renderListFooter={(column) => ( 224 |
addCardPlaceholderHandler(column.id, false)} 227 | > 228 | 229 | 230 | 231 |

Add Task

232 |
233 | )} 234 | allowListFooter={(column) => { 235 | return !column.children.includes( 236 | getAddCardPlaceholderKey(column.id) 237 | ); 238 | }} 239 | onColumnClick={(_, column) => { 240 | if (column?.content?.isExpanded) 241 | toggleCollapsedColumnHandler(column.id); 242 | }} 243 | columnWrapperClassName={(column) => { 244 | const className = column?.content?.isExpanded ? "expanded" : ""; 245 | return className; 246 | }} 247 | /> 248 |
249 |
250 | ); 251 | }; 252 | 253 | export default ClickUpExample; 254 | --------------------------------------------------------------------------------