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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@sirhc77/hours-editor

v0.3.0

Published

A tiny React component for editing "open" hours.

Readme

hours-editor

Two small React components for editing daily and weekly opening hours.

  • There are two flavors: one for a single day (DailyHoursEditor), and another for weekly hours (WeeklyHoursEditor).
  • For the DailyHoursEditor, there is one column.
  • For WeeklyHoursEditor, the columns are days of the week
  • Vertical axis is time
  • Users create time windows by double-clicking in a day column
  • Windows can be dragged and resized (powered by react-rnd)
  • Outputs a clean array of:
type DayOfWeek =
  | "monday"
  | "tuesday"
  | "wednesday"
  | "thursday"
  | "friday"
  | "saturday"
  | "sunday";

interface WeeklyTimeWindow {
  dayOfWeek: DayOfWeek;
  startTime: string; // "HH:MM:SS"
  endTime: string;   // "HH:MM:SS"
}

//Or...

interface DailyTimeWindow {
    startTime: string,
    endTime: string
}

Perfect for “business hours”, “support hours”, “call routing hours”, etc.

UX in one sentence: Double-click to create a window, drag to move, resize to adjust, and you get { dayOfWeek, startTime, endTime }[] out the other end.


Install

npm install @sirhc77/hours-editor 
# or
yarn add @sirhc77/hours-editor react-rnd
# or
pnpm add @sirhc77/hours-editor react-rnd

The only peer dependency is react / react-dom. react-rnd is a runtime dependency used internally for drag/resize.


Quick start

import React, { useState } from "react";
import {
  WeeklyHoursEditor,
  type WeeklyTimeWindow,
} from "@sirhc77/hours-editor";

export function HoursDemo() {
  const [windows, setWindows] = useState<WeeklyTimeWindow[]>([
    {
      dayOfWeek: "monday",
      startTime: "09:00:00",
      endTime: "17:00:00",
    },
  ]);

  return (
    <div style={{ padding: 16 }}>
      <WeeklyHoursEditor
        value={windows}
        onChange={setWindows}
        stepMinutes={30}
        dayStartHour={8}
        dayEndHour={20}
      />

      <pre style={{ marginTop: 16, fontSize: 11 }}>
        {JSON.stringify(windows, null, 2)}
      </pre>
    </div>
  );
}

Interactions:

  • Double-click in a day column to create a new window.
  • Drag the block vertically to move it earlier/later.
  • Resize the block from the top/bottom to adjust duration.
  • Click the small “×” in the block to remove it.

Times snap to stepMinutes boundaries and are constrained to [dayStartHour, dayEndHour].


Props

For both DailyHoursEditor and WeeklyHoursEditor:

stepMinutes?: number;

  • Snap size in minutes for drag/resize.
  • Typical values: 30 (half hours), 60 (hours).
  • Default: 30.

minWindowMinutes?: number;

  • Minimum duration of a window, in minutes.
  • Default: 30.

defaultWindowMinutes?: number;

  • Default duration when creating a new window (on double-click), in minutes.
  • Default: 60.

dayStartHour?: number;

  • Start of the visible day (0–23).
  • Example: 8 => 8:00 at the top.
  • Default: 8.

dayEndHour?: number;

  • End of the visible day (1–24).
  • Example: 20 => 20:00 at the bottom.
  • Default: 20.
layoutProps?: {
    pxPerMinute: number;
    laneWidthPx: number;
    gutterWidthPx: number;
}
  • Customize the grid layout.
  • pxPerMinute is the number of pixels per minute on the screen. Default: 1.
  • laneWidthPx is the width of a day column. Default: 140
  • gutterWidthPx is the width of the gutter between columns. Default: 40

locale?: string;

  • Locale for time display.

Additional properties for DailyHoursEditor:

value: DailyTimeWindow[];

Current list of open-hour windows. Controlled value.

onChange: (value: DailyTimeWindow[]) => void;

Called whenever the user changes the grid.

Additional properties for WeeklyHoursEditor:

value: WeeklyTimeWindow[];

Current list of open-hour windows. Controlled value.

onChange: (value: WeeklyTimeWindow[]) => void;

Called whenever the user changes the grid.

startOfWeek?: DayOfWeek;

  • First day of the week.

Types

export type DayOfWeek =
  | "monday"
  | "tuesday"
  | "wednesday"
  | "thursday"
  | "friday"
  | "saturday"
  | "sunday";

export interface WeeklyTimeWindow {
  dayOfWeek: DayOfWeek;
  startTime: string; // "HH:MM:SS", 24h
  endTime: string;   // "HH:MM:SS", 24h
}

export interface DailyTimeWindow {
    startTime: string,
    endTime: string
}

Time model & snapping

Internally, everything is done in minutes since midnight and then converted to "HH:MM:SS".

  • dayStartHour and dayEndHour define the visible vertical range.

  • stepMinutes defines the snap granularity for:

    • drag (moving windows)
    • resize (changing window length)
    • initial placement on double-click
  • When a window is dragged or resized:

    • Start/end times are snapped to the nearest multiple of stepMinutes.
    • Windows are clamped so they never go outside [dayStartHour, dayEndHour].
    • There is always at least minWindowMinutes between startTime and endTime.

So if you use:

<WeeklyHoursEditor
  value={...}
  onChange={...}
  stepMinutes={30}
  dayStartHour={9}
  dayEndHour={18}
/>

You’ll get windows aligned to 09:00, 09:30, 10:00, …, 18:00, and nothing outside that range.


Using with Zod + React Hook Form

The component plays nicely with schemas like:

import { z } from "zod";

const TimeWindowSchema = z.object({
  dayOfWeek: z.enum([
    "monday",
    "tuesday",
    "wednesday",
    "thursday",
    "friday",
    "saturday",
    "sunday",
  ]),
  startTime: z.string(), // or z.iso.time()
  endTime: z.string(),   // or z.iso.time()
});

const AddHourTypeCodeBodySchema = z.object({
  type: z.enum(["standard", "exception"]),
  timeZone: z.string(),
  effectiveDate: z.string(),              // or z.iso.datetime()
  effectiveUntil: z.string().optional(),  // or z.iso.datetime().optional()
  timeWindows: z.array(TimeWindowSchema),
});

type AddHourTypeCodeBody = z.infer<typeof AddHourTypeCodeBodySchema>;

Example form integration:

import React from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
  WeeklyHoursEditor,
  type TimeWindow,
} from "@megafarad/weekly-hours-editor";
import { AddHourTypeCodeBodySchema } from "./schemas";

type FormValues = AddHourTypeCodeBody;

export function HourTypeCodeForm() {
  const {
    control,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(AddHourTypeCodeBodySchema),
    defaultValues: {
      type: "standard",
      timeZone: "America/New_York",
      effectiveDate: "",
      effectiveUntil: undefined,
      timeWindows: [],
    },
  });

  const onSubmit = (data: FormValues) => {
    console.log("Payload:", data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Your other fields here */}
      <div>
        <label>Time zone</label>
        <input {...register("timeZone")} />
        {errors.timeZone && <p>{errors.timeZone.message}</p>}
      </div>

      <div style={{ marginTop: 16 }}>
        <label style={{ display: "block", marginBottom: 4 }}>
          Weekly hours
        </label>
        <Controller
          name="timeWindows"
          control={control}
          render={({ field }) => (
            <WeeklyHoursEditor
              value={field.value as TimeWindow[]}
              onChange={field.onChange}
              stepMinutes={30}
              dayStartHour={8}
              dayEndHour={20}
            />
          )}
        />
        {errors.timeWindows && (
          <p style={{ color: "red", fontSize: 12 }}>
            {Array.isArray(errors.timeWindows)
              ? "One or more time windows are invalid."
              : (errors.timeWindows.message as string)}
          </p>
        )}
      </div>

      <button type="submit" style={{ marginTop: 16 }}>
        Save
      </button>
    </form>
  );
}

Since WeeklyHoursEditor is controlled via value / onChange, react-hook-form just sees it as another field containing an array of TimeWindow.


Styling & theming

By default the component:

  • Uses inline styles for layout (no CSS or Tailwind required).
  • Tries to be visually neutral: white background, light gray lines, blue blocks.

If you want to theme it:

  • Wrap it in a container with your own fonts/colors.

  • Fork or wrap the component to add custom styles to:

    • day headers
    • blocks
    • grid lines / gutter

A future version might expose a renderWindow or className-style hooks for deeper customization.


Limitations / roadmap

Current v0 is intentionally small:

  • One “kind” of window (all blue). No types/labels per window yet.
  • No keyboard navigation.
  • No multi-select editing or copy/paste (e.g. copy Monday → Friday).
  • No localization for the time scale (just HH:00 text).

Nice future ideas:

  • renderWindow prop for custom block content (labels, icons, etc.).
  • onCreateWindow / onDeleteWindow callbacks for custom confirmation logic.
  • Optional disabled mode (read-only weekly hours view).
  • 30-minute grid by default, with optional 15-minute support.

License

MIT