@tangle-network/ui 11.16.3 → 11.16.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.16.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 11fbba5: Chat text renders at the 15px body size. The user bubble, assistant prose, legacy `ChatMessage` and thinking timer wrote their size tokens as `text-[var(--font-size-*)]`, which Tailwind compiles to a color, so assistant prose fell back to the Markdown default of 14px and the user bubble inherited 16px. They now use `text-[length:var(...)]`, and the dist check fails on the untyped form.
8
+
9
+ The streaming caret follows the last word instead of starting a new line. A code block that scrolls sideways takes keyboard focus as a named region (axe `scrollable-region-focusable`). The Show more toggle under a tool's output or error sits inside the card's inset instead of against its edge.
10
+
3
11
  ## 11.16.3
4
12
 
5
13
  ### Patch Changes
@@ -53,13 +53,14 @@ function spacingBefore(prev, next) {
53
53
  return "mt-3";
54
54
  }
55
55
  const ROW_BLEED = "-mx-3";
56
+ const STREAMING_CARET = "[&>:last-child:not(ul,ol)]:after:ml-0.5 [&>:last-child:not(ul,ol)]:after:inline-block [&>:last-child:not(ul,ol)]:after:h-[1em] [&>:last-child:not(ul,ol)]:after:w-0.5 [&>:last-child:not(ul,ol)]:after:animate-pulse [&>:last-child:not(ul,ol)]:after:rounded-full [&>:last-child:not(ul,ol)]:after:bg-muted-foreground [&>:last-child:not(ul,ol)]:after:align-text-bottom [&>:last-child:not(ul,ol)]:after:content-[''] [&>:is(ul,ol):last-child>li:last-child]:after:ml-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:inline-block [&>:is(ul,ol):last-child>li:last-child]:after:h-[1em] [&>:is(ul,ol):last-child>li:last-child]:after:w-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:animate-pulse [&>:is(ul,ol):last-child>li:last-child]:after:rounded-full [&>:is(ul,ol):last-child>li:last-child]:after:bg-muted-foreground [&>:is(ul,ol):last-child>li:last-child]:after:align-text-bottom [&>:is(ul,ol):last-child>li:last-child]:after:content-['']";
56
57
  function AssistantMessage({ item }) {
57
58
  return /* @__PURE__ */ jsxs("div", { children: [
58
59
  item.content && /* @__PURE__ */ jsx(Markdown, {
59
- className: "tangle-prose text-[var(--font-size-base)] leading-[1.5]",
60
+ className: cn("tangle-prose text-[length:var(--font-size-base)] leading-[1.5]", item.isStreaming && STREAMING_CARET),
60
61
  children: item.content
61
62
  }),
62
- item.isStreaming && /* @__PURE__ */ jsx("span", {
63
+ item.isStreaming && !item.content && /* @__PURE__ */ jsx("span", {
63
64
  "aria-hidden": true,
64
65
  "data-streaming-caret": "",
65
66
  className: "ml-0.5 inline-block h-[1em] w-0.5 animate-pulse rounded-full bg-muted-foreground align-text-bottom"
@@ -19,10 +19,10 @@ function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, classNa
19
19
  }), /* @__PURE__ */ jsxs("div", {
20
20
  className: cn("min-w-0 max-w-[85%] space-y-1", isUser ? "rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background" : isAssistant ? "text-foreground" : "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]"),
21
21
  children: [isUser ? /* @__PURE__ */ jsx("div", {
22
- className: "whitespace-pre-wrap text-[var(--font-size-base)] leading-[var(--line-height-base)]",
22
+ className: "whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]",
23
23
  children: content
24
24
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [content && /* @__PURE__ */ jsx(Markdown, {
25
- className: "tangle-prose text-[var(--font-size-base)] leading-[var(--line-height-base)]",
25
+ className: "tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]",
26
26
  children: content
27
27
  }), isStreaming && /* @__PURE__ */ jsx("span", { className: "ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-[var(--brand-cool)] align-text-bottom" })] }), toolCalls]
28
28
  })]
@@ -29,7 +29,7 @@ function ThinkingIndicator({ className }) {
29
29
  })
30
30
  ]
31
31
  }), elapsed > 3 && /* @__PURE__ */ jsxs("span", {
32
- className: "text-[var(--font-size-xs)] tabular-nums text-muted-foreground",
32
+ className: "text-[length:var(--font-size-xs)] tabular-nums text-muted-foreground",
33
33
  children: [elapsed, "s"]
34
34
  })]
35
35
  });
@@ -23,10 +23,10 @@ function UserBubble({ text, timestamp, actions }) {
23
23
  className: "relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background",
24
24
  children: [timestamp ? /* @__PURE__ */ jsx("span", {
25
25
  "data-user-message-time": "",
26
- className: "pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
26
+ className: "pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
27
27
  children: formatTime(timestamp)
28
28
  }) : null, /* @__PURE__ */ jsx("div", {
29
- className: "break-words whitespace-pre-wrap text-[var(--font-size-base)] leading-[1.5]",
29
+ className: "break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]",
30
30
  children: text
31
31
  })]
32
32
  }), actions ? /* @__PURE__ */ jsx(MessageActions, { children: actions }) : null]
@@ -1,4 +1,4 @@
1
- import { focusRing } from "../lib/focus.js";
1
+ import { focusRing, focusRingInset } from "../lib/focus.js";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { memo, useCallback, useEffect, useRef, useState } from "react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -93,6 +93,12 @@ const CodeBlock = memo(({ code, language, label, showLineNumbers = false, light,
93
93
  overflowX: "auto"
94
94
  };
95
95
  const codeTagProps = { style: { fontFamily: "var(--font-mono, 'JetBrains Mono', ui-monospace, monospace)" } };
96
+ const scrollRegion = {
97
+ tabIndex: 0,
98
+ role: "region",
99
+ "aria-label": headerLabel ? `${headerLabel} code` : "Code",
100
+ className: focusRingInset
101
+ };
96
102
  return /* @__PURE__ */ jsxs("div", {
97
103
  className: cn("group relative overflow-hidden rounded-lg border font-mono", "bg-card border-border", className),
98
104
  ...props,
@@ -121,8 +127,10 @@ const CodeBlock = memo(({ code, language, label, showLineNumbers = false, light,
121
127
  customStyle,
122
128
  codeTagProps,
123
129
  wrapLines: false,
130
+ ...scrollRegion,
124
131
  children: code
125
132
  }) : /* @__PURE__ */ jsx("pre", {
133
+ ...scrollRegion,
126
134
  style: {
127
135
  ...syntaxTheme.hljs,
128
136
  ...customStyle
@@ -7,7 +7,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
7
7
  * "Show more / Show less" toggle. Content shorter than the cap renders
8
8
  * as-is with no toggle.
9
9
  */
10
- function ExpandableContent({ children, collapsedMaxPx = 180, fadeClassName = "from-muted", className }) {
10
+ function ExpandableContent({ children, collapsedMaxPx = 180, fadeClassName = "from-muted", className, toggleClassName }) {
11
11
  const [expanded, setExpanded] = useState(false);
12
12
  const [overflowing, setOverflowing] = useState(false);
13
13
  const contentRef = useRef(null);
@@ -32,7 +32,7 @@ function ExpandableContent({ children, collapsedMaxPx = 180, fadeClassName = "fr
32
32
  }), overflowing ? /* @__PURE__ */ jsx("button", {
33
33
  type: "button",
34
34
  onClick: () => setExpanded((value) => !value),
35
- className: "mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
35
+ className: cn("mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground", toggleClassName),
36
36
  children: expanded ? "Show less" : "Show more"
37
37
  }) : null]
38
38
  });
@@ -13,6 +13,7 @@ import { memo } from "react";
13
13
  import { jsx, jsxs } from "react/jsx-runtime";
14
14
  import { AlertCircle, ArrowLeft, ArrowRight, FileText, Loader2 } from "lucide-react";
15
15
  //#region src/run/expanded-tool-detail.tsx
16
+ const FLUSH_TOGGLE = "mx-3 mb-2";
16
17
  const EXT_LANG = {
17
18
  ts: "typescript",
18
19
  tsx: "tsx",
@@ -131,6 +132,7 @@ const ExpandedToolDetail = memo(({ part }) => {
131
132
  })]
132
133
  }), /* @__PURE__ */ jsx(ExpandableContent, {
133
134
  fadeClassName: "from-card",
135
+ toggleClassName: FLUSH_TOGGLE,
134
136
  children: /* @__PURE__ */ jsx(CodeBlock, {
135
137
  code: outputStr.length > 2e3 ? outputStr.slice(0, 2e3) + "\n...(truncated)" : outputStr,
136
138
  language: "json",
@@ -148,6 +150,7 @@ const ExpandedToolDetail = memo(({ part }) => {
148
150
  })]
149
151
  }), /* @__PURE__ */ jsx(ExpandableContent, {
150
152
  fadeClassName: "from-[var(--surface-danger-bg)]",
153
+ toggleClassName: FLUSH_TOGGLE,
151
154
  children: /* @__PURE__ */ jsx("pre", {
152
155
  className: "bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]",
153
156
  children: error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.16.3",
3
+ "version": "11.16.4",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -253,20 +253,6 @@ describe("AgentTimeline tool actions", () => {
253
253
  expect(classes[4]).toContain("mt-4") // prose → user
254
254
  })
255
255
 
256
- it("softens the streaming cursor to a muted caret", () => {
257
- const { container } = render(
258
- <AgentTimeline
259
- items={[
260
- { id: "a-1", kind: "message", role: "assistant", content: "Typing", isStreaming: true },
261
- ]}
262
- />,
263
- )
264
- const caret = container.querySelector("[data-streaming-caret]")
265
- expect(caret).not.toBeNull()
266
- expect(caret!.className).toContain("bg-muted-foreground")
267
- expect(caret!.className).not.toContain("bg-primary")
268
- })
269
-
270
256
  it("renders the source part's real input in the expanded detail", async () => {
271
257
  const user = userEvent.setup()
272
258
  const probePart: ToolPart = {
@@ -162,13 +162,29 @@ function spacingBefore(prev: StepKind | undefined, next: StepKind): string {
162
162
  // wider than the bleed, so nothing clips.
163
163
  const ROW_BLEED = "-mx-3";
164
164
 
165
+ // The streaming caret ends the last line of text. It is drawn as the `::after`
166
+ // of the prose's last block (the last item of a trailing list), so it follows
167
+ // the final word instead of starting a line of its own below the paragraph.
168
+ const STREAMING_CARET =
169
+ "[&>:last-child:not(ul,ol)]:after:ml-0.5 [&>:last-child:not(ul,ol)]:after:inline-block [&>:last-child:not(ul,ol)]:after:h-[1em] [&>:last-child:not(ul,ol)]:after:w-0.5 [&>:last-child:not(ul,ol)]:after:animate-pulse [&>:last-child:not(ul,ol)]:after:rounded-full [&>:last-child:not(ul,ol)]:after:bg-muted-foreground [&>:last-child:not(ul,ol)]:after:align-text-bottom [&>:last-child:not(ul,ol)]:after:content-[''] " +
170
+ "[&>:is(ul,ol):last-child>li:last-child]:after:ml-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:inline-block [&>:is(ul,ol):last-child>li:last-child]:after:h-[1em] [&>:is(ul,ol):last-child>li:last-child]:after:w-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:animate-pulse [&>:is(ul,ol):last-child>li:last-child]:after:rounded-full [&>:is(ul,ol):last-child>li:last-child]:after:bg-muted-foreground [&>:is(ul,ol):last-child>li:last-child]:after:align-text-bottom [&>:is(ul,ol):last-child>li:last-child]:after:content-['']";
171
+
165
172
  function AssistantMessage({ item }: { item: AgentTimelineMessageItem }) {
166
173
  return (
167
174
  <div>
168
175
  {item.content && (
169
- <Markdown className="tangle-prose text-[var(--font-size-base)] leading-[1.5]">{item.content}</Markdown>
176
+ <Markdown
177
+ // `length:` types the token: a bare `text-[var(--x)]` compiles to a
178
+ // color, which left the reading size at the Markdown default.
179
+ className={cn(
180
+ "tangle-prose text-[length:var(--font-size-base)] leading-[1.5]",
181
+ item.isStreaming && STREAMING_CARET,
182
+ )}
183
+ >
184
+ {item.content}
185
+ </Markdown>
170
186
  )}
171
- {item.isStreaming && (
187
+ {item.isStreaming && !item.content && (
172
188
  <span
173
189
  aria-hidden
174
190
  data-streaming-caret=""
@@ -75,12 +75,12 @@ export function ChatMessage({
75
75
  )}
76
76
  >
77
77
  {isUser ? (
78
- <div className="whitespace-pre-wrap text-[var(--font-size-base)] leading-[var(--line-height-base)]">
78
+ <div className="whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]">
79
79
  {content}
80
80
  </div>
81
81
  ) : (
82
82
  <>
83
- {content && <Markdown className="tangle-prose text-[var(--font-size-base)] leading-[var(--line-height-base)]">{content}</Markdown>}
83
+ {content && <Markdown className="tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]">{content}</Markdown>}
84
84
  {isStreaming && (
85
85
  <span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-[var(--brand-cool)] align-text-bottom" />
86
86
  )}
@@ -25,7 +25,7 @@ export function ThinkingIndicator({ className }: ThinkingIndicatorProps) {
25
25
  <span className="h-[var(--indicator-dot-size)] w-[var(--indicator-dot-size)] animate-bounce rounded-full bg-[var(--brand-glow)]" style={{ animationDelay: "300ms" }} />
26
26
  </div>
27
27
  {elapsed > 3 && (
28
- <span className="text-[var(--font-size-xs)] tabular-nums text-muted-foreground">
28
+ <span className="text-[length:var(--font-size-xs)] tabular-nums text-muted-foreground">
29
29
  {elapsed}s
30
30
  </span>
31
31
  )}
@@ -37,12 +37,12 @@ function UserBubble({ text, timestamp, actions }: {
37
37
  {timestamp ? (
38
38
  <span
39
39
  data-user-message-time=""
40
- className="pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
40
+ className="pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
41
41
  >
42
42
  {formatTime(timestamp)}
43
43
  </span>
44
44
  ) : null}
45
- <div className="break-words whitespace-pre-wrap text-[var(--font-size-base)] leading-[1.5]">
45
+ <div className="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">
46
46
  {text}
47
47
  </div>
48
48
  </div>
@@ -9,7 +9,7 @@ import {
9
9
  } from "react";
10
10
  import type SyntaxHighlighter from "react-syntax-highlighter";
11
11
  import { Check, Copy } from "lucide-react";
12
- import { focusRing } from "../lib/focus";
12
+ import { focusRing, focusRingInset } from "../lib/focus";
13
13
  import { cn } from "../lib/utils";
14
14
 
15
15
  // Leave variables unresolved: the browser resolves them on each token span,
@@ -118,6 +118,17 @@ export const CodeBlock = memo(
118
118
  overflowX: "auto" as const,
119
119
  };
120
120
  const codeTagProps = { style: { fontFamily: "var(--font-mono, 'JetBrains Mono', ui-monospace, monospace)" } };
121
+ // A long line scrolls the block sideways, which a keyboard reaches only if
122
+ // the block takes focus (axe scrollable-region-focusable). A label on a bare
123
+ // <pre> is prohibited ARIA, so the block is a named region, as the terminal
124
+ // output regions are.
125
+ const scrollRegion = {
126
+ tabIndex: 0,
127
+ role: "region",
128
+ "aria-label": headerLabel ? `${headerLabel} code` : "Code",
129
+ // Inset: the rounded, overflow-clipped card would cut an outer ring.
130
+ className: focusRingInset,
131
+ } as const;
121
132
 
122
133
  return (
123
134
  <div
@@ -151,11 +162,12 @@ export const CodeBlock = memo(
151
162
  customStyle={customStyle}
152
163
  codeTagProps={codeTagProps}
153
164
  wrapLines={false}
165
+ {...scrollRegion}
154
166
  >
155
167
  {code}
156
168
  </Highlighter>
157
169
  ) : (
158
- <pre style={{ ...syntaxTheme.hljs, ...customStyle }}>
170
+ <pre {...scrollRegion} style={{ ...syntaxTheme.hljs, ...customStyle }}>
159
171
  <code style={{ ...codeTagProps.style, whiteSpace: "pre" }}>{code}</code>
160
172
  </pre>
161
173
  )}
@@ -11,6 +11,9 @@ export interface ExpandableContentProps {
11
11
  */
12
12
  fadeClassName?: string;
13
13
  className?: string;
14
+ /** Classes for the Show more / Show less toggle, e.g. an inset when the
15
+ * content sits flush against its card's edges. */
16
+ toggleClassName?: string;
14
17
  }
15
18
 
16
19
  /**
@@ -23,6 +26,7 @@ export function ExpandableContent({
23
26
  collapsedMaxPx = 180,
24
27
  fadeClassName = "from-muted",
25
28
  className,
29
+ toggleClassName,
26
30
  }: ExpandableContentProps) {
27
31
  const [expanded, setExpanded] = useState(false);
28
32
  const [overflowing, setOverflowing] = useState(false);
@@ -63,7 +67,10 @@ export function ExpandableContent({
63
67
  <button
64
68
  type="button"
65
69
  onClick={() => setExpanded((value) => !value)}
66
- className="mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
70
+ className={cn(
71
+ "mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
72
+ toggleClassName,
73
+ )}
67
74
  >
68
75
  {expanded ? "Show less" : "Show more"}
69
76
  </button>
@@ -23,6 +23,10 @@ export interface ExpandedToolDetailProps {
23
23
  part: ToolPart;
24
24
  }
25
25
 
26
+ // The clamped output sits flush against its card's edges; the toggle under it
27
+ // takes the card's 12px content inset so it does not touch the border.
28
+ const FLUSH_TOGGLE = "mx-3 mb-2";
29
+
26
30
  const EXT_LANG: Record<string, string> = {
27
31
  ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx",
28
32
  rs: "rust", py: "python", go: "go", rb: "ruby",
@@ -147,7 +151,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
147
151
  Output
148
152
  </span>
149
153
  </div>
150
- <ExpandableContent fadeClassName="from-card">
154
+ <ExpandableContent fadeClassName="from-card" toggleClassName={FLUSH_TOGGLE}>
151
155
  <CodeBlock
152
156
  code={
153
157
  outputStr.length > 2000
@@ -170,7 +174,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
170
174
  Error
171
175
  </span>
172
176
  </div>
173
- <ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]">
177
+ <ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]" toggleClassName={FLUSH_TOGGLE}>
174
178
  <pre className="bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]">
175
179
  {error}
176
180
  </pre>