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

ad-bs-calendar

v1.2.3

Published

Dual AD / Bikram Sambat (Nepali) date picker for React. Single and range selection, fully themeable with CSS variables, mobile bottom sheet, keyboard and screen-reader accessible.

Readme

द ad-bs-calendar

A dual AD / Bikram Sambat date picker for React.

  • Three calendars, one payload - render a Gregorian grid, a real BS grid (Baisakh–Chaitra, 29–32 days), or both at once. onChange always reports both.
  • Fully themeable - every colour, size, font, radius and duration is a CSS custom property. No !important required, ever.
  • Mobile-first - turns into a modal bottom sheet on small screens, with 44px touch targets and no iOS zoom-on-focus.
  • Accessible - combobox + grid ARIA, full keyboard support, screen-reader labels in both calendars, reduced-motion and forced-colours aware.
  • Typed - TypeScript declarations ship with the package.
  • Small - ~9.7 kB gzipped, zero runtime dependencies.

Install

npm install ad-bs-calendar
import { DualDatePicker } from 'ad-bs-calendar';
import 'ad-bs-calendar/style.css';   // required - the CSS ships separately

function App() {
  return <DualDatePicker onChange={(d) => console.log(d)} />;
}

Peer dependencies: react and react-dom >= 16.8.

The payload

onChange fires with the same shape regardless of dateMode - the calendar you display never changes the data you receive:

{
  ad: '2024-04-13',          // Gregorian, YYYY-MM-DD
  bs: '2081-01-01',          // Bikram Sambat, YYYY-MM-DD
  bsDetails: {               // full BS breakdown
    year: 2081, yearNp: '२०८१',
    month: 1,   day: 1, dayNp: '१',
    monthName: 'Baisakh', monthNameNp: 'बैशाख',
    formatted: '2081-01-01'
  },
  adDate: Date               // local midnight
}

In pickerMode="range" it fires with { start, end }, where each side is the object above and end is null until the second date is picked. Clearing emits null.

value, defaultValue, minDate and maxDate are always Gregorian "YYYY-MM-DD", in every dateMode. BS is a display concern.

Props

Value

| Prop | Type | Default | | |---|---|---|---| | value | string \| Date \| {start, end} \| null | - | Controlled value. Pass {start, end} in range mode. | | defaultValue | same as value | - | Uncontrolled initial value. | | onChange | (data) => void | - | See The payload. |

Modes

| Prop | Type | Default | | |---|---|---|---| | dateMode | 'dual' \| 'ad' \| 'bs' | 'dual' | dual: AD grid with BS numbers annotated. ad: pure Gregorian, no Devanagari anywhere. bs: a real BS month grid, navigated by BS year/month. | | pickerMode | 'single' \| 'range' | 'single' | Range turns the grid into a start/end picker. The text input becomes read-only. |

Constraints

| Prop | Type | Default | | |---|---|---|---| | minDate / maxDate | string | - | Inclusive bounds, "YYYY-MM-DD". | | shouldDisableDate | (date, bs) => boolean | - | Return true to make a date unselectable (weekends, holidays, booked slots). Receives the local-midnight Date and its BS breakdown, so Nepali-calendar rules need no conversion. Runs after min/max. | | required | boolean | false | Native HTML5 validation. | | disabled | boolean | false | |

Form

| Prop | Type | Default | | |---|---|---|---| | id | string | auto | | | name | string | - | Emits a hidden input with the ISO value (the visible field holds formatted display text). In range mode: ${name}_start and ${name}_end. | | autoFocus | boolean | false | | | autoComplete | string | 'off' | | | placeholder | string | auto | |

Unknown props (aria-label, data-*, onKeyUp, …) are forwarded to the input.

Presentation

| Prop | Type | Default | | |---|---|---|---| | className | string | - | On the wrapper. | | classNames | object | {} | Per-part classes - see Styling. | | style | CSSProperties | - | On the wrapper. The place for --rdc-* overrides. | | themeColor | string | - | Shorthand for --rdc-primary + --rdc-selected. | | theme | 'light' \| 'dark' \| 'auto' | 'light' | auto follows prefers-color-scheme. | | labels | object | English | Every user-facing string - see Localisation. |

Behaviour

| Prop | Type | Default | | |---|---|---|---| | weekStartsOn | 06 | 0 | 0 = Sunday. | | closeOnSelect | boolean | true | | | presets | Preset[] | - | Quick-select chips in the popover - see Presets. | | inline | boolean | false | Always-visible calendar, no input, no popover. | | showFooter | boolean | true | | | showTodayButton | boolean | true | | | showClearButton | boolean | true | | | portal | boolean | false | Render the popover into document.body. Turn on when an ancestor clips (overflow: hidden), creates a containing block (transform, filter), or creates a stacking context (backdrop-filter, z-index cards) that would paint siblings over the popover. | | portalContainer | Element | document.body | | | sheetBreakpoint | number | 480 | Viewport width (px) at or below which the popover becomes a bottom sheet. | | onOpen / onClose | () => void | - | |

Presets

Quick-select chips rendered between the grid and the footer. In single mode a preset's value is a date; in range mode it's { start, end }. A function value is re-evaluated on every render, so relative presets stay correct in long-lived pages:

<DualDatePicker
  pickerMode="range"
  presets={[
    { label: 'Last 7 days',  value: () => ({ start: addDays(new Date(), -6), end: new Date() }) },
    { label: 'This month',   value: () => ({
        start: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
        end:   new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0)
    }) },
    { label: 'Bardiya trip', value: { start: '2026-10-02', end: '2026-10-09' } }
  ]}
/>

Chips that violate minDate/maxDate/shouldDisableDate render disabled. Clicking one commits the value, navigates the grid to it, and (with closeOnSelect) closes the popover.

Disabling dates

shouldDisableDate runs for every rendered day, typed input, preset, and the Today button:

{/* Saturday is Nepal's weekend */}
<DualDatePicker shouldDisableDate={(date) => date.getDay() === 6} />

{/* BS rules work directly - the second argument is the BS breakdown */}
<DualDatePicker shouldDisableDate={(date, bs) => bs.day === 1} />

A typed date that hits the callback reports the errorDateUnavailable label ("Date unavailable.") rather than the min/max message.

Styling

Everything visual is a CSS custom property. Override them wherever you like:

/* globally */
:root {
  --rdc-primary: #16a34a;
  --rdc-radius: 12px;
  --rdc-cell-size: 48px;
}
/* per instance */
<DualDatePicker style={{ '--rdc-primary': '#dc2626', '--rdc-font-family': 'Inter, sans-serif' }} />

/* or with a class */
<DualDatePicker className="brand-picker" />

Tokens come in two kinds. Base tokens (--rdc-primary, --rdc-bg, --rdc-border, …) hold literal defaults. Derived tokens (--rdc-today-dot, --rdc-surface, --rdc-selected, …) default to a base token and are resolved where they're used, so overriding one base token cascades everywhere it feeds:

:root { --rdc-primary: #16a34a; }   /* today ring + dot, focus ring, buttons, ornaments, glow */
:root { --rdc-today-dot: #f59e0b; } /* ...or retarget just the one */

Both levels work at any scope - global, per class, or inline.

Tokens

Colour

| Token | Default | | |---|---|---| | --rdc-primary | #2563eb | Accent: focus rings, today, buttons. | | --rdc-primary-contrast | #ffffff | Text on --rdc-primary. | | --rdc-danger | #ef4444 | Errors, clear-hover. | | --rdc-bg / --rdc-surface | #ffffff | Page / popover background. | | --rdc-text-main | #1f2937 | | | --rdc-text-light | #6b7280 | Weekday labels, BS numbers, footer. | | --rdc-text-disabled | #d1d5db | | | --rdc-border | #e5e7eb | | | --rdc-hover | #eff6ff | Cell hover, range fill. | | --rdc-selected / --rdc-selected-text | --rdc-primary / #ffffff | | | --rdc-selected-inner-ring | rgb(255 255 255 / 0.3) | | | --rdc-range-bg / --rdc-range-preview-bg | --rdc-hover | | | --rdc-disabled-bg / --rdc-disabled-text | #f9fafb / --rdc-text-disabled | | | --rdc-today-border / --rdc-today-bg / --rdc-today-dot | --rdc-primary / --rdc-hover / --rdc-primary | | | --rdc-input-bg, --rdc-input-border, --rdc-input-text, --rdc-input-placeholder, --rdc-input-readonly-bg, --rdc-input-disabled-bg | | | | --rdc-backdrop | rgb(15 23 42 / 0.45) | Bottom-sheet scrim. |

Typography

--rdc-font-family, --rdc-font-size, --rdc-input-font-size, --rdc-select-font-size, --rdc-weekday-font-size, --rdc-day-font-size, --rdc-bs-font-size, --rdc-footer-font-size, --rdc-font-weight-medium, --rdc-font-weight-bold

Metrics

--rdc-width (280px), --rdc-popover-width (300px), --rdc-popover-padding, --rdc-popover-offset, --rdc-input-padding-y, --rdc-input-padding-x, --rdc-cell-size (42px), --rdc-cell-gap, --rdc-radius, --rdc-radius-sm, --rdc-radius-input, --rdc-radius-sheet

Effects

--rdc-shadow, --rdc-shadow-cell-hover, --rdc-focus-ring-width, --rdc-focus-ring-color, --rdc-today-glow, --rdc-today-glow-soft, --rdc-ornament-color, --rdc-ornament-opacity, --rdc-popover-wash, --rdc-footer-wash, --rdc-cell-hover-scale, --rdc-z-index, --rdc-duration, --rdc-duration-popover, --rdc-ease-spring

Set --rdc-ornament-opacity: 0 to drop the decorative corner accents, and --rdc-cell-hover-scale: 1 to stop cells lifting on hover.

Class hooks

Built-in classes (rdc-wrapper, rdc-input, rdc-popover, rdc-cell, …) are stable API. To add your own without fighting specificity, use classNames:

<DualDatePicker
  classNames={{
    wrapper: 'w-full', input: 'ring-1 ring-slate-200',
    popover: 'shadow-2xl', day: 'font-mono', todayButton: 'uppercase'
  }}
/>

Keys: wrapper, input, popover, backdrop, header, navButton, select, grid, weekday, day, presets, presetButton, footer, todayButton, clearButton.

State classes on .rdc-cell: selected, active, today, disabled, range-start, range-end, in-range, in-range-preview.

Dark mode

<DualDatePicker theme="auto" />   {/* or "dark" */}

The wrapper and popover carry data-rdc-theme, so you can retarget the tokens yourself:

.rdc-wrapper[data-rdc-theme="dark"] { --rdc-bg: #0b1220; }

Mobile

At or below sheetBreakpoint (480px), the popover becomes a modal bottom sheet: backdrop, safe-area padding for the home indicator, body-scroll lock, focus moved into the sheet (which dismisses the on-screen keyboard that would otherwise cover it), and max-height: 90dvh with internal scrolling for landscape.

Handled for you regardless of breakpoint:

  • Touch targets - cells grow to 44px and buttons to 40px on pointer: coarse.
  • No iOS zoom - the input renders at 16px on coarse pointers; anything smaller makes Safari zoom the viewport on focus and never zoom back.
  • No sticky hover - the hover lift is gated behind hover: hover, so cells don't stay scaled after a tap.
  • No double-tap delay - touch-action: manipulation on cells.

Manual typing isn't available in sheet mode (focus lives in the sheet, as with native mobile pickers). Use sheetBreakpoint={0} to keep the popover on all sizes.

Accessibility

  • The input is a combobox (aria-expanded, aria-controls, aria-activedescendant); the popover is a dialog; the grid is a real grid/row/gridcell/columnheader tree.
  • Every day is labelled in the active calendar(s) - dual mode reads "13 APR 2024 (1 Baisakh 2081 BS)". Today gets aria-current="date".
  • A polite live region announces the visible month on navigation.
  • Errors surface through aria-invalid and native setCustomValidity.
  • prefers-reduced-motion disables all animation; forced-colors hands rendering to the OS palette.

Keyboard

| Key | | |---|---| | | Move by a day / week (opens the picker if closed) | | Home / End | Start / end of the week (respects weekStartsOn) | | PageUp / PageDown | ± 1 month (BS months in bs mode) | | Shift + PageUp/PageDown | ± 1 year | | Enter | Select the focused day | | Esc | Close |

Localisation

<DualDatePicker
  labels={{
    weekdays: ['आइत', 'सोम', 'मंगल', 'बुध', 'बिहि', 'शुक्र', 'शनि'],
    today: 'आज',
    clear: 'हटाउनुहोस्',
    previousMonth: 'अघिल्लो महिना',
    nextMonth: 'अर्को महिना',
    errorRequired: 'मिति छान्नुहोस्।'
  }}
/>

Partial overrides merge over the English defaults. Also available: months, calendar, month, year, placeholder, placeholderRange, requiredSuffix, errorRangeRequired, errorFormat, errorInvalidBs, errorInvalidAd, errorOutOfRange, errorDateUnavailable, presets, rangeSeparator.

Recipes

Controlled, in a form

const [dob, setDob] = useState(null);

<form onSubmit={...}>
  <DualDatePicker
    name="dob"                       {/* submits ISO via a hidden input */}
    value={dob}
    onChange={(d) => setDob(d?.ad ?? null)}
    maxDate={new Date().toISOString().slice(0, 10)}
    required
  />
</form>

Range, BS-only, Monday-first

<DualDatePicker pickerMode="range" dateMode="bs" weekStartsOn={1} onChange={({ start, end }) => ...} />

Inside a modal or a scroll container

<DualDatePicker portal />   {/* escapes overflow:hidden / transformed ancestors */}

Conversion utilities

The AD ↔ BS layer is exported on its own - useful for server-side formatting or validation:

import { adToBs, bsToAd, parseAdDate, formatDate, toNepaliDigits, getBsMonthDays, getBsYearRange } from 'ad-bs-calendar';

adToBs('2024-04-13').formatted   // '2081-01-01'
bsToAd(2081, 1, 1)               // Date - 2024-04-13, local midnight
parseAdDate('2024-04-13')        // Date - local midnight, null if invalid
toNepaliDigits(2081)             // '२०८१'
getBsYearRange()                 // [1970, 2099]

parseAdDate exists because new Date("2024-04-13") parses as UTC midnight, which is the previous day in any timezone west of Greenwich. Use it instead of the Date constructor for date-only strings.

Supported span: 1913-04-13 → 2043-04-13 AD (1970–2099 BS), the extent of the BS month-length table. Dates outside it return { year: null, monthName: 'Out of Range' } from adToBs and are disabled in dual mode.

Development

| | | |---|---| | npm run dev | Vite playground (src/dev.jsx) | | npm test | Vitest suite (103 tests) | | npm run build | Build dist/ |

License

MIT