@stackone/malachite 2.26.0 → 2.27.1

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/COMPONENTS.md CHANGED
@@ -79,7 +79,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
79
79
  - **`SegmentedControl`** — A rounded track of mutually-exclusive options — the segmented “one of these” control. Owns the track background, the selected-thumb chrome and the `radiogroup` semantics, so consumers never hand-roll a track around `PillButton` (whose `default` variant tokens are tuned for standalone filter pills, not a sunken track). Single tab stop with arrow-key move-and-select; selection is conveyed via `aria-checked` and a foreground change, not background colour alone. Restyle via the `segmentedControl` theme tokens. _Key props:_ `options` (`value`, `label`, `icon`, `disabled`), `value`, `onChange`, `SegmentedControlSize` (`small` | `medium` | `large`), `fullWidth`, `disabled`, `aria-label`, `aria-labelledby`, `className`.
80
80
  - **`Toggle`** — On/off switch. Add `label` for a visible caption, and `variant` to give it standalone toolbar-filter chrome — pick the family it sits beside: `pill` matches the filter-bar inputs (`Search`, `MultiSelect`), `button` renders the whole control as an outline `Button` with the switch as its trailing icon. `bordered` is shorthand for `variant="pill"`. `tone="pending"` paints the checked track amber, for a switch whose change has not been written yet — pair it with a `SelectionBar` reporting the same diff. The unchecked track never changes: nothing is pending about a switch that is off. Forwards a ref to its root element, so it can anchor an `InlineConfirm`. _Key props:_ `checked`, `onChange`, `disabled`, `ToggleSize` (`small` | `medium`), `ToggleTone` (`default` | `pending`), `label`, `ToggleVariant` (`pill` | `button`), `bordered`, `aria-label`, `aria-labelledby`, `aria-describedby`, `id`.
81
81
  - **`Dropdown`** — Single-select dropdown. `DropdownSize` is `small` (28px) | `medium` (32px) | `large` (42px), matching Button; `fullWidth` fills the container instead of sizing to content, which is what a table cell or a toolbar slot needs — without it the field column sizes to its content, so a consumer has to compute a width; `width` fixes that width outright, the way `MultiSelect` does, for a toolbar lining the two up rather than letting each hug its own label — a narrower container still wins, since the width clamps to it; Until this release a `search` trigger could not go below the intrinsic width of its bare `input`, so a `width` under roughly 200px was ignored; it now shrinks with the rest. The open menu fills the trigger and left-aligns under it, as Figma draws every tier of `Dropdown Component - Standard`, so `width` sizes both — the menu is the width of the control that opened it and does not widen for a long option, which ellipsises instead, so a row of controls and their open menus stay on one set of edges; below 160px the menu keeps that floor rather than shrinking to a chip nobody can read, and `minWidth` raises that floor without widening the trigger — for a picker a column pins narrow whose options carry a subtitle the trigger never shows; near a viewport edge it anchors to the trigger's other side before falling back to the viewport, so the two share an edge wherever one fits; `small` is the chip that sits beside a page title and rounds tighter (10px) than the taller sizes. These names each moved up a tier in 0.71 — the old `xsmall`/`small`/`medium` are now `small`/`medium`/`large`. Pass `value` for a controlled selection whose source of truth lives elsewhere (URL or app state): the trigger follows it without the consumer remounting via `key`, which used to discard focus mid-interaction. `defaultValue` keeps the uncontrolled behaviour. `clearable` adds a clear control to the trigger once something is selected and reports it as `onItemSelected(undefined)` — without it a selection cannot be undone, so filter bars had to invent a sentinel "any" item to carry that meaning. `requiredIndicator` switches the label between the library default of naming the optional fields, an `asterisk` on the required ones for designs drawn that way, and `none` for a field whose value is supplied rather than asked for — a locked control is still required, but a marker only sends the reader looking for something to fill in. Visual only: `aria-required` is set from `required` whichever is chosen. `showSubtitleInTrigger` promotes the selected item's `subtitle` into the closed trigger, on one line under the value, so the consequence of a choice stays on screen once the menu shuts — the trigger trades its fixed height for a floor and grows by that line, and either line too long for it ellipsises, with a single tooltip carrying whichever lines are cut off, value first. It does not combine with `search`, whose trigger is a single-line text input with nowhere to put a second line, so the type rejects the pair rather than letting a consumer ask for a subtitle and get none. `subtitleLayout` decides how a subtitle sits in the open menu: the default `inline` keeps it on the label's row after a divider, which suits a short qualifier, while `stacked` gives it its own line beneath the label — one line either way, ellipsised rather than wrapped, so inline a sentence-length subtitle is cut back to a few words. Every option row carries a tooltip for whatever it had to cut, across all three of label, subtitle and `rightSideText`, so nothing the row hides is unreadable. Passing `formControl` is enough to show validation on the same terms as `Input`, though only validation: the trigger still paints `value` or `defaultValue`, never the value the form holds. The resolver's message renders on its own line beneath the field in the destructive colour, politely announced, with `aria-invalid` and `aria-describedby` wired, while an explicit `helperText` keeps its own line above it and stays dimmed, and `error={false}` suppresses the resolver outright. _Key props:_ `DropdownItem[]`, `DropdownSize`, `fullWidth`, `width`, `value`, `defaultValue`, `label`, `search`, `showSubtitleInTrigger`, `subtitleLayout`, `clearable`, `required`, `requiredIndicator`, `error`, `helperText`, `loading`, `formControl`, `aria-label`, `aria-labelledby`, `id`.
82
- - **`MultiSelect`** — Multi-select dropdown. `width` fixes the trigger, as `Dropdown` and `Search` take it, so a toolbar can put its controls on one width — and, as with those two, a container narrower than the value still wins; the menu fills the trigger and left-aligns under it, so that one prop sizes both; `minWidth` raises the 160px floor a narrow trigger falls back to. `requiredIndicator` follows the same three-way convention the single-select controls do, so a form choosing asterisks does not get "(Optional)" on one of its fields. _Key props:_ `MultiSelectOption[]`, `MultiSelectSize`, `width`, `minWidth`, `label`, `searchable`, `inlineSearch`, `chips`, `chipsRemovable`, `required`, `requiredIndicator`, `aria-label`, `aria-labelledby`, `id`.
82
+ - **`MultiSelect`** — Multi-select dropdown. `width` fixes the trigger, as `Dropdown` and `Search` take it, so a toolbar can put its controls on one width — and, as with those two, a container narrower than the value still wins; the menu fills the trigger and left-aligns under it, so that one prop sizes both; `minWidth` raises the 160px floor a narrow trigger falls back to. An option label too long for the menu ellipsises, and hovering it shows the full label — above the description in `hover` mode — as `Dropdown` does for its options. `requiredIndicator` follows the same three-way convention the single-select controls do, so a form choosing asterisks does not get "(Optional)" on one of its fields. _Key props:_ `MultiSelectOption[]`, `MultiSelectSize`, `width`, `minWidth`, `label`, `searchable`, `inlineSearch`, `chips`, `chipsRemovable`, `required`, `requiredIndicator`, `aria-label`, `aria-labelledby`, `id`.
83
83
  - **`QueryInput`** — A textbox with inline syntax colouring and a suggestion menu anchored to the caret. It carries no grammar of its own: the consumer parses the value, hands back `tokens` to colour and `suggestions` to offer, so the vocabulary lives in the consuming app rather than here. _Key props:_ `value`, `onChange`, `tokens`, `suggestions`, `activeRange`, `onAccept`, `onQueryContextChange`, `onSubmit`, `placeholder`, `size`, `disabled`, `loading`, `emptyMessage`, `className`, `style`, `aria-label`, `aria-labelledby`.
84
84
  - **`SettingRow`** — One setting: a labelled row with its control on the trailing edge, and the sentence that explains it underneath. `control` may be a render prop receiving the label’s id, so a control that no `<label htmlFor>` can reach — `Toggle` renders a `<button role="switch">` — still names itself via `aria-labelledby`. _Key props:_ `label`, `badge`, `description`, `control`, `className`.
85
85
 
@@ -1,129 +1,137 @@
1
- import { jsxs as a, jsx as r, Fragment as z } from "react/jsx-runtime";
2
- import { useState as $ } from "react";
3
- import { Button as b } from "../Button.js";
4
- import w from "../CustomIcons/SparkleIcon.js";
5
- import { Tag as S } from "../Tag.js";
6
- import C from "../Tooltip.js";
7
- import { Typography as n } from "../Typography/index.js";
8
- const N = {
1
+ import { jsxs as r, jsx as t, Fragment as T } from "react/jsx-runtime";
2
+ import { useState as S } from "react";
3
+ import { Button as N } from "../Button.js";
4
+ import C from "../CustomIcons/SparkleIcon.js";
5
+ import I from "../Tooltip.js";
6
+ import { Typography as l } from "../Typography/index.js";
7
+ const w = {
9
8
  changed: "Changed",
10
9
  added: "Added",
11
10
  removed: "To be removed"
12
- }, I = (e, t) => e === "removed" ? `Marked for removal by ${t}. Accept to remove it, or revert to keep it.` : `${N[e]} by ${t}. Accept to keep it, or revert to restore the previous value.`, R = (e) => e == null ? !0 : typeof e == "string" ? e.trim() === "" : Array.isArray(e) ? e.length === 0 : !1, P = ({
11
+ }, R = (e, s) => e === "removed" ? `Marked for removal by ${s}. Accept to remove it, or revert to keep it.` : `${w[e]} by ${s}. Accept to keep it, or revert to restore the previous value.`, j = (e) => e == null ? !0 : typeof e == "string" ? e.trim() === "" : Array.isArray(e) ? e.length === 0 : !1, L = ({
13
12
  label: e,
14
- fieldName: t,
13
+ fieldName: s,
15
14
  before: o,
16
- after: s,
15
+ after: a,
17
16
  kind: c = "changed",
18
17
  origin: d = "assistant",
19
18
  children: m,
20
19
  onApprove: A,
21
- onDeny: k,
20
+ onDeny: $,
21
+ headerActions: p,
22
22
  onPreviewRevert: i,
23
- disabled: p = !1,
23
+ disabled: f = !1,
24
24
  className: h = ""
25
25
  }) => {
26
- const [T, f] = $(!1), l = t ?? e, v = () => {
27
- i !== void 0 && (f(!0), i(!0));
28
- }, g = () => {
29
- i !== void 0 && (f(!1), i(!1));
30
- }, u = /* @__PURE__ */ a("div", { className: "flex flex-wrap items-center gap-2", children: [
31
- e !== void 0 && /* @__PURE__ */ r(n.Text, { size: "small", fontWeight: "semi-bold", children: e }),
32
- /* @__PURE__ */ r(C, { text: I(c, d), children: /* @__PURE__ */ r("span", { "aria-hidden": "true", children: /* @__PURE__ */ r(
33
- S,
34
- {
35
- type: "assistant",
36
- value: "AI",
37
- icon: /* @__PURE__ */ r(w, { size: 11 }),
38
- fontSize: "xsmall",
39
- paddingSize: "compact",
40
- textTransform: "none"
41
- }
42
- ) }) }),
43
- /* @__PURE__ */ r("span", { className: "sr-only", children: `${N[c]} by ${d}` })
44
- ] }), x = /* @__PURE__ */ a(z, { children: [
45
- (o !== void 0 || s !== void 0) && /* @__PURE__ */ a("div", { className: "flex flex-wrap items-baseline gap-1", children: [
46
- /* @__PURE__ */ a(n.SecondaryText, { size: "small", children: [
26
+ const [z, g] = S(!1), n = s ?? (typeof e == "string" ? e : void 0), u = () => {
27
+ i !== void 0 && (g(!0), i(!0));
28
+ }, v = () => {
29
+ i !== void 0 && (g(!1), i(!1));
30
+ }, x = /* @__PURE__ */ r("div", { className: "flex flex-wrap items-center gap-2", children: [
31
+ typeof e == "string" ? /* @__PURE__ */ t(l.Text, { size: "small", fontWeight: "semi-bold", children: e }) : e,
32
+ /* @__PURE__ */ t("span", { className: "sr-only", children: `${w[c]} by ${d}` })
33
+ ] }), y = { fontSize: 11, lineHeight: 1 }, b = /* @__PURE__ */ r(
34
+ "div",
35
+ {
36
+ className: "flex flex-shrink-0 items-center gap-1 rounded-md px-1",
37
+ style: { background: "var(--malachite-tag-assistant-background)", height: 24 },
38
+ children: [
39
+ /* @__PURE__ */ t(I, { text: R(c, d), children: /* @__PURE__ */ t(
40
+ "span",
41
+ {
42
+ "aria-hidden": "true",
43
+ className: "flex flex-shrink-0 items-center justify-center",
44
+ style: { color: "var(--malachite-tag-assistant-foreground)" },
45
+ children: /* @__PURE__ */ t(C, { size: 11 })
46
+ }
47
+ ) }),
48
+ /* @__PURE__ */ t(
49
+ N,
50
+ {
51
+ variant: "destructive",
52
+ size: "xsmall",
53
+ iconOnly: !0,
54
+ "aria-label": n === void 0 ? "Revert change" : `Revert ${n}`,
55
+ disabled: f,
56
+ onClick: $,
57
+ onMouseEnter: u,
58
+ onMouseLeave: v,
59
+ onFocus: u,
60
+ onBlur: v,
61
+ icon: (
62
+ /* Reload rather than a cross: reverting restores the previous value rather
63
+ than dismissing the field. */
64
+ /* @__PURE__ */ t("span", { "aria-hidden": "true", style: y, children: "↺" })
65
+ )
66
+ }
67
+ ),
68
+ /* @__PURE__ */ t(
69
+ N,
70
+ {
71
+ variant: "success",
72
+ size: "xsmall",
73
+ iconOnly: !0,
74
+ "aria-label": n === void 0 ? "Accept change" : `Accept ${n}`,
75
+ disabled: f,
76
+ onClick: A,
77
+ icon: /* @__PURE__ */ t("span", { "aria-hidden": "true", style: y, children: "✓" })
78
+ }
79
+ )
80
+ ]
81
+ }
82
+ ), k = /* @__PURE__ */ r(T, { children: [
83
+ (o !== void 0 || a !== void 0) && /* @__PURE__ */ r("div", { className: "flex flex-wrap items-baseline gap-1", children: [
84
+ /* @__PURE__ */ r(l.SecondaryText, { size: "small", children: [
47
85
  "Was ",
48
- R(o) ? /* @__PURE__ */ r("i", { children: "empty" }) : /* @__PURE__ */ r("s", { children: o }),
49
- s !== void 0 ? " →" : null
86
+ j(o) ? /* @__PURE__ */ t("i", { children: "empty" }) : /* @__PURE__ */ t("s", { children: o }),
87
+ a !== void 0 ? " →" : null
50
88
  ] }),
51
- s !== void 0 && /* @__PURE__ */ r(
52
- n.Text,
89
+ a !== void 0 && /* @__PURE__ */ t(
90
+ l.Text,
53
91
  {
54
92
  size: "small",
55
93
  style: { color: "var(--malachite-assistant-foreground)" },
56
- children: s
94
+ children: a
57
95
  }
58
96
  )
59
97
  ] }),
60
- T && /* @__PURE__ */ r(n.SecondaryText, { size: "small", children: "Previewing the revert — release to keep the AI value" })
61
- ] }), y = /* @__PURE__ */ a("div", { className: "flex flex-shrink-0 items-center gap-1", children: [
62
- /* @__PURE__ */ a(
63
- b,
64
- {
65
- variant: "surface",
66
- size: "xsmall",
67
- "aria-label": l === void 0 ? "Revert change" : `Revert ${l}`,
68
- disabled: p,
69
- onClick: k,
70
- onMouseEnter: v,
71
- onMouseLeave: g,
72
- onFocus: v,
73
- onBlur: g,
74
- className: "hover:border-[var(--malachite-assistant-foreground)] hover:text-[var(--malachite-assistant-foreground)]",
75
- children: [
76
- /* @__PURE__ */ r("span", { "aria-hidden": "true", className: "mr-1", children: "↺" }),
77
- "Revert"
78
- ]
79
- }
80
- ),
81
- /* @__PURE__ */ a(
82
- b,
83
- {
84
- variant: "filled",
85
- size: "xsmall",
86
- "aria-label": l === void 0 ? "Accept change" : `Accept ${l}`,
87
- disabled: p,
88
- onClick: A,
89
- children: [
90
- /* @__PURE__ */ r("span", { "aria-hidden": "true", className: "mr-1", children: "✓" }),
91
- "Accept"
92
- ]
93
- }
94
- )
98
+ z && /* @__PURE__ */ t(l.SecondaryText, { size: "small", children: "Previewing the revert — release to keep the AI value" })
95
99
  ] });
96
- return m !== void 0 ? /* @__PURE__ */ a(
100
+ return m !== void 0 ? /* @__PURE__ */ r(
97
101
  "div",
98
102
  {
99
103
  className: `flex flex-col gap-2 rounded-lg p-2 ${h}`,
100
- style: { background: "var(--malachite-tag-assistant-background)" },
104
+ style: { background: "var(--malachite-assistant-surface-background)" },
101
105
  children: [
102
- /* @__PURE__ */ a("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
103
- u,
104
- y
106
+ /* @__PURE__ */ r("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
107
+ x,
108
+ /* @__PURE__ */ r("div", { className: "flex flex-wrap items-center gap-2", children: [
109
+ p,
110
+ b
111
+ ] })
105
112
  ] }),
106
113
  m,
107
- /* @__PURE__ */ r("div", { className: "flex flex-col gap-1", children: x })
114
+ /* @__PURE__ */ t("div", { className: "flex flex-col gap-1", children: k })
108
115
  ]
109
116
  }
110
- ) : /* @__PURE__ */ a(
117
+ ) : /* @__PURE__ */ r(
111
118
  "div",
112
119
  {
113
120
  className: `flex items-start gap-2 rounded-lg p-2 ${h}`,
114
- style: { background: "var(--malachite-tag-assistant-background)" },
121
+ style: { background: "var(--malachite-assistant-surface-background)" },
115
122
  children: [
116
- /* @__PURE__ */ a("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
117
- u,
118
- x
123
+ /* @__PURE__ */ r("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
124
+ x,
125
+ k
119
126
  ] }),
120
- y
127
+ p,
128
+ b
121
129
  ]
122
130
  }
123
131
  );
124
132
  };
125
133
  export {
126
- P as ChangeReviewItem,
127
- P as default
134
+ L as ChangeReviewItem,
135
+ L as default
128
136
  };
129
137
  //# sourceMappingURL=ChangeReviewItem.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChangeReviewItem.js","sources":["../../../src/components/ChangeReview/ChangeReviewItem.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport Button from '../Button';\nimport SparkleIcon from '../CustomIcons/SparkleIcon';\nimport Tag from '../Tag';\nimport Tooltip from '../Tooltip';\nimport { Typography } from '../Typography';\n\nexport type ChangeKind = 'changed' | 'added' | 'removed';\n\nexport interface ChangeReviewItemProps {\n /**\n * What changed — a field name, or the name of a group of them. Omit it when the field this\n * item annotates already carries its own visible heading elsewhere on the page (so the name\n * doesn't render twice) — pass `fieldName` instead, so the badge and actions still know what\n * they belong to.\n */\n label?: string;\n /**\n * Names the field the badge and actions act on, for assistive tech. Defaults to `label`, so\n * passing only `label` keeps working as before. Set it explicitly when `label` is omitted —\n * otherwise \"Accept\"/\"Revert\" would have nothing telling a screen reader which field they act\n * on.\n */\n fieldName?: string;\n /** How it read before. A node, so a caller can show chips or code rather than a string. */\n before?: React.ReactNode;\n /** How it reads now. Usually omitted: the field itself is showing it. */\n after?: React.ReactNode;\n kind?: ChangeKind;\n /** What made the change. Names the assistant by default. */\n origin?: string;\n /**\n * The field the change is annotating. When given, the tag/label/actions form a header band\n * above it and the before/after line moves below it, so \"Was …\" reads next to the value it\n * describes instead of stacked with the header. Omit it for the standalone card.\n */\n children?: React.ReactNode;\n onApprove: () => void;\n onDeny: () => void;\n /**\n * Fires while the revert control is hovered or focused, and again when it stops being\n * either. A callback rather than internal state because the thing being previewed — the\n * field this change came from — lives elsewhere on the page, outside what `Item` renders;\n * only the caller can swap what that field shows.\n */\n onPreviewRevert?: (previewing: boolean) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst KIND_WORDS: Record<ChangeKind, string> = {\n changed: 'Changed',\n added: 'Added',\n removed: 'To be removed',\n};\n\n/**\n * What the \"AI\" pill means, for its tooltip. The badge is two letters, which says who wrote\n * the value but not that the reader is expected to do something about it — and the accept and\n * revert controls it refers to are hidden until the row is hovered or focused.\n */\nconst badgeTooltip = (kind: ChangeKind, origin: string): string =>\n kind === 'removed'\n ? `Marked for removal by ${origin}. Accept to remove it, or revert to keep it.`\n : `${KIND_WORDS[kind]} by ${origin}. Accept to keep it, or revert to restore the previous value.`;\n\n// A multi-select with nothing chosen reaches this as `[]`, not `undefined` — that, a blank or\n// whitespace-only string, and an absent value all read as \"nothing was there\" rather than\n// printing nothing (or, worse, the literal word \"undefined\").\nconst isEmptyDisplacedValue = (value: React.ReactNode): boolean => {\n if (value === undefined || value === null) {\n return true;\n }\n if (typeof value === 'string') {\n return value.trim() === '';\n }\n if (Array.isArray(value)) {\n return value.length === 0;\n }\n return false;\n};\n\n/**\n * Says that something on screen was changed by a machine, shows what it displaced, and offers\n * the two ways out.\n *\n * Accept is deliberately the heavier control (filled, dark) against Revert's quiet outline —\n * the design leans toward accepting the assistant's change rather than presenting both as\n * equal options. That is the designer's call to make, not this component's.\n *\n * The actions are always visible. Revealing them on hover was tried and dropped: a change\n * waiting on a decision is the point of the row, and hiding the only two controls that\n * resolve it made them easy to miss — and unreachable on a touch screen, which has no hover\n * to reveal them with.\n */\nconst ChangeReviewItem: React.FC<ChangeReviewItemProps> = ({\n label,\n fieldName,\n before,\n after,\n kind = 'changed',\n origin = 'assistant',\n children,\n onApprove,\n onDeny,\n onPreviewRevert,\n disabled = false,\n className = '',\n}) => {\n const [isPreviewingRevert, setIsPreviewingRevert] = useState(false);\n const accessibleFieldName = fieldName ?? label;\n\n // No-ops when `onPreviewRevert` is absent, which also keeps `isPreviewingRevert` from ever\n // flipping true — so the hint below never renders unless a caller asked for the feature.\n const startRevertPreview = () => {\n if (onPreviewRevert === undefined) {\n return;\n }\n setIsPreviewingRevert(true);\n onPreviewRevert(true);\n };\n\n const endRevertPreview = () => {\n if (onPreviewRevert === undefined) {\n return;\n }\n setIsPreviewingRevert(false);\n onPreviewRevert(false);\n };\n\n const badgeAndLabel = (\n <div className=\"flex flex-wrap items-center gap-2\">\n {label !== undefined && (\n <Typography.Text size=\"small\" fontWeight=\"semi-bold\">\n {label}\n </Typography.Text>\n )}\n {/* The pill itself only says \"AI\" — the fuller \"Changed by <origin>\" phrase that\n names who made the change lives in the sr-only span beside it, so a screen\n reader still hears the origin without the visible badge repeating it. */}\n <Tooltip text={badgeTooltip(kind, origin)}>\n {/* `aria-hidden` stays on the pill: the sr-only span below already carries the\n origin, and the tooltip repeats it for a reader who only has the badge. */}\n <span aria-hidden=\"true\">\n <Tag\n type=\"assistant\"\n value=\"AI\"\n icon={<SparkleIcon size={11} />}\n fontSize=\"xsmall\"\n paddingSize=\"compact\"\n textTransform=\"none\"\n />\n </span>\n </Tooltip>\n <span className=\"sr-only\">{`${KIND_WORDS[kind]} by ${origin}`}</span>\n </div>\n );\n\n const displaysChange = before !== undefined || after !== undefined;\n\n const beforeAfter = (\n <>\n {displaysChange && (\n <div className=\"flex flex-wrap items-baseline gap-1\">\n <Typography.SecondaryText size=\"small\">\n {/* Struck through, so a glance separates what it was from what it is\n without having to read the labels. */}\n Was {isEmptyDisplacedValue(before) ? <i>empty</i> : <s>{before}</s>}\n {after !== undefined ? ' →' : null}\n </Typography.SecondaryText>\n {after !== undefined && (\n <Typography.Text\n size=\"small\"\n style={{ color: 'var(--malachite-assistant-foreground)' }}\n >\n {after}\n </Typography.Text>\n )}\n </div>\n )}\n {isPreviewingRevert && (\n <Typography.SecondaryText size=\"small\">\n Previewing the revert — release to keep the AI value\n </Typography.SecondaryText>\n )}\n </>\n );\n\n // Always visible, not revealed on hover: a change waiting on a decision is the point of\n // the row, and hiding the only two controls that resolve it behind a hover made them easy\n // to miss entirely — and unreachable on a touch screen.\n const actions = (\n <div className=\"flex flex-shrink-0 items-center gap-1\">\n <Button\n // `surface`, not `outline`: this sits on the assistant's tinted field, and a\n // transparent button on a tint stops reading as a button.\n variant=\"surface\"\n size=\"xsmall\"\n aria-label={\n accessibleFieldName === undefined\n ? 'Revert change'\n : `Revert ${accessibleFieldName}`\n }\n disabled={disabled}\n onClick={onDeny}\n onMouseEnter={startRevertPreview}\n onMouseLeave={endRevertPreview}\n onFocus={startRevertPreview}\n onBlur={endRevertPreview}\n // The variant's own hover only darkens the border to grey; the mock wants\n // Revert's border and text to pick up the assistant violet specifically.\n className=\"hover:border-[var(--malachite-assistant-foreground)] hover:text-[var(--malachite-assistant-foreground)]\"\n >\n {/* Literal glyphs, not icon components: the mock sets them inline with the\n label at 11px, and no bundled icon reads that small without looking like a\n different weight than the text beside it. Reload (not a cross) because\n reverting restores the previous value rather than dismissing the field.\n The gap is a margin rather than a space: the button lays its children out\n with flex, which strips the leading whitespace off a bare text node. */}\n <span aria-hidden=\"true\" className=\"mr-1\">\n ↺\n </span>\n Revert\n </Button>\n <Button\n variant=\"filled\"\n size=\"xsmall\"\n aria-label={\n accessibleFieldName === undefined\n ? 'Accept change'\n : `Accept ${accessibleFieldName}`\n }\n disabled={disabled}\n onClick={onApprove}\n >\n <span aria-hidden=\"true\" className=\"mr-1\">\n ✓\n </span>\n Accept\n </Button>\n </div>\n );\n\n // With a field to wrap, the header (tag/label/actions) and the before/after line split\n // to opposite sides of `children` instead of sitting together above it — see the\n // component doc comment on `children`.\n if (children !== undefined) {\n return (\n <div\n className={`flex flex-col gap-2 rounded-lg p-2 ${className}`}\n style={{ background: 'var(--malachite-tag-assistant-background)' }}\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n {badgeAndLabel}\n {actions}\n </div>\n {children}\n <div className=\"flex flex-col gap-1\">{beforeAfter}</div>\n </div>\n );\n }\n\n return (\n <div\n className={`flex items-start gap-2 rounded-lg p-2 ${className}`}\n style={{ background: 'var(--malachite-tag-assistant-background)' }}\n >\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {badgeAndLabel}\n {beforeAfter}\n </div>\n {actions}\n </div>\n );\n};\n\nexport default ChangeReviewItem;\nexport { ChangeReviewItem };\n"],"names":["KIND_WORDS","badgeTooltip","kind","origin","isEmptyDisplacedValue","value","ChangeReviewItem","label","fieldName","before","after","children","onApprove","onDeny","onPreviewRevert","disabled","className","isPreviewingRevert","setIsPreviewingRevert","useState","accessibleFieldName","startRevertPreview","endRevertPreview","badgeAndLabel","jsxs","Typography","jsx","Tooltip","Tag","SparkleIcon","beforeAfter","Fragment","actions","Button"],"mappings":"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"}
1
+ {"version":3,"file":"ChangeReviewItem.js","sources":["../../../src/components/ChangeReview/ChangeReviewItem.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport Button from '../Button';\nimport SparkleIcon from '../CustomIcons/SparkleIcon';\nimport Tooltip from '../Tooltip';\nimport { Typography } from '../Typography';\n\nexport type ChangeKind = 'changed' | 'added' | 'removed';\n\nexport interface ChangeReviewItemProps {\n /**\n * What changed — a field name, or the name of a group of them. Omit it when the field this\n * item annotates already carries its own visible heading elsewhere on the page (so the name\n * doesn't render twice) — pass `fieldName` instead, so the badge and actions still know what\n * they belong to.\n *\n * A node rather than a string, because a field's heading is not always only its name: one\n * with an explainer behind an info icon has to hand the whole heading over, or it loses the\n * icon for as long as it is marked. A node carries no accessible name, so `fieldName` is\n * required alongside anything that is not a plain string.\n */\n label?: React.ReactNode;\n /**\n * Names the field the badge and actions act on, for assistive tech. Defaults to `label`, so\n * passing only `label` keeps working as before. Set it explicitly when `label` is omitted —\n * otherwise \"Accept\"/\"Revert\" would have nothing telling a screen reader which field they act\n * on.\n */\n fieldName?: string;\n /** How it read before. A node, so a caller can show chips or code rather than a string. */\n before?: React.ReactNode;\n /** How it reads now. Usually omitted: the field itself is showing it. */\n after?: React.ReactNode;\n kind?: ChangeKind;\n /** What made the change. Names the assistant by default. */\n origin?: string;\n /**\n * The field the change is annotating. When given, the tag/label/actions form a header band\n * above it and the before/after line moves below it, so \"Was …\" reads next to the value it\n * describes instead of stacked with the header. Omit it for the standalone card.\n */\n children?: React.ReactNode;\n /**\n * The caller's own controls for this field, in the header band beside accept and revert.\n * A capability's Remove belongs to the field rather than to the review, but while the field\n * is marked the header is the only row its heading has, so without a slot here the button\n * has nowhere to sit that is not on top of something.\n */\n headerActions?: React.ReactNode;\n onApprove: () => void;\n onDeny: () => void;\n /**\n * Fires while the revert control is hovered or focused, and again when it stops being\n * either. A callback rather than internal state because the thing being previewed — the\n * field this change came from — lives elsewhere on the page, outside what `Item` renders;\n * only the caller can swap what that field shows.\n */\n onPreviewRevert?: (previewing: boolean) => void;\n disabled?: boolean;\n className?: string;\n}\n\nconst KIND_WORDS: Record<ChangeKind, string> = {\n changed: 'Changed',\n added: 'Added',\n removed: 'To be removed',\n};\n\n/**\n * What the \"AI\" pill means, for its tooltip. The badge is two letters, which says who wrote\n * the value but not that the reader is expected to do something about it — and the accept and\n * revert controls it refers to are hidden until the row is hovered or focused.\n */\nconst badgeTooltip = (kind: ChangeKind, origin: string): string =>\n kind === 'removed'\n ? `Marked for removal by ${origin}. Accept to remove it, or revert to keep it.`\n : `${KIND_WORDS[kind]} by ${origin}. Accept to keep it, or revert to restore the previous value.`;\n\n// A multi-select with nothing chosen reaches this as `[]`, not `undefined` — that, a blank or\n// whitespace-only string, and an absent value all read as \"nothing was there\" rather than\n// printing nothing (or, worse, the literal word \"undefined\").\nconst isEmptyDisplacedValue = (value: React.ReactNode): boolean => {\n if (value === undefined || value === null) {\n return true;\n }\n if (typeof value === 'string') {\n return value.trim() === '';\n }\n if (Array.isArray(value)) {\n return value.length === 0;\n }\n return false;\n};\n\n/**\n * Says that something on screen was changed by a machine, shows what it displaced, and offers\n * the two ways out.\n *\n * Accept is deliberately the heavier control (filled, dark) against Revert's quiet outline —\n * the design leans toward accepting the assistant's change rather than presenting both as\n * equal options. That is the designer's call to make, not this component's.\n *\n * The actions are always visible. Revealing them on hover was tried and dropped: a change\n * waiting on a decision is the point of the row, and hiding the only two controls that\n * resolve it made them easy to miss — and unreachable on a touch screen, which has no hover\n * to reveal them with.\n */\nconst ChangeReviewItem: React.FC<ChangeReviewItemProps> = ({\n label,\n fieldName,\n before,\n after,\n kind = 'changed',\n origin = 'assistant',\n children,\n onApprove,\n onDeny,\n headerActions,\n onPreviewRevert,\n disabled = false,\n className = '',\n}) => {\n const [isPreviewingRevert, setIsPreviewingRevert] = useState(false);\n const accessibleFieldName = fieldName ?? (typeof label === 'string' ? label : undefined);\n\n // No-ops when `onPreviewRevert` is absent, which also keeps `isPreviewingRevert` from ever\n // flipping true — so the hint below never renders unless a caller asked for the feature.\n const startRevertPreview = () => {\n if (onPreviewRevert === undefined) {\n return;\n }\n setIsPreviewingRevert(true);\n onPreviewRevert(true);\n };\n\n const endRevertPreview = () => {\n if (onPreviewRevert === undefined) {\n return;\n }\n setIsPreviewingRevert(false);\n onPreviewRevert(false);\n };\n\n const badgeAndLabel = (\n <div className=\"flex flex-wrap items-center gap-2\">\n {typeof label === 'string' ? (\n <Typography.Text size=\"small\" fontWeight=\"semi-bold\">\n {label}\n </Typography.Text>\n ) : (\n label\n )}\n <span className=\"sr-only\">{`${KIND_WORDS[kind]} by ${origin}`}</span>\n </div>\n );\n\n /*\n * The mark and the actions, as one tray.\n *\n * One box with the tag fill rather than two boxes meeting: the sparkle keeps that fill and\n * no shape of its own, so it reads as a marker, and the two actions sit inset on the lighter\n * assistant surface. Two violets a step apart separate what this is from what you can do\n * about it, with no border needed.\n *\n * The glyphs are sized in the style rather than by a class: the button centres whatever it\n * is given, and a bare arrow inherits the control's own font size otherwise.\n *\n * Always visible, not revealed on hover: a change waiting on a decision is the point of the\n * row, and hiding the only two controls that resolve it made them easy to miss entirely,\n * and unreachable on a touch screen.\n */\n const glyphStyle: React.CSSProperties = { fontSize: 11, lineHeight: 1 };\n\n const tray = (\n <div\n className=\"flex flex-shrink-0 items-center gap-1 rounded-md px-1\"\n style={{ background: 'var(--malachite-tag-assistant-background)', height: 24 }}\n >\n <Tooltip text={badgeTooltip(kind, origin)}>\n <span\n aria-hidden=\"true\"\n className=\"flex flex-shrink-0 items-center justify-center\"\n style={{ color: 'var(--malachite-tag-assistant-foreground)' }}\n >\n <SparkleIcon size={11} />\n </span>\n </Tooltip>\n <Button\n variant=\"destructive\"\n size=\"xsmall\"\n iconOnly\n aria-label={\n accessibleFieldName === undefined\n ? 'Revert change'\n : `Revert ${accessibleFieldName}`\n }\n disabled={disabled}\n onClick={onDeny}\n onMouseEnter={startRevertPreview}\n onMouseLeave={endRevertPreview}\n onFocus={startRevertPreview}\n onBlur={endRevertPreview}\n icon={\n /* Reload rather than a cross: reverting restores the previous value rather\n than dismissing the field. */\n <span aria-hidden=\"true\" style={glyphStyle}>\n ↺\n </span>\n }\n />\n <Button\n variant=\"success\"\n size=\"xsmall\"\n iconOnly\n aria-label={\n accessibleFieldName === undefined\n ? 'Accept change'\n : `Accept ${accessibleFieldName}`\n }\n disabled={disabled}\n onClick={onApprove}\n icon={\n <span aria-hidden=\"true\" style={glyphStyle}>\n ✓\n </span>\n }\n />\n </div>\n );\n\n const displaysChange = before !== undefined || after !== undefined;\n\n const beforeAfter = (\n <>\n {displaysChange && (\n <div className=\"flex flex-wrap items-baseline gap-1\">\n <Typography.SecondaryText size=\"small\">\n {/* Struck through, so a glance separates what it was from what it is\n without having to read the labels. */}\n Was {isEmptyDisplacedValue(before) ? <i>empty</i> : <s>{before}</s>}\n {after !== undefined ? ' →' : null}\n </Typography.SecondaryText>\n {after !== undefined && (\n <Typography.Text\n size=\"small\"\n style={{ color: 'var(--malachite-assistant-foreground)' }}\n >\n {after}\n </Typography.Text>\n )}\n </div>\n )}\n {isPreviewingRevert && (\n <Typography.SecondaryText size=\"small\">\n Previewing the revert — release to keep the AI value\n </Typography.SecondaryText>\n )}\n </>\n );\n\n // With a field to wrap, the header (tag/label/actions) and the before/after line split\n // to opposite sides of `children` instead of sitting together above it — see the\n // component doc comment on `children`.\n if (children !== undefined) {\n return (\n <div\n className={`flex flex-col gap-2 rounded-lg p-2 ${className}`}\n style={{ background: 'var(--malachite-assistant-surface-background)' }}\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n {badgeAndLabel}\n <div className=\"flex flex-wrap items-center gap-2\">\n {headerActions}\n {tray}\n </div>\n </div>\n {children}\n <div className=\"flex flex-col gap-1\">{beforeAfter}</div>\n </div>\n );\n }\n\n return (\n <div\n className={`flex items-start gap-2 rounded-lg p-2 ${className}`}\n style={{ background: 'var(--malachite-assistant-surface-background)' }}\n >\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {badgeAndLabel}\n {beforeAfter}\n </div>\n {headerActions}\n {tray}\n </div>\n );\n};\n\nexport default ChangeReviewItem;\nexport { ChangeReviewItem };\n"],"names":["KIND_WORDS","badgeTooltip","kind","origin","isEmptyDisplacedValue","value","ChangeReviewItem","label","fieldName","before","after","children","onApprove","onDeny","headerActions","onPreviewRevert","disabled","className","isPreviewingRevert","setIsPreviewingRevert","useState","accessibleFieldName","startRevertPreview","endRevertPreview","badgeAndLabel","jsxs","jsx","Typography","glyphStyle","tray","Tooltip","SparkleIcon","Button","beforeAfter","Fragment"],"mappings":"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"}
@@ -11,13 +11,13 @@ const k = {
11
11
  streaming: "Streaming",
12
12
  completed: "Completed",
13
13
  error: "Failed"
14
- }, z = "rounded-tl-[12px] rounded-tr-[12px] rounded-br-[4px] rounded-bl-[12px]", A = "rounded-tl-[12px] rounded-tr-[12px] rounded-br-[12px] rounded-bl-[4px]", C = ({ model: i, inputTokens: r, outputTokens: a, totalTokens: n }) => {
15
- const o = [
14
+ }, z = "rounded-tl-[12px] rounded-tr-[12px] rounded-br-[4px] rounded-bl-[12px]", A = "rounded-tl-[12px] rounded-tr-[12px] rounded-br-[12px] rounded-bl-[4px]", C = ({ model: i, inputTokens: r, outputTokens: a, totalTokens: o }) => {
15
+ const n = [
16
16
  r === void 0 ? void 0 : `${h(r)} in`,
17
17
  a === void 0 ? void 0 : `${h(a)} out`,
18
- n === void 0 ? void 0 : `${h(n)} total`
18
+ o === void 0 ? void 0 : `${h(o)} total`
19
19
  ].filter((m) => m !== void 0);
20
- return [i, ...o].join(" · ");
20
+ return [i, ...n].join(" · ");
21
21
  }, x = ({
22
22
  label: i,
23
23
  icon: r,
@@ -37,8 +37,8 @@ const k = {
37
37
  role: i,
38
38
  variant: r = "plain",
39
39
  content: a,
40
- children: n,
41
- name: o,
40
+ children: o,
41
+ name: n,
42
42
  avatarSrc: m,
43
43
  status: b,
44
44
  loading: f = !1,
@@ -49,14 +49,14 @@ const k = {
49
49
  }) => {
50
50
  const s = /* @__PURE__ */ l("div", { className: "flex min-w-0 flex-col gap-2", children: [
51
51
  a ? /* @__PURE__ */ e(c.MarkdownBlock, { text: a, variant: "flow", omitMargin: !0 }) : null,
52
- n,
52
+ o,
53
53
  f && !a ? /* @__PURE__ */ e($, {}) : null
54
54
  ] });
55
55
  if (i === "user") {
56
56
  const v = r === "bubble" ? `${z} border-[var(--malachite-chat-user-bubble-border)] bg-[var(--malachite-chat-user-bubble-background)]` : "rounded-lg border-[var(--malachite-accent-border)] bg-[var(--malachite-accent-background-glass)]";
57
- return /* @__PURE__ */ l("div", { className: `flex min-w-0 items-start justify-end gap-2 ${t}`, children: [
57
+ return /* @__PURE__ */ l("div", { className: `flex min-w-0 items-end justify-end gap-2 ${t}`, children: [
58
58
  /* @__PURE__ */ e("div", { className: `min-w-0 max-w-[85%] border border-solid px-3 py-2 ${v}`, children: s }),
59
- /* @__PURE__ */ e("div", { className: "shrink-0 pt-[2px]", children: /* @__PURE__ */ e(N, { src: m, name: o, size: 24, shape: "square" }) })
59
+ /* @__PURE__ */ e("div", { className: "shrink-0", children: /* @__PURE__ */ e(N, { src: m, name: n, size: 24, shape: "square" }) })
60
60
  ] });
61
61
  }
62
62
  const g = u || d || p;
@@ -71,8 +71,8 @@ const k = {
71
71
  {
72
72
  className: `flex min-w-0 flex-col gap-3 rounded-[var(--malachite-radius-xxl,14px)] border border-solid border-[var(--malachite-card-border)] bg-[var(--malachite-card-background)] p-4 ${t}`,
73
73
  children: [
74
- (o || b) && /* @__PURE__ */ l("div", { className: "flex items-center gap-2", children: [
75
- o ? /* @__PURE__ */ e("span", { className: "rounded-md bg-[var(--malachite-card-dimmed-background)] px-2 py-1 leading-none", children: /* @__PURE__ */ e(c.Text, { size: "small", fontWeight: "semi-bold", children: o }) }) : null,
74
+ (n || b) && /* @__PURE__ */ l("div", { className: "flex items-center gap-2", children: [
75
+ n ? /* @__PURE__ */ e("span", { className: "rounded-md bg-[var(--malachite-card-dimmed-background)] px-2 py-1 leading-none", children: /* @__PURE__ */ e(c.Text, { size: "small", fontWeight: "semi-bold", children: n }) }) : null,
76
76
  b ? /* @__PURE__ */ e(c.SecondaryText, { size: "xsmall", textTransform: "uppercase", children: k[b] }) : null
77
77
  ] }),
78
78
  s,
@@ -1 +1 @@
1
- {"version":3,"file":"ChatMessage.js","sources":["../../../src/components/Chat/ChatMessage.tsx"],"sourcesContent":["import React from 'react';\nimport Avatar from '../Avatar';\nimport Button from '../Button';\nimport CopyIcon from '../CustomIcons/CopyIcon';\nimport ThumbsDownIcon from '../CustomIcons/ThumbsDownIcon';\nimport ThumbsUpIcon from '../CustomIcons/ThumbsUpIcon';\nimport Typography from '../Typography';\nimport ChatThinking from './ChatThinking';\nimport { compactCount } from './tokenText';\n\nexport type ChatRole = 'user' | 'assistant';\n\n/**\n * Three distinct treatments, not two plus a repaint of one: `plain` is the bare turn, `card`\n * frames a reply with its own status, copy/rating actions and a token line, and `bubble` is a\n * message-app chat style with no chrome beyond the bubble itself. All three appear in the\n * design-sync previews — keep them that way rather than folding `bubble` back into `card`.\n */\nexport type ChatMessageVariant = 'plain' | 'card' | 'bubble';\n\nexport type ChatMessageStatus = 'streaming' | 'completed' | 'error';\n\nexport type ChatFeedback = 'up' | 'down';\n\nexport interface ChatMessageUsage {\n /** The model as a reader would name it, e.g. \"Claude Haiku 4.5\". */\n model: string;\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n}\n\nexport interface ChatMessageProps {\n role: ChatRole;\n /**\n * `card` frames the turn with a header and a footer — for a surface where a reply carries\n * its own status and cost. `plain` is the bare turn. `bubble` wraps the turn in a coloured,\n * asymmetrically-rounded bubble instead — no header, no footer, just the shape.\n */\n variant?: ChatMessageVariant;\n /** Markdown. Rendered through `Typography.MarkdownBlock`, so a reply may carry formatting. */\n content?: string;\n /** Anything belonging with the message — tool calls, reasoning, a busy indicator. */\n children?: React.ReactNode;\n /** Names a user turn and its avatar; labels an assistant turn's header. */\n name?: string;\n avatarSrc?: string;\n /** Shown in a `card` header. Omitted means the turn reports no state. */\n status?: ChatMessageStatus;\n /**\n * The reply has been asked for but nothing has arrived. Renders the same placeholder\n * `Chat.Thinking` does, in the body's place, so the turn does not jump when text lands.\n */\n loading?: boolean;\n /**\n * Offers a copy control. The handler decides what to copy: a turn may hold tool calls and\n * reasoning this component never sees, so it cannot honestly claim to know the text.\n */\n onCopy?: () => void;\n /** Offers thumbs up and down. */\n onFeedback?: (feedback: ChatFeedback) => void;\n usage?: ChatMessageUsage;\n className?: string;\n}\n\nconst STATUS_LABELS: Record<ChatMessageStatus, string> = {\n streaming: 'Streaming',\n completed: 'Completed',\n error: 'Failed',\n};\n\n/**\n * The flattened corner points at its author — bottom-right on a right-aligned user bubble,\n * bottom-left on a left-aligned assistant one — so who spoke is legible before a word is read.\n * Not decoration: swapping these would point every bubble at the wrong side of the transcript.\n */\nconst USER_BUBBLE_RADIUS = 'rounded-tl-[12px] rounded-tr-[12px] rounded-br-[4px] rounded-bl-[12px]';\nconst ASSISTANT_BUBBLE_RADIUS =\n 'rounded-tl-[12px] rounded-tr-[12px] rounded-br-[12px] rounded-bl-[4px]';\n\nconst usageText = ({ model, inputTokens, outputTokens, totalTokens }: ChatMessageUsage): string => {\n const counts = [\n inputTokens === undefined ? undefined : `${compactCount(inputTokens)} in`,\n outputTokens === undefined ? undefined : `${compactCount(outputTokens)} out`,\n totalTokens === undefined ? undefined : `${compactCount(totalTokens)} total`,\n ].filter((part): part is string => part !== undefined);\n\n return [model, ...counts].join(' · ');\n};\n\n/**\n * `outline` rather than `ghost`: a bare glyph does not read as something to press, and ghost\n * only draws its border on hover, which a pointer finds and nothing else does.\n */\nconst MessageAction = ({\n label,\n icon,\n onClick,\n}: {\n label: string;\n icon: React.ReactNode;\n onClick: () => void;\n}) => (\n <Button\n variant=\"outline\"\n size=\"xsmall\"\n iconOnly\n icon={icon}\n tooltip={label}\n aria-label={label}\n onClick={onClick}\n />\n);\n\n/**\n * One turn in a transcript.\n *\n * A user turn sits to the right and hugs its own text, the way a sent message reads. An\n * assistant turn runs the full width in `plain`/`card`, so a long reply is not read through a\n * narrow column; in `bubble` both roles hug their own text instead, the way a messaging app reads.\n */\nconst ChatMessage: React.FC<ChatMessageProps> = ({\n role,\n variant = 'plain',\n content,\n children,\n name,\n avatarSrc,\n status,\n loading = false,\n onCopy,\n onFeedback,\n usage,\n className = '',\n}) => {\n const body = (\n <div className=\"flex min-w-0 flex-col gap-2\">\n {content ? <Typography.MarkdownBlock text={content} variant=\"flow\" omitMargin /> : null}\n {children}\n {loading && !content ? <ChatThinking /> : null}\n </div>\n );\n\n if (role === 'user') {\n const boxClasses =\n variant === 'bubble'\n ? `${USER_BUBBLE_RADIUS} border-[var(--malachite-chat-user-bubble-border)] bg-[var(--malachite-chat-user-bubble-background)]`\n : 'rounded-lg border-[var(--malachite-accent-border)] bg-[var(--malachite-accent-background-glass)]';\n\n return (\n <div className={`flex min-w-0 items-start justify-end gap-2 ${className}`}>\n <div className={`min-w-0 max-w-[85%] border border-solid px-3 py-2 ${boxClasses}`}>\n {body}\n </div>\n <div className=\"shrink-0 pt-[2px]\">\n <Avatar src={avatarSrc} name={name} size={24} shape=\"square\" />\n </div>\n </div>\n );\n }\n\n const footer = onCopy || onFeedback || usage;\n\n if (variant === 'plain') {\n return <div className={`flex min-w-0 flex-col ${className}`}>{body}</div>;\n }\n\n if (variant === 'bubble') {\n // Hugs its content rather than filling the column: a flex row (not the plain/card\n // column) so the bubble's width tracks its text and only caps at the same 85% the\n // user turn already uses, instead of stretching to the full transcript width.\n return (\n <div className={`flex min-w-0 items-start ${className}`}>\n <div\n className={`min-w-0 max-w-[85%] border border-solid px-3 py-2 text-[var(--malachite-chat-assistant-bubble-foreground)] ${ASSISTANT_BUBBLE_RADIUS} border-[var(--malachite-chat-assistant-bubble-border)] bg-[var(--malachite-chat-assistant-bubble-background)]`}\n >\n {body}\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`flex min-w-0 flex-col gap-3 rounded-[var(--malachite-radius-xxl,14px)] border border-solid border-[var(--malachite-card-border)] bg-[var(--malachite-card-background)] p-4 ${className}`}\n >\n {(name || status) && (\n <div className=\"flex items-center gap-2\">\n {name ? (\n <span className=\"rounded-md bg-[var(--malachite-card-dimmed-background)] px-2 py-1 leading-none\">\n <Typography.Text size=\"small\" fontWeight=\"semi-bold\">\n {name}\n </Typography.Text>\n </span>\n ) : null}\n {status ? (\n <Typography.SecondaryText size=\"xsmall\" textTransform=\"uppercase\">\n {STATUS_LABELS[status]}\n </Typography.SecondaryText>\n ) : null}\n </div>\n )}\n {body}\n {footer ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n {onCopy ? (\n <MessageAction\n label=\"Copy\"\n icon={<CopyIcon size={13} />}\n onClick={onCopy}\n />\n ) : null}\n {onFeedback ? (\n <>\n <MessageAction\n label=\"Good response\"\n icon={<ThumbsUpIcon size={13} />}\n onClick={() => onFeedback('up')}\n />\n <MessageAction\n label=\"Bad response\"\n icon={<ThumbsDownIcon size={13} />}\n onClick={() => onFeedback('down')}\n />\n </>\n ) : null}\n </div>\n {usage ? (\n <span className=\"flex min-w-0 items-center gap-1.5\">\n {/* Decorative, so it is hidden: the model is named in the text\n beside it and a screen reader gains nothing from a dot. */}\n <span\n aria-hidden=\"true\"\n className=\"h-2 w-2 shrink-0 rounded-full bg-[var(--malachite-warning-background)]\"\n />\n <Typography.SecondaryText size=\"xsmall\">\n {usageText(usage)}\n </Typography.SecondaryText>\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n};\n\nexport default ChatMessage;\nexport { ChatMessage };\n"],"names":["STATUS_LABELS","USER_BUBBLE_RADIUS","ASSISTANT_BUBBLE_RADIUS","usageText","model","inputTokens","outputTokens","totalTokens","counts","compactCount","part","MessageAction","label","icon","onClick","jsx","Button","ChatMessage","role","variant","content","children","name","avatarSrc","status","loading","onCopy","onFeedback","usage","className","body","jsxs","Typography","ChatThinking","boxClasses","Avatar","footer","CopyIcon","Fragment","ThumbsUpIcon","ThumbsDownIcon"],"mappings":"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"}
1
+ {"version":3,"file":"ChatMessage.js","sources":["../../../src/components/Chat/ChatMessage.tsx"],"sourcesContent":["import React from 'react';\nimport Avatar from '../Avatar';\nimport Button from '../Button';\nimport CopyIcon from '../CustomIcons/CopyIcon';\nimport ThumbsDownIcon from '../CustomIcons/ThumbsDownIcon';\nimport ThumbsUpIcon from '../CustomIcons/ThumbsUpIcon';\nimport Typography from '../Typography';\nimport ChatThinking from './ChatThinking';\nimport { compactCount } from './tokenText';\n\nexport type ChatRole = 'user' | 'assistant';\n\n/**\n * Three distinct treatments, not two plus a repaint of one: `plain` is the bare turn, `card`\n * frames a reply with its own status, copy/rating actions and a token line, and `bubble` is a\n * message-app chat style with no chrome beyond the bubble itself. All three appear in the\n * design-sync previews — keep them that way rather than folding `bubble` back into `card`.\n */\nexport type ChatMessageVariant = 'plain' | 'card' | 'bubble';\n\nexport type ChatMessageStatus = 'streaming' | 'completed' | 'error';\n\nexport type ChatFeedback = 'up' | 'down';\n\nexport interface ChatMessageUsage {\n /** The model as a reader would name it, e.g. \"Claude Haiku 4.5\". */\n model: string;\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n}\n\nexport interface ChatMessageProps {\n role: ChatRole;\n /**\n * `card` frames the turn with a header and a footer — for a surface where a reply carries\n * its own status and cost. `plain` is the bare turn. `bubble` wraps the turn in a coloured,\n * asymmetrically-rounded bubble instead — no header, no footer, just the shape.\n */\n variant?: ChatMessageVariant;\n /** Markdown. Rendered through `Typography.MarkdownBlock`, so a reply may carry formatting. */\n content?: string;\n /** Anything belonging with the message — tool calls, reasoning, a busy indicator. */\n children?: React.ReactNode;\n /** Names a user turn and its avatar; labels an assistant turn's header. */\n name?: string;\n avatarSrc?: string;\n /** Shown in a `card` header. Omitted means the turn reports no state. */\n status?: ChatMessageStatus;\n /**\n * The reply has been asked for but nothing has arrived. Renders the same placeholder\n * `Chat.Thinking` does, in the body's place, so the turn does not jump when text lands.\n */\n loading?: boolean;\n /**\n * Offers a copy control. The handler decides what to copy: a turn may hold tool calls and\n * reasoning this component never sees, so it cannot honestly claim to know the text.\n */\n onCopy?: () => void;\n /** Offers thumbs up and down. */\n onFeedback?: (feedback: ChatFeedback) => void;\n usage?: ChatMessageUsage;\n className?: string;\n}\n\nconst STATUS_LABELS: Record<ChatMessageStatus, string> = {\n streaming: 'Streaming',\n completed: 'Completed',\n error: 'Failed',\n};\n\n/**\n * The flattened corner points at its author — bottom-right on a right-aligned user bubble,\n * bottom-left on a left-aligned assistant one — so who spoke is legible before a word is read.\n * Not decoration: swapping these would point every bubble at the wrong side of the transcript.\n */\nconst USER_BUBBLE_RADIUS = 'rounded-tl-[12px] rounded-tr-[12px] rounded-br-[4px] rounded-bl-[12px]';\nconst ASSISTANT_BUBBLE_RADIUS =\n 'rounded-tl-[12px] rounded-tr-[12px] rounded-br-[12px] rounded-bl-[4px]';\n\nconst usageText = ({ model, inputTokens, outputTokens, totalTokens }: ChatMessageUsage): string => {\n const counts = [\n inputTokens === undefined ? undefined : `${compactCount(inputTokens)} in`,\n outputTokens === undefined ? undefined : `${compactCount(outputTokens)} out`,\n totalTokens === undefined ? undefined : `${compactCount(totalTokens)} total`,\n ].filter((part): part is string => part !== undefined);\n\n return [model, ...counts].join(' · ');\n};\n\n/**\n * `outline` rather than `ghost`: a bare glyph does not read as something to press, and ghost\n * only draws its border on hover, which a pointer finds and nothing else does.\n */\nconst MessageAction = ({\n label,\n icon,\n onClick,\n}: {\n label: string;\n icon: React.ReactNode;\n onClick: () => void;\n}) => (\n <Button\n variant=\"outline\"\n size=\"xsmall\"\n iconOnly\n icon={icon}\n tooltip={label}\n aria-label={label}\n onClick={onClick}\n />\n);\n\n/**\n * One turn in a transcript.\n *\n * A user turn sits to the right and hugs its own text, the way a sent message reads. An\n * assistant turn runs the full width in `plain`/`card`, so a long reply is not read through a\n * narrow column; in `bubble` both roles hug their own text instead, the way a messaging app reads.\n */\nconst ChatMessage: React.FC<ChatMessageProps> = ({\n role,\n variant = 'plain',\n content,\n children,\n name,\n avatarSrc,\n status,\n loading = false,\n onCopy,\n onFeedback,\n usage,\n className = '',\n}) => {\n const body = (\n <div className=\"flex min-w-0 flex-col gap-2\">\n {content ? <Typography.MarkdownBlock text={content} variant=\"flow\" omitMargin /> : null}\n {children}\n {loading && !content ? <ChatThinking /> : null}\n </div>\n );\n\n if (role === 'user') {\n const boxClasses =\n variant === 'bubble'\n ? `${USER_BUBBLE_RADIUS} border-[var(--malachite-chat-user-bubble-border)] bg-[var(--malachite-chat-user-bubble-background)]`\n : 'rounded-lg border-[var(--malachite-accent-border)] bg-[var(--malachite-accent-background-glass)]';\n\n return (\n <div className={`flex min-w-0 items-end justify-end gap-2 ${className}`}>\n <div className={`min-w-0 max-w-[85%] border border-solid px-3 py-2 ${boxClasses}`}>\n {body}\n </div>\n {/* At the foot of the bubble, where its flattened corner already points. Level\n with the first line it sat clear of a long turn's last line, which is the\n end the thread reads towards. */}\n <div className=\"shrink-0\">\n <Avatar src={avatarSrc} name={name} size={24} shape=\"square\" />\n </div>\n </div>\n );\n }\n\n const footer = onCopy || onFeedback || usage;\n\n if (variant === 'plain') {\n return <div className={`flex min-w-0 flex-col ${className}`}>{body}</div>;\n }\n\n if (variant === 'bubble') {\n // Hugs its content rather than filling the column: a flex row (not the plain/card\n // column) so the bubble's width tracks its text and only caps at the same 85% the\n // user turn already uses, instead of stretching to the full transcript width.\n return (\n <div className={`flex min-w-0 items-start ${className}`}>\n <div\n className={`min-w-0 max-w-[85%] border border-solid px-3 py-2 text-[var(--malachite-chat-assistant-bubble-foreground)] ${ASSISTANT_BUBBLE_RADIUS} border-[var(--malachite-chat-assistant-bubble-border)] bg-[var(--malachite-chat-assistant-bubble-background)]`}\n >\n {body}\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`flex min-w-0 flex-col gap-3 rounded-[var(--malachite-radius-xxl,14px)] border border-solid border-[var(--malachite-card-border)] bg-[var(--malachite-card-background)] p-4 ${className}`}\n >\n {(name || status) && (\n <div className=\"flex items-center gap-2\">\n {name ? (\n <span className=\"rounded-md bg-[var(--malachite-card-dimmed-background)] px-2 py-1 leading-none\">\n <Typography.Text size=\"small\" fontWeight=\"semi-bold\">\n {name}\n </Typography.Text>\n </span>\n ) : null}\n {status ? (\n <Typography.SecondaryText size=\"xsmall\" textTransform=\"uppercase\">\n {STATUS_LABELS[status]}\n </Typography.SecondaryText>\n ) : null}\n </div>\n )}\n {body}\n {footer ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1\">\n {onCopy ? (\n <MessageAction\n label=\"Copy\"\n icon={<CopyIcon size={13} />}\n onClick={onCopy}\n />\n ) : null}\n {onFeedback ? (\n <>\n <MessageAction\n label=\"Good response\"\n icon={<ThumbsUpIcon size={13} />}\n onClick={() => onFeedback('up')}\n />\n <MessageAction\n label=\"Bad response\"\n icon={<ThumbsDownIcon size={13} />}\n onClick={() => onFeedback('down')}\n />\n </>\n ) : null}\n </div>\n {usage ? (\n <span className=\"flex min-w-0 items-center gap-1.5\">\n {/* Decorative, so it is hidden: the model is named in the text\n beside it and a screen reader gains nothing from a dot. */}\n <span\n aria-hidden=\"true\"\n className=\"h-2 w-2 shrink-0 rounded-full bg-[var(--malachite-warning-background)]\"\n />\n <Typography.SecondaryText size=\"xsmall\">\n {usageText(usage)}\n </Typography.SecondaryText>\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n};\n\nexport default ChatMessage;\nexport { ChatMessage };\n"],"names":["STATUS_LABELS","USER_BUBBLE_RADIUS","ASSISTANT_BUBBLE_RADIUS","usageText","model","inputTokens","outputTokens","totalTokens","counts","compactCount","part","MessageAction","label","icon","onClick","jsx","Button","ChatMessage","role","variant","content","children","name","avatarSrc","status","loading","onCopy","onFeedback","usage","className","body","jsxs","Typography","ChatThinking","boxClasses","Avatar","footer","CopyIcon","Fragment","ThumbsUpIcon","ThumbsDownIcon"],"mappings":"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"}