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

p1-time-ui

v0.11.0

Published

Lightweight, headless-ish React date + time + range pickers. Tailwind-friendly, MIT, no dependencies beyond React + clsx.

Readme

p1-time-ui

A small, MIT-licensed, Tailwind-friendly React date + time + range picker set. Six components covering single + range pickers across dates, times, and combined date-times — all with a unified controlled-component string API.

  • Six componentsTimePicker, TimeRangePicker, DatePicker, DateRangePicker, DateTimePicker, DateTimeRangePicker. The range pickers use a single shared popover with two parallel columns / two calendars, so picking a start and an end takes two clicks without re-opening anything.
  • No design-system lock-in / no date library — just React + a tiny clsx. The whole package is ~14 KB minified+gzipped (50 KB unminified).
  • Strings on the API surface'HH:mm' for times, 'YYYY-MM-DD' for dates, 'YYYY-MM-DDTHH:mm' for date-times. Date objects are scratch values only, so timezone and DST never bite you.
  • Typed end-to-end — strict TypeScript declarations ship in dist/index.d.ts.
  • Themable — every visible colour reads from a --p1tu-* CSS variable with a sensible default, so consumers can match their design system without forking the component.
  • Keyboard-first — typing into the input is the primary path; the popover is the discovery aid.

Install

yarn add p1-time-ui
# or
npm install p1-time-ui

Peer deps: react >= 18, react-dom >= 18.

Styling (Tailwind required)

p1-time-ui ships no CSS. Every component renders Tailwind CSS utility classes directly in its markup, so your app must have Tailwind configured and Tailwind's content scanner must see this package's compiled files — otherwise the utilities the components depend on are tree-shaken out of your build and the pickers render unstyled.

Add the package's dist to your content globs (Tailwind v3):

// tailwind.config.js
export default {
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    // Let Tailwind discover the classes baked into p1-time-ui:
    './node_modules/p1-time-ui/dist/**/*.{js,mjs}',
  ],
  // ...
}

On Tailwind v4 (CSS-first config) add an @source directive instead:

@import 'tailwindcss';
@source '../node_modules/p1-time-ui/dist';

The components carry their own colours via --p1tu-* CSS variables with sensible defaults (see Theming), so once Tailwind is scanning them they look right with zero extra CSS — overriding a variable is optional.

The pickers

TimePicker (single time)

import { useState } from 'react'
import { TimePicker } from 'p1-time-ui'

const [time, setTime] = useState('09:00')

<TimePicker
  label="Start time"
  value={time}
  onChange={setTime}
  step={15}            // 5 / 10 / 15 / 30 / 60 — must divide 60
  min="07:00"
  max="22:00"
/>

The popover is a multi-section digital clock (MUI X style): an Hours column and a Minutes column side by side, rather than one long scroll of every HH:mm. Pick the hour (the cursor hands off to the minutes column), then the minute (which completes the value and closes). The selected value in each column is a filled accent pill. Keyboard: ↑/↓ move within the active column, ←/→ switch columns, Enter commits, Esc closes. Free-typing HH:mm in the field still works as the primary fast path. The step prop drives the Minutes column (e.g. 1500 / 15 / 30 / 45); buildHours / buildMinutesForHour are exported if you need the same lists outside the component.

TimeRangePicker (start + end times, one popover)

import { useState } from 'react'
import { TimeRangePicker } from 'p1-time-ui'

const [range, setRange] = useState({ start: '09:00', end: '10:00' })

<TimeRangePicker
  value={range}
  onChange={setRange}
  step={15}
  min="07:00"
  max="22:00"
  defaultDurationMinutes={60}   // auto-fills end to start+60min
  enforceEndAfterStart           // dim end slots that are ≤ start
/>

The popover renders two parallel scrollable columns (Start | End). defaultDurationMinutes auto-fills the end whenever the user picks a new start — until they manually touch the end field, then the auto-fill goes silent. Clearing the end re-arms it.

DateRangePicker / RangeCalendar render the selected span as one continuous band (square cells, gap-0) with the start and end as filled circles — hover previews the band to the day under the cursor.

Single-input mode — pass singleInput to collapse the two fields into one combobox showing "09:00 → 10:00". The popover stacks a Start / End segmented tab over a single multi-section Hours|Minutes clock; picking the start time auto-advances the tab to End, and picking the end completes + closes. defaultDurationMinutes, enforceEndAfterStart, and the label caption all apply.

<TimeRangePicker
  singleInput
  label="Time"
  value={range}
  onChange={setRange}
  step={15}
  defaultDurationMinutes={60}
/>

DatePicker (single date)

import { useState } from 'react'
import { DatePicker } from 'p1-time-ui'

const [date, setDate] = useState('2026-05-26')

<DatePicker
  label="Visit"
  value={date}
  onChange={setDate}
  min="2026-01-01"
  max="2026-12-31"
  firstDayOfWeek={1}             // 0=Sun, 1=Mon (default 1)
  locale="en-GB"
  displayFormat="DD-MM-YYYY"     // input shows 26-05-2026 (default YYYY-MM-DD)
  isDateDisabled={(d) => d.getDay() === 0}  // disable Sundays
/>

Six-row month calendar with prev/next chevrons, weekday header, localised month title. Picked day = solid pill, today = ring, dates outside the visible month or outside [min, max] are dimmed.

The value / onChange contract is always canonical ISO YYYY-MM-DD — that doesn't change. displayFormat only controls what the text input renders and accepts when typed: 'YYYY-MM-DD' (default), 'DD-MM-YYYY', 'DD/MM/YYYY', or 'MM/DD/YYYY'. The same conversion is available standalone via the exported formatDateForDisplay(iso, format) / parseDateFromDisplay(text, format) helpers.

DateRangePicker (start + end dates, two-month popover)

import { useState } from 'react'
import { DateRangePicker } from 'p1-time-ui'

const [range, setRange] = useState({ start: '2026-05-26', end: '2026-06-02' })

<DateRangePicker
  value={range}
  onChange={setRange}
  monthsToShow={2}              // 1 or 2 (default 2)
  min="2026-01-01"
  max="2027-12-31"
  enforceEndAfterStart           // clear end if a new start is ≥ it
/>

Two side-by-side calendars (consecutive months). Click a start day → hover over a candidate end day → preview the range with a soft fill → click to commit. Clicking before the current start treats it as a fresh start. Date inputs accept typed YYYY-MM-DD on either side.

DateTimePicker (single date + time)

import { useState } from 'react'
import { DateTimePicker } from 'p1-time-ui'

const [dt, setDt] = useState('2026-05-26T09:00')

<DateTimePicker
  value={dt}                    // 'YYYY-MM-DDTHH:mm' local
  onChange={setDt}
  step={15}
  minDate="2026-01-01"
  maxDate="2026-12-31"
  minTime="07:00"
  maxTime="22:00"
/>

Composes DatePicker and TimePicker side by side. The combined value is an ISO-local string 'YYYY-MM-DDTHH:mm' (no timezone designator — the value represents a local wall-clock moment). Half-filled values stay coherent: clearing one side reduces the output to just the other side (or empty), never a malformed 'T09:00'.

DateTimeRangePicker (start + end date+time)

import { useState } from 'react'
import { DateTimeRangePicker } from 'p1-time-ui'

const [range, setRange] = useState({
  start: '2026-05-26T09:00',
  end: '2026-05-26T10:00',
})

<DateTimeRangePicker
  value={range}
  onChange={setRange}
  step={15}
  minDate="2026-01-01"
  maxDate="2027-12-31"
  defaultDurationMinutes={60}
/>

Stacks a DateRangePicker (dates) on top of a TimeRangePicker (times). The two sub-pickers work independently — clicking into the dates row opens the calendar, into the times row opens the slot columns — so users can edit either dimension without committing the other. defaultDurationMinutes auto-fills the end time when the start time changes.

Theming

Override any of these CSS variables on the picker root (or anywhere above it in the cascade) to retheme:

| Variable | Default | What it sets | | --- | --- | --- | | --p1tu-bg | #fafafa | Input field background | | --p1tu-bg-disabled | #ffffff | Disabled input background | | --p1tu-text | #111827 | Typed value colour | | --p1tu-label | #6b7280 | Floating label colour | | --p1tu-placeholder | #9ca3af | Placeholder colour | | --p1tu-border | #e5e7eb | Idle border | | --p1tu-border-error | #dc2626 | Error border | | --p1tu-border-disabled | #f5f5f5 | Disabled border | | --p1tu-icon | #9ca3af | Chevron / arrow colour | | --p1tu-icon-hover | #6b7280 | Chevron hover colour | | --p1tu-arrow | #9ca3af | separator between start/end fields | | --p1tu-divider | #e5e7eb | Column divider inside the range popover | | --p1tu-column-title | #6b7280 | Column / weekday headers | | --p1tu-slot-text | #111827 | Slot / day text | | --p1tu-slot-selected-bg | #1f2937 (date) / #eef2ff (time) | Selected pill background | | --p1tu-slot-selected-text | #ffffff (date) / #1e293b (time) | Selected pill text | | --p1tu-slot-hover-bg | #f9fafb | Slot hover background | | --p1tu-slot-dim | #cbd5e1 | Dimmed (out-of-range / out-of-month) text | | --p1tu-range-bg | #eef2ff | In-range day-cell fill on DateRangePicker | | --p1tu-today-ring | #cbd5e1 | Today-marker ring on the calendar | | --p1tu-focus-ring | #1f2937 | Keyboard-focus outline on calendar day cells + active range-tab underline |

Utility exports

Same time + date helpers the pickers use, exported so consumers can do their own slot / day math (e.g. for server-side validation parity):

import {
  // Time
  parseTime, formatTime, buildSlots, addMinutes, compareTime,
  isWithinBounds, pad2, MINUTES_PER_DAY,

  // Date
  parseDate, formatDate, formatDateLocale,
  addDays, addMonths, compareDate,
  isSameDay, isSameMonth, isWithinDateBounds,
  monthGrid, monthTitle, weekdayShortLabels,
} from 'p1-time-ui'

import type { Weekday } from 'p1-time-ui'

Development

yarn install
yarn build       # tsup → dist/{index.js,index.mjs,index.d.ts}
yarn test        # vitest run
yarn typecheck   # tsc --noEmit
yarn lint        # eslint src

Current test count: 160 (80 utility unit tests + 80 component interaction tests).

License

MIT © Mathematica AI