@tangle-network/ui 11.22.0 → 11.22.2
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 +12 -0
- package/dist/primitives/input.js +2 -2
- package/dist/primitives/select.js +1 -1
- package/dist/primitives/switch.js +2 -2
- package/dist/primitives/terminal-display.js +1 -1
- package/package.json +2 -2
- package/src/primitives/input.tsx +2 -2
- package/src/primitives/placeholder-tone.test.tsx +56 -0
- package/src/primitives/select.tsx +2 -2
- package/src/primitives/switch.test.tsx +35 -0
- package/src/primitives/switch.tsx +7 -2
- package/src/primitives/terminal-display.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.22.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 11.22.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- eb11ddd: Switch: draw the off state as an outlined track with a filled thumb in `muted-foreground`, which clears 3:1 against the card and the canvas. The old filled `bg-input` track and `bg-background` thumb looked like one flat shape in both themes, so an unchecked switch could not be seen.
|
|
14
|
+
|
|
3
15
|
## 11.22.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/primitives/input.js
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
|
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),
|
|
10
10
|
...props,
|
|
11
11
|
ref,
|
|
12
|
-
children: /* @__PURE__ */ jsx(SwitchPrimitives.Thumb, { className: cn("pointer-events-none block h-4 w-4 rounded-full
|
|
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") })
|
|
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-
|
|
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.
|
|
3
|
+
"version": "11.22.2",
|
|
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.
|
|
152
|
+
"@tangle-network/brand": "^1.15.4",
|
|
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",
|
package/src/primitives/input.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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],
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { Switch } from "./switch";
|
|
4
|
+
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
|
|
7
|
+
/** The classes that apply in a state: unprefixed ones, plus that state's variant. */
|
|
8
|
+
function stateClasses(el: Element, state: "checked" | "unchecked"): string[] {
|
|
9
|
+
return (el.getAttribute("class") ?? "")
|
|
10
|
+
.split(/\s+/)
|
|
11
|
+
.filter((c) => !c.startsWith("data-[") || c.startsWith(`data-[state=${state}]:`))
|
|
12
|
+
.map((c) => c.replace(`data-[state=${state}]:`, ""));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("Switch", () => {
|
|
16
|
+
it("draws its off state in muted-foreground, so an unchecked switch is visible on a card or the canvas", () => {
|
|
17
|
+
render(<Switch aria-label="Reminders" />);
|
|
18
|
+
const track = screen.getByRole("switch", { name: "Reminders" });
|
|
19
|
+
const thumb = track.firstElementChild as Element;
|
|
20
|
+
expect(track).toHaveAttribute("data-state", "unchecked");
|
|
21
|
+
expect(stateClasses(track, "unchecked")).toContain("border-muted-foreground");
|
|
22
|
+
expect(stateClasses(track, "unchecked")).not.toContain("bg-input");
|
|
23
|
+
expect(stateClasses(thumb, "unchecked")).toContain("bg-muted-foreground");
|
|
24
|
+
expect(stateClasses(thumb, "unchecked")).not.toContain("bg-background");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("fills the track with primary once it is on", () => {
|
|
28
|
+
render(<Switch aria-label="Reminders" />);
|
|
29
|
+
const track = screen.getByRole("switch", { name: "Reminders" });
|
|
30
|
+
fireEvent.click(track);
|
|
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");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -11,7 +11,12 @@ const Switch = React.forwardRef<
|
|
|
11
11
|
>(({ className, ...props }, ref) => (
|
|
12
12
|
<SwitchPrimitives.Root
|
|
13
13
|
className={cn(
|
|
14
|
-
|
|
14
|
+
// Off is an outlined track with a filled thumb, both in muted-foreground,
|
|
15
|
+
// which clears 3:1 against the card and the canvas in both themes (a
|
|
16
|
+
// switch's boundary is non-text content). The old bg-input track under a
|
|
17
|
+
// bg-background thumb read as one flat shape, so an off switch was not
|
|
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",
|
|
15
20
|
focusRing,
|
|
16
21
|
className,
|
|
17
22
|
)}
|
|
@@ -20,7 +25,7 @@ const Switch = React.forwardRef<
|
|
|
20
25
|
>
|
|
21
26
|
<SwitchPrimitives.Thumb
|
|
22
27
|
className={cn(
|
|
23
|
-
"pointer-events-none block h-4 w-4 rounded-full
|
|
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",
|
|
24
29
|
)}
|
|
25
30
|
/>
|
|
26
31
|
</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-
|
|
177
|
+
className="flex-1 bg-transparent text-foreground outline-none placeholder:text-[var(--text-dim)]"
|
|
178
178
|
{...props}
|
|
179
179
|
/>
|
|
180
180
|
<TerminalCursor />
|