@kinetixui/ui 0.4.1 → 0.4.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/dist/index.d.ts +25 -0
- package/dist/index.js +82 -5
- package/package.json +2 -2
- package/src/components/chart.tsx +146 -3
- package/src/components/number-input.tsx +5 -2
- package/tailwind.config.ts +3 -0
package/dist/index.d.ts
CHANGED
|
@@ -474,6 +474,28 @@ type ChartConfig = {
|
|
|
474
474
|
declare const ChartContainer: React$1.ForwardRefExoticComponent<Omit<React$1.ClassAttributes<HTMLDivElement> & React$1.HTMLAttributes<HTMLDivElement> & {
|
|
475
475
|
config: ChartConfig;
|
|
476
476
|
children: React$1.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
|
|
477
|
+
/**
|
|
478
|
+
* Text alternative for the chart (WCAG 1.1.1). A chart conveys its data by
|
|
479
|
+
* colour + position, which a screen reader can't relay — pass a one-line
|
|
480
|
+
* summary of what it shows and the trend. Falls back to "Chart".
|
|
481
|
+
* Cartesian charts should also get `accessibilityLayer` on the Recharts
|
|
482
|
+
* primitive for keyboard data-point navigation.
|
|
483
|
+
*/
|
|
484
|
+
label?: string;
|
|
485
|
+
/**
|
|
486
|
+
* Render a placeholder in place of the chart. `"loading"` shows a shimmer,
|
|
487
|
+
* `"empty"` / `"error"` show a message (override with `stateMessage`).
|
|
488
|
+
*/
|
|
489
|
+
state?: "loading" | "empty" | "error";
|
|
490
|
+
stateMessage?: string;
|
|
491
|
+
/**
|
|
492
|
+
* Visually-hidden `<table>` rendered after the chart so a screen reader can
|
|
493
|
+
* read the underlying numbers. Pair with a concise `label`.
|
|
494
|
+
*/
|
|
495
|
+
srTable?: {
|
|
496
|
+
columns: React$1.ReactNode[];
|
|
497
|
+
rows: React$1.ReactNode[][];
|
|
498
|
+
};
|
|
477
499
|
}, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
|
|
478
500
|
declare const ChartStyle: ({ id, config }: {
|
|
479
501
|
id: string;
|
|
@@ -1120,6 +1142,9 @@ declare const Metric: React$1.ForwardRefExoticComponent<MetricProps & React$1.Re
|
|
|
1120
1142
|
|
|
1121
1143
|
/**
|
|
1122
1144
|
* NumberInput — a numeric field with increment / decrement controls.
|
|
1145
|
+
* The wrapper shows `--shadow-focus` on `focus-within`; each stepper also
|
|
1146
|
+
* draws its own `--ring` inset outline on `:focus-visible` so keyboard users
|
|
1147
|
+
* can tell which control is focused.
|
|
1123
1148
|
*/
|
|
1124
1149
|
interface NumberInputProps extends Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "defaultValue" | "onChange"> {
|
|
1125
1150
|
value?: number;
|
package/dist/index.js
CHANGED
|
@@ -1545,14 +1545,50 @@ function useChart() {
|
|
|
1545
1545
|
if (!context) throw new Error("useChart must be used within a <ChartContainer />");
|
|
1546
1546
|
return context;
|
|
1547
1547
|
}
|
|
1548
|
-
var ChartContainer = React32.forwardRef(({
|
|
1548
|
+
var ChartContainer = React32.forwardRef(({
|
|
1549
|
+
id,
|
|
1550
|
+
className,
|
|
1551
|
+
children,
|
|
1552
|
+
config,
|
|
1553
|
+
label,
|
|
1554
|
+
state,
|
|
1555
|
+
stateMessage,
|
|
1556
|
+
srTable,
|
|
1557
|
+
role,
|
|
1558
|
+
"aria-label": ariaLabel,
|
|
1559
|
+
...props
|
|
1560
|
+
}, ref) => {
|
|
1549
1561
|
const uniqueId = React32.useId();
|
|
1550
1562
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
|
1563
|
+
const name = ariaLabel ?? label ?? "Chart";
|
|
1564
|
+
const innerRef = React32.useRef(null);
|
|
1565
|
+
const setRef = React32.useCallback(
|
|
1566
|
+
(node) => {
|
|
1567
|
+
innerRef.current = node;
|
|
1568
|
+
if (typeof ref === "function") ref(node);
|
|
1569
|
+
else if (ref) ref.current = node;
|
|
1570
|
+
},
|
|
1571
|
+
[ref]
|
|
1572
|
+
);
|
|
1573
|
+
React32.useEffect(() => {
|
|
1574
|
+
const el = innerRef.current;
|
|
1575
|
+
if (!el) return;
|
|
1576
|
+
const scrub = () => {
|
|
1577
|
+
el.querySelector(".recharts-surface")?.setAttribute("aria-label", name);
|
|
1578
|
+
el.querySelectorAll('[role="img"]:not([data-chart])').forEach((n) => n.removeAttribute("role"));
|
|
1579
|
+
};
|
|
1580
|
+
scrub();
|
|
1581
|
+
const mo = new MutationObserver(scrub);
|
|
1582
|
+
mo.observe(el, { childList: true, subtree: true });
|
|
1583
|
+
return () => mo.disconnect();
|
|
1584
|
+
}, [name]);
|
|
1551
1585
|
return /* @__PURE__ */ jsx37(ChartContext.Provider, { value: { config }, children: /* @__PURE__ */ jsxs15(
|
|
1552
1586
|
"div",
|
|
1553
1587
|
{
|
|
1554
1588
|
"data-chart": chartId,
|
|
1555
|
-
ref,
|
|
1589
|
+
ref: setRef,
|
|
1590
|
+
role: role ?? "img",
|
|
1591
|
+
"aria-label": name,
|
|
1556
1592
|
className: cn(
|
|
1557
1593
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
1558
1594
|
className
|
|
@@ -1560,12 +1596,53 @@ var ChartContainer = React32.forwardRef(({ id, className, children, config, ...p
|
|
|
1560
1596
|
...props,
|
|
1561
1597
|
children: [
|
|
1562
1598
|
/* @__PURE__ */ jsx37(ChartStyle, { id: chartId, config }),
|
|
1563
|
-
/* @__PURE__ */ jsx37(RechartsPrimitive.ResponsiveContainer, { children })
|
|
1599
|
+
state ? /* @__PURE__ */ jsx37(ChartState, { state, message: stateMessage }) : /* @__PURE__ */ jsx37(RechartsPrimitive.ResponsiveContainer, { children }),
|
|
1600
|
+
srTable && !state ? /* @__PURE__ */ jsx37(ChartSrTable, { ...srTable }) : null
|
|
1564
1601
|
]
|
|
1565
1602
|
}
|
|
1566
1603
|
) });
|
|
1567
1604
|
});
|
|
1568
1605
|
ChartContainer.displayName = "Chart";
|
|
1606
|
+
var STATE_COPY = {
|
|
1607
|
+
loading: "Loading chart\u2026",
|
|
1608
|
+
empty: "No data to show",
|
|
1609
|
+
error: "Couldn't load this chart"
|
|
1610
|
+
};
|
|
1611
|
+
function ChartState({
|
|
1612
|
+
state,
|
|
1613
|
+
message
|
|
1614
|
+
}) {
|
|
1615
|
+
return /* @__PURE__ */ jsxs15(
|
|
1616
|
+
"div",
|
|
1617
|
+
{
|
|
1618
|
+
role: state === "error" ? "alert" : "status",
|
|
1619
|
+
className: cn(
|
|
1620
|
+
"flex h-full w-full flex-col items-center justify-center gap-2 rounded-md border border-dashed p-6 text-center text-xs",
|
|
1621
|
+
state === "error" ? "border-destructive/40 text-destructive" : "border-border text-muted-foreground"
|
|
1622
|
+
),
|
|
1623
|
+
children: [
|
|
1624
|
+
state === "loading" ? /* @__PURE__ */ jsx37("div", { "aria-hidden": true, className: "flex w-full max-w-[220px] items-end justify-between gap-1.5", children: [40, 72, 56, 88, 48, 64, 80].map((h, i) => /* @__PURE__ */ jsx37(
|
|
1625
|
+
"span",
|
|
1626
|
+
{
|
|
1627
|
+
className: "w-full animate-pulse rounded-sm bg-muted",
|
|
1628
|
+
style: { height: `${h}px`, animationDelay: `${i * 90}ms` }
|
|
1629
|
+
},
|
|
1630
|
+
i
|
|
1631
|
+
)) }) : /* @__PURE__ */ jsx37("svg", { "aria-hidden": true, viewBox: "0 0 24 24", className: "size-5", fill: "none", stroke: "currentColor", strokeWidth: 1.5, children: state === "empty" ? /* @__PURE__ */ jsx37("path", { d: "M3 3v18h18M7 15l4-4 3 3 5-6", strokeLinecap: "round", strokeLinejoin: "round" }) : /* @__PURE__ */ jsx37("path", { d: "M12 9v4m0 4h.01M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.7 3.86a2 2 0 0 0-3.42 0Z", strokeLinecap: "round", strokeLinejoin: "round" }) }),
|
|
1632
|
+
/* @__PURE__ */ jsx37("span", { children: message ?? STATE_COPY[state] })
|
|
1633
|
+
]
|
|
1634
|
+
}
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
function ChartSrTable({
|
|
1638
|
+
columns,
|
|
1639
|
+
rows
|
|
1640
|
+
}) {
|
|
1641
|
+
return /* @__PURE__ */ jsxs15("table", { className: "sr-only", children: [
|
|
1642
|
+
/* @__PURE__ */ jsx37("thead", { children: /* @__PURE__ */ jsx37("tr", { children: columns.map((c, i) => /* @__PURE__ */ jsx37("th", { scope: "col", children: c }, i)) }) }),
|
|
1643
|
+
/* @__PURE__ */ jsx37("tbody", { children: rows.map((r, i) => /* @__PURE__ */ jsx37("tr", { children: r.map((cell, j) => /* @__PURE__ */ jsx37("td", { children: cell }, j)) }, i)) })
|
|
1644
|
+
] });
|
|
1645
|
+
}
|
|
1569
1646
|
var ChartStyle = ({ id, config }) => {
|
|
1570
1647
|
const colorConfig = Object.entries(config).filter(([, c]) => c.theme || c.color);
|
|
1571
1648
|
if (!colorConfig.length) return null;
|
|
@@ -3530,7 +3607,7 @@ var NumberInput = React60.forwardRef(
|
|
|
3530
3607
|
"aria-label": "Decrease",
|
|
3531
3608
|
disabled: disabled || min != null && current <= min,
|
|
3532
3609
|
onClick: () => set(current - step),
|
|
3533
|
-
className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
|
|
3610
|
+
className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
3534
3611
|
children: /* @__PURE__ */ jsx66(Minus3, { className: "size-4" })
|
|
3535
3612
|
}
|
|
3536
3613
|
),
|
|
@@ -3560,7 +3637,7 @@ var NumberInput = React60.forwardRef(
|
|
|
3560
3637
|
"aria-label": "Increase",
|
|
3561
3638
|
disabled: disabled || max != null && current >= max,
|
|
3562
3639
|
onClick: () => set(current + step),
|
|
3563
|
-
className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
|
|
3640
|
+
className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
3564
3641
|
children: /* @__PURE__ */ jsx66(Plus, { className: "size-4" })
|
|
3565
3642
|
}
|
|
3566
3643
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinetixui/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
|
|
6
6
|
"keywords": [
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"react-dom": ">=18"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
|
-
"@kinetixui/tokens": "0.4.
|
|
92
|
+
"@kinetixui/tokens": "0.4.3",
|
|
93
93
|
"@testing-library/jest-dom": "^6.6.3",
|
|
94
94
|
"@testing-library/react": "^16.1.0",
|
|
95
95
|
"@testing-library/user-event": "^14.5.2",
|
package/src/components/chart.tsx
CHANGED
|
@@ -27,16 +27,78 @@ const ChartContainer = React.forwardRef<
|
|
|
27
27
|
React.ComponentProps<"div"> & {
|
|
28
28
|
config: ChartConfig;
|
|
29
29
|
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
|
|
30
|
+
/**
|
|
31
|
+
* Text alternative for the chart (WCAG 1.1.1). A chart conveys its data by
|
|
32
|
+
* colour + position, which a screen reader can't relay — pass a one-line
|
|
33
|
+
* summary of what it shows and the trend. Falls back to "Chart".
|
|
34
|
+
* Cartesian charts should also get `accessibilityLayer` on the Recharts
|
|
35
|
+
* primitive for keyboard data-point navigation.
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Render a placeholder in place of the chart. `"loading"` shows a shimmer,
|
|
40
|
+
* `"empty"` / `"error"` show a message (override with `stateMessage`).
|
|
41
|
+
*/
|
|
42
|
+
state?: "loading" | "empty" | "error";
|
|
43
|
+
stateMessage?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Visually-hidden `<table>` rendered after the chart so a screen reader can
|
|
46
|
+
* read the underlying numbers. Pair with a concise `label`.
|
|
47
|
+
*/
|
|
48
|
+
srTable?: { columns: React.ReactNode[]; rows: React.ReactNode[][] };
|
|
30
49
|
}
|
|
31
|
-
>(({
|
|
50
|
+
>(({
|
|
51
|
+
id,
|
|
52
|
+
className,
|
|
53
|
+
children,
|
|
54
|
+
config,
|
|
55
|
+
label,
|
|
56
|
+
state,
|
|
57
|
+
stateMessage,
|
|
58
|
+
srTable,
|
|
59
|
+
role,
|
|
60
|
+
"aria-label": ariaLabel,
|
|
61
|
+
...props
|
|
62
|
+
}, ref) => {
|
|
32
63
|
const uniqueId = React.useId();
|
|
33
64
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
|
65
|
+
const name = ariaLabel ?? label ?? "Chart";
|
|
66
|
+
|
|
67
|
+
const innerRef = React.useRef<HTMLDivElement | null>(null);
|
|
68
|
+
const setRef = React.useCallback(
|
|
69
|
+
(node: HTMLDivElement | null) => {
|
|
70
|
+
innerRef.current = node;
|
|
71
|
+
if (typeof ref === "function") ref(node);
|
|
72
|
+
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
73
|
+
},
|
|
74
|
+
[ref],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// The container carries the text alternative (role="img" + aria-label).
|
|
78
|
+
// Recharts additionally (a) tags individual sector / dot <path>s with
|
|
79
|
+
// role="img" and no <title> — redundant noise that fails axe's svg-img-alt —
|
|
80
|
+
// and (b) leaves its accessibilityLayer <svg role="application"> unnamed.
|
|
81
|
+
// Fix both on the rendered output; re-run on Recharts re-layout.
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
const el = innerRef.current;
|
|
84
|
+
if (!el) return;
|
|
85
|
+
const scrub = () => {
|
|
86
|
+
el.querySelector(".recharts-surface")?.setAttribute("aria-label", name);
|
|
87
|
+
el.querySelectorAll('[role="img"]:not([data-chart])').forEach((n) => n.removeAttribute("role"));
|
|
88
|
+
};
|
|
89
|
+
scrub();
|
|
90
|
+
const mo = new MutationObserver(scrub);
|
|
91
|
+
mo.observe(el, { childList: true, subtree: true });
|
|
92
|
+
return () => mo.disconnect();
|
|
93
|
+
}, [name]);
|
|
34
94
|
|
|
35
95
|
return (
|
|
36
96
|
<ChartContext.Provider value={{ config }}>
|
|
37
97
|
<div
|
|
38
98
|
data-chart={chartId}
|
|
39
|
-
ref={
|
|
99
|
+
ref={setRef}
|
|
100
|
+
role={role ?? "img"}
|
|
101
|
+
aria-label={name}
|
|
40
102
|
className={cn(
|
|
41
103
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
42
104
|
className,
|
|
@@ -44,13 +106,94 @@ const ChartContainer = React.forwardRef<
|
|
|
44
106
|
{...props}
|
|
45
107
|
>
|
|
46
108
|
<ChartStyle id={chartId} config={config} />
|
|
47
|
-
|
|
109
|
+
{state ? (
|
|
110
|
+
<ChartState state={state} message={stateMessage} />
|
|
111
|
+
) : (
|
|
112
|
+
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
|
113
|
+
)}
|
|
114
|
+
{srTable && !state ? <ChartSrTable {...srTable} /> : null}
|
|
48
115
|
</div>
|
|
49
116
|
</ChartContext.Provider>
|
|
50
117
|
);
|
|
51
118
|
});
|
|
52
119
|
ChartContainer.displayName = "Chart";
|
|
53
120
|
|
|
121
|
+
const STATE_COPY = {
|
|
122
|
+
loading: "Loading chart…",
|
|
123
|
+
empty: "No data to show",
|
|
124
|
+
error: "Couldn't load this chart",
|
|
125
|
+
} as const;
|
|
126
|
+
|
|
127
|
+
function ChartState({
|
|
128
|
+
state,
|
|
129
|
+
message,
|
|
130
|
+
}: {
|
|
131
|
+
state: "loading" | "empty" | "error";
|
|
132
|
+
message?: string;
|
|
133
|
+
}) {
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
role={state === "error" ? "alert" : "status"}
|
|
137
|
+
className={cn(
|
|
138
|
+
"flex h-full w-full flex-col items-center justify-center gap-2 rounded-md border border-dashed p-6 text-center text-xs",
|
|
139
|
+
state === "error" ? "border-destructive/40 text-destructive" : "border-border text-muted-foreground",
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
{state === "loading" ? (
|
|
143
|
+
<div aria-hidden className="flex w-full max-w-[220px] items-end justify-between gap-1.5">
|
|
144
|
+
{[40, 72, 56, 88, 48, 64, 80].map((h, i) => (
|
|
145
|
+
<span
|
|
146
|
+
key={i}
|
|
147
|
+
className="w-full animate-pulse rounded-sm bg-muted"
|
|
148
|
+
style={{ height: `${h}px`, animationDelay: `${i * 90}ms` }}
|
|
149
|
+
/>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
) : (
|
|
153
|
+
<svg aria-hidden viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth={1.5}>
|
|
154
|
+
{state === "empty" ? (
|
|
155
|
+
<path d="M3 3v18h18M7 15l4-4 3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />
|
|
156
|
+
) : (
|
|
157
|
+
<path d="M12 9v4m0 4h.01M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.7 3.86a2 2 0 0 0-3.42 0Z" strokeLinecap="round" strokeLinejoin="round" />
|
|
158
|
+
)}
|
|
159
|
+
</svg>
|
|
160
|
+
)}
|
|
161
|
+
<span>{message ?? STATE_COPY[state]}</span>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ChartSrTable({
|
|
167
|
+
columns,
|
|
168
|
+
rows,
|
|
169
|
+
}: {
|
|
170
|
+
columns: React.ReactNode[];
|
|
171
|
+
rows: React.ReactNode[][];
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<table className="sr-only">
|
|
175
|
+
<thead>
|
|
176
|
+
<tr>
|
|
177
|
+
{columns.map((c, i) => (
|
|
178
|
+
<th key={i} scope="col">
|
|
179
|
+
{c}
|
|
180
|
+
</th>
|
|
181
|
+
))}
|
|
182
|
+
</tr>
|
|
183
|
+
</thead>
|
|
184
|
+
<tbody>
|
|
185
|
+
{rows.map((r, i) => (
|
|
186
|
+
<tr key={i}>
|
|
187
|
+
{r.map((cell, j) => (
|
|
188
|
+
<td key={j}>{cell}</td>
|
|
189
|
+
))}
|
|
190
|
+
</tr>
|
|
191
|
+
))}
|
|
192
|
+
</tbody>
|
|
193
|
+
</table>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
54
197
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
55
198
|
const colorConfig = Object.entries(config).filter(([, c]) => c.theme || c.color);
|
|
56
199
|
if (!colorConfig.length) return null;
|
|
@@ -6,6 +6,9 @@ import { cn } from "../lib/utils";
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* NumberInput — a numeric field with increment / decrement controls.
|
|
9
|
+
* The wrapper shows `--shadow-focus` on `focus-within`; each stepper also
|
|
10
|
+
* draws its own `--ring` inset outline on `:focus-visible` so keyboard users
|
|
11
|
+
* can tell which control is focused.
|
|
9
12
|
*/
|
|
10
13
|
export interface NumberInputProps
|
|
11
14
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "defaultValue" | "onChange"> {
|
|
@@ -48,7 +51,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
48
51
|
aria-label="Decrease"
|
|
49
52
|
disabled={disabled || (min != null && current <= min)}
|
|
50
53
|
onClick={() => set(current - step)}
|
|
51
|
-
className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
54
|
+
className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
52
55
|
>
|
|
53
56
|
<Minus className="size-4" />
|
|
54
57
|
</button>
|
|
@@ -73,7 +76,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
73
76
|
aria-label="Increase"
|
|
74
77
|
disabled={disabled || (max != null && current >= max)}
|
|
75
78
|
onClick={() => set(current + step)}
|
|
76
|
-
className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
79
|
+
className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
77
80
|
>
|
|
78
81
|
<Plus className="size-4" />
|
|
79
82
|
</button>
|