@tangle-network/ui 11.16.10 → 11.16.11

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,11 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.16.11
4
+
5
+ ### Patch Changes
6
+
7
+ - cbfa303: A terminal output whose height cap hides lines ends in a "Show all stdout (N lines)" control that shows the rest in place, so a region that stops flush with its frame no longer hides that more output exists.
8
+
3
9
  ## 11.16.10
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  import { focusRing } from "../lib/focus.js";
2
2
  import { cn } from "../lib/utils.js";
3
- import { memo, useId, useState } from "react";
3
+ import { memo, useId, useLayoutEffect, useRef, useState } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
6
6
  //#region src/tool-previews/command-preview.tsx
@@ -146,6 +146,49 @@ function CommandText({ command }) {
146
146
  }, i) : token) });
147
147
  }
148
148
  /**
149
+ * One output stream, capped at a readable height. When the cap hides lines,
150
+ * a control says so and shows the rest in place: a region that ends flush
151
+ * with its frame gave no sign that more output was there.
152
+ */
153
+ function OutputRegion({ label, text, className }) {
154
+ const ref = useRef(null);
155
+ const [clipped, setClipped] = useState(false);
156
+ const [full, setFull] = useState(false);
157
+ useLayoutEffect(() => {
158
+ const el = ref.current;
159
+ if (!el || full) return;
160
+ const check = () => setClipped(el.scrollHeight > el.clientHeight + 1);
161
+ check();
162
+ if (typeof ResizeObserver === "undefined") return;
163
+ const observer = new ResizeObserver(check);
164
+ observer.observe(el);
165
+ return () => observer.disconnect();
166
+ }, [full]);
167
+ const lines = text.replace(/\n$/, "").split("\n").length;
168
+ return /* @__PURE__ */ jsxs("div", {
169
+ className,
170
+ children: [/* @__PURE__ */ jsx("pre", {
171
+ ref,
172
+ tabIndex: 0,
173
+ role: "region",
174
+ "aria-label": label,
175
+ className: cn("overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] px-3 py-2 leading-relaxed", full ? "" : "max-h-80", focusRing),
176
+ children: text
177
+ }), clipped && !full ? /* @__PURE__ */ jsxs("button", {
178
+ type: "button",
179
+ onClick: () => setFull(true),
180
+ className: cn("w-full border-t border-white/10 px-3 py-1.5 text-left font-sans text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground", focusRing),
181
+ children: [
182
+ "Show all ",
183
+ label,
184
+ " (",
185
+ lines,
186
+ " lines)"
187
+ ]
188
+ }) : null]
189
+ });
190
+ }
191
+ /**
149
192
  * One shell command as a terminal block: a `$ command` prompt line with its
150
193
  * exit status, and stdout and stderr in separate regions below it.
151
194
  *
@@ -208,26 +251,20 @@ const CommandPreview = memo(({ part, defaultExpanded = false, className }) => {
208
251
  id: bodyId,
209
252
  className: "border-t border-white/10",
210
253
  children: [
211
- output.stdout ? /* @__PURE__ */ jsx("pre", {
212
- tabIndex: 0,
213
- role: "region",
214
- "aria-label": "stdout",
215
- className: cn("max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] px-3 py-2 leading-relaxed", focusRing),
216
- children: output.stdout
254
+ output.stdout ? /* @__PURE__ */ jsx(OutputRegion, {
255
+ label: "stdout",
256
+ text: output.stdout,
257
+ className: ""
217
258
  }) : null,
218
- output.stderr ? /* @__PURE__ */ jsx("pre", {
219
- tabIndex: 0,
220
- role: "region",
221
- "aria-label": "stderr",
222
- className: cn("max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]", focusRing),
223
- children: output.stderr
259
+ output.stderr ? /* @__PURE__ */ jsx(OutputRegion, {
260
+ label: "stderr",
261
+ text: output.stderr,
262
+ className: "border-t border-white/10 text-[var(--surface-danger-text)]"
224
263
  }) : null,
225
- errorText ? /* @__PURE__ */ jsx("pre", {
226
- tabIndex: 0,
227
- role: "region",
228
- "aria-label": "error",
229
- className: cn("max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]", focusRing),
230
- children: errorText
264
+ errorText ? /* @__PURE__ */ jsx(OutputRegion, {
265
+ label: "error",
266
+ text: errorText,
267
+ className: "border-t border-white/10 text-[var(--surface-danger-text)]"
231
268
  }) : null
232
269
  ]
233
270
  }) : null]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.16.10",
3
+ "version": "11.16.11",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -80,3 +80,21 @@ describe("CommandPreview", () => {
80
80
  expect(screen.getByRole("region", { name: "stdout" })).toHaveTextContent("M a");
81
81
  });
82
82
  });
83
+
84
+ describe("CommandPreview output", () => {
85
+ it("offers the rest of an output its height cap hides", () => {
86
+ const scroll = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
87
+ const client = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "clientHeight");
88
+ Object.defineProperty(HTMLElement.prototype, "scrollHeight", { configurable: true, get: () => 900 });
89
+ Object.defineProperty(HTMLElement.prototype, "clientHeight", { configurable: true, get: () => 320 });
90
+ try {
91
+ render(<CommandPreview part={part({ output: "a\nb\nc" })} defaultExpanded />);
92
+ fireEvent.click(screen.getByRole("button", { name: "Show all stdout (3 lines)" }));
93
+ expect(screen.getByRole("region", { name: "stdout" }).className).not.toContain("max-h-80");
94
+ expect(screen.queryByRole("button", { name: /Show all stdout/ })).toBeNull();
95
+ } finally {
96
+ if (scroll) Object.defineProperty(HTMLElement.prototype, "scrollHeight", scroll);
97
+ if (client) Object.defineProperty(HTMLElement.prototype, "clientHeight", client);
98
+ }
99
+ });
100
+ });
@@ -1,4 +1,4 @@
1
- import { memo, useId, useState } from "react";
1
+ import { memo, useId, useLayoutEffect, useRef, useState } from "react";
2
2
  import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
3
3
  import { focusRing } from "../lib/focus";
4
4
  import { cn } from "../lib/utils";
@@ -205,6 +205,65 @@ function CommandText({ command }: { command: string }) {
205
205
  );
206
206
  }
207
207
 
208
+ /**
209
+ * One output stream, capped at a readable height. When the cap hides lines,
210
+ * a control says so and shows the rest in place: a region that ends flush
211
+ * with its frame gave no sign that more output was there.
212
+ */
213
+ function OutputRegion({
214
+ label,
215
+ text,
216
+ className,
217
+ }: {
218
+ label: string;
219
+ text: string;
220
+ className?: string;
221
+ }) {
222
+ const ref = useRef<HTMLPreElement>(null);
223
+ const [clipped, setClipped] = useState(false);
224
+ const [full, setFull] = useState(false);
225
+ useLayoutEffect(() => {
226
+ const el = ref.current;
227
+ if (!el || full) return;
228
+ const check = () => setClipped(el.scrollHeight > el.clientHeight + 1);
229
+ check();
230
+ if (typeof ResizeObserver === "undefined") return;
231
+ const observer = new ResizeObserver(check);
232
+ observer.observe(el);
233
+ return () => observer.disconnect();
234
+ }, [full]);
235
+ const lines = text.replace(/\n$/, "").split("\n").length;
236
+ return (
237
+ <div className={className}>
238
+ <pre
239
+ ref={ref}
240
+ tabIndex={0}
241
+ role="region"
242
+ aria-label={label}
243
+ className={cn(
244
+ "overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] px-3 py-2 leading-relaxed",
245
+ full ? "" : "max-h-80",
246
+ focusRing,
247
+ )}
248
+ >
249
+ {text}
250
+ </pre>
251
+ {clipped && !full ? (
252
+ <button
253
+ type="button"
254
+ onClick={() => setFull(true)}
255
+ className={cn(
256
+ "w-full border-t border-white/10 px-3 py-1.5 text-left font-sans text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
257
+ focusRing,
258
+ )}
259
+ >
260
+ Show all {label} ({lines} lines)
261
+ </button>
262
+ ) : null}
263
+ </div>
264
+ );
265
+ }
266
+
208
267
  /**
209
268
  * One shell command as a terminal block: a `$ command` prompt line with its
210
269
  * exit status, and stdout and stderr in separate regions below it.
@@ -305,43 +364,25 @@ export const CommandPreview = memo(
305
364
  {expanded && hasBody ? (
306
365
  <div id={bodyId} className="border-t border-white/10">
307
366
  {output.stdout ? (
308
- <pre
309
- tabIndex={0}
310
- role="region"
311
- aria-label="stdout"
312
- className={cn(
313
- "max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] px-3 py-2 leading-relaxed",
314
- focusRing,
315
- )}
316
- >
317
- {output.stdout}
318
- </pre>
367
+ <OutputRegion
368
+ label="stdout"
369
+ text={output.stdout}
370
+ className=""
371
+ />
319
372
  ) : null}
320
373
  {output.stderr ? (
321
- <pre
322
- tabIndex={0}
323
- role="region"
324
- aria-label="stderr"
325
- className={cn(
326
- "max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
327
- focusRing,
328
- )}
329
- >
330
- {output.stderr}
331
- </pre>
374
+ <OutputRegion
375
+ label="stderr"
376
+ text={output.stderr}
377
+ className="border-t border-white/10 text-[var(--surface-danger-text)]"
378
+ />
332
379
  ) : null}
333
380
  {errorText ? (
334
- <pre
335
- tabIndex={0}
336
- role="region"
337
- aria-label="error"
338
- className={cn(
339
- "max-h-80 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] border-t border-white/10 px-3 py-2 leading-relaxed text-[var(--surface-danger-text)]",
340
- focusRing,
341
- )}
342
- >
343
- {errorText}
344
- </pre>
381
+ <OutputRegion
382
+ label="error"
383
+ text={errorText}
384
+ className="border-t border-white/10 text-[var(--surface-danger-text)]"
385
+ />
345
386
  ) : null}
346
387
  </div>
347
388
  ) : null}