feat(v-kanban): ✨ v-kanban playground
This commit is contained in:
parent
27df47803c
commit
1fac998968
@ -1,9 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx" ,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"vComponents/*": ["./node_modules/vComponents/dist/components/*"],
|
||||
"VCalendar": ["./node_modules/vCalendar/dist/VCalendar"],
|
||||
"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/theme-one-dark": "^6.1.2",
|
||||
"@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",
|
||||
"@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/react-codemirror": "^4.21.21",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"config": "^3.3.9",
|
||||
"embla-carousel-react": "^8.0.0-rc19",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-md5": "^0.8.3",
|
||||
"next": "14.0.4",
|
||||
"node-forge": "^1.3.1",
|
||||
"pristinejs": "^1.1.0",
|
||||
"react": "^18",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18",
|
||||
"react-dropzone-component": "^3.2.0",
|
||||
"react-icons": "^4.12.0",
|
||||
@ -49,6 +62,8 @@
|
||||
"react-toastify": "^9.1.3",
|
||||
"rosetta": "^1.1.0",
|
||||
"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",
|
||||
"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 { DocumentPlusIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/solid'
|
||||
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'
|
||||
|
||||
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,
|
||||
"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",
|
||||
"title": "Tooltip",
|
||||
"descripcion": null,
|
||||
"icon": "FcMms",
|
||||
"path": "/via-ui/tooltip",
|
||||
"orden": 2.1,
|
||||
"orden": 2.2,
|
||||
"id_menu_padre": "via-ui",
|
||||
"badge": null
|
||||
},
|
||||
@ -55,7 +65,17 @@
|
||||
"descripcion": null,
|
||||
"icon": "FcTemplate",
|
||||
"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",
|
||||
"badge": null
|
||||
},
|
||||
@ -65,8 +85,68 @@
|
||||
"descripcion": null,
|
||||
"icon": "FcIdea",
|
||||
"path": "/via-ui/test",
|
||||
"orden": 2.1,
|
||||
"orden": 2.40,
|
||||
"id_menu_padre": "via-ui",
|
||||
"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,18 +1,19 @@
|
||||
import React from 'react'
|
||||
import CodePlayground from '@/components/CodePlayground'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
||||
|
||||
const TabPage = () => {
|
||||
const [tab, setTab] = React.useState('data')
|
||||
const initialCode =
|
||||
`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
||||
|
||||
const TabsExample = () => {
|
||||
const [tab, setTab] = React.useState('data');
|
||||
return (
|
||||
<Tabs activeValue={tab}>
|
||||
<>
|
||||
<Tabs activeValue={tab} onChange={setTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="data" onClick={() => setTab('data')}>
|
||||
Datos del Evento
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="docs" onClick={() => setTab('docs')}>
|
||||
Documentos del Evento
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="data">Datos del Evento</TabsTrigger>
|
||||
<TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="data">
|
||||
@ -23,7 +24,32 @@ const TabPage = () => {
|
||||
tabs docs
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render(<TabsExample />);
|
||||
`
|
||||
|
||||
const TabExamplePage = () => {
|
||||
const [code, setCode] = useState(initialCode)
|
||||
|
||||
const scope = {
|
||||
React,
|
||||
useState,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
}
|
||||
|
||||
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 TabPage
|
||||
export default TabExamplePage
|
||||
|
@ -1,13 +1,17 @@
|
||||
import CodePlayground from '@/components/CodePlayground'
|
||||
import React, { useState } from 'react'
|
||||
import React 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';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab';
|
||||
`
|
||||
|
||||
const initialCode =
|
||||
`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
|
||||
const files = {
|
||||
'/App.js': {
|
||||
code: `
|
||||
|
||||
const TabsExample = () => {
|
||||
const [tab, setTab] = React.useState('data');
|
||||
const [tab, setTab] = useState('data');
|
||||
return (
|
||||
<>
|
||||
<Tabs activeValue={tab} onChange={setTab}>
|
||||
@ -28,29 +32,24 @@ const initialCode =
|
||||
);
|
||||
};
|
||||
|
||||
render(<TabsExample />);
|
||||
`
|
||||
|
||||
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,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
export default TabsExample;`
|
||||
},
|
||||
'/node_modules/via-ui/tab/index.js': {
|
||||
hidden: true,
|
||||
code: tabComponentCode
|
||||
}
|
||||
}
|
||||
|
||||
const TestPage = () => {
|
||||
console.log('tabComponentCode', tabComponentCode)
|
||||
return (
|
||||
<>
|
||||
<div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'>
|
||||
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
|
||||
</div>
|
||||
</>
|
||||
<SandpackProvider template="react" files={files}>
|
||||
<SandpackLayout>
|
||||
<SandpackCodeEditor />
|
||||
<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 */
|
||||
@import 'additional-styles/toastify.css';
|
||||
@import 'additional-styles/react-big-calendar.css';
|
||||
@import 'component-styles/loading.css';
|
||||
@import 'component-styles/sidebar.css';
|
||||
@import 'component-styles/navbar.css';
|
||||
|
@ -61,6 +61,15 @@ const config: Config = {
|
||||
blue: '2px solid rgba(0, 112, 244, 0.5)'
|
||||
},
|
||||
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': {
|
||||
principal: '#323338',
|
||||
disabled: '#cbd5e1'
|
||||
|
Loading…
Reference in New Issue
Block a user