diff --git a/jsconfig.json b/jsconfig.json
index 0405287..f376ff5 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -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/*"]
}
}
diff --git a/package.json b/package.json
index b628df8..532ba10 100644
--- a/package.json
+++ b/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"
},
diff --git a/src/components/Icons/index.jsx b/src/components/Icons/index.jsx
index 6a72b7a..8ffc2bb 100644
--- a/src/components/Icons/index.jsx
+++ b/src/components/Icons/index.jsx
@@ -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 = {
diff --git a/src/components/v-kanban/components/BoardColumn/boardColumn.types.ts b/src/components/v-kanban/components/BoardColumn/boardColumn.types.ts
new file mode 100644
index 0000000..99a0a33
--- /dev/null
+++ b/src/components/v-kanban/components/BoardColumn/boardColumn.types.ts
@@ -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;
+}
diff --git a/src/components/v-kanban/components/BoardColumn/index.tsx b/src/components/v-kanban/components/BoardColumn/index.tsx
new file mode 100644
index 0000000..76047f1
--- /dev/null
+++ b/src/components/v-kanban/components/BoardColumn/index.tsx
@@ -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 (
+
+
+
+ {column.title}
+
+
+
+
+
+
+
+
+
+
+
+ {tasks.map((task) => (
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+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
+ ? (
+
+ {children}
+
+ )
+ : (
+
+
+ {children}
+
+
+
+ )
+ )
+}
diff --git a/src/components/v-kanban/components/Container/container.type.ts b/src/components/v-kanban/components/Container/container.type.ts
new file mode 100644
index 0000000..762d7cc
--- /dev/null
+++ b/src/components/v-kanban/components/Container/container.type.ts
@@ -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;
+}
diff --git a/src/components/v-kanban/components/Container/index.tsx b/src/components/v-kanban/components/Container/index.tsx
new file mode 100644
index 0000000..779b2c5
--- /dev/null
+++ b/src/components/v-kanban/components/Container/index.tsx
@@ -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 (
+
+
+
+
{title}
+
{description}
+
+
+ Drag Handle
+
+
+
+ {children}
+
+ Add Item
+
+
+ )
+}
+
+export default Container
diff --git a/src/components/v-kanban/components/Item/index.tsx b/src/components/v-kanban/components/Item/index.tsx
new file mode 100644
index 0000000..08464e6
--- /dev/null
+++ b/src/components/v-kanban/components/Item/index.tsx
@@ -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 (
+
+
+ {title}
+
+ Drag Handle
+
+
+
+ )
+}
+
+export default Items
diff --git a/src/components/v-kanban/components/KanbanBoard/index.tsx b/src/components/v-kanban/components/KanbanBoard/index.tsx
new file mode 100644
index 0000000..fba110d
--- /dev/null
+++ b/src/components/v-kanban/components/KanbanBoard/index.tsx
@@ -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(columns)
+ const pickedUpTaskColumn = useRef(null)
+ const columnsId = useMemo(() => _columns.map((col) => col.id), [_columns])
+
+ const [_tasks, setTasks] = useState(tasks)
+
+ const [activeColumn, setActiveColumn] = useState(null)
+
+ const [activeTask, setActiveTask] = useState(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 (
+
+
+ { !searchHeader.hide && }
+
+
+
+
+ {_columns.map((col) => (
+ task.columnId === col.id)}
+ />
+ ))}
+ {}}>
+
+
+
+ Add section
+
+
+
+
+
+
+ {isClient && 'document' in window &&
+ createPortal(
+
+ {activeColumn && (
+ task.columnId === activeColumn.id
+ )}
+ />
+ )}
+ {activeTask && }
+ ,
+ document.body
+ )}
+
+
+ )
+}
diff --git a/src/components/v-kanban/components/KanbanBoard/kanbanBoard.type.ts b/src/components/v-kanban/components/KanbanBoard/kanbanBoard.type.ts
new file mode 100644
index 0000000..9686a39
--- /dev/null
+++ b/src/components/v-kanban/components/KanbanBoard/kanbanBoard.type.ts
@@ -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,
+}
diff --git a/src/components/v-kanban/components/SearchHeader/index.tsx b/src/components/v-kanban/components/SearchHeader/index.tsx
new file mode 100644
index 0000000..bfaedd0
--- /dev/null
+++ b/src/components/v-kanban/components/SearchHeader/index.tsx
@@ -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 (
+
+ {upperButtons}
+ {!hideInput && (
+
+
+ isMobile && setIsFocusedSearch(!isFocusedSearch)}
+ >
+
+
+
+ )}
+ {upperRightButtons}
+ {showNewButton === true && (
+
newButtonOnClick()}
+ >
+ {newButtonIcon || }
+
+ {newButtonText || (i18n.t('common.newItem') !== 'txtNone' ? i18n.t('common.newItem') : 'New Task')}
+
+
+ ) }
+
+ )
+}
+
+export default SearchHeader
diff --git a/src/components/v-kanban/components/TaskCard/index.tsx b/src/components/v-kanban/components/TaskCard/index.tsx
new file mode 100644
index 0000000..2092c86
--- /dev/null
+++ b/src/components/v-kanban/components/TaskCard/index.tsx
@@ -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 (
+
+
+ {renderHeader
+ ? renderHeader(task)
+ : (
+
+ {task.id}
+
+ )}
+
+
+ {renderContent ? renderContent(task) : task.content}
+
+
+ )
+}
diff --git a/src/components/v-kanban/components/TaskCard/taskCard.types.ts b/src/components/v-kanban/components/TaskCard/taskCard.types.ts
new file mode 100644
index 0000000..db5f770
--- /dev/null
+++ b/src/components/v-kanban/components/TaskCard/taskCard.types.ts
@@ -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;
+}
diff --git a/src/components/v-kanban/components/icons.tsx b/src/components/v-kanban/components/icons.tsx
new file mode 100644
index 0000000..4618275
--- /dev/null
+++ b/src/components/v-kanban/components/icons.tsx
@@ -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
+}
diff --git a/src/components/v-kanban/components/ui/badge.tsx b/src/components/v-kanban/components/ui/badge.tsx
new file mode 100644
index 0000000..7533c9f
--- /dev/null
+++ b/src/components/v-kanban/components/ui/badge.tsx
@@ -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,
+ VariantProps {}
+
+function Badge ({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/src/components/v-kanban/components/ui/button.tsx b/src/components/v-kanban/components/ui/button.tsx
new file mode 100644
index 0000000..6921f5f
--- /dev/null
+++ b/src/components/v-kanban/components/ui/button.tsx
@@ -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,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button'
+ return (
+
+ )
+ }
+)
+Button.displayName = 'Button'
+
+export { Button, buttonVariants }
diff --git a/src/components/v-kanban/components/ui/card.tsx b/src/components/v-kanban/components/ui/card.tsx
new file mode 100644
index 0000000..4902b39
--- /dev/null
+++ b/src/components/v-kanban/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = 'Card'
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = 'CardHeader'
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = 'CardTitle'
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = 'CardDescription'
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = 'CardContent'
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = 'CardFooter'
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/components/v-kanban/components/ui/carousel.tsx b/src/components/v-kanban/components/ui/carousel.tsx
new file mode 100644
index 0000000..df75b22
--- /dev/null
+++ b/src/components/v-kanban/components/ui/carousel.tsx
@@ -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;
+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[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel () {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error('useCarousel must be used within a ')
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & 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) => {
+ 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 (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = 'Carousel'
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = 'CarouselContent'
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = 'CarouselItem'
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = 'CarouselPrevious'
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+CarouselNext.displayName = 'CarouselNext'
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext
+}
diff --git a/src/components/v-kanban/components/ui/dropdown-menu.tsx b/src/components/v-kanban/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..26ee53a
--- /dev/null
+++ b/src/components/v-kanban/components/ui/dropdown-menu.tsx
@@ -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,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup
+}
diff --git a/src/components/v-kanban/components/ui/scroll-area.tsx b/src/components/v-kanban/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..ab822aa
--- /dev/null
+++ b/src/components/v-kanban/components/ui/scroll-area.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'vertical', ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/src/components/v-kanban/constants/defaults.ts b/src/components/v-kanban/constants/defaults.ts
new file mode 100644
index 0000000..294689c
--- /dev/null
+++ b/src/components/v-kanban/constants/defaults.ts
@@ -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'
+ }
+]
diff --git a/src/components/v-kanban/hooks/index.js b/src/components/v-kanban/hooks/index.js
new file mode 100644
index 0000000..1b811b7
--- /dev/null
+++ b/src/components/v-kanban/hooks/index.js
@@ -0,0 +1,3 @@
+export { useIsClient } from './useIsClient'
+export { useWindowSize } from './useWindowSize'
+export { useScreenType } from './useScreenType'
diff --git a/src/components/v-kanban/hooks/useIsClient.ts b/src/components/v-kanban/hooks/useIsClient.ts
new file mode 100644
index 0000000..a9587e6
--- /dev/null
+++ b/src/components/v-kanban/hooks/useIsClient.ts
@@ -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 Contenido del componente
;
+ * };
+ *
+ * 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
+}
diff --git a/src/components/v-kanban/hooks/useScreenType.ts b/src/components/v-kanban/hooks/useScreenType.ts
new file mode 100644
index 0000000..21e3007
--- /dev/null
+++ b/src/components/v-kanban/hooks/useScreenType.ts
@@ -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 (
+ *
+ * {screenType.isMobile ? : }
+ *
+ * );
+ * };
+ *
+ * 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(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
+}
diff --git a/src/components/v-kanban/hooks/useWindowSize.ts b/src/components/v-kanban/hooks/useWindowSize.ts
new file mode 100644
index 0000000..1fe1841
--- /dev/null
+++ b/src/components/v-kanban/hooks/useWindowSize.ts
@@ -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 (
+ *
+ *
Ancho: {width}
+ *
Alto: {height}
+ *
+ * )}
+ *
+ * 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
+}
diff --git a/src/components/v-kanban/index.jsx b/src/components/v-kanban/index.jsx
new file mode 100644
index 0000000..64629ae
--- /dev/null
+++ b/src/components/v-kanban/index.jsx
@@ -0,0 +1 @@
+export { KanbanBoard } from './components/KanbanBoard'
diff --git a/src/components/v-kanban/lib/utils.js b/src/components/v-kanban/lib/utils.js
new file mode 100644
index 0000000..d3eec68
--- /dev/null
+++ b/src/components/v-kanban/lib/utils.js
@@ -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)
+}
diff --git a/src/components/v-kanban/multipleContainersKeyboardPreset.ts b/src/components/v-kanban/multipleContainersKeyboardPreset.ts
new file mode 100644
index 0000000..802d648
--- /dev/null
+++ b/src/components/v-kanban/multipleContainersKeyboardPreset.ts
@@ -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
+}
diff --git a/src/components/v-kanban/utils.ts b/src/components/v-kanban/utils.ts
new file mode 100644
index 0000000..3254917
--- /dev/null
+++ b/src/components/v-kanban/utils.ts
@@ -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 (
+ entry: T | null | undefined
+): entry is T & {
+ data: DataRef;
+} {
+ if (!entry) {
+ return false
+ }
+
+ const data = entry.data.current
+
+ if (data?.type === 'Column' || data?.type === 'Task') {
+ return true
+ }
+
+ return false
+}
diff --git a/src/components/vCalendar/index.jsx b/src/components/vCalendar/index.jsx
new file mode 100644
index 0000000..be11ed6
--- /dev/null
+++ b/src/components/vCalendar/index.jsx
@@ -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 (
+
+
{props.event.title}
+ {props.event.observaciones}
+ {/* Do Something */}
+
+ )
+ }
+
+ 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 (
+
+ {/* */}
+
+ this.handleNamvigate(this, 'TODAY')}
+ >
+ Today
+
+
+
+
+ this.handleNamvigate(this, 'PREV')}
+ >
+
+
+ this.handleNamvigate(this, 'NEXT')}
+ >
+
+
+
+
+ {/* */}
+
+ {this.props.label}
+
+
+ {/* */}
+
+ this.handleDayChange(e, this.view)}
+ defaultValue={'week'}
+ >
+ Day
+ Week
+ Month
+ Agenda
+
+
+
+ {/* */}
+
+
+
+ Nuevo
+
+
+
+ )
+ }
+ }
+ }
+
+ 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 (
+
+
+
+ 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)}
+ />
+
)
+}
diff --git a/src/data/menu.json b/src/data/menu.json
index 45774c8..ecb9217 100644
--- a/src/data/menu.json
+++ b/src/data/menu.json
@@ -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
}
]
\ No newline at end of file
diff --git a/src/pages/test/dndkit/[...index].jsx b/src/pages/test/dndkit/[...index].jsx
new file mode 100644
index 0000000..909c48c
--- /dev/null
+++ b/src/pages/test/dndkit/[...index].jsx
@@ -0,0 +1,11 @@
+import VKanban from '@/components/v-kanban'
+
+const DndKitPage = () => {
+ return (
+
+
+
+ )
+}
+
+export default DndKitPage
diff --git a/src/pages/v-calendar/VCalendar/[...index].jsx b/src/pages/v-calendar/VCalendar/[...index].jsx
new file mode 100644
index 0000000..acffe7d
--- /dev/null
+++ b/src/pages/v-calendar/VCalendar/[...index].jsx
@@ -0,0 +1,14 @@
+import VCalendar from '@/components/vCalendar'
+import React from 'react'
+
+const VCalendarPage = () => {
+ return (
+ <>
+ { alert('onSave') }}
+ />
+ >
+ )
+}
+
+export default VCalendarPage
diff --git a/src/pages/v-kanban/KanbanBoard/[...index].jsx b/src/pages/v-kanban/KanbanBoard/[...index].jsx
new file mode 100644
index 0000000..d243bf2
--- /dev/null
+++ b/src/pages/v-kanban/KanbanBoard/[...index].jsx
@@ -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 (
+
+ )
+}
+
+render( )
+`
+
+const KanbanBoardPage = () => {
+ const [code, setCode] = useState(initialCode)
+
+ const scope = {
+ React,
+ useState,
+ KanbanBoard
+ }
+
+ return (
+
+ { setCode(newCode) }} scope={scope} />
+
+ )
+}
+
+export default KanbanBoardPage
diff --git a/src/pages/via-ui/button/[...index].jsx b/src/pages/via-ui/button/[...index].jsx
new file mode 100644
index 0000000..b55bb49
--- /dev/null
+++ b/src/pages/via-ui/button/[...index].jsx
@@ -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 (
+ <>
+
+ Click me!
+
+ >
+ );
+ };
+
+ render( );
+`
+
+const ExampleComponentPage = () => {
+ const [code, setCode] = useState(initialCode)
+
+ const scope = {
+ React,
+ useState,
+ Button
+ }
+
+ return (
+ <>
+
+ { setCode(newCode) }} scope={scope} />
+
+ >
+ )
+}
+
+export default ExampleComponentPage
diff --git a/src/pages/via-ui/stepper/[...index].jsx b/src/pages/via-ui/stepper/[...index].jsx
new file mode 100644
index 0000000..d1c1cea
--- /dev/null
+++ b/src/pages/via-ui/stepper/[...index].jsx
@@ -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 (
+
+
+ {steps.map((step) => (
+ handleStepChange(step)}
+ isClickable={true}
+ >
+ {'Step ' + (steps.indexOf(step) + 1)}
+
+ ))}
+
+
+
+ {'Content for ' + activeStep}
+
+
+ Atrás
+
+
+ Adelante
+
+
+
+
+ );
+};
+
+render( );
+`
+
+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 (
+ <>
+
+ { setCode(newCode) }} scope={scope} />
+
+ >
+ )
+}
+
+export default ExampleComponentPage
diff --git a/src/pages/via-ui/tab/[...index].jsx b/src/pages/via-ui/tab/[...index].jsx
index d465f67..d2c6340 100644
--- a/src/pages/via-ui/tab/[...index].jsx
+++ b/src/pages/via-ui/tab/[...index].jsx
@@ -1,29 +1,55 @@
-import React from 'react'
+import CodePlayground from '@/components/CodePlayground'
+import React, { useState } from 'react'
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
-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 (
+ <>
+
+
+ Datos del Evento
+ Documentos del Evento
+
+
+
+ tabs data
+
+
+
+ tabs docs
+
+
+ >
+ );
+ };
+
+ render( );
+`
+
+const TabExamplePage = () => {
+ const [code, setCode] = useState(initialCode)
+
+ const scope = {
+ React,
+ useState,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger
+ }
return (
-
-
- setTab('data')}>
- Datos del Evento
-
- setTab('docs')}>
- Documentos del Evento
-
-
-
-
- tabs data
-
-
-
- tabs docs
-
-
+ <>
+
+ { setCode(newCode) }} scope={scope} />
+
+ >
)
}
-export default TabPage
+export default TabExamplePage
diff --git a/src/pages/via-ui/test/[...index].jsx b/src/pages/via-ui/test/[...index].jsx
index 72c1a13..d2c5d03 100644
--- a/src/pages/via-ui/test/[...index].jsx
+++ b/src/pages/via-ui/test/[...index].jsx
@@ -1,56 +1,55 @@
-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 initialCode =
-`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
-
- const TabsExample = () => {
- const [tab, setTab] = React.useState('data');
- return (
- <>
-
-
- Datos del Evento
- Documentos del Evento
-
-
-
- tabs data
-
-
-
- tabs docs
-
-
- >
- );
- };
-
- render( );
+const tabComponentCode = `
+ import React, { useState } from 'react';
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab';
`
-const ExampleComponentPage = () => {
- const [code, setCode] = useState(initialCode)
+const files = {
+ '/App.js': {
+ code: `
- // 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
+ const TabsExample = () => {
+ const [tab, setTab] = useState('data');
+ return (
+ <>
+
+
+ Datos del Evento
+ Documentos del Evento
+
+
+
+ tabs data
+
+
+
+ tabs docs
+
+
+ >
+ );
+ };
+
+ export default TabsExample;`
+ },
+ '/node_modules/via-ui/tab/index.js': {
+ hidden: true,
+ code: tabComponentCode
}
+}
+const TestPage = () => {
+ console.log('tabComponentCode', tabComponentCode)
return (
- <>
-
- { setCode(newCode) }} scope={scope} />
-
- >
+
+
+
+
+
+
)
}
-export default ExampleComponentPage
+export default TestPage
diff --git a/src/styles/additional-styles/react-big-calendar.css b/src/styles/additional-styles/react-big-calendar.css
new file mode 100644
index 0000000..760205a
--- /dev/null
+++ b/src/styles/additional-styles/react-big-calendar.css
@@ -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;
+}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 0eb0451..fb74989 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -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';
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 1d30e23..28d39d8 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -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'