{"_id":"@alphinex/dates","_rev":"3-420c7015d5fcecfb194af92e8bf0bbf3","name":"@alphinex/dates","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@alphinex/dates","version":"1.0.0","license":"UNLICENSED","_id":"@alphinex/dates@1.0.0","maintainers":[{"name":"anasaliqureshi","email":"anas@alphinex.com"}],"dist":{"shasum":"735730b3f3a665915af2312fae46f80b426734df","tarball":"https://registry.npmjs.org/@alphinex/dates/-/dates-1.0.0.tgz","fileCount":5,"integrity":"sha512-Z/yVHRWHCqiA7WwPoXTj1ql5824l2X1kU5Ty5wrOqg6JyBSlGlUH2vUcvLqJl0yvVgVZmV8fFEPop7+JCDvCWQ==","signatures":[{"sig":"MEUCIQDHT0NBUqYut9+dO3e7kjTl1i08wLD33W5/915tRVkJIgIgfGEM82eewD9aUVE6tI44U/DdWhJElAHQoPGlL+vVpto=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":273066},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"private":false,"scripts":{"dev":"vite build --watch","lint":"eslint .","test":"vitest run","build":"vite build","clean":"rimraf dist .turbo","typecheck":"tsc --noEmit"},"_npmUser":{"name":"anasaliqureshi","email":"anas@alphinex.com"},"description":"Intl-based date/time engine + DatePicker, DateRangePicker, TimePicker, DateTimePicker and the shared month-grid cell engine. No date-fns/dayjs/luxon. See documentation/PRODUCT-RESEARCH-MASTER-PLAN.md §10.3 (Sprint 37).","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","dependencies":{"@alphinex/ui":"2.5.1","@alphinex/core":"1.0.0","@alphinex/hooks":"1.0.1","@alphinex/icons":"1.1.0","@alphinex/theme":"1.1.0","@alphinex/utils":"1.0.0"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.0.5","jsdom":"^25.0.1","react":"^19.0.0","eslint":"^9.17.0","vitest":"^2.1.8","axe-core":"^4.10.2","react-dom":"^19.0.0","typescript":"^5.7.2","@types/react":"^19.0.2","@types/react-dom":"^19.0.2","@vitejs/plugin-react":"^4.3.4","@alphinex/build-config":"0.0.0","@testing-library/react":"^16.1.0","@alphinex/eslint-config":"0.0.0","@testing-library/jest-dom":"^6.6.3","@alphinex/typescript-config":"0.0.0","@testing-library/user-event":"^14.5.2"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0"},"peerDependenciesMeta":{"react":{"optional":false},"react-dom":{"optional":false}},"_npmOperationalInternal":{"tmp":"tmp/dates_1.0.0_1786298608646_0.895475590938325","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@alphinex/dates","version":"1.0.1","license":"UNLICENSED","_id":"@alphinex/dates@1.0.1","maintainers":[{"name":"anasaliqureshi","email":"anas@alphinex.com"}],"dist":{"shasum":"60d9ca0421a4f49c0840af58e46ff593000cc11a","tarball":"https://registry.npmjs.org/@alphinex/dates/-/dates-1.0.1.tgz","fileCount":5,"integrity":"sha512-KrYCfHAi/7UWX+hvElezf46/a8bI2BpYrA+wh8f/JbbYpUQYYnL0ZzFqSXC8Yw84dHPseyPJVFsMZfDx1z5m4A==","signatures":[{"sig":"MEYCIQCorWtEb0WrMI6WWLBjP4Ltj4lZCyyu6BPd4elHI6z+0QIhAO4JpAdoE6lwKIHyA5jFns5CRVXkWVsLgaCZVpQv3VHN","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":273066},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"private":false,"scripts":{"dev":"vite build --watch","lint":"eslint .","test":"vitest run","build":"vite build","clean":"rimraf dist .turbo","typecheck":"tsc --noEmit"},"_npmUser":{"name":"anasaliqureshi","email":"anas@alphinex.com"},"description":"Intl-based date/time engine + DatePicker, DateRangePicker, TimePicker, DateTimePicker and the shared month-grid cell engine. No date-fns/dayjs/luxon. See documentation/PRODUCT-RESEARCH-MASTER-PLAN.md §10.3 (Sprint 37).","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","dependencies":{"@alphinex/ui":"2.6.0","@alphinex/core":"1.0.0","@alphinex/hooks":"1.0.2","@alphinex/icons":"1.2.0","@alphinex/theme":"1.1.0","@alphinex/utils":"1.1.0"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.0.5","jsdom":"^25.0.1","react":"^19.0.0","eslint":"^9.17.0","vitest":"^2.1.8","axe-core":"^4.10.2","react-dom":"^19.0.0","typescript":"^5.7.2","@types/react":"^19.0.2","@types/react-dom":"^19.0.2","@vitejs/plugin-react":"^4.3.4","@alphinex/build-config":"0.0.0","@testing-library/react":"^16.1.0","@alphinex/eslint-config":"0.0.0","@testing-library/jest-dom":"^6.6.3","@alphinex/typescript-config":"0.0.0","@testing-library/user-event":"^14.5.2"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0"},"peerDependenciesMeta":{"react":{"optional":false},"react-dom":{"optional":false}},"_npmOperationalInternal":{"tmp":"tmp/dates_1.0.1_1786525483195_0.5779337223613261","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@alphinex/dates","version":"1.0.2","private":false,"description":"Intl-based date/time engine + DatePicker, DateRangePicker, TimePicker, DateTimePicker and the shared month-grid cell engine. No date-fns/dayjs/luxon. See documentation/PRODUCT-RESEARCH-MASTER-PLAN.md §10.3 (Sprint 37).","license":"UNLICENSED","type":"module","sideEffects":false,"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"dependencies":{"@alphinex/core":"1.0.0","@alphinex/theme":"1.1.0","@alphinex/icons":"1.2.0","@alphinex/hooks":"1.0.2","@alphinex/utils":"1.1.0","@alphinex/ui":"2.7.0"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0"},"peerDependenciesMeta":{"react":{"optional":false},"react-dom":{"optional":false}},"devDependencies":{"typescript":"^5.7.2","vite":"^6.0.5","vitest":"^2.1.8","react":"^19.0.0","react-dom":"^19.0.0","@types/react":"^19.0.2","@types/react-dom":"^19.0.2","@testing-library/react":"^16.1.0","@testing-library/jest-dom":"^6.6.3","@testing-library/user-event":"^14.5.2","@vitejs/plugin-react":"^4.3.4","axe-core":"^4.10.2","jsdom":"^25.0.1","eslint":"^9.17.0","@alphinex/build-config":"0.0.0","@alphinex/eslint-config":"0.0.0","@alphinex/typescript-config":"0.0.0"},"scripts":{"build":"vite build","dev":"vite build --watch","lint":"eslint .","typecheck":"tsc --noEmit","test":"vitest run","clean":"rimraf dist .turbo"},"_nodeVersion":"22.14.0","_id":"@alphinex/dates@1.0.2","dist":{"integrity":"sha512-y6pY+4rnn/ciaqeyCCZA4Eu0xZwi6tez1h4EskdIHw+/9jAQm2KFGb0NXxVhSval8w02VfehGREEDs67DC3V3Q==","shasum":"a06e43c2a5a709cf27f2e93823321cb884486bf2","tarball":"https://registry.npmjs.org/@alphinex/dates/-/dates-1.0.2.tgz","fileCount":5,"unpackedSize":273066,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCUIhzG9IhTJfXoMw4Hqy0PLJVAuSCeZLrwIHwhA8n4zwIhAMlnxFfM529z0qH8JsXpnLeZH4bSJ0tObC5YMjBP6m37"}]},"_npmUser":{"name":"anasaliqureshi","email":"anas@alphinex.com"},"directories":{},"maintainers":[{"name":"anasaliqureshi","email":"anas@alphinex.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dates_1.0.2_1786546531271_0.6736180972244219"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-09T18:03:28.511Z","modified":"2026-08-12T14:55:31.617Z","1.0.0":"2026-08-09T18:03:28.846Z","1.0.1":"2026-08-12T09:04:43.361Z","1.0.2":"2026-08-12T14:55:31.435Z"},"license":"UNLICENSED","description":"Intl-based date/time engine + DatePicker, DateRangePicker, TimePicker, DateTimePicker and the shared month-grid cell engine. No date-fns/dayjs/luxon. See documentation/PRODUCT-RESEARCH-MASTER-PLAN.md §10.3 (Sprint 37).","maintainers":[{"name":"anasaliqureshi","email":"anas@alphinex.com"}],"readme":"# @alphinex/dates\n\nDate and time for the Alphinex platform: an `Intl`-based engine, a month-grid cell engine, and\nfour pickers built on top of them.\n\n**No `date-fns`, no `dayjs`, no `luxon`.** Everything here is native `Date` plus `Intl`, so adding\na date field to your app adds no third-party date library to your bundle.\n\n```bash\npnpm add @alphinex/dates\n```\n\nRequires a `<ThemeProvider>` above it (the grids read `dir` to mirror their arrow-key axis under\nRTL), the same as `Slider` and `Tooltip`.\n\n---\n\n## Quick start\n\n```tsx\nimport { ThemeProvider } from \"@alphinex/theme\";\nimport { DatePicker } from \"@alphinex/dates\";\nimport { useState } from \"react\";\n\nfunction InvoiceForm() {\n  const [dueAt, setDueAt] = useState<Date | null>(null);\n\n  return (\n    <ThemeProvider>\n      <DatePicker\n        aria-label=\"Due date\"\n        value={dueAt}\n        onChange={setDueAt}\n        placeholder=\"Pick a date\"\n      />\n    </ThemeProvider>\n  );\n}\n```\n\n---\n\n## Components\n\n### `DatePicker`\n\nA text field that accepts a typed date and opens a calendar.\n\n```tsx\n<DatePicker\n  aria-label=\"Due date\"\n  value={dueAt}\n  onChange={setDueAt}\n  min={new Date()} // nothing in the past\n  disabledDaysOfWeek={[0, 6]} // no weekends\n  disabledDates={publicHolidays}\n  showYearControls // adds « » year paging\n  locale=\"en-GB\"\n/>\n```\n\nYou can always **type** into it. The text is parsed in the locale's own field order, so `03/04/2025`\nis 3 April in `en-GB` and 4 March in `en-US` — read from `Intl`, not from a hardcoded table.\nWritten months work too (`Mar 20, 2024`, `20 März 2024`), which matters because the field displays\na month-name format by default.\n\nUnparseable text is **reverted**, not cleared: a typo can't destroy a date you had already chosen.\nEmptying the field clears the value.\n\n| Key      | Does                                           |\n| -------- | ---------------------------------------------- |\n| `↓`      | Open the calendar and move focus into the grid |\n| `Enter`  | Commit what you typed                          |\n| `Escape` | Discard what you typed / close the calendar    |\n\n### `DateRangePicker`\n\nTwo date fields sharing one two-month calendar.\n\n```tsx\nconst [stay, setStay] = useState({ start: null, end: null });\n\n<DateRangePicker\n  value={stay}\n  onChange={setStay}\n  startLabel=\"Check in\"\n  endLabel=\"Check out\"\n  min={new Date()}\n/>;\n```\n\nWhich end a calendar click sets follows the **focused field**: click into \"Check out\" and pick a\nday, and only the end moves. `onChange` fires twice per range — once with `end: null` while it is\nhalf-picked, then again with both ends — so a controlled parent always sees the true state. A range\npicked backwards is ordered rather than rejected.\n\n### `TimePicker`\n\nA time field with stepped suggestions.\n\n```tsx\n<TimePicker\n  aria-label=\"Start time\"\n  value={startTime}\n  onChange={setStartTime}\n  minuteStep={15}\n  minTime=\"08:00\"\n  maxTime=\"18:00\"\n/>\n```\n\nThe list is a **convenience, not a constraint** — typing `14:37` is accepted even on a 15-minute\nlist. Pass `enforceBounds` if `minTime`/`maxTime` should be hard limits. `2:30 pm`, `14:30`, `1430`\nand `930` all parse.\n\nChanging the time never changes the date: the calendar day of the existing value is preserved.\n\n### `DateTimePicker`\n\nOne field for both, with a calendar and a time list in a single popup.\n\n```tsx\n<DateTimePicker\n  aria-label=\"Starts at\"\n  value={startsAt}\n  onChange={setStartsAt}\n  minuteStep={30}\n  defaultTime=\"09:00\" // given to a date picked before any time was chosen\n/>\n```\n\nChoosing a day never disturbs the time and choosing a time never disturbs the day, so the two\nhalves can be set in either order. The popup does not close by itself — use **Done**, `Escape`, or\nclick away.\n\n### `Calendar`\n\nThe month grid the pickers are built from, usable standalone as an inline calendar.\n\n```tsx\n// Single (default)\n<Calendar value={date} onChange={setDate} />\n\n// Multiple\n<Calendar mode=\"multiple\" value={dates} onChange={setDates} maxSelected={5} />\n\n// Range, two months side by side\n<Calendar mode=\"range\" numberOfMonths={2} value={range} onChange={setRange} />\n```\n\n| Key                           | Does                           |\n| ----------------------------- | ------------------------------ |\n| `←` `→`                       | ± one day (mirrored under RTL) |\n| `↑` `↓`                       | ± one week                     |\n| `Home` / `End`                | First / last day of the week   |\n| `PageUp` / `PageDown`         | ± one month                    |\n| `Shift` + `PageUp`/`PageDown` | ± one year                     |\n| `Enter` / `Space`             | Select the focused day         |\n\nFocus follows across month boundaries, paging the view when it needs to. Days blocked by\n`disabledDates`/`disabledDaysOfWeek` stay **focusable** so a screen reader announces why they can't\nbe chosen; only `min`/`max` stop arrow movement.\n\nUseful options: `weekStartsOn` (0 = Sunday, default 1 = Monday) · `showOutsideDays` ·\n`fixedWeeks` (always six rows, so a popup doesn't resize while paging) · `numberOfMonths` ·\n`weekendDays` · `footer` · `today` (inject a clock so tests don't depend on the day they run).\n\n---\n\n## With `@alphinex/forms`\n\n```tsx\nimport {\n  DatePickerField,\n  DateRangePickerField,\n  DateTimePickerField,\n  TimePickerField,\n} from \"@alphinex/forms\";\n\n<Form form={form} onSubmit={save}>\n  <DatePickerField name=\"dueAt\" label=\"Due date\" min={new Date()} />\n  <TimePickerField name=\"reminderAt\" label=\"Reminder\" minuteStep={15} />\n  <DateTimePickerField name=\"startsAt\" label=\"Starts at\" />\n  <DateRangePickerField name=\"stay\" label=\"Stay\" />\n</Form>;\n```\n\nEach field stores a `Date` (or a `{ start, end }` pair for the range) and accepts ISO strings in\n`defaultValues` — a form hydrated from JSON works without any conversion on your side.\n\n---\n\n## The engine\n\nEvery function below is exported and usable on its own, without rendering anything.\n\n```ts\nimport { addMonths, toISODate, parseDateInput, buildMonthGrid } from \"@alphinex/dates\";\n```\n\n**Arithmetic** — `addDays` `addWeeks` `addMonths` `addYears` `addHours` `addMinutes` ·\n`startOfDay` `endOfDay` `startOfWeek` `endOfWeek` `startOfMonth` `endOfMonth` `startOfYear`\n`endOfYear` · `isSameDay` `isSameMonth` `isSameYear` `isToday` `isBeforeDay` `isAfterDay`\n`isWithinDayRange` · `differenceInDays` `daysInMonth` `clampDate` `withTime` · `createDate`\n`isValidDate`\n\n**Formatting** — `formatDate` `formatTime` `formatDateTime` `formatMonthYear` `formatDayLabel` ·\n`toISODate` `toISOTime` `toISODateTime` · `weekdayLabels` `weekdayFullLabels` `monthLabels`\n\n**Parsing** — `parseDateInput` `parseTimeInput` `parseDateTimeInput` `parseISODate` ·\n`coerceDate` `applyParsedTime` `getDateOrder`\n\n**Grids** — `buildMonthGrid` `getMonthMatrix` `getYearRange` `moveFocusedDate` ·\n`isDateDisabled` `isOutOfBounds`\n\n### Two things worth knowing\n\n**`toISODate` reads the local calendar, not UTC.** `new Date(2024, 0, 5, 0, 30).toISOString()`\ngives `2024-01-04` anywhere east of Greenwich — the single most common date bug in web apps. Use\n`toISODate` / `toISODateTime` to serialise, and `coerceDate` / `parseISODate` to read back.\n\n**`addMonths` clamps instead of overflowing.** Native `setMonth` turns 31 January + 1 month into\n2 March, which shows up as a picker appearing to skip February. Here it is 29 February (or the 28th\nin a non-leap year). `addYears` does the same.\n\n```ts\naddMonths(new Date(2024, 0, 31), 1); // 29 Feb 2024\naddYears(new Date(2024, 1, 29), 1); // 28 Feb 2025\n```\n\n### Custom calendars\n\n`buildMonthGrid` is the same cell engine `Calendar` and `@alphinex/calendar`'s month view use, so\na bespoke calendar agrees with the pickers by construction:\n\n```ts\nconst weeks = buildMonthGrid({\n  month: new Date(),\n  weekStartsOn: 1,\n  range: { start, end },\n  constraints: { min, max, disabledDates },\n  fixedWeeks: true,\n});\n// weeks: DayCell[][] — each cell has date, key, dayOfMonth, isOutsideMonth,\n// isToday, isSelected, isDisabled, isRangeStart, isRangeEnd, isInRange, isWeekend\n```\n\n---\n\n## Accessibility\n\n- `Calendar` is a WAI-ARIA `grid` with roving tabindex — one tab stop for the whole month.\n- The pickers follow the date-picker **combobox** pattern: the input is a `combobox` with\n  `aria-haspopup=\"dialog\"`, the popup is a non-modal `dialog` that traps `Tab` and restores focus.\n- Trigger buttons are named after their field (`\"Open calendar for Due date\"`), so two date fields\n  on one page are distinguishable.\n- Every component is asserted against `axe-core` in its own test suite.\n\n## License\n\nUNLICENSED — internal to Alphinex Solutions.\n","readmeFilename":""}