@nepdatejs/datepicker 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@nepdatejs/datepicker",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Framework-free BS/AD date picker behaviour: typed input, validation, BS/AD switch, single and range values.",
5
+ "keywords": [
6
+ "nepali",
7
+ "bikram-sambat",
8
+ "bs",
9
+ "nepali-date",
10
+ "date",
11
+ "calendar",
12
+ "datepicker",
13
+ "nepal"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Shishir Rai",
17
+ "homepage": "https://github.com/nepdate/nepdate/tree/main/packages/datepicker#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/nepdate/nepdate.git",
21
+ "directory": "packages/datepicker"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/nepdate/nepdate/issues"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "exports": {
29
+ ".": {
30
+ "@nepdatejs/source": "./src/index.ts",
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "main": "./dist/index.cjs",
43
+ "module": "./dist/index.js",
44
+ "types": "./dist/index.d.cts",
45
+ "unpkg": "./dist/nepdate-datepicker.global.js",
46
+ "jsdelivr": "./dist/nepdate-datepicker.global.js",
47
+ "files": [
48
+ "dist",
49
+ "src",
50
+ "!src/**/*.test.ts",
51
+ "README.md",
52
+ "LICENSE"
53
+ ],
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "dependencies": {
58
+ "@nepdatejs/core": "^0.1.0",
59
+ "@nepdatejs/calendar": "^0.1.0"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "scripts": {
65
+ "build": "tsdown",
66
+ "typecheck": "tsc -p tsconfig.json",
67
+ "check:package": "publint --strict && attw --pack"
68
+ }
6
69
  }
package/src/base.ts ADDED
@@ -0,0 +1,130 @@
1
+ import {
2
+ getConfig,
3
+ today,
4
+ type CalendarDate,
5
+ type DateFormatPattern,
6
+ type LocaleCode,
7
+ } from '@nepdatejs/core';
8
+ import {
9
+ isDayList,
10
+ type CalendarOptions,
11
+ type CalendarSelection,
12
+ type CalendarStore,
13
+ type DateRange,
14
+ } from '@nepdatejs/calendar';
15
+ import { readIso, type InputLimits } from './input';
16
+ import type { PickerBaseOptions } from './types';
17
+
18
+ /** The panel calendar's options that come straight from the picker's options. */
19
+ export function panelOptions(
20
+ options: PickerBaseOptions,
21
+ calendar: CalendarOptions['calendar'],
22
+ ): CalendarOptions {
23
+ return {
24
+ calendar,
25
+ locale: options.locale,
26
+ min: readIso(options.min),
27
+ max: readIso(options.max),
28
+ isDateDisabled: options.isDateDisabled,
29
+ holidays: options.holidays,
30
+ events: options.events,
31
+ showOtherCalendar: options.showOtherCalendar,
32
+ weekStartsOn: options.weekStartsOn,
33
+ weekendDays: options.weekendDays,
34
+ today: options.today,
35
+ };
36
+ }
37
+
38
+ /** What may be typed: the panel's effective range, so typing and clicking agree. */
39
+ export function inputLimits(
40
+ options: PickerBaseOptions,
41
+ panel: CalendarStore,
42
+ ): InputLimits {
43
+ const { minDate, maxDate } = panel.getState();
44
+
45
+ return {
46
+ min: minDate,
47
+ max: maxDate,
48
+ hasMin: readIso(options.min) !== null,
49
+ hasMax: readIso(options.max) !== null,
50
+ isDateDisabled: options.isDateDisabled,
51
+ };
52
+ }
53
+
54
+ export function localeOf(options: PickerBaseOptions): LocaleCode {
55
+ return options.locale ?? getConfig().defaultLocale;
56
+ }
57
+
58
+ export function formatOf(options: PickerBaseOptions): DateFormatPattern {
59
+ return options.displayFormat ?? 'iso';
60
+ }
61
+
62
+ export function todayOf(options: PickerBaseOptions): CalendarDate {
63
+ return options.today ?? today('AD');
64
+ }
65
+
66
+ export function isRange(selection: CalendarSelection): selection is DateRange {
67
+ return !isDayList(selection) && !('calendar' in selection);
68
+ }
69
+
70
+ export function isSingleDay(selection: CalendarSelection): selection is CalendarDate {
71
+ return !isDayList(selection) && 'calendar' in selection;
72
+ }
73
+
74
+ /**
75
+ * Listener set plus batching: changes made inside `act` notify once at the end,
76
+ * and callbacks queued with `after` run after listeners have seen the new state.
77
+ */
78
+ export function createEmitter(rebuild: () => void): {
79
+ subscribe(listener: () => void): () => void;
80
+ act(change: () => void): void;
81
+ after(callback: () => void): void;
82
+ notify(): void;
83
+ clear(): void;
84
+ } {
85
+ const listeners = new Set<() => void>();
86
+ const queued: (() => void)[] = [];
87
+ let depth = 0;
88
+
89
+ function notify(): void {
90
+ rebuild();
91
+ for (const listener of [...listeners]) {
92
+ listener();
93
+ }
94
+ }
95
+
96
+ return {
97
+ subscribe(listener) {
98
+ listeners.add(listener);
99
+
100
+ return () => {
101
+ listeners.delete(listener);
102
+ };
103
+ },
104
+ act(change) {
105
+ depth++;
106
+ try {
107
+ change();
108
+ } finally {
109
+ depth--;
110
+ }
111
+ if (depth === 0) {
112
+ notify();
113
+ for (const callback of queued.splice(0)) {
114
+ callback();
115
+ }
116
+ }
117
+ },
118
+ after(callback) {
119
+ queued.push(callback);
120
+ },
121
+ notify() {
122
+ if (depth === 0) {
123
+ notify();
124
+ }
125
+ },
126
+ clear() {
127
+ listeners.clear();
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,397 @@
1
+ import {
2
+ formatNumber,
3
+ formatTime,
4
+ fromIsoDateTime,
5
+ getConfig,
6
+ getLocale,
7
+ now,
8
+ parseTime,
9
+ toIsoDate,
10
+ toIsoDateTime,
11
+ type AdDate,
12
+ type BsDate,
13
+ type CalendarDate,
14
+ type CalendarSystem,
15
+ type TimeOfDay,
16
+ } from '@nepdatejs/core';
17
+ import type { CalendarState, CalendarStore } from '@nepdatejs/calendar';
18
+ import { createEmitter, localeOf } from './base';
19
+ import { createDatePicker } from './picker';
20
+ import type { PresetState, SinglePreset } from './presets';
21
+ import type { DateError, PickerBaseOptions } from './types';
22
+
23
+ export interface DateTimeDetail {
24
+ readonly ad: AdDate;
25
+ readonly bs: BsDate;
26
+ readonly time: TimeOfDay;
27
+ /** The same instant as the value. */
28
+ readonly iso: string;
29
+ }
30
+
31
+ export interface DateTimePickerOptions extends PickerBaseOptions {
32
+ /** An ISO timestamp with offset ("2026-10-18T14:30:00+05:45"), a local "YYYY-MM-DDTHH:MM", or "". */
33
+ readonly value?: string | undefined;
34
+ readonly defaultValue?: string | undefined;
35
+ readonly onChange?:
36
+ ((value: string, detail: DateTimeDetail | null) => void) | undefined;
37
+ /** The zone the date and time are read in, and whose offset the value carries. Default: core config (Nepal). */
38
+ readonly timeZone?: string | undefined;
39
+ /** Show 12-hour times (2:30 PM). Default false (14:30). */
40
+ readonly hour12?: boolean | undefined;
41
+ /** Minutes offered in the column: 1–30. Default 5. Any minute can still be typed. */
42
+ readonly minuteStep?: number | undefined;
43
+ /** The time used when a date is picked before any time. Default "00:00". */
44
+ readonly defaultTime?: string | undefined;
45
+ readonly presets?: readonly SinglePreset[] | undefined;
46
+ }
47
+
48
+ /** One hour or minute in the time columns (Figma "Time Item"). */
49
+ export interface TimeCell {
50
+ readonly value: number;
51
+ readonly label: string;
52
+ readonly isSelected: boolean;
53
+ }
54
+
55
+ export interface DateTimePickerState {
56
+ /** ISO timestamp with the zone's offset, or "" until both date and time are valid. */
57
+ readonly value: string;
58
+ readonly detail: DateTimeDetail | null;
59
+ readonly dateText: string;
60
+ readonly timeText: string;
61
+ readonly time: TimeOfDay | null;
62
+ readonly calendar: CalendarSystem;
63
+ readonly canSwitchCalendar: boolean;
64
+ /** The panel is showing. Always true for an inline picker. */
65
+ readonly open: boolean;
66
+ /** The calendar sits in the page and never closes (option `inline`). */
67
+ readonly inline: boolean;
68
+ readonly error: (DateError & { readonly field: 'date' | 'time' }) | null;
69
+ readonly equivalentText: string | null;
70
+ readonly placeholder: string;
71
+ readonly timePlaceholder: string;
72
+ readonly hours: readonly TimeCell[];
73
+ readonly minutes: readonly TimeCell[];
74
+ readonly presets: readonly PresetState[];
75
+ readonly calendarState: CalendarState;
76
+ }
77
+
78
+ export interface DateTimePickerActions {
79
+ typeDate(text: string): void;
80
+ typeTime(text: string): void;
81
+ blurDate(): void;
82
+ blurTime(): void;
83
+ /** Pick a day; the time stays (or becomes defaultTime). Returns false when disabled. */
84
+ selectDate(date: CalendarDate): boolean;
85
+ selectHour(hour: number): void;
86
+ selectMinute(minute: number): void;
87
+ /** Today and the current time in the zone. Returns false when today is not allowed. */
88
+ selectNow(): boolean;
89
+ applyPreset(id: string): boolean;
90
+ open(): void;
91
+ close(): void;
92
+ toggle(): void;
93
+ clear(): void;
94
+ setValue(value: string): void;
95
+ switchCalendar(calendar: CalendarSystem): void;
96
+ reset(): void;
97
+ }
98
+
99
+ export interface DateTimePickerStore {
100
+ getState(): DateTimePickerState;
101
+ subscribe(listener: () => void): () => void;
102
+ setOptions(changes: Partial<DateTimePickerOptions>): void;
103
+ readonly actions: DateTimePickerActions;
104
+ readonly calendar: CalendarStore;
105
+ destroy(): void;
106
+ }
107
+
108
+ /**
109
+ * A BS/AD date and time picker without UI. The date half is a regular date
110
+ * picker; the value is the instant as an ISO timestamp carrying the zone's
111
+ * offset, so servers store one unambiguous moment.
112
+ */
113
+ export function createDateTimePicker(
114
+ initial: DateTimePickerOptions = {},
115
+ ): DateTimePickerStore {
116
+ let options: DateTimePickerOptions = { ...initial };
117
+ const start = fromIsoDateTime(options.value ?? options.defaultValue, zone());
118
+ let time: TimeOfDay | null = start?.time ?? null;
119
+ let timeText = time ? fmt(time) : '';
120
+ let timeError: DateError | null = null;
121
+ let value = '';
122
+ let destroyed = false;
123
+
124
+ const datePicker = createDatePicker({
125
+ ...dateOptions(),
126
+ defaultValue: start ? toIsoDay(start.date) : '',
127
+ closeOnSelect: false,
128
+ onChange: () => {
129
+ emitter.act(() => {
130
+ if (datePicker.getState().date && !time) {
131
+ time = parseTime(options.defaultTime) ?? Object.freeze({ hour: 0, minute: 0 });
132
+ timeText = fmt(time);
133
+ timeError = null;
134
+ }
135
+ commit(true);
136
+ });
137
+ },
138
+ });
139
+ value = compose();
140
+ let snapshot = build();
141
+ const emitter = createEmitter(() => {
142
+ snapshot = build();
143
+ });
144
+ datePicker.subscribe(() => {
145
+ emitter.notify();
146
+ });
147
+
148
+ function zone(): string {
149
+ return options.timeZone ?? getConfig().timeZone;
150
+ }
151
+
152
+ function fmt(t: TimeOfDay): string {
153
+ return formatTime(t, { hour12: options.hour12 ?? false, locale: localeOf(options) });
154
+ }
155
+
156
+ function dateOptions(): PickerBaseOptions & {
157
+ presets?: readonly SinglePreset[] | undefined;
158
+ } {
159
+ const {
160
+ value: _value,
161
+ defaultValue: _default,
162
+ onChange: _onChange,
163
+ timeZone: _zone,
164
+ hour12: _hour12,
165
+ minuteStep: _step,
166
+ defaultTime: _defaultTime,
167
+ ...rest
168
+ } = options;
169
+
170
+ return rest;
171
+ }
172
+
173
+ function compose(): string {
174
+ const day = datePicker.getState().date;
175
+
176
+ return day && time ? toIsoDateTime(day.ad, time, zone()) : '';
177
+ }
178
+
179
+ function detail(): DateTimeDetail | null {
180
+ const day = datePicker.getState().date;
181
+
182
+ return day && time && value
183
+ ? Object.freeze({ ad: day.ad, bs: day.bs, time, iso: value })
184
+ : null;
185
+ }
186
+
187
+ /** Recompute the value; queue onChange when it changed and the user caused it. */
188
+ function commit(byUser: boolean): void {
189
+ const next = compose();
190
+ if (next === value) {
191
+ return;
192
+ }
193
+ value = next;
194
+ if (byUser) {
195
+ const info = detail();
196
+ emitter.after(() => options.onChange?.(next, info));
197
+ }
198
+ }
199
+
200
+ function build(): DateTimePickerState {
201
+ const inner = datePicker.getState();
202
+ const locale = localeOf(options);
203
+ const step = Math.min(30, Math.max(1, Math.floor(options.minuteStep ?? 5)));
204
+ const { am, pm } = getLocale(locale).time;
205
+ const hour12 = options.hour12 ?? false;
206
+
207
+ return Object.freeze({
208
+ value,
209
+ detail: detail(),
210
+ dateText: inner.text,
211
+ timeText,
212
+ time,
213
+ calendar: inner.calendar,
214
+ canSwitchCalendar: inner.canSwitchCalendar,
215
+ open: inner.open,
216
+ inline: inner.inline,
217
+ error: inner.error
218
+ ? { ...inner.error, field: 'date' as const }
219
+ : timeError
220
+ ? { ...timeError, field: 'time' as const }
221
+ : null,
222
+ equivalentText: inner.equivalentText,
223
+ placeholder: inner.placeholder,
224
+ timePlaceholder: hour12 ? `h:mm ${am}` : 'HH:MM',
225
+ hours: Array.from({ length: 24 }, (_, hour) =>
226
+ Object.freeze({
227
+ value: hour,
228
+ label: hour12
229
+ ? `${formatNumber(hour % 12 === 0 ? 12 : hour % 12, locale)} ${hour < 12 ? am : pm}`
230
+ : formatNumber(String(hour).padStart(2, '0'), locale),
231
+ isSelected: time?.hour === hour,
232
+ }),
233
+ ),
234
+ minutes: Array.from({ length: Math.ceil(60 / step) }, (_, index) =>
235
+ Object.freeze({
236
+ value: index * step,
237
+ label: formatNumber(String(index * step).padStart(2, '0'), locale),
238
+ isSelected: time?.minute === index * step,
239
+ }),
240
+ ),
241
+ presets: inner.presets,
242
+ calendarState: inner.calendarState,
243
+ });
244
+ }
245
+
246
+ function setTime(next: TimeOfDay | null, text: string, byUser: boolean): void {
247
+ time = next;
248
+ timeText = text;
249
+ timeError = null;
250
+ commit(byUser);
251
+ }
252
+
253
+ const actions: DateTimePickerActions = {
254
+ typeDate(text) {
255
+ datePicker.actions.type(text);
256
+ },
257
+ blurDate() {
258
+ datePicker.actions.blur();
259
+ },
260
+ typeTime(text) {
261
+ if (destroyed) {
262
+ return;
263
+ }
264
+ emitter.act(() => {
265
+ const parsed = parseTime(text);
266
+ if (text.trim() === '' || parsed) {
267
+ setTime(parsed, text, true);
268
+ } else {
269
+ time = null;
270
+ timeText = text;
271
+ timeError = {
272
+ code: 'format',
273
+ message: getLocale(localeOf(options)).messages.invalidTime,
274
+ };
275
+ commit(true);
276
+ }
277
+ });
278
+ },
279
+ blurTime() {
280
+ emitter.act(() => {
281
+ if (!timeError && time) {
282
+ timeText = fmt(time);
283
+ }
284
+ });
285
+ },
286
+ selectDate(date) {
287
+ return !destroyed && datePicker.actions.select(date);
288
+ },
289
+ selectHour(hour) {
290
+ emitter.act(() => {
291
+ const next = Object.freeze({ hour, minute: time?.minute ?? 0 });
292
+ setTime(next, fmt(next), true);
293
+ });
294
+ },
295
+ selectMinute(minute) {
296
+ emitter.act(() => {
297
+ const next = Object.freeze({ hour: time?.hour ?? 0, minute });
298
+ setTime(next, fmt(next), true);
299
+ });
300
+ },
301
+ selectNow() {
302
+ if (destroyed) {
303
+ return false;
304
+ }
305
+ const current = now(zone());
306
+ let picked = false;
307
+ emitter.act(() => {
308
+ time = current.time;
309
+ timeText = fmt(current.time);
310
+ timeError = null;
311
+ picked = datePicker.actions.select(current.date);
312
+ commit(true);
313
+ });
314
+
315
+ return picked;
316
+ },
317
+ applyPreset(id) {
318
+ return !destroyed && datePicker.actions.applyPreset(id);
319
+ },
320
+ open() {
321
+ datePicker.actions.open();
322
+ },
323
+ close() {
324
+ datePicker.actions.close();
325
+ },
326
+ toggle() {
327
+ datePicker.actions.toggle();
328
+ },
329
+ clear() {
330
+ emitter.act(() => {
331
+ datePicker.actions.setValue('');
332
+ setTime(null, '', true);
333
+ });
334
+ },
335
+ setValue(next) {
336
+ emitter.act(() => {
337
+ applyValue(next);
338
+ });
339
+ },
340
+ switchCalendar(calendar) {
341
+ datePicker.actions.switchCalendar(calendar);
342
+ },
343
+ reset() {
344
+ emitter.act(() => {
345
+ const start = fromIsoDateTime(options.defaultValue, zone());
346
+ datePicker.actions.setValue(start ? toIsoDay(start.date) : '');
347
+ setTime(start?.time ?? null, start ? fmt(start.time) : '', true);
348
+ });
349
+ },
350
+ };
351
+
352
+ /** An outside value; ignored when it is the instant already shown (echo guard). */
353
+ function applyValue(next: string | undefined): void {
354
+ const parsed = fromIsoDateTime(next, zone());
355
+ const incoming = parsed ? toIsoDateTime(parsed.date, parsed.time, zone()) : '';
356
+ if (incoming === value) {
357
+ return;
358
+ }
359
+ datePicker.actions.setValue(parsed ? toIsoDay(parsed.date) : '');
360
+ time = parsed?.time ?? null;
361
+ timeText = time ? fmt(time) : '';
362
+ timeError = null;
363
+ commit(false);
364
+ }
365
+
366
+ return {
367
+ getState: () => snapshot,
368
+ subscribe: (listener) => emitter.subscribe(listener),
369
+ setOptions(changes) {
370
+ if (destroyed) {
371
+ return;
372
+ }
373
+ emitter.act(() => {
374
+ options = { ...options, ...changes };
375
+ datePicker.setOptions(dateOptions());
376
+ if ('value' in changes) {
377
+ applyValue(changes.value);
378
+ }
379
+ if (time && !timeError) {
380
+ timeText = fmt(time);
381
+ }
382
+ commit(false);
383
+ });
384
+ },
385
+ actions,
386
+ calendar: datePicker.calendar,
387
+ destroy() {
388
+ destroyed = true;
389
+ datePicker.destroy();
390
+ emitter.clear();
391
+ },
392
+ };
393
+ }
394
+
395
+ function toIsoDay(date: CalendarDate): string {
396
+ return toIsoDate(date);
397
+ }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @nepdatejs/datepicker: BS/AD date picker behaviour, and nothing else. Typed
3
+ * text ⇄ value, validation messages, the BS/AD switch, open/close, single and
4
+ * range values. Renders nothing; adapters do (docs/plan packages/datepicker).
5
+ */
6
+ export { createDatePicker } from './picker';
7
+ export { createDateRangePicker } from './range';
8
+ export { maskDateText } from './mask';
9
+ export { createMultiDatePicker } from './multi';
10
+ export {
11
+ createDateTimePicker,
12
+ type DateTimeDetail,
13
+ type DateTimePickerActions,
14
+ type DateTimePickerOptions,
15
+ type DateTimePickerState,
16
+ type DateTimePickerStore,
17
+ type TimeCell,
18
+ } from './datetime';
19
+ export {
20
+ checkDate,
21
+ equivalentText,
22
+ formatInput,
23
+ parseInput,
24
+ placeholderText,
25
+ type InputLimits,
26
+ type ParseResult,
27
+ } from './input';
28
+ export {
29
+ resolveRangePreset,
30
+ resolveSinglePreset,
31
+ type CustomPreset,
32
+ type PresetContext,
33
+ type PresetRange,
34
+ type PresetState,
35
+ type RangePreset,
36
+ type RangePresetId,
37
+ type SinglePreset,
38
+ type SinglePresetId,
39
+ } from './presets';
40
+ export type {
41
+ DateDetail,
42
+ DateError,
43
+ DateErrorCode,
44
+ DatePickerActions,
45
+ DatePickerOptions,
46
+ DatePickerState,
47
+ DatePickerStore,
48
+ DateRangePickerActions,
49
+ DateRangePickerOptions,
50
+ DateRangePickerState,
51
+ DateRangePickerStore,
52
+ MultiDatePickerActions,
53
+ MultiDatePickerOptions,
54
+ MultiDatePickerState,
55
+ MultiDatePickerStore,
56
+ PickerBaseOptions,
57
+ RangeDetail,
58
+ RangeField,
59
+ RangeValue,
60
+ } from './types';