@kinetixui/ui 0.16.1 → 0.18.0
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 +4 -4
- package/dist/index.js +498 -248
- package/package.json +3 -2
- package/src/components/audio-player.tsx +2 -2
- package/src/components/badge.tsx +2 -2
- package/src/components/banner.tsx +2 -2
- package/src/components/button.tsx +18 -14
- package/src/components/calendar.tsx +1 -1
- package/src/components/chart.tsx +2 -2
- package/src/components/checkbox.tsx +2 -2
- package/src/components/circular-progress.tsx +1 -1
- package/src/components/code-block.tsx +1 -1
- package/src/components/color-picker.tsx +6 -4
- package/src/components/data-grid.tsx +192 -11
- package/src/components/diff-viewer.tsx +9 -8
- package/src/components/fab.tsx +1 -1
- package/src/components/file-upload.tsx +13 -15
- package/src/components/inform.tsx +1 -1
- package/src/components/input-group.tsx +1 -1
- package/src/components/input-otp.tsx +2 -1
- package/src/components/input.tsx +3 -3
- package/src/components/json-viewer.tsx +12 -10
- package/src/components/kbd.tsx +1 -1
- package/src/components/list.tsx +41 -27
- package/src/components/markdown-editor.tsx +3 -2
- package/src/components/message-bubble.tsx +3 -3
- package/src/components/modal.tsx +2 -2
- package/src/components/multi-select.tsx +28 -5
- package/src/components/native-select.tsx +3 -3
- package/src/components/notification-center.tsx +2 -2
- package/src/components/progress.tsx +1 -1
- package/src/components/radio-group.tsx +3 -3
- package/src/components/scroll-area.tsx +1 -1
- package/src/components/select.tsx +3 -3
- package/src/components/skeleton.tsx +1 -1
- package/src/components/slider.tsx +8 -3
- package/src/components/sonner.tsx +1 -1
- package/src/components/spinner.tsx +3 -3
- package/src/components/stepper.tsx +3 -3
- package/src/components/switch.tsx +1 -1
- package/src/components/tab-bar.tsx +1 -1
- package/src/components/table-of-contents.tsx +1 -1
- package/src/components/tag.tsx +1 -1
- package/src/components/textarea.tsx +3 -3
- package/src/components/timeline.tsx +1 -1
- package/src/components/tooltip.tsx +1 -1
- package/src/components/tour.tsx +45 -2
- package/src/components/virtual-list.tsx +2 -1
- package/src/components-a11y.test.tsx +69 -0
- package/src/components-behavior.test.tsx +1 -1
- package/src/components-keyboard.test.tsx +774 -0
- package/src/lib/utils.test.ts +12 -0
- package/src/lib/utils.ts +22 -1
- package/src/stories/Checkbox.stories.tsx +1 -1
- package/src/stories/CircularProgress.stories.tsx +4 -4
- package/src/stories/InputOTP.stories.tsx +2 -2
- package/src/stories/List.stories.tsx +2 -2
- package/src/stories/MarkdownEditor.stories.tsx +1 -1
- package/src/stories/MultiSelect.stories.tsx +1 -0
- package/src/stories/NativeSelect.stories.tsx +1 -1
- package/src/stories/NumberInput.stories.tsx +3 -3
- package/src/stories/Progress.stories.tsx +3 -3
- package/src/stories/Select.stories.tsx +2 -2
- package/src/stories/Slider.stories.tsx +3 -3
- package/src/stories/Switch.stories.tsx +1 -1
- package/src/stories/ToggleGroup.stories.tsx +6 -6
- package/tailwind.config.ts +20 -1
|
@@ -14,10 +14,10 @@ import { cn } from "../lib/utils";
|
|
|
14
14
|
const textareaVariants = cva(
|
|
15
15
|
[
|
|
16
16
|
"flex w-full min-h-[100px] resize-y rounded-sm border border-input bg-background px-3 py-3",
|
|
17
|
-
"font-sans text-
|
|
17
|
+
"font-sans text-body-md text-foreground",
|
|
18
18
|
"placeholder:text-muted-foreground",
|
|
19
19
|
"outline-none transition-colors",
|
|
20
|
-
"focus-visible:border-
|
|
20
|
+
"focus-visible:border-action focus-visible:shadow-focus",
|
|
21
21
|
"disabled:cursor-not-allowed disabled:opacity-disabled",
|
|
22
22
|
"aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:border-destructive aria-[invalid=true]:focus-visible:shadow-focus-destructive",
|
|
23
23
|
],
|
|
@@ -25,7 +25,7 @@ const textareaVariants = cva(
|
|
|
25
25
|
variants: {
|
|
26
26
|
state: {
|
|
27
27
|
Default: "",
|
|
28
|
-
Focus: "border-
|
|
28
|
+
Focus: "border-action shadow-focus",
|
|
29
29
|
Error: "border-destructive",
|
|
30
30
|
Disabled: "opacity-disabled pointer-events-none resize-none",
|
|
31
31
|
},
|
|
@@ -35,7 +35,7 @@ const Body = ({ item, align }: { item: TimelineItem; align: "left" | "right" })
|
|
|
35
35
|
|
|
36
36
|
const Rail = ({ item, isLast }: { item: TimelineItem; isLast: boolean }) => (
|
|
37
37
|
<div className="flex flex-col items-center">
|
|
38
|
-
<span className="flex size-2.5 shrink-0 items-center justify-center rounded-full bg-
|
|
38
|
+
<span className="flex size-2.5 shrink-0 items-center justify-center rounded-full bg-action">{item.icon}</span>
|
|
39
39
|
{!isLast && <span aria-hidden className="my-1 w-px flex-1 bg-border" />}
|
|
40
40
|
</div>
|
|
41
41
|
);
|
|
@@ -17,7 +17,7 @@ const TooltipContent = React.forwardRef<
|
|
|
17
17
|
ref={ref}
|
|
18
18
|
sideOffset={sideOffset}
|
|
19
19
|
className={cn(
|
|
20
|
-
"z-overlay overflow-hidden rounded-md bg-
|
|
20
|
+
"z-overlay overflow-hidden rounded-md bg-action px-3 py-1.5 text-xs text-action-foreground font-sans",
|
|
21
21
|
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
22
22
|
"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",
|
|
23
23
|
className,
|
package/src/components/tour.tsx
CHANGED
|
@@ -55,6 +55,43 @@ const Tour: React.FC<TourProps> = ({
|
|
|
55
55
|
|
|
56
56
|
React.useEffect(() => setMounted(true), []);
|
|
57
57
|
|
|
58
|
+
const titleId = React.useId();
|
|
59
|
+
const dialogRef = React.useRef<HTMLDivElement>(null);
|
|
60
|
+
const returnFocusRef = React.useRef<HTMLElement | null>(null);
|
|
61
|
+
|
|
62
|
+
// Remember what had focus when the tour opened and give it back when it closes.
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (!open) return;
|
|
65
|
+
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
66
|
+
return () => returnFocusRef.current?.focus();
|
|
67
|
+
}, [open]);
|
|
68
|
+
|
|
69
|
+
// Move focus into the dialog on open and on every step, so a screen reader announces the new title.
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
if (open && mounted) dialogRef.current?.focus();
|
|
72
|
+
}, [open, mounted, stepIndex]);
|
|
73
|
+
|
|
74
|
+
// aria-modal only *tells* assistive tech the page is inert; keep Tab inside the card ourselves.
|
|
75
|
+
const trapTab = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
76
|
+
if (e.key !== "Tab") return;
|
|
77
|
+
const dialog = e.currentTarget;
|
|
78
|
+
const focusable = Array.from(dialog.querySelectorAll<HTMLElement>("button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex='-1'])"));
|
|
79
|
+
if (focusable.length === 0) {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const first = focusable[0]!;
|
|
84
|
+
const last = focusable[focusable.length - 1]!;
|
|
85
|
+
const active = document.activeElement;
|
|
86
|
+
if (e.shiftKey && (active === first || active === dialog)) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
last.focus();
|
|
89
|
+
} else if (!e.shiftKey && active === last) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
first.focus();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
58
95
|
React.useEffect(() => {
|
|
59
96
|
if (!open || !step) return;
|
|
60
97
|
const update = () => {
|
|
@@ -114,12 +151,18 @@ const Tour: React.FC<TourProps> = ({
|
|
|
114
151
|
style={spotlightStyle}
|
|
115
152
|
/>
|
|
116
153
|
<div
|
|
154
|
+
ref={dialogRef}
|
|
117
155
|
role="dialog"
|
|
118
156
|
aria-modal="true"
|
|
119
|
-
|
|
157
|
+
aria-labelledby={titleId}
|
|
158
|
+
tabIndex={-1}
|
|
159
|
+
onKeyDown={trapTab}
|
|
160
|
+
className="fixed z-overlay w-80 rounded-lg border border-border bg-popover p-4 font-sans text-popover-foreground shadow-lg outline-none transition-all duration-fast"
|
|
120
161
|
style={cardStyle}
|
|
121
162
|
>
|
|
122
|
-
<p className="text-title-sm font-medium text-foreground">
|
|
163
|
+
<p id={titleId} className="text-title-sm font-medium text-foreground">
|
|
164
|
+
{step.title}
|
|
165
|
+
</p>
|
|
123
166
|
<div className="mt-1.5 text-body-sm text-muted-foreground">{step.content}</div>
|
|
124
167
|
<div className="mt-4 flex items-center justify-between">
|
|
125
168
|
<span className="text-label-sm text-muted-foreground">
|
|
@@ -63,7 +63,8 @@ function VirtualList<T>({
|
|
|
63
63
|
<div
|
|
64
64
|
ref={containerRef}
|
|
65
65
|
role="list"
|
|
66
|
-
|
|
66
|
+
tabIndex={0}
|
|
67
|
+
className={cn("overflow-y-auto font-sans outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring", className)}
|
|
67
68
|
style={{ height, ...style }}
|
|
68
69
|
onScroll={(e) => {
|
|
69
70
|
onScroll?.(e);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import axe from "axe-core";
|
|
3
|
+
import { render, cleanup } from "@testing-library/react";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rendered accessibility pass: mount every story in jsdom and run axe-core over
|
|
8
|
+
* the resulting DOM. This catches what the static contrast/token checks can't —
|
|
9
|
+
* missing accessible names, invalid ARIA, bad roles, duplicate ids, label
|
|
10
|
+
* association. jsdom has no layout or stylesheet, so `color-contrast` (covered
|
|
11
|
+
* by `pnpm check:contrast`) and page-level rules are off.
|
|
12
|
+
*
|
|
13
|
+
* KNOWN lists violations that are tolerated, keyed `story:rule`. It is empty: every story
|
|
14
|
+
* is clean. A new violation fails; if you must baseline one, a later fix fails until the
|
|
15
|
+
* entry is deleted, so the list can only shrink.
|
|
16
|
+
*/
|
|
17
|
+
const stories = import.meta.glob("./stories/*.stories.tsx", { eager: true }) as Record<
|
|
18
|
+
string,
|
|
19
|
+
Record<string, unknown>
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
const KNOWN = new Set<string>([]);
|
|
23
|
+
|
|
24
|
+
afterEach(cleanup);
|
|
25
|
+
|
|
26
|
+
async function violationsFor(container: HTMLElement) {
|
|
27
|
+
const results = await axe.run(container, {
|
|
28
|
+
rules: {
|
|
29
|
+
"color-contrast": { enabled: false },
|
|
30
|
+
region: { enabled: false },
|
|
31
|
+
"landmark-one-main": { enabled: false },
|
|
32
|
+
"page-has-heading-one": { enabled: false },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return results.violations.map((v) => v.id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("component a11y (axe)", () => {
|
|
39
|
+
const entries = Object.entries(stories).sort(([a], [b]) => a.localeCompare(b));
|
|
40
|
+
|
|
41
|
+
for (const [path, mod] of entries) {
|
|
42
|
+
const name = path.split("/").pop()!.replace(".stories.tsx", "");
|
|
43
|
+
const meta = mod.default as { component?: React.ComponentType<unknown>; args?: Record<string, unknown> };
|
|
44
|
+
const storyKey = ["Default", "Playground", "Variants", "States"].find(
|
|
45
|
+
(k) => mod[k] && typeof mod[k] === "object",
|
|
46
|
+
);
|
|
47
|
+
const story = storyKey
|
|
48
|
+
? (mod[storyKey] as { render?: (...a: unknown[]) => React.ReactNode; args?: Record<string, unknown> })
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
it(`${name} has no new axe violations`, async () => {
|
|
52
|
+
const args = { ...meta?.args, ...story?.args };
|
|
53
|
+
const node =
|
|
54
|
+
typeof story?.render === "function"
|
|
55
|
+
? story.render(args, { args } as never)
|
|
56
|
+
: meta?.component
|
|
57
|
+
? React.createElement(meta.component, args)
|
|
58
|
+
: null;
|
|
59
|
+
if (node === null) return;
|
|
60
|
+
|
|
61
|
+
const { container } = render(node as React.ReactElement);
|
|
62
|
+
const found = new Set((await violationsFor(container)).map((id) => `${name}:${id}`));
|
|
63
|
+
const fresh = [...found].filter((k) => !KNOWN.has(k));
|
|
64
|
+
const stale = [...KNOWN].filter((k) => k.startsWith(`${name}:`) && !found.has(k));
|
|
65
|
+
expect(fresh, `new axe violations (story:rule)`).toEqual([]);
|
|
66
|
+
expect(stale, `fixed — remove from KNOWN`).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -65,7 +65,7 @@ describe("Badge", () => {
|
|
|
65
65
|
);
|
|
66
66
|
const link = screen.getByRole("link", { name: "New" });
|
|
67
67
|
expect(link).toHaveAttribute("href", "/new");
|
|
68
|
-
expect(link).toHaveClass("bg-
|
|
68
|
+
expect(link).toHaveClass("bg-action"); // badge classes merged onto the <a>
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
|