@fusion.dev/helix-core 0.159.0 → 0.161.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.
@@ -1,3 +1,4 @@
1
+ import { valueAsDate as asDate, dateAsValue as fromDate } from '../value-as/value-as.js';
1
2
  const SAMPLE = new Date(Date.UTC(2006, 10, 22, 15, 4, 5));
2
3
  export const segmentLayout = (kind, locale, options = {}) => {
3
4
  const format = kind === 'date'
@@ -40,15 +41,37 @@ export const segmentLayout = (kind, locale, options = {}) => {
40
41
  return slots;
41
42
  };
42
43
  export const isTwelveHourLayout = (layout) => layout.some((slot) => 'kind' in slot && slot.kind === 'dayPeriod');
43
- const daysIn = (month, year) => month === undefined ? 31 : new Date(Date.UTC(year ?? 2000, month, 0)).getUTCDate();
44
- export const segmentRange = (kind, segments, twelve) => {
44
+ const yearNow = (system) => system ? system.parts(new Date()).year : new Date().getFullYear();
45
+ const yearsAround = (system) => {
46
+ const now = yearNow(system);
47
+ return [now, now + 1, now + 2];
48
+ };
49
+ const monthsIn = (year, system) => {
50
+ if (!system) {
51
+ return 12;
52
+ }
53
+ return year === undefined
54
+ ? Math.max(...yearsAround(system).map((one) => system.months(one)))
55
+ : system.months(year);
56
+ };
57
+ const daysIn = (month, year, system) => {
58
+ if (!system) {
59
+ return month === undefined ? 31 : new Date(Date.UTC(year ?? 2000, month, 0)).getUTCDate();
60
+ }
61
+ if (month === undefined) {
62
+ return 31;
63
+ }
64
+ const years = year === undefined ? yearsAround(system) : [year];
65
+ return Math.max(...years.filter((one) => month <= system.months(one)).map((one) => system.days(one, month)), 1);
66
+ };
67
+ export const segmentRange = (kind, segments, twelve, system) => {
45
68
  switch (kind) {
46
69
  case 'year':
47
70
  return { max: 9999, min: 1 };
48
71
  case 'month':
49
- return { max: 12, min: 1 };
72
+ return { max: monthsIn(segments.year, system), min: 1 };
50
73
  case 'day':
51
- return { max: daysIn(segments.month, segments.year), min: 1 };
74
+ return { max: daysIn(segments.month, segments.year, system), min: 1 };
52
75
  case 'hour':
53
76
  return twelve ? { max: 12, min: 1 } : { max: 23, min: 0 };
54
77
  case 'minute':
@@ -58,8 +81,12 @@ export const segmentRange = (kind, segments, twelve) => {
58
81
  return { max: 1, min: 0 };
59
82
  }
60
83
  };
84
+ const halfCleared = (segments) => Number.isNaN(segments.dayPeriod ?? 0);
61
85
  export const shownValue = (kind, segments, twelve) => {
62
86
  if (kind === 'dayPeriod') {
87
+ if (halfCleared(segments)) {
88
+ return undefined;
89
+ }
63
90
  return segments.hour === undefined ? segments.dayPeriod : segments.hour >= 12 ? 1 : 0;
64
91
  }
65
92
  const value = segments[kind];
@@ -68,14 +95,28 @@ export const shownValue = (kind, segments, twelve) => {
68
95
  }
69
96
  return value;
70
97
  };
71
- export const setSegment = (segments, kind, shown, twelve) => {
98
+ export const setSegment = (segments, kind, shown, twelve, system) => {
72
99
  const next = { ...segments };
73
100
  if (shown === undefined) {
101
+ if (kind === 'dayPeriod' && twelve && segments.hour !== undefined) {
102
+ next.hour = segments.hour % 12;
103
+ next.dayPeriod = Number.NaN;
104
+ return next;
105
+ }
106
+ if (kind === 'hour' && twelve && segments.hour !== undefined && !halfCleared(segments)) {
107
+ next.dayPeriod = segments.hour >= 12 ? 1 : 0;
108
+ }
74
109
  delete next[kind];
75
110
  return next;
76
111
  }
77
112
  if (kind === 'hour' && twelve) {
78
- const half = segments.hour !== undefined ? (segments.hour >= 12 ? 1 : 0) : (segments.dayPeriod ?? 0);
113
+ const half = halfCleared(segments)
114
+ ? 0
115
+ : segments.hour !== undefined
116
+ ? segments.hour >= 12
117
+ ? 1
118
+ : 0
119
+ : (segments.dayPeriod ?? 0);
79
120
  next.hour = (shown % 12) + half * 12;
80
121
  }
81
122
  else if (kind === 'dayPeriod') {
@@ -87,17 +128,20 @@ export const setSegment = (segments, kind, shown, twelve) => {
87
128
  else {
88
129
  next[kind] = shown;
89
130
  }
131
+ if (next.month !== undefined) {
132
+ next.month = Math.min(next.month, monthsIn(next.year, system));
133
+ }
90
134
  if (next.day !== undefined) {
91
- next.day = Math.min(next.day, daysIn(next.month, next.year));
135
+ next.day = Math.min(next.day, daysIn(next.month, next.year, system));
92
136
  }
93
137
  return next;
94
138
  };
95
- export const stepSegment = (segments, kind, by, twelve, thisYear = new Date().getFullYear()) => {
96
- const { max, min } = segmentRange(kind, segments, twelve);
139
+ export const stepSegment = (segments, kind, by, twelve, thisYear, system) => {
140
+ const { max, min } = segmentRange(kind, segments, twelve, system);
97
141
  const shown = shownValue(kind, segments, twelve);
98
142
  let next;
99
143
  if (shown === undefined) {
100
- next = kind === 'year' ? thisYear : by > 0 ? min : max;
144
+ next = kind === 'year' ? (thisYear ?? yearNow(system)) : by > 0 ? min : max;
101
145
  }
102
146
  else if (kind === 'year') {
103
147
  next = Math.min(max, Math.max(min, shown + by));
@@ -106,17 +150,17 @@ export const stepSegment = (segments, kind, by, twelve, thisYear = new Date().ge
106
150
  const span = max - min + 1;
107
151
  next = ((((shown - min + by) % span) + span) % span) + min;
108
152
  }
109
- return setSegment(segments, kind, next, twelve);
153
+ return setSegment(segments, kind, next, twelve, system);
110
154
  };
111
- export const typeDigit = (segments, kind, typed, digit, twelve) => {
112
- const { max, min } = segmentRange(kind, segments, twelve);
155
+ export const typeDigit = (segments, kind, typed, digit, twelve, system) => {
156
+ const { max, min } = segmentRange(kind, segments, twelve, system);
113
157
  let text = `${typed}${digit}`;
114
158
  if (Number(text) > max) {
115
159
  text = digit;
116
160
  }
117
161
  const value = Number(text);
118
162
  const full = text.length >= String(max).length || value * 10 > max;
119
- const next = value >= min ? setSegment(segments, kind, value, twelve) : segments;
163
+ const next = value >= min ? setSegment(segments, kind, value, twelve, system) : segments;
120
164
  return { advance: full && value >= min, segments: next, typed: full ? '' : text };
121
165
  };
122
166
  const pageStep = {
@@ -127,24 +171,24 @@ const pageStep = {
127
171
  second: 15,
128
172
  year: 5,
129
173
  };
130
- export const segmentKey = (key, { kind, locale, segments, twelve, typed }) => {
174
+ export const segmentKey = (key, { kind, locale, segments, system, twelve, typed }) => {
131
175
  const done = (next) => next === undefined ? null : { advance: false, segments: next, typed: '' };
132
176
  if (key === 'ArrowUp' || key === 'ArrowDown') {
133
- return done(stepSegment(segments, kind, key === 'ArrowUp' ? 1 : -1, twelve));
177
+ return done(stepSegment(segments, kind, key === 'ArrowUp' ? 1 : -1, twelve, undefined, system));
134
178
  }
135
179
  if (key === 'PageUp' || key === 'PageDown') {
136
180
  const by = (key === 'PageUp' ? 1 : -1) * (pageStep[kind] ?? 1);
137
- return done(stepSegment(segments, kind, by, twelve));
181
+ return done(stepSegment(segments, kind, by, twelve, undefined, system));
138
182
  }
139
183
  if (key === 'Home' || key === 'End') {
140
- const { max, min } = segmentRange(kind, segments, twelve);
141
- return done(setSegment(segments, kind, key === 'Home' ? min : max, twelve));
184
+ const { max, min } = segmentRange(kind, segments, twelve, system);
185
+ return done(setSegment(segments, kind, key === 'Home' ? min : max, twelve, system));
142
186
  }
143
187
  if (key === 'Backspace' || key === 'Delete') {
144
- return done(setSegment(segments, kind, undefined, twelve));
188
+ return done(setSegment(segments, kind, undefined, twelve, system));
145
189
  }
146
190
  if (/^\d$/.test(key) && kind !== 'dayPeriod') {
147
- return typeDigit(segments, kind, typed, key, twelve);
191
+ return typeDigit(segments, kind, typed, key, twelve, system);
148
192
  }
149
193
  if (kind === 'dayPeriod' && key.length === 1) {
150
194
  const letter = key.toLowerCase();
@@ -154,9 +198,13 @@ export const segmentKey = (key, { kind, locale, segments, twelve, typed }) => {
154
198
  }
155
199
  return null;
156
200
  };
157
- export const segmentsOf = (kind, value) => {
201
+ export const segmentsOf = (kind, value, system) => {
158
202
  if (kind === 'date') {
159
203
  const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
204
+ const date = system && parts ? asDate(value) : null;
205
+ if (system && date) {
206
+ return { ...system.parts(date) };
207
+ }
160
208
  return parts ? { day: Number(parts[3]), month: Number(parts[2]), year: Number(parts[1]) } : {};
161
209
  }
162
210
  const parts = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value);
@@ -171,20 +219,24 @@ export const segmentsOf = (kind, value) => {
171
219
  ...(parts[3] === undefined ? {} : { second: Number(parts[3]) }),
172
220
  };
173
221
  };
174
- export const segmentsValue = (kind, segments, layout) => {
222
+ export const segmentsValue = (kind, segments, layout, system) => {
175
223
  const shown = layout.flatMap((slot) => ('kind' in slot ? [slot.kind] : []));
176
224
  const needed = shown.filter((one) => one !== 'dayPeriod');
177
- if (needed.some((one) => segments[one] === undefined)) {
225
+ if (needed.some((one) => segments[one] === undefined) ||
226
+ (shown.includes('dayPeriod') && halfCleared(segments))) {
178
227
  return '';
179
228
  }
180
229
  const pad = (number, width = 2) => String(number ?? 0).padStart(width, '0');
230
+ if (kind === 'date' && system) {
231
+ return fromDate(system.from(segments.year ?? 1, segments.month ?? 1, segments.day ?? 1));
232
+ }
181
233
  if (kind === 'date') {
182
234
  return `${pad(segments.year, 4)}-${pad(segments.month)}-${pad(segments.day)}`;
183
235
  }
184
236
  const clock = `${pad(segments.hour)}:${pad(segments.minute)}`;
185
237
  return shown.includes('second') ? `${clock}:${pad(segments.second)}` : clock;
186
238
  };
187
- export const isPartial = (kind, segments, layout) => segmentsValue(kind, segments, layout) === '' &&
239
+ export const isPartial = (kind, segments, layout, system) => segmentsValue(kind, segments, layout, system) === '' &&
188
240
  layout.some((slot) => 'kind' in slot && slot.kind !== 'dayPeriod' && segments[slot.kind] !== undefined);
189
241
  export const segmentText = (kind, segments, twelve, locale) => {
190
242
  const shown = shownValue(kind, segments, twelve);
@@ -205,7 +257,7 @@ export const segmentText = (kind, segments, twelve, locale) => {
205
257
  }
206
258
  return kind === 'hour' && twelve ? String(shown) : String(shown).padStart(width, '0');
207
259
  };
208
- export const segmentValueText = (kind, segments, twelve, locale) => {
260
+ export const segmentValueText = (kind, segments, twelve, locale, system) => {
209
261
  const text = segmentText(kind, segments, twelve, locale);
210
262
  const month = segments.month;
211
263
  if (kind !== 'month' || month === undefined) {
@@ -214,7 +266,9 @@ export const segmentValueText = (kind, segments, twelve, locale) => {
214
266
  const name = new Intl.DateTimeFormat(locale || undefined, {
215
267
  month: 'long',
216
268
  timeZone: 'UTC',
217
- }).format(new Date(Date.UTC(2000, month - 1, 1)));
269
+ }).format(system
270
+ ? system.from(segments.year ?? yearNow(system), month, 1)
271
+ : new Date(Date.UTC(2000, month - 1, 1)));
218
272
  return `${month} – ${name}`;
219
273
  };
220
274
  export const segmentName = (kind, locale) => {
@@ -226,3 +280,97 @@ export const segmentName = (kind, locale) => {
226
280
  return kind;
227
281
  }
228
282
  };
283
+ const ZEROS = [
284
+ 0x660, 0x6f0, 0x966, 0x9e6, 0xa66, 0xae6, 0xb66, 0xbe6, 0xc66, 0xce6, 0xd66, 0xe50, 0xed0, 0xf20,
285
+ 0x1040, 0x17e0, 0x1810, 0xff10,
286
+ ];
287
+ const latinDigits = (text) => text.replace(/\p{Nd}/gu, (char) => {
288
+ const code = char.charCodeAt(0);
289
+ const zero = ZEROS.find((one) => code >= one && code < one + 10);
290
+ return zero === undefined ? char : String(code - zero);
291
+ });
292
+ const wholeYear = (year, digits, now) => {
293
+ if (digits > 2) {
294
+ return year;
295
+ }
296
+ const century = Math.floor(now / 100) * 100;
297
+ const near = century + year;
298
+ return near > now + 20 ? near - 100 : near;
299
+ };
300
+ export const readWrittenDate = (text, locale, system) => {
301
+ const plain = latinDigits(text).toLowerCase();
302
+ const numbers = [...plain.matchAll(/\d+/g)].map((match) => match[0]);
303
+ let order = segmentLayout('date', locale).flatMap((slot) => 'kind' in slot && slot.kind !== 'dayPeriod' ? [slot.kind] : []);
304
+ const found = {};
305
+ if (!system) {
306
+ const names = (style) => Array.from({ length: 12 }, (_, index) => new Intl.DateTimeFormat(locale || undefined, { month: style, timeZone: 'UTC' })
307
+ .format(new Date(Date.UTC(2000, index, 1)))
308
+ .toLowerCase()
309
+ .replace(/\.$/, ''));
310
+ const words = plain.match(/\p{L}+/gu) ?? [];
311
+ for (const style of ['long', 'short']) {
312
+ const at = names(style).findIndex((name) => name.length > 1 && words.some((word) => word === name));
313
+ if (at >= 0 && found.month === undefined) {
314
+ found.month = at + 1;
315
+ order = order.filter((kind) => kind !== 'month');
316
+ }
317
+ }
318
+ }
319
+ if (numbers.length !== order.length || numbers.length === 0) {
320
+ return '';
321
+ }
322
+ if (numbers[0].length >= 3) {
323
+ const shown = order;
324
+ order = ['year', 'month', 'day'].filter((kind) => shown.includes(kind));
325
+ }
326
+ const now = system ? system.parts(new Date()).year : new Date().getFullYear();
327
+ order.forEach((kind, index) => {
328
+ const digits = numbers[index];
329
+ found[kind] = kind === 'year' ? wholeYear(Number(digits), digits.length, now) : Number(digits);
330
+ });
331
+ const { day, month, year } = found;
332
+ if (day === undefined || month === undefined || year === undefined || year < 1 || year > 9999) {
333
+ return '';
334
+ }
335
+ if (month < 1 || month > monthsIn(year, system) || day < 1 || day > daysIn(month, year, system)) {
336
+ return '';
337
+ }
338
+ return system
339
+ ? fromDate(system.from(year, month, day))
340
+ : segmentsValue('date', found, [{ kind: 'year' }, { kind: 'month' }, { kind: 'day' }]);
341
+ };
342
+ export const readWrittenTime = (text, locale, seconds = false) => {
343
+ const plain = latinDigits(text).toLowerCase();
344
+ const numbers = [...plain.matchAll(/\d+/g)].map((match) => Number(match[0]));
345
+ const letters = plain.replace(/[\d\s:.,h]/g, '');
346
+ const words = [0, 1].map((half) => segmentText('dayPeriod', { dayPeriod: half }, true, locale).toLowerCase().replace(/\./g, ''));
347
+ const bare = letters.replace(/\./g, '');
348
+ const half = bare === ''
349
+ ? undefined
350
+ : words.indexOf(bare) >= 0
351
+ ? words.indexOf(bare)
352
+ : /^a(m)?$/.test(bare)
353
+ ? 0
354
+ : /^p(m)?$/.test(bare)
355
+ ? 1
356
+ : -1;
357
+ if (half === -1 || numbers.length === 0 || numbers.length > 3) {
358
+ return '';
359
+ }
360
+ if (numbers.length === 1 && /\d{3,4}/.test(plain)) {
361
+ const run = numbers[0];
362
+ numbers.splice(0, 1, Math.floor(run / 100), run % 100);
363
+ }
364
+ let [hour = 0, minute = 0, second = 0] = numbers;
365
+ if (half !== undefined) {
366
+ if (hour < 1 || hour > 12) {
367
+ return '';
368
+ }
369
+ hour = (hour % 12) + half * 12;
370
+ }
371
+ if (hour > 23 || minute > 59 || second > 59) {
372
+ return '';
373
+ }
374
+ const pad = (number) => String(number).padStart(2, '0');
375
+ return seconds ? `${pad(hour)}:${pad(minute)}:${pad(second)}` : `${pad(hour)}:${pad(minute)}`;
376
+ };
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * release, so the two never disagree; a spec beside this holds them to it.
7
7
  * Nothing else in the running code knows which release it is.
8
8
  */
9
- export declare const VERSION = "0.159.0";
9
+ export declare const VERSION = "0.161.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.159.0';
1
+ export const VERSION = '0.161.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.159.0",
4
+ "version": "0.161.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -56,11 +56,11 @@
56
56
  "bugs": "https://github.com/project-fusion/fusion/issues",
57
57
  "dependencies": {
58
58
  "@floating-ui/dom": "^1.8.0",
59
- "@fusion.dev/helix-icons": "0.159.0"
59
+ "@fusion.dev/helix-icons": "0.161.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@fusion.dev/helix-cem": "0.159.0",
63
- "@fusion.dev/helix-testing": "0.159.0",
62
+ "@fusion.dev/helix-cem": "0.161.0",
63
+ "@fusion.dev/helix-testing": "0.161.0",
64
64
  "@lit-labs/ssr": "^4.1.0",
65
65
  "lit": "^3.2.1"
66
66
  },