noph-ui 0.52.0 → 0.53.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.
Files changed (103) hide show
  1. package/dist/animation.d.ts +1 -13
  2. package/dist/animation.js +18 -13
  3. package/dist/button/Button.svelte +2 -14
  4. package/dist/button/IconButton.svelte +5 -18
  5. package/dist/button/SegmentedButton.svelte +8 -11
  6. package/dist/card/Card.svelte +3 -19
  7. package/dist/card/types.d.ts +12 -2
  8. package/dist/carousel/Carousel.svelte +77 -14
  9. package/dist/carousel/CarouselItem.svelte +26 -14
  10. package/dist/carousel/carouselMorph.js +1 -1
  11. package/dist/carousel/carouselStrategy.js +25 -27
  12. package/dist/checkbox/Checkbox.svelte +1 -8
  13. package/dist/chip/FilterChip.svelte +1 -13
  14. package/dist/date-picker/Calendar.svelte +31 -30
  15. package/dist/date-picker/Calendar.svelte.d.ts +2 -0
  16. package/dist/date-picker/CalendarHeader.svelte +84 -62
  17. package/dist/date-picker/CalendarHeader.svelte.d.ts +3 -0
  18. package/dist/date-picker/DatePickerDialog.svelte +134 -75
  19. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -3
  20. package/dist/date-picker/DateRangePicker.svelte +226 -78
  21. package/dist/date-picker/DateRangePicker.svelte.d.ts +3 -3
  22. package/dist/date-picker/DockedDatePicker.svelte +76 -464
  23. package/dist/date-picker/DockedDatePicker.svelte.d.ts +2 -2
  24. package/dist/date-picker/DockedDateTimePicker.svelte +128 -525
  25. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +2 -2
  26. package/dist/date-picker/DockedPickerField.svelte +221 -0
  27. package/dist/date-picker/DockedPickerField.svelte.d.ts +9 -0
  28. package/dist/date-picker/MonthPanel.svelte +291 -0
  29. package/dist/date-picker/MonthPanel.svelte.d.ts +12 -0
  30. package/dist/date-picker/SelectionList.svelte +7 -6
  31. package/dist/date-picker/SelectionList.svelte.d.ts +1 -0
  32. package/dist/date-picker/TimeColumn.svelte +2 -6
  33. package/dist/date-picker/TimeColumn.svelte.d.ts +1 -0
  34. package/dist/date-picker/YearGrid.svelte +2 -11
  35. package/dist/date-picker/YearGrid.svelte.d.ts +1 -0
  36. package/dist/date-picker/dateUtils.d.ts +4 -0
  37. package/dist/date-picker/dateUtils.js +15 -88
  38. package/dist/date-picker/exitVisibility.svelte.js +13 -6
  39. package/dist/date-picker/formatters.d.ts +16 -0
  40. package/dist/date-picker/formatters.js +48 -15
  41. package/dist/date-picker/internalTypes.d.ts +59 -1
  42. package/dist/date-picker/shortcuts.d.ts +11 -0
  43. package/dist/date-picker/shortcuts.js +40 -0
  44. package/dist/date-picker/timeUtils.d.ts +8 -3
  45. package/dist/date-picker/timeUtils.js +33 -51
  46. package/dist/date-picker/types.d.ts +22 -0
  47. package/dist/icons/Icon.svelte +1 -4
  48. package/dist/internal/exit.css +0 -14
  49. package/dist/internal/field.css +337 -0
  50. package/dist/internal/focus-ring.css +23 -8
  51. package/dist/internal/pop-in.css +19 -0
  52. package/dist/internal/visually-hidden.css +11 -0
  53. package/dist/list/Item.svelte +32 -18
  54. package/dist/menu/Menu.svelte +6 -25
  55. package/dist/menu/Menu.svelte.d.ts +1 -0
  56. package/dist/navigation-bar/NavigationBarItem.svelte +6 -30
  57. package/dist/navigation-drawer/NavigationDrawerItem.svelte +4 -11
  58. package/dist/navigation-rail/NavigationRailItem.svelte +2 -13
  59. package/dist/press.svelte.d.ts +0 -1
  60. package/dist/press.svelte.js +0 -11
  61. package/dist/progress/CircularProgress.svelte +26 -81
  62. package/dist/progress/CircularProgress.svelte.d.ts +1 -0
  63. package/dist/progress/LinearProgress.svelte +19 -151
  64. package/dist/progress/LinearProgress.svelte.d.ts +1 -0
  65. package/dist/progress/four-color.css +34 -0
  66. package/dist/radio/Radio.svelte +6 -9
  67. package/dist/ripple/Ripple.svelte +8 -16
  68. package/dist/search/Search.svelte +2 -9
  69. package/dist/search/Search.svelte.d.ts +1 -0
  70. package/dist/select/Check.svelte +8 -31
  71. package/dist/select/Check.svelte.d.ts +1 -1
  72. package/dist/select/NativeSelect.svelte +97 -366
  73. package/dist/select/NativeSelect.svelte.d.ts +1 -0
  74. package/dist/select/Option.svelte +1 -9
  75. package/dist/select/Select.svelte +139 -746
  76. package/dist/select/Select.svelte.d.ts +1 -0
  77. package/dist/select/VirtualList.svelte +0 -1
  78. package/dist/sheet/Sheet.svelte +11 -22
  79. package/dist/slider/Slider.svelte +13 -37
  80. package/dist/slider/Slider.svelte.d.ts +1 -0
  81. package/dist/switch/Switch.svelte +2 -8
  82. package/dist/tabs/Tab.svelte +2 -4
  83. package/dist/text-field/TextField.svelte +81 -609
  84. package/dist/text-field/TextField.svelte.d.ts +1 -0
  85. package/dist/time-picker/ClockDial.svelte +8 -25
  86. package/dist/time-picker/ClockDial.svelte.d.ts +2 -0
  87. package/dist/time-picker/DockedTimePicker.svelte +102 -244
  88. package/dist/time-picker/DockedTimePicker.svelte.d.ts +2 -2
  89. package/dist/time-picker/PeriodSelector.svelte +8 -7
  90. package/dist/time-picker/PeriodSelector.svelte.d.ts +1 -0
  91. package/dist/time-picker/TimeInputs.svelte +13 -2
  92. package/dist/time-picker/TimePickerDialog.svelte +32 -26
  93. package/dist/time-picker/TimePickerDialog.svelte.d.ts +2 -2
  94. package/dist/time-picker/TimePickerPanel.svelte +26 -4
  95. package/dist/time-picker/TimeSelectors.svelte +4 -9
  96. package/dist/time-picker/TimeSelectors.svelte.d.ts +1 -0
  97. package/dist/time-picker/internalTypes.d.ts +2 -0
  98. package/dist/time-picker/timePickerState.svelte.js +3 -9
  99. package/dist/tooltip/RichTooltip.svelte +2 -23
  100. package/dist/tooltip/RichTooltip.svelte.d.ts +1 -0
  101. package/dist/tooltip/Tooltip.svelte +2 -24
  102. package/dist/tooltip/Tooltip.svelte.d.ts +1 -0
  103. package/package.json +12 -12
@@ -1,5 +1,5 @@
1
- import { getFormatter, numericDateOptions, pad } from './formatters.js';
2
- import { getDayPeriodLabels, HOURS_IN_DAY, MINUTES_IN_HOUR, timeOptions, uses12HourClock, withMinutes, } from './timeUtils.js';
1
+ import { getFormatter, getPattern, numericDateOptions, pad, readFields } from './formatters.js';
2
+ import { getDayPeriodLabels, HOURS_IN_DAY, MINUTES_IN_HOUR, readHour, timeOptions, uses12HourClock, withMinutes, } from './timeUtils.js';
3
3
  export { compareTimes, formatMinutes, formatTime, formatTwoDigits, getDayPeriodLabels, getHourLabels, getTimePattern, HOURS_IN_DAY, isMinuteWithin, isTimeWithin, MINUTES_IN_DAY, MINUTES_IN_HOUR, minutesOfDay, parseISOTime, parseTimeInput, toISOTime, uses12HourClock, withMinutes, } from './timeUtils.js';
4
4
  export const DAYS_IN_WEEK = 7;
5
5
  export const MAX_CALENDAR_ROWS = 6;
@@ -93,37 +93,18 @@ export const getMonthNames = (locale, month) => {
93
93
  const numericOptions = numericDateOptions;
94
94
  export const formatDate = (date, locale) => getFormatter(locale, numericOptions).format(date);
95
95
  export const formatDateMedium = (date, locale) => getFormatter(locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(date);
96
+ /** The headline of the modal picker, as M3 shows it: "Mon, Aug 17". */
97
+ export const formatDateHeadline = (date, locale) => getFormatter(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(date);
98
+ /** Each end in the headline of the range picker, as M3 shows it: "Aug 17". */
99
+ export const formatMonthDay = (date, locale) => getFormatter(locale, { month: 'short', day: 'numeric' }).format(date);
96
100
  export const formatDateLong = (date, locale) => getFormatter(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).format(date);
97
101
  export const formatMonthYear = (date, locale) => getFormatter(locale, { year: 'numeric', month: 'long' }).format(date);
98
- const getFieldOrder = (locale) => getFormatter(locale, numericOptions)
99
- .formatToParts(new Date(2024, 0, 2))
100
- .filter((part) => part.type === 'year' || part.type === 'month' || part.type === 'day')
101
- .map((part) => part.type);
102
- export const getDatePattern = (locale) => getFormatter(locale, numericOptions)
103
- .formatToParts(new Date(2024, 0, 2))
104
- .map((part) => {
105
- if (part.type === 'year')
106
- return 'YYYY';
107
- if (part.type === 'month')
108
- return 'MM';
109
- if (part.type === 'day')
110
- return 'DD';
111
- return part.value;
112
- })
113
- .join('');
102
+ export const getDatePattern = (locale) => getPattern(locale, numericOptions);
114
103
  export const parseDateInput = (value, locale) => {
115
- const groups = value.match(/\d+/g);
116
- if (!groups || groups.length !== 3)
104
+ const { year, month, day } = readFields(value, locale, numericOptions) ?? {};
105
+ if (year?.length !== 4 || !month || !day)
117
106
  return undefined;
118
- const order = getFieldOrder(locale);
119
- const year = order.indexOf('year');
120
- const month = order.indexOf('month');
121
- const day = order.indexOf('day');
122
- if (year < 0 || month < 0 || day < 0)
123
- return undefined;
124
- if (groups[year].length !== 4)
125
- return undefined;
126
- return createDate(Number(groups[year]), Number(groups[month]) - 1, Number(groups[day]));
107
+ return createDate(Number(year), Number(month) - 1, Number(day));
127
108
  };
128
109
  const leadingOffset = (month, firstDayOfWeek) => (startOfMonth(month).getDay() - firstDayOfWeek + DAYS_IN_WEEK) % DAYS_IN_WEEK;
129
110
  export const getWeekRowCount = (month, firstDayOfWeek) => {
@@ -139,7 +120,6 @@ export const getCalendarDays = (month, firstDayOfWeek, rows = MAX_CALENDAR_ROWS)
139
120
  return { date, inMonth: date.getMonth() === first.getMonth() };
140
121
  });
141
122
  };
142
- const DATE_TIME_PROBE = new Date(2024, 0, 2, 13, 45);
143
123
  export const toISODateTime = (date) => `${toISODate(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
144
124
  export const createDateTime = (year, month, day, hours, minutes) => {
145
125
  if (hours < 0 || hours >= HOURS_IN_DAY || minutes < 0 || minutes >= MINUTES_IN_HOUR) {
@@ -169,66 +149,13 @@ const dateTimeOptions = (hour12) => ({
169
149
  ...timeOptions(hour12),
170
150
  });
171
151
  export const formatDateTime = (date, locale, hour12 = uses12HourClock(locale)) => getFormatter(locale, dateTimeOptions(hour12)).format(date);
172
- export const getDateTimePattern = (locale, hour12 = uses12HourClock(locale)) => {
173
- const [am, pm] = getDayPeriodLabels(locale);
174
- return getFormatter(locale, dateTimeOptions(hour12))
175
- .formatToParts(DATE_TIME_PROBE)
176
- .map((part) => {
177
- if (part.type === 'year')
178
- return 'YYYY';
179
- if (part.type === 'month')
180
- return 'MM';
181
- if (part.type === 'day')
182
- return 'DD';
183
- if (part.type === 'hour')
184
- return hour12 ? 'hh' : 'HH';
185
- if (part.type === 'minute')
186
- return 'mm';
187
- if (part.type === 'dayPeriod')
188
- return `${am}/${pm}`;
189
- return part.value;
190
- })
191
- .join('');
192
- };
193
- const getDateTimeFieldOrder = (locale, hour12) => getFormatter(locale, dateTimeOptions(hour12))
194
- .formatToParts(DATE_TIME_PROBE)
195
- .filter((part) => part.type === 'year' ||
196
- part.type === 'month' ||
197
- part.type === 'day' ||
198
- part.type === 'hour' ||
199
- part.type === 'minute')
200
- .map((part) => part.type);
152
+ export const getDateTimePattern = (locale, hour12 = uses12HourClock(locale)) => getPattern(locale, dateTimeOptions(hour12), getDayPeriodLabels(locale).join('/'));
201
153
  export const parseDateTimeInput = (value, locale, hour12 = uses12HourClock(locale)) => {
202
- const groups = value.match(/\d+/g);
203
- if (!groups || groups.length !== 5)
154
+ const { year, month, day, hour, minute } = readFields(value, locale, dateTimeOptions(hour12)) ?? {};
155
+ if (year?.length !== 4 || !month || !day || !hour || !minute)
204
156
  return undefined;
205
- const order = getDateTimeFieldOrder(locale, hour12);
206
- if (order.length !== 5)
157
+ const hours = readHour(hour, value, locale, hour12);
158
+ if (hours === undefined)
207
159
  return undefined;
208
- const at = (field) => {
209
- const index = order.indexOf(field);
210
- return index < 0 ? undefined : groups[index];
211
- };
212
- const year = at('year');
213
- const month = at('month');
214
- const day = at('day');
215
- const minute = at('minute');
216
- const hour = at('hour');
217
- if (!year || !month || !day || !hour || !minute)
218
- return undefined;
219
- if (year.length !== 4)
220
- return undefined;
221
- let hours = Number(hour);
222
- if (hour12) {
223
- const [am, pm] = getDayPeriodLabels(locale);
224
- const text = value.toLocaleLowerCase(locale);
225
- const isPm = text.includes(pm.toLocaleLowerCase(locale)) || /\bpm\b/.test(text);
226
- const isAm = text.includes(am.toLocaleLowerCase(locale)) || /\bam\b/.test(text);
227
- if (isPm === isAm)
228
- return undefined;
229
- if (hours > 12)
230
- return undefined;
231
- hours = (hours % 12) + (isPm ? 12 : 0);
232
- }
233
160
  return createDateTime(Number(year), Number(month) - 1, Number(day), hours, Number(minute));
234
161
  };
@@ -1,22 +1,29 @@
1
1
  import { afterExit } from '../animation.js';
2
2
  export const exitVisibility = () => {
3
3
  let visible = $state(false);
4
- let cancelExit;
5
- $effect(() => () => cancelExit?.());
4
+ let pending;
5
+ $effect(() => () => pending?.cancel());
6
6
  return {
7
7
  get visible() {
8
8
  return visible;
9
9
  },
10
10
  show: () => {
11
- cancelExit?.();
12
- cancelExit = undefined;
11
+ // Opened again while the last close still animates: its cleanup runs now, so the new
12
+ // opening starts from the state a finished close leaves.
13
+ const unfinished = pending;
14
+ pending = undefined;
15
+ unfinished?.cancel();
16
+ unfinished?.onDone?.();
13
17
  visible = true;
14
18
  },
15
19
  scheduleExit: (element, onDone) => {
16
- cancelExit = afterExit(element, () => {
20
+ const exit = { cancel: () => { }, onDone };
21
+ pending = exit;
22
+ exit.cancel = afterExit(element, () => {
23
+ if (pending === exit)
24
+ pending = undefined;
17
25
  visible = false;
18
26
  onDone?.();
19
- cancelExit = undefined;
20
27
  });
21
28
  },
22
29
  };
@@ -2,3 +2,19 @@ export declare const pad: (value: number, length?: number) => string;
2
2
  export declare const getFormatter: (locale: string | undefined, options: Intl.DateTimeFormatOptions) => Intl.DateTimeFormat;
3
3
  export declare const formatTwoDigits: (value: number, locale?: string) => string;
4
4
  export declare const numericDateOptions: Intl.DateTimeFormatOptions;
5
+ /** A moment whose every field differs, to read a locale's format from. */
6
+ export declare const PROBE: Date;
7
+ type Field = 'year' | 'month' | 'day' | 'hour' | 'minute';
8
+ /**
9
+ * A hint like `DD.MM.YYYY` or `hh:mm AM/PM` for a text field's supporting text, written the way the
10
+ * locale formats with `options`. `dayPeriod` stands in for the locale's AM and PM.
11
+ */
12
+ export declare const getPattern: (locale: string | undefined, options: Intl.DateTimeFormatOptions, dayPeriod?: string) => string;
13
+ /**
14
+ * Splits typed text into its numbers and names each by the field the locale writes in its place.
15
+ * The field order comes from the locale's own format rather than a per-locale regex. Digits typed
16
+ * without separators, such as 08172025, are cut into the fields when they fill them exactly. Text
17
+ * with a number too many or too few is not read.
18
+ */
19
+ export declare const readFields: (value: string, locale: string | undefined, options: Intl.DateTimeFormatOptions) => Partial<Record<Field, string>> | undefined;
20
+ export {};
@@ -2,25 +2,58 @@ export const pad = (value, length = 2) => `${value}`.padStart(length, '0');
2
2
  const formatterCache = new Map();
3
3
  export const getFormatter = (locale, options) => {
4
4
  const key = `${locale ?? ''}|${JSON.stringify(options)}`;
5
- let formatter = formatterCache.get(key);
6
- if (!formatter) {
7
- formatter = new Intl.DateTimeFormat(locale, options);
8
- formatterCache.set(key, formatter);
9
- }
10
- return formatter;
5
+ return formatterCache.getOrInsertComputed(key, () => new Intl.DateTimeFormat(locale, options));
11
6
  };
12
7
  const numberFormatterCache = new Map();
13
- export const formatTwoDigits = (value, locale) => {
14
- const key = locale ?? '';
15
- let formatter = numberFormatterCache.get(key);
16
- if (!formatter) {
17
- formatter = new Intl.NumberFormat(locale, { minimumIntegerDigits: 2, useGrouping: false });
18
- numberFormatterCache.set(key, formatter);
19
- }
20
- return formatter.format(value);
21
- };
8
+ export const formatTwoDigits = (value, locale) => numberFormatterCache
9
+ .getOrInsertComputed(locale ?? '', () => new Intl.NumberFormat(locale, { minimumIntegerDigits: 2, useGrouping: false }))
10
+ .format(value);
22
11
  export const numericDateOptions = {
23
12
  year: 'numeric',
24
13
  month: '2-digit',
25
14
  day: '2-digit',
26
15
  };
16
+ /** A moment whose every field differs, to read a locale's format from. */
17
+ export const PROBE = new Date(2024, 0, 2, 13, 45);
18
+ const TOKENS = {
19
+ year: 'YYYY',
20
+ month: 'MM',
21
+ day: 'DD',
22
+ minute: 'mm',
23
+ };
24
+ /**
25
+ * A hint like `DD.MM.YYYY` or `hh:mm AM/PM` for a text field's supporting text, written the way the
26
+ * locale formats with `options`. `dayPeriod` stands in for the locale's AM and PM.
27
+ */
28
+ export const getPattern = (locale, options, dayPeriod = '') => getFormatter(locale, options)
29
+ .formatToParts(PROBE)
30
+ .map((part) => {
31
+ if (part.type === 'hour')
32
+ return options.hour12 ? 'hh' : 'HH';
33
+ if (part.type === 'dayPeriod')
34
+ return dayPeriod;
35
+ return TOKENS[part.type] ?? part.value;
36
+ })
37
+ .join('');
38
+ const WIDTHS = { year: 4, month: 2, day: 2, hour: 2, minute: 2 };
39
+ /**
40
+ * Splits typed text into its numbers and names each by the field the locale writes in its place.
41
+ * The field order comes from the locale's own format rather than a per-locale regex. Digits typed
42
+ * without separators, such as 08172025, are cut into the fields when they fill them exactly. Text
43
+ * with a number too many or too few is not read.
44
+ */
45
+ export const readFields = (value, locale, options) => {
46
+ const order = getFormatter(locale, options)
47
+ .formatToParts(PROBE)
48
+ .map((part) => part.type)
49
+ .filter((type) => type in TOKENS || type === 'hour');
50
+ let groups = value.match(/\d+/g);
51
+ const digits = groups?.length === 1 && order.length > 1 ? groups[0] : '';
52
+ if (digits.length === order.reduce((sum, field) => sum + WIDTHS[field], 0)) {
53
+ let at = 0;
54
+ groups = order.map((field) => digits.slice(at, (at += WIDTHS[field])));
55
+ }
56
+ if (!groups || groups.length !== order.length)
57
+ return undefined;
58
+ return Object.fromEntries(order.map((field, index) => [field, groups[index]]));
59
+ };
@@ -1,7 +1,65 @@
1
+ import type { DockedFieldProps } from '../internal/fieldTypes.js';
2
+ import type { TextFieldElement } from '../text-field/types.js';
3
+ import type { Snippet } from 'svelte';
1
4
  import type { HTMLAttributes } from 'svelte/elements';
2
- import type { TimeOption } from './types.ts';
5
+ import type { DatePickerShortcutLabels, TimeOption } from './types.ts';
3
6
  export interface TimeColumnProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
4
7
  options: TimeOption[];
5
8
  value?: number;
6
9
  onselect?: (value: number) => void;
7
10
  }
11
+ export interface DockedPickerFieldProps extends Omit<HTMLAttributes<TextFieldElement>, 'onchange'>, DockedFieldProps {
12
+ /** Names its classes and tokens, such as np-docked-date-picker. */
13
+ kind: 'date-picker' | 'date-time-picker' | 'time-picker';
14
+ value?: string | number | null;
15
+ /** The value the field holds, as the picker writes it. */
16
+ current: string | undefined;
17
+ /** The value a form reset brings back. */
18
+ resetValue: string | undefined;
19
+ menuElement?: HTMLDivElement;
20
+ /** The format to type in, shown as supporting text. */
21
+ pattern: string;
22
+ openLabel: string;
23
+ invalidMessage: string;
24
+ /** The value the field holds, as text in the field. */
25
+ format: () => string;
26
+ /** Typed text as a value the picker takes, or undefined. */
27
+ parse: (text: string) => string | undefined;
28
+ icon: Snippet;
29
+ onopen?: () => void;
30
+ /** Once the menu has closed and finished its animation. */
31
+ onclosed?: () => void;
32
+ onrestore?: () => void;
33
+ /** A usable value was typed. */
34
+ ontyped?: (value: string) => void;
35
+ /** A key pressed anywhere in the menu. */
36
+ onmenukeydown?: (event: KeyboardEvent) => void;
37
+ onchange?: (value: string | undefined) => void;
38
+ children: Snippet;
39
+ }
40
+ export interface MonthPanelProps {
41
+ /** The month on show, its first day. */
42
+ month: Date;
43
+ selected?: Date;
44
+ min?: Date;
45
+ max?: Date;
46
+ locale?: string;
47
+ firstDayOfWeek: number;
48
+ yearRange: [number, number];
49
+ isDateEnabled?: (date: Date) => boolean;
50
+ adjacentMonthDays?: boolean;
51
+ today: Date;
52
+ previousMonthLabel: string;
53
+ nextMonthLabel: string;
54
+ previousYearLabel: string;
55
+ nextYearLabel: string;
56
+ selectMonthLabel: string;
57
+ selectYearLabel: string;
58
+ selectedDateLabel: string;
59
+ shortcutLabels?: Partial<DatePickerShortcutLabels>;
60
+ /** Shows another month, given by any day in it. */
61
+ onmonth: (date: Date) => void;
62
+ onselect: (date: Date) => void;
63
+ /** Enter on a day, to save it and close. */
64
+ onconfirm?: (date: Date) => void;
65
+ }
@@ -0,0 +1,11 @@
1
+ import type { DatePickerShortcutLabels } from './types.ts';
2
+ /** The keys a calendar takes, shown in the tooltips of the controls they stand for. */
3
+ export declare const DEFAULT_SHORTCUT_LABELS: DatePickerShortcutLabels;
4
+ /** A control's tooltip: what it does, then its key, as in "Previous year (Shift+Page up)". */
5
+ export declare const withShortcut: (label: string, keys: string) => string;
6
+ /**
7
+ * The picker key an event presses. The picker listens on the whole of itself, so the keys work
8
+ * wherever focus is in it. A text field takes them as typing, and the days take Page up and Page
9
+ * down first, as they move focus.
10
+ */
11
+ export declare const pickerShortcut: (event: KeyboardEvent) => keyof DatePickerShortcutLabels | undefined;
@@ -0,0 +1,40 @@
1
+ /** The keys a calendar takes, shown in the tooltips of the controls they stand for. */
2
+ export const DEFAULT_SHORTCUT_LABELS = {
3
+ previousMonth: 'Page up',
4
+ nextMonth: 'Page down',
5
+ previousYear: 'Shift+Page up',
6
+ nextYear: 'Shift+Page down',
7
+ monthList: 'Shift+M',
8
+ yearList: 'Shift+Y',
9
+ };
10
+ /** A control's tooltip: what it does, then its key, as in "Previous year (Shift+Page up)". */
11
+ export const withShortcut = (label, keys) => (keys ? `${label} (${keys})` : label);
12
+ /**
13
+ * The picker key an event presses. The picker listens on the whole of itself, so the keys work
14
+ * wherever focus is in it. A text field takes them as typing, and the days take Page up and Page
15
+ * down first, as they move focus.
16
+ */
17
+ export const pickerShortcut = (event) => {
18
+ if (event.defaultPrevented || event.ctrlKey || event.altKey || event.metaKey)
19
+ return undefined;
20
+ if (event.target instanceof Element && event.target.closest('input, textarea, [contenteditable]'))
21
+ return undefined;
22
+ if (event.key === 'PageUp')
23
+ return event.shiftKey ? 'previousYear' : 'previousMonth';
24
+ if (event.key === 'PageDown')
25
+ return event.shiftKey ? 'nextYear' : 'nextMonth';
26
+ if (!event.shiftKey)
27
+ return undefined;
28
+ // A layout without Latin letters, such as Cyrillic, has M and Y where QWERTY has them. Only a
29
+ // letter falls back to its key position, so a symbol on that key, like `?` on AZERTY, is typing.
30
+ if (!/^\p{L}$/u.test(event.key))
31
+ return undefined;
32
+ const letter = /^[a-z]$/i.test(event.key)
33
+ ? event.key.toLowerCase()
34
+ : event.code.slice(3).toLowerCase();
35
+ if (letter === 'm')
36
+ return 'monthList';
37
+ if (letter === 'y')
38
+ return 'yearList';
39
+ return undefined;
40
+ };
@@ -10,6 +10,10 @@ export declare const toISOTime: (minutes: number) => string;
10
10
  export declare const parseISOTime: (value: ISOTime | number | null | undefined) => number | undefined;
11
11
  export declare const clampMinutes: (minutes: number, min?: number, max?: number) => number;
12
12
  export declare const isMinuteWithin: (minutes: number, min?: number, max?: number) => boolean;
13
+ /** A minute step as the pickers use it: a whole number of at least 1. */
14
+ export declare const stepOf: (minuteStep: number) => number;
15
+ /** Whether an hour has a minute on the step grid that `usable` lets through. */
16
+ export declare const isHourReachable: (hour: number, step: number, usable: (minutes: number) => boolean) => boolean;
13
17
  /**
14
18
  * Snaps the minute part to the nearest multiple of `step` without ever rolling into the next hour,
15
19
  * so dragging the dial past the last step lands on it rather than changing the hour.
@@ -27,8 +31,9 @@ export declare const formatMinutes: (minutes: number, locale?: string, hour12?:
27
31
  /** A hint like `HH:mm` or `hh:mm AM/PM`, for a text field's supporting text. */
28
32
  export declare const getTimePattern: (locale?: string, hour12?: boolean) => string;
29
33
  /**
30
- * Reads typed text as minutes since midnight. The field order comes from the locale's own format
31
- * rather than a per-locale regex, and the day period is matched against the locale's labels with an
32
- * `am`/`pm` fallback.
34
+ * Reads a typed hour as the hour of the day. A 12-hour clock takes 1 to 12 and needs its day period
35
+ * in `value`, matched against the locale's labels with an `am`/`pm` fallback.
33
36
  */
37
+ export declare const readHour: (hour: string, value: string, locale: string | undefined, hour12: boolean) => number | undefined;
38
+ /** Reads typed text as minutes since midnight. */
34
39
  export declare const parseTimeInput: (value: string, locale?: string, hour12?: boolean) => number | undefined;
@@ -1,9 +1,8 @@
1
- import { formatTwoDigits, getFormatter, pad } from './formatters.js';
1
+ import { formatTwoDigits, getFormatter, getPattern, pad, PROBE, readFields } from './formatters.js';
2
2
  export { formatTwoDigits };
3
3
  export const MINUTES_IN_HOUR = 60;
4
4
  export const HOURS_IN_DAY = 24;
5
5
  export const MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR;
6
- const TIME_PROBE = new Date(2024, 0, 2, 13, 45);
7
6
  export const minutesOfDay = (date) => date.getHours() * MINUTES_IN_HOUR + date.getMinutes();
8
7
  export const withMinutes = (date, minutes) => {
9
8
  const copy = new Date(date);
@@ -37,6 +36,16 @@ export const clampMinutes = (minutes, min, max) => {
37
36
  return minutes;
38
37
  };
39
38
  export const isMinuteWithin = (minutes, min, max) => (min === undefined || minutes >= min) && (max === undefined || minutes <= max);
39
+ /** A minute step as the pickers use it: a whole number of at least 1. */
40
+ export const stepOf = (minuteStep) => Math.max(1, Math.trunc(minuteStep) || 1);
41
+ /** Whether an hour has a minute on the step grid that `usable` lets through. */
42
+ export const isHourReachable = (hour, step, usable) => {
43
+ for (let minute = 0; minute < MINUTES_IN_HOUR; minute += step) {
44
+ if (usable(hour * MINUTES_IN_HOUR + minute))
45
+ return true;
46
+ }
47
+ return false;
48
+ };
40
49
  /**
41
50
  * Snaps the minute part to the nearest multiple of `step` without ever rolling into the next hour,
42
51
  * so dragging the dial past the last step lands on it rather than changing the hour.
@@ -80,60 +89,33 @@ export const getHourLabels = (locale, hour12) => {
80
89
  };
81
90
  export const formatTime = (date, locale, hour12 = uses12HourClock(locale)) => getFormatter(locale, timeOptions(hour12)).format(date);
82
91
  /** The same as {@link formatTime}, taking minutes since midnight instead of a `Date`. */
83
- export const formatMinutes = (minutes, locale, hour12 = uses12HourClock(locale)) => formatTime(withMinutes(TIME_PROBE, minutes), locale, hour12);
92
+ export const formatMinutes = (minutes, locale, hour12 = uses12HourClock(locale)) => formatTime(withMinutes(PROBE, minutes), locale, hour12);
84
93
  /** A hint like `HH:mm` or `hh:mm AM/PM`, for a text field's supporting text. */
85
- export const getTimePattern = (locale, hour12 = uses12HourClock(locale)) => {
86
- const [am, pm] = getDayPeriodLabels(locale);
87
- return getFormatter(locale, timeOptions(hour12))
88
- .formatToParts(TIME_PROBE)
89
- .map((part) => {
90
- if (part.type === 'hour')
91
- return hour12 ? 'hh' : 'HH';
92
- if (part.type === 'minute')
93
- return 'mm';
94
- if (part.type === 'dayPeriod')
95
- return `${am}/${pm}`;
96
- return part.value;
97
- })
98
- .join('');
99
- };
100
- const getTimeFieldOrder = (locale, hour12) => getFormatter(locale, timeOptions(hour12))
101
- .formatToParts(TIME_PROBE)
102
- .filter((part) => part.type === 'hour' || part.type === 'minute')
103
- .map((part) => part.type);
94
+ export const getTimePattern = (locale, hour12 = uses12HourClock(locale)) => getPattern(locale, timeOptions(hour12), getDayPeriodLabels(locale).join('/'));
104
95
  /**
105
- * Reads typed text as minutes since midnight. The field order comes from the locale's own format
106
- * rather than a per-locale regex, and the day period is matched against the locale's labels with an
107
- * `am`/`pm` fallback.
96
+ * Reads a typed hour as the hour of the day. A 12-hour clock takes 1 to 12 and needs its day period
97
+ * in `value`, matched against the locale's labels with an `am`/`pm` fallback.
108
98
  */
109
- export const parseTimeInput = (value, locale, hour12 = uses12HourClock(locale)) => {
110
- const groups = value.match(/\d+/g);
111
- if (!groups || groups.length !== 2)
112
- return undefined;
113
- const order = getTimeFieldOrder(locale, hour12);
114
- if (order.length !== 2)
115
- return undefined;
116
- const hour = groups[order.indexOf('hour')];
117
- const minute = groups[order.indexOf('minute')];
118
- if (hour === undefined || minute === undefined)
99
+ export const readHour = (hour, value, locale, hour12) => {
100
+ const hours = Number(hour);
101
+ if (!hour12)
102
+ return hours < HOURS_IN_DAY ? hours : undefined;
103
+ const [am, pm] = getDayPeriodLabels(locale);
104
+ const text = value.toLocaleLowerCase(locale);
105
+ const isPm = text.includes(pm.toLocaleLowerCase(locale)) || /\bpm\b/.test(text);
106
+ const isAm = text.includes(am.toLocaleLowerCase(locale)) || /\bam\b/.test(text);
107
+ if (isPm === isAm || hours < 1 || hours > 12)
119
108
  return undefined;
120
- let hours = Number(hour);
121
- const minutes = Number(minute);
122
- if (minutes >= MINUTES_IN_HOUR)
109
+ return (hours % 12) + (isPm ? 12 : 0);
110
+ };
111
+ /** Reads typed text as minutes since midnight. */
112
+ export const parseTimeInput = (value, locale, hour12 = uses12HourClock(locale)) => {
113
+ const fields = readFields(value, locale, timeOptions(hour12));
114
+ if (!fields?.hour || !fields.minute)
123
115
  return undefined;
124
- if (hour12) {
125
- const [am, pm] = getDayPeriodLabels(locale);
126
- const text = value.toLocaleLowerCase(locale);
127
- const isPm = text.includes(pm.toLocaleLowerCase(locale)) || /\bpm\b/.test(text);
128
- const isAm = text.includes(am.toLocaleLowerCase(locale)) || /\bam\b/.test(text);
129
- if (isPm === isAm)
130
- return undefined;
131
- if (hours > 12 || hours < 1)
132
- return undefined;
133
- hours = (hours % 12) + (isPm ? 12 : 0);
134
- }
135
- else if (hours >= HOURS_IN_DAY) {
116
+ const hours = readHour(fields.hour, value, locale, hour12);
117
+ const minutes = Number(fields.minute);
118
+ if (hours === undefined || minutes >= MINUTES_IN_HOUR)
136
119
  return undefined;
137
- }
138
120
  return hours * MINUTES_IN_HOUR + minutes;
139
121
  };
@@ -5,6 +5,8 @@ import type { TextFieldElement } from '../text-field/types.js';
5
5
  import type { HTMLAttributes } from 'svelte/elements';
6
6
  export type ISODate = string;
7
7
  export type ISODateTime = string;
8
+ /** A modal picker's view: the calendar, or text fields to type into. */
9
+ export type DatePickerMode = 'calendar' | 'input';
8
10
  export interface DateRange {
9
11
  start?: ISODate;
10
12
  end?: ISODate;
@@ -25,9 +27,20 @@ interface DatePickerCommonLabelProps {
25
27
  confirmLabel?: string;
26
28
  selectedDateLabel?: string;
27
29
  }
30
+ /** The keys of a calendar's shortcuts, as its tooltips name them. */
31
+ export interface DatePickerShortcutLabels {
32
+ previousMonth: string;
33
+ nextMonth: string;
34
+ previousYear: string;
35
+ nextYear: string;
36
+ monthList: string;
37
+ yearList: string;
38
+ }
28
39
  interface MonthStepperLabelProps {
29
40
  nextMonthLabel?: string;
30
41
  previousMonthLabel?: string;
42
+ /** The keys named in the tooltips, in English by default. An empty one leaves its key out. */
43
+ shortcutLabels?: Partial<DatePickerShortcutLabels>;
31
44
  }
32
45
  interface DockedDateFieldProps extends DockedFieldProps {
33
46
  displayMonth?: ISODate;
@@ -78,6 +91,8 @@ export interface DatePickerDialogProps extends Omit<HTMLAttributes<HTMLDialogEle
78
91
  form?: string;
79
92
  title?: string;
80
93
  headline?: string;
94
+ /** The view it opens in. Bindable, so it holds the view a person switched to. */
95
+ mode?: DatePickerMode;
81
96
  modeToggle?: boolean;
82
97
  label?: string;
83
98
  supportingText?: string;
@@ -100,6 +115,11 @@ export interface DateRangePickerProps extends Omit<HTMLAttributes<HTMLDialogElem
100
115
  form?: string;
101
116
  startLabel?: string;
102
117
  endLabel?: string;
118
+ /** The view it opens in. Bindable, so it holds the view a person switched to. */
119
+ mode?: DatePickerMode;
120
+ modeToggle?: boolean;
121
+ calendarModeLabel?: string;
122
+ inputModeLabel?: string;
103
123
  onchange?: (value: DateRange) => void;
104
124
  onconfirm?: (value: DateRange) => void;
105
125
  oncancel?: () => void;
@@ -123,6 +143,8 @@ export interface CalendarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onc
123
143
  tabStopDate?: Date;
124
144
  focusRoot?: HTMLElement;
125
145
  onselect?: (date: Date) => void;
146
+ /** Enter on a day: the picker saves it and closes. Without it, Enter picks the day as a click does. */
147
+ onconfirm?: (date: Date) => void;
126
148
  onfocusday?: (date: Date) => void;
127
149
  onmonthstep?: (delta: number) => void;
128
150
  monthSubhead?: Snippet<[string]>;
@@ -22,13 +22,10 @@
22
22
  text-transform: none;
23
23
  display: inline-block;
24
24
  white-space: nowrap;
25
- word-wrap: normal;
25
+ overflow-wrap: normal;
26
26
  direction: ltr;
27
27
  font-feature-settings: 'liga';
28
- -webkit-font-feature-settings: 'liga';
29
28
  -webkit-font-smoothing: antialiased;
30
- transition-property: font-variation-settings;
31
- transition-timing-function: ease-in;
32
29
  transition: font-variation-settings 0.3s;
33
30
  font-variation-settings: var(--np-icon-settings, 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24);
34
31
  }
@@ -1,10 +1,4 @@
1
- /* Browsers without an `overlay` transition, such as Safari, drop a closing dialog or popover from
2
- the top layer at once, so its exit animation would never show. While it animates out it carries
3
- data-np-exiting (see exitAnimation in animation.ts) and these rules keep it shown, over the page
4
- but not blocking it. Browsers with the transition keep it in the top layer themselves. */
5
1
  @supports not (overlay: auto) {
6
- /* Important, as each component hides its closed state with a rule of its own, scoped and so
7
- more specific than this one. It only ever holds for the moment of leaving. */
8
2
  [popover][data-np-exiting]:not(:popover-open),
9
3
  dialog[data-np-exiting]:not([open]) {
10
4
  display: var(--np-exit-display, block) !important;
@@ -12,18 +6,11 @@
12
6
  pointer-events: none !important;
13
7
  }
14
8
 
15
- /* A closed dialog is no longer :modal and would lose its place. Components that place their
16
- dialog themselves win over this. */
17
9
  :where(dialog[data-np-exiting]:not([open])) {
18
10
  position: fixed;
19
11
  inset-block: 0;
20
12
  }
21
13
 
22
- /* Its ::backdrop is gone as well, so a dialog with a scrim draws one of its own that fades: a
23
- wide shadow around it, and one inside for a dialog whose box is transparent, as it sits under
24
- the content. A shadow rather than a pseudo element covers the screen even while the dialog
25
- moves. A dialog that is its own surface turns the inner one off with
26
- --np-exit-scrim-inner: 0 0 transparent. */
27
14
  dialog.np-exit-scrim[data-np-exiting]:not([open]) {
28
15
  --np-exit-scrim-color: color-mix(in srgb, var(--np-color-scrim) 32%, transparent);
29
16
  box-shadow:
@@ -39,7 +26,6 @@
39
26
  }
40
27
  }
41
28
 
42
- /* Registered, so the scrim's colour can fade. */
43
29
  @property --np-exit-scrim-color {
44
30
  syntax: '<color>';
45
31
  inherits: false;