timeline-scheduler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +370 -0
- package/dist/components/tl-grid.d.ts +12 -0
- package/dist/components/tl-item.d.ts +73 -0
- package/dist/timeline-scheduler.cjs.js +637 -0
- package/dist/timeline-scheduler.d.ts +103 -0
- package/dist/timeline-scheduler.es.js +1801 -0
- package/dist/types.d.ts +63 -0
- package/dist/utils/layout.d.ts +7 -0
- package/dist/utils/layout.test.d.ts +1 -0
- package/dist/utils/time.d.ts +16 -0
- package/dist/utils/time.test.d.ts +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# timeline-scheduler
|
|
2
|
+
|
|
3
|
+
A framework-agnostic Web Component for scheduling and visualising events across multiple resources on a daily timeline.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
- **Zero dependencies** — built with Lit, works in any framework or plain HTML
|
|
8
|
+
- **Drag & drop** — move events across resources and time with snapping
|
|
9
|
+
- **Resize** — drag event edges to adjust duration
|
|
10
|
+
- **Hold to create** — press and hold on empty space to draw a new event
|
|
11
|
+
- **Navigation** — built-in prev/next day buttons with date picker and zoom controls
|
|
12
|
+
- **Overlap handling** — overlapping events are automatically stacked in sub-rows
|
|
13
|
+
- **Multi-day events** — events spanning midnight are clipped and shown on each day
|
|
14
|
+
- **Keyboard accessible** — Tab between items, arrow keys to move them
|
|
15
|
+
- **Customisable** — CSS custom properties for theming, custom item renderer, and full event hooks
|
|
16
|
+
- **TypeScript** — fully typed
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install timeline-scheduler
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<script type="module">
|
|
32
|
+
import "timeline-scheduler";
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<timeline-scheduler id="tl"></timeline-scheduler>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const tl = document.getElementById("tl");
|
|
40
|
+
|
|
41
|
+
tl.resources = [
|
|
42
|
+
{ id: "r1", name: "Alice Johnson", avatar: "https://example.com/alice.jpg" },
|
|
43
|
+
{ id: "r2", name: "Bob Smith" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
tl.items = [
|
|
47
|
+
{
|
|
48
|
+
id: "i1",
|
|
49
|
+
resourceId: "r1",
|
|
50
|
+
name: "Team sync",
|
|
51
|
+
color: "#3b82f6",
|
|
52
|
+
start: new Date("2024-04-13T09:00:00"),
|
|
53
|
+
end: new Date("2024-04-13T10:00:00"),
|
|
54
|
+
description: "Weekly check-in",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
tl.date = new Date("2024-04-13");
|
|
59
|
+
tl.showNav = true;
|
|
60
|
+
tl.draggable = true;
|
|
61
|
+
tl.resizable = true;
|
|
62
|
+
tl.creatable = true;
|
|
63
|
+
|
|
64
|
+
// Keep data in sync after drag / resize
|
|
65
|
+
tl.addEventListener("change", (e) => {
|
|
66
|
+
tl.items = e.detail.items;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Navigate to the new date when the user clicks prev/next
|
|
70
|
+
tl.addEventListener("date-change", (e) => {
|
|
71
|
+
tl.date = e.detail.date;
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### React
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import "timeline-scheduler";
|
|
79
|
+
import type { TimelineScheduler } from "timeline-scheduler";
|
|
80
|
+
|
|
81
|
+
declare global {
|
|
82
|
+
namespace JSX {
|
|
83
|
+
interface IntrinsicElements {
|
|
84
|
+
"timeline-scheduler": React.DetailedHTMLProps<
|
|
85
|
+
React.HTMLAttributes<TimelineScheduler>,
|
|
86
|
+
TimelineScheduler
|
|
87
|
+
>;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function App() {
|
|
93
|
+
const ref = useRef<TimelineScheduler>(null);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const tl = ref.current!;
|
|
97
|
+
tl.resources = [...];
|
|
98
|
+
tl.items = [...];
|
|
99
|
+
tl.showNav = true;
|
|
100
|
+
tl.draggable = true;
|
|
101
|
+
|
|
102
|
+
tl.addEventListener("change", (e: Event) => {
|
|
103
|
+
tl.items = (e as CustomEvent).detail.items;
|
|
104
|
+
});
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
return <timeline-scheduler ref={ref} style={{ maxHeight: "600px" }} />;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Features
|
|
114
|
+
|
|
115
|
+
### Navigation bar
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
tl.showNav = true;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Shows previous/next day buttons, a clickable date label that opens the native date picker, and zoom controls. Control individual parts:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
tl.showDateNav = false; // hide prev/next + date picker
|
|
125
|
+
tl.showZoomControls = false; // hide zoom buttons
|
|
126
|
+
// nav bar auto-hides when both are false
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Zoom
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
tl.zoom = 2; // 1 (default), 2, or 4
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
At zoom > 1 the timeline scrolls horizontally while resource labels and the time header stay sticky.
|
|
136
|
+
|
|
137
|
+
### Hold to create
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
tl.creatable = true;
|
|
141
|
+
|
|
142
|
+
tl.addEventListener("item-create", (e) => {
|
|
143
|
+
const { resourceId, start, end } = e.detail;
|
|
144
|
+
tl.items = [
|
|
145
|
+
...tl.items,
|
|
146
|
+
{
|
|
147
|
+
id: crypto.randomUUID(),
|
|
148
|
+
resourceId,
|
|
149
|
+
name: "New event",
|
|
150
|
+
color: "#64748b",
|
|
151
|
+
start,
|
|
152
|
+
end,
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Press and hold on empty space for ~400 ms, then drag to set the duration.
|
|
159
|
+
|
|
160
|
+
### Custom item renderer
|
|
161
|
+
|
|
162
|
+
Replace the default name + time display with your own content:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
tl.renderItem = (item) => {
|
|
166
|
+
const el = document.createElement("span");
|
|
167
|
+
el.textContent = `★ ${item.name}`;
|
|
168
|
+
el.style.fontWeight = "600";
|
|
169
|
+
return el;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
tl.renderItem = null; // reset to default
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Resize constraints
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
tl.minDurationMinutes = 30; // can't resize shorter than 30 min
|
|
179
|
+
tl.maxDurationMinutes = 240; // can't resize longer than 4 hours
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Multi-day items
|
|
183
|
+
|
|
184
|
+
Items whose `start` and `end` span multiple days are automatically clipped to the visible day. An event from April 12 at 15:00 to April 13 at 09:00 will show as 15:00–24:00 on the 12th and 00:00–09:00 on the 13th.
|
|
185
|
+
|
|
186
|
+
### Keyboard navigation
|
|
187
|
+
|
|
188
|
+
| Key | Action |
|
|
189
|
+
| -------------------------- | ----------------------------------- |
|
|
190
|
+
| `ArrowLeft` / `ArrowRight` | Move focused item one snap interval |
|
|
191
|
+
| `Tab` / `Shift+Tab` | Move focus between items |
|
|
192
|
+
|
|
193
|
+
### Built-in context menu
|
|
194
|
+
|
|
195
|
+
Right-clicking an item opens a context menu with **Edit**, **Delete**, and **Close**. Edit opens a modal to change the name, resource, and times. Disable with `editable = false` to handle it yourself:
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
tl.editable = false;
|
|
199
|
+
|
|
200
|
+
tl.addEventListener("item-contextmenu", (e) => {
|
|
201
|
+
const { item, x, y } = e.detail;
|
|
202
|
+
// show your own menu at (x, y)
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Theming
|
|
207
|
+
|
|
208
|
+
```css
|
|
209
|
+
timeline-scheduler {
|
|
210
|
+
max-height: 600px;
|
|
211
|
+
--tl-resource-col-width: 200px;
|
|
212
|
+
--tl-font-family: "Inter", sans-serif;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Dark mode:
|
|
217
|
+
|
|
218
|
+
```css
|
|
219
|
+
timeline-scheduler {
|
|
220
|
+
--tl-bg: #0f172a;
|
|
221
|
+
--tl-header-bg: #1e293b;
|
|
222
|
+
--tl-resource-border-color: #334155;
|
|
223
|
+
--tl-grid-line-color: #334155;
|
|
224
|
+
--tl-text-color: #f1f5f9;
|
|
225
|
+
--tl-time-label-color: #64748b;
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Reference
|
|
232
|
+
|
|
233
|
+
### Properties
|
|
234
|
+
|
|
235
|
+
#### Data
|
|
236
|
+
|
|
237
|
+
| Property | Type | Default | Description |
|
|
238
|
+
| ----------- | ---------------- | ------- | ----------------------------------------------- |
|
|
239
|
+
| `resources` | `Resource[]` | `[]` | Rows to display |
|
|
240
|
+
| `items` | `TimelineItem[]` | `[]` | Events to render |
|
|
241
|
+
| `date` | `Date` | today | The day shown. Items are filtered to this date. |
|
|
242
|
+
|
|
243
|
+
#### Time range
|
|
244
|
+
|
|
245
|
+
| Property | Type | Default | Description |
|
|
246
|
+
| ------------- | -------- | ------- | ------------------------------------------- |
|
|
247
|
+
| `startHour` | `number` | `0` | First visible hour (0–23) |
|
|
248
|
+
| `endHour` | `number` | `24` | Last visible hour (1–24) |
|
|
249
|
+
| `snapMinutes` | `number` | `15` | Snap interval in minutes during drag/resize |
|
|
250
|
+
|
|
251
|
+
#### Interaction
|
|
252
|
+
|
|
253
|
+
| Property | Type | Default | Description |
|
|
254
|
+
| -------------------- | --------- | ------- | ------------------------------------------------------- |
|
|
255
|
+
| `draggable` | `boolean` | `true` | Allow drag & drop |
|
|
256
|
+
| `resizable` | `boolean` | `false` | Allow resizing by dragging edges |
|
|
257
|
+
| `readonly` | `boolean` | `false` | Disable all interaction |
|
|
258
|
+
| `creatable` | `boolean` | `false` | Allow hold-to-create on empty space |
|
|
259
|
+
| `editable` | `boolean` | `true` | Allow built-in edit modal on double-click / right-click |
|
|
260
|
+
| `minDurationMinutes` | `number` | `0` | Minimum duration during resize (0 = no limit) |
|
|
261
|
+
| `maxDurationMinutes` | `number` | `0` | Maximum duration during resize (0 = no limit) |
|
|
262
|
+
|
|
263
|
+
#### Display
|
|
264
|
+
|
|
265
|
+
| Property | Type | Default | Description |
|
|
266
|
+
| ------------------ | ------------------------------------------- | ------- | ----------------------------------------- |
|
|
267
|
+
| `showNav` | `boolean` | `false` | Show the navigation bar |
|
|
268
|
+
| `showDateNav` | `boolean` | `true` | Show prev/next + date picker in nav bar |
|
|
269
|
+
| `showZoomControls` | `boolean` | `true` | Show zoom buttons in nav bar |
|
|
270
|
+
| `zoom` | `number` | `1` | Horizontal zoom factor (`1`, `2`, or `4`) |
|
|
271
|
+
| `showTime` | `boolean` | `true` | Show start–end time inside event blocks |
|
|
272
|
+
| `showAvatar` | `boolean` | `true` | Show avatar / initials in resource column |
|
|
273
|
+
| `showEventCount` | `boolean` | `true` | Show event count below resource name |
|
|
274
|
+
| `showTooltip` | `boolean` | `true` | Show hover tooltip on events |
|
|
275
|
+
| `showNowLine` | `boolean` | `true` | Show current-time indicator line |
|
|
276
|
+
| `renderItem` | `((item: TimelineItem) => unknown) \| null` | `null` | Custom render function for event content |
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Events
|
|
281
|
+
|
|
282
|
+
| Event | Detail | Fires when |
|
|
283
|
+
| ------------------ | ------------------------------------ | ------------------------------------------------------------- |
|
|
284
|
+
| `change` | `{ items: TimelineItem[] }` | An item is moved or resized. Always write back to `tl.items`. |
|
|
285
|
+
| `item-click` | `{ item }` | An item is clicked |
|
|
286
|
+
| `item-dblclick` | `{ item }` | An item is double-clicked |
|
|
287
|
+
| `item-hover` | `{ item, type: 'enter' \| 'leave' }` | Pointer enters or leaves an item |
|
|
288
|
+
| `item-contextmenu` | `{ item, x, y }` | An item is right-clicked |
|
|
289
|
+
| `item-dragstart` | `{ item }` | Drag begins |
|
|
290
|
+
| `item-dragend` | `{ item, resourceId, start, end }` | Drag released — final position |
|
|
291
|
+
| `item-resizestart` | `{ item }` | Resize begins |
|
|
292
|
+
| `item-resizeend` | `{ item, start, end }` | Resize released — final times |
|
|
293
|
+
| `item-create` | `{ resourceId, start, end }` | New item created via hold-to-create |
|
|
294
|
+
| `date-change` | `{ date }` | User navigated to a different day |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### CSS custom properties
|
|
299
|
+
|
|
300
|
+
| Property | Default | Description |
|
|
301
|
+
| ---------------------------- | ------------ | ----------------------------------------- |
|
|
302
|
+
| `--tl-font-family` | `sans-serif` | Font used throughout the component |
|
|
303
|
+
| `--tl-bg` | `#ffffff` | Background color |
|
|
304
|
+
| `--tl-header-bg` | `#f8fafc` | Time header and nav bar background |
|
|
305
|
+
| `--tl-resource-col-width` | `160px` | Width of the resource label column |
|
|
306
|
+
| `--tl-resource-border-color` | `#e2e8f0` | Border between resource rows |
|
|
307
|
+
| `--tl-grid-line-color` | `#e2e8f0` | Hour grid lines |
|
|
308
|
+
| `--tl-text-color` | `#1e293b` | Primary text color |
|
|
309
|
+
| `--tl-time-label-color` | `#94a3b8` | Hour labels in the header |
|
|
310
|
+
| `--tl-item-radius` | `4px` | Border radius of event blocks |
|
|
311
|
+
| `--tl-item-text-color` | `#ffffff` | Text color inside event blocks |
|
|
312
|
+
| `--tl-item-opacity-dragging` | `0.4` | Opacity of the source item while dragging |
|
|
313
|
+
| `--tl-focus-color` | `#3b82f6` | Focus outline color |
|
|
314
|
+
| `--tl-now-line-color` | `#ef4444` | Current-time indicator color |
|
|
315
|
+
| `--tl-create-ghost-color` | `#3b82f6` | Hold-to-create ghost block color |
|
|
316
|
+
| `--tl-tooltip-bg` | `#1e293b` | Tooltip background |
|
|
317
|
+
| `--tl-tooltip-color` | `#ffffff` | Tooltip text color |
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### TypeScript types
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import type {
|
|
325
|
+
Resource,
|
|
326
|
+
TimelineItem,
|
|
327
|
+
ChangeDetail,
|
|
328
|
+
ItemClickDetail,
|
|
329
|
+
ItemDblClickDetail,
|
|
330
|
+
ItemHoverDetail,
|
|
331
|
+
ItemContextMenuDetail,
|
|
332
|
+
ItemDragStartDetail,
|
|
333
|
+
ItemDragEndDetail,
|
|
334
|
+
ItemResizeStartDetail,
|
|
335
|
+
ItemResizeEndDetail,
|
|
336
|
+
ItemCreateDetail,
|
|
337
|
+
DateChangeDetail,
|
|
338
|
+
} from "timeline-scheduler";
|
|
339
|
+
|
|
340
|
+
interface Resource {
|
|
341
|
+
id: string;
|
|
342
|
+
name: string;
|
|
343
|
+
avatar?: string; // URL — falls back to coloured initials
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface TimelineItem {
|
|
347
|
+
id: string;
|
|
348
|
+
resourceId: string;
|
|
349
|
+
name: string;
|
|
350
|
+
color: string; // any valid CSS color
|
|
351
|
+
start: Date;
|
|
352
|
+
end: Date;
|
|
353
|
+
description?: string; // shown in the hover tooltip
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Development
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
npm install
|
|
363
|
+
npm run dev # playground at localhost:5173
|
|
364
|
+
npm test # run tests
|
|
365
|
+
npm run build # build dist/
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
export declare class TlGrid extends LitElement {
|
|
3
|
+
startHour: number;
|
|
4
|
+
endHour: number;
|
|
5
|
+
static styles: import("lit").CSSResult;
|
|
6
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
7
|
+
}
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
'tl-grid': TlGrid;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import type { TimelineItem } from '../types';
|
|
3
|
+
export interface TlItemDragStartDetail {
|
|
4
|
+
item: TimelineItem;
|
|
5
|
+
pointerId: number;
|
|
6
|
+
clientX: number;
|
|
7
|
+
clientY: number;
|
|
8
|
+
itemClientLeft: number;
|
|
9
|
+
itemClientWidth: number;
|
|
10
|
+
}
|
|
11
|
+
export interface TlItemClickDetail {
|
|
12
|
+
item: TimelineItem;
|
|
13
|
+
}
|
|
14
|
+
export interface TlItemHoverDetail {
|
|
15
|
+
item: TimelineItem;
|
|
16
|
+
type: 'enter' | 'leave';
|
|
17
|
+
}
|
|
18
|
+
export interface TlItemResizeStartDetail {
|
|
19
|
+
item: TimelineItem;
|
|
20
|
+
edge: 'left' | 'right';
|
|
21
|
+
pointerId: number;
|
|
22
|
+
clientX: number;
|
|
23
|
+
itemClientLeft: number;
|
|
24
|
+
itemClientWidth: number;
|
|
25
|
+
}
|
|
26
|
+
export interface TlItemContextMenuDetail {
|
|
27
|
+
item: TimelineItem;
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
}
|
|
31
|
+
export interface TlItemKeyMoveDetail {
|
|
32
|
+
item: TimelineItem;
|
|
33
|
+
direction: 'left' | 'right';
|
|
34
|
+
}
|
|
35
|
+
export interface TlItemDblClickDetail {
|
|
36
|
+
item: TimelineItem;
|
|
37
|
+
}
|
|
38
|
+
export declare class TlItemElement extends LitElement {
|
|
39
|
+
item: TimelineItem;
|
|
40
|
+
dragEnabled: boolean;
|
|
41
|
+
resizable: boolean;
|
|
42
|
+
showTime: boolean;
|
|
43
|
+
showTooltip: boolean;
|
|
44
|
+
readonly: boolean;
|
|
45
|
+
/** Optional custom render function for item content. Replaces the default name + time display. */
|
|
46
|
+
renderContent: ((item: TimelineItem) => unknown) | null;
|
|
47
|
+
private _tooltipVisible;
|
|
48
|
+
private _tooltipRect;
|
|
49
|
+
private _downX;
|
|
50
|
+
private _downY;
|
|
51
|
+
private _downPointerId;
|
|
52
|
+
private _holdTimer;
|
|
53
|
+
private _lastClickTime;
|
|
54
|
+
static styles: import("lit").CSSResult;
|
|
55
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
56
|
+
private _formatDuration;
|
|
57
|
+
private _onPointerDown;
|
|
58
|
+
private _onHoldMove;
|
|
59
|
+
private _onHoldUp;
|
|
60
|
+
private _onHoldCancel;
|
|
61
|
+
private _cleanHoldListeners;
|
|
62
|
+
private _startDrag;
|
|
63
|
+
private _onPointerEnter;
|
|
64
|
+
private _onPointerLeave;
|
|
65
|
+
private _onContextMenu;
|
|
66
|
+
private _onKeyDown;
|
|
67
|
+
private _onResizeStart;
|
|
68
|
+
}
|
|
69
|
+
declare global {
|
|
70
|
+
interface HTMLElementTagNameMap {
|
|
71
|
+
'tl-item': TlItemElement;
|
|
72
|
+
}
|
|
73
|
+
}
|