feat(v-kanban): v-kanban playground

This commit is contained in:
Jorge Mario Arita Ramírez 2024-01-17 12:20:19 -06:00
parent 27df47803c
commit 1fac998968
41 changed files with 3313 additions and 72 deletions

View File

@ -1,9 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx" ,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"],
"vComponents/*": ["./node_modules/vComponents/dist/components/*"], "vComponents/*": ["./node_modules/vComponents/dist/components/*"],
"VCalendar": ["./node_modules/vCalendar/dist/VCalendar"],
"via-ui/*": ["./node_modules/via-ui/dist/components/*"] "via-ui/*": ["./node_modules/via-ui/dist/components/*"]
} }
} }

View File

@ -29,16 +29,29 @@
"@codemirror/lang-javascript": "^6.2.1", "@codemirror/lang-javascript": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@codesandbox/sandpack-react": "^2.10.0", "@codesandbox/sandpack-react": "^2.10.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^1.7.17", "@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.1.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-scroll-area": "^1.0.5",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/codemirror-theme-monokai": "^4.21.21", "@uiw/codemirror-theme-monokai": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21", "@uiw/react-codemirror": "^4.21.21",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"config": "^3.3.9", "config": "^3.3.9",
"embla-carousel-react": "^8.0.0-rc19",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"js-md5": "^0.8.3", "js-md5": "^0.8.3",
"next": "14.0.4", "next": "14.0.4",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"pristinejs": "^1.1.0", "pristinejs": "^1.1.0",
"react": "^18", "react": "^18",
"react-beautiful-dnd": "^13.1.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18", "react-dom": "^18",
"react-dropzone-component": "^3.2.0", "react-dropzone-component": "^3.2.0",
"react-icons": "^4.12.0", "react-icons": "^4.12.0",
@ -49,6 +62,8 @@
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"rosetta": "^1.1.0", "rosetta": "^1.1.0",
"v-functions": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-functions.git", "v-functions": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-functions.git",
"v-kanban": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-kanban.git#dev_ja",
"vCalendar": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-calendar.git",
"vComponents": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-components.git#dev_ja", "vComponents": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/v-components.git#dev_ja",
"via-ui": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/via-ui.git#dev_ja" "via-ui": "git+https://2bdcc0300e0ed5ac01f9dcd51368f7ac74fdb85a@git.via-asesores.com/libraries/via-ui.git#dev_ja"
}, },

View File

@ -2,7 +2,7 @@ import { BsSearch } from 'react-icons/bs'
import { IoEllipsisVertical, IoFilter } from 'react-icons/io5' import { IoEllipsisVertical, IoFilter } from 'react-icons/io5'
import { DocumentPlusIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/solid' import { DocumentPlusIcon, EyeIcon, PencilSquareIcon } from '@heroicons/react/24/solid'
import { FaChevronLeft, FaChevronRight, FaPaperclip, FaUser } from 'react-icons/fa' import { FaChevronLeft, FaChevronRight, FaPaperclip, FaUser } from 'react-icons/fa'
import { RiEyeLine } from 'react-icons/ri' // import { RiEyeLine } from 'react-icons/ri'
import { LuEye, LuCode2, LuClipboardList } from 'react-icons/lu' import { LuEye, LuCode2, LuClipboardList } from 'react-icons/lu'
export const Icons = { export const Icons = {

View File

@ -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;
}

View 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>
)
)
}

View File

@ -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;
}

View 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

View 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

View 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>
)
}

View File

@ -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,
}

View 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

View 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>
)
}

View File

@ -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;
}

View 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
}

View 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 }

View 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 }

View 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 }

View 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
}

View 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
}

View 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 }

View 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'
}
]

View File

@ -0,0 +1,3 @@
export { useIsClient } from './useIsClient'
export { useWindowSize } from './useWindowSize'
export { useScreenType } from './useScreenType'

View 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
}

View 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
}

View 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
}

View File

@ -0,0 +1 @@
export { KanbanBoard } from './components/KanbanBoard'

View 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)
}

View 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
}

View 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
}

View 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>)
}

View File

@ -39,13 +39,23 @@
"id_menu_padre": null, "id_menu_padre": null,
"badge": "New" "badge": "New"
}, },
{
"id_menu": "button",
"title": "Button",
"descripcion": null,
"icon": "FcRotateToPortrait",
"path": "/via-ui/button",
"orden": 2.1,
"id_menu_padre": "via-ui",
"badge": null
},
{ {
"id_menu": "tooltip", "id_menu": "tooltip",
"title": "Tooltip", "title": "Tooltip",
"descripcion": null, "descripcion": null,
"icon": "FcMms", "icon": "FcMms",
"path": "/via-ui/tooltip", "path": "/via-ui/tooltip",
"orden": 2.1, "orden": 2.2,
"id_menu_padre": "via-ui", "id_menu_padre": "via-ui",
"badge": null "badge": null
}, },
@ -55,7 +65,17 @@
"descripcion": null, "descripcion": null,
"icon": "FcTemplate", "icon": "FcTemplate",
"path": "/via-ui/tab", "path": "/via-ui/tab",
"orden": 2.1, "orden": 2.3,
"id_menu_padre": "via-ui",
"badge": null
},
{
"id_menu": "stepper",
"title": "Stepper",
"descripcion": null,
"icon": "FcSerialTasks",
"path": "/via-ui/stepper",
"orden": 2.4,
"id_menu_padre": "via-ui", "id_menu_padre": "via-ui",
"badge": null "badge": null
}, },
@ -65,8 +85,68 @@
"descripcion": null, "descripcion": null,
"icon": "FcIdea", "icon": "FcIdea",
"path": "/via-ui/test", "path": "/via-ui/test",
"orden": 2.1, "orden": 2.40,
"id_menu_padre": "via-ui", "id_menu_padre": "via-ui",
"badge": null "badge": null
},
{
"id_menu": "v-calendar",
"title": "v-calendar",
"descripcion": null,
"icon": "FcCalendar",
"path": null,
"orden": 3,
"id_menu_padre": null,
"badge": null
},
{
"id_menu": "VCalendar",
"title": "VCalendar",
"descripcion": null,
"icon": "FcCalendar",
"path": "/v-calendar/VCalendar",
"orden": 3.1,
"id_menu_padre": "v-calendar",
"badge": null
},
{
"id_menu": "v-kanban",
"title": "v-kanban",
"descripcion": null,
"icon": "FcTemplate",
"path": null,
"orden": 4,
"id_menu_padre": null,
"badge": null
},
{
"id_menu": "KanbanBoard",
"title": "KanbanBoard",
"descripcion": null,
"icon": "FcTemplate",
"path": "/v-kanban/KanbanBoard",
"orden": 4.1,
"id_menu_padre": "v-kanban",
"badge": null
},
{
"id_menu": "test",
"title": "Test",
"descripcion": null,
"icon": "FcBiomass",
"path": null,
"orden": 5,
"id_menu_padre": null,
"badge": null
},
{
"id_menu": "dndkit",
"title": "dnd kit",
"descripcion": null,
"icon": "FcCollect",
"path": "/test/dndkit",
"orden": 5.1,
"id_menu_padre": "test",
"badge": null
} }
] ]

View File

@ -0,0 +1,11 @@
import VKanban from '@/components/v-kanban'
const DndKitPage = () => {
return (
<div>
<VKanban />
</div>
)
}
export default DndKitPage

View File

@ -0,0 +1,14 @@
import VCalendar from '@/components/vCalendar'
import React from 'react'
const VCalendarPage = () => {
return (
<>
<VCalendar
onSave={() => { alert('onSave') }}
/>
</>
)
}
export default VCalendarPage

View 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

View 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

View 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

View File

@ -1,29 +1,55 @@
import React from 'react' import CodePlayground from '@/components/CodePlayground'
import React, { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab' import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
const TabPage = () => { const initialCode =
const [tab, setTab] = React.useState('data') `// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
const TabsExample = () => {
const [tab, setTab] = React.useState('data');
return (
<>
<Tabs activeValue={tab} onChange={setTab}>
<TabsList>
<TabsTrigger value="data">Datos del Evento</TabsTrigger>
<TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
</TabsList>
<TabsContent value="data">
tabs data
</TabsContent>
<TabsContent value="docs">
tabs docs
</TabsContent>
</Tabs>
</>
);
};
render(<TabsExample />);
`
const TabExamplePage = () => {
const [code, setCode] = useState(initialCode)
const scope = {
React,
useState,
Tabs,
TabsContent,
TabsList,
TabsTrigger
}
return ( return (
<Tabs activeValue={tab}> <>
<TabsList> <div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'>
<TabsTrigger value="data" onClick={() => setTab('data')}> <CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} />
Datos del Evento </div>
</TabsTrigger> </>
<TabsTrigger value="docs" onClick={() => setTab('docs')}>
Documentos del Evento
</TabsTrigger>
</TabsList>
<TabsContent value="data">
tabs data
</TabsContent>
<TabsContent value="docs">
tabs docs
</TabsContent>
</Tabs>
) )
} }
export default TabPage export default TabExamplePage

View File

@ -1,56 +1,55 @@
import CodePlayground from '@/components/CodePlayground' import React from 'react'
import React, { useState } from 'react' import { SandpackProvider, SandpackLayout, SandpackCodeEditor, SandpackPreview } from '@codesandbox/sandpack-react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab' const tabComponentCode = `
import React, { useState } from 'react';
const initialCode = import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab';
`// import { Tabs, TabsContent, TabsList, TabsTrigger } from 'via-ui/tab'
const TabsExample = () => {
const [tab, setTab] = React.useState('data');
return (
<>
<Tabs activeValue={tab} onChange={setTab}>
<TabsList>
<TabsTrigger value="data">Datos del Evento</TabsTrigger>
<TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
</TabsList>
<TabsContent value="data">
tabs data
</TabsContent>
<TabsContent value="docs">
tabs docs
</TabsContent>
</Tabs>
</>
);
};
render(<TabsExample />);
` `
const ExampleComponentPage = () => { const files = {
const [code, setCode] = useState(initialCode) '/App.js': {
code: `
// El objeto scope debería incluir todos los componentes y funciones que el código JSX necesita const TabsExample = () => {
const scope = { const [tab, setTab] = useState('data');
React, return (
useState, <>
Tabs, <Tabs activeValue={tab} onChange={setTab}>
TabsContent, <TabsList>
TabsList, <TabsTrigger value="data">Datos del Evento</TabsTrigger>
TabsTrigger <TabsTrigger value="docs">Documentos del Evento</TabsTrigger>
</TabsList>
<TabsContent value="data">
tabs data
</TabsContent>
<TabsContent value="docs">
tabs docs
</TabsContent>
</Tabs>
</>
);
};
export default TabsExample;`
},
'/node_modules/via-ui/tab/index.js': {
hidden: true,
code: tabComponentCode
} }
}
const TestPage = () => {
console.log('tabComponentCode', tabComponentCode)
return ( return (
<> <SandpackProvider template="react" files={files}>
<div className='rounded-lg bg-white p-4 shadow-lg dark:!bg-[#222222]'> <SandpackLayout>
<CodePlayground code={code} onChange={(newCode) => { setCode(newCode) }} scope={scope} /> <SandpackCodeEditor />
</div> <SandpackPreview />
</> </SandpackLayout>
</SandpackProvider>
) )
} }
export default ExampleComponentPage export default TestPage

View 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;
}

View File

@ -82,6 +82,7 @@
/* Additional Styles */ /* Additional Styles */
@import 'additional-styles/toastify.css'; @import 'additional-styles/toastify.css';
@import 'additional-styles/react-big-calendar.css';
@import 'component-styles/loading.css'; @import 'component-styles/loading.css';
@import 'component-styles/sidebar.css'; @import 'component-styles/sidebar.css';
@import 'component-styles/navbar.css'; @import 'component-styles/navbar.css';

View File

@ -61,6 +61,15 @@ const config: Config = {
blue: '2px solid rgba(0, 112, 244, 0.5)' blue: '2px solid rgba(0, 112, 244, 0.5)'
}, },
colors: { colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
'theme-text': { 'theme-text': {
principal: '#323338', principal: '#323338',
disabled: '#cbd5e1' disabled: '#cbd5e1'