@parallelworks/ui 0.94.0 → 0.95.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/README.md CHANGED
@@ -121,6 +121,7 @@ Each need has one component. Reach for it before styling markup by hand.
121
121
  | A message set into the page: guidance, a warning, an error | `Callout`; `variant="bar"` across a surface under its header |
122
122
  | A passing notice, or the progress of work in the background | `toast` |
123
123
  | How full something is, how far a task has come | `Meter`, `ProgressBar` |
124
+ | Counts at the top of a page, such as jobs running and failed: a value, its label, a lamp, a bar, or a link to the list | `StatTile`, laid out in a `StatTileGroup`, which folds to two columns on a phone with `wide` tiles and any that would sit alone across the row |
124
125
  | Pages of a long list | `Pagination`; in a `ListPage`, as its `pager`, on the count's line under the last row, where a "Load more" button goes too |
125
126
  | A grouped page of settings | `SettingsCard` with `SettingRow`s |
126
127
  | A person or an agent | `Avatar`, colored by name without a picture, or by kind; `AvatarGroup` for an overlapping stack with a "+N" chip |
@@ -1326,3 +1327,20 @@ link; put page content inside it without nesting a second `<main>`.
1326
1327
  Frame styles use the `pw-ui` cascade layer. Put broad host element defaults
1327
1328
  (such as anchor colors and scrollbar styling) in a lower-priority layer declared
1328
1329
  before the package styles, so they do not replace navigation states.
1330
+
1331
+ Applications that supply their own navigation items can load only the layout:
1332
+
1333
+ ```tsx
1334
+ import { AppFrame, AppFrameToggle } from '@parallelworks/ui/app-frame'
1335
+ import '@parallelworks/ui/app-frame/layout.css'
1336
+ ```
1337
+
1338
+ This includes the sidebar, scrolling regions, resize handle, mobile drawer,
1339
+ toggle and close buttons, focus indicators, and skip link. Style custom navigation
1340
+ items in the host, including their selected, hover, rail, and mobile states.
1341
+
1342
+ When using `AppFrameNavItem`, also import
1343
+ `@parallelworks/ui/app-frame/navigation.css`. The existing
1344
+ `@parallelworks/ui/app-frame.css` import includes both stylesheets and continues
1345
+ to work without changes. Use either the combined import or the two separate
1346
+ imports; there is no need to load both forms.
@@ -60,66 +60,6 @@
60
60
  flex-shrink: 0;
61
61
  padding: 8px 12px;
62
62
  }
63
- .pw-frame-nav-item {
64
- display: flex;
65
- align-items: center;
66
- gap: 8px;
67
- min-height: 36px;
68
- min-width: 0;
69
- width: 100%;
70
- border: 0;
71
- border-radius: 8px;
72
- padding: 8px;
73
- background: transparent;
74
- color: inherit;
75
- font: inherit;
76
- font-size: 14px;
77
- font-weight: 500;
78
- line-height: 20px;
79
- cursor: pointer;
80
- text-decoration: none;
81
- text-align: start;
82
- transition: background-color 150ms ease-out;
83
- }
84
- .pw-frame-nav-item:hover,
85
- .pw-frame-toggle:hover {
86
- background: var(--theme-sidebar-hover, var(--theme-hover));
87
- }
88
- .pw-frame-nav-item[aria-current] {
89
- background: var(--theme-sidebar-active-bg, var(--theme-accent-subtle));
90
- color: var(--theme-sidebar-active-text, var(--theme-accent));
91
- }
92
- .pw-frame-nav-item:is(:disabled, [aria-disabled]) {
93
- opacity: 0.5;
94
- cursor: not-allowed;
95
- }
96
- .pw-frame-nav-icon {
97
- display: flex;
98
- flex: 0 0 18px;
99
- align-items: center;
100
- justify-content: center;
101
- }
102
- .pw-frame-nav-icon svg {
103
- width: 18px;
104
- height: 18px;
105
- }
106
- .pw-frame-nav-label {
107
- flex: 1;
108
- min-width: 0;
109
- overflow: hidden;
110
- text-overflow: ellipsis;
111
- white-space: nowrap;
112
- }
113
- .pw-frame-nav-badge {
114
- flex-shrink: 0;
115
- font-size: 12px;
116
- font-variant-numeric: tabular-nums;
117
- }
118
- [data-sidebar-mode="rail"] .pw-frame-nav-item {
119
- padding-inline: 0;
120
- justify-content: center;
121
- height: 40px;
122
- }
123
63
  .pw-frame-toggle {
124
64
  display: inline-flex;
125
65
  align-items: center;
@@ -134,6 +74,9 @@
134
74
  cursor: pointer;
135
75
  flex-shrink: 0;
136
76
  }
77
+ .pw-frame-toggle:hover {
78
+ background: var(--theme-sidebar-hover, var(--theme-hover));
79
+ }
137
80
  .pw-frame-toggle svg {
138
81
  width: 18px;
139
82
  height: 18px;
@@ -249,7 +192,6 @@
249
192
  .pw-frame-drawer .pw-frame-brand {
250
193
  padding-top: 0;
251
194
  }
252
- .pw-frame-drawer .pw-frame-nav-item,
253
195
  .pw-app-frame[data-mobile] .pw-frame-toggle {
254
196
  min-height: 44px;
255
197
  }
@@ -273,7 +215,75 @@
273
215
  @media (prefers-reduced-motion: reduce) {
274
216
  .pw-frame-sidebar,
275
217
  .pw-frame-backdrop,
276
- .pw-frame-drawer,
218
+ .pw-frame-drawer {
219
+ transition: none;
220
+ }
221
+ }
222
+ }
223
+ @layer pw-ui {
224
+ .pw-frame-nav-item {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 8px;
228
+ min-height: 36px;
229
+ min-width: 0;
230
+ width: 100%;
231
+ border: 0;
232
+ border-radius: 8px;
233
+ padding: 8px;
234
+ background: transparent;
235
+ color: inherit;
236
+ font: inherit;
237
+ font-size: 14px;
238
+ font-weight: 500;
239
+ line-height: 20px;
240
+ cursor: pointer;
241
+ text-decoration: none;
242
+ text-align: start;
243
+ transition: background-color 150ms ease-out;
244
+ }
245
+ .pw-frame-nav-item:hover {
246
+ background: var(--theme-sidebar-hover, var(--theme-hover));
247
+ }
248
+ .pw-frame-nav-item[aria-current] {
249
+ background: var(--theme-sidebar-active-bg, var(--theme-accent-subtle));
250
+ color: var(--theme-sidebar-active-text, var(--theme-accent));
251
+ }
252
+ .pw-frame-nav-item:is(:disabled, [aria-disabled]) {
253
+ opacity: 0.5;
254
+ cursor: not-allowed;
255
+ }
256
+ .pw-frame-nav-icon {
257
+ display: flex;
258
+ flex: 0 0 18px;
259
+ align-items: center;
260
+ justify-content: center;
261
+ }
262
+ .pw-frame-nav-icon svg {
263
+ width: 18px;
264
+ height: 18px;
265
+ }
266
+ .pw-frame-nav-label {
267
+ flex: 1;
268
+ min-width: 0;
269
+ overflow: hidden;
270
+ text-overflow: ellipsis;
271
+ white-space: nowrap;
272
+ }
273
+ .pw-frame-nav-badge {
274
+ flex-shrink: 0;
275
+ font-size: 12px;
276
+ font-variant-numeric: tabular-nums;
277
+ }
278
+ [data-sidebar-mode="rail"] .pw-frame-nav-item {
279
+ padding-inline: 0;
280
+ justify-content: center;
281
+ height: 40px;
282
+ }
283
+ .pw-frame-drawer .pw-frame-nav-item {
284
+ min-height: 44px;
285
+ }
286
+ @media (prefers-reduced-motion: reduce) {
277
287
  .pw-frame-nav-item {
278
288
  transition: none;
279
289
  }
@@ -0,0 +1,222 @@
1
+ @layer pw-ui {
2
+ /* The frame uses the same sidebar tokens as the theme generator. */
3
+ .pw-app-frame {
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 100dvh;
7
+ min-height: 0;
8
+ isolation: isolate;
9
+ background: var(--theme-sidebar-bg, var(--theme-muted-panel-bg));
10
+ color: var(--theme-app);
11
+ }
12
+ .pw-frame-body {
13
+ display: flex;
14
+ flex: 1;
15
+ min-height: 0;
16
+ position: relative;
17
+ overflow: hidden;
18
+ }
19
+ .pw-frame-sidebar,
20
+ .pw-frame-drawer {
21
+ display: flex;
22
+ flex-direction: column;
23
+ flex-shrink: 0;
24
+ min-height: 0;
25
+ background: var(--theme-sidebar-bg, var(--theme-muted-panel-bg));
26
+ color: var(--theme-sidebar-text, var(--theme-app));
27
+ }
28
+ .pw-frame-sidebar {
29
+ position: relative;
30
+ transition: width 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
31
+ }
32
+ .pw-frame-sidebar[data-resizing] {
33
+ transition: none;
34
+ user-select: none;
35
+ }
36
+ .pw-frame-brand {
37
+ padding: 12px 12px 4px;
38
+ flex-shrink: 0;
39
+ min-width: 0;
40
+ overflow: hidden;
41
+ }
42
+ .pw-frame-links {
43
+ flex: 1;
44
+ overflow-x: hidden;
45
+ overflow-y: auto;
46
+ padding: 12px;
47
+ min-width: 0;
48
+ min-height: 0;
49
+ scrollbar-width: none;
50
+ }
51
+ .pw-frame-links::-webkit-scrollbar {
52
+ display: none;
53
+ }
54
+ .pw-frame-links nav {
55
+ display: grid;
56
+ grid-template-columns: minmax(0, 1fr);
57
+ gap: 4px;
58
+ }
59
+ .pw-frame-footer {
60
+ flex-shrink: 0;
61
+ padding: 8px 12px;
62
+ }
63
+ .pw-frame-toggle {
64
+ display: inline-flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ width: 36px;
68
+ height: 36px;
69
+ border: 0;
70
+ border-radius: 8px;
71
+ padding: 0;
72
+ color: inherit;
73
+ background: transparent;
74
+ cursor: pointer;
75
+ flex-shrink: 0;
76
+ }
77
+ .pw-frame-toggle:hover {
78
+ background: var(--theme-sidebar-hover, var(--theme-hover));
79
+ }
80
+ .pw-frame-toggle svg {
81
+ width: 18px;
82
+ height: 18px;
83
+ }
84
+ .pw-frame-sidebar :focus-visible,
85
+ .pw-frame-drawer :focus-visible {
86
+ outline: 2px solid currentColor;
87
+ outline-offset: 2px;
88
+ }
89
+ .pw-frame-resize {
90
+ position: absolute;
91
+ z-index: 1;
92
+ inset-block: 0;
93
+ inset-inline-end: -4px;
94
+ width: 8px;
95
+ cursor: col-resize;
96
+ touch-action: none;
97
+ }
98
+ .pw-frame-resize::after {
99
+ content: "";
100
+ position: absolute;
101
+ inset-block: 8px;
102
+ inset-inline-start: 3px;
103
+ width: 2px;
104
+ border-radius: 2px;
105
+ }
106
+ .pw-frame-resize:hover::after,
107
+ .pw-frame-resize:focus-visible::after {
108
+ background: var(--theme-sidebar-text, var(--theme-accent));
109
+ opacity: 0.5;
110
+ }
111
+ .pw-frame-surface-wrap {
112
+ flex: 1;
113
+ display: flex;
114
+ flex-direction: column;
115
+ min-width: 0;
116
+ min-height: 0;
117
+ padding: 8px;
118
+ padding-inline-start: 0;
119
+ }
120
+ .pw-frame-surface {
121
+ flex: 1;
122
+ display: flex;
123
+ flex-direction: column;
124
+ min-width: 0;
125
+ min-height: 0;
126
+ overflow: hidden;
127
+ isolation: isolate;
128
+ border: 1px solid var(--theme-border);
129
+ border-radius: 12px;
130
+ background: var(--theme-app-bg);
131
+ background-clip: padding-box;
132
+ }
133
+ .pw-frame-header,
134
+ .pw-frame-banner {
135
+ flex-shrink: 0;
136
+ }
137
+ .pw-frame-main {
138
+ flex: 1;
139
+ min-width: 0;
140
+ min-height: 0;
141
+ overflow: auto;
142
+ }
143
+ .pw-app-frame[data-full-bleed] .pw-frame-surface-wrap {
144
+ padding: 0;
145
+ }
146
+ .pw-app-frame[data-full-bleed] .pw-frame-surface {
147
+ border: 0;
148
+ border-radius: 0;
149
+ }
150
+ .pw-app-frame[data-mobile] .pw-frame-surface-wrap {
151
+ padding: 8px;
152
+ }
153
+ .pw-app-frame[data-mobile][data-full-bleed] .pw-frame-surface-wrap {
154
+ padding: 0;
155
+ }
156
+ .pw-frame-backdrop {
157
+ position: absolute;
158
+ inset: 0;
159
+ z-index: var(--layer-drawer, 1100);
160
+ background: var(--theme-backdrop, rgb(0 0 0 / 0.35));
161
+ transition: opacity 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
162
+ }
163
+ .pw-frame-backdrop[data-starting-style],
164
+ .pw-frame-backdrop[data-ending-style] {
165
+ opacity: 0;
166
+ }
167
+ .pw-frame-drawer {
168
+ position: absolute;
169
+ inset-block: 8px;
170
+ inset-inline-start: 8px;
171
+ max-width: calc(100% - 48px);
172
+ z-index: var(--layer-drawer, 1100);
173
+ border-radius: 12px;
174
+ box-shadow: 0 12px 32px rgb(0 0 0 / 0.2);
175
+ outline: none;
176
+ transform: translateX(0);
177
+ transition: transform 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
178
+ }
179
+ .pw-frame-drawer[data-starting-style],
180
+ .pw-frame-drawer[data-ending-style] {
181
+ transform: translateX(calc(-100% - 16px));
182
+ }
183
+ .pw-frame-drawer:dir(rtl)[data-starting-style],
184
+ .pw-frame-drawer:dir(rtl)[data-ending-style] {
185
+ transform: translateX(calc(100% + 16px));
186
+ }
187
+ .pw-frame-mobile-close {
188
+ display: flex;
189
+ justify-content: flex-end;
190
+ padding: 4px 4px 0;
191
+ }
192
+ .pw-frame-drawer .pw-frame-brand {
193
+ padding-top: 0;
194
+ }
195
+ .pw-app-frame[data-mobile] .pw-frame-toggle {
196
+ min-height: 44px;
197
+ }
198
+ .pw-app-frame[data-mobile] .pw-frame-toggle {
199
+ width: 44px;
200
+ height: 44px;
201
+ }
202
+ .pw-frame-skip {
203
+ position: fixed;
204
+ inset-block-start: -100px;
205
+ inset-inline-start: 8px;
206
+ padding: 8px 16px;
207
+ border-radius: 6px;
208
+ background: var(--theme-panel-bg);
209
+ color: var(--theme-app);
210
+ z-index: var(--layer-popover, 1400);
211
+ }
212
+ .pw-frame-skip:focus {
213
+ inset-block-start: 8px;
214
+ }
215
+ @media (prefers-reduced-motion: reduce) {
216
+ .pw-frame-sidebar,
217
+ .pw-frame-backdrop,
218
+ .pw-frame-drawer {
219
+ transition: none;
220
+ }
221
+ }
222
+ }
@@ -0,0 +1,69 @@
1
+ @layer pw-ui {
2
+ .pw-frame-nav-item {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 8px;
6
+ min-height: 36px;
7
+ min-width: 0;
8
+ width: 100%;
9
+ border: 0;
10
+ border-radius: 8px;
11
+ padding: 8px;
12
+ background: transparent;
13
+ color: inherit;
14
+ font: inherit;
15
+ font-size: 14px;
16
+ font-weight: 500;
17
+ line-height: 20px;
18
+ cursor: pointer;
19
+ text-decoration: none;
20
+ text-align: start;
21
+ transition: background-color 150ms ease-out;
22
+ }
23
+ .pw-frame-nav-item:hover {
24
+ background: var(--theme-sidebar-hover, var(--theme-hover));
25
+ }
26
+ .pw-frame-nav-item[aria-current] {
27
+ background: var(--theme-sidebar-active-bg, var(--theme-accent-subtle));
28
+ color: var(--theme-sidebar-active-text, var(--theme-accent));
29
+ }
30
+ .pw-frame-nav-item:is(:disabled, [aria-disabled]) {
31
+ opacity: 0.5;
32
+ cursor: not-allowed;
33
+ }
34
+ .pw-frame-nav-icon {
35
+ display: flex;
36
+ flex: 0 0 18px;
37
+ align-items: center;
38
+ justify-content: center;
39
+ }
40
+ .pw-frame-nav-icon svg {
41
+ width: 18px;
42
+ height: 18px;
43
+ }
44
+ .pw-frame-nav-label {
45
+ flex: 1;
46
+ min-width: 0;
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ }
51
+ .pw-frame-nav-badge {
52
+ flex-shrink: 0;
53
+ font-size: 12px;
54
+ font-variant-numeric: tabular-nums;
55
+ }
56
+ [data-sidebar-mode="rail"] .pw-frame-nav-item {
57
+ padding-inline: 0;
58
+ justify-content: center;
59
+ height: 40px;
60
+ }
61
+ .pw-frame-drawer .pw-frame-nav-item {
62
+ min-height: 44px;
63
+ }
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .pw-frame-nav-item {
66
+ transition: none;
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,76 @@
1
+ import { type MouseEventHandler, type ReactElement, type ReactNode } from 'react';
2
+ import type { LinkTarget } from './AppLink.js';
3
+ import { type BarTone, type ProgressSegment } from './Meter.js';
4
+ import { type BadgeVariant } from './StatusBadge.js';
5
+ /** A tile's lamp, as a status's: lit in a tone, unlit with `off`, or an icon in its place. Its
6
+ * tone says what the state means, as every status lamp's does: work under way is `info` with
7
+ * `pulse`, something waiting its turn or off (pending jobs, idle nodes) is `off`, a failure is
8
+ * `error`, work done is `success`, and `warning` is only for a state that needs a look. */
9
+ export interface StatTileLamp {
10
+ variant?: BadgeVariant | undefined;
11
+ /** Unlit, a hollow ring, for a count of something waiting or off, such as pending jobs or idle
12
+ * nodes. */
13
+ off?: boolean | undefined;
14
+ /** A ping from the lamp, for work under way. It pings only while the count is above zero. */
15
+ pulse?: boolean | undefined;
16
+ /** An icon in place of the lamp, such as a `RunStatusIcon`. */
17
+ icon?: ReactNode | undefined;
18
+ }
19
+ /** A tile's bar: a `ProgressBar` in one or more stretches, such as nodes in use, then partly in
20
+ * use, then down, on a track whose empty rest is the idle ones. */
21
+ export interface StatTileBar {
22
+ value: number;
23
+ max?: number;
24
+ tone?: BarTone;
25
+ segments?: ReadonlyArray<ProgressSegment> | undefined;
26
+ /** What the bar shows, read by assistive technology, such as "60 of 102 nodes in use". */
27
+ 'aria-label': string;
28
+ getAriaValueText?: ((formatted: string | null, value: number | null) => string) | undefined;
29
+ }
30
+ export interface StatTileProps extends LinkTarget {
31
+ /** The count. A number reads in the app's locale, grouped as it groups digits; anything else,
32
+ * such as a dash for a count not known, shows as given. */
33
+ value: ReactNode;
34
+ /** Quieter text after the value, such as "/ 102 free". */
35
+ suffix?: ReactNode;
36
+ /** What the value counts, under it, such as "Running". A long one wraps. */
37
+ label: ReactNode;
38
+ /** A status lamp level with the label, for a count of things in one state. */
39
+ lamp?: StatTileLamp | undefined;
40
+ /** A bar under the label: a `ProgressBar`'s value and stretches, or a bar of the host's own,
41
+ * such as one in a tooltip. */
42
+ bar?: StatTileBar | ReactElement | undefined;
43
+ /** How a number is written, such as `{ notation: 'compact' }` for 1.3M; whole and grouped
44
+ * by default. */
45
+ format?: Intl.NumberFormatOptions | undefined;
46
+ /** What assistive technology reads for the tile, when the value, suffix and label in a row
47
+ * don't read well, such as "35 of 102 nodes free". */
48
+ spoken?: string | undefined;
49
+ /** Makes the tile a button. With `href` it is a link instead. */
50
+ onClick?: MouseEventHandler<HTMLButtonElement> | undefined;
51
+ /** In a `StatTileGroup`, the tile takes a row of its own on a phone, such as one with a bar. */
52
+ wide?: boolean | undefined;
53
+ /** Sketches the value while it loads; the label shows, since it is known. */
54
+ loading?: boolean | undefined;
55
+ /** The locale numbers are written in; the provider's `locale`, then the browser's, without it. */
56
+ locale?: string | undefined;
57
+ className?: string | undefined;
58
+ }
59
+ /** A count and what it counts, as a tile at the top of a page, such as a queue's running jobs:
60
+ * a large value, a label under it, and optionally a status lamp beside the label and a bar
61
+ * under it. With `href` or `onClick`, the whole tile is a link or a button, such as to the list
62
+ * the count is of. Lay several out with `StatTileGroup`.
63
+ *
64
+ * Assistive technology reads the value and label as one phrase, such as "3 Running", not as two
65
+ * lines; `spoken` words it otherwise. */
66
+ export declare function StatTile({ value, suffix, label, lamp, bar, format, spoken, onClick, wide, loading, locale: localeProp, href, reloadDocument, target, className, }: StatTileProps): import("react").JSX.Element;
67
+ export interface StatTileGroupProps {
68
+ /** `StatTile`s, each a direct child, so the group reads their `wide`. */
69
+ children: ReactNode;
70
+ className?: string | undefined;
71
+ }
72
+ /** Tiles in a row as wide as the content around them, edge to edge with it. Where the row is
73
+ * too narrow for them, such as on a phone, they take two columns, with a `wide` tile across
74
+ * both, and any tile that would sit alone in half a row across both too:
75
+ * four counts and a wide tile read as two rows of two and the wide one under them. */
76
+ export declare function StatTileGroup({ children, className }: StatTileGroupProps): import("react").JSX.Element;
@@ -0,0 +1,141 @@
1
+ import { cx } from "../cx.js";
2
+ import { useLocale, useStrings } from "./Provider.js";
3
+ import { Skeleton } from "./Skeleton.js";
4
+ import { Card } from "./Card.js";
5
+ import { ProgressBar } from "./Meter.js";
6
+ import { formatInteger } from "../format.js";
7
+ import { StatusDot } from "./StatusBadge.js";
8
+ import { Children, createContext, isValidElement, useContext } from "react";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
+ //#region src/components/StatTile.tsx
11
+ var PhoneSpanContext = createContext(false);
12
+ var numberFormats = /* @__PURE__ */ new Map();
13
+ function formatValue(value, locale, format) {
14
+ if (!format) return formatInteger(value, locale);
15
+ const key = `${locale ?? ""}|${JSON.stringify(format)}`;
16
+ let formatter = numberFormats.get(key);
17
+ if (!formatter) {
18
+ formatter = new Intl.NumberFormat(locale, format);
19
+ numberFormats.set(key, formatter);
20
+ }
21
+ return formatter.format(value);
22
+ }
23
+ /** A count and what it counts, as a tile at the top of a page, such as a queue's running jobs:
24
+ * a large value, a label under it, and optionally a status lamp beside the label and a bar
25
+ * under it. With `href` or `onClick`, the whole tile is a link or a button, such as to the list
26
+ * the count is of. Lay several out with `StatTileGroup`.
27
+ *
28
+ * Assistive technology reads the value and label as one phrase, such as "3 Running", not as two
29
+ * lines; `spoken` words it otherwise. */
30
+ function StatTile({ value, suffix, label, lamp, bar, format, spoken, onClick, wide, loading = false, locale: localeProp, href, reloadDocument, target, className }) {
31
+ const providerLocale = useLocale();
32
+ const { loading: loadingText } = useStrings().common;
33
+ const locale = localeProp ?? providerLocale;
34
+ const groupSpan = useContext(PhoneSpanContext);
35
+ const shown = typeof value === "number" ? formatValue(value, locale, format) : value;
36
+ const pulse = lamp?.pulse === true && !(typeof value === "number" && value <= 0);
37
+ return /* @__PURE__ */ jsxs(Card, {
38
+ size: "fill",
39
+ href,
40
+ reloadDocument,
41
+ target,
42
+ render: href === void 0 && onClick ? /* @__PURE__ */ jsx("button", {
43
+ type: "button",
44
+ onClick
45
+ }) : void 0,
46
+ className: cx("flex h-full min-w-0 flex-col px-4 py-3", (groupSpan || wide) && "col-span-2 @2xl/stat-tiles:col-span-1", className),
47
+ children: [
48
+ loading ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
49
+ className: "sr-only",
50
+ children: label
51
+ }), /* @__PURE__ */ jsx("span", {
52
+ className: "sr-only",
53
+ children: loadingText
54
+ })] }) : /* @__PURE__ */ jsx("span", {
55
+ className: "sr-only",
56
+ children: spoken ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [
57
+ shown,
58
+ suffix != null && /* @__PURE__ */ jsxs(Fragment$1, { children: [" ", suffix] }),
59
+ " ",
60
+ label
61
+ ] })
62
+ }),
63
+ /* @__PURE__ */ jsx("span", {
64
+ "aria-hidden": "true",
65
+ className: "flex min-w-0 items-baseline gap-1.5",
66
+ children: loading ? /* @__PURE__ */ jsx(Skeleton, {
67
+ width: "3ch",
68
+ height: 24,
69
+ className: "my-1 text-2xl"
70
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
71
+ className: "min-w-0 truncate text-2xl leading-8 font-semibold tracking-tight text-(--theme-app)",
72
+ children: shown
73
+ }), suffix != null && /* @__PURE__ */ jsx("span", {
74
+ className: "min-w-0 truncate text-sm text-(--theme-muted-text-color)",
75
+ children: suffix
76
+ })] })
77
+ }),
78
+ /* @__PURE__ */ jsxs("span", {
79
+ "aria-hidden": "true",
80
+ className: "flex min-w-0 items-start gap-1.5 text-[13px] leading-5 text-(--theme-muted-text-color)",
81
+ children: [lamp && (lamp.icon != null ? /* @__PURE__ */ jsx("span", {
82
+ className: "inline-flex h-lh shrink-0 items-center",
83
+ children: lamp.icon
84
+ }) : /* @__PURE__ */ jsx("span", {
85
+ className: "inline-flex h-lh shrink-0 items-center",
86
+ children: /* @__PURE__ */ jsx(StatusDot, {
87
+ variant: lamp.variant,
88
+ off: lamp.off,
89
+ pulse
90
+ })
91
+ })), /* @__PURE__ */ jsx("span", {
92
+ className: "min-w-0 break-words",
93
+ children: label
94
+ })]
95
+ }),
96
+ bar !== void 0 && /* @__PURE__ */ jsx("div", {
97
+ className: "mt-2",
98
+ children: isValidElement(bar) ? bar : /* @__PURE__ */ jsx(ProgressBar, {
99
+ ...bar,
100
+ size: "sm",
101
+ showValue: "none",
102
+ locale
103
+ })
104
+ })
105
+ ]
106
+ });
107
+ }
108
+ /** Which tiles, by place, would sit alone in half a row of two, laid out in order with each wide
109
+ * one on a row of its own. */
110
+ function loneTiles(wide) {
111
+ const lone = /* @__PURE__ */ new Set();
112
+ let second = false;
113
+ wide.forEach((isWide, index) => {
114
+ if (isWide) second = false;
115
+ else if (second) second = false;
116
+ else if (index === wide.length - 1 || wide[index + 1]) lone.add(index);
117
+ else second = true;
118
+ });
119
+ return lone;
120
+ }
121
+ /** Tiles in a row as wide as the content around them, edge to edge with it. Where the row is
122
+ * too narrow for them, such as on a phone, they take two columns, with a `wide` tile across
123
+ * both, and any tile that would sit alone in half a row across both too:
124
+ * four counts and a wide tile read as two rows of two and the wide one under them. */
125
+ function StatTileGroup({ children, className }) {
126
+ const tiles = Children.toArray(children).filter(isValidElement);
127
+ const lone = loneTiles(tiles.map((tile) => tile.props.wide === true));
128
+ return /* @__PURE__ */ jsx("div", {
129
+ className: cx("@container/stat-tiles w-full", className),
130
+ children: /* @__PURE__ */ jsx("div", {
131
+ className: "grid grid-cols-2 gap-3 @2xl/stat-tiles:grid-cols-(--stat-tile-columns)",
132
+ style: { "--stat-tile-columns": `repeat(${Math.max(tiles.length, 1)}, minmax(0, 1fr))` },
133
+ children: tiles.map((tile, index) => /* @__PURE__ */ jsx(PhoneSpanContext.Provider, {
134
+ value: lone.has(index),
135
+ children: tile
136
+ }, tile.key))
137
+ })
138
+ });
139
+ }
140
+ //#endregion
141
+ export { StatTile, StatTileGroup };