ordinant
v0.0.5
Published
A Headless, Shadcn-compatible Scheduler/Gantt component for React built with Tailwind CSS and @dnd-kit.
Downloads
523
Maintainers
Readme
Ordinant
A modern, headless, and fully customizable Scheduler/Gantt component for React. Built with Shadcn UI, Tailwind CSS, and @dnd-kit.
Ordinant provides a set of composable primitives to build complex scheduling interfaces with ease. It handles the hard parts—virtualization, drag-and-drop logic, and time scales—while giving you full control over the UI rendering.
🚀 Features
- 🎨 Shadcn UI Compatible: Designed to fit perfectly with modern design systems.
- 🏗 Headless Architecture: You control the markup. Use our primitives or build your own.
- 🖱 Drag & Drop: Built-in support for moving and resizing events (powered by
@dnd-kit). - ⚡ Virtualization: Efficiently renders large datasets by only drawing what's visible.
- 📱 Responsive: Works on various screen sizes with touch support.
- 🌗 Dark Mode: Native Tailwind dark mode support.
📦 Installation
Install the package and its peer dependencies:
npm install ordinant @dnd-kit/core @dnd-kit/utilities clsx tailwind-merge🎨 Tailwind Setup
Since Ordinant uses Tailwind CSS classes for its default styles, you need to configure your tailwind.config.ts (or .js) to scan the library files. This ensures the necessary styles are generated.
Add the ordinant path to your content array:
// tailwind.config.ts
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
// Add Ordinant to the content list:
"./node_modules/ordinant/dist/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};💻 Usage
Ordinant is composition-based. You wrap your app in a SchedulerProvider and then build your layout using the available components.
Here is a complete example of a draggable scheduler:
import { useState, useRef } from "react";
import {
Scheduler,
SchedulerProvider,
SchedulerBody,
TimelineHeader,
DnDSchedulerRow,
DnDSchedulerEvent,
SchedulerResourceCell,
SchedulerTimelineCell,
SchedulerDnDWrapper,
useSchedulerContext,
} from "ordinant";
// Minimal setup component
const MyScheduler = () => {
// 1. Setup State
const [events, setEvents] = useState([
{
id: "1",
resourceId: "r1",
start: new Date().setHours(9, 0, 0, 0),
end: new Date().setHours(12, 0, 0, 0),
data: { title: "Team Meeting", color: "bg-blue-500" },
},
]);
const resources = [
{ id: "r1", data: { name: "Conference Room A" } },
{ id: "r2", data: { name: "Conference Room B" } },
];
// 2. Refs & Context Helper
const scrollRef = useRef<HTMLDivElement>(null);
// 3. Handle Updates (Drag/Resize)
const handleUpdate = ({ eventId, newStart, newEnd, newResourceId }) => {
setEvents((prev) =>
prev.map((e) =>
e.id === eventId
? {
...e,
start: newStart,
end: newEnd,
resourceId: newResourceId ?? e.resourceId,
}
: e
)
);
};
return (
<SchedulerProvider
startDate={new Date(new Date().setHours(0, 0, 0, 0))}
endDate={new Date(new Date().setHours(24, 0, 0, 0))}
rowHeight={60}
>
<div className="h-[600px] w-full border rounded-lg bg-white">
<Scheduler className="h-full">
<SchedulerBody ref={scrollRef} className="overflow-auto relative">
<SchedulerContent
resources={resources}
events={events}
onUpdate={handleUpdate}
scrollRef={scrollRef}
/>
</SchedulerBody>
</Scheduler>
</div>
</SchedulerProvider>
);
};
// Helper component to access Context (for width calculation)
const SchedulerContent = ({ resources, events, onUpdate, scrollRef }) => {
const { getTimelineWidth } = useSchedulerContext();
const width = getTimelineWidth();
const SIDEBAR_WIDTH = 200;
return (
<div style={{ width: width + SIDEBAR_WIDTH, minWidth: "100%" }}>
{/* Sticky Header */}
<div className="sticky top-0 z-10 flex border-b bg-gray-50">
<div
className="shrink-0 border-r p-4 font-bold flex items-center"
style={{ width: SIDEBAR_WIDTH }}
>
Resources
</div>
<TimelineHeader scrollRef={scrollRef} className="flex-1" />
</div>
{/* Draggable Area */}
<SchedulerDnDWrapper
onEventUpdate={onUpdate}
renderEvent={(e) => (
<div
className={`h-full w-full rounded px-2 text-xs text-white flex items-center ${e.data.color}`}
>
{e.data.title}
</div>
)}
>
{resources.map((resource) => (
<DnDSchedulerRow key={resource.id} resourceId={resource.id}>
{/* Resource Column */}
<SchedulerResourceCell style={{ width: SIDEBAR_WIDTH }}>
<span className="font-medium p-4">{resource.data.name}</span>
</SchedulerResourceCell>
{/* Timeline Column */}
<SchedulerTimelineCell>
{events
.filter((e) => e.resourceId === resource.id)
.map((event) => (
<DnDSchedulerEvent
key={event.id}
event={event}
className={`rounded text-xs text-white ${event.data.color}`}
>
<div className="px-2 truncate">{event.data.title}</div>
</DnDSchedulerEvent>
))}
</SchedulerTimelineCell>
</DnDSchedulerRow>
))}
</SchedulerDnDWrapper>
</div>
);
};
export default MyScheduler;🧩 Key Components
| Component | Description |
| ----------------------- | ------------------------------------------------------------- |
| SchedulerProvider | Context provider handling time scales, filtering, and config. |
| TimelineHeader | Virtualized time axis header (sticky support). |
| SchedulerDnDWrapper | Wraps the content to enable Drag & Drop. |
| DnDSchedulerRow | A droppable row (resource track). |
| DnDSchedulerEvent | A draggable and resizable event item. |
| SchedulerResourceCell | The left-side cell for resource details. |
| SchedulerTimelineCell | The right-side cell where events are placed. |
🤝 Contributing
Contributions are welcome! This library uses a "Lite Monorepo" structure:
src/components: The library source code.src/examples: Demo applications used for development.
Run npm run dev to start the development server with the demo.
📄 License
MIT © 2024
