@unofficialbox/box-open-elements 0.14.0 → 0.16.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.
@@ -0,0 +1,39 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /**
3
+ * A row of independent controls, with the keyboard behaviour a toolbar owes.
4
+ *
5
+ * ```html
6
+ * <box-toolbar label="Document actions">
7
+ * <button type="button">Share</button>
8
+ * <button type="button">Download</button>
9
+ * </box-toolbar>
10
+ * ```
11
+ *
12
+ * The controls are yours — this contributes `role="toolbar"`, the accessible
13
+ * name, and roving tabindex, so the group is one tab stop and the arrow keys
14
+ * move within it.
15
+ *
16
+ * Not to be confused with `box-button-group`, which is a `radiogroup`: that one
17
+ * is for picking exactly one of a set, and its children answer to a shared
18
+ * value. A toolbar's controls are independent of each other.
19
+ */
20
+ export declare class Toolbar extends BaseElement {
21
+ static readonly tagName: string;
22
+ static get observedAttributes(): string[];
23
+ private listenersBound;
24
+ private controlObserver;
25
+ /** Accessible name for the group, announced before its controls. */
26
+ get label(): string;
27
+ set label(next: string);
28
+ /** `horizontal` (default) or `vertical`; picks which arrow keys navigate. */
29
+ get orientation(): string;
30
+ set orientation(next: string);
31
+ /** The controls this toolbar is managing, in document order. */
32
+ get controls(): HTMLElement[];
33
+ /** Every focusable control, disabled ones included. */
34
+ private focusableElements;
35
+ disconnectedCallback(): void;
36
+ protected renderTemplate(): void;
37
+ protected setupListeners(): void;
38
+ protected update(): void;
39
+ }
@@ -0,0 +1,170 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { applyRovingTabindex, handleRovingKeydown } from "../../foundations/a11y/index.js";
3
+ import { boePanel } from "../../foundations/geometry/index.js";
4
+ const DEFAULT_TAG_NAME = "box-toolbar";
5
+ const ORIENTATIONS = new Set(["horizontal", "vertical"]);
6
+ /**
7
+ * Controls a toolbar will manage. Deliberately the natively focusable ones:
8
+ * roving tabindex works by moving `tabindex` between elements, which only
9
+ * focuses what the browser already considers focusable. A custom element host
10
+ * is not focusable unless it says so, so wrapping one in a toolbar requires it
11
+ * to carry its own `tabindex`.
12
+ */
13
+ const FOCUSABLE = "button, a[href], input, select, textarea, [tabindex]";
14
+ const toolbarStyles = `
15
+ :host {
16
+ display: block;
17
+ color: inherit;
18
+ font: inherit;
19
+ }
20
+
21
+ [part="toolbar"] {
22
+ display: flex;
23
+ flex-wrap: wrap;
24
+ align-items: center;
25
+ gap: ${boePanel.gap};
26
+ }
27
+
28
+ :host([orientation="vertical"]) [part="toolbar"] {
29
+ flex-direction: column;
30
+ align-items: stretch;
31
+ flex-wrap: nowrap;
32
+ }
33
+ `;
34
+ /**
35
+ * A row of independent controls, with the keyboard behaviour a toolbar owes.
36
+ *
37
+ * ```html
38
+ * <box-toolbar label="Document actions">
39
+ * <button type="button">Share</button>
40
+ * <button type="button">Download</button>
41
+ * </box-toolbar>
42
+ * ```
43
+ *
44
+ * The controls are yours — this contributes `role="toolbar"`, the accessible
45
+ * name, and roving tabindex, so the group is one tab stop and the arrow keys
46
+ * move within it.
47
+ *
48
+ * Not to be confused with `box-button-group`, which is a `radiogroup`: that one
49
+ * is for picking exactly one of a set, and its children answer to a shared
50
+ * value. A toolbar's controls are independent of each other.
51
+ */
52
+ export class Toolbar extends BaseElement {
53
+ static tagName = DEFAULT_TAG_NAME;
54
+ static get observedAttributes() {
55
+ return ["label", "orientation"];
56
+ }
57
+ listenersBound = false;
58
+ controlObserver = null;
59
+ /** Accessible name for the group, announced before its controls. */
60
+ get label() {
61
+ return this.getAttribute("label") ?? "";
62
+ }
63
+ set label(next) {
64
+ this.setAttribute("label", next);
65
+ }
66
+ /** `horizontal` (default) or `vertical`; picks which arrow keys navigate. */
67
+ get orientation() {
68
+ const raw = this.getAttribute("orientation");
69
+ return raw && ORIENTATIONS.has(raw) ? raw : "horizontal";
70
+ }
71
+ set orientation(next) {
72
+ this.setAttribute("orientation", next);
73
+ }
74
+ /** The controls this toolbar is managing, in document order. */
75
+ get controls() {
76
+ return this.focusableElements().filter(element => !element.hasAttribute("disabled"));
77
+ }
78
+ /** Every focusable control, disabled ones included. */
79
+ focusableElements() {
80
+ const slot = this.shadowRoot?.querySelector("slot");
81
+ if (!slot) {
82
+ return [];
83
+ }
84
+ return slot
85
+ .assignedElements()
86
+ .flatMap(element => element.matches(FOCUSABLE)
87
+ ? [element]
88
+ : Array.from(element.querySelectorAll(FOCUSABLE)));
89
+ }
90
+ disconnectedCallback() {
91
+ this.controlObserver?.disconnect();
92
+ this.controlObserver = null;
93
+ this.listenersBound = false;
94
+ }
95
+ renderTemplate() {
96
+ if (!this.shadowRoot) {
97
+ return;
98
+ }
99
+ this.shadowRoot.innerHTML = `
100
+ <style>${toolbarStyles}</style>
101
+ <div part="toolbar" role="toolbar"><slot></slot></div>
102
+ `;
103
+ this.listenersBound = false;
104
+ }
105
+ setupListeners() {
106
+ if (!this.shadowRoot || this.listenersBound) {
107
+ return;
108
+ }
109
+ this.listenersBound = true;
110
+ // A toolbar's controls are the host's children, so the set changes without
111
+ // this element re-rendering. Without this the tab stops describe whatever
112
+ // was there when it first rendered.
113
+ this.shadowRoot.querySelector("slot")?.addEventListener("slotchange", () => {
114
+ this.update();
115
+ });
116
+ // `slotchange` covers assigned children being swapped, but not a control
117
+ // deep in a wrapper, and not `disabled` toggling — the two ways a host
118
+ // routinely changes which controls are live.
119
+ this.controlObserver ??= new MutationObserver(() => {
120
+ this.update();
121
+ });
122
+ this.controlObserver.observe(this, {
123
+ attributeFilter: ["disabled"],
124
+ attributes: true,
125
+ childList: true,
126
+ subtree: true,
127
+ });
128
+ this.shadowRoot.querySelector('[part="toolbar"]')?.addEventListener("keydown", event => {
129
+ handleRovingKeydown(event, this.controls, {
130
+ orientation: this.orientation,
131
+ });
132
+ });
133
+ // Clicking a control makes it the tab stop, so returning by Tab lands where
134
+ // the reader last was rather than back at the first control.
135
+ this.addEventListener("focusin", event => {
136
+ const index = this.controls.indexOf(event.target);
137
+ if (index >= 0) {
138
+ applyRovingTabindex(this.controls, index);
139
+ }
140
+ });
141
+ }
142
+ update() {
143
+ if (!this.shadowRoot) {
144
+ return;
145
+ }
146
+ const toolbar = this.shadowRoot.querySelector('[part="toolbar"]');
147
+ toolbar?.setAttribute("aria-orientation", this.orientation);
148
+ if (this.label) {
149
+ toolbar?.setAttribute("aria-label", this.label);
150
+ }
151
+ else {
152
+ toolbar?.removeAttribute("aria-label");
153
+ }
154
+ const controls = this.controls;
155
+ // Keep whichever control already holds the tab stop, so a re-render for an
156
+ // unrelated reason does not send focus back to the start.
157
+ const active = controls.findIndex(control => control.tabIndex === 0);
158
+ // Disabled controls are excluded from the rotation, so roving tabindex
159
+ // never touches them — and a button's default tabIndex is 0. Left alone,
160
+ // one re-enabled at runtime (which is the norm: a Clear Selection that
161
+ // wakes when something is selected) becomes a second tab stop the arrow
162
+ // keys cannot reach. Claim every focusable control first, then hand the
163
+ // stop to one.
164
+ for (const element of this.focusableElements()) {
165
+ element.tabIndex = -1;
166
+ }
167
+ applyRovingTabindex(controls, active >= 0 ? active : 0);
168
+ }
169
+ }
170
+ Toolbar.register();
@@ -0,0 +1,55 @@
1
+ import { FormattedValue } from "./formatted-value.js";
2
+ /**
3
+ * How long something takes or lasts, in the reader's locale.
4
+ *
5
+ * ```html
6
+ * <box-formatted-duration value="5400"></box-formatted-duration>
7
+ * ```
8
+ *
9
+ * `value` is a count of seconds or an ISO 8601 duration (`PT1H30M`), because
10
+ * hosts have both: an API field is usually a number, while `<time datetime>`
11
+ * wants the ISO form. Whichever comes in, the ISO form goes back out in
12
+ * `datetime`, so the exact quantity survives for anything reading the document
13
+ * rather than looking at it.
14
+ *
15
+ * Not to be confused with `box-relative-time`, which renders *when* something
16
+ * happened relative to now ("3 days ago"). This renders *how long* — a
17
+ * retention period, an SLA, a processing time — and carries no direction.
18
+ *
19
+ * `Intl.DurationFormat` does the rendering where the browser has it. Where it
20
+ * does not, the same output is composed from `Intl.NumberFormat`'s unit style
21
+ * and `Intl.ListFormat`, both long supported; the two paths were compared
22
+ * across styles and locales and agree.
23
+ */
24
+ export declare class FormattedDuration extends FormattedValue {
25
+ static readonly tagName: string;
26
+ static get observedAttributes(): string[];
27
+ /** Seconds, or an ISO 8601 duration such as `PT1H30M`. */
28
+ get value(): string;
29
+ set value(next: string);
30
+ /**
31
+ * `short` (default, `1 hr, 30 min`), `long` (`1 hour, 30 minutes`) or
32
+ * `narrow` (`1h 30m`).
33
+ *
34
+ * Named `format-style` rather than `style` because `style` is a global HTML
35
+ * attribute: naming it after the `Intl` option would put CSS on the host and
36
+ * be silently ignored as a formatting instruction.
37
+ */
38
+ get formatStyle(): string;
39
+ set formatStyle(next: string);
40
+ /**
41
+ * How many units to show, default 2.
42
+ *
43
+ * A duration carries more precision than a reader wants: 1 day, 3 hours, 12
44
+ * minutes and 8 seconds is a number to scan past, not to read. Two units is
45
+ * enough to convey magnitude.
46
+ */
47
+ get maxUnits(): number;
48
+ set maxUnits(next: number);
49
+ protected usesTimeElement(): boolean;
50
+ protected formatted(): {
51
+ machine?: string;
52
+ text: string;
53
+ } | null;
54
+ private render;
55
+ }
@@ -0,0 +1,131 @@
1
+ import { FormattedValue } from "./formatted-value.js";
2
+ import { parseDuration, resolveDurationParts, toIsoDuration, } from "../../foundations/format/index.js";
3
+ const DEFAULT_TAG_NAME = "box-formatted-duration";
4
+ /** Whether the split came out as a plain zero, which `Intl` renders as nothing. */
5
+ const isZero = (parts) => Object.values(parts).every(amount => amount === 0);
6
+ const DURATION_STYLES = new Set(["long", "short", "narrow"]);
7
+ /** `Intl` names duration fields in the plural; the parts are singular. */
8
+ const INTL_FIELD = {
9
+ day: "days",
10
+ hour: "hours",
11
+ minute: "minutes",
12
+ second: "seconds",
13
+ };
14
+ /** The unit identifiers `Intl.NumberFormat` uses, for the fallback path. */
15
+ const NUMBER_UNIT = {
16
+ day: "day",
17
+ hour: "hour",
18
+ minute: "minute",
19
+ second: "second",
20
+ };
21
+ /**
22
+ * How long something takes or lasts, in the reader's locale.
23
+ *
24
+ * ```html
25
+ * <box-formatted-duration value="5400"></box-formatted-duration>
26
+ * ```
27
+ *
28
+ * `value` is a count of seconds or an ISO 8601 duration (`PT1H30M`), because
29
+ * hosts have both: an API field is usually a number, while `<time datetime>`
30
+ * wants the ISO form. Whichever comes in, the ISO form goes back out in
31
+ * `datetime`, so the exact quantity survives for anything reading the document
32
+ * rather than looking at it.
33
+ *
34
+ * Not to be confused with `box-relative-time`, which renders *when* something
35
+ * happened relative to now ("3 days ago"). This renders *how long* — a
36
+ * retention period, an SLA, a processing time — and carries no direction.
37
+ *
38
+ * `Intl.DurationFormat` does the rendering where the browser has it. Where it
39
+ * does not, the same output is composed from `Intl.NumberFormat`'s unit style
40
+ * and `Intl.ListFormat`, both long supported; the two paths were compared
41
+ * across styles and locales and agree.
42
+ */
43
+ export class FormattedDuration extends FormattedValue {
44
+ static tagName = DEFAULT_TAG_NAME;
45
+ static get observedAttributes() {
46
+ return ["format-style", "locale", "max-units", "value"];
47
+ }
48
+ /** Seconds, or an ISO 8601 duration such as `PT1H30M`. */
49
+ get value() {
50
+ return this.getAttribute("value") ?? "";
51
+ }
52
+ set value(next) {
53
+ this.setAttribute("value", next);
54
+ }
55
+ /**
56
+ * `short` (default, `1 hr, 30 min`), `long` (`1 hour, 30 minutes`) or
57
+ * `narrow` (`1h 30m`).
58
+ *
59
+ * Named `format-style` rather than `style` because `style` is a global HTML
60
+ * attribute: naming it after the `Intl` option would put CSS on the host and
61
+ * be silently ignored as a formatting instruction.
62
+ */
63
+ get formatStyle() {
64
+ const raw = this.getAttribute("format-style");
65
+ return raw && DURATION_STYLES.has(raw) ? raw : "short";
66
+ }
67
+ set formatStyle(next) {
68
+ this.setAttribute("format-style", next);
69
+ }
70
+ /**
71
+ * How many units to show, default 2.
72
+ *
73
+ * A duration carries more precision than a reader wants: 1 day, 3 hours, 12
74
+ * minutes and 8 seconds is a number to scan past, not to read. Two units is
75
+ * enough to convey magnitude.
76
+ */
77
+ get maxUnits() {
78
+ const raw = Number(this.getAttribute("max-units"));
79
+ return Number.isFinite(raw) && raw >= 1 ? Math.trunc(raw) : 2;
80
+ }
81
+ set maxUnits(next) {
82
+ this.setAttribute("max-units", String(next));
83
+ }
84
+ usesTimeElement() {
85
+ // A duration is a valid `datetime` value, so `<time>` is honest here.
86
+ return true;
87
+ }
88
+ formatted() {
89
+ const seconds = parseDuration(this.value);
90
+ if (seconds === null) {
91
+ return null;
92
+ }
93
+ const parts = resolveDurationParts(seconds, this.maxUnits);
94
+ const machine = toIsoDuration(parts);
95
+ const style = this.formatStyle;
96
+ try {
97
+ return { machine, text: this.render(parts, style) };
98
+ }
99
+ catch {
100
+ // A locale Intl rejects, or a runtime missing both paths. The duration is
101
+ // real, so fall back to the machine form rather than losing it.
102
+ return { machine, text: machine };
103
+ }
104
+ }
105
+ render(parts, style) {
106
+ const durationFormat = Intl.DurationFormat;
107
+ if (durationFormat) {
108
+ const input = Object.fromEntries(Object.entries(parts).map(([unit, amount]) => [INTL_FIELD[unit], amount]));
109
+ // A zero duration otherwise formats to the empty string, leaving a visible
110
+ // element with no text in it — zero is a real answer. Scoped to that case:
111
+ // asking for it always appends "0 sec" to every other duration in
112
+ // browsers that honour the option, which Node's Intl quietly does not.
113
+ const options = isZero(parts)
114
+ ? { secondsDisplay: "always", style }
115
+ : { style };
116
+ return new durationFormat(this.locale, options).format(input);
117
+ }
118
+ const unitDisplay = style === "long" ? "long" : style === "narrow" ? "narrow" : "short";
119
+ const pieces = Object.entries(parts).map(([unit, amount]) => new Intl.NumberFormat(this.locale, {
120
+ style: "unit",
121
+ unit: NUMBER_UNIT[unit],
122
+ unitDisplay,
123
+ }).format(amount));
124
+ // `narrow` runs the units together (`1h 30m`); the wider styles take the
125
+ // locale's own unit-list joiner, which is what DurationFormat uses too.
126
+ return style === "narrow"
127
+ ? pieces.join(" ")
128
+ : new Intl.ListFormat(this.locale, { style: "long", type: "unit" }).format(pieces);
129
+ }
130
+ }
131
+ FormattedDuration.register();
@@ -0,0 +1 @@
1
+ export * from "../components/output/formatted-duration.js";
@@ -0,0 +1 @@
1
+ export * from "../components/output/formatted-duration.js";
@@ -0,0 +1 @@
1
+ export * from "../components/actions/toolbar.js";
@@ -0,0 +1 @@
1
+ export * from "../components/actions/toolbar.js";
@@ -80,3 +80,37 @@ export interface BoeFileSizeParts {
80
80
  * is reported whole.
81
81
  */
82
82
  export declare const resolveFileSizeParts: (bytes: number, units?: BoeByteUnits) => BoeFileSizeParts;
83
+ /** The duration units a formatted duration can be broken into, largest first. */
84
+ export type BoeDurationUnit = "day" | "hour" | "minute" | "second";
85
+ /** A duration split into whole units, ready for `Intl` to render. */
86
+ export type BoeDurationParts = Partial<Record<BoeDurationUnit, number>>;
87
+ /**
88
+ * Parse a duration to whole seconds.
89
+ *
90
+ * Accepts a count of seconds (a number, or a numeric string) or an ISO 8601
91
+ * duration such as `PT1H30M`, because hosts have both: an API field is usually
92
+ * a number, while `<time datetime>` wants the ISO form. Weeks, months and years
93
+ * are deliberately not accepted — `P1M` is ambiguous between a month and a
94
+ * minute in the calendar sense, and a month is not a fixed number of seconds,
95
+ * so rendering one as though it were would be a lie about how long the duration
96
+ * actually is.
97
+ *
98
+ * Negative and unparseable input is `null`. A duration is an elapsed quantity,
99
+ * so a negative one is a host bug rather than a direction — `box-relative-time`
100
+ * is the component that expresses "ago".
101
+ */
102
+ export declare const parseDuration: (value: string | number | null | undefined) => number | null;
103
+ /**
104
+ * Split whole seconds into at most `maxUnits` consecutive units.
105
+ *
106
+ * Consecutive, not merely the largest: 90061 seconds is "1 day, 1 hour" rather
107
+ * than "1 day, 1 minute", because a reader scanning a duration wants its
108
+ * magnitude, and skipping an empty unit to reach a smaller non-empty one
109
+ * overstates the precision.
110
+ *
111
+ * A duration of zero returns whole seconds rather than nothing, so "0 sec"
112
+ * renders instead of the element disappearing — zero is a real answer.
113
+ */
114
+ export declare const resolveDurationParts: (totalSeconds: number, maxUnits?: number) => BoeDurationParts;
115
+ /** Render a parts object back to an ISO 8601 duration, for `<time datetime>`. */
116
+ export declare const toIsoDuration: (parts: BoeDurationParts) => string;
@@ -120,3 +120,102 @@ export const resolveFileSizeParts = (bytes, units = "decimal") => {
120
120
  const exponent = Math.min(Math.floor(Math.log(magnitude) / Math.log(base)), suffixes.length - 1);
121
121
  return { suffix: suffixes[exponent], value: bytes / base ** exponent };
122
122
  };
123
+ const ISO_DURATION = /^P(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
124
+ const SECONDS_PER = {
125
+ day: 86_400,
126
+ hour: 3_600,
127
+ minute: 60,
128
+ second: 1,
129
+ };
130
+ const DURATION_ORDER = ["day", "hour", "minute", "second"];
131
+ /**
132
+ * Parse a duration to whole seconds.
133
+ *
134
+ * Accepts a count of seconds (a number, or a numeric string) or an ISO 8601
135
+ * duration such as `PT1H30M`, because hosts have both: an API field is usually
136
+ * a number, while `<time datetime>` wants the ISO form. Weeks, months and years
137
+ * are deliberately not accepted — `P1M` is ambiguous between a month and a
138
+ * minute in the calendar sense, and a month is not a fixed number of seconds,
139
+ * so rendering one as though it were would be a lie about how long the duration
140
+ * actually is.
141
+ *
142
+ * Negative and unparseable input is `null`. A duration is an elapsed quantity,
143
+ * so a negative one is a host bug rather than a direction — `box-relative-time`
144
+ * is the component that expresses "ago".
145
+ */
146
+ export const parseDuration = (value) => {
147
+ if (value == null || value === "") {
148
+ return null;
149
+ }
150
+ if (typeof value === "number") {
151
+ return Number.isFinite(value) && value >= 0 ? Math.round(value) : null;
152
+ }
153
+ const trimmed = value.trim();
154
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
155
+ return Math.round(Number(trimmed));
156
+ }
157
+ const match = ISO_DURATION.exec(trimmed);
158
+ if (!match || trimmed === "P" || trimmed === "PT") {
159
+ return null;
160
+ }
161
+ const [, days, hours, minutes, seconds] = match;
162
+ const total = Number(days ?? 0) * SECONDS_PER.day +
163
+ Number(hours ?? 0) * SECONDS_PER.hour +
164
+ Number(minutes ?? 0) * SECONDS_PER.minute +
165
+ Number(seconds ?? 0);
166
+ return Number.isFinite(total) ? Math.round(total) : null;
167
+ };
168
+ /**
169
+ * Split whole seconds into at most `maxUnits` consecutive units.
170
+ *
171
+ * Consecutive, not merely the largest: 90061 seconds is "1 day, 1 hour" rather
172
+ * than "1 day, 1 minute", because a reader scanning a duration wants its
173
+ * magnitude, and skipping an empty unit to reach a smaller non-empty one
174
+ * overstates the precision.
175
+ *
176
+ * A duration of zero returns whole seconds rather than nothing, so "0 sec"
177
+ * renders instead of the element disappearing — zero is a real answer.
178
+ */
179
+ export const resolveDurationParts = (totalSeconds, maxUnits = 2) => {
180
+ const limit = Math.max(1, Math.trunc(maxUnits));
181
+ if (totalSeconds <= 0) {
182
+ return { second: 0 };
183
+ }
184
+ const collected = [];
185
+ let remaining = Math.round(totalSeconds);
186
+ let started = false;
187
+ for (const unit of DURATION_ORDER) {
188
+ const size = SECONDS_PER[unit];
189
+ const amount = Math.floor(remaining / size);
190
+ if (!started && amount === 0) {
191
+ continue;
192
+ }
193
+ started = true;
194
+ collected.push([unit, amount]);
195
+ remaining -= amount * size;
196
+ if (collected.length === limit) {
197
+ break;
198
+ }
199
+ }
200
+ // Trailing zeroes are noise: an exact hour is "1 hr", not "1 hr, 0 min". Only
201
+ // trailing ones go — an interior zero is load-bearing, since "1 day, 0 hr" as
202
+ // the first two units of 1d 0h 5m would otherwise become "1 day, 5 min" and
203
+ // claim a precision the split does not have.
204
+ while (collected.length > 1 && collected[collected.length - 1][1] === 0) {
205
+ collected.pop();
206
+ }
207
+ return Object.fromEntries(collected);
208
+ };
209
+ /** Render a parts object back to an ISO 8601 duration, for `<time datetime>`. */
210
+ export const toIsoDuration = (parts) => {
211
+ const date = parts.day ? `${parts.day}D` : "";
212
+ const time = [
213
+ parts.hour ? `${parts.hour}H` : "",
214
+ parts.minute ? `${parts.minute}M` : "",
215
+ parts.second ? `${parts.second}S` : "",
216
+ ].join("");
217
+ if (!date && !time) {
218
+ return "PT0S";
219
+ }
220
+ return `P${date}${time ? `T${time}` : ""}`;
221
+ };
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from "./components/actions/link-button.js";
19
19
  export * from "./components/actions/menu.js";
20
20
  export * from "./components/actions/segmented-control.js";
21
21
  export * from "./components/actions/menu-item.js";
22
+ export * from "./components/actions/toolbar.js";
22
23
  export * from "./components/collections/card.js";
23
24
  export * from "./components/collections/carousel.js";
24
25
  export * from "./components/collections/datalist-item.js";
@@ -88,6 +89,7 @@ export * from "./components/output/code-block.js";
88
89
  export * from "./components/output/formatted-date.js";
89
90
  export * from "./components/output/relative-time.js";
90
91
  export * from "./components/output/formatted-number.js";
92
+ export * from "./components/output/formatted-duration.js";
91
93
  export * from "./components/output/formatted-file-size.js";
92
94
  export * from "./components/layout/app-shell.js";
93
95
  export * from "./components/layout/divider.js";
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export * from "./components/actions/link-button.js";
19
19
  export * from "./components/actions/menu.js";
20
20
  export * from "./components/actions/segmented-control.js";
21
21
  export * from "./components/actions/menu-item.js";
22
+ export * from "./components/actions/toolbar.js";
22
23
  // Components: collections
23
24
  export * from "./components/collections/card.js";
24
25
  export * from "./components/collections/carousel.js";
@@ -96,6 +97,7 @@ export * from "./components/output/code-block.js";
96
97
  export * from "./components/output/formatted-date.js";
97
98
  export * from "./components/output/relative-time.js";
98
99
  export * from "./components/output/formatted-number.js";
100
+ export * from "./components/output/formatted-duration.js";
99
101
  export * from "./components/output/formatted-file-size.js";
100
102
  // Components: layout
101
103
  export * from "./components/layout/app-shell.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unofficialbox/box-open-elements",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Box Open Elements: an open, framework-agnostic design system and web component library for Box-style experiences",
5
5
  "license": "MIT",
6
6
  "homepage": "https://unofficialbox.github.io/box-open-elements",