@crewlethq/ui 0.86.0 → 0.87.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.
@@ -1,12 +1,12 @@
1
- export { ArtifactCard, ArtifactCardSkeleton, ArtifactPreviewGlyph } from '../chunk-7IWZN5Q2.js';
1
+ export { ArtifactCard, ArtifactCardSkeleton, ArtifactPreviewGlyph } from '../chunk-CWCQCOIG.js';
2
2
  import '../chunk-I4IUX3JF.js';
3
3
  import '../chunk-HF5L56YQ.js';
4
4
  import '../chunk-EJNJMHQG.js';
5
5
  import '../chunk-RE3FBGQQ.js';
6
- import '../chunk-U2IQNDDR.js';
7
- import '../chunk-AV2DXAEL.js';
8
6
  import '../chunk-YTLWOHOY.js';
9
7
  import '../chunk-XZY6S3UC.js';
8
+ import '../chunk-U2IQNDDR.js';
9
+ import '../chunk-AV2DXAEL.js';
10
10
  import '../chunk-J5T5TDM2.js';
11
11
  import '../chunk-YQ7Q52XU.js';
12
12
  import '../chunk-DOY3KC6I.js';
@@ -0,0 +1,85 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ /** One seat a message reached, or will reach. */
3
+ export interface DeliveryReceiptRecipient {
4
+ /** A stable key for the seat: its handle. */
5
+ id: string;
6
+ /** How the reader knows the seat: "pm", "Product Manager". */
7
+ name: string;
8
+ /**
9
+ * Why the message reached the seat, in the caller's words, drawn in
10
+ * brackets after the seats that share it: "every message". Leave it out
11
+ * where the reason needs no note (a mention, a reply to their thread).
12
+ */
13
+ reason?: string | undefined;
14
+ /** The seat has answered: it replied in the thread, or in the conversation. */
15
+ answered?: boolean | undefined;
16
+ }
17
+ /**
18
+ * `sent` is a receipt under a message that was posted ("Reached pm"); `draft`
19
+ * is the composer's word on what a post will do before it is sent ("Will
20
+ * reach pm").
21
+ */
22
+ export type DeliveryReceiptTense = "sent" | "draft";
23
+ export interface DeliveryReceiptLabels {
24
+ /** The reach of a posted message: "Reached pm; qa, sre (every message)". */
25
+ reached: (who: string) => string;
26
+ /** The reach of a draft: "Will reach pm". */
27
+ reaches: (who: string) => string;
28
+ /** A posted message that reached no agent. */
29
+ none: string;
30
+ /** A draft that will reach no agent. */
31
+ noneDraft: string;
32
+ /** The seats that answered: "pm answered". */
33
+ answered: (who: string) => string;
34
+ /** The seats in one reason's group: "qa, sre". */
35
+ list: (names: readonly string[]) => string;
36
+ /** One reason's note: "(every message)". */
37
+ reason: (words: string) => string;
38
+ /** The groups of different reasons, joined: "pm; qa, sre (every message)". */
39
+ groups: (groups: readonly string[]) => string;
40
+ /** A routing cap left seats out. */
41
+ truncated: string;
42
+ }
43
+ export declare const DELIVERY_RECEIPT_LABELS: DeliveryReceiptLabels;
44
+ export interface DeliveryReceiptProps extends Omit<HTMLAttributes<HTMLElement>, "role" | "children"> {
45
+ /** The seats, in the order the routing reached them. Empty is "no agent". */
46
+ recipients: readonly DeliveryReceiptRecipient[];
47
+ /** `sent` (the default) under a message, `draft` over a composer. */
48
+ tense?: DeliveryReceiptTense | undefined;
49
+ /**
50
+ * A routing cap left seats out (a whole-room address in a very large
51
+ * room), so the list is not everybody the message named.
52
+ */
53
+ truncated?: boolean | undefined;
54
+ /**
55
+ * A short sentence after a "no agent" line, saying why or what to do:
56
+ * "This room has no agent members. Mention one to ask it."
57
+ */
58
+ hint?: ReactNode;
59
+ /**
60
+ * Whether the line is its own polite live region (false by default). A
61
+ * composer's draft line that changes as a mention is typed is announced
62
+ * by passing true; a receipt in a transcript is not, since a log that
63
+ * announced every receipt would talk over the conversation.
64
+ */
65
+ announce?: boolean | undefined;
66
+ labels?: Partial<DeliveryReceiptLabels> | undefined;
67
+ }
68
+ /**
69
+ * Who a person's message reached, and who answered: "Reached pm", then "pm
70
+ * answered"; "Reached qa, sre (every message)"; "Reached no agent". Over a
71
+ * composer, the same line in the future tense says who a post WILL reach
72
+ * before it is sent.
73
+ *
74
+ * QUIET BY DESIGN. It sits in a message's footer at the compact step in the
75
+ * tertiary ink, because it is a fact a person checks rather than reads: the
76
+ * one tone it raises is the warning on a message that reached no agent, the
77
+ * case where a person would otherwise wait for an answer nobody was asked
78
+ * for. Colour is never alone: the mark and the words say it.
79
+ *
80
+ * ONE STOP TO A SCREEN READER: the parts are spans in one paragraph, so a
81
+ * reader moving through the log hears "Reached pm. pm answered" at once. The
82
+ * middle dot the eye reads between the parts is hidden from it, and a full
83
+ * stop only it hears stands in its place; the marks are hidden too.
84
+ */
85
+ export declare function DeliveryReceipt({ recipients, tense, truncated, hint, announce, labels: overrides, className, ...rest }: DeliveryReceiptProps): import("react").JSX.Element;
@@ -0,0 +1,4 @@
1
+ import "../ChatMessage/ChatScale.css";
2
+ import "./DeliveryReceipt.css";
3
+ export { DELIVERY_RECEIPT_LABELS, DeliveryReceipt } from "./DeliveryReceipt.js";
4
+ export type { DeliveryReceiptLabels, DeliveryReceiptProps, DeliveryReceiptRecipient, DeliveryReceiptTense, } from "./DeliveryReceipt.js";
@@ -0,0 +1,3 @@
1
+ export { DELIVERY_RECEIPT_LABELS, DeliveryReceipt } from '../chunk-NGJWRR7E.js';
2
+ import '../chunk-UT6CW3C7.js';
3
+ import '../chunk-ODEQTPFM.js';
@@ -0,0 +1,46 @@
1
+ /*
2
+ * Who a person's message reached and who answered, under its words; and the
3
+ * same line over a composer, before the message is sent.
4
+ */
5
+
6
+ .crewlet-delivery-receipt {
7
+ display: flex;
8
+ flex-wrap: wrap;
9
+ align-items: center;
10
+ column-gap: var(--spacing-1);
11
+ max-width: 100%;
12
+ margin: 0;
13
+ color: var(--color-text-tertiary);
14
+ font-size: var(--crewlet-chat-meta);
15
+ line-height: var(--font-line-height-snug);
16
+ }
17
+
18
+ .crewlet-delivery-receipt__part {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ gap: var(--spacing-1);
22
+ min-width: 0;
23
+ }
24
+
25
+ .crewlet-delivery-receipt__mark {
26
+ display: inline-flex;
27
+ flex: none;
28
+ }
29
+
30
+ .crewlet-delivery-receipt__answered {
31
+ color: var(--color-text-secondary);
32
+ }
33
+
34
+ /* The one raised tone: a message nobody was asked to answer. Its mark takes
35
+ the warning ink, its words the secondary ink; the hint stays quiet. */
36
+ .crewlet-delivery-receipt--none {
37
+ color: var(--color-text-secondary);
38
+ }
39
+
40
+ .crewlet-delivery-receipt--none .crewlet-delivery-receipt__mark {
41
+ color: var(--color-feedback-warning-ink);
42
+ }
43
+
44
+ .crewlet-delivery-receipt__hint {
45
+ color: var(--color-text-tertiary);
46
+ }
@@ -73,13 +73,23 @@ export type InboxRowTone = 'default' | 'danger';
73
73
  export interface InboxRowProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'onClick' | 'title'> {
74
74
  /** Who the row is from: a seat's or a person's name. */
75
75
  title: ReactNode;
76
- /** A word after the name where the row is not a plain question or update: "coding run", "stopped". */
76
+ /**
77
+ * After the name, in the quiet ink: a word where the row is not a plain
78
+ * question or update ("coding run", "stopped"), or what the person did
79
+ * ("replied to a thread in Design"). The name and the qualifier read as one
80
+ * run of words that wraps to a second line before it stops, so a long
81
+ * qualifier never hides the name.
82
+ */
77
83
  qualifier?: ReactNode;
78
84
  /** `danger` sets the qualifier in the danger ink, for a seat that has stopped. */
79
85
  qualifierTone?: InboxRowTone | undefined;
80
86
  /** At the end of the first line: when it arrived. */
81
87
  meta?: ReactNode;
82
- /** Before the words: an avatar, drawn at the target size. */
88
+ /**
89
+ * Before the words: an avatar or a mark, at the target size. Its slot is at
90
+ * least a target square and grows to fit a larger mark, such as a 32px face,
91
+ * with the name's line beside its top.
92
+ */
83
93
  leading?: ReactNode;
84
94
  /** What the row says, in up to three lines. */
85
95
  children?: ReactNode;
@@ -159,7 +169,8 @@ export interface InboxRowProps extends Omit<LiHTMLAttributes<HTMLLIElement>, 'on
159
169
  * caller names in the head (`status`): every word stays legible, and the
160
170
  * state is never carried by a decoration alone.
161
171
  *
162
- * NOTHING IS CUT AT THE EDGE. The name ellipsizes, the time never wraps, and
172
+ * NOTHING IS CUT AT THE EDGE. The name and its qualifier wrap to two lines
173
+ * before they stop, the time keeps a column of its own and never wraps, and
163
174
  * the words take up to three lines before they stop, so a row says what it is
164
175
  * rather than its first few words.
165
176
  */
@@ -1,4 +1,4 @@
1
- export { INBOX_ROW_LEAVE, InboxGroup, InboxList, InboxMore, InboxRow, InboxToggle } from '../chunk-F6ZUM66S.js';
1
+ export { INBOX_ROW_LEAVE, InboxGroup, InboxList, InboxMore, InboxRow, InboxToggle } from '../chunk-VKNZQB2G.js';
2
2
  import '../chunk-RK4XPHVG.js';
3
3
  import '../chunk-6C7GQOBV.js';
4
4
  import '../chunk-PCAT2EFB.js';
@@ -223,19 +223,36 @@
223
223
  }
224
224
 
225
225
  /*
226
- * The avatar is centred on the head's line, the name it belongs to: its box
227
- * is one line tall and the avatar, a target taller than the line, overflows
228
- * it evenly into the control's top pad. Hung from the line's top by a pixel
229
- * it stood 3.6px under the name's middle, its top a step further from the
230
- * row's edge than its side.
226
+ * THE LEADING SLOT FITS WHAT IT HOLDS. It is at least a target square (and at
227
+ * least a line tall), with its mark centred in it, and it grows to a larger
228
+ * mark, such as a 32px face, rather than letting it spill: a fixed one-line
229
+ * box let a 32px face overflow it by 8px, up and to the left, until it all but
230
+ * touched the row's edge.
231
+ *
232
+ * THE NAME'S LINE IS CENTRED ON A TARGET-SIZED MARK by moving the words down
233
+ * rather than the mark up: the words start half the difference between the
234
+ * target and a line lower, and the control's top pad is that much shorter, so
235
+ * a row with a target-sized avatar keeps its height and its name where they
236
+ * were, with the avatar inside the control instead of over its pad. A larger
237
+ * mark hangs from the same top, beside the name's line and the one under it.
231
238
  */
239
+ .crewlet-inbox-row__control--leading {
240
+ --crewlet-inbox-row-lift: max(0px, (var(--size-target-min) - 1lh) / 2);
241
+
242
+ padding-top: calc(var(--spacing-2) + var(--spacing-1) / 2 - var(--crewlet-inbox-row-lift));
243
+ }
244
+
245
+ .crewlet-inbox-row__control--leading > .crewlet-inbox-row__main {
246
+ padding-top: var(--crewlet-inbox-row-lift);
247
+ }
248
+
232
249
  .crewlet-inbox-row__leading {
233
250
  display: inline-flex;
234
251
  flex: none;
235
252
  align-items: center;
236
253
  justify-content: center;
237
- width: var(--size-target-min);
238
- height: 1lh;
254
+ min-width: var(--size-target-min);
255
+ min-height: max(var(--size-target-min), 1lh);
239
256
  }
240
257
 
241
258
  .crewlet-inbox-row__main {
@@ -245,24 +262,49 @@
245
262
  min-width: 0;
246
263
  }
247
264
 
265
+ /*
266
+ * THE HEAD IS ONE RUN OF WORDS: who, then what they did, wrapping as words do
267
+ * and stopping at two lines with an ellipsis. The name comes first, so it is
268
+ * the last thing a long qualifier can push out of sight. As one line of boxes
269
+ * the name was the only one allowed to shrink, so a qualifier longer than a
270
+ * word ("replied to a thread in Product Manager") squeezed the name to
271
+ * nothing and pushed the time past the row's edge.
272
+ *
273
+ * THE TIME FLOATS AT THE END OF THE FIRST LINE, so only that line gives up
274
+ * room to it and the second runs the row's full width: "replied to a thread
275
+ * in #engineering" keeps the room's name, the part of the line most worth
276
+ * reading, where a column of its own took the time's width from both lines.
277
+ * The float sits in a block of its own inside the clamp, which is where every
278
+ * engine lays a float out and still counts the clamped lines.
279
+ */
248
280
  .crewlet-inbox-row__head {
249
- display: flex;
250
- align-items: baseline;
251
- gap: var(--spacing-1);
281
+ display: -webkit-box;
282
+ -webkit-box-orient: vertical;
283
+ -webkit-line-clamp: 2;
284
+ line-clamp: 2;
252
285
  min-width: 0;
286
+ overflow: hidden;
287
+ overflow-wrap: anywhere;
288
+ color: var(--color-text-tertiary);
253
289
  }
254
290
 
255
- .crewlet-inbox-row__title {
256
- min-width: 0;
257
- overflow: hidden;
258
- text-overflow: ellipsis;
291
+ .crewlet-inbox-row__head-text {
292
+ display: block;
293
+ }
294
+
295
+ /* The time (and any status), in the head's own line box, so the time sits on the name's baseline. */
296
+ .crewlet-inbox-row__head-end {
297
+ float: inline-end;
298
+ margin-inline-start: var(--spacing-2);
259
299
  white-space: nowrap;
300
+ }
301
+
302
+ .crewlet-inbox-row__title {
260
303
  font-weight: var(--font-weight-medium);
261
304
  color: var(--color-text-primary);
262
305
  }
263
306
 
264
307
  .crewlet-inbox-row__qualifier {
265
- flex: none;
266
308
  font-size: var(--font-size-xs);
267
309
  color: var(--color-text-tertiary);
268
310
  }
@@ -272,8 +314,6 @@
272
314
  }
273
315
 
274
316
  .crewlet-inbox-row__meta {
275
- flex: none;
276
- margin-inline-start: auto;
277
317
  font-size: var(--font-size-xs);
278
318
  font-variant-numeric: tabular-nums;
279
319
  white-space: nowrap;
@@ -321,21 +361,12 @@
321
361
  color: var(--color-text-secondary);
322
362
  }
323
363
 
364
+ /* At the end of the first line, before the time when the row has both. */
324
365
  .crewlet-inbox-row__status {
325
366
  display: inline-flex;
326
- flex: none;
327
367
  align-items: center;
328
368
  }
329
369
 
330
- /* With a status, the time stays at the end and the status sits beside it. */
331
- .crewlet-inbox-row__status + .crewlet-inbox-row__meta {
332
- margin-inline-start: 0;
333
- }
334
-
335
- .crewlet-inbox-row__head > .crewlet-inbox-row__status {
336
- margin-inline-start: auto;
337
- }
338
-
339
370
  /*
340
371
  * What came of the row: a glyph in the tone's ink, then words in the
341
372
  * secondary ink, on one line that wraps rather than cuts.
@@ -391,7 +422,9 @@
391
422
 
392
423
  /*
393
424
  * The task, under the words and lined up with them: the control's start pad,
394
- * the leading slot and the gap after it.
425
+ * a target-sized leading slot and the gap after it. The task sits outside the
426
+ * control, so it lines up with the words for a mark of the target size, the
427
+ * size `leading` asks for.
395
428
  */
396
429
  .crewlet-inbox-row__aside {
397
430
  min-width: 0;
@@ -1,2 +1,2 @@
1
- export { ListDetail } from '../chunk-SNJM5KPV.js';
1
+ export { ListDetail } from '../chunk-P33VNG7Q.js';
2
2
  import '../chunk-ODEQTPFM.js';
@@ -65,6 +65,12 @@
65
65
  min-height: 0;
66
66
  }
67
67
 
68
+ /* A LIST THAT GROWS SCROLLS IN ITS OWN COLUMN, whatever it holds. The pane
69
+ left the scrolling to its content, which a DirectoryList does for its
70
+ groups and an InboxList does not: an activity list taller than the screen
71
+ ran past the pane's end, where the screen clipped its later rows and its
72
+ "older" link, with nothing to scroll them into view. A list that scrolls
73
+ itself fills the pane and never makes it overflow. */
68
74
  .crewlet-list-detail__list {
69
75
  display: flex;
70
76
  flex-direction: column;
@@ -72,6 +78,7 @@
72
78
  min-height: 0;
73
79
  padding-right: var(--crewlet-list-detail-gutter);
74
80
  border-right: 1px solid var(--color-border-default);
81
+ overflow-y: auto;
75
82
  }
76
83
 
77
84
  /* A DIRECTORY RUNS TO THE HAIRLINE. A DirectoryList is navigation: its lines
@@ -106,7 +113,6 @@
106
113
  .crewlet-list-detail__list {
107
114
  padding-right: 0;
108
115
  border-right: 0;
109
- overflow-y: auto;
110
116
  }
111
117
 
112
118
  /* The list is the pane that scrolls now, and takes the detail's end where
@@ -1,6 +1,6 @@
1
- export { PresenterView, formatStopwatch } from '../chunk-WCJTOKTY.js';
2
- import '../chunk-O6EV6GKI.js';
1
+ export { PresenterView, formatStopwatch } from '../chunk-LMEMH6DO.js';
3
2
  import '../chunk-5AJR6X75.js';
3
+ import '../chunk-O6EV6GKI.js';
4
4
  import '../chunk-RK4XPHVG.js';
5
5
  import '../chunk-DGXZ635L.js';
6
6
  import '../chunk-XSPCITPN.js';
@@ -1,6 +1,6 @@
1
- export { RAIL_LIST_LABELS, RailList } from '../chunk-REWKR3S4.js';
2
- import '../chunk-BRK3HXGZ.js';
1
+ export { RAIL_LIST_LABELS, RailList } from '../chunk-M576OESX.js';
3
2
  import '../chunk-3IWZVEXA.js';
3
+ import '../chunk-BRK3HXGZ.js';
4
4
  import '../chunk-O5CEG7TQ.js';
5
5
  import '../chunk-YGRTX67J.js';
6
6
  import '../chunk-TEV5REGB.js';
@@ -1,4 +1,4 @@
1
- export { RichEditor, preloadRichEditor } from '../chunk-3AMWXJQX.js';
1
+ export { RichEditor, preloadRichEditor } from '../chunk-AUSFKGKL.js';
2
2
  export { RICH_EDITOR_LABELS, richEditorLabels } from '../chunk-2DMTW2Y3.js';
3
3
  import '../chunk-KOJPDCGQ.js';
4
4
  import '../chunk-KBZO4B33.js';
@@ -1,6 +1,6 @@
1
- export { SetupWizard } from '../chunk-WRZPO4YL.js';
2
- import '../chunk-TMML7BHT.js';
1
+ export { SetupWizard } from '../chunk-3BVNNQZY.js';
3
2
  import '../chunk-FPDIHEM4.js';
3
+ import '../chunk-TMML7BHT.js';
4
4
  import '../chunk-YJQXESOJ.js';
5
5
  import '../chunk-CYQBDZ6M.js';
6
6
  import '../chunk-NAHA62WD.js';
@@ -9,7 +9,7 @@ import { jsx } from 'react/jsx-runtime';
9
9
  var engine = null;
10
10
  var loading = null;
11
11
  function loadEngine() {
12
- loading ??= import('./Editor-VGNB35NJ.js').then(
12
+ loading ??= import('./Editor-5D5Z3LBQ.js').then(
13
13
  (module) => {
14
14
  engine = module.Editor;
15
15
  return engine;
@@ -0,0 +1,106 @@
1
+ import { VisuallyHidden } from './chunk-UT6CW3C7.js';
2
+ import { cx } from './chunk-ODEQTPFM.js';
3
+ import './ChatScale-L4SIHSKL.css';
4
+ import './DeliveryReceipt-RMZW2DMT.css';
5
+ import { CheckGlyph, TriangleAlertGlyph } from '@crewlethq/icons/glyphs';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
7
+
8
+ var DELIVERY_RECEIPT_LABELS = {
9
+ reached: (who) => `Reached ${who}`,
10
+ reaches: (who) => `Will reach ${who}`,
11
+ none: "Reached no agent",
12
+ noneDraft: "Will reach no agent",
13
+ answered: (who) => `${who} answered`,
14
+ list: (names) => names.join(", "),
15
+ reason: (words) => `(${words})`,
16
+ groups: (groups) => groups.join("; "),
17
+ truncated: "Some seats were not reached"
18
+ };
19
+ function reachOf(recipients, labels) {
20
+ const order = [];
21
+ const byReason = /* @__PURE__ */ new Map();
22
+ for (const r of recipients) {
23
+ const key = r.reason ?? "";
24
+ const names = byReason.get(key);
25
+ if (names) {
26
+ names.push(r.name);
27
+ } else {
28
+ order.push(key);
29
+ byReason.set(key, [r.name]);
30
+ }
31
+ }
32
+ return labels.groups(
33
+ order.map((key) => {
34
+ const list = labels.list(byReason.get(key) ?? []);
35
+ return key ? `${list} ${labels.reason(key)}` : list;
36
+ })
37
+ );
38
+ }
39
+ function DeliveryReceipt({
40
+ recipients,
41
+ tense = "sent",
42
+ truncated = false,
43
+ hint,
44
+ announce = false,
45
+ labels: overrides,
46
+ className,
47
+ ...rest
48
+ }) {
49
+ const labels = overrides ? { ...DELIVERY_RECEIPT_LABELS, ...overrides } : DELIVERY_RECEIPT_LABELS;
50
+ const none = recipients.length === 0;
51
+ const answered = tense === "sent" ? recipients.filter((r) => r.answered).map((r) => r.name) : [];
52
+ const reach = none ? tense === "draft" ? labels.noneDraft : labels.none : (tense === "draft" ? labels.reaches : labels.reached)(
53
+ reachOf(recipients, labels)
54
+ );
55
+ const parts = [
56
+ {
57
+ key: "reach",
58
+ mark: none ? /* @__PURE__ */ jsx(TriangleAlertGlyph, { size: "xs" }) : void 0,
59
+ words: reach
60
+ }
61
+ ];
62
+ if (truncated && !none)
63
+ parts.push({ key: "truncated", words: labels.truncated });
64
+ if (answered.length > 0) {
65
+ parts.push({
66
+ key: "answered",
67
+ className: "crewlet-delivery-receipt__answered",
68
+ mark: /* @__PURE__ */ jsx(CheckGlyph, { size: "xs" }),
69
+ words: labels.answered(labels.list(answered))
70
+ });
71
+ }
72
+ if (none && hint)
73
+ parts.push({
74
+ key: "hint",
75
+ className: "crewlet-delivery-receipt__hint",
76
+ words: hint
77
+ });
78
+ return /* @__PURE__ */ jsx(
79
+ "p",
80
+ {
81
+ ...rest,
82
+ role: announce ? "status" : void 0,
83
+ "data-tense": tense,
84
+ className: cx(
85
+ "crewlet-delivery-receipt",
86
+ none && "crewlet-delivery-receipt--none",
87
+ className
88
+ ),
89
+ children: parts.map((part, i) => /* @__PURE__ */ jsxs(
90
+ "span",
91
+ {
92
+ className: cx("crewlet-delivery-receipt__part", part.className),
93
+ children: [
94
+ i > 0 ? /* @__PURE__ */ jsx("span", { className: "crewlet-delivery-receipt__dot", "aria-hidden": "true", children: "\xB7" }) : null,
95
+ part.mark ? /* @__PURE__ */ jsx("span", { className: "crewlet-delivery-receipt__mark", "aria-hidden": "true", children: part.mark }) : null,
96
+ /* @__PURE__ */ jsx("span", { children: part.words }),
97
+ i < parts.length - 1 ? /* @__PURE__ */ jsx(VisuallyHidden, { children: ". " }) : null
98
+ ]
99
+ },
100
+ part.key
101
+ ))
102
+ }
103
+ );
104
+ }
105
+
106
+ export { DELIVERY_RECEIPT_LABELS, DeliveryReceipt };
@@ -1,5 +1,5 @@
1
1
  import { cx } from './chunk-ODEQTPFM.js';
2
- import './ListDetail-Q7SFVKSO.css';
2
+ import './ListDetail-WXBCLONL.css';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
 
5
5
  function ListDetail({ list, children, showing = "list", className, ...rest }) {
@@ -4,7 +4,7 @@ import { usePresence, releaseFocus } from './chunk-PCAT2EFB.js';
4
4
  import { useHeadingLevel, headingTag } from './chunk-PQADV6P6.js';
5
5
  import { VisuallyHidden } from './chunk-UT6CW3C7.js';
6
6
  import { cx } from './chunk-ODEQTPFM.js';
7
- import './InboxList-X5Q5WWAI.css';
7
+ import './InboxList-NVPIL3WU.css';
8
8
  import { useId, useState, useRef, useLayoutEffect, useEffect, useCallback, cloneElement, Children, isValidElement } from 'react';
9
9
  import { ChevronDownGlyph } from '@crewlethq/icons/glyphs';
10
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -222,60 +222,81 @@ function InboxRow({
222
222
  told.current = true;
223
223
  onLeft?.();
224
224
  }, [gone, onLeft]);
225
+ const ids = useId();
226
+ const part = (name) => `${ids}-${name}`;
227
+ const hasQualifier = qualifier !== void 0;
228
+ const hasStatus = status !== void 0 && status !== null;
229
+ const hasMeta = meta !== void 0;
230
+ const hasOutcome = outcome !== void 0 && outcome !== null;
231
+ const labelledBy = [
232
+ leading === void 0 ? null : part("leading"),
233
+ part("title"),
234
+ hasQualifier ? part("qualifier") : null,
235
+ hasStatus || hasMeta ? part("end") : null,
236
+ part("text"),
237
+ hasOutcome ? part("outcome") : null,
238
+ unread ? part("unread") : null,
239
+ done ? part("done") : null
240
+ ].filter(Boolean).join(" ");
225
241
  const contents = /* @__PURE__ */ jsxs(Fragment, { children: [
226
242
  leading === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
227
- /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__leading", children: leading }),
243
+ /* @__PURE__ */ jsx("span", { id: part("leading"), className: "crewlet-inbox-row__leading", children: leading }),
228
244
  " "
229
245
  ] }),
230
246
  /* @__PURE__ */ jsxs("span", { className: "crewlet-inbox-row__main", children: [
231
- /* @__PURE__ */ jsxs("span", { className: "crewlet-inbox-row__head", children: [
232
- /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__title", children: title }),
233
- qualifier === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
247
+ /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__head", children: /* @__PURE__ */ jsxs("span", { className: "crewlet-inbox-row__head-text", children: [
248
+ hasStatus || hasMeta ? /* @__PURE__ */ jsxs("span", { id: part("end"), className: "crewlet-inbox-row__head-end", children: [
249
+ hasStatus ? /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__status", children: status }) : null,
250
+ hasStatus && hasMeta ? " " : null,
251
+ hasMeta ? /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__meta", children: meta }) : null
252
+ ] }) : null,
253
+ /* @__PURE__ */ jsx("span", { id: part("title"), className: "crewlet-inbox-row__title", children: title }),
254
+ hasQualifier ? /* @__PURE__ */ jsxs(Fragment, { children: [
234
255
  " ",
235
- /* @__PURE__ */ jsx("span", { className: cx("crewlet-inbox-row__qualifier", qualifierTone === "danger" && "crewlet-inbox-row__qualifier--danger"), children: qualifier })
236
- ] }),
237
- status === void 0 || status === null ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
238
- " ",
239
- /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__status", children: status })
240
- ] }),
241
- meta === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
242
- " ",
243
- /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__meta", children: meta })
244
- ] })
245
- ] }),
256
+ /* @__PURE__ */ jsx(
257
+ "span",
258
+ {
259
+ id: part("qualifier"),
260
+ className: cx("crewlet-inbox-row__qualifier", qualifierTone === "danger" && "crewlet-inbox-row__qualifier--danger"),
261
+ children: qualifier
262
+ }
263
+ )
264
+ ] }) : null
265
+ ] }) }),
246
266
  " ",
247
- /* @__PURE__ */ jsxs("span", { className: "crewlet-inbox-row__text", children: [
267
+ /* @__PURE__ */ jsxs("span", { id: part("text"), className: "crewlet-inbox-row__text", children: [
248
268
  children,
249
269
  excerpt === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
250
270
  " ",
251
271
  /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__excerpt", children: excerpt })
252
272
  ] })
253
273
  ] }),
254
- outcome === void 0 || outcome === null ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
274
+ hasOutcome ? /* @__PURE__ */ jsxs(Fragment, { children: [
255
275
  " ",
256
- /* @__PURE__ */ jsxs("span", { className: cx("crewlet-inbox-row__outcome", `crewlet-inbox-row__outcome--${outcomeTone}`), children: [
276
+ /* @__PURE__ */ jsxs("span", { id: part("outcome"), className: cx("crewlet-inbox-row__outcome", `crewlet-inbox-row__outcome--${outcomeTone}`), children: [
257
277
  outcomeIcon === void 0 ? null : /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "crewlet-inbox-row__outcome-icon", children: outcomeIcon }),
258
278
  /* @__PURE__ */ jsx("span", { className: "crewlet-inbox-row__outcome-text", children: outcome })
259
279
  ] })
260
- ] }),
280
+ ] }) : null,
261
281
  unread ? /* @__PURE__ */ jsxs(Fragment, { children: [
262
282
  " ",
263
- /* @__PURE__ */ jsx(VisuallyHidden, { children: unreadLabel })
283
+ /* @__PURE__ */ jsx(VisuallyHidden, { id: part("unread"), children: unreadLabel })
264
284
  ] }) : null,
265
285
  done ? /* @__PURE__ */ jsxs(Fragment, { children: [
266
286
  " ",
267
- /* @__PURE__ */ jsx(VisuallyHidden, { children: doneLabel })
287
+ /* @__PURE__ */ jsx(VisuallyHidden, { id: part("done"), children: doneLabel })
268
288
  ] }) : null
269
289
  ] })
270
290
  ] });
271
291
  const controlClass = cx(
272
292
  "crewlet-inbox-row__control",
293
+ leading !== void 0 && "crewlet-inbox-row__control--leading",
273
294
  unread && "crewlet-inbox-row__control--unread",
274
295
  actionable && "crewlet-inbox-row__control--actionable",
275
296
  done && "crewlet-inbox-row__control--done"
276
297
  );
277
298
  const current = selected ? "true" : void 0;
278
- const control = href !== void 0 ? /* @__PURE__ */ jsx("a", { className: controlClass, href, "aria-current": current, onClick, children: contents }) : /* @__PURE__ */ jsx("button", { type: "button", className: controlClass, "aria-current": current, onClick, children: contents });
299
+ const control = href !== void 0 ? /* @__PURE__ */ jsx("a", { className: controlClass, href, "aria-current": current, "aria-labelledby": labelledBy, onClick, children: contents }) : /* @__PURE__ */ jsx("button", { type: "button", className: controlClass, "aria-current": current, "aria-labelledby": labelledBy, onClick, children: contents });
279
300
  if (gone) return null;
280
301
  return /* @__PURE__ */ jsx(
281
302
  "li",