@tangle-network/ui 11.2.3 → 11.2.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,15 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 2088fb9: Retune the token spine to a neutral-grey surface ladder with indigo as trim rather than as field, in both themes. Surfaces separate by their own fill so a card, a nested panel and an overlay each read as a distinct plane, and the canvas sits off pure black so long reading sessions land away from the glare end of the range.
8
+
9
+ Faint text in `Input`, `Textarea`, `StatCard` and `TerminalLine` now takes a dedicated `--text-dim` token instead of a faded stronger one. A translucent foreground renders as its colour composited over the plane behind it, so those hints, subtitles and timestamps measured differently on a card than on the canvas and fell under the 4.5:1 floor on both.
10
+
11
+ **Worth a look after upgrading:** in dark mode `--sidebar-background` is now one step BELOW the canvas rather than above it, so the nav reads as chrome the content sits in front of instead of as another raised card. Apps that composited their own surfaces on top of the sidebar assuming it was the lighter plane should give that area a visual pass. Light mode is unchanged — the sidebar is still paper on a grey canvas.
12
+
3
13
  ## 11.2.3
4
14
 
5
15
  ### Patch Changes
@@ -163,7 +163,7 @@ var Input = React2.forwardRef(
163
163
  ),
164
164
  input,
165
165
  error && /* @__PURE__ */ jsx2("p", { className: "text-[var(--surface-danger-text)] text-sm font-medium", children: error }),
166
- hint && !error && /* @__PURE__ */ jsx2("p", { className: "text-muted-foreground/70 text-sm", children: hint })
166
+ hint && !error && /* @__PURE__ */ jsx2("p", { className: "text-[var(--text-dim)] text-sm", children: hint })
167
167
  ] });
168
168
  }
169
169
  );
@@ -203,7 +203,7 @@ var Textarea = React2.forwardRef(
203
203
  ),
204
204
  textarea,
205
205
  error && /* @__PURE__ */ jsx2("p", { className: "text-[var(--surface-danger-text)] text-sm", children: error }),
206
- hint && !error && /* @__PURE__ */ jsx2("p", { className: "text-muted-foreground/70 text-sm", children: hint })
206
+ hint && !error && /* @__PURE__ */ jsx2("p", { className: "text-[var(--text-dim)] text-sm", children: hint })
207
207
  ] });
208
208
  }
209
209
  );
@@ -755,7 +755,7 @@ var StatCard = React10.forwardRef(
755
755
  /* @__PURE__ */ jsxs9("div", { className: "space-y-1", children: [
756
756
  /* @__PURE__ */ jsx11("p", { className: "text-muted-foreground text-sm", children: title }),
757
757
  /* @__PURE__ */ jsx11("p", { className: "font-bold text-3xl tracking-tight", children: value }),
758
- subtitle && /* @__PURE__ */ jsx11("p", { className: "text-muted-foreground opacity-70 text-xs", children: subtitle }),
758
+ subtitle && /* @__PURE__ */ jsx11("p", { className: "text-[var(--text-dim)] text-xs", children: subtitle }),
759
759
  trend && trendStatus && /* @__PURE__ */ jsxs9(
760
760
  "div",
761
761
  {
@@ -872,7 +872,7 @@ var TerminalLine = React11.forwardRef(
872
872
  children: [
873
873
  (type === "input" || type === "command") && /* @__PURE__ */ jsx12("span", { className: "shrink-0 select-none text-[var(--surface-success-text)]", children: prompt }),
874
874
  type === "thinking" && /* @__PURE__ */ jsx12("span", { className: "shrink-0 select-none", children: "..." }),
875
- timestamp && /* @__PURE__ */ jsxs10("span", { className: "shrink-0 select-none text-muted-foreground opacity-50", children: [
875
+ timestamp && /* @__PURE__ */ jsxs10("span", { className: "shrink-0 select-none text-[var(--text-dim)]", children: [
876
876
  "[",
877
877
  timestamp,
878
878
  "]"
package/dist/index.js CHANGED
@@ -109,7 +109,7 @@ import {
109
109
  UploadProgress,
110
110
  useTheme,
111
111
  useToast
112
- } from "./chunk-XZFJAF6G.js";
112
+ } from "./chunk-HSMVDKL3.js";
113
113
  import {
114
114
  Avatar,
115
115
  AvatarFallback,
@@ -44,7 +44,7 @@ import {
44
44
  UploadProgress,
45
45
  useTheme,
46
46
  useToast
47
- } from "./chunk-XZFJAF6G.js";
47
+ } from "./chunk-HSMVDKL3.js";
48
48
  import {
49
49
  Avatar,
50
50
  AvatarFallback,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.2.3",
3
+ "version": "11.2.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",
@@ -141,7 +141,7 @@
141
141
  "react-dom": "^18 || ^19",
142
142
  "react-router": "^7 || ^8",
143
143
  "yjs": "^13.6.0",
144
- "@tangle-network/brand": "^1.1.0"
144
+ "@tangle-network/brand": "^1.3.0"
145
145
  },
146
146
  "peerDependenciesMeta": {
147
147
  "@nanostores/react": {
@@ -0,0 +1,127 @@
1
+ import { render, screen } from "@testing-library/react"
2
+ import { describe, expect, it } from "vitest"
3
+ import { Input, Textarea } from "./input"
4
+ import { StatCard } from "./stat-card"
5
+ import { TerminalLine } from "./terminal-display"
6
+
7
+ /**
8
+ * The quietest line in each primitive — a field hint, a stat subtitle, a log
9
+ * timestamp — must take its colour from a TOKEN, never from a faded stronger
10
+ * one.
11
+ *
12
+ * A translucent foreground does not render the colour it names: it renders that
13
+ * colour composited over whatever plane is behind it. So `text-muted-foreground/70`
14
+ * measures one ratio on a card and a different one on the canvas, and the same
15
+ * markup is legible in one place and not in another. That is how these three
16
+ * landed at 3.65:1, 2.91:1 and 3.4:1 while looking, in the source, like they had
17
+ * been styled deliberately.
18
+ *
19
+ * These assert the RULE rather than the current class string — the point is that
20
+ * faint text carries no alpha, not that it carries one specific token today.
21
+ */
22
+
23
+ /**
24
+ * A class with its variant chain removed: `dark:hover:text-white/50` → `text-white/50`.
25
+ *
26
+ * Split on the colons that separate variants, which are the ones at bracket
27
+ * depth zero — an arbitrary value carries its own (`text-[color:var(--x)]`,
28
+ * `supports-[display:grid]:`) and splitting on those would mangle the utility.
29
+ */
30
+ function bareUtility(cls: string): string {
31
+ let depth = 0
32
+ let start = 0
33
+ for (let i = 0; i < cls.length; i++) {
34
+ const c = cls[i]
35
+ if (c === "[") depth++
36
+ else if (c === "]") depth--
37
+ else if (c === ":" && depth === 0) start = i + 1
38
+ }
39
+ return cls.slice(start)
40
+ }
41
+
42
+ /** Colour utilities, where a trailing `/NN` is an alpha modifier rather than a fraction. */
43
+ const COLOUR = "text|bg|border|ring|outline|decoration|placeholder|caret|accent|fill|stroke|divide|from|via|to|shadow"
44
+ /** `text-white/70`, `bg-card/60`, `text-[var(--x)]/40`. */
45
+ const ALPHA_MODIFIER = new RegExp(`^(?:${COLOUR})-.+/\\d{1,3}$`)
46
+ /** `opacity-50` — the same fade by another route. */
47
+ const OPACITY_UTILITY = /^opacity-\d{1,3}$/
48
+
49
+ function expectSolidColour(el: HTMLElement | null, what: string) {
50
+ expect(el, `${what} should render`).not.toBeNull()
51
+ // Per class, not across the whole string: a variant prefix has no whitespace
52
+ // in it, so a single regex over the joined list never sees where one class
53
+ // ends — and `dark:text-white/50` slips past a pattern anchored on spaces.
54
+ const faded = (el?.className ?? "")
55
+ .split(/\s+/)
56
+ .filter(Boolean)
57
+ .filter((cls) => {
58
+ const bare = bareUtility(cls)
59
+ return ALPHA_MODIFIER.test(bare) || OPACITY_UTILITY.test(bare)
60
+ })
61
+ expect(faded, `${what} must take a token, not a faded colour`).toEqual([])
62
+ }
63
+
64
+ describe("the fade detector itself", () => {
65
+ // The guard below is only worth as much as this table. An earlier version
66
+ // anchored on whitespace and so never saw a variant prefix — `dark:text-white/50`
67
+ // read as clean, which is the exact mistake the guard exists to catch.
68
+ const FADED = [
69
+ "text-muted-foreground/70",
70
+ "text-[var(--text-dim)]/70",
71
+ "dark:text-white/50",
72
+ "hover:text-muted-foreground/70",
73
+ "group-hover:text-foreground/40",
74
+ "md:bg-card/60",
75
+ "supports-[display:grid]:text-white/50",
76
+ "opacity-50",
77
+ "hover:opacity-50",
78
+ ]
79
+ const SOLID = [
80
+ "text-[var(--text-dim)]",
81
+ "text-sm",
82
+ "text-muted-foreground",
83
+ "dark:text-white",
84
+ // A slash that is a FRACTION, not an alpha modifier — must not trip the guard.
85
+ "w-1/2",
86
+ "basis-1/3",
87
+ // A colon inside an arbitrary value, which must survive variant-stripping.
88
+ "text-[color:var(--text-dim)]",
89
+ ]
90
+ const isFaded = (cls: string) =>
91
+ ALPHA_MODIFIER.test(bareUtility(cls)) || OPACITY_UTILITY.test(bareUtility(cls))
92
+
93
+ for (const cls of FADED) {
94
+ it(`catches ${cls}`, () => expect(isFaded(cls)).toBe(true))
95
+ }
96
+ for (const cls of SOLID) {
97
+ it(`allows ${cls}`, () => expect(isFaded(cls)).toBe(false))
98
+ }
99
+ })
100
+
101
+ describe("faint text is a token, not a faded one", () => {
102
+ it("Input renders its hint at full strength", () => {
103
+ render(<Input label="Name" hint="Shown on your public profile" />)
104
+ expectSolidColour(
105
+ screen.getByText("Shown on your public profile"),
106
+ "the Input hint",
107
+ )
108
+ })
109
+
110
+ it("Textarea renders its hint at full strength", () => {
111
+ render(<Textarea label="Bio" hint="Markdown is supported" />)
112
+ expectSolidColour(
113
+ screen.getByText("Markdown is supported"),
114
+ "the Textarea hint",
115
+ )
116
+ })
117
+
118
+ it("StatCard renders its subtitle at full strength", () => {
119
+ render(<StatCard title="Sandboxes" value={12} subtitle="up from 9" />)
120
+ expectSolidColour(screen.getByText("up from 9"), "the StatCard subtitle")
121
+ })
122
+
123
+ it("TerminalLine renders its timestamp at full strength", () => {
124
+ render(<TerminalLine timestamp="12:04:11">compiling</TerminalLine>)
125
+ expectSolidColour(screen.getByText("[12:04:11]"), "the TerminalLine timestamp")
126
+ })
127
+ })
@@ -65,7 +65,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
65
65
  {input}
66
66
  {error && <p className="text-[var(--surface-danger-text)] text-sm font-medium">{error}</p>}
67
67
  {hint && !error && (
68
- <p className="text-muted-foreground/70 text-sm">{hint}</p>
68
+ <p className="text-[var(--text-dim)] text-sm">{hint}</p>
69
69
  )}
70
70
  </div>
71
71
  );
@@ -125,7 +125,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
125
125
  {textarea}
126
126
  {error && <p className="text-[var(--surface-danger-text)] text-sm">{error}</p>}
127
127
  {hint && !error && (
128
- <p className="text-muted-foreground/70 text-sm">{hint}</p>
128
+ <p className="text-[var(--text-dim)] text-sm">{hint}</p>
129
129
  )}
130
130
  </div>
131
131
  );
@@ -58,8 +58,12 @@ const StatCard = React.forwardRef<HTMLDivElement, StatCardProps>(
58
58
  <div className="space-y-1">
59
59
  <p className="text-muted-foreground text-sm">{title}</p>
60
60
  <p className="font-bold text-3xl tracking-tight">{value}</p>
61
+ {/* A token, never `opacity-70` over the muted tone: a translucent
62
+ foreground renders as the token COMPOSITED over whatever plane sits
63
+ behind it, so the same class lands a different colour — and a
64
+ different contrast ratio — on a card than on the canvas. */}
61
65
  {subtitle && (
62
- <p className="text-muted-foreground opacity-70 text-xs">{subtitle}</p>
66
+ <p className="text-[var(--text-dim)] text-xs">{subtitle}</p>
63
67
  )}
64
68
  {trend && trendStatus && (
65
69
  <div
@@ -113,7 +113,7 @@ const TerminalLine = React.forwardRef<HTMLDivElement, TerminalLineProps>(
113
113
  <span className="shrink-0 select-none">...</span>
114
114
  )}
115
115
  {timestamp && (
116
- <span className="shrink-0 select-none text-muted-foreground opacity-50">
116
+ <span className="shrink-0 select-none text-[var(--text-dim)]">
117
117
  [{timestamp}]
118
118
  </span>
119
119
  )}
@@ -11,6 +11,7 @@ type Story = StoryObj;
11
11
 
12
12
  const THEMES: { name: string; theme?: string }[] = [
13
13
  { name: "Tangle (neutral)", theme: undefined }, // default :root dark
14
+ { name: "Tangle · dark", theme: "tangle-dark" }, // the same spine, named
14
15
  { name: "Tangle · light", theme: "tangle-light" },
15
16
  { name: "Aubergine (bazaar)", theme: "aubergine" },
16
17
  { name: "Aubergine · light", theme: "aubergine-light" },