@operato/utils 10.16.2 → 10.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,32 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ### [10.29.1](https://github.com/hatiolab/operato/compare/v11.0.0...v10.29.1) (2026-09-26)
7
+
8
+
9
+ ### :mega: Other
10
+
11
+ * **release:** return the version to 10.29.0 -- 11.0.0 was a mistaken major bump ([6ef39c1](https://github.com/hatiolab/operato/commit/6ef39c1107f0a6614ac78255fdb700e3eecfd0f7))
12
+
13
+
14
+
15
+ ## [11.0.0](https://github.com/hatiolab/operato/compare/v10.29.0...v11.0.0) (2026-09-26)
16
+
17
+ **Note:** Version bump only for package @operato/utils
18
+
19
+
20
+
21
+
22
+
23
+ ## [10.22.0](https://github.com/hatiolab/operato/compare/v10.21.0...v10.22.0) (2026-09-24)
24
+
25
+
26
+ ### :rocket: New Features
27
+
28
+ * **utils:** when - a moment drawn in the screen's language and a chosen zone ([1f36cdb](https://github.com/hatiolab/operato/commit/1f36cdb0467c09c3d47748a305deedec63ddc5be))
29
+
30
+
31
+
6
32
  ### [10.16.2](https://github.com/hatiolab/operato/compare/v10.16.1...v10.16.2) (2026-09-18)
7
33
 
8
34
  **Note:** Version bump only for package @operato/utils
@@ -23,30 +23,30 @@
23
23
  <div class='clearfix'>
24
24
 
25
25
  <div class='fl pad1y space-right2'>
26
- <span class="strong">92.67% </span>
26
+ <span class="strong">95.06% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>329/355</span>
28
+ <span class='fraction'>501/527</span>
29
29
  </div>
30
30
 
31
31
 
32
32
  <div class='fl pad1y space-right2'>
33
- <span class="strong">82.6% </span>
33
+ <span class="strong">86.29% </span>
34
34
  <span class="quiet">Branches</span>
35
- <span class='fraction'>57/69</span>
35
+ <span class='fraction'>107/124</span>
36
36
  </div>
37
37
 
38
38
 
39
39
  <div class='fl pad1y space-right2'>
40
- <span class="strong">92.85% </span>
40
+ <span class="strong">95.23% </span>
41
41
  <span class="quiet">Functions</span>
42
- <span class='fraction'>13/14</span>
42
+ <span class='fraction'>20/21</span>
43
43
  </div>
44
44
 
45
45
 
46
46
  <div class='fl pad1y space-right2'>
47
- <span class="strong">92.67% </span>
47
+ <span class="strong">95.06% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>329/355</span>
49
+ <span class='fraction'>501/527</span>
50
50
  </div>
51
51
 
52
52
 
@@ -80,17 +80,17 @@
80
80
  </thead>
81
81
  <tbody><tr>
82
82
  <td class="file high" data-value="src"><a href="src/index.html">src</a></td>
83
- <td data-value="97.18" class="pic high">
84
- <div class="chart"><div class="cover-fill" style="width: 97%"></div><div class="cover-empty" style="width: 3%"></div></div>
83
+ <td data-value="98.72" class="pic high">
84
+ <div class="chart"><div class="cover-fill" style="width: 98%"></div><div class="cover-empty" style="width: 2%"></div></div>
85
85
  </td>
86
- <td data-value="97.18" class="pct high">97.18%</td>
87
- <td data-value="142" class="abs high">138/142</td>
88
- <td data-value="95.55" class="pct high">95.55%</td>
89
- <td data-value="45" class="abs high">43/45</td>
86
+ <td data-value="98.72" class="pct high">98.72%</td>
87
+ <td data-value="314" class="abs high">310/314</td>
88
+ <td data-value="93" class="pct high">93%</td>
89
+ <td data-value="100" class="abs high">93/100</td>
90
90
  <td data-value="100" class="pct high">100%</td>
91
- <td data-value="6" class="abs high">6/6</td>
92
- <td data-value="97.18" class="pct high">97.18%</td>
93
- <td data-value="142" class="abs high">138/142</td>
91
+ <td data-value="13" class="abs high">13/13</td>
92
+ <td data-value="98.72" class="pct high">98.72%</td>
93
+ <td data-value="314" class="abs high">310/314</td>
94
94
  </tr>
95
95
 
96
96
  <tr>
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2026-09-17T16:05:34.844Z
119
+ at 2026-09-24T03:02:49.729Z
120
120
  </div>
121
121
  <script src="prettify.js"></script>
122
122
  <script>
@@ -216,6 +216,256 @@ BRF:37
216
216
  BRH:37
217
217
  end_of_record
218
218
  TN:
219
+ SF:src/when.ts
220
+ FN:31,availableZones
221
+ FN:36,nextZone
222
+ FN:50,resolveZone
223
+ FN:57,msOf
224
+ FN:113,formatWhen
225
+ FN:139,zoneAbbr
226
+ FN:164,dayHeadings
227
+ FNF:7
228
+ FNH:7
229
+ FNDA:8,availableZones
230
+ FNDA:5,nextZone
231
+ FNDA:5,resolveZone
232
+ FNDA:24,msOf
233
+ FNDA:15,formatWhen
234
+ FNDA:5,zoneAbbr
235
+ FNDA:5,dayHeadings
236
+ DA:1,1
237
+ DA:2,1
238
+ DA:3,1
239
+ DA:4,1
240
+ DA:5,1
241
+ DA:6,1
242
+ DA:7,1
243
+ DA:8,1
244
+ DA:9,1
245
+ DA:10,1
246
+ DA:11,1
247
+ DA:12,1
248
+ DA:13,1
249
+ DA:14,1
250
+ DA:15,1
251
+ DA:16,1
252
+ DA:17,1
253
+ DA:18,1
254
+ DA:19,1
255
+ DA:20,1
256
+ DA:21,1
257
+ DA:22,1
258
+ DA:23,1
259
+ DA:24,1
260
+ DA:25,1
261
+ DA:26,1
262
+ DA:27,1
263
+ DA:28,1
264
+ DA:29,1
265
+ DA:30,1
266
+ DA:31,1
267
+ DA:32,8
268
+ DA:33,8
269
+ DA:34,1
270
+ DA:35,1
271
+ DA:36,1
272
+ DA:37,5
273
+ DA:38,5
274
+ DA:39,5
275
+ DA:40,5
276
+ DA:41,5
277
+ DA:42,1
278
+ DA:43,1
279
+ DA:44,1
280
+ DA:45,1
281
+ DA:46,1
282
+ DA:47,1
283
+ DA:48,1
284
+ DA:49,1
285
+ DA:50,1
286
+ DA:51,5
287
+ DA:52,5
288
+ DA:53,5
289
+ DA:54,5
290
+ DA:55,1
291
+ DA:56,1
292
+ DA:57,1
293
+ DA:58,24
294
+ DA:59,24
295
+ DA:60,24
296
+ DA:61,3
297
+ DA:62,3
298
+ DA:63,3
299
+ DA:64,3
300
+ DA:65,3
301
+ DA:66,1
302
+ DA:67,1
303
+ DA:68,1
304
+ DA:69,1
305
+ DA:70,1
306
+ DA:71,1
307
+ DA:72,1
308
+ DA:73,1
309
+ DA:74,1
310
+ DA:75,1
311
+ DA:76,1
312
+ DA:77,1
313
+ DA:78,1
314
+ DA:79,1
315
+ DA:80,1
316
+ DA:81,1
317
+ DA:82,1
318
+ DA:83,1
319
+ DA:84,1
320
+ DA:85,1
321
+ DA:86,1
322
+ DA:87,1
323
+ DA:88,1
324
+ DA:89,1
325
+ DA:90,1
326
+ DA:91,1
327
+ DA:92,1
328
+ DA:93,1
329
+ DA:94,1
330
+ DA:95,1
331
+ DA:96,1
332
+ DA:97,1
333
+ DA:98,1
334
+ DA:99,1
335
+ DA:100,1
336
+ DA:101,1
337
+ DA:102,1
338
+ DA:103,1
339
+ DA:104,1
340
+ DA:105,1
341
+ DA:106,1
342
+ DA:107,1
343
+ DA:108,1
344
+ DA:109,1
345
+ DA:110,1
346
+ DA:111,1
347
+ DA:112,1
348
+ DA:113,1
349
+ DA:114,15
350
+ DA:115,15
351
+ DA:116,15
352
+ DA:117,15
353
+ DA:118,15
354
+ DA:119,15
355
+ DA:120,15
356
+ DA:121,15
357
+ DA:122,15
358
+ DA:123,15
359
+ DA:124,15
360
+ DA:125,15
361
+ DA:126,15
362
+ DA:127,1
363
+ DA:128,1
364
+ DA:129,1
365
+ DA:130,1
366
+ DA:131,15
367
+ DA:132,1
368
+ DA:133,1
369
+ DA:134,1
370
+ DA:135,1
371
+ DA:136,1
372
+ DA:137,1
373
+ DA:138,1
374
+ DA:139,1
375
+ DA:140,5
376
+ DA:141,4
377
+ DA:142,5
378
+ DA:143,5
379
+ DA:144,5
380
+ DA:145,5
381
+ DA:146,5
382
+ DA:147,5
383
+ DA:148,1
384
+ DA:149,1
385
+ DA:150,5
386
+ DA:151,1
387
+ DA:152,1
388
+ DA:153,1
389
+ DA:154,1
390
+ DA:155,1
391
+ DA:156,1
392
+ DA:157,1
393
+ DA:158,1
394
+ DA:159,1
395
+ DA:160,1
396
+ DA:161,1
397
+ DA:162,1
398
+ DA:163,1
399
+ DA:164,1
400
+ DA:165,5
401
+ DA:166,5
402
+ DA:167,5
403
+ DA:168,15
404
+ DA:169,15
405
+ DA:170,15
406
+ DA:171,5
407
+ DA:172,5
408
+ LF:172
409
+ LH:172
410
+ BRDA:31,0,0,8
411
+ BRDA:32,1,0,24
412
+ BRDA:32,2,0,24
413
+ BRDA:36,3,0,5
414
+ BRDA:40,4,0,1
415
+ BRDA:40,5,0,4
416
+ BRDA:50,6,0,5
417
+ BRDA:51,7,0,1
418
+ BRDA:52,8,0,4
419
+ BRDA:52,9,0,1
420
+ BRDA:53,10,0,3
421
+ BRDA:53,11,0,2
422
+ BRDA:57,12,0,24
423
+ BRDA:58,13,0,2
424
+ BRDA:58,14,0,2
425
+ BRDA:58,15,0,2
426
+ BRDA:59,16,0,22
427
+ BRDA:59,17,0,16
428
+ BRDA:59,18,0,16
429
+ BRDA:59,19,0,16
430
+ BRDA:60,20,0,6
431
+ BRDA:60,21,0,4
432
+ BRDA:60,22,0,3
433
+ BRDA:62,23,0,1
434
+ BRDA:62,24,0,2
435
+ BRDA:64,25,0,3
436
+ BRDA:113,26,0,15
437
+ BRDA:118,27,0,1
438
+ BRDA:119,28,0,14
439
+ BRDA:119,29,0,1
440
+ BRDA:120,30,0,13
441
+ BRDA:120,31,0,13
442
+ BRDA:120,32,0,10
443
+ BRDA:122,33,0,0
444
+ BRDA:124,34,0,13
445
+ BRDA:124,35,0,0
446
+ BRDA:126,36,0,1
447
+ BRDA:139,37,0,5
448
+ BRDA:140,38,0,1
449
+ BRDA:141,39,0,4
450
+ BRDA:142,40,0,0
451
+ BRDA:144,41,0,4
452
+ BRDA:144,42,0,0
453
+ BRDA:146,43,0,3
454
+ BRDA:146,44,0,3
455
+ BRDA:146,45,0,3
456
+ BRDA:146,46,0,0
457
+ BRDA:147,47,0,1
458
+ BRDA:146,48,0,21
459
+ BRDA:164,49,0,5
460
+ BRDA:167,50,0,15
461
+ BRDA:168,51,0,14
462
+ BRDA:168,52,0,9
463
+ BRDA:168,53,0,6
464
+ BRDA:169,54,0,14
465
+ BRF:55
466
+ BRH:50
467
+ end_of_record
468
+ TN:
219
469
  SF:src/reactive-controllers/tooltip-reactive-controller.ts
220
470
  FN:32,TooltipReactiveController
221
471
  FN:116,TooltipReactiveController.handleMouseOver
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A moment as the screen says it - pure: no lit, no i18n, no DOM. A value comes in, text goes out.
3
+ *
4
+ * Screens called `toLocaleString()` with no language, so a date followed the browser's setting instead
5
+ * of the screen's language: a Korean screen showed `8/22/2026, 5:23:33 PM` (operato-twin, 2026-08-25,
6
+ * fifteen places). The language is therefore always passed in here.
7
+ *
8
+ * The time zone is chosen per display, not per screen (user decision). A setting that followed you
9
+ * from screen to screen would be undone on the next one and leave you unsure which clock you were
10
+ * reading; a display that names its own zone cannot be mixed up with the one beside it.
11
+ *
12
+ * Nothing absent is made up. Some deployments set no domain time zone; that choice is then left out,
13
+ * visibly - the caller can tell what is missing and say so where it is drawn.
14
+ *
15
+ * Moved from operato-twin (client/when-core.ts) with its cases, ADR-0060 decision 3. plant wrote a
16
+ * language-only copy and was waiting for this one because it also carries the zone.
17
+ */
18
+ /** Which clock to read a moment on. */
19
+ export type ZoneChoice = 'local' | 'utc' | 'domain';
20
+ /** The order a display steps through - your own clock first, then UTC, then the domain's. */
21
+ export declare const ZONE_ORDER: readonly ZoneChoice[];
22
+ /**
23
+ * The choices open right now - two when no domain time zone is set.
24
+ *
25
+ * What was left out shows in the length of the list (compare with `ZONE_ORDER`), so a caller can say
26
+ * why there are only two.
27
+ */
28
+ export declare function availableZones(domainZone: string | null | undefined): ZoneChoice[];
29
+ /** The next choice, wrapping from the last to the first and skipping any that cannot be chosen. */
30
+ export declare function nextZone(current: ZoneChoice, domainZone: string | null | undefined): ZoneChoice;
31
+ /**
32
+ * The zone name to hand `Intl` - `undefined` means the viewer's own zone.
33
+ *
34
+ * A domain choice with no domain zone does not fall back to `undefined`: that would show another
35
+ * zone's time as the domain's. It answers `null`, "cannot be drawn". Callers use `availableZones`
36
+ * so they never pass it.
37
+ */
38
+ export declare function resolveZone(choice: ZoneChoice, domainZone: string | null | undefined): string | undefined | null;
39
+ /** Milliseconds from a number, an ISO string or a `Date`. Unreadable is `null` - never "Invalid Date". */
40
+ export declare function msOf(value: unknown): number | null;
41
+ /** How much to write. */
42
+ export type WhenStyle =
43
+ /** Date and hour:minute - the default for lists and details. */
44
+ 'datetime'
45
+ /** The date only. */
46
+ | 'date'
47
+ /** Hour:minute only - for something within today. */
48
+ | 'time'
49
+ /** Down to the second - for following a flow as it happens. */
50
+ | 'precise';
51
+ /**
52
+ * One moment, drawn in the language and the zone it is given.
53
+ *
54
+ * Leaving the language out follows the browser, which is why this module exists - so the caller always
55
+ * passes `lang`. An empty one is passed on as `undefined`, and that is the only place it happens.
56
+ *
57
+ * `null` when it cannot be drawn: no readable value (`msOf` is `null`) or no zone to read it on
58
+ * (`resolveZone` is `null`). The screen gets nothing false to show.
59
+ */
60
+ export declare function formatWhen(value: unknown, opts: {
61
+ lang?: string;
62
+ zone?: string | null;
63
+ style?: WhenStyle;
64
+ }): string | null;
65
+ /**
66
+ * A zone's short name - `KST`, `UTC`.
67
+ *
68
+ * Where there is summer time the name changes with the date (`PST`/`PDT`), so it is taken at that
69
+ * moment. `null` when there is none.
70
+ */
71
+ export declare function zoneAbbr(zone: string | null | undefined, lang: string | undefined, at: number): string | null;
72
+ /**
73
+ * Where the date changes in a list - a date heading goes only there.
74
+ *
75
+ * A date on every row widens the time column and pushes what is beside it. The date needs saying once,
76
+ * where it changes, and on the first row too, so a list of one day still shows its date (operato-twin,
77
+ * 2026-08-27: a history of `16:04` rows gave no way to tell the day).
78
+ *
79
+ * It takes dates **already drawn**, so the day changes in the zone the person chose. Cutting the first
80
+ * ten characters of an ISO string would read UTC and go wrong near midnight.
81
+ *
82
+ * A row with no date (empty string) gets no heading and does not clear the one before it.
83
+ */
84
+ export declare function dayHeadings(days: readonly string[]): string[];
@@ -0,0 +1,158 @@
1
+ /**
2
+ * A moment as the screen says it - pure: no lit, no i18n, no DOM. A value comes in, text goes out.
3
+ *
4
+ * Screens called `toLocaleString()` with no language, so a date followed the browser's setting instead
5
+ * of the screen's language: a Korean screen showed `8/22/2026, 5:23:33 PM` (operato-twin, 2026-08-25,
6
+ * fifteen places). The language is therefore always passed in here.
7
+ *
8
+ * The time zone is chosen per display, not per screen (user decision). A setting that followed you
9
+ * from screen to screen would be undone on the next one and leave you unsure which clock you were
10
+ * reading; a display that names its own zone cannot be mixed up with the one beside it.
11
+ *
12
+ * Nothing absent is made up. Some deployments set no domain time zone; that choice is then left out,
13
+ * visibly - the caller can tell what is missing and say so where it is drawn.
14
+ *
15
+ * Moved from operato-twin (client/when-core.ts) with its cases, ADR-0060 decision 3. plant wrote a
16
+ * language-only copy and was waiting for this one because it also carries the zone.
17
+ */
18
+ /** The order a display steps through - your own clock first, then UTC, then the domain's. */
19
+ export const ZONE_ORDER = ['local', 'utc', 'domain'];
20
+ /**
21
+ * The choices open right now - two when no domain time zone is set.
22
+ *
23
+ * What was left out shows in the length of the list (compare with `ZONE_ORDER`), so a caller can say
24
+ * why there are only two.
25
+ */
26
+ export function availableZones(domainZone) {
27
+ return ZONE_ORDER.filter(z => z !== 'domain' || !!domainZone);
28
+ }
29
+ /** The next choice, wrapping from the last to the first and skipping any that cannot be chosen. */
30
+ export function nextZone(current, domainZone) {
31
+ const zones = availableZones(domainZone);
32
+ const at = zones.indexOf(current);
33
+ /* A current choice that is no longer open (the domain zone went away) starts over. */
34
+ return zones[at < 0 ? 0 : (at + 1) % zones.length];
35
+ }
36
+ /**
37
+ * The zone name to hand `Intl` - `undefined` means the viewer's own zone.
38
+ *
39
+ * A domain choice with no domain zone does not fall back to `undefined`: that would show another
40
+ * zone's time as the domain's. It answers `null`, "cannot be drawn". Callers use `availableZones`
41
+ * so they never pass it.
42
+ */
43
+ export function resolveZone(choice, domainZone) {
44
+ if (choice === 'local')
45
+ return undefined;
46
+ if (choice === 'utc')
47
+ return 'UTC';
48
+ return domainZone || null;
49
+ }
50
+ /** Milliseconds from a number, an ISO string or a `Date`. Unreadable is `null` - never "Invalid Date". */
51
+ export function msOf(value) {
52
+ if (value instanceof Date)
53
+ return Number.isFinite(value.getTime()) ? value.getTime() : null;
54
+ if (typeof value === 'number')
55
+ return Number.isFinite(value) ? value : null;
56
+ if (typeof value === 'string' && value.trim()) {
57
+ const t = Date.parse(value);
58
+ return Number.isFinite(t) ? t : null;
59
+ }
60
+ return null;
61
+ }
62
+ /*
63
+ * A 24-hour clock in every language.
64
+ *
65
+ * These are screens where times are compared (when it came in, when it left, which came first), and a
66
+ * 12-hour clock makes you read morning or afternoon each time. Japanese, Chinese and Spanish already
67
+ * default to 24 hours; only Korean and English differed, so the five are made one.
68
+ *
69
+ * The order of the date and the month names stay the language's own (`2026년 8월 22일` ·
70
+ * `Aug 22, 2026` · `22 ago 2026`). Only the hour:minute form is ours.
71
+ */
72
+ const HOUR_CYCLE = { hourCycle: 'h23' };
73
+ const FIELDS = {
74
+ datetime: { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', ...HOUR_CYCLE },
75
+ date: { year: 'numeric', month: 'short', day: 'numeric' },
76
+ time: { hour: '2-digit', minute: '2-digit', ...HOUR_CYCLE },
77
+ precise: {
78
+ month: 'short',
79
+ day: 'numeric',
80
+ hour: '2-digit',
81
+ minute: '2-digit',
82
+ second: '2-digit',
83
+ ...HOUR_CYCLE
84
+ }
85
+ };
86
+ /**
87
+ * One moment, drawn in the language and the zone it is given.
88
+ *
89
+ * Leaving the language out follows the browser, which is why this module exists - so the caller always
90
+ * passes `lang`. An empty one is passed on as `undefined`, and that is the only place it happens.
91
+ *
92
+ * `null` when it cannot be drawn: no readable value (`msOf` is `null`) or no zone to read it on
93
+ * (`resolveZone` is `null`). The screen gets nothing false to show.
94
+ */
95
+ export function formatWhen(value, opts) {
96
+ var _a;
97
+ const ms = msOf(value);
98
+ if (ms === null)
99
+ return null;
100
+ if (opts.zone === null)
101
+ return null;
102
+ const fields = FIELDS[(_a = opts.style) !== null && _a !== void 0 ? _a : 'datetime'];
103
+ try {
104
+ return new Intl.DateTimeFormat(opts.lang || undefined, {
105
+ ...fields,
106
+ ...(opts.zone ? { timeZone: opts.zone } : {})
107
+ }).format(new Date(ms));
108
+ }
109
+ catch (_b) {
110
+ /* An unknown zone name (a typo in a deployment's setting) throws. It is not drawn on another clock
111
+ instead: the caller has to know it could not be drawn so the screen can say that. */
112
+ return null;
113
+ }
114
+ }
115
+ /**
116
+ * A zone's short name - `KST`, `UTC`.
117
+ *
118
+ * Where there is summer time the name changes with the date (`PST`/`PDT`), so it is taken at that
119
+ * moment. `null` when there is none.
120
+ */
121
+ export function zoneAbbr(zone, lang, at) {
122
+ var _a, _b;
123
+ if (zone === null)
124
+ return null;
125
+ try {
126
+ const parts = new Intl.DateTimeFormat(lang || undefined, {
127
+ timeZoneName: 'short',
128
+ ...(zone ? { timeZone: zone } : {})
129
+ }).formatToParts(new Date(at));
130
+ return (_b = (_a = parts.find(p => p.type === 'timeZoneName')) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : null;
131
+ }
132
+ catch (_c) {
133
+ return null;
134
+ }
135
+ }
136
+ /**
137
+ * Where the date changes in a list - a date heading goes only there.
138
+ *
139
+ * A date on every row widens the time column and pushes what is beside it. The date needs saying once,
140
+ * where it changes, and on the first row too, so a list of one day still shows its date (operato-twin,
141
+ * 2026-08-27: a history of `16:04` rows gave no way to tell the day).
142
+ *
143
+ * It takes dates **already drawn**, so the day changes in the zone the person chose. Cutting the first
144
+ * ten characters of an ISO string would read UTC and go wrong near midnight.
145
+ *
146
+ * A row with no date (empty string) gets no heading and does not clear the one before it.
147
+ */
148
+ export function dayHeadings(days) {
149
+ const heads = [];
150
+ let prev = '';
151
+ for (const day of days) {
152
+ heads.push(day && day !== prev ? day : '');
153
+ if (day)
154
+ prev = day;
155
+ }
156
+ return heads;
157
+ }
158
+ //# sourceMappingURL=when.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"when.js","sourceRoot":"","sources":["../../src/when.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A moment as the screen says it - pure: no lit, no i18n, no DOM. A value comes in, text goes out.\n *\n * Screens called `toLocaleString()` with no language, so a date followed the browser's setting instead\n * of the screen's language: a Korean screen showed `8/22/2026, 5:23:33 PM` (operato-twin, 2026-08-25,\n * fifteen places). The language is therefore always passed in here.\n *\n * The time zone is chosen per display, not per screen (user decision). A setting that followed you\n * from screen to screen would be undone on the next one and leave you unsure which clock you were\n * reading; a display that names its own zone cannot be mixed up with the one beside it.\n *\n * Nothing absent is made up. Some deployments set no domain time zone; that choice is then left out,\n * visibly - the caller can tell what is missing and say so where it is drawn.\n *\n * Moved from operato-twin (client/when-core.ts) with its cases, ADR-0060 decision 3. plant wrote a\n * language-only copy and was waiting for this one because it also carries the zone.\n */\n\n/** Which clock to read a moment on. */\nexport type ZoneChoice = 'local' | 'utc' | 'domain'\n\n/** The order a display steps through - your own clock first, then UTC, then the domain's. */\nexport const ZONE_ORDER: readonly ZoneChoice[] = ['local', 'utc', 'domain']\n\n/**\n * The choices open right now - two when no domain time zone is set.\n *\n * What was left out shows in the length of the list (compare with `ZONE_ORDER`), so a caller can say\n * why there are only two.\n */\nexport function availableZones(domainZone: string | null | undefined): ZoneChoice[] {\n return ZONE_ORDER.filter(z => z !== 'domain' || !!domainZone)\n}\n\n/** The next choice, wrapping from the last to the first and skipping any that cannot be chosen. */\nexport function nextZone(current: ZoneChoice, domainZone: string | null | undefined): ZoneChoice {\n const zones = availableZones(domainZone)\n const at = zones.indexOf(current)\n /* A current choice that is no longer open (the domain zone went away) starts over. */\n return zones[at < 0 ? 0 : (at + 1) % zones.length]\n}\n\n/**\n * The zone name to hand `Intl` - `undefined` means the viewer's own zone.\n *\n * A domain choice with no domain zone does not fall back to `undefined`: that would show another\n * zone's time as the domain's. It answers `null`, \"cannot be drawn\". Callers use `availableZones`\n * so they never pass it.\n */\nexport function resolveZone(choice: ZoneChoice, domainZone: string | null | undefined): string | undefined | null {\n if (choice === 'local') return undefined\n if (choice === 'utc') return 'UTC'\n return domainZone || null\n}\n\n/** Milliseconds from a number, an ISO string or a `Date`. Unreadable is `null` - never \"Invalid Date\". */\nexport function msOf(value: unknown): number | null {\n if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.getTime() : null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim()) {\n const t = Date.parse(value)\n return Number.isFinite(t) ? t : null\n }\n return null\n}\n\n/** How much to write. */\nexport type WhenStyle =\n /** Date and hour:minute - the default for lists and details. */\n | 'datetime'\n /** The date only. */\n | 'date'\n /** Hour:minute only - for something within today. */\n | 'time'\n /** Down to the second - for following a flow as it happens. */\n | 'precise'\n\n/*\n * A 24-hour clock in every language.\n *\n * These are screens where times are compared (when it came in, when it left, which came first), and a\n * 12-hour clock makes you read morning or afternoon each time. Japanese, Chinese and Spanish already\n * default to 24 hours; only Korean and English differed, so the five are made one.\n *\n * The order of the date and the month names stay the language's own (`2026년 8월 22일` ·\n * `Aug 22, 2026` · `22 ago 2026`). Only the hour:minute form is ours.\n */\nconst HOUR_CYCLE: Intl.DateTimeFormatOptions = { hourCycle: 'h23' }\n\nconst FIELDS: Record<WhenStyle, Intl.DateTimeFormatOptions> = {\n datetime: { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', ...HOUR_CYCLE },\n date: { year: 'numeric', month: 'short', day: 'numeric' },\n time: { hour: '2-digit', minute: '2-digit', ...HOUR_CYCLE },\n precise: {\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n ...HOUR_CYCLE\n }\n}\n\n/**\n * One moment, drawn in the language and the zone it is given.\n *\n * Leaving the language out follows the browser, which is why this module exists - so the caller always\n * passes `lang`. An empty one is passed on as `undefined`, and that is the only place it happens.\n *\n * `null` when it cannot be drawn: no readable value (`msOf` is `null`) or no zone to read it on\n * (`resolveZone` is `null`). The screen gets nothing false to show.\n */\nexport function formatWhen(\n value: unknown,\n opts: { lang?: string; zone?: string | null; style?: WhenStyle }\n): string | null {\n const ms = msOf(value)\n if (ms === null) return null\n if (opts.zone === null) return null\n const fields = FIELDS[opts.style ?? 'datetime']\n try {\n return new Intl.DateTimeFormat(opts.lang || undefined, {\n ...fields,\n ...(opts.zone ? { timeZone: opts.zone } : {})\n }).format(new Date(ms))\n } catch {\n /* An unknown zone name (a typo in a deployment's setting) throws. It is not drawn on another clock\n instead: the caller has to know it could not be drawn so the screen can say that. */\n return null\n }\n}\n\n/**\n * A zone's short name - `KST`, `UTC`.\n *\n * Where there is summer time the name changes with the date (`PST`/`PDT`), so it is taken at that\n * moment. `null` when there is none.\n */\nexport function zoneAbbr(zone: string | null | undefined, lang: string | undefined, at: number): string | null {\n if (zone === null) return null\n try {\n const parts = new Intl.DateTimeFormat(lang || undefined, {\n timeZoneName: 'short',\n ...(zone ? { timeZone: zone } : {})\n }).formatToParts(new Date(at))\n return parts.find(p => p.type === 'timeZoneName')?.value ?? null\n } catch {\n return null\n }\n}\n\n/**\n * Where the date changes in a list - a date heading goes only there.\n *\n * A date on every row widens the time column and pushes what is beside it. The date needs saying once,\n * where it changes, and on the first row too, so a list of one day still shows its date (operato-twin,\n * 2026-08-27: a history of `16:04` rows gave no way to tell the day).\n *\n * It takes dates **already drawn**, so the day changes in the zone the person chose. Cutting the first\n * ten characters of an ISO string would read UTC and go wrong near midnight.\n *\n * A row with no date (empty string) gets no heading and does not clear the one before it.\n */\nexport function dayHeadings(days: readonly string[]): string[] {\n const heads: string[] = []\n let prev = ''\n for (const day of days) {\n heads.push(day && day !== prev ? day : '')\n if (day) prev = day\n }\n return heads\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};