@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.
Files changed (67) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.js +498 -248
  3. package/package.json +3 -2
  4. package/src/components/audio-player.tsx +2 -2
  5. package/src/components/badge.tsx +2 -2
  6. package/src/components/banner.tsx +2 -2
  7. package/src/components/button.tsx +18 -14
  8. package/src/components/calendar.tsx +1 -1
  9. package/src/components/chart.tsx +2 -2
  10. package/src/components/checkbox.tsx +2 -2
  11. package/src/components/circular-progress.tsx +1 -1
  12. package/src/components/code-block.tsx +1 -1
  13. package/src/components/color-picker.tsx +6 -4
  14. package/src/components/data-grid.tsx +192 -11
  15. package/src/components/diff-viewer.tsx +9 -8
  16. package/src/components/fab.tsx +1 -1
  17. package/src/components/file-upload.tsx +13 -15
  18. package/src/components/inform.tsx +1 -1
  19. package/src/components/input-group.tsx +1 -1
  20. package/src/components/input-otp.tsx +2 -1
  21. package/src/components/input.tsx +3 -3
  22. package/src/components/json-viewer.tsx +12 -10
  23. package/src/components/kbd.tsx +1 -1
  24. package/src/components/list.tsx +41 -27
  25. package/src/components/markdown-editor.tsx +3 -2
  26. package/src/components/message-bubble.tsx +3 -3
  27. package/src/components/modal.tsx +2 -2
  28. package/src/components/multi-select.tsx +28 -5
  29. package/src/components/native-select.tsx +3 -3
  30. package/src/components/notification-center.tsx +2 -2
  31. package/src/components/progress.tsx +1 -1
  32. package/src/components/radio-group.tsx +3 -3
  33. package/src/components/scroll-area.tsx +1 -1
  34. package/src/components/select.tsx +3 -3
  35. package/src/components/skeleton.tsx +1 -1
  36. package/src/components/slider.tsx +8 -3
  37. package/src/components/sonner.tsx +1 -1
  38. package/src/components/spinner.tsx +3 -3
  39. package/src/components/stepper.tsx +3 -3
  40. package/src/components/switch.tsx +1 -1
  41. package/src/components/tab-bar.tsx +1 -1
  42. package/src/components/table-of-contents.tsx +1 -1
  43. package/src/components/tag.tsx +1 -1
  44. package/src/components/textarea.tsx +3 -3
  45. package/src/components/timeline.tsx +1 -1
  46. package/src/components/tooltip.tsx +1 -1
  47. package/src/components/tour.tsx +45 -2
  48. package/src/components/virtual-list.tsx +2 -1
  49. package/src/components-a11y.test.tsx +69 -0
  50. package/src/components-behavior.test.tsx +1 -1
  51. package/src/components-keyboard.test.tsx +774 -0
  52. package/src/lib/utils.test.ts +12 -0
  53. package/src/lib/utils.ts +22 -1
  54. package/src/stories/Checkbox.stories.tsx +1 -1
  55. package/src/stories/CircularProgress.stories.tsx +4 -4
  56. package/src/stories/InputOTP.stories.tsx +2 -2
  57. package/src/stories/List.stories.tsx +2 -2
  58. package/src/stories/MarkdownEditor.stories.tsx +1 -1
  59. package/src/stories/MultiSelect.stories.tsx +1 -0
  60. package/src/stories/NativeSelect.stories.tsx +1 -1
  61. package/src/stories/NumberInput.stories.tsx +3 -3
  62. package/src/stories/Progress.stories.tsx +3 -3
  63. package/src/stories/Select.stories.tsx +2 -2
  64. package/src/stories/Slider.stories.tsx +3 -3
  65. package/src/stories/Switch.stories.tsx +1 -1
  66. package/src/stories/ToggleGroup.stories.tsx +6 -6
  67. 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-[14px] leading-5 tracking-[0.25px] text-foreground",
17
+ "font-sans text-body-md text-foreground",
18
18
  "placeholder:text-muted-foreground",
19
19
  "outline-none transition-colors",
20
- "focus-visible:border-primary focus-visible:shadow-focus",
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-primary shadow-focus",
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-primary">{item.icon}</span>
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-primary px-3 py-1.5 text-xs text-primary-foreground font-sans",
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,
@@ -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
- className="fixed z-overlay w-80 rounded-lg border border-border bg-popover p-4 font-sans text-popover-foreground shadow-lg transition-all duration-fast"
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">{step.title}</p>
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
- className={cn("overflow-y-auto font-sans", className)}
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-primary"); // badge classes merged onto the <a>
68
+ expect(link).toHaveClass("bg-action"); // badge classes merged onto the <a>
69
69
  });
70
70
  });
71
71