@tangle-network/ui 11.22.1 → 11.22.3

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,17 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.22.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 191de4b: Switch: draw the on state as an `--accent-text` track carrying a `bg-card` thumb. The `bg-primary` track measured 2.4:1 against the dark card and canvas, and its `bg-background` thumb 2.5:1 against the track, so a checked switch did not clear the 3:1 non-text floor in dark themes. Accent text clears 3:1 against the card and the canvas in every theme, which `@tangle-network/brand` now asserts per theme.
8
+
9
+ ## 11.22.2
10
+
11
+ ### Patch Changes
12
+
13
+ - e78325b: Input, Textarea, Select and the terminal prompt draw placeholder text in `--text-dim`, the faintest text tier, instead of `muted-foreground`. An empty field's example text no longer looks like an entered value; `--text-dim` still clears 4.5:1 on the field well in both themes.
14
+
3
15
  ## 11.22.1
4
16
 
5
17
  ### Patch Changes
@@ -7,7 +7,7 @@ import { cva } from "class-variance-authority";
7
7
  import * as React from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/primitives/input.tsx
10
- const inputVariants = cva(cn("flex w-full rounded-lg border px-4 py-2 text-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium file:text-sm", fieldPresentation), {
10
+ const inputVariants = cva(cn("flex w-full rounded-lg border px-4 py-2 text-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium file:text-sm", fieldPresentation), {
11
11
  variants: {
12
12
  variant: {
13
13
  default: "",
@@ -78,7 +78,7 @@ const Textarea = React.forwardRef(({ className, variant: _variant, size = "defau
78
78
  const errorId = error ? `${textareaId}-error` : void 0;
79
79
  const textarea = /* @__PURE__ */ jsx("textarea", {
80
80
  id: textareaId,
81
- className: cn("flex min-h-[120px] w-full resize-y rounded-lg border px-4 py-3 text-sm", "placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", fieldPresentation, error && "border-[var(--surface-danger-border)] hover:border-[var(--surface-danger-border)] focus:border-[var(--focus-border-danger)] focus:ring-[var(--focus-halo-danger)]", size === "compact" && "min-h-24 px-3 py-2", size === "touch" && "text-base", className),
81
+ className: cn("flex min-h-[120px] w-full resize-y rounded-lg border px-4 py-3 text-sm", "placeholder:text-[var(--text-dim)]", "disabled:cursor-not-allowed disabled:opacity-50", fieldPresentation, error && "border-[var(--surface-danger-border)] hover:border-[var(--surface-danger-border)] focus:border-[var(--focus-border-danger)] focus:ring-[var(--focus-halo-danger)]", size === "compact" && "min-h-24 px-3 py-2", size === "touch" && "text-base", className),
82
82
  ref,
83
83
  ...props,
84
84
  "aria-describedby": descriptionIds(describedBy, hintId, errorId),
@@ -11,7 +11,7 @@ const SelectGroup = SelectPrimitive.Group;
11
11
  const SelectValue = SelectPrimitive.Value;
12
12
  const SelectTrigger = React.forwardRef(({ className, children, size = "default", ...props }, ref) => /* @__PURE__ */ jsxs(SelectPrimitive.Trigger, {
13
13
  ref,
14
- className: cn("flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", fieldPresentation, "data-[placeholder]:text-muted-foreground [&>span]:min-w-0 [&>span]:text-left", "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform", size !== "default" && controlSizes[size], className),
14
+ className: cn("flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm shadow-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", fieldPresentation, "data-[placeholder]:text-[var(--text-dim)] [&>span]:min-w-0 [&>span]:text-left", "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform", size !== "default" && controlSizes[size], className),
15
15
  ...props,
16
16
  children: [children, /* @__PURE__ */ jsx(SelectPrimitive.Icon, {
17
17
  asChild: true,
@@ -6,10 +6,10 @@ import { jsx } from "react/jsx-runtime";
6
6
  import * as SwitchPrimitives from "@radix-ui/react-switch";
7
7
  //#region src/primitives/switch.tsx
8
8
  const Switch = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(SwitchPrimitives.Root, {
9
- className: cn("peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 shadow-sm transition-[background-color,border-color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-transparent data-[state=checked]:bg-primary data-[state=unchecked]:border-muted-foreground data-[state=unchecked]:bg-transparent", focusRing, className),
9
+ className: cn("peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 shadow-sm transition-[background-color,border-color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-transparent data-[state=checked]:bg-[var(--accent-text)] data-[state=unchecked]:border-muted-foreground data-[state=unchecked]:bg-transparent", focusRing, className),
10
10
  ...props,
11
11
  ref,
12
- children: /* @__PURE__ */ jsx(SwitchPrimitives.Thumb, { className: cn("pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-[translate,background-color] data-[state=checked]:translate-x-4 data-[state=checked]:bg-background data-[state=unchecked]:translate-x-0 data-[state=unchecked]:bg-muted-foreground") })
12
+ children: /* @__PURE__ */ jsx(SwitchPrimitives.Thumb, { className: cn("pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-[translate,background-color] data-[state=checked]:translate-x-4 data-[state=checked]:bg-card data-[state=unchecked]:translate-x-0 data-[state=unchecked]:bg-muted-foreground") })
13
13
  }));
14
14
  Switch.displayName = SwitchPrimitives.Root.displayName;
15
15
  //#endregion
@@ -97,7 +97,7 @@ const TerminalInput = React.forwardRef(({ className, onSubmit, variant: _variant
97
97
  value,
98
98
  onChange: (e) => setValue(e.target.value),
99
99
  onKeyDown: handleKeyDown,
100
- className: "flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground",
100
+ className: "flex-1 bg-transparent text-foreground outline-none placeholder:text-[var(--text-dim)]",
101
101
  ...props
102
102
  }),
103
103
  /* @__PURE__ */ jsx(TerminalCursor, {})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.22.1",
3
+ "version": "11.22.3",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -149,7 +149,7 @@
149
149
  "peerDependencies": {
150
150
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
151
151
  "@nanostores/react": ">=0.8.4 <3.0.0",
152
- "@tangle-network/brand": "^1.15.4",
152
+ "@tangle-network/brand": "^1.15.6",
153
153
  "@tiptap/core": ">=3.20.4 <4.0.0",
154
154
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
155
155
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -10,7 +10,7 @@ import { cva, type VariantProps } from "class-variance-authority";
10
10
 
11
11
  const inputVariants = cva(
12
12
  cn(
13
- "flex w-full rounded-lg border px-4 py-2 text-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium file:text-sm",
13
+ "flex w-full rounded-lg border px-4 py-2 text-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 file:border-0 file:bg-transparent file:font-medium file:text-sm",
14
14
  fieldPresentation,
15
15
  ),
16
16
  {
@@ -117,7 +117,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
117
117
  id={textareaId}
118
118
  className={cn(
119
119
  "flex min-h-[120px] w-full resize-y rounded-lg border px-4 py-3 text-sm",
120
- "placeholder:text-muted-foreground",
120
+ "placeholder:text-[var(--text-dim)]",
121
121
  "disabled:cursor-not-allowed disabled:opacity-50",
122
122
  fieldPresentation,
123
123
  error && focusFieldInvalid,
@@ -0,0 +1,56 @@
1
+ import { render, screen } from "@testing-library/react"
2
+ import { describe, expect, it } from "vitest"
3
+ import { Input, Textarea } from "./input"
4
+ import { Select, SelectTrigger, SelectValue } from "./select"
5
+
6
+ /**
7
+ * An empty field's example text must not read as an entered value.
8
+ *
9
+ * Placeholders used `text-muted-foreground`, which is `--text-muted`: the tier
10
+ * one step below body text (#4c4c4c against #2d2d2d in light). An empty PTIN
11
+ * field showing "P01234567" then looked like saved data. `--text-dim` is the
12
+ * faintest TEXT tier — still 4.5:1 on every plane including the field well,
13
+ * which tokens.test.ts asserts — so it is the quietest colour that is still
14
+ * legible, and three tier steps from the value.
15
+ */
16
+ const PLACEHOLDER_TIER = "--text-dim"
17
+
18
+ function placeholderColours(el: HTMLElement): string[] {
19
+ return el.className
20
+ .split(/\s+/)
21
+ .filter((cls) => /^(?:placeholder|data-\[placeholder\]):text-/.test(cls))
22
+ }
23
+
24
+ function expectQuietPlaceholder(el: HTMLElement, what: string) {
25
+ const colours = placeholderColours(el)
26
+ expect(colours, `${what} must declare a placeholder colour`).not.toEqual([])
27
+ for (const cls of colours) {
28
+ expect(cls, `${what} placeholder must use ${PLACEHOLDER_TIER}`).toContain(
29
+ `var(${PLACEHOLDER_TIER})`,
30
+ )
31
+ expect(cls, `${what} placeholder must not be faded with alpha`).not.toMatch(/\/\d{1,3}$/)
32
+ }
33
+ }
34
+
35
+ describe("placeholder text sits on the faintest text tier", () => {
36
+ it("Input", () => {
37
+ render(<Input aria-label="PTIN" placeholder="P01234567" />)
38
+ expectQuietPlaceholder(screen.getByLabelText("PTIN"), "Input")
39
+ })
40
+
41
+ it("Textarea", () => {
42
+ render(<Textarea aria-label="Notes" placeholder="Details" />)
43
+ expectQuietPlaceholder(screen.getByLabelText("Notes"), "Textarea")
44
+ })
45
+
46
+ it("SelectTrigger", () => {
47
+ render(
48
+ <Select>
49
+ <SelectTrigger aria-label="Filing status">
50
+ <SelectValue placeholder="Choose" />
51
+ </SelectTrigger>
52
+ </Select>,
53
+ )
54
+ expectQuietPlaceholder(screen.getByLabelText("Filing status"), "SelectTrigger")
55
+ })
56
+ })
@@ -22,9 +22,9 @@ const SelectTrigger = React.forwardRef<
22
22
  <SelectPrimitive.Trigger
23
23
  ref={ref}
24
24
  className={cn(
25
- "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
25
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm shadow-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
26
26
  fieldPresentation,
27
- "data-[placeholder]:text-muted-foreground [&>span]:min-w-0 [&>span]:text-left",
27
+ "data-[placeholder]:text-[var(--text-dim)] [&>span]:min-w-0 [&>span]:text-left",
28
28
  // The designed open state: accent border and surface while the menu is up.
29
29
  "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform",
30
30
  size !== "default" && controlSizes[size],
@@ -24,12 +24,16 @@ describe("Switch", () => {
24
24
  expect(stateClasses(thumb, "unchecked")).not.toContain("bg-background");
25
25
  });
26
26
 
27
- it("fills the track with primary once it is on", () => {
27
+ it("draws its on state as an accent-text track with a card thumb, so a checked switch clears 3:1 in every theme", () => {
28
28
  render(<Switch aria-label="Reminders" />);
29
29
  const track = screen.getByRole("switch", { name: "Reminders" });
30
30
  fireEvent.click(track);
31
31
  expect(track).toHaveAttribute("data-state", "checked");
32
- expect(stateClasses(track, "checked")).toContain("bg-primary");
33
- expect(stateClasses(track.firstElementChild as Element, "checked")).toContain("bg-background");
32
+ const thumb = track.firstElementChild as Element;
33
+ expect(stateClasses(track, "checked")).toContain("bg-[var(--accent-text)]");
34
+ expect(stateClasses(track, "checked")).toContain("border-transparent");
35
+ expect(stateClasses(track, "checked")).not.toContain("bg-primary");
36
+ expect(stateClasses(thumb, "checked")).toContain("bg-card");
37
+ expect(stateClasses(thumb, "checked")).not.toContain("bg-background");
34
38
  });
35
39
  });
@@ -16,7 +16,13 @@ const Switch = React.forwardRef<
16
16
  // switch's boundary is non-text content). The old bg-input track under a
17
17
  // bg-background thumb read as one flat shape, so an off switch was not
18
18
  // visible at all. --border-strong was tried and measured 2.2:1 on a white card.
19
- "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 shadow-sm transition-[background-color,border-color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-transparent data-[state=checked]:bg-primary data-[state=unchecked]:border-muted-foreground data-[state=unchecked]:bg-transparent",
19
+ // On is an --accent-text track carrying a card-coloured thumb. Accent text
20
+ // is held to text contrast on the card and the canvas, so the track's edge
21
+ // and the thumb on it (the same pair, inverted) both clear 3:1 in every
22
+ // theme. The bg-primary fill is tuned to carry white text and measured
23
+ // 2.4:1 on the dark card, with a bg-background thumb at 2.5:1 on it.
24
+ // brand's switch-contrast.test.ts asserts these pairs for every theme.
25
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 shadow-sm transition-[background-color,border-color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-transparent data-[state=checked]:bg-[var(--accent-text)] data-[state=unchecked]:border-muted-foreground data-[state=unchecked]:bg-transparent",
20
26
  focusRing,
21
27
  className,
22
28
  )}
@@ -25,7 +31,7 @@ const Switch = React.forwardRef<
25
31
  >
26
32
  <SwitchPrimitives.Thumb
27
33
  className={cn(
28
- "pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-[translate,background-color] data-[state=checked]:translate-x-4 data-[state=checked]:bg-background data-[state=unchecked]:translate-x-0 data-[state=unchecked]:bg-muted-foreground",
34
+ "pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-[translate,background-color] data-[state=checked]:translate-x-4 data-[state=checked]:bg-card data-[state=unchecked]:translate-x-0 data-[state=unchecked]:bg-muted-foreground",
29
35
  )}
30
36
  />
31
37
  </SwitchPrimitives.Root>
@@ -174,7 +174,7 @@ const TerminalInput = React.forwardRef<HTMLInputElement, TerminalInputProps>(
174
174
  value={value}
175
175
  onChange={(e) => setValue(e.target.value)}
176
176
  onKeyDown={handleKeyDown}
177
- className="flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground"
177
+ className="flex-1 bg-transparent text-foreground outline-none placeholder:text-[var(--text-dim)]"
178
178
  {...props}
179
179
  />
180
180
  <TerminalCursor />