@stackone/malachite 2.25.1 → 2.27.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/COMPONENTS.md CHANGED
@@ -9,6 +9,8 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
9
9
  - **`Chat`** — A chat column: `Chat.Thread` scrolls and pins itself to the newest message, `Chat.Composer` keeps its own height and owns the Enter/Shift+Enter convention, and `Chat.Message`, `Chat.ToolCall` and `Chat.Thinking` render one turn each. A user turn sits right and hugs its text; an assistant turn runs full width, and `variant="card"` frames it with its state, copy and rating controls, and a token/model line. `variant="bubble"` is a third treatment, not a repaint of either: tinted bubbles whose flattened corner points at whoever spoke, so authorship reads before the words do. `Chat.Context` lists what the conversation can see — a removable chip per source, and an add affordance only when a handler is given. `Chat.Thinking` and a loading turn draw bars in the shape of the answer rather than a spinner. The row under a composer is `ComposerChip`, `ContextMeter` and `ModelPicker` — PillButton tabs, so it reads as attached to the field; `ModelPicker` opens a portalled menu with a submenu per model. The transcript is a live `log` region, so a streamed reply is announced, and it shows a centred empty state until the first turn. _Key props:_ `Chat`: `children`, `className`. `Thread`: `aria-label`, `emptyTitle`, `emptyDescription`, `emptyIcon`. `Message`: `role`, `variant` (`plain` | `card`), `content`, `children`, `name`, `avatarSrc`, `status` (`streaming` | `completed` | `error`), `loading`, `onCopy`, `onFeedback`, `usage`. `ToolCall`: `name`, `status` (`running` | `completed` | `error`), `callId`, `input`, `output`, `defaultExpanded`. `Thinking`: `label`. `Composer`: `value`, `onChange`, `onSend`, `onStop`, `busy`, `disabled`, `placeholder`, `maxLength`, `rows`, `children`. `ComposerChip`: `label`, `icon`, `trailingIcon`, `onClick`, `disabled`. `ContextMeter`: `used`, `limit`, `cached`, `label`. `ModelPicker`: `models`, `value`, `optionValue`, `onChange`, `openUp`, `disabled`. `Context`: `label`, `items` (`{id, label, onRemove?}`), `onAdd`, `className`.
10
10
  - **`Surface`** — Rounded, bordered card container with the standard elevation shadow. Use `overflow="clip"` to keep the rounded clipping while letting a `position: sticky` descendant still pin (`hidden` would trap it); `visible` disables clipping. _Key props:_ `children`, `padding`, `overflow`, `className`, `style`.
11
11
  - **`SurfaceRow`** — A full-bleed row inside a Surface (used as `Surface.Row`). Owns hover highlight, colour transition and an optional bottom divider. Pass `onClick`/`href` to make the whole row clickable (renders as a real button/anchor, so keyboard nav works). For a selectable row use `role="radio"` + `selected` and put a presentational `<Radio>` inside as the indicator; `selected` then paints the same treatment `Table` gives `selectedRowKey`. Under `role="checkbox"` it only announces — a multi-select row carries its own checkbox, and a highlight marking one of many says nothing when most rows are checked. `selection` overrides that either way (`highlight` / `announce`). Use `trailing` for a control that must stay interactive — it renders outside the row button, which a nested control could not be. `tone="muted"` tints a row whose subject is inactive, which `disabled` does not: a muted row stays clickable. _Key props:_ `divider`, `disabled`, `tone` (`default` | `muted`), `onClick`, `href`, `selected`, `selection` (`highlight` | `announce`), `trailing`, `role`, `className`, `style`.
12
+ - **`SurfaceExpandableRow`** — A row that opens to reveal a body (used as `Surface.ExpandableRow`), for a line that carries its own controls. Not `Accordion`, which puts its header inside the toggle button, so a header holding a dropdown or a second button would be invalid: here the row surface takes the click and a cell opts out by wrapping its controls in `ExpandableRow.Controls`, while the arrow stays a real button so the keyboard reaches it. That opt-out is explicit rather than sniffing the event target for an interactive ancestor, because a control rendering into a shadow root retargets the event to its host. `sticky` pins the row while its body scrolls, offset by `stickyTopOffset` for whatever is already pinned above it; it only pins while open, since a closed row has nothing to scroll past. _Key props:_ `header`, `children`, `expanded`, `onToggle`, `sticky`, `stickyTopOffset`, `divider`, `tone`, `aria-label`, `className`, `style`.
13
+ - **`DisclosureArrow`** — The expand/collapse triangle disclosure UI shares (`Accordion`, `Surface.ExpandableRow`). Owns its colour states so they never drift between usages: `text-primary` when expanded, `text-placeholder` otherwise, and dark on hover inside a `group` ancestor — put `group` on the clickable header or row so that hover state activates. _Key props:_ `expanded`, `size`, `className`.
12
14
  - **`Card`** — Generic content card.
13
15
  - **`SelectableCard`** — A card that is one option in a radio group — eyebrow, optional status tag, title, description, a count label and a top-right action slot. Use inside a `CardStrip`. Selection is controlled by the consumer: pass `selected`, which drives both the visual treatment and `aria-checked`. A selected card meets the sheet below the way a browser tab meets its toolbar — its bottom corners flare outward onto the strip’s rule — so its outline is one SVG path rather than borders, which would split into two lines at the tangent. `variant` picks the palette: `profile` (green), `library` (neutral) or `editing` (amber, for a profile with unsaved changes). `actions` stands in the card’s top-right corner, on the eyebrow row, and renders outside the control so a menu or icon button inside it stays clickable and keeps its own tab stop. Nothing else marks that corner: the flared outline, the tint and the green eyebrow already say which card is selected, so a radio there would only repeat them. `isDefault` marks the card as the account default with a pin beside the title, named for assistive tech and tooltipped for everyone else — it belongs with the name it qualifies rather than among the actions, which read as things to press. For a row-shaped selectable use `Surface.Row` instead. _Key props:_ `value`, `title`, `titleIcon`, `isDefault`, `variant` (`profile` | `library` | `editing`), `eyebrow`, `status` (`label`, `type`), `description`, `countLabel`, `actions`, `selected`, `loading` (`loadingLabel`, `loadingHint`), `disabled`, `dimmed`, `onSelect`, `tabIndex`, `aria-label`, `className`, `style`.
14
16
  - **`CardStrip`** — Horizontally scrolling radio group of cards, with edge arrows that appear only when the content overflows, an edge fade, scroll-snap and arrow-key navigation matching `SegmentedControl`. Owns interaction only: it finds its options by looking for `role="radio"` descendants carrying `data-value`, so any such child works (`SelectableCard` emits both). Pass a non-selectable terminal tile as `CardStrip.AddTile` — it sits inside the group but outside the radio set, and must be a direct child or it scrolls with the cards. Use `leading` to pin a card that should not scroll away; the strip tells the cards which slot they are in, so a pinned `SelectableCard` drops the left rule the scrolling ones draw. Set `bleed` when the strip sits in a `PageLayout`: the strip stretches past the horizontal padding of the layout so its bottom rule runs the full width of the page card, while the cards stay aligned to the gutter. With no cards at all the strip degenerates to its pinned slots alone — no trough, no scroller and no arrows — with the add tile standing beside `leading` and taking `primary` emphasis automatically, since adding the first card is the only thing on offer. _Key props:_ `value`, `tone` (`profile` | `library` | `editing` — read off the selected card unless set), `onChange`, `children`, `leading`, `arrows`, `fadeEdges`, `snap`, `bleed`, `scrollSelectedIntoView`, `aria-label`, `aria-labelledby`, `className`, `style`. `CardStrip.AddTile`: `label`, `icon`, `onClick`, `disabled`, `emphasis` (`primary` | `secondary` — left to the strip unless set: `primary` while there are no cards, `secondary` once there are), `className`.
@@ -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":";;;;;;AA6DA,MAAMA,IAAyC;AAAA,EAC3C,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS;AACb,GAOMC,IAAe,CAACC,GAAkBC,MACpCD,MAAS,YACH,yBAAyBC,CAAM,iDAC/B,GAAGH,EAAWE,CAAI,CAAC,OAAOC,CAAM,iEAKpCC,IAAwB,CAACC,MACAA,KAAU,OAC1B,KAEP,OAAOA,KAAU,WACVA,EAAM,WAAW,KAExB,MAAM,QAAQA,CAAK,IACZA,EAAM,WAAW,IAErB,IAgBLC,IAAoD,CAAC;AAAA,EACvD,OAAAC;AAAA,EACA,WAAAC;AAAA,EACA,QAAAC;AAAA,EACA,OAAAC;AAAA,EACA,MAAAR,IAAO;AAAA,EACP,QAAAC,IAAS;AAAA,EACT,UAAAQ;AAAA,EACA,WAAAC;AAAA,EACA,QAAAC;AAAA,EACA,eAAAC;AAAA,EACA,iBAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,WAAAC,IAAY;AAChB,MAAM;AACF,QAAM,CAACC,GAAoBC,CAAqB,IAAIC,EAAS,EAAK,GAC5DC,IAAsBb,MAAc,OAAOD,KAAU,WAAWA,IAAQ,SAIxEe,IAAqB,MAAM;AAC7B,IAAIP,MAAoB,WAGxBI,EAAsB,EAAI,GAC1BJ,EAAgB,EAAI;AAAA,EACxB,GAEMQ,IAAmB,MAAM;AAC3B,IAAIR,MAAoB,WAGxBI,EAAsB,EAAK,GAC3BJ,EAAgB,EAAK;AAAA,EACzB,GAEMS,IACF,gBAAAC,EAAC,OAAA,EAAI,WAAU,qCACV,UAAA;AAAA,IAAA,OAAOlB,KAAU,WACd,gBAAAmB,EAACC,EAAW,MAAX,EAAgB,MAAK,SAAQ,YAAW,aACpC,UAAApB,EAAA,CACL,IAEAA;AAAA,IAEJ,gBAAAmB,EAAC,QAAA,EAAK,WAAU,WAAW,UAAA,GAAG1B,EAAWE,CAAI,CAAC,OAAOC,CAAM,GAAA,CAAG;AAAA,EAAA,GAClE,GAkBEyB,IAAkC,EAAE,UAAU,IAAI,YAAY,EAAA,GAE9DC,IACF,gBAAAJ;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,WAAU;AAAA,MACV,OAAO,EAAE,YAAY,6CAA6C,QAAQ,GAAA;AAAA,MAE1E,UAAA;AAAA,QAAA,gBAAAC,EAACI,GAAA,EAAQ,MAAM7B,EAAaC,GAAMC,CAAM,GACpC,UAAA,gBAAAuB;AAAA,UAAC;AAAA,UAAA;AAAA,YACG,eAAY;AAAA,YACZ,WAAU;AAAA,YACV,OAAO,EAAE,OAAO,4CAAA;AAAA,YAEhB,UAAA,gBAAAA,EAACK,GAAA,EAAY,MAAM,GAAA,CAAI;AAAA,UAAA;AAAA,QAAA,GAE/B;AAAA,QACA,gBAAAL;AAAA,UAACM;AAAA,UAAA;AAAA,YACG,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,UAAQ;AAAA,YACR,cACIX,MAAwB,SAClB,kBACA,UAAUA,CAAmB;AAAA,YAEvC,UAAAL;AAAA,YACA,SAASH;AAAA,YACT,cAAcS;AAAA,YACd,cAAcC;AAAA,YACd,SAASD;AAAA,YACT,QAAQC;AAAA,YACR;AAAA;AAAA;AAAA,gCAGK,QAAA,EAAK,eAAY,QAAO,OAAOK,GAAY,UAAA,IAAA,CAE5C;AAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAGR,gBAAAF;AAAA,UAACM;AAAA,UAAA;AAAA,YACG,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,UAAQ;AAAA,YACR,cACIX,MAAwB,SAClB,kBACA,UAAUA,CAAmB;AAAA,YAEvC,UAAAL;AAAA,YACA,SAASJ;AAAA,YACT,MACI,gBAAAc,EAAC,QAAA,EAAK,eAAY,QAAO,OAAOE,GAAY,UAAA,IAAA,CAE5C;AAAA,UAAA;AAAA,QAAA;AAAA,MAER;AAAA,IAAA;AAAA,EAAA,GAMFK,IACF,gBAAAR,EAAAS,GAAA,EACK,UAAA;AAAA,KAJczB,MAAW,UAAaC,MAAU,WAK7C,gBAAAe,EAAC,OAAA,EAAI,WAAU,uCACX,UAAA;AAAA,MAAA,gBAAAA,EAACE,EAAW,eAAX,EAAyB,MAAK,SAEe,UAAA;AAAA,QAAA;AAAA,QACrCvB,EAAsBK,CAAM,IAAI,gBAAAiB,EAAC,OAAE,UAAA,QAAA,CAAK,IAAO,gBAAAA,EAAC,KAAA,EAAG,UAAAjB,EAAA,CAAO;AAAA,QAC9DC,MAAU,SAAY,OAAO;AAAA,MAAA,GAClC;AAAA,MACCA,MAAU,UACP,gBAAAgB;AAAA,QAACC,EAAW;AAAA,QAAX;AAAA,UACG,MAAK;AAAA,UACL,OAAO,EAAE,OAAO,wCAAA;AAAA,UAEf,UAAAjB;AAAA,QAAA;AAAA,MAAA;AAAA,IACL,GAER;AAAA,IAEHQ,KACG,gBAAAQ,EAACC,EAAW,eAAX,EAAyB,MAAK,SAAQ,UAAA,uDAAA,CAEvC;AAAA,EAAA,GAER;AAMJ,SAAIhB,MAAa,SAET,gBAAAc;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,WAAW,sCAAsCR,CAAS;AAAA,MAC1D,OAAO,EAAE,YAAY,gDAAA;AAAA,MAErB,UAAA;AAAA,QAAA,gBAAAQ,EAAC,OAAA,EAAI,WAAU,qDACV,UAAA;AAAA,UAAAD;AAAA,UACD,gBAAAC,EAAC,OAAA,EAAI,WAAU,qCACV,UAAA;AAAA,YAAAX;AAAA,YACAe;AAAA,UAAA,EAAA,CACL;AAAA,QAAA,GACJ;AAAA,QACClB;AAAA,QACD,gBAAAe,EAAC,OAAA,EAAI,WAAU,uBAAuB,UAAAO,EAAA,CAAY;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,IAM1D,gBAAAR;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,WAAW,yCAAyCR,CAAS;AAAA,MAC7D,OAAO,EAAE,YAAY,gDAAA;AAAA,MAErB,UAAA;AAAA,QAAA,gBAAAQ,EAAC,OAAA,EAAI,WAAU,sCACV,UAAA;AAAA,UAAAD;AAAA,UACAS;AAAA,QAAA,GACL;AAAA,QACCnB;AAAA,QACAe;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGb;"}
@@ -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":";;;;;;;;;AAiEA,MAAMA,IAAmD;AAAA,EACrD,WAAW;AAAA,EACX,WAAW;AAAA,EACX,OAAO;AACX,GAOMC,IAAqB,0EACrBC,IACF,0EAEEC,IAAY,CAAC,EAAE,OAAAC,GAAO,aAAAC,GAAa,cAAAC,GAAc,aAAAC,QAA4C;AAC/F,QAAMC,IAAS;AAAA,IACXH,MAAgB,SAAY,SAAY,GAAGI,EAAaJ,CAAW,CAAC;AAAA,IACpEC,MAAiB,SAAY,SAAY,GAAGG,EAAaH,CAAY,CAAC;AAAA,IACtEC,MAAgB,SAAY,SAAY,GAAGE,EAAaF,CAAW,CAAC;AAAA,EAAA,EACtE,OAAO,CAACG,MAAyBA,MAAS,MAAS;AAErD,SAAO,CAACN,GAAO,GAAGI,CAAM,EAAE,KAAK,KAAK;AACxC,GAMMG,IAAgB,CAAC;AAAA,EACnB,OAAAC;AAAA,EACA,MAAAC;AAAA,EACA,SAAAC;AACJ,MAKI,gBAAAC;AAAA,EAACC;AAAA,EAAA;AAAA,IACG,SAAQ;AAAA,IACR,MAAK;AAAA,IACL,UAAQ;AAAA,IACR,MAAAH;AAAA,IACA,SAASD;AAAA,IACT,cAAYA;AAAA,IACZ,SAAAE;AAAA,EAAA;AACJ,GAUEG,IAA0C,CAAC;AAAA,EAC7C,MAAAC;AAAA,EACA,SAAAC,IAAU;AAAA,EACV,SAAAC;AAAA,EACA,UAAAC;AAAA,EACA,MAAAC;AAAA,EACA,WAAAC;AAAA,EACA,QAAAC;AAAA,EACA,SAAAC,IAAU;AAAA,EACV,QAAAC;AAAA,EACA,YAAAC;AAAA,EACA,OAAAC;AAAA,EACA,WAAAC,IAAY;AAChB,MAAM;AACF,QAAMC,IACF,gBAAAC,EAAC,OAAA,EAAI,WAAU,+BACV,UAAA;AAAA,IAAAX,IAAU,gBAAAL,EAACiB,EAAW,eAAX,EAAyB,MAAMZ,GAAS,SAAQ,QAAO,YAAU,GAAA,CAAC,IAAK;AAAA,IAClFC;AAAA,IACAI,KAAW,CAACL,IAAU,gBAAAL,EAACkB,KAAa,IAAK;AAAA,EAAA,GAC9C;AAGJ,MAAIf,MAAS,QAAQ;AACjB,UAAMgB,IACFf,MAAY,WACN,GAAGlB,CAAkB,yGACrB;AAEV,WACI,gBAAA8B,EAAC,OAAA,EAAI,WAAW,4CAA4CF,CAAS,IACjE,UAAA;AAAA,MAAA,gBAAAd,EAAC,OAAA,EAAI,WAAW,qDAAqDmB,CAAU,IAC1E,UAAAJ,GACL;AAAA,MAIA,gBAAAf,EAAC,OAAA,EAAI,WAAU,YACX,UAAA,gBAAAA,EAACoB,GAAA,EAAO,KAAKZ,GAAW,MAAAD,GAAY,MAAM,IAAI,OAAM,UAAS,EAAA,CACjE;AAAA,IAAA,GACJ;AAAA,EAER;AAEA,QAAMc,IAASV,KAAUC,KAAcC;AAEvC,SAAIT,MAAY,4BACJ,OAAA,EAAI,WAAW,yBAAyBU,CAAS,IAAK,UAAAC,GAAK,IAGnEX,MAAY,WAKR,gBAAAJ,EAAC,OAAA,EAAI,WAAW,4BAA4Bc,CAAS,IACjD,UAAA,gBAAAd;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,WAAW,8GAA8Gb,CAAuB;AAAA,MAE/I,UAAA4B;AAAA,IAAA;AAAA,EAAA,GAET,IAKJ,gBAAAC;AAAA,IAAC;AAAA,IAAA;AAAA,MACG,WAAW,8KAA8KF,CAAS;AAAA,MAEhM,UAAA;AAAA,SAAAP,KAAQE,MACN,gBAAAO,EAAC,OAAA,EAAI,WAAU,2BACV,UAAA;AAAA,UAAAT,IACG,gBAAAP,EAAC,QAAA,EAAK,WAAU,kFACZ,4BAACiB,EAAW,MAAX,EAAgB,MAAK,SAAQ,YAAW,aACpC,UAAAV,EAAA,CACL,GACJ,IACA;AAAA,UACHE,IACG,gBAAAT,EAACiB,EAAW,eAAX,EAAyB,MAAK,UAAS,eAAc,aACjD,UAAAhC,EAAcwB,CAAM,EAAA,CACzB,IACA;AAAA,QAAA,GACR;AAAA,QAEHM;AAAA,QACAM,IACG,gBAAAL,EAAC,OAAA,EAAI,WAAU,qDACX,UAAA;AAAA,UAAA,gBAAAA,EAAC,OAAA,EAAI,WAAU,2BACV,UAAA;AAAA,YAAAL,IACG,gBAAAX;AAAA,cAACJ;AAAA,cAAA;AAAA,gBACG,OAAM;AAAA,gBACN,MAAM,gBAAAI,EAACsB,GAAA,EAAS,MAAM,GAAA,CAAI;AAAA,gBAC1B,SAASX;AAAA,cAAA;AAAA,YAAA,IAEb;AAAA,YACHC,IACG,gBAAAI,EAAAO,GAAA,EACI,UAAA;AAAA,cAAA,gBAAAvB;AAAA,gBAACJ;AAAA,gBAAA;AAAA,kBACG,OAAM;AAAA,kBACN,MAAM,gBAAAI,EAACwB,GAAA,EAAa,MAAM,GAAA,CAAI;AAAA,kBAC9B,SAAS,MAAMZ,EAAW,IAAI;AAAA,gBAAA;AAAA,cAAA;AAAA,cAElC,gBAAAZ;AAAA,gBAACJ;AAAA,gBAAA;AAAA,kBACG,OAAM;AAAA,kBACN,MAAM,gBAAAI,EAACyB,GAAA,EAAe,MAAM,GAAA,CAAI;AAAA,kBAChC,SAAS,MAAMb,EAAW,MAAM;AAAA,gBAAA;AAAA,cAAA;AAAA,YACpC,EAAA,CACJ,IACA;AAAA,UAAA,GACR;AAAA,UACCC,IACG,gBAAAG,EAAC,QAAA,EAAK,WAAU,qCAGZ,UAAA;AAAA,YAAA,gBAAAhB;AAAA,cAAC;AAAA,cAAA;AAAA,gBACG,eAAY;AAAA,gBACZ,WAAU;AAAA,cAAA;AAAA,YAAA;AAAA,YAEd,gBAAAA,EAACiB,EAAW,eAAX,EAAyB,MAAK,UAC1B,UAAA7B,EAAUyB,CAAK,EAAA,CACpB;AAAA,UAAA,EAAA,CACJ,IACA;AAAA,QAAA,EAAA,CACR,IACA;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGhB;"}