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 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
+ ![timeline-scheduler preview](scheduler.png)
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
+ }