@fusion.dev/helix-core 0.159.0 → 0.160.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.
@@ -50,7 +50,7 @@ export const calendarLocale = (locale, calendar = '') => {
50
50
  let tag = tagged.get(key);
51
51
  if (tag === undefined) {
52
52
  const own = new Intl.DateTimeFormat(locale || undefined).resolvedOptions();
53
- const wanted = DRAWN.includes(calendar)
53
+ const wanted = DRAWN.includes(calendar) || MONTH_CALENDARS.includes(calendar)
54
54
  ? calendar
55
55
  : DRAWN.includes(own.calendar)
56
56
  ? ''
@@ -69,39 +69,64 @@ export const addMonths = (date, months) => {
69
69
  return utcDate(target.getUTCFullYear(), target.getUTCMonth(), Math.min(date.getUTCDate(), last));
70
70
  };
71
71
  export const MONTH_CALENDARS = [
72
+ 'chinese',
72
73
  'coptic',
74
+ 'dangi',
75
+ 'ethioaa',
73
76
  'ethiopic',
74
77
  'hebrew',
75
78
  'indian',
79
+ 'islamic',
76
80
  'islamic-civil',
81
+ 'islamic-rgsa',
82
+ 'islamic-tbla',
77
83
  'islamic-umalqura',
78
84
  'persian',
79
85
  ];
86
+ const gregorianDays = (year, month) => utcDate(year, month, 0).getUTCDate();
80
87
  const GREGORIAN = {
81
88
  add: (date, months) => addMonths(date, months),
89
+ addYears: (date, years) => addMonths(date, years * 12),
82
90
  day: (date) => date.getUTCDate(),
91
+ days: gregorianDays,
92
+ from: (year, month, day) => {
93
+ const kept = Math.min(12, Math.max(1, month));
94
+ return utcDate(year, kept - 1, Math.min(Math.max(1, day), gregorianDays(year, kept)));
95
+ },
83
96
  key: (date) => monthOf(date),
97
+ months: () => 12,
98
+ parts: (date) => ({
99
+ day: date.getUTCDate(),
100
+ month: date.getUTCMonth() + 1,
101
+ year: date.getUTCFullYear(),
102
+ }),
84
103
  same: (left, right) => sameMonth(left, right),
85
104
  start: (date) => utcDate(date.getUTCFullYear(), date.getUTCMonth(), 1),
86
105
  };
87
106
  const intlMonths = (calendar) => {
88
- const format = new Intl.DateTimeFormat(`en-u-ca-${calendar}-nu-latn`, {
89
- day: 'numeric',
90
- timeZone: 'UTC',
91
- });
92
- const days = new Map();
93
- const day = (date) => {
94
- const at = date.getTime();
95
- let found = days.get(at);
107
+ const tag = `en-u-ca-${calendar}-nu-latn`;
108
+ const format = new Intl.DateTimeFormat(tag, { day: 'numeric', timeZone: 'UTC', year: 'numeric' });
109
+ const named = new Intl.DateTimeFormat(tag, { month: 'long', timeZone: 'UTC' });
110
+ const read = new Map();
111
+ const at = (date) => {
112
+ const time = date.getTime();
113
+ let found = read.get(time);
96
114
  if (found === undefined) {
97
- found = Number(format.formatToParts(date).find((part) => part.type === 'day')?.value);
98
- if (days.size > 4000) {
99
- days.clear();
115
+ const parts = format.formatToParts(date);
116
+ const part = (type) => Number(parts.find((one) => one.type === type)?.value);
117
+ const year = part('relatedYear');
118
+ found = [Number.isNaN(year) ? part('year') : year, part('day')];
119
+ if (!(found[1] >= 1)) {
120
+ found[1] = at(addDays(date, -1))[1] + 1;
121
+ }
122
+ if (read.size > 4000) {
123
+ read.clear();
100
124
  }
101
- days.set(at, found);
125
+ read.set(time, found);
102
126
  }
103
127
  return found;
104
128
  };
129
+ const day = (date) => at(date)[1];
105
130
  const start = (date) => addDays(date, 1 - day(date));
106
131
  const length = (first) => {
107
132
  for (const count of [29, 30, 31, 28, 32]) {
@@ -116,16 +141,78 @@ const intlMonths = (calendar) => {
116
141
  }
117
142
  return 30;
118
143
  };
144
+ const anchor = start(utcDate(2000, 0, 1));
145
+ const anchorYear = at(anchor)[0];
146
+ const span = 36_525 / (at(utcDate(2100, 0, 1))[0] - anchorYear);
147
+ const years = new Map();
148
+ const firsts = (year) => {
149
+ let found = years.get(year);
150
+ if (found !== undefined) {
151
+ return found;
152
+ }
153
+ let probe = addDays(anchor, Math.round((year - anchorYear + 0.5) * span));
154
+ for (let step = 0; step < 60 && at(probe)[0] !== year; step += 1) {
155
+ probe = addDays(probe, (year - at(probe)[0]) * 20);
156
+ }
157
+ let first = start(probe);
158
+ while (at(addDays(first, -1))[0] === year) {
159
+ const earlier = start(addDays(first, -1));
160
+ if (earlier.getTime() >= first.getTime()) {
161
+ break;
162
+ }
163
+ first = earlier;
164
+ }
165
+ found = [];
166
+ while (at(first)[0] === year && found.length < 14) {
167
+ found.push(first);
168
+ first = addDays(first, length(first));
169
+ }
170
+ if (years.size > 400) {
171
+ years.clear();
172
+ }
173
+ years.set(year, found);
174
+ return found;
175
+ };
176
+ const months = (year) => Math.max(1, firsts(year).length);
177
+ const firstOf = (year, month) => firsts(year)[Math.min(months(year), Math.max(1, month)) - 1] ?? anchor;
178
+ const days = (year, month) => length(firstOf(year, month));
179
+ const from = (year, month, wanted) => {
180
+ const first = firstOf(year, month);
181
+ return addDays(first, Math.min(Math.max(1, wanted), length(first)) - 1);
182
+ };
183
+ const parts = (date) => {
184
+ const [year, dayOf] = at(date);
185
+ const all = firsts(year);
186
+ let month = all.length;
187
+ while (month > 1 && all[month - 1].getTime() > date.getTime()) {
188
+ month -= 1;
189
+ }
190
+ return { day: dayOf, month, year };
191
+ };
119
192
  return {
120
- add: (date, months) => {
193
+ add: (date, count) => {
121
194
  let first = start(date);
122
- for (let step = 0; step < Math.abs(months); step += 1) {
123
- first = months > 0 ? addDays(first, length(first)) : start(addDays(first, -1));
195
+ for (let step = 0; step < Math.abs(count); step += 1) {
196
+ first = count > 0 ? addDays(first, length(first)) : start(addDays(first, -1));
124
197
  }
125
198
  return addDays(first, Math.min(day(date), length(first)) - 1);
126
199
  },
200
+ addYears: (date, count) => {
201
+ const { day: dayOf, month, year } = parts(date);
202
+ const name = named.format(firstOf(year, month));
203
+ const names = firsts(year + count).map((first) => named.format(first));
204
+ let index = names.indexOf(name);
205
+ if (index < 0) {
206
+ index = names.findLastIndex((one) => one.startsWith(name) || name.startsWith(one));
207
+ }
208
+ return from(year + count, index < 0 ? month : index + 1, dayOf);
209
+ },
127
210
  day,
211
+ days,
212
+ from,
128
213
  key: (date) => fromDate(start(date)),
214
+ months,
215
+ parts,
129
216
  same: (left, right) => start(left).getTime() === start(right).getTime(),
130
217
  start,
131
218
  };
@@ -181,27 +268,29 @@ export const periodMove = (key, at, along) => {
181
268
  return null;
182
269
  }
183
270
  };
184
- export const monthOut = (first, min, max) => {
185
- const month = monthOf(first);
186
- return (min !== '' && month < min.slice(0, 7)) || (max !== '' && month > max.slice(0, 7));
187
- };
188
- export const yearOut = (year, min, max) => (min !== '' && year < Number(min.slice(0, 4))) || (max !== '' && year > Number(max.slice(0, 4)));
189
- export const periodCells = ({ max, min, page, shown, today, view }) => {
271
+ export const monthOut = (first, min, max, system = GREGORIAN) => (min !== '' && fromDate(addDays(system.add(first, 1), -1)) < min) ||
272
+ (max !== '' && fromDate(first) > max);
273
+ export const yearOut = (year, min, max, system = GREGORIAN) => (min !== '' && fromDate(addDays(system.from(year + 1, 1, 1), -1)) < min) ||
274
+ (max !== '' && fromDate(system.from(year, 1, 1)) > max);
275
+ export const periodCells = ({ max, min, page, shown, system = GREGORIAN, today, view, }) => {
190
276
  const years = view === 'year';
191
- const start = years ? yearPageOf(page) : page;
192
- return Array.from({ length: 12 }, (_, index) => {
193
- const at = years ? start + index : index;
194
- const first = utcDate(years ? at : start, years ? 0 : index, 1);
277
+ const now = asDate(today);
278
+ const held = system.parts(shown).year;
279
+ const count = years ? YEARS_SHOWN : system.months(page);
280
+ const startYear = years ? yearPageOf(page) : page;
281
+ return Array.from({ length: count }, (_, index) => {
282
+ const at = years ? startYear + index : index;
283
+ const first = years ? system.from(at, 1, 1) : system.from(page, index + 1, 1);
195
284
  return {
196
285
  at,
197
- chosen: years ? at === shown.getUTCFullYear() : monthOf(first) === monthOf(shown),
198
- current: years ? String(at) === today.slice(0, 4) : monthOf(first) === today.slice(0, 7),
286
+ chosen: years ? at === held : system.same(first, shown),
287
+ current: now !== null && (years ? system.parts(now).year === at : system.same(first, now)),
199
288
  first,
200
- out: years ? yearOut(at, min, max) : monthOut(first, min, max),
289
+ out: years ? yearOut(at, min, max, system) : monthOut(first, min, max, system),
201
290
  };
202
291
  });
203
292
  };
204
- export const periodKey = (view, stop, year, key, along) => {
293
+ export const periodKey = (view, stop, year, key, along, months = () => 12) => {
205
294
  const index = view === 'year' ? stop - yearPageOf(stop) : stop;
206
295
  const delta = periodMove(key, index, along);
207
296
  if (delta === null) {
@@ -210,6 +299,19 @@ export const periodKey = (view, stop, year, key, along) => {
210
299
  if (view === 'year') {
211
300
  return { focus: stop + delta, pageYear: stop + delta };
212
301
  }
213
- const months = year * 12 + stop + delta;
214
- return { focus: ((months % 12) + 12) % 12, pageYear: Math.floor(months / 12) };
302
+ if (key === 'PageUp' || key === 'PageDown') {
303
+ const to = year + Math.sign(delta);
304
+ return { focus: Math.min(stop, months(to) - 1), pageYear: to };
305
+ }
306
+ let at = stop + delta;
307
+ let on = year;
308
+ while (at < 0) {
309
+ on -= 1;
310
+ at += months(on);
311
+ }
312
+ while (at >= months(on)) {
313
+ at -= months(on);
314
+ on += 1;
315
+ }
316
+ return { focus: at, pageYear: on };
215
317
  };
@@ -1,9 +1,11 @@
1
+ import type { MonthSystem } from '../calendar/calendar.js';
1
2
  /** One part of a date or a time a reader sets on its own. */
2
3
  export type SegmentKind = 'day' | 'dayPeriod' | 'hour' | 'minute' | 'month' | 'second' | 'year';
3
4
  /** What a segmented field holds: a date, or a time of day. */
4
5
  export type SegmentedKind = 'date' | 'time';
5
6
  /**
6
- * The parts set so far, each a number: the month 1 to 12, the hour 0 to 23
7
+ * The parts set so far, each a number: the month 1 to 12, or to 13 in a
8
+ * calendar whose year can have 13, the hour 0 to 23
7
9
  * whatever clock it is shown on, and the half of the day 0 for before noon and
8
10
  * 1 for after. A part not yet set is absent.
9
11
  */
@@ -37,7 +39,7 @@ export declare const isTwelveHourLayout: (layout: readonly SegmentSlot[]) => boo
37
39
  * The lowest and highest a segment can be, as it is shown: the hour on a twelve
38
40
  * hour clock is 1 to 12, and a day goes as high as its month allows.
39
41
  */
40
- export declare const segmentRange: (kind: SegmentKind, segments: Segments, twelve: boolean) => {
42
+ export declare const segmentRange: (kind: SegmentKind, segments: Segments, twelve: boolean, system?: MonthSystem) => {
41
43
  max: number;
42
44
  min: number;
43
45
  };
@@ -45,16 +47,16 @@ export declare const segmentRange: (kind: SegmentKind, segments: Segments, twelv
45
47
  export declare const shownValue: (kind: SegmentKind, segments: Segments, twelve: boolean) => number | undefined;
46
48
  /**
47
49
  * Sets a segment from the number shown for it, which on a twelve hour clock
48
- * is put back together with the half of the day, and keeps the day inside its
49
- * month.
50
+ * is put back together with the half of the day, and keeps the month inside
51
+ * its year and the day inside its month.
50
52
  */
51
- export declare const setSegment: (segments: Segments, kind: SegmentKind, shown: number | undefined, twelve: boolean) => Segments;
53
+ export declare const setSegment: (segments: Segments, kind: SegmentKind, shown: number | undefined, twelve: boolean, system?: MonthSystem) => Segments;
52
54
  /**
53
55
  * A segment moved by a key's step, wrapping at its ends as a dial does, and
54
56
  * starting from where a reader would look for it when it is empty: the lowest
55
57
  * going up, the highest going down, and this year for a year.
56
58
  */
57
- export declare const stepSegment: (segments: Segments, kind: SegmentKind, by: number, twelve: boolean, thisYear?: number) => Segments;
59
+ export declare const stepSegment: (segments: Segments, kind: SegmentKind, by: number, twelve: boolean, thisYear?: number, system?: MonthSystem) => Segments;
58
60
  /**
59
61
  * A digit typed into a segment, given what was typed into it already.
60
62
  *
@@ -64,7 +66,7 @@ export declare const stepSegment: (segments: Segments, kind: SegmentKind, by: nu
64
66
  * on, which is React Aria's typing: `3` in a month is March and moves to the
65
67
  * day, `1` waits for a second digit.
66
68
  */
67
- export declare const typeDigit: (segments: Segments, kind: SegmentKind, typed: string, digit: string, twelve: boolean) => {
69
+ export declare const typeDigit: (segments: Segments, kind: SegmentKind, typed: string, digit: string, twelve: boolean, system?: MonthSystem) => {
68
70
  advance: boolean;
69
71
  segments: Segments;
70
72
  typed: string;
@@ -77,6 +79,8 @@ export interface SegmentKeyState {
77
79
  locale: string;
78
80
  /** The field's parts. */
79
81
  segments: Segments;
82
+ /** The calendar a date's parts are counted in, Gregorian where none is given. */
83
+ system?: MonthSystem | undefined;
80
84
  /** Whether the field is on a twelve hour clock. */
81
85
  twelve: boolean;
82
86
  /** The digits typed into the segment so far. */
@@ -92,13 +96,16 @@ export interface SegmentKeyState {
92
96
  * the half whose word starts with it. `advance` says the segment is full and
93
97
  * focus moves on.
94
98
  */
95
- export declare const segmentKey: (key: string, { kind, locale, segments, twelve, typed }: SegmentKeyState) => {
99
+ export declare const segmentKey: (key: string, { kind, locale, segments, system, twelve, typed }: SegmentKeyState) => {
96
100
  advance: boolean;
97
101
  segments: Segments;
98
102
  typed: string;
99
103
  } | null;
100
- /** The segments a value names, or none where it names nothing whole. */
101
- export declare const segmentsOf: (kind: SegmentedKind, value: string) => Segments;
104
+ /**
105
+ * The segments a value names, or none where it names nothing whole: a date's
106
+ * year, month, and day as `system` counts them, Gregorian where none is given.
107
+ */
108
+ export declare const segmentsOf: (kind: SegmentedKind, value: string, system?: MonthSystem) => Segments;
102
109
  /**
103
110
  * The value the segments add up to, or the empty string while any segment the
104
111
  * layout shows is unset.
@@ -106,9 +113,9 @@ export declare const segmentsOf: (kind: SegmentedKind, value: string) => Segment
106
113
  * A date that does not exist, the thirtieth of February, cannot be reached,
107
114
  * the day being kept inside its month as it is set.
108
115
  */
109
- export declare const segmentsValue: (kind: SegmentedKind, segments: Segments, layout: readonly SegmentSlot[]) => string;
116
+ export declare const segmentsValue: (kind: SegmentedKind, segments: Segments, layout: readonly SegmentSlot[], system?: MonthSystem) => string;
110
117
  /** Whether a reader has set some segments and not all of them. */
111
- export declare const isPartial: (kind: SegmentedKind, segments: Segments, layout: readonly SegmentSlot[]) => boolean;
118
+ export declare const isPartial: (kind: SegmentedKind, segments: Segments, layout: readonly SegmentSlot[], system?: MonthSystem) => boolean;
112
119
  /**
113
120
  * What a segment shows: its number, padded the way the locale pads it, the
114
121
  * locale's own word for the half of the day, or dashes while it is unset.
@@ -117,8 +124,33 @@ export declare const segmentText: (kind: SegmentKind, segments: Segments, twelve
117
124
  /**
118
125
  * What a screen reader hears for a segment's value: what it shows, and for a
119
126
  * month its name as well, "8 – August", which is React Aria's reading. A
120
- * month heard as a number alone leaves a reader counting.
127
+ * month heard as a number alone leaves a reader counting. In a calendar with
128
+ * months of its own, the name is that calendar's month in the year the
129
+ * segments hold, or in this year: "7 – Mehr".
121
130
  */
122
- export declare const segmentValueText: (kind: SegmentKind, segments: Segments, twelve: boolean, locale: string) => string;
131
+ export declare const segmentValueText: (kind: SegmentKind, segments: Segments, twelve: boolean, locale: string, system?: MonthSystem) => string;
123
132
  /** What a locale calls a segment, such as "month" or "AM/PM", from `Intl`. */
124
133
  export declare const segmentName: (kind: SegmentKind, locale: string) => string;
134
+ /**
135
+ * The date a reader wrote in the order and the digits of `locale`, as
136
+ * `YYYY-MM-DD`, or empty where it is no date.
137
+ *
138
+ * `8/4/2026` in `en-US` and `4.8.2026` in `de-DE` are August 4, 2026, and a
139
+ * month written as a word, `Aug 4, 2026` or `4. August 2026`, is read by its
140
+ * name in the Gregorian calendar. A first number of four digits is a year,
141
+ * as ISO writes it. A year of two digits is the nearest year within eighty
142
+ * years back and twenty forward. `Intl` formats and does not parse, so this
143
+ * reads the numbers back in the order `segmentLayout` finds the locale
144
+ * writing them. Under `system`, the numbers are that calendar's year, month,
145
+ * and day.
146
+ */
147
+ export declare const readWrittenDate: (text: string, locale: string, system?: MonthSystem) => string;
148
+ /**
149
+ * The time a reader wrote, as `HH:mm`, or `HH:mm:ss` with `seconds`, or empty
150
+ * where it is no time.
151
+ *
152
+ * Reads `2:30 PM`, `2.30pm`, `14h30`, and `3 pm` alike, with the half of the
153
+ * day in the locale's own words or as `am` and `pm`, and digits in the
154
+ * locale's own script.
155
+ */
156
+ export declare const readWrittenTime: (text: string, locale: string, seconds?: boolean) => string;