npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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/scheduler

Styling — 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 with enforceBusinessHours, blocks) time outside the window
  • blackouts: { 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 with showBlackouts: false — they still block. When editable, clicking a blackout opens a built-in editor (label, from/until, delete) emitting blackoutUpdated/blackoutDeleted; set id so your handler can persist. To let staff draw new lockouts, flip useBuiltInEditor off and turn rangeSelected payloads into blackouts
  • droppable: false — events cannot be created in or moved into the resource
  • color — accent for its events (event.color overrides 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