feat(v-kanban): ✨ v-kanban playground
This commit is contained in:
parent
27df47803c
commit
1fac998968
@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx" ,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
"vComponents/*": ["./node_modules/vComponents/dist/components/*"],
|
"vComponents/*": ["./node_modules/vComponents/dist/components/*"],
|
||||||
|
"VCalendar": ["./node_modules/vCalendar/dist/VCalendar"],
|
||||||
"via-ui/*": ["./node_modules/via-ui/dist/components/*"]
|
"via-ui/*": ["./node_modules/via-ui/dist/components/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
package.json
15
package.json
@ -29,16 +29,29 @@
|
|||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@codesandbox/sandpack-react": "^2.10.0",
|
"@codesandbox/sandpack-react": "^2.10.0",
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
|
"@heroicons/react": "^2.1.1",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@uiw/codemirror-theme-monokai": "^4.21.21",
|
"@uiw/codemirror-theme-monokai": "^4.21.21",
|
||||||
"@uiw/react-codemirror": "^4.21.21",
|
"@uiw/react-codemirror": "^4.21.21",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"config": "^3.3.9",
|
"config": "^3.3.9",
|
||||||
|
"embla-carousel-react": "^8.0.0-rc19",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"pristinejs": "^1.1.0",
|
"pristinejs": "^1.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
"react-dnd": "^16.0.1",
|
||||||
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-dropzone-component": "^3.2.0",
|
"react-dropzone-component": "^3.2.0",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
@ -49,6 +62,8 @@
|
|||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"rosetta": "^1.1.0",
|
"rosetta": "^1.1.0",
|
||||||
"v-functions": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-functions.git",
|
"v-functions": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-functions.git",
|
||||||
|
"v-kanban": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-kanban.git#dev_ja",
|
||||||
|
"vCalendar": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-calendar.git",
|
||||||
"vComponents": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-components.git#dev_ja",
|
"vComponents": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-components.git#dev_ja",
|
||||||
"via-ui": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/via-ui.git#dev_ja"
|
"via-ui": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/via-ui.git#dev_ja"
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ import { BsSearch } from 'react-icons/bs'
|
|||||||
import { IoEllipsisVertical, IoFilter } from 'react-icons/io5'
|
import { IoEllipsisVertical, IoFilter } from 'react-icons/io5'
|
||||||
import { DocumentPlusIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/solid'
|
import { DocumentPlusIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/solid'
|
||||||
import { FaChevronLeft, FaChevronRight, FaPaperclip, FaUser } from 'react-icons/fa'
|
import { FaChevronLeft, FaChevronRight, FaPaperclip, FaUser } from 'react-icons/fa'
|
||||||
import { RiEyeLine } from 'react-icons/ri'
|
// import { RiEyeLine } from 'react-icons/ri'
|
||||||
import { LuEye, LuCode2, LuClipboardList } from 'react-icons/lu'
|
import { LuEye, LuCode2, LuClipboardList } from 'react-icons/lu'
|
||||||
|
|
||||||
export const Icons = {
|
export const Icons = {
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||||
|
import { Task } from '../TaskCard/taskCard.types'
|
||||||
|
|
||||||
|
export interface Column {
|
||||||
|
id: UniqueIdentifier;
|
||||||
|
title: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColumnType = 'Column';
|
||||||
|
|
||||||
|
export interface ColumnDragData {
|
||||||
|
type: ColumnType;
|
||||||
|
column: Column;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoardColumnProps {
|
||||||
|
column: Column;
|
||||||
|
tasks: Task[];
|
||||||
|
isOverlay?: boolean;
|
||||||
|
}
|
163
src/components/v-kanban/components/BoardColumn/index.tsx
Normal file
163
src/components/v-kanban/components/BoardColumn/index.tsx
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
|
// DnD: https://docs.dndkit.com/
|
||||||
|
import { SortableContext, useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { useDndContext } from '@dnd-kit/core'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
|
||||||
|
// via-ui
|
||||||
|
import { Card, CardContent, CardHeader } from '../ui/card'
|
||||||
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area'
|
||||||
|
|
||||||
|
// Other
|
||||||
|
import { TaskCard } from '../TaskCard'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
import { cn } from '../../lib/utils'
|
||||||
|
import { BoardColumnProps, ColumnDragData } from './boardColumn.types'
|
||||||
|
import { useScreenType } from '../../hooks'
|
||||||
|
import { Button } from '../ui/button'
|
||||||
|
import { Icons } from '../icons'
|
||||||
|
import Tooltip from 'via-ui/tooltip'
|
||||||
|
|
||||||
|
export const BoardColumn = ({ column, tasks, isOverlay }: BoardColumnProps) => {
|
||||||
|
const screenType = useScreenType()
|
||||||
|
|
||||||
|
const tasksIds = useMemo(() => {
|
||||||
|
return tasks.map((task) => task.id)
|
||||||
|
}, [tasks])
|
||||||
|
|
||||||
|
const {
|
||||||
|
setNodeRef,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({
|
||||||
|
id: column.id,
|
||||||
|
data: {
|
||||||
|
type: 'Column',
|
||||||
|
column
|
||||||
|
} satisfies ColumnDragData,
|
||||||
|
attributes: {
|
||||||
|
roleDescription: `Column: ${column.title}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transition,
|
||||||
|
transform: CSS.Translate.toString(transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants = cva(
|
||||||
|
'group/board-column flex shrink-0 cursor-pointer select-none snap-center flex-col rounded-lg bg-gray-100 shadow-md hover:bg-gray-200',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
dragging: {
|
||||||
|
default: 'border-2 border-transparent',
|
||||||
|
over: 'border-4 border-dashed border-gray-400 opacity-30',
|
||||||
|
overlay: 'rotate-2 ring-2 ring-black'
|
||||||
|
},
|
||||||
|
screenType: {
|
||||||
|
desktop: 'h-[500px] max-h-[500px] w-[300px] max-w-full',
|
||||||
|
mobile: 'h-[75vh] w-full min-w-full pb-2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const variantsTask = cva(
|
||||||
|
'flex grow gap-2 p-2', {
|
||||||
|
variants: {
|
||||||
|
screenType: {
|
||||||
|
desktop: 'flex-col',
|
||||||
|
mobile: 'flex-col'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={variants({
|
||||||
|
dragging: isOverlay ? 'overlay' : isDragging ? 'over' : undefined,
|
||||||
|
screenType: screenType.isMobile ? 'mobile' : 'desktop'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CardHeader
|
||||||
|
className={cn(
|
||||||
|
'relative space-between flex flex-row items-center border-b-2 p-2 text-left font-semibold rounded-t-lg text-white',
|
||||||
|
column.color ? column.color : 'bg-gray-500'
|
||||||
|
)}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<span className='mr-auto'>
|
||||||
|
{column.title}
|
||||||
|
</span>
|
||||||
|
<Tooltip className='hidden group-hover/board-column:block' placement='top' content='Add task'>
|
||||||
|
<Button className='group/plus-circle -my-1 flex h-auto w-auto cursor-pointer items-center justify-center bg-transparent p-0.5 hover:bg-gray-200 group-hover/board-column:block'>
|
||||||
|
<Icons.plusCircle className='h-5 w-5 group-hover/plus-circle:text-black'/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<ScrollArea>
|
||||||
|
<CardContent className={variantsTask({
|
||||||
|
screenType: screenType.isMobile ? 'mobile' : 'desktop'
|
||||||
|
})}>
|
||||||
|
<SortableContext items={tasksIds}>
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<TaskCard key={task.id} task={task} />
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</CardContent>
|
||||||
|
<ScrollBar orientation={screenType.isMobile ? 'horizontal' : 'vertical'} />
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BoardContainer ({ children }: { children: React.ReactNode }) {
|
||||||
|
const dndContext = useDndContext()
|
||||||
|
const screenType = useScreenType()
|
||||||
|
|
||||||
|
const variations = cva('flex md:px-0 lg:justify-center', {
|
||||||
|
variants: {
|
||||||
|
dragging: {
|
||||||
|
default: 'snap-x snap-mandatory',
|
||||||
|
active: 'snap-none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const variationsColumn = cva('flex w-full max-w-full items-start justify-start gap-4', {
|
||||||
|
variants: {
|
||||||
|
screenType: {
|
||||||
|
desktop: 'flex-row',
|
||||||
|
mobile: 'flex-row overflow-x-auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
screenType.isMobile
|
||||||
|
? (
|
||||||
|
<div className={variationsColumn({ screenType: 'mobile' })}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ScrollArea
|
||||||
|
className={variations({ dragging: dndContext.active ? 'active' : 'default' })}
|
||||||
|
>
|
||||||
|
<div className={variationsColumn({ screenType: 'desktop' })}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default interface ContainerProps {
|
||||||
|
id: UniqueIdentifier;
|
||||||
|
children: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
onAddItem?: () => void;
|
||||||
|
}
|
62
src/components/v-kanban/components/Container/index.tsx
Normal file
62
src/components/v-kanban/components/Container/index.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ContainerProps from './container.type'
|
||||||
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Button } from '../ui/button'
|
||||||
|
|
||||||
|
const Container = ({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
onAddItem
|
||||||
|
}: ContainerProps) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
setNodeRef,
|
||||||
|
listeners,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
type: 'container'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={{
|
||||||
|
transition,
|
||||||
|
transform: CSS.Translate.toString(transform)
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
'flex h-full w-full flex-col gap-y-4 rounded-xl bg-gray-50 p-4',
|
||||||
|
isDragging && 'opacity-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<h1 className="text-xl text-gray-800">{title}</h1>
|
||||||
|
<p className="text-sm text-gray-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="rounded-xl border p-2 text-xs shadow-lg hover:shadow-xl"
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
Drag Handle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
<Button variant="ghost" onClick={onAddItem}>
|
||||||
|
Add Item
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Container
|
52
src/components/v-kanban/components/Item/index.tsx
Normal file
52
src/components/v-kanban/components/Item/index.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||||
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
type ItemsType = {
|
||||||
|
id: UniqueIdentifier;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Items = ({ id, title }: ItemsType) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({
|
||||||
|
id,
|
||||||
|
data: {
|
||||||
|
type: 'item'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...attributes}
|
||||||
|
style={{
|
||||||
|
transition,
|
||||||
|
transform: CSS.Translate.toString(transform)
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
'w-full cursor-pointer rounded-xl border border-transparent bg-white px-2 py-4 shadow-md hover:border-gray-200',
|
||||||
|
isDragging && 'opacity-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{title}
|
||||||
|
<button
|
||||||
|
className="rounded-xl border p-2 text-xs shadow-lg hover:shadow-xl"
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
Drag Handle
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Items
|
363
src/components/v-kanban/components/KanbanBoard/index.tsx
Normal file
363
src/components/v-kanban/components/KanbanBoard/index.tsx
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
import React, { useMemo, useRef, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
|
// DndKit:
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
type DragEndEvent,
|
||||||
|
type DragOverEvent,
|
||||||
|
DragOverlay,
|
||||||
|
type DragStartEvent,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
KeyboardSensor,
|
||||||
|
Announcements,
|
||||||
|
UniqueIdentifier,
|
||||||
|
TouchSensor,
|
||||||
|
MouseSensor
|
||||||
|
} from '@dnd-kit/core'
|
||||||
|
import { SortableContext, arrayMove } from '@dnd-kit/sortable'
|
||||||
|
import { TaskCard } from '../TaskCard'
|
||||||
|
|
||||||
|
// Others
|
||||||
|
import { BoardColumn, BoardContainer } from '../BoardColumn'
|
||||||
|
import { hasDraggableData } from '../../utils'
|
||||||
|
import { coordinateGetter } from '../../multipleContainersKeyboardPreset'
|
||||||
|
import { useIsClient, useScreenType } from '../../hooks'
|
||||||
|
import { initialTasks, ColumnId, defaultCols } from '../../constants/defaults'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { Column } from '../BoardColumn/boardColumn.types'
|
||||||
|
import { Task } from '../TaskCard/taskCard.types'
|
||||||
|
import { KanbanBoardProps } from './kanbanBoard.type'
|
||||||
|
import SearchHeader from '../SearchHeader'
|
||||||
|
import { Card, CardHeader } from '../ui/card'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
import { Icons } from '../icons'
|
||||||
|
import { cn } from '../../lib/utils'
|
||||||
|
// import { Button } from '../ui/button'
|
||||||
|
|
||||||
|
const defaultSearchHeader = {
|
||||||
|
hide: false,
|
||||||
|
upperButtons: null,
|
||||||
|
hideInput: false,
|
||||||
|
inputSearchStyles: {},
|
||||||
|
inputSearchClassName: '',
|
||||||
|
inputPlaceholder: 'Buscar...',
|
||||||
|
upperRightButtons: null,
|
||||||
|
newButtonOnClick: () => {},
|
||||||
|
newButtonStyles: {},
|
||||||
|
showNewButton: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KanbanBoard = ({
|
||||||
|
columns = defaultCols,
|
||||||
|
tasks = initialTasks,
|
||||||
|
i18n = { t: () => { return 'txtNone' } },
|
||||||
|
searchHeader = defaultSearchHeader
|
||||||
|
}: KanbanBoardProps) => {
|
||||||
|
const screenType = useScreenType()
|
||||||
|
|
||||||
|
const [_columns, setColumns] = useState<Column[]>(columns)
|
||||||
|
const pickedUpTaskColumn = useRef<ColumnId | null>(null)
|
||||||
|
const columnsId = useMemo(() => _columns.map((col) => col.id), [_columns])
|
||||||
|
|
||||||
|
const [_tasks, setTasks] = useState<Task[]>(tasks)
|
||||||
|
|
||||||
|
const [activeColumn, setActiveColumn] = useState<Column | null>(null)
|
||||||
|
|
||||||
|
const [activeTask, setActiveTask] = useState<Task | null>(null)
|
||||||
|
|
||||||
|
const isClient = useIsClient()
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 250,
|
||||||
|
tolerance: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 250,
|
||||||
|
tolerance: 5
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
function getDraggingTaskData (taskId: UniqueIdentifier, columnId: ColumnId) {
|
||||||
|
const tasksInColumn = _tasks.filter((task) => task.columnId === columnId)
|
||||||
|
const taskPosition = tasksInColumn.findIndex((task) => task.id === taskId)
|
||||||
|
const column = _columns.find((col) => col.id === columnId)
|
||||||
|
return {
|
||||||
|
tasksInColumn,
|
||||||
|
taskPosition,
|
||||||
|
column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart (event: DragStartEvent) {
|
||||||
|
if (!hasDraggableData(event.active)) return
|
||||||
|
const data = event.active.data.current
|
||||||
|
if (data?.type === 'Column') {
|
||||||
|
setActiveColumn(data.column)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.type === 'Task') {
|
||||||
|
setActiveTask(data.task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver (event: DragOverEvent) {
|
||||||
|
const { active, over } = event
|
||||||
|
if (!over) return
|
||||||
|
|
||||||
|
const activeId = active.id
|
||||||
|
const overId = over.id
|
||||||
|
|
||||||
|
if (activeId === overId) return
|
||||||
|
|
||||||
|
if (!hasDraggableData(active) || !hasDraggableData(over)) return
|
||||||
|
|
||||||
|
const activeData = active.data.current
|
||||||
|
const overData = over.data.current
|
||||||
|
|
||||||
|
const isActiveATask = activeData?.type === 'Task'
|
||||||
|
const isOverATask = activeData?.type === 'Task'
|
||||||
|
|
||||||
|
if (!isActiveATask) return
|
||||||
|
|
||||||
|
// Im dropping a Task over another Task
|
||||||
|
if (isActiveATask && isOverATask) {
|
||||||
|
setTasks((tasks) => {
|
||||||
|
const activeIndex = tasks.findIndex((t) => t.id === activeId)
|
||||||
|
const overIndex = tasks.findIndex((t) => t.id === overId)
|
||||||
|
const activeTask = tasks[activeIndex]
|
||||||
|
const overTask = tasks[overIndex]
|
||||||
|
if (
|
||||||
|
activeTask &&
|
||||||
|
overTask &&
|
||||||
|
activeTask.columnId !== overTask.columnId
|
||||||
|
) {
|
||||||
|
activeTask.columnId = overTask.columnId
|
||||||
|
return arrayMove(tasks, activeIndex, overIndex - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrayMove(tasks, activeIndex, overIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOverAColumn = overData?.type === 'Column'
|
||||||
|
|
||||||
|
// Im dropping a Task over a column
|
||||||
|
if (isActiveATask && isOverAColumn) {
|
||||||
|
setTasks((tasks) => {
|
||||||
|
const activeIndex = tasks.findIndex((t) => t.id === activeId)
|
||||||
|
const activeTask = tasks[activeIndex]
|
||||||
|
if (activeTask) {
|
||||||
|
activeTask.columnId = overId as ColumnId
|
||||||
|
return arrayMove(tasks, activeIndex, activeIndex)
|
||||||
|
}
|
||||||
|
return tasks
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd (event: DragEndEvent) {
|
||||||
|
setActiveColumn(null)
|
||||||
|
setActiveTask(null)
|
||||||
|
|
||||||
|
const { active, over } = event
|
||||||
|
if (!over) return
|
||||||
|
|
||||||
|
const activeId = active.id
|
||||||
|
const overId = over.id
|
||||||
|
|
||||||
|
if (!hasDraggableData(active)) return
|
||||||
|
|
||||||
|
const activeData = active.data.current
|
||||||
|
|
||||||
|
if (activeId === overId) return
|
||||||
|
|
||||||
|
const isActiveAColumn = activeData?.type === 'Column'
|
||||||
|
if (!isActiveAColumn) return
|
||||||
|
|
||||||
|
setColumns((columns) => {
|
||||||
|
const activeColumnIndex = columns.findIndex((col) => col.id === activeId)
|
||||||
|
|
||||||
|
const overColumnIndex = columns.findIndex((col) => col.id === overId)
|
||||||
|
|
||||||
|
return arrayMove(columns, activeColumnIndex, overColumnIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const announcements: Announcements = {
|
||||||
|
onDragStart ({ active }) {
|
||||||
|
if (!hasDraggableData(active)) return
|
||||||
|
if (active.data.current?.type === 'Column') {
|
||||||
|
const startColumnIdx = columnsId.findIndex((id) => id === active.id)
|
||||||
|
const startColumn = _columns[startColumnIdx]
|
||||||
|
return `Picked up Column ${startColumn?.title} at position: ${
|
||||||
|
startColumnIdx + 1
|
||||||
|
} of ${columnsId.length}`
|
||||||
|
} else if (active.data.current?.type === 'Task') {
|
||||||
|
pickedUpTaskColumn.current = active.data.current.task.columnId
|
||||||
|
const { tasksInColumn, taskPosition, column } = getDraggingTaskData(
|
||||||
|
active.id,
|
||||||
|
pickedUpTaskColumn.current
|
||||||
|
)
|
||||||
|
return `Picked up Task ${
|
||||||
|
active.data.current.task.content
|
||||||
|
} at position: ${taskPosition + 1} of ${
|
||||||
|
tasksInColumn.length
|
||||||
|
} in column ${column?.title}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragOver ({ active, over }) {
|
||||||
|
if (!hasDraggableData(active) || !hasDraggableData(over)) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
active.data.current?.type === 'Column' &&
|
||||||
|
over.data.current?.type === 'Column'
|
||||||
|
) {
|
||||||
|
const overColumnIdx = columnsId.findIndex((id) => id === over.id)
|
||||||
|
return `Column ${active.data.current.column.title} was moved over ${
|
||||||
|
over.data.current.column.title
|
||||||
|
} at position ${overColumnIdx + 1} of ${columnsId.length}`
|
||||||
|
} else if (
|
||||||
|
active.data.current?.type === 'Task' &&
|
||||||
|
over.data.current?.type === 'Task'
|
||||||
|
) {
|
||||||
|
const { tasksInColumn, taskPosition, column } = getDraggingTaskData(
|
||||||
|
over.id,
|
||||||
|
over.data.current.task.columnId
|
||||||
|
)
|
||||||
|
if (over.data.current.task.columnId !== pickedUpTaskColumn.current) {
|
||||||
|
return `Task ${
|
||||||
|
active.data.current.task.content
|
||||||
|
} was moved over column ${column?.title} in position ${
|
||||||
|
taskPosition + 1
|
||||||
|
} of ${tasksInColumn.length}`
|
||||||
|
}
|
||||||
|
return `Task was moved over position ${taskPosition + 1} of ${
|
||||||
|
tasksInColumn.length
|
||||||
|
} in column ${column?.title}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd ({ active, over }) {
|
||||||
|
toast.success(`${active.data.current?.type} dropped`)
|
||||||
|
|
||||||
|
// Resetear a estado anterior
|
||||||
|
|
||||||
|
if (!hasDraggableData(active) || !hasDraggableData(over)) {
|
||||||
|
pickedUpTaskColumn.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
active.data.current?.type === 'Column' &&
|
||||||
|
over.data.current?.type === 'Column'
|
||||||
|
) {
|
||||||
|
const overColumnPosition = columnsId.findIndex((id) => id === over.id)
|
||||||
|
|
||||||
|
return `Column ${
|
||||||
|
active.data.current.column.title
|
||||||
|
} was dropped into position ${overColumnPosition + 1} of ${
|
||||||
|
columnsId.length
|
||||||
|
}`
|
||||||
|
} else if (
|
||||||
|
active.data.current?.type === 'Task' &&
|
||||||
|
over.data.current?.type === 'Task'
|
||||||
|
) {
|
||||||
|
const { tasksInColumn, taskPosition, column } = getDraggingTaskData(
|
||||||
|
over.id,
|
||||||
|
over.data.current.task.columnId
|
||||||
|
)
|
||||||
|
if (over.data.current.task.columnId !== pickedUpTaskColumn.current) {
|
||||||
|
return `Task was dropped into column ${column?.title} in position ${
|
||||||
|
taskPosition + 1
|
||||||
|
} of ${tasksInColumn.length}`
|
||||||
|
}
|
||||||
|
return `Task was dropped into position ${taskPosition + 1} of ${
|
||||||
|
tasksInColumn.length
|
||||||
|
} in column ${column?.title}`
|
||||||
|
}
|
||||||
|
pickedUpTaskColumn.current = null
|
||||||
|
},
|
||||||
|
onDragCancel ({ active }) {
|
||||||
|
pickedUpTaskColumn.current = null
|
||||||
|
if (!hasDraggableData(active)) return
|
||||||
|
return `Dragging ${active.data.current?.type} cancelled.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants = cva(
|
||||||
|
'group flex shrink-0 cursor-pointer snap-center flex-col rounded-lg border-none bg-gray-50/90 shadow-md hover:bg-gray-100',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
screenType: {
|
||||||
|
desktop: 'h-[500px] max-h-[500px] w-[300px] max-w-full',
|
||||||
|
mobile: 'max-h-auto h-auto w-full min-w-full'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='rounded-lg bg-white p-2 shadow-md'>
|
||||||
|
|
||||||
|
{ !searchHeader.hide && <SearchHeader i18n={i18n} searchHeader={searchHeader} />}
|
||||||
|
|
||||||
|
<DndContext
|
||||||
|
accessibility={{
|
||||||
|
announcements
|
||||||
|
}}
|
||||||
|
sensors={sensors}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
>
|
||||||
|
<BoardContainer>
|
||||||
|
<SortableContext items={columnsId}>
|
||||||
|
{_columns.map((col) => (
|
||||||
|
<BoardColumn
|
||||||
|
key={col.id}
|
||||||
|
column={col}
|
||||||
|
tasks={_tasks.filter((task) => task.columnId === col.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Card className={variants({ screenType: screenType.isMobile ? 'mobile' : 'desktop' })} onClick={() => {}}>
|
||||||
|
<CardHeader className={cn('space-between flex select-none flex-row items-center justify-center border-b-2 bg-slate-300 p-2 font-semibold text-gray-500',
|
||||||
|
screenType.isMobile ? 'rounded-lg' : 'rounded-t-lg'
|
||||||
|
)}>
|
||||||
|
<span className='flex items-center gap-1 whitespace-nowrap'>
|
||||||
|
<Icons.plusCircle className='h-5 w-5'/>
|
||||||
|
Add section
|
||||||
|
</span>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</SortableContext>
|
||||||
|
</BoardContainer>
|
||||||
|
|
||||||
|
{isClient && 'document' in window &&
|
||||||
|
createPortal(
|
||||||
|
<DragOverlay>
|
||||||
|
{activeColumn && (
|
||||||
|
<BoardColumn
|
||||||
|
isOverlay
|
||||||
|
column={activeColumn}
|
||||||
|
tasks={_tasks.filter(
|
||||||
|
(task) => task.columnId === activeColumn.id
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTask && <TaskCard task={activeTask} isOverlay />}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Column } from '../BoardColumn/boardColumn.types'
|
||||||
|
import { Task } from '../TaskCard/taskCard.types'
|
||||||
|
|
||||||
|
export interface SearchHeaderProps {
|
||||||
|
hide?: boolean;
|
||||||
|
searchHeaderStyles?: React.CSSProperties;
|
||||||
|
searchHeaderClassName?: string;
|
||||||
|
upperButtons?: React.ReactNode;
|
||||||
|
hideInput?: boolean;
|
||||||
|
inputSearchStyles?: React.CSSProperties;
|
||||||
|
inputSearchClassName?: string;
|
||||||
|
inputPlaceholder?: string;
|
||||||
|
upperRightButtons?: React.ReactNode;
|
||||||
|
showNewButton?: boolean;
|
||||||
|
newButtonOnClick?: () => void;
|
||||||
|
newButtonStyles?: React.CSSProperties;
|
||||||
|
newButtonClassName?: string;
|
||||||
|
newButtonText?: string;
|
||||||
|
newButtonIcon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface I18n {
|
||||||
|
t: (key: string, options?: any) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KanbanBoardProps {
|
||||||
|
columns?: Column[],
|
||||||
|
tasks?: Task[],
|
||||||
|
i18n?: I18n,
|
||||||
|
searchHeader?: SearchHeaderProps,
|
||||||
|
}
|
85
src/components/v-kanban/components/SearchHeader/index.tsx
Normal file
85
src/components/v-kanban/components/SearchHeader/index.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { I18n, SearchHeaderProps } from '../KanbanBoard/kanbanBoard.type'
|
||||||
|
import { Icons } from '../icons'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
|
||||||
|
const SearchHeader: React.FC<{ searchHeader: SearchHeaderProps, i18n?: I18n }> = ({ searchHeader, i18n }) => {
|
||||||
|
const isMobile = true
|
||||||
|
const [isFocusedSearch, setIsFocusedSearch] = React.useState(false)
|
||||||
|
const {
|
||||||
|
upperButtons,
|
||||||
|
hideInput,
|
||||||
|
searchHeaderStyles,
|
||||||
|
searchHeaderClassName,
|
||||||
|
inputSearchClassName,
|
||||||
|
inputSearchStyles,
|
||||||
|
inputPlaceholder,
|
||||||
|
upperRightButtons,
|
||||||
|
showNewButton,
|
||||||
|
newButtonOnClick,
|
||||||
|
newButtonStyles,
|
||||||
|
newButtonClassName,
|
||||||
|
newButtonText,
|
||||||
|
newButtonIcon
|
||||||
|
} = searchHeader
|
||||||
|
|
||||||
|
const gridClass = React.useMemo(() => {
|
||||||
|
if (upperButtons && upperRightButtons) {
|
||||||
|
return 'grid-cols-[minmax(auto,min-content),auto,minmax(auto,min-content),50px] sm:grid-cols-[minmax(auto,min-content),auto,minmax(auto,min-content),100px]'
|
||||||
|
} else if (upperButtons || upperRightButtons) {
|
||||||
|
return 'grid-cols-[minmax(auto,min-content),auto,50px] sm:grid-cols-[minmax(auto,min-content),auto,100px]'
|
||||||
|
} else {
|
||||||
|
return 'grid-cols-[auto,50px] sm:grid-cols-[auto,100px]'
|
||||||
|
}
|
||||||
|
}, [upperButtons, upperRightButtons])
|
||||||
|
|
||||||
|
const variantsButton = cva(
|
||||||
|
'flex cursor-pointer items-center justify-center bg-theme-app-500 px-2 py-1 text-white drop-shadow-sm transition-all duration-500 ease-in-out hover:bg-theme-app-600', {
|
||||||
|
variants: {
|
||||||
|
screenType: {
|
||||||
|
desktop: 'rounded-lg',
|
||||||
|
mobile: 'rounded-r-lg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`form-group relative grid w-full gap-1 pb-2 ${searchHeaderClassName} ${gridClass}`} style={searchHeaderStyles}>
|
||||||
|
{upperButtons}
|
||||||
|
{!hideInput && (
|
||||||
|
<div className={`flex w-full ${inputSearchClassName} ${!upperButtons && !upperRightButtons && !showNewButton ? 'col-span-full' : ''}`}>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
style={inputSearchStyles}
|
||||||
|
className='focus:border-r-none flex w-full !rounded-l-md border-y border-l !border-slate-300 px-2 py-1 drop-shadow-sm focus:!border-theme-app-500 focus:!ring-0 dark:bg-[#272727] dark:text-white dark:focus:!border-theme-app-500 dark:focus:!ring-0'
|
||||||
|
placeholder={inputPlaceholder}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={variantsButton({
|
||||||
|
screenType: isMobile && !isFocusedSearch ? 'mobile' : 'desktop'
|
||||||
|
})}
|
||||||
|
onClick={() => isMobile && setIsFocusedSearch(!isFocusedSearch)}
|
||||||
|
>
|
||||||
|
<Icons.search className="m-auto h-4 w-4 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upperRightButtons}
|
||||||
|
{showNewButton === true && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className={`ml-1 inline-flex w-auto items-center justify-center rounded-lg bg-emerald-600 px-2 py-0.5 text-center text-sm font-medium text-white drop-shadow-sm hover:bg-emerald-700 focus:outline-none focus:ring-4 focus:ring-emerald-300 md:px-5 dark:bg-emerald-600 dark:hover:bg-emerald-700 dark:focus:ring-emerald-800 ${newButtonClassName}`}
|
||||||
|
style={newButtonStyles}
|
||||||
|
onClick={() => newButtonOnClick()}
|
||||||
|
>
|
||||||
|
{newButtonIcon || <Icons.new className='h-5 w-5' />}
|
||||||
|
<span className='ml-2 hidden whitespace-nowrap sm:!inline'>
|
||||||
|
{newButtonText || (i18n.t('common.newItem') !== 'txtNone' ? i18n.t('common.newItem') : 'New Task')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchHeader
|
87
src/components/v-kanban/components/TaskCard/index.tsx
Normal file
87
src/components/v-kanban/components/TaskCard/index.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// DndKit documentation: https://docs.dndkit.com/
|
||||||
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
|
||||||
|
// via-ui
|
||||||
|
import { Card, CardContent, CardHeader } from '../ui/card'
|
||||||
|
import { Badge } from '../ui/badge'
|
||||||
|
|
||||||
|
// Others
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
import { TaskCardProps, TaskDragData } from './taskCard.types'
|
||||||
|
import { useScreenType } from '../../hooks'
|
||||||
|
|
||||||
|
export const TaskCard = ({
|
||||||
|
task,
|
||||||
|
isOverlay,
|
||||||
|
renderHeader,
|
||||||
|
renderContent
|
||||||
|
}: TaskCardProps) => {
|
||||||
|
const screenType = useScreenType()
|
||||||
|
|
||||||
|
const {
|
||||||
|
setNodeRef,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({
|
||||||
|
id: task.id,
|
||||||
|
data: {
|
||||||
|
type: 'Task',
|
||||||
|
task
|
||||||
|
} satisfies TaskDragData,
|
||||||
|
attributes: {
|
||||||
|
roleDescription: 'Task'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transition,
|
||||||
|
transform: CSS.Translate.toString(transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants = cva(
|
||||||
|
'rounded-xl border border-transparent bg-white shadow-md hover:border-gray-200',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
dragging: {
|
||||||
|
over: 'opacity-30 ring-2',
|
||||||
|
overlay: 'rotate-2 bg-white/60 ring-2 ring-black'
|
||||||
|
},
|
||||||
|
screenType: {
|
||||||
|
desktop: '',
|
||||||
|
mobile: 'w-full'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={ variants({
|
||||||
|
dragging: isOverlay ? 'overlay' : isDragging ? 'over' : undefined,
|
||||||
|
screenType: screenType.isMobile ? 'mobile' : 'desktop'
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CardHeader
|
||||||
|
className="relative flex flex-row border-b-2 p-2"
|
||||||
|
>
|
||||||
|
{renderHeader
|
||||||
|
? renderHeader(task)
|
||||||
|
: (
|
||||||
|
<Badge variant={'outline'} className="font-semibold hover:border-gray-700">
|
||||||
|
{task.id}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="whitespace-pre-wrap px-3 pb-6 pt-3 text-left">
|
||||||
|
{renderContent ? renderContent(task) : task.content}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { UniqueIdentifier } from '@dnd-kit/core'
|
||||||
|
import { ColumnId } from '../../constants/defaults'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
id: UniqueIdentifier;
|
||||||
|
columnId: ColumnId;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskCardProps {
|
||||||
|
task: Task;
|
||||||
|
isOverlay?: boolean;
|
||||||
|
renderHeader?: (task: Task) => React.ReactElement;
|
||||||
|
renderContent?: (task: Task) => React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskType = 'Task';
|
||||||
|
|
||||||
|
export interface TaskDragData {
|
||||||
|
type: TaskType;
|
||||||
|
task: Task;
|
||||||
|
}
|
17
src/components/v-kanban/components/icons.tsx
Normal file
17
src/components/v-kanban/components/icons.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { BsSearch } from 'react-icons/bs'
|
||||||
|
import { FaCheck, FaChevronRight, FaRegCircle, FaArrowLeft, FaArrowRight, FaPlus } from 'react-icons/fa6'
|
||||||
|
import { LuGripVertical, LuPlusCircle } from 'react-icons/lu'
|
||||||
|
import { DocumentPlusIcon } from '@heroicons/react/24/solid'
|
||||||
|
|
||||||
|
export const Icons = {
|
||||||
|
search: BsSearch,
|
||||||
|
check: FaCheck,
|
||||||
|
chevronRight: FaChevronRight,
|
||||||
|
circle: FaRegCircle,
|
||||||
|
gripVertical: LuGripVertical,
|
||||||
|
new: DocumentPlusIcon,
|
||||||
|
plus: FaPlus,
|
||||||
|
plusCircle: LuPlusCircle,
|
||||||
|
arrowLeft: FaArrowLeft,
|
||||||
|
arrowRight: FaArrowRight
|
||||||
|
}
|
36
src/components/v-kanban/components/ui/badge.tsx
Normal file
36
src/components/v-kanban/components/ui/badge.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||||
|
secondary:
|
||||||
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent',
|
||||||
|
outline: 'text-foreground'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge ({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
53
src/components/v-kanban/components/ui/button.tsx
Normal file
53
src/components/v-kanban/components/ui/button.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-blue-500 text-white hover:bg-blue-600',
|
||||||
|
destructive: 'bg-red-500 text-white hover:bg-red-600',
|
||||||
|
outline: 'border border-gray-300 bg-white hover:bg-gray-100 hover:text-gray-800',
|
||||||
|
secondary: 'bg-green-500 text-white hover:bg-green-600',
|
||||||
|
ghost: 'hover:bg-gray-100 hover:text-gray-800',
|
||||||
|
link: 'text-blue-500 underline-offset-4 hover:underline'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-10 px-4 py-2',
|
||||||
|
sm: 'h-9 rounded-md px-3',
|
||||||
|
lg: 'h-11 rounded-md px-8',
|
||||||
|
icon: 'h-10 w-10'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button'
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
79
src/components/v-kanban/components/ui/card.tsx
Normal file
79
src/components/v-kanban/components/ui/card.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = 'Card'
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = 'CardHeader'
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'text-2xl font-semibold leading-none tracking-tight',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = 'CardTitle'
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = 'CardDescription'
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = 'CardContent'
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn('flex items-center p-6 pt-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = 'CardFooter'
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
258
src/components/v-kanban/components/ui/carousel.tsx
Normal file
258
src/components/v-kanban/components/ui/carousel.tsx
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import useEmblaCarousel, {
|
||||||
|
type UseEmblaCarouselType
|
||||||
|
} from 'embla-carousel-react'
|
||||||
|
|
||||||
|
import { Button } from './button'
|
||||||
|
import { cn } from '../../lib/utils'
|
||||||
|
import { Icons } from '../icons'
|
||||||
|
|
||||||
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
|
type CarouselProps = {
|
||||||
|
opts?: CarouselOptions;
|
||||||
|
plugins?: CarouselPlugin;
|
||||||
|
orientation?: 'horizontal' | 'vertical';
|
||||||
|
setApi?: (api: CarouselApi) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CarouselContextProps = {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
|
scrollPrev: () => void;
|
||||||
|
scrollNext: () => void;
|
||||||
|
canScrollPrev: boolean;
|
||||||
|
canScrollNext: boolean;
|
||||||
|
} & CarouselProps;
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||||
|
|
||||||
|
function useCarousel () {
|
||||||
|
const context = React.useContext(CarouselContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useCarousel must be used within a <Carousel />')
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const Carousel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
orientation = 'horizontal',
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: orientation === 'horizontal' ? 'x' : 'y'
|
||||||
|
},
|
||||||
|
plugins
|
||||||
|
)
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||||
|
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||||
|
|
||||||
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanScrollPrev(api.canScrollPrev())
|
||||||
|
setCanScrollNext(api.canScrollNext())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scrollPrev = React.useCallback(() => {
|
||||||
|
api?.scrollPrev()
|
||||||
|
}, [api])
|
||||||
|
|
||||||
|
const scrollNext = React.useCallback(() => {
|
||||||
|
api?.scrollNext()
|
||||||
|
}, [api])
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollPrev()
|
||||||
|
} else if (event.key === 'ArrowRight') {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollNext()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollPrev, scrollNext]
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api || !setApi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setApi(api)
|
||||||
|
}, [api, setApi])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(api)
|
||||||
|
api.on('reInit', onSelect)
|
||||||
|
api.on('select', onSelect)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api?.off('select', onSelect)
|
||||||
|
}
|
||||||
|
}, [api, onSelect])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn('relative', className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
{...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Carousel.displayName = 'Carousel'
|
||||||
|
|
||||||
|
const CarouselContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { carouselRef, orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={carouselRef} className="overflow-visible">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex',
|
||||||
|
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselContent.displayName = 'CarouselContent'
|
||||||
|
|
||||||
|
const CarouselItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { orientation } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
className={cn(
|
||||||
|
'min-w-0 shrink-0 grow-0 basis-full',
|
||||||
|
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselItem.displayName = 'CarouselItem'
|
||||||
|
|
||||||
|
const CarouselPrevious = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
'absolute h-8 w-8 rounded-full',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-left-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}>
|
||||||
|
<Icons.arrowLeft className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselPrevious.displayName = 'CarouselPrevious'
|
||||||
|
|
||||||
|
const CarouselNext = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<typeof Button>
|
||||||
|
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
'absolute h-8 w-8 rounded-full',
|
||||||
|
orientation === 'horizontal'
|
||||||
|
? '-right-12 top-1/2 -translate-y-1/2'
|
||||||
|
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}>
|
||||||
|
<Icons.arrowRight className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CarouselNext.displayName = 'CarouselNext'
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CarouselApi,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselPrevious,
|
||||||
|
CarouselNext
|
||||||
|
}
|
197
src/components/v-kanban/components/ui/dropdown-menu.tsx
Normal file
197
src/components/v-kanban/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Icons } from '../icons'
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<Icons.chevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Icons.check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Icons.circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1.5 text-sm font-semibold',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup
|
||||||
|
}
|
45
src/components/v-kanban/components/ui/scroll-area.tsx
Normal file
45
src/components/v-kanban/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
||||||
|
import { cn } from '../../lib/utils'
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative overflow-hidden', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
'flex touch-none select-none transition-colors',
|
||||||
|
orientation === 'vertical' &&
|
||||||
|
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||||
|
orientation === 'horizontal' &&
|
||||||
|
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-gray-400"/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
))
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
90
src/components/v-kanban/constants/defaults.ts
Normal file
90
src/components/v-kanban/constants/defaults.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Column } from '../components/BoardColumn/boardColumn.types'
|
||||||
|
import { Task } from '../components/TaskCard/taskCard.types'
|
||||||
|
|
||||||
|
export const defaultCols = [
|
||||||
|
{
|
||||||
|
id: 'todo' as const,
|
||||||
|
title: 'Todo',
|
||||||
|
color: 'bg-blue-500'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'in-progress' as const,
|
||||||
|
title: 'In progress',
|
||||||
|
color: 'bg-yellow-500'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'done' as const,
|
||||||
|
title: 'Done',
|
||||||
|
color: 'bg-green-500'
|
||||||
|
}
|
||||||
|
] satisfies Column[]
|
||||||
|
|
||||||
|
export type ColumnId = (typeof defaultCols)[number]['id'];
|
||||||
|
|
||||||
|
export const initialTasks: Task[] = [
|
||||||
|
{
|
||||||
|
id: 'task1',
|
||||||
|
columnId: 'done',
|
||||||
|
content: 'Project initiation and planning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task2',
|
||||||
|
columnId: 'done',
|
||||||
|
content: 'Gather requirements from stakeholders'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task3',
|
||||||
|
columnId: 'done',
|
||||||
|
content: 'Create wireframes and mockups'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task4',
|
||||||
|
columnId: 'in-progress',
|
||||||
|
content: 'Develop homepage layout'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task5',
|
||||||
|
columnId: 'in-progress',
|
||||||
|
content: 'Design color scheme and typography'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task6',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Implement user authentication'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task7',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Build contact us page'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task8',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Create product catalog'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task9',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Develop about us page'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task10',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Optimize website for mobile devices'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task11',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Integrate payment gateway'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task12',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Perform testing and bug fixing'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'task13',
|
||||||
|
columnId: 'todo',
|
||||||
|
content: 'Launch website and deploy to server'
|
||||||
|
}
|
||||||
|
]
|
3
src/components/v-kanban/hooks/index.js
Normal file
3
src/components/v-kanban/hooks/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { useIsClient } from './useIsClient'
|
||||||
|
export { useWindowSize } from './useWindowSize'
|
||||||
|
export { useScreenType } from './useScreenType'
|
49
src/components/v-kanban/hooks/useIsClient.ts
Normal file
49
src/components/v-kanban/hooks/useIsClient.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook de React para determinar si el entorno actual es del lado del cliente.
|
||||||
|
*
|
||||||
|
* En el contexto de aplicaciones de React, especialmente aquellas que utilizan
|
||||||
|
* renderización en el lado del servidor (SSR), es crucial identificar si el
|
||||||
|
* código se está ejecutando en el entorno del cliente (navegador) o del servidor.
|
||||||
|
* Este hook proporciona una manera confiable de determinar este contexto,
|
||||||
|
* permitiendo ejecutar código específico del lado del cliente de manera segura.
|
||||||
|
*
|
||||||
|
* El hook utiliza `useState` para establecer el estado inicial basado en la
|
||||||
|
* disponibilidad del objeto `window`, un indicativo claro del entorno del cliente.
|
||||||
|
* A través de `useEffect`, ajusta este estado solo cuando el componente se monta
|
||||||
|
* en el navegador, garantizando una detección precisa y evitando ejecuciones
|
||||||
|
* innecesarias en el lado del servidor.
|
||||||
|
*
|
||||||
|
* @returns {boolean} Retorna `true` si el código se está ejecutando en el entorno del cliente.
|
||||||
|
* De lo contrario, retorna `false`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* import React from 'react';
|
||||||
|
* import useIsClient from './useIsClient';
|
||||||
|
*
|
||||||
|
* const MyComponent: React.FC = () => {
|
||||||
|
* const isClient = useIsClient();
|
||||||
|
*
|
||||||
|
* if (isClient) {
|
||||||
|
* // Ejemplo: Código seguro para ejecutar en el lado del cliente
|
||||||
|
* console.log('Ancho de la ventana:', window.innerWidth);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* return <div>Contenido del componente</div>;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* export default MyComponent;
|
||||||
|
*/
|
||||||
|
export const useIsClient = (): boolean => {
|
||||||
|
const [isClient, setIsClient] = useState(typeof window === 'object')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Este efecto no se ejecutará en el servidor, solo se activa en el entorno del cliente.
|
||||||
|
if (!isClient) {
|
||||||
|
setIsClient(true)
|
||||||
|
}
|
||||||
|
}, [isClient])
|
||||||
|
|
||||||
|
return isClient
|
||||||
|
}
|
77
src/components/v-kanban/hooks/useScreenType.ts
Normal file
77
src/components/v-kanban/hooks/useScreenType.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface ScreenType {
|
||||||
|
isLargeDesktop: boolean;
|
||||||
|
isDesktop: boolean;
|
||||||
|
isMobile: boolean;
|
||||||
|
isTablet: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook personalizado de React para determinar el tipo de pantalla basado en el ancho del viewport.
|
||||||
|
*
|
||||||
|
* Este hook es útil para el diseño responsivo, permitiendo ajustar la interfaz de usuario
|
||||||
|
* y la lógica del componente según el tamaño de la pantalla. Utiliza la API de `window` para
|
||||||
|
* escuchar cambios en el tamaño de la ventana y ajusta el estado del tipo de pantalla en consecuencia.
|
||||||
|
*
|
||||||
|
* Los breakpoints para mobile, tablet, desktop y large desktop son configurables según las necesidades
|
||||||
|
* del proyecto.
|
||||||
|
*
|
||||||
|
* @returns {ScreenType} Objeto que contiene booleanos para cada tipo de pantalla (mobile, tablet, desktop, largeDesktop).
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* import React from 'react';
|
||||||
|
* import useScreenType from './useScreenType';
|
||||||
|
*
|
||||||
|
* const MyComponent: React.FC = () => {
|
||||||
|
* const screenType = useScreenType();
|
||||||
|
*
|
||||||
|
* return (
|
||||||
|
* <div>
|
||||||
|
* {screenType.isMobile ? <MobileComponent /> : <DesktopComponent />}
|
||||||
|
* </div>
|
||||||
|
* );
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* export default MyComponent;
|
||||||
|
*/
|
||||||
|
export const useScreenType = (): ScreenType => {
|
||||||
|
// Estado inicial para SSR o cliente
|
||||||
|
const initialState: ScreenType = {
|
||||||
|
isLargeDesktop: false,
|
||||||
|
isDesktop: false,
|
||||||
|
isMobile: true,
|
||||||
|
isTablet: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const [screenType, setScreenType] = useState<ScreenType>(initialState)
|
||||||
|
|
||||||
|
const getScreenType = useCallback((): ScreenType => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const width = window.innerWidth
|
||||||
|
return {
|
||||||
|
isLargeDesktop: width >= 1200,
|
||||||
|
isDesktop: width >= 992 && width < 1200,
|
||||||
|
isTablet: width >= 768 && width < 992,
|
||||||
|
isMobile: width < 768
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return initialState
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const handleResize = () => {
|
||||||
|
setScreenType(getScreenType())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize()
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}, [getScreenType])
|
||||||
|
|
||||||
|
return screenType
|
||||||
|
}
|
46
src/components/v-kanban/hooks/useWindowSize.ts
Normal file
46
src/components/v-kanban/hooks/useWindowSize.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook de React para obtener el ancho y alto de la ventana del navegador.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* import React from 'react'
|
||||||
|
*
|
||||||
|
* const MyComponent: React.FC = () => {
|
||||||
|
* const { width, height } = useWindowSize()
|
||||||
|
* return (
|
||||||
|
* <div>
|
||||||
|
* <p>Ancho: {width}</p>
|
||||||
|
* <p>Alto: {height}</p>
|
||||||
|
* </div>
|
||||||
|
* )}
|
||||||
|
*
|
||||||
|
* export default MyComponent
|
||||||
|
*
|
||||||
|
* @returns {object} Retorna un objeto con las propiedades `width` y `height` que representan el ancho y alto de la ventana del navegador.
|
||||||
|
*/
|
||||||
|
export function useWindowSize (): { width: number, height: number } {
|
||||||
|
const [size, setSize] = React.useState({
|
||||||
|
width: null,
|
||||||
|
height: null
|
||||||
|
})
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
1
src/components/v-kanban/index.jsx
Normal file
1
src/components/v-kanban/index.jsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { KanbanBoard } from './components/KanbanBoard'
|
13
src/components/v-kanban/lib/utils.js
Normal file
13
src/components/v-kanban/lib/utils.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn (...inputs) {
|
||||||
|
const classes = inputs
|
||||||
|
.filter((input) => input)
|
||||||
|
.flatMap((input) =>
|
||||||
|
typeof input === 'object'
|
||||||
|
? Object.entries(input).filter(([, value]) => value).map(([key]) => key)
|
||||||
|
: input.split(' ')
|
||||||
|
)
|
||||||
|
|
||||||
|
return twMerge(...classes)
|
||||||
|
}
|
109
src/components/v-kanban/multipleContainersKeyboardPreset.ts
Normal file
109
src/components/v-kanban/multipleContainersKeyboardPreset.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import {
|
||||||
|
closestCorners,
|
||||||
|
getFirstCollision,
|
||||||
|
KeyboardCode,
|
||||||
|
DroppableContainer,
|
||||||
|
KeyboardCoordinateGetter
|
||||||
|
} from '@dnd-kit/core'
|
||||||
|
|
||||||
|
const directions: string[] = [
|
||||||
|
KeyboardCode.Down,
|
||||||
|
KeyboardCode.Right,
|
||||||
|
KeyboardCode.Up,
|
||||||
|
KeyboardCode.Left
|
||||||
|
]
|
||||||
|
|
||||||
|
export const coordinateGetter: KeyboardCoordinateGetter = (
|
||||||
|
event,
|
||||||
|
{ context: { active, droppableRects, droppableContainers, collisionRect } }
|
||||||
|
) => {
|
||||||
|
if (directions.includes(event.code)) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!active || !collisionRect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredContainers: DroppableContainer[] = []
|
||||||
|
|
||||||
|
droppableContainers.getEnabled().forEach((entry) => {
|
||||||
|
if (!entry || entry?.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = droppableRects.get(entry.id)
|
||||||
|
|
||||||
|
if (!rect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = entry.data.current
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const { type, children } = data
|
||||||
|
|
||||||
|
if (type === 'Column' && children?.length > 0) {
|
||||||
|
if (active.data.current?.type !== 'Column') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.code) {
|
||||||
|
case KeyboardCode.Down:
|
||||||
|
if (active.data.current?.type === 'Column') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (collisionRect.top < rect.top) {
|
||||||
|
// find all droppable areas below
|
||||||
|
filteredContainers.push(entry)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case KeyboardCode.Up:
|
||||||
|
if (active.data.current?.type === 'Column') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (collisionRect.top > rect.top) {
|
||||||
|
// find all droppable areas above
|
||||||
|
filteredContainers.push(entry)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case KeyboardCode.Left:
|
||||||
|
if (collisionRect.left >= rect.left + rect.width) {
|
||||||
|
// find all droppable areas to left
|
||||||
|
filteredContainers.push(entry)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case KeyboardCode.Right:
|
||||||
|
// find all droppable areas to right
|
||||||
|
if (collisionRect.left + collisionRect.width <= rect.left) {
|
||||||
|
filteredContainers.push(entry)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const collisions = closestCorners({
|
||||||
|
active,
|
||||||
|
collisionRect,
|
||||||
|
droppableRects,
|
||||||
|
droppableContainers: filteredContainers,
|
||||||
|
pointerCoordinates: null
|
||||||
|
})
|
||||||
|
const closestId = getFirstCollision(collisions, 'id')
|
||||||
|
|
||||||
|
if (closestId != null) {
|
||||||
|
const newDroppable = droppableContainers.get(closestId)
|
||||||
|
const newNode = newDroppable?.node.current
|
||||||
|
const newRect = newDroppable?.rect.current
|
||||||
|
|
||||||
|
if (newNode && newRect) {
|
||||||
|
return {
|
||||||
|
x: newRect.left,
|
||||||
|
y: newRect.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
23
src/components/v-kanban/utils.ts
Normal file
23
src/components/v-kanban/utils.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Active, DataRef, Over } from '@dnd-kit/core'
|
||||||
|
import { ColumnDragData } from './components/BoardColumn'
|
||||||
|
import { TaskDragData } from './components/TaskCard'
|
||||||
|
|
||||||
|
type DraggableData = ColumnDragData | TaskDragData;
|
||||||
|
|
||||||
|
export function hasDraggableData<T extends Active | Over> (
|
||||||
|
entry: T | null | undefined
|
||||||
|
): entry is T & {
|
||||||
|
data: DataRef<DraggableData>;
|
||||||
|
} {
|
||||||
|
if (!entry) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = entry.data.current
|
||||||
|
|
||||||
|
if (data?.type === 'Column' || data?.type === 'Task') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
229
src/components/vCalendar/index.jsx
Normal file
229
src/components/vCalendar/index.jsx
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Calendar, momentLocalizer } from 'react-big-calendar'
|
||||||
|
import moment from 'moment'
|
||||||
|
import Toolbar from 'react-big-calendar/lib/Toolbar'
|
||||||
|
import { FaCalendarDay, FaChevronLeft, FaChevronRight } from 'react-icons/fa'
|
||||||
|
import { DocumentPlusIcon } from '@heroicons/react/24/solid'
|
||||||
|
|
||||||
|
VCalendar.PropTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
name: PropTypes.string,
|
||||||
|
headers: PropTypes.array,
|
||||||
|
events: PropTypes.array,
|
||||||
|
model: PropTypes.object,
|
||||||
|
isEdit: PropTypes.bool,
|
||||||
|
onSave: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
reactBigCalendar: PropTypes.func,
|
||||||
|
onSuccessUpload: PropTypes.func,
|
||||||
|
tooblar: PropTypes.object,
|
||||||
|
showSaveNew: PropTypes.bool,
|
||||||
|
showCancel: PropTypes.bool,
|
||||||
|
cols: PropTypes.number,
|
||||||
|
i18n: PropTypes.object,
|
||||||
|
presets: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
VCalendar.defaultProps = {
|
||||||
|
children: null,
|
||||||
|
name: '',
|
||||||
|
headers: [],
|
||||||
|
events: [],
|
||||||
|
model: {},
|
||||||
|
isEdit: false,
|
||||||
|
onSave: () => {},
|
||||||
|
onCancel: () => {},
|
||||||
|
reactBigCalendar: () => {},
|
||||||
|
onSuccessUpload: () => {},
|
||||||
|
tooblar: {},
|
||||||
|
showSaveNew: true,
|
||||||
|
showCancel: true,
|
||||||
|
cols: 1,
|
||||||
|
i18n: {},
|
||||||
|
presets: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VCalendar ({ children, name, headers, events, model, isEdit, onSave, onCancel, onSuccessUpload, showSaveNew = true, showCancel = true, cols = 1, i18n = { t: () => { return 'txtNone' } }, presets }) {
|
||||||
|
moment.locale('es', {
|
||||||
|
month: {
|
||||||
|
dow: 1 // for starrting day from monday
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const localizer = momentLocalizer(moment)
|
||||||
|
const views = ['day', 'week', 'month', 'agenda']
|
||||||
|
|
||||||
|
const EventComponent = ({ events, change }) => (props) => {
|
||||||
|
// console.log('change', events, change)
|
||||||
|
return (
|
||||||
|
<div title={props.event.title}>
|
||||||
|
<h1 >{props.event.title}</h1>
|
||||||
|
<h4>{props.event.observaciones}</h4>
|
||||||
|
{/* <button onClick={props.change}>Do Something</button> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDayChange = (event, mconte) => {
|
||||||
|
mconte(event.target.value)
|
||||||
|
}
|
||||||
|
const handleNamvigate = (detail, elem) => {
|
||||||
|
detail.navigate(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomToolbar = ({ handleChange }) => {
|
||||||
|
return class BaseToolBar extends Toolbar {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDayChange = (event, mconte) => {
|
||||||
|
mconte(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNamvigate = (detail, elem) => {
|
||||||
|
detail.navigate(elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNavigateToEvent = () => {
|
||||||
|
onSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 content-start gap-1 p-2 md:grid-cols-5">
|
||||||
|
{/* <!-- Primera fila --> */}
|
||||||
|
<div classNames="col-span-1 flex justify-start md:flex md:justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="via-button flex items-center border-2 border-blue-600 !bg-blue-500 px-8 text-white transition-colors duration-500 ease-in-out hover:!bg-blue-400"
|
||||||
|
onClick={() => this.handleNamvigate(this, 'TODAY')}
|
||||||
|
>
|
||||||
|
<FaCalendarDay className="mr-2 shrink-0" /> Today
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-1 flex justify-end md:flex md:justify-start">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="via-button flex w-auto items-center justify-center border-2 border-blue-600 !bg-blue-500 px-3 transition-colors duration-500 ease-in-out hover:!bg-blue-400"
|
||||||
|
onClick={() => this.handleNamvigate(this, 'PREV')}
|
||||||
|
>
|
||||||
|
<FaChevronLeft className="mr-2 shrink-0 !text-white" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="via-button flex w-auto items-center justify-center border-2 border-blue-600 !bg-blue-500 px-3 transition-colors duration-500 ease-in-out hover:!bg-blue-400"
|
||||||
|
onClick={() => this.handleNamvigate(this, 'NEXT')}
|
||||||
|
>
|
||||||
|
<FaChevronRight className="mr-2 shrink-0 !text-white"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <!-- Segunda fila --> */}
|
||||||
|
<div className="col-span-2 flex items-center justify-center font-semibold md:col-span-1 ">
|
||||||
|
<label>{this.props.label}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <!-- Tercera fila --> */}
|
||||||
|
<div className="col-span-1 content-end">
|
||||||
|
<select
|
||||||
|
className=" bg-white via-input text-base font-normal text-gray-700 transition ease-in-out focus:border-blue-600 focus:bg-white focus:text-gray-700 focus:outline-none"
|
||||||
|
aria-label="Default select example"
|
||||||
|
onChange={(e) => this.handleDayChange(e, this.view)}
|
||||||
|
defaultValue={'week'}
|
||||||
|
>
|
||||||
|
<option value="day">Day</option>
|
||||||
|
<option value="week">Week</option>
|
||||||
|
<option value="month">Month</option>
|
||||||
|
<option value="agenda">Agenda</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <!-- Cuarta fila --> */}
|
||||||
|
<div className="col-span-1 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={this.handleNavigateToEvent}
|
||||||
|
className=" via-button bg-emerald-500 px-6 text-gray-100 transition duration-150 hover:bg-emerald-800"
|
||||||
|
>
|
||||||
|
<DocumentPlusIcon className="mr-2 h-6 w-6 shrink-0 text-white"/>
|
||||||
|
Nuevo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventStyleGetter = (event, start, end, isSelected) => {
|
||||||
|
const backgroundColor = '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||||
|
const style = {
|
||||||
|
backgroundColor: event.color ? event.color : backgroundColor,
|
||||||
|
borderRadius: '5px',
|
||||||
|
opacity: 1,
|
||||||
|
color: 'white',
|
||||||
|
border: '5px',
|
||||||
|
display: 'block'
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleNavigation = (date, view, action) => {
|
||||||
|
console.log(date, view, action)
|
||||||
|
// it returns current date, view options[month,day,week,agenda] and action like prev, next or today
|
||||||
|
}
|
||||||
|
const handleEventSelection = (e) => {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
console.log(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPopupForm = (slotInfo) => {
|
||||||
|
console.log(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectEventSlotHandler = (slotInfo) => {
|
||||||
|
openPopupForm(slotInfo) // OPEN POPUP FOR CREATE/EDIT EVENT
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = handleChange
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<Calendar
|
||||||
|
localizer={localizer}
|
||||||
|
selectable={true}
|
||||||
|
formats={{
|
||||||
|
dayHeaderFormat: (date, culture, localizer) =>
|
||||||
|
localizer.format(date, 'dddd D', culture)
|
||||||
|
}}
|
||||||
|
dayLayoutAlgorithm="no-overlap"
|
||||||
|
dayStyle={{
|
||||||
|
backgroundColor: '#fff', // Fondo blanco para la vista de día
|
||||||
|
borderBottom: '2px solid #ddd', // Línea de separación entre días
|
||||||
|
padding: '10px', // Ajusta el relleno para un aspecto más espaciado
|
||||||
|
boxShadow: '0 0 5px rgba(0, 0, 0, 0.1)', // Sombra suave
|
||||||
|
borderRadius: '5px' // Bordes redondeados
|
||||||
|
}}
|
||||||
|
views={views}
|
||||||
|
events={events}
|
||||||
|
style={{ height: 600 }}
|
||||||
|
showMultiDayTimes
|
||||||
|
onSelectSlot={event => { onSelectEventSlotHandler(event) }}
|
||||||
|
culture={['es']}
|
||||||
|
onNavigate={handleNavigation}
|
||||||
|
onSelectEvent={handleEventSelection}
|
||||||
|
components={{
|
||||||
|
event: EventComponent({ events, change }),
|
||||||
|
// data as events array and change is custom method passed into component(for perform any functionality on parent state)
|
||||||
|
toolbar: CustomToolbar({ events, change })
|
||||||
|
}}
|
||||||
|
eventPropGetter={(eventStyleGetter)}
|
||||||
|
// onSelectSlot={(slotInfo) => onSelectEventSlotHandler(slotInfo)}
|
||||||
|
/>
|
||||||
|
</div>)
|
||||||
|
}
|
@ -39,13 +39,23 @@
|
|||||||
"id_menu_padre": null,
|
"id_menu_padre": null,
|
||||||
"badge": "New"
|
"badge": "New"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "button",
|
||||||
|
"title": "Button",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcRotateToPortrait",
|
||||||
|
"path": "/via-ui/button",
|
||||||
|
"orden": 2.1,
|
||||||
|
"id_menu_padre": "via-ui",
|
||||||
|
"badge": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id_menu": "tooltip",
|
"id_menu": "tooltip",
|
||||||
"title": "Tooltip",
|
"title": "Tooltip",
|
||||||
"descripcion": null,
|
"descripcion": null,
|
||||||
"icon": "FcMms",
|
"icon": "FcMms",
|
||||||
"path": "/via-ui/tooltip",
|
"path": "/via-ui/tooltip",
|
||||||
"orden": 2.1,
|
"orden": 2.2,
|
||||||
"id_menu_padre": "via-ui",
|
"id_menu_padre": "via-ui",
|
||||||
"badge": null
|
"badge": null
|
||||||
},
|
},
|
||||||
@ -55,7 +65,17 @@
|
|||||||
"descripcion": null,
|
"descripcion": null,
|
||||||
"icon": "FcTemplate",
|
"icon": "FcTemplate",
|
||||||
"path": "/via-ui/tab",
|
"path": "/via-ui/tab",
|
||||||
"orden": 2.1,
|
"orden": 2.3,
|
||||||
|
"id_menu_padre": "via-ui",
|
||||||
|
"badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "stepper",
|
||||||
|
"title": "Stepper",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcSerialTasks",
|
||||||
|
"path": "/via-ui/stepper",
|
||||||
|
"orden": 2.4,
|
||||||
"id_menu_padre": "via-ui",
|
"id_menu_padre": "via-ui",
|
||||||
"badge": null
|
"badge": null
|
||||||
},
|
},
|
||||||
@ -65,8 +85,68 @@
|
|||||||
"descripcion": null,
|
"descripcion": null,
|
||||||
"icon": "FcIdea",
|
"icon": "FcIdea",
|
||||||
"path": "/via-ui/test",
|
"path": "/via-ui/test",
|
||||||
"orden": 2.1,
|
"orden": 2.40,
|
||||||
"id_menu_padre": "via-ui",
|
"id_menu_padre": "via-ui",
|
||||||
"badge": null
|
"badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "v-calendar",
|
||||||
|
"title": "v-calendar",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcCalendar",
|
||||||
|
"path": null,
|
||||||
|
"orden": 3,
|
||||||
|
"id_menu_padre": null,
|
||||||
|
"badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "VCalendar",
|
||||||
|
"title": "VCalendar",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcCalendar",
|
||||||
|
"path": "/v-calendar/VCalendar",
|
||||||
|
"orden": 3.1,
|
||||||
|
"id_menu_padre": "v-calendar",
|
||||||
|
"badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "v-kanban",
|
||||||
|
"title": "v-kanban",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcTemplate",
|
||||||
|
"path": null,
|
||||||
|
"orden": 4,
|
||||||
|
"id_menu_padre": null,
|
||||||
|
"badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "KanbanBoard",
|
||||||
|
"title": "KanbanBoard",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcTemplate",
|
||||||
|
"path": "/v-kanban/KanbanBoard",
|
||||||
|
"orden": 4.1,
|
||||||
|
"id_menu_padre": "v-kanban",
|
||||||
|
"badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "test",
|
||||||
|
"title": "Test",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcBiomass",
|
||||||
|
"path": null,
|
||||||
|
"orden": 5,
|
||||||
|
"id_menu_padre": null,
|
||||||
|
"badge": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id_menu": "dndkit",
|
||||||
|
"title": "dnd kit",
|
||||||
|
"descripcion": null,
|
||||||
|
"icon": "FcCollect",
|
||||||
|
"path": "/test/dndkit",
|
||||||
|
"orden": 5.1,
|
||||||
|
"id_menu_padre": "test",
|
||||||
|
"badge": null
|
||||||
}
|
}
|
||||||
]
|
]
|
11
src/pages/test/dndkit/[...index].jsx
Normal file
11
src/pages/test/dndkit/[...index].jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import VKanban from '@/components/v-kanban'
|
||||||
|
|
||||||
|
const DndKitPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<VKanban />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DndKitPage
|
14
src/pages/v-calendar/VCalendar/[...index].jsx
Normal file
14
src/pages/v-calendar/VCalendar/[...index].jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import VCalendar from '@/components/vCalendar'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const VCalendarPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VCalendar
|
||||||
|
onSave={() => { alert('onSave') }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VCalendarPage
|
41
src/pages/v-kanban/KanbanBoard/[...index].jsx
Normal file
41
src/pages/v-kanban/KanbanBoard/[...index].jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import CodePlayground from '@/components/CodePlayground'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { KanbanBoard } from 'v-kanban'
|
||||||
|
|
||||||
|
const initialCode =
|
||||||
|
`//import { KanbanBoard } from 'v-kanban'
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ id: 1, title: 'To Do', items: [{ id: 1, content: 'Task 1' }, { id: 2, content: 'Task 2' }] },
|
||||||
|
{ id: 2, title: 'In Progress', items: [{ id: 3, content: 'Task 3' }] },
|
||||||
|
{ id: 3, title: 'Done', items: [{ id: 4, content: 'Task 4' }] }
|
||||||
|
]
|
||||||
|
|
||||||
|
const KanbanBoardExample = () => {
|
||||||
|
return (
|
||||||
|
<KanbanBoard
|
||||||
|
// columns={columns}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<KanbanBoardExample />)
|
||||||
|
`
|
||||||
|
|
||||||
|
const KanbanBoardPage = () => {
|
||||||
|
const [code, setCode] = useState(initialCode)
|
||||||
|
|
||||||
|
const scope = {
|
||||||
|
React,
|
||||||
|
useState,
|
||||||
|
KanbanBoard
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='rounded-lg bg-white p-4 dark:!bg-[#222222]'>
|
||||||
|
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KanbanBoardPage
|
39
src/pages/via-ui/button/[...index].jsx
Normal file
39
src/pages/via-ui/button/[...index].jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import CodePlayground from '@/components/CodePlayground'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import Button from 'via-ui/button'
|
||||||
|
|
||||||
|
const initialCode =
|
||||||
|
`// import { Button } from 'via-ui'
|
||||||
|
|
||||||
|
const ButtonExample = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="success" size="lg">
|
||||||
|
Click me!
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<ButtonExample />);
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExampleComponentPage = () => {
|
||||||
|
const [code, setCode] = useState(initialCode)
|
||||||
|
|
||||||
|
const scope = {
|
||||||
|
React,
|
||||||
|
useState,
|
||||||
|
Button
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'>
|
||||||
|
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExampleComponentPage
|
86
src/pages/via-ui/stepper/[...index].jsx
Normal file
86
src/pages/via-ui/stepper/[...index].jsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import CodePlayground from '@/components/CodePlayground'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Stepper, StepperList, StepperTrigger, StepContent, StepLabel } from 'via-ui/stepper'
|
||||||
|
|
||||||
|
const initialCode = `
|
||||||
|
// import { Stepper, StepperList, StepperTrigger, StepContent, StepLabel, Button } from 'via-ui/stepper'
|
||||||
|
|
||||||
|
const StepperExample = () => {
|
||||||
|
const [activeStep, setActiveStep] = useState('step1');
|
||||||
|
const steps = ['step1', 'step2', 'step3'];
|
||||||
|
|
||||||
|
const handleStepChange = (step) => {
|
||||||
|
setActiveStep(step);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
const currentIndex = steps.indexOf(activeStep);
|
||||||
|
if (currentIndex < steps.length - 1) {
|
||||||
|
setActiveStep(steps[currentIndex + 1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
const currentIndex = steps.indexOf(activeStep);
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
setActiveStep(steps[currentIndex - 1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stepper activeStep={activeStep}>
|
||||||
|
<StepperList>
|
||||||
|
{steps.map((step) => (
|
||||||
|
<StepperTrigger
|
||||||
|
key={step}
|
||||||
|
value={step}
|
||||||
|
onStepChange={() => handleStepChange(step)}
|
||||||
|
isClickable={true}
|
||||||
|
>
|
||||||
|
<StepLabel>{'Step ' + (steps.indexOf(step) + 1)}</StepLabel>
|
||||||
|
</StepperTrigger>
|
||||||
|
))}
|
||||||
|
</StepperList>
|
||||||
|
|
||||||
|
<StepContent value={activeStep}>
|
||||||
|
<p>{'Content for ' + activeStep}</p>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<button className='via-button bg-blue-500 text-white' onClick={handleBack} disabled={activeStep === steps[0]}>
|
||||||
|
Atrás
|
||||||
|
</button>
|
||||||
|
<button className='via-button bg-green-500 text-white' onClick={handleNext} disabled={activeStep === steps[steps.length - 1]}>
|
||||||
|
Adelante
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</StepContent>
|
||||||
|
</Stepper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<StepperExample />);
|
||||||
|
`
|
||||||
|
|
||||||
|
const ExampleComponentPage = () => {
|
||||||
|
const [code, setCode] = useState(initialCode)
|
||||||
|
|
||||||
|
// El objeto scope debería incluir todos los componentes y funciones que el código JSX necesita
|
||||||
|
const scope = {
|
||||||
|
React,
|
||||||
|
useState,
|
||||||
|
Stepper,
|
||||||
|
StepperList,
|
||||||
|
StepperTrigger,
|
||||||
|
StepContent,
|
||||||
|
StepLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'>
|
||||||
|
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExampleComponentPage
|
@ -1,29 +1,55 @@
|
|||||||
import React from 'react'
|
import CodePlayground from '@/components/CodePlayground'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
||||||
|
|
||||||
const TabPage = () => {
|
const initialCode =
|
||||||
const [tab, setTab] = React.useState('data')
|
`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
||||||
|
|
||||||
|
const TabsExample = () => {
|
||||||
|
const [tab, setTab] = React.useState('data');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs activeValue={tab} onChange={setTab}>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="data">Datos del Evento</TabsTrigger>
|
||||||
|
<TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="data">
|
||||||
|
tabs data
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="docs">
|
||||||
|
tabs docs
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<TabsExample />);
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabExamplePage = () => {
|
||||||
|
const [code, setCode] = useState(initialCode)
|
||||||
|
|
||||||
|
const scope = {
|
||||||
|
React,
|
||||||
|
useState,
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs activeValue={tab}>
|
<>
|
||||||
<TabsList>
|
<div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'>
|
||||||
<TabsTrigger value="data" onClick={() => setTab('data')}>
|
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
|
||||||
Datos del Evento
|
</div>
|
||||||
</TabsTrigger>
|
</>
|
||||||
<TabsTrigger value="docs" onClick={() => setTab('docs')}>
|
|
||||||
Documentos del Evento
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="data">
|
|
||||||
tabs data
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="docs">
|
|
||||||
tabs docs
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TabPage
|
export default TabExamplePage
|
||||||
|
@ -1,56 +1,55 @@
|
|||||||
import CodePlayground from '@/components/CodePlayground'
|
import React from 'react'
|
||||||
import React, { useState } from 'react'
|
import { SandpackProvider, SandpackLayout, SandpackCodeEditor, SandpackPreview } from '@codesandbox/sandpack-react'
|
||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
const tabComponentCode = `
|
||||||
|
import React, { useState } from 'react';
|
||||||
const initialCode =
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab';
|
||||||
`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
|
||||||
|
|
||||||
const TabsExample = () => {
|
|
||||||
const [tab, setTab] = React.useState('data');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tabs activeValue={tab} onChange={setTab}>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="data">Datos del Evento</TabsTrigger>
|
|
||||||
<TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="data">
|
|
||||||
tabs data
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="docs">
|
|
||||||
tabs docs
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render(<TabsExample />);
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ExampleComponentPage = () => {
|
const files = {
|
||||||
const [code, setCode] = useState(initialCode)
|
'/App.js': {
|
||||||
|
code: `
|
||||||
|
|
||||||
// El objeto scope debería incluir todos los componentes y funciones que el código JSX necesita
|
const TabsExample = () => {
|
||||||
const scope = {
|
const [tab, setTab] = useState('data');
|
||||||
React,
|
return (
|
||||||
useState,
|
<>
|
||||||
Tabs,
|
<Tabs activeValue={tab} onChange={setTab}>
|
||||||
TabsContent,
|
<TabsList>
|
||||||
TabsList,
|
<TabsTrigger value="data">Datos del Evento</TabsTrigger>
|
||||||
TabsTrigger
|
<TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="data">
|
||||||
|
tabs data
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="docs">
|
||||||
|
tabs docs
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabsExample;`
|
||||||
|
},
|
||||||
|
'/node_modules/via-ui/tab/index.js': {
|
||||||
|
hidden: true,
|
||||||
|
code: tabComponentCode
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestPage = () => {
|
||||||
|
console.log('tabComponentCode', tabComponentCode)
|
||||||
return (
|
return (
|
||||||
<>
|
<SandpackProvider template="react" files={files}>
|
||||||
<div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'>
|
<SandpackLayout>
|
||||||
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
|
<SandpackCodeEditor />
|
||||||
</div>
|
<SandpackPreview />
|
||||||
</>
|
</SandpackLayout>
|
||||||
|
</SandpackProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExampleComponentPage
|
export default TestPage
|
||||||
|
695
src/styles/additional-styles/react-big-calendar.css
Normal file
695
src/styles/additional-styles/react-big-calendar.css
Normal file
@ -0,0 +1,695 @@
|
|||||||
|
.rbc-btn {
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
button.rbc-btn {
|
||||||
|
overflow: visible;
|
||||||
|
text-transform: none;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button[disabled].rbc-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
button.rbc-input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.rbc-calendar {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-align-items: stretch;
|
||||||
|
-ms-flex-align: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.rbc-calendar *,
|
||||||
|
.rbc-calendar *:before,
|
||||||
|
.rbc-calendar *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
.rbc-abs-full,
|
||||||
|
.rbc-row-bg {
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.rbc-ellipsis,
|
||||||
|
.rbc-event-label,
|
||||||
|
.rbc-row-segment .rbc-event-content,
|
||||||
|
.rbc-show-more {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-rtl {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
.rbc-off-range {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
.rbc-off-range-bg {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
.rbc-header {
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-flex: 1 0 0%;
|
||||||
|
-ms-flex: 1 0 0%;
|
||||||
|
flex: 1 0 0%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 3px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 90%;
|
||||||
|
min-height: 0;
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-header + .rbc-header {
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-header + .rbc-header {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-header > a,
|
||||||
|
.rbc-header > a:active,
|
||||||
|
.rbc-header > a:visited {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.rbc-row-content {
|
||||||
|
position: relative;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.rbc-today {
|
||||||
|
background-color: #eaf6ff;
|
||||||
|
}
|
||||||
|
.rbc-toolbar {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-wrap: wrap;
|
||||||
|
-ms-flex-wrap: wrap;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
-webkit-justify-content: center;
|
||||||
|
-ms-flex-pack: center;
|
||||||
|
justify-content: center;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.rbc-toolbar .rbc-toolbar-label {
|
||||||
|
-webkit-flex-grow: 1;
|
||||||
|
-ms-flex-positive: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button {
|
||||||
|
color: #373a3c;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: none;
|
||||||
|
background-image: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: .375rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button:active,
|
||||||
|
.rbc-toolbar button.rbc-active {
|
||||||
|
background-image: none;
|
||||||
|
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
border-color: #adadad;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button:active:hover,
|
||||||
|
.rbc-toolbar button.rbc-active:hover,
|
||||||
|
.rbc-toolbar button:active:focus,
|
||||||
|
.rbc-toolbar button.rbc-active:focus {
|
||||||
|
color: #373a3c;
|
||||||
|
background-color: #d4d4d4;
|
||||||
|
border-color: #8c8c8c;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button:focus {
|
||||||
|
color: #373a3c;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
border-color: #adadad;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button:hover {
|
||||||
|
color: #373a3c;
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
border-color: #adadad;
|
||||||
|
}
|
||||||
|
.rbc-btn-group {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-btn-group > button:first-child:not(:last-child) {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-btn-group > button:last-child:not(:first-child) {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-btn-group > button:first-child:not(:last-child) {
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-btn-group > button:last-child:not(:first-child) {
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-btn-group > button:not(:first-child):not(:last-child) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-btn-group button + button {
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-btn-group button + button {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
.rbc-btn-group + .rbc-btn-group,
|
||||||
|
.rbc-btn-group + button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.rbc-event {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 5px;
|
||||||
|
background-color: #3174ad;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.rbc-slot-selecting .rbc-event {
|
||||||
|
cursor: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.rbc-event.rbc-selected {
|
||||||
|
background-color: #265985;
|
||||||
|
}
|
||||||
|
.rbc-event:focus {
|
||||||
|
outline: 0px auto #3b99fc;
|
||||||
|
}
|
||||||
|
.rbc-event-label {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
.rbc-event-overlaps {
|
||||||
|
box-shadow: -1px 1px 5px 0px rgba(51, 51, 51, 0.5);
|
||||||
|
}
|
||||||
|
.rbc-event-continues-prior {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-event-continues-after {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-event-continues-earlier {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-event-continues-later {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-row {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: row;
|
||||||
|
-ms-flex-direction: row;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.rbc-row-segment {
|
||||||
|
padding: 0 1px 1px 1px;
|
||||||
|
}
|
||||||
|
.rbc-selected-cell {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.rbc-show-more {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
z-index: 4;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 85%;
|
||||||
|
height: auto;
|
||||||
|
line-height: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-month-view {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-flex: 1 0 0;
|
||||||
|
-ms-flex: 1 0 0px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
width: 100%;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.rbc-month-header {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: row;
|
||||||
|
-ms-flex-direction: row;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.rbc-month-row {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-flex: 1 0 0;
|
||||||
|
-ms-flex: 1 0 0px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
-webkit-flex-basis: 0px;
|
||||||
|
-ms-flex-preferred-size: 0px;
|
||||||
|
flex-basis: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.rbc-month-row + .rbc-month-row {
|
||||||
|
border-top: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-date-cell {
|
||||||
|
-webkit-flex: 1 1 0;
|
||||||
|
-ms-flex: 1 1 0px;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 5px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.rbc-date-cell.rbc-now {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.rbc-date-cell > a,
|
||||||
|
.rbc-date-cell > a:active,
|
||||||
|
.rbc-date-cell > a:visited {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.rbc-row-bg {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: row;
|
||||||
|
-ms-flex-direction: row;
|
||||||
|
flex-direction: row;
|
||||||
|
-webkit-flex: 1 0 0;
|
||||||
|
-ms-flex: 1 0 0px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.rbc-day-bg {
|
||||||
|
-webkit-flex: 1 0 0%;
|
||||||
|
-ms-flex: 1 0 0%;
|
||||||
|
flex: 1 0 0%;
|
||||||
|
}
|
||||||
|
.rbc-day-bg + .rbc-day-bg {
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-day-bg + .rbc-day-bg {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-overlay {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.25);
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.rbc-overlay > * + * {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.rbc-overlay-header {
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
margin: -10px -10px 5px -10px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-flex: 1 0 0;
|
||||||
|
-ms-flex: 1 0 0px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table tbody > tr > td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table .rbc-agenda-time-cell {
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td {
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table tbody > tr + tr {
|
||||||
|
border-top: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table thead > tr > th {
|
||||||
|
padding: 3px 5px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-agenda-view table.rbc-agenda-table thead > tr > th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.rbc-agenda-time-cell {
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.rbc-agenda-time-cell .rbc-continues-after:after {
|
||||||
|
content: ' »';
|
||||||
|
}
|
||||||
|
.rbc-agenda-time-cell .rbc-continues-prior:before {
|
||||||
|
content: '« ';
|
||||||
|
}
|
||||||
|
.rbc-agenda-date-cell,
|
||||||
|
.rbc-agenda-time-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-agenda-event-cell {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.rbc-time-column {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
.rbc-time-column .rbc-timeslot-group {
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.rbc-timeslot-group {
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
min-height: 40px;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-flow: column nowrap;
|
||||||
|
-ms-flex-flow: column nowrap;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
}
|
||||||
|
.rbc-time-gutter,
|
||||||
|
.rbc-header-gutter {
|
||||||
|
-webkit-flex: none;
|
||||||
|
-ms-flex: none;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.rbc-label {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.rbc-day-slot {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-events-container {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-events-container.rbc-is-rtl {
|
||||||
|
left: 10px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-event {
|
||||||
|
border: 1px solid #265985;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
max-height: 100%;
|
||||||
|
min-height: 20px;
|
||||||
|
-webkit-flex-flow: column wrap;
|
||||||
|
-ms-flex-flow: column wrap;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
-webkit-align-items: flex-start;
|
||||||
|
-ms-flex-align: start;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-event-label {
|
||||||
|
-webkit-flex: none;
|
||||||
|
-ms-flex: none;
|
||||||
|
flex: none;
|
||||||
|
padding-right: 5px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-event-content {
|
||||||
|
width: 100%;
|
||||||
|
-webkit-flex: 1 1 0;
|
||||||
|
-ms-flex: 1 1 0px;
|
||||||
|
flex: 1 1 0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-time-slot {
|
||||||
|
border-top: 1px solid #f7f7f7;
|
||||||
|
}
|
||||||
|
.rbc-time-view-resources .rbc-time-gutter,
|
||||||
|
.rbc-time-view-resources .rbc-time-header-gutter {
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
background-color: white;
|
||||||
|
border-right: 1px solid #DDD;
|
||||||
|
z-index: 10;
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
.rbc-time-view-resources .rbc-time-header {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.rbc-time-view-resources .rbc-time-header-content {
|
||||||
|
min-width: auto;
|
||||||
|
-webkit-flex: 1 0 0;
|
||||||
|
-ms-flex: 1 0 0px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
-webkit-flex-basis: 0px;
|
||||||
|
-ms-flex-preferred-size: 0px;
|
||||||
|
flex-basis: 0px;
|
||||||
|
}
|
||||||
|
.rbc-time-view-resources .rbc-day-slot {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
.rbc-time-view-resources .rbc-header,
|
||||||
|
.rbc-time-view-resources .rbc-day-bg {
|
||||||
|
width: 140px;
|
||||||
|
-webkit-flex: 1 1 0;
|
||||||
|
-ms-flex: 1 1 0px;
|
||||||
|
flex: 1 1 0;
|
||||||
|
-webkit-flex-basis: 0 px;
|
||||||
|
-ms-flex-preferred-size: 0 px;
|
||||||
|
flex-basis: 0 px;
|
||||||
|
}
|
||||||
|
.rbc-time-header-content + .rbc-time-header-content {
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
.rbc-time-slot {
|
||||||
|
-webkit-flex: 1 0 0;
|
||||||
|
-ms-flex: 1 0 0px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
.rbc-time-slot.rbc-now {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.rbc-day-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.rbc-slot-selection {
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
font-size: 75%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
.rbc-slot-selecting {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.rbc-time-view {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-time-gutter {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-allday-cell {
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-allday-cell + .rbc-allday-cell {
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-allday-events {
|
||||||
|
position: relative;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-row {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
.rbc-time-header {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex: 0 0 auto;
|
||||||
|
-ms-flex: 0 0 auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
-webkit-flex-direction: row;
|
||||||
|
-ms-flex-direction: row;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.rbc-time-header.rbc-overflowing {
|
||||||
|
border-right: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-time-header.rbc-overflowing {
|
||||||
|
border-right-width: 0;
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-time-header > .rbc-row:first-child {
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-time-header > .rbc-row.rbc-row-resource {
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-time-header-content {
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-time-header-content {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-time-content {
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex: 1 0 0%;
|
||||||
|
-ms-flex: 1 0 0%;
|
||||||
|
flex: 1 0 0%;
|
||||||
|
-webkit-align-items: flex-start;
|
||||||
|
-ms-flex-align: start;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 2px solid #DDD;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.rbc-time-content > .rbc-time-gutter {
|
||||||
|
-webkit-flex: none;
|
||||||
|
-ms-flex: none;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.rbc-time-content > * + * > * {
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-time-content > * + * > * {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.rbc-time-content > .rbc-day-slot {
|
||||||
|
width: 100%;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
.rbc-current-time-indicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #74ad31;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
@ -82,6 +82,7 @@
|
|||||||
|
|
||||||
/* Additional Styles */
|
/* Additional Styles */
|
||||||
@import 'additional-styles/toastify.css';
|
@import 'additional-styles/toastify.css';
|
||||||
|
@import 'additional-styles/react-big-calendar.css';
|
||||||
@import 'component-styles/loading.css';
|
@import 'component-styles/loading.css';
|
||||||
@import 'component-styles/sidebar.css';
|
@import 'component-styles/sidebar.css';
|
||||||
@import 'component-styles/navbar.css';
|
@import 'component-styles/navbar.css';
|
||||||
|
@ -61,6 +61,15 @@ const config: Config = {
|
|||||||
blue: '2px solid rgba(0, 112, 244, 0.5)'
|
blue: '2px solid rgba(0, 112, 244, 0.5)'
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
|
},
|
||||||
'theme-text': {
|
'theme-text': {
|
||||||
principal: '#323338',
|
principal: '#323338',
|
||||||
disabled: '#cbd5e1'
|
disabled: '#cbd5e1'
|
||||||
|
Loading…
Reference in New Issue
Block a user