@spunto/design-system 0.35.0 → 0.36.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spunto/design-system",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Spunto's shared design system — warm/flame tokens, color constants, and UI primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,10 +20,13 @@ export interface TelemetryLogLine {
20
20
  attributes?: Record<string, string>
21
21
  }
22
22
 
23
- /** Tailwind text classes of an OTel severity text ("ERROR", "WARN", "INFO2"…). */
23
+ /**
24
+ * Tailwind text classes of a severity text: OTel's ("ERROR", "WARN", "INFO2"…) and what loggers
25
+ * write on their own — Python's `CRITICAL`, which OTel maps to FATAL but a store keeps as written.
26
+ */
24
27
  export function severityClass(severity: string | null | undefined): string {
25
28
  const s = (severity ?? "").toUpperCase()
26
- if (s.startsWith("ERROR") || s.startsWith("FATAL")) return "text-destructive"
29
+ if (s.startsWith("ERROR") || s.startsWith("FATAL") || s.startsWith("CRITICAL")) return "text-destructive"
27
30
  if (s.startsWith("WARN")) return "text-amber-600 dark:text-amber-400"
28
31
  return "text-muted-foreground"
29
32
  }
@@ -12,6 +12,7 @@ import {
12
12
  orderSpans,
13
13
  percentile,
14
14
  routeOf,
15
+ severityClass,
15
16
  SpanWaterfall,
16
17
  spanTree,
17
18
  timeBuckets,
@@ -242,3 +243,13 @@ describe("SpanWaterfall — repli", () => {
242
243
  expect(screen.queryByText("agent.cmd spawn")).toBeNull()
243
244
  })
244
245
  })
246
+
247
+ describe("severityClass", () => {
248
+ it("colore en rouge ce qui est grave, quel que soit le vocabulaire du logger", () => {
249
+ for (const sev of ["ERROR", "error", "ERROR2", "FATAL", "CRITICAL", "critical"]) expect(severityClass(sev)).toBe("text-destructive")
250
+ expect(severityClass("WARN")).toContain("amber")
251
+ expect(severityClass("WARNING")).toContain("amber")
252
+ expect(severityClass("INFO")).toBe("text-muted-foreground")
253
+ expect(severityClass(null)).toBe("text-muted-foreground")
254
+ })
255
+ })
@@ -14,7 +14,7 @@
14
14
  // machine), then how it was set up.
15
15
 
16
16
  import { createContext, useContext, useState, type ReactNode } from "react"
17
- import { ChevronRightIcon, ExternalLinkIcon, GitBranchIcon, Loader2Icon, XCircleIcon } from "lucide-react"
17
+ import { CheckIcon, ChevronRightIcon, CopyIcon, ExternalLinkIcon, GitBranchIcon, Loader2Icon, XCircleIcon } from "lucide-react"
18
18
 
19
19
  import { cn, formatRelativeTime } from "../../utils"
20
20
  import { TaskCommandLog } from "./command-log"
@@ -55,6 +55,27 @@ function Fact({ label, children }: { label: string; children: ReactNode }) {
55
55
  )
56
56
  }
57
57
 
58
+ /** The branch is what you type next (`git fetch`, `git checkout`), so it is one click from the clipboard. */
59
+ function CopyBranchButton({ branch }: { branch: string }) {
60
+ const [copied, setCopied] = useState(false)
61
+ return (
62
+ <button
63
+ type="button"
64
+ title={copied ? "Copied" : "Copy branch name"}
65
+ aria-label="Copy branch name"
66
+ onClick={() => {
67
+ navigator.clipboard?.writeText(branch).then(() => {
68
+ setCopied(true)
69
+ setTimeout(() => setCopied(false), 1500)
70
+ })
71
+ }}
72
+ className="mt-0.5 shrink-0 text-muted-foreground transition-colors hover:text-foreground"
73
+ >
74
+ {copied ? <CheckIcon className="h-3 w-3 text-emerald-600 dark:text-emerald-400" /> : <CopyIcon className="h-3 w-3" />}
75
+ </button>
76
+ )
77
+ }
78
+
58
79
  function Block({
59
80
  variant,
60
81
  title,
@@ -140,7 +161,8 @@ export function TaskDetails({
140
161
  <Fact label="Branch">
141
162
  <span className="flex items-start gap-1 font-mono break-all">
142
163
  <GitBranchIcon className="mt-0.5 h-3 w-3 shrink-0 text-muted-foreground" />
143
- {task.branch}
164
+ <span className="min-w-0">{task.branch}</span>
165
+ <CopyBranchButton branch={task.branch} />
144
166
  </span>
145
167
  </Fact>
146
168
  </div>
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { ReactNode } from "react"
9
9
  import { describe, it, expect, vi } from "vitest"
10
- import { render, screen, fireEvent } from "@testing-library/react"
10
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react"
11
11
 
12
12
  import { renderInlineMarkdown } from "./agent-markdown"
13
13
  import { emptyCommandTerminalState, planCommandTerminalWrites } from "./command-terminal"
@@ -15,6 +15,7 @@ import { TaskComposer } from "./composer"
15
15
  import { TaskBoot, bootSteps } from "./machine"
16
16
  import { formatCost, formatTokens } from "./session-usage"
17
17
  import { TaskCockpit } from "./task-cockpit"
18
+ import { TaskDetails } from "./task-details"
18
19
  import { TaskList } from "./task-list"
19
20
  import { taskSpan } from "./task-state"
20
21
  import { TaskTitle } from "./task-title"
@@ -382,3 +383,14 @@ describe("TaskCockpit panels", () => {
382
383
  expect(screen.getByText("session panel")).toBeTruthy()
383
384
  })
384
385
  })
386
+
387
+ describe("TaskDetails", () => {
388
+ it("copies the branch name from its icon", async () => {
389
+ const writeText = vi.fn().mockResolvedValue(undefined)
390
+ Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true })
391
+ render(<TaskDetails task={task()} />)
392
+ fireEvent.click(screen.getByRole("button", { name: "Copy branch name" }))
393
+ expect(writeText).toHaveBeenCalledWith("task/fix-login-a1b2c3")
394
+ await waitFor(() => expect(screen.getByTitle("Copied")).toBeTruthy())
395
+ })
396
+ })