noph-ui 0.46.1 → 0.47.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 (58) hide show
  1. package/README.md +2 -1
  2. package/dist/button/ButtonGroup.svelte +1 -3
  3. package/dist/date-picker/DockedDatePicker.svelte +2 -3
  4. package/dist/date-picker/DockedDateTimePicker.svelte +2 -3
  5. package/dist/date-picker/dateUtils.d.ts +2 -13
  6. package/dist/date-picker/dateUtils.js +4 -66
  7. package/dist/date-picker/formatters.d.ts +4 -0
  8. package/dist/date-picker/formatters.js +26 -0
  9. package/dist/date-picker/timeUtils.d.ts +34 -0
  10. package/dist/date-picker/timeUtils.js +139 -0
  11. package/dist/fab/FabMenu.svelte +1 -2
  12. package/dist/form-reset.d.ts +2 -1
  13. package/dist/form-reset.js +2 -2
  14. package/dist/icons/KeyboardIcon.svelte +5 -0
  15. package/dist/icons/KeyboardIcon.svelte.d.ts +26 -0
  16. package/dist/icons/ScheduleIcon.svelte +5 -0
  17. package/dist/icons/ScheduleIcon.svelte.d.ts +26 -0
  18. package/dist/icons/index.d.ts +2 -0
  19. package/dist/icons/index.js +2 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/keyboard-nav.js +3 -2
  23. package/dist/media.d.ts +4 -0
  24. package/dist/media.js +4 -0
  25. package/dist/menu/Menu.svelte +2 -1
  26. package/dist/progress/CircularProgress.svelte +9 -9
  27. package/dist/progress/LinearProgress.svelte +1 -2
  28. package/dist/ripple/Ripple.svelte +7 -8
  29. package/dist/select/Select.svelte +9 -10
  30. package/dist/select/VirtualList.svelte +12 -13
  31. package/dist/slider/Slider.svelte +5 -6
  32. package/dist/tabs/Tabs.svelte +21 -14
  33. package/dist/time-picker/ClockDial.svelte +528 -0
  34. package/dist/time-picker/ClockDial.svelte.d.ts +4 -0
  35. package/dist/time-picker/DockedTimePicker.svelte +378 -0
  36. package/dist/time-picker/DockedTimePicker.svelte.d.ts +7 -0
  37. package/dist/time-picker/PeriodSelector.svelte +156 -0
  38. package/dist/time-picker/PeriodSelector.svelte.d.ts +4 -0
  39. package/dist/time-picker/TimeInputs.svelte +233 -0
  40. package/dist/time-picker/TimeInputs.svelte.d.ts +4 -0
  41. package/dist/time-picker/TimePickerDialog.svelte +229 -0
  42. package/dist/time-picker/TimePickerDialog.svelte.d.ts +7 -0
  43. package/dist/time-picker/TimePickerPanel.svelte +203 -0
  44. package/dist/time-picker/TimePickerPanel.svelte.d.ts +4 -0
  45. package/dist/time-picker/TimeSelectors.svelte +157 -0
  46. package/dist/time-picker/TimeSelectors.svelte.d.ts +4 -0
  47. package/dist/time-picker/index.d.ts +4 -0
  48. package/dist/time-picker/index.js +4 -0
  49. package/dist/time-picker/timePickerState.svelte.d.ts +27 -0
  50. package/dist/time-picker/timePickerState.svelte.js +63 -0
  51. package/dist/time-picker/types.d.ts +148 -0
  52. package/dist/time-picker/types.js +1 -0
  53. package/dist/tooltip/Tooltip.svelte +2 -4
  54. package/dist/types.d.ts +1 -0
  55. package/dist/types.js +1 -0
  56. package/dist/validity.d.ts +2 -0
  57. package/dist/validity.js +4 -0
  58. package/package.json +1 -1
package/README.md CHANGED
@@ -73,7 +73,8 @@ Live demos and usage examples for every component are available on [noph.dev](ht
73
73
  [Select](https://noph.dev/components/select) ·
74
74
  [Slider](https://noph.dev/components/slider) ·
75
75
  [Switch](https://noph.dev/components/switch) ·
76
- [Text fields](https://noph.dev/components/text-field)
76
+ [Text fields](https://noph.dev/components/text-field) ·
77
+ [TimePicker](https://noph.dev/components/time-picker)
77
78
 
78
79
  **Navigation**
79
80
  [Menus](https://noph.dev/components/menu) ·
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
+ import { reducedMotion } from '../media.js'
2
3
  import { onMount } from 'svelte'
3
- import { MediaQuery } from 'svelte/reactivity'
4
4
  import { expandedWidths, parseMotion, resolveItem } from './buttonGroup.js'
5
5
  import type { ButtonGroupProps } from './types.js'
6
6
 
@@ -13,8 +13,6 @@
13
13
  ...attributes
14
14
  }: ButtonGroupProps = $props()
15
15
 
16
- const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)', false)
17
-
18
16
  const settleRatio = 0.75
19
17
 
20
18
  type Timing = { duration: number; easing: string }
@@ -4,7 +4,7 @@
4
4
  import Divider from '../divider/Divider.svelte'
5
5
  import CalendarToday from '../icons/CalendarToday.svelte'
6
6
  import Menu from '../menu/Menu.svelte'
7
- import { onFormReset } from '../form-reset.js'
7
+ import { formReset } from '../form-reset.js'
8
8
  import { syncOpenEffect } from '../popover.svelte.js'
9
9
  import TextField from '../text-field/TextField.svelte'
10
10
  import { onMount, tick } from 'svelte'
@@ -197,8 +197,6 @@
197
197
  })
198
198
  }
199
199
 
200
- $effect(() => onFormReset(valueInput, restoreDefault))
201
-
202
200
  const isShowing = () => !!menuElement?.matches(':popover-open')
203
201
 
204
202
  const openPicker = () => {
@@ -343,6 +341,7 @@
343
341
  </TextField>
344
342
  <input
345
343
  bind:this={valueInput}
344
+ {@attach formReset(restoreDefault)}
346
345
  class="np-docked-date-picker-value"
347
346
  type="hidden"
348
347
  value={current ?? ''}
@@ -4,7 +4,7 @@
4
4
  import Divider from '../divider/Divider.svelte'
5
5
  import CalendarToday from '../icons/CalendarToday.svelte'
6
6
  import Menu from '../menu/Menu.svelte'
7
- import { onFormReset } from '../form-reset.js'
7
+ import { formReset } from '../form-reset.js'
8
8
  import { syncOpenEffect } from '../popover.svelte.js'
9
9
  import TextField from '../text-field/TextField.svelte'
10
10
  import { onMount, tick } from 'svelte'
@@ -278,8 +278,6 @@
278
278
  })
279
279
  }
280
280
 
281
- $effect(() => onFormReset(valueInput, restoreDefault))
282
-
283
281
  const isShowing = () => !!menuElement?.matches(':popover-open')
284
282
 
285
283
  const openPicker = () => {
@@ -436,6 +434,7 @@
436
434
  </TextField>
437
435
  <input
438
436
  bind:this={valueInput}
437
+ {@attach formReset(restoreDefault)}
439
438
  class="np-docked-date-time-picker-value"
440
439
  type="hidden"
441
440
  value={current ?? ''}
@@ -1,3 +1,5 @@
1
+ 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';
2
+ export type { ISOTime } from './timeUtils.js';
1
3
  export declare const DAYS_IN_WEEK = 7;
2
4
  export declare const MAX_CALENDAR_ROWS = 6;
3
5
  export declare const DEFAULT_YEAR_RANGE: [number, number];
@@ -39,22 +41,9 @@ export interface CalendarDay {
39
41
  }
40
42
  export declare const getWeekRowCount: (month: Date, firstDayOfWeek: number) => number;
41
43
  export declare const getCalendarDays: (month: Date, firstDayOfWeek: number, rows?: number) => CalendarDay[];
42
- export declare const MINUTES_IN_HOUR = 60;
43
- export declare const HOURS_IN_DAY = 24;
44
- export declare const MINUTES_IN_DAY: number;
45
- export declare const minutesOfDay: (date: Date) => number;
46
- export declare const withMinutes: (date: Date, minutes: number) => Date;
47
- export declare const toISOTime: (minutes: number) => string;
48
44
  export declare const toISODateTime: (date: Date) => string;
49
45
  export declare const createDateTime: (year: number, month: number, day: number, hours: number, minutes: number) => Date | undefined;
50
46
  export declare const parseISODateTime: (value: string | null | undefined, dateOnlyMinutes?: number) => Date | undefined;
51
- export declare const compareTimes: (a: Date, b: Date) => number;
52
- export declare const isTimeWithin: (date: Date, min?: Date, max?: Date) => boolean;
53
- export declare const uses12HourClock: (locale?: string) => boolean;
54
- export declare const getDayPeriodLabels: (locale?: string) => [string, string];
55
- export declare const getHourLabels: (locale: string | undefined, hour12: boolean) => string[];
56
- export declare const formatTwoDigits: (value: number, locale?: string) => string;
57
- export declare const formatTime: (date: Date, locale?: string, hour12?: boolean) => string;
58
47
  export declare const formatDateTime: (date: Date, locale?: string, hour12?: boolean) => string;
59
48
  export declare const getDateTimePattern: (locale?: string, hour12?: boolean) => string;
60
49
  export declare const parseDateTimeInput: (value: string, locale?: string, hour12?: boolean) => Date | undefined;
@@ -1,7 +1,9 @@
1
+ import { getFormatter, numericDateOptions, pad } from './formatters.js';
2
+ import { getDayPeriodLabels, HOURS_IN_DAY, MINUTES_IN_HOUR, timeOptions, uses12HourClock, withMinutes, } from './timeUtils.js';
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';
1
4
  export const DAYS_IN_WEEK = 7;
2
5
  export const MAX_CALENDAR_ROWS = 6;
3
6
  export const DEFAULT_YEAR_RANGE = [1900, 2100];
4
- const pad = (value, length = 2) => `${value}`.padStart(length, '0');
5
7
  export const toISODate = (date) => `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
6
8
  export const parseISODate = (value) => {
7
9
  if (!value)
@@ -62,16 +64,6 @@ export const clampMonthInYear = (year, month, min, max) => {
62
64
  const upper = max && year === max.getFullYear() ? max.getMonth() : 11;
63
65
  return Math.min(Math.max(month, lower), upper);
64
66
  };
65
- const formatterCache = new Map();
66
- const getFormatter = (locale, options) => {
67
- const key = `${locale ?? ''}|${JSON.stringify(options)}`;
68
- let formatter = formatterCache.get(key);
69
- if (!formatter) {
70
- formatter = new Intl.DateTimeFormat(locale, options);
71
- formatterCache.set(key, formatter);
72
- }
73
- return formatter;
74
- };
75
67
  export const getFirstDayOfWeek = (locale) => {
76
68
  try {
77
69
  const resolved = new Intl.Locale(locale ?? getFormatter(locale, {}).resolvedOptions().locale);
@@ -98,11 +90,7 @@ export const getMonthNames = (locale, month) => {
98
90
  const formatter = getFormatter(locale, { month });
99
91
  return Array.from({ length: 12 }, (_, index) => formatter.format(new Date(2024, index, 1)));
100
92
  };
101
- const numericOptions = {
102
- year: 'numeric',
103
- month: '2-digit',
104
- day: '2-digit',
105
- };
93
+ const numericOptions = numericDateOptions;
106
94
  export const formatDate = (date, locale) => getFormatter(locale, numericOptions).format(date);
107
95
  export const formatDateMedium = (date, locale) => getFormatter(locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(date);
108
96
  export const formatDateLong = (date, locale) => getFormatter(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).format(date);
@@ -151,17 +139,7 @@ export const getCalendarDays = (month, firstDayOfWeek, rows = MAX_CALENDAR_ROWS)
151
139
  return { date, inMonth: date.getMonth() === first.getMonth() };
152
140
  });
153
141
  };
154
- export const MINUTES_IN_HOUR = 60;
155
- export const HOURS_IN_DAY = 24;
156
- export const MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR;
157
142
  const DATE_TIME_PROBE = new Date(2024, 0, 2, 13, 45);
158
- export const minutesOfDay = (date) => date.getHours() * MINUTES_IN_HOUR + date.getMinutes();
159
- export const withMinutes = (date, minutes) => {
160
- const copy = new Date(date);
161
- copy.setHours(Math.floor(minutes / MINUTES_IN_HOUR), minutes % MINUTES_IN_HOUR, 0, 0);
162
- return copy;
163
- };
164
- export const toISOTime = (minutes) => `${pad(Math.floor(minutes / MINUTES_IN_HOUR))}:${pad(minutes % MINUTES_IN_HOUR)}`;
165
143
  export const toISODateTime = (date) => `${toISODate(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
166
144
  export const createDateTime = (year, month, day, hours, minutes) => {
167
145
  if (hours < 0 || hours >= HOURS_IN_DAY || minutes < 0 || minutes >= MINUTES_IN_HOUR) {
@@ -186,50 +164,10 @@ export const parseISODateTime = (value, dateOnlyMinutes = 0) => {
186
164
  }
187
165
  return createDateTime(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes));
188
166
  };
189
- const minuteStamp = (date) => {
190
- const copy = new Date(date);
191
- copy.setSeconds(0, 0);
192
- return copy.getTime();
193
- };
194
- export const compareTimes = (a, b) => minuteStamp(a) - minuteStamp(b);
195
- export const isTimeWithin = (date, min, max) => (!min || compareTimes(date, min) >= 0) && (!max || compareTimes(date, max) <= 0);
196
- const hourOptions = (hour12) => ({
197
- hour: '2-digit',
198
- hour12,
199
- });
200
- const timeOptions = (hour12) => ({
201
- ...hourOptions(hour12),
202
- minute: '2-digit',
203
- });
204
167
  const dateTimeOptions = (hour12) => ({
205
168
  ...numericOptions,
206
169
  ...timeOptions(hour12),
207
170
  });
208
- export const uses12HourClock = (locale) => getFormatter(locale, { hour: 'numeric' }).resolvedOptions().hour12 ?? false;
209
- export const getDayPeriodLabels = (locale) => {
210
- const formatter = getFormatter(locale, { hour: 'numeric', hour12: true });
211
- const label = (hour, fallback) => formatter.formatToParts(new Date(2024, 0, 2, hour)).find((part) => part.type === 'dayPeriod')
212
- ?.value ?? fallback;
213
- return [label(9, 'AM'), label(21, 'PM')];
214
- };
215
- export const getHourLabels = (locale, hour12) => {
216
- const formatter = getFormatter(locale, hourOptions(hour12));
217
- return Array.from({ length: hour12 ? 12 : HOURS_IN_DAY }, (_, hour) => {
218
- const parts = formatter.formatToParts(new Date(2024, 0, 2, hour));
219
- return parts.find((part) => part.type === 'hour')?.value ?? `${hour}`;
220
- });
221
- };
222
- const numberFormatterCache = new Map();
223
- export const formatTwoDigits = (value, locale) => {
224
- const key = locale ?? '';
225
- let formatter = numberFormatterCache.get(key);
226
- if (!formatter) {
227
- formatter = new Intl.NumberFormat(locale, { minimumIntegerDigits: 2, useGrouping: false });
228
- numberFormatterCache.set(key, formatter);
229
- }
230
- return formatter.format(value);
231
- };
232
- export const formatTime = (date, locale, hour12 = uses12HourClock(locale)) => getFormatter(locale, timeOptions(hour12)).format(date);
233
171
  export const formatDateTime = (date, locale, hour12 = uses12HourClock(locale)) => getFormatter(locale, dateTimeOptions(hour12)).format(date);
234
172
  export const getDateTimePattern = (locale, hour12 = uses12HourClock(locale)) => {
235
173
  const [am, pm] = getDayPeriodLabels(locale);
@@ -0,0 +1,4 @@
1
+ export declare const pad: (value: number, length?: number) => string;
2
+ export declare const getFormatter: (locale: string | undefined, options: Intl.DateTimeFormatOptions) => Intl.DateTimeFormat;
3
+ export declare const formatTwoDigits: (value: number, locale?: string) => string;
4
+ export declare const numericDateOptions: Intl.DateTimeFormatOptions;
@@ -0,0 +1,26 @@
1
+ export const pad = (value, length = 2) => `${value}`.padStart(length, '0');
2
+ const formatterCache = new Map();
3
+ export const getFormatter = (locale, options) => {
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;
11
+ };
12
+ 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
+ };
22
+ export const numericDateOptions = {
23
+ year: 'numeric',
24
+ month: '2-digit',
25
+ day: '2-digit',
26
+ };
@@ -0,0 +1,34 @@
1
+ import { formatTwoDigits } from './formatters.js';
2
+ export { formatTwoDigits };
3
+ export type ISOTime = string;
4
+ export declare const MINUTES_IN_HOUR = 60;
5
+ export declare const HOURS_IN_DAY = 24;
6
+ export declare const MINUTES_IN_DAY: number;
7
+ export declare const minutesOfDay: (date: Date) => number;
8
+ export declare const withMinutes: (date: Date, minutes: number) => Date;
9
+ export declare const toISOTime: (minutes: number) => string;
10
+ export declare const parseISOTime: (value: ISOTime | number | null | undefined) => number | undefined;
11
+ export declare const clampMinutes: (minutes: number, min?: number, max?: number) => number;
12
+ export declare const isMinuteWithin: (minutes: number, min?: number, max?: number) => boolean;
13
+ /**
14
+ * Snaps the minute part to the nearest multiple of `step` without ever rolling into the next hour,
15
+ * so dragging the dial past the last step lands on it rather than changing the hour.
16
+ */
17
+ export declare const snapToStep: (minutes: number, step: number) => number;
18
+ export declare const timeOptions: (hour12: boolean) => Intl.DateTimeFormatOptions;
19
+ export declare const compareTimes: (a: Date, b: Date) => number;
20
+ export declare const isTimeWithin: (date: Date, min?: Date, max?: Date) => boolean;
21
+ export declare const uses12HourClock: (locale?: string) => boolean;
22
+ export declare const getDayPeriodLabels: (locale?: string) => [string, string];
23
+ export declare const getHourLabels: (locale: string | undefined, hour12: boolean) => string[];
24
+ export declare const formatTime: (date: Date, locale?: string, hour12?: boolean) => string;
25
+ /** The same as {@link formatTime}, taking minutes since midnight instead of a `Date`. */
26
+ export declare const formatMinutes: (minutes: number, locale?: string, hour12?: boolean) => string;
27
+ /** A hint like `HH:mm` or `hh:mm AM/PM`, for a text field's supporting text. */
28
+ export declare const getTimePattern: (locale?: string, hour12?: boolean) => string;
29
+ /**
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.
33
+ */
34
+ export declare const parseTimeInput: (value: string, locale?: string, hour12?: boolean) => number | undefined;
@@ -0,0 +1,139 @@
1
+ import { formatTwoDigits, getFormatter, pad } from './formatters.js';
2
+ export { formatTwoDigits };
3
+ export const MINUTES_IN_HOUR = 60;
4
+ export const HOURS_IN_DAY = 24;
5
+ export const MINUTES_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR;
6
+ const TIME_PROBE = new Date(2024, 0, 2, 13, 45);
7
+ export const minutesOfDay = (date) => date.getHours() * MINUTES_IN_HOUR + date.getMinutes();
8
+ export const withMinutes = (date, minutes) => {
9
+ const copy = new Date(date);
10
+ copy.setHours(Math.floor(minutes / MINUTES_IN_HOUR), minutes % MINUTES_IN_HOUR, 0, 0);
11
+ return copy;
12
+ };
13
+ export const toISOTime = (minutes) => `${pad(Math.floor(minutes / MINUTES_IN_HOUR))}:${pad(minutes % MINUTES_IN_HOUR)}`;
14
+ export const parseISOTime = (value) => {
15
+ if (value === null || value === undefined || value === '')
16
+ return undefined;
17
+ if (typeof value === 'number') {
18
+ if (!Number.isFinite(value))
19
+ return undefined;
20
+ const minutes = Math.trunc(value);
21
+ return minutes >= 0 && minutes < MINUTES_IN_DAY ? minutes : undefined;
22
+ }
23
+ const match = /^(\d{1,2}):(\d{2})(?::\d{2}(?:\.\d+)?)?$/.exec(value.trim());
24
+ if (!match)
25
+ return undefined;
26
+ const hours = Number(match[1]);
27
+ const minutes = Number(match[2]);
28
+ if (hours >= HOURS_IN_DAY || minutes >= MINUTES_IN_HOUR)
29
+ return undefined;
30
+ return hours * MINUTES_IN_HOUR + minutes;
31
+ };
32
+ export const clampMinutes = (minutes, min, max) => {
33
+ if (min !== undefined && minutes < min)
34
+ return min;
35
+ if (max !== undefined && minutes > max)
36
+ return max;
37
+ return minutes;
38
+ };
39
+ export const isMinuteWithin = (minutes, min, max) => (min === undefined || minutes >= min) && (max === undefined || minutes <= max);
40
+ /**
41
+ * Snaps the minute part to the nearest multiple of `step` without ever rolling into the next hour,
42
+ * so dragging the dial past the last step lands on it rather than changing the hour.
43
+ */
44
+ export const snapToStep = (minutes, step) => {
45
+ if (!Number.isFinite(step) || step <= 1)
46
+ return minutes;
47
+ const hour = Math.floor(minutes / MINUTES_IN_HOUR);
48
+ const last = Math.floor((MINUTES_IN_HOUR - 1) / step) * step;
49
+ const snapped = Math.min(Math.round((minutes % MINUTES_IN_HOUR) / step) * step, last);
50
+ return hour * MINUTES_IN_HOUR + snapped;
51
+ };
52
+ const hourOptions = (hour12) => ({
53
+ hour: '2-digit',
54
+ hour12,
55
+ });
56
+ export const timeOptions = (hour12) => ({
57
+ ...hourOptions(hour12),
58
+ minute: '2-digit',
59
+ });
60
+ const minuteStamp = (date) => {
61
+ const copy = new Date(date);
62
+ copy.setSeconds(0, 0);
63
+ return copy.getTime();
64
+ };
65
+ export const compareTimes = (a, b) => minuteStamp(a) - minuteStamp(b);
66
+ export const isTimeWithin = (date, min, max) => (!min || compareTimes(date, min) >= 0) && (!max || compareTimes(date, max) <= 0);
67
+ export const uses12HourClock = (locale) => getFormatter(locale, { hour: 'numeric' }).resolvedOptions().hour12 ?? false;
68
+ export const getDayPeriodLabels = (locale) => {
69
+ const formatter = getFormatter(locale, { hour: 'numeric', hour12: true });
70
+ const label = (hour, fallback) => formatter.formatToParts(new Date(2024, 0, 2, hour)).find((part) => part.type === 'dayPeriod')
71
+ ?.value ?? fallback;
72
+ return [label(9, 'AM'), label(21, 'PM')];
73
+ };
74
+ export const getHourLabels = (locale, hour12) => {
75
+ const formatter = getFormatter(locale, hourOptions(hour12));
76
+ return Array.from({ length: hour12 ? 12 : HOURS_IN_DAY }, (_, hour) => {
77
+ const parts = formatter.formatToParts(new Date(2024, 0, 2, hour));
78
+ return parts.find((part) => part.type === 'hour')?.value ?? `${hour}`;
79
+ });
80
+ };
81
+ export const formatTime = (date, locale, hour12 = uses12HourClock(locale)) => getFormatter(locale, timeOptions(hour12)).format(date);
82
+ /** 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);
84
+ /** 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);
104
+ /**
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.
108
+ */
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)
119
+ return undefined;
120
+ let hours = Number(hour);
121
+ const minutes = Number(minute);
122
+ if (minutes >= MINUTES_IN_HOUR)
123
+ 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) {
136
+ return undefined;
137
+ }
138
+ return hours * MINUTES_IN_HOUR + minutes;
139
+ };
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">
2
+ import { reducedMotion } from '../media.js'
2
3
  import CloseIcon from '../icons/CloseIcon.svelte'
3
4
  import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
4
5
  import { popoverController, syncOpenEffect } from '../popover.svelte.js'
5
6
  import { PRESS_DURATION } from '../press.svelte.js'
6
7
  import { onMount } from 'svelte'
7
- import { MediaQuery } from 'svelte/reactivity'
8
8
  import Fab from './Fab.svelte'
9
9
  import type { FabMenuProps } from './types.js'
10
10
 
@@ -27,7 +27,6 @@
27
27
  const anchorName = `--np-fab-menu-${uid}`
28
28
 
29
29
  let menuElement: HTMLDivElement | undefined = $state()
30
- const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)', false)
31
30
 
32
31
  const ITEMS = 'button, a[href], [role="menuitem"]'
33
32
  const attach = rovingTabindex(ITEMS)
@@ -1 +1,2 @@
1
- export declare const onFormReset: (control: HTMLInputElement | HTMLSelectElement | undefined, restore: () => void) => (() => void) | undefined;
1
+ import type { Attachment } from 'svelte/attachments';
2
+ export declare const formReset: (restore: () => void) => Attachment<HTMLInputElement | HTMLSelectElement>;
@@ -1,6 +1,6 @@
1
1
  import { on } from 'svelte/events';
2
- export const onFormReset = (control, restore) => {
3
- const form = control?.form;
2
+ export const formReset = (restore) => (control) => {
3
+ const form = control.form;
4
4
  if (!form)
5
5
  return;
6
6
  return on(form, 'reset', restore);
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"
2
+ ><path
3
+ d="M160-200q-33 0-56.5-23.5T80-280v-400q0-33 23.5-56.5T160-760h640q33 0 56.5 23.5T880-680v400q0 33-23.5 56.5T800-200H160Zm0-80h640v-400H160v400Zm160-40h320v-80H320v80ZM200-440h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80ZM200-560h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80Zm120 0h80v-80h-80v80ZM160-280v-400 400Z"
4
+ /></svg
5
+ >
@@ -0,0 +1,26 @@
1
+ export default KeyboardIcon;
2
+ type KeyboardIcon = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const KeyboardIcon: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"
2
+ ><path
3
+ d="m612-292 56-56-148-148v-184h-80v216l172 172ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z"
4
+ /></svg
5
+ >
@@ -0,0 +1,26 @@
1
+ export default ScheduleIcon;
2
+ type ScheduleIcon = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const ScheduleIcon: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -8,4 +8,6 @@ export { default as ChevronRightIcon } from './ChevronRightIcon.svelte';
8
8
  export { default as CloseIcon } from './CloseIcon.svelte';
9
9
  export { default as EditCalendarIcon } from './EditCalendarIcon.svelte';
10
10
  export { default as Icon } from './Icon.svelte';
11
+ export { default as KeyboardIcon } from './KeyboardIcon.svelte';
12
+ export { default as ScheduleIcon } from './ScheduleIcon.svelte';
11
13
  export { default as SearchIcon } from './SearchIcon.svelte';
@@ -8,4 +8,6 @@ export { default as ChevronRightIcon } from './ChevronRightIcon.svelte';
8
8
  export { default as CloseIcon } from './CloseIcon.svelte';
9
9
  export { default as EditCalendarIcon } from './EditCalendarIcon.svelte';
10
10
  export { default as Icon } from './Icon.svelte';
11
+ export { default as KeyboardIcon } from './KeyboardIcon.svelte';
12
+ export { default as ScheduleIcon } from './ScheduleIcon.svelte';
11
13
  export { default as SearchIcon } from './SearchIcon.svelte';
package/dist/index.d.ts CHANGED
@@ -26,5 +26,6 @@ export * from './snackbar/index.js';
26
26
  export * from './switch/index.js';
27
27
  export * from './tabs/index.js';
28
28
  export * from './text-field/index.js';
29
+ export * from './time-picker/index.js';
29
30
  export * from './toolbar/index.js';
30
31
  export * from './tooltip/index.js';
package/dist/index.js CHANGED
@@ -26,5 +26,6 @@ export * from './snackbar/index.js';
26
26
  export * from './switch/index.js';
27
27
  export * from './tabs/index.js';
28
28
  export * from './text-field/index.js';
29
+ export * from './time-picker/index.js';
29
30
  export * from './toolbar/index.js';
30
31
  export * from './tooltip/index.js';
@@ -1,3 +1,4 @@
1
+ import { on } from 'svelte/events';
1
2
  const focusable = (items) => items.filter((item) => !item.matches(':disabled'));
2
3
  const focusedItem = (node, itemSelector, items) => {
3
4
  const active = document.activeElement;
@@ -32,7 +33,7 @@ export const rovingTabindex = (itemSelector, options = {}) => {
32
33
  setTabstop(target);
33
34
  };
34
35
  sync();
35
- node.addEventListener('focusin', onFocusIn);
36
+ const offFocusIn = on(node, 'focusin', onFocusIn);
36
37
  const observer = new MutationObserver(sync);
37
38
  observer.observe(node, {
38
39
  attributes: true,
@@ -41,7 +42,7 @@ export const rovingTabindex = (itemSelector, options = {}) => {
41
42
  childList: true,
42
43
  });
43
44
  return () => {
44
- node.removeEventListener('focusin', onFocusIn);
45
+ offFocusIn();
45
46
  observer.disconnect();
46
47
  };
47
48
  };
@@ -0,0 +1,4 @@
1
+ import { MediaQuery } from 'svelte/reactivity';
2
+ export declare const reducedMotion: MediaQuery;
3
+ export declare const forcedColors: MediaQuery;
4
+ export declare const coarsePointer: MediaQuery;
package/dist/media.js ADDED
@@ -0,0 +1,4 @@
1
+ import { MediaQuery } from 'svelte/reactivity';
2
+ export const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)', false);
3
+ export const forcedColors = new MediaQuery('(forced-colors: active)', false);
4
+ export const coarsePointer = new MediaQuery('(hover: none) and (pointer: coarse)', false);
@@ -13,6 +13,7 @@
13
13
  open = $bindable(),
14
14
  style,
15
15
  popover = 'auto',
16
+ role = 'menu',
16
17
  anchor,
17
18
  coverAnchor = true,
18
19
  ontoggle,
@@ -71,7 +72,7 @@
71
72
  <svelte:window bind:innerHeight onresize={refreshValues} />
72
73
  <div
73
74
  {...attributes}
74
- role="menu"
75
+ {role}
75
76
  bind:this={element}
76
77
  ontoggle={(event) => {
77
78
  let { newState } = event