@unofficialbox/box-open-elements 0.12.0 → 0.14.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.
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +40 -2
- package/dist/components/feedback/indicator.d.ts +34 -0
- package/dist/components/feedback/indicator.js +167 -0
- package/dist/components/feedback/toast.js +1 -17
- package/dist/components/feedback/tone.d.ts +18 -0
- package/dist/components/feedback/tone.js +23 -0
- package/dist/components/forms/calendar.d.ts +25 -0
- package/dist/components/forms/calendar.js +94 -2
- package/dist/components/forms/tile-group.d.ts +58 -0
- package/dist/components/forms/tile-group.js +280 -0
- package/dist/components/output/code-block.d.ts +49 -0
- package/dist/components/output/code-block.js +236 -0
- package/dist/components/output/formatted-date.d.ts +38 -0
- package/dist/components/output/formatted-date.js +90 -0
- package/dist/components/output/formatted-file-size.d.ts +31 -0
- package/dist/components/output/formatted-file-size.js +61 -0
- package/dist/components/output/formatted-number.d.ts +53 -0
- package/dist/components/output/formatted-number.js +130 -0
- package/dist/components/output/formatted-value.d.ts +44 -0
- package/dist/components/output/formatted-value.js +100 -0
- package/dist/components/output/index.d.ts +6 -0
- package/dist/components/output/index.js +9 -0
- package/dist/components/output/relative-time.d.ts +41 -0
- package/dist/components/output/relative-time.js +82 -0
- package/dist/element-maps.d.ts +15 -1
- package/dist/entries/code-block.d.ts +1 -0
- package/dist/entries/code-block.js +1 -0
- package/dist/entries/formatted-date.d.ts +1 -0
- package/dist/entries/formatted-date.js +1 -0
- package/dist/entries/formatted-file-size.d.ts +1 -0
- package/dist/entries/formatted-file-size.js +1 -0
- package/dist/entries/formatted-number.d.ts +1 -0
- package/dist/entries/formatted-number.js +1 -0
- package/dist/entries/indicator.d.ts +1 -0
- package/dist/entries/indicator.js +1 -0
- package/dist/entries/relative-time.d.ts +1 -0
- package/dist/entries/relative-time.js +1 -0
- package/dist/entries/tile-group.d.ts +1 -0
- package/dist/entries/tile-group.js +1 -0
- package/dist/foundations/format/format.d.ts +82 -0
- package/dist/foundations/format/format.js +122 -0
- package/dist/foundations/format/index.d.ts +1 -0
- package/dist/foundations/format/index.js +3 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FormattedValue } from "./formatted-value.js";
|
|
2
|
+
/**
|
|
3
|
+
* A byte count, rendered as a readable size.
|
|
4
|
+
*
|
|
5
|
+
* ```html
|
|
6
|
+
* <box-formatted-file-size value="2517630"></box-formatted-file-size>
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* The magnitude goes through `Intl.NumberFormat`, so a German reader sees
|
|
10
|
+
* "2,52 MB" rather than "2.52 MB". That is the reason this exists as a
|
|
11
|
+
* component rather than a `toFixed` call in each consumer: the decimal
|
|
12
|
+
* separator is not the same everywhere, and a file size is one of the most
|
|
13
|
+
* frequently rendered values in a content platform.
|
|
14
|
+
*
|
|
15
|
+
* Defaults to decimal units (powers of 1000, `kB`/`MB`), matching what the Box
|
|
16
|
+
* product reports. `units="binary"` switches to powers of 1024 with IEC names
|
|
17
|
+
* (`KiB`/`MiB`) for hosts that need to agree with a filesystem instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare class FormattedFileSize extends FormattedValue {
|
|
20
|
+
static readonly tagName: string;
|
|
21
|
+
static get observedAttributes(): string[];
|
|
22
|
+
/** The size in bytes. */
|
|
23
|
+
get value(): string;
|
|
24
|
+
set value(next: string);
|
|
25
|
+
/** `decimal` (default) or `binary`. */
|
|
26
|
+
get units(): string;
|
|
27
|
+
set units(next: string);
|
|
28
|
+
protected formatted(): {
|
|
29
|
+
text: string;
|
|
30
|
+
} | null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { FormattedValue } from "./formatted-value.js";
|
|
2
|
+
import { parseNumber, resolveByteUnits, resolveFileSizeParts, } from "../../foundations/format/index.js";
|
|
3
|
+
const DEFAULT_TAG_NAME = "box-formatted-file-size";
|
|
4
|
+
/**
|
|
5
|
+
* A byte count, rendered as a readable size.
|
|
6
|
+
*
|
|
7
|
+
* ```html
|
|
8
|
+
* <box-formatted-file-size value="2517630"></box-formatted-file-size>
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* The magnitude goes through `Intl.NumberFormat`, so a German reader sees
|
|
12
|
+
* "2,52 MB" rather than "2.52 MB". That is the reason this exists as a
|
|
13
|
+
* component rather than a `toFixed` call in each consumer: the decimal
|
|
14
|
+
* separator is not the same everywhere, and a file size is one of the most
|
|
15
|
+
* frequently rendered values in a content platform.
|
|
16
|
+
*
|
|
17
|
+
* Defaults to decimal units (powers of 1000, `kB`/`MB`), matching what the Box
|
|
18
|
+
* product reports. `units="binary"` switches to powers of 1024 with IEC names
|
|
19
|
+
* (`KiB`/`MiB`) for hosts that need to agree with a filesystem instead.
|
|
20
|
+
*/
|
|
21
|
+
export class FormattedFileSize extends FormattedValue {
|
|
22
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
23
|
+
static get observedAttributes() {
|
|
24
|
+
return ["locale", "maximum-fraction-digits", "units", "value"];
|
|
25
|
+
}
|
|
26
|
+
/** The size in bytes. */
|
|
27
|
+
get value() {
|
|
28
|
+
return this.getAttribute("value") ?? "";
|
|
29
|
+
}
|
|
30
|
+
set value(next) {
|
|
31
|
+
this.setAttribute("value", next);
|
|
32
|
+
}
|
|
33
|
+
/** `decimal` (default) or `binary`. */
|
|
34
|
+
get units() {
|
|
35
|
+
return resolveByteUnits(this.getAttribute("units"));
|
|
36
|
+
}
|
|
37
|
+
set units(next) {
|
|
38
|
+
this.setAttribute("units", next);
|
|
39
|
+
}
|
|
40
|
+
formatted() {
|
|
41
|
+
const bytes = parseNumber(this.value);
|
|
42
|
+
if (bytes === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const { suffix, value } = resolveFileSizeParts(bytes, resolveByteUnits(this.units));
|
|
46
|
+
const requested = parseNumber(this.getAttribute("maximum-fraction-digits"));
|
|
47
|
+
// Whole bytes have no fractional part worth showing; above that, one
|
|
48
|
+
// decimal is the convention every file browser uses — "2.5 MB", not
|
|
49
|
+
// "2.517 MB", which is precision no reader asked for.
|
|
50
|
+
const maximumFractionDigits = requested ?? (suffix === "B" ? 0 : 1);
|
|
51
|
+
try {
|
|
52
|
+
return {
|
|
53
|
+
text: `${new Intl.NumberFormat(this.locale, { maximumFractionDigits }).format(value)} ${suffix}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { text: `${String(value)} ${suffix}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
FormattedFileSize.register();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FormattedValue } from "./formatted-value.js";
|
|
2
|
+
/**
|
|
3
|
+
* A number, rendered in the reader's locale.
|
|
4
|
+
*
|
|
5
|
+
* ```html
|
|
6
|
+
* <box-formatted-number value="1234.5" format-style="currency" currency="USD"></box-formatted-number>
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* The attribute is `format-style` rather than `style`, because `style` is a
|
|
10
|
+
* global HTML attribute: setting it would put CSS on the host and be silently
|
|
11
|
+
* ignored as a formatting instruction. Naming it after the `Intl` option would
|
|
12
|
+
* be tidier right up until the first host wrote `style="currency"` and watched
|
|
13
|
+
* nothing happen.
|
|
14
|
+
*
|
|
15
|
+
* `percent` follows `Intl`, which multiplies by 100: pass `0.42` to render
|
|
16
|
+
* "42%". That surprises people, so it is worth stating rather than quietly
|
|
17
|
+
* doubling the convention.
|
|
18
|
+
*/
|
|
19
|
+
export declare class FormattedNumber extends FormattedValue {
|
|
20
|
+
static readonly tagName: string;
|
|
21
|
+
static get observedAttributes(): string[];
|
|
22
|
+
/** The number to render. */
|
|
23
|
+
get value(): string;
|
|
24
|
+
set value(next: string);
|
|
25
|
+
/** `decimal` (default), `currency`, `percent` or `unit`. */
|
|
26
|
+
get formatStyle(): string;
|
|
27
|
+
set formatStyle(next: string);
|
|
28
|
+
/** ISO 4217 code, required by `Intl` when the style is `currency`. */
|
|
29
|
+
get currency(): string;
|
|
30
|
+
set currency(next: string);
|
|
31
|
+
/**
|
|
32
|
+
* A sanctioned unit identifier, e.g. `megabyte`, when the style is `unit`.
|
|
33
|
+
*
|
|
34
|
+
* `Intl` accepts a closed list and throws on anything else, so this is not a
|
|
35
|
+
* free-text label — "widgets" will not work, and the component falls back to
|
|
36
|
+
* a plain number rather than losing the value over it.
|
|
37
|
+
*/
|
|
38
|
+
get unit(): string;
|
|
39
|
+
set unit(next: string);
|
|
40
|
+
/**
|
|
41
|
+
* How wide to write the unit: `short` (2.5 MB), `narrow` (2.5MB) or `long`
|
|
42
|
+
* (2.5 megabytes).
|
|
43
|
+
*
|
|
44
|
+
* `Intl` defaults to `short`, which is what a table wants. `long` is for
|
|
45
|
+
* prose, where an abbreviation a reader has to expand in their head is worse
|
|
46
|
+
* than the words.
|
|
47
|
+
*/
|
|
48
|
+
get unitDisplay(): string;
|
|
49
|
+
set unitDisplay(next: string);
|
|
50
|
+
protected formatted(): {
|
|
51
|
+
text: string;
|
|
52
|
+
} | null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { FormattedValue } from "./formatted-value.js";
|
|
2
|
+
import { parseNumber } from "../../foundations/format/index.js";
|
|
3
|
+
const DEFAULT_TAG_NAME = "box-formatted-number";
|
|
4
|
+
const NUMBER_STYLES = new Set(["decimal", "currency", "percent", "unit"]);
|
|
5
|
+
const UNIT_DISPLAYS = new Set(["short", "narrow", "long"]);
|
|
6
|
+
/**
|
|
7
|
+
* A number, rendered in the reader's locale.
|
|
8
|
+
*
|
|
9
|
+
* ```html
|
|
10
|
+
* <box-formatted-number value="1234.5" format-style="currency" currency="USD"></box-formatted-number>
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* The attribute is `format-style` rather than `style`, because `style` is a
|
|
14
|
+
* global HTML attribute: setting it would put CSS on the host and be silently
|
|
15
|
+
* ignored as a formatting instruction. Naming it after the `Intl` option would
|
|
16
|
+
* be tidier right up until the first host wrote `style="currency"` and watched
|
|
17
|
+
* nothing happen.
|
|
18
|
+
*
|
|
19
|
+
* `percent` follows `Intl`, which multiplies by 100: pass `0.42` to render
|
|
20
|
+
* "42%". That surprises people, so it is worth stating rather than quietly
|
|
21
|
+
* doubling the convention.
|
|
22
|
+
*/
|
|
23
|
+
export class FormattedNumber extends FormattedValue {
|
|
24
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
25
|
+
static get observedAttributes() {
|
|
26
|
+
return [
|
|
27
|
+
"currency",
|
|
28
|
+
"format-style",
|
|
29
|
+
"locale",
|
|
30
|
+
"maximum-fraction-digits",
|
|
31
|
+
"minimum-fraction-digits",
|
|
32
|
+
"unit",
|
|
33
|
+
"unit-display",
|
|
34
|
+
"value",
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
/** The number to render. */
|
|
38
|
+
get value() {
|
|
39
|
+
return this.getAttribute("value") ?? "";
|
|
40
|
+
}
|
|
41
|
+
set value(next) {
|
|
42
|
+
this.setAttribute("value", next);
|
|
43
|
+
}
|
|
44
|
+
/** `decimal` (default), `currency`, `percent` or `unit`. */
|
|
45
|
+
get formatStyle() {
|
|
46
|
+
const raw = this.getAttribute("format-style");
|
|
47
|
+
return raw && NUMBER_STYLES.has(raw) ? raw : "decimal";
|
|
48
|
+
}
|
|
49
|
+
set formatStyle(next) {
|
|
50
|
+
this.setAttribute("format-style", next);
|
|
51
|
+
}
|
|
52
|
+
/** ISO 4217 code, required by `Intl` when the style is `currency`. */
|
|
53
|
+
get currency() {
|
|
54
|
+
return this.getAttribute("currency") ?? "";
|
|
55
|
+
}
|
|
56
|
+
set currency(next) {
|
|
57
|
+
this.setAttribute("currency", next);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A sanctioned unit identifier, e.g. `megabyte`, when the style is `unit`.
|
|
61
|
+
*
|
|
62
|
+
* `Intl` accepts a closed list and throws on anything else, so this is not a
|
|
63
|
+
* free-text label — "widgets" will not work, and the component falls back to
|
|
64
|
+
* a plain number rather than losing the value over it.
|
|
65
|
+
*/
|
|
66
|
+
get unit() {
|
|
67
|
+
return this.getAttribute("unit") ?? "";
|
|
68
|
+
}
|
|
69
|
+
set unit(next) {
|
|
70
|
+
this.setAttribute("unit", next);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* How wide to write the unit: `short` (2.5 MB), `narrow` (2.5MB) or `long`
|
|
74
|
+
* (2.5 megabytes).
|
|
75
|
+
*
|
|
76
|
+
* `Intl` defaults to `short`, which is what a table wants. `long` is for
|
|
77
|
+
* prose, where an abbreviation a reader has to expand in their head is worse
|
|
78
|
+
* than the words.
|
|
79
|
+
*/
|
|
80
|
+
get unitDisplay() {
|
|
81
|
+
const raw = this.getAttribute("unit-display");
|
|
82
|
+
return raw && UNIT_DISPLAYS.has(raw) ? raw : "short";
|
|
83
|
+
}
|
|
84
|
+
set unitDisplay(next) {
|
|
85
|
+
this.setAttribute("unit-display", next);
|
|
86
|
+
}
|
|
87
|
+
formatted() {
|
|
88
|
+
const numeric = parseNumber(this.value);
|
|
89
|
+
if (numeric === null) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const options = { style: this.formatStyle };
|
|
93
|
+
if (this.formatStyle === "currency") {
|
|
94
|
+
if (!this.currency) {
|
|
95
|
+
// Intl throws without one. Falling back to a plain decimal keeps a real
|
|
96
|
+
// number on screen instead of hiding it over a missing attribute.
|
|
97
|
+
options.style = "decimal";
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
options.currency = this.currency;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (this.formatStyle === "unit") {
|
|
104
|
+
if (!this.unit) {
|
|
105
|
+
options.style = "decimal";
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
options.unit = this.unit;
|
|
109
|
+
options.unitDisplay = this.unitDisplay;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const min = parseNumber(this.getAttribute("minimum-fraction-digits"));
|
|
113
|
+
const max = parseNumber(this.getAttribute("maximum-fraction-digits"));
|
|
114
|
+
if (min !== null) {
|
|
115
|
+
options.minimumFractionDigits = min;
|
|
116
|
+
}
|
|
117
|
+
if (max !== null) {
|
|
118
|
+
options.maximumFractionDigits = max;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return { text: new Intl.NumberFormat(this.locale, options).format(numeric) };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// A bad currency code, unit, or digit range throws. The number is real,
|
|
125
|
+
// so render it plainly rather than losing it.
|
|
126
|
+
return { text: String(numeric) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
FormattedNumber.register();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared behaviour for the read-only `box-formatted-*` renderers.
|
|
4
|
+
*
|
|
5
|
+
* These components render a typed value and nothing else: no interaction, no
|
|
6
|
+
* state, no events. What they do share is the awkward part — resolving a
|
|
7
|
+
* locale, deciding what a broken value looks like, and keeping a
|
|
8
|
+
* machine-readable form beside the human one.
|
|
9
|
+
*
|
|
10
|
+
* **A value that cannot be formatted hides the element.** Not `Invalid Date`,
|
|
11
|
+
* not `NaN`, not the raw string echoed back. A malformed value is a host bug,
|
|
12
|
+
* and rendering its wreckage puts the bug in front of a reader who can do
|
|
13
|
+
* nothing about it. The host is left with an empty inline box, which is what a
|
|
14
|
+
* missing value should look like anyway.
|
|
15
|
+
*/
|
|
16
|
+
export declare abstract class FormattedValue extends BaseElement {
|
|
17
|
+
protected valueEl: HTMLElement;
|
|
18
|
+
/**
|
|
19
|
+
* The formatted text, plus an optional machine-readable form for the
|
|
20
|
+
* `datetime` attribute. `null` means the value could not be formatted.
|
|
21
|
+
*/
|
|
22
|
+
protected abstract formatted(): {
|
|
23
|
+
machine?: string;
|
|
24
|
+
text: string;
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to render into `<time>` rather than `<span>`.
|
|
28
|
+
*
|
|
29
|
+
* Only the instant-valued renderers should; `<time>` on a file size would be
|
|
30
|
+
* a lie to anything reading the document semantically.
|
|
31
|
+
*/
|
|
32
|
+
protected usesTimeElement(): boolean;
|
|
33
|
+
/** Extra styles a subclass wants appended. */
|
|
34
|
+
protected valueStyles(): string;
|
|
35
|
+
/**
|
|
36
|
+
* The locale to format in. Absent means the host's own — deliberately
|
|
37
|
+
* `undefined` rather than a hardcoded tag.
|
|
38
|
+
*/
|
|
39
|
+
get locale(): string | undefined;
|
|
40
|
+
set locale(value: string | undefined);
|
|
41
|
+
protected renderTemplate(): void;
|
|
42
|
+
protected setupListeners(): void;
|
|
43
|
+
protected update(): void;
|
|
44
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { resolveLocale } from "../../foundations/format/index.js";
|
|
3
|
+
const valueStyles = `
|
|
4
|
+
:host {
|
|
5
|
+
display: inline;
|
|
6
|
+
color: inherit;
|
|
7
|
+
font: inherit;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
:host([hidden]) {
|
|
11
|
+
display: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Numbers and dates are scanned in columns far more often than they are read
|
|
15
|
+
in prose — a modified date beside a file name, a size in a table. Tabular
|
|
16
|
+
figures keep those columns aligned without the host having to ask. */
|
|
17
|
+
:host([tabular]) [part="value"] {
|
|
18
|
+
font-variant-numeric: tabular-nums;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[part="value"] {
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
/**
|
|
26
|
+
* Shared behaviour for the read-only `box-formatted-*` renderers.
|
|
27
|
+
*
|
|
28
|
+
* These components render a typed value and nothing else: no interaction, no
|
|
29
|
+
* state, no events. What they do share is the awkward part — resolving a
|
|
30
|
+
* locale, deciding what a broken value looks like, and keeping a
|
|
31
|
+
* machine-readable form beside the human one.
|
|
32
|
+
*
|
|
33
|
+
* **A value that cannot be formatted hides the element.** Not `Invalid Date`,
|
|
34
|
+
* not `NaN`, not the raw string echoed back. A malformed value is a host bug,
|
|
35
|
+
* and rendering its wreckage puts the bug in front of a reader who can do
|
|
36
|
+
* nothing about it. The host is left with an empty inline box, which is what a
|
|
37
|
+
* missing value should look like anyway.
|
|
38
|
+
*/
|
|
39
|
+
export class FormattedValue extends BaseElement {
|
|
40
|
+
valueEl;
|
|
41
|
+
/**
|
|
42
|
+
* Whether to render into `<time>` rather than `<span>`.
|
|
43
|
+
*
|
|
44
|
+
* Only the instant-valued renderers should; `<time>` on a file size would be
|
|
45
|
+
* a lie to anything reading the document semantically.
|
|
46
|
+
*/
|
|
47
|
+
usesTimeElement() {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/** Extra styles a subclass wants appended. */
|
|
51
|
+
valueStyles() {
|
|
52
|
+
return valueStyles;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The locale to format in. Absent means the host's own — deliberately
|
|
56
|
+
* `undefined` rather than a hardcoded tag.
|
|
57
|
+
*/
|
|
58
|
+
get locale() {
|
|
59
|
+
return resolveLocale(this.getAttribute("locale"));
|
|
60
|
+
}
|
|
61
|
+
set locale(value) {
|
|
62
|
+
if (!value) {
|
|
63
|
+
this.removeAttribute("locale");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.setAttribute("locale", value);
|
|
67
|
+
}
|
|
68
|
+
renderTemplate() {
|
|
69
|
+
if (!this.shadowRoot) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const tag = this.usesTimeElement() ? "time" : "span";
|
|
73
|
+
this.shadowRoot.innerHTML = `<style>${this.valueStyles()}</style><${tag} part="value"></${tag}>`;
|
|
74
|
+
this.valueEl = this.shadowRoot.querySelector('[part="value"]');
|
|
75
|
+
}
|
|
76
|
+
setupListeners() {
|
|
77
|
+
// Output only: nothing to listen to.
|
|
78
|
+
}
|
|
79
|
+
update() {
|
|
80
|
+
if (!this.valueEl) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const result = this.formatted();
|
|
84
|
+
// toggleAttribute rather than `.hidden = `, so a host that set `hidden`
|
|
85
|
+
// itself is not fought with on every update.
|
|
86
|
+
this.toggleAttribute("hidden", result === null);
|
|
87
|
+
if (!result) {
|
|
88
|
+
this.valueEl.textContent = "";
|
|
89
|
+
this.valueEl.removeAttribute("datetime");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.valueEl.textContent = result.text;
|
|
93
|
+
if (result.machine) {
|
|
94
|
+
this.valueEl.setAttribute("datetime", result.machine);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.valueEl.removeAttribute("datetime");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Read-only renderers for typed values: dates, relative durations, numbers and
|
|
2
|
+
// file sizes, all locale-aware and all hiding themselves when handed something
|
|
3
|
+
// they cannot format.
|
|
4
|
+
export * from "./formatted-value.js";
|
|
5
|
+
export * from "./code-block.js";
|
|
6
|
+
export * from "./formatted-date.js";
|
|
7
|
+
export * from "./relative-time.js";
|
|
8
|
+
export * from "./formatted-number.js";
|
|
9
|
+
export * from "./formatted-file-size.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FormattedValue } from "./formatted-value.js";
|
|
2
|
+
/**
|
|
3
|
+
* How long ago something happened, in the reader's locale.
|
|
4
|
+
*
|
|
5
|
+
* ```html
|
|
6
|
+
* <box-relative-time value="2026-08-24T09:00:00Z"></box-relative-time>
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* Renders into `<time datetime="…">` carrying the exact instant, because
|
|
10
|
+
* "4 days ago" is an approximation and the precise value is what a reader
|
|
11
|
+
* reaches for when the approximation is not enough. `box-due-badge` makes the
|
|
12
|
+
* same trade for the same reason.
|
|
13
|
+
*
|
|
14
|
+
* `reference-time` pins what "now" means. It exists for the same reason
|
|
15
|
+
* `box-due-badge` has one: a component whose output depends on the wall clock
|
|
16
|
+
* cannot be tested or screenshotted deterministically, and a host rendering a
|
|
17
|
+
* list wants every row measured against one instant rather than each against
|
|
18
|
+
* the moment it happened to render.
|
|
19
|
+
*/
|
|
20
|
+
export declare class RelativeTime extends FormattedValue {
|
|
21
|
+
static readonly tagName: string;
|
|
22
|
+
static get observedAttributes(): string[];
|
|
23
|
+
protected usesTimeElement(): boolean;
|
|
24
|
+
/** The instant being described, as ISO 8601 or epoch milliseconds. */
|
|
25
|
+
get value(): string;
|
|
26
|
+
set value(next: string);
|
|
27
|
+
/** What "now" means. Absent uses the current time. */
|
|
28
|
+
get referenceTime(): string;
|
|
29
|
+
set referenceTime(next: string);
|
|
30
|
+
/**
|
|
31
|
+
* `auto` lets Intl say "yesterday" where the language has a word for it;
|
|
32
|
+
* `always` forces "1 day ago". Defaults to `auto`, because the idiomatic
|
|
33
|
+
* form is what a reader expects.
|
|
34
|
+
*/
|
|
35
|
+
get numeric(): string;
|
|
36
|
+
set numeric(next: string);
|
|
37
|
+
protected formatted(): {
|
|
38
|
+
machine: string;
|
|
39
|
+
text: string;
|
|
40
|
+
} | null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { FormattedValue } from "./formatted-value.js";
|
|
2
|
+
import { parseInstant, resolveRelativeParts } from "../../foundations/format/index.js";
|
|
3
|
+
const DEFAULT_TAG_NAME = "box-relative-time";
|
|
4
|
+
const NUMERIC_MODES = new Set(["always", "auto"]);
|
|
5
|
+
/**
|
|
6
|
+
* How long ago something happened, in the reader's locale.
|
|
7
|
+
*
|
|
8
|
+
* ```html
|
|
9
|
+
* <box-relative-time value="2026-08-24T09:00:00Z"></box-relative-time>
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Renders into `<time datetime="…">` carrying the exact instant, because
|
|
13
|
+
* "4 days ago" is an approximation and the precise value is what a reader
|
|
14
|
+
* reaches for when the approximation is not enough. `box-due-badge` makes the
|
|
15
|
+
* same trade for the same reason.
|
|
16
|
+
*
|
|
17
|
+
* `reference-time` pins what "now" means. It exists for the same reason
|
|
18
|
+
* `box-due-badge` has one: a component whose output depends on the wall clock
|
|
19
|
+
* cannot be tested or screenshotted deterministically, and a host rendering a
|
|
20
|
+
* list wants every row measured against one instant rather than each against
|
|
21
|
+
* the moment it happened to render.
|
|
22
|
+
*/
|
|
23
|
+
export class RelativeTime extends FormattedValue {
|
|
24
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
25
|
+
static get observedAttributes() {
|
|
26
|
+
return ["locale", "numeric", "reference-time", "value"];
|
|
27
|
+
}
|
|
28
|
+
usesTimeElement() {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/** The instant being described, as ISO 8601 or epoch milliseconds. */
|
|
32
|
+
get value() {
|
|
33
|
+
return this.getAttribute("value") ?? "";
|
|
34
|
+
}
|
|
35
|
+
set value(next) {
|
|
36
|
+
this.setAttribute("value", next);
|
|
37
|
+
}
|
|
38
|
+
/** What "now" means. Absent uses the current time. */
|
|
39
|
+
get referenceTime() {
|
|
40
|
+
return this.getAttribute("reference-time") ?? "";
|
|
41
|
+
}
|
|
42
|
+
set referenceTime(next) {
|
|
43
|
+
if (!next) {
|
|
44
|
+
this.removeAttribute("reference-time");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.setAttribute("reference-time", next);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `auto` lets Intl say "yesterday" where the language has a word for it;
|
|
51
|
+
* `always` forces "1 day ago". Defaults to `auto`, because the idiomatic
|
|
52
|
+
* form is what a reader expects.
|
|
53
|
+
*/
|
|
54
|
+
get numeric() {
|
|
55
|
+
const raw = this.getAttribute("numeric");
|
|
56
|
+
return raw && NUMERIC_MODES.has(raw) ? raw : "auto";
|
|
57
|
+
}
|
|
58
|
+
set numeric(next) {
|
|
59
|
+
this.setAttribute("numeric", next);
|
|
60
|
+
}
|
|
61
|
+
formatted() {
|
|
62
|
+
const instant = parseInstant(this.value);
|
|
63
|
+
if (!instant) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// An unparseable reference falls back to now rather than hiding the
|
|
67
|
+
// element: the instant being described is still valid, and a broken
|
|
68
|
+
// reference should not erase it.
|
|
69
|
+
const reference = parseInstant(this.referenceTime) ?? new Date();
|
|
70
|
+
const { unit, value } = resolveRelativeParts(instant, reference);
|
|
71
|
+
try {
|
|
72
|
+
const formatter = new Intl.RelativeTimeFormat(this.locale, {
|
|
73
|
+
numeric: this.numeric,
|
|
74
|
+
});
|
|
75
|
+
return { machine: instant.toISOString(), text: formatter.format(value, unit) };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return { machine: instant.toISOString(), text: instant.toISOString() };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
RelativeTime.register();
|
package/dist/element-maps.d.ts
CHANGED
|
@@ -37,6 +37,13 @@ import type { ColorPicker } from "./components/forms/color-picker.js";
|
|
|
37
37
|
import type { Combobox } from "./components/forms/combobox.js";
|
|
38
38
|
import type { CommandPalette } from "./components/overlays/command-palette.js";
|
|
39
39
|
import type { CommentThread } from "./patterns/comments/comment-thread.js";
|
|
40
|
+
import type { CodeBlock } from "./components/output/code-block.js";
|
|
41
|
+
import type { FormattedDate } from "./components/output/formatted-date.js";
|
|
42
|
+
import type { Indicator } from "./components/feedback/indicator.js";
|
|
43
|
+
import type { TileGroup } from "./components/forms/tile-group.js";
|
|
44
|
+
import type { FormattedFileSize } from "./components/output/formatted-file-size.js";
|
|
45
|
+
import type { FormattedNumber } from "./components/output/formatted-number.js";
|
|
46
|
+
import type { RelativeTime } from "./components/output/relative-time.js";
|
|
40
47
|
import type { CompareView } from "./patterns/diff/compare-view.js";
|
|
41
48
|
import type { ContactDatalistItem } from "./components/identity/contact-datalist-item.js";
|
|
42
49
|
import type { ContentExplorer } from "./patterns/content-explorer/content-explorer.js";
|
|
@@ -180,6 +187,13 @@ declare global {
|
|
|
180
187
|
"box-combobox": Combobox;
|
|
181
188
|
"box-command-palette": CommandPalette;
|
|
182
189
|
"box-comment-thread": CommentThread;
|
|
190
|
+
"box-code-block": CodeBlock;
|
|
191
|
+
"box-formatted-date": FormattedDate;
|
|
192
|
+
"box-indicator": Indicator;
|
|
193
|
+
"box-tile-group": TileGroup;
|
|
194
|
+
"box-formatted-file-size": FormattedFileSize;
|
|
195
|
+
"box-formatted-number": FormattedNumber;
|
|
196
|
+
"box-relative-time": RelativeTime;
|
|
183
197
|
"box-compare-view": CompareView;
|
|
184
198
|
"box-contact-datalist-item": ContactDatalistItem;
|
|
185
199
|
"box-content-explorer": ContentExplorer;
|
|
@@ -293,4 +307,4 @@ declare global {
|
|
|
293
307
|
}
|
|
294
308
|
}
|
|
295
309
|
/** Every element tag the library registers, for host-side narrowing. */
|
|
296
|
-
export type BoxElementTagName = "box-access-stats" | "box-accordion" | "box-activity-density" | "box-agent-chat" | "box-alert" | "box-annotation-inspector" | "box-annotation-thread" | "box-annotation-toolbar" | "box-app-shell" | "box-audit-log" | "box-avatar" | "box-badge" | "box-badgeable" | "box-bar-chart" | "box-breadcrumb" | "box-bulk-action-bar" | "box-button" | "box-button-group" | "box-calendar" | "box-card" | "box-carousel" | "box-category-selector" | "box-chart-panel" | "box-checkbox" | "box-checkbox-group" | "box-chip" | "box-collaborator-avatars" | "box-color-picker" | "box-combobox" | "box-command-palette" | "box-comment-thread" | "box-compare-view" | "box-contact-datalist-item" | "box-content-explorer" | "box-content-picker" | "box-content-sidebar" | "box-content-uploader" | "box-context-menu" | "box-datalist-item" | "box-date-field" | "box-dialog" | "box-diff-viewer" | "box-divider" | "box-grid" | "box-donut-chart" | "box-draggable-list" | "box-drawer" | "box-drop-zone" | "box-dropdown" | "box-dual-listbox" | "box-due-badge" | "box-empty-state" | "box-error-mask" | "box-explorer-action-menu" | "box-explorer-breadcrumbs" | "box-explorer-items" | "box-explorer-list" | "box-explorer-table" | "box-explorer-toolbar" | "box-fieldset" | "box-file-request-builder" | "box-filter-bar" | "box-form-wizard" | "box-governance-panel" | "box-grid-view" | "box-guide-tooltip" | "box-help-text" | "box-icon-button" | "box-illustration" | "box-invite-collaborators-modal" | "box-item-details-panel" | "box-item-form" | "box-line-chart" | "box-lineage-graph" | "box-link-button" | "box-menu" | "box-menu-item" | "box-metadata-filter-builder" | "box-metadata-inspector" | "box-metric-card" | "box-multi-select" | "box-nav-sidebar" | "box-notification-bell" | "box-notification-inbox" | "box-nudge" | "box-number-input" | "box-pagination" | "box-permission-matrix" | "box-persona" | "box-pill-cloud" | "box-pill-selector-dropdown" | "box-popover" | "box-presence" | "box-preview-element" | "box-preview-header" | "box-progress-bar" | "box-progress-ring" | "box-progress-steps" | "box-provenance-strip" | "box-radio-group" | "box-range-slider" | "box-rating" | "box-review-queue-item" | "box-rich-text-input" | "box-run-trace" | "box-saved-view-picker" | "box-search-field" | "box-search-results-header" | "box-section" | "box-segmented-control" | "box-select" | "box-share-panel" | "box-shortcuts-overlay" | "box-sidebar-toggle-button" | "box-signature-ceremony" | "box-skeleton" | "box-slider" | "box-spin-button" | "box-spinner" | "box-split-view" | "box-path" | "box-switch" | "box-table" | "box-tabs" | "box-tag-input" | "box-task-assignment-panel" | "box-text-area" | "box-text-field" | "box-thumbnail-card" | "box-time-field" | "box-timeline" | "box-toast" | "box-tooltip" | "box-tree" | "box-tree-grid" | "box-unified-share-modal" | "box-version-graph" | "box-version-list" | "box-wizard-summary" | "box-work-queue" | "box-workload-board";
|
|
310
|
+
export type BoxElementTagName = "box-access-stats" | "box-accordion" | "box-activity-density" | "box-agent-chat" | "box-alert" | "box-annotation-inspector" | "box-annotation-thread" | "box-annotation-toolbar" | "box-app-shell" | "box-audit-log" | "box-avatar" | "box-badge" | "box-badgeable" | "box-bar-chart" | "box-breadcrumb" | "box-bulk-action-bar" | "box-button" | "box-button-group" | "box-calendar" | "box-card" | "box-carousel" | "box-category-selector" | "box-chart-panel" | "box-checkbox" | "box-checkbox-group" | "box-chip" | "box-collaborator-avatars" | "box-color-picker" | "box-combobox" | "box-command-palette" | "box-comment-thread" | "box-code-block" | "box-formatted-date" | "box-indicator" | "box-tile-group" | "box-formatted-file-size" | "box-formatted-number" | "box-relative-time" | "box-compare-view" | "box-contact-datalist-item" | "box-content-explorer" | "box-content-picker" | "box-content-sidebar" | "box-content-uploader" | "box-context-menu" | "box-datalist-item" | "box-date-field" | "box-dialog" | "box-diff-viewer" | "box-divider" | "box-grid" | "box-donut-chart" | "box-draggable-list" | "box-drawer" | "box-drop-zone" | "box-dropdown" | "box-dual-listbox" | "box-due-badge" | "box-empty-state" | "box-error-mask" | "box-explorer-action-menu" | "box-explorer-breadcrumbs" | "box-explorer-items" | "box-explorer-list" | "box-explorer-table" | "box-explorer-toolbar" | "box-fieldset" | "box-file-request-builder" | "box-filter-bar" | "box-form-wizard" | "box-governance-panel" | "box-grid-view" | "box-guide-tooltip" | "box-help-text" | "box-icon-button" | "box-illustration" | "box-invite-collaborators-modal" | "box-item-details-panel" | "box-item-form" | "box-line-chart" | "box-lineage-graph" | "box-link-button" | "box-menu" | "box-menu-item" | "box-metadata-filter-builder" | "box-metadata-inspector" | "box-metric-card" | "box-multi-select" | "box-nav-sidebar" | "box-notification-bell" | "box-notification-inbox" | "box-nudge" | "box-number-input" | "box-pagination" | "box-permission-matrix" | "box-persona" | "box-pill-cloud" | "box-pill-selector-dropdown" | "box-popover" | "box-presence" | "box-preview-element" | "box-preview-header" | "box-progress-bar" | "box-progress-ring" | "box-progress-steps" | "box-provenance-strip" | "box-radio-group" | "box-range-slider" | "box-rating" | "box-review-queue-item" | "box-rich-text-input" | "box-run-trace" | "box-saved-view-picker" | "box-search-field" | "box-search-results-header" | "box-section" | "box-segmented-control" | "box-select" | "box-share-panel" | "box-shortcuts-overlay" | "box-sidebar-toggle-button" | "box-signature-ceremony" | "box-skeleton" | "box-slider" | "box-spin-button" | "box-spinner" | "box-split-view" | "box-path" | "box-switch" | "box-table" | "box-tabs" | "box-tag-input" | "box-task-assignment-panel" | "box-text-area" | "box-text-field" | "box-thumbnail-card" | "box-time-field" | "box-timeline" | "box-toast" | "box-tooltip" | "box-tree" | "box-tree-grid" | "box-unified-share-modal" | "box-version-graph" | "box-version-list" | "box-wizard-summary" | "box-work-queue" | "box-workload-board";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/code-block.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/code-block.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/formatted-date.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/formatted-date.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/formatted-file-size.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/formatted-file-size.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/formatted-number.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/formatted-number.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/feedback/indicator.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/feedback/indicator.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/relative-time.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/output/relative-time.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/forms/tile-group.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/forms/tile-group.js";
|