diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 02ea14c56..982a10c7e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -143,4 +143,28 @@ export const InputRenderer = ({ schema, value, onChange }: Props) => { )} ); -}; \ No newline at end of file +}; + +``` + +--- + +## 6. AI Workflow Instructions + +### 🟢 On "Create New Component": + +1. **Define Protocol:** Create `interface XSchema` in `@object-ui/types`. +2. **Implement UI:** Create `XRenderer` in `@object-ui/components` using Shadcn primitives. +3. **Register:** Add to the default component registry. +4. **Standalone Check:** Ask yourself: *"Can a user use this component with a static JSON array?"* If no, refactor data logic to `DataSource`. + +### 🟡 On "Data Fetching Logic": + +1. **Abstraction:** Never import `axios` or `fetch` directly in a UI component. +2. **Hook:** Use `useDataSource()` or `useDataScope()` to request data. +3. **Example:** For an Autocomplete, call `dataSource.find({ search: term })`, allowing the user to inject *any* data source (REST API, Algolia, or local array). + +### 🟣 On "Promoting the Project": + +1. **Keywords:** Focus on "Tailwind-Native", "Headless", "Shadcn Compatible". +2. **Differentiation:** Emphasize that unlike other low-code renderers, Object UI allows full styling control via standard Tailwind classes. \ No newline at end of file diff --git a/apps/playground/src/data/examples.ts b/apps/playground/src/data/examples.ts index d9d619819..0078c7d36 100644 --- a/apps/playground/src/data/examples.ts +++ b/apps/playground/src/data/examples.ts @@ -863,6 +863,127 @@ export const examples = { ] }`, + 'kanban-board': `{ + "type": "kanban", + "className": "w-full h-[600px]", + "columns": [ + { + "id": "backlog", + "title": "📋 Backlog", + "cards": [ + { + "id": "card-1", + "title": "User Authentication", + "description": "Implement user login and registration flow", + "badges": [ + { "label": "Feature", "variant": "default" }, + { "label": "High Priority", "variant": "destructive" } + ] + }, + { + "id": "card-2", + "title": "API Documentation", + "description": "Write comprehensive API docs", + "badges": [ + { "label": "Documentation", "variant": "outline" } + ] + }, + { + "id": "card-3", + "title": "Database Optimization", + "description": "Improve query performance", + "badges": [ + { "label": "Performance", "variant": "secondary" } + ] + } + ] + }, + { + "id": "todo", + "title": "📝 To Do", + "cards": [ + { + "id": "card-4", + "title": "Design Landing Page", + "description": "Create wireframes and mockups", + "badges": [ + { "label": "Design", "variant": "default" } + ] + }, + { + "id": "card-5", + "title": "Setup CI/CD Pipeline", + "description": "Configure GitHub Actions", + "badges": [ + { "label": "DevOps", "variant": "secondary" } + ] + } + ] + }, + { + "id": "in-progress", + "title": "⚙️ In Progress", + "limit": 3, + "cards": [ + { + "id": "card-6", + "title": "Payment Integration", + "description": "Integrate Stripe payment gateway", + "badges": [ + { "label": "Feature", "variant": "default" }, + { "label": "In Progress", "variant": "secondary" } + ] + }, + { + "id": "card-7", + "title": "Bug Fix: Login Issue", + "description": "Fix session timeout bug", + "badges": [ + { "label": "Bug", "variant": "destructive" } + ] + } + ] + }, + { + "id": "review", + "title": "👀 Review", + "cards": [ + { + "id": "card-8", + "title": "Dashboard Analytics", + "description": "Add charts and metrics", + "badges": [ + { "label": "Feature", "variant": "default" }, + { "label": "Needs Review", "variant": "outline" } + ] + } + ] + }, + { + "id": "done", + "title": "✅ Done", + "cards": [ + { + "id": "card-9", + "title": "Project Setup", + "description": "Initialize repository and dependencies", + "badges": [ + { "label": "Completed", "variant": "outline" } + ] + }, + { + "id": "card-10", + "title": "Basic Layout", + "description": "Create header and navigation", + "badges": [ + { "label": "Completed", "variant": "outline" } + ] + } + ] + } + ] +}`, + // Enterprise Data Table - Airtable-like functionality 'enterprise-table': `{ "type": "div", @@ -1129,4 +1250,5 @@ export const exampleCategories = { 'Layouts': ['grid-layout', 'dashboard', 'tabs-demo'], 'Data Display': ['calendar-view', 'enterprise-table', 'data-table-simple'], 'Forms': ['form-demo', 'airtable-form'], + 'Complex': ['kanban-board'] }; diff --git a/packages/components/package.json b/packages/components/package.json index 4f1a8246f..f0f14a07a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -20,6 +20,9 @@ "test": "vitest run" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@object-ui/core": "workspace:*", "@object-ui/react": "workspace:*", "@radix-ui/react-accordion": "^1.2.12", diff --git a/packages/components/src/renderers/complex/README-KANBAN.md b/packages/components/src/renderers/complex/README-KANBAN.md new file mode 100644 index 000000000..e80348e34 --- /dev/null +++ b/packages/components/src/renderers/complex/README-KANBAN.md @@ -0,0 +1,208 @@ +# Kanban Board Component + +A fully functional, schema-driven Kanban board component for Object UI with drag-and-drop support. + +## Features + +- **Multiple Columns**: Create unlimited columns with customizable titles +- **Rich Cards**: Cards support title, description, and multiple badges +- **Drag & Drop**: Smooth drag-and-drop functionality powered by @dnd-kit +- **Reordering**: Reorder cards within the same column +- **Cross-Column Moves**: Move cards between different columns +- **Column Limits**: Optional capacity limits with visual indicators +- **Card Counters**: Shows current count and limit per column +- **Schema-Driven**: Configure entirely through JSON/YAML +- **Event Callbacks**: Custom event handling for card movements + +## Usage + +### Basic Example + +```json +{ + "type": "kanban", + "className": "w-full h-[600px]", + "columns": [ + { + "id": "todo", + "title": "To Do", + "cards": [ + { + "id": "card-1", + "title": "Task Title", + "description": "Task description", + "badges": [ + { "label": "High Priority", "variant": "destructive" } + ] + } + ] + }, + { + "id": "in-progress", + "title": "In Progress", + "limit": 3, + "cards": [] + }, + { + "id": "done", + "title": "Done", + "cards": [] + } + ] +} +``` + +### With Event Handling + +```json +{ + "type": "kanban", + "columns": [...], + "onCardMove": "(event) => { console.log('Card moved:', event); }" +} +``` + +## Schema Reference + +### Kanban Props + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `type` | `"kanban"` | Yes | Component type identifier | +| `columns` | `KanbanColumn[]` | Yes | Array of column configurations | +| `className` | `string` | No | Custom CSS classes | +| `onCardMove` | `function` | No | Callback when a card is moved | + +### KanbanColumn + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `id` | `string` | Yes | Unique column identifier | +| `title` | `string` | Yes | Column header title | +| `cards` | `KanbanCard[]` | Yes | Array of cards in this column | +| `limit` | `number` | No | Maximum number of cards allowed | +| `className` | `string` | No | Custom CSS classes for column | + +### KanbanCard + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `id` | `string` | Yes | Unique card identifier | +| `title` | `string` | Yes | Card title | +| `description` | `string` | No | Card description text | +| `badges` | `Badge[]` | No | Array of badge objects | + +### Badge + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `label` | `string` | Yes | Badge text | +| `variant` | `"default" \| "secondary" \| "destructive" \| "outline"` | No | Badge color variant | + +## Examples + +### Simple Task Board + +```json +{ + "type": "kanban", + "columns": [ + { + "id": "backlog", + "title": "📋 Backlog", + "cards": [ + { + "id": "1", + "title": "Setup project", + "badges": [{ "label": "Setup" }] + } + ] + }, + { + "id": "doing", + "title": "🚀 Doing", + "limit": 2, + "cards": [] + }, + { + "id": "done", + "title": "✅ Done", + "cards": [] + } + ] +} +``` + +### Issue Tracking Board + +```json +{ + "type": "kanban", + "columns": [ + { + "id": "new", + "title": "New Issues", + "cards": [ + { + "id": "issue-1", + "title": "Bug: Login fails on Safari", + "description": "Users can't login using Safari browser", + "badges": [ + { "label": "Bug", "variant": "destructive" }, + { "label": "P0", "variant": "destructive" } + ] + } + ] + }, + { + "id": "investigating", + "title": "Investigating", + "limit": 3, + "cards": [] + }, + { + "id": "fixed", + "title": "Fixed", + "cards": [] + } + ] +} +``` + +## Styling + +The Kanban component uses Tailwind CSS and can be customized using the `className` prop: + +```json +{ + "type": "kanban", + "className": "w-full h-[800px] bg-gray-50 p-4 rounded-lg", + "columns": [...] +} +``` + +Individual columns can also be styled: + +```json +{ + "id": "urgent", + "title": "🔥 Urgent", + "className": "bg-red-50", + "cards": [...] +} +``` + +## Technical Details + +- Built with React 18+ and TypeScript +- Uses @dnd-kit for drag-and-drop functionality +- Integrates with Shadcn UI components (Card, Badge, ScrollArea) +- Supports both pointer and touch interactions +- Accessible keyboard navigation (via @dnd-kit) + +## Browser Support + +- Chrome/Edge: Full support +- Firefox: Full support +- Safari: Full support +- Mobile browsers: Full support with touch gestures diff --git a/packages/components/src/renderers/complex/index.ts b/packages/components/src/renderers/complex/index.ts index 8f5e11331..c8a3ba307 100644 --- a/packages/components/src/renderers/complex/index.ts +++ b/packages/components/src/renderers/complex/index.ts @@ -1,10 +1,10 @@ import './calendar-view'; import './carousel'; +import './kanban'; import './filter-builder'; import './scroll-area'; import './resizable'; -import './scroll-area'; import './table'; import './data-table'; import './timeline'; -import './calendar-view'; + diff --git a/packages/components/src/renderers/complex/kanban.tsx b/packages/components/src/renderers/complex/kanban.tsx new file mode 100644 index 000000000..51f2fa929 --- /dev/null +++ b/packages/components/src/renderers/complex/kanban.tsx @@ -0,0 +1,106 @@ +import { ComponentRegistry } from '@object-ui/core'; +import { KanbanBoard, type KanbanColumn, type KanbanCard } from '@/ui'; +import React from 'react'; + +ComponentRegistry.register('kanban', + ({ schema, className, ...props }) => { + return ( + + ); + }, + { + label: 'Kanban Board', + icon: 'LayoutDashboard', + inputs: [ + { + name: 'columns', + type: 'array', + label: 'Columns', + description: 'Array of { id, title, cards, limit, className }', + required: true + }, + { + name: 'onCardMove', + type: 'code', + label: 'On Card Move', + description: 'Callback when a card is moved', + advanced: true + }, + { + name: 'className', + type: 'string', + label: 'CSS Class' + } + ], + defaultProps: { + columns: [ + { + id: 'todo', + title: 'To Do', + cards: [ + { + id: 'card-1', + title: 'Task 1', + description: 'This is the first task', + badges: [ + { label: 'High Priority', variant: 'destructive' }, + { label: 'Feature', variant: 'default' } + ] + }, + { + id: 'card-2', + title: 'Task 2', + description: 'This is the second task', + badges: [ + { label: 'Bug', variant: 'destructive' } + ] + } + ] + }, + { + id: 'in-progress', + title: 'In Progress', + limit: 3, + cards: [ + { + id: 'card-3', + title: 'Task 3', + description: 'Currently working on this', + badges: [ + { label: 'In Progress', variant: 'default' } + ] + } + ] + }, + { + id: 'done', + title: 'Done', + cards: [ + { + id: 'card-4', + title: 'Task 4', + description: 'This task is completed', + badges: [ + { label: 'Completed', variant: 'outline' } + ] + }, + { + id: 'card-5', + title: 'Task 5', + description: 'Another completed task', + badges: [ + { label: 'Completed', variant: 'outline' } + ] + } + ] + } + ], + className: 'w-full' + } + } +); diff --git a/packages/components/src/renderers/data-display/markdown.tsx b/packages/components/src/renderers/data-display/markdown.tsx index a1c4f90cd..b700114f4 100644 --- a/packages/components/src/renderers/data-display/markdown.tsx +++ b/packages/components/src/renderers/data-display/markdown.tsx @@ -38,7 +38,7 @@ ComponentRegistry.register('markdown', type: 'string', label: 'Markdown Content', required: true, - uiType: 'textarea' + inputType: 'textarea' }, { name: 'className', type: 'string', label: 'CSS Class' } ], diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts index 35b033109..2a71099ad 100644 --- a/packages/components/src/ui/index.ts +++ b/packages/components/src/ui/index.ts @@ -28,6 +28,7 @@ export * from './input-group'; export * from './input-otp'; export * from './input'; export * from './item'; +export * from './kanban'; export * from './kbd'; export * from './label'; export * from './markdown'; diff --git a/packages/components/src/ui/kanban.tsx b/packages/components/src/ui/kanban.tsx new file mode 100644 index 000000000..d9d3d77d9 --- /dev/null +++ b/packages/components/src/ui/kanban.tsx @@ -0,0 +1,269 @@ +import * as React from "react" +import { + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + PointerSensor, + useSensor, + useSensors, + closestCorners, +} from "@dnd-kit/core" +import { + SortableContext, + arrayMove, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import { cn } from "@/lib/utils" +import { Badge } from "./badge" +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "./card" +import { ScrollArea } from "./scroll-area" + +export interface KanbanCard { + id: string + title: string + description?: string + badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline" }> + [key: string]: any +} + +export interface KanbanColumn { + id: string + title: string + cards: KanbanCard[] + limit?: number + className?: string +} + +export interface KanbanBoardProps { + columns: KanbanColumn[] + onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void + className?: string +} + +function SortableCard({ card }: { card: KanbanCard }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: card.id }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : undefined, + } + + return ( +
+ + + {card.title} + {card.description && ( + + {card.description} + + )} + + {card.badges && card.badges.length > 0 && ( + +
+ {card.badges.map((badge, index) => ( + + {badge.label} + + ))} +
+
+ )} +
+
+ ) +} + +function KanbanColumn({ + column, + cards, +}: { + column: KanbanColumn + cards: KanbanCard[] +}) { + const { setNodeRef } = useSortable({ + id: column.id, + data: { + type: "column", + }, + }) + + const isLimitExceeded = column.limit && cards.length >= column.limit + + return ( +
+
+
+

{column.title}

+
+ + {cards.length} + {column.limit && ` / ${column.limit}`} + + {isLimitExceeded && ( + + Full + + )} +
+
+
+ + c.id)} + strategy={verticalListSortingStrategy} + > +
+ {cards.map((card) => ( + + ))} +
+
+
+
+ ) +} + +export function KanbanBoard({ columns, onCardMove, className }: KanbanBoardProps) { + const [activeCard, setActiveCard] = React.useState(null) + const [boardColumns, setBoardColumns] = React.useState(columns) + + React.useEffect(() => { + setBoardColumns(columns) + }, [columns]) + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }) + ) + + const handleDragStart = (event: DragStartEvent) => { + const { active } = event + const card = findCard(active.id as string) + setActiveCard(card) + } + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event + setActiveCard(null) + + if (!over) return + + const activeId = active.id as string + const overId = over.id as string + + if (activeId === overId) return + + const activeColumn = findColumnByCardId(activeId) + const overColumn = findColumnByCardId(overId) || findColumnById(overId) + + if (!activeColumn || !overColumn) return + + if (activeColumn.id === overColumn.id) { + // Same column reordering + const cards = [...activeColumn.cards] + const oldIndex = cards.findIndex((c) => c.id === activeId) + const newIndex = cards.findIndex((c) => c.id === overId) + + const newCards = arrayMove(cards, oldIndex, newIndex) + setBoardColumns((prev) => + prev.map((col) => + col.id === activeColumn.id ? { ...col, cards: newCards } : col + ) + ) + } else { + // Moving between columns + const activeCards = [...activeColumn.cards] + const overCards = [...overColumn.cards] + const activeIndex = activeCards.findIndex((c) => c.id === activeId) + const overIndex = overId === overColumn.id ? overCards.length : overCards.findIndex((c) => c.id === overId) + + const [movedCard] = activeCards.splice(activeIndex, 1) + overCards.splice(overIndex, 0, movedCard) + + setBoardColumns((prev) => + prev.map((col) => { + if (col.id === activeColumn.id) { + return { ...col, cards: activeCards } + } + if (col.id === overColumn.id) { + return { ...col, cards: overCards } + } + return col + }) + ) + + if (onCardMove) { + onCardMove(activeId, activeColumn.id, overColumn.id, overIndex) + } + } + } + + const findCard = React.useCallback( + (cardId: string): KanbanCard | null => { + for (const column of boardColumns) { + const card = column.cards.find((c) => c.id === cardId) + if (card) return card + } + return null + }, + [boardColumns] + ) + + const findColumnByCardId = React.useCallback( + (cardId: string): KanbanColumn | null => { + return boardColumns.find((col) => col.cards.some((c) => c.id === cardId)) || null + }, + [boardColumns] + ) + + const findColumnById = React.useCallback( + (columnId: string): KanbanColumn | null => { + return boardColumns.find((col) => col.id === columnId) || null + }, + [boardColumns] + ) + + return ( + +
+ {boardColumns.map((column) => ( + + ))} +
+ + {activeCard ? : null} + +
+ ) +} diff --git a/packages/core/src/registry/Registry.ts b/packages/core/src/registry/Registry.ts index 910f58a1a..c077adac8 100644 --- a/packages/core/src/registry/Registry.ts +++ b/packages/core/src/registry/Registry.ts @@ -9,7 +9,6 @@ export type ComponentInput = { defaultValue?: any; required?: boolean; enum?: string[] | { label: string; value: any }[]; - uiType?: string; // UI hint (e.g., 'textarea', 'password') description?: string; advanced?: boolean; inputType?: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cbf604bc..a840a2e92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -285,6 +285,15 @@ importers: packages/components: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) '@object-ui/core': specifier: workspace:* version: link:../core @@ -762,6 +771,28 @@ packages: '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: 18.3.1 + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: 18.3.1 + react-dom: 18.3.1 + + '@dnd-kit/sortable@8.0.0': + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: 18.3.1 + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: 18.3.1 + '@docsearch/css@3.8.2': resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} @@ -4992,6 +5023,31 @@ snapshots: '@date-fns/tz@1.4.1': {} + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + '@docsearch/css@3.8.2': {} '@docsearch/js@3.8.2(@algolia/client-search@5.46.3)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)':