@jouleworks/scheduler
v0.2.1
Published
Resource scheduler component for Angular — day and timeline views, drag-and-drop event creation, move and resize, extensible editor modal, per-resource rules (business hours, blackouts/lockouts, validators). Styled with Tailwind CSS v4.
Maintainers
Readme
@jouleworks/scheduler
Resource scheduler component for Angular 22+, styled with Tailwind CSS v4.
- Two views with a toolbar switcher: day (resources as columns, vertical time axis) and timeline (resources as rows, horizontal time axis)
- Drag on empty space to create an event (ghost preview with live validation)
- Drag events to move them across time and between resources; edge handles to resize
- Built-in new/edit modal — extend it with your own form fields
- Per-resource rules: business hours, blackout/lockout periods, non-droppable resources, custom validators
- Events spanning midnight render on both days with continuation markers; edits never truncate the hidden edge
- Overlap lane-packing, current-time indicator, snap-to-grid, Escape cancels a drag
- Dark mode via Tailwind
dark:variants — follows your app's dark strategy (class or media) - Signals throughout, zoneless-ready,
OnPush, no dependencies beyond Angular
Install
npm install @jouleworks/schedulerStyling — pick one
The component's templates use Tailwind utilities. Either:
A. Your app already uses Tailwind v4 — let it scan this package in your root stylesheet (produces exactly the classes needed, deduped with yours):
@import "tailwindcss";
@source "../node_modules/@jouleworks/scheduler";B. No Tailwind — import the precompiled stylesheet (≈15 kB min, theme variables + used utilities, no preflight/reset):
// angular.json "styles": [...], or in styles.css:
@import "@jouleworks/scheduler/styles.css";Don't do both, or you'll ship the utilities twice.
Dark mode: the component uses dark: variants throughout. With option A it
follows whatever dark strategy your app configures (e.g.
@custom-variant dark (&:where(.dark, .dark *)) for class-based toggling);
with option B (the precompiled stylesheet) dark styles apply via
prefers-color-scheme: dark.
Quick start
import { JwaScheduler, SchedulerResource, SchedulerEvent } from '@jouleworks/scheduler';
@Component({
imports: [JwaScheduler],
template: `
<div class="h-[80vh]">
<jwa-scheduler
[resources]="rooms"
[events]="events()"
[dayStartHour]="8"
[dayEndHour]="22"
(eventCreated)="onCreated($event)"
(eventUpdated)="onUpdated($event)"
(eventDeleted)="onDeleted($event)"
/>
</div>
`,
})
export class SchedulePage {
rooms: SchedulerResource[] = [
{ id: 1, name: 'Main Stage', color: '#4f46e5' },
{ id: 2, name: 'Workshop', businessHours: { start: '10:00', end: '17:00' } },
{
id: 3,
name: 'Panel Room',
blackouts: [{ start: new Date('2026-08-07T15:00'), end: new Date('2026-08-07T18:00'), label: 'Contract — AV changeover' }],
},
{ id: 4, name: 'Lounge', droppable: false },
];
events = signal<SchedulerEvent[]>([]);
onCreated(draft: SchedulerEventDraft) {
this.events.update((list) => [...list, { ...draft, id: crypto.randomUUID() }]);
}
onUpdated({ event }: SchedulerEventUpdate) {
this.events.update((list) => list.map((e) => (e.id === event.id ? event : e)));
}
onDeleted(event: SchedulerEvent) {
this.events.update((list) => list.filter((e) => e.id !== event.id));
}
}The component is controlled: it never mutates your events array. Apply
the output events to your own store (or REST API) and pass the new array back
in. The host element fills its container — give the parent a height.
Inputs
| Input | Default | Purpose |
| --- | --- | --- |
| resources | required | SchedulerResource[] — one column (day) / row (timeline) each |
| events | required | SchedulerEvent[] |
| date | today | Displayed day (two-way: [(date)]) |
| view | 'day' | 'day' or 'timeline' (two-way: [(view)]) |
| views | both | Views offered in the toolbar switcher (fewer than 2 hides it) |
| dayStartHour / dayEndHour | 8 / 20 | Visible window |
| slotMinutes | 30 | Faint slot-line interval |
| snapMinutes | 15 | Drag/resize snapping granularity |
| hourHeight | 64 | Day view: pixels per hour |
| resourceMinWidth | 200 | Day view: min column width (columns stretch to fill) |
| timelineHourWidth | 120 | Timeline: pixels per hour |
| timelineRowHeight | 88 | Timeline: resource row height |
| timelineResourceWidth | 176 | Timeline: sticky resource label column width |
| defaultEventMinutes | 60 | Length of double-click-created events |
| showToolbar | true | Built-in prev/today/next + view switcher toolbar |
| editable | true | Master switch for all editing interactions |
| useBuiltInEditor | true | When false, no modal: listen to rangeSelected/eventClicked and open your own UI |
| enforceBusinessHours | false | Reject drops outside a resource's businessHours (always shaded) |
| enforceBlackouts | true | Reject drops overlapping a resource's blackouts |
| showBlackouts | true | Render blackout blocks; turn off for public views (enforcement is separate) |
| eventValidator | – | (candidate, ctx) => true \| string — custom rules; a string blocks the change and is shown on the ghost / in the editor |
| defaultMeta | – | () => TMeta seeding meta on new drafts so editor-field bindings are safe |
| eventClass | – | (event, resource) => string — extra classes per chip (e.g. dashed border for open slots) |
| locale | browser | BCP-47 locale for time labels |
| scrollTime | – | "HH:mm" scrolled to the start of the time axis (also imperative: scrollToTime()) |
Outputs
| Output | Payload | When |
| --- | --- | --- |
| eventCreated | SchedulerEventDraft (no id yet — assign one) | Editor saved a new event |
| eventUpdated | { event, previous, source: 'drag' \| 'resize' \| 'editor' } | Move, resize, or editor save |
| eventDeleted | SchedulerEvent | Delete pressed in the editor |
| eventClicked | { event, resource, nativeEvent } | Any event click (fires before the editor opens) |
| rangeSelected | { resource, start, end } | Drag-create finished while useBuiltInEditor is false |
| blackoutClicked | { resource, blackout, nativeEvent } | An editable blackout was clicked (before the editor opens) |
| blackoutUpdated | { resource, blackout, previous } | Built-in blackout editor saved (label/times changed) |
| blackoutDeleted | { resource, blackout } | Delete pressed in the blackout editor |
| dateChange / viewChange | Date / view | Toolbar navigation and view switching |
Template extension points
<jwa-scheduler ...>
<!-- right side of the toolbar -->
<div jwaToolbarActions>…</div>
<!-- resource header (column head / row label) -->
<ng-template jwaResourceHeader let-resource>…</ng-template>
<!-- event chip body; clipped flags mark midnight-spanning continuations -->
<ng-template jwaEventContent let-event let-resource="resource" let-clippedEnd="clippedEnd">…</ng-template>
<!-- extra fields inside the editor modal; draft.meta is yours -->
<ng-template jwaEditorFields let-draft let-isNew="isNew">
<select [(ngModel)]="draft.meta.panelId">…</select>
</ng-template>
</jwa-scheduler>Per-resource rules
businessHours: { start: 'HH:mm', end: 'HH:mm' }— shades (and withenforceBusinessHours, blocks) time outside the windowblackouts: { id?, start: Date, end: Date, label?: string, editable?: boolean }[]— absolute unavailable periods (contract holds, lockouts). Hatched with the label; drops rejected with a message. Hide from public views withshowBlackouts: false— they still block. When editable, clicking a blackout opens a built-in editor (label, from/until, delete) emittingblackoutUpdated/blackoutDeleted; setidso your handler can persist. To let staff draw new lockouts, flipuseBuiltInEditoroff and turnrangeSelectedpayloads into blackoutsdroppable: false— events cannot be created in or moved into the resourcecolor— accent for its events (event.coloroverrides per event)event.editable: false— locks a single event (no move/resize/edit/delete)eventValidator— arbitrary rules (min duration, conflict prevention, role checks…) with the target resource, all events,isNew, and the change source in context
License
MIT © Jouleworks Development LLC
