@dorsk/tsumikit 0.2.5 → 0.2.7
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/dist/components/layouts/AutoGrid.svelte +41 -6
- package/dist/components/layouts/AutoGrid.svelte.d.ts +8 -0
- package/dist/components/layouts/Container.svelte +19 -3
- package/dist/components/layouts/Container.svelte.d.ts +3 -1
- package/dist/components/molecules/Timestamp.svelte +35 -18
- package/dist/components/molecules/Timestamp.svelte.d.ts +13 -3
- package/dist/timestamp.d.ts +26 -11
- package/dist/timestamp.js +37 -14
- package/package.json +1 -1
|
@@ -7,30 +7,65 @@
|
|
|
7
7
|
// gutter. `maxCols` optionally caps how many columns the grid will ever show:
|
|
8
8
|
// it raises the effective per-column minimum to the width N columns would
|
|
9
9
|
// need, so auto-fit can never pack more than N across.
|
|
10
|
+
//
|
|
11
|
+
// `max` caps each column's width: instead of growing to fill the row (the
|
|
12
|
+
// default `1fr`), columns top out at `max` and the grid left-packs them
|
|
13
|
+
// (auto-fill + justify-content:start) so a 3-item and a 4-item section both
|
|
14
|
+
// show uniform fixed-width columns rather than stretching to fill. `align`
|
|
15
|
+
// controls cross-axis alignment of items within their row; it defaults to
|
|
16
|
+
// `start` so cards don't stretch to the tallest sibling.
|
|
10
17
|
import type { Snippet } from 'svelte';
|
|
11
18
|
|
|
12
19
|
let {
|
|
13
20
|
as = 'div',
|
|
14
21
|
min = '14rem',
|
|
22
|
+
max,
|
|
15
23
|
gap = 'var(--sp-4)',
|
|
16
24
|
maxCols,
|
|
25
|
+
align = 'start',
|
|
26
|
+
justify,
|
|
17
27
|
class: klass = '',
|
|
18
28
|
children,
|
|
19
29
|
...rest
|
|
20
30
|
}: {
|
|
21
31
|
as?: 'div' | 'section' | 'ul' | 'ol';
|
|
22
32
|
min?: string;
|
|
33
|
+
/** Maximum column width. When set, columns stop growing at this width and
|
|
34
|
+
* the grid left-packs uniform tracks instead of stretching to fill. */
|
|
35
|
+
max?: string;
|
|
23
36
|
gap?: string;
|
|
24
37
|
maxCols?: number;
|
|
38
|
+
/** Cross-axis alignment of items in their row (align-items). Defaults to `start`. */
|
|
39
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
40
|
+
/** Inline (main-axis) distribution of tracks (justify-content). Defaults to
|
|
41
|
+
* `start` when `max` is set, otherwise the grid's natural `stretch`. */
|
|
42
|
+
justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch';
|
|
25
43
|
class?: string;
|
|
26
44
|
children?: Snippet;
|
|
27
45
|
[key: string]: unknown;
|
|
28
46
|
} = $props();
|
|
29
47
|
|
|
48
|
+
// Columns grow to fill (`1fr`) by default; when `max` is given they cap there.
|
|
49
|
+
let track = $derived(max != null ? max : '1fr');
|
|
50
|
+
// auto-fill keeps capped tracks from stretching to fill the row; auto-fit
|
|
51
|
+
// (the default) collapses empty tracks so columns expand to use the space.
|
|
52
|
+
let mode = $derived(max != null ? 'auto-fill' : 'auto-fit');
|
|
53
|
+
// Left-pack capped grids by default so columns don't drift to fill the row.
|
|
54
|
+
let justifyValue = $derived(justify ?? (max != null ? 'start' : null));
|
|
55
|
+
|
|
30
56
|
let style = $derived(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
[
|
|
58
|
+
`--ag-min: ${min}`,
|
|
59
|
+
`--ag-track: ${track}`,
|
|
60
|
+
`--ag-mode: ${mode}`,
|
|
61
|
+
maxCols != null ? `--ag-gap: ${gap}` : null,
|
|
62
|
+
maxCols != null ? `--ag-cols: ${maxCols}` : null,
|
|
63
|
+
`gap: ${gap}`,
|
|
64
|
+
`align-items: ${align}`,
|
|
65
|
+
justifyValue ? `justify-content: ${justifyValue}` : null
|
|
66
|
+
]
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join('; ')
|
|
34
69
|
);
|
|
35
70
|
</script>
|
|
36
71
|
|
|
@@ -47,7 +82,7 @@
|
|
|
47
82
|
<style>
|
|
48
83
|
.autogrid-c {
|
|
49
84
|
display: grid;
|
|
50
|
-
grid-template-columns: repeat(
|
|
85
|
+
grid-template-columns: repeat(var(--ag-mode), minmax(min(100%, var(--ag-min)), var(--ag-track)));
|
|
51
86
|
}
|
|
52
87
|
|
|
53
88
|
/* Cap at N columns: the per-column floor becomes the larger of `min` and the
|
|
@@ -56,10 +91,10 @@
|
|
|
56
91
|
overflowing when the container is narrower than a single `min` track. */
|
|
57
92
|
.autogrid-c.capped {
|
|
58
93
|
grid-template-columns: repeat(
|
|
59
|
-
|
|
94
|
+
var(--ag-mode),
|
|
60
95
|
minmax(
|
|
61
96
|
min(100%, max(var(--ag-min), (100% - (var(--ag-cols) - 1) * var(--ag-gap)) / var(--ag-cols))),
|
|
62
|
-
|
|
97
|
+
var(--ag-track)
|
|
63
98
|
)
|
|
64
99
|
);
|
|
65
100
|
}
|
|
@@ -2,8 +2,16 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
as?: 'div' | 'section' | 'ul' | 'ol';
|
|
4
4
|
min?: string;
|
|
5
|
+
/** Maximum column width. When set, columns stop growing at this width and
|
|
6
|
+
* the grid left-packs uniform tracks instead of stretching to fill. */
|
|
7
|
+
max?: string;
|
|
5
8
|
gap?: string;
|
|
6
9
|
maxCols?: number;
|
|
10
|
+
/** Cross-axis alignment of items in their row (align-items). Defaults to `start`. */
|
|
11
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
12
|
+
/** Inline (main-axis) distribution of tracks (justify-content). Defaults to
|
|
13
|
+
* `start` when `max` is set, otherwise the grid's natural `stretch`. */
|
|
14
|
+
justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch';
|
|
7
15
|
class?: string;
|
|
8
16
|
children?: Snippet;
|
|
9
17
|
[key: string]: unknown;
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Centered, max-width content column with token gutters that respect safe-area
|
|
3
3
|
// insets. `size` overrides the default --content-max; `pad` toggles vertical
|
|
4
|
-
// padding.
|
|
4
|
+
// padding. `fullWidth` releases the max-width constraint and lets the content
|
|
5
|
+
// bleed to the full viewport width even when nested inside a centered ancestor
|
|
6
|
+
// (the `margin-inline: calc(50% - 50vw)` trick), for edge-to-edge sections.
|
|
7
|
+
// Polymorphic via `as` so it can be a <main>, <section>, etc.
|
|
5
8
|
import type { Snippet } from 'svelte';
|
|
6
9
|
|
|
7
10
|
let {
|
|
8
11
|
as = 'div',
|
|
9
12
|
size,
|
|
10
13
|
pad = false,
|
|
14
|
+
fullWidth = false,
|
|
11
15
|
class: klass = '',
|
|
12
16
|
children,
|
|
13
17
|
...rest
|
|
14
18
|
}: {
|
|
15
19
|
as?: 'div' | 'main' | 'section' | 'article';
|
|
16
|
-
/** Max width (any CSS length). Defaults to --content-max. */
|
|
20
|
+
/** Max width (any CSS length). Defaults to --content-max. Ignored when `fullWidth`. */
|
|
17
21
|
size?: string;
|
|
18
22
|
pad?: boolean;
|
|
23
|
+
/** Break out to the full viewport width, ignoring `size`/--content-max. */
|
|
24
|
+
fullWidth?: boolean;
|
|
19
25
|
class?: string;
|
|
20
26
|
children?: Snippet;
|
|
21
27
|
[key: string]: unknown;
|
|
@@ -26,7 +32,8 @@
|
|
|
26
32
|
this={as}
|
|
27
33
|
class="container ct {klass}"
|
|
28
34
|
class:pad
|
|
29
|
-
|
|
35
|
+
class:full={fullWidth}
|
|
36
|
+
style={!fullWidth && size ? `max-width: ${size}` : undefined}
|
|
30
37
|
{...rest}
|
|
31
38
|
>
|
|
32
39
|
{@render children?.()}
|
|
@@ -37,4 +44,13 @@
|
|
|
37
44
|
padding-top: var(--sp-6);
|
|
38
45
|
padding-bottom: var(--sp-12);
|
|
39
46
|
}
|
|
47
|
+
|
|
48
|
+
/* Break out of any centered ancestor to span the full viewport width.
|
|
49
|
+
`margin-inline: calc(50% - 50vw)` pulls each edge out to the viewport,
|
|
50
|
+
keeping the element in normal flow (no transform/overflow side-effects). */
|
|
51
|
+
.ct.full {
|
|
52
|
+
max-width: none;
|
|
53
|
+
width: 100vw;
|
|
54
|
+
margin-inline: calc(50% - 50vw);
|
|
55
|
+
}
|
|
40
56
|
</style>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
as?: 'div' | 'main' | 'section' | 'article';
|
|
4
|
-
/** Max width (any CSS length). Defaults to --content-max. */
|
|
4
|
+
/** Max width (any CSS length). Defaults to --content-max. Ignored when `fullWidth`. */
|
|
5
5
|
size?: string;
|
|
6
6
|
pad?: boolean;
|
|
7
|
+
/** Break out to the full viewport width, ignoring `size`/--content-max. */
|
|
8
|
+
fullWidth?: boolean;
|
|
7
9
|
class?: string;
|
|
8
10
|
children?: Snippet;
|
|
9
11
|
[key: string]: unknown;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// A timestamp you can read
|
|
3
|
-
// renders in the chosen `mode` (
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// A timestamp you can read five ways and inspect in one place. Inline it
|
|
3
|
+
// renders in the chosen `mode` (date / time / datetime / relative / iso),
|
|
4
|
+
// in the viewer's zone or UTC via the `utc` flag; by default clicking it
|
|
5
|
+
// opens a popover that lays out the same instant as ISO-UTC, local, relative,
|
|
6
|
+
// the IANA zone and the unix epoch — so the one value answers "when,
|
|
7
|
+
// exactly?" without leaving the row. The popover is read-only
|
|
7
8
|
// unless you opt into `selectable`, which adds buttons to switch the inline
|
|
8
9
|
// display mode (handy in a demo / settings surface, rarely in a data row).
|
|
9
10
|
//
|
|
@@ -19,26 +20,35 @@
|
|
|
19
20
|
toDate,
|
|
20
21
|
toEpochSeconds,
|
|
21
22
|
toISO,
|
|
22
|
-
|
|
23
|
+
toLocale,
|
|
23
24
|
type TimeInput,
|
|
24
25
|
type TimestampMode
|
|
25
26
|
} from '../../timestamp';
|
|
26
27
|
|
|
27
28
|
type Tone = 'inherit' | 'default' | 'muted' | 'faint' | 'danger' | 'accent';
|
|
29
|
+
type Size = 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl' | '2xl';
|
|
28
30
|
|
|
29
31
|
let {
|
|
30
32
|
value,
|
|
31
|
-
mode = '
|
|
33
|
+
mode = 'datetime',
|
|
34
|
+
utc = false,
|
|
32
35
|
details = true,
|
|
33
36
|
selectable = false,
|
|
34
37
|
mono = false,
|
|
35
38
|
tone = 'muted',
|
|
39
|
+
size,
|
|
36
40
|
tickMs = 30_000
|
|
37
41
|
}: {
|
|
38
|
-
/** The instant: a Date, epoch milliseconds, or an ISO/parseable string.
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
/** The instant: a Date, epoch milliseconds, or an ISO/parseable string.
|
|
43
|
+
* null/undefined (or unparseable input) renders an inert "—". */
|
|
44
|
+
value: TimeInput | null | undefined;
|
|
45
|
+
/** Inline display mode. Default 'datetime'. */
|
|
41
46
|
mode?: TimestampMode;
|
|
47
|
+
/** Render the date/time/datetime modes in UTC rather than the viewer's
|
|
48
|
+
* zone. Default false. Ignored by 'iso' (always UTC) and 'relative'
|
|
49
|
+
* (zoneless). Handy for calendar-date fields, where a local midnight-UTC
|
|
50
|
+
* value would otherwise show the wrong day. */
|
|
51
|
+
utc?: boolean;
|
|
42
52
|
/** Click to open the details popover (UTC / local / relative / zone /
|
|
43
53
|
* epoch). Default true. When false, renders as a bare inline <time>. */
|
|
44
54
|
details?: boolean;
|
|
@@ -50,6 +60,9 @@
|
|
|
50
60
|
/** Text colour, mirroring <Text> tones. Default 'muted' — timestamps read
|
|
51
61
|
* as subdued metadata; pass 'inherit' to blend with surrounding copy. */
|
|
52
62
|
tone?: Tone;
|
|
63
|
+
/** Font size, mirroring <Text> sizes (maps to --fs-* tokens). Omit to
|
|
64
|
+
* inherit the surrounding size. */
|
|
65
|
+
size?: Size;
|
|
53
66
|
/** How often relative mode re-renders so "3m ago" stays fresh. */
|
|
54
67
|
tickMs?: number;
|
|
55
68
|
} = $props();
|
|
@@ -70,24 +83,28 @@
|
|
|
70
83
|
});
|
|
71
84
|
|
|
72
85
|
const date = $derived(toDate(value));
|
|
73
|
-
const label = $derived(formatTimestamp(value, current, now));
|
|
86
|
+
const label = $derived(formatTimestamp(value, current, now, utc));
|
|
74
87
|
// datetime= wants a valid ISO string; omit it entirely on bad input.
|
|
75
88
|
const machine = $derived(date ? toISO(date) : undefined);
|
|
76
89
|
const showPopover = $derived(details || selectable);
|
|
77
90
|
const cls = $derived(`ts tone-${tone}${mono ? ' mono' : ''}`);
|
|
91
|
+
// Size maps straight onto the --fs-* scale; null leaves font-size unset so the
|
|
92
|
+
// timestamp inherits its surrounding run.
|
|
93
|
+
const sizeVar = $derived(size ? `var(--fs-${size})` : null);
|
|
78
94
|
|
|
79
95
|
const MODES: { id: TimestampMode; name: string }[] = [
|
|
80
|
-
{ id: '
|
|
81
|
-
{ id: 'local', name: 'Local' },
|
|
96
|
+
{ id: 'date', name: 'Date' },
|
|
82
97
|
{ id: 'time', name: 'Time' },
|
|
83
|
-
{ id: '
|
|
98
|
+
{ id: 'datetime', name: 'Date+time' },
|
|
99
|
+
{ id: 'relative', name: 'Relative' },
|
|
100
|
+
{ id: 'iso', name: 'ISO' }
|
|
84
101
|
];
|
|
85
102
|
|
|
86
103
|
const rows = $derived(
|
|
87
104
|
date
|
|
88
105
|
? [
|
|
89
106
|
{ k: 'UTC', v: toISO(date) },
|
|
90
|
-
{ k: 'Local', v:
|
|
107
|
+
{ k: 'Local', v: toLocale(date, 'datetime') },
|
|
91
108
|
{ k: 'Relative', v: relativeTime(date, now) },
|
|
92
109
|
{ k: 'Time zone', v: localTimeZone() },
|
|
93
110
|
{ k: 'Unix', v: String(toEpochSeconds(date)) }
|
|
@@ -98,11 +115,11 @@
|
|
|
98
115
|
|
|
99
116
|
{#if !date}
|
|
100
117
|
<!-- Unparseable input: degrade to an inert dash rather than empty text. -->
|
|
101
|
-
<time class="{cls} ts-invalid">—</time>
|
|
118
|
+
<time class="{cls} ts-invalid" style:font-size={sizeVar}>—</time>
|
|
102
119
|
{:else if showPopover}
|
|
103
120
|
<Popover label="Timestamp details" placement="bottom-start" bare>
|
|
104
121
|
{#snippet trigger()}
|
|
105
|
-
<time class="{cls} ts-trigger" datetime={machine}>{label}</time>
|
|
122
|
+
<time class="{cls} ts-trigger" style:font-size={sizeVar} datetime={machine}>{label}</time>
|
|
106
123
|
{/snippet}
|
|
107
124
|
<div class="ts-panel">
|
|
108
125
|
{#if selectable}
|
|
@@ -129,7 +146,7 @@
|
|
|
129
146
|
</div>
|
|
130
147
|
</Popover>
|
|
131
148
|
{:else}
|
|
132
|
-
<time class={cls} datetime={machine}>{label}</time>
|
|
149
|
+
<time class={cls} style:font-size={sizeVar} datetime={machine}>{label}</time>
|
|
133
150
|
{/if}
|
|
134
151
|
|
|
135
152
|
<style>
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { type TimeInput, type TimestampMode } from '../../timestamp';
|
|
2
2
|
type Tone = 'inherit' | 'default' | 'muted' | 'faint' | 'danger' | 'accent';
|
|
3
|
+
type Size = 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl' | '2xl';
|
|
3
4
|
type $$ComponentProps = {
|
|
4
|
-
/** The instant: a Date, epoch milliseconds, or an ISO/parseable string.
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** The instant: a Date, epoch milliseconds, or an ISO/parseable string.
|
|
6
|
+
* null/undefined (or unparseable input) renders an inert "—". */
|
|
7
|
+
value: TimeInput | null | undefined;
|
|
8
|
+
/** Inline display mode. Default 'datetime'. */
|
|
7
9
|
mode?: TimestampMode;
|
|
10
|
+
/** Render the date/time/datetime modes in UTC rather than the viewer's
|
|
11
|
+
* zone. Default false. Ignored by 'iso' (always UTC) and 'relative'
|
|
12
|
+
* (zoneless). Handy for calendar-date fields, where a local midnight-UTC
|
|
13
|
+
* value would otherwise show the wrong day. */
|
|
14
|
+
utc?: boolean;
|
|
8
15
|
/** Click to open the details popover (UTC / local / relative / zone /
|
|
9
16
|
* epoch). Default true. When false, renders as a bare inline <time>. */
|
|
10
17
|
details?: boolean;
|
|
@@ -16,6 +23,9 @@ type $$ComponentProps = {
|
|
|
16
23
|
/** Text colour, mirroring <Text> tones. Default 'muted' — timestamps read
|
|
17
24
|
* as subdued metadata; pass 'inherit' to blend with surrounding copy. */
|
|
18
25
|
tone?: Tone;
|
|
26
|
+
/** Font size, mirroring <Text> sizes (maps to --fs-* tokens). Omit to
|
|
27
|
+
* inherit the surrounding size. */
|
|
28
|
+
size?: Size;
|
|
19
29
|
/** How often relative mode re-renders so "3m ago" stays fresh. */
|
|
20
30
|
tickMs?: number;
|
|
21
31
|
};
|
package/dist/timestamp.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
export type TimeInput = Date | number | string;
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Which slice of the instant the inline text shows. Orthogonal to the zone:
|
|
4
|
+
* `date`/`time`/`datetime` honour the `utc` flag, while `iso` is always UTC
|
|
5
|
+
* (by definition) and `relative` is zoneless.
|
|
6
|
+
*/
|
|
7
|
+
export type TimestampMode = 'date' | 'time' | 'datetime' | 'relative' | 'iso';
|
|
8
|
+
/**
|
|
9
|
+
* Coerce a loose input to a Date, or null when it's missing or can't be parsed.
|
|
10
|
+
* Accepting null/undefined lets callers pass an optional field straight through
|
|
11
|
+
* — the empty case lands on the same "no date" path as bad input.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toDate(value: TimeInput | null | undefined): Date | null;
|
|
6
14
|
/** Full ISO 8601, UTC — `2026-06-14T07:30:00.000Z`. */
|
|
7
15
|
export declare function toISO(d: Date): string;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Locale-formatted date and/or time, in the viewer's zone or UTC. The single
|
|
18
|
+
* `Intl`-backed formatter behind the `date`/`time`/`datetime` modes — splitting
|
|
19
|
+
* the slice (what) from the zone (where) so a date-only field can render `utc`
|
|
20
|
+
* without the off-by-one a local midnight-UTC value otherwise shows.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toLocale(d: Date, parts?: 'date' | 'time' | 'datetime', utc?: boolean): string;
|
|
12
23
|
/** Unix epoch in whole seconds. */
|
|
13
24
|
export declare function toEpochSeconds(d: Date): number;
|
|
14
25
|
/** The viewer's IANA time zone — `Asia/Tokyo`, `UTC`, … */
|
|
@@ -18,6 +29,10 @@ export declare function localTimeZone(): string;
|
|
|
18
29
|
* falling back to a locale date once past ~30 days. Future instants read
|
|
19
30
|
* "in 3m" etc. `now` is injectable so callers (and tests) control the clock.
|
|
20
31
|
*/
|
|
21
|
-
export declare function relativeTime(value: TimeInput, now?: number): string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
32
|
+
export declare function relativeTime(value: TimeInput | null | undefined, now?: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Render a date in the chosen inline mode. `utc` selects UTC over the viewer's
|
|
35
|
+
* zone for the date/time/datetime modes (ignored by `iso`, always UTC, and
|
|
36
|
+
* `relative`, zoneless). Returns '' for unparseable input.
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatTimestamp(value: TimeInput | null | undefined, mode: TimestampMode, now?: number, utc?: boolean): string;
|
package/dist/timestamp.js
CHANGED
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
// `TimeInput` (Date | epoch ms | ISO string) and tolerates bad input by
|
|
5
5
|
// returning '' rather than throwing, so a single malformed value never takes
|
|
6
6
|
// down a table row.
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Coerce a loose input to a Date, or null when it's missing or can't be parsed.
|
|
9
|
+
* Accepting null/undefined lets callers pass an optional field straight through
|
|
10
|
+
* — the empty case lands on the same "no date" path as bad input.
|
|
11
|
+
*/
|
|
8
12
|
export function toDate(value) {
|
|
13
|
+
if (value == null)
|
|
14
|
+
return null;
|
|
9
15
|
const d = value instanceof Date ? value : new Date(value);
|
|
10
16
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
11
17
|
}
|
|
@@ -13,14 +19,27 @@ export function toDate(value) {
|
|
|
13
19
|
export function toISO(d) {
|
|
14
20
|
return d.toISOString();
|
|
15
21
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Locale-formatted date and/or time, in the viewer's zone or UTC. The single
|
|
24
|
+
* `Intl`-backed formatter behind the `date`/`time`/`datetime` modes — splitting
|
|
25
|
+
* the slice (what) from the zone (where) so a date-only field can render `utc`
|
|
26
|
+
* without the off-by-one a local midnight-UTC value otherwise shows.
|
|
27
|
+
*/
|
|
28
|
+
export function toLocale(d, parts = 'datetime', utc = false) {
|
|
29
|
+
const opts = {};
|
|
30
|
+
if (utc)
|
|
31
|
+
opts.timeZone = 'UTC';
|
|
32
|
+
if (parts !== 'time') {
|
|
33
|
+
opts.year = 'numeric';
|
|
34
|
+
opts.month = 'numeric';
|
|
35
|
+
opts.day = 'numeric';
|
|
36
|
+
}
|
|
37
|
+
if (parts !== 'date') {
|
|
38
|
+
opts.hour = '2-digit';
|
|
39
|
+
opts.minute = '2-digit';
|
|
40
|
+
opts.second = '2-digit';
|
|
41
|
+
}
|
|
42
|
+
return new Intl.DateTimeFormat(undefined, opts).format(d);
|
|
24
43
|
}
|
|
25
44
|
/** Unix epoch in whole seconds. */
|
|
26
45
|
export function toEpochSeconds(d) {
|
|
@@ -56,18 +75,22 @@ export function relativeTime(value, now = Date.now()) {
|
|
|
56
75
|
return suffix(`${days}d`);
|
|
57
76
|
return d.toLocaleDateString();
|
|
58
77
|
}
|
|
59
|
-
/**
|
|
60
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Render a date in the chosen inline mode. `utc` selects UTC over the viewer's
|
|
80
|
+
* zone for the date/time/datetime modes (ignored by `iso`, always UTC, and
|
|
81
|
+
* `relative`, zoneless). Returns '' for unparseable input.
|
|
82
|
+
*/
|
|
83
|
+
export function formatTimestamp(value, mode, now, utc = false) {
|
|
61
84
|
const d = toDate(value);
|
|
62
85
|
if (!d)
|
|
63
86
|
return '';
|
|
64
87
|
switch (mode) {
|
|
65
88
|
case 'iso':
|
|
66
89
|
return toISO(d);
|
|
67
|
-
case '
|
|
68
|
-
return toLocal(d);
|
|
90
|
+
case 'date':
|
|
69
91
|
case 'time':
|
|
70
|
-
|
|
92
|
+
case 'datetime':
|
|
93
|
+
return toLocale(d, mode, utc);
|
|
71
94
|
case 'relative':
|
|
72
95
|
return relativeTime(d, now);
|
|
73
96
|
}
|
package/package.json
CHANGED