@tangle-network/ui 11.14.0 → 11.15.1

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.
@@ -1,181 +1,119 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
1
+ import * as React from "react";
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it } from "vitest";
3
4
  import { Metric, MetricStrip } from "./metric-strip";
4
5
 
5
- describe("MetricStrip", () => {
6
- it("pairs each label with its value as a described term", () => {
7
- render(
8
- <MetricStrip>
9
- <Metric label="Balance" value="$248.55" />
10
- </MetricStrip>,
11
- );
12
- // The label is wrapped for truncation, so assert the term it belongs to.
13
- expect(screen.getByText("Balance").closest("dt")).not.toBeNull();
14
- expect(screen.getByText("$248.55").tagName).toBe("DD");
15
- });
6
+ afterEach(cleanup);
16
7
 
17
- // axe's `definition-list` rule allows only dt, dd, script, template and div
18
- // as direct children of a dl. The hint is a second dd for this reason: a <p>
19
- // between the terms is a violation, and it was one.
20
- it("puts only permitted elements directly inside the dl", () => {
8
+ describe("MetricStrip", () => {
9
+ it("pairs labels, values and qualifiers as native described terms", () => {
21
10
  const { container } = render(
22
11
  <MetricStrip>
23
- <Metric label="Spend" value="$12.00" hint="This month" />
12
+ <Metric label="Balance" value="$248.55" hint="Personal wallet" />
24
13
  <Metric label="Runs" value="41" />
25
14
  </MetricStrip>,
26
15
  );
27
- const dl = container.querySelector("dl") as HTMLElement;
16
+ expect(screen.getByText("Balance").closest("dt")).not.toBeNull();
17
+ expect(screen.getByText("$248.55").tagName).toBe("DD");
18
+ expect(screen.getByText("Personal wallet").tagName).toBe("DD");
19
+ const dl = container.querySelector("dl")!;
28
20
  const permitted = new Set(["DT", "DD", "SCRIPT", "TEMPLATE", "DIV"]);
29
- for (const child of Array.from(dl.children)) {
30
- expect(permitted).toContain(child.tagName);
31
- }
21
+ for (const child of Array.from(dl.children)) expect(permitted).toContain(child.tagName);
22
+ expect(dl.querySelectorAll("dt")).toHaveLength(2);
23
+ expect(dl.querySelectorAll("dd")).toHaveLength(3);
32
24
  });
33
25
 
34
- it("renders the hint as a dd rather than a paragraph", () => {
35
- render(
36
- <MetricStrip>
37
- <Metric label="Spend" value="$12.00" hint="Personal wallet" />
38
- </MetricStrip>,
39
- );
40
- const hint = screen.getByText("Personal wallet");
41
- expect(hint.tagName).toBe("DD");
42
- });
43
-
44
- it("omits the hint entirely when not given", () => {
26
+ it("omits absent qualifiers but preserves a numeric zero qualifier inside dd", () => {
45
27
  const { container } = render(
46
28
  <MetricStrip>
47
- <Metric label="Runs" value="41" />
29
+ <Metric label="Unknown" value="—" />
30
+ <Metric label="Known zero" value={0} hint={0} />
31
+ <Metric label="Unavailable" value={null} hint={false} />
32
+ <Metric label="Empty hint" value="" hint="" />
48
33
  </MetricStrip>,
49
34
  );
50
- expect(container.querySelectorAll("dd")).toHaveLength(1);
35
+ expect(container.querySelectorAll("dd")).toHaveLength(5);
36
+ expect(screen.getByText("Unknown").closest("div")!.querySelector("dd")!.textContent).toBe("—");
37
+ const zero = screen.getByText("Known zero").closest("div")!;
38
+ expect(Array.from(zero.querySelectorAll("dd")).map((node) => node.textContent)).toEqual(["0", "0"]);
39
+ expect(Array.from(zero.childNodes).every((node) => node.nodeType === Node.ELEMENT_NODE)).toBe(true);
40
+ expect(screen.getByText("Unavailable").closest("div")!.querySelector("dd")!.textContent).toBe("");
51
41
  });
52
42
 
53
- // `attention` means a person has to do something, so it shows a pill next to
54
- // the label. A zero on its own is not attention.
55
- it("shows a pill beside the label only when attention is set", () => {
56
- const { rerender, container } = render(
57
- <MetricStrip>
58
- <Metric label="Balance" value="$0.00" />
59
- </MetricStrip>,
60
- );
43
+ it("does not infer attention from zero; explicit danger retains its pill and value tone", () => {
44
+ const { rerender, container } = render(<MetricStrip><Metric label="Balance" value="$0.00" /></MetricStrip>);
61
45
  expect(container.querySelector("svg")).toBeNull();
62
-
63
- rerender(
64
- <MetricStrip>
65
- <Metric
66
- attention={{ tone: "danger", label: "Empty" }}
67
- label="Balance"
68
- value="$0.00"
69
- />
70
- </MetricStrip>,
71
- );
46
+ rerender(<MetricStrip><Metric label="Balance" value="$0.00" attention={{ tone: "danger", label: "Empty" }} /></MetricStrip>);
72
47
  expect(screen.getByText("Empty")).toBeInTheDocument();
73
48
  expect(container.querySelector("svg")).not.toBeNull();
74
- });
75
-
76
- it("raises the value's tone only for a danger attention", () => {
77
- const { container } = render(
78
- <MetricStrip>
79
- <Metric
80
- attention={{ tone: "danger", label: "Empty" }}
81
- label="Balance"
82
- value="$0.00"
83
- />
84
- </MetricStrip>,
85
- );
86
- const value = screen.getByText("$0.00");
87
- expect(value.className).toContain("--surface-danger-text");
49
+ expect(screen.getByText("$0.00").className).toContain("--surface-danger-text");
88
50
  expect(container.querySelectorAll("dd")).toHaveLength(1);
51
+ rerender(<MetricStrip><Metric label="Balance" value="$0.00" attention={{ tone: "warning", label: "Review" }} /></MetricStrip>);
52
+ expect(screen.getByText("$0.00").className).not.toContain("--surface-danger-text");
89
53
  });
90
54
 
91
- // The divider is drawn only where it belongs, never drawn and then
92
- // suppressed: `border-l` alongside `border-l-0` variants puts both in one
93
- // conflict group whose winner is decided by emitted-rule order, and that
94
- // resolved the wrong way at every breakpoint — every item kept a left border,
95
- // including the first in each row.
96
- it("draws the divider without a suppressing counterpart", () => {
97
- const { container } = render(
98
- <MetricStrip>
99
- <Metric label="Balance" value="$0.00" />
100
- </MetricStrip>,
101
- );
102
- const item = (container.querySelector("dl") as HTMLElement)
103
- .children[0] as HTMLElement;
104
- expect(item.className).not.toContain("border-l-0");
55
+ it("retains the default two / sm:four grid", () => {
56
+ const { container } = render(<MetricStrip><Metric label="Runs" value={0} /></MetricStrip>);
57
+ const dl = container.querySelector("dl")!;
58
+ expect(dl.className.split(" ")).toEqual(expect.arrayContaining(["grid-cols-2", "sm:grid-cols-4"]));
59
+ expect(dl.className).not.toContain("lg:grid-cols");
105
60
  });
106
61
 
107
- // The row-start test has to name the same column count the grid does, at each
108
- // breakpoint. Nothing in the type system ties them together, so changing the
109
- // grid to three columns without moving the divider puts a rule mid-row. Read
110
- // both out of the rendered classes and require them to agree, so the coupling
111
- // is enforced rather than only described.
112
- it("keeps the divider's row-start test matching the grid's column count", () => {
62
+ it.each([3, 4, 5, 6] as const)("owns the grid and all nonoverlapping row-start guards for %i columns", (columns) => {
113
63
  const { container } = render(
114
- <MetricStrip>
115
- <Metric label="Balance" value="$0.00" />
64
+ <MetricStrip columns={columns}>
65
+ {Array.from({ length: columns }, (_, i) => <Metric key={i} label={`Metric ${i}`} value={i} />)}
116
66
  </MetricStrip>,
117
67
  );
118
- const dl = container.querySelector("dl") as HTMLElement;
119
- const item = dl.children[0] as HTMLElement;
120
-
121
- // `grid-cols-N` unprefixed is the base breakpoint; `<bp>:grid-cols-N` is that
122
- // breakpoint. The divider spells the same breakpoints as `max-<bp>:` / `<bp>:`.
123
- const columnsAt = (prefix: string) => {
124
- const re = prefix
125
- ? new RegExp(`(?:^| )${prefix}:grid-cols-(\\d+)(?: |$)`)
126
- : /(?:^| )grid-cols-(\d+)(?: |$)/;
127
- const m = dl.className.match(re);
128
- return m ? Number(m[1]) : null;
129
- };
130
- const modulusAt = (prefix: string) => {
131
- const m = item.className.match(
132
- new RegExp(`(?:^| )${prefix}:\\[&:not\\(:nth-child\\((\\d+)n\\+1\\)\\)\\]:border-l(?: |$)`),
133
- );
134
- return m ? Number(m[1]) : null;
135
- };
136
-
137
- // base grid (below sm) is guarded by the `max-sm:` divider rule
138
- expect(modulusAt("max-sm")).toBe(columnsAt(""));
139
- // the `sm:` grid is guarded by the `sm:` divider rule
140
- expect(modulusAt("sm")).toBe(columnsAt("sm"));
141
-
142
- // Every grid-cols breakpoint the strip declares must have a divider rule.
143
- const declared = [...dl.className.matchAll(/(?:^| )(?:([a-z]+):)?grid-cols-\d+(?= |$)/g)]
144
- .map((m) => m[1] ?? "");
145
- for (const bp of declared) {
146
- const guard = bp === "" ? "max-sm" : bp;
147
- expect(
148
- modulusAt(guard),
149
- `grid-cols at "${bp || "base"}" has no matching ${guard}: divider rule`,
150
- ).not.toBeNull();
68
+ const dl = container.querySelector("dl")!;
69
+ expect(dl).not.toHaveAttribute("columns");
70
+ expect(dl.className).toContain("max-sm:[&>div:not(:nth-child(2n+1))]:border-l");
71
+ if (columns <= 4) {
72
+ expect(dl.className).toContain(`sm:grid-cols-${columns}`);
73
+ expect(dl.className).toContain(`sm:[&>div:not(:nth-child(${columns}n+1))]:border-l`);
74
+ } else {
75
+ expect(dl.className).toContain("sm:grid-cols-3");
76
+ expect(dl.className).toContain("sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l");
77
+ expect(dl.className).toContain(`lg:grid-cols-${columns}`);
78
+ expect(dl.className).toContain(`lg:[&>div:not(:nth-child(${columns}n+1))]:border-l`);
151
79
  }
80
+ expect(dl.className).not.toContain("border-l-0");
81
+ for (const item of Array.from(dl.children)) expect(item.className).not.toContain("nth-child");
152
82
  });
153
83
 
154
- it("titles the hint too, so a truncated qualifier stays recoverable", () => {
155
- render(
156
- <MetricStrip>
157
- <Metric
158
- hint="Personal wallet (0x1234abcd5678efgh)"
159
- label="Balance"
160
- value="$248.55"
161
- />
162
- </MetricStrip>,
163
- );
164
- expect(screen.getByText("Personal wallet (0x1234abcd5678efgh)")).toHaveAttribute(
165
- "title",
166
- "Personal wallet (0x1234abcd5678efgh)",
167
- );
84
+ it("does not give a standalone Metric a strip divider", () => {
85
+ const { container } = render(<Metric label="Runs" value={0} />);
86
+ expect(container.firstElementChild!.className).not.toContain("border-l");
168
87
  });
169
88
 
170
- it("titles a string value so a truncated figure stays readable", () => {
171
- render(
172
- <MetricStrip>
173
- <Metric label="Balance" value="$1,284,003.10" />
89
+ it("wraps full labels, values and qualifiers without relying on a mouse title", () => {
90
+ render(<MetricStrip><Metric label="Very long account label without loss" value="$1,284,003.10" hint="Personal wallet (0x1234abcd5678efgh)" /></MetricStrip>);
91
+ for (const text of ["Very long account label without loss", "$1,284,003.10", "Personal wallet (0x1234abcd5678efgh)"]) {
92
+ const node = screen.getByText(text);
93
+ expect(node.className).toContain("[overflow-wrap:anywhere]");
94
+ expect(node.className).not.toContain("truncate");
95
+ }
96
+ expect(screen.getByText("$1,284,003.10")).toHaveAttribute("title", "$1,284,003.10");
97
+ expect(screen.getByText("Personal wallet (0x1234abcd5678efgh)")).toHaveAttribute("title", "Personal wallet (0x1234abcd5678efgh)");
98
+ });
99
+
100
+ it("preserves refs, attributes, caller classes and fragment/conditional DOM order", () => {
101
+ const strip = React.createRef<HTMLDListElement>();
102
+ const item = React.createRef<HTMLDivElement>();
103
+ const { container } = render(
104
+ <MetricStrip ref={strip} aria-label="Usage" className="mb-6 sm:grid-cols-3">
105
+ <><Metric ref={item} data-testid="first" className="max-sm:col-span-2 max-sm:border-b" label="Spend" value={0} /></>
106
+ {false && <Metric label="Hidden" value={0} />}
107
+ <Metric className="max-sm:border-l-0!" label="Calls" value={0} />
108
+ <Metric className="max-sm:border-l!" label="Average" value="—" />
174
109
  </MetricStrip>,
175
110
  );
176
- expect(screen.getByText("$1,284,003.10")).toHaveAttribute(
177
- "title",
178
- "$1,284,003.10",
179
- );
111
+ expect(strip.current).toBe(container.querySelector("dl"));
112
+ expect(strip.current).toHaveAttribute("aria-label", "Usage");
113
+ expect(item.current).toBe(screen.getByTestId("first"));
114
+ expect(item.current!.className).toContain("max-sm:col-span-2");
115
+ expect(strip.current!.className).toContain("sm:grid-cols-3");
116
+ expect(strip.current!.className).not.toContain("sm:grid-cols-4");
117
+ expect(Array.from(strip.current!.querySelectorAll("dt")).map((node) => node.textContent)).toEqual(["Spend", "Calls", "Average"]);
180
118
  });
181
119
  });
@@ -2,32 +2,32 @@ import * as React from "react";
2
2
  import { cn } from "../lib/utils";
3
3
  import { StatusPill, type StatusTone } from "./status-pill";
4
4
 
5
- /**
6
- * The headline figures for a console page, as ONE statement rather than four.
7
- *
8
- * A row of separate stat cards is the default and it is usually wrong here.
9
- * Each card is a box that must be tall enough for its longest member, so a row
10
- * mixing "$248.55 / Personal wallet" against a bare "0" leaves the short ones
11
- * mostly empty; and four bordered boxes read as four unrelated facts when they
12
- * are four readings of one account. `MetricStrip` puts them on one plane
13
- * divided by hairlines: same information, one object, no holes.
14
- *
15
- * The divider is a border on the item rather than a `divide-*` utility on the
16
- * parent, because these wrap. `divide-x` draws from DOM order and leaves a
17
- * stray rule at the start of every wrapped line; a per-item leading border
18
- * suppressed at each row start does not.
19
- */
5
+ // Each finite preset owns its grid AND its row-start rules. Keep the strings
6
+ // literal so host Tailwind consumers scanning the published source emit them.
7
+ // Ranges do not overlap: no drawn border is subsequently suppressed.
8
+ const layouts = {
9
+ 3: "grid-cols-2 sm:grid-cols-3 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(3n+1))]:border-l",
10
+ 4: "grid-cols-2 sm:grid-cols-4 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:[&>div:not(:nth-child(4n+1))]:border-l",
11
+ 5: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(5n+1))]:border-l",
12
+ 6: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 max-sm:[&>div:not(:nth-child(2n+1))]:border-l sm:max-lg:[&>div:not(:nth-child(3n+1))]:border-l lg:[&>div:not(:nth-child(6n+1))]:border-l",
13
+ } as const;
14
+
15
+ /** Headline readings on one plane, with native definition-list semantics. */
20
16
  export interface MetricStripProps
21
17
  extends React.HTMLAttributes<HTMLDListElement> {
22
18
  children: React.ReactNode;
19
+ /** Desktop columns. Default retains the existing two / sm:four layout.
20
+ * Five and six use three columns between sm and lg. Never inferred from data. */
21
+ columns?: 3 | 4 | 5 | 6;
23
22
  }
24
23
 
25
24
  const MetricStrip = React.forwardRef<HTMLDListElement, MetricStripProps>(
26
- ({ className, children, ...props }, ref) => (
25
+ ({ className, children, columns = 4, ...props }, ref) => (
27
26
  <dl
28
27
  ref={ref}
29
28
  className={cn(
30
- "grid grid-cols-2 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)] sm:grid-cols-4",
29
+ "grid min-w-0 rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]",
30
+ layouts[columns],
31
31
  className,
32
32
  )}
33
33
  {...props}
@@ -43,72 +43,38 @@ export interface MetricProps extends React.HTMLAttributes<HTMLDivElement> {
43
43
  value: React.ReactNode;
44
44
  /** The qualifying line: which wallet, which window, what the limit is. */
45
45
  hint?: React.ReactNode;
46
- /** Raises the value's tone and shows a pill beside the label. A metric is
47
- * `attention` only when a person has to DO something — not merely when a
48
- * number is zero. */
46
+ /** Attention is a caller-owned action requirement, never inferred from zero. */
49
47
  attention?: { tone: StatusTone; label: string };
50
48
  }
51
49
 
52
- /**
53
- * One reading inside a `MetricStrip`.
54
- *
55
- * `<dt>` carries the label and `<dd>` the value, so the pair is announced as a
56
- * described term rather than as two loose strings — the semantic that makes a
57
- * figure legible without the visual grouping.
58
- *
59
- * The value is `tabular-nums`: these sit in a row and change on a timer, and
60
- * proportional digits make the column jitter every time a 1 becomes an 8.
61
- */
50
+ /** A described term; the strip, not this item, decides where separators belong. */
62
51
  const Metric = React.forwardRef<HTMLDivElement, MetricProps>(
63
52
  ({ className, label, value, hint, attention, ...props }, ref) => (
64
53
  <div
65
54
  ref={ref}
66
- className={cn(
67
- // Leading hairline, DRAWN only where a divider belongs — never drawn
68
- // and then suppressed. Stating it as `border-l` plus `border-l-0`
69
- // variants puts both in one conflict group, where the winner is decided
70
- // by emitted-rule order rather than by intent; that resolved the wrong
71
- // way at every breakpoint and left a rule down the start of each row.
72
- // `:not(:nth-child(Nn+1))` is the row-start test for the column count
73
- // at that breakpoint, so no rule ever contradicts another.
74
- //
75
- // N is COUPLED to the grid template on `MetricStrip`: 2 columns below
76
- // `sm`, 4 from `sm`. A new breakpoint there — `md:grid-cols-3`, say —
77
- // needs its `md:[&:not(:nth-child(3n+1))]:border-l` here in the same
78
- // change, or the dividers land mid-row at that width.
79
- "border-border p-4 sm:p-5",
80
- "max-sm:[&:not(:nth-child(2n+1))]:border-l",
81
- "sm:[&:not(:nth-child(4n+1))]:border-l",
82
- className,
83
- )}
55
+ className={cn("min-w-0 border-border p-4 sm:p-5", className)}
84
56
  {...props}
85
57
  >
86
- <dt className="flex items-center gap-2 text-muted-foreground text-sm">
87
- <span className="truncate">{label}</span>
58
+ <dt className="flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm">
59
+ <span className="min-w-0 whitespace-normal [overflow-wrap:anywhere]">{label}</span>
88
60
  {attention && (
89
61
  <StatusPill tone={attention.tone}>{attention.label}</StatusPill>
90
62
  )}
91
63
  </dt>
92
- {/* `truncate`, and a step down until the column is wide enough for the
93
- longest figure this strip carries. A balance renders as "$248.55" on a
94
- funded account and as "$1,284,003.10" on a busy one, and at four
95
- columns the wide case has nowhere to go — it either overflows its cell
96
- or pushes the whole strip past the page. */}
64
+ {/* Values and qualifiers must be readable on touch, not only in a hover
65
+ title. Wrap even unbroken values without making the grid wider. */}
97
66
  <dd
98
67
  className={cn(
99
- "mt-1 truncate font-semibold text-xl tabular-nums tracking-tight lg:text-2xl",
68
+ "mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl",
100
69
  attention?.tone === "danger" && "text-[var(--surface-danger-text)]",
101
70
  )}
102
71
  title={typeof value === "string" ? value : undefined}
103
72
  >
104
73
  {value}
105
74
  </dd>
106
- {hint && (
107
- // Titled for the same reason the value is: the line truncates, and a
108
- // hint is where the qualifying detail lives — which wallet, which
109
- // window — so a clipped one is the case most worth recovering.
75
+ {hint !== undefined && hint !== null && hint !== false && hint !== "" && (
110
76
  <dd
111
- className="mt-0.5 truncate text-[var(--text-dim)] text-xs"
77
+ className="mt-0.5 whitespace-normal [overflow-wrap:anywhere] text-[var(--text-dim)] text-xs"
112
78
  title={typeof hint === "string" ? hint : undefined}
113
79
  >
114
80
  {hint}
@@ -3,7 +3,7 @@
3
3
  import * as SelectPrimitive from "@radix-ui/react-select";
4
4
  import { Check, ChevronDown, ChevronUp } from "lucide-react";
5
5
  import * as React from "react";
6
- import { focusField } from "../lib/focus";
6
+ import { controlSizes, fieldPresentation } from "../lib/control-presentation";
7
7
  import { cn } from "../lib/utils";
8
8
 
9
9
  const Select = SelectPrimitive.Root;
@@ -14,20 +14,27 @@ const SelectValue = SelectPrimitive.Value;
14
14
 
15
15
  const SelectTrigger = React.forwardRef<
16
16
  React.ElementRef<typeof SelectPrimitive.Trigger>,
17
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18
- >(({ className, children, ...props }, ref) => (
17
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
18
+ /** Default remains 36px; compact follows density, touch is at least 44px. */
19
+ size?: "default" | "compact" | "touch";
20
+ }
21
+ >(({ className, children, size = "default", ...props }, ref) => (
19
22
  <SelectPrimitive.Trigger
20
23
  ref={ref}
21
24
  className={cn(
22
- "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-card px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
23
- focusField,
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",
26
+ fieldPresentation,
27
+ "data-[placeholder]:text-muted-foreground [&>span]:min-w-0 [&>span]:text-left",
28
+ // The designed open state: accent border and surface while the menu is up.
29
+ "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform",
30
+ size !== "default" && controlSizes[size],
24
31
  className,
25
32
  )}
26
33
  {...props}
27
34
  >
28
35
  {children}
29
36
  <SelectPrimitive.Icon asChild>
30
- <ChevronDown className="h-4 w-4 opacity-50" />
37
+ <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
31
38
  </SelectPrimitive.Icon>
32
39
  </SelectPrimitive.Trigger>
33
40
  ));
@@ -76,7 +83,7 @@ const SelectContent = React.forwardRef<
76
83
  <SelectPrimitive.Content
77
84
  ref={ref}
78
85
  className={cn(
79
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-card text-foreground shadow-[var(--shadow-card)] data-[state=closed]:animate-out data-[state=open]:animate-in",
86
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-card text-foreground shadow-[var(--shadow-dropdown)] data-[state=closed]:animate-out data-[state=open]:animate-in",
80
87
  position === "popper" &&
81
88
  "data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
82
89
  className,
@@ -119,14 +126,14 @@ const SelectItem = React.forwardRef<
119
126
  <SelectPrimitive.Item
120
127
  ref={ref}
121
128
  className={cn(
122
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none focus:bg-muted/50 focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
129
+ "relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pr-8 pl-2 text-sm outline-none transition-colors focus:bg-[var(--accent-surface-strong)] focus:text-foreground data-[state=checked]:bg-[var(--accent-surface-strong)] data-[state=checked]:font-medium data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
123
130
  className,
124
131
  )}
125
132
  {...props}
126
133
  >
127
134
  <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
128
135
  <SelectPrimitive.ItemIndicator>
129
- <Check className="h-4 w-4" />
136
+ <Check className="h-4 w-4 text-[var(--accent-text)]" />
130
137
  </SelectPrimitive.ItemIndicator>
131
138
  </span>
132
139
  <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>