@nepdatejs/react 0.0.0-stage → 0.1.1

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/dist/date-text-BX3Uag0O.cjs +84 -0
  4. package/dist/date-text-BX3Uag0O.cjs.map +1 -0
  5. package/dist/date-text-DLwhBmYj.js +61 -0
  6. package/dist/date-text-DLwhBmYj.js.map +1 -0
  7. package/dist/date-text-Dipetpbp.d.cts +37 -0
  8. package/dist/date-text-Dipetpbp.d.ts +37 -0
  9. package/dist/headless-AEI6dxTJ.cjs +619 -0
  10. package/dist/headless-AEI6dxTJ.cjs.map +1 -0
  11. package/dist/headless-CVj0DhD4.js +560 -0
  12. package/dist/headless-CVj0DhD4.js.map +1 -0
  13. package/dist/headless-ChE3zuYj.d.ts +195 -0
  14. package/dist/headless-CrIIPnOF.d.cts +195 -0
  15. package/dist/headless.cjs +11 -0
  16. package/dist/headless.d.cts +2 -0
  17. package/dist/headless.d.ts +2 -0
  18. package/dist/headless.js +2 -0
  19. package/dist/index.cjs +915 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +245 -0
  22. package/dist/index.d.ts +245 -0
  23. package/dist/index.js +895 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/server.cjs +6 -0
  26. package/dist/server.d.cts +2 -0
  27. package/dist/server.d.ts +2 -0
  28. package/dist/server.js +1 -0
  29. package/package.json +110 -3
  30. package/src/bindings.ts +247 -0
  31. package/src/calendar-body.tsx +133 -0
  32. package/src/calendar.tsx +34 -0
  33. package/src/date-picker.tsx +343 -0
  34. package/src/date-range-picker.tsx +225 -0
  35. package/src/date-text.tsx +110 -0
  36. package/src/date-time-picker.tsx +289 -0
  37. package/src/headless.ts +21 -0
  38. package/src/hooks.ts +476 -0
  39. package/src/icons.tsx +35 -0
  40. package/src/index.ts +21 -0
  41. package/src/multi-date-picker.tsx +245 -0
  42. package/src/page-locale.ts +23 -0
  43. package/src/panel.ts +67 -0
  44. package/src/presets.tsx +44 -0
  45. package/src/server.ts +8 -0
  46. package/src/time-columns.tsx +60 -0
  47. package/src/use-store.ts +196 -0
@@ -0,0 +1,133 @@
1
+ import type { CSSProperties, ReactElement } from 'react';
2
+ import type { CalendarBindings } from './bindings';
3
+ import { Icon } from './icons';
4
+
5
+ /** The calendar markup (@nepdatejs/styles contract) for any set of bindings. */
6
+ export function CalendarBody({
7
+ bindings,
8
+ className,
9
+ theme,
10
+ }: {
11
+ bindings: CalendarBindings;
12
+ className?: string | undefined;
13
+ theme?: string | undefined;
14
+ }): ReactElement {
15
+ const { state } = bindings;
16
+ const tiles =
17
+ state.view === 'month'
18
+ ? state.months
19
+ : state.view === 'year'
20
+ ? state.years
21
+ : state.fiscalYears;
22
+
23
+ return (
24
+ <div
25
+ className={className ? `ndp-calendar ${className}` : 'ndp-calendar'}
26
+ data-ndp-theme={theme}
27
+ >
28
+ <div className="ndp-calendar__header">
29
+ <button className="ndp-icon-button" {...bindings.getPrevProps()}>
30
+ <Icon name="chevron-left" />
31
+ </button>
32
+ <div className="ndp-calendar__title" aria-live="polite">
33
+ {bindings.getTitleButtons().map((button) => (
34
+ <button
35
+ key={button.key}
36
+ className="ndp-calendar__title-button"
37
+ {...button.props}
38
+ >
39
+ {button.label}
40
+ </button>
41
+ ))}
42
+ </div>
43
+ <button className="ndp-icon-button" {...bindings.getNextProps()}>
44
+ <Icon name="chevron-right" />
45
+ </button>
46
+ </div>
47
+ {state.view === 'day' ? (
48
+ <table className="ndp-grid" {...bindings.getGridProps()}>
49
+ <thead>
50
+ <tr role="row">
51
+ {state.weekdays.map((weekday) => (
52
+ <th
53
+ key={weekday.index}
54
+ className="ndp-weekday"
55
+ scope="col"
56
+ role="columnheader"
57
+ abbr={weekday.longLabel}
58
+ data-weekend={weekday.isWeekend || undefined}
59
+ >
60
+ {weekday.label}
61
+ </th>
62
+ ))}
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ {state.weeks.map((week, row) => (
67
+ <tr key={row} role="row">
68
+ {week.map((cell, column) =>
69
+ cell ? (
70
+ <td
71
+ key={cell.key}
72
+ role="gridcell"
73
+ aria-selected={cell.isSelected || undefined}
74
+ >
75
+ <button className="ndp-day" {...bindings.getDayProps(cell)}>
76
+ <span className="ndp-sr-only">{cell.ariaLabel}</span>
77
+ <span className="ndp-day__label" aria-hidden="true">
78
+ {cell.label}
79
+ </span>
80
+ {cell.subLabel !== null && (
81
+ <span className="ndp-day__sub" aria-hidden="true">
82
+ {cell.subLabel}
83
+ </span>
84
+ )}
85
+ {cell.events[0] && (
86
+ <span
87
+ className="ndp-day__dot"
88
+ aria-hidden="true"
89
+ style={
90
+ cell.events[0].color
91
+ ? ({
92
+ '--ndp-dot-color': cell.events[0].color,
93
+ } as CSSProperties)
94
+ : undefined
95
+ }
96
+ />
97
+ )}
98
+ </button>
99
+ </td>
100
+ ) : (
101
+ <td key={`empty-${column}`} role="gridcell" />
102
+ ),
103
+ )}
104
+ </tr>
105
+ ))}
106
+ </tbody>
107
+ </table>
108
+ ) : (
109
+ <div className="ndp-options" role="group" aria-label={state.title}>
110
+ {tiles.map((tile, index) => (
111
+ <button
112
+ key={tile.value}
113
+ className="ndp-option"
114
+ {...bindings.getOptionProps(tile, index, tiles)}
115
+ >
116
+ <span className="ndp-sr-only">
117
+ {tile.subLabel ? `${tile.label} (${tile.subLabel})` : tile.label}
118
+ </span>
119
+ <span className="ndp-option__label" aria-hidden="true">
120
+ {tile.label}
121
+ </span>
122
+ {tile.subLabel !== null && (
123
+ <span className="ndp-option__sub" aria-hidden="true">
124
+ {tile.subLabel}
125
+ </span>
126
+ )}
127
+ </button>
128
+ ))}
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,34 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { CalendarDate } from '@nepdatejs/core';
3
+ import type { CalendarOptions, CalendarSelection } from '@nepdatejs/calendar';
4
+ import { CalendarBody } from './calendar-body';
5
+ import { useCalendar } from './hooks';
6
+
7
+ export interface CalendarProps extends Omit<
8
+ CalendarOptions,
9
+ 'onSelect' | 'visibleMonth' | 'onVisibleMonthChange'
10
+ > {
11
+ /** Show this month (any day in it); pair with onMonthChange to control it. */
12
+ month?: CalendarDate | undefined;
13
+ onMonthChange?: ((month: CalendarDate) => void) | undefined;
14
+ onSelect?: ((selection: CalendarSelection) => void) | undefined;
15
+ className?: string | undefined;
16
+ theme?: 'light' | 'dark' | 'auto' | undefined;
17
+ }
18
+
19
+ /** A standalone BS/AD month view with keyboard support. */
20
+ export function Calendar({
21
+ month,
22
+ onMonthChange,
23
+ className,
24
+ theme,
25
+ ...options
26
+ }: CalendarProps): ReactElement {
27
+ const bindings = useCalendar({
28
+ ...options,
29
+ visibleMonth: month,
30
+ onVisibleMonthChange: onMonthChange,
31
+ });
32
+
33
+ return <CalendarBody bindings={bindings} className={className} theme={theme} />;
34
+ }
@@ -0,0 +1,343 @@
1
+ import { useEffect, useRef, type CSSProperties, type ReactElement } from 'react';
2
+ import {
3
+ getLocale,
4
+ type CalendarDate,
5
+ type CalendarSystem,
6
+ type DateFormatPattern,
7
+ type LocaleCode,
8
+ type Weekday,
9
+ } from '@nepdatejs/core';
10
+ import type { EventMarker, HolidayInput, HolidayLookup } from '@nepdatejs/calendar';
11
+ import type { DateDetail, SinglePreset } from '@nepdatejs/datepicker';
12
+ import { restoreFocus } from '@nepdatejs/dom';
13
+ import { CalendarBody } from './calendar-body';
14
+ import { useDatePicker } from './hooks';
15
+ import { Icon } from './icons';
16
+ import { usePageLocale } from './page-locale';
17
+ import { usePanel } from './panel';
18
+ import type { PanelLayout } from '@nepdatejs/dom';
19
+ import { PANEL_WIDTH, PANEL_WIDTH_WITH_PRESETS, PanelBody } from './presets';
20
+
21
+ export interface DatePickerProps {
22
+ /** The AD date as "YYYY-MM-DD", or "": the same value a native date input uses. */
23
+ value?: string | undefined;
24
+ /** The starting value when the field keeps its own state, as in a plain form with `name`. */
25
+ defaultValue?: string | undefined;
26
+ onChange?: ((value: string, date: DateDetail | null) => void) | undefined;
27
+ /** Show and type dates in this calendar only, and hide the BS/AD switch. */
28
+ calendar?: CalendarSystem | undefined;
29
+ /** The calendar shown first when the viewer has not chosen one. */
30
+ defaultCalendar?: CalendarSystem | undefined;
31
+ /** Called when the viewer switches BS/AD. */
32
+ onCalendarChange?: ((calendar: CalendarSystem) => void) | undefined;
33
+ /** Earliest and latest allowed days, as AD "YYYY-MM-DD". */
34
+ min?: string | undefined;
35
+ max?: string | undefined;
36
+ isDateDisabled?: ((date: CalendarDate) => boolean) | undefined;
37
+ /** Names, digits and messages; defaults to the page language (ne when <html lang="ne">). */
38
+ locale?: LocaleCode | undefined;
39
+ /** Submit the AD value with a plain form under this name. */
40
+ name?: string | undefined;
41
+ id?: string | undefined;
42
+ placeholder?: string | undefined;
43
+ required?: boolean | undefined;
44
+ disabled?: boolean | undefined;
45
+ clearable?: boolean | undefined;
46
+ /** Show the same day in the other calendar under the field. */
47
+ showEquivalent?: boolean | undefined;
48
+ holidays?: readonly HolidayInput[] | HolidayLookup | undefined;
49
+ events?: ((date: CalendarDate) => readonly EventMarker[]) | undefined;
50
+ weekStartsOn?: Weekday | undefined;
51
+ weekendDays?: readonly Weekday[] | undefined;
52
+ /** Show the other calendar's day number under each day. Default true. */
53
+ showOtherCalendar?: boolean | undefined;
54
+ displayFormat?: DateFormatPattern | undefined;
55
+ closeOnSelect?: boolean | undefined;
56
+ /** Shortcuts beside the calendar: 'today', 'yesterday', 'tomorrow' or { id, label, resolve }. */
57
+ presets?: readonly SinglePreset[] | undefined;
58
+ /** Remember the viewer's BS/AD choice in localStorage under this key. */
59
+ persistCalendar?: string | undefined;
60
+ /** Override "today" (tests, server rendering). */
61
+ today?: CalendarDate | undefined;
62
+ /** Add the dashes of YYYY-MM-DD while the user types digits. Default true. */
63
+ mask?: boolean | undefined;
64
+ /** Show the calendar in the page, always open, instead of in a popover. */
65
+ inline?: boolean | undefined;
66
+ /** 'sheet' opens a bottom sheet; 'popover' sits beside the field; 'auto' (default) uses the sheet below 480px. */
67
+ panelLayout?: PanelLayout | undefined;
68
+ theme?: 'light' | 'dark' | 'auto' | undefined;
69
+ className?: string | undefined;
70
+ 'aria-label'?: string | undefined;
71
+ 'aria-invalid'?: boolean | undefined;
72
+ 'aria-describedby'?: string | undefined;
73
+ }
74
+
75
+ /**
76
+ * A BS/AD date field: type a date or pick it from a calendar. The value is
77
+ * always the AD day as "YYYY-MM-DD", so forms, validation and storage are
78
+ * unchanged and a BS date can never be sent as if it were AD.
79
+ */
80
+ export function DatePicker(props: DatePickerProps): ReactElement {
81
+ const {
82
+ name,
83
+ placeholder,
84
+ required,
85
+ disabled = false,
86
+ clearable = true,
87
+ showEquivalent = true,
88
+ persistCalendar,
89
+ panelLayout,
90
+ theme,
91
+ className,
92
+ 'aria-label': ariaLabel,
93
+ 'aria-invalid': ariaInvalid,
94
+ 'aria-describedby': ariaDescribedBy,
95
+ locale: localeProp,
96
+ onCalendarChange,
97
+ ...options
98
+ } = props;
99
+ const locale = usePageLocale(localeProp);
100
+ const picker = useDatePicker({
101
+ ...options,
102
+ locale,
103
+ onCalendarChange: (calendar: CalendarSystem) => {
104
+ remember(persistCalendar, calendar);
105
+ onCalendarChange?.(calendar);
106
+ },
107
+ });
108
+ const { state, store, calendar } = picker;
109
+ const labels = getLocale(locale);
110
+ const field = useRef<HTMLDivElement | null>(null);
111
+ const box = useRef<HTMLDivElement | null>(null);
112
+ const panel = useRef<HTMLDivElement | null>(null);
113
+ const input = useRef<HTMLInputElement | null>(null);
114
+ const hidden = useRef<HTMLInputElement | null>(null);
115
+
116
+ const panelWidth = state.presets.length > 0 ? PANEL_WIDTH_WITH_PRESETS : PANEL_WIDTH;
117
+ usePanel(
118
+ state.open && !state.inline,
119
+ { anchor: box, panel, field, returnFocus: input },
120
+ () => {
121
+ store.actions.close();
122
+ },
123
+ calendar.focusGrid,
124
+ panelWidth,
125
+ panelLayout,
126
+ );
127
+
128
+ // The viewer's last BS/AD choice, read after hydration so server and client render alike.
129
+ useEffect(() => {
130
+ const stored = read(persistCalendar);
131
+ if (stored) {
132
+ store.actions.switchCalendar(stored);
133
+ }
134
+ }, [persistCalendar, store]);
135
+
136
+ // A form reset puts the field back to its default value.
137
+ useEffect(() => {
138
+ const form = hidden.current?.form ?? input.current?.form;
139
+ if (!form) {
140
+ return undefined;
141
+ }
142
+ const reset = (): void => {
143
+ store.actions.reset();
144
+ };
145
+ form.addEventListener('reset', reset);
146
+
147
+ return () => {
148
+ form.removeEventListener('reset', reset);
149
+ };
150
+ }, [store]);
151
+
152
+ useEffect(() => {
153
+ input.current?.setCustomValidity(state.error?.message ?? '');
154
+ }, [state.error]);
155
+
156
+ const hint = state.error?.message ?? (showEquivalent ? state.equivalentText : null);
157
+ const inputProps = picker.getInputProps();
158
+ const describedBy =
159
+ [ariaDescribedBy, hint ? picker.ids.hint : undefined].filter(Boolean).join(' ') ||
160
+ undefined;
161
+ const invalid = ariaInvalid === true || state.error !== null;
162
+
163
+ return (
164
+ <div
165
+ ref={field}
166
+ className={className ? `ndp-field ${className}` : 'ndp-field'}
167
+ data-ndp-theme={theme}
168
+ >
169
+ <div
170
+ ref={box}
171
+ className="ndp-input"
172
+ data-invalid={invalid || undefined}
173
+ data-disabled={disabled || undefined}
174
+ >
175
+ <input
176
+ ref={input}
177
+ className="ndp-input__control"
178
+ {...inputProps}
179
+ aria-describedby={describedBy}
180
+ aria-invalid={invalid || undefined}
181
+ aria-label={ariaLabel}
182
+ placeholder={placeholder ?? state.placeholder}
183
+ required={required}
184
+ disabled={disabled}
185
+ />
186
+ <span className="ndp-input__actions">
187
+ {clearable && state.text !== '' && !disabled && (
188
+ <button
189
+ className="ndp-icon-button"
190
+ {...picker.getClearProps()}
191
+ onClick={() => {
192
+ store.actions.clear();
193
+ input.current?.focus();
194
+ }}
195
+ >
196
+ <Icon name="close" />
197
+ </button>
198
+ )}
199
+ {state.canSwitchCalendar && (
200
+ <button
201
+ className="ndp-input__switch"
202
+ disabled={disabled}
203
+ {...picker.getSwitchProps()}
204
+ >
205
+ {labels.calendarNames[state.calendar]}
206
+ </button>
207
+ )}
208
+ {!state.inline && (
209
+ <button
210
+ className="ndp-icon-button"
211
+ disabled={disabled}
212
+ {...picker.getToggleProps()}
213
+ >
214
+ <Icon name="calendar" />
215
+ </button>
216
+ )}
217
+ </span>
218
+ </div>
219
+ {hint !== null && (
220
+ <p
221
+ id={picker.ids.hint}
222
+ className="ndp-hint"
223
+ data-error={state.error ? true : undefined}
224
+ aria-live="polite"
225
+ >
226
+ {hint}
227
+ </p>
228
+ )}
229
+ {name !== undefined && (
230
+ <input
231
+ ref={hidden}
232
+ type="hidden"
233
+ name={name}
234
+ value={state.value}
235
+ disabled={disabled}
236
+ />
237
+ )}
238
+ {state.open && (
239
+ <div
240
+ ref={panel}
241
+ className="ndp-panel"
242
+ popover={state.inline ? undefined : 'manual'}
243
+ data-layout={state.inline ? 'inline' : undefined}
244
+ style={
245
+ state.inline
246
+ ? ({ '--ndp-panel-width': `${String(panelWidth)}px` } as CSSProperties)
247
+ : undefined
248
+ }
249
+ data-ndp-theme={theme}
250
+ {...picker.getPanelProps()}
251
+ aria-label={
252
+ ariaLabel
253
+ ? `${ariaLabel}: ${labels.labels.calendar}`
254
+ : labels.labels.chooseDate
255
+ }
256
+ >
257
+ <PanelBody
258
+ presets={state.presets}
259
+ label={labels.labels.presets}
260
+ onPreset={(id) => {
261
+ if (store.actions.applyPreset(id) && !store.getState().open) {
262
+ restoreFocus(input.current);
263
+ }
264
+ }}
265
+ >
266
+ <CalendarBody
267
+ bindings={{
268
+ ...calendar,
269
+ getDayProps: (cell) => ({
270
+ ...calendar.getDayProps(cell),
271
+ onClick: () => {
272
+ if (
273
+ !cell.isDisabled &&
274
+ store.actions.select(cell.date) &&
275
+ !store.getState().open
276
+ ) {
277
+ restoreFocus(input.current);
278
+ }
279
+ },
280
+ }),
281
+ }}
282
+ />
283
+ </PanelBody>
284
+ <div className="ndp-panel__footer">
285
+ {state.canSwitchCalendar ? (
286
+ <div
287
+ className="ndp-segment"
288
+ role="group"
289
+ aria-label={labels.labels.calendar}
290
+ >
291
+ {(['BS', 'AD'] as const).map((option) => (
292
+ <button
293
+ key={option}
294
+ className="ndp-segment__item"
295
+ {...picker.getCalendarOptionProps(option)}
296
+ >
297
+ {labels.calendarNames[option]}
298
+ </button>
299
+ ))}
300
+ </div>
301
+ ) : (
302
+ <span />
303
+ )}
304
+ <button
305
+ className="ndp-button"
306
+ {...picker.getTodayProps()}
307
+ onClick={() => {
308
+ if (store.actions.selectToday()) {
309
+ restoreFocus(input.current);
310
+ }
311
+ }}
312
+ >
313
+ {labels.labels.today}
314
+ </button>
315
+ </div>
316
+ </div>
317
+ )}
318
+ </div>
319
+ );
320
+ }
321
+
322
+ function read(key: string | undefined): CalendarSystem | null {
323
+ if (!key) {
324
+ return null;
325
+ }
326
+ try {
327
+ const stored = window.localStorage.getItem(key);
328
+ return stored === 'AD' || stored === 'BS' ? stored : null;
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ function remember(key: string | undefined, calendar: CalendarSystem): void {
335
+ if (!key) {
336
+ return;
337
+ }
338
+ try {
339
+ window.localStorage.setItem(key, calendar);
340
+ } catch {
341
+ // Storage can be unavailable (private mode); the choice then lasts for this page only.
342
+ }
343
+ }