Components

Schedule Builder

A recurrence rule assembled as a live English sentence, over the next five real occurrences - computed, never added. The calendar traps are the point: pick day 31 and months without one appear as ghost skipped rows (RRULE semantics - tick Clamp month ends for the other real policy, where day 31 becomes the 30th). Runs are built from local wall-clock parts, so 9:00 AM stays 9:00 AM across a daylight-saving jump - every row prints its UTC offset and flags the ones that differ. “The 2nd Tuesday” and “the last Friday” come from the month's first and last day, no scanning. And the sentence morphs word by word: words are keyed by text + occurrence and glide via layout, so switching “week” to “month” slides “at 9:00 AM” into place instead of retyping it - the sentence reads as one object being edited, not a string being replaced.

Every week on Tuesday and Thursday at 9:00 AM

Repeats
Every
1
week
On
At

Next 5 runs

  1. Tue, Jul 28, 20269:00 AMGMT+0
  2. Thu, Jul 30, 20269:00 AMGMT+0
  3. Tue, Aug 4, 20269:00 AMGMT+0
  4. Thu, Aug 6, 20269:00 AMGMT+0
  5. Tue, Aug 11, 20269:00 AMGMT+0

Rule:

Install

npx moumenlab add schedule-builder

For AI

Open .md

Usage

"use client";

import ScheduleBuilder from "./schedule-builder";

// The rule is the component's value — read it out through onRuleChange and
// serialize it however your backend wants (it maps 1:1 onto RRULE).
export default function ScheduleBuilderExample() {
  return (
    <div className="flex flex-wrap items-start gap-10">
      {/* Default: RRULE semantics — "day 31" SKIPS months without one (ghost rows). */}
      <ScheduleBuilder
        defaultRule={{ freq: "weekly", weekdays: [1, 3, 5], hour: 8 }}
        onRuleChange={(rule) => console.log("rule", rule)}
      />

      {/* clamp: the other real-world month-end policy — day 31 becomes the 30th.
          occurrences controls how many upcoming runs prove the rule;
          morph={false} swaps the word glide for instant text. */}
      <ScheduleBuilder
        defaultRule={{ freq: "monthly", monthDay: 31 }}
        clamp
        occurrences={3}
      />
    </div>
  );
}